From: Daniel Karbach Date: Tue, 8 Jul 2025 10:48:03 +0000 (+0200) Subject: discord bot crew and description X-Git-Url: http://git.localhorst.tv/?a=commitdiff_plain;h=1e9dee2e643153ec2ad13b8b427046b6bbdd4723;p=alttp.git discord bot crew and description --- diff --git a/app/Http/Controllers/DiscordBotController.php b/app/Http/Controllers/DiscordBotController.php index 4e415a4..ed1150c 100644 --- a/app/Http/Controllers/DiscordBotController.php +++ b/app/Http/Controllers/DiscordBotController.php @@ -11,8 +11,8 @@ class DiscordBotController extends Controller { public function recentCommands(DiscordGuild $guild) { - $this->authorize('manage', $guild); - return $guild->bot_commands()->limit(10)->get(); + $this->authorize('view', $guild); + return $guild->bot_commands()->with('user')->limit(10)->get(); } public function sendMessage(Request $request, DiscordGuild $guild) { @@ -23,7 +23,7 @@ class DiscordBotController extends Controller ]); $channel = DiscordChannel::findOrFail($validatedData['channel']); $this->authorize('manage', $channel->guild); - $cmd = DiscordBotCommand::sendMessage($channel, $validatedData['text']); + $cmd = DiscordBotCommand::sendMessage($channel, $validatedData['text'], $request->user()); return $cmd->toJson(); } diff --git a/app/Http/Controllers/DiscordGuildController.php b/app/Http/Controllers/DiscordGuildController.php index 6c1e221..c2a5691 100644 --- a/app/Http/Controllers/DiscordGuildController.php +++ b/app/Http/Controllers/DiscordGuildController.php @@ -12,15 +12,21 @@ class DiscordGuildController extends Controller $validatedData = $request->validate([ 'phrase' => 'string|nullable', ]); + $user_id = $request->user()->id; $guilds = DiscordGuild::query(); if (!$request->user()->can('viewAny', DiscordGuild::class)) { - $guilds = $guilds->where('owner', '=', $request->user()->id); + $guilds->where(function ($query) use ($user_id) { + $query->where('owner', '=', $user_id); + $query->orWhereHas('crew', function ($builder) use ($user_id) { + $builder->where('user_id', '=', $user_id); + }); + }); } if (!empty($validatedData['phrase'])) { - $guilds = $guilds->where('name', 'LIKE', '%'.$validatedData['phrase'].'%'); + $guilds->where('name', 'LIKE', '%'.$validatedData['phrase'].'%'); } - $guilds = $guilds->limit(5); + $guilds->limit(5); return $guilds->get()->toJson(); } @@ -30,6 +36,35 @@ class DiscordGuildController extends Controller return $guild->toJson(); } + public function manageCrew(Request $request, $guild_id) { + $guild = DiscordGuild::where('guild_id', '=', $guild_id)->firstOrFail(); + $this->authorize('administer', $guild); + + $validatedData = $request->validate([ + 'add_user' => 'numeric|exists:App\Models\User,id', + 'modify_user' => 'numeric|exists:App\Models\User,id', + 'remove_user' => 'numeric|exists:App\Models\User,id', + 'role' => 'string|in:admin,manager,member', + ]); + + if (isset($validatedData['add_user'])) { + $guild->crew()->create([ + 'user_id' => $validatedData['add_user'], + ]); + } + if (isset($validatedData['modify_user'], $validatedData['role'])) { + $crew = $guild->crew()->where('user_id', '=', $validatedData['modify_user'])->firstOrFail(); + $crew->role = $validatedData['role']; + $crew->save(); + } + if (isset($validatedData['remove_user'])) { + $crew = $guild->crew()->where('user_id', '=', $validatedData['remove_user'])->firstOrFail(); + $crew->delete(); + } + + return $guild->fresh()->toJson(); + } + public function subscriptions(Request $request, $guild_id) { $guild = DiscordGuild::with([ 'event_subscriptions', @@ -37,7 +72,7 @@ class DiscordGuildController extends Controller 'user_subscriptions', 'user_subscriptions.user', ])->where('guild_id', '=', $guild_id)->firstOrFail(); - $this->authorize('manage', $guild); + $this->authorize('view', $guild); return $guild->toJson(); } @@ -59,10 +94,12 @@ class DiscordGuildController extends Controller $guild->user_subscriptions()->create(['user_id' => $validatedData['add_user']]); } if (isset($validatedData['remove_event'])) { - $guild->event_subscriptions()->where('event_id', '=', $validatedData['remove_event'])->delete(); + $sub = $guild->event_subscriptions()->where('event_id', '=', $validatedData['remove_event'])->firstOrFail(); + $sub->delete(); } if (isset($validatedData['remove_user'])) { - $guild->user_subscriptions()->where('user_id', '=', $validatedData['remove_user'])->delete(); + $sub = $guild->user_subscriptions()->where('user_id', '=', $validatedData['remove_user'])->firstOrFail(); + $sub->delete(); } $guild->load([ 'event_subscriptions', diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index f8b4283..5cbba53 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -21,8 +21,10 @@ class UserController extends Controller $users->whereNotIn('id', $validatedData['exclude_ids']); } if (!empty($validatedData['phrase'])) { - $users->where('username', 'LIKE', '%'.$validatedData['phrase'].'%') - ->orWhere('nickname', 'LIKE', '%'.$validatedData['phrase'].'%'); + $users->where(function ($query) use ($validatedData) { + $query->where('username', 'LIKE', '%'.$validatedData['phrase'].'%'); + $query->orWhere('nickname', 'LIKE', '%'.$validatedData['phrase'].'%'); + }); } $users = $users->limit(5); return $users->get()->toJson(); diff --git a/app/Models/DiscordBotCommand.php b/app/Models/DiscordBotCommand.php index b367063..ef3f5ea 100644 --- a/app/Models/DiscordBotCommand.php +++ b/app/Models/DiscordBotCommand.php @@ -22,6 +22,10 @@ class DiscordBotCommand extends Model { return $channels; } + public function broadcastWith($event) { + $this->load(['user']); + } + public static function episodeEvent(DiscordGuild $guild, Episode $episode): DiscordBotCommand { $cmd = new DiscordBotCommand(); $cmd->discord_guild()->associate($guild); diff --git a/app/Models/DiscordGuild.php b/app/Models/DiscordGuild.php index 37e711b..f1821db 100644 --- a/app/Models/DiscordGuild.php +++ b/app/Models/DiscordGuild.php @@ -52,6 +52,10 @@ class DiscordGuild extends Model return $this->hasMany(DiscordChannel::class)->orderBy('position'); } + public function crew() { + return $this->hasMany(DiscordGuildCrew::class); + } + public function event_subscriptions() { return $this->hasMany(DiscordGuildEventSubscription::class); } @@ -68,4 +72,9 @@ class DiscordGuild extends Model 'guild_id', ]; + protected $with = [ + 'crew', + 'crew.user', + ]; + } diff --git a/app/Models/DiscordGuildCrew.php b/app/Models/DiscordGuildCrew.php new file mode 100644 index 0000000..08aec61 --- /dev/null +++ b/app/Models/DiscordGuildCrew.php @@ -0,0 +1,42 @@ +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); + } + + protected $casts = [ + 'user_id' => 'string', + ]; + + protected $fillable = [ + 'discord_guild_id', + 'user_id', + ]; + + protected $with = [ + 'user', + ]; + +} diff --git a/app/Models/DiscordGuildEventSubscription.php b/app/Models/DiscordGuildEventSubscription.php index af73e9f..c80ef3a 100644 --- a/app/Models/DiscordGuildEventSubscription.php +++ b/app/Models/DiscordGuildEventSubscription.php @@ -31,4 +31,8 @@ class DiscordGuildEventSubscription extends Model { 'event_id', ]; + protected $with = [ + 'event', + ]; + } diff --git a/app/Models/DiscordGuildUserSubscription.php b/app/Models/DiscordGuildUserSubscription.php index fa9811a..df686a9 100644 --- a/app/Models/DiscordGuildUserSubscription.php +++ b/app/Models/DiscordGuildUserSubscription.php @@ -26,13 +26,17 @@ class DiscordGuildUserSubscription extends Model { return $this->belongsTo(User::class); } + protected $casts = [ + 'user_id' => 'string', + ]; + protected $fillable = [ 'discord_guild_id', 'user_id', ]; - protected $casts = [ - 'user_id' => 'string', + protected $with = [ + 'user', ]; } diff --git a/app/Policies/DiscordGuildPolicy.php b/app/Policies/DiscordGuildPolicy.php index 285138c..0f0a463 100644 --- a/app/Policies/DiscordGuildPolicy.php +++ b/app/Policies/DiscordGuildPolicy.php @@ -16,9 +16,8 @@ class DiscordGuildPolicy * @param \App\Models\User $user * @return \Illuminate\Auth\Access\Response|bool */ - public function viewAny(User $user) - { - return $user->isAdmin(); + public function viewAny(User $user): bool { + return false; } /** @@ -28,9 +27,16 @@ class DiscordGuildPolicy * @param \App\Models\DiscordGuild $discordGuild * @return \Illuminate\Auth\Access\Response|bool */ - public function view(User $user, DiscordGuild $discordGuild) - { - return $user->isAdmin() || $discordGuild->owner == $user->id; + public function view(User $user, DiscordGuild $discordGuild): bool { + if ($discordGuild->owner == $user->id) { + return true; + } + foreach ($discordGuild->crew as $crew) { + if ($crew->user_id == $user->id) { + return true; + } + } + return false; } /** @@ -99,9 +105,37 @@ class DiscordGuildPolicy * @param \App\Models\DiscordGuild $discordGuild * @return \Illuminate\Auth\Access\Response|bool */ + public function administer(User $user, DiscordGuild $discordGuild) + { + if ($discordGuild->owner == $user->id) { + return true; + } + foreach ($discordGuild->crew as $crew) { + if ($crew->user_id == $user->id && $crew->role == 'admin') { + return true; + } + } + return false; + } + + /** + * Determine whether the user can perform management tasks for the guild. + * + * @param \App\Models\User $user + * @param \App\Models\DiscordGuild $discordGuild + * @return \Illuminate\Auth\Access\Response|bool + */ public function manage(User $user, DiscordGuild $discordGuild) { - return $user->isAdmin() || $discordGuild->owner == $user->id; + if ($discordGuild->owner == $user->id) { + return true; + } + foreach ($discordGuild->crew as $crew) { + if ($crew->user_id == $user->id && in_array($crew->role, ['admin', 'manager'])) { + return true; + } + } + return false; } } diff --git a/database/migrations/2025_07_07_113659_create_discord_guild_crews_table.php b/database/migrations/2025_07_07_113659_create_discord_guild_crews_table.php new file mode 100644 index 0000000..067c4e2 --- /dev/null +++ b/database/migrations/2025_07_07_113659_create_discord_guild_crews_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('discord_guild_id')->constrained(); + $table->foreignId('user_id')->constrained(); + $table->string('role')->default('member'); + $table->timestamps(); + $table->unique(['discord_guild_id', 'user_id'], 'guild_user_unique'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::dropIfExists('discord_guild_crews'); + } +}; diff --git a/resources/js/components/discord-bot/Controls.jsx b/resources/js/components/discord-bot/Controls.jsx index 6a4580d..bbe46d6 100644 --- a/resources/js/components/discord-bot/Controls.jsx +++ b/resources/js/components/discord-bot/Controls.jsx @@ -14,6 +14,14 @@ const Controls = () => { const { t } = useTranslation(); + const patchGuild = React.useCallback((incoming) => { + if (incoming instanceof Function) { + setGuild(incoming); + } else { + setGuild((existing) => ({ ...existing, ...incoming })); + } + }, []); + return <> @@ -46,7 +54,7 @@ const Controls = () => { : null} {guild ? - + : null} ; diff --git a/resources/js/components/discord-bot/EventSubscriptions.jsx b/resources/js/components/discord-bot/EventSubscriptions.jsx index 0fe140d..2108c9f 100644 --- a/resources/js/components/discord-bot/EventSubscriptions.jsx +++ b/resources/js/components/discord-bot/EventSubscriptions.jsx @@ -5,33 +5,42 @@ import { useTranslation } from 'react-i18next'; import EventSelect from '../common/EventSelect'; import Icon from '../common/Icon'; +import { mayManageGuild } from '../../helpers/permissions'; +import { useUser } from '../../hooks/user'; -const EventSubscriptions = ({ addEvent, removeEvent, subs }) => { +const EventSubscriptions = ({ addEvent, guild, removeEvent, subs }) => { const { t } = useTranslation(); + const { user } = useUser(); + + const mayManage = React.useMemo(() => mayManageGuild(user, guild), [guild, user]); return
- - {t('discordBot.addEvent')} - s.event_id)} - onChange={e => addEvent(e.target.value)} - value="" - /> - + {mayManage ? + + {t('discordBot.addEvent')} + s.event_id)} + onChange={e => addEvent(e.target.value)} + value="" + /> + + : null} {subs.map((esub) => (
{esub.event.title}
-
- -
+ {mayManage ? +
+ +
+ : null}
))}
; @@ -39,6 +48,8 @@ const EventSubscriptions = ({ addEvent, removeEvent, subs }) => { EventSubscriptions.propTypes = { addEvent: PropTypes.func, + guild: PropTypes.shape({ + }), removeEvent: PropTypes.func, subs: PropTypes.arrayOf(PropTypes.shape({ })), diff --git a/resources/js/components/discord-bot/GuildControls.jsx b/resources/js/components/discord-bot/GuildControls.jsx index 02b563b..e4a35c6 100644 --- a/resources/js/components/discord-bot/GuildControls.jsx +++ b/resources/js/components/discord-bot/GuildControls.jsx @@ -6,39 +6,73 @@ import { useTranslation } from 'react-i18next'; import toastr from 'toastr'; import EventSubscriptions from './EventSubscriptions'; +import GuildCrew from './GuildCrew'; import GuildProtocol from './GuildProtocol'; import UserSubscriptions from './UserSubscriptions'; import ErrorBoundary from '../common/ErrorBoundary'; import { compareTitle } from '../../helpers/Event'; import { compareUsername } from '../../helpers/User'; -const GuildControls = ({ guild }) => { +const GuildControls = ({ guild, patchGuild }) => { const [protocol, setProtocol] = React.useState([]); - const [subscriptions, setSubscriptions] = React.useState({}); const { t } = useTranslation(); + const addCrew = React.useCallback(async (user_id) => { + try { + const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/crew`, { + add_user: user_id, + }); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.crewAddError', { error })); + } + }, [guild.guild_id, patchGuild, t]); + + const changeCrewRole = React.useCallback(async (user_id, role) => { + try { + const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/crew`, { + modify_user: user_id, + role, + }); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.crewChangeError', { error })); + } + }, [guild.guild_id, patchGuild, t]); + + const removeCrew = React.useCallback(async (user_id) => { + try { + const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/crew`, { + remove_user: user_id, + }); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.crewRemoveError', { 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`, { add_event: event_id, }); - setSubscriptions(response.data); - } catch (e) { - toastr.error(t('discordBot.eventSubError')); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.eventSubError', { error })); } - }, [guild.guild_id, t]); + }, [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`, { remove_event: event_id, }); - setSubscriptions(response.data); - } catch (e) { - toastr.error(t('discordBot.eventUnsubError')); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.eventUnsubError', { error })); } - }, [guild.guild_id, t]); + }, [guild.guild_id, patchGuild, t]); const addUserSub = React.useCallback(async (user_id) => { @@ -46,22 +80,22 @@ const GuildControls = ({ guild }) => { const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { add_user: user_id, }); - setSubscriptions(response.data); - } catch (e) { - toastr.error(t('discordBot.userSubError')); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.userSubError', { error })); } - }, [guild.guild_id, t]); + }, [guild.guild_id, patchGuild, t]); const removeUserSub = React.useCallback(async (user_id) => { try { const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { remove_user: user_id, }); - setSubscriptions(response.data); - } catch (e) { - toastr.error(t('discordBot.userUnsubError')); + patchGuild(response.data); + } catch (error) { + toastr.error(t('discordBot.userUnsubError', { error })); } - }, [guild.guild_id, t]); + }, [guild.guild_id, patchGuild, t]); React.useEffect(() => { const ctrl = new AbortController(); @@ -79,7 +113,7 @@ const GuildControls = ({ guild }) => { response.data.user_subscriptions.sort((a, b) => { return compareUsername(a.user, b.user); }); - setSubscriptions(response.data); + patchGuild(response.data); }); window.Echo.private(`DiscordGuild.${guild.id}`) .listen('.DiscordBotCommandCreated', e => { @@ -89,36 +123,107 @@ const GuildControls = ({ guild }) => { }) .listen('.DiscordBotCommandUpdated', e => { if (e.model) { - setProtocol(protocol => protocol.map(p => p.id === e.model.id ? { ...p, ...e.mode } : p)); + setProtocol(protocol => protocol.map(p => p.id === e.model.id ? { ...p, ...e.model } : p)); + } + }) + .listen('.DiscordGuildCrewCreated', e => { + if (e.model) { + patchGuild(g => ({ ...g, crew: [...g.crew || [], e.model] })); + } + }) + .listen('.DiscordGuildCrewUpdated', e => { + if (e.model) { + patchGuild(g => ({ ...g, crew: (g.crew || []).map(c => c.id === e.model.id ? { ...c, ...e.model } : c) })); + } + }) + .listen('.DiscordGuildCrewDeleted', e => { + if (e.model) { + patchGuild(g => ({ ...g, crew: (g.crew || []).filter(c => c.id !== e.model.id) })); + } + }) + .listen('.DiscordGuildEventSubscriptionCreated', e => { + if (e.model) { + patchGuild(g => ({ ...g, event_subscriptions: [...g.event_subscriptions || [], e.model] })); + } + }) + .listen('.DiscordGuildEventSubscriptionUpdated', e => { + if (e.model) { + patchGuild(g => ({ ...g, event_subscriptions: (g.event_subscriptions || []).map(c => c.id === e.model.id ? { ...c, ...e.model } : c) })); + } + }) + .listen('.DiscordGuildEventSubscriptionDeleted', e => { + if (e.model) { + patchGuild(g => ({ ...g, event_subscriptions: (g.event_subscriptions || []).filter(c => c.id !== e.model.id) })); + } + }) + .listen('.DiscordGuildUserSubscriptionCreated', e => { + if (e.model) { + patchGuild(g => ({ ...g, user_subscriptions: [...g.user_subscriptions || [], e.model] })); + } + }) + .listen('.DiscordGuildUserSubscriptionUpdated', e => { + if (e.model) { + patchGuild(g => ({ ...g, user_subscriptions: (g.user_subscriptions || []).map(c => c.id === e.model.id ? { ...c, ...e.model } : c) })); + } + }) + .listen('.DiscordGuildUserSubscriptionDeleted', e => { + if (e.model) { + patchGuild(g => ({ ...g, user_subscriptions: (g.user_subscriptions || []).filter(c => c.id !== e.model.id) })); } }); return () => { ctrl.abort(); window.Echo.leave(`DiscordGuild.${guild.id}`); }; - }, [guild.id]); + }, [guild.id, patchGuild]); return <>

