Skip to content

Commit

Permalink
Merge pull request #442 from ECFMP/discord-bot
Browse files Browse the repository at this point in the history
Discord bot
  • Loading branch information
AndyTWF authored Oct 19, 2023
2 parents 2c30dca + c4a5ab9 commit 3a5bc36
Show file tree
Hide file tree
Showing 105 changed files with 3,787 additions and 467 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ DISCORD_AUTH_TOKEN=abc
DISCORD_WEBHOOK_URL=def
DISCORD_USERNAME=FlowBot
DISCORD_AVATAR_URL="${APP_URL}/images/logo.png"
DISCORD_ECFMP_CHANNEL_ID="971531731096203364"

# Sentry monitoring
SENTRY_LARAVEL_DSN=
SENTRY_TRACES_SAMPLE_RATE=1.0

# Discord bot
DISCORD_BOT_SERVICE_URL="discord-bot:80"
DISCORD_BOT_JWT="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlY2ZtcC1mbG93IiwiYXVkIjoiZWNmbXAtZGlzY29yZC1kZXYiLCJpYXQiOjIxMzg0NTQ1NTUsImlzcyI6ImVjZm1wLWF1dGgifQ.PqbCEe_zW1WBLio6aD0P5OksRI1H-hoRRA8168OJg-h11SjyZeDCAAf0CjgZAUy6vUpSdfgN9KH1SgCSM4J38-2txpZLr_VlJTu9_W1mGEVr1_pGjMgbkwx8PMTP1f3J2R0BwGz-324vpPVB9zu6NG9ujS48AD28mDoqMpqc7UOK0_e9WJ7cQBb8BxU10w4TQXbwhjUMyZBIpdiaDK5OsQeXJruo0OjSlltiFJkXPmESTz_DwwTSvIqzmhjzQfNW62RVcnBrnbWaaCg1mC6FSIMffjrEgian_AyAg1iftjy_fa3f-sU-z65xMh8vVwAvJEhYJCA0CZO4lKn_OV0RXqYjCLI4t-Rp6MYULyLbp6QZ88MOvSXd-8GnYnxDE5o5-rLFnQ04LCGx2-yBDPZ80brdxZR26Im1DrNiPyUFadINGf8wwZ4-iWqmY6_QSfJYU1C3Y5s7TxMBFfa934NHICg53gVSEdfCHQIaciOSu91P1FnGIvdtOQV_n7urF5HRYyxOUomSa4MY4m5C1-TJqpBkCsAXbMdC_mIllFWnUCB5uBj5T18mxW1mrGQiF3Vy6_MrSeFoY0LEnd58QpvnG7-OuZWnrIbDDTAnTcI1IMVUA4zcoUvkUcCcRvBQD2bXsPgL9Lh8RmYvjrR2hWhmlJU-k_CPiQOLfW9qOFGp5rA"
DISCORD_CLIENT_REQUEST_APP_ID="ecfmpdev"
17 changes: 16 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,29 @@ jobs:
composer: ["v2"]
steps:
- name: Checkout Code
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: true

- name: Checkout Discord Service
uses: actions/checkout@v4
with:
repository: ecfmp/discord
path: ecfmp-discord

- name: Move Discord Service
run: mv ecfmp-discord ../ecfmp-discord

- name: Build Protobuf
run: (cd protobuf && make pull_builder && make discord_proto)

- name: Configure PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: pcov
tools: composer:${{ matrix.composer }}
extensions: grpc

# Setting up composer dependencies
- name: Get Composer Cache Directory
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Homestead.yaml
Homestead.json
/.vagrant
.phpunit.result.cache
.phpunit.cache
.idea

# Laravel IDE Helper
Expand All @@ -39,3 +40,5 @@ public/mix-manifest.json
storage/imports

.vscode

.DS_STORE
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "protobuf"]
path = protobuf
url = [email protected]:ECFMP/ecfmp-protobuf.git
38 changes: 38 additions & 0 deletions app/Console/Commands/SendEcfmpDiscordMessages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Discord\FlowMeasure\Generator\EcfmpFlowMeasureMessageGenerator;

class SendEcfmpDiscordMessages extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'discord:send-ecfmp-messages';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Send ECFMP Discord messages';

public function handle(EcfmpFlowMeasureMessageGenerator $generator): int
{
if (!config('discord.enabled')) {
Log::info('Skipping discord notifications, disabled in config');
return 0;
}

Log::info('Sending discord notifications');
$generator->generateAndSend();
Log::info('Discord notification sending complete');

return 0;
}
}
32 changes: 32 additions & 0 deletions app/Discord/Client/ClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Discord\Client;

use Ecfmp_discord\DiscordClient;
use Grpc\ChannelCredentials;

