feat(dashboard): ignored users page

This commit is contained in:
sam 2024-10-31 01:17:44 +01:00
parent 8ed9b4b143
commit a22057b9fa
Signed by: sam
GPG key ID: 5F3C3C1B3166639D
8 changed files with 247 additions and 45 deletions

View file

@ -65,48 +65,6 @@ public partial class GuildsController
return NoContent(); return NoContent();
} }
[HttpGet("users")]
public async Task<IActionResult> ListUsersAsync(string id, [FromQuery] string query)
{
var (guildId, _) = await ParseGuildAsync(id);
var members = await memberCache.GetMemberNamesAsync(guildId, query);
return Ok(members.OrderBy(m => m.Name).Select(m => new UserQueryResponse(m.Name, m.Id)));
}
private record UserQueryResponse(string Name, string Id);
[HttpPut("ignored-users/{userId}")]
public async Task<IActionResult> AddIgnoredUserAsync(string id, ulong userId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
if (guildConfig.Channels.IgnoredUsers.Contains(userId))
return NoContent();
var user = await memberCache.TryGetAsync(guildId, DiscordSnowflake.New(userId));
if (user == null)
return NoContent();
guildConfig.Channels.IgnoredUsers.Add(userId);
await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return NoContent();
}
[HttpDelete("ignored-users/{userId}")]
public async Task<IActionResult> RemoveIgnoredUserAsync(string id, ulong userId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
guildConfig.Channels.IgnoredUsers.Remove(userId);
await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return NoContent();
}
[HttpPut("key-roles/{roleId}")] [HttpPut("key-roles/{roleId}")]
public async Task<IActionResult> AddKeyRoleAsync(string id, ulong roleId) public async Task<IActionResult> AddKeyRoleAsync(string id, ulong roleId)
{ {

View file

@ -0,0 +1,106 @@
// 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 <https://www.gnu.org/licenses/>.
using System.Net;
using Catalogger.Backend.Api.Middleware;
using Catalogger.Backend.Extensions;
using Microsoft.AspNetCore.Mvc;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
namespace Catalogger.Backend.Api;
public partial class GuildsController
{
[HttpGet("ignored-users")]
public async Task<IActionResult> GetIgnoredUsersAsync(string id, CancellationToken ct = default)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
// not actually sure how long fetching members might take. timing it out after 10 seconds just in case
// the underlying redis library doesn't support CancellationTokens so we don't pass it down
// we just end the loop early if it expires
cts.CancelAfter(TimeSpan.FromSeconds(10));
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
var output = new List<IgnoredUser>();
foreach (var userId in guildConfig.Channels.IgnoredUsers)
{
if (cts.Token.IsCancellationRequested)
break;
var member = await memberCache.TryGetAsync(guildId, DiscordSnowflake.New(userId));
output.Add(
new IgnoredUser(
Id: userId,
Tag: member != null ? member.User.Value.Tag() : "unknown user"
)
);
}
return Ok(output.OrderBy(i => i.Id));
}
private record IgnoredUser(ulong Id, string Tag);
[HttpPut("ignored-users/{userId}")]
public async Task<IActionResult> AddIgnoredUserAsync(string id, ulong userId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
IUser? user;
var member = await memberCache.TryGetAsync(guildId, DiscordSnowflake.New(userId));
if (member != null)
user = member.User.Value;
else
user = await userCache.GetUserAsync(DiscordSnowflake.New(userId));
if (user == null)
throw new ApiError(HttpStatusCode.NotFound, ErrorCode.BadRequest, "User not found");
if (guildConfig.Channels.IgnoredUsers.Contains(user.ID.Value))
return Ok(new IgnoredUser(user.ID.Value, user.Tag()));
guildConfig.Channels.IgnoredUsers.Add(user.ID.Value);
await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return Ok(new IgnoredUser(user.ID.Value, user.Tag()));
}
[HttpDelete("ignored-users/{userId}")]
public async Task<IActionResult> RemoveIgnoredUserAsync(string id, ulong userId)
{
var (guildId, _) = await ParseGuildAsync(id);
var guildConfig = await guildRepository.GetAsync(guildId);
guildConfig.Channels.IgnoredUsers.Remove(userId);
await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels);
return NoContent();
}
[HttpGet("users")]
public async Task<IActionResult> ListUsersAsync(string id, [FromQuery] string query)
{
var (guildId, _) = await ParseGuildAsync(id);
var members = await memberCache.GetMemberNamesAsync(guildId, query);
return Ok(members.OrderBy(m => m.Name).Select(m => new UserQueryResponse(m.Name, m.Id)));
}
private record UserQueryResponse(string Name, string Id);
}

View file

