mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-06 13:57:54 +00:00
commit
f9c5b8682b
56 changed files with 979 additions and 274 deletions
4
.github/workflows/dotnetcore.yml
vendored
4
.github/workflows/dotnetcore.yml
vendored
|
|
@ -4,9 +4,7 @@ on: [push, pull_request]
|
|||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
|
|
@ -14,6 +12,6 @@ jobs:
|
|||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 3.1.100
|
||||
dotnet-version: 5.0.x
|
||||
- name: Build and test with dotnet
|
||||
run: dotnet test --configuration Release
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This enables XML generation for Swashbuckle -->
|
||||
|
|
|
|||
|
|
@ -86,8 +86,33 @@ namespace PluralKit.Bot
|
|||
|
||||
members.Add(member); // Then add to the final output list
|
||||
}
|
||||
if (members.Count == 0) throw new PKSyntaxError($"You must input at least one member.");
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public static async Task<List<PKGroup>> ParseGroupList(this Context ctx, SystemId? restrictToSystem)
|
||||
{
|
||||
var groups = new List<PKGroup>();
|
||||
|
||||
// Loop through all the given arguments
|
||||
while (ctx.HasNext())
|
||||
{
|
||||
// and attempt to match a group
|
||||
var group = await ctx.MatchGroup();
|
||||
if (group == null)
|
||||
// if we can't, big error. Every group name must be valid.
|
||||
throw new PKError(ctx.CreateGroupNotFoundError(ctx.PopArgument()));
|
||||
|
||||
if (restrictToSystem != null && group.System != restrictToSystem)
|
||||
throw Errors.NotOwnGroupError; // TODO: name *which* group?
|
||||
|
||||
groups.Add(group); // Then add to the final output list
|
||||
}
|
||||
|
||||
if (groups.Count == 0) throw new PKSyntaxError($"You must input at least one group.");
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ namespace PluralKit.Bot
|
|||
return null;
|
||||
|
||||
var channel = await ctx.Shard.GetChannel(id);
|
||||
if (channel == null || channel.Type != ChannelType.Text) return null;
|
||||
if (channel == null || !(channel.Type == ChannelType.Text || channel.Type == ChannelType.News)) return null;
|
||||
|
||||
ctx.PopArgument();
|
||||
return channel;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
using Humanizer;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
|
|
@ -20,9 +22,10 @@ namespace PluralKit.Bot
|
|||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task AutoproxyRoot(Context ctx)
|
||||
public async Task SetAutoproxyMode(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem().CheckGuildContext();
|
||||
// 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);
|
||||
|
|
@ -122,9 +125,86 @@ namespace PluralKit.Bot
|
|||
default: throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (!ctx.MessageContext.AllowAutoproxy)
|
||||
eb.AddField("\u200b", $"{Emojis.Note} Autoproxy is currently **disabled** for your account (<@{ctx.Author.Id}>). To enable it, use `pk;autoproxy account enable`.");
|
||||
|
||||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task AutoproxyTimeout(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
var timeout = ctx.System.LatchTimeout.HasValue
|
||||
? 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()}.");
|
||||
else if (timeout == Duration.Zero)
|
||||
await ctx.Reply("Latch timeout is currently **disabled** for your system. Latch mode autoproxy will never time out.");
|
||||
else
|
||||
await ctx.Reply($"The current latch timeout duration for your system is {timeout.Value.ToTimeSpan().Humanize()}.");
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: somehow parse a more human-friendly date format
|
||||
int newTimeoutHours;
|
||||
if (ctx.Match("off", "stop", "cancel", "no", "disable", "remove")) newTimeoutHours = 0;
|
||||
else if (ctx.Match("reset", "default")) newTimeoutHours = -1;
|
||||
else if (!int.TryParse(ctx.RemainderOrNull(), out newTimeoutHours)) throw new PKError("Duration must be a number of hours.");
|
||||
|
||||
int? overflow = null;
|
||||
if (newTimeoutHours > 100000)
|
||||
{
|
||||
// sanity check to prevent seconds overflow if someone types in 999999999
|
||||
overflow = newTimeoutHours;
|
||||
newTimeoutHours = 0;
|
||||
}
|
||||
|
||||
var newTimeout = newTimeoutHours > -1 ? Duration.FromHours(newTimeoutHours) : (Duration?) null;
|
||||
await _db.Execute(conn => _repo.UpdateSystem(conn, ctx.System.Id,
|
||||
new SystemPatch { LatchTimeout = (int?) newTimeout?.TotalSeconds }));
|
||||
|
||||
if (newTimeoutHours == -1)
|
||||
await ctx.Reply($"{Emojis.Success} Latch timeout reset to default ({ProxyMatcher.DefaultLatchExpiryTime.ToTimeSpan().Humanize()}).");
|
||||
else if (newTimeoutHours == 0 && overflow != null)
|
||||
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out. ({overflow} hours is too long)");
|
||||
else if (newTimeoutHours == 0)
|
||||
await ctx.Reply($"{Emojis.Success} Latch timeout disabled. Latch mode autoproxy will never time out.");
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Success} Latch timeout set to {newTimeout.Value!.ToTimeSpan().Humanize()}.");
|
||||
}
|
||||
|
||||
public async Task AutoproxyAccount(Context ctx)
|
||||
{
|
||||
// todo: this might be useful elsewhere, consider moving it to ctx.MatchToggle
|
||||
if (ctx.Match("enable", "on"))
|
||||
await AutoproxyEnableDisable(ctx, true);
|
||||
else if (ctx.Match("disable", "off"))
|
||||
await AutoproxyEnableDisable(ctx, false);
|
||||
else if (ctx.HasNext())
|
||||
throw new PKSyntaxError("You must pass either \"on\" or \"off\".");
|
||||
else
|
||||
{
|
||||
var statusString = ctx.MessageContext.AllowAutoproxy ? "enabled" : "disabled";
|
||||
await ctx.Reply($"Autoproxy is currently **{statusString}** for account <@{ctx.Author.Id}>.", mentions: new IMention[]{});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AutoproxyEnableDisable(Context ctx, bool allow)
|
||||
{
|
||||
var statusString = allow ? "enabled" : "disabled";
|
||||
if (ctx.MessageContext.AllowAutoproxy == allow)
|
||||
{
|
||||
await ctx.Reply($"{Emojis.Note} Autoproxy is already {statusString} for account <@{ctx.Author.Id}>.", mentions: new IMention[]{});
|
||||
return;
|
||||
}
|
||||
var patch = new AccountPatch { AllowAutoproxy = allow };
|
||||
await _db.Execute(conn => _repo.UpdateAccount(conn, ctx.Author.Id, patch));
|
||||
await ctx.Reply($"{Emojis.Success} Autoproxy {statusString} for account <@{ctx.Author.Id}>.", mentions: new IMention[]{});
|
||||
}
|
||||
|
||||
private Task UpdateAutoproxy(Context ctx, AutoproxyMode autoproxyMode, MemberId? autoproxyMember)
|
||||
{
|
||||
var patch = new SystemGuildPatch {AutoproxyMode = autoproxyMode, AutoproxyMember = autoproxyMember};
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ namespace PluralKit.Bot
|
|||
public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown");
|
||||
public static Command SystemPing = new Command("system ping", "system ping <enable|disable>", "Changes your system's ping preferences");
|
||||
public static Command SystemPrivacy = new Command("system privacy", "system privacy <description|members|fronter|fronthistory|all> <public|private>", "Changes your system's privacy settings");
|
||||
public static Command Autoproxy = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for this server");
|
||||
public static Command AutoproxySet = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for the current server");
|
||||
public static Command AutoproxyTimeout = new Command("autoproxy", "autoproxy timeout [<duration>|off|reset]", "Sets the latch timeout duration for your system");
|
||||
public static Command AutoproxyAccount = new Command("autoproxy", "autoproxy account [on|off]", "Toggles autoproxy globally for the current account");
|
||||
public static Command MemberInfo = new Command("member", "member <member>", "Looks up information about a member");
|
||||
public static Command MemberNew = new Command("member new", "member new <name>", "Creates a new member");
|
||||
public static Command MemberRename = new Command("member rename", "member <member> rename <new name>", "Renames a member");
|
||||
|
|
@ -39,11 +41,15 @@ namespace PluralKit.Bot
|
|||
public static Command MemberProxy = new Command("member proxy", "member <member> proxy [add|remove] [example proxy]", "Changes, adds, or removes a member's proxy tags");
|
||||
public static Command MemberDelete = new Command("member delete", "member <member> delete", "Deletes a member");
|
||||
public static Command MemberAvatar = new Command("member avatar", "member <member> avatar [url|@mention]", "Changes a member's avatar");
|
||||
public static Command MemberGroups = new Command("member group", "member <member> group", "Shows the groups a member is in");
|
||||
public static Command MemberGroupAdd = new Command("member group", "member <member> group add <group> [group 2] [group 3...]", "Adds a member to one or more groups");
|
||||
public static Command MemberGroupRemove = new Command("member group", "member <member> group remove <group> [group 2] [group 3...]", "Removes a member from one or more groups");
|
||||
public static Command MemberServerAvatar = new Command("member serveravatar", "member <member> serveravatar [url|@mention]", "Changes a member's avatar in the current server");
|
||||
public static Command MemberDisplayName = new Command("member displayname", "member <member> displayname [display name]", "Changes a member's display name");
|
||||
public static Command MemberServerName = new Command("member servername", "member <member> servername [server name]", "Changes a member's display name in the current server");
|
||||
public static Command MemberAutoproxy = new Command("member autoproxy", "member <member> autoproxy [on|off]", "Sets whether a member will be autoproxied when autoproxy is set to latch or front mode.");
|
||||
public static Command MemberKeepProxy = new Command("member keepproxy", "member <member> keepproxy [on|off]", "Sets whether to include a member's proxy tags when proxying");
|
||||
public static Command MemberRandom = new Command("random", "random", "Looks up a random member from your system");
|
||||
public static Command MemberRandom = new Command("random", "random", "Shows the info card of a randomly selected member in your system.");
|
||||
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
|
||||
public static Command GroupInfo = new Command("group", "group <name>", "Looks up information about a group");
|
||||
public static Command GroupNew = new Command("group new", "group new <name>", "Creates a new group");
|
||||
|
|
@ -57,10 +63,13 @@ namespace PluralKit.Bot
|
|||
public static Command GroupPrivacy = new Command("group privacy", "group <group> privacy <description|icon|visibility|all> <public|private>", "Changes a group's privacy settings");
|
||||
public static Command GroupIcon = new Command("group icon", "group <group> icon [url|@mention]", "Changes a group's icon");
|
||||
public static Command GroupDelete = new Command("group delete", "group <group> delete", "Deletes a group");
|
||||
public static Command GroupMemberRandom = new Command("group random", "group <group> random", "Shows the info card of a randomly selected member in a group.");
|
||||
public static Command GroupRandom = new Command("random", "random group", "Shows the info card of a randomly selected group in your system.");
|
||||
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "Registers a switch");
|
||||
public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members");
|
||||
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
|
||||
public static Command SwitchDelete = new Command("switch delete", "switch delete [all]", "Deletes the latest switch (or them all)");
|
||||
public static Command SwitchDelete = new Command("switch delete", "switch delete", "Deletes the latest switch");
|
||||
public static Command SwitchDeleteAll = new Command("switch delete", "switch delete all", "Deletes all logged switches");
|
||||
public static Command Link = new Command("link", "link <account>", "Links your system to another account");
|
||||
public static Command Unlink = new Command("unlink", "unlink [account]", "Unlinks your system from an account");
|
||||
public static Command TokenGet = new Command("token", "token", "Gets your system's API token");
|
||||
|
|
@ -71,6 +80,7 @@ namespace PluralKit.Bot
|
|||
public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying");
|
||||
public static Command Message = new Command("message", "message <id|link>", "Looks up a proxied message");
|
||||
public static Command LogChannel = new Command("log channel", "log channel <channel>", "Designates a channel to post proxied messages to");
|
||||
public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel");
|
||||
public static Command LogEnable = new Command("log enable", "log enable all|<channel> [channel 2] [channel 3...]", "Enables message logging in certain channels");
|
||||
public static Command LogDisable = new Command("log disable", "log disable all|<channel> [channel 2] [channel 3...]", "Disables message logging in certain channels");
|
||||
public static Command LogClean = new Command("logclean", "logclean [on|off]", "Toggles whether to clean up other bots' log channels");
|
||||
|
|
@ -87,8 +97,8 @@ namespace PluralKit.Bot
|
|||
|
||||
public static Command[] MemberCommands = {
|
||||
MemberInfo, MemberNew, MemberRename, MemberDisplayName, MemberServerName, MemberDesc, MemberPronouns,
|
||||
MemberColor, MemberBirthday, MemberProxy, MemberKeepProxy, MemberDelete, MemberAvatar, MemberServerAvatar, MemberPrivacy,
|
||||
MemberRandom
|
||||
MemberColor, MemberBirthday, MemberProxy, MemberAutoproxy, MemberKeepProxy, MemberGroups, MemberGroupAdd, MemberGroupRemove,
|
||||
MemberDelete, MemberAvatar, MemberServerAvatar, MemberPrivacy, MemberRandom
|
||||
};
|
||||
|
||||
public static Command[] GroupCommands =
|
||||
|
|
@ -100,12 +110,16 @@ namespace PluralKit.Bot
|
|||
public static Command[] GroupCommandsTargeted =
|
||||
{
|
||||
GroupInfo, GroupAdd, GroupRemove, GroupMemberList, GroupRename, GroupDesc, GroupIcon, GroupPrivacy,
|
||||
GroupDelete
|
||||
GroupDelete, GroupMemberRandom
|
||||
};
|
||||
|
||||
public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete};
|
||||
public static Command[] SwitchCommands = {Switch, SwitchOut, SwitchMove, SwitchDelete, SwitchDeleteAll};
|
||||
|
||||
public static Command[] LogCommands = {LogChannel, LogEnable, LogDisable};
|
||||
public static Command[] AutoproxyCommands = {AutoproxySet, AutoproxyTimeout, AutoproxyAccount};
|
||||
|
||||
public static Command[] LogCommands = {LogChannel, LogChannelClear, LogEnable, LogDisable};
|
||||
|
||||
public static Command[] BlacklistCommands = {BlacklistAdd, BlacklistRemove, BlacklistShow};
|
||||
|
||||
private DiscordShardedClient _client;
|
||||
|
||||
|
|
@ -125,12 +139,12 @@ namespace PluralKit.Bot
|
|||
return HandleGroupCommand(ctx);
|
||||
if (ctx.Match("switch", "sw"))
|
||||
return HandleSwitchCommand(ctx);
|
||||
if (ctx.Match("commands", "cmd", "c"))
|
||||
return CommandHelpRoot(ctx);
|
||||
if (ctx.Match("ap", "autoproxy", "auto"))
|
||||
return ctx.Execute<Autoproxy>(Autoproxy, m => m.AutoproxyRoot(ctx));
|
||||
if (ctx.Match("list", "l", "members"))
|
||||
return HandleAutoproxyCommand(ctx);
|
||||
if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd"))
|
||||
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
|
||||
if (ctx.Match("f", "find", "search", "query", "fd"))
|
||||
return ctx.Execute<SystemList>(SystemFind, m => m.MemberList(ctx, ctx.System));
|
||||
if (ctx.Match("link"))
|
||||
return ctx.Execute<SystemLink>(Link, m => m.LinkSystem(ctx));
|
||||
if (ctx.Match("unlink"))
|
||||
|
|
@ -152,8 +166,6 @@ namespace PluralKit.Bot
|
|||
else return ctx.Execute<Help>(Help, m => m.HelpRoot(ctx));
|
||||
if (ctx.Match("explain"))
|
||||
return ctx.Execute<Help>(Explain, m => m.Explain(ctx));
|
||||
if (ctx.Match("commands"))
|
||||
return ctx.Reply("For the list of commands, see the website: <https://pluralkit.me/commands>");
|
||||
if (ctx.Match("message", "msg"))
|
||||
return ctx.Execute<Misc>(Message, m => m.GetMessage(ctx));
|
||||
if (ctx.Match("log"))
|
||||
|
|
@ -163,6 +175,8 @@ namespace PluralKit.Bot
|
|||
return ctx.Execute<ServerConfig>(LogEnable, m => m.SetLogEnabled(ctx, true));
|
||||
else if (ctx.Match("disable", "off"))
|
||||
return ctx.Execute<ServerConfig>(LogDisable, m => m.SetLogEnabled(ctx, false));
|
||||
else if (ctx.Match("commands"))
|
||||
return PrintCommandList(ctx, "message logging", LogCommands);
|
||||
else return PrintCommandExpectedError(ctx, LogCommands);
|
||||
if (ctx.Match("logclean"))
|
||||
return ctx.Execute<ServerConfig>(LogClean, m => m.SetLogCleanup(ctx));
|
||||
|
|
@ -173,7 +187,9 @@ namespace PluralKit.Bot
|
|||
return ctx.Execute<ServerConfig>(BlacklistRemove, m => m.SetBlacklisted(ctx, false));
|
||||
else if (ctx.Match("list", "show"))
|
||||
return ctx.Execute<ServerConfig>(BlacklistShow, m => m.ShowBlacklisted(ctx));
|
||||
else return PrintCommandExpectedError(ctx, BlacklistAdd, BlacklistRemove, BlacklistShow);
|
||||
else if (ctx.Match("commands"))
|
||||
return PrintCommandList(ctx, "channel blacklisting", BlacklistCommands);
|
||||
else return PrintCommandExpectedError(ctx, BlacklistCommands);
|
||||
if (ctx.Match("proxy", "enable", "disable"))
|
||||
return ctx.Execute<SystemEdit>(SystemProxy, m => m.SystemProxy(ctx));
|
||||
if (ctx.Match("invite")) return ctx.Execute<Misc>(Invite, m => m.Invite(ctx));
|
||||
|
|
@ -187,7 +203,10 @@ namespace PluralKit.Bot
|
|||
if (ctx.Match("permcheck"))
|
||||
return ctx.Execute<Misc>(PermCheck, m => m.PermCheckGuild(ctx));
|
||||
if (ctx.Match("random", "r"))
|
||||
return ctx.Execute<Member>(MemberRandom, m => m.MemberRandom(ctx));
|
||||
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));
|
||||
|
||||
// remove compiler warning
|
||||
return ctx.Reply(
|
||||
|
|
@ -322,12 +341,21 @@ namespace PluralKit.Bot
|
|||
await ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, target));
|
||||
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
|
||||
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.Avatar(ctx, target));
|
||||
else if (ctx.Match("group", "groups"))
|
||||
if (ctx.Match("add", "a"))
|
||||
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
|
||||
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));
|
||||
else if (ctx.Match("displayname", "dn", "dname", "nick", "nickname", "dispname"))
|
||||
await ctx.Execute<MemberEdit>(MemberDisplayName, m => m.DisplayName(ctx, target));
|
||||
else if (ctx.Match("servername", "sn", "sname", "snick", "snickname", "servernick", "servernickname", "serverdisplayname", "guildname", "guildnick", "guildnickname", "serverdn"))
|
||||
await ctx.Execute<MemberEdit>(MemberServerName, m => m.ServerName(ctx, target));
|
||||
else if (ctx.Match("autoproxy", "ap"))
|
||||
await ctx.Execute<MemberEdit>(MemberAutoproxy, m => m.MemberAutoproxy(ctx, target));
|
||||
else if (ctx.Match("keepproxy", "keeptags", "showtags"))
|
||||
await ctx.Execute<MemberEdit>(MemberKeepProxy, m => m.KeepProxy(ctx, target));
|
||||
else if (ctx.Match("privacy"))
|
||||
|
|
@ -336,6 +364,8 @@ namespace PluralKit.Bot
|
|||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private));
|
||||
else if (ctx.Match("public", "shown", "show"))
|
||||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public));
|
||||
else if (ctx.Match("soulscream"))
|
||||
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
|
||||
|
|
@ -366,6 +396,8 @@ namespace PluralKit.Bot
|
|||
await ctx.Execute<Groups>(GroupRemove, g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
|
||||
else if (ctx.Match("members", "list", "ms", "l"))
|
||||
await ctx.Execute<Groups>(GroupMemberList, g => g.ListGroupMembers(ctx, target));
|
||||
else if (ctx.Match("random"))
|
||||
await ctx.Execute<Random>(GroupMemberRandom, r => r.GroupMember(ctx, target));
|
||||
else if (ctx.Match("privacy"))
|
||||
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null));
|
||||
else if (ctx.Match("public", "pub"))
|
||||
|
|
@ -403,6 +435,74 @@ namespace PluralKit.Bot
|
|||
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchDelete, SystemFronter, SystemFrontHistory);
|
||||
}
|
||||
|
||||
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()) {
|
||||
case "system":
|
||||
case "systems":
|
||||
case "s":
|
||||
await PrintCommandList(ctx, "systems", SystemCommands);
|
||||
break;
|
||||
case "member":
|
||||
case "members":
|
||||
case "m":
|
||||
await PrintCommandList(ctx, "members", MemberCommands);
|
||||
break;
|
||||
case "group":
|
||||
case "groups":
|
||||
case "g":
|
||||
await PrintCommandList(ctx, "groups", GroupCommands);
|
||||
break;
|
||||
case "switch":
|
||||
case "switches":
|
||||
case "switching":
|
||||
case "sw":
|
||||
await PrintCommandList(ctx, "switching", SwitchCommands);
|
||||
break;
|
||||
case "log":
|
||||
await PrintCommandList(ctx, "message logging", LogCommands);
|
||||
break;
|
||||
case "blacklist":
|
||||
case "bl":
|
||||
await PrintCommandList(ctx, "channel blacklisting", BlacklistCommands);
|
||||
break;
|
||||
case "autoproxy":
|
||||
case "ap":
|
||||
await PrintCommandList(ctx, "autoproxy", AutoproxyCommands);
|
||||
break;
|
||||
// todo: are there any commands that still need to be added?
|
||||
default:
|
||||
await ctx.Reply("For the full list of commands, see the website: <https://pluralkit.me/commands>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Task HandleAutoproxyCommand(Context ctx)
|
||||
{
|
||||
// todo: merge this with the changes from #251
|
||||
if (ctx.Match("commands"))
|
||||
return PrintCommandList(ctx, "autoproxy", AutoproxyCommands);
|
||||
|
||||
// ctx.CheckSystem();
|
||||
// oops, that breaks stuff! PKErrors before ctx.Execute don't actually do anything.
|
||||
// so we just emulate checking and throwing an error.
|
||||
if (ctx.System == null)
|
||||
return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError.Message}");
|
||||
|
||||
if (ctx.Match("account", "ac"))
|
||||
return ctx.Execute<Autoproxy>(AutoproxyAccount, m => m.AutoproxyAccount(ctx));
|
||||
else if (ctx.Match("timeout", "tm"))
|
||||
return ctx.Execute<Autoproxy>(AutoproxyTimeout, m => m.AutoproxyTimeout(ctx));
|
||||
else
|
||||
return ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx));
|
||||
}
|
||||
|
||||
private async Task PrintCommandNotFoundError(Context ctx, params Command[] potentialCommands)
|
||||
{
|
||||
var commandListStr = CreatePotentialCommandList(potentialCommands);
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ namespace PluralKit.Bot
|
|||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly EmbedService _embeds;
|
||||
|
||||
public Groups(IDatabase db, ModelRepository repo)
|
||||
public Groups(IDatabase db, ModelRepository repo, EmbedService embeds)
|
||||
{
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
_embeds = embeds;
|
||||
}
|
||||
|
||||
public async Task CreateGroup(Context ctx)
|
||||
|
|
@ -177,8 +179,6 @@ namespace PluralKit.Bot
|
|||
{
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
if (img.Url.Length > Limits.MaxUriLength)
|
||||
throw Errors.InvalidUrl(img.Url);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(img.Url);
|
||||
|
||||
await _db.Execute(c => _repo.UpdateGroup(c, target.Id, new GroupPatch {Icon = img.Url}));
|
||||
|
|
@ -282,87 +282,46 @@ namespace PluralKit.Bot
|
|||
public async Task ShowGroupCard(Context ctx, PKGroup target)
|
||||
{
|
||||
await using var conn = await _db.Obtain();
|
||||
|
||||
var system = await GetGroupSystem(ctx, target, conn);
|
||||
var pctx = ctx.LookupContextFor(system);
|
||||
var memberCount = ctx.MatchPrivateFlag(pctx) ? await _repo.GetGroupMemberCount(conn, target.Id, PrivacyLevel.Public) : await _repo.GetGroupMemberCount(conn, target.Id);
|
||||
|
||||
var nameField = target.Name;
|
||||
if (system.Name != null)
|
||||
nameField = $"{nameField} ({system.Name})";
|
||||
|
||||
var eb = new DiscordEmbedBuilder()
|
||||
.WithAuthor(nameField, iconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx)))
|
||||
.WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}");
|
||||
|
||||
if (target.DisplayName != null)
|
||||
eb.AddField("Display Name", target.DisplayName);
|
||||
|
||||
if (target.ListPrivacy.CanAccess(pctx))
|
||||
{
|
||||
if (memberCount == 0 && pctx == LookupContext.ByOwner)
|
||||
// Only suggest the add command if this is actually the owner lol
|
||||
eb.AddField("Members (0)", $"Add one with `pk;group {target.Reference()} add <member>`!", true);
|
||||
else
|
||||
eb.AddField($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true);
|
||||
}
|
||||
|
||||
if (target.DescriptionFor(pctx) is {} desc)
|
||||
eb.AddField("Description", desc);
|
||||
|
||||
if (target.IconFor(pctx) is {} icon)
|
||||
eb.WithThumbnail(icon);
|
||||
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
await ctx.Reply(embed: await _embeds.CreateGroupEmbed(ctx, system, target));
|
||||
}
|
||||
|
||||
public async Task AddRemoveMembers(Context ctx, PKGroup target, AddRemoveOperation op)
|
||||
{
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
var members = await ctx.ParseMemberList(ctx.System.Id);
|
||||
var members = (await ctx.ParseMemberList(ctx.System.Id))
|
||||
.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}))
|
||||
.Select(m => m.Id.Value)
|
||||
.Distinct()
|
||||
.ToHashSet();
|
||||
|
||||
List<MemberId> toAction;
|
||||
|
||||
if (op == AddRemoveOperation.Add)
|
||||
{
|
||||
var membersNotInGroup = members
|
||||
.Where(m => !existingMembersInGroup.Contains(m.Id.Value))
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
toAction = members
|
||||
.Where(m => !existingMembersInGroup.Contains(m.Value))
|
||||
.ToList();
|
||||
await _repo.AddMembersToGroup(conn, target.Id, membersNotInGroup);
|
||||
|
||||
if (membersNotInGroup.Count == members.Count)
|
||||
await ctx.Reply(members.Count == 0 ? $"{Emojis.Success} Member added to group." : $"{Emojis.Success} {"members".ToQuantity(membersNotInGroup.Count)} added to group.");
|
||||
else
|
||||
if (membersNotInGroup.Count == 0)
|
||||
await ctx.Reply(members.Count == 1 ? $"{Emojis.Error} Member not added to group (member already in group)." : $"{Emojis.Error} No members added to group (members already in group).");
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Success} {"members".ToQuantity(membersNotInGroup.Count)} added to group ({"members".ToQuantity(members.Count - membersNotInGroup.Count)} already in group).");
|
||||
await _repo.AddMembersToGroup(conn, target.Id, toAction);
|
||||
}
|
||||
else if (op == AddRemoveOperation.Remove)
|
||||
{
|
||||
var membersInGroup = members
|
||||
.Where(m => existingMembersInGroup.Contains(m.Id.Value))
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
toAction = members
|
||||
.Where(m => existingMembersInGroup.Contains(m.Value))
|
||||
.ToList();
|
||||
await _repo.RemoveMembersFromGroup(conn, target.Id, membersInGroup);
|
||||
|
||||
if (membersInGroup.Count == members.Count)
|
||||
await ctx.Reply(members.Count == 0 ? $"{Emojis.Success} Member removed from group." : $"{Emojis.Success} {"members".ToQuantity(membersInGroup.Count)} removed from group.");
|
||||
else
|
||||
if (membersInGroup.Count == 0)
|
||||
await ctx.Reply(members.Count == 1 ? $"{Emojis.Error} Member not removed from group (member already not in group)." : $"{Emojis.Error} No members removed from group (members already not in group).");
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Success} {"members".ToQuantity(membersInGroup.Count)} removed from group ({"members".ToQuantity(members.Count - membersInGroup.Count)} already not in group).");
|
||||
await _repo.RemoveMembersFromGroup(conn, target.Id, toAction);
|
||||
}
|
||||
else return; // otherwise toAction "may be undefined"
|
||||
|
||||
await ctx.Reply(MiscUtils.GroupAddRemoveResponse<MemberId>(members, toAction, op));
|
||||
}
|
||||
|
||||
public async Task ListGroupMembers(Context ctx, PKGroup target)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Web;
|
||||
|
||||
using Dapper;
|
||||
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
|
|
@ -13,12 +18,14 @@ namespace PluralKit.Bot
|
|||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly EmbedService _embeds;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public Member(EmbedService embeds, IDatabase db, ModelRepository repo)
|
||||
public Member(EmbedService embeds, IDatabase db, ModelRepository repo, HttpClient client)
|
||||
{
|
||||
_embeds = embeds;
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task NewMember(Context ctx) {
|
||||
|
|
@ -61,33 +68,33 @@ namespace PluralKit.Bot
|
|||
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 MemberRandom(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var randGen = new global::System.Random();
|
||||
//Maybe move this somewhere else in the file structure since it doesn't need to get created at every command
|
||||
|
||||
// TODO: don't buffer these, find something else to do ig
|
||||
|
||||
var members = await _db.Execute(c =>
|
||||
{
|
||||
if (ctx.MatchFlag("all", "a"))
|
||||
return _repo.GetSystemMembers(c, ctx.System.Id);
|
||||
return _repo.GetSystemMembers(c, ctx.System.Id)
|
||||
.Where(m => m.MemberVisibility == PrivacyLevel.Public);
|
||||
}).ToListAsync();
|
||||
|
||||
if (members == null || !members.Any())
|
||||
throw Errors.NoMembersError;
|
||||
var randInt = randGen.Next(members.Count);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System)));
|
||||
}
|
||||
|
||||
public async Task ViewMember(Context ctx, PKMember target)
|
||||
{
|
||||
var system = await _db.Execute(c => _repo.GetSystem(c, target.System));
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system)));
|
||||
}
|
||||
|
||||
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
|
||||
return;
|
||||
|
||||
var data = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
var scream = data["soulscream"]!.Value<string>();
|
||||
|
||||
var eb = new DiscordEmbedBuilder()
|
||||
.WithColor(DiscordColor.Red)
|
||||
.WithTitle(name)
|
||||
.WithUrl($"https://onomancer.sibr.dev/reflect?name={encoded}")
|
||||
.WithDescription($"*{scream}*");
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,18 +102,11 @@ namespace PluralKit.Bot
|
|||
}
|
||||
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
await ValidateUrl(avatarArg.Value.Url);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(avatarArg.Value.Url);
|
||||
await UpdateAvatar(location, ctx, target, avatarArg.Value.Url);
|
||||
await PrintResponse(location, ctx, target, avatarArg.Value, guildData);
|
||||
}
|
||||
|
||||
private static Task ValidateUrl(string url)
|
||||
{
|
||||
if (url.Length > Limits.MaxUriLength)
|
||||
throw Errors.InvalidUrl(url);
|
||||
return AvatarUtils.VerifyAvatarOrThrow(url);
|
||||
}
|
||||
|
||||
private Task PrintResponse(AvatarLocation location, Context ctx, PKMember target, ParsedImage avatar,
|
||||
MemberGuildSettings? targetGuildData)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,10 +24,9 @@ namespace PluralKit.Bot
|
|||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task Name(Context ctx, PKMember target) {
|
||||
// TODO: this method is pretty much a 1:1 copy/paste of the above creation method, find a way to clean?
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
public async Task Name(Context ctx, PKMember target)
|
||||
{
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new name for the member.");
|
||||
|
||||
|
|
@ -58,15 +57,10 @@ namespace PluralKit.Bot
|
|||
}
|
||||
}
|
||||
|
||||
private void CheckEditMemberPermission(Context ctx, PKMember target)
|
||||
{
|
||||
if (target.System != ctx.System?.Id) throw Errors.NotOwnMemberError;
|
||||
}
|
||||
|
||||
public async Task Description(Context ctx, PKMember target) {
|
||||
if (await ctx.MatchClear("this member's description"))
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var patch = new MemberPatch {Description = Partial<string>.Null()};
|
||||
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
|
||||
|
|
@ -93,7 +87,7 @@ namespace PluralKit.Bot
|
|||
}
|
||||
else
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var description = ctx.RemainderOrNull().NormalizeLineEndSpacing();
|
||||
if (description.IsLongerThan(Limits.MaxDescriptionLength))
|
||||
|
|
@ -109,7 +103,8 @@ namespace PluralKit.Bot
|
|||
public async Task Pronouns(Context ctx, PKMember target) {
|
||||
if (await ctx.MatchClear("this member's pronouns"))
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
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.");
|
||||
|
|
@ -129,7 +124,7 @@ namespace PluralKit.Bot
|
|||
}
|
||||
else
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var pronouns = ctx.RemainderOrNull().NormalizeLineEndSpacing();
|
||||
if (pronouns.IsLongerThan(Limits.MaxPronounsLength))
|
||||
|
|
@ -147,7 +142,7 @@ namespace PluralKit.Bot
|
|||
var color = ctx.RemainderOrNull();
|
||||
if (await ctx.MatchClear())
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var patch = new MemberPatch {Color = Partial<string>.Null()};
|
||||
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
|
||||
|
|
@ -176,7 +171,7 @@ namespace PluralKit.Bot
|
|||
}
|
||||
else
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
if (color.StartsWith("#")) color = color.Substring(1);
|
||||
if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) throw Errors.InvalidColorError(color);
|
||||
|
|
@ -195,7 +190,7 @@ namespace PluralKit.Bot
|
|||
{
|
||||
if (await ctx.MatchClear("this member's birthday"))
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var patch = new MemberPatch {Birthday = Partial<LocalDate?>.Null()};
|
||||
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
|
||||
|
|
@ -216,7 +211,7 @@ namespace PluralKit.Bot
|
|||
}
|
||||
else
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var birthdayStr = ctx.RemainderOrNull();
|
||||
var birthday = DateUtils.ParseDate(birthdayStr, true);
|
||||
|
|
@ -281,7 +276,7 @@ namespace PluralKit.Bot
|
|||
|
||||
if (await ctx.MatchClear("this member's display name"))
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var patch = new MemberPatch {DisplayName = Partial<string>.Null()};
|
||||
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
|
||||
|
|
@ -298,7 +293,7 @@ namespace PluralKit.Bot
|
|||
}
|
||||
else
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var newDisplayName = ctx.RemainderOrNull();
|
||||
|
||||
|
|
@ -315,7 +310,7 @@ namespace PluralKit.Bot
|
|||
|
||||
if (await ctx.MatchClear("this member's server name"))
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var patch = new MemberGuildPatch {DisplayName = null};
|
||||
await _db.Execute(conn => _repo.UpsertMemberGuild(conn, target.Id, ctx.Guild.Id, patch));
|
||||
|
|
@ -335,7 +330,7 @@ namespace PluralKit.Bot
|
|||
}
|
||||
else
|
||||
{
|
||||
CheckEditMemberPermission(ctx, target);
|
||||
ctx.CheckOwnMember(target);
|
||||
|
||||
var newServerName = ctx.RemainderOrNull();
|
||||
|
||||
|
|
@ -348,8 +343,7 @@ namespace PluralKit.Bot
|
|||
|
||||
public async Task KeepProxy(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
bool newValue;
|
||||
if (ctx.Match("on", "enabled", "true", "yes")) newValue = true;
|
||||
|
|
@ -373,11 +367,37 @@ namespace PluralKit.Bot
|
|||
await ctx.Reply($"{Emojis.Success} Member proxy tags will now not be included in the resulting message when proxying.");
|
||||
}
|
||||
|
||||
public async Task Privacy(Context ctx, PKMember target, PrivacyLevel? newValueFromCommand)
|
||||
public async Task MemberAutoproxy(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
|
||||
bool newValue;
|
||||
if (ctx.Match("on", "enabled", "true", "yes") || ctx.MatchFlag("on", "enabled", "true", "yes")) newValue = true;
|
||||
else if (ctx.Match("off", "disabled", "false", "no") || ctx.MatchFlag("off", "disabled", "false", "no")) newValue = false;
|
||||
else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"on\" or \"off\".");
|
||||
else
|
||||
{
|
||||
if (target.AllowAutoproxy)
|
||||
await ctx.Reply("Latch/front autoproxy are **enabled** for this member. This member will be automatically proxied when autoproxy is set to latch or front mode.");
|
||||
else
|
||||
await ctx.Reply("Latch/front autoproxy are **disabled** for this member. This member will not be automatically proxied when autoproxy is set to latch or front mode.");
|
||||
return;
|
||||
};
|
||||
|
||||
var patch = new MemberPatch {AllowAutoproxy = Partial<bool>.Present(newValue)};
|
||||
await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch));
|
||||
|
||||
if (newValue)
|
||||
await ctx.Reply($"{Emojis.Success} Latch / front autoproxy have been **enabled** for this member.");
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Success} Latch / front autoproxy have been **disabled** for this member.");
|
||||
}
|
||||
|
||||
public async Task Privacy(Context ctx, PKMember target, PrivacyLevel? newValueFromCommand)
|
||||
{
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
// Display privacy settings
|
||||
if (!ctx.HasNext() && newValueFromCommand == null)
|
||||
{
|
||||
|
|
@ -466,8 +486,7 @@ namespace PluralKit.Bot
|
|||
|
||||
public async Task Delete(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
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;
|
||||
|
|
|
|||
90
PluralKit.Bot/Commands/MemberGroup.cs
Normal file
90
PluralKit.Bot/Commands/MemberGroup.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class MemberGroup
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public MemberGroup(IDatabase db, ModelRepository repo)
|
||||
{
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task AddRemove(Context ctx, PKMember target, Groups.AddRemoveOperation op)
|
||||
{
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
var groups = (await ctx.ParseGroupList(ctx.System.Id))
|
||||
.Select(g => g.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
await using var conn = await _db.Obtain();
|
||||
var existingGroups = (await _repo.GetMemberGroups(conn, target.Id).ToListAsync())
|
||||
.Select(g => g.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
List<GroupId> toAction;
|
||||
|
||||
if (op == Groups.AddRemoveOperation.Add)
|
||||
{
|
||||
toAction = groups
|
||||
.Where(group => !existingGroups.Contains(group))
|
||||
.ToList();
|
||||
|
||||
await _repo.AddGroupsToMember(conn, target.Id, toAction);
|
||||
}
|
||||
else if (op == Groups.AddRemoveOperation.Remove)
|
||||
{
|
||||
toAction = groups
|
||||
.Where(group => existingGroups.Contains(group))
|
||||
.ToList();
|
||||
|
||||
await _repo.RemoveGroupsFromMember(conn, target.Id, toAction);
|
||||
}
|
||||
else return; // otherwise toAction "may be unassigned"
|
||||
|
||||
await ctx.Reply(MiscUtils.GroupAddRemoveResponse<GroupId>(groups, toAction, op));
|
||||
}
|
||||
|
||||
public async Task List(Context ctx, PKMember target)
|
||||
{
|
||||
await using var conn = await _db.Obtain();
|
||||
|
||||
var pctx = ctx.LookupContextFor(target.System);
|
||||
|
||||
var groups = await _repo.GetMemberGroups(conn, target.Id)
|
||||
.Where(g => g.Visibility.CanAccess(pctx))
|
||||
.OrderBy(g => g.Name, StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToListAsync();
|
||||
|
||||
var description = "";
|
||||
var msg = "";
|
||||
|
||||
if (groups.Count == 0)
|
||||
description = "This member has no groups.";
|
||||
else
|
||||
description = string.Join("\n", groups.Select(g => $"[`{g.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, embed: (new DiscordEmbedBuilder().WithTitle($"{target.Name}'s groups").WithDescription(description)).Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,7 @@ namespace PluralKit.Bot
|
|||
|
||||
public async Task Proxy(Context ctx, PKMember target)
|
||||
{
|
||||
if (ctx.System == null) throw Errors.NoSystemError;
|
||||
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
ProxyTag ParseProxyTags(string exampleProxy)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace PluralKit.Bot {
|
|||
.Grant(Permissions.ManageWebhooks)
|
||||
.Grant(Permissions.ReadMessageHistory)
|
||||
.Grant(Permissions.SendMessages);
|
||||
var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot&permissions={(long)permissions}";
|
||||
var invite = $"https://discord.com/oauth2/authorize?client_id={clientId}&scope=bot%20applications.commands&permissions={(long)permissions}";
|
||||
await ctx.Reply($"{Emojis.Success} Use this link to add PluralKit to your server:\n<{invite}>");
|
||||
}
|
||||
|
||||
|
|
|
|||
79
PluralKit.Bot/Commands/Random.cs
Normal file
79
PluralKit.Bot/Commands/Random.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot
|
||||
{
|
||||
public class Random
|
||||
{
|
||||
private readonly IDatabase _db;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly EmbedService _embeds;
|
||||
|
||||
private readonly global::System.Random randGen = new global::System.Random();
|
||||
|
||||
public Random(EmbedService embeds, IDatabase db, ModelRepository repo)
|
||||
{
|
||||
_embeds = embeds;
|
||||
_db = db;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
// todo: get postgresql to return one random member/group instead of querying all members/groups
|
||||
|
||||
public async Task Member(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var members = await _db.Execute(c =>
|
||||
{
|
||||
if (ctx.MatchFlag("all", "a"))
|
||||
return _repo.GetSystemMembers(c, ctx.System.Id);
|
||||
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.");
|
||||
|
||||
var randInt = randGen.Next(members.Count);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System)));
|
||||
}
|
||||
|
||||
public async Task Group(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var groups = await _db.Execute(c => c.QueryGroupList(ctx.System.Id));
|
||||
if (!ctx.MatchFlag("all", "a"))
|
||||
groups = groups.Where(g => g.Visibility == PrivacyLevel.Public);
|
||||
|
||||
if (groups == null || !groups.Any())
|
||||
throw new PKError("Your system has no groups! Please create at least one group before using this command.");
|
||||
|
||||
var randInt = randGen.Next(groups.Count());
|
||||
await ctx.Reply(embed: await _embeds.CreateGroupEmbed(ctx, ctx.System, groups.ToArray()[randInt]));
|
||||
}
|
||||
|
||||
public async Task GroupMember(Context ctx, PKGroup group)
|
||||
{
|
||||
var opts = ctx.ParseMemberListOptions(ctx.LookupContextFor(group.System));
|
||||
opts.GroupFilter = group.Id;
|
||||
|
||||
await using var conn = await _db.Obtain();
|
||||
var members = await conn.QueryMemberList(ctx.System.Id, opts.ToQueryOptions());
|
||||
|
||||
if (members == null || !members.Any())
|
||||
throw new PKError("This group has no members! Please add at least one member to this group before using this command.");
|
||||
|
||||
if (!ctx.MatchFlag("all", "a"))
|
||||
members = members.Where(g => g.MemberVisibility == PrivacyLevel.Public);
|
||||
|
||||
var ms = members.ToList();
|
||||
|
||||
var randInt = randGen.Next(ms.Count);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, ms[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,8 +140,6 @@ namespace PluralKit.Bot
|
|||
|
||||
async Task SetIcon(ParsedImage img)
|
||||
{
|
||||
if (img.Url.Length > Limits.MaxUriLength)
|
||||
throw Errors.InvalidUrl(img.Url);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(img.Url);
|
||||
|
||||
await _db.Execute(c => _repo.UpdateSystem(c, ctx.System.Id, new SystemPatch {AvatarUrl = img.Url}));
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ namespace PluralKit.Bot {
|
|||
public static PKError MemberNameTooLongError(int length) => new PKError($"Member name too long ({length}/{Limits.MaxMemberNameLength} characters).");
|
||||
public static PKError MemberPronounsTooLongError(int length) => new PKError($"Member pronouns too long ({length}/{Limits.MaxMemberNameLength} characters).");
|
||||
public static PKError MemberLimitReachedError(int limit) => new PKError($"System has reached the maximum number of members ({limit}). Please delete unused members first in order to create new ones.");
|
||||
public static PKError NoMembersError => new PKError("Your system has no members! Please create at least one member before using this command.");
|
||||
|
||||
public static PKError InvalidColorError(string color) => new PKError($"\"{color}\" is not a valid color. Color must be in 6-digit RGB hex format (eg. #ff0000).");
|
||||
public static PKError BirthdayParseError(string birthday) => new PKError($"\"{birthday}\" could not be parsed as a valid date. Try a format like \"2016-12-24\" or \"May 3 1996\".");
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ namespace PluralKit.Bot
|
|||
{
|
||||
try
|
||||
{
|
||||
return await _proxy.HandleIncomingMessage(shard, evt.Message, ctx, allowAutoproxy: true);
|
||||
return await _proxy.HandleIncomingMessage(shard, evt.Message, ctx, allowAutoproxy: ctx.AllowAutoproxy);
|
||||
}
|
||||
catch (PKError e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,8 +40,10 @@ namespace PluralKit.Bot
|
|||
builder.RegisterType<Member>().AsSelf();
|
||||
builder.RegisterType<MemberAvatar>().AsSelf();
|
||||
builder.RegisterType<MemberEdit>().AsSelf();
|
||||
builder.RegisterType<MemberGroup>().AsSelf();
|
||||
builder.RegisterType<MemberProxy>().AsSelf();
|
||||
builder.RegisterType<Misc>().AsSelf();
|
||||
builder.RegisterType<Random>().AsSelf();
|
||||
builder.RegisterType<ServerConfig>().AsSelf();
|
||||
builder.RegisterType<Switch>().AsSelf();
|
||||
builder.RegisterType<System>().AsSelf();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace PluralKit.Bot
|
|||
public class ProxyMatcher
|
||||
{
|
||||
private static readonly char AutoproxyEscapeCharacter = '\\';
|
||||
private static readonly Duration LatchExpiryTime = Duration.FromHours(6);
|
||||
public static readonly Duration DefaultLatchExpiryTime = Duration.FromHours(6);
|
||||
|
||||
private readonly IClock _clock;
|
||||
private readonly ProxyTagParser _parser;
|
||||
|
|
@ -56,13 +56,13 @@ namespace PluralKit.Bot
|
|||
AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 =>
|
||||
members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]),
|
||||
|
||||
AutoproxyMode.Latch when ctx.LastMessageMember != null && !IsLatchExpired(ctx.LastMessage) =>
|
||||
AutoproxyMode.Latch when ctx.LastMessageMember != null && !IsLatchExpired(ctx) =>
|
||||
members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (member == null) return false;
|
||||
if (member == null || (ctx.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy)) return false;
|
||||
match = new ProxyMatch
|
||||
{
|
||||
Content = messageContent,
|
||||
|
|
@ -75,11 +75,17 @@ namespace PluralKit.Bot
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool IsLatchExpired(ulong? messageId)
|
||||
private bool IsLatchExpired(MessageContext ctx)
|
||||
{
|
||||
if (messageId == null) return true;
|
||||
var timestamp = DiscordUtils.SnowflakeToInstant(messageId.Value);
|
||||
return _clock.GetCurrentInstant() - timestamp > LatchExpiryTime;
|
||||
if (ctx.LastMessage == null) return true;
|
||||
if (ctx.LatchTimeout == 0) return false;
|
||||
|
||||
var timeout = ctx.LatchTimeout.HasValue
|
||||
? Duration.FromSeconds(ctx.LatchTimeout.Value)
|
||||
: DefaultLatchExpiryTime;
|
||||
|
||||
var timestamp = DiscordUtils.SnowflakeToInstant(ctx.LastMessage.Value);
|
||||
return _clock.GetCurrentInstant() - timestamp > timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using App.Metrics;
|
||||
|
|
@ -55,7 +57,9 @@ namespace PluralKit.Bot
|
|||
|
||||
// Permission check after proxy match so we don't get spammed when not actually proxying
|
||||
if (!await CheckBotPermissionsOrError(message.Channel)) return false;
|
||||
if (!CheckProxyNameBoundsOrError(match.Member.ProxyName(ctx))) return false;
|
||||
|
||||
// this method throws, so no need to wrap it in an if statement
|
||||
CheckProxyNameBoundsOrError(match.Member.ProxyName(ctx));
|
||||
|
||||
// Check if the sender account can mention everyone/here + embed links
|
||||
// we need to "mirror" these permissions when proxying to prevent exploits
|
||||
|
|
@ -74,7 +78,7 @@ namespace PluralKit.Bot
|
|||
if (ctx.SystemId == null) return false;
|
||||
|
||||
// Make sure channel is a guild text channel and this is a normal message
|
||||
if (msg.Channel.Type != ChannelType.Text || msg.MessageType != MessageType.Default) return false;
|
||||
if ((msg.Channel.Type != ChannelType.Text && msg.Channel.Type != ChannelType.News) || msg.MessageType != MessageType.Default) return false;
|
||||
|
||||
// Make sure author is a normal user
|
||||
if (msg.Author.IsSystem == true || msg.Author.IsBot || msg.WebhookMessage) return false;
|
||||
|
|
@ -93,16 +97,88 @@ namespace PluralKit.Bot
|
|||
private async Task ExecuteProxy(DiscordClient shard, IPKConnection conn, DiscordMessage trigger, MessageContext ctx,
|
||||
ProxyMatch match, bool allowEveryone, bool allowEmbeds)
|
||||
{
|
||||
// Create reply embed
|
||||
var embeds = new List<DiscordEmbed>();
|
||||
if (trigger.Reference?.Channel?.Id == trigger.ChannelId)
|
||||
{
|
||||
var repliedTo = await FetchReplyOriginalMessage(trigger.Reference);
|
||||
if (repliedTo != null)
|
||||
{
|
||||
var embed = CreateReplyEmbed(repliedTo);
|
||||
if (embed != null)
|
||||
embeds.Add(embed);
|
||||
}
|
||||
|
||||
// TODO: have a clean error for when message can't be fetched instead of just being silent
|
||||
}
|
||||
|
||||
// Send the webhook
|
||||
var content = match.ProxyContent;
|
||||
if (!allowEmbeds) content = content.BreakLinkEmbeds();
|
||||
var proxyMessage = await _webhookExecutor.ExecuteWebhook(trigger.Channel, match.Member.ProxyName(ctx),
|
||||
var proxyMessage = await _webhookExecutor.ExecuteWebhook(trigger.Channel, FixSingleCharacterName(match.Member.ProxyName(ctx)),
|
||||
match.Member.ProxyAvatar(ctx),
|
||||
content, trigger.Attachments, allowEveryone);
|
||||
content, trigger.Attachments, embeds, allowEveryone);
|
||||
|
||||
await HandleProxyExecutedActions(shard, conn, ctx, trigger, proxyMessage, match);
|
||||
}
|
||||
|
||||
private async Task<DiscordMessage> FetchReplyOriginalMessage(DiscordMessageReference reference)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await reference.Channel.GetMessageAsync(reference.Message.Id);
|
||||
}
|
||||
catch (NotFoundException)
|
||||
{
|
||||
_logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but it was not found",
|
||||
reference.Channel.Id, reference.Message.Id);
|
||||
}
|
||||
catch (UnauthorizedException)
|
||||
{
|
||||
_logger.Warning("Attempted to fetch reply message {ChannelId}/{MessageId} but bot was not allowed to",
|
||||
reference.Channel.Id, reference.Message.Id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DiscordEmbed CreateReplyEmbed(DiscordMessage original)
|
||||
{
|
||||
var content = new StringBuilder();
|
||||
|
||||
var hasContent = !string.IsNullOrWhiteSpace(original.Content);
|
||||
if (hasContent)
|
||||
{
|
||||
var msg = original.Content;
|
||||
if (msg.Length > 100)
|
||||
{
|
||||
msg = original.Content.Substring(0, 100);
|
||||
var spoilersInOriginalString = Regex.Matches(original.Content, @"\|\|").Count;
|
||||
var spoilersInTruncatedString = Regex.Matches(msg, @"\|\|").Count;
|
||||
if (spoilersInTruncatedString % 2 == 1 && spoilersInOriginalString % 2 == 0)
|
||||
msg += "||";
|
||||
msg += "…";
|
||||
}
|
||||
|
||||
content.Append($"**[Reply to:]({original.JumpLink})** ");
|
||||
content.Append(msg);
|
||||
if (original.Attachments.Count > 0)
|
||||
content.Append($" {Emojis.Paperclip}");
|
||||
}
|
||||
else
|
||||
{
|
||||
content.Append($"*[(click to see attachment)]({original.JumpLink})*");
|
||||
}
|
||||
|
||||
var username = (original.Author as DiscordMember)?.Nickname ?? original.Author.Username;
|
||||
|
||||
return new DiscordEmbedBuilder()
|
||||
// unicodes: [three-per-em space] [left arrow emoji] [force emoji presentation]
|
||||
.WithAuthor($"{username}\u2004\u21a9\ufe0f", iconUrl: original.Author.GetAvatarUrl(ImageFormat.Png, 1024))
|
||||
.WithDescription(content.ToString())
|
||||
.Build();
|
||||
}
|
||||
|
||||
private async Task HandleProxyExecutedActions(DiscordClient shard, IPKConnection conn, MessageContext ctx,
|
||||
DiscordMessage triggerMessage, DiscordMessage proxyMessage,
|
||||
ProxyMatch match)
|
||||
|
|
@ -185,13 +261,15 @@ namespace PluralKit.Bot
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool CheckProxyNameBoundsOrError(string proxyName)
|
||||
private string FixSingleCharacterName(string proxyName)
|
||||
{
|
||||
if (proxyName.Length < 2) throw Errors.ProxyNameTooShort(proxyName);
|
||||
if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName);
|
||||
if (proxyName.Length == 1) return proxyName += "\u17b5";
|
||||
else return proxyName;
|
||||
}
|
||||
|
||||
// TODO: this never returns false as it throws instead, should this happen?
|
||||
return true;
|
||||
private void CheckProxyNameBoundsOrError(string proxyName)
|
||||
{
|
||||
if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -157,6 +157,42 @@ namespace PluralKit.Bot {
|
|||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task<DiscordEmbed> CreateGroupEmbed(Context ctx, PKSystem system, PKGroup target)
|
||||
{
|
||||
await using var conn = await _db.Obtain();
|
||||
|
||||
var pctx = ctx.LookupContextFor(system);
|
||||
var memberCount = ctx.MatchPrivateFlag(pctx) ? await _repo.GetGroupMemberCount(conn, target.Id, PrivacyLevel.Public) : await _repo.GetGroupMemberCount(conn, target.Id);
|
||||
|
||||
var nameField = target.Name;
|
||||
if (system.Name != null)
|
||||
nameField = $"{nameField} ({system.Name})";
|
||||
|
||||
var eb = new DiscordEmbedBuilder()
|
||||
.WithAuthor(nameField, iconUrl: DiscordUtils.WorkaroundForUrlBug(target.IconFor(pctx)))
|
||||
.WithFooter($"System ID: {system.Hid} | Group ID: {target.Hid} | Created on {target.Created.FormatZoned(system)}");
|
||||
|
||||
if (target.DisplayName != null)
|
||||
eb.AddField("Display Name", target.DisplayName);
|
||||
|
||||
if (target.ListPrivacy.CanAccess(pctx))
|
||||
{
|
||||
if (memberCount == 0 && pctx == LookupContext.ByOwner)
|
||||
// Only suggest the add command if this is actually the owner lol
|
||||
eb.AddField("Members (0)", $"Add one with `pk;group {target.Reference()} add <member>`!", true);
|
||||
else
|
||||
eb.AddField($"Members ({memberCount})", $"(see `pk;group {target.Reference()} list`)", true);
|
||||
}
|
||||
|
||||
if (target.DescriptionFor(pctx) is {} desc)
|
||||
eb.AddField("Description", desc);
|
||||
|
||||
if (target.IconFor(pctx) is {} icon)
|
||||
eb.WithThumbnail(icon);
|
||||
|
||||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task<DiscordEmbed> CreateFronterEmbed(PKSwitch sw, DateTimeZone zone, LookupContext ctx)
|
||||
{
|
||||
var members = await _db.Execute(c => _repo.GetSwitchMembers(c, sw.Id).ToListAsync().AsTask());
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace PluralKit.Bot
|
|||
private static readonly Regex _vanessaRegex = new Regex("Message sent by <@!?(\\d{17,19})> deleted in");
|
||||
private static readonly Regex _salRegex = new Regex("\\(ID: (\\d{17,19})\\)");
|
||||
private static readonly Regex _GearBotRegex = new Regex("\\(``(\\d{17,19})``\\) in <#\\d{17,19}> has been removed.");
|
||||
private static readonly Regex _GiselleRegex = new Regex("\\*\\*Message ID\\*\\*: `(\\d{17,19})`");
|
||||
|
||||
private static readonly Dictionary<ulong, LoggerBot> _bots = new[]
|
||||
{
|
||||
|
|
@ -48,7 +49,8 @@ namespace PluralKit.Bot
|
|||
new LoggerBot("UnbelievaBoat", 292953664492929025, ExtractUnbelievaBoat, webhookName: "UnbelievaBoat"),
|
||||
new LoggerBot("Vanessa", 310261055060443136, fuzzyExtractFunc: ExtractVanessa),
|
||||
new LoggerBot("SafetyAtLast", 401549924199694338, fuzzyExtractFunc: ExtractSAL),
|
||||
new LoggerBot("GearBot", 349977940198555660, fuzzyExtractFunc: ExtractGearBot)
|
||||
new LoggerBot("GearBot", 349977940198555660, fuzzyExtractFunc: ExtractGearBot),
|
||||
new LoggerBot("GiselleBot", 356831787445387285, ExtractGiselleBot)
|
||||
}.ToDictionary(b => b.Id);
|
||||
|
||||
private static readonly Dictionary<string, LoggerBot> _botsByWebhookName = _bots.Values
|
||||
|
|
@ -292,6 +294,13 @@ namespace PluralKit.Bot
|
|||
: (FuzzyExtractResult?) null;
|
||||
}
|
||||
|
||||
private static ulong? ExtractGiselleBot(DiscordMessage msg)
|
||||
{
|
||||
var embed = msg.Embeds.FirstOrDefault();
|
||||
if (embed?.Title == null || embed.Title != "🗑 Message Deleted") return null;
|
||||
var match = _GiselleRegex.Match(embed?.Description);
|
||||
return match.Success ? ulong.Parse(match.Groups[1].Value) : (ulong?) null;
|
||||
}
|
||||
|
||||
public class LoggerBot
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ namespace PluralKit.Bot
|
|||
_logger = logger.ForContext<WebhookExecutorService>();
|
||||
}
|
||||
|
||||
public async Task<DiscordMessage> ExecuteWebhook(DiscordChannel channel, string name, string avatarUrl, string content, IReadOnlyList<DiscordAttachment> attachments, bool allowEveryone)
|
||||
public async Task<DiscordMessage> ExecuteWebhook(DiscordChannel channel, string name, string avatarUrl, string content, IReadOnlyList<DiscordAttachment> attachments, IReadOnlyList<DiscordEmbed> embeds, bool allowEveryone)
|
||||
{
|
||||
_logger.Verbose("Invoking webhook in channel {Channel}", channel.Id);
|
||||
|
||||
// Get a webhook, execute it
|
||||
var webhook = await _webhookCache.GetWebhook(channel);
|
||||
var webhookMessage = await ExecuteWebhookInner(channel, webhook, name, avatarUrl, content, attachments, allowEveryone);
|
||||
var webhookMessage = await ExecuteWebhookInner(channel, webhook, name, avatarUrl, content, attachments, embeds, allowEveryone);
|
||||
|
||||
// Log the relevant metrics
|
||||
_metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied);
|
||||
|
|
@ -58,17 +58,19 @@ namespace PluralKit.Bot
|
|||
return webhookMessage;
|
||||
}
|
||||
|
||||
private async Task<DiscordMessage> ExecuteWebhookInner(DiscordChannel channel, DiscordWebhook webhook, string name, string avatarUrl, string content,
|
||||
IReadOnlyList<DiscordAttachment> attachments, bool allowEveryone, bool hasRetried = false)
|
||||
private async Task<DiscordMessage> ExecuteWebhookInner(
|
||||
DiscordChannel channel, DiscordWebhook webhook, string name, string avatarUrl, string content,
|
||||
IReadOnlyList<DiscordAttachment> attachments, IReadOnlyList<DiscordEmbed> embeds, bool allowEveryone, bool hasRetried = false)
|
||||
{
|
||||
content = content.Truncate(2000);
|
||||
|
||||
|
||||
var dwb = new DiscordWebhookBuilder();
|
||||
dwb.WithUsername(FixClyde(name).Truncate(80));
|
||||
dwb.WithContent(content);
|
||||
dwb.AddMentions(content.ParseAllMentions(allowEveryone, channel.Guild));
|
||||
if (!string.IsNullOrWhiteSpace(avatarUrl))
|
||||
dwb.WithAvatarUrl(avatarUrl);
|
||||
dwb.AddEmbeds(embeds);
|
||||
|
||||
var attachmentChunks = ChunkAttachmentsOrThrow(attachments, 8 * 1024 * 1024);
|
||||
if (attachmentChunks.Count > 0)
|
||||
|
|
@ -99,7 +101,7 @@ namespace PluralKit.Bot
|
|||
_logger.Warning("Error invoking webhook {Webhook} in channel {Channel}", webhook.Id, webhook.ChannelId);
|
||||
|
||||
var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(channel, webhook);
|
||||
return await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, allowEveryone, hasRetried: true);
|
||||
return await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, embeds, allowEveryone, hasRetried: true);
|
||||
}
|
||||
|
||||
throw;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ namespace PluralKit.Bot {
|
|||
public static class AvatarUtils {
|
||||
public static async Task VerifyAvatarOrThrow(string url)
|
||||
{
|
||||
if (url.Length > Limits.MaxUriLength)
|
||||
throw Errors.UrlTooLong(url);
|
||||
|
||||
// List of MIME types we consider acceptable
|
||||
var acceptableMimeTypes = new[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -17,6 +18,37 @@ namespace PluralKit.Bot
|
|||
public static string ProxyTagsString(this PKMember member, string separator = ", ") =>
|
||||
string.Join(separator, member.ProxyTags.Select(t => t.ProxyString.AsCode()));
|
||||
|
||||
|
||||
private static String entityTerm<T>(int count, bool isTarget)
|
||||
{
|
||||
var ret = "";
|
||||
ret += isTarget ? "Member" : "Group";
|
||||
if ((
|
||||
(typeof(T) == typeof(GroupId) && !isTarget) ||
|
||||
(typeof(T) == typeof(MemberId) && isTarget)
|
||||
) && count > 1)
|
||||
ret += "s";
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static String GroupAddRemoveResponse<T>(List<T> entityList, List<T> actionedOn, Groups.AddRemoveOperation op)
|
||||
{
|
||||
var opStr = op == Groups.AddRemoveOperation.Add ? "added to" : "removed from";
|
||||
var inStr = op == Groups.AddRemoveOperation.Add ? "in" : "not in";
|
||||
var notActionedOn = entityList.Count - actionedOn.Count;
|
||||
|
||||
var groupNotActionedPosStr = typeof(T) == typeof(GroupId) ? notActionedOn.ToString() + " " : "";
|
||||
var memberNotActionedPosStr = typeof(T) == typeof(MemberId) ? notActionedOn.ToString() + " " : "";
|
||||
|
||||
if (actionedOn.Count == 0)
|
||||
return $"{Emojis.Error} {entityTerm<T>(notActionedOn, true)} not {opStr} {entityTerm<T>(entityList.Count, false).ToLower()} ({entityTerm<T>(notActionedOn, true).ToLower()} already {inStr} {entityTerm<T>(entityList.Count, false).ToLower()}).";
|
||||
else
|
||||
if (notActionedOn == 0)
|
||||
return $"{Emojis.Success} {entityTerm<T>(actionedOn.Count, true)} {opStr} {entityTerm<T>(actionedOn.Count, false).ToLower()}.";
|
||||
else
|
||||
return $"{Emojis.Success} {entityTerm<T>(actionedOn.Count, true)} {opStr} {actionedOn.Count} {entityTerm<T>(actionedOn.Count, false).ToLower()} ({memberNotActionedPosStr}{entityTerm<T>(actionedOn.Count, true).ToLower()} already {inStr} {groupNotActionedPosStr}{entityTerm<T>(notActionedOn, false).ToLower()}).";
|
||||
}
|
||||
|
||||
public static bool IsOurProblem(this Exception e)
|
||||
{
|
||||
// This function filters out sporadic errors out of our control from being reported to Sentry
|
||||
|
|
@ -44,7 +76,7 @@ namespace PluralKit.Bot
|
|||
if (e is WebhookExecutionErrorOnDiscordsEnd) return false;
|
||||
|
||||
// Socket errors are *not our problem*
|
||||
if (e is SocketException) return false;
|
||||
if (e.GetBaseException() is SocketException) return false;
|
||||
|
||||
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
|
||||
if (e is TaskCanceledException) return false;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace PluralKit.Core
|
|||
internal class Database: IDatabase
|
||||
{
|
||||
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
|
||||
private const int TargetSchemaVersion = 11;
|
||||
private const int TargetSchemaVersion = 12;
|
||||
|
||||
private readonly CoreConfig _config;
|
||||
private readonly ILogger _logger;
|
||||
|
|
|
|||
|
|
@ -24,5 +24,7 @@ namespace PluralKit.Core
|
|||
public Instant? LastSwitchTimestamp { get; }
|
||||
public string? SystemTag { get; }
|
||||
public string? SystemAvatar { get; }
|
||||
public bool AllowAutoproxy { get; }
|
||||
public int? LatchTimeout { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ namespace PluralKit.Core
|
|||
public string? ServerAvatar { get; }
|
||||
public string? Avatar { get; }
|
||||
|
||||
public bool AllowAutoproxy { get; }
|
||||
|
||||
public string ProxyName(MessageContext ctx) => ctx.SystemTag != null
|
||||
? $"{ServerName ?? DisplayName ?? Name} {ctx.SystemTag}"
|
||||
: ServerName ?? DisplayName ?? Name;
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@
|
|||
last_switch_members int[],
|
||||
last_switch_timestamp timestamp,
|
||||
system_tag text,
|
||||
system_avatar text
|
||||
system_avatar text,
|
||||
allow_autoproxy bool,
|
||||
latch_timeout integer
|
||||
)
|
||||
as $$
|
||||
-- CTEs to query "static" (accessible only through args) data
|
||||
with
|
||||
system as (select systems.* from accounts inner join systems on systems.id = accounts.system where accounts.uid = account_id),
|
||||
system as (select systems.*, allow_autoproxy as account_autoproxy from accounts inner join systems on systems.id = accounts.system where accounts.uid = account_id),
|
||||
guild as (select * from servers where id = guild_id),
|
||||
last_message as (select * from messages where messages.guild = guild_id and messages.sender = account_id order by mid desc limit 1)
|
||||
select
|
||||
|
|
@ -37,7 +39,9 @@ as $$
|
|||
system_last_switch.members as last_switch_members,
|
||||
system_last_switch.timestamp as last_switch_timestamp,
|
||||
system.tag as system_tag,
|
||||
system.avatar_url as system_avatar
|
||||
system.avatar_url as system_avatar,
|
||||
system.account_autoproxy as allow_autoproxy,
|
||||
system.latch_timeout as latch_timeout
|
||||
-- We need a "from" clause, so we just use some bogus data that's always present
|
||||
-- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data
|
||||
from (select 1) as _placeholder
|
||||
|
|
@ -62,7 +66,9 @@ create function proxy_members(account_id bigint, guild_id bigint)
|
|||
name text,
|
||||
|
||||
server_avatar text,
|
||||
avatar text
|
||||
avatar text,
|
||||
|
||||
allow_autoproxy bool
|
||||
)
|
||||
as $$
|
||||
select
|
||||
|
|
@ -78,7 +84,9 @@ as $$
|
|||
|
||||
-- Avatar info
|
||||
member_guild.avatar_url as server_avatar,
|
||||
members.avatar_url as avatar
|
||||
members.avatar_url as avatar,
|
||||
|
||||
members.allow_autoproxy as allow_autoproxy
|
||||
from accounts
|
||||
inner join systems on systems.id = accounts.system
|
||||
inner join members on members.system = systems.id
|
||||
|
|
|
|||
10
PluralKit.Core/Database/Migrations/12.sql
Normal file
10
PluralKit.Core/Database/Migrations/12.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- SCHEMA VERSION 12: 2020-12-08 --
|
||||
-- Add disabling front/latch autoproxy per-member --
|
||||
-- Add disabling autoproxy per-account --
|
||||
-- Add configurable latch timeout --
|
||||
|
||||
alter table members add column allow_autoproxy bool not null default true;
|
||||
alter table accounts add column allow_autoproxy bool not null default true;
|
||||
alter table systems add column latch_timeout int; -- in seconds
|
||||
|
||||
update info set schema_version = 12;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public partial class ModelRepository
|
||||
{
|
||||
public async Task UpdateAccount(IPKConnection conn, ulong id, AccountPatch patch)
|
||||
{
|
||||
_logger.Information("Updated account {accountId}: {@AccountPatch}", id, patch);
|
||||
var (query, pms) = patch.Apply(UpdateQueryBuilder.Update("accounts", "uid = @uid"))
|
||||
.WithConstant("uid", id)
|
||||
.Build();
|
||||
await conn.ExecuteAsync(query, pms);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -30,11 +30,6 @@ namespace PluralKit.Core
|
|||
return conn.QuerySingleOrDefaultAsync<int>(query.ToString(), new {Id = id, PrivacyFilter = privacyFilter});
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<PKGroup> GetMemberGroups(IPKConnection conn, MemberId id) =>
|
||||
conn.QueryStreamAsync<PKGroup>(
|
||||
"select groups.* from group_members inner join groups on group_members.group_id = groups.id where group_members.member_id = @Id",
|
||||
new {Id = id});
|
||||
|
||||
public async Task<PKGroup> CreateGroup(IPKConnection conn, SystemId system, string name)
|
||||
{
|
||||
var group = await conn.QueryFirstAsync<PKGroup>(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dapper;
|
||||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public partial class ModelRepository
|
||||
{
|
||||
public IAsyncEnumerable<PKGroup> GetMemberGroups(IPKConnection conn, MemberId id) =>
|
||||
conn.QueryStreamAsync<PKGroup>(
|
||||
"select groups.* from group_members inner join groups on group_members.group_id = groups.id where group_members.member_id = @Id",
|
||||
new {Id = id});
|
||||
|
||||
|
||||
public async Task AddGroupsToMember(IPKConnection conn, MemberId member, IReadOnlyCollection<GroupId> groups)
|
||||
{
|
||||
await using var w =
|
||||
conn.BeginBinaryImport("copy group_members (group_id, member_id) from stdin (format binary)");
|
||||
foreach (var group in groups)
|
||||
{
|
||||
await w.StartRowAsync();
|
||||
await w.WriteAsync(group.Value);
|
||||
await w.WriteAsync(member.Value);
|
||||
}
|
||||
|
||||
await w.CompleteAsync();
|
||||
_logger.Information("Added member {MemberId} to groups {GroupIds}", member, groups);
|
||||
}
|
||||
|
||||
public Task RemoveGroupsFromMember(IPKConnection conn, MemberId member, IReadOnlyCollection<GroupId> groups)
|
||||
{
|
||||
_logger.Information("Removed groups from {MemberId}: {GroupIds}", member, groups);
|
||||
return conn.ExecuteAsync("delete from group_members where member_id = @Member and group_id = any(@Groups)",
|
||||
new {Member = @member, Groups = groups.ToArray() });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ namespace PluralKit.Core {
|
|||
public bool KeepProxy { get; private set; }
|
||||
public Instant Created { get; private set; }
|
||||
public int MessageCount { get; private set; }
|
||||
public bool AllowAutoproxy { get; private set; }
|
||||
|
||||
public PrivacyLevel MemberVisibility { get; private set; }
|
||||
public PrivacyLevel DescriptionPrivacy { get; private set; }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace PluralKit.Core {
|
|||
public Instant Created { get; }
|
||||
public string UiTz { get; set; }
|
||||
public bool PingsEnabled { get; }
|
||||
public int? LatchTimeout { get; }
|
||||
public PrivacyLevel DescriptionPrivacy { get; }
|
||||
public PrivacyLevel MemberListPrivacy { get;}
|
||||
public PrivacyLevel FrontPrivacy { get; }
|
||||
|
|
|
|||
10
PluralKit.Core/Models/Patch/AccountPatch.cs
Normal file
10
PluralKit.Core/Models/Patch/AccountPatch.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace PluralKit.Core
|
||||
{
|
||||
public class AccountPatch: PatchObject
|
||||
{
|
||||
public Partial<bool> AllowAutoproxy { get; set; }
|
||||
|
||||
public override UpdateQueryBuilder Apply(UpdateQueryBuilder b) => b
|
||||
.With("allow_autoproxy", AllowAutoproxy);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ namespace PluralKit.Core
|
|||
public Partial<ProxyTag[]> ProxyTags { get; set; }
|
||||
public Partial<bool> KeepProxy { get; set; }
|
||||
public Partial<int> MessageCount { get; set; }
|
||||
public Partial<bool> AllowAutoproxy { get; set; }
|
||||
public Partial<PrivacyLevel> Visibility { get; set; }
|
||||
public Partial<PrivacyLevel> NamePrivacy { get; set; }
|
||||
public Partial<PrivacyLevel> DescriptionPrivacy { get; set; }
|
||||
|
|
@ -35,6 +36,7 @@ namespace PluralKit.Core
|
|||
.With("proxy_tags", ProxyTags)
|
||||
.With("keep_proxy", KeepProxy)
|
||||
.With("message_count", MessageCount)
|
||||
.With("allow_autoproxy", AllowAutoproxy)
|
||||
.With("member_visibility", Visibility)
|
||||
.With("name_privacy", NamePrivacy)
|
||||
.With("description_privacy", DescriptionPrivacy)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ namespace PluralKit.Core
|
|||
public Partial<PrivacyLevel> FrontPrivacy { get; set; }
|
||||
public Partial<PrivacyLevel> FrontHistoryPrivacy { get; set; }
|
||||
public Partial<bool> PingsEnabled { get; set; }
|
||||
public Partial<int?> LatchTimeout { get; set; }
|
||||
|
||||
public override UpdateQueryBuilder Apply(UpdateQueryBuilder b) => b
|
||||
.With("name", Name)
|
||||
|
|
@ -28,6 +29,7 @@ namespace PluralKit.Core
|
|||
.With("group_list_privacy", GroupListPrivacy)
|
||||
.With("front_privacy", FrontPrivacy)
|
||||
.With("front_history_privacy", FrontHistoryPrivacy)
|
||||
.With("pings_enabled", PingsEnabled);
|
||||
.With("pings_enabled", PingsEnabled)
|
||||
.With("latch_timeout", LatchTimeout);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ namespace PluralKit.Core
|
|||
|
||||
[JsonIgnore] public string ProxyString => $"{Prefix ?? ""}text{Suffix ?? ""}";
|
||||
|
||||
public bool IsEmpty => Prefix == null && Suffix == null;
|
||||
[JsonIgnore] public bool IsEmpty => Prefix == null && Suffix == null;
|
||||
|
||||
public bool Equals(ProxyTag other) => Prefix == other.Prefix && Suffix == other.Suffix;
|
||||
|
||||
|
|
@ -31,4 +31,4 @@ namespace PluralKit.Core
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
PluralKit.Core/Modules/ConfigModule.cs
Normal file
28
PluralKit.Core/Modules/ConfigModule.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Autofac;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public class ConfigModule<T>: Module where T: new()
|
||||
{
|
||||
private readonly string _submodule;
|
||||
|
||||
public ConfigModule(string submodule = null)
|
||||
{
|
||||
_submodule = submodule;
|
||||
}
|
||||
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
// We're assuming IConfiguration is already available somehow - it comes from various places (auto-injected in ASP, etc)
|
||||
|
||||
// Register the CoreConfig and where to find it
|
||||
builder.Register(c => c.Resolve<IConfiguration>().GetSection("PluralKit").Get<CoreConfig>() ?? new CoreConfig()).SingleInstance();
|
||||
|
||||
// Register the submodule config (BotConfig, etc) if specified
|
||||
if (_submodule != null)
|
||||
builder.Register(c => c.Resolve<IConfiguration>().GetSection("PluralKit").GetSection(_submodule).Get<T>() ?? new T()).SingleInstance();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
PluralKit.Core/Modules/DataStoreModule.cs
Normal file
19
PluralKit.Core/Modules/DataStoreModule.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public class DataStoreModule: Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterType<DbConnectionCountHolder>().SingleInstance();
|
||||
builder.RegisterType<Database>().As<IDatabase>().SingleInstance();
|
||||
builder.RegisterType<ModelRepository>().AsSelf().SingleInstance();
|
||||
|
||||
builder.Populate(new ServiceCollection().AddMemoryCache());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
using App.Metrics;
|
||||
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
|
|
@ -19,66 +13,6 @@ using Serilog.Sinks.SystemConsole.Themes;
|
|||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public class DataStoreModule: Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterType<DbConnectionCountHolder>().SingleInstance();
|
||||
builder.RegisterType<Database>().As<IDatabase>().SingleInstance();
|
||||
builder.RegisterType<ModelRepository>().AsSelf().SingleInstance();
|
||||
|
||||
builder.Populate(new ServiceCollection().AddMemoryCache());
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigModule<T>: Module where T: new()
|
||||
{
|
||||
private readonly string _submodule;
|
||||
|
||||
public ConfigModule(string submodule = null)
|
||||
{
|
||||
_submodule = submodule;
|
||||
}
|
||||
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
// We're assuming IConfiguration is already available somehow - it comes from various places (auto-injected in ASP, etc)
|
||||
|
||||
// Register the CoreConfig and where to find it
|
||||
builder.Register(c => c.Resolve<IConfiguration>().GetSection("PluralKit").Get<CoreConfig>() ?? new CoreConfig()).SingleInstance();
|
||||
|
||||
// Register the submodule config (BotConfig, etc) if specified
|
||||
if (_submodule != null)
|
||||
builder.Register(c => c.Resolve<IConfiguration>().GetSection("PluralKit").GetSection(_submodule).Get<T>() ?? new T()).SingleInstance();
|
||||
}
|
||||
}
|
||||
|
||||
public class MetricsModule: Module
|
||||
{
|
||||
private readonly string _onlyContext;
|
||||
|
||||
public MetricsModule(string onlyContext = null)
|
||||
{
|
||||
_onlyContext = onlyContext;
|
||||
}
|
||||
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.Register(c => InitMetrics(c.Resolve<CoreConfig>()))
|
||||
.AsSelf().As<IMetrics>().SingleInstance();
|
||||
}
|
||||
|
||||
private IMetricsRoot InitMetrics(CoreConfig config)
|
||||
{
|
||||
var builder = AppMetrics.CreateDefaultBuilder();
|
||||
if (config.InfluxUrl != null && config.InfluxDb != null)
|
||||
builder.Report.ToInfluxDb(config.InfluxUrl, config.InfluxDb);
|
||||
if (_onlyContext != null)
|
||||
builder.Filter.ByIncludingOnlyContext(_onlyContext);
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
|
||||
public class LoggingModule: Module
|
||||
{
|
||||
private readonly string _component;
|
||||
|
|
@ -176,7 +110,7 @@ namespace PluralKit.Core
|
|||
return Log.Logger = logCfg.CreateLogger();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Serilog why is this necessary for such a simple thing >.>
|
||||
public class UTCTimestampFormatProvider: IFormatProvider
|
||||
{
|
||||
32
PluralKit.Core/Modules/MetricsModule.cs
Normal file
32
PluralKit.Core/Modules/MetricsModule.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using App.Metrics;
|
||||
|
||||
using Autofac;
|
||||
|
||||
namespace PluralKit.Core
|
||||
{
|
||||
public class MetricsModule: Module
|
||||
{
|
||||
private readonly string _onlyContext;
|
||||
|
||||
public MetricsModule(string onlyContext = null)
|
||||
{
|
||||
_onlyContext = onlyContext;
|
||||
}
|
||||
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.Register(c => InitMetrics(c.Resolve<CoreConfig>()))
|
||||
.AsSelf().As<IMetrics>().SingleInstance();
|
||||
}
|
||||
|
||||
private IMetricsRoot InitMetrics(CoreConfig config)
|
||||
{
|
||||
var builder = AppMetrics.CreateDefaultBuilder();
|
||||
if (config.InfluxUrl != null && config.InfluxDb != null)
|
||||
builder.Report.ToInfluxDb(config.InfluxUrl, config.InfluxDb);
|
||||
if (_onlyContext != null)
|
||||
builder.Filter.ByIncludingOnlyContext(_onlyContext);
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@
|
|||
public static readonly string ThumbsUp = "\U0001f44d";
|
||||
public static readonly string RedQuestion = "\u2753";
|
||||
public static readonly string Bell = "\U0001F514";
|
||||
public static readonly string Image = "\U0001F5BC";
|
||||
public static readonly string Paperclip = "\U0001F4CE";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
|
@ -26,4 +26,8 @@
|
|||
<ProjectReference Include="..\PluralKit.Core\PluralKit.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="API" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# PluralKit
|
||||
PluralKit is a Discord bot meant for plural communities. It has features like message proxying through webhooks, switch tracking, system and member profiles, and more.
|
||||
|
||||
**Do you just want to add PluralKit to your server? If so, you don't need any of this. Use the bot's invite link: https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot&permissions=536995904**
|
||||
**Do you just want to add PluralKit to your server? If so, you don't need any of this. Use the bot's invite link: https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904**
|
||||
|
||||
PluralKit has a Discord server for support, feedback, and discussion: https://discord.gg/PczBt78
|
||||
|
||||
# Requirements
|
||||
Running the bot requires [.NET Core](https://dotnet.microsoft.com/download) (v3.1) and a PostgreSQL database. It should function on any system where the prerequisites are set up (including Windows).
|
||||
Running the bot requires [.NET 5](https://dotnet.microsoft.com/download) and a PostgreSQL database. It should function on any system where the prerequisites are set up (including Windows).
|
||||
|
||||
Optionally, it can integrate with [Sentry](https://sentry.io/welcome/) for error reporting and [InfluxDB](https://www.influxdata.com/products/influxdb-overview/) for aggregate statistics.
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ $ docker-compose up -d
|
|||
```
|
||||
|
||||
## Manually
|
||||
* Install the .NET Core 3.1 SDK (see https://dotnet.microsoft.com/download)
|
||||
* Install the .NET 5 SDK (see https://dotnet.microsoft.com/download)
|
||||
* Clone this repository: `git clone https://github.com/xSke/PluralKit`
|
||||
* Create and fill in a `pluralkit.conf` file in the same directory as `docker-compose.yml`
|
||||
* Run the bot: `dotnet run --project PluralKit.Bot`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
module.exports = {
|
||||
title: 'PluralKit',
|
||||
theme: 'default-prefers-color-scheme',
|
||||
|
||||
base: "/",
|
||||
head: [
|
||||
|
|
@ -27,11 +28,11 @@ module.exports = {
|
|||
prevLinks: true,
|
||||
nav: [
|
||||
{ text: "Support server", link: "https://discord.gg/PczBt78" },
|
||||
{ text: "Invite bot", link: "https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot&permissions=536995904" }
|
||||
{ text: "Invite bot", link: "https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904" }
|
||||
],
|
||||
sidebar: [
|
||||
"/",
|
||||
["https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot&permissions=536995904", "Add to your server"],
|
||||
["https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904", "Add to your server"],
|
||||
{
|
||||
title: "Documentation",
|
||||
collapsable: false,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ Accompanying it is an [OpenAPI v3.0 definition](https://github.com/xSke/PluralKi
|
|||
PluralKit has a basic HTTP REST API for querying and modifying your system.
|
||||
The root endpoint of the API is `https://api.pluralkit.me/v1/`.
|
||||
|
||||
#### Authorization header token example
|
||||
```
|
||||
Authorization: z865MC7JNhLtZuSq1NXQYVe+FgZJHBfeBCXOPYYRwH4liDCDrsd7zdOuR45mX257
|
||||
```
|
||||
|
||||
Endpoints will always return all fields, using `null` when a value is missing. On `PATCH` endpoints,
|
||||
missing fields from the JSON request will be ignored and preserved as is, but on `POST` endpoints will
|
||||
be set to `null` or cleared.
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ Words in **\<angle brackets>** or **[square brackets]** mean fill-in-the-blank.
|
|||
- `pk;member <name> proxy [tags]` - Changes the proxy tags of a member. use below add/remove commands for members with multiple tag pairs.
|
||||
- `pk;member <name> proxy add [tags]` - Adds a proxy tag pair to a member.
|
||||
- `pk;member <name> proxy remove [tags]` - Removes a proxy tag from a member.
|
||||
- `pk;member <name> autoproxy [on|off]` - Sets whether a member will be autoproxied when autoproxy is set to latch or front mode.
|
||||
- `pk;member <name> keepproxy [on|off]` - Sets whether to include a member's proxy tags in the proxied message.
|
||||
- `pk;member <name> pronouns [pronouns]` - Changes the pronouns of a member.
|
||||
- `pk;member <name> color [color]` - Changes the color of a member.
|
||||
- `pk;member <name> birthdate [birthdate]` - Changes the birthday of a member.
|
||||
- `pk;member <name> delete` - Deletes a member.
|
||||
- `pk;random` - Shows the member card of a randomly selected member in your system.
|
||||
|
||||
## Group commands
|
||||
*Replace `<name>` with a group's name, 5-character ID or display name. For most commands, adding `-clear` will clear/delete the field.*
|
||||
|
|
@ -62,6 +62,7 @@ Words in **\<angle brackets>** or **[square brackets]** mean fill-in-the-blank.
|
|||
- `pk;group new <name>` - Creates a new group.
|
||||
- `pk;group list` - Lists all groups in your system.
|
||||
- `pk;group <group> list` - Lists all members in a group.
|
||||
- `pk;group <group> random` - Shows the info card of a randomly selected member in a group.
|
||||
- `pk;group <group> rename <new name>` - Renames a group.
|
||||
- `pk;group <group> displayname [display name]` - Shows or changes a group's display name.
|
||||
- `pk;group <group> description [description]` - Shows or changes a group's description.
|
||||
|
|
@ -75,12 +76,18 @@ Words in **\<angle brackets>** or **[square brackets]** mean fill-in-the-blank.
|
|||
- `pk;switch [member...]` - Registers a switch with the given members.
|
||||
- `pk;switch move <time>` - Moves the latest switch backwards in time.
|
||||
- `pk;switch delete` - Deletes the latest switch.
|
||||
- `pk;switch delete all` - Deletes every logged switch.
|
||||
- `pk;switch delete all` - Deletes all logged switches.
|
||||
- `pk;switch out` - Registers a 'switch-out' - a switch with no associated members.
|
||||
|
||||
## Autoproxy commands
|
||||
- `pk;autoproxy [off|front|latch|<member>]` - Sets your system's autoproxy mode for the current server.
|
||||
- `pk;autoproxy timeout [<duration>|off|reset]` - Sets the latch timeout duration for your system.
|
||||
- `pk;autoproxy account [on|off]` - Toggles autoproxy globally for the current account.
|
||||
|
||||
## Server owner commands
|
||||
*(all commands here require Manage Server permission)*
|
||||
- `pk;log channel <channel>` - Sets the given channel to log all proxied messages.
|
||||
- `pk;log channel -clear` - Clears the currently set log channel.
|
||||
- `pk;log disable <#channel> [#channel...]` - Disables logging messages posted in the given channel(s) (useful for staff channels and such).
|
||||
- `pk;log enable <#channel> [#channel...]` - Re-enables logging messages posted in the given channel(s).
|
||||
- `pk;logclean <on/off>` - Enables or disables [log cleanup](./staff/compatibility.md#log-cleanup).
|
||||
|
|
@ -88,6 +95,7 @@ Words in **\<angle brackets>** or **[square brackets]** mean fill-in-the-blank.
|
|||
- `pk;blacklist remove <#channel> [#channel...]` - Removes the given channel(s) from the proxy blacklist.
|
||||
|
||||
## Utility
|
||||
- `pk;random [-group]` - Shows the info card of a randomly selected member [or group] in your system.
|
||||
- `pk;message <message id / message link>` - Looks up information about a proxied message by its message ID or link.
|
||||
- `pk;invite` - Sends the bot invite link for PluralKit.
|
||||
- `pk;import` - Imports a data file from PluralKit or Tupperbox.
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ This bot detects messages with certain tags associated with a profile, then repl
|
|||
#### for example...
|
||||

|
||||
|
||||
For more information, see the links to the left, or click [here](https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot&permissions=536995904) to invite the bot to your server!
|
||||
For more information, see the links to the left, or click [here](https://discord.com/oauth2/authorize?client_id=466378653216014359&scope=bot%20applications.commands&permissions=536995904) to invite the bot to your server!
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# Proxy logging
|
||||
If you want to log every proxied message to a separate channel for moderation purposes, you can use the `pk;log` command with the channel name.For example:
|
||||
If you want to log every proxied message to a separate channel for moderation purposes, you can use the `pk;log channel` command with the channel name.For example:
|
||||
|
||||
pk;log #proxy-log
|
||||
pk;log channel #proxy-log
|
||||
|
||||
This requires you to have the *Manage Server* permission on the server. To disable logging, use the `pk;log` command with no channel name.
|
||||
This requires you to have the *Manage Server* permission on the server. To disable logging, use the `pk;log channel` command with the `-clear` flag as the channel name.
|
||||
|
||||
Log messages have the following format:
|
||||
|
||||
|
|
|
|||
|
|
@ -315,17 +315,23 @@ Since the messages will be posted by PluralKit's webhook, there's no way to dele
|
|||
To delete a PluralKit-proxied message, you can react to it with the :x: emoji. Note that this only works if the message has
|
||||
been sent from your own account.
|
||||
|
||||
### Autoproxying
|
||||
## Autoproxy
|
||||
The bot's *autoproxy* feature allows you to have messages be proxied without directly including the proxy tags. Autoproxy can be set up in various ways. There are three autoproxy modes currently implemented:
|
||||
|
||||
To see your system's current autoproxy settings, simply use the command:
|
||||
|
||||
pk;autoproxy
|
||||
|
||||
To disable autoproxying for the current server, use the command:
|
||||
|
||||
pk;autoproxy off
|
||||
|
||||
*(hint: `pk;autoproxy` can be shortened to `pk;ap` in all related commands)*
|
||||
|
||||
::: tip
|
||||
To disable autoproxy for a single message, add a backslash (`\`) to the beginning of your message.
|
||||
:::
|
||||
|
||||
#### Front mode
|
||||
This autoproxy mode will proxy messages as the current *first* fronter of the system. If you register a switch with `Alice` and `Bob`, messages without proxy tags will be autoproxied as `Alice`.
|
||||
To enable front-mode autoproxying for a given server, use the following command:
|
||||
|
|
@ -347,6 +353,47 @@ To enable member-mode autoproxying for a given server, use the following command
|
|||
|
||||
pk;autoproxy <member>
|
||||
|
||||
### Changing the latch timeout duration
|
||||
By default, latch mode times out after 6 hours. It is possible to change this:
|
||||
|
||||
pk;autoproxy timeout <new duration>
|
||||
|
||||
To reset the duration, use the following command:
|
||||
|
||||
pk;autoproxy timeout reset
|
||||
|
||||
To disable timeout (never timeout), use the following command:
|
||||
|
||||
pk;autoproxy timeout disable
|
||||
|
||||
### Disabling front/latch autoproxy on a per-member basis
|
||||
If a system uses front or latch mode autoproxy, but one member prefers to send messages through the account (and not proxy), you can disable the front and latch modes for that specific member.
|
||||
|
||||
pk;member <name> autoproxy off
|
||||
|
||||
To re-enable front / latch modes for that member, use the following command:
|
||||
|
||||
pk;member <name> autoproxy on
|
||||
|
||||
This will *not* disable member mode autoproxy. If you do not wish to autoproxy, please turn off autoproxy instead of setting autoproxy to a specific member.
|
||||
|
||||
### Disabling autoproxy per-account
|
||||
|
||||
It is possible to fully disable autoproxy for a certain account linked to your system. For example, you might want to do this if a specific member's name is shown on the account.
|
||||
|
||||
To disable autoproxy for the current account, use the following command:
|
||||
|
||||
pk;autoproxy account disable
|
||||
|
||||
To re-enable autoproxy for the current account, use the following command:
|
||||
|
||||
pk;autoproxy account enable
|
||||
|
||||
::: tip
|
||||
This subcommand can also be run in DMs.
|
||||
:::
|
||||
|
||||
|
||||
## Managing switches
|
||||
PluralKit allows you to log member switches through the bot.
|
||||
Essentially, this means you can mark one or more members as *the current fronter(s)* for the duration until the next switch.
|
||||
|
|
|
|||
|
|
@ -14,5 +14,8 @@
|
|||
"vuepress": "^1.3.1",
|
||||
"vuepress-plugin-clean-urls": "^1.1.1",
|
||||
"vuepress-plugin-dehydrate": "^1.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"vuepress-theme-default-prefers-color-scheme": "^1.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2480,6 +2480,13 @@ css-parse@~2.0.0:
|
|||
dependencies:
|
||||
css "^2.0.0"
|
||||
|
||||
css-prefers-color-scheme@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4"
|
||||
integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==
|
||||
dependencies:
|
||||
postcss "^7.0.5"
|
||||
|
||||
css-select-base-adapter@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
|
||||
|
|
@ -7478,6 +7485,13 @@ vuepress-plugin-smooth-scroll@^0.0.3:
|
|||
dependencies:
|
||||
smoothscroll-polyfill "^0.4.3"
|
||||
|
||||
vuepress-theme-default-prefers-color-scheme@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vuepress-theme-default-prefers-color-scheme/-/vuepress-theme-default-prefers-color-scheme-1.1.1.tgz#11389abba0f1c15f2dbea724e80b60937bda70f8"
|
||||
integrity sha512-aLWYuFRk5EFcE4bAGzokAoOD92T/daodnZnuZnzF46jOl/ZtYHFV83uwXlbBUerdQE/IAxgtfuYRELXY5sUIKA==
|
||||
dependencies:
|
||||
css-prefers-color-scheme "^3.1.1"
|
||||
|
||||
vuepress@^1.3.1:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/vuepress/-/vuepress-1.5.2.tgz#b79e84bfaade55ba3ddb59c3a937220913f0599b"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue