attempt to add ignored channels page

This commit is contained in:
sam 2024-10-19 23:27:57 +02:00
parent cb425fe3cd
commit 1c43beb82f
Signed by: sam
GPG key ID: 5F3C3C1B3166639D
13 changed files with 238 additions and 124 deletions

View file

@ -49,6 +49,17 @@ public class AuthController(
return Redirect(url); return Redirect(url);
} }
[HttpGet("add-guild/{id}")]
public IActionResult AddGuild(ulong id)
{
var url =
$"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
+ "&permissions=537250993&scope=bot+applications.commands"
+ $"&guild_id={id}";
return Redirect(url);
}
[HttpPost("callback")] [HttpPost("callback")]
[ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req) public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req)

View file

@ -18,6 +18,7 @@ using Catalogger.Backend.Api.Middleware;
using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database; using Catalogger.Backend.Database;
using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Database.Queries;
using Catalogger.Backend.Database.Redis;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Remora.Discord.API; using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
@ -30,16 +31,10 @@ public partial class GuildsController(
Config config, Config config,
DatabaseContext db, DatabaseContext db,
ChannelCache channelCache, ChannelCache channelCache,
RedisService redisService,
DiscordRequestService discordRequestService DiscordRequestService discordRequestService
) : ApiControllerBase ) : ApiControllerBase
{ {
public IActionResult AddGuild(ulong id) =>
Redirect(
$"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
+ "&permissions=537250993&scope=bot%20applications.commands"
+ $"&guild_id={id}"
);
private async Task<(Snowflake GuildId, Guild Guild)> ParseGuildAsync(string id) private async Task<(Snowflake GuildId, Guild Guild)> ParseGuildAsync(string id)
{ {
var guilds = await discordRequestService.GetGuildsAsync(CurrentToken); var guilds = await discordRequestService.GetGuildsAsync(CurrentToken);
@ -135,6 +130,28 @@ public partial class GuildsController(
.ToList(); .ToList();
var guildConfig = await db.GetGuildAsync(guildId); var guildConfig = await db.GetGuildAsync(guildId);
if (req.IgnoredChannels != null)
{
var categories = channelCache
.GuildChannels(guildId)
.Where(c => c.Type is ChannelType.GuildCategory)
.ToList();
if (
req.IgnoredChannels.Any(cId =>
guildChannels.All(c => c.ID.Value != cId)
&& categories.All(c => c.ID.Value != cId)
)
)
throw new ApiError(
HttpStatusCode.BadRequest,
ErrorCode.BadRequest,
"One or more ignored channels are unknown"
);
guildConfig.Channels.IgnoredChannels = req.IgnoredChannels.ToList();
}
// i love repeating myself wheeeeee // i love repeating myself wheeeeee
if ( if (
req.GuildUpdate == null req.GuildUpdate == null
@ -295,6 +312,7 @@ public partial class GuildsController(
} }
public record ChannelRequest( public record ChannelRequest(
ulong[]? IgnoredChannels = null,
ulong? GuildUpdate = null, ulong? GuildUpdate = null,
ulong? GuildEmojisUpdate = null, ulong? GuildEmojisUpdate = null,
ulong? GuildRoleCreate = null, ulong? GuildRoleCreate = null,

View file

@ -58,7 +58,7 @@ public class Guild
public class ChannelConfig public class ChannelConfig
{ {
public List<ulong> IgnoredChannels { get; init; } = []; public List<ulong> IgnoredChannels { get; set; } = [];
public List<ulong> IgnoredUsers { get; init; } = []; public List<ulong> IgnoredUsers { get; init; } = [];
public Dictionary<ulong, List<ulong>> IgnoredUsersPerChannel { get; init; } = []; public Dictionary<ulong, List<ulong>> IgnoredUsersPerChannel { get; init; } = [];
public Dictionary<ulong, ulong> Redirects { get; init; } = []; public Dictionary<ulong, ulong> Redirects { get; init; } = [];

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { TOKEN_KEY, type User } from "$lib/api"; import { TOKEN_KEY, type User } from "$lib/api";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { import {
Button,
Navbar, Navbar,
NavbarBrand, NavbarBrand,
NavbarToggler, NavbarToggler,
@ -11,6 +11,10 @@
Nav, Nav,
NavItem, NavItem,
NavLink, NavLink,
Dropdown,
DropdownToggle,
DropdownMenu,
DropdownItem,
} from "@sveltestrap/sveltestrap"; } from "@sveltestrap/sveltestrap";
export let user: User | null; export let user: User | null;
@ -32,15 +36,28 @@
<Collapse {isOpen} navbar expand="lg"> <Collapse {isOpen} navbar expand="lg">
<Nav class="ms-auto" navbar> <Nav class="ms-auto" navbar>
<NavItem> <NavItem>
<NavLink href="/">Home</NavLink> <NavLink href="/" active={$page.url.pathname === "/"}>About</NavLink>
</NavItem> </NavItem>
{#if user} {#if user}
<NavItem> <Dropdown nav inNavbar>
<NavLink href="/dash">Dashboard</NavLink> <DropdownToggle nav caret>
</NavItem> <img
<NavItem> src={user.avatar_url}
<NavLink on:click={logOut}>Log out</NavLink> alt="Your avatar"
</NavItem> style="border-radius: 0.75em; height: 1.5em;"
/>
{user.tag}
</DropdownToggle>
<DropdownMenu end>
<DropdownItem
href="/dash"
active={$page.url.pathname.startsWith("/dash")}
>
Dashboard
</DropdownItem>
<DropdownItem on:click={logOut}>Log out</DropdownItem>
</DropdownMenu>
</Dropdown>
{:else} {:else}
<NavItem> <NavItem>
<NavLink href="/api/authorize">Log in with Discord</NavLink> <NavLink href="/api/authorize">Log in with Discord</NavLink>

View file

@ -0,0 +1,77 @@
import type { FullGuild } from "./api";
export const makeFullOptions = (guild: FullGuild, ignore: string[]) => {
const options = [];
options.push(
...guild.categories
.filter((cat) => !ignore.some((k) => k === cat.id))
.map((cat) => ({
label: `${cat.name} (category)`,
value: cat.id,
})),
);
// Filter these channels
const channelsWithoutCategory = guild.channels_without_category.filter(
(c) => c.can_redirect_from && !ignore.some((k) => k === c.id),
);
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_redirect_from && !ignore.some((k) => k === c.id))
.map((c) => ({ value: c.id, label: `#${c.name}` })),
}))
.filter((c) => c.options.length > 0),
);
return options;
};
export const makeFullOptions2 = (guild: FullGuild, ignore: string[]) => {
const options: Array<{ label: string; value: string; group: string }> = [];
options.push(
...guild.categories
.filter((cat) => !ignore.some((k) => k === cat.id))
.map((cat) => ({
label: `${cat.name} (category)`,
value: cat.id,
group: "Categories",
})),
);
// Filter these channels
const channelsWithoutCategory = guild.channels_without_category.filter(
(c) => c.can_redirect_from && !ignore.some((k) => k === c.id),
);
if (channelsWithoutCategory.length > 0)
options.push(
...channelsWithoutCategory.map((c) => ({
value: c.id,
label: `#${c.name}`,
group: "(no category)",
})),
);
options.push(
...guild.categories.flatMap((cat) =>
cat.channels
.filter((c) => c.can_redirect_from && !ignore.some((k) => k === c.id))
.map((c) => ({ value: c.id, label: `#${c.name}`, group: cat.name })),
),
);
return options;
};

View file

@ -12,7 +12,7 @@
<div class="container"> <div class="container">
<slot /> <slot />
<div class="position-absolute top-0 start-50 translate-middle-x"> <div class="position-absolute top-0 start-50 translate-middle-x px-2">
{#each $toastStore as toast} {#each $toastStore as toast}
<Toast> <Toast>
{#if toast.header}<ToastHeader>{toast.header}</ToastHeader>{/if} {#if toast.header}<ToastHeader>{toast.header}</ToastHeader>{/if}

View file

@ -13,8 +13,11 @@
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const code = params.get("code"); const code = params.get("code");
const state = params.get("state"); const state = params.get("state");
const guildId = params.get("guild_id");
if (data.user) { if (data.user) {
if (guildId) return await goto(`/dash/${guildId}`);
addToast({ header: "Cannot log in", body: "You are already logged in." }); addToast({ header: "Cannot log in", body: "You are already logged in." });
await goto("/dash"); await goto("/dash");
return; return;
@ -37,7 +40,11 @@
localStorage.setItem(TOKEN_KEY, resp.token); localStorage.setItem(TOKEN_KEY, resp.token);
if (guildId) {
await goto(`/dash/${guildId}`, { invalidateAll: true });
} else {
await goto("/dash", { invalidateAll: true }); await goto("/dash", { invalidateAll: true });
}
} catch (e) { } catch (e) {
console.error("Callback request failed: ", e); console.error("Callback request failed: ", e);
error = true; error = true;

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { ListGroup, ListGroupItem } from "@sveltestrap/sveltestrap"; import { ListGroup, ListGroupItem } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import GuildCard from "./GuildCard.svelte";
export let data: PageData; export let data: PageData;
@ -15,9 +16,9 @@
<h1>Manage your servers</h1> <h1>Manage your servers</h1>
<div class="row"> <div class="row">
{#if joinedGuilds.length > 0}
<div class="col-lg"> <div class="col-lg">
<h2>Servers you can manage</h2> <h2>Servers you can configure</h2>
{#if joinedGuilds.length > 0}
<ListGroup> <ListGroup>
{#each joinedGuilds as guild (guild.id)} {#each joinedGuilds as guild (guild.id)}
<ListGroupItem tag="a" href="/dash/{guild.id}"> <ListGroupItem tag="a" href="/dash/{guild.id}">
@ -30,14 +31,21 @@
</ListGroupItem> </ListGroupItem>
{/each} {/each}
</ListGroup> </ListGroup>
</div> {:else}
<p>None of the servers you manage have Catalogger added.</p>
{/if} {/if}
{#if unjoinedGuilds.length > 0} </div>
<div class="col-lg"> <div class="col-lg">
<h2>Servers you can add Catalogger to</h2> <h2>Servers you can add Catalogger to</h2>
{#if unjoinedGuilds.length > 0}
<ListGroup> <ListGroup>
{#each unjoinedGuilds as guild (guild.id)} {#each unjoinedGuilds as guild (guild.id)}
<ListGroupItem tag="a" href="/api/add-guild/{guild.id}"> <ListGroupItem
tag="a"
href="/api/add-guild/{guild.id}"
target="_blank"
>
<img <img
src={guild.icon_url} src={guild.icon_url}
alt="Icon for {guild.name}" alt="Icon for {guild.name}"
@ -47,6 +55,8 @@
</ListGroupItem> </ListGroupItem>
{/each} {/each}
</ListGroup> </ListGroup>
</div> {:else}
<p>All of the servers you manage already have Catalogger added.</p>
{/if} {/if}
</div>
</div> </div>

View file

@ -16,6 +16,11 @@
data.guild.config, data.guild.config,
); );
data.guild.config = resp; data.guild.config = resp;
addToast({
header: "Saved log channels.",
body: "Successfully edited log channels and ignored channels.",
});
} catch (e) { } catch (e) {
addToast({ addToast({
header: "Error saving changes to log channels", header: "Error saving changes to log channels",
@ -31,55 +36,43 @@
</svelte:head> </svelte:head>
<div class="d-flex flex-column flex-lg-row justify-content-lg-between"> <div class="d-flex flex-column flex-lg-row justify-content-lg-between">
<Nav pills={true} class="flex-column flex-lg-row"> <Nav pills={true} class="flex-column flex-lg-row mb-2">
<NavItem <NavLink href="#" disabled>Managing {data.guild.name}</NavLink>
><NavLink href="#" disabled>Managing {data.guild.name}</NavLink></NavItem <NavLink
>
<NavItem
><NavLink
href="/dash/{data.guild.id}" href="/dash/{data.guild.id}"
active={$page.url.pathname === `/dash/${data.guild.id}`} active={$page.url.pathname === `/dash/${data.guild.id}`}
> >
Log channels Log channels
</NavLink></NavItem </NavLink>
> <NavLink
<NavItem
><NavLink
href="/dash/{data.guild.id}/redirects" href="/dash/{data.guild.id}/redirects"
active={$page.url.pathname === `/dash/${data.guild.id}/redirects`} active={$page.url.pathname === `/dash/${data.guild.id}/redirects`}
> >
Redirects Redirects
</NavLink></NavItem </NavLink>
> <NavLink
<NavItem
><NavLink
href="/dash/{data.guild.id}/ignored-channels" href="/dash/{data.guild.id}/ignored-channels"
active={$page.url.pathname === active={$page.url.pathname === `/dash/${data.guild.id}/ignored-channels`}
`/dash/${data.guild.id}/ignored-channels`}
> >
Ignored channels Ignored channels
</NavLink></NavItem </NavLink>
> <NavLink
<NavItem
><NavLink
href="/dash/{data.guild.id}/ignored-users" href="/dash/{data.guild.id}/ignored-users"
active={$page.url.pathname === `/dash/${data.guild.id}/ignored-users`} active={$page.url.pathname === `/dash/${data.guild.id}/ignored-users`}
> >
Ignored users Ignored users
</NavLink></NavItem </NavLink>
>
<NavItem <NavLink
><NavLink
href="/dash/{data.guild.id}/key-roles" href="/dash/{data.guild.id}/key-roles"
active={$page.url.pathname === `/dash/${data.guild.id}/key-roles`} active={$page.url.pathname === `/dash/${data.guild.id}/key-roles`}
> >
Key roles Key roles
</NavLink></NavItem </NavLink>
>
</Nav> </Nav>
{#if $page.url.pathname === `/dash/${data.guild.id}`} {#if $page.url.pathname === `/dash/${data.guild.id}` || $page.url.pathname === `/dash/${data.guild.id}/ignored-channels`}
<Button on:click={save}>Save changes</Button> <Button on:click={save} class="mb-2">Save changes</Button>
{/if} {/if}
</div> </div>

View file

@ -9,6 +9,8 @@
$: channels = data.guild.config as GuildChannelConfig; $: channels = data.guild.config as GuildChannelConfig;
</script> </script>
<h3>Log channels</h3>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3">
<div class="p-2"> <div class="p-2">
<Label><strong>Server changes</strong></Label> <Label><strong>Server changes</strong></Label>

View file

@ -5,14 +5,18 @@
type Option = { label: string; value: string }; type Option = { label: string; value: string };
export let options: Array<Group | Option>; export let options: Array<Group | Option>;
export let multiple = false;
export let max: number | undefined = undefined;
export let placeholder: string = "Select a channel"; export let placeholder: string = "Select a channel";
export let value: string | null; export let value: string | string[] | null;
</script> </script>
<Svelecte <Svelecte
bind:value bind:value
{options} {options}
{placeholder} {placeholder}
{multiple}
{max}
labelField="label" labelField="label"
valueField="value" valueField="value"
groupLabelField="label" groupLabelField="label"

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { Label } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import { makeFullOptions } from "$lib/util";
import ChannelSelect from "../ChannelSelect.svelte";
export let data: PageData;
$: ignored = data.guild.config.ignored_channels;
$: options = makeFullOptions(data.guild, ignored);
</script>
<h3>Ignored channels</h3>
<p>
Messages from ignored channels will not be logged. Note that this does not
ignore channel update events, any changes to the channel will still be logged.
</p>
<div class="p-2">
<Label><strong>Ignored channels</strong></Label>
<ChannelSelect bind:value={ignored} {options} multiple={true} />
</div>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { ApiError, FullGuild } from "$lib/api"; import type { ApiError } from "$lib/api";
import { import {
Button, Button,
Label, Label,
@ -10,60 +10,12 @@
import ChannelSelect from "../ChannelSelect.svelte"; import ChannelSelect from "../ChannelSelect.svelte";
import { fastFetch } from "$lib/api"; import { fastFetch } from "$lib/api";
import { addToast } from "$lib/toast"; import { addToast } from "$lib/toast";
import { makeFullOptions } from "$lib/util";
export let data: PageData; export let data: PageData;
$: redirects = data.guild.config.redirects; $: redirects = data.guild.config.redirects;
const makeSourceOptions = (
guild: FullGuild,
redirects: Record<string, string>,
) => {
const options = [];
// We shouldn't list channels that are already being redirected.
const redirectedChannels = Object.keys(redirects);
options.push(
...guild.categories
.filter((cat) => !redirectedChannels.some((k) => k === cat.id))
.map((cat) => ({
label: `${cat.name} (category)`,
value: cat.id,
})),
);
// Filter these channels
const channelsWithoutCategory = guild.channels_without_category.filter(
(c) => c.can_redirect_from && !redirectedChannels.some((k) => k === c.id),
);
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_redirect_from &&
!redirectedChannels.some((k) => k === c.id),
)
.map((c) => ({ value: c.id, label: `#${c.name}` })),
}))
.filter((c) => c.options.length > 0),
);
return options;
};
$: allChannels = [ $: allChannels = [
...data.guild.channels_without_category.map((c) => ({ ...data.guild.channels_without_category.map((c) => ({
id: c.id, id: c.id,
@ -78,7 +30,7 @@
const channelName = (id: string) => const channelName = (id: string) =>
allChannels.find((c) => c.id === id)?.name || `(unknown channel ${id})`; allChannels.find((c) => c.id === id)?.name || `(unknown channel ${id})`;
$: sourceOptions = makeSourceOptions(data.guild, redirects); $: sourceOptions = makeFullOptions(data.guild, Object.keys(redirects));
$: targetOptions = data.options; $: targetOptions = data.options;
let source: string | null = null; let source: string | null = null;
@ -143,7 +95,7 @@
</div> </div>
</div> </div>
<div> <div class="my-2 d-grid d-md-block">
<Button <Button
color="primary" color="primary"
disabled={!source || !target} disabled={!source || !target}