]> git.localhorst.tv Git - alttp.git/commitdiff
discord bot log
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 5 Jul 2025 16:31:05 +0000 (18:31 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sat, 5 Jul 2025 16:31:05 +0000 (18:31 +0200)
15 files changed:
app/DiscordBotCommands/MessageCommand.php
app/DiscordBotCommands/PresenceCommand.php
app/DiscordBotCommands/ResultCommand.php
app/DiscordBotCommands/SyncUserCommand.php
app/Http/Controllers/DiscordBotController.php
app/Models/DiscordBotCommand.php
resources/js/components/discord-bot/ChannelControls.jsx
resources/js/components/discord-bot/Controls.jsx
resources/js/components/discord-bot/GuildControls.jsx [new file with mode: 0644]
resources/js/components/discord-bot/GuildProtocol.jsx [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/js/pages/DiscordBot.jsx
routes/api.php
routes/channels.php

index 746bd5fd93eb29710430726954f887aa8ff72e47..f14737562c914cc43a5a7c32a196034454c6324c 100644 (file)
@@ -5,8 +5,7 @@ namespace App\DiscordBotCommands;
 use App\Models\DiscordBotCommand;
 use Discord\Discord;
 use Discord\Parts\Channel\Channel;
-use Discord\Parts\Channel\Message;
-use Discord\Parts\User\Member;
+use React\Promise\PromiseInterface;
 
 class MessageCommand extends BaseCommand {
 
@@ -14,7 +13,7 @@ class MessageCommand extends BaseCommand {
                parent::__construct($discord, $cmd);
        }
 
-       public function execute() {
+       public function execute(): PromiseInterface {
                if (!$this->hasParameter('text')) {
                        throw new \Exception('missing text parameter');
                }
index 47c15a59adccdc4b46b0334c98046470f77af401..8fa249b734d56a0c486990f1ca67153dcb5036df 100644 (file)
@@ -6,6 +6,7 @@ use App\Models\DiscordBotCommand;
 use Discord\Discord;
 use Discord\Parts\User\Activity;
 use React\Promise\Promise;
+use React\Promise\PromiseInterface;
 
 class PresenceCommand extends BaseCommand {
 
@@ -13,8 +14,8 @@ class PresenceCommand extends BaseCommand {
                parent::__construct($discord, $cmd);
        }
 
-       public function execute() {
-               return new Promise(function($resolve) {
+       public function execute(): PromiseInterface {
+               return new Promise(function ($resolve) {
                        $activity = null;
                        $idle = false;
                        $status = 'online';
index bfe35bde4bd4c114932a97ab61b2298fbbda4e56..3f2030e13e4108d4fd6d0a149f64576eb30c1859 100644 (file)
@@ -5,8 +5,8 @@ namespace App\DiscordBotCommands;
 use App\Models\DiscordBotCommand;
 use Discord\Discord;
 use Discord\Parts\Channel\Channel;
-use Discord\Parts\Channel\Message;
 use Discord\Parts\User\Member;
+use React\Promise\PromiseInterface;
 
 class ResultCommand extends BaseCommand {
 
@@ -14,7 +14,7 @@ class ResultCommand extends BaseCommand {
                parent::__construct($discord, $cmd);
        }
 
-       public function execute() {
+       public function execute(): PromiseInterface {
                if (!$this->hasRoundChannels()) {
                        return \React\Promise\resolve();
                }
index 642c8c756f42b280af537758c0aa8230f44a2911..dc2f74e5cde85429af012cdb0061fe3460b55f9a 100644 (file)
@@ -8,6 +8,7 @@ use Discord\Discord;
 use Discord\Parts\User\User as DiscordUser;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Storage;
+use React\Promise\PromiseInterface;
 
 class SyncUserCommand extends BaseCommand {
 
@@ -15,7 +16,7 @@ class SyncUserCommand extends BaseCommand {
                parent::__construct($discord, $cmd);
        }
 
-       public function execute() {
+       public function execute(): PromiseInterface {
                return $this->fetchUser()
                        ->then(function (DiscordUser $discordUser) {
                                $user = User::find($discordUser->id);
index 99a1b4cccba95a193d57be1c4ccb63178248becd..4e415a4b0241b242819921931afdfb8301bb3c25 100644 (file)
@@ -10,6 +10,11 @@ use Illuminate\Http\Request;
 class DiscordBotController extends Controller
 {
 
+       public function recentCommands(DiscordGuild $guild) {
+               $this->authorize('manage', $guild);
+               return $guild->bot_commands()->limit(10)->get();
+       }
+
        public function sendMessage(Request $request, DiscordGuild $guild) {
                $this->authorize('manage', $guild);
                $validatedData = $request->validate([
index 9397e5b30e63d0c25007cf2ebc8e4544d7d08adf..d67ee9fa8c7062656b882a203d4b2e46460e260e 100644 (file)
@@ -4,15 +4,24 @@ namespace App\Models;
 
 use App\DiscordBotCommands\BaseCommand;
 use Discord\Discord;
-use Discord\Parts\Channel\Channel;
-use Discord\Parts\Guild\Guild;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 
-class DiscordBotCommand extends Model
-{
+class DiscordBotCommand extends Model {
+
+       use BroadcastsEvents;
        use HasFactory;
 
+       public function broadcastOn(string $event): array {
+               $channels = [];
+               if ($this->discord_guild_id) {
+                       $channels[] = new PrivateChannel('DiscordGuild.'.$this->discord_guild_id);
+               }
+               return $channels;
+       }
+
        public static function queueResult(Result $result) {
                $cmd = new DiscordBotCommand();
                $cmd->tournament_id = $result->round->tournament_id;
index ee06e66c2a508a66efb45dc741ea1593e609a7ae..66840d0ded330353f8cd33fd11ef4b2d15bcfeab 100644 (file)
@@ -23,7 +23,7 @@ const ChannelControls = ({ channel, guild }) => {
        }, [channel, guild]);
 
        return <section className="mt-5">
-               <h3>{t('discordBot.channelControls')}</h3>
+               <h2>{t('discordBot.channelControls')}</h2>
                <Row>
                        <Col md={6}>
                                <Form.Group>
index 12c2fb63d6d41d3b7aafbd341d600ddcaf24a13a..f00134573d65cc471d5ff6cabdd964c496b11678 100644 (file)
@@ -3,8 +3,11 @@ import { Col, Form, Row } from 'react-bootstrap';
 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';
 
 const Controls = () => {
        const [channel, setChannel] = React.useState(null);
@@ -38,7 +41,15 @@ const Controls = () => {
                        </Form.Group>
                </Row>
                {guild && channel ?
-                       <ChannelControls channel={channel} guild={guild} />
+                       <ErrorBoundary>
+                               <ChannelControls channel={channel} guild={guild} />
+                       </ErrorBoundary>
+               : null}
+               {guild ?
+                       <ErrorBoundary>
+                               <GuildControls guild={guild} />
+                               <GuildProtocol guild={guild} />
+                       </ErrorBoundary>
                : null}
        </>;
 };
diff --git a/resources/js/components/discord-bot/GuildControls.jsx b/resources/js/components/discord-bot/GuildControls.jsx
new file mode 100644 (file)
index 0000000..545c5ee
--- /dev/null
@@ -0,0 +1,18 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+const GuildControls = ({ guild }) => {
+       const { t } = useTranslation();
+
+       return <section className="mt-5">
+               <h2>{t('discordBot.guildControls')}</h2>
+       </section>;
+};
+
+GuildControls.propTypes = {
+       guild: PropTypes.shape({
+       }),
+};
+
+export default GuildControls;
diff --git a/resources/js/components/discord-bot/GuildProtocol.jsx b/resources/js/components/discord-bot/GuildProtocol.jsx
new file mode 100644 (file)
index 0000000..1121720
--- /dev/null
@@ -0,0 +1,73 @@
+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 { 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>;
+};
+
+GuildProtocol.propTypes = {
+       guild: PropTypes.shape({
+               id: PropTypes.number,
+       }).isRequired,
+};
+
+export default GuildProtocol;
index fc4b2952b9ebf73a4bcf1c1126a5692e997c715f..6c206beda4fd541f48ed4ec72bb46415bf0ee25e 100644 (file)
@@ -140,8 +140,23 @@ export default {
                discordBot: {
                        channel: 'Kanal',
                        channelControls: 'Kanal-Steuerung',
+                       commandStatus: {
+                               done: 'Abgeschlossen',
+                               exception: 'Fehler',
+                               executing: 'Ausführen',
+                               hold: 'Pausiert',
+                               pending: 'Ausstehend',
+                       },
+                       commandTime: '{{ time, L HH:mm:ss }}',
+                       commandType: {
+                               'episode-event': 'Episoden-Event Synchronisation',
+                               message: 'Nachricht',
+                               result: 'Ergebnis',
+                       },
                        controls: 'Steuerung',
                        guild: 'Server',
+                       guildControls: 'Server-Steuerung',
+                       guildProtocol: 'Command Protokoll',
                        heading: 'Discord Bot',
                        invite: 'Bot einladen',
                        message: 'Nachricht',
index b2175c5cc10d6b815b5b08a4a92e06e34d7e9943..7ed99a392067c85e53d7e6843b5cae5e3f43ac6f 100644 (file)
@@ -140,8 +140,23 @@ export default {
                discordBot: {
                        channel: 'Channel',
                        channelControls: 'Channel controls',
+                       commandStatus: {
+                               done: 'Done',
+                               exception: 'Error',
+                               executing: 'Executing',
+                               hold: 'Hold',
+                               pending: 'Pending',
+                       },
+                       commandTime: '{{ time, L HH:mm:ss }}',
+                       commandType: {
+                               'episode-event': 'Episode event synchronization',
+                               message: 'Message',
+                               result: 'Result',
+                       },
                        controls: 'Controls',
                        guild: 'Server',
+                       guildControls: 'Server controls',
+                       guildProtocol: 'Command protocol',
                        heading: 'Discord Bot',
                        invite: 'Invite bot',
                        message: 'Message',
index 111cbbe311951bdad1d224c7d838dfdc218490e9..0f22ed29d981a1a55435474bdfba0639ad8d2b02 100644 (file)
@@ -3,6 +3,7 @@ import { Button, Container } from 'react-bootstrap';
 import { Helmet } from 'react-helmet';
 import { useTranslation } from 'react-i18next';
 
+import ErrorBoundary from '../components/common/ErrorBoundary';
 import Icon from '../components/common/Icon';
 import Controls from '../components/discord-bot/Controls';
 
@@ -30,7 +31,8 @@ export const Component = () => {
                                </Button>
                        </span>
                </p>
-               <h2>{t('discordBot.controls')}</h2>
-               <Controls />
+               <ErrorBoundary>
+                       <Controls />
+               </ErrorBoundary>
        </Container>;
 };
index 94280659e50780b9e297a54cfa1f6bdf986cfc68..81f35b84207a9f0cc8f1e85281e0f88328efbd06 100644 (file)
@@ -46,6 +46,7 @@ Route::get('content', 'App\Http\Controllers\TechniqueController@search');
 Route::get('content/{tech:name}', 'App\Http\Controllers\TechniqueController@single');
 Route::put('content/{content}', 'App\Http\Controllers\TechniqueController@update');
 
+Route::get('discord-bot/{guild}/commands', 'App\Http\Controllers\DiscordBotController@recentCommands');
 Route::post('discord-bot/{guild}/send-message', 'App\Http\Controllers\DiscordBotController@sendMessage');
 
 Route::get('discord-channels/{channel_id}', 'App\Http\Controllers\DiscordChannelController@single');
index b0a25526a12701f2f783d06a128c48433c7be02c..e04f537cd220ed1f6db0e2e76956fa7a28fbee86 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use App\Models\Channel;
+use App\Models\DiscordGuild;
 use App\Models\Tournament;
 use Illuminate\Support\Facades\Broadcast;
 
@@ -28,6 +29,11 @@ Broadcast::channel('Channel.{id}', function ($user, $id) {
        return $user->can('editRestream', $channel);
 });
 
+Broadcast::channel('DiscordGuild.{id}', function ($user, $id) {
+       $guild = DiscordGuild::findOrFail($id);
+       return $user->can('manage', $guild);
+});
+
 Broadcast::channel('Protocol.{id}', function ($user, $id) {
        $tournament = Tournament::findOrFail($id);
        return $user->can('viewProtocol', $tournament);