{t('discordBot.guildControls')}

+ + +

{t('discordBot.guildCrew')}

+ + + +

{t('discordBot.roleDescriptions.heading')}

+
+ {['admin', 'manager', 'member'].map((role) => ( + +
{t(`discordBot.roles.${role}`)}
+
{t(`discordBot.roleDescriptions.${role}`)}
+
+ ))} +
+ +

{t('discordBot.eventSubscriptions')}

+

{t('discordBot.eventSubscriptionDescription')}

{t('discordBot.userSubscriptions')}

+

{t('discordBot.userSubscriptionDescription')}

@@ -126,16 +231,23 @@ const GuildControls = ({ guild }) => {

{t('discordBot.guildProtocol')}

- + + +
; }; GuildControls.propTypes = { guild: PropTypes.shape({ + event_subscriptions: PropTypes.arrayOf(PropTypes.shape({ + })), id: PropTypes.number, guild_id: PropTypes.string, + user_subscriptions: PropTypes.arrayOf(PropTypes.shape({ + })), }), + patchGuild: PropTypes.func, }; export default GuildControls; diff --git a/resources/js/components/discord-bot/GuildCrew.jsx b/resources/js/components/discord-bot/GuildCrew.jsx new file mode 100644 index 0000000..b661306 --- /dev/null +++ b/resources/js/components/discord-bot/GuildCrew.jsx @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Form } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import UserSelect from '../common/UserSelect'; +import Icon from '../common/Icon'; +import UserBox from '../users/Box'; +import { mayAdminGuild } from '../../helpers/permissions'; +import { useUser } from '../../hooks/user'; + +const GuildCrew = ({ addCrew, changeCrewRole, guild, removeCrew }) => { + const { t } = useTranslation(); + const { user } = useUser(); + + const mayAdmin = React.useMemo(() => mayAdminGuild(user, guild), [guild, user]); + + return
+ {mayAdmin ? + + {t('discordBot.addCrew')} + c.user_id)} + onChange={e => addCrew(e.target.value)} + value="" + /> + + : null} + {guild.crew.map((crew) => ( +
+ + {mayAdmin ? +
+ changeCrewRole(crew.user_id, e.target.value)} + value={crew.role} + > + {['admin', 'manager', 'member'].map((role => + + ))} + + +
+ : + {t(`discordBot.roles.${crew.role}`)} + } +
+ ))} +
; +}; + +GuildCrew.propTypes = { + addCrew: PropTypes.func, + changeCrewRole: PropTypes.func, + guild: PropTypes.shape({ + crew: PropTypes.arrayOf(PropTypes.shape({ + })), + }), + removeCrew: PropTypes.func, +}; + +export default GuildCrew; diff --git a/resources/js/components/discord-bot/GuildProtocol.jsx b/resources/js/components/discord-bot/GuildProtocol.jsx index be6bbe5..fd65cbc 100644 --- a/resources/js/components/discord-bot/GuildProtocol.jsx +++ b/resources/js/components/discord-bot/GuildProtocol.jsx @@ -2,6 +2,8 @@ import PropTypes from 'prop-types'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { getUserName } from '../../helpers/User'; + const GuildProtocol = ({ protocol }) => { const { t } = useTranslation(); @@ -13,7 +15,10 @@ const GuildProtocol = ({ protocol }) => {
- {t('discordBot.commandTime', { time: new Date(entry.created_at) })} + {entry.user + ? t('discordBot.commandTimeUser', { time: new Date(entry.created_at), user: getUserName(entry.user) }) + : t('discordBot.commandTime', { time: new Date(entry.created_at) }) + } {entry.executed_at diff --git a/resources/js/components/discord-bot/UserSubscriptions.jsx b/resources/js/components/discord-bot/UserSubscriptions.jsx index 390773c..e67112a 100644 --- a/resources/js/components/discord-bot/UserSubscriptions.jsx +++ b/resources/js/components/discord-bot/UserSubscriptions.jsx @@ -6,33 +6,42 @@ import { useTranslation } from 'react-i18next'; import UserSelect from '../common/UserSelect'; import Icon from '../common/Icon'; import UserBox from '../users/Box'; +import { mayManageGuild } from '../../helpers/permissions'; +import { useUser } from '../../hooks/user'; -const UserSubscriptions = ({ addUser, removeUser, subs }) => { +const UserSubscriptions = ({ addUser, guild, removeUser, subs }) => { const { t } = useTranslation(); + const { user } = useUser(); + + const mayManage = React.useMemo(() => mayManageGuild(user, guild), [guild, user]); return
- - {t('discordBot.addUser')} - s.user_id)} - onChange={e => addUser(e.target.value)} - value="" - /> - + {mayManage ? + + {t('discordBot.addUser')} + s.user_id)} + onChange={e => addUser(e.target.value)} + value="" + /> + + : null} {subs.map((usub) => (
-
- -
+ {mayManage ? +
+ +
+ : null}
))}
; @@ -40,6 +49,8 @@ const UserSubscriptions = ({ addUser, removeUser, subs }) => { UserSubscriptions.propTypes = { addUser: PropTypes.func, + guild: PropTypes.shape({ + }), removeUser: PropTypes.func, subs: PropTypes.arrayOf(PropTypes.shape({ })), diff --git a/resources/js/helpers/permissions.js b/resources/js/helpers/permissions.js index f1be876..94f7784 100644 --- a/resources/js/helpers/permissions.js +++ b/resources/js/helpers/permissions.js @@ -25,6 +25,20 @@ export const isAnyChannelAdmin = user => export const mayEditContent = user => user && hasGlobalRole(user, 'content'); +// Discord Guilds + +const isGuildOwner = (user, guild) => user && guild && user.id === guild.owner; + +const isGuildAdmin = (user, guild) => user && guild?.crew?.find(c => c.role === 'admin' && c.user_id === user.id); +const isGuildManager = (user, guild) => user && guild?.crew?.find(c => c.role == 'manager' && c.user_id === user.id); +const isGuildMember = (user, guild) => user && guild?.crew?.find(c => c.user_id === user.id); + +export const mayViewGuild = (user, guild) => isGuildOwner(user, guild) || isGuildMember(user, guild); + +export const mayManageGuild = (user, guild) => isGuildOwner(user, guild) || isGuildAdmin(user, guild) || isGuildManager(user, guild); + +export const mayAdminGuild = (user, guild) => isGuildOwner(user, guild) || isGuildAdmin(user, guild); + // Episodes export const isCommentator = (user, episode) => { diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 0832514..e96ed3e 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -138,10 +138,12 @@ export default { }, }, discordBot: { + addCrew: 'User hinzufügen', addEvent: 'Event abonnieren', addUser: 'User abonnieren', channel: 'Kanal', channelControls: 'Kanal-Steuerung', + commandPending: 'Steht aus', commandStatus: { done: 'Abgeschlossen', exception: 'Fehler', @@ -150,27 +152,46 @@ export default { pending: 'Ausstehend', }, commandTime: '{{ time, L HH:mm:ss }}', + commandTimeUser: '{{ time, L HH:mm:ss }} von {{ user }}', commandType: { 'episode-event': 'Episoden-Event Synchronisation', message: 'Nachricht', result: 'Ergebnis', }, controls: 'Steuerung', - eventSubError: 'Fehler bim Abonnieren', - eventSubscriptions: 'Event Subscriptions', + crewAddError: 'Fehler bein Hinzufügen', + 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 Turnieren zu verwalten.', + eventSubError: 'Fehler beim Abonnieren', + eventSubscriptionDescription: 'Episoden abonnierter Veranstaltungen werden als Event in Discord angelegt.', + eventSubscriptions: 'Abonnierte Veranstaltungen', eventUnsubError: 'Fehler beim Kündigen', guild: 'Server', guildControls: 'Server-Steuerung', + guildCrew: 'Crew', guildProtocol: 'Command Protokoll', heading: 'Discord Bot', invite: 'Bot einladen', message: 'Nachricht', messageError: 'Fehler beim Senden', messageSuccess: 'Nachricht in Warteschlange', + roles: { + admin: 'Admin', + manager: 'Manager', + member: 'Mitglied', + }, + roleDescriptions: { + admin: 'Darf effektiv alles. Der Server-Owner (User mit der Krone im Discord) ist immer Admin.', + heading: 'Beschreibung der Rollen', + manager: 'Manager können alles außer die Userliste und Rollen bearbeiten.', + member: 'Kann diese Seite einsehen, aber nix anfassen.', + }, selectGuild: 'Bitte Server wählen', sendMessage: 'Nachricht senden', userSubError: 'Fehler beim Abonnieren', - userSubscriptions: 'User Subscriptions', + userSubscriptionDescription: 'Episoden mit Beteiligung als Runner abonnierter Benutzer werden als Event in Discord angelegt.', + userSubscriptions: 'Abonnierte Benutzer', userUnsubError: 'Fehler beim Kündigen', }, episodes: { diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index ae4e457..c84f958 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -138,10 +138,12 @@ export default { }, }, discordBot: { + addCrew: 'Add user', addEvent: 'Subscribe to event', addUser: 'Subscribe to user', channel: 'Channel', channelControls: 'Channel controls', + commandPending: 'Pending execution', commandStatus: { done: 'Done', exception: 'Error', @@ -150,26 +152,45 @@ export default { pending: 'Pending', }, commandTime: '{{ time, L HH:mm:ss }}', + commandTimeUser: '{{ time, L HH:mm:ss }} by {{ user }}', commandType: { 'episode-event': 'Episode event synchronization', message: 'Message', result: 'Result', }, controls: 'Controls', + crewAddError: 'Error adding 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.', eventSubError: 'Error subscribing', + eventSubscriptionDescription: 'Episodes of subscribed events will be posted as discord scheduled events.', eventSubscriptions: 'Event subscriptions', eventUnsubError: 'Error unsubscribing', guild: 'Server', guildControls: 'Server controls', + guildCrew: 'Crew', guildProtocol: 'Command protocol', heading: 'Discord Bot', invite: 'Invite bot', message: 'Message', messageError: 'Error sending message', messageSuccess: 'Message queued', + roles: { + admin: 'Admin', + manager: 'Manager', + member: 'Member', + }, + roleDescriptions: { + admin: 'Effectively in total control. Server owners (user with the crown in discord) is always an admin.', + heading: 'Role descriptions', + manager: 'Managers may do pretty much everything except for administering users and roles.', + member: 'May see this page, but not do anything with it.', + }, selectGuild: 'Please select server', sendMessage: 'Send message', userSubError: 'Error subscribing', + userSubscriptionDescription: 'Episodes where subscribed users are participating as runners will be posted as discord scheduled events.', userSubscriptions: 'User subscriptions', userUnsubError: 'Error unsubscribing', }, diff --git a/resources/js/pages/DiscordBot.jsx b/resources/js/pages/DiscordBot.jsx index 0f22ed2..215a407 100644 --- a/resources/js/pages/DiscordBot.jsx +++ b/resources/js/pages/DiscordBot.jsx @@ -15,8 +15,10 @@ export const Component = () => { return

{t('discordBot.heading')}

+

{t('discordBot.description')}

{t('discordBot.heading')} +

diff --git a/routes/api.php b/routes/api.php index 6f96a37..53430dc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -54,6 +54,7 @@ Route::get('discord-channels/{channel_id}', 'App\Http\Controllers\DiscordChannel 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::post('discord-guilds/{guild_id}/crew', 'App\Http\Controllers\DiscordGuildController@manageCrew'); Route::get('discord-guilds/{guild_id}/subscriptions', 'App\Http\Controllers\DiscordGuildController@subscriptions'); Route::post('discord-guilds/{guild_id}/subscriptions', 'App\Http\Controllers\DiscordGuildController@manageSubscriptions'); diff --git a/routes/channels.php b/routes/channels.php index e04f537..0aee0d2 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -31,7 +31,7 @@ Broadcast::channel('Channel.{id}', function ($user, $id) { Broadcast::channel('DiscordGuild.{id}', function ($user, $id) { $guild = DiscordGuild::findOrFail($id); - return $user->can('manage', $guild); + return $user->can('view', $guild); }); Broadcast::channel('Protocol.{id}', function ($user, $id) {