/**
* This class exists because gRPC has trouble when the client is registered directly to the
* service container (the channel is closed before the request is sent). This class is a
* workaround for that issue.
* @codeCoverageIgnore
*/
class ClientFactory implements ClientFactoryInterface
{
private DiscordClient|null $client = null;

public function create(): DiscordClient
{
if ($this->client === null) {
$this->client = new DiscordClient(
config('discord.service_host'),
[
'credentials' => ChannelCredentials::createInsecure(),
'grpc.primary_user_agent' => config('app.name'),
],
);
}

return $this->client;
}
}
10 changes: 10 additions & 0 deletions app/Discord/Client/ClientFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace App\Discord\Client;

use Ecfmp_discord\DiscordClient;

interface ClientFactoryInterface
{
public function create(): DiscordClient;
}
16 changes: 16 additions & 0 deletions app/Discord/DiscordServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Discord;

use App\Discord\Exception\DiscordServiceException;
use App\Discord\Message\EcfmpMessageInterface;

interface DiscordServiceInterface
{
/**
* Returns the remote message id, or throws an exception if the message could not be sent.
*
* @throws DiscordServiceException
*/
public function sendMessage(string $clientRequestId, EcfmpMessageInterface $message): string;
}
61 changes: 61 additions & 0 deletions app/Discord/DiscordServiceMessageSender.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace App\Discord;

use App\Discord\Client\ClientFactoryInterface;
use App\Discord\Exception\DiscordServiceException;
use App\Discord\Message\EcfmpMessageInterface;
use Ecfmp_discord\CreateRequest;
use Log;

use const Grpc\STATUS_OK;

