Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ MIX_ALGOLIA_APP_ID="${ALGOLIA_APP_ID}"
MIX_ALGOLIA_SECRET=
MIX_ALGOLIA_THREADS_INDEX=threads
MIX_ALGOLIA_ARTICLES_INDEX=articles

TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ New threads will be automatically added to the index and threads which get edite

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

### Twitter sharing (optional)

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.

```
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=
```

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.

## Maintainers

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.
Expand Down
33 changes: 33 additions & 0 deletions app/Console/Commands/PostArticleToTwitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Console\Commands;

use App\Models\Article;
use App\Notifications\PostArticleToTwitter as PostArticleToTwitterNotification;
use Illuminate\Console\Command;
use Illuminate\Notifications\AnonymousNotifiable;

final class PostArticleToTwitter extends Command
{
protected $signature = 'post-article-to-twitter';

protected $description = 'Posts the latest unshared article to Twitter';

private $notifiable;

public function __construct(AnonymousNotifiable $notifiable)
{
parent::__construct();

$this->notifiable = $notifiable;
}

public function handle(): void
{
if ($article = Article::nextForSharing()) {
$this->notifiable->notify(new PostArticleToTwitterNotification($article));

$article->markAsShared();
}
}
}
1 change: 1 addition & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ protected function schedule(Schedule $schedule)
$schedule->command('schedule-monitor:sync')->dailyAt('04:56');
$schedule->command('schedule-monitor:clean')->daily();
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('post-article-to-twitter')->twiceDaily(14, 18);
}

