run dotnet format

This commit is contained in:
spiral 2021-08-27 11:03:47 -04:00
parent 05989242f9
commit ac2671452d
No known key found for this signature in database
GPG key ID: A6059F0CA0E1BD31
278 changed files with 1913 additions and 1808 deletions

View file

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -22,7 +22,7 @@ namespace PluralKit.Bot
public async Task UpdateSystemId(Context ctx)
{
AssertBotAdmin(ctx);
var target = await ctx.MatchSystem();
if (target == null)
throw new PKError("Unknown system.");
@ -38,14 +38,14 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
await _db.Execute(c => _repo.UpdateSystem(c, target.Id, new SystemPatch {Hid = newHid}));
await _db.Execute(c => _repo.UpdateSystem(c, target.Id, new SystemPatch { Hid = newHid }));
await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`).");
}
public async Task UpdateMemberId(Context ctx)
{
AssertBotAdmin(ctx);
var target = await ctx.MatchMember();
if (target == null)
throw new PKError("Unknown member.");
@ -60,8 +60,8 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch {Hid = newHid}));
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch { Hid = newHid }));
await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`).");
}
@ -84,7 +84,7 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?", "Change"))
throw new PKError("ID change cancelled.");
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Hid = newHid}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { Hid = newHid }));
await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`).");
}
@ -109,7 +109,7 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update"))
throw new PKError("Member limit change cancelled.");
await using var conn = await _db.Obtain();
await _repo.UpdateSystem(conn, target.Id, new SystemPatch
{

View file

@ -27,7 +27,7 @@ namespace PluralKit.Bot
{
// no need to check account here, it's already done at CommandTree
ctx.CheckGuildContext();
if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove"))
await AutoproxyOff(ctx);
else if (ctx.Match("latch", "last", "proxy", "stick", "sticky"))
@ -90,7 +90,7 @@ namespace PluralKit.Bot
var commandList = "**pk;autoproxy latch** - Autoproxies as last-proxied member\n**pk;autoproxy front** - Autoproxies as current (first) fronter\n**pk;autoproxy <member>** - Autoproxies as a specific member";
var eb = new EmbedBuilder()
.Title($"Current autoproxy status (for {ctx.Guild.Name.EscapeMarkdown()})");
var fronters = ctx.MessageContext.LastSwitchMembers;
var relevantMember = ctx.MessageContext.AutoproxyMode switch
{
@ -99,36 +99,38 @@ namespace PluralKit.Bot
_ => null
};
switch (ctx.MessageContext.AutoproxyMode) {
switch (ctx.MessageContext.AutoproxyMode)
{
case AutoproxyMode.Off:
eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}");
break;
case AutoproxyMode.Front:
{
if (fronters.Length == 0)
eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch.");
else
{
if (relevantMember == null)
throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
}
if (fronters.Length == 0)
eb.Description("Autoproxy is currently set to **front mode** in this server, but there are currently no fronters registered. Use the `pk;switch` command to log a switch.");
else
{
if (relevantMember == null)
throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
}
break;
}
break;
}
// AutoproxyMember is never null if Mode is Member, this is just to make the compiler shut up
case AutoproxyMode.Member when relevantMember != null: {
eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
break;
}
case AutoproxyMode.Member when relevantMember != null:
{
eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
break;
}
case AutoproxyMode.Latch:
eb.Description("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. To disable, type `pk;autoproxy off`.");
break;
default: throw new ArgumentOutOfRangeException();
}
if (!ctx.MessageContext.AllowAutoproxy)
if (!ctx.MessageContext.AllowAutoproxy)
eb.Field(new("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`."));
return eb.Build();
@ -139,9 +141,9 @@ namespace PluralKit.Bot
if (!ctx.HasNext())
{
var timeout = ctx.System.LatchTimeout.HasValue
? Duration.FromSeconds(ctx.System.LatchTimeout.Value)
: (Duration?) null;
? Duration.FromSeconds(ctx.System.LatchTimeout.Value)
: (Duration?)null;
if (timeout == null)
await ctx.Reply($"You do not have a custom autoproxy timeout duration set. The default latch timeout duration is {ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}.");
else if (timeout == Duration.Zero)
@ -169,9 +171,9 @@ namespace PluralKit.Bot
else newTimeout = timeoutPeriod;
}
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id,
new SystemPatch { LatchTimeout = (int?) newTimeout?.TotalSeconds }));
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id,
new SystemPatch { LatchTimeout = (int?)newTimeout?.TotalSeconds }));
if (newTimeout == null)
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize(4)}).");
else if (newTimeout == Duration.Zero && overflow != Duration.Zero)
@ -213,7 +215,7 @@ namespace PluralKit.Bot
private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember)
{
var patch = new SystemGuildPatch {AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember};
var patch = new SystemGuildPatch { AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember };
return _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
}
}

View file

@ -16,9 +16,9 @@ namespace PluralKit.Bot
if (await ctx.MatchUser() is { } user)
{
var url = user.AvatarUrl("png", 256);
return new ParsedImage {Url = url, Source = AvatarSource.User, SourceUser = user};
return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user };
}
// If we have a positional argument, try to parse it as a URL
var arg = ctx.RemainderOrNull();
if (arg != null)
@ -26,24 +26,24 @@ namespace PluralKit.Bot
// Allow surrounding the URL with <angle brackets> to "de-embed"
if (arg.StartsWith("<") && arg.EndsWith(">"))
arg = arg.Substring(1, arg.Length - 2);
if (!Uri.TryCreate(arg, UriKind.Absolute, out var uri))
throw Errors.InvalidUrl(arg);
if (uri.Scheme != "http" && uri.Scheme != "https")
throw Errors.InvalidUrl(arg);
// ToString URL-decodes, which breaks URLs to spaces; AbsoluteUri doesn't
return new ParsedImage {Url = uri.AbsoluteUri, Source = AvatarSource.Url};
return new ParsedImage { Url = uri.AbsoluteUri, Source = AvatarSource.Url };
}
// If we have an attachment, use that
if (ctx.Message.Attachments.FirstOrDefault() is {} attachment)
if (ctx.Message.Attachments.FirstOrDefault() is { } attachment)
{
var url = attachment.ProxyUrl;
return new ParsedImage {Url = url, Source = AvatarSource.Attachment};
return new ParsedImage { Url = url, Source = AvatarSource.Attachment };
}
// We should only get here if there are no arguments (which would get parsed as URL + throw if error)
// and if there are no attachments (which would have been caught just before)
return null;
@ -63,4 +63,4 @@ namespace PluralKit.Bot
User,
Attachment
}
}
}

View file

