$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) {
namespace App\DiscordBotCommands;
use App\Models\DiscordBotCommand;
+use App\Models\DiscordGuild;
use App\Models\Round;
use App\Models\User;
use Discord\Discord;
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':
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)) {
});
}
+ 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);
protected $guild = null;
protected $member = null;
+ protected $parameterChannel = null;
protected $roundChannel = null;
protected $user = null;
--- /dev/null
+<?php
+
+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;
+
+class MessageCommand extends BaseCommand {
+
+ public function __construct(Discord $discord, DiscordBotCommand $cmd) {
+ parent::__construct($discord, $cmd);
+ }
+
+ public function execute() {
+ if (!$this->hasParameter('text')) {
+ throw new \Exception('missing text parameter');
+ }
+ return $this->fetchParameterChannel()
+ ->then(function (Channel $channel) {
+ $msg = $this->getParameter('text');
+ return $channel->sendMessage($msg);
+ });
+ }
+
+}
--- /dev/null
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\DiscordBotCommand;
+use App\Models\DiscordChannel;
+use App\Models\DiscordGuild;
+use Illuminate\Http\Request;
+
+class DiscordBotController extends Controller
+{
+
+ public function sendMessage(Request $request, DiscordGuild $guild) {
+ $this->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();
+ }
+
+}
];
$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) {
];
$cmd->status = 'pending';
$cmd->save();
+ return $cmd;
+ }
+
+ public function discord_guild() {
+ return $this->belongsTo(DiscordGuild::class);
}
public function tournament() {
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);
$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');
}
{
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;
+ }
+
}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::table('discord_bot_commands', function (Blueprint $table) {
+ $table->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');
+ });
+ }
+};
<span>{resolved ? <ChannelBox channel={resolved} /> : value}</span>
<Button
className="ms-2"
- onClick={() => onChange({ guild: null, target: { name, value: '' }})}
+ onClick={() => onChange({ channel: null, target: { name, value: '' }})}
title={t('button.unset')}
variant="outline-danger"
>
--- /dev/null
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Col, Form, Row } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+const ChannelControls = ({ channel, guild }) => {
+ const [messageText, setMessageText] = React.useState('');
+
+ const { t } = useTranslation();
+
+ const sendMessage = React.useCallback(async (text) => {
+ try {
+ await axios.post(`/api/discord-bot/${guild.id}/send-message`, {
+ channel: channel.id,
+ text,
+ });
+ toastr.success(t('discordBot.messageSuccess'));
+ } catch (e) {
+ toastr.error(t('discordBot.messageError'));
+ }
+ }, [channel, guild]);
+
+ return <section className="mt-5">
+ <h3>{t('discordBot.channelControls')}</h3>
+ <Row>
+ <Col md={6}>
+ <Form.Group>
+ <Form.Label>{t('discordBot.message')}</Form.Label>
+ <Form.Control
+ as="textarea"
+ onChange={({ target: { value } }) => {
+ setMessageText(value);
+ }}
+ value={messageText}
+ />
+ <div className="button-bar">
+ <Button
+ className="mt-2"
+ disabled={!messageText}
+ onClick={() => {
+ if (messageText) sendMessage(messageText);
+ }}
+ variant="discord"
+ >
+ {t('discordBot.sendMessage')}
+ </Button>
+ </div>
+ </Form.Group>
+ </Col>
+ </Row>
+ </section>;
+};
+
+ChannelControls.propTypes = {
+ channel: PropTypes.shape({
+ id: PropTypes.number,
+ }),
+ guild: PropTypes.shape({
+ id: PropTypes.number,
+ }),
+};
+
+export default ChannelControls;
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();
<Form.Control
as={DiscordChannelSelect}
guild={guild.guild_id}
- onChange={({ target: { value } }) => setChannel(value)}
+ onChange={({ channel }) => setChannel(channel)}
types={[]}
- value={channel}
+ value={channel ? channel.channel_id : ''}
/>
:
<Form.Control plaintext readOnly defaultValue={t('discordBot.selectGuild')} />
}
</Form.Group>
</Row>
+ {guild && channel ?
+ <ChannelControls channel={channel} guild={guild} />
+ : null}
</>;
};
} catch (e) {
toastr.error(t('twitchBot.chatError'));
}
- }, [channel, chatText, t]);
+ }, [channel, t]);
const randomChat = React.useCallback(async (category) => {
try {
} catch (e) {
toastr.error(t('twitchBot.chatError'));
}
- }, [channel, chatText, t]);
+ }, [channel, t]);
const adlibChat = React.useCallback(async () => {
try {
} catch (e) {
toastr.error(t('twitchBot.chatError'));
}
- }, [channel, chatText, t]);
+ }, [channel, t]);
const join = React.useCallback(async (bot_nick) => {
try {
},
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',
},
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',
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');