$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');