]> git.localhorst.tv Git - alttp.git/commitdiff
discord event subscriptions master
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 5 Jul 2025 18:36:09 +0000 (20:36 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 5 Jul 2025 18:36:09 +0000 (20:36 +0200)
12 files changed:
app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php
app/Console/Commands/SyncSpeedGaming.php
app/Http/Controllers/DiscordGuildController.php
app/Models/DiscordBotCommand.php
app/Models/DiscordGuildEventSubscription.php
app/Models/DiscordGuildUserSubscription.php
resources/js/components/discord-bot/Controls.jsx
resources/js/components/discord-bot/GuildControls.jsx
resources/js/components/discord-bot/GuildProtocol.jsx
resources/js/i18n/de.js
resources/js/i18n/en.js
routes/api.php

index 727f77fd736613aaae6bcb3552ab1576d99ebfd4..773550a07f843a363e8b5dccb692c36eac41ec65 100644 (file)
@@ -2,7 +2,9 @@
 
 namespace App\Console\Commands;
 
+use App\Models\DiscordBotCommand;
 use App\Models\DiscordGuild;
+use App\Models\DiscordGuildEpisode;
 use App\Models\Episode;
 use Illuminate\Console\Command;
 
@@ -39,11 +41,13 @@ class DiscordEpisodeSubscriptionsCommand extends Command
                return 0;
        }
 
-       private function handleGuild(DiscordGuild $guild) {
+       private function handleGuild(DiscordGuild $guild): void {
                $from = now()->sub(1, 'hour');
                $eventIDs = $guild->event_subscriptions->pluck('event_id')->toArray();
                $userIDs = $guild->user_subscriptions->pluck('user_id')->toArray();
-               if (empty($eventIDs) && empty($userIDs)) return;
+               if (empty($eventIDs) && empty($userIDs)) {
+                       return;
+               }
 
                $query = Episode::with(['channels', 'event', 'players'])
                        ->where('episodes.start', '>', $from)
@@ -61,15 +65,38 @@ class DiscordEpisodeSubscriptionsCommand extends Command
                });
                $episodes = $query->get();
                foreach ($episodes as $episode) {
-                       $this->handleEpisode($episode);
+                       $this->handleEpisode($guild, $episode);
                }
        }
 
-       private function handleEpisode(Episode $episode) {
-               $this->line($episode->start.' '.$episode->event->title.' ' .$episode->title);
+       private function handleEpisode(DiscordGuild $guild, Episode $episode): void {
+               $mtime = $episode->updated_at;
+               foreach ($episode->channels as $channel) {
+                       if ($mtime < $channel->updated_at) {
+                               $mtime = $channel->updated_at;
+                       }
+               }
+               foreach ($episode->confirmedCrew() as $crew) {
+                       if ($mtime < $crew->updated_at) {
+                               $mtime = $crew->updated_at;
+                       }
+               }
                foreach ($episode->players as $player) {
-                       $this->line(' - '.$player->name_override);
+                       if ($mtime < $player->updated_at) {
+                               $mtime = $player->updated_at;
+                       }
+               }
+               $memo = $this->getMemo($guild, $episode);
+               if (is_null($memo->synced_at) || $memo->synced_at < $mtime) {
+                       DiscordBotCommand::episodeEvent($guild, $episode);
                }
        }
 
+       private function getMemo(DiscordGuild $guild, Episode $episode): DiscordGuildEpisode {
+               return DiscordGuildEpisode::firstOrCreate([
+                       'discord_guild_id' => $guild->id,
+                       'episode_id' => $episode->id,
+               ]);
+       }
+
 }
