Skip to content

Commit 9a4bad3

Browse files
committed
Add command to post to twitter
1 parent 3eef476 commit 9a4bad3

File tree

6 files changed

+206
-3
lines changed

6 files changed

+206
-3
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Article;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Notifications\AnonymousNotifiable;
8+
use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
9+
10+
final class PostArticleToTwitter extends Command
11+
{
12+
protected $signature = 'post-article-to-twitter';
13+
14+
protected $description = 'Posts the latest unshared article to Twitter';
15+
16+
private $notifiable;
17+
18+
public function __construct(AnonymousNotifiable $notifiable)
19+
{
20+
parent::__construct();
21+
$this->notifiable = $notifiable;
22+
}
23+
24+
public function handle()
25+
{
26+
if (! $article = Article::nextForSharing()) {
27+
return;
28+
}
29+
30+
$this->notifiable->notify(new PostArticleToTwitterNotification($article));
31+
32+
$article->markAsShared();
33+
}
34+
}

app/Console/Kernel.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ protected function schedule(Schedule $schedule)
2323
$schedule->command('schedule-monitor:sync')->dailyAt('04:56');
2424
$schedule->command('schedule-monitor:clean')->daily();
2525
$schedule->command('horizon:snapshot')->everyFiveMinutes();
26+
$schedule->command('post-article-to-twitter')
27+
->everyMinute()
28+
->when(function () {
29+
// ~2 posts every 24 hours.
30+
return random_int(1, 1440) <= 2;
31+
});
2632
}
2733

2834
/**

app/Models/Article.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ final class Article extends Model
3737
'is_pinned',
3838
'submitted_at',
3939
'approved_at',
40+
'shared_at',
4041
];
4142

4243
/**
@@ -45,6 +46,7 @@ final class Article extends Model
4546
protected $dates = [
4647
'submitted_at',
4748
'approved_at',
49+
'shared_at',
4850
];
4951

5052
public function id(): int
@@ -84,7 +86,7 @@ public function series()
8486

8587
public function updateSeries(Series $series = null): self
8688
{
87-
if (is_null($series)) {
89+
if (null === $series) {
8890
return $this->removeSeries();
8991
}
9092

@@ -124,7 +126,7 @@ public function isSubmitted(): bool
124126

125127
public function isNotSubmitted(): bool
126128
{
127-
return is_null($this->submitted_at);
129+
return null === $this->submitted_at;
128130
}
129131

130132
public function isApproved(): bool
@@ -134,7 +136,7 @@ public function isApproved(): bool
134136

135137
public function isNotApproved(): bool
136138
{
137-
return is_null($this->approved_at);
139+
return null === $this->approved_at;
138140
}
139141

140142
public function isPublished(): bool
@@ -152,6 +154,16 @@ public function isPinned(): bool
152154
return (bool) $this->is_pinned;
153155
}
154156

157+
public function isNotShared(): bool
158+
{
159+
return null === $this->shared_at;
160+
}
161+
162+
public function isShared(): bool
163+
{
164+
return ! $this->isNotShared();
165+
}
166+
155167
public function isAwaitingApproval(): bool
156168
{
157169
return $this->isSubmitted() && $this->isNotApproved();
@@ -204,6 +216,16 @@ public function scopeNotPublished(Builder $query): Builder
204216
});
205217
}
206218

219+
public function scopeShared(Builder $query): Builder
220+
{
221+
return $query->whereNotNull('shared_at');
222+
}
223+
224+
public function scopeNotShared(Builder $query): Builder
225+
{
226+
return $query->whereNull('shared_at');
227+
}
228+
207229
public function scopeForTag(Builder $query, string $tag): Builder
208230
{
209231
return $query->whereHas('tagsRelation', function ($query) use ($tag) {
@@ -272,4 +294,19 @@ public function splitBody($value)
272294
{
273295
return $this->split($value);
274296
}
297+
298+
public function markAsShared()
299+
{
300+
$this->update([
301+
'shared_at' => now(),
302+
]);
303+
}
304+
305+
public static function nextForSharing(): ?self
306+
{
307+
return self::notShared()
308+
->published()
309+
->orderBy('submitted_at', 'asc')
310+
->first();
311+
}
275312
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Notifications;
4+
5+
use App\Models\Article;
6+
use Illuminate\Bus\Queueable;
7+
use Illuminate\Notifications\Notification;
8+
use NotificationChannels\Twitter\TwitterChannel;
9+
use NotificationChannels\Twitter\TwitterStatusUpdate;
10+
11+
class PostArticleToTwitter extends Notification
12+
{
13+
use Queueable;
14+
15+
private $article;
16+
17+
public function __construct(Article $article)
18+
{
19+
$this->article = $article;
20+
}
21+
22+
public function via($notifiable)
23+
{
24+
return [TwitterChannel::class];
25+
}
26+
27+
public function toTwitter($notifiable)
28+
{
29+
$title = $this->article->title();
30+
$url = route('articles.show', $this->article->slug());
31+
$author = $this->article->author()->name();
32+
33+
return new TwitterStatusUpdate("{$title} - {$author}\n\n{$url}");
34+
}
35+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Schema;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Database\Migrations\Migration;
6+
7+
class UpdateArticlesTableAddSharedAtField extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::table('articles', function (Blueprint $table) {
12+
$table->dateTime('shared_at')->after('approved_at')->nullable();
13+
});
14+
}
15+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Tests\Unit\Commands;
4+
5+
use Tests\TestCase;
6+
use App\Models\Article;
7+
use Illuminate\Support\Facades\Notification;
8+
use App\Console\Commands\PostArticleToTwitter;
9+
use Illuminate\Notifications\AnonymousNotifiable;
10+
use Illuminate\Foundation\Testing\DatabaseMigrations;
11+
use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
12+
13+
class PostArticleToTwitterTest extends TestCase
14+
{
15+
use DatabaseMigrations;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
Notification::fake();
21+
}
22+
23+
/** @test */
24+
public function published_articles_can_be_shared_on_twitter()
25+
{
26+
$article = factory(Article::class)->create([
27+
'submitted_at' => now(),
28+
'approved_at' => now(),
29+
]);
30+
31+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
32+
33+
Notification::assertSentTo(
34+
new AnonymousNotifiable,
35+
PostArticleToTwitterNotification::class
36+
);
37+
38+
$this->assertTrue($article->fresh()->isShared());
39+
}
40+
41+
/** @test */
42+
public function already_shared_articles_are_not_shared_again()
43+
{
44+
factory(Article::class)->create([
45+
'submitted_at' => now(),
46+
'approved_at' => now(),
47+
'shared_at' => now(),
48+
]);
49+
50+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
51+
52+
Notification::assertNothingSent();
53+
}
54+
55+
/** @test */
56+
public function unapproved_articles_are_not_shared()
57+
{
58+
factory(Article::class)->create([
59+
'submitted_at' => now(),
60+
]);
61+
62+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
63+
64+
Notification::assertNothingSent();
65+
}
66+
67+
/** @test */
68+
public function unsubmitted_articles_are_not_shared()
69+
{
70+
factory(Article::class)->create();
71+
72+
(new PostArticleToTwitter(new AnonymousNotifiable()))->handle();
73+
74+
Notification::assertNothingSent();
75+
}
76+
}

0 commit comments

Comments
 (0)