@ -55,19 +55,22 @@ namespace PluralKit.Bot
if (!ulong.TryParse(guildIdStr, out var guildId))
throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID.");
try {
try
{
guild = await _rest.GetGuild(guildId);
} catch (Myriad.Rest.Exceptions.ForbiddenException) {
}
catch (Myriad.Rest.Exceptions.ForbiddenException)
{
throw Errors.GuildNotFound(guildId);
}
if (guild != null)
if (guild != null)
senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id);
if (guild == null || senderGuildUser == null)
if (guild == null || senderGuildUser == null)
throw Errors.GuildNotFound(guildId);
}
var requiredPermissions = new []
var requiredPermissions = new[]
{
PermissionSet.ViewChannel,
PermissionSet.SendMessages,
@ -87,7 +90,7 @@ namespace PluralKit.Bot
var botPermissions = _bot.PermissionsIn(channel.Id);
var webhookPermissions = _cache.EveryonePermissions(channel);
var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser);
if ((userPermissions & PermissionSet.ViewChannel) == 0)
{
// If the user can't see this channel, don't calculate permissions for it
@ -100,14 +103,14 @@ namespace PluralKit.Bot
// We use a bitfield so we can set individual permission bits in the loop
// TODO: Rewrite with proper bitfield math
ulong missingPermissionField = 0;
foreach (var requiredPermission in requiredPermissions)
if ((botPermissions & requiredPermission) == 0)
missingPermissionField |= (ulong) requiredPermission;
missingPermissionField |= (ulong)requiredPermission;
if ((webhookPermissions & PermissionSet.UseExternalEmojis) == 0)
{
missingPermissionField |= (ulong) PermissionSet.UseExternalEmojis;
missingPermissionField |= (ulong)PermissionSet.UseExternalEmojis;
missingEmojiPermissions = true;
}
@ -119,7 +122,7 @@ namespace PluralKit.Bot
permissionsMissing[missingPermissionField].Add(channel);
}
}
// Generate the output embed
var eb = new EmbedBuilder()
.Title($"Permission check for **{guild.Name}**");
@ -134,7 +137,7 @@ namespace PluralKit.Bot
{
// Each missing permission field can have multiple missing channels
// so we extract them all and generate a comma-separated list
var missingPermissionNames = ((PermissionSet) missingPermissionField).ToPermissionString();
var missingPermissionNames = ((PermissionSet)missingPermissionField).ToPermissionString();
var channelsList = string.Join("\n", channels
.OrderBy(c => c.Position)
@ -155,7 +158,7 @@ namespace PluralKit.Bot
if (footer.Length > 0)
eb.Footer(new(footer));
// Send! :)
await ctx.Reply(embed: eb.Build());
}
@ -164,7 +167,7 @@ namespace PluralKit.Bot
{
if (!ctx.HasNext() && ctx.Message.MessageReference == null)
throw new PKError("You need to specify a message.");
var failedToGetMessage = "Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you.";
var (messageId, channelId) = ctx.MatchMessage(false);
@ -182,7 +185,7 @@ namespace PluralKit.Bot
// get the message info
var msg = ctx.Message;
try
try
{
msg = await _rest.GetMessage(channelId.Value, messageId.Value);
}
@ -212,13 +215,14 @@ namespace PluralKit.Bot
var members = (await _repo.GetProxyMembers(conn, msg.Author.Id, channel.GuildId.Value)).ToList();
// Run everything through the checks, catch the ProxyCheckFailedException, and reply with the error message.
try
{
try
{
_proxy.ShouldProxy(channel, msg, context);
_matcher.TryMatch(context, members, out var match, msg.Content, msg.Attachments.Length > 0, context.AllowAutoproxy);
await ctx.Reply("I'm not sure why this message was not proxied, sorry.");
} catch (ProxyService.ProxyChecksFailedException e)
}
catch (ProxyService.ProxyChecksFailedException e)
{
await ctx.Reply($"{e.Message}");
}

View file

@ -120,13 +120,13 @@ namespace PluralKit.Bot
GroupDelete, GroupMemberRandom, GroupFrontPercent
};
public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll};
public static Command[] SwitchCommands = { Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll };
public static Command[] AutoproxyCommands = {AutoproxySet, AutoproxyTimeout, AutoproxyAccount};
public static Command[] LogCommands = {LogChannel, LogChannelClear, LogEnable, LogDisable};
public static Command[] AutoproxyCommands = { AutoproxySet, AutoproxyTimeout, AutoproxyAccount };
public static Command[] BlacklistCommands = {BlacklistAdd, BlacklistRemove, BlacklistShow};
public static Command[] LogCommands = { LogChannel, LogChannelClear, LogEnable, LogDisable };
public static Command[] BlacklistCommands = { BlacklistAdd, BlacklistRemove, BlacklistShow };
public Task ExecuteCommand(Context ctx)
{
@ -214,9 +214,9 @@ namespace PluralKit.Bot
return HandleAdminCommand(ctx);
if (ctx.Match("random", "r"))
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx));
else
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx));
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx));
else
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx));
// remove compiler warning
return ctx.Reply(
@ -348,7 +348,7 @@ namespace PluralKit.Bot
await PrintCommandNotFoundError(ctx, SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent,
SystemInfo);
}
private async Task HandleMemberCommand(Context ctx)
{
if (ctx.Match("new", "n", "add", "create", "register"))
@ -392,7 +392,7 @@ namespace PluralKit.Bot
await ctx.Execute<MemberGroup>(MemberGroupAdd, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Add));
else if (ctx.Match("remove", "rem"))
await ctx.Execute<MemberGroup>(MemberGroupRemove, m => m.AddRemove(ctx, target, Groups.AddRemoveOperation.Remove));
else
else
await ctx.Execute<MemberGroup>(MemberGroups, m => m.List(ctx, target));
else if (ctx.Match("serveravatar", "servericon", "serverimage", "serverpfp", "serverpic", "savatar", "spic", "guildavatar", "guildpic", "guildicon", "sicon"))
await ctx.Execute<MemberAvatar>(MemberServerAvatar, m => m.ServerAvatar(ctx, target));
@ -414,8 +414,8 @@ namespace PluralKit.Bot
await ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, target));
else if (!ctx.HasNext()) // Bare command
await ctx.Execute<Member>(MemberInfo, m => m.ViewMember(ctx, target));
else
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName ,MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
else
await PrintCommandNotFoundError(ctx, MemberInfo, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns, MemberColor, MemberBirthday, MemberProxy, MemberDelete, MemberAvatar, SystemList);
}
private async Task HandleGroupCommand(Context ctx)
@ -427,7 +427,7 @@ namespace PluralKit.Bot
await ctx.Execute<Groups>(GroupList, g => g.ListSystemGroups(ctx, null));
else if (ctx.Match("commands", "help"))
await PrintCommandList(ctx, "groups", GroupCommands);
else if (await ctx.MatchGroup() is {} target)
else if (await ctx.MatchGroup() is { } target)
{
// Commands with group argument
if (ctx.Match("rename", "name", "changename", "setname"))
@ -437,7 +437,7 @@ namespace PluralKit.Bot
else if (ctx.Match("description", "info", "bio", "text", "desc"))
await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
else if (ctx.Match("add", "a"))
await ctx.Execute<Groups>(GroupAdd,g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
await ctx.Execute<Groups>(GroupAdd, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
else if (ctx.Match("remove", "rem", "r"))
await ctx.Execute<Groups>(GroupRemove, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
else if (ctx.Match("members", "list", "ms", "l"))
@ -488,14 +488,15 @@ namespace PluralKit.Bot
}
private async Task CommandHelpRoot(Context ctx)
{
{
if (!ctx.HasNext())
{
await ctx.Reply($"{Emojis.Error} You need to pass a target command.\nAvailable command help targets: `system`, `member`, `group`, `switch`, `log`, `blacklist`.\nFor the full list of commands, see the website: <https://pluralkit.me/commands>");
return;
}
switch (ctx.PeekArgument()) {
switch (ctx.PeekArgument())
{
case "system":
case "systems":
case "s":
@ -561,14 +562,14 @@ namespace PluralKit.Bot
await ctx.Reply(
$"{Emojis.Error} Unknown command `pk;{ctx.FullCommand().Truncate(100)}`. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
}
private async Task PrintCommandExpectedError(Context ctx, params Command[] potentialCommands)
{
var commandListStr = CreatePotentialCommandList(potentialCommands);
await ctx.Reply(
$"{Emojis.Error} You need to pass a command. Perhaps you meant to use one of the following commands?\n{commandListStr}\n\nFor a full list of possible commands, see <https://pluralkit.me/commands>.");
}
private static string CreatePotentialCommandList(params Command[] potentialCommands)
{
return string.Join("\n", potentialCommands.Select(cmd => $"- **pk;{cmd.Usage}** - *{cmd.Description}*"));
@ -597,4 +598,4 @@ namespace PluralKit.Bot
return $"System with ID {input.AsCode()} not found.";
}
}
}
}

View file

@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace PluralKit.Bot
{

View file

@ -36,14 +36,14 @@ namespace PluralKit.Bot
public async Task CreateGroup(Context ctx)
{
ctx.CheckSystem();
// Check group name length
var groupName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a group name.");
if (groupName.Length > Limits.MaxGroupNameLength)
throw new PKError($"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters).");
await using var conn = await _db.Obtain();
// Check group cap
var existingGroupCount = await conn.QuerySingleAsync<int>("select count(*) from groups where system = @System", new { System = ctx.System.Id });
var groupLimit = ctx.System.GroupLimitOverride ?? Limits.MaxGroupCount;
@ -52,14 +52,15 @@ namespace PluralKit.Bot
// Warn if there's already a group by this name
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, groupName);
if (existingGroup != null) {
if (existingGroup != null)
{
var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?";
if (!await ctx.PromptYesNo(msg, "Create"))
throw new PKError("Group creation cancelled.");
}
var newGroup = await _repo.CreateGroup(conn, ctx.System.Id, groupName);
var eb = new EmbedBuilder()
.Description($"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:")
.Field(new("View the group card", $"> pk;group **{newGroup.Reference()}**"))
@ -72,23 +73,24 @@ namespace PluralKit.Bot
public async Task RenameGroup(Context ctx, PKGroup target)
{
ctx.CheckOwnGroup(target);
// Check group name length
var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new group name.");
if (newName.Length > Limits.MaxGroupNameLength)
throw new PKError($"New group name too long ({newName.Length}/{Limits.MaxMemberNameLength} characters).");
await using var conn = await _db.Obtain();
// Warn if there's already a group by this name
var existingGroup = await _repo.GetGroupByName(conn, ctx.System.Id, newName);
if (existingGroup != null && existingGroup.Id != target.Id) {
if (existingGroup != null && existingGroup.Id != target.Id)
{
var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to rename this group to that name too?";
if (!await ctx.PromptYesNo(msg, "Rename"))
throw new PKError("Group rename cancelled.");
}
await _repo.UpdateGroup(conn, target.Id, new GroupPatch {Name = newName});
await _repo.UpdateGroup(conn, target.Id, new GroupPatch { Name = newName });
await ctx.Reply($"{Emojis.Success} Group name changed from **{target.Name}** to **{newName}**.");
}
@ -98,8 +100,8 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this group's display name"))
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch {DisplayName = Partial<string>.Null()};
var patch = new GroupPatch { DisplayName = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group display name cleared.");
@ -124,40 +126,40 @@ namespace PluralKit.Bot
var eb = new EmbedBuilder()
.Field(new("Name", target.Name))
.Field(new("Display Name", target.DisplayName ?? "*(none)*"));
if (ctx.System?.Id == target.System)
eb.Description($"To change display name, type `pk;group {target.Reference()} displayname <display name>`.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`.\nTo print the raw display name, type `pk;group {target.Reference()} displayname -raw`.");
await ctx.Reply(embed: eb.Build());
}
}
else
{
ctx.CheckOwnGroup(target);
var newDisplayName = ctx.RemainderOrNull();
var patch = new GroupPatch {DisplayName = Partial<string>.Present(newDisplayName)};
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group display name changed.");
}
}
public async Task GroupDescription(Context ctx, PKGroup target)
{
if (await ctx.MatchClear("this group's description"))
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch {Description = Partial<string>.Null()};
var patch = new GroupPatch { Description = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group description cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
throw Errors.LookupNotAllowed;
if (target.Description == null)
if (ctx.System?.Id == target.System)
@ -170,7 +172,7 @@ namespace PluralKit.Bot
await ctx.Reply(embed: new EmbedBuilder()
.Title("Group description")
.Description(target.Description)
.Field(new("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`."
.Field(new("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`."
+ (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} description -clear`." : "")))
.Build());
}
@ -181,10 +183,10 @@ namespace PluralKit.Bot
var description = ctx.RemainderOrNull().NormalizeLineEndSpacing();
if (description.IsLongerThan(Limits.MaxDescriptionLength))
throw Errors.DescriptionTooLongError(description.Length);
var patch = new GroupPatch {Description = Partial<string>.Present(description)};
var patch = new GroupPatch { Description = Partial<string>.Present(description) };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group description changed.");
}
}
@ -194,19 +196,19 @@ namespace PluralKit.Bot
async Task ClearIcon()
{
ctx.CheckOwnGroup(target);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Icon = null}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { Icon = null }));
await ctx.Reply($"{Emojis.Success} Group icon cleared.");
}
async Task SetIcon(ParsedImage img)
{
ctx.CheckOwnGroup(target);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Icon = img.Url}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { Icon = img.Url }));
var msg = img.Source switch
{
AvatarSource.User => $"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.",
@ -214,11 +216,11 @@ namespace PluralKit.Bot
AvatarSource.Attachment => $"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -231,7 +233,7 @@ namespace PluralKit.Bot
var eb = new EmbedBuilder()
.Title("Group icon")
.Image(new(target.Icon.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
{
eb.Description($"To clear, use `pk;group {target.Reference()} icon -clear`.");
@ -245,7 +247,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this group's icon"))
await ClearIcon();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetIcon(img);
else
await ShowIcon();
@ -257,7 +259,7 @@ namespace PluralKit.Bot
{
ctx.CheckOwnGroup(target);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {BannerImage = null}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { BannerImage = null }));
await ctx.Reply($"{Emojis.Success} Group banner image cleared.");
}
@ -267,7 +269,7 @@ namespace PluralKit.Bot
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, isFullSizeImage: true);
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {BannerImage = img.Url}));
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch { BannerImage = img.Url }));
var msg = img.Source switch
{
@ -279,8 +281,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -307,7 +309,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this group's banner image"))
await ClearBannerImage();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetBannerImage(img);
else
await ShowBannerImage();
@ -319,10 +321,10 @@ namespace PluralKit.Bot
if (await ctx.MatchClear())
{
ctx.CheckOwnGroup(target);
var patch = new GroupPatch {Color = Partial<string>.Null()};
var patch = new GroupPatch { Color = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Group color cleared.");
}
else if (!ctx.HasNext())
@ -349,8 +351,8 @@ namespace PluralKit.Bot
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new GroupPatch {Color = Partial<string>.Present(color.ToLowerInvariant())};
var patch = new GroupPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch));
await ctx.Reply(embed: new EmbedBuilder()
@ -368,9 +370,9 @@ namespace PluralKit.Bot
ctx.CheckSystem();
system = ctx.System;
}
ctx.CheckSystemPrivacy(system, system.GroupListPrivacy);
// TODO: integrate with the normal "search" system
await using var conn = await _db.Obtain();
@ -382,25 +384,25 @@ namespace PluralKit.Bot
else
throw new PKError("You do not have permission to access this information.");
}
var groups = (await conn.QueryGroupList(system.Id))
.Where(g => g.Visibility.CanAccess(pctx))
.OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase)
.ToList();
if (groups.Count == 0)
{
if (system.Id == ctx.System?.Id)
await ctx.Reply("This system has no groups. To create one, use the command `pk;group new <name>`.");
else
await ctx.Reply("This system has no groups.");
return;
}
var title = system.Name != null ? $"Groups of {system.Name} (`{system.Hid}`)" : $"Groups of `{system.Hid}`";
await ctx.Paginate(groups.ToAsyncEnumerable(), groups.Count, 25, title, ctx.System.Color, Renderer);
Task Renderer(EmbedBuilder eb, IEnumerable<ListedGroup> page)
{
eb.WithSimpleLineContent(page.Select(g =>
@ -430,15 +432,15 @@ namespace PluralKit.Bot
.Select(m => m.Id)
.Distinct()
.ToList();
await using var conn = await _db.Obtain();
var existingMembersInGroup = (await conn.QueryMemberList(target.System,
new DatabaseViewsExt.MemberListQueryOptions {GroupFilter = target.Id}))
new DatabaseViewsExt.MemberListQueryOptions { GroupFilter = target.Id }))
.Select(m => m.Id.Value)
.Distinct()
.ToHashSet();
List<MemberId> toAction;
if (op == AddRemoveOperation.Add)
@ -463,21 +465,21 @@ namespace PluralKit.Bot
public async Task ListGroupMembers(Context ctx, PKGroup target)
{
await using var conn = await _db.Obtain();
var targetSystem = await GetGroupSystem(ctx, target, conn);
ctx.CheckSystemPrivacy(targetSystem, target.ListPrivacy);
var opts = ctx.ParseMemberListOptions(ctx.LookupContextFor(target.System));
opts.GroupFilter = target.Id;
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.Hid}`) in ");
if (targetSystem.Name != null)
if (targetSystem.Name != null)
title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)");
else
title.Append($"`{targetSystem.Hid}`");
if (opts.Search != null)
if (opts.Search != null)
title.Append($" matching **{opts.Search}**");
await ctx.RenderMemberList(ctx.LookupContextFor(target.System), _db, target.System, title.ToString(), target.Color, opts);
}
@ -495,29 +497,29 @@ namespace PluralKit.Bot
{
await ctx.Reply(embed: new EmbedBuilder()
.Title($"Current privacy settings for {target.Name}")
.Field(new("Description", target.DescriptionPrivacy.Explanation()) )
.Field(new("Description", target.DescriptionPrivacy.Explanation()))
.Field(new("Icon", target.IconPrivacy.Explanation()))
.Field(new("Member list", target.ListPrivacy.Explanation()))
.Field(new("Visibility", target.Visibility.Explanation()))
.Description($"To edit privacy settings, use the command:\n> pk;group **{target.Reference()}** privacy **<subject>** **<level>**\n\n- `subject` is one of `description`, `icon`, `members`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
.Build());
.Build());
return;
}
async Task SetAll(PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch().WithAllPrivacy(level)));
if (level == PrivacyLevel.Private)
await ctx.Reply($"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the group card.");
else
else
await ctx.Reply($"{Emojis.Success} All {target.Name}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the group card.");
}
async Task SetLevel(GroupPrivacySubject subject, PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch().WithPrivacy(subject, level)));
var subjectName = subject switch
{
GroupPrivacySubject.Description => "description privacy",
@ -526,22 +528,22 @@ namespace PluralKit.Bot
GroupPrivacySubject.Visibility => "visibility",
_ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}")
};
var explanation = (subject, level) switch
{
(GroupPrivacySubject.Description, PrivacyLevel.Private) => "This group's description is now hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Private) => "This group's icon is now hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Private) => "This group is now hidden from group lists and member cards.",
(GroupPrivacySubject.List, PrivacyLevel.Private) => "This group's member list is now hidden from other systems.",
(GroupPrivacySubject.Description, PrivacyLevel.Public) => "This group's description is no longer hidden from other systems.",
(GroupPrivacySubject.Icon, PrivacyLevel.Public) => "This group's icon is no longer hidden from other systems.",
(GroupPrivacySubject.Visibility, PrivacyLevel.Public) => "This group is no longer hidden from group lists and member cards.",
(GroupPrivacySubject.List, PrivacyLevel.Public) => "This group's member list is no longer hidden from other systems.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})")
};
await ctx.Reply($"{Emojis.Success} {target.Name}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}");
}
@ -560,14 +562,14 @@ namespace PluralKit.Bot
throw new PKError($"Group deletion cancelled. Note that you must reply with your group ID (`{target.Hid}`) *verbatim*.");
await _db.Execute(conn => _repo.DeleteGroup(conn, target.Id));
await ctx.Reply($"{Emojis.Success} Group deleted.");
}
public async Task GroupFrontPercent(Context ctx, PKGroup target)
public async Task GroupFrontPercent(Context ctx, PKGroup target)
{
await using var conn = await _db.Obtain();
var targetSystem = await GetGroupSystem(ctx, target, conn);
ctx.CheckSystemPrivacy(targetSystem, targetSystem.FrontHistoryPrivacy);
@ -575,7 +577,7 @@ namespace PluralKit.Bot
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
string durationStr = ctx.RemainderOrNull() ?? "30d";
var now = SystemClock.Instance.GetCurrentInstant();
var rangeStart = DateUtils.ParseDateTime(durationStr, true, targetSystem.Zone);
@ -583,7 +585,7 @@ namespace PluralKit.Bot
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
var title = new StringBuilder($"Frontpercent of {target.DisplayName ?? target.Name} (`{target.Hid}`) in ");
if (targetSystem.Name != null)
if (targetSystem.Name != null)
title.Append($"{targetSystem.Name} (`{targetSystem.Hid}`)");
else
title.Append($"`{targetSystem.Hid}`");

View file

@ -15,7 +15,7 @@ namespace PluralKit.Bot
.Description("PluralKit is a bot designed for plural communities on Discord. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.")
.Field(new("What is this for? What are systems?", "This bot detects messages with certain tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using webhooks. This is useful for multiple people sharing one body (aka \"systems\"), people who wish to roleplay as different characters without having several accounts, or anyone else who may want to post messages as a different person from the same account."))
.Field(new("Why are people's names saying [BOT] next to them?", "These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation."))
.Field(new("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the Getting Started guide](https://pluralkit.me/start) for more information."))
.Field(new("How do I get started?", "To get started using PluralKit, try running the following commands (of course replacing the relevant names with your own):\n**1**. `pk;system new` - Create a system (if you haven't already)\n**2**. `pk;member add John` - Add a new member to your system\n**3**. `pk;member John proxy [text]` - Set up [square brackets] as proxy tags\n**4**. You're done! You can now type [a message in brackets] and it'll be proxied appropriately.\n**5**. Optionally, you may set an avatar from the URL of an image with `pk;member John avatar [link to image]`, or from a file by typing `pk;member John avatar` and sending the message with an attached image.\n\nSee [the Getting Started guide](https://pluralkit.me/start) for more information."))
.Field(new("Useful tips", $"React with {Emojis.Error} on a proxied message to delete it (only if you sent it!)\nReact with {Emojis.RedQuestion} on a proxied message to look up information about it (like who sent it)\nReact with {Emojis.Bell} on a proxied message to \"ping\" the sender\nType **`pk;invite`** to get a link to invite this bot to your own server!"))
.Field(new("More information", "For a full list of commands, see [the command list](https://pluralkit.me/commands).\nFor a more in-depth explanation of message proxying, see [the documentation](https://pluralkit.me/guide#proxying).\nIf you're an existing user of Tupperbox, type `pk;import` and attach a Tupperbox export file (from `tul!export`) to import your data from there."))
.Field(new("Support server", "We also have a Discord server for support, discussion, suggestions, announcements, etc: https://discord.gg/PczBt78"))

View file

@ -28,7 +28,7 @@ namespace PluralKit.Bot
// Otherwise it'll mess up/reformat the ISO strings for ???some??? reason >.>
DateParseHandling = DateParseHandling.None
};
public ImportExport(DataFileService dataFiles, HttpClient client)
{
_dataFiles = dataFiles;
@ -41,7 +41,7 @@ namespace PluralKit.Bot
if (url == null) throw Errors.NoImportFilePassed;
await ctx.BusyIndicator(async () =>
{
{
JObject data;
try
{
@ -68,10 +68,10 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Proceed"))
throw Errors.ImportCancelled;
}
if (data.ContainsKey("accounts")
&& data.Value<JArray>("accounts").Type != JTokenType.Null
&& data.Value<JArray>("accounts").Contains((JToken) ctx.Author.Id.ToString()))
&& data.Value<JArray>("accounts").Contains((JToken)ctx.Author.Id.ToString()))
{
var msg = $"{Emojis.Warn} You seem to importing a system profile belonging to another account. Are you sure you want to proceed?";
if (!await ctx.PromptYesNo(msg, "Import")) throw Errors.ImportCancelled;
@ -95,15 +95,15 @@ namespace PluralKit.Bot
public async Task Export(Context ctx)
{
ctx.CheckSystem();
var json = await ctx.BusyIndicator(async () =>
{
// Make the actual data file
var data = await _dataFiles.ExportSystem(ctx.System);
return JsonConvert.SerializeObject(data, Formatting.None);
});
// Send it as a Discord attachment *in DMs*
var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
@ -112,10 +112,10 @@ namespace PluralKit.Bot
var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id);
var msg = await ctx.Rest.CreateMessage(dm.Id,
new MessageRequest {Content = $"{Emojis.Success} Here you go!"},
new[] {new MultipartFile("system.json", stream)});
new MessageRequest { Content = $"{Emojis.Success} Here you go!" },
new[] { new MultipartFile("system.json", stream) });
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = $"<{msg.Attachments[0].Url}>" });
// If the original message wasn't posted in DMs, send a public reminder
if (ctx.Channel.Type != Channel.ChannelType.Dm)
await ctx.Reply($"{Emojis.Success} Check your DMs!");
@ -128,4 +128,4 @@ namespace PluralKit.Bot
}
}
}
}
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -18,15 +18,15 @@ namespace PluralKit.Bot
public static MemberListOptions ParseMemberListOptions(this Context ctx, LookupContext lookupCtx)
{
var p = new MemberListOptions();
// Short or long list? (parse this first, as it can potentially take a positional argument)
var isFull = ctx.Match("f", "full", "big", "details", "long") || ctx.MatchFlag("f", "full");
p.Type = isFull ? ListType.Long : ListType.Short;
// Search query
if (ctx.HasNext())
p.Search = ctx.RemainderOrNull();
// Include description in search?
if (ctx.MatchFlag("search-description", "filter-description", "in-description", "sd", "description", "desc"))
p.SearchDescription = true;
@ -47,7 +47,7 @@ namespace PluralKit.Bot
p.Reverse = true;
// Privacy filter (default is public only)
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("a", "all")) p.PrivacyFilter = null;
if (ctx.MatchFlag("private-only", "private", "priv")) p.PrivacyFilter = PrivacyLevel.Private;
if (ctx.MatchFlag("public-only", "public", "pub")) p.PrivacyFilter = PrivacyLevel.Public;
@ -55,7 +55,7 @@ namespace PluralKit.Bot
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
// TODO: should this just return null instead of throwing or something? >.>
throw new PKError("You cannot look up private members of another system.");
// 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;
@ -69,13 +69,13 @@ namespace PluralKit.Bot
p.IncludeAvatar = true;
if (ctx.MatchFlag("with-pronouns", "wp"))
p.IncludePronouns = true;
// Always show the sort property, too
if (p.SortProperty == SortProperty.LastSwitch) p.IncludeLastSwitch = true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage= true;
if (p.SortProperty == SortProperty.LastMessage) p.IncludeLastMessage = true;
if (p.SortProperty == SortProperty.MessageCount) p.IncludeMessageCount = true;
if (p.SortProperty == SortProperty.CreationDate) p.IncludeCreated = true;
// Done!
return p;
}
@ -96,119 +96,126 @@ namespace PluralKit.Bot
{
// Add a global footer with the filter/sort string + result count
eb.Footer(new($"{opts.CreateFilterString()}. {"result".ToQuantity(members.Count)}."));
// Then call the specific renderers
if (opts.Type == ListType.Short)
ShortRenderer(eb, page);
else
LongRenderer(eb, page);
return Task.CompletedTask;
}
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
{
// 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.Hid}`] **{m.NameFor(ctx)}** ";
switch (opts.SortProperty) {
case SortProperty.Birthdate: {
var birthday = m.BirthdayFor(lookupCtx);
if (birthday != null)
ret += $"(birthday: {m.BirthdayString})";
break;
}
case SortProperty.MessageCount: {
if (m.MessageCountFor(lookupCtx) is {} count)
ret += $"({count} messages)";
break;
}
case SortProperty.LastSwitch: {
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.LastMessage: {
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.CreationDate: {
if (m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
break;
}
default: {
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is {} count)
ret += $"({count} messages)";
else if (opts.IncludeLastSwitch && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
else if (opts.IncludeLastMessage && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
else if (opts.IncludeCreated && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
else if (opts.IncludePronouns && m.PronounsFor(lookupCtx) is {} pronouns)
ret += $"({pronouns})";
else if (m.HasProxyTags)
switch (opts.SortProperty)
{
case SortProperty.Birthdate:
{
var proxyTagsString = m.ProxyTagsString();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
ret += $"*(*{proxyTagsString}*)*";
var birthday = m.BirthdayFor(lookupCtx);
if (birthday != null)
ret += $"(birthday: {m.BirthdayString})";
break;
}
case SortProperty.MessageCount:
{
if (m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
break;
}
case SortProperty.LastSwitch:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.LastMessage:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
break;
}
case SortProperty.CreationDate:
{
if (m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
break;
}
default:
{
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
ret += $"({count} messages)";
else if (opts.IncludeLastSwitch && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
ret += $"(last switched in: <t:{lastSw.Value.ToUnixTimeSeconds()}>)";
else if (opts.IncludeLastMessage && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
ret += $"(last message: <t:{DiscordUtils.SnowflakeToInstant(lastMsg.Value).ToUnixTimeSeconds()}>)";
else if (opts.IncludeCreated && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
ret += $"(created at <t:{created.ToUnixTimeSeconds()}>)";
else if (opts.IncludePronouns && m.PronounsFor(lookupCtx) is { } pronouns)
ret += $"({pronouns})";
else if (m.HasProxyTags)
{
var proxyTagsString = m.ProxyTagsString();
if (proxyTagsString.Length > 100) // arbitrary threshold for now, tweak?
proxyTagsString = "tags too long, see member card";
ret += $"*(*{proxyTagsString}*)*";
}
break;
}
break;
}
}
return ret;
}));
}
void LongRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
{
var zone = ctx.System?.Zone ?? DateTimeZone.Utc;
foreach (var m in page)
{
var profile = new StringBuilder($"**ID**: {m.Hid}");
if (m.DisplayName != null && m.NamePrivacy.CanAccess(lookupCtx))
profile.Append($"\n**Display name**: {m.DisplayName}");
if (m.PronounsFor(lookupCtx) is {} pronouns)
if (m.PronounsFor(lookupCtx) is { } pronouns)
profile.Append($"\n**Pronouns**: {pronouns}");
if (m.BirthdayFor(lookupCtx) != null)
if (m.BirthdayFor(lookupCtx) != null)
profile.Append($"\n**Birthdate**: {m.BirthdayString}");
if (m.ProxyTags.Count > 0)
if (m.ProxyTags.Count > 0)
profile.Append($"\n**Proxy tags**: {m.ProxyTagsString()}");
if ((opts.IncludeMessageCount || opts.SortProperty == SortProperty.MessageCount) && m.MessageCountFor(lookupCtx) is {} count && count > 0)
if ((opts.IncludeMessageCount || opts.SortProperty == SortProperty.MessageCount) && m.MessageCountFor(lookupCtx) is { } count && count > 0)
profile.Append($"\n**Message count:** {count}");
if ((opts.IncludeLastMessage || opts.SortProperty == SortProperty.LastMessage) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastMessage, out var lastMsg))
profile.Append($"\n**Last message:** {DiscordUtils.SnowflakeToInstant(lastMsg.Value).FormatZoned(zone)}");
if ((opts.IncludeLastSwitch || opts.SortProperty == SortProperty.LastSwitch) && m.MetadataPrivacy.TryGet(lookupCtx, m.LastSwitchTime, out var lastSw))
profile.Append($"\n**Last switched in:** {lastSw.Value.FormatZoned(zone)}");
if ((opts.IncludeCreated || opts.SortProperty == SortProperty.CreationDate) && m.MetadataPrivacy.TryGet(lookupCtx, m.Created, out var created))
profile.Append($"\n**Created on:** {created.FormatZoned(zone)}");
if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is {} avatar)
if (opts.IncludeAvatar && m.AvatarFor(lookupCtx) is { } avatar)
profile.Append($"\n**Avatar URL:** {avatar.TryGetCleanCdnUrl()}");
if (m.DescriptionFor(lookupCtx) is {} desc)
if (m.DescriptionFor(lookupCtx) is { } desc)
profile.Append($"\n\n{desc}");
if (m.MemberVisibility == PrivacyLevel.Private)
profile.Append("\n*(this member is hidden)*");
eb.Field(new(m.NameFor(ctx), profile.ToString().Truncate(1024)));
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -19,7 +19,7 @@ namespace PluralKit.Bot
public GroupId? GroupFilter { get; set; }
public string? Search { get; set; }
public bool SearchDescription { get; set; }
public ListType Type { get; set; }
public bool IncludeMessageCount { get; set; }
public bool IncludeLastSwitch { get; set; }
@ -27,7 +27,7 @@ namespace PluralKit.Bot
public bool IncludeCreated { get; set; }
public bool IncludeAvatar { get; set; }
public bool IncludePronouns { get; set; }
public string CreateFilterString()
{
var str = new StringBuilder();
@ -46,7 +46,7 @@ namespace PluralKit.Bot
SortProperty.Random => "randomly",
_ => new ArgumentOutOfRangeException($"Couldn't find readable string for sort property {SortProperty}")
});
if (Search != null)
{
str.Append($", searching for \"{Search}\"");
@ -67,7 +67,7 @@ namespace PluralKit.Bot
public DatabaseViewsExt.MemberListQueryOptions ToQueryOptions() =>
new DatabaseViewsExt.MemberListQueryOptions
{
PrivacyFilter = PrivacyFilter,
PrivacyFilter = PrivacyFilter,
GroupFilter = GroupFilter,
Search = Search,
SearchDescription = SearchDescription
@ -80,7 +80,7 @@ namespace PluralKit.Bot
{
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
opts.Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
var randGen = new global::System.Random();
var culture = StringComparer.InvariantCultureIgnoreCase;
@ -112,7 +112,7 @@ namespace PluralKit.Bot
.ThenBy(m => m.NameFor(ctx), culture);
}
}
public enum SortProperty
{
Name,

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
private readonly HttpClient _client;
public Member(EmbedService embeds, IDatabase db, ModelRepository repo, HttpClient client)
{
_embeds = embeds;
@ -30,16 +30,18 @@ namespace PluralKit.Bot
_client = client;
}
public async Task NewMember(Context ctx) {
public async Task NewMember(Context ctx)
{
if (ctx.System == null) throw Errors.NoSystemError;
var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name.");
// Hard name length cap
if (memberName.Length > Limits.MaxMemberNameLength) throw Errors.MemberNameTooLongError(memberName.Length);
// Warn if there's already a member by this name
var existingMember = await _db.Execute(c => _repo.GetMemberByName(c, ctx.System.Id, memberName));
if (existingMember != null) {
if (existingMember != null)
{
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?";
if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled.");
}
@ -61,10 +63,13 @@ namespace PluralKit.Bot
Exception imageMatchError = null;
if (avatarArg != null)
{
try {
try
{
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Url);
await _db.Execute(conn => _repo.UpdateMember(conn, member.Id, new MemberPatch { AvatarUrl = avatarArg.Url }));
} catch (Exception e) {
}
catch (Exception e)
{
imageMatchError = e;
}
}
@ -72,7 +77,7 @@ namespace PluralKit.Bot
// Send confirmation and space hint
await ctx.Reply($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member");
if (await _db.Execute(conn => conn.QuerySingleAsync<bool>("select has_private_members(@System)",
new {System = ctx.System.Id}))) //if has private members
new { System = ctx.System.Id }))) //if has private members
await ctx.Reply($"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`.");
if (avatarArg != null)
if (imageMatchError == null)
@ -86,7 +91,7 @@ namespace PluralKit.Bot
else if (memberCount >= Limits.MaxMembersWarnThreshold(memberLimit))
await ctx.Reply($"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Please review your member list for unused or duplicate members.");
}
public async Task ViewMember(Context ctx, PKMember target)
{
var system = await _db.Execute(c => _repo.GetSystem(c, target.System));
@ -96,10 +101,10 @@ namespace PluralKit.Bot
public async Task Soulscream(Context ctx, PKMember target)
{
// this is for a meme, please don't take this code seriously. :)
var name = target.NameFor(ctx.LookupContextFor(target));
var encoded = HttpUtility.UrlEncode(name);
var resp = await _client.GetAsync($"https://onomancer.sibr.dev/api/generateStats2?name={encoded}");
if (resp.StatusCode != HttpStatusCode.OK)
// lol
@ -116,4 +121,4 @@ namespace PluralKit.Bot
await ctx.Reply(embed: eb.Build());
}
}
}
}

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot
_repo = repo;
_client = client;
}
private async Task AvatarClear(AvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
{
await UpdateAvatar(location, ctx, target, null);
@ -36,7 +36,7 @@ namespace PluralKit.Bot
{
if (mgs?.AvatarUrl != null)
await ctx.Reply($"{Emojis.Success} Member avatar cleared. Note that this member has a server-specific avatar set here, type `pk;member {target.Reference()} serveravatar clear` if you wish to clear that too.");
else
else
await ctx.Reply($"{Emojis.Success} Member avatar cleared.");
}
}
@ -57,10 +57,10 @@ namespace PluralKit.Bot
if (location == AvatarLocation.Server)
throw new PKError($"This member does not have a server avatar set. Type `pk;member {target.Reference()} avatar` to see their global avatar.");
}
var field = location == AvatarLocation.Server ? $"server avatar (for {ctx.Guild.Name})" : "avatar";
var cmd = location == AvatarLocation.Server ? "serveravatar" : "avatar";
var eb = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s {field}")
.Image(new(currentValue?.TryGetCleanCdnUrl()));
@ -75,7 +75,7 @@ namespace PluralKit.Bot
var guildData = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
await AvatarCommandTree(AvatarLocation.Server, ctx, target, guildData);
}
public async Task Avatar(Context ctx, PKMember target)
{
var guildData = ctx.Guild != null ?
@ -119,7 +119,7 @@ namespace PluralKit.Bot
AvatarLocation.Member => "avatar",
_ => throw new ArgumentOutOfRangeException(nameof(location))
};
var serverFrag = location switch
{
AvatarLocation.Server => $" This avatar will now be used when proxying in this server (**{ctx.Guild.Name}**).",
@ -137,8 +137,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = avatar.Source != AvatarSource.Attachment;
return hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(avatar.Url)).Build())
return hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(avatar.Url)).Build())
: ctx.Reply(msg);
}

