Skip to content

Commit b50e6a1

Browse files
joedixondriesvints
andauthored
Share articles on Twitter (#554)
* Install twitter notifications * Add command to post to twitter * Add twitter handle to user * Use handle in tweets * Apply PHP CS Fixer changes * Update tweet content * Update code style * Rename twitter_handle field * Update factory call * Update factory * Apply fixes from StyleCI * Invert null check * Update sharing schedule * Remove example env vars * Update code style * Rename migration * Format template * Apply fixes from StyleCI * Store tweet id * Apply fixes from StyleCI * Update code style * Apply fixes from StyleCI * Update readme * Update Kernel.php * Update _user_info.blade.php Co-authored-by: Dries Vints <dries.vints@gmail.com>
1 parent d531520 commit b50e6a1

File tree

21 files changed

+1004
-353
lines changed

21 files changed

+1004
-353
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ MIX_ALGOLIA_APP_ID="${ALGOLIA_APP_ID}"
2929
MIX_ALGOLIA_SECRET=
3030
MIX_ALGOLIA_THREADS_INDEX=threads
3131
MIX_ALGOLIA_ARTICLES_INDEX=articles
32+
33+
TWITTER_CONSUMER_KEY=
34+
TWITTER_CONSUMER_SECRET=
35+
TWITTER_ACCESS_TOKEN=
36+
TWITTER_ACCESS_SECRET=

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ New threads will be automatically added to the index and threads which get edite
8686

8787
`php artisan scout:flush App\\Models\\Thread`
8888

89+
### Twitter sharing (optional)
90+
91+
To enable published articles to be automatically shared to on Twitter, you'll need to [create a Twitter app](https://developer.twitter.com/apps/). Once the app has been created, update the below variables in your `.env` file. The consumer key and secret and access token and secret can be found in the `Keys and tokens` section of the Twitter developers UI.
92+
93+
```
94+
TWITTER_CONSUMER_KEY=
95+
TWITTER_CONSUMER_SECRET=
96+
TWITTER_ACCESS_TOKEN=
97+
TWITTER_ACCESS_SECRET=
98+
```
99+
100+
Approved articles are shared in the order they were submitted for approval. Articles are shared twice per day at 14:00 and 18:00 UTC. Once an article has been shared, it will not be shared again.
101+
89102
## Maintainers
90103

91104
The Laravel.io portal is currently maintained by [Dries Vints](https://github.com/driesvints) and [Joe Dixon](https://github.com/joedixon). If you have any questions please don't hesitate to create an issue on this repo.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\Article;
6+
use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
7+
use Illuminate\Console\Command;
8+
use Illuminate\Notifications\AnonymousNotifiable;
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+
22+
$this->notifiable = $notifiable;
23+
}
24+
25+
public function handle(): void
26+
{
27+
if ($article = Article::nextForSharing()) {
28+
$this->notifiable->notify(new PostArticleToTwitterNotification($article));
29+
30+
$article->markAsShared();
31+
}
32+
}
33+
}

app/Console/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ 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')->twiceDaily(14, 18);
2627
}
2728