@ -40,6 +40,7 @@ public partial class GuildsController(
RoleCache roleCache, RoleCache roleCache,
IMemberCache memberCache, IMemberCache memberCache,
IInviteCache inviteCache, IInviteCache inviteCache,
UserCache userCache,
DiscordRequestService discordRequestService, DiscordRequestService discordRequestService,
IDiscordRestUserAPI userApi, IDiscordRestUserAPI userApi,
WebhookExecutorService webhookExecutor WebhookExecutorService webhookExecutor

View file

@ -13,6 +13,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis;
using Catalogger.Backend.Extensions; using Catalogger.Backend.Extensions;
using LazyCache; using LazyCache;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;

View file

@ -56,6 +56,12 @@
> >
Ignored channels Ignored channels
</NavLink> </NavLink>
<NavLink
href="/dash/{data.guild.id}/ignored-users"
active={$page.url.pathname === `/dash/${data.guild.id}/ignored-users`}
>
Ignored users
</NavLink>
<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`}
@ -70,7 +76,7 @@
</NavLink> </NavLink>
</Nav> </Nav>
{#if $page.url.pathname === `/dash/${data.guild.id}` || $page.url.pathname === `/dash/${data.guild.id}/ignored-channels`} {#if $page.url.pathname === `/dash/${data.guild.id}`}
<Button on:click={save} class="mb-2">Save changes</Button> <Button on:click={save} class="mb-2">Save changes</Button>
{/if} {/if}
</div> </div>

View file

@ -82,8 +82,9 @@
<h3>Ignored channels</h3> <h3>Ignored channels</h3>
<p> <p>
Messages from ignored channels will not be logged. Note that this does not Messages from ignored channels will not be logged. Changes to ignored channels
ignore channel update events, any changes to the channel will still be logged. will also not be logged, but note that ignored channels being <em>deleted</em>
(or new channels being created in an ignored category) will still be logged.
</p> </p>
<div> <div>

View file

@ -0,0 +1,119 @@
<script lang="ts">
import {
Button,
Label,
ListGroup,
ListGroupItem,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import { addToast } from "$lib/toast";
import apiFetch, { fastFetch, TOKEN_KEY, type ApiError } from "$lib/api";
import Svelecte from "svelecte";
export let data: PageData;
let toIgnore: string | null = null;
const idRegex = /^\d{15,}$/;
const addIgnore = async () => {
if (!toIgnore) return;
try {
const user = await apiFetch<{ id: string; tag: string }>(
"PUT",
`/api/guilds/${data.guild.id}/ignored-users/${toIgnore}`,
);
data.users.push(user);
data.users = data.users;
addToast({
header: "Ignored user",
body: `Added ${user.tag} to the list of ignored users.`,
});
toIgnore = null;
} catch (e) {
addToast({
header: "Error ignoring user",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const removeIgnore = async (id: string) => {
try {
await fastFetch(
"DELETE",
`/api/guilds/${data.guild.id}/ignored-users/${id}`,
);
const user = data.users.find((u) => u.id === id);
const idx = data.users.findIndex((u) => u.id === id);
if (idx > -1) data.users.splice(idx, 1);
data.users = data.users;
addToast({
header: "Stopped ignoring user",
body: `Removed ${user?.tag || "unknown user " + id} from the list of ignored users.`,
});
} catch (e) {
addToast({
header: "Error removing user",
body:
(e as ApiError).message || "Unknown error. Please try again later.",
});
}
};
const fetchProps: RequestInit = {
method: "GET",
headers: {
Authorization: localStorage.getItem(TOKEN_KEY) || "",
},
};
</script>
<h3>Ignored users</h3>
<p>Messages from ignored users will not be logged.</p>
<div>
<Label><strong>Ignore a new user</strong></Label>
<Svelecte
bind:value={toIgnore}
fetch="/api/guilds/{data.guild.id}/users?query=[query]"
{fetchProps}
multiple={false}
searchable={true}
creatable={true}
labelField="name"
valueField="id"
creatablePrefix="user with ID "
/>
{#if toIgnore && !idRegex.test(toIgnore)}
<p class="text-danger mt-2">
If you're not ignoring a member of your server, you need to give a
<strong>user ID</strong>, not their username.
</p>
{/if}
</div>
<div class="my-2 d-grid d-md-block">
<Button
color="primary"
on:click={() => addIgnore()}
disabled={!toIgnore || !idRegex.test(toIgnore)}
>
Ignore user
</Button>
</div>
<h4>Currently ignored users</h4>
<ListGroup>
{#each data.users as user (user.id)}
<ListGroupItem class="d-flex justify-content-between align-items-center">
<span>{user.tag} (ID: {user.id})</span>
<Button color="link" on:click={() => removeIgnore(user.id)}>
Stop ignoring
</Button>
</ListGroupItem>
{/each}
</ListGroup>

View file

@ -0,0 +1,10 @@
import apiFetch from "$lib/api";
export const load = async ({ params }) => {
const users = await apiFetch<Array<{ id: string; tag: string }>>(
"GET",
`/api/guilds/${params.guildId}/ignored-users`,
);
return { users };
};