From: Daniel Karbach Date: Fri, 28 Nov 2025 12:12:53 +0000 (+0100) Subject: rough thread supports for round channels X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=ae7d8b34ee78fcc470c9c88b86e13ded3d852ac9;p=alttp.git rough thread supports for round channels --- diff --git a/app/Console/Commands/DiscordBotCommand.php b/app/Console/Commands/DiscordBotCommand.php index c6dce9a..4abbe92 100644 --- a/app/Console/Commands/DiscordBotCommand.php +++ b/app/Console/Commands/DiscordBotCommand.php @@ -8,8 +8,10 @@ use App\Models\DiscordBotCommand as CommandModel; 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; @@ -134,6 +136,31 @@ class DiscordBotCommand extends Command $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(); }); diff --git a/app/DiscordBotCommands/BaseCommand.php b/app/DiscordBotCommands/BaseCommand.php index af647ce..e6cc945 100644 --- a/app/DiscordBotCommands/BaseCommand.php +++ b/app/DiscordBotCommands/BaseCommand.php @@ -2,8 +2,8 @@ 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; @@ -12,6 +12,7 @@ use App\Models\User; 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; @@ -108,24 +109,44 @@ abstract class BaseCommand { 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; }); } @@ -169,7 +190,19 @@ abstract class BaseCommand { 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() { diff --git a/app/DiscordBotCommands/ResultCommand.php b/app/DiscordBotCommands/ResultCommand.php index 3f2030e..211e23b 100644 --- a/app/DiscordBotCommands/ResultCommand.php +++ b/app/DiscordBotCommands/ResultCommand.php @@ -5,6 +5,7 @@ namespace App\DiscordBotCommands; 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; @@ -19,15 +20,7 @@ class ResultCommand extends BaseCommand { 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); @@ -37,7 +30,19 @@ class ResultCommand extends BaseCommand { } 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); + } }); } diff --git a/app/Http/Controllers/DiscordChannelController.php b/app/Http/Controllers/DiscordChannelController.php index 20921ce..de4ff54 100644 --- a/app/Http/Controllers/DiscordChannelController.php +++ b/app/Http/Controllers/DiscordChannelController.php @@ -23,14 +23,15 @@ class DiscordChannelController extends Controller $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(); } diff --git a/resources/js/components/common/DiscordChannelSelect.jsx b/resources/js/components/common/DiscordChannelSelect.jsx index f726620..737a32d 100644 --- a/resources/js/components/common/DiscordChannelSelect.jsx +++ b/resources/js/components/common/DiscordChannelSelect.jsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'; import Icon from './Icon'; import ChannelBox from '../discord-guilds/ChannelBox'; import debounce from '../../helpers/debounce'; +import { sortChannels } from '../../helpers/discord'; const DiscordChannelSelect = ({ guild, @@ -52,7 +53,7 @@ const DiscordChannelSelect = ({ signal: ctrl.signal, }); ctrl = null; - setResults(response.data); + setResults(sortChannels(response.data)); } catch (e) { ctrl = null; console.error(e); diff --git a/resources/js/components/tournament/DiscordForm.jsx b/resources/js/components/tournament/DiscordForm.jsx index d896504..c59b5bb 100644 --- a/resources/js/components/tournament/DiscordForm.jsx +++ b/resources/js/components/tournament/DiscordForm.jsx @@ -3,7 +3,7 @@ import { withFormik } from 'formik'; 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'; @@ -19,42 +19,48 @@ const DiscordForm = ({ touched, tournament, values, -}) => -
-
- {i18n.t('tournaments.discordSettings')} - - - {i18n.t('tournaments.discordRoundCategory')} - - - - - - {i18n.t('tournaments.discordRoundTemplate')} - - - - -
-
; +}) => { + const { t } = useTranslation(); + + return
+
+ {t('tournaments.discordSettings')} + + + {t('tournaments.discordRoundCategory')} + + + + + + {t('tournaments.discordRoundTemplate')} + + + + {t('tournaments.discordRoundTemplateDescription')} + + + +
+
; +}; DiscordForm.propTypes = { errors: PropTypes.shape({ @@ -105,4 +111,4 @@ export default withFormik({ round_category: yup.string(), round_template: yup.string(), }), -})(withTranslation()(DiscordForm)); +})(DiscordForm); diff --git a/resources/js/helpers/discord.js b/resources/js/helpers/discord.js index bf0e349..8855889 100644 --- a/resources/js/helpers/discord.js +++ b/resources/js/helpers/discord.js @@ -4,3 +4,50 @@ const scope = 'bot+applications.commands'; // 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); +}; diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 13b5cf9..b1fedd9 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -916,6 +916,7 @@ export default { 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', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 19ecf16..4e3b211 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -916,6 +916,7 @@ export default { 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',