use App\Models\DiscordChannel;
use App\Models\DiscordGuild;
use App\Models\DiscordRole;
+use App\Models\Tournament;
use Discord\Discord;
use Discord\Parts\Channel\Channel;
+use Discord\Parts\Channel\Message;
use Discord\Parts\Guild\Guild;
use Discord\Parts\Guild\Role;
use Discord\Parts\User\Activity;
$this->error('guild role delete: '.$e->getMessage());
}
});
+ $discord->on(Event::MESSAGE_CREATE, function (Message $message, Discord $discord) {
+ // did I send this?
+ if ($message->user_id != $discord->id) {
+ return;
+ }
+ // is it thread creation?
+ if ($message->type != 18) {
+ return;
+ }
+ // was it sent on a guild channel?
+ if (!$message->guild_id || !$message->channel_id) {
+ return;
+ }
+ // does it belong to a tournament
+ $tournament = Tournament::query()
+ ->where('locked', '=', '0')
+ ->where('discord', '=', $message->guild_id)
+ ->where('discord_round_category', '=', $message->channel_id)
+ ->first();
+ if (!$tournament) {
+ return;
+ }
+ // then begone with it
+ $message->delete();
+ });
$discord->getLoop()->addSignal(SIGINT, function () use ($discord) {
$discord->close();
});
namespace App\DiscordBotCommands;
-use App\Models\ChannelCrew;
use App\Models\DiscordBotCommand;
+use App\Models\DiscordChannel;
use App\Models\DiscordGuild;
use App\Models\Episode;
use App\Models\EpisodeCrew;
use Discord\Discord;
use Discord\Parts\Channel\Channel;
use Discord\Parts\Guild\Guild;
+use Discord\Parts\Thread\Thread;
use Discord\Parts\User\Member;
use Discord\Parts\User\User as DiscordUser;
use Illuminate\Support\Facades\App;
if (isset($this->roundChannel)) {
return \React\Promise\resolve($this->roundChannel);
}
+ $parent = $this->getRoundChannelParent();
+ if (!$parent || $parent->type == 4) {
+ return $this->fetchGuild()
+ ->then(function (Guild $guild) {
+ $channel = $guild->channels->find(function (Channel $c) {
+ return $c->name == $this->getRoundChannelName();
+ });
+ if ($channel) {
+ return $channel;
+ }
+ $channel = $guild->channels->create([
+ 'name' => $this->getRoundChannelName(),
+ 'is_private' => true,
+ 'parent_id' => $this->command->tournament->discord_round_category,
+ ]);
+ return $guild->channels->save($channel);
+ })
+ ->then(function (Channel $channel) {
+ $this->roundChannel = $channel;
+ return $channel;
+ });
+ }
return $this->fetchGuild()
- ->then(function (Guild $guild) {
- $channel = $guild->channels->find(function (Channel $c) {
- return $c->name == $this->getRoundChannelName();
+ ->then(function (Guild $guild) use ($parent) {
+ return $guild->channels->fetch($parent->channel_id);
+ })
+ ->then(function (Channel $channel) {
+ $thread = $channel->threads->find(function (Thread $t) {
+ return $t->name == $this->getRoundChannelName();
});
- if ($channel) {
- return $channel;
+ if ($thread) {
+ return $thread;
}
- $channel = $guild->channels->create([
- 'name' => $this->getRoundChannelName(),
- 'is_private' => true,
- 'parent_id' => $this->command->tournament->discord_round_category,
- ]);
- return $guild->channels->save($channel);
+ return $channel->startThread($this->getRoundChannelName(), $channel->guild->premium_tier >= 2);
})
- ->then(function (Channel $channel) {
- $this->roundChannel = $channel;
- return $channel;
+ ->then(function (Thread $thread) {
+ $this->roundChannel = $thread;
+ return $thread;
});
}
protected function getRoundChannelName() {
$round = $this->getRound();
- return sprintf($this->command->tournament->discord_round_template, $round->number);
+ $replace = [
+ '%d' => $round->number,
+ '{group}' => $round->group,
+ '{number}' => $round->number,
+ '{title}' => $round->title,
+ ];
+ return trim(str_replace(array_keys($replace), array_values($replace), $this->command->tournament->discord_round_template));
+ }
+
+ protected function getRoundChannelParent(): DiscordChannel|null {
+ return DiscordChannel::query()
+ ->where('channel_id', '=', $this->command->tournament->discord_round_category)
+ ->first();
}
protected function getUser() {
use App\Models\DiscordBotCommand;
use Discord\Discord;
use Discord\Parts\Channel\Channel;
+use Discord\Parts\Thread\Thread;
use Discord\Parts\User\Member;
use React\Promise\PromiseInterface;
return \React\Promise\resolve();
}
return $this->fetchRoundChannel()
- ->then(function (Channel $channel) {
- return $this->fetchMember();
- })
- ->then(function (Member $member) {
- return $this->roundChannel->setPermissions($member, [
- 'view_channel',
- ]);
- })
- ->then(function () {
+ ->then(function (Channel|Thread $channel) {
$user = $this->getUser();
$round = $this->getRound();
$result = $user->findResult($round);
} else {
$msg = __('discord_commands.result.finish', ['name' => $user->getName(), 'time' => $result->formatTime()]);
}
- return $this->roundChannel->sendMessage($msg);
+ return $channel->sendMessage($msg);
+ })
+ ->then(function () {
+ return $this->fetchMember();
+ })
+ ->then(function (Member $member) {
+ if ($this->roundChannel instanceof Channel) {
+ return $this->roundChannel->setPermissions($member, [
+ 'view_channel',
+ ]);
+ } else {
+ return $this->roundChannel->addMember($member);
+ }
});
}
$channels = $guild->channels();
if (!empty($validatedData['parents'])) {
- $channels = $channels->whereIn('parent', $validatedData['parents']);
+ $channels->whereIn('parent', $validatedData['parents']);
}
if (!empty($validatedData['phrase'])) {
- $channels = $channels->where('name', 'LIKE', '%'.$validatedData['phrase'].'%');
+ $channels->where('name', 'LIKE', '%'.$validatedData['phrase'].'%');
}
if (!empty($validatedData['types'])) {
- $channels = $channels->whereIn('type', $validatedData['types']);
+ $channels->whereIn('type', $validatedData['types']);
}
+ $channels->orderBy('position');
return $channels->get()->toJson();
}
import Icon from './Icon';
import ChannelBox from '../discord-guilds/ChannelBox';
import debounce from '../../helpers/debounce';
+import { sortChannels } from '../../helpers/discord';
const DiscordChannelSelect = ({
guild,
signal: ctrl.signal,
});
ctrl = null;
- setResults(response.data);
+ setResults(sortChannels(response.data));
} catch (e) {
ctrl = null;
console.error(e);
import PropTypes from 'prop-types';
import React from 'react';
import { Button, Form } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import toastr from 'toastr';
import DiscordChannelSelect from '../common/DiscordChannelSelect';
touched,
tournament,
values,
-}) =>
-<Form noValidate onSubmit={handleSubmit}>
- <fieldset>
- <legend>{i18n.t('tournaments.discordSettings')}</legend>
- <Form.Group controlId="tournament.discord_round_category">
- <Form.Label>
- {i18n.t('tournaments.discordRoundCategory')}
- </Form.Label>
- <DiscordChannelSelect
- guild={tournament.discord}
- isInvalid={!!(touched.round_category && errors.round_category)}
- name="round_category"
- onBlur={handleBlur}
- onChange={handleChange}
- types={[4]}
- value={values.round_category || ''}
- />
- </Form.Group>
- <Form.Group controlId="tournament.discord_round_template">
- <Form.Label>
- {i18n.t('tournaments.discordRoundTemplate')}
- </Form.Label>
- <Form.Control
- isInvalid={!!(touched.round_template && errors.round_template)}
- name="round_template"
- onBlur={handleBlur}
- onChange={handleChange}
- type="text"
- value={values.round_template || ''}
- />
- </Form.Group>
- <Button className="mt-3" type="submit" variant="primary">
- {i18n.t('button.save')}
- </Button>
- </fieldset>
-</Form>;
+}) => {
+ const { t } = useTranslation();
+
+ return <Form noValidate onSubmit={handleSubmit}>
+ <fieldset>
+ <legend>{t('tournaments.discordSettings')}</legend>
+ <Form.Group controlId="tournament.discord_round_category">
+ <Form.Label>
+ {t('tournaments.discordRoundCategory')}
+ </Form.Label>
+ <DiscordChannelSelect
+ guild={tournament.discord}
+ isInvalid={!!(touched.round_category && errors.round_category)}
+ name="round_category"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ types={[0, 4]}
+ value={values.round_category || ''}
+ />
+ </Form.Group>
+ <Form.Group controlId="tournament.discord_round_template">
+ <Form.Label>
+ {t('tournaments.discordRoundTemplate')}
+ </Form.Label>
+ <Form.Control
+ isInvalid={!!(touched.round_template && errors.round_template)}
+ name="round_template"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ type="text"
+ value={values.round_template || ''}
+ />
+ <Form.Text muted>
+ {t('tournaments.discordRoundTemplateDescription')}
+ </Form.Text>
+ </Form.Group>
+ <Button className="mt-3" type="submit" variant="primary">
+ {t('button.save')}
+ </Button>
+ </fieldset>
+ </Form>;
+};
DiscordForm.propTypes = {
errors: PropTypes.shape({
round_category: yup.string(),
round_template: yup.string(),
}),
-})(withTranslation()(DiscordForm));
+})(DiscordForm);
// Manage Roles, Manage Channels, View Channels, Manage Events, Create Events
const permissions = '17601044415504';
export const INVITE_URL = `${authEndpoint}?client_id=${clientId}&scope=${scope}&permissions=${permissions}`;
+
+const compareChannelType = (a, b) => {
+ if (a.type === b.type) return 0;
+ if (a.type === 5) return -1;
+ if (b.type === 5) return 1;
+ if (a.type === 0) return -1;
+ if (b.type === 0) return 1;
+ return 0;
+}
+
+const compareChannelPosition = (a, b) => a.position - b.position;
+
+const compareChannel = (a, b) => {
+ return compareChannelType(a, b) || compareChannelPosition(a, b);
+};
+
+const flattenChannels = (channels) => {
+ channels.sort(compareChannel);
+ const flat = [];
+ channels.forEach((channel) => {
+ flat.push(channel);
+ if (channel.children) {
+ flat.push(...flattenChannels(channel.children));
+ }
+ })
+ return flat;
+}
+
+export const sortChannels = (channels) => {
+ const parents = [];
+ channels.forEach((channel) => {
+ if (!channel.parent_id) {
+ parents.push(channel);
+ return;
+ }
+ const parent = channels.find((c) => c.channel_id === channel.parent_id);
+ if (!parent) {
+ parents.push(channel);
+ return;
+ }
+ if (!parent.children) {
+ parent.children = [];
+ }
+ parent.children.push(channel);
+ });
+ return flattenChannels(parents);
+};
discordNoCategory: 'Keine Kategorie',
discordRoundCategory: 'Kategorie für Runden-Kanäle',
discordRoundTemplate: 'Template für Runden-Kanäle',
+ discordRoundTemplateDescription: 'Verfügbare Platzhalter: {group}, {number}, {title}. Leer lassen zum deaktivieren. Großbuchstaben sowie Leerzeichen lässt Discord nur bei Threads zu.',
discordSettings: 'Discord Einstellungen',
discordSettingsError: 'Fehler beim Speichern der Discord Einstellungen',
discordSettingsSuccess: 'Discord Einstellungen gespeichert',
discordNoCategory: 'No category',
discordRoundCategory: 'Category for round channels',
discordRoundTemplate: 'Template for round channels',
+ discordRoundTemplateDescription: 'Available replacements: {group}, {number}, {title}. Leave empty to disable. Discord will not allow capital letters and spaces in channels, only threads.',
discordSettings: 'Discord settings',
discordSettingsError: 'Error saving discord settings',
discordSettingsSuccess: 'Discord settings saved',