/**
Expand Down
6 changes: 6 additions & 0 deletions app/Http/Requests/UpdateProfileRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function rules()
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users,email,'.Auth::id(),
'username' => 'required|alpha_dash|max:255|unique:users,username,'.Auth::id(),
'twitter' => 'max:255|nullable|unique:users,twitter,'.Auth::id(),
'bio' => 'max:160',
];
}
Expand All @@ -35,4 +36,9 @@ public function username(): string
{
return (string) $this->get('username');
}

public function twitter(): ?string
{
return $this->get('twitter');
}
}
3 changes: 2 additions & 1 deletion app/Jobs/UpdateProfile.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class UpdateProfile
public function __construct(User $user, array $attributes = [])
{
$this->user = $user;
$this->attributes = Arr::only($attributes, ['name', 'email', 'username', 'github_username', 'bio']);
$this->attributes = Arr::only($attributes, ['name', 'email', 'username', 'github_username', 'bio', 'twitter']);
}

public static function fromRequest(User $user, UpdateProfileRequest $request): self
Expand All @@ -32,6 +32,7 @@ public static function fromRequest(User $user, UpdateProfileRequest $request): s
'email' => $request->email(),
'username' => strtolower($request->username()),
'bio' => trim(strip_tags($request->bio())),
'twitter' => $request->twitter(),
]);
}

Expand Down
20 changes: 20 additions & 0 deletions app/Listeners/StoreTweetIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Listeners;

use App\Notifications\PostArticleToTwitter;
use Illuminate\Notifications\Events\NotificationSent;

final class StoreTweetIdentifier
{
public function handle(NotificationSent $event): void
{
if ($event->notification instanceof PostArticleToTwitter) {
$event->notification->article()->update([
'tweet_id' => $event->response->id,
]);
}
}
}
44 changes: 41 additions & 3 deletions app/Models/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ final class Article extends Model
'original_url',
'slug',
'is_pinned',
'tweet_id',
'submitted_at',
'approved_at',
'shared_at',
];

/**
Expand All @@ -45,6 +47,7 @@ final class Article extends Model
protected $dates = [
'submitted_at',
'approved_at',
'shared_at',
];

public function id(): int
Expand Down Expand Up @@ -84,7 +87,7 @@ public function series()

public function updateSeries(Series $series = null): self
{
if (is_null($series)) {
if ($series === null) {
return $this->removeSeries();
}

Expand Down Expand Up @@ -124,7 +127,7 @@ public function isSubmitted(): bool

public function isNotSubmitted(): bool
{
return is_null($this->submitted_at);
return $this->submitted_at === null;
}

public function isApproved(): bool
Expand All @@ -134,7 +137,7 @@ public function isApproved(): bool

public function isNotApproved(): bool
{
return is_null($this->approved_at);
return $this->approved_at === null;
}

public function isPublished(): bool
Expand All @@ -152,6 +155,16 @@ public function isPinned(): bool
return (bool) $this->is_pinned;
}

public function isNotShared(): bool
{
return $this->shared_at === null;
}

public function isShared(): bool
{
return ! $this->isNotShared();
}

public function isAwaitingApproval(): bool
{
return $this->isSubmitted() && $this->isNotApproved();
Expand Down Expand Up @@ -204,6 +217,16 @@ public function scopeNotPublished(Builder $query): Builder
});
}

public function scopeShared(Builder $query): Builder
{
return $query->whereNotNull('shared_at');
}

public function scopeNotShared(Builder $query): Builder
{
return $query->whereNull('shared_at');
}

public function scopeForTag(Builder $query, string $tag): Builder
{
return $query->whereHas('tagsRelation', function ($query) use ($tag) {
Expand Down Expand Up @@ -272,4 +295,19 @@ public function splitBody($value)
{
return $this->split($value);
}

public function markAsShared()
{
$this->update([
'shared_at' => now(),
]);
}

public static function nextForSharing(): ?self
{
return self::notShared()
->published()
->orderBy('submitted_at', 'asc')
->first();
}
}
46 changes: 46 additions & 0 deletions app/Notifications/PostArticleToTwitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Notifications;

use App\Models\Article;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use NotificationChannels\Twitter\TwitterChannel;
use NotificationChannels\Twitter\TwitterStatusUpdate;

class PostArticleToTwitter extends Notification
{
use Queueable;

private $article;

public function __construct(Article $article)
{
$this->article = $article;
}

public function via($notifiable)
{
return [TwitterChannel::class];
}

public function toTwitter($notifiable)
{
return new TwitterStatusUpdate($this->generateTweet());
}

public function generateTweet()
{
$title = $this->article->title();
$url = route('articles.show', $this->article->slug());
$author = $this->article->author();
$author = $author->twitter() ? "@{$author->twitter()}" : $author->name();

return "{$title} by {$author}\n\n{$url}";
}

public function article()
{
return $this->article;
}
}
5 changes: 5 additions & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
use App\Events\ReplyWasCreated;
use App\Listeners\SendArticleApprovedNotification;
use App\Listeners\SendNewReplyNotification;
use App\Listeners\StoreTweetIdentifier;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Notifications\Events\NotificationSent;

class EventServiceProvider extends ServiceProvider
{
Expand All @@ -22,5 +24,8 @@ class EventServiceProvider extends ServiceProvider
ArticleWasApproved::class => [
SendArticleApprovedNotification::class,
],
NotificationSent::class => [
StoreTweetIdentifier::class,
],
];
}
6 changes: 6 additions & 0 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ final class User extends Authenticatable implements MustVerifyEmail
protected $fillable = [
'name',
'email',
'twitter',
'username',
'password',
'ip',
Expand Down Expand Up @@ -84,6 +85,11 @@ public function githubUsername(): string
return $this->github_username;
}

public function twitter(): ?string
{
return $this->twitter;
}

public function gravatarUrl($size = 100): string
{
$hash = md5(strtolower(trim($this->email)));
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"laravel/framework": "^8.10",
"laravel/horizon": "^5.2",
"laravel/socialite": "^5.0",
"laravel-notification-channels/twitter": "^5.0",
"laravel/tinker": "^2.0",
"laravel/ui": "^3.0",
"lasserafn/php-initial-avatar-generator": "^2.0",
Expand Down Expand Up @@ -89,4 +90,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}
Loading