// 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.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; using Catalogger.Backend.Database.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Microsoft.AspNetCore.Mvc; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; namespace Catalogger.Backend.Api; [Route("/api/guilds/{id}")] public partial class GuildsController( ILogger logger, DatabaseConnection dbConn, GuildRepository guildRepository, InviteRepository inviteRepository, WatchlistRepository watchlistRepository, GuildCache guildCache, EmojiCache emojiCache, ChannelCache channelCache, RoleCache roleCache, IMemberCache memberCache, IInviteCache inviteCache, UserCache userCache, DiscordRequestService discordRequestService, IDiscordRestUserAPI userApi, WebhookExecutorService webhookExecutor ) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); private async Task<(Snowflake GuildId, Guild Guild)> ParseGuildAsync(string id) { var guilds = await discordRequestService.GetGuildsAsync(CurrentToken); var guild = guilds.FirstOrDefault(g => g.CanManage && g.Id == id); if (guild == null) throw new ApiError( HttpStatusCode.NotFound, ErrorCode.UnknownGuild, "Unknown server, or you're not in it, or you can't manage it." ); if (!DiscordSnowflake.TryParse(guild.Id, out var guildId)) throw new CataloggerError("Invalid snowflake passed to GuildsController.ToResponse"); return (guildId.Value, guild); } [Authorize] [HttpGet] public async Task GetGuildAsync(string id) { var (guildId, guild) = await ParseGuildAsync(id); var guildConfig = await guildRepository.GetAsync(guildId); var channels = channelCache .GuildChannels(guildId) .OrderBy(c => c.Position.OrDefault(0)) .ToList(); var channelsWithoutCategories = channels .Where(c => !c.ParentID.IsDefined() && c.Type is not ChannelType.GuildCategory) .Select(ToChannel); var categories = channels .Where(c => c.Type is ChannelType.GuildCategory) .Select(c => new GuildCategory( c.ID.ToString(), c.Name.Value!, channels .Where(c2 => c2.ParentID.IsDefined(out var parentId) && parentId == c.ID) .Select(ToChannel) )); var roles = roleCache .GuildRoles(guildId) .OrderByDescending(r => r.Position) .Select(r => new GuildRole( r.ID.ToString(), r.Name, r.Position, r.Colour.ToPrettyString() )); return Ok( new GuildResponse( guild.Id, guild.Name, guild.IconUrl, categories, channelsWithoutCategories, roles, guildConfig.Channels, guildConfig.KeyRoles ) ); } private static GuildChannel ToChannel(IChannel channel) => new( channel.ID.ToString(), channel.Name.Value!, channel.Type is ChannelType.GuildText, channel.Type is ChannelType.GuildText or ChannelType.GuildAnnouncement or ChannelType.GuildForum or ChannelType.GuildMedia or ChannelType.GuildVoice ); private record GuildResponse( string Id, string Name, string IconUrl, IEnumerable Categories, IEnumerable ChannelsWithoutCategory, IEnumerable Roles, Database.Models.Guild.ChannelConfig Config, ulong[] KeyRoles ); private record GuildCategory(string Id, string Name, IEnumerable Channels); private record GuildChannel(string Id, string Name, bool CanLogTo, bool CanRedirectFrom); private record GuildRole(string Id, string Name, int Position, string Colour); [Authorize] [HttpPatch] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task PatchGuildAsync(string id, [FromBody] ChannelRequest req) { var (guildId, _) = await ParseGuildAsync(id); var guildChannels = channelCache .GuildChannels(guildId) .Where(c => c.Type is ChannelType.GuildText) .ToList(); var guildConfig = await guildRepository.GetAsync(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 if ( req.GuildUpdate == null || (req.GuildUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildUpdate)) ) guildConfig.Channels.GuildUpdate = req.GuildUpdate ?? 0; if ( req.GuildEmojisUpdate == null || ( req.GuildEmojisUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildEmojisUpdate) ) ) guildConfig.Channels.GuildEmojisUpdate = req.GuildEmojisUpdate ?? 0; if ( req.GuildRoleCreate == null || ( req.GuildRoleCreate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildRoleCreate) ) ) guildConfig.Channels.GuildRoleCreate = req.GuildRoleCreate ?? 0; if ( req.GuildRoleUpdate == null || ( req.GuildRoleUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildRoleUpdate) ) ) guildConfig.Channels.GuildRoleUpdate = req.GuildRoleUpdate ?? 0; if ( req.GuildRoleDelete == null || ( req.GuildRoleDelete == 0 || guildChannels.Any(c => c.ID.Value == req.GuildRoleDelete) ) ) guildConfig.Channels.GuildRoleDelete = req.GuildRoleDelete ?? 0; if ( req.ChannelCreate == null || (req.ChannelCreate == 0 || guildChannels.Any(c => c.ID.Value == req.ChannelCreate)) ) guildConfig.Channels.ChannelCreate = req.ChannelCreate ?? 0; if ( req.ChannelUpdate == null || (req.ChannelUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.ChannelUpdate)) ) guildConfig.Channels.ChannelUpdate = req.ChannelUpdate ?? 0; if ( req.ChannelDelete == null || (req.ChannelDelete == 0 || guildChannels.Any(c => c.ID.Value == req.ChannelDelete)) ) guildConfig.Channels.ChannelDelete = req.ChannelDelete ?? 0; if ( req.GuildMemberAdd == null || (req.GuildMemberAdd == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberAdd)) ) guildConfig.Channels.GuildMemberAdd = req.GuildMemberAdd ?? 0; if ( req.GuildMemberUpdate == null || ( req.GuildMemberUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberUpdate) ) ) guildConfig.Channels.GuildMemberUpdate = req.GuildMemberUpdate ?? 0; if ( req.GuildKeyRoleUpdate == null || ( req.GuildKeyRoleUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildKeyRoleUpdate) ) ) guildConfig.Channels.GuildKeyRoleUpdate = req.GuildKeyRoleUpdate ?? 0; if ( req.GuildMemberNickUpdate == null || ( req.GuildMemberNickUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberNickUpdate) ) ) guildConfig.Channels.GuildMemberNickUpdate = req.GuildMemberNickUpdate ?? 0; if ( req.GuildMemberAvatarUpdate == null || ( req.GuildMemberAvatarUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberAvatarUpdate) ) ) guildConfig.Channels.GuildMemberAvatarUpdate = req.GuildMemberAvatarUpdate ?? 0; if ( req.GuildMemberTimeout == null || ( req.GuildMemberTimeout == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberTimeout) ) ) guildConfig.Channels.GuildMemberTimeout = req.GuildMemberTimeout ?? 0; if ( req.GuildMemberRemove == null || ( req.GuildMemberRemove == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberRemove) ) ) guildConfig.Channels.GuildMemberRemove = req.GuildMemberRemove ?? 0; if ( req.GuildMemberKick == null || ( req.GuildMemberKick == 0 || guildChannels.Any(c => c.ID.Value == req.GuildMemberKick) ) ) guildConfig.Channels.GuildMemberKick = req.GuildMemberKick ?? 0; if ( req.GuildBanAdd == null || (req.GuildBanAdd == 0 || guildChannels.Any(c => c.ID.Value == req.GuildBanAdd)) ) guildConfig.Channels.GuildBanAdd = req.GuildBanAdd ?? 0; if ( req.GuildBanRemove == null || (req.GuildBanRemove == 0 || guildChannels.Any(c => c.ID.Value == req.GuildBanRemove)) ) guildConfig.Channels.GuildBanRemove = req.GuildBanRemove ?? 0; if ( req.InviteCreate == null || (req.InviteCreate == 0 || guildChannels.Any(c => c.ID.Value == req.InviteCreate)) ) guildConfig.Channels.InviteCreate = req.InviteCreate ?? 0; if ( req.InviteDelete == null || (req.InviteDelete == 0 || guildChannels.Any(c => c.ID.Value == req.InviteDelete)) ) guildConfig.Channels.InviteDelete = req.InviteDelete ?? 0; if ( req.MessageUpdate == null || (req.MessageUpdate == 0 || guildChannels.Any(c => c.ID.Value == req.MessageUpdate)) ) guildConfig.Channels.MessageUpdate = req.MessageUpdate ?? 0; if ( req.MessageDelete == null || (req.MessageDelete == 0 || guildChannels.Any(c => c.ID.Value == req.MessageDelete)) ) guildConfig.Channels.MessageDelete = req.MessageDelete ?? 0; if ( req.MessageDeleteBulk == null || ( req.MessageDeleteBulk == 0 || guildChannels.Any(c => c.ID.Value == req.MessageDeleteBulk) ) ) guildConfig.Channels.MessageDeleteBulk = req.MessageDeleteBulk ?? 0; await guildRepository.UpdateChannelConfigAsync(guildId, guildConfig.Channels); return Ok(guildConfig.Channels); } public record ChannelRequest( ulong[]? IgnoredChannels = null, ulong? GuildUpdate = null, ulong? GuildEmojisUpdate = null, ulong? GuildRoleCreate = null, ulong? GuildRoleUpdate = null, ulong? GuildRoleDelete = null, ulong? ChannelCreate = null, ulong? ChannelUpdate = null, ulong? ChannelDelete = null, ulong? GuildMemberAdd = null, ulong? GuildMemberUpdate = null, ulong? GuildKeyRoleUpdate = null, ulong? GuildMemberNickUpdate = null, ulong? GuildMemberAvatarUpdate = null, ulong? GuildMemberTimeout = null, ulong? GuildMemberRemove = null, ulong? GuildMemberKick = null, ulong? GuildBanAdd = null, ulong? GuildBanRemove = null, ulong? InviteCreate = null, ulong? InviteDelete = null, ulong? MessageUpdate = null, ulong? MessageDelete = null, ulong? MessageDeleteBulk = null ); }