View file

@ -24,10 +24,10 @@ namespace PluralKit.Bot
_client = client;
}
public async Task Name(Context ctx, PKMember target)
public async Task Name(Context ctx, PKMember target)
{
ctx.CheckSystem().CheckOwnMember(target);
ctx.CheckSystem().CheckOwnMember(target);
var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new name for the member.");
// Hard name length cap
@ -35,14 +35,14 @@ namespace PluralKit.Bot
// Warn if there's already a member by this name
var existingMember = await _db.Execute(conn => _repo.GetMemberByName(conn, ctx.System.Id, newName));
if (existingMember != null && existingMember.Id != target.Id)
if (existingMember != null && existingMember.Id != target.Id)
{
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?";
if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Member renaming cancelled.");
}
// Rename the member
var patch = new MemberPatch {Name = Partial<string>.Present(newName)};
var patch = new MemberPatch { Name = Partial<string>.Present(newName) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member renamed.");
@ -57,15 +57,16 @@ namespace PluralKit.Bot
}
}
public async Task Description(Context ctx, PKMember target) {
public async Task Description(Context ctx, PKMember target)
{
if (await ctx.MatchClear("this member's description"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Description = Partial<string>.Null()};
var patch = new MemberPatch { Description = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member description cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
@ -81,7 +82,7 @@ namespace PluralKit.Bot
await ctx.Reply(embed: new EmbedBuilder()
.Title("Member description")
.Description(target.Description)
.Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`."
.Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`."
+ (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} description -clear`." : "")))
.Build());
}
@ -92,23 +93,24 @@ namespace PluralKit.Bot
var description = ctx.RemainderOrNull().NormalizeLineEndSpacing();
if (description.IsLongerThan(Limits.MaxDescriptionLength))
throw Errors.DescriptionTooLongError(description.Length);
var patch = new MemberPatch {Description = Partial<string>.Present(description)};
var patch = new MemberPatch { Description = Partial<string>.Present(description) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member description changed.");
}
}
public async Task Pronouns(Context ctx, PKMember target) {
public async Task Pronouns(Context ctx, PKMember target)
{
if (await ctx.MatchClear("this member's pronouns"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Pronouns = Partial<string>.Null()};
var patch = new MemberPatch { Pronouns = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member pronouns cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.PronounPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
@ -131,10 +133,10 @@ namespace PluralKit.Bot
var pronouns = ctx.RemainderOrNull().NormalizeLineEndSpacing();
if (pronouns.IsLongerThan(Limits.MaxPronounsLength))
throw Errors.MemberPronounsTooLongError(pronouns.Length);
var patch = new MemberPatch {Pronouns = Partial<string>.Present(pronouns)};
var patch = new MemberPatch { Pronouns = Partial<string>.Present(pronouns) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member pronouns changed.");
}
}
@ -145,7 +147,7 @@ namespace PluralKit.Bot
async Task ClearBannerImage()
{
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch {BannerImage = null}));
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch { BannerImage = null }));
await ctx.Reply($"{Emojis.Success} Member banner image cleared.");
}
@ -153,7 +155,7 @@ namespace PluralKit.Bot
{
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, isFullSizeImage: true);
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch {BannerImage = img.Url}));
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch { BannerImage = img.Url }));
var msg = img.Source switch
{
@ -165,8 +167,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -186,7 +188,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this member's banner image"))
await ClearBannerImage();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetBannerImage(img);
else
await ShowBannerImage();
@ -198,10 +200,10 @@ namespace PluralKit.Bot
if (await ctx.MatchClear())
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Color = Partial<string>.Null()};
var patch = new MemberPatch { Color = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member color cleared.");
}
else if (!ctx.HasNext())
@ -230,8 +232,8 @@ namespace PluralKit.Bot
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new MemberPatch {Color = Partial<string>.Present(color.ToLowerInvariant())};
var patch = new MemberPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply(embed: new EmbedBuilder()
@ -246,17 +248,17 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("this member's birthday"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {Birthday = Partial<LocalDate?>.Null()};
var patch = new MemberPatch { Birthday = Partial<LocalDate?>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member birthdate cleared.");
}
}
else if (!ctx.HasNext())
{
if (!target.BirthdayPrivacy.CanAccess(ctx.LookupContextFor(target.System)))
throw Errors.LookupNotAllowed;
if (target.Birthday == null)
await ctx.Reply("This member does not have a birthdate set."
+ (ctx.System?.Id == target.System ? $" To set one, type `pk;member {target.Reference()} birthdate <birthdate>`." : ""));
@ -267,22 +269,22 @@ namespace PluralKit.Bot
else
{
ctx.CheckOwnMember(target);
var birthdayStr = ctx.RemainderOrNull();
var birthday = DateUtils.ParseDate(birthdayStr, true);
if (birthday == null) throw Errors.BirthdayParseError(birthdayStr);
var patch = new MemberPatch {Birthday = Partial<LocalDate?>.Present(birthday)};
var patch = new MemberPatch { Birthday = Partial<LocalDate?>.Present(birthday) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member birthdate changed.");
}
}
private async Task<EmbedBuilder> CreateMemberNameInfoEmbed(Context ctx, PKMember target)
{
var lcx = ctx.LookupContextFor(target);
MemberGuildSettings memberGuildConfig = null;
if (ctx.Guild != null)
memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id));
@ -329,12 +331,12 @@ namespace PluralKit.Bot
await ctx.Reply(successStr);
}
if (await ctx.MatchClear("this member's display name"))
{
ctx.CheckOwnMember(target);
var patch = new MemberPatch {DisplayName = Partial<string>.Null()};
var patch = new MemberPatch { DisplayName = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await PrintSuccess($"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\".");
@ -365,25 +367,25 @@ namespace PluralKit.Bot
else
{
ctx.CheckOwnMember(target);
var newDisplayName = ctx.RemainderOrNull();
var patch = new MemberPatch {DisplayName = Partial<string>.Present(newDisplayName)};
var patch = new MemberPatch { DisplayName = Partial<string>.Present(newDisplayName) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await PrintSuccess($"{Emojis.Success} Member display name changed. This member will now be proxied using the name \"{newDisplayName}\".");
}
}
public async Task ServerName(Context ctx, PKMember target)
{
ctx.CheckGuildContext();
if (await ctx.MatchClear("this member's server name"))
{
ctx.CheckOwnMember(target);
var patch = new MemberGuildPatch {DisplayName = null};
var patch = new MemberGuildPatch { DisplayName = null };
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
if (target.DisplayName != null)
@ -419,16 +421,16 @@ namespace PluralKit.Bot
else
{
ctx.CheckOwnMember(target);
var newServerName = ctx.RemainderOrNull();
var patch = new MemberGuildPatch {DisplayName = newServerName};
var patch = new MemberGuildPatch { DisplayName = newServerName };
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} Member server name changed. This member will now be proxied using the name \"{newServerName}\" in this server ({ctx.Guild.Name}).");
}
}
public async Task KeepProxy(Context ctx, PKMember target)
{
ctx.CheckSystem().CheckOwnMember(target);
@ -446,9 +448,9 @@ namespace PluralKit.Bot
return;
};
var patch = new MemberPatch {KeepProxy = Partial<bool>.Present(newValue)};
var patch = new MemberPatch { KeepProxy = Partial<bool>.Present(newValue) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
if (newValue)
await ctx.Reply($"{Emojis.Success} Member proxy tags will now be included in the resulting message when proxying.");
else
@ -473,7 +475,7 @@ namespace PluralKit.Bot
return;
};
var patch = new MemberPatch {AllowAutoproxy = Partial<bool>.Present(newValue)};
var patch = new MemberPatch { AllowAutoproxy = Partial<bool>.Present(newValue) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
if (newValue)
@ -491,18 +493,18 @@ namespace PluralKit.Bot
{
await ctx.Reply(embed: new EmbedBuilder()
.Title($"Current privacy settings for {target.NameFor(ctx)}")
.Field(new("Name (replaces name with display name if member has one)",target.NamePrivacy.Explanation()))
.Field(new("Name (replaces name with display name if member has one)", target.NamePrivacy.Explanation()))
.Field(new("Description", target.DescriptionPrivacy.Explanation()))
.Field(new("Avatar", target.AvatarPrivacy.Explanation()))
.Field(new("Birthday", target.BirthdayPrivacy.Explanation()))
.Field(new("Pronouns", target.PronounPrivacy.Explanation()))
.Field(new("Meta (message count, last front, last message)",target.MetadataPrivacy.Explanation()))
.Field(new("Meta (message count, last front, last message)", target.MetadataPrivacy.Explanation()))
.Field(new("Visibility", target.MemberVisibility.Explanation()))
.Description("To edit privacy settings, use the command:\n`pk;member <member> privacy <subject> <level>`\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `created`, `messages`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
.Build());
.Build());
return;
}
// Get guild settings (mostly for warnings and such)
MemberGuildSettings guildSettings = null;
if (ctx.Guild != null)
@ -511,17 +513,17 @@ namespace PluralKit.Bot
async Task SetAll(PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch().WithAllPrivacy(level)));
if (level == PrivacyLevel.Private)
await ctx.Reply($"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see nothing on the member card.");
else
else
await ctx.Reply($"{Emojis.Success} All {target.NameFor(ctx)}'s privacy settings have been set to **{level.LevelName()}**. Other accounts will now see everything on the member card.");
}
async Task SetLevel(MemberPrivacySubject subject, PrivacyLevel level)
{
await _db.Execute(c => _repo.UpdateMember(c, target.Id, new MemberPatch().WithPrivacy(subject, level)));
var subjectName = subject switch
{
MemberPrivacySubject.Name => "name privacy",
@ -533,7 +535,7 @@ namespace PluralKit.Bot
MemberPrivacySubject.Visibility => "visibility",
_ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}")
};
var explanation = (subject, level) switch
{
(MemberPrivacySubject.Name, PrivacyLevel.Private) => "This member's name is now hidden from other systems, and will be replaced by the member's display name.",
@ -543,7 +545,7 @@ namespace PluralKit.Bot
(MemberPrivacySubject.Pronouns, PrivacyLevel.Private) => "This member's pronouns are now hidden from other systems.",
(MemberPrivacySubject.Metadata, PrivacyLevel.Private) => "This member's metadata (eg. created timestamp, message count, etc) is now hidden from other systems.",
(MemberPrivacySubject.Visibility, PrivacyLevel.Private) => "This member is now hidden from member lists.",
(MemberPrivacySubject.Name, PrivacyLevel.Public) => "This member's name is no longer hidden from other systems.",
(MemberPrivacySubject.Description, PrivacyLevel.Public) => "This member's description is no longer hidden from other systems.",
(MemberPrivacySubject.Avatar, PrivacyLevel.Public) => "This member's avatar is no longer hidden from other systems.",
@ -551,16 +553,16 @@ namespace PluralKit.Bot
(MemberPrivacySubject.Pronouns, PrivacyLevel.Public) => "This member's pronouns are no longer hidden other systems.",
(MemberPrivacySubject.Metadata, PrivacyLevel.Public) => "This member's metadata (eg. created timestamp, message count, etc) is no longer hidden from other systems.",
(MemberPrivacySubject.Visibility, PrivacyLevel.Public) => "This member is no longer hidden from member lists.",
_ => throw new InvalidOperationException($"Invalid subject/level tuple ({subject}, {level})")
};
await ctx.Reply($"{Emojis.Success} {target.NameFor(ctx)}'s **{subjectName}** has been set to **{level.LevelName()}**. {explanation}");
// Name privacy only works given a display name
if (subject == MemberPrivacySubject.Name && level == PrivacyLevel.Private && target.DisplayName == null)
await ctx.Reply($"{Emojis.Warn} This member does not have a display name set, and name privacy **will not take effect**.");
// Avatar privacy doesn't apply when proxying if no server avatar is set
if (subject == MemberPrivacySubject.Avatar && level == PrivacyLevel.Private && guildSettings?.AvatarUrl == null)
await ctx.Reply($"{Emojis.Warn} This member does not have a server avatar set, so *proxying* will **still show the member avatar**. If you want to hide your avatar when proxying here, set a server avatar: `pk;member {target.Reference()} serveravatar`");
@ -571,17 +573,17 @@ namespace PluralKit.Bot
else
await SetLevel(ctx.PopMemberPrivacySubject(), ctx.PopPrivacyLevel());
}
public async Task Delete(Context ctx, PKMember target)
{
ctx.CheckSystem().CheckOwnMember(target);
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx)}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
await _db.Execute(conn => _repo.DeleteMember(conn, target.Id));
await ctx.Reply($"{Emojis.Success} Member deleted.");
}
}
}
}

View file

@ -76,14 +76,14 @@ namespace PluralKit.Bot
description = "This member has no groups.";
else
description = string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**"));
if (pctx == LookupContext.ByOwner)
{
msg += $"\n\nTo add this member to one or more groups, use `pk;m {target.Reference()} 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()} group remove <group> [group 2] [group 3...]`";
}
await ctx.Reply(msg, (new EmbedBuilder().Title($"{target.Name}'s groups").Description(description)).Build());
}
}

View file

@ -11,7 +11,7 @@ namespace PluralKit.Bot
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public MemberProxy(IDatabase db, ModelRepository repo)
{
_db = db;
@ -31,20 +31,20 @@ namespace PluralKit.Bot
if (prefixAndSuffix.Length > 2) throw Errors.ProxyMultipleText;
return new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1]);
}
async Task<bool> WarnOnConflict(ProxyTag newTag)
{
var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing";
var conflicts = (await _db.Execute(conn => conn.QueryAsync<PKMember>(query,
new {Prefix = newTag.Prefix, Suffix = newTag.Suffix, Existing = target.Id, system = target.System}))).ToList();
new { Prefix = newTag.Prefix, Suffix = newTag.Suffix, Existing = target.Id, system = target.System }))).ToList();
if (conflicts.Count <= 0) return true;
var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**");
var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?";
return await ctx.PromptYesNo(msg, "Proceed");
}
// "Sub"command: clear flag
if (await ctx.MatchClear())
{
@ -55,10 +55,10 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Clear"))
throw Errors.GenericCancelled();
}
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(new ProxyTag[0])};
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(new ProxyTag[0]) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Proxy tags cleared.");
}
// "Sub"command: no arguments; will print proxy tags
@ -73,20 +73,20 @@ namespace PluralKit.Bot
else if (ctx.Match("add", "append"))
{
if (!ctx.HasNext(skipFlags: false)) throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`).");
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false));
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target);
if (target.ProxyTags.Contains(tagToAdd))
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
if (tagToAdd.ProxyString.Length > Limits.MaxProxyTagLength)
throw new PKError($"Proxy tag too long ({tagToAdd.ProxyString.Length} > {Limits.MaxProxyTagLength} characters).");
if (!await WarnOnConflict(tagToAdd))
throw Errors.GenericCancelled();
var newTags = target.ProxyTags.ToList();
newTags.Add(tagToAdd);
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray())};
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()}.");
@ -103,7 +103,7 @@ namespace PluralKit.Bot
var newTags = target.ProxyTags.ToList();
newTags.Remove(tagToRemove);
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray())};
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags.ToArray()) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}.");
@ -122,14 +122,14 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Replace"))
throw Errors.GenericCancelled();
}
if (!await WarnOnConflict(requestedTag))
throw Errors.GenericCancelled();
var newTags = new[] {requestedTag};
var patch = new MemberPatch {ProxyTags = Partial<ProxyTag[]>.Present(newTags)};
var newTags = new[] { requestedTag };
var patch = new MemberPatch { ProxyTags = Partial<ProxyTag[]>.Present(newTags) };
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()}.");
}
}

