From: Daniel Karbach Date: Thu, 28 Aug 2025 17:10:48 +0000 (+0200) Subject: discord guild channel subscriptions X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=14759bbbcd0fd69b88317b9378088733a977d548;p=alttp.git discord guild channel subscriptions --- diff --git a/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php b/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php index 8c59b6f..5b20165 100644 --- a/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php +++ b/app/Console/Commands/DiscordEpisodeSubscriptionsCommand.php @@ -44,6 +44,7 @@ class DiscordEpisodeSubscriptionsCommand extends Command private function handleGuild(DiscordGuild $guild): void { $from = now()->sub(1, 'hour'); $until = now()->add(14, 'days'); + $channelIDs = $guild->channel_subscriptions->pluck('channel_id')->toArray(); $eventIDs = $guild->event_subscriptions->pluck('event_id')->toArray(); $modeIDs = $guild->ladder_subscriptions->pluck('step_ladder_mode_id')->toArray(); $userIDs = $guild->user_subscriptions->pluck('user_id')->toArray(); @@ -57,9 +58,14 @@ class DiscordEpisodeSubscriptionsCommand extends Command ->where('episodes.confirmed', '=', true) ->orderBy('episodes.start', 'ASC') ->limit(20); - $query->where(function ($subquery) use ($eventIDs, $modeIDs, $userIDs) { + $query->where(function ($subquery) use ($channelIDs, $eventIDs, $modeIDs, $userIDs) { + if (!empty($channelIDs)) { + $subquery->orWhereHas('channels', function ($builder) use ($channelIDs) { + $builder->whereIn('channel_episode.channel_id', $channelIDs); + }); + } if (!empty($eventIDs)) { - $subquery->whereIn('episodes.event_id', $eventIDs); + $subquery->orWhereIn('episodes.event_id', $eventIDs); } if (!empty($modeIDs)) { $subquery->orWhereIn('episodes.step_ladder_mode_id', $modeIDs); diff --git a/app/Http/Controllers/DiscordGuildController.php b/app/Http/Controllers/DiscordGuildController.php index 979c675..59c0904 100644 --- a/app/Http/Controllers/DiscordGuildController.php +++ b/app/Http/Controllers/DiscordGuildController.php @@ -67,6 +67,8 @@ class DiscordGuildController extends Controller public function subscriptions(Request $request, $guild_id) { $guild = DiscordGuild::with([ + 'channel_subscriptions', + 'channel_subscriptions.channel', 'event_subscriptions', 'event_subscriptions.event', 'ladder_subscriptions', @@ -83,14 +85,19 @@ class DiscordGuildController extends Controller $this->authorize('manage', $guild); $validatedData = $request->validate([ + 'add_channel' => 'numeric|exists:App\Models\Channel,id', 'add_event' => 'numeric|exists:App\Models\Event,id', 'add_ladder' => 'numeric|exists:App\Models\StepLadderMode,id', 'add_user' => 'numeric|exists:App\Models\User,id', + 'remove_channel' => 'numeric|exists:App\Models\Channel,id', 'remove_event' => 'numeric|exists:App\Models\Event,id', 'remove_ladder' => 'numeric|exists:App\Models\StepLadderMode,id', 'remove_user' => 'numeric|exists:App\Models\User,id', ]); + if (isset($validatedData['add_channel'])) { + $guild->channel_subscriptions()->create(['channel_id' => $validatedData['add_channel']]); + } if (isset($validatedData['add_event'])) { $guild->event_subscriptions()->create(['event_id' => $validatedData['add_event']]); } @@ -100,6 +107,10 @@ class DiscordGuildController extends Controller if (isset($validatedData['add_user'])) { $guild->user_subscriptions()->create(['user_id' => $validatedData['add_user']]); } + if (isset($validatedData['remove_channel'])) { + $sub = $guild->channel_subscriptions()->where('channel_id', '=', $validatedData['remove_channel'])->firstOrFail(); + $sub->delete(); + } if (isset($validatedData['remove_event'])) { $sub = $guild->event_subscriptions()->where('event_id', '=', $validatedData['remove_event'])->firstOrFail(); $sub->delete(); @@ -113,6 +124,8 @@ class DiscordGuildController extends Controller $sub->delete(); } $guild->load([ + 'channel_subscriptions', + 'channel_subscriptions.channel', 'event_subscriptions', 'event_subscriptions.event', 'ladder_subscriptions', diff --git a/app/Models/DiscordGuild.php b/app/Models/DiscordGuild.php index bfbd2d5..134099f 100644 --- a/app/Models/DiscordGuild.php +++ b/app/Models/DiscordGuild.php @@ -57,6 +57,10 @@ class DiscordGuild extends Model { return $this->hasMany(DiscordGuildCrew::class); } + public function channel_subscriptions() { + return $this->hasMany(DiscordGuildChannelSubscription::class); + } + public function event_subscriptions() { return $this->hasMany(DiscordGuildEventSubscription::class); } @@ -78,6 +82,13 @@ class DiscordGuild extends Model { $any = count($this->event_subscriptions) + count($this->ladder_subscriptions) + count($this->user_subscriptions); $title = 'Creating events for:'; $text = ''; + if (count($this->channel_subscriptions)) { + $text .= "Channels:\n"; + foreach ($this->channel_subscriptions as $csub) { + $text .= '- ['.$csub->channel->title.']('.$csub->channel->stream_link.")\n"; + } + $text .= "\n"; + } if (count($this->event_subscriptions)) { $text .= "Events:\n"; foreach ($this->event_subscriptions as $esub) { diff --git a/app/Models/DiscordGuildChannelSubscription.php b/app/Models/DiscordGuildChannelSubscription.php new file mode 100644 index 0000000..d5e5734 --- /dev/null +++ b/app/Models/DiscordGuildChannelSubscription.php @@ -0,0 +1,38 @@ +discord_guild_id) { + $channels[] = new PrivateChannel('DiscordGuild.'.$this->discord_guild_id); + } + return $channels; + } + + public function channel() { + return $this->belongsTo(Channel::class); + } + + public function guild() { + return $this->belongsTo(DiscordGuild::class); + } + + protected $fillable = [ + 'channel_id', + 'discord_guild_id', + ]; + + protected $with = [ + 'channel', + ]; + +} diff --git a/database/migrations/2025_08_28_161722_create_discord_guild_channel_subscriptions_table.php b/database/migrations/2025_08_28_161722_create_discord_guild_channel_subscriptions_table.php new file mode 100644 index 0000000..18b03e0 --- /dev/null +++ b/database/migrations/2025_08_28_161722_create_discord_guild_channel_subscriptions_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('discord_guild_id')->constrained(); + $table->foreignId('channel_id')->constrained(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::dropIfExists('discord_guild_channel_subscriptions'); + } +}; diff --git a/resources/js/components/common/ChannelSelect.jsx b/resources/js/components/common/ChannelSelect.jsx index 61b34e9..a9a90d4 100644 --- a/resources/js/components/common/ChannelSelect.jsx +++ b/resources/js/components/common/ChannelSelect.jsx @@ -115,10 +115,13 @@ const ChannelSelect = ({ onChange({ - channel: result, - target: { value: result.id }, - })} + onClick={() => { + onChange({ + target: { name, value: result.id }, + }); + setSearch(''); + setShowResults(false); + }} > {result.title} diff --git a/resources/js/components/discord-bot/ChannelSubscriptions.jsx b/resources/js/components/discord-bot/ChannelSubscriptions.jsx new file mode 100644 index 0000000..a659c40 --- /dev/null +++ b/resources/js/components/discord-bot/ChannelSubscriptions.jsx @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Form } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import ChannelSelect from '../common/ChannelSelect'; +import Icon from '../common/Icon'; +import { mayManageGuild } from '../../helpers/permissions'; +import { useUser } from '../../hooks/user'; + +const ChannelSubscriptions = ({ addChannel, guild, removeChannel, subs }) => { + const { t } = useTranslation(); + const { user } = useUser(); + + const mayManage = React.useMemo(() => mayManageGuild(user, guild), [guild, user]); + + return
+ {mayManage ? + + {t('discordBot.addChannel')} + s.channel_id)} + onChange={e => addChannel(e.target.value)} + value="" + /> + + : null} + {subs.map((csub) => ( +
+
{csub.channel.title}
+ {mayManage ? +
+ +
+ : null} +
+ ))} +
; +}; + +ChannelSubscriptions.propTypes = { + addChannel: PropTypes.func, + guild: PropTypes.shape({ + }), + removeChannel: PropTypes.func, + subs: PropTypes.arrayOf(PropTypes.shape({ + })), +}; + +export default ChannelSubscriptions; diff --git a/resources/js/components/discord-bot/GuildControls.jsx b/resources/js/components/discord-bot/GuildControls.jsx index 499d77d..fa86db4 100644 --- a/resources/js/components/discord-bot/GuildControls.jsx +++ b/resources/js/components/discord-bot/GuildControls.jsx @@ -5,6 +5,7 @@ import { Col, Row } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import toastr from 'toastr'; +import ChannelSubscriptions from './ChannelSubscriptions'; import EventSubscriptions from './EventSubscriptions'; import GuildCrew from './GuildCrew'; import GuildProtocol from './GuildProtocol'; @@ -53,6 +54,17 @@ const GuildControls = ({ guild, patchGuild }) => { } }, [guild.guild_id, patchGuild, t]); + const addChannelSub = React.useCallback(async (channel_id) => { + try { + const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { + add_channel: channel_id, + }); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.channelSubError', { error })); + } + }, [guild.guild_id, patchGuild, t]); + const addEventSub = React.useCallback(async (event_id) => { try { const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { @@ -64,6 +76,17 @@ const GuildControls = ({ guild, patchGuild }) => { } }, [guild.guild_id, patchGuild, t]); + const removeChannelSub = React.useCallback(async (channel_id) => { + try { + const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { + remove_channel: channel_id, + }); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.channelUnsubError', { error })); + } + }, [guild.guild_id, patchGuild, t]); + const removeEventSub = React.useCallback(async (event_id) => { try { const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { @@ -163,6 +186,21 @@ const GuildControls = ({ guild, patchGuild }) => { patchGuild(g => ({ ...g, crew: (g.crew || []).filter(c => c.id !== e.model.id) })); } }) + .listen('.DiscordGuildChannelSubscriptionCreated', e => { + if (e.model) { + patchGuild(g => ({ ...g, channel_subscriptions: [...g.channel_subscriptions || [], e.model] })); + } + }) + .listen('.DiscordGuildChannelSubscriptionUpdated', e => { + if (e.model) { + patchGuild(g => ({ ...g, channel_subscriptions: (g.channel_subscriptions || []).map(c => c.id === e.model.id ? { ...c, ...e.model } : c) })); + } + }) + .listen('.DiscordGuildChannelSubscriptionDeleted', e => { + if (e.model) { + patchGuild(g => ({ ...g, channel_subscriptions: (g.channel_subscriptions || []).filter(c => c.id !== e.model.id) })); + } + }) .listen('.DiscordGuildEventSubscriptionCreated', e => { if (e.model) { patchGuild(g => ({ ...g, event_subscriptions: [...g.event_subscriptions || [], e.model] })); @@ -264,9 +302,21 @@ const GuildControls = ({ guild, patchGuild }) => { /> + +

{t('discordBot.channelSubscriptions')}

+

{t('discordBot.channelSubscriptionDescription')}

+ + + +

{t('discordBot.ladderSubscriptions')}

-

{t('discordBot.ladderSubscriptionDescription')}

+

{t('discordBot.ladderSubscriptionDescription')}

{ GuildControls.propTypes = { guild: PropTypes.shape({ + channel_subscriptions: PropTypes.arrayOf(PropTypes.shape({ + })), event_subscriptions: PropTypes.arrayOf(PropTypes.shape({ })), id: PropTypes.number, diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index a28d86d..c083c53 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -139,6 +139,7 @@ export default { }, discordBot: { addCrew: 'User hinzufügen', + addChannel: 'Kanal abonnieren', addEvent: 'Event abonnieren', addLadderMode: 'Ladder Mode abonnieren', addUser: 'User abonnieren', @@ -170,6 +171,10 @@ export default { crewChangeError: 'Fehler beim Speichern', crewRemoveError: 'Fehler beim Entfernen', description: 'Dieser Bot kann automatisch Discord Events für abonnierte Veranstaltungen und Benutzer erstellen. Er ist auch in der Lage, Diskussions-Kanäle für Async Turniere zu verwalten.', + channelSubError: 'Fehler beim Abonnieren', + channelSubscriptionDescription: 'Restreams auf abonnierten Kanälen werden als Event in Discord angelegt.', + channelSubscriptions: 'Abonnierte Kanäle', + channelUnsubError: 'Fehler beim Kündigen', eventSubError: 'Fehler beim Abonnieren', eventSubscriptionDescription: 'Episoden abonnierter Veranstaltungen werden als Event in Discord angelegt.', eventSubscriptions: 'Abonnierte Veranstaltungen', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index fd7d9f6..61ae69e 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -139,6 +139,7 @@ export default { }, discordBot: { addCrew: 'Add user', + addChannel: 'Subscribe to channel', addEvent: 'Subscribe to event', addLadderMode: 'Subscribe to ladder mode', addUser: 'Subscribe to user', @@ -170,6 +171,10 @@ export default { crewChangeError: 'Error modifying user', crewRemoveError: 'Error removing user', description: 'This bot can automatically create scheduled events on your discord for events and users your discord is subscribed to. It also manages creation and access for async tournament discussion channels.', + channelSubError: 'Error subscribing', + channelSubscriptionDescription: 'Restreams on subscribed channels will be posted as discord scheduled events.', + channelSubscriptions: 'Channel subscriptions', + channelUnsubError: 'Error unsubscribing', eventSubError: 'Error subscribing', eventSubscriptionDescription: 'Episodes of subscribed events will be posted as discord scheduled events.', eventSubscriptions: 'Event subscriptions',