diff --git a/.dockerignore b/.dockerignore index ac2851ee..c59e6784 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,8 +11,11 @@ !.git !proto !scripts/run-clustered.sh +!dashboard +!scheduled_tasks # Re-exclude host build artifact directories **/bin **/obj -**/target \ No newline at end of file +**/target +**/node_modules \ No newline at end of file diff --git a/.github/workflows/beta-bot.yml b/.github/workflows/beta-bot.yml deleted file mode 100644 index e9f34a8b..00000000 --- a/.github/workflows/beta-bot.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: "Update Beta Bot" -on: - push: - branches: [dev] - -jobs: - update-bot: - runs-on: ubuntu-latest - steps: - - name: "Update Beta Bot" - uses: fjogeleit/http-request-action@master - with: - url: https://api-beta.pluralkit.me/v1/update - bearerToken: ${{ secrets.WATCHTOWER_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml new file mode 100644 index 00000000..5c06c835 --- /dev/null +++ b/.github/workflows/dashboard.yml @@ -0,0 +1,34 @@ +name: Build dashboard Docker image + +on: + push: + branches: [main] + paths: + - 'dashboard/**' + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + packages: write + if: github.repository == 'PluralKit/PluralKit' + steps: + - uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CR_PAT }} + - uses: actions/checkout@v2 + - run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + - uses: docker/build-push-action@v2 + with: + # https://github.com/docker/build-push-action/issues/378 + context: . + file: dashboard/Dockerfile + push: true + tags: | + ghcr.io/pluralkit/dashboard:${{ env.BRANCH_NAME }} + ghcr.io/pluralkit/dashboard:${{ github.sha }} + ghcr.io/pluralkit/dashboard:latest + cache-from: type=registry,ref=ghcr.io/pluralkit/dashboard:${{ env.BRANCH_NAME }} + cache-to: type=inline diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8dbcdc7c..7ac9075f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest permissions: packages: write - if: github.repository == 'xSke/PluralKit' + if: github.repository == 'PluralKit/PluralKit' steps: - uses: docker/login-action@v1 with: @@ -22,8 +22,8 @@ jobs: context: . push: true tags: | - ghcr.io/xske/pluralkit:${{ env.BRANCH_NAME }} - ghcr.io/xske/pluralkit:${{ github.sha }} - ghcr.io/xske/pluralkit:latest - cache-from: type=registry,ref=ghcr.io/xske/pluralkit:${{ env.BRANCH_NAME }} + ghcr.io/pluralkit/pluralkit:${{ env.BRANCH_NAME }} + ghcr.io/pluralkit/pluralkit:${{ github.sha }} + ghcr.io/pluralkit/pluralkit:latest + cache-from: type=registry,ref=ghcr.io/pluralkit/pluralkit:${{ env.BRANCH_NAME }} cache-to: type=inline diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml index 9bd64c82..1ca6213d 100644 --- a/.github/workflows/gateway.yml +++ b/.github/workflows/gateway.yml @@ -4,15 +4,15 @@ on: push: branches: [main] paths: - - 'gateway/' - - 'proto/' + - 'gateway/**' + - 'proto/**' jobs: deploy: runs-on: ubuntu-latest permissions: packages: write - if: github.repository == 'xSke/PluralKit' + if: github.repository == 'PluralKit/PluralKit' steps: - uses: docker/login-action@v1 with: @@ -25,7 +25,7 @@ jobs: with: # https://github.com/docker/build-push-action/issues/378 context: . - file: Dockerfile.gateway + file: gateway/Dockerfile push: true tags: | ghcr.io/pluralkit/gateway:${{ env.BRANCH_NAME }} diff --git a/.github/workflows/scheduled_tasks.yml b/.github/workflows/scheduled_tasks.yml new file mode 100644 index 00000000..fb863ecd --- /dev/null +++ b/.github/workflows/scheduled_tasks.yml @@ -0,0 +1,33 @@ +name: Build scheduled tasks runner Docker image + +on: + push: + branches: [main] + paths: + - 'services/scheduled_tasks/**' + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + packages: write + if: github.repository == 'PluralKit/PluralKit' + steps: + - uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CR_PAT }} + - uses: actions/checkout@v2 + - run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + - uses: docker/build-push-action@v2 + with: + # https://github.com/docker/build-push-action/issues/378 + context: services/scheduled_tasks/ + push: true + tags: | + ghcr.io/pluralkit/scheduled_tasks:${{ env.BRANCH_NAME }} + ghcr.io/pluralkit/scheduled_tasks:${{ github.sha }} + ghcr.io/pluralkit/scheduled_tasks:latest + cache-from: type=registry,ref=ghcr.io/pluralkit/scheduledtasks:${{ env.BRANCH_NAME }} + cache-to: type=inline diff --git a/.gitignore b/.gitignore index 8edae534..f43417ac 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ target/ tags/ .DS_Store mono_crash* +.DotSettings # Dependencies node_modules/ diff --git a/Dockerfile b/Dockerfile index 3930b9e5..9d97795c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,6 @@ COPY Myriad/Myriad.csproj /app/Myriad/ COPY PluralKit.API/PluralKit.API.csproj /app/PluralKit.API/ COPY PluralKit.Bot/PluralKit.Bot.csproj /app/PluralKit.Bot/ COPY PluralKit.Core/PluralKit.Core.csproj /app/PluralKit.Core/ -COPY PluralKit.ScheduledTasks/PluralKit.ScheduledTasks.csproj /app/PluralKit.ScheduledTasks/ COPY PluralKit.Tests/PluralKit.Tests.csproj /app/PluralKit.Tests/ COPY .git/ /app/.git COPY proto/ /app/proto @@ -20,7 +19,7 @@ RUN dotnet build -c Release -o bin # Build runtime stage (doesn't include SDK) FROM mcr.microsoft.com/dotnet/aspnet:6.0 -LABEL org.opencontainers.image.source = "https://github.com/xSke/PluralKit" +LABEL org.opencontainers.image.source = "https://github.com/PluralKit/PluralKit" WORKDIR /app COPY --from=build /app ./ diff --git a/Myriad/Myriad.csproj b/Myriad/Myriad.csproj index 62bbe267..7439b9ee 100644 --- a/Myriad/Myriad.csproj +++ b/Myriad/Myriad.csproj @@ -23,7 +23,7 @@ - + diff --git a/Myriad/Rest/DiscordApiClient.cs b/Myriad/Rest/DiscordApiClient.cs index 7fd89356..38ff3635 100644 --- a/Myriad/Rest/DiscordApiClient.cs +++ b/Myriad/Rest/DiscordApiClient.cs @@ -10,7 +10,7 @@ namespace Myriad.Rest; public class DiscordApiClient { - public const string UserAgent = "DiscordBot (https://github.com/xSke/PluralKit/tree/main/Myriad/, v1)"; + public const string UserAgent = "DiscordBot (https://github.com/PluralKit/PluralKit/tree/main/Myriad/, v1)"; private const string DefaultApiBaseUrl = "https://discord.com/api/v10"; private readonly BaseRestClient _client; diff --git a/Myriad/Types/Channel.cs b/Myriad/Types/Channel.cs index bca93019..418c3eb2 100644 --- a/Myriad/Types/Channel.cs +++ b/Myriad/Types/Channel.cs @@ -22,8 +22,8 @@ public record Channel public ulong? GuildId { get; init; } public int? Position { get; init; } public string? Name { get; init; } - public string? Topic { get; init; } - public bool? Nsfw { get; init; } + // public string? Topic { get; init; } + // public bool? Nsfw { get; init; } public ulong? ParentId { get; init; } public Overwrite[]? PermissionOverwrites { get; init; } public User[]? Recipients { get; init; } // NOTE: this may be null for stub channel objects diff --git a/Myriad/Types/Component/MessageComponent.cs b/Myriad/Types/Component/MessageComponent.cs index 2240a2fe..9421fb89 100644 --- a/Myriad/Types/Component/MessageComponent.cs +++ b/Myriad/Types/Component/MessageComponent.cs @@ -3,7 +3,7 @@ namespace Myriad.Types; public record MessageComponent { public ComponentType Type { get; init; } - public ButtonStyle? Style { get; init; } + public ButtonStyle? Style { get; set; } public string? Label { get; init; } public Emoji? Emoji { get; init; } public string? CustomId { get; init; } diff --git a/Myriad/Types/Embed.cs b/Myriad/Types/Embed.cs index b71b633a..5c4e2af8 100644 --- a/Myriad/Types/Embed.cs +++ b/Myriad/Types/Embed.cs @@ -11,8 +11,8 @@ public record Embed public EmbedFooter? Footer { get; init; } public EmbedImage? Image { get; init; } public EmbedThumbnail? Thumbnail { get; init; } - public EmbedVideo? Video { get; init; } - public EmbedProvider? Provider { get; init; } + // public EmbedVideo? Video { get; init; } + // public EmbedProvider? Provider { get; init; } public EmbedAuthor? Author { get; init; } public Field[]? Fields { get; init; } diff --git a/Myriad/Types/Emoji.cs b/Myriad/Types/Emoji.cs index 2e41b90a..7c65a39e 100644 --- a/Myriad/Types/Emoji.cs +++ b/Myriad/Types/Emoji.cs @@ -4,5 +4,5 @@ public record Emoji { public ulong? Id { get; init; } public string? Name { get; init; } - public bool? Animated { get; init; } + // public bool? Animated { get; init; } } \ No newline at end of file diff --git a/Myriad/Types/Guild.cs b/Myriad/Types/Guild.cs index 2e44237b..e0ce73b4 100644 --- a/Myriad/Types/Guild.cs +++ b/Myriad/Types/Guild.cs @@ -12,19 +12,19 @@ public record Guild { public ulong Id { get; init; } public string Name { get; init; } - public string? Icon { get; init; } - public string? Splash { get; init; } - public string? DiscoverySplash { get; init; } - public bool? Owner { get; init; } + // public string? Icon { get; init; } + // public string? Splash { get; init; } + // public string? DiscoverySplash { get; init; } + // public bool? Owner { get; init; } public ulong OwnerId { get; init; } - public string Region { get; init; } - public ulong? AfkChannelId { get; init; } - public int AfkTimeout { get; init; } - public bool? WidgetEnabled { get; init; } - public ulong? WidgetChannelId { get; init; } - public int VerificationLevel { get; init; } + // public string Region { get; init; } + // public ulong? AfkChannelId { get; init; } + // public int AfkTimeout { get; init; } + // public bool? WidgetEnabled { get; init; } + // public ulong? WidgetChannelId { get; init; } + // public int VerificationLevel { get; init; } public PremiumTier PremiumTier { get; init; } public Role[] Roles { get; init; } - public string[] Features { get; init; } + // public string[] Features { get; init; } } \ No newline at end of file diff --git a/Myriad/Types/GuildMember.cs b/Myriad/Types/GuildMember.cs index 097c8938..9f163801 100644 --- a/Myriad/Types/GuildMember.cs +++ b/Myriad/Types/GuildMember.cs @@ -10,5 +10,5 @@ public record GuildMemberPartial public string? Avatar { get; init; } public string? Nick { get; init; } public ulong[] Roles { get; init; } - public string JoinedAt { get; init; } + // public string JoinedAt { get; init; } } \ No newline at end of file diff --git a/Myriad/Types/Message.cs b/Myriad/Types/Message.cs index 55928de9..3016ccbf 100644 --- a/Myriad/Types/Message.cs +++ b/Myriad/Types/Message.cs @@ -46,29 +46,30 @@ public record Message public MessageActivity? Activity { get; init; } public User Author { get; init; } public string? Content { get; init; } - public string? Timestamp { get; init; } - public string? EditedTimestamp { get; init; } - public bool Tts { get; init; } - public bool MentionEveryone { get; init; } + // public string? Timestamp { get; init; } + // public string? EditedTimestamp { get; init; } + // public bool Tts { get; init; } + // public bool MentionEveryone { get; init; } public User.Extra[] Mentions { get; init; } - public ulong[] MentionRoles { get; init; } + // public ulong[] MentionRoles { get; init; } + public MessageComponent[]? Components { get; init; } public Attachment[] Attachments { get; init; } public Embed[]? Embeds { get; init; } public Sticker[]? StickerItems { get; init; } - public Sticker[]? Stickers { get; init; } - public Reaction[] Reactions { get; init; } - public bool Pinned { get; init; } + // public Sticker[]? Stickers { get; init; } + // public Reaction[] Reactions { get; init; } + // public bool Pinned { get; init; } public ulong? WebhookId { get; init; } public ulong? ApplicationId { get; init; } public MessageType Type { get; init; } public Reference? MessageReference { get; set; } - public MessageFlags Flags { get; init; } + // public MessageFlags Flags { get; init; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional ReferencedMessage { get; init; } - public MessageComponent[]? Components { get; init; } + // public MessageComponent[]? Components { get; init; } public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId); @@ -82,8 +83,8 @@ public record Message public int Size { get; init; } public string Url { get; init; } public string ProxyUrl { get; init; } - public int? Width { get; init; } - public int? Height { get; init; } + // public int? Width { get; init; } + // public int? Height { get; init; } } public record Reaction diff --git a/Myriad/Types/Role.cs b/Myriad/Types/Role.cs index 4347b117..777a43ee 100644 --- a/Myriad/Types/Role.cs +++ b/Myriad/Types/Role.cs @@ -4,10 +4,10 @@ public record Role { public ulong Id { get; init; } public string Name { get; init; } - public uint Color { get; init; } - public bool Hoist { get; init; } + // public uint Color { get; init; } + // public bool Hoist { get; init; } public int Position { get; init; } public PermissionSet Permissions { get; init; } - public bool Managed { get; init; } + // public bool Managed { get; init; } public bool Mentionable { get; init; } } \ No newline at end of file diff --git a/Myriad/Types/User.cs b/Myriad/Types/User.cs index 359ab1fb..49ac434c 100644 --- a/Myriad/Types/User.cs +++ b/Myriad/Types/User.cs @@ -26,10 +26,10 @@ public record User public string? Avatar { get; init; } public bool Bot { get; init; } public bool? System { get; init; } - public Flags PublicFlags { get; init; } + // public Flags PublicFlags { get; init; } public record Extra: User { - public GuildMemberPartial? Member { get; init; } + // public GuildMemberPartial? Member { get; init; } } } \ No newline at end of file diff --git a/Myriad/packages.lock.json b/Myriad/packages.lock.json index a0c90f89..e606fd58 100644 --- a/Myriad/packages.lock.json +++ b/Myriad/packages.lock.json @@ -24,9 +24,9 @@ }, "Grpc.Tools": { "type": "Direct", - "requested": "[2.37.0, )", - "resolved": "2.37.0", - "contentHash": "cud/urkbw3QoQ8+kNeCy2YI0sHrh7td/1cZkVbH6hDLIXX7zzmJbV/KjYSiqiYtflQf+S5mJPLzDQWScN/QdDg==" + "requested": "[2.47.0, )", + "resolved": "2.47.0", + "contentHash": "nInNoLfT/zR7+0VNIC4Lu5nF8azjTz3KwHB1ckwsYUxvof4uSxIt/LlCKb/NH7GPfXfdvqDDinguPpP5t55nuA==" }, "Polly": { "type": "Direct", diff --git a/PluralKit.API/APIJsonExt.cs b/PluralKit.API/APIJsonExt.cs index 91386bc3..796a17c1 100644 --- a/PluralKit.API/APIJsonExt.cs +++ b/PluralKit.API/APIJsonExt.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; + using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -25,6 +27,25 @@ public static class APIJsonExt return o; } + + public static JObject EmbedJson(string title, string type) + { + var o = new JObject(); + + o.Add("type", "rich"); + o.Add("provider_name", "PluralKit " + type); + o.Add("provider_url", "https://pluralkit.me"); + o.Add("title", title); + + return o; + } + + public static async Task WriteJSON(this HttpResponse resp, int statusCode, string jsonText) + { + resp.StatusCode = statusCode; + resp.Headers.Add("content-type", "application/json"); + await resp.WriteAsync(jsonText); + } } public struct FrontersReturnNew diff --git a/PluralKit.API/Middleware/AuthorizationTokenHandlerMiddleware.cs b/PluralKit.API/AuthorizationTokenHandlerMiddleware.cs similarity index 100% rename from PluralKit.API/Middleware/AuthorizationTokenHandlerMiddleware.cs rename to PluralKit.API/AuthorizationTokenHandlerMiddleware.cs diff --git a/PluralKit.API/Controllers/PrivateController.cs b/PluralKit.API/Controllers/PrivateController.cs index 1043e5a4..6e0fc25b 100644 --- a/PluralKit.API/Controllers/PrivateController.cs +++ b/PluralKit.API/Controllers/PrivateController.cs @@ -135,6 +135,9 @@ public class PrivateController: PKControllerBase // guilds.Select(g => new HashEntry(g.Value("id"), true)).ToArray() // ); + if (system.Token == null) + system = await _repo.UpdateSystem(system.Id, new SystemPatch { Token = StringUtils.GenerateToken() }); + var o = new JObject(); o.Add("system", system.ToJson(LookupContext.ByOwner)); diff --git a/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs b/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs new file mode 100644 index 00000000..23a118be --- /dev/null +++ b/PluralKit.API/Controllers/v2/AutoproxyControllerV2.cs @@ -0,0 +1,77 @@ +using Microsoft.AspNetCore.Mvc; + +using Newtonsoft.Json.Linq; + +using PluralKit.Core; + +namespace PluralKit.API; + +[ApiController] +[Route("v2")] +public class AutoproxyControllerV2: PKControllerBase +{ + public AutoproxyControllerV2(IServiceProvider svc) : base(svc) { } + + // asp.net why + + [HttpGet("systems/{systemRef}/autoproxy")] + public Task GetWrapper([FromRoute] string systemRef, [FromQuery] ulong? guild_id, [FromQuery] ulong? channel_id) + => Entrypoint(systemRef, guild_id, channel_id, null); + + [HttpPatch("systems/{systemRef}/autoproxy")] + public Task PatchWrapper([FromRoute] string systemRef, [FromQuery] ulong? guild_id, [FromQuery] ulong? channel_id, [FromBody] JObject? data) + => Entrypoint(systemRef, guild_id, channel_id, data); + + public async Task Entrypoint(string systemRef, ulong? guild_id, ulong? channel_id, JObject? data) + { + var system = await ResolveSystem(systemRef); + if (ContextFor(system) != LookupContext.ByOwner) + throw Errors.GenericMissingPermissions; + + if (guild_id == null || channel_id != null) + throw Errors.Unimplemented; + + var settings = await _repo.GetAutoproxySettings(system.Id, guild_id, channel_id); + if (settings == null) + return NotFound(); + + if (HttpContext.Request.Method == "GET") + return await Get(settings); + else if (HttpContext.Request.Method == "PATCH") + return await Patch(system, guild_id, channel_id, data, settings); + else return StatusCode(415); + } + + private async Task Get(AutoproxySettings settings) + { + string hid = null; + if (settings.AutoproxyMember != null) + hid = (await _repo.GetMember(settings.AutoproxyMember.Value))?.Hid; + + return Ok(settings.ToJson(hid)); + } + + private async Task Patch(PKSystem system, ulong? guildId, ulong? channelId, JObject data, AutoproxySettings oldData) + { + var updateMember = data.ContainsKey("autoproxy_member"); + + PKMember? member = null; + if (updateMember) + member = await ResolveMember(data.Value("autoproxy_member")); + + var patch = AutoproxyPatch.FromJson(data, member?.Id); + + patch.AssertIsValid(); + if (updateMember && member == null) + patch.Errors.Add(new("autoproxy_member", "Member not found.")); + if (updateMember && ((patch.AutoproxyMode.IsPresent && patch.AutoproxyMode.Value == AutoproxyMode.Latch) || oldData.AutoproxyMode == AutoproxyMode.Latch)) + patch.Errors.Add(new("autoproxy_member", "Cannot update autoproxy member if autoproxy mode is set to latch")); + if (patch.Errors.Count > 0) + throw new ModelParseError(patch.Errors); + + var res = await _repo.UpdateAutoproxy(system.Id, guildId, channelId, patch); + if (!updateMember && oldData.AutoproxyMember != null) + member = await _repo.GetMember(oldData.AutoproxyMember.Value); + return Ok(res.ToJson(member?.Hid)); + } +} \ No newline at end of file diff --git a/PluralKit.API/Controllers/v2/GroupControllerV2.cs b/PluralKit.API/Controllers/v2/GroupControllerV2.cs index 01eee1e2..25c88127 100644 --- a/PluralKit.API/Controllers/v2/GroupControllerV2.cs +++ b/PluralKit.API/Controllers/v2/GroupControllerV2.cs @@ -97,6 +97,21 @@ public class GroupControllerV2: PKControllerBase return Ok(group.ToJson(ContextFor(group), system.Hid)); } + [HttpGet("groups/{groupRef}/oembed.json")] + public async Task GroupEmbed(string groupRef) + { + var group = await ResolveGroup(groupRef); + if (group == null) + throw Errors.GroupNotFound; + var system = await _repo.GetSystem(group.System); + + var name = group.DisplayName ?? group.Name; + if (system.Name != null) + name += $" ({system.Name})"; + + return Ok(APIJsonExt.EmbedJson(name, "Group")); + } + [HttpPatch("groups/{groupRef}")] public async Task DoGroupPatch(string groupRef, [FromBody] JObject data) { diff --git a/PluralKit.API/Controllers/v2/MemberControllerV2.cs b/PluralKit.API/Controllers/v2/MemberControllerV2.cs index dfa1eb87..6a517727 100644 --- a/PluralKit.API/Controllers/v2/MemberControllerV2.cs +++ b/PluralKit.API/Controllers/v2/MemberControllerV2.cs @@ -79,6 +79,21 @@ public class MemberControllerV2: PKControllerBase return Ok(member.ToJson(ContextFor(member), systemStr: system.Hid)); } + [HttpGet("members/{memberRef}/oembed.json")] + public async Task MemberEmbed(string memberRef) + { + var member = await ResolveMember(memberRef); + if (member == null) + throw Errors.MemberNotFound; + var system = await _repo.GetSystem(member.System); + + var name = member.DisplayName ?? member.Name; + if (system.Name != null) + name += $" ({system.Name})"; + + return Ok(APIJsonExt.EmbedJson(name, "Member")); + } + [HttpPatch("members/{memberRef}")] public async Task DoMemberPatch(string memberRef, [FromBody] JObject data) { diff --git a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs index 2e580d99..7ff5ea0c 100644 --- a/PluralKit.API/Controllers/v2/SwitchControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SwitchControllerV2.cs @@ -183,7 +183,7 @@ public class SwitchControllerV2: PKControllerBase if (await _repo.GetSwitches(system.Id).Select(x => x.Timestamp).ContainsAsync(value)) throw Errors.SameSwitchTimestampError; - await _repo.MoveSwitch(sw.Id, value); + sw = await _repo.MoveSwitch(sw.Id, value); var members = await _db.Execute(conn => _repo.GetSwitchMembers(conn, sw.Id)).ToListAsync(); return Ok(new FrontersReturnNew diff --git a/PluralKit.API/Controllers/v2/SystemControllerV2.cs b/PluralKit.API/Controllers/v2/SystemControllerV2.cs index 4842e0e9..8990a98e 100644 --- a/PluralKit.API/Controllers/v2/SystemControllerV2.cs +++ b/PluralKit.API/Controllers/v2/SystemControllerV2.cs @@ -20,6 +20,16 @@ public class SystemControllerV2: PKControllerBase return Ok(system.ToJson(ContextFor(system))); } + [HttpGet("{systemRef}/oembed.json")] + public async Task SystemEmbed(string systemRef) + { + var system = await ResolveSystem(systemRef); + if (system == null) + throw Errors.SystemNotFound; + + return Ok(APIJsonExt.EmbedJson(system.Name ?? $"System with ID `{system.Hid}`", "System")); + } + [HttpPatch("{systemRef}")] public async Task DoSystemPatch(string systemRef, [FromBody] JObject data) { diff --git a/PluralKit.API/PluralKit.API.csproj b/PluralKit.API/PluralKit.API.csproj index 3ef156fb..cf916fa7 100644 --- a/PluralKit.API/PluralKit.API.csproj +++ b/PluralKit.API/PluralKit.API.csproj @@ -32,7 +32,7 @@ - + diff --git a/PluralKit.API/PluralKit.API.csproj.DotSettings b/PluralKit.API/PluralKit.API.csproj.DotSettings deleted file mode 100644 index 3bceb3b3..00000000 --- a/PluralKit.API/PluralKit.API.csproj.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - True - True - True \ No newline at end of file diff --git a/PluralKit.API/Startup.cs b/PluralKit.API/Startup.cs index 06c7b6b8..ca2b7eec 100644 --- a/PluralKit.API/Startup.cs +++ b/PluralKit.API/Startup.cs @@ -107,31 +107,19 @@ public class Startup // handle common ISEs that are generated by invalid user input if (exc.Error.IsUserError()) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsync("{\"message\":\"400: Bad Request\",\"code\":0}"); - } + await ctx.Response.WriteJSON(400, "{\"message\":\"400: Bad Request\",\"code\":0}"); else if (exc.Error is not PKError) - { - ctx.Response.StatusCode = 500; - await ctx.Response.WriteAsync("{\"message\":\"500: Internal Server Error\",\"code\":0}"); - } + await ctx.Response.WriteJSON(500, "{\"message\":\"500: Internal Server Error\",\"code\":0}"); // for some reason, if we don't specifically cast to ModelParseError, it uses the base's ToJson method else if (exc.Error is ModelParseError fe) - { - ctx.Response.StatusCode = fe.ResponseCode; - await ctx.Response.WriteAsync(JsonConvert.SerializeObject(fe.ToJson())); - } + await ctx.Response.WriteJSON(fe.ResponseCode, JsonConvert.SerializeObject(fe.ToJson())); else { var err = (PKError)exc.Error; - ctx.Response.StatusCode = err.ResponseCode; - - var json = JsonConvert.SerializeObject(err.ToJson()); - await ctx.Response.WriteAsync(json); + await ctx.Response.WriteJSON(err.ResponseCode, JsonConvert.SerializeObject(err.ToJson())); } await ctx.Response.CompleteAsync(); @@ -145,7 +133,15 @@ public class Startup app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); - app.UseEndpoints(endpoints => endpoints.MapControllers()); + app.UseEndpoints(endpoints => + { + // register base / legacy routes + endpoints.MapMethods("", new string[] { }, (context) => { context.Response.Redirect("https://pluralkit.me/api"); return Task.CompletedTask; }); + endpoints.MapMethods("v1/{*_}", new string[] { }, (context) => context.Response.WriteJSON(410, "{\"message\":\"Unsupported API version\",\"code\":0}")); + + // register controllers + endpoints.MapControllers(); + }); // metrics app.UseMetricsAllMiddleware(); diff --git a/PluralKit.API/app.config b/PluralKit.API/app.config deleted file mode 100644 index 757836f3..00000000 --- a/PluralKit.API/app.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/PluralKit.API/openapi.yaml b/PluralKit.API/openapi.yaml deleted file mode 100644 index 22a8ff3f..00000000 --- a/PluralKit.API/openapi.yaml +++ /dev/null @@ -1,853 +0,0 @@ -# Draft version. Refer to the official API documentation for more accurate information. -openapi: 3.0.0 - -info: - title: PluralKit - version: "1.1" - - description: | - This is the API for [PluralKit](https://pluralkit.me/)! :) - - The API itself is stable, but this document (the OpenAPI description) is still subject to change, and may be updated, corrected or restructured in the future (as long as it's still coherent with the real API). - - # Authentication - Authentication is handled using a "system token". At the moment, the only way - to obtain a system token is to use the `pk;token` command through the Discord bot. - - This will generate an opaque string you must pass as the `Authorization` header to API requests. - Many API endpoints are available anonymously, but most of them will hide information from - unauthenticated requests to align with the relevant privacy settings. - - # Errors - Errors are just returned as HTTP response codes. Most error responses include a human-readable - error message as the body, but this should not be relied on. Just read the response codes :) - - # OpenAPI version history - - **1.1**: Granular member privacy - - **1.0**: (initial definition version) - - license: - name: GNU Affero General Public License, Version 3 - url: https://www.gnu.org/licenses/agpl-3.0.en.html - -externalDocs: - url: https://pluralkit.me/api - description: For more information, see the official PluralKit API documentation on the website. - -servers: - - url: https://api.pluralkit.me/v1 - description: Primary API server (v1) - -paths: - /s: - get: - summary: Returns your own system. - description: Requires authentication, and will returns the system the token belongs to. - tags: [Systems] - operationId: GetOwnSystem - security: - - TokenAuth: [] - responses: - "200": - $ref: "#/components/responses/SystemResponse" - "401": - $ref: "#/components/responses/UnauthorizedError" - patch: - summary: Updates an existing system. - description: Requires authentication, and will update the system the token belongs to. - tags: [Systems] - operationId: UpdateSystem - security: - - TokenAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/System" - - responses: - "200": - description: The system was updated. Returns the updated system object. - content: - application/json: - schema: - $ref: "#/components/schemas/System" - "401": - $ref: "#/components/responses/UnauthorizedError" - - /s/{id}: - parameters: - - $ref: "#/components/parameters/SystemID" - get: - summary: Gets a system by its ID. - tags: [Systems] - operationId: GetSystem - description: Partial information may be returned if not authenticated with this system's token. - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/System" - "404": - description: System with the given ID not found. - - /s/{id}/members: - parameters: - - $ref: "#/components/parameters/SystemID" - get: - summary: Gets a system's members. - description: | - If the API token does not belong to this system, this list may exclude any private members in the system. - tags: [Systems, Members] - operationId: GetSystemMembers - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/System" - "403": - description: The system's member list is private, and the given token either missing, invalid, or does not correspond to the system. - "404": - description: System with the given ID not found. - - /s/{id}/fronters: - # TODO: rename to "latest switch" or something? - parameters: - - $ref: "#/components/parameters/SystemID" - get: - summary: Gets a system's current fronters. - tags: [Systems, Switches] - operationId: GetSystemFronters - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/FullSwitch" - "403": - description: The system's current fronters are private, and the given token is either missing, invalid, or does not correspond to the system. - "404": - description: System with the given ID was either not found, or does not have any switches registered. - - /s/{id}/switches: - parameters: - - $ref: "#/components/parameters/SystemID" - get: - summary: Gets a system's switch history. - description: | - Will return the system's switch history, up to 100 entries at a time, in reverse-chronological (latest first) order. - - For pagination, see the `before` query parameter. - tags: [Systems, Switches] - operationId: GetSystemSwitches - parameters: - - in: query - name: before - schema: - type: string - format: date-time - description: | - If provided, will only return switches that happened *before* (and not including) this timestamp. - - This can be used for pagination by calling the endpoint again with the timestamp of the last switch of the previous response. - responses: - "200": - description: OK - content: - application/json: - schema: - type: array - maxItems: 100 - items: - $ref: "#/components/schemas/Switch" - "403": - description: The system's switch history is private, and the given token is either missing, invalid, or does not correspond to the system. - "404": - description: System with the given ID not found. - - /s/switches: - post: - summary: Registers a new switch. - tags: [Switches] - operationId: RegisterSwitch - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/Switch" - security: - - TokenAuth: [] - responses: - "204": - description: The switch was logged. - "401": - $ref: "#/components/responses/UnauthorizedError" - - /m: - post: - summary: Creates a new member in your system. - tags: [Members] - operationId: CreateMember - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/Member" - security: - - TokenAuth: [] - responses: - "200": - description: The member was created. Returns the newly created member object. - content: - application/json: - schema: - $ref: "#/components/schemas/Member" - "401": - $ref: "#/components/responses/UnauthorizedError" - - /m/{id}: - parameters: - - $ref: "#/components/parameters/MemberID" - get: - summary: Gets a member by their ID. - tags: [Members] - operationId: GetMember - security: - - {} - - TokenAuth: [] - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/Member" - "404": - $ref: "#/components/responses/MemberNotFoundError" - patch: - summary: Updates a member. - tags: [Members] - operationId: UpdateMember - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/Member" - security: - - TokenAuth: [] - responses: - "200": - description: The member was updated. Returns trhe updated member object. - content: - application/json: - schema: - $ref: "#/components/schemas/Member" - "401": - $ref: "#/components/responses/MemberAuthError" - "404": - $ref: "#/components/responses/MemberNotFoundError" - - delete: - summary: Deletes a member. - tags: [Members] - operationId: DeleteMember - security: - - TokenAuth: [] - responses: - "200": # TODO: should be 204 (but *is* 200) - description: Member successfully deleted. - "401": - $ref: "#/components/responses/MemberAuthError" - "404": - $ref: "#/components/responses/MemberNotFoundError" - - /a/{id}: - parameters: - - in: path - name: id - required: true - description: A Discord user ID. - schema: - $ref: "#/components/schemas/Snowflake" - - get: - summary: Gets a system by (one of) its associated Discord accounts. - description: | - Note that it's currently not possible to get a system's registered accounts given a system ID through the API. - Consider this endpoint "one-way". - tags: [Systems, Accounts] - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/System" - "404": - description: The given user ID does not correspond to any systems. - - /msg/{id}: - parameters: - - in: path - name: id - required: true - description: | - A Discord message ID. - - This may refer to either the original "trigger message" posted by the user, - or to the resulting webhook message posted by PluralKit. - The former may be useful for eg. logging bot integration. - schema: - $ref: "#/components/schemas/Snowflake" - - get: - summary: Gets information about a proxied message by its message ID. - tags: [Proxying] - responses: - "200": - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/Message" - "404": - description: The given message ID does not correspond to a message proxied by PluralKit. - -components: - responses: - UnauthorizedError: - description: System token is missing or invalid. - - NotOwnSystemError: - description: The given system does not correspond with this token. - - MemberAuthError: - # TODO (relevant, ish): This is always returned as 401 but should be split into "invalid token" (401) and "not own member" (403) responses - description: System token is missing, invalid, or the corresponding system does not own the given member. - - MemberNotFoundError: - description: A member by the given ID was not found. - - SystemResponse: - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/System" - - MemberListResponse: - description: OK - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Member" - - parameters: - SystemID: - in: path - name: id - required: true - description: The ID of the system in question. - schema: - $ref: "#/components/schemas/ID" - - MemberID: - in: path - name: id - required: true - description: The ID of the member in question. - schema: - $ref: "#/components/schemas/ID" - - schemas: - ID: - title: ID (system or member) - type: string - readOnly: true - minLength: 5 - maxLength: 5 - pattern: "^[a-z]{5}$" - example: "abcde" - description: | - A unique identifier for a system or a member, as a randomly generated string of five lowercase letters. - - These IDs are guaranteed to be globally unique for the given model type (a system can have the same ID as a member, but two systems or two members can never share an ID). - - The IDs can on rare occasions change - eg. if a profane word is generated and later regenerated, or as a potential future Patreon perk. However, it's still reasonable to assume that the IDs are constant, and that tey won't change without the user's knowledge, so it's safe to store in things like settings menus and config files. - System: - properties: - id: - $ref: "#/components/schemas/ID" - - name: - type: string - nullable: true - maxLength: 100 - description: The user-provided name of the system. - example: "Boxes of Foxes" - - description: - type: string - nullable: true - maxLength: 1000 - description: | - The user-provided system description. - - May contain rich text in Markdown, including standard Markdown-formatted links, or Discord-formatted emoji/user/channel references. - example: |- - This system is very cool. - - It has cool people. - tag: - type: string - maxLength: 78 - description: | - The system tag, which is appended to the names (or display names, if set) of members when proxying through the bot. - - A space will automatically be inserted between the name and the tag, so no need to include one at the start. - - The character limit here is 78, as Discord's name length limit for webhooks is 80 characters. A 78-character system tag is thus the longest tag that can still accomodate a single-letter member name and the separating space. - nullable: true - example: "| BoF" - - avatar_url: - type: string - format: url - nullable: true - maxLength: 256 - example: "https://i.imgur.com/Abcdefg.png" - description: | - A link to the avatar/icon of the system. - - If used for proxying, the image must be at most 1000 pixels in width *and* height, and at most 1 MiB in size. This restriction is on Discord's end and is not verified through the API (it's simply stored as a string). - - tz: - type: string - format: timezone - nullable: true - default: UTC - example: America/New_York - description: | - The system's registered time zone as an Olson time zone ID. - - This is used in the bot to convert various time stamps in commands on behalf of this system. - - created: - type: string - format: date-time - readOnly: true - description: The creation timestamp of the system. - - description_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The system's description privacy setting, either "public" or "private". - - If this is set to "private", the field `description` will be returned as `null` on all requests not authorized with this system's token. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - - Because of this, there is no way for an unauthorized user to tell the difference between a private description and a `null` description - this is intentional. - example: public - - member_list_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The system's member list privacy setting, either "public" or "private". - - If this is set to "private", this system's member list can not be queried without proper authorization. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - example: public - - front_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The system's current fronter privacy setting, either "public" or "private". - - If this is set to "private", this system's current fronter can not be queried without proper authorization. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - example: public - - front_history_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The system's front history privacy setting, either "public" or "private". - - If this is set to "private", this system's front/switch history can not be queried without proper authorization. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - example: public - - Member: - properties: - id: - $ref: "#/components/schemas/ID" - - name: - type: string - maxLength: 100 - description: The user-provided name of the member. - example: "Myriad Kit" - - display_name: - type: string - maxLength: 100 - description: The member's "display name", which will override the member's normal name when proxying. - nullable: true - example: Myriad 'Big Boss' Kit - - description: - type: string - maxLength: 1000 - description: | - The user-provided description of the member. - - May contain rich text in Markdown, including standard Markdown-formatted links, or Discord-formatted emoji/user/channel references. - - If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. - - example: Myriad is very cool and rad, and they love snuggling. - - color: - type: string - format: color - minLength: 6 - maxLength: 6 - nullable: true - pattern: "^[0-9A-Fa-f]{6}$" - description: | - The member's "color", displayed on member cards, as a 6-character hexadecimal color code (no leading #). - - If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. - - example: "FF0000" - - birthday: - type: string - format: date - example: "2018-07-11" - nullable: true - description: | - The user-provided birthdate of the member. - - "Year-less" birthdays are supported. In this case, the year should be set to `0004`, and that specific year should be special-cased and hidden from the user. Previous versions used the year `0001` for the same purpose, and this value may still be both read and written with the API and should be treated the same as `0004`. - - The year `0004` was chosen because it is a leap year in the Gregorian calendar, and thus the date `0004-02-29` can be properly represented. - - If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. - - pronouns: - type: string - maxLength: 100 - example: "they/them or xe/xem" - nullable: true - description: | - The user-provided pronouns of the member. - - There is no specific schema, just a freeform text field. - - If this member is private, and the request is not authorized with the member's system token, this field will always be returned as `null`. - - avatar_url: - type: string - format: url - nullable: true - maxLength: 256 - example: "https://i.imgur.com/Abcdefg.png" - description: | - A link to the avatar/icon of the member. - - If used for proxying, the image must be at most 1000 pixels in width *and* height, and at most 1 MiB in size. This restriction is on Discord's end and is not verified through the API (it's simply stored as a string). - - privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - This field is deprecated. - - At the moment, it's still included in member objects, with a value that mirrors the `visibility` field. Writing to this field will set *every* privacy setting to the written value. - example: public - deprecated: true - - visibility: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current visibility, either "public" or "private". - - If this is set to "private", the member will not appear in public member lists. It can still be looked up using its 5-character member ID, but will return limited information. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - example: public - - name_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current name privacy setting, either "public" or "private". - - If this is set to "private", the member's returned `name` will be replaced by their `display_name` when applicable. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - - Because of this, there is no way for an unauthorized user to tell the difference between a private name being replaced by the display name, and an empty display name with the name set - this is intentional. - example: public - - description_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current description privacy setting, either "public" or "private". - - If this is set to "private", the field `description` will be returned as `null` on all requests not authorized with this system's token. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - - Because of this, there is no way for an unauthorized user to tell the difference between a private description and a `null` description - this is intentional. - example: public - - avatar_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current avatar privacy setting, either "public" or "private". - - If this is set to "private", the field `avatar_url` will be returned as `null` on all requests not authorized with this system's token. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - - Because of this, there is no way for an unauthorized user to tell the difference between a private avatar and a `null` avatar - this is intentional. - example: public - - pronouns_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current pronouns privacy setting, either "public" or "private". - - If this is set to "private", the field `pronouns` will be returned as `null` on all requests not authorized with this system's token. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - - Because of this, there is no way for an unauthorized user to tell the difference between private pronouns and `null` pronouns - this is intentional. - example: public - - birthday_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current birthday privacy setting, either "public" or "private". - - If this is set to "private", the field `birthday` will be returned as `null` on all requests not authorized with this system's token. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - - Because of this, there is no way for an unauthorized user to tell the difference between a private birthday and a `null` birthday - this is intentional. - example: public - - metadata_privacy: - allOf: - - $ref: "#/components/schemas/PrivacySetting" - - description: | - The member's current metadata privacy setting, either "public" or "private". - - If this is set to "private", the field `created` on all requests not authorized with this system's token. - - In addition, this field will be returned as `null` if the request is not authorized with this system's token. - example: public - - proxy_tags: - type: array - items: - $ref: "#/components/schemas/ProxyTag" - description: | - An unordered list of the member's proxy tag pairs. - - It is valid for a member to have any number of proxy tags, including none at all. - - prefix: - type: string - nullable: true - description: | - Previous versions of the API only supported a single proxy tag pair per member. - This field will contain the prefix of the first proxy tag registered, or `null` if missing. - Setting it will write to the first proxy tag's prefix, creating it if not present. - - This field is deprecated and will be removed in API v2. - deprecated: true - example: "{{" - - suffix: - type: string - nullable: true - description: | - Previous versions of the API only supported a single proxy tag pair per member. - This field will contain the suffix of the first proxy tag registered, or `null` if missing. - Setting it will write to the first proxy tag's suffix, creating it if not present. - - This field is deprecated and will be removed in API v2. - deprecated: true - example: "}}" - - keep_proxy: - type: boolean - default: false - description: | - Whether or not to include the used proxy tags in proxied messages. - - created: - type: string - format: date-time - readOnly: true - description: The creation timestamp of the member. May be returned as `null` depending on the value of `metadata_privacy` and the request authorization. - nullable: true - - PrivacySetting: - title: Privacy Setting - type: string - nullable: true - description: A privacy setting for systems or members, either public or private. May occasionally be `null` in cases where you are not authorized to view the privacy setting's state. - enum: [public, private] - example: public - - ProxyTag: - title: Proxy Tag - description: | - Represents a proxy tag to match messages on. - - A "proxy tag" consists of a prefix and a suffix, and a given proxy tag set matches a string - if that string begins with the prefix and ends with the suffix. - It's often represented to the user with the word `text` between them, eg. `[text]`, `{{text`, and so on. - - For example, the proxy tag pair "[" and "]" will match any string \[in square brackets\]. - - Either the prefix or the suffix may be missing (or both may be present), but it is invalid for - both values to be null. - properties: - prefix: - type: string - nullable: true - description: | - The proxy tag prefix. This is the string that goes *before* the inner text. - - An empty prefix is represented as `null` and an empty string will be converted as such. - example: "{{" - maxLength: 100 - suffix: - type: string - nullable: true - description: | - The proxy tag suffix. This is the string that goes *after* the inner text. - - An empty suffix is represented as `null` and an empty string will be converted as such. - example: "}}" - maxLength: 100 - - Switch: - title: Logged Switch - properties: - timestamp: - type: string - format: date-time - description: The timestamp the switch was logged. - readOnly: true - members: - type: array - description: | - A list of the IDs of the members in this switch. The order is significant. It is valid for the switch to have no members at all. - items: - $ref: "#/components/schemas/ID" - - FullSwitch: - title: Logged Switch (with full Member object) - properties: - timestamp: - type: string - format: date-time - description: The timestamp the switch was logged. - members: - type: array - description: | - A list of the members in this switch. The order is significant. It is valid for the switch to have no members at all. - items: - $ref: "#/components/schemas/Member" - - Snowflake: - title: Snowflake (Discord ID) - description: | - A unique identifier used by Discord for its objects (accounts, guilds, channels, messages). - - This is internally stored as a 64-bit unsigned integer, but is represented as a string in the API - to accomodate languages that store numbers as floating point values (and thus would not be able to represent it losslessly). - type: string - pattern: "^[0-9]{17,19}" - minLength: 17 - maxLength: 19 - example: "466378653216014359" # PK's account ID :3 - - Message: - title: Message Info - description: | - An object containing information about a proxied message, including message, user and channel IDs, as well as the system and member it's related to. - - For privacy and performance reasons, this endpoint does not return the *contents* of the original message. This data isn't stored in the database either way - but given the channel and message ID, it can be fetched from Discord's own API. - properties: - timestamp: - type: string - format: date-time - readOnly: true - description: The time the message was proxied. - id: - allOf: - - $ref: "#/components/schemas/Snowflake" - - description: "The ID of the proxied webhook message posted by PluralKit." - example: "123456789012345678" - original: - allOf: - - $ref: "#/components/schemas/Snowflake" - - description: "The ID of the original (now-deleted) trigger message containing the proxy tags." - example: "123456789012345678" - sender: - allOf: - - $ref: "#/components/schemas/Snowflake" - - description: "The ID of the Discord user that sent the trigger message." - example: "123456789012345678" - channel: - allOf: - - $ref: "#/components/schemas/Snowflake" - - description: "The ID of the Discord channel the relevant messages were posted in." - example: "123456789012345678" - system: - allOf: - - $ref: "#/components/schemas/System" - - description: "The system that proxied the message." - member: - allOf: - - $ref: "#/components/schemas/Member" - - description: "The member that proxied the message." - - securitySchemes: - TokenAuth: - type: apiKey - in: header - name: Authorization - description: A system token obtained from the `pk;proxy` command in the Discord bot. \ No newline at end of file diff --git a/PluralKit.API/packages.lock.json b/PluralKit.API/packages.lock.json index 230cc70b..c88cf343 100644 --- a/PluralKit.API/packages.lock.json +++ b/PluralKit.API/packages.lock.json @@ -53,9 +53,9 @@ }, "Grpc.Tools": { "type": "Direct", - "requested": "[2.37.0, )", - "resolved": "2.37.0", - "contentHash": "cud/urkbw3QoQ8+kNeCy2YI0sHrh7td/1cZkVbH6hDLIXX7zzmJbV/KjYSiqiYtflQf+S5mJPLzDQWScN/QdDg==" + "requested": "[2.47.0, )", + "resolved": "2.47.0", + "contentHash": "nInNoLfT/zR7+0VNIC4Lu5nF8azjTz3KwHB1ckwsYUxvof4uSxIt/LlCKb/NH7GPfXfdvqDDinguPpP5t55nuA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Direct", diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index cfcfe589..f8d21342 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -73,7 +73,7 @@ public class Bot } }; - _services.Resolve().OnEventReceived += (e) => OnEventReceivedInner(e.Item1, e.Item2); + _services.Resolve().OnEventReceived += (e) => OnEventReceived(e.Item1, e.Item2); // Init the shard stuff _services.Resolve().Init(); diff --git a/PluralKit.Bot/BotMetrics.cs b/PluralKit.Bot/BotMetrics.cs index 42d8b64b..23eb324e 100644 --- a/PluralKit.Bot/BotMetrics.cs +++ b/PluralKit.Bot/BotMetrics.cs @@ -22,6 +22,20 @@ public static class BotMetrics Context = "Bot" }; + public static MeterOptions DatabaseDMCacheHits => new() + { + Name = "Database DM Cache Hits", + MeasurementUnit = Unit.Calls, + Context = "Bot" + }; + + public static MeterOptions DMCacheMisses => new() + { + Name = "DM Cache Misses", + MeasurementUnit = Unit.Calls, + Context = "Bot" + }; + public static MeterOptions CommandsRun => new() { Name = "Commands run", diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 08601749..3dc1b908 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -92,6 +92,7 @@ public partial class CommandTree public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel"); public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); public static Command LogDisable = new Command("log disable", "log disable all| [channel 2] [channel 3...]", "Disables message logging in certain channels"); + public static Command LogShow = new Command("log show", "log show", "Displays the current list of channels where logging is disabled"); public static Command LogClean = new Command("logclean", "logclean [on|off]", "Toggles whether to clean up other bots' log channels"); public static Command BlacklistShow = new Command("blacklist show", "blacklist show", "Displays the current proxy blacklist"); public static Command BlacklistAdd = new Command("blacklist add", "blacklist add all| [channel 2] [channel 3...]", "Adds certain channels to the proxy blacklist"); @@ -142,7 +143,7 @@ public partial class CommandTree AutoproxyOff, AutoproxyFront, AutoproxyLatch, AutoproxyMember }; - public static Command[] LogCommands = { LogChannel, LogChannelClear, LogEnable, LogDisable }; + public static Command[] LogCommands = { LogChannel, LogChannelClear, LogEnable, LogDisable, LogShow }; public static Command[] BlacklistCommands = { BlacklistAdd, BlacklistRemove, BlacklistShow }; } \ No newline at end of file diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 29be7bf0..b5b65dc9 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -48,7 +48,7 @@ public partial class CommandTree return ctx.Execute(Message, m => m.GetMessage(ctx)); if (ctx.Match("edit", "e")) return ctx.Execute(MessageEdit, m => m.EditMessage(ctx)); - if (ctx.Match("reproxy", "rp")) + if (ctx.Match("reproxy", "rp", "crimes")) return ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) @@ -57,6 +57,8 @@ public partial class CommandTree return ctx.Execute(LogEnable, m => m.SetLogEnabled(ctx, true)); else if (ctx.Match("disable", "off")) return ctx.Execute(LogDisable, m => m.SetLogEnabled(ctx, false)); + else if (ctx.Match("list", "show")) + return ctx.Execute(LogShow, m => m.ShowLogDisabledChannels(ctx)); else if (ctx.Match("commands")) return PrintCommandList(ctx, "message logging", LogCommands); else return PrintCommandExpectedError(ctx, LogCommands); diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index d1233467..6398f34f 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -28,8 +28,8 @@ public class Context private Command? _currentCommand; - public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message, int commandParseOffset, - PKSystem senderSystem, SystemConfig config, MessageContext messageContext) + public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message, + int commandParseOffset, PKSystem senderSystem, SystemConfig config) { Message = (Message)message; ShardId = shardId; @@ -37,7 +37,6 @@ public class Context Channel = channel; System = senderSystem; Config = config; - MessageContext = messageContext; Cache = provider.Resolve(); Database = provider.Resolve(); Repository = provider.Resolve(); @@ -61,7 +60,6 @@ public class Context public readonly Guild Guild; public readonly int ShardId; public readonly Cluster Cluster; - public readonly MessageContext MessageContext; public Task BotPermissions => Cache.PermissionsIn(Channel.Id); public Task UserPermissions => Cache.PermissionsFor((MessageCreateEvent)Message); @@ -96,12 +94,12 @@ public class Context AllowedMentions = mentions ?? new AllowedMentions() }); - if (embed != null) - { - // Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) - // This may need to be changed at some point but works well enough for now - await _commandMessageService.RegisterMessage(msg.Id, msg.ChannelId, Author.Id); - } + // if (embed != null) + // { + // Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example) + // but since we can, we just store all sent messages for possible deletion + await _commandMessageService.RegisterMessage(msg.Id, msg.ChannelId, Author.Id); + // } return msg; } @@ -110,6 +108,12 @@ public class Context { _currentCommand = commandDef; + if (deprecated && commandDef != null) + { + await Reply($"{Emojis.Warn} This command has been removed. please use `pk;{commandDef.Key}` instead."); + return; + } + try { using (_metrics.Measure.Timer.Time(BotMetrics.CommandTime, new MetricTags("Command", commandDef?.Key ?? "null"))) @@ -130,9 +134,6 @@ public class Context // Got a complaint the old error was a bit too patronizing. Hopefully this is better? await Reply($"{Emojis.Error} Operation timed out, sorry. Try again, perhaps?"); } - - if (deprecated && commandDef != null) - await Reply($"{Emojis.Warn} This command is deprecated and will be removed soon. In the future, please use `pk;{commandDef.Key}`."); } /// diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 90c3bb39..d877dfb7 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -103,6 +103,13 @@ public static class ContextArgumentsExt ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw"); public static bool MatchToggle(this Context ctx, bool? defaultValue = null) + { + var value = ctx.MatchToggleOrNull(defaultValue); + if (value == null) throw new PKError("You must pass either \"on\" or \"off\" to this command."); + return value.Value; + } + + public static bool? MatchToggleOrNull(this Context ctx, bool? defaultValue = null) { if (defaultValue != null && ctx.MatchClearInner()) return defaultValue.Value; @@ -114,8 +121,7 @@ public static class ContextArgumentsExt return true; else if (ctx.Match(noToggles) || ctx.MatchFlag(noToggles)) return false; - else - throw new PKError("You must pass either \"on\" or \"off\" to this command."); + else return null; } public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId) diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 237708a3..e1b263b0 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -89,10 +89,12 @@ public class Autoproxy var eb = new EmbedBuilder() .Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})"); - var fronters = ctx.MessageContext.LastSwitchMembers; + var sw = await ctx.Repository.GetLatestSwitch(ctx.System.Id); + var fronters = sw == null ? new() : await ctx.Database.Execute(c => ctx.Repository.GetSwitchMembers(c, sw.Id)).ToListAsync(); + var relevantMember = settings.AutoproxyMode switch { - AutoproxyMode.Front => fronters.Length > 0 ? await ctx.Repository.GetMember(fronters[0]) : null, + AutoproxyMode.Front => fronters.Count > 0 ? fronters[0] : null, AutoproxyMode.Member when settings.AutoproxyMember.HasValue => await ctx.Repository.GetMember(settings.AutoproxyMember.Value), _ => null }; @@ -104,7 +106,7 @@ public class Autoproxy break; case AutoproxyMode.Front: { - if (fronters.Length == 0) + if (fronters.Count == 0) { eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch."); } @@ -135,7 +137,8 @@ public class Autoproxy default: throw new ArgumentOutOfRangeException(); } - if (!ctx.MessageContext.AllowAutoproxy) + var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); + if (!allowAutoproxy) eb.Field(new Embed.Field("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`.")); return eb.Build(); diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index 05623a4d..0c6836a4 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -268,8 +268,7 @@ public class Checks try { _proxy.ShouldProxy(channel, msg, context); - _matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, - context.AllowAutoproxy); + _matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, true); await ctx.Reply("I'm not sure why this message was not proxied, sorry."); } diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 5831abf5..e6591e90 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -17,10 +17,12 @@ public class Config { var items = new List(); + var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); + items.Add(new( "autoproxy account", "Whether autoproxy is enabled for the current account", - EnabledDisabled(ctx.MessageContext.AllowAutoproxy), + EnabledDisabled(allowAutoproxy), "enabled" )); @@ -122,16 +124,18 @@ public class Config public async Task AutoproxyAccount(Context ctx) { + var allowAutoproxy = await ctx.Repository.GetAutoproxyEnabled(ctx.Author.Id); + if (!ctx.HasNext()) { - await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(ctx.MessageContext.AllowAutoproxy)}** for account <@{ctx.Author.Id}>."); + await ctx.Reply($"Autoproxy is currently **{EnabledDisabled(allowAutoproxy)}** for account <@{ctx.Author.Id}>."); return; } var allow = ctx.MatchToggle(true); var statusString = EnabledDisabled(allow); - if (ctx.MessageContext.AllowAutoproxy == allow) + if (allowAutoproxy == allow) { await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>."); return; diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index 4b92f369..eb47d19c 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -374,38 +374,39 @@ public class Groups public async Task GroupColor(Context ctx, PKGroup target) { - var color = ctx.RemainderOrNull(); - if (await ctx.MatchClear()) - { - ctx.CheckOwnGroup(target); + var isOwnSystem = ctx.System?.Id == target.System; + var matchedRaw = ctx.MatchRaw(); + var matchedClear = await ctx.MatchClear(); - var patch = new GroupPatch { Color = Partial.Null() }; - await ctx.Repository.UpdateGroup(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Group color cleared."); - } - else if (!ctx.HasNext()) + if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) { if (target.Color == null) - if (ctx.System?.Id == target.System) - await ctx.Reply( - $"This group does not have a color set. To set one, type `pk;group {target.Reference(ctx)} color `."); - else - await ctx.Reply("This group does not have a color set."); + await ctx.Reply( + "This group does not have a color set." + (isOwnSystem ? $" To set one, type `pk;group {target.Reference(ctx)} color `." : "")); + else if (matchedRaw) + await ctx.Reply("```\n#" + target.Color + "\n```"); else await ctx.Reply(embed: new EmbedBuilder() .Title("Group color") .Color(target.Color.ToDiscordColor()) .Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) .Description($"This group's color is **#{target.Color}**." - + (ctx.System?.Id == target.System - ? $" To clear it, type `pk;group {target.Reference(ctx)} color -clear`." - : "")) + + (isOwnSystem ? $" To clear it, type `pk;group {target.Reference(ctx)} color -clear`." : "")) .Build()); + return; + } + + ctx.CheckSystem().CheckOwnGroup(target); + + if (matchedClear) + { + await ctx.Repository.UpdateGroup(target.Id, new() { Color = Partial.Null() }); + + await ctx.Reply($"{Emojis.Success} Group color cleared."); } else { - ctx.CheckOwnGroup(target); + var color = ctx.RemainderOrNull(); if (color.StartsWith("#")) color = color.Substring(1); if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index f8289773..ce73c522 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -1,5 +1,5 @@ -using Myriad.Builders; using Myriad.Types; +using Myriad.Rest.Types.Requests; using PluralKit.Core; @@ -10,64 +10,159 @@ public class Help private static Embed helpEmbed = new() { Title = "PluralKit", - Description = "PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.", - Fields = new[] - { - new Embed.Field - ( - "What is this for? What are systems?", - "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks." - + " This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account." - ), - new - ( - "Why are people's names saying [BOT] next to them?", - "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation." - ), - new - ( - "How do I get started?", - String.Join("\n", new[] - { - "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):", - "**1**. `pk;system new` - Create a system (if you haven't already)", - "**2**. `pk;member add John` - Add a new member to your system", - "**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags", - "**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.", - "**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.", - "\nSee [the Getting Started guide](https://pluralkit.me/start) for more information." - }) - ), - new - ( - "Useful tips", - String.Join("\n", new[] { - $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)", - $"React with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)", - $"React with {Emojis.Bell} on a proxied message to \"ping\" the sender", - "Type **`pk;invite`** to get a link to invite this bot to your own server!" - }) - ), - new - ( - "More information", - String.Join("\n", new[] { - "For a full list of commands, see [the command list](https://pluralkit.me/commands).", - "For a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).", - "If you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there." - }) - ), - new - ( - "Support server", - "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78" - ) - }, - Footer = new("By @Ske#6201 | Myriad by @Layl#8888 | GitHub: https://github.com/xSke/PluralKit/ | Website: https://pluralkit.me/"), + Description = "PluralKit is a bot designed for plural communities on Discord, and is open for anyone to use. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.", + Footer = new("By @Ske#6201 | Myriad by @Layl#8888 | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"), Color = DiscordUtils.Blue, }; - public Task HelpRoot(Context ctx) => ctx.Reply(embed: helpEmbed); + private static Dictionary helpEmbedPages = new Dictionary + { + { + "basicinfo", + new Embed.Field[] + { + new + ( + "What is this for? What are systems?", + "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks." + + " This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account." + ), + new + ( + "Why are people's names saying [BOT] next to them?", + "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation." + ), + } + }, + { + "gettingstarted", + new Embed.Field[] + { + new + ( + "How do I get started?", + String.Join("\n", new[] + { + "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):", + "**1**. `pk;system new` - Create a system (if you haven't already)", + "**2**. `pk;member add John` - Add a new member to your system", + "**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags", + "**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.", + "**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.", + "\nSee [the Getting Started guide](https://pluralkit.me/start) for more information." + }) + ), + } + }, + { + "usefultips", + new Embed.Field[] + { + new + ( + "Useful tips", + String.Join("\n", new[] { + $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)", + $"React with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)", + $"React with {Emojis.Bell} on a proxied message to \"ping\" the sender", + "Type **`pk;invite`** to get a link to invite this bot to your own server!" + }) + ), + } + }, + { + "moreinfo", + new Embed.Field[] + { + new + ( + "More information", + String.Join("\n", new[] { + "For a full list of commands, see [the command list](https://pluralkit.me/commands), or type `pk;commands`.", + "For a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).", + "If you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there.", + "We also have a [web dashboard](https://dash.pluralkit.me) to edit your system info online." + }) + ), + new + ( + "Support server", + "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78" + ), + } + } + }; + + private static MessageComponent helpPageButtons(ulong userId) => new MessageComponent + { + Type = ComponentType.ActionRow, + Components = new[] + { + new MessageComponent + { + Type = ComponentType.Button, + Style = ButtonStyle.Secondary, + Label = "Basic Info", + CustomId = $"help-menu-basicinfo-{userId}", + Emoji = new() { Name = "\u2139" }, + }, + new() + { + Type = ComponentType.Button, + Style = ButtonStyle.Secondary, + Label = "Getting Started", + CustomId = $"help-menu-gettingstarted-{userId}", + Emoji = new() { Name = "\u2753", }, + }, + new() + { + Type = ComponentType.Button, + Style = ButtonStyle.Secondary, + Label = "Useful Tips", + CustomId = $"help-menu-usefultips-{userId}", + Emoji = new() { Name = "\U0001f4a1", }, + + }, + new() + { + Type = ComponentType.Button, + Style = ButtonStyle.Secondary, + Label = "More Info", + CustomId = $"help-menu-moreinfo-{userId}", + Emoji = new() { Id = 986379675066593330, }, + } + } + }; + + public Task HelpRoot(Context ctx) + => ctx.Rest.CreateMessage(ctx.Channel.Id, new MessageRequest + { + Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } }, + Components = new[] { helpPageButtons(ctx.Author.Id) }, + }); + + public static Task ButtonClick(InteractionContext ctx) + { + if (!ctx.CustomId.Contains(ctx.User.Id.ToString())) + return ctx.Ignore(); + + var buttons = helpPageButtons(ctx.User.Id); + + if (ctx.Event.Message.Components.First().Components.Where(x => x.CustomId == ctx.CustomId).First().Style == ButtonStyle.Primary) + return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new() + { + Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } }, + Components = new[] { buttons } + }); + + buttons.Components.Where(x => x.CustomId == ctx.CustomId).First().Style = ButtonStyle.Primary; + + return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new() + { + Embeds = new[] { helpEmbed with { Fields = helpEmbedPages.GetValueOrDefault(ctx.CustomId.Split("-")[2]) } }, + Components = new[] { buttons } + }); + } private static string explanation = String.Join("\n\n", new[] { diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index 2d027145..ed02dfba 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -225,41 +225,39 @@ public class MemberEdit public async Task Color(Context ctx, PKMember target) { - var color = ctx.RemainderOrNull(); - if (await ctx.MatchClear()) + var isOwnSystem = ctx.System?.Id == target.System; + var matchedRaw = ctx.MatchRaw(); + var matchedClear = await ctx.MatchClear(); + + if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) { - ctx.CheckOwnMember(target); - - var patch = new MemberPatch { Color = Partial.Null() }; - await ctx.Repository.UpdateMember(target.Id, patch); - - await ctx.Reply($"{Emojis.Success} Member color cleared."); - } - else if (!ctx.HasNext()) - { - // if (!target.ColorPrivacy.CanAccess(ctx.LookupContextFor(target.System))) - // throw Errors.LookupNotAllowed; - if (target.Color == null) - if (ctx.System?.Id == target.System) - await ctx.Reply( - $"This member does not have a color set. To set one, type `pk;member {target.Reference(ctx)} color `."); - else - await ctx.Reply("This member does not have a color set."); + await ctx.Reply( + "This member does not have a color set." + (isOwnSystem ? $" To set one, type `pk;member {target.Reference(ctx)} color `." : "")); + else if (matchedRaw) + await ctx.Reply("```\n#" + target.Color + "\n```"); else await ctx.Reply(embed: new EmbedBuilder() .Title("Member color") .Color(target.Color.ToDiscordColor()) .Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) .Description($"This member's color is **#{target.Color}**." - + (ctx.System?.Id == target.System - ? $" To clear it, type `pk;member {target.Reference(ctx)} color -clear`." - : "")) + + (isOwnSystem ? $" To clear it, type `pk;member {target.Reference(ctx)} color -clear`." : "")) .Build()); + return; + } + + ctx.CheckSystem().CheckOwnMember(target); + + if (matchedClear) + { + await ctx.Repository.UpdateMember(target.Id, new() { Color = Partial.Null() }); + + await ctx.Reply($"{Emojis.Success} Member color cleared."); } else { - ctx.CheckOwnMember(target); + var color = ctx.RemainderOrNull(); if (color.StartsWith("#")) color = color.Substring(1); if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color); diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 65d47608..49384356 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -2,6 +2,8 @@ using System.Text; using System.Text.RegularExpressions; +using Autofac; + using Myriad.Builders; using Myriad.Cache; using Myriad.Extensions; @@ -45,7 +47,7 @@ public class ProxiedMessage _logChannel = logChannel; // _cache = cache; _metrics = metrics; - _proxy = proxy; + _proxy = proxy; } public async Task ReproxyMessage(Context ctx) @@ -61,7 +63,7 @@ public class ProxiedMessage throw new PKError("Could not find a member to reproxy the message with."); // Fetch members and get the ProxyMember for `target` - List members; + List members; using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime)) members = (await _repo.GetProxyMembers(ctx.Author.Id, msg.Message.Guild!.Value)).ToList(); var match = members.Find(x => x.Id == target.Id); @@ -70,7 +72,7 @@ public class ProxiedMessage try { - await _proxy.ExecuteReproxy(ctx.Message, msg.Message, match); + await _proxy.ExecuteReproxy(ctx.Message, msg.Message, members, match); if (ctx.Guild == null) await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success }); @@ -126,8 +128,7 @@ public class ProxiedMessage if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages)) await _rest.DeleteMessage(ctx.Channel.Id, ctx.Message.Id); - await _logChannel.LogMessage(ctx.MessageContext, msg.Message, ctx.Message, editedMsg, - originalMsg!.Content!); + await _logChannel.LogMessage(msg.Message, ctx.Message, editedMsg, originalMsg!.Content!); } catch (NotFoundException) { @@ -140,13 +141,12 @@ public class ProxiedMessage var editType = isReproxy ? "reproxy" : "edit"; var editTypeAction = isReproxy ? "reproxied" : "edited"; - // todo: is it correct to get a connection here? - await using var conn = await ctx.Database.Obtain(); FullMessage? msg = null; var (referencedMessage, _) = ctx.MatchMessage(false); if (referencedMessage != null) { + await using var conn = await ctx.Database.Obtain(); msg = await ctx.Repository.GetMessage(conn, referencedMessage.Value); if (msg == null) throw new PKError("This is not a message proxied by PluralKit."); @@ -161,6 +161,7 @@ public class ProxiedMessage if (recent == null) throw new PKSyntaxError($"Could not find a recent message to {editType}."); + await using var conn = await ctx.Database.Obtain(); msg = await ctx.Repository.GetMessage(conn, recent.Mid); if (msg == null) throw new PKSyntaxError($"Could not find a recent message to {editType}."); @@ -305,14 +306,14 @@ public class ProxiedMessage private async Task DeleteCommandMessage(Context ctx, ulong messageId) { - var message = await ctx.Repository.GetCommandMessage(messageId); - if (message == null) + var (authorId, channelId) = await ctx.Services.Resolve().GetCommandMessage(messageId); + if (authorId == null) throw Errors.MessageNotFound(messageId); - if (message.AuthorId != ctx.Author.Id) + if (authorId != ctx.Author.Id) throw new PKError("You can only delete command messages queried by this account."); - await ctx.Rest.DeleteMessage(message.ChannelId, message.MessageId); + await ctx.Rest.DeleteMessage(channelId!.Value, messageId); if (ctx.Guild != null) await ctx.Rest.DeleteMessage(ctx.Message); diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index aecc3d4d..25bbdf85 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -127,7 +127,7 @@ public class Misc .Footer(new(String.Join(" \u2022 ", new[] { $"PluralKit {BuildInfoService.Version}", (isCluster ? $"Cluster {_botConfig.Cluster.NodeIndex}" : ""), - "https://github.com/xSke/PluralKit", + "https://github.com/PluralKit/PluralKit", "Last restarted:", }))) .Timestamp(process.StartTime.ToString("O")); diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 5cf8bb46..48ce8ee9 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -146,6 +146,57 @@ public class ServerConfig }); } + public async Task ShowLogDisabledChannels(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + var config = await ctx.Repository.GetGuild(ctx.Guild.Id); + + // Resolve all channels from the cache and order by position + var channels = (await Task.WhenAll(config.LogBlacklist + .Select(id => _cache.TryGetChannel(id)))) + .Where(c => c != null) + .OrderBy(c => c.Position) + .ToList(); + + if (channels.Count == 0) + { + await ctx.Reply("This server has no channels where logging is disabled."); + return; + } + + await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, + $"Channels where logging is disabled for {ctx.Guild.Name}", + null, + async (eb, l) => + { + async Task CategoryName(ulong? id) => + id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)"; + + ulong? lastCategory = null; + + var fieldValue = new StringBuilder(); + foreach (var channel in l) + { + if (lastCategory != channel!.ParentId && fieldValue.Length > 0) + { + eb.Field(new Embed.Field(await CategoryName(lastCategory), fieldValue.ToString())); + fieldValue.Clear(); + } + else + { + fieldValue.Append("\n"); + } + + fieldValue.Append(channel.Mention()); + lastCategory = channel.ParentId; + } + + eb.Field(new Embed.Field(await CategoryName(lastCategory), fieldValue.ToString())); + }); + } + + public async Task SetBlacklisted(Context ctx, bool shouldAdd) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); @@ -180,27 +231,26 @@ public class ServerConfig public async Task SetLogCleanup(Context ctx) { - await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant())); + var eb = new EmbedBuilder() + .Title("Log cleanup settings") + .Field(new Embed.Field("Supported bots", botList)); + + if (ctx.Guild == null) + { + eb.Description("Run this command in a server to enable/disable log cleanup."); + await ctx.Reply(embed: eb.Build()); + return; + } + + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); - bool newValue; - if (ctx.Match("enable", "on", "yes")) - { - newValue = true; - } - else if (ctx.Match("disable", "off", "no")) - { - newValue = false; - } - else - { - var eb = new EmbedBuilder() - .Title("Log cleanup settings") - .Field(new Embed.Field("Supported bots", botList)); + bool? newValue = ctx.MatchToggleOrNull(); + if (newValue == null) + { var guildCfg = await ctx.Repository.GetGuild(ctx.Guild.Id); if (guildCfg.LogCleanupEnabled) eb.Description( @@ -212,9 +262,9 @@ public class ServerConfig return; } - await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogCleanupEnabled = newValue }); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogCleanupEnabled = newValue.Value }); - if (newValue) + if (newValue.Value) await ctx.Reply( $"{Emojis.Success} Log cleanup has been **enabled** for this server. Messages deleted by PluralKit will now be cleaned up from logging channels managed by the following bots:\n- **{botList}**\n\n{Emojis.Note} Make sure PluralKit has the **Manage Messages** permission in the channels in question.\n{Emojis.Note} Also, make sure to blacklist the logging channel itself from the bots in question to prevent conflicts."); else diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 1ecb3ca1..fc06cb9f 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -132,12 +132,16 @@ public class SystemEdit public async Task Color(Context ctx, PKSystem target) { var isOwnSystem = ctx.System?.Id == target.Id; + var matchedRaw = ctx.MatchRaw(); + var matchedClear = await ctx.MatchClear(); - if (!isOwnSystem || !ctx.HasNext(false)) + if (!isOwnSystem || !(ctx.HasNext() || matchedClear)) { if (target.Color == null) await ctx.Reply( "This system does not have a color set." + (isOwnSystem ? " To set one, type `pk;system color `." : "")); + else if (matchedRaw) + await ctx.Reply("```\n#" + target.Color + "\n```"); else await ctx.Reply(embed: new EmbedBuilder() .Title("System color") @@ -151,7 +155,7 @@ public class SystemEdit ctx.CheckSystem().CheckOwnSystem(target); - if (await ctx.MatchClear()) + if (matchedClear) { await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { Color = Partial.Null() }); @@ -273,7 +277,7 @@ public class SystemEdit await ctx.Reply( $"{Emojis.Success} System server tag changed. Member names will now end with {newTag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'."); - if (!ctx.MessageContext.TagEnabled) + if (!settings.TagEnabled) await ctx.Reply(setDisabledWarning); } @@ -284,7 +288,7 @@ public class SystemEdit await ctx.Reply( $"{Emojis.Success} System server tag cleared. Member names will now end with the global system tag, if there is one set."); - if (!ctx.MessageContext.TagEnabled) + if (!settings.TagEnabled) await ctx.Reply(setDisabledWarning); } @@ -293,7 +297,7 @@ public class SystemEdit await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { TagEnabled = newValue }); - await ctx.Reply(PrintEnableDisableResult(newValue, newValue != ctx.MessageContext.TagEnabled)); + await ctx.Reply(PrintEnableDisableResult(newValue, newValue != settings.TagEnabled)); } string PrintEnableDisableResult(bool newValue, bool changedValue) @@ -308,20 +312,20 @@ public class SystemEdit if (newValue) { - if (ctx.MessageContext.TagEnabled) + if (settings.TagEnabled) { - if (ctx.MessageContext.SystemGuildTag == null) + if (settings.Tag == null) str += " However, you do not have a system tag specific to this server. Messages will be proxied using your global system tag, if there is one set."; else str += - $" Your current system tag in '{ctx.Guild.Name}' is {ctx.MessageContext.SystemGuildTag.AsCode()}."; + $" Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}."; } else { - if (ctx.MessageContext.SystemGuildTag != null) + if (settings.Tag != null) str += - $" Member names will now end with the server-specific tag {ctx.MessageContext.SystemGuildTag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'."; + $" Member names will now end with the server-specific tag {settings.Tag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'."; else str += " Member names will now end with the global system tag when proxied in the current server, if there is one set."; @@ -529,8 +533,8 @@ public class SystemEdit await ctx.Reply( $"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{target.Hid}`).\n" - +$"**Note: this action is permanent,** but you will get a copy of your system's data that can be re-imported into PluralKit at a later date sent to you in DMs." - +" If you don't want this to happen, use `pk;s delete -no-export` instead."); + + $"**Note: this action is permanent,** but you will get a copy of your system's data that can be re-imported into PluralKit at a later date sent to you in DMs." + + " If you don't want this to happen, use `pk;s delete -no-export` instead."); if (!await ctx.ConfirmWithReply(target.Hid)) throw new PKError( $"System deletion cancelled. Note that you must reply with your system ID (`{target.Hid}`) *verbatim*."); diff --git a/PluralKit.Bot/Commands/SystemLink.cs b/PluralKit.Bot/Commands/SystemLink.cs index 4fe256d0..593f1ad1 100644 --- a/PluralKit.Bot/Commands/SystemLink.cs +++ b/PluralKit.Bot/Commands/SystemLink.cs @@ -32,7 +32,7 @@ public class SystemLink ulong id; if (!ctx.MatchUserRaw(out id)) - throw new PKSyntaxError("You must pass an account to link with (either ID or @mention)."); + throw new PKSyntaxError("You must pass an account to unlink from (either ID or @mention)."); var accountIds = (await ctx.Repository.GetSystemAccounts(ctx.System.Id)).ToList(); if (!accountIds.Contains(id)) throw Errors.AccountNotLinked; diff --git a/PluralKit.Bot/Handlers/InteractionCreated.cs b/PluralKit.Bot/Handlers/InteractionCreated.cs index 25a9c0f8..43721175 100644 --- a/PluralKit.Bot/Handlers/InteractionCreated.cs +++ b/PluralKit.Bot/Handlers/InteractionCreated.cs @@ -21,11 +21,14 @@ public class InteractionCreated: IEventHandler if (evt.Type == Interaction.InteractionType.MessageComponent) { var customId = evt.Data?.CustomId; - if (customId != null) - { - var ctx = new InteractionContext(evt, _services); + if (customId == null) return; + + var ctx = new InteractionContext(evt, _services); + + if (customId.Contains("help-menu")) + await Help.ButtonClick(ctx); + else await _interactionDispatch.Dispatch(customId, ctx); - } } } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 09a1a8c5..000e05ae 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -63,7 +63,8 @@ public class MessageCreated: IEventHandler if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply) return; if (IsDuplicateMessage(evt)) return; - if (!(await _cache.PermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.SendMessages)) return; + var botPermissions = await _cache.PermissionsIn(evt.ChannelId); + if (!botPermissions.HasFlag(PermissionSet.SendMessages)) return; // spawn off saving the private channel into another thread // it is not a fatal error if this fails, and it shouldn't block message processing @@ -77,36 +78,33 @@ public class MessageCreated: IEventHandler _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived); _lastMessageCache.AddMessage(evt); - // Get message context from DB (tracking w/ metrics) - MessageContext ctx; - using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) - ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel.Id); + // if the message was not sent by an user account, only try running log cleanup + if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true) + { + await TryHandleLogClean(channel, evt); + return; + } // Try each handler until we find one that succeeds - if (await TryHandleLogClean(evt, ctx)) + + if (await TryHandleCommand(shardId, evt, guild, channel)) return; - // Only do command/proxy handling if it's a user account - if (evt.Author.Bot || evt.WebhookId != null || evt.Author.System == true) - return; - - if (await TryHandleCommand(shardId, evt, guild, channel, ctx)) - return; - await TryHandleProxy(evt, guild, channel, ctx); + await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions); } - private async ValueTask TryHandleLogClean(MessageCreateEvent evt, MessageContext ctx) + private async Task TryHandleLogClean(Channel channel, MessageCreateEvent evt) { - var channel = await _cache.GetChannel(evt.ChannelId); - if (!evt.Author.Bot || channel.Type != Channel.ChannelType.GuildText || - !ctx.LogCleanupEnabled) return false; + if (evt.GuildId == null) return; + if (channel.Type != Channel.ChannelType.GuildText) return; - await _loggerClean.HandleLoggerBotCleanup(evt); - return true; + var guildSettings = await _repo.GetGuild(evt.GuildId!.Value); + + if (guildSettings.LogCleanupEnabled) + await _loggerClean.HandleLoggerBotCleanup(evt); } - private async ValueTask TryHandleCommand(int shardId, MessageCreateEvent evt, Guild? guild, - Channel channel, MessageContext ctx) + private async ValueTask TryHandleCommand(int shardId, MessageCreateEvent evt, Guild? guild, Channel channel) { var content = evt.Content; if (content == null) return false; @@ -117,17 +115,6 @@ public class MessageCreated: IEventHandler if (!HasCommandPrefix(content, ourUserId, out var cmdStart) || cmdStart == content.Length) return false; - if (ctx.IsDeleting) - { - await _rest.CreateMessage(evt.ChannelId, new() - { - Content = $"{Emojis.Error} Your system is currently being deleted." - + " Due to database issues, it is not possible to use commands while a system is being deleted. Please wait a few minutes and try again.", - MessageReference = new(guild?.Id, channel.Id, evt.Id) - }); - return true; - } - // Trim leading whitespace from command without actually modifying the string // This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string var trimStartLengthDiff = @@ -136,9 +123,9 @@ public class MessageCreated: IEventHandler try { - var system = ctx.SystemId != null ? await _repo.GetSystem(ctx.SystemId.Value) : null; - var config = ctx.SystemId != null ? await _repo.GetSystemConfig(ctx.SystemId.Value) : null; - await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, ctx)); + var system = await _repo.GetSystemByAccount(evt.Author.Id); + var config = system != null ? await _repo.GetSystemConfig(system.Id) : null; + await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config)); } catch (PKError) { @@ -169,17 +156,16 @@ public class MessageCreated: IEventHandler return false; } - private async ValueTask TryHandleProxy(MessageCreateEvent evt, Guild guild, Channel channel, - MessageContext ctx) + private async ValueTask TryHandleProxy(MessageCreateEvent evt, Guild guild, Channel channel, ulong rootChannel, PermissionSet botPermissions) { - if (ctx.IsDeleting) return false; - - var botPermissions = await _cache.PermissionsIn(channel.Id); + // Get message context from DB (tracking w/ metrics) + MessageContext ctx; + using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime)) + ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel); try { - return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, ctx.AllowAutoproxy, - botPermissions); + return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, true, botPermissions); } // Catch any failed proxy checks so they get ignored in the global error handler diff --git a/PluralKit.Bot/Handlers/ReactionAdded.cs b/PluralKit.Bot/Handlers/ReactionAdded.cs index 482aa39c..155e2bf8 100644 --- a/PluralKit.Bot/Handlers/ReactionAdded.cs +++ b/PluralKit.Bot/Handlers/ReactionAdded.cs @@ -73,10 +73,10 @@ public class ReactionAdded: IEventHandler return; } - var commandMsg = await _commandMessageService.GetCommandMessage(evt.MessageId); - if (commandMsg != null) + var (authorId, _) = await _commandMessageService.GetCommandMessage(evt.MessageId); + if (authorId != null) { - await HandleCommandDeleteReaction(evt, commandMsg); + await HandleCommandDeleteReaction(evt, authorId.Value); return; } } @@ -141,11 +141,11 @@ public class ReactionAdded: IEventHandler await _repo.DeleteMessage(evt.MessageId); } - private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEvent evt, CommandMessage? msg) + private async ValueTask HandleCommandDeleteReaction(MessageReactionAddEvent evt, ulong? authorId) { // Can only delete your own message // (except in DMs, where msg will be null) - if (msg != null && msg.AuthorId != evt.UserId) + if (authorId != null && authorId != evt.UserId) return; // todo: don't try to delete the user's own messages in DMs diff --git a/PluralKit.Bot/Init.cs b/PluralKit.Bot/Init.cs index 57af88a8..20156915 100644 --- a/PluralKit.Bot/Init.cs +++ b/PluralKit.Bot/Init.cs @@ -20,6 +20,10 @@ public class Init { private static async Task Main(string[] args) { + // set cluster config from Nomad node index env variable + if (Environment.GetEnvironmentVariable("NOMAD_ALLOC_INDEX") is { } nodeIndex) + Environment.SetEnvironmentVariable("PluralKit__Bot__Cluster__NodeName", $"pluralkit-{nodeIndex}"); + // Load configuration and run global init stuff var config = InitUtils.BuildConfiguration(args).Build(); InitUtils.InitStatic(); diff --git a/PluralKit.Bot/PluralKit.Bot.csproj b/PluralKit.Bot/PluralKit.Bot.csproj index 4680475f..71bb0f22 100644 --- a/PluralKit.Bot/PluralKit.Bot.csproj +++ b/PluralKit.Bot/PluralKit.Bot.csproj @@ -24,7 +24,7 @@ - + diff --git a/PluralKit.Bot/PluralKit.Bot.csproj.DotSettings b/PluralKit.Bot/PluralKit.Bot.csproj.DotSettings deleted file mode 100644 index 466ae44c..00000000 --- a/PluralKit.Bot/PluralKit.Bot.csproj.DotSettings +++ /dev/null @@ -1,6 +0,0 @@ - - True - True - True - True - True \ No newline at end of file diff --git a/PluralKit.Bot/Proxy/ProxyMatcher.cs b/PluralKit.Bot/Proxy/ProxyMatcher.cs index 9c4a5b68..87cbf329 100644 --- a/PluralKit.Bot/Proxy/ProxyMatcher.cs +++ b/PluralKit.Bot/Proxy/ProxyMatcher.cs @@ -43,6 +43,10 @@ public class ProxyMatcher { match = default; + if (!ctx.AllowAutoproxy) + throw new ProxyService.ProxyChecksFailedException( + "Autoproxy is disabled for your account. Type `pk;cfg autoproxy account enable` to re-enable it."); + // Skip autoproxy match if we hit the escape character if (messageContent.StartsWith(AutoproxyEscapeCharacter)) throw new ProxyService.ProxyChecksFailedException( diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index aeaf6e87..e1a6c51d 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -198,7 +198,7 @@ public class ProxyService await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match); } - public async Task ExecuteReproxy(Message trigger, PKMessage msg, ProxyMember member) + public async Task ExecuteReproxy(Message trigger, PKMessage msg, List members, ProxyMember member) { var originalMsg = await _rest.GetMessageOrNull(msg.Channel, msg.Mid); if (originalMsg == null) @@ -213,24 +213,34 @@ public class ProxyService throw new ProxyChecksFailedException( "Proxying was disabled in this channel by a server administrator (via the proxy blacklist)."); + var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, msg.Guild!.Value, null); + var prevMatched = _matcher.TryMatch(ctx, autoproxySettings, members, out var prevMatch, originalMsg.Content, + originalMsg.Attachments.Length > 0, false); + var match = new ProxyMatch { Member = member, + Content = prevMatched ? prevMatch.Content : originalMsg.Content, + ProxyTags = member.ProxyTags.FirstOrDefault(), }; var messageChannel = await _rest.GetChannelOrNull(msg.Channel!); - var rootChannel = await _rest.GetChannelOrNull(messageChannel.IsThread() ? messageChannel.ParentId!.Value : messageChannel.Id); + var rootChannel = messageChannel.IsThread() ? await _rest.GetChannelOrNull(messageChannel.ParentId!.Value) : messageChannel; var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null; var guild = await _rest.GetGuildOrNull(msg.Guild!.Value); + var guildMember = await _rest.GetGuildMember(msg.Guild!.Value, trigger.Author.Id); // Grab user permissions - var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, trigger.Author.Id, null); + var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, trigger.Author.Id, guildMember); var allowEveryone = senderPermissions.HasFlag(PermissionSet.MentionEveryone); // Make sure user has permissions to send messages if (!senderPermissions.HasFlag(PermissionSet.SendMessages)) throw new PKError("You don't have permission to send messages in the channel that message is in."); + // Mangle embeds (for reply embed color changing) + var mangledEmbeds = originalMsg.Embeds!.Select(embed => MangleReproxyEmbed(embed, member)).Where(embed => embed != null).ToArray(); + // Send the reproxied webhook var proxyMessage = await _webhookExecutor.ExecuteWebhook(new ProxyRequest { @@ -239,15 +249,15 @@ public class ProxyService ThreadId = threadId, Name = match.Member.ProxyName(ctx), AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)), - Content = originalMsg.Content!, + Content = match.ProxyContent!, Attachments = originalMsg.Attachments!, FileSizeLimit = guild.FileSizeLimit(), - Embeds = originalMsg.Embeds!.ToArray(), + Embeds = mangledEmbeds, Stickers = originalMsg.StickerItems!, AllowEveryone = allowEveryone }); - var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, msg.Guild!.Value, null); + await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match, deletePrevious: false); await _rest.DeleteMessage(originalMsg.ChannelId!, originalMsg.Id!); } @@ -271,6 +281,34 @@ public class ProxyService } } + private Embed? MangleReproxyEmbed(Embed embed, ProxyMember member) + { + // XXX: This is a naïve implementation of detecting reply embeds: looking for the same Unicode + // characters as used in the reply embed generation, since we don't _really_ have a good way + // to detect whether an embed is a PluralKit reply embed right now, whether a message is in + // reply to another message isn't currently stored anywhere in the database. + // + // unicodes: [three-per-em space] [left arrow emoji] [force emoji presentation] + if (embed.Author != null && embed.Author!.Name.EndsWith("\u2004\u21a9\ufe0f")) + { + return new Embed + { + Type = "rich", + Author = embed.Author!, + Description = embed.Description!, + Color = member.Color?.ToDiscordColor() + }; + } + + // XXX: remove non-rich embeds as including them breaks link embeds completely + else if (embed.Type != "rich") + { + return null; + } + + return embed; + } + private Embed CreateReplyEmbed(ProxyMatch match, Message trigger, Message repliedTo, string? nickname, string? avatar) { @@ -377,8 +415,8 @@ public class ProxyService { var sentMessage = new PKMessage { - Channel = triggerMessage.ChannelId, - Guild = triggerMessage.GuildId, + Channel = proxyMessage.ChannelId, + Guild = proxyMessage.GuildId, Member = match.Member.Id, Mid = proxyMessage.Id, OriginalMid = triggerMessage.Id, @@ -389,7 +427,7 @@ public class ProxyService => _repo.AddMessage(sentMessage); Task LogMessageToChannel() => - _logChannel.LogMessage(ctx, sentMessage, triggerMessage, proxyMessage).AsTask(); + _logChannel.LogMessage(sentMessage, triggerMessage, proxyMessage).AsTask(); Task SaveLatchAutoproxy() => autoproxySettings.AutoproxyMode == AutoproxyMode.Latch ? _repo.UpdateAutoproxy(ctx.SystemId.Value, triggerMessage.GuildId, null, new() diff --git a/PluralKit.Bot/Proxy/ProxyTagParser.cs b/PluralKit.Bot/Proxy/ProxyTagParser.cs index d13dd9f5..2c748f9c 100644 --- a/PluralKit.Bot/Proxy/ProxyTagParser.cs +++ b/PluralKit.Bot/Proxy/ProxyTagParser.cs @@ -78,7 +78,7 @@ public class ProxyTagParser // We got a match, extract inner text inner = input.Substring(prefix.Length, input.Length - prefix.Length - suffix.Length); - // (see https://github.com/xSke/PluralKit/pull/181) + // (see https://github.com/PluralKit/PluralKit/pull/181) return inner.Trim() != "\U0000fe0f"; } diff --git a/PluralKit.Bot/Services/CommandMessageService.cs b/PluralKit.Bot/Services/CommandMessageService.cs index ee097d2a..5ef84ad1 100644 --- a/PluralKit.Bot/Services/CommandMessageService.cs +++ b/PluralKit.Bot/Services/CommandMessageService.cs @@ -8,28 +8,36 @@ namespace PluralKit.Bot; public class CommandMessageService { - private readonly IClock _clock; - private readonly IDatabase _db; + private readonly RedisService _redis; private readonly ILogger _logger; - private readonly ModelRepository _repo; + private static readonly TimeSpan CommandMessageRetention = TimeSpan.FromHours(24); - public CommandMessageService(IDatabase db, ModelRepository repo, IClock clock, ILogger logger) + public CommandMessageService(RedisService redis, IClock clock, ILogger logger) { - _db = db; - _repo = repo; - _clock = clock; + _redis = redis; _logger = logger.ForContext(); } public async Task RegisterMessage(ulong messageId, ulong channelId, ulong authorId) { + if (_redis.Connection == null) return; + _logger.Debug( "Registering command response {MessageId} from author {AuthorId} in {ChannelId}", messageId, authorId, channelId ); - await _repo.SaveCommandMessage(messageId, channelId, authorId); + + await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}", expiry: CommandMessageRetention); } - public async Task GetCommandMessage(ulong messageId) => - await _repo.GetCommandMessage(messageId); + public async Task<(ulong?, ulong?)> GetCommandMessage(ulong messageId) + { + var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString()); + if (str.HasValue) + { + var split = ((string)str).Split("-"); + return (ulong.Parse(split[0]), ulong.Parse(split[1])); + } + return (null, null); + } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/EmbedService.cs b/PluralKit.Bot/Services/EmbedService.cs index 65167f26..fb671565 100644 --- a/PluralKit.Bot/Services/EmbedService.cs +++ b/PluralKit.Bot/Services/EmbedService.cs @@ -72,7 +72,8 @@ public class EmbedService .Thumbnail(new Embed.EmbedThumbnail(system.AvatarUrl.TryGetCleanCdnUrl())) .Footer(new Embed.EmbedFooter( $"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}")) - .Color(color); + .Color(color) + .Url($"https://dash.pluralkit.me/profile/s/{system.Hid}"); if (system.DescriptionPrivacy.CanAccess(ctx)) eb.Image(new Embed.EmbedImage(system.BannerImage)); @@ -143,6 +144,9 @@ public class EmbedService $"System ID: {systemHid} | Member ID: {member.Hid} | Sender: {triggerMessage.Author.Username}#{triggerMessage.Author.Discriminator} ({triggerMessage.Author.Id}) | Message ID: {proxiedMessage.Id} | Original Message ID: {triggerMessage.Id}")) .Timestamp(timestamp.ToDateTimeOffset().ToString("O")); + if (oldContent == "") + oldContent = "*no message content*"; + if (oldContent != null) embed.Field(new Embed.Field("Old message", oldContent?.NormalizeLineEndSpacing().Truncate(1000))); @@ -179,8 +183,7 @@ public class EmbedService .ToListAsync(); var eb = new EmbedBuilder() - // TODO: add URL of website when that's up - .Author(new Embed.EmbedAuthor(name, IconUrl: avatar.TryGetCleanCdnUrl())) + .Author(new Embed.EmbedAuthor(name, IconUrl: avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}")) // .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray) .Color(color) .Footer(new Embed.EmbedFooter( @@ -264,7 +267,7 @@ public class EmbedService } var eb = new EmbedBuilder() - .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx))) + .Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}")) .Color(color); eb.Footer(new Embed.EmbedFooter($"System ID: {system.Hid} | Group ID: {target.Hid}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}")); diff --git a/PluralKit.Bot/Services/LogChannelService.cs b/PluralKit.Bot/Services/LogChannelService.cs index 3257dae1..35eace35 100644 --- a/PluralKit.Bot/Services/LogChannelService.cs +++ b/PluralKit.Bot/Services/LogChannelService.cs @@ -34,17 +34,16 @@ public class LogChannelService _logger = logger.ForContext(); } - public async ValueTask LogMessage(MessageContext ctx, PKMessage proxiedMessage, Message trigger, - Message hookMessage, string oldContent = null) + public async ValueTask LogMessage(PKMessage proxiedMessage, Message trigger, Message hookMessage, string oldContent = null) { - var logChannelId = await GetAndCheckLogChannel(ctx, trigger, proxiedMessage); + var logChannelId = await GetAndCheckLogChannel(trigger, proxiedMessage); if (logChannelId == null) return; var triggerChannel = await _cache.GetChannel(proxiedMessage.Channel); - var system = await _repo.GetSystem(ctx.SystemId.Value); var member = await _repo.GetMember(proxiedMessage.Member!.Value); + var system = await _repo.GetSystem(member.System); // Send embed! var embed = _embed.CreateLoggedMessageEmbed(trigger, hookMessage, system.Hid, member, triggerChannel.Name, @@ -54,8 +53,7 @@ public class LogChannelService await _rest.CreateMessage(logChannelId.Value, new MessageRequest { Content = url, Embeds = new[] { embed } }); } - private async Task GetAndCheckLogChannel(MessageContext ctx, Message trigger, - PKMessage proxiedMessage) + private async Task GetAndCheckLogChannel(Message trigger, PKMessage proxiedMessage) { if (proxiedMessage.Guild == null && proxiedMessage.Channel != trigger.ChannelId) // a very old message is being edited outside of its original channel @@ -63,18 +61,15 @@ public class LogChannelService return null; var guildId = proxiedMessage.Guild ?? trigger.GuildId.Value; - var logChannelId = ctx.LogChannel; - var isBlacklisted = ctx.InLogBlacklist; - if (proxiedMessage.Guild != trigger.GuildId) - { - // we're editing a message from a different server, get log channel info from the database - var guild = await _repo.GetGuild(proxiedMessage.Guild.Value); - logChannelId = guild.LogChannel; - isBlacklisted = guild.LogBlacklist.Any(x => x == trigger.ChannelId); - } + // get log channel info from the database + var guild = await _repo.GetGuild(guildId); + var logChannelId = guild.LogChannel; + var isBlacklisted = guild.LogBlacklist.Any(x => x == trigger.ChannelId); - if (ctx.SystemId == null || logChannelId == null || isBlacklisted) return null; + // if (ctx.SystemId == null || + // removed the above, there shouldn't be a way to get to this code path if you don't have a system registered + if (logChannelId == null || isBlacklisted) return null; // Find log channel and check if valid var logChannel = await FindLogChannel(guildId, logChannelId.Value); @@ -86,7 +81,7 @@ public class LogChannelService { _logger.Information( "Does not have permission to log proxy, ignoring (channel: {ChannelId}, guild: {GuildId}, bot permissions: {BotPermissions})", - logChannel.Id, trigger.GuildId!.Value, perms); + logChannel.Id, guildId, perms); return null; } diff --git a/PluralKit.Bot/Services/PrivateChannelService.cs b/PluralKit.Bot/Services/PrivateChannelService.cs index e5b4fa3c..dc1ea03a 100644 --- a/PluralKit.Bot/Services/PrivateChannelService.cs +++ b/PluralKit.Bot/Services/PrivateChannelService.cs @@ -1,3 +1,5 @@ +using App.Metrics; + using Serilog; using Myriad.Gateway; @@ -9,13 +11,13 @@ namespace PluralKit.Bot; public class PrivateChannelService { - private static readonly Dictionary _channelsCache = new(); - + private readonly IMetrics _metrics; private readonly ILogger _logger; private readonly ModelRepository _repo; private readonly DiscordApiClient _rest; - public PrivateChannelService(ILogger logger, ModelRepository repo, DiscordApiClient rest) + public PrivateChannelService(IMetrics metrics, ILogger logger, ModelRepository repo, DiscordApiClient rest) { + _metrics = metrics; _logger = logger; _repo = repo; _rest = rest; @@ -23,35 +25,32 @@ public class PrivateChannelService public async Task TrySavePrivateChannel(MessageCreateEvent evt) { - if (evt.GuildId != null) return; - if (_channelsCache.TryGetValue(evt.Author.Id, out _)) return; - - await SaveDmChannel(evt.Author.Id, evt.ChannelId); + if (evt.GuildId == null) await SaveDmChannel(evt.Author.Id, evt.ChannelId); } public async Task GetOrCreateDmChannel(ulong userId) { - if (_channelsCache.TryGetValue(userId, out var cachedChannelId)) - return cachedChannelId; - var channelId = await _repo.GetDmChannel(userId); - if (channelId == null) + if (channelId != null) { - var channel = await _rest.CreateDm(userId); - channelId = channel.Id; + _metrics.Measure.Meter.Mark(BotMetrics.DatabaseDMCacheHits); + return channelId.Value; } - // spawn off saving the channel as to not block the current thread - _ = SaveDmChannel(userId, channelId.Value); + _metrics.Measure.Meter.Mark(BotMetrics.DMCacheMisses); - return channelId.Value; + var channel = await _rest.CreateDm(userId); + + // spawn off saving the channel as to not block the current thread + _ = SaveDmChannel(userId, channel.Id); + + return channel.Id; } private async Task SaveDmChannel(ulong userId, ulong channelId) { try { - _channelsCache.Add(userId, channelId); await _repo.UpdateAccount(userId, new() { DmChannel = channelId }); } catch (Exception e) diff --git a/PluralKit.Bot/Services/RedisGatewayService.cs b/PluralKit.Bot/Services/RedisGatewayService.cs index 8671dc9c..72c7193d 100644 --- a/PluralKit.Bot/Services/RedisGatewayService.cs +++ b/PluralKit.Bot/Services/RedisGatewayService.cs @@ -31,7 +31,7 @@ public class RedisGatewayService _redis = await ConnectionMultiplexer.ConnectAsync(_config.RedisGatewayUrl); _logger.Debug("Subscribing to shard {ShardId} on redis", shardId); - + var channel = await _redis.GetSubscriber().SubscribeAsync($"evt-{shardId}"); channel.OnMessage((evt) => Handle(shardId, evt)); } diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index c9596545..e7aec9bd 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -174,7 +174,8 @@ public class WebhookExecutorService // We don't care about whether the sending succeeds, and we don't want to *wait* for it, so we just fork it off var _ = TrySendRemainingAttachments(webhook, req.Name, req.AvatarUrl, attachmentChunks, req.ThreadId); - return webhookMessage; + // for some reason discord may(?) return a null guildid here??? + return webhookMessage with { GuildId = webhookMessage.GuildId ?? req.GuildId }; } private async Task TrySendRemainingAttachments(Webhook webhook, string name, string avatarUrl, diff --git a/PluralKit.Bot/Utils/InteractionContext.cs b/PluralKit.Bot/Utils/InteractionContext.cs index 47929df8..82e3e5a8 100644 --- a/PluralKit.Bot/Utils/InteractionContext.cs +++ b/PluralKit.Bot/Utils/InteractionContext.cs @@ -36,7 +36,7 @@ public class InteractionContext await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage, new InteractionApplicationCommandCallbackData { - // Components = _evt.Message.Components + Components = Event.Message.Components }); } diff --git a/PluralKit.Bot/Utils/SentryUtils.cs b/PluralKit.Bot/Utils/SentryUtils.cs index 97471821..9e715387 100644 --- a/PluralKit.Bot/Utils/SentryUtils.cs +++ b/PluralKit.Bot/Utils/SentryUtils.cs @@ -17,10 +17,12 @@ public class SentryEnricher: ISentryEnricher { private readonly Bot _bot; + private readonly BotConfig _config; - public SentryEnricher(Bot bot) + public SentryEnricher(Bot bot, BotConfig config) { _bot = bot; + _config = config; } // TODO: should this class take the Scope by dependency injection instead? @@ -37,6 +39,8 @@ public class SentryEnricher: {"message", evt.Id.ToString()} }); scope.SetTag("shard", shardId.ToString()); + if (_config.Cluster != null) + scope.SetTag("cluster", _config.Cluster!.NodeIndex.ToString()); // Also report information about the bot's permissions in the channel // We get a lot of permission errors so this'll be useful for determining problems @@ -56,6 +60,8 @@ public class SentryEnricher: {"messages", string.Join(",", evt.Ids)} }); scope.SetTag("shard", shardId.ToString()); + if (_config.Cluster != null) + scope.SetTag("cluster", _config.Cluster!.NodeIndex.ToString()); } public void Enrich(Scope scope, int shardId, MessageUpdateEvent evt) @@ -68,6 +74,8 @@ public class SentryEnricher: {"message", evt.Id.ToString()} }); scope.SetTag("shard", shardId.ToString()); + if (_config.Cluster != null) + scope.SetTag("cluster", _config.Cluster!.NodeIndex.ToString()); } public void Enrich(Scope scope, int shardId, MessageDeleteEvent evt) @@ -80,6 +88,8 @@ public class SentryEnricher: {"message", evt.Id.ToString()} }); scope.SetTag("shard", shardId.ToString()); + if (_config.Cluster != null) + scope.SetTag("cluster", _config.Cluster!.NodeIndex.ToString()); } public void Enrich(Scope scope, int shardId, MessageReactionAddEvent evt) @@ -94,5 +104,7 @@ public class SentryEnricher: {"reaction", evt.Emoji.Name} }); scope.SetTag("shard", shardId.ToString()); + if (_config.Cluster != null) + scope.SetTag("cluster", _config.Cluster!.NodeIndex.ToString()); } } \ No newline at end of file diff --git a/PluralKit.Bot/packages.lock.json b/PluralKit.Bot/packages.lock.json index 33b4bd77..87383ffd 100644 --- a/PluralKit.Bot/packages.lock.json +++ b/PluralKit.Bot/packages.lock.json @@ -24,9 +24,9 @@ }, "Grpc.Tools": { "type": "Direct", - "requested": "[2.37.0, )", - "resolved": "2.37.0", - "contentHash": "cud/urkbw3QoQ8+kNeCy2YI0sHrh7td/1cZkVbH6hDLIXX7zzmJbV/KjYSiqiYtflQf+S5mJPLzDQWScN/QdDg==" + "requested": "[2.47.0, )", + "resolved": "2.47.0", + "contentHash": "nInNoLfT/zR7+0VNIC4Lu5nF8azjTz3KwHB1ckwsYUxvof4uSxIt/LlCKb/NH7GPfXfdvqDDinguPpP5t55nuA==" }, "Humanizer.Core": { "type": "Direct", diff --git a/PluralKit.Core/CoreConfig.cs b/PluralKit.Core/CoreConfig.cs index 5c2a11f0..a4f06c7a 100644 --- a/PluralKit.Core/CoreConfig.cs +++ b/PluralKit.Core/CoreConfig.cs @@ -5,6 +5,7 @@ namespace PluralKit.Core; public class CoreConfig { public string Database { get; set; } + public string? DatabasePassword { get; set; } public string RedisAddr { get; set; } public bool UseRedisMetrics { get; set; } = false; public string SentryUrl { get; set; } diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index 8d3547eb..e5e1b0d0 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -35,7 +35,7 @@ internal partial class Database: IDatabase _migrator = migrator; _logger = logger.ForContext(); - _connectionString = new NpgsqlConnectionStringBuilder(_config.Database) + var connectionString = new NpgsqlConnectionStringBuilder(_config.Database) { Pooling = true, Enlist = false, @@ -43,7 +43,12 @@ internal partial class Database: IDatabase // Lower timeout than default (15s -> 2s), should ideally fail-fast instead of hanging Timeout = 2 - }.ConnectionString; + }; + + if (_config.DatabasePassword != null) + connectionString.Password = _config.DatabasePassword; + + _connectionString = connectionString.ConnectionString; } private static readonly PostgresCompiler _compiler = new(); diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index a03cb9da..70db64f6 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -14,7 +14,6 @@ public class MessageContext /// /// Whether a system is being deleted (no actions should be taken, or commands ran) /// - public bool IsDeleting { get; } public ulong? LogChannel { get; } public bool InBlacklist { get; } public bool InLogBlacklist { get; } diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index bebfd7b3..80445c64 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -1,7 +1,6 @@ create function message_context(account_id bigint, guild_id bigint, channel_id bigint) returns table ( system_id int, - is_deleting bool, log_channel bigint, in_blacklist bool, in_log_blacklist bool, @@ -28,7 +27,6 @@ as $$ guild as (select * from servers where id = guild_id) select system.id as system_id, - system.is_deleting, guild.log_channel, (channel_id = any (guild.blacklist)) as in_blacklist, (channel_id = any (guild.log_blacklist)) as in_log_blacklist, diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Account.cs b/PluralKit.Core/Database/Repository/ModelRepository.Account.cs index d8676cf5..241a4406 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Account.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Account.cs @@ -9,6 +9,9 @@ public partial class ModelRepository public async Task GetDmChannel(ulong id) => await _db.Execute(c => c.QueryFirstOrDefaultAsync("select dm_channel from accounts where uid = @id", new { id = id })); + public async Task GetAutoproxyEnabled(ulong id) + => await _db.QueryFirst(new Query("accounts").Select("allow_autoproxy").Where("uid", id)); + public async Task UpdateAccount(ulong id, AccountPatch patch) { _logger.Information("Updated account {accountId}: {@AccountPatch}", id, patch); diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs b/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs index c0ad0f3a..5e153176 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Autoproxy.cs @@ -6,7 +6,7 @@ namespace PluralKit.Core; public partial class ModelRepository { - public async Task UpdateAutoproxy(SystemId system, ulong? guildId, ulong? channelId, AutoproxyPatch patch) + public Task UpdateAutoproxy(SystemId system, ulong? guildId, ulong? channelId, AutoproxyPatch patch) { var locationStr = guildId != null ? "guild" : (channelId != null ? "channel" : "global"); _logger.Information("Updated autoproxy for {SystemId} in location {location}: {@AutoproxyPatch}", system, locationStr, patch); @@ -17,7 +17,7 @@ public partial class ModelRepository .Where("channel_id", channelId ?? 0) ); _ = _dispatch.Dispatch(system, guildId, channelId, patch); - await _db.ExecuteQuery(query); + return _db.QueryFirst(query, "returning *"); } // todo: this might break with differently scoped autoproxy diff --git a/PluralKit.Core/Database/Repository/ModelRepository.CommandMessage.cs b/PluralKit.Core/Database/Repository/ModelRepository.CommandMessage.cs deleted file mode 100644 index c5072d9d..00000000 --- a/PluralKit.Core/Database/Repository/ModelRepository.CommandMessage.cs +++ /dev/null @@ -1,36 +0,0 @@ -using SqlKata; - -namespace PluralKit.Core; - -public partial class ModelRepository -{ - public Task SaveCommandMessage(ulong messageId, ulong channelId, ulong authorId) - { - var query = new Query("command_messages").AsInsert(new - { - message_id = messageId, - channel_id = channelId, - author_id = authorId, - }); - return _db.ExecuteQuery(query); - } - - public Task GetCommandMessage(ulong messageId) - { - var query = new Query("command_messages").Where("message_id", messageId); - return _db.QueryFirst(query); - } - - public Task DeleteCommandMessagesBefore(ulong messageIdThreshold) - { - var query = new Query("command_messages").AsDelete().Where("message_id", "<", messageIdThreshold); - return _db.QueryFirst(query); - } -} - -public class CommandMessage -{ - public ulong AuthorId { get; set; } - public ulong MessageId { get; set; } - public ulong ChannelId { get; set; } -} \ No newline at end of file diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Stats.cs b/PluralKit.Core/Database/Repository/ModelRepository.Stats.cs index 6c08781a..6eaae8e4 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Stats.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Stats.cs @@ -4,20 +4,6 @@ namespace PluralKit.Core; public partial class ModelRepository { - public async Task UpdateStats() - { - await _db.Execute(conn => - conn.ExecuteAsync("update info set system_count = (select count(*) from systems)")); - await _db.Execute(conn => - conn.ExecuteAsync("update info set member_count = (select count(*) from members)")); - await _db.Execute(conn => - conn.ExecuteAsync("update info set group_count = (select count(*) from groups)")); - await _db.Execute(conn => - conn.ExecuteAsync("update info set switch_count = (select count(*) from switches)")); - // await _db.Execute(conn => - // conn.ExecuteAsync("update info set message_count = (select count(*) from messages)")); - } - public Task GetStats() => _db.Execute(conn => conn.QuerySingleAsync("select * from info")); diff --git a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs index 4b3988bd..11d5592b 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.Switch.cs @@ -92,11 +92,11 @@ public partial class ModelRepository _logger.Information("Updated {SwitchId} members: {Members}", switchId, members); } - public async Task MoveSwitch(SwitchId id, Instant time) + public async Task MoveSwitch(SwitchId id, Instant time) { _logger.Information("Updated {SwitchId} timestamp: {SwitchTimestamp}", id, time); var query = new Query("switches").AsUpdate(new { timestamp = time }).Where("id", id); - await _db.ExecuteQuery(query); + var ret = await _db.QueryFirst(query, extraSql: "returning *"); _ = _dispatch.Dispatch(id, new UpdateDispatchData { Event = DispatchEvent.UPDATE_SWITCH, @@ -105,6 +105,7 @@ public partial class ModelRepository timestamp = time.FormatExport(), }), }); + return ret; } public async Task DeleteSwitch(SwitchId id) diff --git a/PluralKit.Core/Database/Repository/ModelRepository.System.cs b/PluralKit.Core/Database/Repository/ModelRepository.System.cs index 96e554ea..50f26b6e 100644 --- a/PluralKit.Core/Database/Repository/ModelRepository.System.cs +++ b/PluralKit.Core/Database/Repository/ModelRepository.System.cs @@ -144,7 +144,6 @@ public partial class ModelRepository public async Task DeleteSystem(SystemId id) { - await _db.Execute(c => c.QueryAsync("update systems set is_deleting = true where id = @id", new { id = id })); var query = new Query("systems").AsDelete().Where("id", id); await _db.ExecuteQuery(query); _logger.Information("Deleted {SystemId}", id); diff --git a/PluralKit.Core/Models/Autoproxy.cs b/PluralKit.Core/Models/Autoproxy.cs index a8df8078..53756837 100644 --- a/PluralKit.Core/Models/Autoproxy.cs +++ b/PluralKit.Core/Models/Autoproxy.cs @@ -16,7 +16,7 @@ public class AutoproxySettings { public AutoproxyMode AutoproxyMode { get; } public MemberId? AutoproxyMember { get; } - public Instant LastLatchTimestamp { get; } + public Instant? LastLatchTimestamp { get; } } public static class AutoproxyExt @@ -27,7 +27,8 @@ public static class AutoproxyExt // tbd o.Add("autoproxy_mode", settings.AutoproxyMode.ToString().ToLower()); - o.Add("autoproxy_member", memberHid); + o.Add("autoproxy_member", settings.AutoproxyMode == AutoproxyMode.Front ? null : memberHid); + o.Add("last_latch_timestamp", settings.LastLatchTimestamp?.FormatExport()); return o; } diff --git a/PluralKit.Core/Models/Patch/AutoproxyPatch.cs b/PluralKit.Core/Models/Patch/AutoproxyPatch.cs index 0f42bc5a..66d3c35c 100644 --- a/PluralKit.Core/Models/Patch/AutoproxyPatch.cs +++ b/PluralKit.Core/Models/Patch/AutoproxyPatch.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json.Linq; + using NodaTime; using SqlKata; @@ -16,4 +18,30 @@ public class AutoproxyPatch: PatchObject .With("autoproxy_member", AutoproxyMember) .With("last_latch_timestamp", LastLatchTimestamp) ); + + public new void AssertIsValid() + { + // this is checked in FromJson + // not really the best way to do this, maybe fix at some point? + if ((int?)AutoproxyMode.Value == -1) + Errors.Add(new("autoproxy_mode")); + } + + public static AutoproxyPatch FromJson(JObject o, MemberId? autoproxyMember = null) + { + var p = new AutoproxyPatch(); + + if (o.ContainsKey("autoproxy_mode")) + { + var (autoproxyMode, error) = o.Value("autoproxy_mode").ParseAutoproxyMode(); + if (error != null) + p.AutoproxyMode = Partial.Present((AutoproxyMode)(-1)); + else + p.AutoproxyMode = autoproxyMode.Value; + } + + p.AutoproxyMember = autoproxyMember ?? Partial.Absent; + + return p; + } } \ No newline at end of file diff --git a/PluralKit.Core/PluralKit.Core.csproj.DotSettings b/PluralKit.Core/PluralKit.Core.csproj.DotSettings deleted file mode 100644 index 33bc593a..00000000 --- a/PluralKit.Core/PluralKit.Core.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/PluralKit.Core/Utils/BulkImporter/PluralKitImport.cs b/PluralKit.Core/Utils/BulkImporter/PluralKitImport.cs index 2c9304e8..816dd618 100644 --- a/PluralKit.Core/Utils/BulkImporter/PluralKitImport.cs +++ b/PluralKit.Core/Utils/BulkImporter/PluralKitImport.cs @@ -30,23 +30,23 @@ public partial class BulkImporter await _repo.UpdateSystem(_system.Id, patch, _conn); - var configPatch = new SystemConfigPatch(); - if (importFile.ContainsKey("config")) - configPatch = SystemConfigPatch.FromJson(importFile.Value("config")); + { + var configPatch = SystemConfigPatch.FromJson(importFile.Value("config")); - if (importFile.ContainsKey("timezone")) - configPatch.UiTz = importFile.Value("timezone"); + if (importFile.ContainsKey("timezone")) + configPatch.UiTz = importFile.Value("timezone"); - configPatch.AssertIsValid(); - if (configPatch.Errors.Count > 0) - throw new ImportException($"Field config.{patch.Errors[0].Key} in export file is invalid."); + configPatch.AssertIsValid(); + if (configPatch.Errors.Count > 0) + throw new ImportException($"Field config.{patch.Errors[0].Key} in export file is invalid."); - await _repo.UpdateSystemConfig(_system.Id, configPatch, _conn); + await _repo.UpdateSystemConfig(_system.Id, configPatch, _conn); + } var members = importFile.Value("members"); - var groups = importFile.Value("groups"); var switches = importFile.Value("switches"); + var groups = importFile.Value("groups"); var newMembers = members.Count(m => { diff --git a/PluralKit.ScheduledTasks/Metrics.cs b/PluralKit.ScheduledTasks/Metrics.cs deleted file mode 100644 index d6111e20..00000000 --- a/PluralKit.ScheduledTasks/Metrics.cs +++ /dev/null @@ -1,25 +0,0 @@ -using App.Metrics; -using App.Metrics.Gauge; - -public static class Metrics -{ - public static GaugeOptions Guilds => new() - { - Name = "Guilds", - MeasurementUnit = Unit.None, - Context = "Bot" - }; - public static GaugeOptions Channels => new() - { - Name = "Channels", - MeasurementUnit = Unit.None, - Context = "Bot" - }; - - public static GaugeOptions WebhookCacheSize => new() - { - Name = "Webhook Cache Size", - Context = "Bot", - MeasurementUnit = Unit.Items - }; -} \ No newline at end of file diff --git a/PluralKit.ScheduledTasks/PluralKit.ScheduledTasks.csproj b/PluralKit.ScheduledTasks/PluralKit.ScheduledTasks.csproj deleted file mode 100644 index e9024230..00000000 --- a/PluralKit.ScheduledTasks/PluralKit.ScheduledTasks.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net6.0 - - - - - - - - true - - diff --git a/PluralKit.ScheduledTasks/Startup.cs b/PluralKit.ScheduledTasks/Startup.cs deleted file mode 100644 index 3c51e3ae..00000000 --- a/PluralKit.ScheduledTasks/Startup.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; - -using Autofac; - -using Microsoft.Extensions.Configuration; - -using PluralKit.Core; - -namespace PluralKit.ScheduledTasks; - -internal class Startup -{ - private static async Task Main(string[] args) - { - // Load configuration and run global init stuff - var config = InitUtils.BuildConfiguration(args).Build(); - InitUtils.InitStatic(); - - await BuildInfoService.LoadVersion(); - - var services = BuildContainer(config); - - var cfg = services.Resolve(); - if (cfg.UseRedisMetrics) - await services.Resolve().InitAsync(cfg); - - services.Resolve().Run(); - - await Task.Delay(-1); - } - - private static IContainer BuildContainer(IConfiguration config) - { - var builder = new ContainerBuilder(); - - builder.RegisterInstance(config); - builder.RegisterModule(new ConfigModule()); - builder.RegisterModule(new LoggingModule("ScheduledTasks")); - builder.RegisterModule(new MetricsModule()); - builder.RegisterModule(); - builder.RegisterType().AsSelf().SingleInstance(); - - return builder.Build(); - } -} \ No newline at end of file diff --git a/PluralKit.ScheduledTasks/TaskHandler.cs b/PluralKit.ScheduledTasks/TaskHandler.cs deleted file mode 100644 index 2fe44670..00000000 --- a/PluralKit.ScheduledTasks/TaskHandler.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Linq; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -using App.Metrics; - -using NodaTime; -using NodaTime.Extensions; - -using Newtonsoft.Json; - -using PluralKit.Core; - -using Serilog; - -namespace PluralKit.ScheduledTasks; - -public class TaskHandler -{ - private static readonly Duration CommandMessageRetention = Duration.FromHours(24); - private readonly IDatabase _db; - private readonly RedisService _redis; - private readonly bool _useRedisMetrics; - - private readonly ILogger _logger; - private readonly IMetrics _metrics; - private readonly ModelRepository _repo; - private Timer _periodicTask; - - public TaskHandler(ILogger logger, IMetrics metrics, CoreConfig config, IDatabase db, RedisService redis, ModelRepository repo) - { - _logger = logger; - _metrics = metrics; - _db = db; - _redis = redis; - _repo = repo; - - _useRedisMetrics = config.UseRedisMetrics; - } - - public void Run() - { - _logger.Information("Starting scheduled task runner..."); - var timeNow = SystemClock.Instance.GetCurrentInstant(); - var timeTillNextWholeMinute = - TimeSpan.FromMilliseconds(60000 - timeNow.ToUnixTimeMilliseconds() % 60000 + 250); - _periodicTask = new Timer(_ => - { - var __ = UpdatePeriodic(); - }, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1)); - } - - private async Task UpdatePeriodic() - { - _logger.Information("Running per-minute scheduled tasks."); - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - _logger.Information("Updating database stats..."); - await _repo.UpdateStats(); - - // Collect bot cluster statistics from Redis (if it's enabled) - if (_useRedisMetrics) - await CollectBotStats(); - - // Clean up message cache in postgres - await CleanupOldMessages(); - - stopwatch.Stop(); - _logger.Information("Ran scheduled tasks in {Time}", stopwatch.ElapsedDuration()); - } - - private async Task CollectBotStats() - { - var redisStats = await _redis.Connection.GetDatabase().HashGetAllAsync("pluralkit:cluster_stats"); - - var stats = redisStats.Select(v => JsonConvert.DeserializeObject(v.Value)); - - _metrics.Measure.Gauge.SetValue(Metrics.Guilds, stats.Sum(x => x.GuildCount)); - _metrics.Measure.Gauge.SetValue(Metrics.Channels, stats.Sum(x => x.ChannelCount)); - - // Aggregate DB stats - // just fetching from database here - actual updating of the data is done elsewiere - var counts = await _repo.GetStats(); - _metrics.Measure.Gauge.SetValue(CoreMetrics.SystemCount, counts.SystemCount); - _metrics.Measure.Gauge.SetValue(CoreMetrics.MemberCount, counts.MemberCount); - _metrics.Measure.Gauge.SetValue(CoreMetrics.GroupCount, counts.GroupCount); - _metrics.Measure.Gauge.SetValue(CoreMetrics.SwitchCount, counts.SwitchCount); - _metrics.Measure.Gauge.SetValue(CoreMetrics.MessageCount, counts.MessageCount); - - // Database info - // this is pretty much always inaccurate but oh well - _metrics.Measure.Gauge.SetValue(CoreMetrics.DatabaseConnections, stats.Sum(x => x.DatabaseConnectionCount)); - - foreach (var stat in redisStats) - _metrics.Measure.Gauge.SetValue( - CoreMetrics.DatabaseConnectionsByCluster, - new MetricTags("cluster_id", stat.Name), - JsonConvert.DeserializeObject(stat.Value).DatabaseConnectionCount - ); - - // Other shiz - _metrics.Measure.Gauge.SetValue(Metrics.WebhookCacheSize, stats.Sum(x => x.WebhookCacheSize)); - - await Task.WhenAll(((IMetricsRoot)_metrics).ReportRunner.RunAllAsync()); - _logger.Debug("Submitted metrics to backend"); - } - - private async Task CleanupOldMessages() - { - var deleteThresholdInstant = SystemClock.Instance.GetCurrentInstant() - CommandMessageRetention; - var deleteThresholdSnowflake = InstantToSnowflake(deleteThresholdInstant); - - var deletedRows = await _repo.DeleteCommandMessagesBefore(deleteThresholdSnowflake); - - _logger.Information( - "Pruned {DeletedRows} command messages older than retention {Retention} (older than {DeleteThresholdInstant} / {DeleteThresholdSnowflake})", - deletedRows, CommandMessageRetention, deleteThresholdInstant, deleteThresholdSnowflake); - } - - // we don't have access to PluralKit.Bot here, so this needs to be vendored - public static ulong InstantToSnowflake(Instant time) => - (ulong)(time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22; -} \ No newline at end of file diff --git a/PluralKit.ScheduledTasks/packages.lock.json b/PluralKit.ScheduledTasks/packages.lock.json deleted file mode 100644 index 572a17f6..00000000 --- a/PluralKit.ScheduledTasks/packages.lock.json +++ /dev/null @@ -1,1484 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net6.0": { - "App.Metrics": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "qQTp6o1pKC/L8yKpmUovenlDDw0HNuQ3gdKkq92BbpluEZTJLQ8AiX0NEpevoUgEwL5aHnonHq0E3yOHgoaaIA==", - "dependencies": { - "App.Metrics.Core": "4.1.0", - "App.Metrics.Formatters.Json": "4.1.0" - } - }, - "App.Metrics.Abstractions": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "HolXOB3x6/TQeaHPhMnxYvk5jaFsYgkZ7/OIzjBloRniLz/QE6pW5B7WqyiJ1a1PtCKZmjh/UA1MAB/Dj+eg3Q==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "1.0.0" - } - }, - "App.Metrics.Concurrency": { - "type": "Transitive", - "resolved": "2.0.1", - "contentHash": "XJ7eYseDig2/S61DygC8XCTckHHKNnGVGR9qTGjdeJ2x3LElKIQuScrhnEuxU3J6pqs0+UMjkATEeE7WsOf87w==", - "dependencies": { - "NETStandard.Library": "1.6.1" - } - }, - "App.Metrics.Core": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "us3u1po1KyPywv/zOqCSXjWZxldWz1yW2zGbRcnsDunv3Sem6M8+DnMYjAnoTplREo9mrm0tuSR5fIwnDg7kUA==", - "dependencies": { - "App.Metrics.Abstractions": "4.1.0", - "App.Metrics.Concurrency": "2.0.1", - "App.Metrics.Formatters.Ascii": "4.1.0", - "Microsoft.CSharp": "4.4.0" - } - }, - "App.Metrics.Formatters.Ascii": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "/OKvOt8AJT9K7EuuXLsTQ6zKmRua4X3NaSxkHZbOAJJ8ouelZGHkAvXRcJlTLoPHiBEW3vbJj/twGsIVC8U3kw==", - "dependencies": { - "App.Metrics.Abstractions": "4.1.0" - } - }, - "App.Metrics.Formatters.InfluxDB": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "N3LKXX7lcSPMOGvtOeWE0IKirT1Xq+1AHI6Jg2/NtZYdPezK3z4G1sGKflsF+cbmSojD7WSH9mFwn/Vec8QyWQ==", - "dependencies": { - "App.Metrics.Core": "4.1.0" - } - }, - "App.Metrics.Formatters.Json": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "OCdjSSRIkK0x4dy6NJ8b4H+wVUSAFxqtlL+tBSWNVC79N3K3abLG50NNdeMc79jDNq07M/qb2ow00tsuHiNA0g==", - "dependencies": { - "App.Metrics.Abstractions": "4.1.0", - "System.Text.Json": "4.6.0" - } - }, - "App.Metrics.Reporting.InfluxDB": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4wqe8OboSLt/k1MSaqfcAx+mhArquKUZ8ObyHCVxpaTiiJuSIT5D6KMaf4GaOLjS2C5sdQLrrX87IGcvV3b2GQ==", - "dependencies": { - "App.Metrics.Abstractions": "4.1.0", - "App.Metrics.Formatters.InfluxDB": "4.1.0" - } - }, - "Autofac": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "tRVRXGxwXbQmPy1ZGso115O55ffVW4mWtufjOy7hduQ1BNVR1j7RQQjxpYuB6tJw5OrgqRWYVJLJ8RwYNz/j+A==", - "dependencies": { - "System.Diagnostics.DiagnosticSource": "4.7.1" - } - }, - "Autofac.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "7.1.0", - "contentHash": "nm6rZREZ9tjdKXu9cmT69zHkZODagjCPlRRCOhiS1VqFFis9LQnMl18L4OYr8yfCW1WAQkFDD2CNaK/kF5Eqeg==", - "dependencies": { - "Autofac": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" - } - }, - "Dapper": { - "type": "Transitive", - "resolved": "2.0.35", - "contentHash": "/xAgd8BO8EDnJ0sURWEV8LptHHvTKxoYiT63YUF2U/yWE2VyUCqR2jcrtEyNngT9Kjzppecz95UKiBla3PnR7g==", - "dependencies": { - "System.Reflection.Emit.Lightweight": "4.7.0" - } - }, - "Dapper.Contrib": { - "type": "Transitive", - "resolved": "2.0.35", - "contentHash": "yVrsIV1OkdUZ8BGQbrO0EkthnPWtgs6TV2pfOtTC93G8y2BwZ0nnBJifJj+ICzN7c7COsBlVg6P6eYUwdwJj1Q==", - "dependencies": { - "Dapper": "2.0.35", - "Microsoft.CSharp": "4.7.0", - "System.Reflection.Emit": "4.7.0" - } - }, - "Elasticsearch.Net": { - "type": "Transitive", - "resolved": "7.8.1", - "contentHash": "vGHlxY72LH8/DcKb/QDpvrIelQIUFxNnXa+HmS/ifX7M7dgwmTpA2i4SagQ65gg7oi088cteUuDl4fKIystg7Q==", - "dependencies": { - "Microsoft.CSharp": "4.6.0", - "System.Buffers": "4.5.0", - "System.Diagnostics.DiagnosticSource": "4.5.1" - } - }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.13.0", - "contentHash": "/6VgKCh0P59x/rYsBkCvkUanF0TeUYzwV9hzLIWgt23QRBaKHoxaaMkidEWhKibLR88c3PVCXyyrx9Xlb+Ne6w==", - "dependencies": { - "System.Memory": "4.5.2", - "System.Runtime.CompilerServices.Unsafe": "4.5.2" - } - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.8.26", - "contentHash": "OiKusGL20vby4uDEswj2IgkdchC1yQ6rwbIkZDVBPIR6al2b7n3pC91elBul9q33KaBgRKhbZH3+2Ur4fnWx2A==" - }, - "IPNetwork2": { - "type": "Transitive", - "resolved": "2.5.381", - "contentHash": "MUx9JEtZINtK8bqBAOdz8PpGMK5Rfw6NtQ6gzux95AK8EOJ2naQTRKLUOJfm9aPb0rCfZgSoVKBU4XSXUoKxRw==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "1.0.0", - "contentHash": "K63Y4hORbBcKLWH5wnKgzyn7TOfYzevIEwIedQHBIkmkEBA9SCqgvom+XTuE+fAFGvINGkhFItaZ2dvMGdT5iw==" - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "xdl25cxDgwVxF9ckD9vJ5AdjzRE1vTGLYj9kZf6aL317ZneUijkxd/nSuzN1gEuO74dwG/Yfr1zfs636D6YZsA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.10" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "pR6mRkJx67/itEnEpnBiiATeH/P6RnhqvriD6RdQsXepO+uisfUrd149CTGPc1G5J0Qf9bwSCJkb/MYkuQ6mqw==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "3.1.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.10", - "Microsoft.Extensions.Logging.Abstractions": "3.1.10", - "Microsoft.Extensions.Options": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "HHBhCP3wAJe7UIXjim0wFXty0WG/rZAP3aZyy03uuaxiOOPHJjbUdY6K9qkfQuP+hsRzfiT+np5k4rFmcSo3og==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "UEfngyXt8XYhmekUza9JsWlA37pNOtZAjcK5EEKQrHo2LDKJmZVmcyAUFlkzCcf97OSr+w/MiDLifDDNQk9agw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "B9nQBk0GZVkOgSB1oB9V/7kvxhBvLCqm2x4m8MIoSxrd9yga8MVq2HWqnai8zZdH1WL6OlOG5mCVrwgAVwNNJg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration.CommandLine": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "RsN2xbSa7Gre429++1G2DkdAgCvVIYmJxC2L+tRmGLe/R3FOt0zH8Vri7ZmZkoOxQXks2oxqEYdGeUa1u/2NtA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "6L8lamClsrfofdWEEIFZzGx0TLfFKRRilXsdjn6Mzu73OeOZ6r6shBCYsAe38cx9JzqBLHh5l0slGBhh0yMCEw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "9//fdlaFDxkfjYPgdwYySJCtjVNTYFqnqX07Oai0eendh+Jl/SfmSAwrXyMTNgRv+jWJ2fQs85MG0cK7nAoGdQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.10", - "Microsoft.Extensions.FileProviders.Physical": "3.1.10" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "HZRvVDPpYXYtZI2zA/xuzBeA7lOPXfhXNsPiMq3O7QhLuXIGoyeRN3Ssxh9uOA+wLjTQLZQVTmzQutTWwVyuvg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "3.1.10", - "Microsoft.Extensions.Configuration.FileExtensions": "3.1.10" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "fla8hKhQmld2s/7arhUxlu3dzZLBFJLg4BQiQZdqKND4MlmnMU9jhoxY4MMlSYl6MtxumtwASHMJnuV9f96IQQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.10" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "bhjtAN7Ix5WOAr47RK16Lr1l2eizSBMCYQSavkooZyf6Xdf8XWAYGWsGsPqUFOeeRxzhpRho051rXaLn5wskVw==" - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "Pwu/7fpw1G7WjO1DxPmGfrw6ciruiLHH6k26uNex9Sn/s229uKcwds7GTBUAPbpoh4MI3qo21nqmLBo3N7gVfg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "3.1.10" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "5zIjKJGUtuBSvPSOZEiX1MnuOjSl9L4jv1+f24lO076wtZ6cBTQ34EN0jbwUYJgRX1C4ZgoSdwFZ1ZBSo61zxQ==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "3.1.10", - "Microsoft.Extensions.FileSystemGlobbing": "3.1.10" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "TzHIUBWnzsViPS/20DnC6wf5kXdRAUZlIYwTYOT9S6heuOA4Re//UmHWsDR3PusAzly5dkdDW0RV0dDZ2vEebQ==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "GjP+4cUFdsNk/Px6BlJ7p7x7ibpawcaAV4tfrRJTv2s6Nb7yz5OEKA0kbNl1ZXKa6uMQzbNqc5+B/tJsqzgIXg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "3.1.10", - "Microsoft.Extensions.DependencyInjection": "3.1.10", - "Microsoft.Extensions.Logging.Abstractions": "3.1.10", - "Microsoft.Extensions.Options": "3.1.10" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "CusdV4eIv+CGb9Fy6a+JcRqpcVJREmvFI8eHk3nQ76VLtEAIJpKQY5r5sRSs5w6NevNi2ukdnKleH0YCPudFZQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.10", - "Microsoft.Extensions.Primitives": "3.1.10" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "3.1.10", - "contentHash": "YDuQS3BeaVY6PCWUm5f6qFTYsxhwntQrcfwUzbohU/0rZBL5XI+UsD5SgggHKHX+rFY4laaT428q608Sw/mDsw==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "Microsoft.Win32.Registry": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0" - } - }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "1.6.1", - "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.AppContext": "4.3.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Console": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.Compression.ZipFile": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.Net.Http": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Net.Sockets": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Timer": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0", - "System.Xml.XDocument": "4.3.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "12.0.3", - "contentHash": "6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==" - }, - "NodaTime": { - "type": "Transitive", - "resolved": "3.0.3", - "contentHash": "sTXjtPsRddI6iaRL2iT80zBOiHTnSCy2rEHxobUKvRhr5nt7BbSIPb4cGtVf202OW0glaJMLr/5xg79FIFMHsA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.7.1" - } - }, - "NodaTime.Serialization.JsonNet": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "buSE64oL5eHDiImMgFRF74X/URygSpklFsblyqXafjSW6lMsB7iWfGO5lu7D7Zikj9bXggnMa90a5EqgpPJEYg==", - "dependencies": { - "Newtonsoft.Json": "12.0.1", - "NodaTime": "[3.0.0, 4.0.0)" - } - }, - "Npgsql": { - "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.6.0" - } - }, - "Npgsql.NodaTime": { - "type": "Transitive", - "resolved": "4.1.5", - "contentHash": "Rz3Lm8ijL0CQXvl9ZlYFsW70CiC+5D5D4m8KE7CwSsgpaB+FmpP2q3hwqoHWXqUKyWiuI2lglrI7pUuaySMTag==", - "dependencies": { - "NodaTime": "2.4.7", - "Npgsql": "4.1.5" - } - }, - "Pipelines.Sockets.Unofficial": { - "type": "Transitive", - "resolved": "2.2.0", - "contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==", - "dependencies": { - "System.IO.Pipelines": "5.0.0" - } - }, - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" - }, - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" - }, - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", - "dependencies": { - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" - } - }, - "runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", - "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" - }, - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" - }, - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" - }, - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" - }, - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" - }, - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "2.10.0", - "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "3.0.1", - "contentHash": "U0xbGoZuxJRjE3C5vlCfrf9a4xHTmbrCXKmaA14cHAqiT1Qir0rkV7Xss9GpPJR3MRYH19DFUUqZ9hvWeJrzdQ==", - "dependencies": { - "Microsoft.Extensions.Logging": "2.0.0", - "Serilog": "2.8.0" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "pNroKVjo+rDqlxNG5PXkRLpfSCuDOBY0ri6jp9PLe505ljqwhwZz8ospy2vWhQlFu5GkIesh3FcDs4n7sWZODA==", - "dependencies": { - "Serilog": "2.8.0" - } - }, - "Serilog.Formatting.Elasticsearch": { - "type": "Transitive", - "resolved": "8.4.1", - "contentHash": "768KS00+XwQSxVIYKJ4KWdqyLd5/w3DKndf+94U8NCk7qpXCeZl4HlczsDeyVsNPTyRF6MVss6Wr9uj4rhprfA==", - "dependencies": { - "Serilog": "2.8.0" - } - }, - "Serilog.NodaTime": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "F+eeRNlJZK3g9z2+c/v/WZTGHeqmnwOseQ0jMiVnW2XiKRLY9hLBopBRPbmdkhQNYtYpO9PTjcVRMHQ0Z44MmA==", - "dependencies": { - "NodaTime": "[3.0.0, 4.0.0)", - "Serilog": "2.9.0" - } - }, - "Serilog.Sinks.Async": { - "type": "Transitive", - "resolved": "1.4.1-dev-00071", - "contentHash": "6fSXIPZuJUolE0mboqHE+pHOVZdW5vxqM1lbicz3giKtwOdycOAr9vz6oQzGPHUhGZOz4JJeymw39/G+Q5dwvw==", - "dependencies": { - "Serilog": "2.8.0" - } - }, - "Serilog.Sinks.Console": { - "type": "Transitive", - "resolved": "4.0.0-dev-00834", - "contentHash": "DrM9ibdcrKCi1IQOEY764Z84uCH7mrLGy6P0zHpT8Ha6k3KyepDDDujmAf5XquOK97VrGRfyaFxnr8b42hcUgw==", - "dependencies": { - "Serilog": "2.8.0", - "System.Console": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0" - } - }, - "Serilog.Sinks.Elasticsearch": { - "type": "Transitive", - "resolved": "8.4.1", - "contentHash": "SM17WdHUshJSm44uC45jEUW4Wzp9wCltbWry5iY5fNgxJ3PkIkW6I8p+WviU5lx/bayCvAoB5uO07UK2qjBSAQ==", - "dependencies": { - "Elasticsearch.Net": "7.8.1", - "Microsoft.CSharp": "4.6.0", - "Serilog": "2.8.0", - "Serilog.Formatting.Compact": "1.0.0", - "Serilog.Formatting.Elasticsearch": "8.4.1", - "Serilog.Sinks.File": "4.0.0", - "Serilog.Sinks.PeriodicBatching": "2.1.1", - "System.Diagnostics.DiagnosticSource": "4.5.1" - } - }, - "Serilog.Sinks.File": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "U0b34w+ZikbqWEZ3ui7BdzxY/19zwrdhLtI3o6tfmLdD3oXxg7n2TZJjwCCTlKPgRuYic9CBWfrZevbb70mTaw==", - "dependencies": { - "Serilog": "2.5.0", - "System.IO.FileSystem": "4.0.1", - "System.Text.Encoding.Extensions": "4.0.11", - "System.Threading.Timer": "4.0.1" - } - }, - "Serilog.Sinks.PeriodicBatching": { - "type": "Transitive", - "resolved": "2.1.1", - "contentHash": "L1iZtcEzQdEIYCPvhYJYB2RofPg+i1NhHJfS+DpXLyLSMS6OXebqaI1fxWhmJRIjD9D9BuXi23FkZTQDiP7cHw==", - "dependencies": { - "Serilog": "2.0.0", - "System.Collections.Concurrent": "4.0.12", - "System.Threading.Timer": "4.0.1" - } - }, - "SqlKata": { - "type": "Transitive", - "resolved": "2.3.7", - "contentHash": "erKffEMhrS2IFKXjYV83M4uc1IOCl91yeP/3uY5yIm6pRNFDNrqnTk3La1en6EGDlMRol9abTNO1erQCYf08tg==", - "dependencies": { - "System.Collections.Concurrent": "4.3.0" - } - }, - "SqlKata.Execution": { - "type": "Transitive", - "resolved": "2.3.7", - "contentHash": "LybTYj99riLRH7YQNt9Kuc8VpZOvaQ7H4sQBrj2zefktS8LASOaXsHRYC/k8NEcj25w6huQpOi+HrEZ5qHXl0w==", - "dependencies": { - "Humanizer.Core": "2.8.26", - "SqlKata": "2.3.7", - "dapper": "1.50.5" - } - }, - "StackExchange.Redis": { - "type": "Transitive", - "resolved": "2.2.88", - "contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==", - "dependencies": { - "Pipelines.Sockets.Unofficial": "2.2.0", - "System.Diagnostics.PerformanceCounter": "5.0.0" - } - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A==" - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==", - "dependencies": { - "System.Security.Cryptography.ProtectedData": "5.0.0", - "System.Security.Permissions": "5.0.0" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "4.7.1", - "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" - }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "Microsoft.Win32.Registry": "5.0.0", - "System.Configuration.ConfigurationManager": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "5.0.0" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "System.Interactive.Async": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "QaqhQVDiULcu4vm6o89+iP329HcK44cETHOYgy/jfEjtzeFy0ZxmuM7nel9ocjnKxEM4yh1mli7hgh8Q9o+/Iw==", - "dependencies": { - "System.Linq.Async": "5.0.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.IO.Compression": "4.3.0" - } - }, - "System.IO.Compression.ZipFile": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", - "dependencies": { - "System.Buffers": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q==" - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Linq.Async": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "cPtIuuH8TIjVHSi2ewwReWGW1PfChPE0LxPIDlfwVcLuTM9GANFTXiMB7k3aC4sk3f0cQU25LNKzx+jZMxijqw==" - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Linq": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.2", - "contentHash": "fvq1GNmUFwbKv+aLVYYdgu/+gc8Nu9oFujOxIjPrsf+meis9JBzTPDL6aP/eeGOz9yPj6rRLUbOjKMpsMEWpNg==" - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "a4OLB4IITxAXJeV74MDx49Oq2+PsF6Sml54XAFv+2RyWwtDBcabzoxiiJRhdhx+gaohLh4hEGCLQyBozXoQPqA==" - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "4.7.1", - "contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ==" - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", - "dependencies": { - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.Apple": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Linq": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", - "dependencies": { - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA==" - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Cng": "4.3.0", - "System.Security.Cryptography.Csp": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Windows.Extensions": "5.0.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "4.6.0", - "contentHash": "4F8Xe+JIkVoDJ8hDAZ7HqLkjctN/6WItJIzQaifBwClC7wmoLSda/Sv2i6i1kycqDb3hWF4JCVbpAweyOKHEUA==" - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==", - "dependencies": { - "System.Drawing.Common": "5.0.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Tasks.Extensions": "4.3.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "pluralkit.core": { - "type": "Project", - "dependencies": { - "App.Metrics": "4.1.0", - "App.Metrics.Reporting.InfluxDB": "4.1.0", - "Autofac": "6.0.0", - "Autofac.Extensions.DependencyInjection": "7.1.0", - "Dapper": "2.0.35", - "Dapper.Contrib": "2.0.35", - "Google.Protobuf": "3.13.0", - "Microsoft.Extensions.Caching.Memory": "3.1.10", - "Microsoft.Extensions.Configuration": "3.1.10", - "Microsoft.Extensions.Configuration.Binder": "3.1.10", - "Microsoft.Extensions.Configuration.CommandLine": "3.1.10", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "3.1.10", - "Microsoft.Extensions.Configuration.Json": "3.1.10", - "Microsoft.Extensions.DependencyInjection": "3.1.10", - "Microsoft.Extensions.Logging": "3.1.10", - "Newtonsoft.Json": "12.0.3", - "NodaTime": "3.0.3", - "NodaTime.Serialization.JsonNet": "3.0.0", - "Npgsql": "4.1.5", - "Npgsql.NodaTime": "4.1.5", - "Serilog": "2.10.0", - "Serilog.Extensions.Logging": "3.0.1", - "Serilog.Formatting.Compact": "1.1.0", - "Serilog.NodaTime": "3.0.0", - "Serilog.Sinks.Async": "1.4.1-dev-00071", - "Serilog.Sinks.Console": "4.0.0-dev-00834", - "Serilog.Sinks.Elasticsearch": "8.4.1", - "Serilog.Sinks.File": "4.1.0", - "SqlKata": "2.3.7", - "SqlKata.Execution": "2.3.7", - "StackExchange.Redis": "2.2.88", - "System.Interactive.Async": "5.0.0", - "ipnetwork2": "2.5.381" - } - } - } - } -} \ No newline at end of file diff --git a/PluralKit.sln b/PluralKit.sln index fec6b6e3..b1a66c75 100644 --- a/PluralKit.sln +++ b/PluralKit.sln @@ -11,8 +11,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Tests", "PluralKi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Myriad", "Myriad\Myriad.csproj", "{ACB9BF37-F29C-4068-A7D1-2EFF2C308C4B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.ScheduledTasks", "PluralKit.ScheduledTasks\PluralKit.ScheduledTasks.csproj", "{374A8EB3-655D-4230-982B-459AE3553991}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/README.md b/README.md index 1cde6b24..3c569737 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Running the bot requires [.NET 5](https://dotnet.microsoft.com/download) and a P Optionally, it can integrate with [Sentry](https://sentry.io/welcome/) for error reporting and [InfluxDB](https://www.influxdata.com/products/influxdb-overview/) for aggregate statistics. # Configuration -Configuring the bot is done through a JSON configuration file. An example of the configuration format can be seen in [`pluralkit.conf.example`](https://github.com/xSke/PluralKit/blob/master/pluralkit.conf.example). +Configuring the bot is done through a JSON configuration file. An example of the configuration format can be seen in [`pluralkit.conf.example`](https://github.com/PluralKit/PluralKit/blob/master/pluralkit.conf.example). The configuration file needs to be placed in the bot's working directory (usually the repository root) and must be called `pluralkit.conf`. The configuration file is in JSON format (albeit with a `.conf` extension). The following keys are available (using `.` to indicate a nested object level), bolded key names are required: @@ -31,7 +31,7 @@ The bot can also take configuration from environment variables, which will overr ## Docker The easiest way to get the bot running is with Docker. The repository contains a `docker-compose.yml` file ready to use. -* Clone this repository: `git clone https://github.com/xSke/PluralKit` +* Clone this repository: `git clone https://github.com/PluralKit/PluralKit` * Create a `pluralkit.conf` file in the same directory as `docker-compose.yml` containing at least a `PluralKit.Bot.Token` field * (`PluralKit.Database` is overridden in `docker-compose.yml` to point to the Postgres container) * Build the bot: `docker-compose build` @@ -39,7 +39,7 @@ The easiest way to get the bot running is with Docker. The repository contains a In other words: ``` -$ git clone https://github.com/xSke/PluralKit +$ git clone https://github.com/PluralKit/PluralKit $ cd PluralKit $ cp pluralkit.conf.example pluralkit.conf $ nano pluralkit.conf # (or vim, or whatever) @@ -48,7 +48,7 @@ $ docker-compose up -d ## Manually * Install the .NET 6 SDK (see https://dotnet.microsoft.com/download) -* Clone this repository: `git clone https://github.com/xSke/PluralKit` +* Clone this repository: `git clone https://github.com/PluralKit/PluralKit` * Create and fill in a `pluralkit.conf` file in the same directory as `docker-compose.yml` * Run the bot: `dotnet run --project PluralKit.Bot` * Alternatively, `dotnet build -c Release -o build/`, then `dotnet build/PluralKit.Bot.dll` diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..176e82f5 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +dashboard \ No newline at end of file diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile new file mode 100644 index 00000000..8a048b69 --- /dev/null +++ b/dashboard/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:latest as builder + +RUN apk add nodejs-current yarn go git + +COPY dashboard/ /build +COPY .git/ /build/.git + +WORKDIR /build + +RUN yarn install --frozen-lockfile +RUN yarn build + +RUN sh -c 'go build -ldflags "-X main.version=$(git rev-parse HEAD)"' + +FROM alpine:latest + +COPY --from=builder /build/dashboard /bin/dashboard + +ENTRYPOINT /bin/dashboard \ No newline at end of file diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..278cb1af --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,12 @@ +# PluralKit Dashboard + +This project is built using [Vite](https://vitejs.dev/), using the svelte-ts template. + +Some of the other stuff used to get this working: +* sveltestrap (https://sveltestrap.js.org/) +* svelte-navigator (https://github.com/mefechoel/svelte-navigator) +* svelte-toggle (https://github.com/metonym/svelte-toggle) +* svelecte (https://mskocik.github.io/svelecte/) +* svelte-icons (https://github.com/Introvertuous/svelte-icons) +* discord-markdown (https://github.com/brussell98/discord-markdown) +* moment (https://momentjs.com/) diff --git a/dashboard/go.mod b/dashboard/go.mod new file mode 100644 index 00000000..ab7352b4 --- /dev/null +++ b/dashboard/go.mod @@ -0,0 +1,5 @@ +module dashboard + +go 1.18 + +require github.com/go-chi/chi v1.5.4 diff --git a/dashboard/go.sum b/dashboard/go.sum new file mode 100644 index 00000000..874ed9a3 --- /dev/null +++ b/dashboard/go.sum @@ -0,0 +1,2 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 00000000..cadbfaf4 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,16 @@ + + + + + + + PluralKit | home + + + + + +
+ + + diff --git a/dashboard/main.go b/dashboard/main.go new file mode 100644 index 00000000..7396b021 --- /dev/null +++ b/dashboard/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "embed" + "encoding/json" + "fmt" + "html" + "io" + "net/http" + "strings" + + "github.com/go-chi/chi" +) + +//go:embed dist/* +var fs embed.FS + +type entity struct { + AvatarURL *string `json:"avatar_url"` + IconURL *string `json:"icon_url"` + Description *string `json:"description"` + Color *string `json:"color"` +} + +var baseURL = "https://api.pluralkit.me/v2" + +var version = "dev" +var versionJS string + +const defaultEmbed = ` ` + +func main() { + versionJS = "" + + r := chi.NewRouter() + + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("X-PluralKit-Version", version) + next.ServeHTTP(rw, r) + }) + }) + + r.NotFound(notFoundHandler) + + r.Get("/profile/{type}/{id}", func(rw http.ResponseWriter, r *http.Request) { + defer func() { + if a := recover(); a != nil { + notFoundHandler(rw, r) + return + } + }() + createEmbed(rw, r) + }) + + http.ListenAndServe(":8080", r) +} + +func notFoundHandler(rw http.ResponseWriter, r *http.Request) { + var data []byte + var err error + + // lol + if strings.HasSuffix(r.URL.Path, ".js") { + data, err = fs.ReadFile("dist" + r.URL.Path) + rw.Header().Add("content-type", "application/javascript") + } else if strings.HasSuffix(r.URL.Path, ".css") { + data, err = fs.ReadFile("dist" + r.URL.Path) + rw.Header().Add("content-type", "text/css") + } else if strings.HasSuffix(r.URL.Path, ".map") { + data, err = fs.ReadFile("dist" + r.URL.Path) + } else { + data, err = fs.ReadFile("dist/index.html") + rw.Header().Add("content-type", "text/html") + data = []byte(strings.Replace(string(data), ``, defaultEmbed+versionJS, 1)) + } + + if err != nil { + panic(err) + } + + rw.Write(data) +} + +// explanation for createEmbed: +// we don't care about errors, we just want to return a HTML page as soon as possible +// `panic(nil)` is caught by upstream, which then returns the raw HTML page + +func createEmbed(rw http.ResponseWriter, r *http.Request) { + entityType := chi.URLParam(r, "type") + id := chi.URLParam(r, "id") + + var path string + + switch entityType { + case "s": + path = "/systems/" + id + case "m": + path = "/members/" + id + case "g": + path = "/groups/" + id + default: + panic(nil) + } + + res, err := http.Get(baseURL + path) + if err != nil { + panic(nil) + } + if res.StatusCode != 200 { + panic(nil) + } + + var data entity + body, _ := io.ReadAll(res.Body) + err = json.Unmarshal(body, &data) + if err != nil { + panic(nil) + } + + text := fmt.Sprintf(`%s`, baseURL, path, "\n") + + if data.AvatarURL != nil { + text += fmt.Sprintf(`%s`, html.EscapeString(*data.AvatarURL), "\n") + } else if data.IconURL != nil { + text += fmt.Sprintf(`%s`, html.EscapeString(*data.IconURL), "\n") + } + + if data.Description != nil { + text += fmt.Sprintf(`%s`, html.EscapeString(*data.Description), "\n") + } + + if data.Color != nil { + text += fmt.Sprintf(`%s`, html.EscapeString(*data.Color), "\n") + } + + html, err := fs.ReadFile("dist/index.html") + if err != nil { + panic(nil) + } + html = []byte(strings.Replace(string(html), ``, text+versionJS, 1)) + + rw.Header().Add("content-type", "text/html") + rw.Write(html) +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 00000000..22beaa00 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,42 @@ +{ + "name": "pluralkit-dashboard", + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", + "@tsconfig/svelte": "^2.0.1", + "svelte": "^3.44.0", + "svelte-check": "^2.2.7", + "svelte-toggle": "^3.1.0", + "tslib": "^2.3.1", + "typescript": "^4.4.4", + "vite": "^2.7.0" + }, + "dependencies": { + "@sentry/browser": "^6.19.5", + "@sentry/tracing": "^6.19.5", + "@types/twemoji": "^12.1.2", + "axios": "^0.24.0", + "bootstrap": "^5.1.3", + "bootstrap-dark-5": "^1.1.3", + "core-js-pure": "^3.23.4", + "discord-markdown": "^2.5.1", + "gh-pages": "^3.2.3", + "import": "^0.0.6", + "moment": "^2.29.1", + "sass": "^1.52.2", + "svelecte": "^3.4.5", + "svelte-autosize": "^1.0.1", + "svelte-icons": "^2.1.0", + "svelte-navigator": "^3.1.5", + "svelte-preprocess": "^4.10.6", + "sveltestrap": "^5.6.3", + "twemoji": "^13.1.0" + } +} diff --git a/dashboard/public/myriad.png b/dashboard/public/myriad.png new file mode 100644 index 00000000..d58bd01a Binary files /dev/null and b/dashboard/public/myriad.png differ diff --git a/dashboard/src/App.svelte b/dashboard/src/App.svelte new file mode 100644 index 00000000..ac324263 --- /dev/null +++ b/dashboard/src/App.svelte @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + +
+ + Please provide a system ID in the URL. + + + + + + + Please provide a member ID in the URL. + + + + + Please provide a group ID in the URL. + + + + \ No newline at end of file diff --git a/dashboard/src/api/errors.ts b/dashboard/src/api/errors.ts new file mode 100644 index 00000000..c6b4ff70 --- /dev/null +++ b/dashboard/src/api/errors.ts @@ -0,0 +1,28 @@ +enum ErrorType { + Unknown = 0, + InvalidToken = 401, + NotFound = 404, + InternalServerError = 500, +} + +interface ApiError { + code: number, + type: ErrorType, + message?: string, + data?: any, +} + +export function parse(code: number, data?: any): ApiError { + var type = ErrorType[ErrorType[code]] ?? ErrorType.Unknown; + if (code >= 500) type = ErrorType.InternalServerError; + + var err: ApiError = { code, type }; + + if (data) { + var d = data; + err.message = d.message; + err.data = d; + } + + return err; +} diff --git a/dashboard/src/api/index.ts b/dashboard/src/api/index.ts new file mode 100644 index 00000000..260bb0b2 --- /dev/null +++ b/dashboard/src/api/index.ts @@ -0,0 +1,58 @@ +import axios from 'axios'; +import * as Sentry from '@sentry/browser'; + +const baseUrl = () => localStorage.isBeta ? "https://api.beta.pluralkit.me" : "https://api.pluralkit.me"; + +const methods = ['get', 'post', 'delete', 'patch', 'put']; +const noop = () => {}; + +const scheduled = []; +const runAPI = () => { + if (scheduled.length == 0) return; + const {axiosData, res, rej} = scheduled.shift(); + axios(axiosData) + .then((resp) => res(parseData(resp.status, resp.data))) + .catch((err) => { + Sentry.captureException("Fetch error", err); + rej(err); + }); +} + +setInterval(runAPI, 500); + +export default function() { + const route = []; + const handler = { + get(_, name) { + if (route.length == 0 && name != "private") + route.push("v2"); + if (methods.includes(name)) { + return ({ data = undefined, auth = true, token = null, query = null } = {}) => new Promise((res, rej) => scheduled.push({ res, rej, axiosData: { + url: baseUrl() + "/" + route.join("/") + (query ? `?${Object.keys(query).map(x => `${x}=${query[x]}`).join("&")}` : ""), + method: name, + headers: { + authorization: token ?? (auth ? localStorage.getItem("pk-token") : undefined), + "content-type": name == "get" ? undefined : "application/json" + }, + data: !!data ? JSON.stringify(data) : undefined, + validateStatus: () => true, + }})); + } + route.push(name); + return new Proxy(noop, handler); + }, + apply(target, _, args) { + route.push(...args.filter(x => x != null)); + return new Proxy(noop, handler); + } + } + return new Proxy(noop, handler); +} + +import * as errors from './errors'; + +function parseData(code: number, data: any) { + if (code == 200) return data; + if (code == 204) return; + throw errors.parse(code, data); +} \ No newline at end of file diff --git a/dashboard/src/api/parse-timestamps.ts b/dashboard/src/api/parse-timestamps.ts new file mode 100644 index 00000000..53ec0d62 --- /dev/null +++ b/dashboard/src/api/parse-timestamps.ts @@ -0,0 +1,26 @@ +import moment from 'moment' + +const timestampRegex = //g +const parseTimestamps = (html: string) => { + return html.replaceAll( + timestampRegex, + (match, p1, p2) => { + const timestamp = moment.unix(parseInt(p1)) + const format: string = p2 ? p2[1] : 'f' + if (format !== 'R') { + let dateTimeFormatOptions = { + t: 'HH:mm', + T: 'HH:mm:ss', + d: 'DD/MM/YYYY', + D: 'DD MMMM YYYY', + f: 'DD MMMM YYYY HH:mm', + F: 'dddd, DD MMMM YYYY HH:mm', + }[format] + return timestamp.format(dateTimeFormatOptions) + } + return timestamp.fromNow() + }, + ) +} + +export default parseTimestamps diff --git a/dashboard/src/api/types.ts b/dashboard/src/api/types.ts new file mode 100644 index 00000000..3aaea446 --- /dev/null +++ b/dashboard/src/api/types.ts @@ -0,0 +1,88 @@ +interface SystemPrivacy { + description_privacy?: string, + member_list_privacy?: string, + front_privacy?: string, + front_history_privacy?: string, + group_list_privacy?: string +} + +export interface System { + id?: string; + uuid?: string; + name?: string; + description?: string; + tag?: string; + avatar_url?: string; + banner?: string; + timezone?: string; + created?: string; + privacy?: SystemPrivacy; + color?: string; +} + +export interface Config { + timezone: string; + pings_enabled: boolean; + member_default_private?: boolean; + group_default_private?: boolean; + show_private_info?: boolean; + member_limit: number; + group_limit: number; + description_templates: string[]; +} + +export interface MemberPrivacy { + visibility?: string, + description_privacy?: string, + name_privacy?: string, + birthday_privacy?: string, + pronoun_privacy?: string, + avatar_privacy?: string, + metadata_privacy?: string +} + +interface proxytag { + prefix?: string, + suffix?: string +} + +export interface Member { + id?: string; + uuid?: string; + name?: string; + display_name?: string; + color?: string; + birthday?: string; + pronouns?: string; + avatar_url?: string; + banner?: string; + description?: string; + created?: string; + keep_proxy?: boolean + system?: string; + proxy_tags?: Array; + privacy?: MemberPrivacy +} + +export interface GroupPrivacy { + description_privacy?: string, + icon_privacy?: string, + list_privacy?: string, + visibility?: string, + name_privacy?: string, + metadata_privacy?: string +} + +export interface Group { + id?: string; + uuid?: string; + name?: string; + display_name?: string; + description?: string; + icon?: string; + banner?: string; + color?: string; + privacy?: GroupPrivacy; + created?: string; + members?: string[]; +} \ No newline at end of file diff --git a/dashboard/src/assets/default_avatar.png b/dashboard/src/assets/default_avatar.png new file mode 100644 index 00000000..753346aa Binary files /dev/null and b/dashboard/src/assets/default_avatar.png differ diff --git a/dashboard/src/lib/CardsHeader.svelte b/dashboard/src/lib/CardsHeader.svelte new file mode 100644 index 00000000..3cab06ad --- /dev/null +++ b/dashboard/src/lib/CardsHeader.svelte @@ -0,0 +1,58 @@ + + + +
+
+ +
+ {@html htmlName} ({item.id}) +
+
+ {#if item && (item.avatar_url || item.icon)} + {if (event.key === "Enter") {avatarOpen = true}}} on:click|stopPropagation={toggleAvatarModal} class="rounded-circle avatar" src={icon_url} alt={altText} /> + {:else} + icon (default) + {/if} +
+ +
+ {altText} +
+
+
\ No newline at end of file diff --git a/dashboard/src/lib/ListPagination.svelte b/dashboard/src/lib/ListPagination.svelte new file mode 100644 index 00000000..65392242 --- /dev/null +++ b/dashboard/src/lib/ListPagination.svelte @@ -0,0 +1,73 @@ + +{#if pageAmount > 1} + + {#if currentPage !== 1} + + {e.preventDefault(); currentPage -= 1}}> + + {:else} + + + + {/if} + {#if currentPage > 2} + + {e.preventDefault(); currentPage = 1}}>1 + + {/if} + {#if currentPage === 4} + + {e.preventDefault(); currentPage = 2}}>2 + + {/if} + {#if currentPage > 4} + + ... + + {/if} + {#if currentPage > 1} + + {e.preventDefault(); currentPage -= 1}}>{currentPage - 1} + + {/if} + + {currentPage} + + {#if currentPage < pageAmount} + + {e.preventDefault(); currentPage += 1}}>{currentPage + 1} + + {/if} + {#if currentPage < pageAmount - 3} + + ... + + {/if} + {#if currentPage === pageAmount - 3} + + {e.preventDefault(); currentPage = pageAmount - 1}}>{pageAmount - 1} + + {/if} + {#if currentPage < pageAmount - 1} + + { e.preventDefault(); currentPage = pageAmount}}>{pageAmount} + + {/if} + {#if currentPage !== pageAmount} + + {e.preventDefault(); currentPage += 1}}> + + {:else} + + + + {/if} + +{/if} \ No newline at end of file diff --git a/dashboard/src/lib/Navigation.svelte b/dashboard/src/lib/Navigation.svelte new file mode 100644 index 00000000..16a00e5d --- /dev/null +++ b/dashboard/src/lib/Navigation.svelte @@ -0,0 +1,67 @@ + + + PluralKit + + + + + + + \ No newline at end of file diff --git a/dashboard/src/lib/group/Body.svelte b/dashboard/src/lib/group/Body.svelte new file mode 100644 index 00000000..0a6d2ce0 --- /dev/null +++ b/dashboard/src/lib/group/Body.svelte @@ -0,0 +1,168 @@ + + + +{#if !editMode && !memberMode} + + {#if group.id} + + ID: {group.id} + + {/if} + {#if group.name} + + Name: {group.name} + + {/if} + {#if group.display_name} + + Display Name: {@html htmlDisplayName} + + {/if} + {#if group.created && !isPublic} + + Created: {created} + + {/if} + {#if group.color} + + Color: {group.color} + + {/if} + {#if group.banner} + + Banner: + +
+ {`Group +
+
+ + {/if} + {#if group.privacy} + + Privacy: + + + Edit privacy + + + + + + + {/if} +
+
+ Description:
+ {@html htmlDescription && htmlDescription} +
+{#if (group.banner && ((settings && settings.appearance.banner_bottom) || !settings))} +group banner +{/if} + +{#if !isPublic} + + {#if isMainDash} + + {/if} +{/if} + +{#if !isPage} + + {:else if !isPublic} + +{/if} + + +{:else if editMode} + +{:else if memberMode} + +{/if} +
+ + \ No newline at end of file diff --git a/dashboard/src/lib/group/Edit.svelte b/dashboard/src/lib/group/Edit.svelte new file mode 100644 index 00000000..b90670fd --- /dev/null +++ b/dashboard/src/lib/group/Edit.svelte @@ -0,0 +1,151 @@ + + +{#each err as error} +{@html error} +{/each} +{#if success} +Group information updated! +{/if} + + + + + + + +