View file

@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Threading.Tasks;
using Myriad.Builders;
@ -17,7 +17,7 @@ namespace PluralKit.Bot
public class ProxiedMessage
{
private static readonly Duration EditTimeout = Duration.FromMinutes(10);
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
@ -48,7 +48,7 @@ namespace PluralKit.Bot
if (ctx.System.Id != msg.System.Id)
throw new PKError("Can't edit a message sent by a different system.");
if (_cache.GetRootChannel(msg.Message.Channel).Id != msg.Message.Channel)
throw new PKError("PluralKit cannot edit messages in threads.");
@ -61,7 +61,7 @@ namespace PluralKit.Bot
try
{
var editedMsg = await _webhookExecutor.EditWebhookMessage(msg.Message.Channel, msg.Message.Mid, newContent);
if (ctx.Guild == null)
await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new() { Name = Emojis.Success });
@ -75,7 +75,7 @@ namespace PluralKit.Bot
throw new PKError("Could not edit message.");
}
}
private async Task<FullMessage> GetMessageToEdit(Context ctx)
{
await using var conn = await _db.Obtain();
@ -112,14 +112,14 @@ namespace PluralKit.Bot
var lastMessage = await _repo.GetLastMessage(conn, ctx.Guild.Id, ctx.Channel.Id, ctx.Author.Id);
if (lastMessage == null)
return null;
var timestamp = DiscordUtils.SnowflakeToInstant(lastMessage.Mid);
if (_clock.GetCurrentInstant() - timestamp > EditTimeout)
return null;
return lastMessage;
}
public async Task GetMessage(Context ctx)
{
var (messageId, _) = ctx.MatchMessage(true);
@ -129,7 +129,7 @@ namespace PluralKit.Bot
throw new PKSyntaxError("You must pass a message ID or link.");
throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link.");
}
var message = await _db.Execute(c => _repo.GetMessage(c, messageId.Value));
if (message == null) throw Errors.MessageNotFound(messageId.Value);
@ -155,4 +155,4 @@ namespace PluralKit.Bot
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message));
}
}
}
}

