Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discord bot #442

Merged
merged 63 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
7453559
build(deps-dev): bump axios from 1.3.4 to 1.5.0
dependabot[bot] Aug 28, 2023
87fa1d6
build(deps-dev): bump postcss from 8.4.21 to 8.4.29
dependabot[bot] Sep 4, 2023
dc866b8
build(deps-dev): bump @tailwindcss/forms from 0.5.3 to 0.5.6
dependabot[bot] Sep 4, 2023
1192a98
feat: view division discord webhooks
AndyTWF Sep 12, 2023
30e1986
feat: create + edit division webhooks
AndyTWF Sep 12, 2023
6df6723
feat: attach firs to division discord webhooks
AndyTWF Sep 12, 2023
9031f23
style: pint
AndyTWF Sep 12, 2023
fb29069
fix: remove testing code
AndyTWF Sep 12, 2023
d5d5afe
dev: bump pint
AndyTWF Sep 12, 2023
840fc95
style: pint
AndyTWF Sep 12, 2023
b6de69e
Merge pull request #422 from ECFMP/division-discord-webhooks
AndyTWF Sep 12, 2023
003ecfe
build(deps): bump spatie/laravel-markdown from 2.2.6 to 2.4.0
dependabot[bot] Sep 12, 2023
5a7c790
build(deps-dev): bump barryvdh/laravel-debugbar from 3.8.1 to 3.9.2
dependabot[bot] Sep 12, 2023
3370be4
build(deps): bump sentry/sentry-laravel from 3.3.2 to 3.8.0
dependabot[bot] Sep 12, 2023
06d9d23
build(deps-dev): bump laravel-lang/lang from 12.19.2 to 13.2.2
dependabot[bot] Sep 12, 2023
c81bc1a
build(deps): bump laravel/socialite from 5.6.1 to 5.9.0
dependabot[bot] Sep 12, 2023
269c3d0
Merge pull request #421 from ECFMP/dependabot/composer/sentry/sentry-…
AndyTWF Sep 13, 2023
c2ea6ad
Merge pull request #420 from ECFMP/dependabot/composer/laravel/social…
AndyTWF Sep 13, 2023
fd30975
Merge pull request #419 from ECFMP/dependabot/npm_and_yarn/tailwindcs…
AndyTWF Sep 13, 2023
03d1a08
Merge pull request #418 from ECFMP/dependabot/npm_and_yarn/postcss-8.…
AndyTWF Sep 13, 2023
b9a1f79
Merge pull request #417 from ECFMP/dependabot/composer/barryvdh/larav…
AndyTWF Sep 13, 2023
590b31c
Merge pull request #416 from ECFMP/dependabot/composer/laravel-lang/l…
AndyTWF Sep 13, 2023
33bcccb
Merge pull request #414 from ECFMP/dependabot/npm_and_yarn/axios-1.5.0
AndyTWF Sep 13, 2023
f074508
Merge pull request #411 from ECFMP/dependabot/composer/spatie/laravel…
AndyTWF Sep 13, 2023
e33ca6f
build(deps-dev): bump vite from 4.2.1 to 4.4.9
dependabot[bot] Sep 13, 2023
e06e9cb
build(deps-dev): bump autoprefixer from 10.4.13 to 10.4.15
dependabot[bot] Sep 13, 2023
ad6332a
fix: allow discord tags to be searchable by description
AndyTWF Sep 13, 2023
02b5394
Merge pull request #423 from ECFMP/division-discord-webhooks
AndyTWF Sep 13, 2023
3b0b667
Merge pull request #403 from ECFMP/dependabot/npm_and_yarn/vite-4.4.9
AndyTWF Sep 13, 2023
6654644
Merge pull request #407 from ECFMP/dependabot/npm_and_yarn/autoprefix…
AndyTWF Sep 13, 2023
a4be825
fix: remove url from division webhooks table
AndyTWF Sep 14, 2023
15cbe1a
fix: move discord webhooks to admin
AndyTWF Sep 14, 2023
867183a
style: pint
AndyTWF Sep 14, 2023
38e765d
Merge pull request #424 from ECFMP/discord-webhook-tweaks
AndyTWF Sep 14, 2023
076837e
build: docker compose healthcheck
AndyTWF Oct 6, 2023
e2ed48c
deps: protobuf
AndyTWF Oct 6, 2023
fac1af8
dev: schema dump
AndyTWF Oct 8, 2023
f603bff
refactor: rename table to disambiguate purpose
AndyTWF Oct 8, 2023
b21982e
WIP: Start discord notifications
AndyTWF Oct 10, 2023
88d22cb
dev: add mac metadata to gitignore
AndyTWF Oct 11, 2023
d9b17a8
dev: remove unused docker parameter
AndyTWF Oct 11, 2023
2c2ca32
deps: bump protobuf
AndyTWF Oct 11, 2023
51af00d
feat: create new messages in discord via service
AndyTWF Oct 11, 2023
40a2df3
dev: ignore phpunit cache
AndyTWF Oct 11, 2023
bc7e5c7
refactor: rename classes
AndyTWF Oct 11, 2023
cd9e90f
refactor: rename relation
AndyTWF Oct 11, 2023
2c7963e
refactor: more classname refactoring
AndyTWF Oct 11, 2023
624b563
style: pint
AndyTWF Oct 11, 2023
19605bb
deps: bump protobuf
AndyTWF Oct 13, 2023
1f94e06
refactor: rename model methods
AndyTWF Oct 13, 2023
ae21aee
fix: schema
AndyTWF Oct 13, 2023
15043b7
refactor: start queries ecfmp webhook
AndyTWF Oct 13, 2023
d9332a9
repositories for loading flow measures for sending to ecfmp
AndyTWF Oct 15, 2023
6a1f6c2
feat: discord messages sent by services
AndyTWF Oct 16, 2023
558bd96
build: protobuf bump
AndyTWF Oct 18, 2023
14f9e0d
build: grpc in actions
AndyTWF Oct 18, 2023
168c360
test: fix test name clash
AndyTWF Oct 18, 2023
bddd23a
test: fix tests
AndyTWF Oct 18, 2023
43fe261
style: pint
AndyTWF Oct 18, 2023
d96ee70
build: checkout discord service on actions
AndyTWF Oct 18, 2023
095a347
build: move repo
AndyTWF Oct 18, 2023
d45d82c
build: protobuf in actions
AndyTWF Oct 18, 2023
c4a5ab9
build: protobuf
AndyTWF Oct 18, 2023
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
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
Loading