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();
->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);
public function subscriptions(Request $request, $guild_id) {
$guild = DiscordGuild::with([
+ 'channel_subscriptions',
+ 'channel_subscriptions.channel',
'event_subscriptions',
'event_subscriptions.event',
'ladder_subscriptions',
$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']]);
}
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();
$sub->delete();
}
$guild->load([
+ 'channel_subscriptions',
+ 'channel_subscriptions.channel',
'event_subscriptions',
'event_subscriptions.event',
'ladder_subscriptions',
return $this->hasMany(DiscordGuildCrew::class);
}
+ public function channel_subscriptions() {
+ return $this->hasMany(DiscordGuildChannelSubscription::class);
+ }
+
public function event_subscriptions() {
return $this->hasMany(DiscordGuildEventSubscription::class);
}
$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) {
--- /dev/null
+<?php
+
+namespace App\Models;
+
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
+use Illuminate\Database\Eloquent\Model;
+
+class DiscordGuildChannelSubscription extends Model {
+
+ use BroadcastsEvents;
+
+ public function broadcastOn(string $event): array {
+ $channels = [];
+ if ($this->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',
+ ];
+
+}
--- /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::create('discord_guild_channel_subscriptions', function (Blueprint $table) {
+ $table->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');
+ }
+};
<ListGroup.Item
action
key={result.id}
- onClick={() => onChange({
- channel: result,
- target: { value: result.id },
- })}
+ onClick={() => {
+ onChange({
+ target: { name, value: result.id },
+ });
+ setSearch('');
+ setShowResults(false);
+ }}
>
{result.title}
</ListGroup.Item>
--- /dev/null
+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 <div>
+ {mayManage ?
+ <Form.Group controlId="csubs.addChannel">
+ <Form.Label>{t('discordBot.addChannel')}</Form.Label>
+ <Form.Control
+ as={ChannelSelect}
+ excludeIds={subs.map(s => s.channel_id)}
+ onChange={e => addChannel(e.target.value)}
+ value=""
+ />
+ </Form.Group>
+ : null}
+ {subs.map((csub) => (
+ <div className="d-flex align-items-center justify-content-between my-2" key={csub.id}>
+ <div>{csub.channel.title}</div>
+ {mayManage ?
+ <div className="button-bar">
+ <Button
+ onClick={() => removeChannel(csub.channel_id)}
+ size="sm"
+ title={t('button.remove')}
+ variant="outline-danger"
+ >
+ <Icon.DELETE title="" />
+ </Button>
+ </div>
+ : null}
+ </div>
+ ))}
+ </div>;
+};
+
+ChannelSubscriptions.propTypes = {
+ addChannel: PropTypes.func,
+ guild: PropTypes.shape({
+ }),
+ removeChannel: PropTypes.func,
+ subs: PropTypes.arrayOf(PropTypes.shape({
+ })),
+};
+
+export default ChannelSubscriptions;
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';
}
}, [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`, {
}
}, [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`, {
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] }));
/>
</ErrorBoundary>
</Col>
+ <Col className="my-5" md={6}>
+ <h3>{t('discordBot.channelSubscriptions')}</h3>
+ <p style={{ minHeight: '6.5em' }}>{t('discordBot.channelSubscriptionDescription')}</p>
+ <ErrorBoundary>
+ <ChannelSubscriptions
+ addChannel={addChannelSub}
+ guild={guild}
+ removeChannel={removeChannelSub}
+ subs={guild.channel_subscriptions || []}
+ />
+ </ErrorBoundary>
+ </Col>
<Col className="my-5" md={6}>
<h3>{t('discordBot.ladderSubscriptions')}</h3>
- <p style={{ minHeight: '3.5em' }}>{t('discordBot.ladderSubscriptionDescription')}</p>
+ <p style={{ minHeight: '6.5em' }}>{t('discordBot.ladderSubscriptionDescription')}</p>
<ErrorBoundary>
<LadderSubscriptions
addMode={addLadderSub}
GuildControls.propTypes = {
guild: PropTypes.shape({
+ channel_subscriptions: PropTypes.arrayOf(PropTypes.shape({
+ })),
event_subscriptions: PropTypes.arrayOf(PropTypes.shape({
})),
id: PropTypes.number,
},
discordBot: {
addCrew: 'User hinzufügen',
+ addChannel: 'Kanal abonnieren',
addEvent: 'Event abonnieren',
addLadderMode: 'Ladder Mode abonnieren',
addUser: 'User abonnieren',
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',
},
discordBot: {
addCrew: 'Add user',
+ addChannel: 'Subscribe to channel',
addEvent: 'Subscribe to event',
addLadderMode: 'Subscribe to ladder mode',
addUser: 'Subscribe to user',
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',