View file

@ -21,7 +21,8 @@ using Myriad.Rest.Exceptions;
using Myriad.Rest.Types.Requests;
using Myriad.Types;
namespace PluralKit.Bot {
namespace PluralKit.Bot
{
public class Misc
{
private readonly BotConfig _botConfig;
@ -55,31 +56,31 @@ namespace PluralKit.Bot {
_proxy = proxy;
_matcher = matcher;
}
public async Task Invite(Context ctx)
{
var clientId = _botConfig.ClientId ?? _cluster.Application?.Id;
var permissions =
var permissions =
PermissionSet.AddReactions |
PermissionSet.AttachFiles |
PermissionSet.AttachFiles |
PermissionSet.EmbedLinks |
PermissionSet.ManageMessages |
PermissionSet.ManageWebhooks |
PermissionSet.ReadMessageHistory |
PermissionSet.ReadMessageHistory |
PermissionSet.SendMessages;
var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(ulong)permissions}";
await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
}
public async Task Stats(Context ctx)
{
var timeBefore = SystemClock.Instance.GetCurrentInstant();
var msg = await ctx.Reply($"...");
var timeAfter = SystemClock.Instance.GetCurrentInstant();
var apiLatency = timeAfter - timeBefore;
var messagesReceived = _metrics.Snapshot.GetForContext("Bot").Meters.FirstOrDefault(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name)?.Value;
var messagesProxied = _metrics.Snapshot.GetForContext("Bot").Meters.FirstOrDefault(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name)?.Value;
var commandsRun = _metrics.Snapshot.GetForContext("Bot").Meters.FirstOrDefault(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name)?.Value;
@ -94,7 +95,7 @@ namespace PluralKit.Bot {
var shardTotal = ctx.Cluster.Shards.Count;
var shardUpTotal = _shards.Shards.Where(x => x.Connected).Count();
var shardInfo = _shards.GetShardInfo(ctx.Shard);
var process = Process.GetCurrentProcess();
var memoryUsage = process.WorkingSet64;
@ -102,7 +103,7 @@ namespace PluralKit.Bot {
var shardUptime = now - shardInfo.LastConnectionTime;
var embed = new EmbedBuilder();
if (messagesReceived != null) embed.Field(new("Messages processed",$"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true));
if (messagesReceived != null) embed.Field(new("Messages processed", $"{messagesReceived.OneMinuteRate * 60:F1}/m ({messagesReceived.FifteenMinuteRate * 60:F1}/m over 15m)", true));
if (messagesProxied != null) embed.Field(new("Messages proxied", $"{messagesProxied.OneMinuteRate * 60:F1}/m ({messagesProxied.FifteenMinuteRate * 60:F1}/m over 15m)", true));
if (commandsRun != null) embed.Field(new("Commands executed", $"{commandsRun.OneMinuteRate * 60:F1}/m ({commandsRun.FifteenMinuteRate * 60:F1}/m over 15m)", true));
@ -114,10 +115,10 @@ namespace PluralKit.Bot {
.Field(new("Latency", $"API: {apiLatency.TotalMilliseconds:F0} ms, shard: {shardInfo.ShardLatency.Milliseconds} ms", true))
.Field(new("Total numbers", $"{totalSystems:N0} systems, {totalMembers:N0} members, {totalGroups:N0} groups, {totalSwitches:N0} switches, {totalMessages:N0} messages"))
.Timestamp(now.ToDateTimeOffset().ToString("O"))
.Footer(new($"PluralKit {BuildInfoService.Version} • https://github.com/xSke/PluralKit"));;
.Footer(new($"PluralKit {BuildInfoService.Version} • https://github.com/xSke/PluralKit")); ;
await ctx.Rest.EditMessage(msg.ChannelId, msg.Id,
new MessageEditRequest {Content = "", Embed = embed.Build()});
new MessageEditRequest { Content = "", Embed = embed.Build() });
}
}
}
}

View file

@ -1,4 +1,4 @@
using PluralKit.Core;
using PluralKit.Core;
namespace PluralKit.Bot
{
@ -14,7 +14,7 @@ namespace PluralKit.Bot
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a privacy level (`public` or `private`)");
throw new PKSyntaxError($"Invalid privacy level {ctx.PopArgument().AsCode()} (must be `public` or `private`).");
}
@ -22,25 +22,25 @@ namespace PluralKit.Bot
{
if (!SystemPrivacyUtils.TryParseSystemPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError($"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `members`, `front`, `fronthistory`, `groups`, or `all`).");
ctx.PopArgument();
return subject;
}
public static MemberPrivacySubject PopMemberPrivacySubject(this Context ctx)
{
if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError($"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`).");
ctx.PopArgument();
return subject;
}
public static GroupPrivacySubject PopGroupPrivacySubject(this Context ctx)
{
if (!GroupPrivacyUtils.TryParseGroupPrivacy(ctx.PeekArgument(), out var subject))
throw new PKSyntaxError($"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `description`, `icon`, `visibility`, or `all`).");
ctx.PopArgument();
return subject;
}
@ -51,7 +51,7 @@ namespace PluralKit.Bot
if (ctx.MatchFlag("a", "all")) privacy = false;
if (pctx == LookupContext.ByNonOwner && !privacy) throw Errors.LookupNotAllowed;
return privacy;
return privacy;
}
}
}
}

