mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-08 14:57:54 +00:00
Merge branch 'main' into rust-command-parser
This commit is contained in:
commit
77276c15e6
119 changed files with 4837 additions and 1017 deletions
|
|
@ -63,14 +63,14 @@ public class ApplicationCommandProxiedMessage
|
|||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
|
||||
// check for command messages
|
||||
var (authorId, channelId) = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (authorId != null)
|
||||
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (cmessage != null)
|
||||
{
|
||||
if (authorId != ctx.User.Id)
|
||||
if (cmessage.AuthorId != ctx.User.Id)
|
||||
throw new PKError("You can only delete command messages queried by this account.");
|
||||
|
||||
var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == channelId;
|
||||
await DeleteMessageInner(ctx, channelId!.Value, messageId, isDM);
|
||||
var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == cmessage.ChannelId;
|
||||
await DeleteMessageInner(ctx, cmessage.GuildId, cmessage.ChannelId, messageId, isDM);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -78,10 +78,10 @@ public class ApplicationCommandProxiedMessage
|
|||
var message = await ctx.Repository.GetFullMessage(messageId);
|
||||
if (message != null)
|
||||
{
|
||||
if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.User.Id)
|
||||
if (message.Message.Sender != ctx.User.Id && (ctx.System != null && message.System?.Id != ctx.System.Id))
|
||||
throw new PKError("You can only delete your own messages.");
|
||||
|
||||
await DeleteMessageInner(ctx, message.Message.Channel, message.Message.Mid, false);
|
||||
await DeleteMessageInner(ctx, message.Message.Guild ?? 0, message.Message.Channel, message.Message.Mid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -89,9 +89,9 @@ public class ApplicationCommandProxiedMessage
|
|||
throw Errors.MessageNotFound(messageId);
|
||||
}
|
||||
|
||||
internal async Task DeleteMessageInner(InteractionContext ctx, ulong channelId, ulong messageId, bool isDM = false)
|
||||
internal async Task DeleteMessageInner(InteractionContext ctx, ulong guildId, ulong channelId, ulong messageId, bool isDM = false)
|
||||
{
|
||||
if (!((await _cache.BotPermissionsIn(channelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
if (!((await _cache.BotPermissionsIn(guildId, channelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
throw new PKError("PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the message."
|
||||
+ " Please contact a server administrator to remedy this.");
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ public class ApplicationCommandProxiedMessage
|
|||
// (if not, PK shouldn't send messages on their behalf)
|
||||
var member = await _rest.GetGuildMember(ctx.GuildId, ctx.User.Id);
|
||||
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
|
||||
if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.ChannelId, member)).HasFlag(requiredPerms))
|
||||
if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.GuildId, ctx.ChannelId, member)).HasFlag(requiredPerms))
|
||||
{
|
||||
throw new PKError("You do not have permission to send messages in this channel.");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -101,9 +101,7 @@ public class Bot
|
|||
{
|
||||
// we HandleGatewayEvent **before** getting the own user, because the own user is set in HandleGatewayEvent for ReadyEvent
|
||||
await _cache.HandleGatewayEvent(evt);
|
||||
|
||||
await _cache.TryUpdateSelfMember(_config.ClientId, evt);
|
||||
|
||||
await OnEventReceivedInner(shardId, evt);
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +173,16 @@ public class Bot
|
|||
}
|
||||
|
||||
using var _ = LogContext.PushProperty("EventId", Guid.NewGuid());
|
||||
using var __ = LogContext.Push(await serviceScope.Resolve<SerilogGatewayEnricherFactory>().GetEnricher(shardId, evt));
|
||||
// this fails when cache lookup fails, so put it in a try-catch
|
||||
try
|
||||
{
|
||||
using var __ = LogContext.Push(await serviceScope.Resolve<SerilogGatewayEnricherFactory>().GetEnricher(shardId, evt));
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
|
||||
await HandleError(handler, evt, serviceScope, exc);
|
||||
}
|
||||
_logger.Verbose("Received gateway event: {@Event}", evt);
|
||||
|
||||
try
|
||||
|
|
@ -243,7 +250,7 @@ public class Bot
|
|||
if (!exc.ShowToUser()) return;
|
||||
|
||||
// Once we've sent it to Sentry, report it to the user (if we have permission to)
|
||||
var reportChannel = handler.ErrorChannelFor(evt, _config.ClientId);
|
||||
var (guildId, reportChannel) = handler.ErrorChannelFor(evt, _config.ClientId);
|
||||
if (reportChannel == null)
|
||||
{
|
||||
if (evt is InteractionCreateEvent ice && ice.Type == Interaction.InteractionType.ApplicationCommand)
|
||||
|
|
@ -251,7 +258,7 @@ public class Bot
|
|||
return;
|
||||
}
|
||||
|
||||
var botPerms = await _cache.BotPermissionsIn(reportChannel.Value);
|
||||
var botPerms = await _cache.BotPermissionsIn(guildId ?? 0, reportChannel.Value);
|
||||
if (botPerms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks))
|
||||
await _errorMessageService.SendErrorMessage(reportChannel.Value, sentryEvent.EventId.ToString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ public class BotConfig
|
|||
|
||||
public string? GatewayQueueUrl { get; set; }
|
||||
public bool UseRedisRatelimiter { get; set; } = false;
|
||||
public bool UseRedisCache { get; set; } = false;
|
||||
|
||||
public string? HttpCacheUrl { get; set; }
|
||||
public bool HttpUseInnerCache { get; set; } = false;
|
||||
|
||||
public string? RedisGatewayUrl { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -136,4 +136,11 @@ public static class BotMetrics
|
|||
DurationUnit = TimeUnit.Seconds,
|
||||
Context = "Bot"
|
||||
};
|
||||
|
||||
public static MeterOptions CacheDebug => new()
|
||||
{
|
||||
Name = "Bad responses to cache lookups",
|
||||
Context = "Bot",
|
||||
MeasurementUnit = Unit.Calls
|
||||
};
|
||||
}
|
||||
|
|
@ -82,6 +82,7 @@ public partial class CommandTree
|
|||
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
|
||||
public static Command SwitchEdit = new Command("switch edit", "switch edit <member> [member 2] [member 3...]", "Edits the members in the latest switch");
|
||||
public static Command SwitchEditOut = new Command("switch edit out", "switch edit out", "Turns the latest switch into a switch-out");
|
||||
public static Command SwitchCopy = new Command("switch copy", "switch copy <member> [member 2] [member 3...]", "Makes a new switch with the listed members added");
|
||||
public static Command SwitchDelete = new Command("switch delete", "switch delete", "Deletes the latest switch");
|
||||
public static Command SwitchDeleteAll = new Command("switch delete", "switch delete all", "Deletes all logged switches");
|
||||
public static Command Link = new Command("link", "link <account>", "Links your system to another account");
|
||||
|
|
@ -92,6 +93,7 @@ public partial class CommandTree
|
|||
public static Command Export = new Command("export", "export", "Exports system information to a data file");
|
||||
public static Command Help = new Command("help", "help", "Shows help information about PluralKit");
|
||||
public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying");
|
||||
public static Command Dashboard = new Command("dashboard", "dashboard", "Get a link to the PluralKit dashboard");
|
||||
public static Command Message = new Command("message", "message <id|link> [delete|author]", "Looks up a proxied message");
|
||||
public static Command MessageEdit = new Command("edit", "edit [link] <text>", "Edit a previously proxied message");
|
||||
public static Command MessageReproxy = new Command("reproxy", "reproxy [link] <member>", "Reproxy a previously proxied message using a different member");
|
||||
|
|
@ -137,7 +139,7 @@ public partial class CommandTree
|
|||
|
||||
public static Command[] SwitchCommands =
|
||||
{
|
||||
Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll
|
||||
Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll, SwitchCopy
|
||||
};
|
||||
|
||||
public static Command[] ConfigCommands =
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public partial class CommandTree
|
|||
return CommandHelpRoot(ctx);
|
||||
if (ctx.Match("ap", "autoproxy", "auto"))
|
||||
return HandleAutoproxyCommand(ctx);
|
||||
if (ctx.Match("config", "cfg"))
|
||||
if (ctx.Match("config", "cfg", "configure"))
|
||||
return HandleConfigCommand(ctx);
|
||||
if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls"))
|
||||
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
|
||||
|
|
@ -105,6 +105,8 @@ public partial class CommandTree
|
|||
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx, ctx.System));
|
||||
else
|
||||
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, ctx.System));
|
||||
if (ctx.Match("dashboard", "dash"))
|
||||
return ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx));
|
||||
|
||||
// remove compiler warning
|
||||
return ctx.Reply(
|
||||
|
|
@ -430,13 +432,15 @@ public partial class CommandTree
|
|||
await ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx));
|
||||
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
|
||||
await ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx));
|
||||
else if (ctx.Match("copy", "add", "duplicate", "dupe"))
|
||||
await ctx.Execute<Switch>(SwitchCopy, m => m.SwitchEdit(ctx, true));
|
||||
else if (ctx.Match("commands", "help"))
|
||||
await PrintCommandList(ctx, "switching", SwitchCommands);
|
||||
else if (ctx.HasNext()) // there are following arguments
|
||||
await ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
|
||||
SwitchDelete, SystemFronter, SystemFrontHistory);
|
||||
SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
|
||||
}
|
||||
|
||||
private async Task CommandHelpRoot(Context ctx)
|
||||
|
|
@ -544,6 +548,8 @@ public partial class CommandTree
|
|||
return ctx.Execute<Config>(null, m => m.HidDisplayCaps(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids"))
|
||||
return ctx.Execute<Config>(null, m => m.HidListPadding(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit"))
|
||||
return ctx.Execute<Config>(null, m => m.LimitUpdate(ctx));
|
||||
|
||||
// todo: maybe add the list of configuration keys here?
|
||||
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings.");
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public class Context
|
|||
public readonly int ShardId;
|
||||
public readonly Cluster Cluster;
|
||||
|
||||
public Task<PermissionSet> BotPermissions => Cache.BotPermissionsIn(Channel.Id);
|
||||
public Task<PermissionSet> BotPermissions => Cache.BotPermissionsIn(Guild?.Id ?? 0, Channel.Id);
|
||||
public Task<PermissionSet> UserPermissions => Cache.PermissionsForMCE((MessageCreateEvent)Message);
|
||||
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ public class Context
|
|||
// {
|
||||
// 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);
|
||||
await _commandMessageService.RegisterMessage(msg.Id, Guild?.Id ?? 0, msg.ChannelId, Author.Id);
|
||||
// }
|
||||
|
||||
return msg;
|
||||
|
|
|
|||
|
|
@ -91,8 +91,12 @@ public static class ContextArgumentsExt
|
|||
public static bool MatchClear(this Context ctx)
|
||||
=> ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear");
|
||||
|
||||
public static bool MatchRaw(this Context ctx) =>
|
||||
ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw");
|
||||
public static ReplyFormat MatchFormat(this Context ctx)
|
||||
{
|
||||
if (ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
|
||||
if (ctx.Match("pt", "plaintext") || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
|
||||
return ReplyFormat.Standard;
|
||||
}
|
||||
|
||||
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
|
||||
{
|
||||
|
|
@ -184,4 +188,11 @@ public static class ContextArgumentsExt
|
|||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ReplyFormat
|
||||
{
|
||||
Standard,
|
||||
Raw,
|
||||
Plaintext
|
||||
}
|
||||
|
|
@ -188,7 +188,8 @@ public static class ContextEntityArgumentsExt
|
|||
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
|
||||
return null;
|
||||
|
||||
var channel = await ctx.Cache.TryGetChannel(id);
|
||||
// todo: match channels in other guilds
|
||||
var channel = await ctx.Cache.TryGetChannel(ctx.Guild!.Id, id);
|
||||
if (channel == null)
|
||||
channel = await ctx.Rest.GetChannelOrNull(id);
|
||||
if (channel == null)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
using Humanizer;
|
||||
using Dapper;
|
||||
using SqlKata;
|
||||
|
||||
using Myriad.Builders;
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Cache;
|
||||
using Myriad.Rest;
|
||||
using Myriad.Types;
|
||||
|
||||
|
|
@ -14,11 +18,64 @@ public class Admin
|
|||
{
|
||||
private readonly BotConfig _botConfig;
|
||||
private readonly DiscordApiClient _rest;
|
||||
private readonly IDiscordCache _cache;
|
||||
|
||||
public Admin(BotConfig botConfig, DiscordApiClient rest)
|
||||
public Admin(BotConfig botConfig, DiscordApiClient rest, IDiscordCache cache)
|
||||
{
|
||||
_botConfig = botConfig;
|
||||
_rest = rest;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateEmbed(Context ctx, PKSystem system)
|
||||
{
|
||||
string UntilLimit(int count, int limit)
|
||||
{
|
||||
var brackets = new List<int> { 10, 25, 50, 100 };
|
||||
if (count == limit)
|
||||
return "(at limit)";
|
||||
|
||||
foreach (var x in brackets)
|
||||
{
|
||||
if (limit - x <= count)
|
||||
return $"(approx. {x} to limit)";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
|
||||
{
|
||||
async Task<(ulong Id, User? User)> Inner(ulong id)
|
||||
{
|
||||
var user = await _cache.GetOrFetchUser(_rest, id);
|
||||
return (id, user);
|
||||
}
|
||||
|
||||
return Task.WhenAll(ids.Select(Inner));
|
||||
}
|
||||
|
||||
var config = await ctx.Repository.GetSystemConfig(system.Id);
|
||||
|
||||
// Fetch/render info for all accounts simultaneously
|
||||
var accounts = await ctx.Repository.GetSystemAccounts(system.Id);
|
||||
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)");
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Title("System info")
|
||||
.Color(DiscordUtils.Green)
|
||||
.Field(new Embed.Field("System ID", $"`{system.Hid}`"))
|
||||
.Field(new Embed.Field("Linked accounts", string.Join("\n", users).Truncate(1000)));
|
||||
|
||||
var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
var memberCount = await ctx.Repository.GetSystemMemberCount(system.Id);
|
||||
eb.Field(new Embed.Field("Member limit", $"{memberLimit} {UntilLimit(memberCount, memberLimit)}", true));
|
||||
|
||||
var groupLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
var groupCount = await ctx.Repository.GetSystemGroupCount(system.Id);
|
||||
eb.Field(new Embed.Field("Group limit", $"{groupLimit} {UntilLimit(groupCount, groupLimit)}", true));
|
||||
|
||||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task UpdateSystemId(Context ctx)
|
||||
|
|
@ -37,6 +94,8 @@ public class Admin
|
|||
if (existingSystem != null)
|
||||
throw new PKError($"Another system already exists with ID `{newHid}`.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change"))
|
||||
throw new PKError("ID change cancelled.");
|
||||
|
||||
|
|
@ -60,6 +119,9 @@ public class Admin
|
|||
if (existingMember != null)
|
||||
throw new PKError($"Another member already exists with ID `{newHid}`.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo(
|
||||
$"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?",
|
||||
"Change"
|
||||
|
|
@ -86,6 +148,9 @@ public class Admin
|
|||
if (existingGroup != null)
|
||||
throw new PKError($"Another group already exists with ID `{newHid}`.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?",
|
||||
"Change"
|
||||
))
|
||||
|
|
@ -103,6 +168,8 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown system.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll"))
|
||||
throw new PKError("ID change cancelled.");
|
||||
|
||||
|
|
@ -124,6 +191,9 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown member.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo(
|
||||
$"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?",
|
||||
"Reroll"
|
||||
|
|
@ -148,6 +218,9 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown group.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?",
|
||||
"Change"
|
||||
))
|
||||
|
|
@ -176,7 +249,7 @@ public class Admin
|
|||
var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply($"Current member limit is **{currentLimit}** members.");
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +257,7 @@ public class Admin
|
|||
if (!int.TryParse(newLimitStr, out var newLimit))
|
||||
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update"))
|
||||
throw new PKError("Member limit change cancelled.");
|
||||
|
||||
|
|
@ -204,7 +278,7 @@ public class Admin
|
|||
var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply($"Current group limit is **{currentLimit}** groups.");
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +286,7 @@ public class Admin
|
|||
if (!int.TryParse(newLimitStr, out var newLimit))
|
||||
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update"))
|
||||
throw new PKError("Group limit change cancelled.");
|
||||
|
||||
|
|
@ -243,6 +318,7 @@ public class Admin
|
|||
throw Errors.AccountInOtherSystem(existingAccount, ctx.Config);
|
||||
|
||||
var system = await ctx.Repository.GetSystem(systemId.Value!);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account"))
|
||||
throw new PKError("System recovery cancelled.");
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ public class Checks
|
|||
var error = "Channel not found or you do not have permissions to access it.";
|
||||
|
||||
// todo: this breaks if channel is not in cache and bot does not have View Channel permissions
|
||||
// with new cache it breaks if channel is not in current guild
|
||||
var channel = await ctx.MatchChannel();
|
||||
if (channel == null || channel.GuildId == null)
|
||||
throw new PKError(error);
|
||||
|
|
@ -156,7 +157,8 @@ public class Checks
|
|||
if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
||||
throw new PKError(error);
|
||||
|
||||
var botPermissions = await _cache.BotPermissionsIn(channel.Id);
|
||||
// todo: permcheck channel outside of guild?
|
||||
var botPermissions = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id);
|
||||
|
||||
// We use a bitfield so we can set individual permission bits
|
||||
ulong missingPermissions = 0;
|
||||
|
|
@ -231,11 +233,11 @@ public class Checks
|
|||
var channel = await _rest.GetChannelOrNull(channelId.Value);
|
||||
if (channel == null)
|
||||
throw new PKError("Unable to get the channel associated with this message.");
|
||||
|
||||
var rootChannel = await _cache.GetRootChannel(channel.Id);
|
||||
if (channel.GuildId == null)
|
||||
throw new PKError("PluralKit is not able to proxy messages in DMs.");
|
||||
|
||||
var rootChannel = await _cache.GetRootChannel(channel.GuildId!.Value, channel.Id);
|
||||
|
||||
// using channel.GuildId here since _rest.GetMessage() doesn't return the GuildId
|
||||
var context = await ctx.Repository.GetMessageContext(msg.Author.Id, channel.GuildId.Value, rootChannel.Id, msg.ChannelId);
|
||||
var members = (await ctx.Repository.GetProxyMembers(msg.Author.Id, channel.GuildId.Value)).ToList();
|
||||
|
|
|
|||
|
|
@ -536,4 +536,10 @@ public class Config
|
|||
}
|
||||
else throw new PKError(badInputError);
|
||||
}
|
||||
|
||||
public Task LimitUpdate(Context ctx)
|
||||
{
|
||||
throw new PKError("You cannot update your own member or group limits. If you need a limit update, please join the " +
|
||||
"support server and ask in #bot-support: https://discord.gg/PczBt78");
|
||||
}
|
||||
}
|
||||
|
|
@ -53,41 +53,55 @@ public class GroupMember
|
|||
|
||||
public async Task ListMemberGroups(Context ctx, PKMember target)
|
||||
{
|
||||
var pctx = ctx.DirectLookupContextFor(target.System);
|
||||
var targetSystem = await ctx.Repository.GetSystem(target.System);
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
|
||||
opts.MemberFilter = target.Id;
|
||||
|
||||
var groups = await ctx.Repository.GetMemberGroups(target.Id)
|
||||
.Where(g => g.Visibility.CanAccess(pctx))
|
||||
.OrderBy(g => (g.DisplayName ?? g.Name), StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToListAsync();
|
||||
|
||||
var description = "";
|
||||
var msg = "";
|
||||
|
||||
if (groups.Count == 0)
|
||||
description = "This member has no groups.";
|
||||
else
|
||||
description = string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.DisplayName ?? g.Name}**"));
|
||||
|
||||
if (pctx == LookupContext.ByOwner)
|
||||
var title = new StringBuilder($"Groups containing {target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
|
||||
if (ctx.Guild != null)
|
||||
{
|
||||
msg +=
|
||||
$"\n\nTo add this member to one or more groups, use `pk;m {target.Reference(ctx)} group add <group> [group 2] [group 3...]`";
|
||||
if (groups.Count > 0)
|
||||
msg +=
|
||||
$"\nTo remove this member from one or more groups, use `pk;m {target.Reference(ctx)} group remove <group> [group 2] [group 3...]`";
|
||||
var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id);
|
||||
if (guildSettings.DisplayName != null)
|
||||
title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else if (targetSystem.NameFor(ctx) != null)
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targetSystem.NameFor(ctx) != null)
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
|
||||
}
|
||||
if (opts.Search != null)
|
||||
title.Append($" matching **{opts.Search.Truncate(100)}**");
|
||||
|
||||
await ctx.Reply(msg, new EmbedBuilder().Title($"{target.Name}'s groups").Description(description).Build());
|
||||
await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(),
|
||||
target.Color, opts);
|
||||
}
|
||||
|
||||
public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op)
|
||||
{
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
var members = (await ctx.ParseMemberList(ctx.System.Id))
|
||||
List<MemberId> members;
|
||||
if (ctx.MatchFlag("all", "a"))
|
||||
{
|
||||
members = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
|
||||
new DatabaseViewsExt.ListQueryOptions { })))
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
members = (await ctx.ParseMemberList(ctx.System.Id))
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var existingMembersInGroup = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
|
||||
new DatabaseViewsExt.ListQueryOptions { GroupFilter = target.Id })))
|
||||
|
|
@ -127,7 +141,7 @@ public class GroupMember
|
|||
var targetSystem = await GetGroupSystem(ctx, target);
|
||||
ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy);
|
||||
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
|
||||
opts.GroupFilter = target.Id;
|
||||
|
||||
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
|
||||
|
|
|
|||
|
|
@ -132,40 +132,47 @@ public class Groups
|
|||
|
||||
// No perms check, display name isn't covered by member privacy
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noDisplayNameSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing displayname for group {target.Reference(ctx)}");
|
||||
await ctx.Reply(target.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noDisplayNameSetMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Field(new Embed.Field("Name", target.Name))
|
||||
.Field(new Embed.Field("Display Name", target.DisplayName));
|
||||
var eb = new EmbedBuilder()
|
||||
.Field(new Embed.Field("Name", target.Name))
|
||||
.Field(new Embed.Field("Display Name", target.DisplayName));
|
||||
|
||||
var reference = target.Reference(ctx);
|
||||
var reference = target.Reference(ctx);
|
||||
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Description(
|
||||
$"To change display name, type `pk;group {reference} displayname <display name>`.\n"
|
||||
+ $"To clear it, type `pk;group {reference} displayname -clear`.\n"
|
||||
+ $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Description(
|
||||
$"To change display name, type `pk;group {reference} displayname <display name>`.\n"
|
||||
+ $"To clear it, type `pk;group {reference} displayname -clear`.\n"
|
||||
+ $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
|
||||
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
|
||||
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -184,6 +191,8 @@ public class Groups
|
|||
else
|
||||
{
|
||||
var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
|
||||
if (newDisplayName.Length > Limits.MaxGroupNameLength)
|
||||
throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
|
||||
|
||||
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
|
||||
await ctx.Repository.UpdateGroup(target.Id, patch);
|
||||
|
|
@ -201,30 +210,41 @@ public class Groups
|
|||
noDescriptionSetMessage +=
|
||||
$" To set one, type `pk;group {target.Reference(ctx)} description <description>`.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Description == null)
|
||||
{
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for group {target.Reference(ctx)}");
|
||||
await ctx.Reply(target.Description, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Description == null)
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -385,7 +405,7 @@ public class Groups
|
|||
public async Task GroupColor(Context ctx, PKGroup target)
|
||||
{
|
||||
var isOwnSystem = ctx.System?.Id == target.System;
|
||||
var matchedRaw = ctx.MatchRaw();
|
||||
var matchedFormat = ctx.MatchFormat();
|
||||
var matchedClear = ctx.MatchClear();
|
||||
|
||||
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
|
||||
|
|
@ -393,8 +413,10 @@ public class Groups
|
|||
if (target.Color == null)
|
||||
await ctx.Reply(
|
||||
"This group does not have a color set." + (isOwnSystem ? $" To set one, type `pk;group {target.Reference(ctx)} color <color>`." : ""));
|
||||
else if (matchedRaw)
|
||||
else if (matchedFormat == ReplyFormat.Raw)
|
||||
await ctx.Reply("```\n#" + target.Color + "\n```");
|
||||
else if (matchedFormat == ReplyFormat.Plaintext)
|
||||
await ctx.Reply(target.Color);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group color")
|
||||
|
|
@ -446,7 +468,7 @@ public class Groups
|
|||
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
|
||||
// - RenderGroupList checks the indivual privacy for each member (NameFor, etc)
|
||||
// the own system is always allowed to look up their list
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id));
|
||||
await ctx.RenderGroupList(
|
||||
ctx.LookupContextFor(system.Id),
|
||||
system.Id,
|
||||
|
|
@ -460,8 +482,8 @@ public class Groups
|
|||
{
|
||||
var title = new StringBuilder("Groups of ");
|
||||
|
||||
if (target.Name != null)
|
||||
title.Append($"{target.Name} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
if (target.NameFor(ctx) != null)
|
||||
title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{target.DisplayHid(ctx.Config)}`");
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,26 @@ public class Help
|
|||
|
||||
private static Dictionary<string, Embed.Field[]> helpEmbedPages = new Dictionary<string, Embed.Field[]>
|
||||
{
|
||||
{
|
||||
"default",
|
||||
new Embed.Field[]
|
||||
{
|
||||
new
|
||||
(
|
||||
"System Recovery",
|
||||
"In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. "
|
||||
+ "In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. "
|
||||
+ "To get it, run `pk;token` and then store it in a safe place.\n\n"
|
||||
+ "Keep your token safe, if other people get access to it they can also use it to access your system. "
|
||||
+ "If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one."
|
||||
),
|
||||
new
|
||||
(
|
||||
"Use the buttons below to see more info!",
|
||||
""
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"basicinfo",
|
||||
new Embed.Field[]
|
||||
|
|
@ -31,7 +51,7 @@ public class Help
|
|||
(
|
||||
"Why are people's names saying [APP] or [BOT] next to them?",
|
||||
"These people are not actually apps or bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation."
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -137,7 +157,9 @@ public class Help
|
|||
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!**" } },
|
||||
Content = $"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ](<https://pluralkit.me/faq/#why-do-most-of-pluralkit-s-messages-look-blank-or-empty>)",
|
||||
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description,
|
||||
Fields = helpEmbedPages.GetValueOrDefault("default") } },
|
||||
Components = new[] { helpPageButtons(ctx.Author.Id) },
|
||||
});
|
||||
|
||||
|
|
@ -151,7 +173,7 @@ public class Help
|
|||
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!**" } },
|
||||
Embeds = new[] { helpEmbed with { Fields = helpEmbedPages.GetValueOrDefault("default") } },
|
||||
Components = new[] { buttons }
|
||||
});
|
||||
|
||||
|
|
@ -172,4 +194,6 @@ public class Help
|
|||
});
|
||||
|
||||
public Task Explain(Context ctx) => ctx.Reply(explanation);
|
||||
|
||||
public Task Dashboard(Context ctx) => ctx.Reply("The PluralKit dashboard is at <https://dash.pluralkit.me>");
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ namespace PluralKit.Bot;
|
|||
|
||||
public static class ContextListExt
|
||||
{
|
||||
public static ListOptions ParseListOptions(this Context ctx, LookupContext lookupCtx)
|
||||
public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext)
|
||||
{
|
||||
var p = new ListOptions();
|
||||
|
||||
|
|
@ -55,10 +55,13 @@ public static class ContextListExt
|
|||
if (ctx.MatchFlag("private-only", "po")) p.PrivacyFilter = PrivacyLevel.Private;
|
||||
|
||||
// PERM CHECK: If we're trying to access non-public members of another system, error
|
||||
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
|
||||
if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner)
|
||||
// TODO: should this just return null instead of throwing or something? >.>
|
||||
throw Errors.NotOwnInfo;
|
||||
|
||||
//this is for searching
|
||||
p.Context = lookupContext;
|
||||
|
||||
// Additional fields to include in the search results
|
||||
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
|
||||
p.IncludeLastSwitch = true;
|
||||
|
|
@ -124,11 +127,14 @@ public static class ContextListExt
|
|||
|
||||
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
|
||||
{
|
||||
// if there are both 5 and 6 character Hids they should be padded to align correctly.
|
||||
var shouldPad = page.Any(x => x.Hid.Length > 5);
|
||||
|
||||
// We may end up over the description character limit
|
||||
// so run it through a helper that "makes it work" :)
|
||||
eb.WithSimpleLineContent(page.Select(m =>
|
||||
{
|
||||
var ret = $"[`{m.DisplayHid(ctx.Config, isList: true)}`] **{m.NameFor(ctx)}** ";
|
||||
var ret = $"[`{m.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{m.NameFor(ctx)}** ";
|
||||
|
||||
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
|
||||
ret += $"({count} messages)";
|
||||
|
|
@ -234,11 +240,14 @@ public static class ContextListExt
|
|||
|
||||
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedGroup> page)
|
||||
{
|
||||
// if there are both 5 and 6 character Hids they should be padded to align correctly.
|
||||
var shouldPad = page.Any(x => x.Hid.Length > 5);
|
||||
|
||||
// We may end up over the description character limit
|
||||
// so run it through a helper that "makes it work" :)
|
||||
eb.WithSimpleLineContent(page.Select(g =>
|
||||
{
|
||||
var ret = $"[`{g.DisplayHid(ctx.Config, isList: true)}`] **{g.NameFor(ctx)}** ";
|
||||
var ret = $"[`{g.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{g.NameFor(ctx)}** ";
|
||||
|
||||
switch (opts.SortProperty)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ public class ListOptions
|
|||
public bool Reverse { get; set; }
|
||||
|
||||
public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public;
|
||||
public LookupContext Context { get; set; } = LookupContext.ByNonOwner;
|
||||
public GroupId? GroupFilter { get; set; }
|
||||
public MemberId? MemberFilter { get; set; }
|
||||
public string? Search { get; set; }
|
||||
public bool SearchDescription { get; set; }
|
||||
|
||||
|
|
@ -96,8 +98,10 @@ public class ListOptions
|
|||
{
|
||||
PrivacyFilter = PrivacyFilter,
|
||||
GroupFilter = GroupFilter,
|
||||
MemberFilter = MemberFilter,
|
||||
Search = Search,
|
||||
SearchDescription = SearchDescription
|
||||
SearchDescription = SearchDescription,
|
||||
Context = Context
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,30 +70,41 @@ public class MemberEdit
|
|||
noDescriptionSetMessage +=
|
||||
$" To set one, type `pk;member {target.Reference(ctx)} description <description>`.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Description == null)
|
||||
{
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for member {target.Reference(ctx)}");
|
||||
await ctx.Reply(target.Description, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Description == null)
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,26 +137,37 @@ public class MemberEdit
|
|||
|
||||
ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy);
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Pronouns == null)
|
||||
{
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing pronouns for member {target.Reference(ctx)}");
|
||||
await ctx.Reply(target.Pronouns, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Pronouns == null)
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply(
|
||||
$"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
await ctx.Reply(
|
||||
$"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -232,7 +254,7 @@ public class MemberEdit
|
|||
public async Task Color(Context ctx, PKMember target)
|
||||
{
|
||||
var isOwnSystem = ctx.System?.Id == target.System;
|
||||
var matchedRaw = ctx.MatchRaw();
|
||||
var matchedFormat = ctx.MatchFormat();
|
||||
var matchedClear = ctx.MatchClear();
|
||||
|
||||
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
|
||||
|
|
@ -240,8 +262,10 @@ public class MemberEdit
|
|||
if (target.Color == null)
|
||||
await ctx.Reply(
|
||||
"This member does not have a color set." + (isOwnSystem ? $" To set one, type `pk;member {target.Reference(ctx)} color <color>`." : ""));
|
||||
else if (matchedRaw)
|
||||
else if (matchedFormat == ReplyFormat.Raw)
|
||||
await ctx.Reply("```\n#" + target.Color + "\n```");
|
||||
else if (matchedFormat == ReplyFormat.Plaintext)
|
||||
await ctx.Reply(target.Color);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member color")
|
||||
|
|
@ -388,12 +412,26 @@ public class MemberEdit
|
|||
|
||||
// No perms check, display name isn't covered by member privacy
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if what's next is "raw"/"plaintext" we need to check for null
|
||||
if (format != ReplyFormat.Standard)
|
||||
if (target.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noDisplayNameSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing displayname for member {target.Reference(ctx)}");
|
||||
await ctx.Reply(target.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -450,12 +488,26 @@ public class MemberEdit
|
|||
// No perms check, display name isn't covered by member privacy
|
||||
var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id);
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if what's next is "raw"/"plaintext" we need to check for null
|
||||
if (format != ReplyFormat.Standard)
|
||||
if (memberGuildConfig.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noServerNameSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing servername for member {target.Reference(ctx)}");
|
||||
await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class MemberProxy
|
|||
if (!ctx.HasNext(false))
|
||||
throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`).");
|
||||
|
||||
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false));
|
||||
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
|
||||
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
|
||||
if (target.ProxyTags.Contains(tagToAdd))
|
||||
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
|
||||
|
|
@ -87,10 +87,17 @@ public class MemberProxy
|
|||
if (!ctx.HasNext(false))
|
||||
throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`).");
|
||||
|
||||
var tagToRemove = ParseProxyTags(ctx.RemainderOrNull(false));
|
||||
var remainder = ctx.RemainderOrNull(false);
|
||||
var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing());
|
||||
if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
|
||||
if (!target.ProxyTags.Contains(tagToRemove))
|
||||
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
|
||||
{
|
||||
// Legacy support for when line endings weren't normalized
|
||||
tagToRemove = ParseProxyTags(remainder);
|
||||
if (!target.ProxyTags.Contains(tagToRemove))
|
||||
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
|
||||
}
|
||||
|
||||
|
||||
var newTags = target.ProxyTags.ToList();
|
||||
newTags.Remove(tagToRemove);
|
||||
|
|
@ -102,7 +109,7 @@ public class MemberProxy
|
|||
// Subcommand: bare proxy tag given
|
||||
else
|
||||
{
|
||||
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false));
|
||||
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
|
||||
if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
|
||||
|
||||
// This is mostly a legacy command, so it's gonna warn if there's
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ public class ProxiedMessage
|
|||
try
|
||||
{
|
||||
var editedMsg =
|
||||
await _webhookExecutor.EditWebhookMessage(msg.Channel, msg.Mid, newContent, clearEmbeds);
|
||||
await _webhookExecutor.EditWebhookMessage(msg.Guild ?? 0, msg.Channel, msg.Mid, newContent, clearEmbeds);
|
||||
|
||||
if (ctx.Guild == null)
|
||||
await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success });
|
||||
|
|
@ -352,7 +352,9 @@ public class ProxiedMessage
|
|||
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
||||
showContent = false;
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
if (format != ReplyFormat.Standard)
|
||||
{
|
||||
var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid);
|
||||
if (discordMessage == null || !showContent)
|
||||
|
|
@ -365,21 +367,32 @@ public class ProxiedMessage
|
|||
return;
|
||||
}
|
||||
|
||||
await ctx.Reply($"```{content}```");
|
||||
|
||||
if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline))
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
await ctx.Rest.CreateMessage(
|
||||
ctx.Channel.Id,
|
||||
new MessageRequest
|
||||
{
|
||||
Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment."
|
||||
},
|
||||
new[] { new MultipartFile("message.txt", stream, null, null, null) });
|
||||
await ctx.Reply($"```{content}```");
|
||||
|
||||
if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline))
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
await ctx.Rest.CreateMessage(
|
||||
ctx.Channel.Id,
|
||||
new MessageRequest
|
||||
{
|
||||
Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment."
|
||||
},
|
||||
new[] { new MultipartFile("message.txt", stream, null, null, null) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing contents of message {message.Message.Mid}");
|
||||
await ctx.Reply(content, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDelete)
|
||||
|
|
@ -387,7 +400,7 @@ public class ProxiedMessage
|
|||
if (!showContent)
|
||||
throw new PKError(noShowContentError);
|
||||
|
||||
if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.Author.Id)
|
||||
if (message.Message.Sender != ctx.Author.Id && (ctx.System != null && message.System?.Id != ctx.System.Id))
|
||||
throw new PKError("You can only delete your own messages.");
|
||||
|
||||
await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid);
|
||||
|
|
@ -423,14 +436,14 @@ public class ProxiedMessage
|
|||
|
||||
private async Task DeleteCommandMessage(Context ctx, ulong messageId)
|
||||
{
|
||||
var (authorId, channelId) = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (authorId == null)
|
||||
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (cmessage == null)
|
||||
throw Errors.MessageNotFound(messageId);
|
||||
|
||||
if (authorId != ctx.Author.Id)
|
||||
if (cmessage!.AuthorId != ctx.Author.Id)
|
||||
throw new PKError("You can only delete command messages queried by this account.");
|
||||
|
||||
await ctx.Rest.DeleteMessage(channelId!.Value, messageId);
|
||||
await ctx.Rest.DeleteMessage(cmessage.ChannelId, messageId);
|
||||
|
||||
if (ctx.Guild != null)
|
||||
await ctx.Rest.DeleteMessage(ctx.Message);
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public class Random
|
|||
{
|
||||
ctx.CheckSystemPrivacy(group.System, group.ListPrivacy);
|
||||
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System));
|
||||
opts.GroupFilter = group.Id;
|
||||
|
||||
var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions()));
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public class ServerConfig
|
|||
if (channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildPublicThread && channel.Type != Channel.ChannelType.GuildPrivateThread)
|
||||
throw new PKError("PluralKit cannot log messages to this type of channel.");
|
||||
|
||||
var perms = await _cache.BotPermissionsIn(channel.Id);
|
||||
var perms = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id);
|
||||
if (!perms.HasFlag(PermissionSet.SendMessages))
|
||||
throw new PKError("PluralKit is missing **Send Messages** permissions in the new log channel.");
|
||||
if (!perms.HasFlag(PermissionSet.EmbedLinks))
|
||||
|
|
@ -104,7 +104,7 @@ public class ServerConfig
|
|||
|
||||
// Resolve all channels from the cache and order by position
|
||||
var channels = (await Task.WhenAll(blacklist.Blacklist
|
||||
.Select(id => _cache.TryGetChannel(id))))
|
||||
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
|
||||
.Where(c => c != null)
|
||||
.OrderBy(c => c.Position)
|
||||
.ToList();
|
||||
|
|
@ -121,7 +121,7 @@ public class ServerConfig
|
|||
async (eb, l) =>
|
||||
{
|
||||
async Task<string> CategoryName(ulong? id) =>
|
||||
id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";
|
||||
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
|
||||
|
||||
ulong? lastCategory = null;
|
||||
|
||||
|
|
@ -153,8 +153,9 @@ public class ServerConfig
|
|||
var config = await ctx.Repository.GetGuild(ctx.Guild.Id);
|
||||
|
||||
// Resolve all channels from the cache and order by position
|
||||
// todo: GetAllChannels?
|
||||
var channels = (await Task.WhenAll(config.LogBlacklist
|
||||
.Select(id => _cache.TryGetChannel(id))))
|
||||
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
|
||||
.Where(c => c != null)
|
||||
.OrderBy(c => c.Position)
|
||||
.ToList();
|
||||
|
|
@ -171,7 +172,7 @@ public class ServerConfig
|
|||
async (eb, l) =>
|
||||
{
|
||||
async Task<string> CategoryName(ulong? id) =>
|
||||
id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";
|
||||
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
|
||||
|
||||
ulong? lastCategory = null;
|
||||
|
||||
|
|
@ -204,7 +205,8 @@ public class ServerConfig
|
|||
var affectedChannels = new List<Channel>();
|
||||
if (ctx.Match("all"))
|
||||
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
|
||||
.Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
|
||||
// All the channel types you can proxy in
|
||||
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
|
||||
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
|
||||
else
|
||||
while (ctx.HasNext())
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace PluralKit.Bot;
|
|||
|
||||
public class Switch
|
||||
{
|
||||
|
||||
public async Task SwitchDo(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
|
@ -103,12 +104,69 @@ public class Switch
|
|||
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
|
||||
}
|
||||
|
||||
public async Task SwitchEdit(Context ctx)
|
||||
public async Task SwitchEdit(Context ctx, bool newSwitch = false)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var members = await ctx.ParseMemberList(ctx.System.Id);
|
||||
await DoEditCommand(ctx, members);
|
||||
var newMembers = await ctx.ParseMemberList(ctx.System.Id);
|
||||
|
||||
await using var conn = await ctx.Database.Obtain();
|
||||
var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id);
|
||||
if (currentSwitch == null)
|
||||
throw Errors.NoRegisteredSwitches;
|
||||
var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask();
|
||||
|
||||
if (ctx.MatchFlag("first", "f"))
|
||||
newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers);
|
||||
else if (ctx.MatchFlag("remove", "r"))
|
||||
newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers);
|
||||
else if (ctx.MatchFlag("append", "a"))
|
||||
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
|
||||
else if (ctx.MatchFlag("prepend", "p"))
|
||||
newMembers = PrependToSwitch(newMembers, currentSwitchMembers);
|
||||
|
||||
if (newSwitch)
|
||||
{
|
||||
// if there's no edit flag, assume we're appending
|
||||
if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p"))
|
||||
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
|
||||
await DoSwitchCommand(ctx, newMembers);
|
||||
}
|
||||
else
|
||||
await DoEditCommand(ctx, newMembers);
|
||||
}
|
||||
|
||||
public List<PKMember> PrependToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
members.AddRange(currentSwitchMembers);
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public List<PKMember> AppendToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
currentSwitchMembers.AddRange(members);
|
||||
members = currentSwitchMembers;
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public List<PKMember> RemoveFromSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
var memberIds = members.Select(m => m.Id.Value);
|
||||
currentSwitchMembers = currentSwitchMembers.Where(m => !memberIds.Contains(m.Id.Value)).ToList();
|
||||
members = currentSwitchMembers;
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public List<PKMember> FirstInSwitch(PKMember member, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
currentSwitchMembers = currentSwitchMembers.Where(m => m.Id != member.Id).ToList();
|
||||
var members = new List<PKMember> { member };
|
||||
members.AddRange(currentSwitchMembers);
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public async Task SwitchEditOut(Context ctx)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ using PluralKit.Core;
|
|||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
using Myriad.Builders;
|
||||
using Myriad.Types;
|
||||
|
||||
public class System
|
||||
{
|
||||
private readonly EmbedService _embeds;
|
||||
|
|
@ -29,9 +32,25 @@ public class System
|
|||
var system = await ctx.Repository.CreateSystem(systemName);
|
||||
await ctx.Repository.AddAccount(system.Id, ctx.Author.Id);
|
||||
|
||||
// TODO: better message, perhaps embed like in groups?
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;system help` for more information about commands you can use now. Now that you have that set up, check out the getting started guide on setting up members and proxies: <https://pluralkit.me/start>");
|
||||
var eb = new EmbedBuilder()
|
||||
.Title(
|
||||
$"{Emojis.Success} Your system has been created.")
|
||||
.Field(new Embed.Field("Getting Started",
|
||||
"New to PK? Check out our Getting Started guide on setting up members and proxies: https://pluralkit.me/start\n" +
|
||||
"Otherwise, type `pk;system` to view your system and `pk;system help` for more information about commands you can use."))
|
||||
.Field(new Embed.Field($"{Emojis.Warn} Notice {Emojis.Warn}", "PluralKit is a bot meant to help you share information about your system. " +
|
||||
"Member descriptions are meant to be the equivalent to a Discord About Me. Because of this, any info you put in PK is **public by default**.\n" +
|
||||
"Note that this does **not** include message content, only member fields. For more information, check out " +
|
||||
"[the privacy section of the user guide](https://pluralkit.me/guide/#privacy). "))
|
||||
.Field(new Embed.Field("System Recovery", "In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. " +
|
||||
"In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. " +
|
||||
"To get it, run `pk;token` and then store it in a safe place.\n\n" +
|
||||
"Keep your token safe, if other people get access to it they can also use it to access your system. " +
|
||||
"If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one."))
|
||||
.Field(new Embed.Field("Questions?",
|
||||
"Please join the PK server https://discord.gg/PczBt78 if you have any questions, we're happy to help"));
|
||||
await ctx.Reply($"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ](<https://pluralkit.me/faq/#why-do-most-of-pluralkit-s-messages-look-blank-or-empty>)", eb.Build());
|
||||
|
||||
}
|
||||
|
||||
public async Task DisplayId(Context ctx, PKSystem target)
|
||||
|
|
|
|||
|
|
@ -37,24 +37,35 @@ public class SystemEdit
|
|||
if (isOwnSystem)
|
||||
noNameSetMessage += " Type `pk;system name <name>` to set one.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
if (target.Name != null)
|
||||
await ctx.Reply($"```\n{target.Name}\n```");
|
||||
else
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Name == null)
|
||||
{
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"` ``\n{target.Name}\n` ``");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing name for system {target.DisplayHid()}");
|
||||
await ctx.Reply(target.Name, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Name != null)
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**."
|
||||
+ (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "")
|
||||
+ $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
else
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**."
|
||||
+ (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "")
|
||||
+ $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -91,24 +102,35 @@ public class SystemEdit
|
|||
|
||||
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
if (settings.DisplayName != null)
|
||||
await ctx.Reply($"```\n{settings.DisplayName}\n```");
|
||||
else
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (settings.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"` ``\n{settings.DisplayName}\n` ``");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing servername for system {target.DisplayHid()}");
|
||||
await ctx.Reply(settings.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (settings.DisplayName != null)
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**."
|
||||
+ (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "")
|
||||
+ $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
else
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**."
|
||||
+ (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "")
|
||||
+ $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -143,28 +165,39 @@ public class SystemEdit
|
|||
if (isOwnSystem)
|
||||
noDescriptionSetMessage += " To set one, type `pk;s description <description>`.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Description == null)
|
||||
{
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"` ``\n{target.Description}\n` ``");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for system {target.DisplayHid()}");
|
||||
await ctx.Reply(target.Description, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Description == null)
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System description")
|
||||
.Description(target.Description)
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
"To print the description with formatting, type `pk;s description -raw`."
|
||||
+ (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description <new description>`." : "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System description")
|
||||
.Description(target.Description)
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
"To print the description with formatting, type `pk;s description -raw`."
|
||||
+ (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description <new description>`." : "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +224,7 @@ public class SystemEdit
|
|||
public async Task Color(Context ctx, PKSystem target)
|
||||
{
|
||||
var isOwnSystem = ctx.System?.Id == target.Id;
|
||||
var matchedRaw = ctx.MatchRaw();
|
||||
var matchedFormat = ctx.MatchFormat();
|
||||
var matchedClear = ctx.MatchClear();
|
||||
|
||||
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
|
||||
|
|
@ -199,8 +232,10 @@ public class SystemEdit
|
|||
if (target.Color == null)
|
||||
await ctx.Reply(
|
||||
"This system does not have a color set." + (isOwnSystem ? " To set one, type `pk;system color <color>`." : ""));
|
||||
else if (matchedRaw)
|
||||
else if (matchedFormat == ReplyFormat.Raw)
|
||||
await ctx.Reply("```\n#" + target.Color + "\n```");
|
||||
else if (matchedFormat == ReplyFormat.Plaintext)
|
||||
await ctx.Reply(target.Color);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System color")
|
||||
|
|
@ -246,22 +281,33 @@ public class SystemEdit
|
|||
? "You currently have no system tag set. To set one, type `pk;s tag <tag>`."
|
||||
: "This system currently has no system tag set.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Tag == null)
|
||||
{
|
||||
await ctx.Reply(noTagSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Tag}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Tag}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing tag for system {target.DisplayHid()}");
|
||||
await ctx.Reply(target.Tag, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Tag == null)
|
||||
await ctx.Reply(noTagSetMessage);
|
||||
else
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}."
|
||||
+ (isOwnSystem ? "To change it, type `pk;s tag <tag>`. To clear it, type `pk;s tag -clear`." : ""));
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}."
|
||||
+ (isOwnSystem ? "To change it, type `pk;s tag <tag>`. To clear it, type `pk;s tag -clear`." : ""));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -296,15 +342,22 @@ public class SystemEdit
|
|||
|
||||
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
|
||||
|
||||
async Task Show(bool raw = false)
|
||||
async Task Show(ReplyFormat format = ReplyFormat.Standard)
|
||||
{
|
||||
if (settings.Tag != null)
|
||||
{
|
||||
if (raw)
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```{settings.Tag}```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing servertag for system {target.DisplayHid()}");
|
||||
await ctx.Reply(settings.Tag, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = $"Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}";
|
||||
if (!settings.TagEnabled)
|
||||
|
|
@ -400,8 +453,8 @@ public class SystemEdit
|
|||
await EnableDisable(false);
|
||||
else if (ctx.Match("enable") || ctx.MatchFlag("enable"))
|
||||
await EnableDisable(true);
|
||||
else if (ctx.MatchRaw())
|
||||
await Show(true);
|
||||
else if (ctx.MatchFormat() != ReplyFormat.Standard)
|
||||
await Show(ctx.MatchFormat());
|
||||
else if (!ctx.HasNext(false))
|
||||
await Show();
|
||||
else
|
||||
|
|
@ -418,24 +471,35 @@ public class SystemEdit
|
|||
if (isOwnSystem)
|
||||
noPronounsSetMessage += " To set some, type `pk;system pronouns <pronouns>`";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Pronouns == null)
|
||||
{
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing pronouns for system {target.DisplayHid()}");
|
||||
await ctx.Reply(target.Pronouns, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Pronouns == null)
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`."
|
||||
+ (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`."
|
||||
+ (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class SystemList
|
|||
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
|
||||
// - RenderMemberList checks the indivual privacy for each member (NameFor, etc)
|
||||
// the own system is always allowed to look up their list
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id));
|
||||
await ctx.RenderMemberList(
|
||||
ctx.LookupContextFor(target.Id),
|
||||
target.Id,
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ public interface IEventHandler<in T> where T : IGatewayEvent
|
|||
{
|
||||
Task Handle(int shardId, T evt);
|
||||
|
||||
ulong? ErrorChannelFor(T evt, ulong userId) => null;
|
||||
(ulong?, ulong?) ErrorChannelFor(T evt, ulong userId) => (null, null);
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
_dmCache = dmCache;
|
||||
}
|
||||
|
||||
public ulong? ErrorChannelFor(MessageCreateEvent evt, ulong userId) => evt.ChannelId;
|
||||
public (ulong?, ulong?) ErrorChannelFor(MessageCreateEvent evt, ulong userId) => (evt.GuildId, evt.ChannelId);
|
||||
private bool IsDuplicateMessage(Message msg) =>
|
||||
// We consider a message duplicate if it has the same ID as the previous message that hit the gateway
|
||||
_lastMessageCache.GetLastMessage(msg.ChannelId)?.Current.Id == msg.Id;
|
||||
|
|
@ -63,7 +63,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply) return;
|
||||
if (IsDuplicateMessage(evt)) return;
|
||||
|
||||
var botPermissions = await _cache.BotPermissionsIn(evt.ChannelId);
|
||||
var botPermissions = await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId);
|
||||
if (!botPermissions.HasFlag(PermissionSet.SendMessages)) return;
|
||||
|
||||
// spawn off saving the private channel into another thread
|
||||
|
|
@ -71,8 +71,8 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
_ = _dmCache.TrySavePrivateChannel(evt);
|
||||
|
||||
var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null;
|
||||
var channel = await _cache.GetChannel(evt.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(evt.ChannelId);
|
||||
var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(evt.GuildId ?? 0, evt.ChannelId);
|
||||
|
||||
// Log metrics and message info
|
||||
_metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
|
||||
|
|
@ -90,7 +90,8 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
if (await TryHandleCommand(shardId, evt, guild, channel))
|
||||
return;
|
||||
|
||||
await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions);
|
||||
if (evt.GuildId != null)
|
||||
await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions);
|
||||
}
|
||||
|
||||
private async Task TryHandleLogClean(Channel channel, MessageCreateEvent evt)
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
|
||||
return;
|
||||
|
||||
var channel = await _cache.GetChannel(evt.ChannelId);
|
||||
var guildIdMaybe = evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0;
|
||||
|
||||
var channel = await _cache.GetChannel(guildIdMaybe, evt.ChannelId); // todo: is this correct for message update?
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel))
|
||||
return;
|
||||
var rootChannel = await _cache.GetRootChannel(channel.Id);
|
||||
var rootChannel = await _cache.GetRootChannel(guildIdMaybe, channel.Id);
|
||||
var guild = await _cache.GetGuild(channel.GuildId!.Value);
|
||||
var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId)?.Current;
|
||||
|
||||
|
|
@ -69,7 +71,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId);
|
||||
|
||||
var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);
|
||||
var botPermissions = await _cache.BotPermissionsIn(channel.Id);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildIdMaybe, channel.Id);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -91,7 +93,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
private async Task<MessageCreateEvent> GetMessageCreateEvent(MessageUpdateEvent evt, CachedMessage lastMessage,
|
||||
Channel channel)
|
||||
{
|
||||
var referencedMessage = await GetReferencedMessage(evt.ChannelId, lastMessage.ReferencedMessage);
|
||||
var referencedMessage = await GetReferencedMessage(evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0, evt.ChannelId, lastMessage.ReferencedMessage);
|
||||
|
||||
var messageReference = lastMessage.ReferencedMessage != null
|
||||
? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value)
|
||||
|
|
@ -118,12 +120,12 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
return equivalentEvt;
|
||||
}
|
||||
|
||||
private async Task<Message?> GetReferencedMessage(ulong channelId, ulong? referencedMessageId)
|
||||
private async Task<Message?> GetReferencedMessage(ulong guildId, ulong channelId, ulong? referencedMessageId)
|
||||
{
|
||||
if (referencedMessageId == null)
|
||||
return null;
|
||||
|
||||
var botPermissions = await _cache.BotPermissionsIn(channelId);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildId, channelId);
|
||||
if (!botPermissions.HasFlag(PermissionSet.ReadMessageHistory))
|
||||
{
|
||||
_logger.Warning(
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
// but we aren't able to get DMs from bots anyway, so it's not really needed
|
||||
if (evt.GuildId != null && (evt.Member?.User?.Bot ?? false)) return;
|
||||
|
||||
var channel = await _cache.GetChannel(evt.ChannelId);
|
||||
var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId);
|
||||
|
||||
// check if it's a command message first
|
||||
// since this can happen in DMs as well
|
||||
|
|
@ -75,10 +75,10 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
return;
|
||||
}
|
||||
|
||||
var (authorId, _) = await _commandMessageService.GetCommandMessage(evt.MessageId);
|
||||
if (authorId != null)
|
||||
var cmessage = await _commandMessageService.GetCommandMessage(evt.MessageId);
|
||||
if (cmessage != null)
|
||||
{
|
||||
await HandleCommandDeleteReaction(evt, authorId.Value, false);
|
||||
await HandleCommandDeleteReaction(evt, cmessage.AuthorId, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, PKMessage msg)
|
||||
{
|
||||
if (!(await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
return;
|
||||
|
||||
var isSameSystem = msg.Member != null && await _repo.IsMemberOwnedByAccount(msg.Member.Value, evt.UserId);
|
||||
|
|
@ -150,7 +150,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
if (authorId != null && authorId != evt.UserId)
|
||||
return;
|
||||
|
||||
if (!((await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
if (!((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
return;
|
||||
|
||||
// todo: don't try to delete the user's own messages in DMs
|
||||
|
|
@ -206,14 +206,14 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg)
|
||||
{
|
||||
if (!(await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
return;
|
||||
|
||||
// Check if the "pinger" has permission to send messages in this channel
|
||||
// (if not, PK shouldn't send messages on their behalf)
|
||||
var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId);
|
||||
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
|
||||
if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.ChannelId, member)).HasFlag(requiredPerms)) return;
|
||||
if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.GuildId ?? 0, evt.ChannelId, member)).HasFlag(requiredPerms)) return;
|
||||
|
||||
if (msg.Member == null) return;
|
||||
|
||||
|
|
@ -266,7 +266,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt)
|
||||
{
|
||||
if ((await _cache.BotPermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
if ((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
await _rest.DeleteUserReaction(evt.ChannelId, evt.MessageId, evt.Emoji, evt.UserId);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,10 +42,10 @@ public class Init
|
|||
|
||||
using var _ = SentrySdk.Init(opts =>
|
||||
{
|
||||
opts.Dsn = services.Resolve<CoreConfig>().SentryUrl;
|
||||
opts.Dsn = services.Resolve<CoreConfig>().SentryUrl ?? "";
|
||||
opts.Release = BuildInfoService.FullVersion;
|
||||
opts.AutoSessionTracking = true;
|
||||
opts.DisableTaskUnobservedTaskExceptionCapture();
|
||||
// opts.DisableTaskUnobservedTaskExceptionCapture();
|
||||
});
|
||||
|
||||
var config = services.Resolve<BotConfig>();
|
||||
|
|
@ -56,8 +56,6 @@ public class Init
|
|||
await redis.InitAsync(coreConfig);
|
||||
|
||||
var cache = services.Resolve<IDiscordCache>();
|
||||
if (cache is RedisDiscordCache)
|
||||
await (cache as RedisDiscordCache).InitAsync(coreConfig.RedisAddr);
|
||||
|
||||
if (config.Cluster == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -84,20 +84,32 @@ public class YesNoPrompt: BaseInteractive
|
|||
|
||||
var queue = _ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>();
|
||||
|
||||
var messageDispatch = queue.WaitFor(MessagePredicate, Timeout, cts.Token);
|
||||
async Task WaitForMessage()
|
||||
{
|
||||
try
|
||||
{
|
||||
await queue.WaitFor(MessagePredicate, Timeout, cts.Token);
|
||||
}
|
||||
catch (TimeoutException e)
|
||||
{
|
||||
if (e.Message != "HandlerQueue#WaitFor timed out")
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
await Start();
|
||||
|
||||
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
|
||||
var messageDispatch = WaitForMessage();
|
||||
|
||||
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("YesNoPrompt timed out")));
|
||||
|
||||
try
|
||||
{
|
||||
var doneTask = await Task.WhenAny(_tcs.Task, messageDispatch);
|
||||
if (doneTask == messageDispatch)
|
||||
await Finish();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Finish();
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,25 @@ public class BotModule: Module
|
|||
{
|
||||
var botConfig = c.Resolve<BotConfig>();
|
||||
|
||||
if (botConfig.UseRedisCache)
|
||||
return new RedisDiscordCache(c.Resolve<ILogger>(), botConfig.ClientId);
|
||||
if (botConfig.HttpCacheUrl != null)
|
||||
{
|
||||
var cache = new HttpDiscordCache(c.Resolve<ILogger>(),
|
||||
c.Resolve<HttpClient>(), botConfig.HttpCacheUrl, botConfig.Cluster?.TotalShards ?? 1, botConfig.ClientId, botConfig.HttpUseInnerCache);
|
||||
|
||||
var metrics = c.Resolve<IMetrics>();
|
||||
|
||||
cache.OnDebug += (_, ev) =>
|
||||
{
|
||||
var (remote, key) = ev;
|
||||
metrics.Measure.Meter.Mark(BotMetrics.CacheDebug, new MetricTags(
|
||||
new[] { "remote", "key" },
|
||||
new[] { remote.ToString(), key }
|
||||
));
|
||||
};
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
return new MemoryDiscordCache(botConfig.ClientId);
|
||||
}).AsSelf().SingleInstance();
|
||||
builder.RegisterType<PrivateChannelService>().AsSelf().SingleInstance();
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.32.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.47.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
|
||||
<PackageReference Include="Sentry" Version="3.11.1" />
|
||||
<PackageReference Include="Sentry" Version="4.12.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public class ProxyService
|
|||
public async Task<bool> HandleIncomingMessage(MessageCreateEvent message, MessageContext ctx,
|
||||
Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions)
|
||||
{
|
||||
var rootChannel = await _cache.GetRootChannel(message.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(message.GuildId!.Value, message.ChannelId);
|
||||
|
||||
if (!ShouldProxy(channel, rootChannel, message, ctx))
|
||||
return false;
|
||||
|
|
@ -111,31 +111,10 @@ public class ProxyService
|
|||
return true;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
internal bool CanProxyInChannel(Channel ch, bool isRootChannel = false)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
// this is explicitly selecting known channel types so that when Discord add new
|
||||
// ones, users don't get flooded with error codes if that new channel type doesn't
|
||||
// support a feature we need for proxying
|
||||
return ch.Type switch
|
||||
{
|
||||
Channel.ChannelType.GuildText => true,
|
||||
Channel.ChannelType.GuildPublicThread => true,
|
||||
Channel.ChannelType.GuildPrivateThread => true,
|
||||
Channel.ChannelType.GuildNews => true,
|
||||
Channel.ChannelType.GuildNewsThread => true,
|
||||
Channel.ChannelType.GuildVoice => true,
|
||||
Channel.ChannelType.GuildStageVoice => true,
|
||||
Channel.ChannelType.GuildForum => isRootChannel,
|
||||
Channel.ChannelType.GuildMedia => isRootChannel,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// Proxy checks that give user errors
|
||||
public async Task<string> CanProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
|
||||
{
|
||||
if (!(CanProxyInChannel(channel) && CanProxyInChannel(rootChannel, true)))
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel))
|
||||
return $"PluralKit cannot proxy messages in this type of channel.";
|
||||
|
||||
// Check if the message does not go over any Discord Nitro limits
|
||||
|
|
@ -159,6 +138,7 @@ public class ProxyService
|
|||
return null;
|
||||
}
|
||||
|
||||
// Proxy checks that don't give user errors unless `pk;debug proxy` is used
|
||||
public bool ShouldProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
|
||||
{
|
||||
// Make sure author has a system
|
||||
|
|
@ -189,9 +169,9 @@ public class ProxyService
|
|||
throw new ProxyChecksFailedException(
|
||||
"Your system has proxying disabled in this server. Type `pk;proxy on` to enable it.");
|
||||
|
||||
// Make sure we have either an attachment or message content
|
||||
// Make sure we have an attachment, message content, or poll
|
||||
var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0;
|
||||
if (isMessageBlank && msg.Attachments.Length == 0)
|
||||
if (isMessageBlank && msg.Attachments.Length == 0 && msg.Poll == null)
|
||||
throw new ProxyChecksFailedException("Message cannot be blank.");
|
||||
|
||||
if (msg.Activity != null)
|
||||
|
|
@ -227,8 +207,8 @@ public class ProxyService
|
|||
var content = match.ProxyContent;
|
||||
if (!allowEmbeds) content = content.BreakLinkEmbeds();
|
||||
|
||||
var messageChannel = await _cache.GetChannel(trigger.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);
|
||||
var messageChannel = await _cache.GetChannel(trigger.GuildId!.Value, trigger.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(trigger.GuildId!.Value, trigger.ChannelId);
|
||||
var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
|
||||
var guild = await _cache.GetGuild(trigger.GuildId.Value);
|
||||
var guildMember = await _rest.GetGuildMember(trigger.GuildId!.Value, trigger.Author.Id);
|
||||
|
|
@ -242,6 +222,7 @@ public class ProxyService
|
|||
GuildId = trigger.GuildId!.Value,
|
||||
ChannelId = rootChannel.Id,
|
||||
ThreadId = threadId,
|
||||
MessageId = trigger.Id,
|
||||
Name = await FixSameName(messageChannel.Id, ctx, match.Member),
|
||||
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
|
||||
Content = content,
|
||||
|
|
@ -252,6 +233,7 @@ public class ProxyService
|
|||
AllowEveryone = allowEveryone,
|
||||
Flags = trigger.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null,
|
||||
Tts = tts,
|
||||
Poll = trigger.Poll,
|
||||
});
|
||||
await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match);
|
||||
}
|
||||
|
|
@ -310,6 +292,7 @@ public class ProxyService
|
|||
GuildId = guild.Id,
|
||||
ChannelId = rootChannel.Id,
|
||||
ThreadId = threadId,
|
||||
MessageId = originalMsg.Id,
|
||||
Name = match.Member.ProxyName(ctx),
|
||||
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
|
||||
Content = match.ProxyContent!,
|
||||
|
|
@ -320,6 +303,7 @@ public class ProxyService
|
|||
AllowEveryone = allowEveryone,
|
||||
Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null,
|
||||
Tts = tts,
|
||||
Poll = originalMsg.Poll,
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,22 +9,35 @@ public class AvatarHostingService
|
|||
private readonly BotConfig _config;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public AvatarHostingService(BotConfig config, HttpClient client)
|
||||
public AvatarHostingService(BotConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_client = client;
|
||||
_client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ParsedImage> TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId, PKSystem? system)
|
||||
{
|
||||
var uploaded = await TryUploadAvatar(input.Url, type, userId, system);
|
||||
if (uploaded != null)
|
||||
try
|
||||
{
|
||||
// todo: make new image type called Cdn?
|
||||
return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn };
|
||||
}
|
||||
var uploaded = await TryUploadAvatar(input.Url, type, userId, system);
|
||||
if (uploaded != null)
|
||||
{
|
||||
// todo: make new image type called Cdn?
|
||||
return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn };
|
||||
}
|
||||
|
||||
return input;
|
||||
return input;
|
||||
}
|
||||
catch (TaskCanceledException e)
|
||||
{
|
||||
// don't show an internal error to users
|
||||
if (e.Message.Contains("HttpClient.Timeout"))
|
||||
throw new PKError("Temporary error setting image, please try again later");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> TryUploadAvatar(string? avatarUrl, RehostedImageType type, ulong userId, PKSystem? system)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public class CommandMessageService
|
|||
_logger = logger.ForContext<CommandMessageService>();
|
||||
}
|
||||
|
||||
public async Task RegisterMessage(ulong messageId, ulong channelId, ulong authorId)
|
||||
public async Task RegisterMessage(ulong messageId, ulong guildId, ulong channelId, ulong authorId)
|
||||
{
|
||||
if (_redis.Connection == null) return;
|
||||
|
||||
|
|
@ -27,17 +27,19 @@ public class CommandMessageService
|
|||
messageId, authorId, channelId
|
||||
);
|
||||
|
||||
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}", expiry: CommandMessageRetention);
|
||||
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}-{guildId}", expiry: CommandMessageRetention);
|
||||
}
|
||||
|
||||
public async Task<(ulong?, ulong?)> GetCommandMessage(ulong messageId)
|
||||
public async Task<CommandMessage?> 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 new CommandMessage(ulong.Parse(split[0]), ulong.Parse(split[1]), ulong.Parse(split[2]));
|
||||
}
|
||||
return (null, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record CommandMessage(ulong AuthorId, ulong ChannelId, ulong GuildId);
|
||||
|
|
@ -263,7 +263,7 @@ public class EmbedService
|
|||
else if (system.NameFor(ctx) != null)
|
||||
nameField = $"{nameField} ({system.NameFor(ctx)})";
|
||||
else
|
||||
nameField = $"{nameField} ({system.Name})";
|
||||
nameField = $"{nameField}";
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}"))
|
||||
|
|
@ -336,7 +336,7 @@ public class EmbedService
|
|||
|
||||
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null)
|
||||
{
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel);
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel);
|
||||
var ctx = LookupContext.ByNonOwner;
|
||||
|
||||
var serverMsg = await _rest.GetMessageOrNull(msg.Message.Channel, msg.Message.Mid);
|
||||
|
|
@ -403,14 +403,15 @@ public class EmbedService
|
|||
var roles = memberInfo?.Roles?.ToList();
|
||||
if (roles != null && roles.Count > 0 && showContent)
|
||||
{
|
||||
var rolesString = string.Join(", ", (await Task.WhenAll(roles
|
||||
.Select(async id =>
|
||||
var guild = await _cache.GetGuild(channel.GuildId!.Value);
|
||||
var rolesString = string.Join(", ", (roles
|
||||
.Select(id =>
|
||||
{
|
||||
var role = await _cache.TryGetRole(id);
|
||||
var role = Array.Find(guild.Roles, r => r.Id == id);
|
||||
if (role != null)
|
||||
return role;
|
||||
return new Role { Name = "*(unknown role)*", Position = 0 };
|
||||
})))
|
||||
}))
|
||||
.OrderByDescending(role => role.Position)
|
||||
.Select(role => role.Name));
|
||||
eb.Field(new Embed.Field($"Account roles ({roles.Count})", rolesString.Truncate(1024)));
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class LogChannelService
|
|||
if (logChannelId == null)
|
||||
return;
|
||||
|
||||
var triggerChannel = await _cache.GetChannel(proxiedMessage.Channel);
|
||||
var triggerChannel = await _cache.GetChannel(proxiedMessage.Guild!.Value, proxiedMessage.Channel);
|
||||
|
||||
var member = await _repo.GetMember(proxiedMessage.Member!.Value);
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
|
@ -63,7 +63,7 @@ public class LogChannelService
|
|||
return null;
|
||||
|
||||
var guildId = proxiedMessage.Guild ?? trigger.GuildId.Value;
|
||||
var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(guildId, trigger.ChannelId);
|
||||
|
||||
// get log channel info from the database
|
||||
var guild = await _repo.GetGuild(guildId);
|
||||
|
|
@ -109,7 +109,7 @@ public class LogChannelService
|
|||
private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId)
|
||||
{
|
||||
// TODO: fetch it directly on cache miss?
|
||||
if (await _cache.TryGetChannel(channelId) is Channel channel)
|
||||
if (await _cache.TryGetChannel(guildId, channelId) is Channel channel)
|
||||
return channel;
|
||||
|
||||
if (await _rest.GetChannelOrNull(channelId) is Channel restChannel)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public class LoggerCleanService
|
|||
private static readonly Regex _basicRegex = new("(\\d{17,19})");
|
||||
private static readonly Regex _dynoRegex = new("Message ID: (\\d{17,19})");
|
||||
private static readonly Regex _carlRegex = new("Message ID: (\\d{17,19})");
|
||||
private static readonly Regex _makiRegex = new("Message ID: (\\d{17,19})");
|
||||
private static readonly Regex _circleRegex = new("\\(`(\\d{17,19})`\\)");
|
||||
private static readonly Regex _loggerARegex = new("Message = (\\d{17,19})");
|
||||
private static readonly Regex _loggerBRegex = new("MessageID:(\\d{17,19})");
|
||||
|
|
@ -60,6 +61,7 @@ public class LoggerCleanService
|
|||
new LoggerBot("Dyno#8389", 470724017205149701, ExtractDyno), // webhook
|
||||
new LoggerBot("Dyno#5714", 470723870270160917, ExtractDyno), // webhook
|
||||
new LoggerBot("Dyno#1961", 347378323418251264, ExtractDyno), // webhook
|
||||
new LoggerBot("Maki", 563434444321587202, ExtractMaki), // webhook
|
||||
new LoggerBot("Auttaja", 242730576195354624, ExtractAuttaja), // webhook
|
||||
new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot),
|
||||
new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot),
|
||||
|
|
@ -98,10 +100,10 @@ public class LoggerCleanService
|
|||
|
||||
public async ValueTask HandleLoggerBotCleanup(Message msg)
|
||||
{
|
||||
var channel = await _cache.GetChannel(msg.ChannelId);
|
||||
var channel = await _cache.GetChannel(msg.GuildId!.Value, msg.ChannelId!);
|
||||
|
||||
if (channel.Type != Channel.ChannelType.GuildText) return;
|
||||
if (!(await _cache.BotPermissionsIn(channel.Id)).HasFlag(PermissionSet.ManageMessages)) return;
|
||||
if (!(await _cache.BotPermissionsIn(msg.GuildId!.Value, channel.Id)).HasFlag(PermissionSet.ManageMessages)) return;
|
||||
|
||||
// If this message is from a *webhook*, check if the application ID matches one of the bots we know
|
||||
// If it's from a *bot*, check the bot ID to see if we know it.
|
||||
|
|
@ -231,6 +233,15 @@ public class LoggerCleanService
|
|||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
private static ulong? ExtractMaki(Message msg)
|
||||
{
|
||||
// Embed, Message Author Name field: "Message Deleted", footer is "Message ID: [id]"
|
||||
var embed = msg.Embeds?.FirstOrDefault();
|
||||
if (embed.Author?.Name == null || embed?.Footer == null || !embed.Author.Name.StartsWith("Message Deleted")) return null;
|
||||
var match = _makiRegex.Match(embed.Footer.Text ?? "");
|
||||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
private static FuzzyExtractResult? ExtractCircle(Message msg)
|
||||
{
|
||||
// Like Auttaja, Circle has both embed and compact modes, but the regex works for both.
|
||||
|
|
|
|||
|
|
@ -54,33 +54,6 @@ public class PeriodicStatCollector
|
|||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
// Aggregate guild/channel stats
|
||||
var guildCount = 0;
|
||||
var channelCount = 0;
|
||||
|
||||
// No LINQ today, sorry
|
||||
await foreach (var guild in _cache.GetAllGuilds())
|
||||
{
|
||||
guildCount++;
|
||||
foreach (var channel in await _cache.GetGuildChannels(guild.Id))
|
||||
if (DiscordUtils.IsValidGuildChannel(channel))
|
||||
channelCount++;
|
||||
}
|
||||
|
||||
if (_config.UseRedisMetrics)
|
||||
{
|
||||
var db = _redis.Connection.GetDatabase();
|
||||
await db.HashSetAsync("pluralkit:cluster_stats", new StackExchange.Redis.HashEntry[] {
|
||||
new(_botConfig.Cluster.NodeIndex, JsonConvert.SerializeObject(new ClusterMetricInfo
|
||||
{
|
||||
GuildCount = guildCount,
|
||||
ChannelCount = channelCount,
|
||||
DatabaseConnectionCount = _countHolder.ConnectionCount,
|
||||
WebhookCacheSize = _webhookCache.CacheSize,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Process info
|
||||
var process = Process.GetCurrentProcess();
|
||||
_metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessPhysicalMemory, process.WorkingSet64);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using App.Metrics;
|
|||
|
||||
using Humanizer;
|
||||
|
||||
using NodaTime.Text;
|
||||
|
||||
using Myriad.Cache;
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Rest;
|
||||
|
|
@ -35,6 +37,7 @@ public record ProxyRequest
|
|||
public ulong GuildId { get; init; }
|
||||
public ulong ChannelId { get; init; }
|
||||
public ulong? ThreadId { get; init; }
|
||||
public ulong MessageId { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string? AvatarUrl { get; init; }
|
||||
public string? Content { get; init; }
|
||||
|
|
@ -45,6 +48,7 @@ public record ProxyRequest
|
|||
public bool AllowEveryone { get; init; }
|
||||
public Message.MessageFlags? Flags { get; init; }
|
||||
public bool Tts { get; init; }
|
||||
public Message.MessagePoll? Poll { get; init; }
|
||||
}
|
||||
|
||||
public class WebhookExecutorService
|
||||
|
|
@ -83,7 +87,7 @@ public class WebhookExecutorService
|
|||
return webhookMessage;
|
||||
}
|
||||
|
||||
public async Task<Message> EditWebhookMessage(ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false)
|
||||
public async Task<Message> EditWebhookMessage(ulong guildId, ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false)
|
||||
{
|
||||
var allowedMentions = newContent.ParseMentions() with
|
||||
{
|
||||
|
|
@ -92,7 +96,7 @@ public class WebhookExecutorService
|
|||
};
|
||||
|
||||
ulong? threadId = null;
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, channelId);
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, guildId, channelId);
|
||||
if (channel.IsThread())
|
||||
{
|
||||
threadId = channelId;
|
||||
|
|
@ -154,6 +158,26 @@ public class WebhookExecutorService
|
|||
}).ToArray();
|
||||
}
|
||||
|
||||
if (req.Poll is Message.MessagePoll poll)
|
||||
{
|
||||
int? duration = null;
|
||||
if (poll.Expiry is string expiry)
|
||||
{
|
||||
var then = OffsetDateTimePattern.ExtendedIso.Parse(expiry).Value.ToInstant();
|
||||
var now = DiscordUtils.SnowflakeToInstant(req.MessageId);
|
||||
// in theory .TotalHours should be exact, but just in case
|
||||
duration = (int)Math.Round((then - now).TotalMinutes / 60.0);
|
||||
}
|
||||
webhookReq.Poll = new ExecuteWebhookRequest.WebhookPoll
|
||||
{
|
||||
Question = poll.Question,
|
||||
Answers = poll.Answers,
|
||||
Duration = duration,
|
||||
AllowMultiselect = poll.AllowMultiselect,
|
||||
LayoutType = poll.LayoutType
|
||||
};
|
||||
}
|
||||
|
||||
Message webhookMessage;
|
||||
using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -49,17 +49,17 @@ public static class MiscUtils
|
|||
if (e is WebhookExecutionErrorOnDiscordsEnd) return false;
|
||||
|
||||
// Socket errors are *not our problem*
|
||||
if (e.GetBaseException() is SocketException) return false;
|
||||
// if (e.GetBaseException() is SocketException) return false;
|
||||
|
||||
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
|
||||
if (e is TaskCanceledException) return false;
|
||||
// if (e is TaskCanceledException) return false;
|
||||
|
||||
// Sometimes Discord just times everything out.
|
||||
if (e is TimeoutException) return false;
|
||||
// if (e is TimeoutException) return false;
|
||||
if (e is UnknownDiscordRequestException tde && tde.Message == "Request Timeout") return false;
|
||||
|
||||
// HTTP/2 streams are complicated and break sometimes.
|
||||
if (e is HttpRequestException) return false;
|
||||
// if (e is HttpRequestException) return false;
|
||||
|
||||
// This may expanded at some point.
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ public static class ModelUtils
|
|||
|
||||
|
||||
public static string DisplayHid(this PKSystem system, SystemConfig? cfg = null, bool isList = false) => HidTransform(system.Hid, cfg, isList);
|
||||
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false) => HidTransform(group.Hid, cfg, isList);
|
||||
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false) => HidTransform(member.Hid, cfg, isList);
|
||||
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false) =>
|
||||
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(group.Hid, cfg, isList, shouldPad);
|
||||
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(member.Hid, cfg, isList, shouldPad);
|
||||
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) =>
|
||||
HidUtils.HidTransform(
|
||||
hid,
|
||||
cfg != null && cfg.HidDisplaySplit,
|
||||
cfg != null && cfg.HidDisplayCaps,
|
||||
isList ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists
|
||||
isList && shouldPad ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists
|
||||
);
|
||||
|
||||
private static string EntityReference(string hid, string name)
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ public class SerilogGatewayEnricherFactory
|
|||
{
|
||||
props.Add(new LogEventProperty("ChannelId", new ScalarValue(channel.Value)));
|
||||
|
||||
if (await _cache.TryGetChannel(channel.Value) != null)
|
||||
var guildIdForCache = guild != null ? guild.Value : 0;
|
||||
|
||||
if (await _cache.TryGetChannel(guildIdForCache, channel.Value) != null)
|
||||
{
|
||||
var botPermissions = await _cache.BotPermissionsIn(channel.Value);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildIdForCache, channel.Value);
|
||||
props.Add(new LogEventProperty("BotPermissions", new ScalarValue(botPermissions)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@
|
|||
},
|
||||
"Sentry": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.11.1, )",
|
||||
"resolved": "3.11.1",
|
||||
"contentHash": "T/NLfs6MMkUSYsPEDajB9ad0124T18I0uUod5MNOev3iwjvcnIEQBrStEX2olbIxzqfvGXzQ/QFqTfA2ElLPlA=="
|
||||
"requested": "[4.12.1, )",
|
||||
"resolved": "4.12.1",
|
||||
"contentHash": "OLf7885OKHWLaTLTyw884mwOT4XKCWj2Hz5Wuz/TJemJqXwCIdIljkJBIoeHviRUPvtB7ulDgeYXf/Z7ScToSA=="
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue