From: Daniel Karbach Date: Sat, 5 Jul 2025 18:36:09 +0000 (+0200) Subject: discord event subscriptions X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=7c45c345014e7fd4fb4d575b0f82a2e4dc3a1d47;p=alttp.git discord event subscriptions --- diff --git a/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php b/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php index 727f77f..773550a 100644 --- a/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php +++ b/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php @@ -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, + ]); + } + } diff --git a/app/Console/Commands/SyncSpeedGaming.php b/app/Console/Commands/SyncSpeedGaming.php index 6e673ad..9dac52e 100644 --- a/app/Console/Commands/SyncSpeedGaming.php +++ b/app/Console/Commands/SyncSpeedGaming.php @@ -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; } diff --git a/app/Http/Controllers/DiscordGuildController.php b/app/Http/Controllers/DiscordGuildController.php index 3037a92..3b4a02a 100644 --- a/app/Http/Controllers/DiscordGuildController.php +++ b/app/Http/Controllers/DiscordGuildController.php @@ -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(); + } + } diff --git a/app/Models/DiscordBotCommand.php b/app/Models/DiscordBotCommand.php index d67ee9f..b367063 100644 --- a/app/Models/DiscordBotCommand.php +++ b/app/Models/DiscordBotCommand.php @@ -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 = [ diff --git a/app/Models/DiscordGuildEventSubscription.php b/app/Models/DiscordGuildEventSubscription.php index 11a5da9..7bc0002 100644 --- a/app/Models/DiscordGuildEventSubscription.php +++ b/app/Models/DiscordGuildEventSubscription.php @@ -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); + } + } diff --git a/app/Models/DiscordGuildUserSubscription.php b/app/Models/DiscordGuildUserSubscription.php index d1a3e6a..4e826de 100644 --- a/app/Models/DiscordGuildUserSubscription.php +++ b/app/Models/DiscordGuildUserSubscription.php @@ -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); + } + } diff --git a/resources/js/components/discord-bot/Controls.jsx b/resources/js/components/discord-bot/Controls.jsx index f001345..6a4580d 100644 --- a/resources/js/components/discord-bot/Controls.jsx +++ b/resources/js/components/discord-bot/Controls.jsx @@ -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 ? - : null} ; diff --git a/resources/js/components/discord-bot/GuildControls.jsx b/resources/js/components/discord-bot/GuildControls.jsx index 545c5ee..b725580 100644 --- a/resources/js/components/discord-bot/GuildControls.jsx +++ b/resources/js/components/discord-bot/GuildControls.jsx @@ -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

{t('discordBot.guildControls')}

+ + +

{t('discordBot.eventSubscriptions')}

+ {subscriptions.event_subscriptions ? subscriptions.event_subscriptions.map(esub => +
{esub.event.title}
+ ): null} + + +

{t('discordBot.userSubscriptions')}

+ {subscriptions.user_subscriptions ? subscriptions.user_subscriptions.map(usub => +
{usub.user.username}
+ ): null} + +
+

{t('discordBot.guildProtocol')}

+
; }; GuildControls.propTypes = { guild: PropTypes.shape({ + id: PropTypes.number, + guild_id: PropTypes.string, }), }; diff --git a/resources/js/components/discord-bot/GuildProtocol.jsx b/resources/js/components/discord-bot/GuildProtocol.jsx index 1121720..be6bbe5 100644 --- a/resources/js/components/discord-bot/GuildProtocol.jsx +++ b/resources/js/components/discord-bot/GuildProtocol.jsx @@ -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
-

{t('discordBot.guildProtocol')}

- {loading ? - - : - protocol.map((entry) => -
-
- {t(`discordBot.commandType.${entry.command}`)} - {t(`discordBot.commandStatus.${entry.status}`)} -
-
- - {t('discordBot.commandTime', { time: new Date(entry.created_at) })} - - - {entry.executed_at - ? t('discordBot.commandTime', { time: new Date(entry.executed_at) }) - : t('discordBot.commandPending') - } - -
-
- ) - } -
; + return protocol.map((entry) => +
+
+ {t(`discordBot.commandType.${entry.command}`)} + {t(`discordBot.commandStatus.${entry.status}`)} +
+
+ + {t('discordBot.commandTime', { time: new Date(entry.created_at) })} + + + {entry.executed_at + ? t('discordBot.commandTime', { time: new Date(entry.executed_at) }) + : t('discordBot.commandPending') + } + +
+
+ ); }; 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; diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 6c206be..b972766 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -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', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 7ed99a3..72ac234 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -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', diff --git a/routes/api.php b/routes/api.php index 81f35b8..7a31fa7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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');