View file

@ -33,7 +33,7 @@ namespace PluralKit.Bot
return _repo.GetSystemMembers(c, ctx.System.Id)
.Where(m => m.MemberVisibility == PrivacyLevel.Public);
}).ToListAsync();
if (members == null || !members.Any())
throw new PKError("Your system has no members! Please create at least one member before using this command.");

View file

@ -29,23 +29,23 @@ namespace PluralKit.Bot
public async Task SetLogChannel(Context ctx)
{
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
if (await ctx.MatchClear("the server log channel"))
{
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch {LogChannel = null}));
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, new GuildPatch { LogChannel = null }));
await ctx.Reply($"{Emojis.Success} Proxy logging channel cleared.");
return;
}
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a #channel to set, or `clear` to clear it.");
Channel channel = null;
var channelString = ctx.PeekArgument();
channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
var patch = new GuildPatch {LogChannel = channel.Id};
var patch = new GuildPatch { LogChannel = channel.Id };
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to #{channel.Name}.");
}
@ -59,12 +59,12 @@ namespace PluralKit.Bot
affectedChannels = _cache.GetGuildChannels(ctx.Guild.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
ulong? logChannel = null;
await using (var conn = await _db.Obtain())
@ -76,8 +76,8 @@ namespace PluralKit.Bot
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
else
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {LogBlacklist = blacklist.ToArray()};
var patch = new GuildPatch { LogBlacklist = blacklist.ToArray() };
await _repo.UpsertGuild(conn, ctx.Guild.Id, patch);
}
@ -91,7 +91,7 @@ namespace PluralKit.Bot
ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var blacklist = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id));
// Resolve all channels from the cache and order by position
var channels = blacklist.Blacklist
.Select(id => _cache.GetChannelOrNull(id))
@ -112,7 +112,7 @@ namespace PluralKit.Bot
{
string CategoryName(ulong? id) =>
id != null ? _cache.GetChannel(id.Value).Name : "(no category)";
ulong? lastCategory = null;
var fieldValue = new StringBuilder();
@ -144,13 +144,13 @@ namespace PluralKit.Bot
affectedChannels = _cache.GetGuildChannels(ctx.Guild.Id).Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
await using (var conn = await _db.Obtain())
{
var guild = await _repo.GetGuild(conn, ctx.Guild.Id);
@ -159,8 +159,8 @@ namespace PluralKit.Bot
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
else
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
var patch = new GuildPatch {Blacklist = blacklist.ToArray()};
var patch = new GuildPatch { Blacklist = blacklist.ToArray() };
await _repo.UpsertGuild(conn, ctx.Guild.Id, patch);
}
@ -186,14 +186,14 @@ namespace PluralKit.Bot
var guildCfg = await _db.Execute(c => _repo.GetGuild(c, ctx.Guild.Id));
if (guildCfg.LogCleanupEnabled)
eb.Description("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
else
eb.Description("Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
else
eb.Description("Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`.");
await ctx.Reply(embed: eb.Build());
return;
}
var patch = new GuildPatch {LogCleanupEnabled = newValue};
var patch = new GuildPatch { LogCleanupEnabled = newValue };
await _db.Execute(conn => _repo.UpsertGuild(conn, ctx.Guild.Id, patch));
if (newValue)

View file

@ -30,7 +30,7 @@ namespace PluralKit.Bot
public async Task SwitchOut(Context ctx)
{
ctx.CheckSystem();
// Switch with no members = switch-out
await DoSwitchCommand(ctx, new PKMember[] { });
}
@ -61,35 +61,35 @@ namespace PluralKit.Bot
else
await ctx.Reply($"{Emojis.Success} Switch registered. Current fronter is now {string.Join(", ", members.Select(m => m.NameFor(ctx)))}.");
}
public async Task SwitchMove(Context ctx)
{
ctx.CheckSystem();
var timeToMove = ctx.RemainderOrNull() ?? throw new PKSyntaxError("Must pass a date or time to move the switch to.");
var tz = TzdbDateTimeZoneSource.Default.ForId(ctx.System.UiTz ?? "UTC");
var result = DateUtils.ParseDateTime(timeToMove, true, tz);
if (result == null) throw Errors.InvalidDateTime(timeToMove);
await using var conn = await _db.Obtain();
var time = result.Value;
if (time.ToInstant() > SystemClock.Instance.GetCurrentInstant()) throw Errors.SwitchTimeInFuture;
// Fetch the last two switches for the system to do bounds checking on
var lastTwoSwitches = await _repo.GetSwitches(conn, ctx.System.Id).Take(2).ToListAsync();
// If we don't have a switch to move, don't bother
if (lastTwoSwitches.Count == 0) throw Errors.NoRegisteredSwitches;
// If there's a switch *behind* the one we move, we check to make srue we're not moving the time further back than that
if (lastTwoSwitches.Count == 2)
{
if (lastTwoSwitches[1].Timestamp > time.ToInstant())
throw Errors.SwitchMoveBeforeSecondLast(lastTwoSwitches[1].Timestamp.InZone(tz));
}
// Now we can actually do the move, yay!
// But, we do a prompt to confirm.
var lastSwitchMembers = _repo.GetSwitchMembers(conn, lastTwoSwitches[0].Id);
@ -98,16 +98,16 @@ namespace PluralKit.Bot
var lastSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - lastTwoSwitches[0].Timestamp).FormatDuration();
var newSwitchTime = time.ToInstant().ToUnixTimeSeconds();
var newSwitchDeltaStr = (SystemClock.Instance.GetCurrentInstant() - time.ToInstant()).FormatDuration();
// yeet
var msg = $"{Emojis.Warn} This will move the latest switch ({lastSwitchMemberStr}) from <t:{lastSwitchTime}> ({lastSwitchDeltaStr} ago) to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago). Is this OK?";
if (!await ctx.PromptYesNo(msg, "Move Switch")) throw Errors.SwitchMoveCancelled;
// aaaand *now* we do the move
await _repo.MoveSwitch(conn, lastTwoSwitches[0].Id, time.ToInstant());
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
}
public async Task SwitchDelete(Context ctx)
{
ctx.CheckSystem();
@ -122,7 +122,7 @@ namespace PluralKit.Bot
await ctx.Reply($"{Emojis.Success} Cleared system switches!");
return;
}
await using var conn = await _db.Obtain();
// Fetch the last two switches for the system to do bounds checking on
@ -148,8 +148,8 @@ namespace PluralKit.Bot
if (!await ctx.PromptYesNo(msg, "Delete Switch")) throw Errors.SwitchDeleteCancelled;
await _repo.DeleteSwitch(conn, lastTwoSwitches[0].Id);
await ctx.Reply($"{Emojis.Success} Switch deleted.");
}
}
}
}

