namespace App\Console\Commands;
+use App\Models\DiscordBotCommand;
use App\Models\DiscordGuild;
+use App\Models\DiscordGuildEpisode;
use App\Models\Episode;
use Illuminate\Console\Command;
return 0;
}
- private function handleGuild(DiscordGuild $guild) {
+ private function handleGuild(DiscordGuild $guild): void {
$from = now()->sub(1, 'hour');
$eventIDs = $guild->event_subscriptions->pluck('event_id')->toArray();
$userIDs = $guild->user_subscriptions->pluck('user_id')->toArray();
- if (empty($eventIDs) && empty($userIDs)) return;
+ if (empty($eventIDs) && empty($userIDs)) {
+ return;
+ }
$query = Episode::with(['channels', 'event', 'players'])
->where('episodes.start', '>', $from)
});
$episodes = $query->get();
foreach ($episodes as $episode) {
- $this->handleEpisode($episode);
+ $this->handleEpisode($guild, $episode);
}
}
- private function handleEpisode(Episode $episode) {
- $this->line($episode->start.' '.$episode->event->title.' ' .$episode->title);
+ private function handleEpisode(DiscordGuild $guild, Episode $episode): void {
+ $mtime = $episode->updated_at;
+ foreach ($episode->channels as $channel) {
+ if ($mtime < $channel->updated_at) {
+ $mtime = $channel->updated_at;
+ }
+ }
+ foreach ($episode->confirmedCrew() as $crew) {
+ if ($mtime < $crew->updated_at) {
+ $mtime = $crew->updated_at;
+ }
+ }
foreach ($episode->players as $player) {
- $this->line(' - '.$player->name_override);
+ if ($mtime < $player->updated_at) {
+ $mtime = $player->updated_at;
+ }
+ }
+ $memo = $this->getMemo($guild, $episode);
+ if (is_null($memo->synced_at) || $memo->synced_at < $mtime) {
+ DiscordBotCommand::episodeEvent($guild, $episode);
}
}
+ private function getMemo(DiscordGuild $guild, Episode $episode): DiscordGuildEpisode {
+ return DiscordGuildEpisode::firstOrCreate([
+ 'discord_guild_id' => $guild->id,
+ 'episode_id' => $episode->id,
+ ]);
+ }
+
}
$this->purgeChannels($episode, $sgEntry);
$channelIds = [];
foreach ($sgEntry['channels'] as $sgChannel) {
- if ($sgChannel['initials'] == 'NONE' || $sgChannel['name'] == 'Undecided, Not SG') continue;
+ if ($sgChannel['initials'] == 'NONE' || $sgChannel['name'] == 'Undecided, Not SG') {
+ continue;
+ }
try {
$channel = $this->syncChannel($episode, $sgChannel);
$channelIds[] = $channel->id;
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$this->error('error syncing channel '.$sgChannel['id'].': '.$e->getMessage());
}
}
foreach ($sgEntry['broadcasters'] as $sgCrew) {
try {
$this->syncCrew($episode, $sgCrew, 'brd', 'setup');
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$this->error('error syncing broadcaster '.$sgCrew['id'].': '.$e->getMessage());
}
}
foreach ($sgEntry['commentators'] as $sgCrew) {
try {
$this->syncCrew($episode, $sgCrew, 'comm', 'commentary');
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$this->error('error syncing commentator '.$sgCrew['id'].': '.$e->getMessage());
}
}
foreach ($sgEntry['helpers'] as $sgCrew) {
try {
$this->syncCrew($episode, $sgCrew, 'help', 'setup');
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$this->error('error syncing helper '.$sgCrew['id'].': '.$e->getMessage());
}
}
foreach ($sgEntry['trackers'] as $sgCrew) {
try {
$this->syncCrew($episode, $sgCrew, 'track', 'tracking');
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$this->error('error syncing tracker '.$sgCrew['id'].': '.$e->getMessage());
}
}
foreach ($sgEntry['match1']['players'] as $sgPlayer) {
try {
$this->syncPlayer($episode, $sgPlayer);
- } catch (Exception $e) {
+ } catch (\Exception $e) {
$this->error('error syncing player '.$sgPlayer['id'].': '.$e->getMessage());
}
}
if (!empty($player['discordId'])) {
$user = User::find($player['discordId']);
if ($user) {
- if (empty($user->stream_link)) {
- if (!empty($sgPlayer['publicStream'])) {
- $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['publicStream']);
+ if (!$user->stream_link) {
+ if (!empty($player['publicStream'])) {
+ $user->stream_link = 'https://twitch.tv/'.strtolower($player['publicStream']);
$user->save();
- } else if (!empty($sgPlayer['streamingFrom'])) {
- $user->stream_link = 'https://twitch.tv/'.strtolower($sgPlayer['streamingFrom']);
+ } elseif (!empty($player['streamingFrom'])) {
+ $user->stream_link = 'https://twitch.tv/'.strtolower($player['streamingFrom']);
$user->save();
}
}
['username', 'LIKE', $tag[0]],
['discriminator', '=', $tag[1]],
]);
- if ($user) return $user;
+ if ($user) {
+ return $user;
+ }
}
return null;
}
return $guild->toJson();
}
+ public function subscriptions(Request $request, $guild_id) {
+ $guild = DiscordGuild::with([
+ 'event_subscriptions',
+ 'event_subscriptions.event',
+ 'user_subscriptions',
+ 'user_subscriptions.user',
+ ])->where('guild_id', '=', $guild_id)->firstOrFail();
+ $this->authorize('manage', $guild);
+ return $guild->toJson();
+ }
+
}
return $channels;
}
- public static function queueResult(Result $result) {
+ public static function episodeEvent(DiscordGuild $guild, Episode $episode): DiscordBotCommand {
+ $cmd = new DiscordBotCommand();
+ $cmd->discord_guild()->associate($guild);
+ $cmd->command = 'episode-event';
+ $cmd->parameters = [
+ 'episode' => $episode->id,
+ ];
+ $cmd->status = 'pending';
+ $cmd->save();
+ return $cmd;
+ }
+
+ public static function queueResult(Result $result): DiscordBotCommand {
$cmd = new DiscordBotCommand();
$cmd->tournament_id = $result->round->tournament_id;
$cmd->command = 'result';
return $cmd;
}
- public static function sendMessage(DiscordChannel $channel, $text, User $user = null) {
+ public static function sendMessage(DiscordChannel $channel, string $text, User $user = null): DiscordBotCommand {
$cmd = new DiscordBotCommand();
$cmd->discord_guild_id = $channel->discord_guild_id;
if ($user) {
return $cmd;
}
- public static function syncUser($user_id) {
+ public static function syncUser($user_id): DiscordBotCommand {
$cmd = new DiscordBotCommand();
$cmd->command = 'sync-user';
$cmd->parameters = [
namespace App\Models;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Model;
class DiscordGuildEventSubscription 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 event() {
+ return $this->belongsTo(Event::class);
+ }
+
+ public function guild() {
+ return $this->belongsTo(DiscordGuild::class);
+ }
+
}
namespace App\Models;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Model;
class DiscordGuildUserSubscription 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 guild() {
+ return $this->belongsTo(DiscordGuild::class);
+ }
+
+ public function user() {
+ return $this->belongsTo(User::class);
+ }
+
}
import ChannelControls from './ChannelControls';
import GuildControls from './GuildControls';
-import GuildProtocol from './GuildProtocol';
import DiscordChannelSelect from '../common/DiscordChannelSelect';
import DiscordSelect from '../common/DiscordSelect';
import ErrorBoundary from '../common/ErrorBoundary';
{guild ?
<ErrorBoundary>
<GuildControls guild={guild} />
- <GuildProtocol guild={guild} />
</ErrorBoundary>
: null}
</>;
+import axios from 'axios';
import PropTypes from 'prop-types';
import React from 'react';
+import { Col, Row } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
+import GuildProtocol from './GuildProtocol';
+
const GuildControls = ({ guild }) => {
+ const [protocol, setProtocol] = React.useState([]);
+ const [subscriptions, setSubscriptions] = React.useState({});
+
const { t } = useTranslation();
+ React.useEffect(() => {
+ const ctrl = new AbortController();
+ axios
+ .get(`/api/discord-bot/${guild.id}/commands`, { signal: ctrl.signal })
+ .then(response => {
+ setProtocol(response.data);
+ });
+ axios
+ .get(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { signal: ctrl.signal })
+ .then(response => {
+ setSubscriptions(response.data);
+ });
+ window.Echo.private(`DiscordGuild.${guild.id}`)
+ .listen('.DiscordBotCommandCreated', e => {
+ if (e.model) {
+ setProtocol(protocol => [e.model, ...protocol]);
+ }
+ })
+ .listen('.DiscordBotCommandUpdated', e => {
+ if (e.model) {
+ setProtocol(protocol => protocol.map(p => p.id === e.model.id ? { ...p, ...e.mode } : p));
+ }
+ });
+ return () => {
+ ctrl.abort();
+ window.Echo.leave(`DiscordGuild.${guild.id}`);
+ };
+ }, [guild.id]);
+
return <section className="mt-5">
<h2>{t('discordBot.guildControls')}</h2>
+ <Row>
+ <Col md={6}>
+ <h3>{t('discordBot.eventSubscriptions')}</h3>
+ {subscriptions.event_subscriptions ? subscriptions.event_subscriptions.map(esub =>
+ <div key={esub.id}>{esub.event.title}</div>
+ ): null}
+ </Col>
+ <Col md={6}>
+ <h3>{t('discordBot.userSubscriptions')}</h3>
+ {subscriptions.user_subscriptions ? subscriptions.user_subscriptions.map(usub =>
+ <div key={usub.id}>{usub.user.username}</div>
+ ): null}
+ </Col>
+ </Row>
+ <h3>{t('discordBot.guildProtocol')}</h3>
+ <GuildProtocol protocol={protocol} />
</section>;
};
GuildControls.propTypes = {
guild: PropTypes.shape({
+ id: PropTypes.number,
+ guild_id: PropTypes.string,
}),
};
-import axios from 'axios';
import PropTypes from 'prop-types';
import React from 'react';
import { useTranslation } from 'react-i18next';
-import Loading from '../common/Loading';
-
-const GuildProtocol = ({ guild }) => {
- const [loading, setLoading] = React.useState(true);
- const [protocol, setProtocol] = React.useState([]);
-
+const GuildProtocol = ({ protocol }) => {
const { t } = useTranslation();
- React.useEffect(() => {
- const ctrl = new AbortController();
- axios
- .get(`/api/discord-bot/${guild.id}/commands`, { signal: ctrl.signal })
- .then(response => {
- setLoading(false);
- setProtocol(response.data);
- });
- window.Echo.private(`DiscordGuild.${guild.id}`)
- .listen('.DiscordBotCommandCreated', e => {
- if (e.model) {
- setProtocol(protocol => [e.model, ...protocol]);
- }
- })
- .listen('.DiscordBotCommandUpdated', e => {
- if (e.model) {
- setProtocol(protocol => protocol.map(p => p.id === e.model.id ? { ...p, ...e.mode } : p));
- }
- });
- return () => {
- ctrl.abort();
- window.Echo.leave(`DiscordGuild.${guild.id}`);
- };
- }, [guild.id]);
-
- return <section className="mt-5">
- <h2>{t('discordBot.guildProtocol')}</h2>
- {loading ?
- <Loading />
- :
- protocol.map((entry) =>
- <div className="discord-bot-protocol border-top" key={entry.id}>
- <div className="d-flex justify-content-between">
- <span>{t(`discordBot.commandType.${entry.command}`)}</span>
- <span>{t(`discordBot.commandStatus.${entry.status}`)}</span>
- </div>
- <div className="d-flex justify-content-between">
- <span className="text-muted">
- {t('discordBot.commandTime', { time: new Date(entry.created_at) })}
- </span>
- <span className="text-muted">
- {entry.executed_at
- ? t('discordBot.commandTime', { time: new Date(entry.executed_at) })
- : t('discordBot.commandPending')
- }
- </span>
- </div>
- </div>
- )
- }
- </section>;
+ return protocol.map((entry) =>
+ <div className="discord-bot-protocol border-top" key={entry.id}>
+ <div className="d-flex justify-content-between">
+ <span>{t(`discordBot.commandType.${entry.command}`)}</span>
+ <span>{t(`discordBot.commandStatus.${entry.status}`)}</span>
+ </div>
+ <div className="d-flex justify-content-between">
+ <span className="text-muted">
+ {t('discordBot.commandTime', { time: new Date(entry.created_at) })}
+ </span>
+ <span className="text-muted">
+ {entry.executed_at
+ ? t('discordBot.commandTime', { time: new Date(entry.executed_at) })
+ : t('discordBot.commandPending')
+ }
+ </span>
+ </div>
+ </div>
+ );
};
GuildProtocol.propTypes = {
- guild: PropTypes.shape({
+ protocol: PropTypes.arrayOf(PropTypes.shape({
+ command: PropTypes.string,
+ created_at: PropTypes.string,
+ executed_at: PropTypes.string,
id: PropTypes.number,
- }).isRequired,
+ status: PropTypes.string,
+ })).isRequired,
};
export default GuildProtocol;
result: 'Ergebnis',
},
controls: 'Steuerung',
+ eventSubscriptions: 'Event Subscriptions',
guild: 'Server',
guildControls: 'Server-Steuerung',
guildProtocol: 'Command Protokoll',
messageError: 'Fehler beim Senden',
messageSuccess: 'Nachricht in Warteschlange',
selectGuild: 'Bitte Server wählen',
- sendMessage: 'Nachricht senden'
+ sendMessage: 'Nachricht senden',
+ userSubscriptions: 'User Subscriptions',
},
episodes: {
addRestream: 'Neuer Restream',
result: 'Result',
},
controls: 'Controls',
+ eventSubscriptions: 'Event subscriptions',
guild: 'Server',
guildControls: 'Server controls',
guildProtocol: 'Command protocol',
messageSuccess: 'Message queued',
selectGuild: 'Please select server',
sendMessage: 'Send message',
+ userSubscriptions: 'User subscriptions',
},
episodes: {
addRestream: 'Add Restream',
Route::get('discord-guilds', 'App\Http\Controllers\DiscordGuildController@search');
Route::get('discord-guilds/{guild_id}', 'App\Http\Controllers\DiscordGuildController@single');
Route::get('discord-guilds/{guild_id}/channels', 'App\Http\Controllers\DiscordChannelController@search');
+Route::get('discord-guilds/{guild_id}/subscriptions', 'App\Http\Controllers\DiscordGuildController@subscriptions');
Route::get('episodes', 'App\Http\Controllers\EpisodeController@search');
Route::post('episodes/{episode}/add-restream', 'App\Http\Controllers\EpisodeController@addRestream');