From 29533d4d443e223533b694b6c31df2723f1cdecc Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 2 Jul 2025 14:03:17 +0200 Subject: [PATCH] add discord bot chat command --- app/Console/Commands/DiscordBotCommand.php | 1 + app/DiscordBotCommands/BaseCommand.php | 28 +++++++- app/DiscordBotCommands/MessageCommand.php | 28 ++++++++ app/Http/Controllers/DiscordBotController.php | 25 +++++++ app/Models/DiscordBotCommand.php | 27 +++++++- app/Models/DiscordGuild.php | 4 ++ app/Policies/DiscordGuildPolicy.php | 13 ++++ ...5_link_discord_bot_commands_and_guilds.php | 28 ++++++++ .../common/DiscordChannelSelect.jsx | 2 +- .../discord-bot/ChannelControls.jsx | 65 +++++++++++++++++++ .../js/components/discord-bot/Controls.jsx | 10 ++- .../js/components/twitch-bot/Controls.jsx | 6 +- resources/js/i18n/de.js | 5 ++ resources/js/i18n/en.js | 5 ++ routes/api.php | 2 + 15 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 app/DiscordBotCommands/MessageCommand.php create mode 100644 app/Http/Controllers/DiscordBotController.php create mode 100644 database/migrations/2025_07_02_095525_link_discord_bot_commands_and_guilds.php create mode 100644 resources/js/components/discord-bot/ChannelControls.jsx diff --git a/app/Console/Commands/DiscordBotCommand.php b/app/Console/Commands/DiscordBotCommand.php index 18faf8d..769f284 100644 --- a/app/Console/Commands/DiscordBotCommand.php +++ b/app/Console/Commands/DiscordBotCommand.php @@ -60,6 +60,7 @@ class DiscordBotCommand extends Command $discord->getLoop()->addPeriodicTimer(1, function () use ($discord) { $command = CommandModel::where('status', '=', 'pending')->oldest()->first(); if ($command) { + $this->line('executing command '.$command->id.': '.$command->command); try { $command->execute($discord); } catch (\Exception $e) { diff --git a/app/DiscordBotCommands/BaseCommand.php b/app/DiscordBotCommands/BaseCommand.php index e6861e0..61152c1 100644 --- a/app/DiscordBotCommands/BaseCommand.php +++ b/app/DiscordBotCommands/BaseCommand.php @@ -3,6 +3,7 @@ namespace App\DiscordBotCommands; use App\Models\DiscordBotCommand; +use App\Models\DiscordGuild; use App\Models\Round; use App\Models\User; use Discord\Discord; @@ -16,6 +17,8 @@ abstract class BaseCommand { public static function resolve(Discord $discord, DiscordBotCommand $cmd) { switch ($cmd->command) { + case 'message': + return new MessageCommand($discord, $cmd); case 'presence': return new PresenceCommand($discord, $cmd); case 'result': @@ -42,8 +45,13 @@ abstract class BaseCommand { if (isset($this->guild)) { return \React\Promise\resolve($this->guild); } + if (is_null($this->command->discord_guild)) { + $g = DiscordGuild::where('guild_id', '=', $this->command->tournament->discord)->firstOrFail(); + $this->command->discord_guild()->associate($g); + $this->command->save(); + } return $this->discord->guilds - ->fetch($this->command->tournament->discord) + ->fetch($this->command->discord_guild->guild_id) ->then(function (Guild $guild) { $this->guild = $guild; if ($guild->preferred_locale && !($this->command->tournament && $this->command->tournament->locale)) { @@ -64,6 +72,23 @@ abstract class BaseCommand { }); } + protected function fetchParameterChannel() { + if (isset($this->parameterChannel)) { + return \React\Promise\resolve($this->parameterChannel); + } + if (!$this->hasParameter('channel_id')) { + throw new \Exception('missing channel_id parameter'); + } + return $this->fetchGuild() + ->then(function (Guild $guild) { + return $guild->channels->fetch($this->getParameter('channel_id')); + }) + ->then(function (Channel $channel) { + $this->parameterChannel = $channel; + return $channel; + }); + } + protected function fetchRoundChannel() { if (isset($this->roundChannel)) { return \React\Promise\resolve($this->roundChannel); @@ -139,6 +164,7 @@ abstract class BaseCommand { protected $guild = null; protected $member = null; + protected $parameterChannel = null; protected $roundChannel = null; protected $user = null; diff --git a/app/DiscordBotCommands/MessageCommand.php b/app/DiscordBotCommands/MessageCommand.php new file mode 100644 index 0000000..746bd5f --- /dev/null +++ b/app/DiscordBotCommands/MessageCommand.php @@ -0,0 +1,28 @@ +hasParameter('text')) { + throw new \Exception('missing text parameter'); + } + return $this->fetchParameterChannel() + ->then(function (Channel $channel) { + $msg = $this->getParameter('text'); + return $channel->sendMessage($msg); + }); + } + +} diff --git a/app/Http/Controllers/DiscordBotController.php b/app/Http/Controllers/DiscordBotController.php new file mode 100644 index 0000000..99a1b4c --- /dev/null +++ b/app/Http/Controllers/DiscordBotController.php @@ -0,0 +1,25 @@ +authorize('manage', $guild); + $validatedData = $request->validate([ + 'channel' => 'required|exists:App\\Models\\DiscordChannel,id', + 'text' => 'string', + ]); + $channel = DiscordChannel::findOrFail($validatedData['channel']); + $this->authorize('manage', $channel->guild); + $cmd = DiscordBotCommand::sendMessage($channel, $validatedData['text']); + return $cmd->toJson(); + } + +} diff --git a/app/Models/DiscordBotCommand.php b/app/Models/DiscordBotCommand.php index 7d7c3a8..8a8b81b 100644 --- a/app/Models/DiscordBotCommand.php +++ b/app/Models/DiscordBotCommand.php @@ -23,6 +23,23 @@ class DiscordBotCommand extends Model ]; $cmd->status = 'pending'; $cmd->save(); + return $cmd; + } + + public static function sendMessage(DiscordChannel $channel, $text, User $user = null) { + $cmd = new DiscordBotCommand(); + $cmd->discord_guild_id = $channel->discord_guild_id; + if ($user) { + $cmd->user()->associate($user); + } + $cmd->command = 'message'; + $cmd->parameters = [ + 'channel_id' => $channel->channel_id, + 'text' => $text, + ]; + $cmd->status = 'pending'; + $cmd->save(); + return $cmd; } public static function syncUser($user_id) { @@ -33,6 +50,11 @@ class DiscordBotCommand extends Model ]; $cmd->status = 'pending'; $cmd->save(); + return $cmd; + } + + public function discord_guild() { + return $this->belongsTo(DiscordGuild::class); } public function tournament() { @@ -49,11 +71,10 @@ class DiscordBotCommand extends Model try { BaseCommand::resolve($discord, $this) ->execute() - ->otherwise(function (\Throwable $e) { - $this->setException($e); - }) ->done(function($v = null) { $this->setDone(); + }, function (\Throwable $e) { + $this->setException($e); }); } catch (\Exception $e) { $this->setException($e); diff --git a/app/Models/DiscordGuild.php b/app/Models/DiscordGuild.php index bcbd12d..db0825a 100644 --- a/app/Models/DiscordGuild.php +++ b/app/Models/DiscordGuild.php @@ -44,6 +44,10 @@ class DiscordGuild extends Model $model->channels()->whereNotIn('channel_id', $channel_ids)->delete(); } + public function bot_commands() { + return $this->hasMany(DiscordBotCommand::class)->orderBy('created_at', 'DESC'); + } + public function channels() { return $this->hasMany(DiscordChannel::class)->orderBy('position'); } diff --git a/app/Policies/DiscordGuildPolicy.php b/app/Policies/DiscordGuildPolicy.php index b437656..285138c 100644 --- a/app/Policies/DiscordGuildPolicy.php +++ b/app/Policies/DiscordGuildPolicy.php @@ -91,4 +91,17 @@ class DiscordGuildPolicy { return false; } + + /** + * Determine whether the user can perform administrative tasks for the guild. + * + * @param \App\Models\User $user + * @param \App\Models\DiscordGuild $discordGuild + * @return \Illuminate\Auth\Access\Response|bool + */ + public function manage(User $user, DiscordGuild $discordGuild) + { + return $user->isAdmin() || $discordGuild->owner == $user->id; + } + } diff --git a/database/migrations/2025_07_02_095525_link_discord_bot_commands_and_guilds.php b/database/migrations/2025_07_02_095525_link_discord_bot_commands_and_guilds.php new file mode 100644 index 0000000..31c7815 --- /dev/null +++ b/database/migrations/2025_07_02_095525_link_discord_bot_commands_and_guilds.php @@ -0,0 +1,28 @@ +foreignId('discord_guild_id')->nullable()->default(null)->constrained(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('discord_bot_commands', function (Blueprint $table) { + $table->dropColumn('discord_guild_id'); + }); + } +}; diff --git a/resources/js/components/common/DiscordChannelSelect.jsx b/resources/js/components/common/DiscordChannelSelect.jsx index 01ee3b0..653db1a 100644 --- a/resources/js/components/common/DiscordChannelSelect.jsx +++ b/resources/js/components/common/DiscordChannelSelect.jsx @@ -83,7 +83,7 @@ const DiscordChannelSelect = ({ {resolved ? : value} + + + + + ; +}; + +ChannelControls.propTypes = { + channel: PropTypes.shape({ + id: PropTypes.number, + }), + guild: PropTypes.shape({ + id: PropTypes.number, + }), +}; + +export default ChannelControls; diff --git a/resources/js/components/discord-bot/Controls.jsx b/resources/js/components/discord-bot/Controls.jsx index 408e314..12c2fb6 100644 --- a/resources/js/components/discord-bot/Controls.jsx +++ b/resources/js/components/discord-bot/Controls.jsx @@ -2,11 +2,12 @@ import React from 'react'; import { Col, Form, Row } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import ChannelControls from './ChannelControls'; import DiscordChannelSelect from '../common/DiscordChannelSelect'; import DiscordSelect from '../common/DiscordSelect'; const Controls = () => { - const [channel, setChannel] = React.useState(''); + const [channel, setChannel] = React.useState(null); const [guild, setGuild] = React.useState(null); const { t } = useTranslation(); @@ -27,15 +28,18 @@ const Controls = () => { setChannel(value)} + onChange={({ channel }) => setChannel(channel)} types={[]} - value={channel} + value={channel ? channel.channel_id : ''} /> : } + {guild && channel ? + + : null} ; }; diff --git a/resources/js/components/twitch-bot/Controls.jsx b/resources/js/components/twitch-bot/Controls.jsx index 394f546..55cbd00 100644 --- a/resources/js/components/twitch-bot/Controls.jsx +++ b/resources/js/components/twitch-bot/Controls.jsx @@ -54,7 +54,7 @@ const Controls = () => { } catch (e) { toastr.error(t('twitchBot.chatError')); } - }, [channel, chatText, t]); + }, [channel, t]); const randomChat = React.useCallback(async (category) => { try { @@ -66,7 +66,7 @@ const Controls = () => { } catch (e) { toastr.error(t('twitchBot.chatError')); } - }, [channel, chatText, t]); + }, [channel, t]); const adlibChat = React.useCallback(async () => { try { @@ -78,7 +78,7 @@ const Controls = () => { } catch (e) { toastr.error(t('twitchBot.chatError')); } - }, [channel, chatText, t]); + }, [channel, t]); const join = React.useCallback(async (bot_nick) => { try { diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 6c17132..fc4b295 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -139,11 +139,16 @@ export default { }, discordBot: { channel: 'Kanal', + channelControls: 'Kanal-Steuerung', controls: 'Steuerung', guild: 'Server', heading: 'Discord Bot', invite: 'Bot einladen', + message: 'Nachricht', + messageError: 'Fehler beim Senden', + messageSuccess: 'Nachricht in Warteschlange', selectGuild: 'Bitte Server wählen', + sendMessage: 'Nachricht senden' }, episodes: { addRestream: 'Neuer Restream', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 1341118..b2175c5 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -139,11 +139,16 @@ export default { }, discordBot: { channel: 'Channel', + channelControls: 'Channel controls', controls: 'Controls', guild: 'Server', heading: 'Discord Bot', invite: 'Invite bot', + message: 'Message', + messageError: 'Error sending message', + messageSuccess: 'Message queued', selectGuild: 'Please select server', + sendMessage: 'Send message', }, episodes: { addRestream: 'Add Restream', diff --git a/routes/api.php b/routes/api.php index b8f2033..9428065 100644 --- a/routes/api.php +++ b/routes/api.php @@ -46,6 +46,8 @@ 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::post('discord-bot/{guild}/send-message', 'App\Http\Controllers\DiscordBotController@sendMessage'); + Route::get('discord-channels/{channel_id}', 'App\Http\Controllers\DiscordChannelController@single'); Route::get('discord-guilds', 'App\Http\Controllers\DiscordGuildController@search'); -- 2.39.5