From cb425fe3cd1fd1477326700b17f20e143bf280cf Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 19 Oct 2024 20:47:55 +0200 Subject: [PATCH] feat(dashboard): add redirects page --- .../Api/GuildsController.Redirects.cs | 92 +++++++++ Catalogger.Backend/Api/GuildsController.cs | 2 +- Catalogger.Frontend/src/lib/api.ts | 35 +++- .../src/routes/dash/[guildId]/+layout.ts | 27 ++- .../src/routes/dash/[guildId]/+page.svelte | 128 +++++++------ .../dash/[guildId]/ChannelSelect.svelte | 4 +- .../dash/[guildId]/redirects/+page.svelte | 174 +++++++++++++++++- 7 files changed, 400 insertions(+), 62 deletions(-) create mode 100644 Catalogger.Backend/Api/GuildsController.Redirects.cs diff --git a/Catalogger.Backend/Api/GuildsController.Redirects.cs b/Catalogger.Backend/Api/GuildsController.Redirects.cs new file mode 100644 index 0000000..9c9f11a --- /dev/null +++ b/Catalogger.Backend/Api/GuildsController.Redirects.cs @@ -0,0 +1,92 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Net; +using Catalogger.Backend.Api.Middleware; +using Catalogger.Backend.Database.Queries; +using Microsoft.AspNetCore.Mvc; +using Remora.Discord.API.Abstractions.Objects; + +namespace Catalogger.Backend.Api; + +public partial class GuildsController +{ + [HttpPost("redirects")] + public async Task CreateRedirectAsync( + string id, + [FromBody] CreateRedirectRequest req + ) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildChannels = channelCache.GuildChannels(guildId).ToList(); + var guildConfig = await db.GetGuildAsync(guildId.Value); + + Console.WriteLine($"Source: {req.Source}, target: {req.Target}"); + + var source = guildChannels.FirstOrDefault(c => + c.ID.Value == req.Source + && c.Type + is ChannelType.GuildText + or ChannelType.GuildCategory + or ChannelType.GuildAnnouncement + or ChannelType.GuildForum + or ChannelType.GuildMedia + or ChannelType.GuildVoice + ); + if (source == null) + throw new ApiError( + HttpStatusCode.BadRequest, + ErrorCode.BadRequest, + "Unknown source channel ID or it's not a valid source" + ); + var target = guildChannels.FirstOrDefault(c => + c.ID.Value == req.Target && c.Type is ChannelType.GuildText + ); + if (target == null) + throw new ApiError( + HttpStatusCode.BadRequest, + ErrorCode.BadRequest, + "Unknown target channel ID or it's not a valid target" + ); + + guildConfig.Channels.Redirects[source.ID.Value] = target.ID.Value; + db.Update(guildConfig); + await db.SaveChangesAsync(); + + return NoContent(); + } + + [HttpDelete("redirects/{channelId}")] + public async Task DeleteRedirectAsync(string id, ulong channelId) + { + var (guildId, _) = await ParseGuildAsync(id); + var guildConfig = await db.GetGuildAsync(guildId.Value); + + if (!guildConfig.Channels.Redirects.ContainsKey(channelId)) + throw new ApiError( + HttpStatusCode.BadRequest, + ErrorCode.BadRequest, + "That channel is already not being redirected" + ); + + guildConfig.Channels.Redirects.Remove(channelId, out _); + db.Update(guildConfig); + await db.SaveChangesAsync(); + + return NoContent(); + } + + public record CreateRedirectRequest(ulong Source, ulong Target); +} diff --git a/Catalogger.Backend/Api/GuildsController.cs b/Catalogger.Backend/Api/GuildsController.cs index df301c3..e34575d 100644 --- a/Catalogger.Backend/Api/GuildsController.cs +++ b/Catalogger.Backend/Api/GuildsController.cs @@ -26,7 +26,7 @@ using Remora.Rest.Core; namespace Catalogger.Backend.Api; [Route("/api/guilds/{id}")] -public class GuildsController( +public partial class GuildsController( Config config, DatabaseContext db, ChannelCache channelCache, diff --git a/Catalogger.Frontend/src/lib/api.ts b/Catalogger.Frontend/src/lib/api.ts index 52a5b79..a3c1e2e 100644 --- a/Catalogger.Frontend/src/lib/api.ts +++ b/Catalogger.Frontend/src/lib/api.ts @@ -1,11 +1,40 @@ export const TOKEN_KEY = "catalogger-token"; -export default async function apiFetch( - method: "GET" | "POST" | "PATCH" | "DELETE", +export type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE"; + +export async function fastFetch( + method: HttpMethod, path: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any body: any = null, -) { +): Promise { + const token = localStorage.getItem(TOKEN_KEY); + const headers = { + ...(body != null + ? { "Content-Type": "application/json; charset=utf-8" } + : {}), + ...(token ? { Authorization: token } : {}), + }; + + const reqBody = body ? JSON.stringify(body) : undefined; + + console.debug("Sending", method, "request to", path, "with body", reqBody); + + const resp = await fetch(path, { + method, + body: body ? JSON.stringify(body) : undefined, + headers, + }); + if (resp.status < 200 || resp.status > 299) + throw (await resp.json()) as ApiError; +} + +export default async function apiFetch( + method: HttpMethod, + path: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: any = null, +): Promise { const token = localStorage.getItem(TOKEN_KEY); const headers = { ...(body != null diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.ts b/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.ts index 8c062d8..d3f1d1c 100644 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.ts +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/+layout.ts @@ -8,7 +8,32 @@ export const load = async ({ params }) => { `/api/guilds/${params.guildId}`, ); - return { guild }; + const options = []; + + const channelsWithoutCategory = guild.channels_without_category.filter( + (c) => c.can_log_to, + ); + if (channelsWithoutCategory.length > 0) + options.push({ + label: "(no category)", + options: channelsWithoutCategory.map((c) => ({ + value: c.id, + label: `#${c.name}`, + })), + }); + + options.push( + ...guild.categories + .map((cat) => ({ + label: cat.name, + options: cat.channels + .filter((c) => c.can_log_to) + .map((c) => ({ value: c.id, label: `#${c.name}` })), + })) + .filter((c) => c.options.length > 0), + ); + + return { guild, options }; } catch (e) { const err = e as ApiError; console.log("Fetching guild", params.guildId, ":", e); diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/+page.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/+page.svelte index bb22c87..19a65f7 100644 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/+page.svelte +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/+page.svelte @@ -1,89 +1,85 @@
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- + Key roles can be designated with the /key-roles command or the "Key roles" tab above. @@ -91,48 +87,72 @@
- +
- +
- + Note that this will not log timeouts naturally expiring.
- +
- +
- +
- +
- +
- +
- +
- + This will only be triggered by bots, not normal users.
diff --git a/Catalogger.Frontend/src/routes/dash/[guildId]/ChannelSelect.svelte b/Catalogger.Frontend/src/routes/dash/[guildId]/ChannelSelect.svelte index d774598..2d2ea4f 100644 --- a/Catalogger.Frontend/src/routes/dash/[guildId]/ChannelSelect.svelte +++ b/Catalogger.Frontend/src/routes/dash/[guildId]/ChannelSelect.svelte @@ -4,9 +4,9 @@ type Group = { label: string; options: Option[] }; type Option = { label: string; value: string }; - export let options: Group[]; + export let options: Array; export let placeholder: string = "Select a channel"; - export let value: string; + export let value: string | null; aaaaaaaaa \ No newline at end of file + + +

Add new redirect

+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ +{#if Object.keys(redirects).length > 0} +

Existing redirects

+ + + {#each Object.keys(redirects) as redirectSource} + + {channelName(redirectSource)} ➜ {channelName( + redirects[redirectSource], + )} + + + {/each} + +{:else} +

There are no redirects configured.

+{/if}