View file

@ -9,15 +9,16 @@ namespace PluralKit.Bot
private readonly EmbedService _embeds;
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public System(EmbedService embeds, IDatabase db, ModelRepository repo)
{
_embeds = embeds;
_db = db;
_repo = repo;
}
public async Task Query(Context ctx, PKSystem system) {
public async Task Query(Context ctx, PKSystem system)
{
if (system == null) throw Errors.NoSystemError;
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(ctx, system, ctx.LookupContextFor(system)));
@ -37,9 +38,9 @@ namespace PluralKit.Bot
await _repo.AddAccount(c, system.Id, ctx.Author.Id);
return system;
});
// 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>");
}
}
}
}

View file

@ -33,7 +33,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("your system's name"))
{
var clearPatch = new SystemPatch {Name = null};
var clearPatch = new SystemPatch { Name = null };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, clearPatch));
await ctx.Reply($"{Emojis.Success} System name cleared.");
@ -49,28 +49,29 @@ namespace PluralKit.Bot
await ctx.Reply("Your system currently does not have a name. Type `pk;system name <name>` to set one.");
return;
}
if (newSystemName != null && newSystemName.Length > Limits.MaxSystemNameLength)
throw Errors.SystemNameTooLongError(newSystemName.Length);
var patch = new SystemPatch {Name = newSystemName};
var patch = new SystemPatch { Name = newSystemName };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System name changed.");
}
public async Task Description(Context ctx) {
public async Task Description(Context ctx)
{
ctx.CheckSystem();
if (await ctx.MatchClear("your system's description"))
{
var patch = new SystemPatch {Description = null};
var patch = new SystemPatch { Description = null };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System description cleared.");
return;
}
var newDescription = ctx.RemainderOrNull()?.NormalizeLineEndSpacing();
if (newDescription == null)
{
@ -88,27 +89,28 @@ namespace PluralKit.Bot
else
{
if (newDescription.Length > Limits.MaxDescriptionLength) throw Errors.DescriptionTooLongError(newDescription.Length);
var patch = new SystemPatch {Description = newDescription};
var patch = new SystemPatch { Description = newDescription };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System description changed.");
}
}
public async Task Color(Context ctx) {
public async Task Color(Context ctx)
{
ctx.CheckSystem();
if (await ctx.MatchClear())
{
var patch = new SystemPatch {Color = Partial<string>.Null()};
var patch = new SystemPatch { Color = Partial<string>.Null() };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System color cleared.");
}
else if (!ctx.HasNext())
else if (!ctx.HasNext())
{
if (ctx.System.Color == null)
if (ctx.System.Color == null)
await ctx.Reply(
$"Your system does not have a color set. To set one, type `pk;system color <color>`.");
else
@ -126,7 +128,7 @@ namespace PluralKit.Bot
if (color.StartsWith("#")) color = color.Substring(1);
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
var patch = new SystemPatch {Color = Partial<string>.Present(color.ToLowerInvariant())};
var patch = new SystemPatch { Color = Partial<string>.Present(color.ToLowerInvariant()) };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply(embed: new EmbedBuilder()
@ -136,18 +138,19 @@ namespace PluralKit.Bot
.Build());
}
}
public async Task Tag(Context ctx)
{
ctx.CheckSystem();
if (await ctx.MatchClear("your system's tag"))
{
var patch = new SystemPatch {Tag = null};
var patch = new SystemPatch { Tag = null };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System tag cleared.");
} else if (!ctx.HasNext(skipFlags: false))
}
else if (!ctx.HasNext(skipFlags: false))
{
if (ctx.System.Tag == null)
await ctx.Reply($"You currently have no system tag. To set one, type `pk;s tag <tag>`.");
@ -160,10 +163,10 @@ namespace PluralKit.Bot
if (newTag != null)
if (newTag.Length > Limits.MaxSystemTagLength)
throw Errors.SystemTagTooLongError(newTag.Length);
var patch = new SystemPatch {Tag = newTag};
var patch = new SystemPatch { Tag = newTag };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"{Emojis.Success} System tag changed. Member names will now end with {newTag.AsCode()} when proxied.");
}
}
@ -198,9 +201,9 @@ namespace PluralKit.Bot
{
var newTag = ctx.RemainderOrNull(skipFlags: false);
if (newTag != null && newTag.Length > Limits.MaxSystemTagLength)
throw Errors.SystemTagTooLongError(newTag.Length);
throw Errors.SystemTagTooLongError(newTag.Length);
var patch = new SystemGuildPatch {Tag = newTag};
var patch = new SystemGuildPatch { Tag = newTag };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} System server tag changed. Member names will now end with {newTag.AsCode()} when proxied in the current server '{ctx.Guild.Name}'.");
@ -211,7 +214,7 @@ namespace PluralKit.Bot
async Task Clear()
{
var patch = new SystemGuildPatch {Tag = null};
var patch = new SystemGuildPatch { Tag = null };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await ctx.Reply($"{Emojis.Success} System server tag cleared. Member names will now end with the global system tag, if there is one set.");
@ -222,7 +225,7 @@ namespace PluralKit.Bot
async Task EnableDisable(bool newValue)
{
var patch = new SystemGuildPatch {TagEnabled = newValue};
var patch = new SystemGuildPatch { TagEnabled = newValue };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, ctx.Guild.Id, patch));
await ctx.Reply(PrintEnableDisableResult(newValue, newValue != ctx.MessageContext.TagEnabled));
@ -253,7 +256,7 @@ namespace PluralKit.Bot
str += $" Member names will now end with the global system tag when proxied in the current server, if there is one set.";
}
}
return str;
}
@ -268,14 +271,14 @@ namespace PluralKit.Bot
else
await Set();
}
public async Task Avatar(Context ctx)
{
ctx.CheckSystem();
async Task ClearIcon()
{
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {AvatarUrl = null}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { AvatarUrl = null }));
await ctx.Reply($"{Emojis.Success} System icon cleared.");
}
@ -283,8 +286,8 @@ namespace PluralKit.Bot
{
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {AvatarUrl = img.Url}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { AvatarUrl = img.Url }));
var msg = img.Source switch
{
AvatarSource.User => $"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.",
@ -292,11 +295,11 @@ namespace PluralKit.Bot
AvatarSource.Attachment => $"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.",
_ => throw new ArgumentOutOfRangeException()
};
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -316,7 +319,7 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("your system's icon"))
await ClearIcon();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetIcon(img);
else
await ShowIcon();
@ -328,7 +331,7 @@ namespace PluralKit.Bot
async Task ClearImage()
{
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {BannerImage = null}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { BannerImage = null }));
await ctx.Reply($"{Emojis.Success} System banner image cleared.");
}
@ -336,7 +339,7 @@ namespace PluralKit.Bot
{
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, isFullSizeImage: true);
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {BannerImage = img.Url}));
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch { BannerImage = img.Url }));
var msg = img.Source switch
{
@ -348,8 +351,8 @@ namespace PluralKit.Bot
// The attachment's already right there, no need to preview it.
var hasEmbed = img.Source != AvatarSource.Attachment;
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
await (hasEmbed
? ctx.Reply(msg, embed: new EmbedBuilder().Image(new(img.Url)).Build())
: ctx.Reply(msg));
}
@ -369,13 +372,14 @@ namespace PluralKit.Bot
if (await ctx.MatchClear("your system's banner image"))
await ClearImage();
else if (await ctx.MatchImage() is {} img)
else if (await ctx.MatchImage() is { } img)
await SetImage(img);
else
await ShowImage();
}
public async Task Delete(Context ctx) {
public async Task Delete(Context ctx)
{
ctx.CheckSystem();
await ctx.Reply($"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{ctx.System.Hid}`).\n**Note: this action is permanent.**");
@ -383,10 +387,10 @@ namespace PluralKit.Bot
throw new PKError($"System deletion cancelled. Note that you must reply with your system ID (`{ctx.System.Hid}`) *verbatim*.");
await _db.Execute(conn => _repo.DeleteSystem(conn, ctx.System.Id));
await ctx.Reply($"{Emojis.Success} System deleted.");
}
public async Task SystemProxy(Context ctx)
{
ctx.CheckSystem();
@ -415,7 +419,7 @@ namespace PluralKit.Bot
return;
}
var patch = new SystemGuildPatch {ProxyEnabled = newValue};
var patch = new SystemGuildPatch { ProxyEnabled = newValue };
await _db.Execute(conn => _repo.UpsertSystemGuild(conn, ctx.System.Id, guild.Id, patch));
if (newValue)
@ -423,20 +427,20 @@ namespace PluralKit.Bot
else
await ctx.Reply($"Message proxying in {serverText} is now **disabled** for your system.");
}
public async Task SystemTimezone(Context ctx)
public async Task SystemTimezone(Context ctx)
{
if (ctx.System == null) throw Errors.NoSystemError;
if (await ctx.MatchClear())
{
var clearPatch = new SystemPatch {UiTz = "UTC"};
var clearPatch = new SystemPatch { UiTz = "UTC" };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, clearPatch));
await ctx.Reply($"{Emojis.Success} System time zone cleared (set to UTC).");
return;
}
var zoneStr = ctx.RemainderOrNull();
if (zoneStr == null)
{
@ -451,8 +455,8 @@ namespace PluralKit.Bot
var currentTime = SystemClock.Instance.GetCurrentInstant().InZone(zone);
var msg = $"This will change the system time zone to **{zone.Id}**. The current time is **{currentTime.FormatZoned()}**. Is this correct?";
if (!await ctx.PromptYesNo(msg, "Change Timezone")) throw Errors.TimezoneChangeCancelled;
var patch = new SystemPatch {UiTz = zone.Id};
var patch = new SystemPatch { UiTz = zone.Id };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply($"System time zone changed to **{zone.Id}**.");
@ -522,45 +526,49 @@ namespace PluralKit.Bot
await SetLevel(ctx.PopSystemPrivacySubject(), ctx.PopPrivacyLevel());
}
public async Task SystemPing(Context ctx)
{
ctx.CheckSystem();
public async Task SystemPing(Context ctx)
{
ctx.CheckSystem();
if (!ctx.HasNext())
{
if (ctx.System.PingsEnabled) {await ctx.Reply("Reaction pings are currently **enabled** for your system. To disable reaction pings, type `pk;s ping disable`.");}
else {await ctx.Reply("Reaction pings are currently **disabled** for your system. To enable reaction pings, type `pk;s ping enable`.");}
}
else {
if (ctx.Match("on", "enable")) {
var patch = new SystemPatch {PingsEnabled = true};
if (!ctx.HasNext())
{
if (ctx.System.PingsEnabled) { await ctx.Reply("Reaction pings are currently **enabled** for your system. To disable reaction pings, type `pk;s ping disable`."); }
else { await ctx.Reply("Reaction pings are currently **disabled** for your system. To enable reaction pings, type `pk;s ping enable`."); }
}
else
{
if (ctx.Match("on", "enable"))
{
var patch = new SystemPatch { PingsEnabled = true };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply("Reaction pings have now been enabled.");
}
if (ctx.Match("off", "disable")) {
var patch = new SystemPatch {PingsEnabled = false};
if (ctx.Match("off", "disable"))
{
var patch = new SystemPatch { PingsEnabled = false };
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id, patch));
await ctx.Reply("Reaction pings have now been disabled.");
}
}
}
}
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr)
{
// First, if we're given a flag emoji, we extract the flag emoji code from it.
zoneStr = Core.StringUtils.ExtractCountryFlag(zoneStr) ?? zoneStr;
// Then, we find all *locations* matching either the given country code or the country name.
var locations = TzdbDateTimeZoneSource.Default.Zone1970Locations;
var matchingLocations = locations.Where(l => l.Countries.Any(c =>
string.Equals(c.Code, zoneStr, StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(c.Name, zoneStr, StringComparison.InvariantCultureIgnoreCase)));
// Then, we find all (unique) time zone IDs that match.
var matchingZones = matchingLocations.Select(l => DateTimeZoneProviders.Tzdb.GetZoneOrNull(l.ZoneId))
.Distinct().ToList();
// If the set of matching zones is empty (ie. we didn't find anything), we try a few other things.
if (matchingZones.Count == 0)
{
@ -587,13 +595,13 @@ namespace PluralKit.Bot
matchingZones = allZones.Select(z => DateTimeZoneProviders.Tzdb.GetZoneOrNull(z))
.Where(z => z.GetUtcOffset(SystemClock.Instance.GetCurrentInstant()) == offset).ToList();
}
// If we have a list of viable time zones, we ask the user which is correct.
// If we only have one, return that one.
if (matchingZones.Count == 1)
return matchingZones.First();
// Otherwise, prompt and return!
return await ctx.Choose("There were multiple matches for your time zone query. Please select the region that matches you the closest:", matchingZones,
z =>
@ -605,4 +613,4 @@ namespace PluralKit.Bot
});
}
}
}
}