class DiscordServiceMessageSender implements DiscordServiceInterface
{
private readonly ClientFactoryInterface $discordClientFactory;

public function __construct(ClientFactoryInterface $discordClientFactory)
{
$this->discordClientFactory = $discordClientFactory;
}

public function sendMessage(string $clientRequestId, EcfmpMessageInterface $message): string
{
$client = $this->discordClientFactory->create();

// Wait for 1 second for the channel to be ready
$channelReady = $client->waitForReady(1000000);
if (!$channelReady) {
Log::error('Discord grpc channel not ready');
throw new DiscordServiceException('Discord grpc channel not ready');
}

/**
* @var $response \Ecfmp_discord\CreateResponse
*/
[$response, $status] = $client->Create(
new CreateRequest(
[
'channel' => $message->channel(),
'content' => $message->content(),
'embeds' => $message->embeds()->toProtobuf(),
]
),
[
'authorization' => [config('discord.service_token')],
'x-client-request-id' => [$clientRequestId],
],
)->wait();

if ($status->code !== STATUS_OK) {
Log::error('Discord grpc call failed', [
'code' => $status->code,
'details' => $status->details,
]);

throw new DiscordServiceException('Discord grpc call failed');
}

return $response->getId();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
/**
* To hide the details of how we go about doing Discord things...
*
* This class is the interface for interacting with Discord.
* This class is the interface for interacting with Discord via webhooks.
*/
interface DiscordInterface
interface DiscordWebhookInterface
{
public function sendMessage(MessageInterface $message): bool;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Illuminate\Support\Facades\Http;
use Log;

class DiscordMessageSender implements DiscordInterface
class DiscordWebhookSender implements DiscordWebhookInterface
{
public function sendMessage(MessageInterface $message): bool
{
Expand Down
9 changes: 9 additions & 0 deletions app/Discord/Exception/DiscordServiceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Discord\Exception;

use RuntimeException;

class DiscordServiceException extends RuntimeException
{
}
6 changes: 3 additions & 3 deletions app/Discord/FlowMeasure/Associator/FlowMeasureAssociator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Discord\Message\Associator\AssociatorInterface;
use App\Enums\DiscordNotificationType as DiscordNotificationTypeEnum;
use App\Models\DiscordNotification;
use App\Models\DivisionDiscordNotification;
use App\Models\DiscordNotificationType;
use App\Models\FlowMeasure;

Expand All @@ -20,9 +20,9 @@ public function __construct(FlowMeasure $flowMeasure, DiscordNotificationTypeEnu
}


public function associate(DiscordNotification $notification): void
public function associate(DivisionDiscordNotification $notification): void
{
$this->flowMeasure->discordNotifications()->attach(
$this->flowMeasure->divisionDiscordNotifications()->attach(
[
$notification->id => [
'discord_notification_type_id' => DiscordNotificationType::idFromEnum($this->type),
Expand Down
31 changes: 25 additions & 6 deletions app/Discord/FlowMeasure/Content/FlowMeasureRecipientsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,51 @@
namespace App\Discord\FlowMeasure\Content;

use App\Discord\FlowMeasure\Provider\PendingMessageInterface;
use App\Discord\FlowMeasure\Provider\PendingWebhookMessageInterface;
use App\Discord\Message\Tag\Tag;
use App\Enums\DiscordNotificationType;
use App\Models\DiscordNotification;
use App\Models\DivisionDiscordNotification;
use App\Models\DiscordTag;
use App\Models\DivisionDiscordWebhook;
use App\Models\FlightInformationRegion;
use Carbon\Carbon;

class FlowMeasureRecipientsFactory
{
public function makeRecipients(PendingMessageInterface $pendingMessage): FlowMeasureRecipientsInterface
public function makeRecipients(PendingWebhookMessageInterface $pendingMessage): FlowMeasureRecipientsInterface
{
if ($this->hasRecentlyBeenNotifiedToWebhook($pendingMessage)) {
return new NoRecipients();
}

return $this->divisionRecipients($pendingMessage);
}

public function makeEcfmpRecipients(PendingMessageInterface $pendingMessage): FlowMeasureRecipientsInterface
{
if ($this->hasRecentlyBeenNotified($pendingMessage)) {
return new NoRecipients();
}

return $pendingMessage->webhook()->id() === null
? $this->ecfmpRecipients($pendingMessage)
: $this->divisionRecipients($pendingMessage);
return $this->ecfmpRecipients($pendingMessage);
}

private function hasRecentlyBeenNotifiedToWebhook(PendingWebhookMessageInterface $pendingMessage): bool
{
$measure = $pendingMessage->flowMeasure();
return $pendingMessage->type(
) === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && $measure->notifiedDivisionNotifications->firstWhere(
fn (DivisionDiscordNotification $notification) => $notification->created_at > Carbon::now()->subHour() &&
$notification->pivot->notified_as === $measure->identifier
) !== null;
}

private function hasRecentlyBeenNotified(PendingMessageInterface $pendingMessage): bool
{
$measure = $pendingMessage->flowMeasure();
return $pendingMessage->type(
) === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && $measure->notifiedDiscordNotifications->firstWhere(
) === DiscordNotificationType::FLOW_MEASURE_ACTIVATED && $measure->notifiedEcfmpNotifications->firstWhere(
fn (DiscordNotification $notification) => $notification->created_at > Carbon::now()->subHour() &&
$notification->pivot->notified_as === $measure->identifier
) !== null;
Expand All @@ -44,7 +63,7 @@ private function ecfmpRecipients(PendingMessageInterface $pendingMessage): FlowM
);
}

private function divisionRecipients(PendingMessageInterface $pendingMessage): FlowMeasureRecipientsInterface
private function divisionRecipients(PendingWebhookMessageInterface $pendingMessage): FlowMeasureRecipientsInterface
{
$recipients = DivisionDiscordWebhook::find($pendingMessage->webhook()->id())
->flightInformationRegions
Expand Down
2 changes: 1 addition & 1 deletion app/Discord/FlowMeasure/Embed/ActivatedEmbeds.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function embeds(): EmbedCollection
: IdentifierAndActiveStatus::create($this->pendingMessage->flowMeasure())
)
->withDescription(new EventName($this->pendingMessage->flowMeasure()))
->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), is_null($this->pendingMessage->webhook()->id()))
->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), $this->pendingMessage->isEcfmp())
->withField(Field::makeInline(new Restriction($this->pendingMessage->flowMeasure())))
->withField(Field::makeInline(new StartTime($this->pendingMessage->flowMeasure())))
->withField(Field::makeInline(new EndTime($this->pendingMessage->flowMeasure())))
Expand Down
2 changes: 1 addition & 1 deletion app/Discord/FlowMeasure/Embed/NotifiedEmbeds.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function embeds(): EmbedCollection
: IdentifierAndNotifiedStatus::create($this->pendingMessage->flowMeasure())
)
->withDescription(new EventName($this->pendingMessage->flowMeasure()))
->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), is_null($this->pendingMessage->webhook()->id()))
->withField(Field::make(new IssuingUser($this->pendingMessage->flowMeasure())), $this->pendingMessage->isEcfmp())
->withField(Field::makeInline(new Restriction($this->pendingMessage->flowMeasure())))
->withField(Field::makeInline(new StartTime($this->pendingMessage->flowMeasure())))
->withField(Field::makeInline(new EndTime($this->pendingMessage->flowMeasure())))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Discord\FlowMeasure\Generator;

use App\Discord\FlowMeasure\Helper\EcfmpNotificationReissuer;
use App\Discord\FlowMeasure\Provider\PendingEcfmpMessage;
use App\Discord\FlowMeasure\Sender\EcfmpFlowMeasureSender;
use App\Repository\FlowMeasureNotification\RepositoryInterface;

class EcfmpFlowMeasureMessageGenerator
{
private readonly EcfmpFlowMeasureSender $sender;

/** @var RepositoryInterface[] */
private readonly array $repositories;

public function __construct(EcfmpFlowMeasureSender $sender, array $repositories)
{
$this->sender = $sender;
$this->repositories = $repositories;
}

public function generateAndSend(): void
{
foreach ($this->repositories as $repository) {
foreach ($repository->flowMeasuresToBeSentToEcfmp() as $measure) {
$pendingMessage = new PendingEcfmpMessage(
$measure->measure,
$repository->notificationType(),
new EcfmpNotificationReissuer($measure, $repository->notificationType())
);

$this->sender->send($pendingMessage);
}
}
}
}
Loading

0 comments on commit 3a5bc36

Please sign in to comment.