index 6e673ad97dbd529f6eaec167fae7c0ab2ec39ee5..9dac52ec41f31a182070692507f686765c5d9643 100644 (file)
@@ -126,11 +126,13 @@ class SyncSpeedGaming extends Command {
                $this->purgeChannels($episode, $sgEntry);
                $channelIds = [];
                foreach ($sgEntry['channels'] as $sgChannel) {
-                       if ($sgChannel['initials'] == 'NONE' || $sgChannel['name'] == 'Undecided, Not SG') continue;
+                       if ($sgChannel['initials'] == 'NONE' || $sgChannel['name'] == 'Undecided, Not SG') {
+                               continue;
+                       }
                        try {
                                $channel = $this->syncChannel($episode, $sgChannel);
                                $channelIds[] = $channel->id;
-                       } catch (Exception $e) {
+                       } catch (\Exception $e) {
                                $this->error('error syncing channel '.$sgChannel['id'].': '.$e->getMessage());
                        }
                }
@@ -142,7 +144,7 @@ class SyncSpeedGaming extends Command {
                foreach ($sgEntry['broadcasters'] as $sgCrew) {
                        try {
                                $this->syncCrew($episode, $sgCrew, 'brd', 'setup');
-                       } catch (Exception $e) {
+                       } catch (\Exception $e) {
                                $this->error('error syncing broadcaster '.$sgCrew['id'].': '.$e->getMessage());
                        }
                }
@@ -151,7 +153,7 @@ class SyncSpeedGaming extends Command {
                foreach ($sgEntry['commentators'] as $sgCrew) {
                        try {
                                $this->syncCrew($episode, $sgCrew, 'comm', 'commentary');
-                       } catch (Exception $e) {
+                       } catch (\Exception $e) {
                                $this->error('error syncing commentator '.$sgCrew['id'].': '.$e->getMessage());
                        }
                }
@@ -160,7 +162,7 @@ class SyncSpeedGaming extends Command {
                foreach ($sgEntry['helpers'] as $sgCrew) {
                        try {
                                $this->syncCrew($episode, $sgCrew, 'help', 'setup');
-                       } catch (Exception $e) {
+                       } catch (\Exception $e) {
                                $this->error('error syncing helper '.$sgCrew['id'].': '.$e->getMessage());
                        }
                }
@@ -169,7 +171,7 @@ class SyncSpeedGaming extends Command {
                foreach ($sgEntry['trackers'] as $sgCrew) {
                        try {
                                $this->syncCrew($episode, $sgCrew, 'track', 'tracking');
-                       } catch (Exception $e) {
+                       } catch (\Exception $e) {
                                $this->error('error syncing tracker '.$sgCrew['id'].': '.$e->getMessage());
                        }
                }
@@ -178,7 +180,7 @@ class SyncSpeedGaming extends Command {
                foreach ($sgEntry['match1']['players'] as $sgPlayer) {
                        try {
                                $this->syncPlayer($episode, $sgPlayer);
-                       } catch (Exception $e) {
+                       } catch (\Exception $e) {
                                $this->error('error syncing player '.$sgPlayer['id'].': '.$e->getMessage());
                        }
                }
@@ -303,12 +305,12 @@ class SyncSpeedGaming extends Command {
                if (!empty($player['discordId'])) {
                        $user = User::find($player['discordId']);
                        if ($user) {
-                               if (empty($user->stream_link)) {
-                                       if (!empty($sgPlayer['publicStream'])) {
-                                               $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['publicStream']);
+                               if (!$user->stream_link) {
+                                       if (!empty($player['publicStream'])) {
+                                               $user->stream_link = 'https://twitch.tv/'.strtolower($player['publicStream']);
                                                $user->save();
-                                       } else if (!empty($sgPlayer['streamingFrom'])) {
-                                               $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['streamingFrom']);
+                                       } elseif (!empty($player['streamingFrom'])) {
+                                               $user->stream_link = 'https://twitch.tv/'.strtolower($player['streamingFrom']);
                                                $user->save();
                                        }
                                }
@@ -326,7 +328,9 @@ class SyncSpeedGaming extends Command {
                                        ['username', 'LIKE', $tag[0]],
                                        ['discriminator', '=', $tag[1]],
                                ]);
-                       if ($user) return $user;
+                       if ($user) {
+                               return $user;
+                       }
                }
                return null;
        }
index 3037a920fa2a961178cc5b4304cba3d3ff4c57df..3b4a02af5f4a2d66bb4250bb940cfa8e947ce732 100644 (file)
@@ -30,4 +30,15 @@ class DiscordGuildController extends Controller
                return $guild->toJson();
        }
 
+       public function subscriptions(Request $request, $guild_id) {
+               $guild = DiscordGuild::with([
+                       'event_subscriptions',
+                       'event_subscriptions.event',
+                       'user_subscriptions',
+                       'user_subscriptions.user',
+               ])->where('guild_id', '=', $guild_id)->firstOrFail();
+               $this->authorize('manage', $guild);
+               return $guild->toJson();
+       }
+
 }
index d67ee9fa8c7062656b882a203d4b2e46460e260e..b3670637dda4241b16a4f318116ff69db80a898e 100644 (file)
@@ -22,7 +22,19 @@ class DiscordBotCommand extends Model {
                return $channels;
        }
 
-       public static function queueResult(Result $result) {
+       public static function episodeEvent(DiscordGuild $guild, Episode $episode): DiscordBotCommand {
+               $cmd = new DiscordBotCommand();
+               $cmd->discord_guild()->associate($guild);
+               $cmd->command = 'episode-event';
+               $cmd->parameters = [
+                       'episode' => $episode->id,
+               ];
+               $cmd->status = 'pending';
+               $cmd->save();
+               return $cmd;
+       }
+
+       public static function queueResult(Result $result): DiscordBotCommand {
                $cmd = new DiscordBotCommand();
                $cmd->tournament_id = $result->round->tournament_id;
                $cmd->command = 'result';
@@ -35,7 +47,7 @@ class DiscordBotCommand extends Model {
                return $cmd;
        }
 
-       public static function sendMessage(DiscordChannel $channel, $text, User $user = null) {
+       public static function sendMessage(DiscordChannel $channel, string $text, User $user = null): DiscordBotCommand {
                $cmd = new DiscordBotCommand();
                $cmd->discord_guild_id = $channel->discord_guild_id;
                if ($user) {
@@ -51,7 +63,7 @@ class DiscordBotCommand extends Model {
                return $cmd;
        }
 
-       public static function syncUser($user_id) {
+       public static function syncUser($user_id): DiscordBotCommand {
                $cmd = new DiscordBotCommand();
                $cmd->command = 'sync-user';
                $cmd->parameters = [
index 11a5da90d94ea8c1d49a85e30d7dd12b4c20f41e..7bc00022c3745d5127d27405fcc428f593ee798d 100644 (file)
@@ -2,7 +2,28 @@
 
 namespace App\Models;
 
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Model;
 
 class DiscordGuildEventSubscription extends Model {
+
+       use BroadcastsEvents;
+
+       public function broadcastOn(string $event): array {
+               $channels = [];
+               if ($this->discord_guild_id) {
+                       $channels[] = new PrivateChannel('DiscordGuild.'.$this->discord_guild_id);
+               }
+               return $channels;
+       }
+
+       public function event() {
+               return $this->belongsTo(Event::class);
+       }
+
+       public function guild() {
+               return $this->belongsTo(DiscordGuild::class);
+       }
+
 }
index d1a3e6aac8d5acce6f3a41bc2a0e83f2b9e70d02..4e826de0784ca7bdd373bc3f7102231074fb7f27 100644 (file)
@@ -2,7 +2,28 @@
 
 namespace App\Models;
 
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Model;
 
 class DiscordGuildUserSubscription extends Model {
+
+       use BroadcastsEvents;
+
+       public function broadcastOn(string $event): array {
+               $channels = [];
+               if ($this->discord_guild_id) {
+                       $channels[] = new PrivateChannel('DiscordGuild.'.$this->discord_guild_id);
+               }
+               return $channels;
+       }
+
+       public function guild() {
+               return $this->belongsTo(DiscordGuild::class);
+       }
+
+       public function user() {
+               return $this->belongsTo(User::class);
+       }
+
 }
index f00134573d65cc471d5ff6cabdd964c496b11678..6a4580dd834c9fd1008012a62070a4aaec8a3aa8 100644 (file)
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
 
 import ChannelControls from './ChannelControls';
 import GuildControls from './GuildControls';
-import GuildProtocol from './GuildProtocol';
 import DiscordChannelSelect from '../common/DiscordChannelSelect';
 import DiscordSelect from '../common/DiscordSelect';
 import ErrorBoundary from '../common/ErrorBoundary';
@@ -48,7 +47,6 @@ const Controls = () => {
                {guild ?
                        <ErrorBoundary>
                                <GuildControls guild={guild} />
-                               <GuildProtocol guild={guild} />
                        </ErrorBoundary>
                : null}
        </>;
index 545c5ee9314ae5acbd6743f1806b60a5025dabb9..b7255800f38653a3acf50386562421aa159832a3 100644 (file)
@@ -1,17 +1,71 @@
+import axios from 'axios';
 import PropTypes from 'prop-types';
 import React from 'react';
+import { Col, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 
+import GuildProtocol from './GuildProtocol';
+
 const GuildControls = ({ guild }) => {
+       const [protocol, setProtocol] = React.useState([]);
+       const [subscriptions, setSubscriptions] = React.useState({});
+
        const { t } = useTranslation();
 
+       React.useEffect(() => {
+               const ctrl = new AbortController();
+               axios
+                       .get(`/api/discord-bot/${guild.id}/commands`, { signal: ctrl.signal })
+                       .then(response => {
+                               setProtocol(response.data);
+                       });
+               axios
+                       .get(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { signal: ctrl.signal })
+                       .then(response => {
+                               setSubscriptions(response.data);
+                       });
+               window.Echo.private(`DiscordGuild.${guild.id}`)
+                       .listen('.DiscordBotCommandCreated', e => {
+                               if (e.model) {
+                                       setProtocol(protocol => [e.model, ...protocol]);
+                               }
+                       })
+                       .listen('.DiscordBotCommandUpdated', e => {
+                               if (e.model) {
+                                       setProtocol(protocol => protocol.map(p => p.id === e.model.id ? { ...p, ...e.mode } : p));
+                               }
+                       });
+               return () => {
+                       ctrl.abort();
+                       window.Echo.leave(`DiscordGuild.${guild.id}`);
+               };
+       }, [guild.id]);
+
        return <section className="mt-5">
                <h2>{t('discordBot.guildControls')}</h2>
+               <Row>
+                       <Col md={6}>
+                               <h3>{t('discordBot.eventSubscriptions')}</h3>
+                               {subscriptions.event_subscriptions ? subscriptions.event_subscriptions.map(esub =>
+                                       <div key={esub.id}>{esub.event.title}</div>
+                               ): null}
+                       </Col>
+                       <Col md={6}>
+                               <h3>{t('discordBot.userSubscriptions')}</h3>
+                               {subscriptions.user_subscriptions ? subscriptions.user_subscriptions.map(usub =>
+                                       <div key={usub.id}>{usub.user.username}</div>
+                               ): null}
+                       </Col>
+               </Row>
+               <h3>{t('discordBot.guildProtocol')}</h3>
+               <GuildProtocol protocol={protocol} />
        </section>;
 };
 
 GuildControls.propTypes = {
        guild: PropTypes.shape({
+               id: PropTypes.number,
+               guild_id: PropTypes.string,
        }),
 };
 
index 112172079f788643c8a5c18b4564e888b74ba0fc..be6bbe5878574d621d212cabdcede7b29306886b 100644 (file)
@@ -1,73 +1,39 @@
-import axios from 'axios';
 import PropTypes from 'prop-types';
 import React from 'react';
 import { useTranslation } from 'react-i18next';
 
-import Loading from '../common/Loading';
-
-const GuildProtocol = ({ guild }) => {
-       const [loading, setLoading] = React.useState(true);
-       const [protocol, setProtocol] = React.useState([]);
-
+const GuildProtocol = ({ protocol }) => {
        const { t } = useTranslation();
 
-       React.useEffect(() => {
-               const ctrl = new AbortController();
-               axios
-                       .get(`/api/discord-bot/${guild.id}/commands`, { signal: ctrl.signal })
-                       .then(response => {
-                               setLoading(false);
-                               setProtocol(response.data);
-                       });
-               window.Echo.private(`DiscordGuild.${guild.id}`)
-                       .listen('.DiscordBotCommandCreated', e => {
-                               if (e.model) {
-                                       setProtocol(protocol => [e.model, ...protocol]);
-                               }
-                       })
-                       .listen('.DiscordBotCommandUpdated', e => {
-                               if (e.model) {
-                                       setProtocol(protocol => protocol.map(p => p.id === e.model.id ? { ...p, ...e.mode } : p));
-                               }
-                       });
-               return () => {
-                       ctrl.abort();
-                       window.Echo.leave(`DiscordGuild.${guild.id}`);
-               };
-       }, [guild.id]);
-
-       return <section className="mt-5">
-               <h2>{t('discordBot.guildProtocol')}</h2>
-               {loading ?
-                       <Loading />
-               :
-                       protocol.map((entry) =>
-                               <div className="discord-bot-protocol border-top" key={entry.id}>
-                                       <div className="d-flex justify-content-between">
-                                               <span>{t(`discordBot.commandType.${entry.command}`)}</span>
-                                               <span>{t(`discordBot.commandStatus.${entry.status}`)}</span>
-                                       </div>
-                                       <div className="d-flex justify-content-between">
-                                               <span className="text-muted">
-                                                       {t('discordBot.commandTime', { time: new Date(entry.created_at) })}
-                                               </span>
-                                               <span className="text-muted">
-                                                       {entry.executed_at
-                                                               ? t('discordBot.commandTime', { time: new Date(entry.executed_at) })
-                                                               : t('discordBot.commandPending')
-                                                       }
-                                               </span>
-                                       </div>
-                               </div>
-                       )
-               }
-       </section>;
+       return protocol.map((entry) =>
+               <div className="discord-bot-protocol border-top" key={entry.id}>
+                       <div className="d-flex justify-content-between">
+                               <span>{t(`discordBot.commandType.${entry.command}`)}</span>
+                               <span>{t(`discordBot.commandStatus.${entry.status}`)}</span>
+                       </div>
+                       <div className="d-flex justify-content-between">
+                               <span className="text-muted">
+                                       {t('discordBot.commandTime', { time: new Date(entry.created_at) })}
+                               </span>
+                               <span className="text-muted">
+                                       {entry.executed_at
+                                               ? t('discordBot.commandTime', { time: new Date(entry.executed_at) })
+                                               : t('discordBot.commandPending')
+                                       }
+                               </span>
+                       </div>
+               </div>
+       );
 };
 
 GuildProtocol.propTypes = {
-       guild: PropTypes.shape({
+       protocol: PropTypes.arrayOf(PropTypes.shape({
+               command: PropTypes.string,
+               created_at: PropTypes.string,
+               executed_at: PropTypes.string,
                id: PropTypes.number,
-       }).isRequired,
+               status: PropTypes.string,
+       })).isRequired,
 };
 
 export default GuildProtocol;
index 6c206beda4fd541f48ed4ec72bb46415bf0ee25e..b972766a01acbe96f7f31c763eed42fa82337e40 100644 (file)
@@ -154,6 +154,7 @@ export default {
                                result: 'Ergebnis',
                        },
                        controls: 'Steuerung',
+                       eventSubscriptions: 'Event Subscriptions',
                        guild: 'Server',
                        guildControls: 'Server-Steuerung',
                        guildProtocol: 'Command Protokoll',
@@ -163,7 +164,8 @@ export default {
                        messageError: 'Fehler beim Senden',
                        messageSuccess: 'Nachricht in Warteschlange',
                        selectGuild: 'Bitte Server wählen',
-                       sendMessage: 'Nachricht senden'
+                       sendMessage: 'Nachricht senden',
+                       userSubscriptions: 'User Subscriptions',
                },
                episodes: {
                        addRestream: 'Neuer Restream',
index 7ed99a392067c85e53d7e6843b5cae5e3f43ac6f..72ac2345301eee8858dac46bcde0a63885321b97 100644 (file)
@@ -154,6 +154,7 @@ export default {
                                result: 'Result',
                        },
                        controls: 'Controls',
+                       eventSubscriptions: 'Event subscriptions',
                        guild: 'Server',
                        guildControls: 'Server controls',
                        guildProtocol: 'Command protocol',
@@ -164,6 +165,7 @@ export default {
                        messageSuccess: 'Message queued',
                        selectGuild: 'Please select server',
                        sendMessage: 'Send message',
+                       userSubscriptions: 'User subscriptions',
                },
                episodes: {
                        addRestream: 'Add Restream',
index 81f35b84207a9f0cc8f1e85281e0f88328efbd06..7a31fa78b8463752951c0af3f6e2e3cae7dccbde 100644 (file)
@@ -54,6 +54,7 @@ Route::get('discord-channels/{channel_id}', 'App\Http\Controllers\DiscordChannel
 Route::get('discord-guilds', 'App\Http\Controllers\DiscordGuildController@search');
 Route::get('discord-guilds/{guild_id}', 'App\Http\Controllers\DiscordGuildController@single');
 Route::get('discord-guilds/{guild_id}/channels', 'App\Http\Controllers\DiscordChannelController@search');
+Route::get('discord-guilds/{guild_id}/subscriptions', 'App\Http\Controllers\DiscordGuildController@subscriptions');
 
 Route::get('episodes', 'App\Http\Controllers\EpisodeController@search');
 Route::post('episodes/{episode}/add-restream', 'App\Http\Controllers\EpisodeController@addRestream');