2829
/**

app/Http/Requests/UpdateProfileRequest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public function rules()
1212
'name' => 'required|max:255',
1313
'email' => 'required|email|max:255|unique:users,email,'.Auth::id(),
1414
'username' => 'required|alpha_dash|max:255|unique:users,username,'.Auth::id(),
15+
'twitter' => 'max:255|nullable|unique:users,twitter,'.Auth::id(),
1516
'bio' => 'max:160',
1617
];
1718
}
@@ -35,4 +36,9 @@ public function username(): string
3536
{
3637
return (string) $this->get('username');
3738
}
39+
40+
public function twitter(): ?string
41+
{
42+
return $this->get('twitter');
43+
}
3844
}

app/Jobs/UpdateProfile.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class UpdateProfile
2222
public function __construct(User $user, array $attributes = [])
2323
{
2424
$this->user = $user;
25-
$this->attributes = Arr::only($attributes, ['name', 'email', 'username', 'github_username', 'bio']);
25+
$this->attributes = Arr::only($attributes, ['name', 'email', 'username', 'github_username', 'bio', 'twitter']);
2626
}
2727

2828
public static function fromRequest(User $user, UpdateProfileRequest $request): self
@@ -32,6 +32,7 @@ public static function fromRequest(User $user, UpdateProfileRequest $request): s
3232
'email' => $request->email(),
3333
'username' => strtolower($request->username()),
3434
'bio' => trim(strip_tags($request->bio())),
35+
'twitter' => $request->twitter(),
3536
]);
3637
}
3738

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Listeners;
6+
7+
use App\Notifications\PostArticleToTwitter;
8+
use Illuminate\Notifications\Events\NotificationSent;
9+
10+
final class StoreTweetIdentifier
11+
{
12+
public function handle(NotificationSent $event): void
13+
{
14+
if ($event->notification instanceof PostArticleToTwitter) {
15+
$event->notification->article()->update([
16+
'tweet_id' => $event->response->id,
17+
]);
18+
}
19+
}
20+
}

app/Models/Article.php

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ final class Article extends Model
3535
'original_url',
3636
'slug',
3737
'is_pinned',
38+
'tweet_id',
3839
'submitted_at',
3940
'approved_at',
41+
'shared_at',
4042
];
4143

4244
/**
@@ -45,6 +47,7 @@ final class Article extends Model
4547
protected $dates = [
4648
'submitted_at',
4749
'approved_at',
50+
'shared_at',
4851
];
4952

5053
public function id(): int
@@ -84,7 +87,7 @@ public function series()
8487

8588
public function updateSeries(Series $series = null): self
8689
{
87-
if (is_null($series)) {
90+
if ($series === null) {
8891
return $this->removeSeries();
8992
}
9093

@@ -124,7 +127,7 @@ public function isSubmitted(): bool
124127

125128
public function isNotSubmitted(): bool
126129
{
127-
return is_null($this->submitted_at);
130+
return $this->submitted_at === null;
128131
}
129132

130133
public function isApproved(): bool
@@ -134,7 +137,7 @@ public function isApproved(): bool
134137

135138
public function isNotApproved(): bool
136139
{
137-
return is_null($this->approved_at);
140+
return $this->approved_at === null;
138141
}
139142

140143
public function isPublished(): bool
@@ -152,6 +155,16 @@ public function isPinned(): bool
152155
return (bool) $this->is_pinned;
153156
}
154157

158+
public function isNotShared(): bool
159+
{
160+
return $this->shared_at === null;
161+
}
162+
163+
public function isShared(): bool
164+
{
165+
return ! $this->isNotShared();
166+
}
167+
155168
public function isAwaitingApproval(): bool
156169
{
157170
return $this->isSubmitted() && $this->isNotApproved();
@@ -204,6 +217,16 @@ public function scopeNotPublished(Builder $query): Builder
204217
});
205218
}
206219

220+
public function scopeShared(Builder $query): Builder
221+
{
222+
return $query->whereNotNull('shared_at');
223+
}
224+
225+
public function scopeNotShared(Builder $query): Builder
226+
{
227+
return $query->whereNull('shared_at');
228+
}
229+
207230
public function scopeForTag(Builder $query, string $tag): Builder
208231
{
209232
return $query->whereHas('tagsRelation', function ($query) use ($tag) {
@@ -272,4 +295,19 @@ public function splitBody($value)
272295
{
273296
return $this->split($value);
274297
}
298+
299+
public function markAsShared()
300+
{
301+
$this->update([
302+
'shared_at' => now(),
303+
]);
304+
}
305+
306+
public static function nextForSharing(): ?self
307+
{
308+
return self::notShared()
309+
->published()
310+
->orderBy('submitted_at', 'asc')
311+
->first();
312+
}
275313
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
return new TwitterStatusUpdate($this->generateTweet());
30+
}
31+
32+
public function generateTweet()
33+
{
34+
$title = $this->article->title();
35+
$url = route('articles.show', $this->article->slug());
36+
$author = $this->article->author();
37+
$author = $author->twitter() ? "@{$author->twitter()}" : $author->name();
38+
39+
return "{$title} by {$author}\n\n{$url}";
40+
}
41+
42+
public function article()
43+
{
44+
return $this->article;
45+
}
46+
}

app/Providers/EventServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
use App\Events\ReplyWasCreated;
77
use App\Listeners\SendArticleApprovedNotification;
88
use App\Listeners\SendNewReplyNotification;
9+
use App\Listeners\StoreTweetIdentifier;
910
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
11+
use Illuminate\Notifications\Events\NotificationSent;
1012

1113
class EventServiceProvider extends ServiceProvider
1214
{
@@ -22,5 +24,8 @@ class EventServiceProvider extends ServiceProvider
2224
ArticleWasApproved::class => [
2325
SendArticleApprovedNotification::class,
2426
],
27+
NotificationSent::class => [
28+
StoreTweetIdentifier::class,
29+
],
2530
];
2631
}

0 commit comments

Comments
 (0)