View file

@ -21,7 +21,7 @@ namespace PluralKit.Bot
_db = db;
_repo = repo;
}
struct FrontHistoryEntry
{
public readonly Instant? LastTime;
@ -40,10 +40,10 @@ namespace PluralKit.Bot
ctx.CheckSystemPrivacy(system, system.FrontPrivacy);
await using var conn = await _db.Obtain();
var sw = await _repo.GetLatestSwitch(conn, system.Id);
if (sw == null) throw Errors.NoRegisteredSwitches;
await ctx.Reply(embed: await _embeds.CreateFronterEmbed(sw, system.Zone, ctx.LookupContextFor(system)));
}
@ -54,7 +54,7 @@ namespace PluralKit.Bot
// Gotta be careful here: if we dispose of the connection while the IAE is alive, boom
await using var conn = await _db.Obtain();
var totalSwitches = await _repo.GetSwitchCount(conn, system.Id);
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
@ -78,10 +78,10 @@ namespace PluralKit.Bot
var lastSw = entry.LastTime;
var sw = entry.ThisSwitch;
// Fetch member list and format
await using var conn = await _db.Obtain();
var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id)).ToListAsync();
var membersStr = members.Any() ? string.Join(", ", members.Select(m => m.NameFor(ctx))) : "no fronter";
@ -111,7 +111,7 @@ namespace PluralKit.Bot
}
);
}
public async Task SystemFrontPercent(Context ctx, PKSystem system)
{
if (system == null) throw Errors.NoSystemError;
@ -121,7 +121,7 @@ namespace PluralKit.Bot
if (totalSwitches == 0) throw Errors.NoRegisteredSwitches;
string durationStr = ctx.RemainderOrNull() ?? "30d";
var now = SystemClock.Instance.GetCurrentInstant();
var rangeStart = DateUtils.ParseDateTime(durationStr, true, system.Zone);
@ -129,7 +129,7 @@ namespace PluralKit.Bot
if (rangeStart.Value.ToInstant() > now) throw Errors.FrontPercentTimeInFuture;
var title = new StringBuilder($"Frontpercent of ");
if (system.Name != null)
if (system.Name != null)
title.Append($"{system.Name} (`{system.Hid}`)");
else
title.Append($"`{system.Hid}`");

View file

@ -18,13 +18,13 @@ namespace PluralKit.Bot
_db = db;
_repo = repo;
}
public async Task LinkSystem(Context ctx)
{
ctx.CheckSystem();
await using var conn = await _db.Obtain();
var account = await ctx.MatchUser() ?? throw new PKSyntaxError("You must pass an account to link with (either ID or @mention).");
var accountIds = await _repo.GetSystemAccounts(conn, ctx.System.Id);
if (accountIds.Contains(account.Id))
@ -32,7 +32,7 @@ namespace PluralKit.Bot
var existingAccount = await _repo.GetSystemByAccount(conn, account.Id);
if (existingAccount != null)
throw Errors.AccountInOtherSystem(existingAccount);
throw Errors.AccountInOtherSystem(existingAccount);
var msg = $"{account.Mention()}, please confirm the link.";
if (!await ctx.PromptYesNo(msg, "Confirm", user: account, matchFlag: false)) throw Errors.MemberLinkCancelled;
@ -43,7 +43,7 @@ namespace PluralKit.Bot
public async Task UnlinkAccount(Context ctx)
{
ctx.CheckSystem();
await using var conn = await _db.Obtain();
ulong id;
@ -55,7 +55,7 @@ namespace PluralKit.Bot
var accountIds = (await _repo.GetSystemAccounts(conn, ctx.System.Id)).ToList();
if (!accountIds.Contains(id)) throw Errors.AccountNotLinked;
if (accountIds.Count == 1) throw Errors.UnlinkingLastAccount;
var msg = $"Are you sure you want to unlink <@{id}> from your system?";
if (!await ctx.PromptYesNo(msg, "Unlink")) throw Errors.MemberUnlinkCancelled;

View file

@ -8,7 +8,7 @@ namespace PluralKit.Bot
public class SystemList
{
private readonly IDatabase _db;
public SystemList(IDatabase db)
{
_db = db;
@ -26,15 +26,15 @@ namespace PluralKit.Bot
private string GetEmbedTitle(PKSystem target, MemberListOptions opts)
{
var title = new StringBuilder("Members of ");
if (target.Name != null)
if (target.Name != null)
title.Append($"{target.Name} (`{target.Hid}`)");
else
else
title.Append($"`{target.Hid}`");
if (opts.Search != null)
title.Append($" matching **{opts.Search}**");
return title.ToString();
}
}

View file

@ -25,7 +25,7 @@ namespace PluralKit.Bot
// Get or make a token
var token = ctx.System.Token ?? await MakeAndSetNewToken(ctx.System);
try
{
// DM the user a security disclaimer, and then the token in a separate message (for easy copying on mobile)
@ -34,7 +34,7 @@ namespace PluralKit.Bot
{
Content = $"{Emojis.Warn} Please note that this grants access to modify (and delete!) all your system data, so keep it safe and secure. If it leaks or you need a new one, you can invalidate this one with `pk;token refresh`.\n\nYour token is below:"
});
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest {Content = token});
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = token });
// If we're not already in a DM, reply with a reminder to check
if (ctx.Channel.Type != Channel.ChannelType.Dm)
@ -50,15 +50,15 @@ namespace PluralKit.Bot
private async Task<string> MakeAndSetNewToken(PKSystem system)
{
var patch = new SystemPatch {Token = StringUtils.GenerateToken()};
var patch = new SystemPatch { Token = StringUtils.GenerateToken() };
system = await _db.Execute(conn => _repo.UpdateSystem(conn, system.Id, patch));
return system.Token;
}
public async Task RefreshToken(Context ctx)
{
ctx.CheckSystem();
if (ctx.System.Token == null)
{
// If we don't have a token, call the other method instead
@ -67,19 +67,20 @@ namespace PluralKit.Bot
return;
}
try {
try
{
// DM the user an invalidation disclaimer, and then the token in a separate message (for easy copying on mobile)
var dm = await ctx.Cache.GetOrCreateDmChannel(ctx.Rest, ctx.Author.Id);
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest
{
Content = $"{Emojis.Warn} Your previous API token has been invalidated. You will need to change it anywhere it's currently used.\n\nYour token is below:"
});
// Make the new token after sending the first DM; this ensures if we can't DM, we also don't end up
// breaking their existing token as a side effect :)
var token = await MakeAndSetNewToken(ctx.System);
await ctx.Rest.CreateMessage(dm.Id, new MessageRequest { Content = token });
// If we're not already in a DM, reply with a reminder to check
if (ctx.Channel.Type != Channel.ChannelType.Dm)
await ctx.Reply($"{Emojis.Success} Check your DMs!");