Connect a Discord server via OAuth (bot authorization) and schedule/publish messages to its channels, with mentions and rich embeds. - Connect: custom Socialite Discord provider (bot scope) maps the authorized guild to a SocialAccount; throws if no server was authorized. - Publish: DiscordPublisher posts via the global bot token, validates the chosen channel belongs to the connected guild (anti cross-guild), optimizes media, builds allowed_mentions only from explicit mention chips (no accidental pings), and renders rich embeds. - Compose: per-post channel picker (live lookup), mention autocomplete and an embed editor, gated by a required-channel compliance rule; Discord post preview. - Enum/config/content-type wiring, ConnectionVerifier health check, throttled lookup endpoints, i18n (en/es/pt-BR), and tests. Operators must create a Discord application and set DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN and DISCORD_CLIENT_REDIRECT.
94 lines
2.8 KiB
PHP
94 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Socialite;
|
|
|
|
use Laravel\Socialite\Two\AbstractProvider;
|
|
use Laravel\Socialite\Two\ProviderInterface;
|
|
use Laravel\Socialite\Two\User;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Discord bot-authorization OAuth. The user authorizes adding our shared bot to
|
|
* one of their servers; the access-token response carries the `guild` the bot
|
|
* was added to, which becomes the connected account (id = guild id). Channel
|
|
* listing, posting and mentions all use the global bot token, not this token.
|
|
*/
|
|
class DiscordProvider extends AbstractProvider implements ProviderInterface
|
|
{
|
|
protected $scopes = ['bot', 'identify', 'guilds'];
|
|
|
|
protected $scopeSeparator = ' ';
|
|
|
|
/**
|
|
* The token-exchange response, stashed so the guild it carries can become
|
|
* the Socialite user without an extra API call.
|
|
*
|
|
* @var array<string, mixed>
|
|
*/
|
|
private array $tokenResponse = [];
|
|
|
|
protected function getAuthUrl($state): string
|
|
{
|
|
return $this->buildAuthUrlFromBase(config('trypost.platforms.discord.oauth_api').'/authorize', $state);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function getCodeFields($state = null): array
|
|
{
|
|
return array_merge(parent::getCodeFields($state), [
|
|
'permissions' => (string) config('trypost.platforms.discord.permissions'),
|
|
'integration_type' => 0,
|
|
]);
|
|
}
|
|
|
|
protected function getTokenUrl(): string
|
|
{
|
|
return config('trypost.platforms.discord.oauth_api').'/token';
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getAccessTokenResponse($code): array
|
|
{
|
|
return $this->tokenResponse = parent::getAccessTokenResponse($code);
|
|
}
|
|
|
|
/**
|
|
* The bot-authorization token response includes the guild the bot joined.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function getUserByToken($token): array
|
|
{
|
|
return (array) data_get($this->tokenResponse, 'guild', []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $user The guild object.
|
|
*/
|
|
protected function mapUserToObject(array $user): User
|
|
{
|
|
$guildId = data_get($user, 'id');
|
|
|
|
// No guild means the user authorized without adding the bot to a server
|
|
// (or cancelled). Fail loudly so the callback shows an error instead of
|
|
// persisting a broken account with a null guild id.
|
|
if (blank($guildId)) {
|
|
throw new RuntimeException('Discord authorization did not include a server.');
|
|
}
|
|
|
|
$icon = data_get($user, 'icon');
|
|
|
|
return (new User)->setRaw($user)->map([
|
|
'id' => $guildId,
|
|
'nickname' => data_get($user, 'name'),
|
|
'name' => data_get($user, 'name'),
|
|
'avatar' => $guildId && $icon ? "https://cdn.discordapp.com/icons/{$guildId}/{$icon}.png" : null,
|
|
]);
|
|
}
|
|
}
|