mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-08 06:47:56 +00:00
Merge remote-tracking branch 'upstream/main' into rust-command-parser
This commit is contained in:
commit
f721b850d4
183 changed files with 5121 additions and 1909 deletions
|
|
@ -33,7 +33,10 @@ public class ApplicationCommandProxiedMessage
|
|||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
var msg = await ctx.Repository.GetFullMessage(messageId);
|
||||
if (msg == null)
|
||||
throw Errors.MessageNotFound(messageId);
|
||||
{
|
||||
await QueryCommandMessage(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
var showContent = true;
|
||||
var channel = await _rest.GetChannelOrNull(msg.Message.Channel);
|
||||
|
|
@ -58,6 +61,20 @@ public class ApplicationCommandProxiedMessage
|
|||
await ctx.Reply(embeds: embeds.ToArray());
|
||||
}
|
||||
|
||||
private async Task QueryCommandMessage(InteractionContext ctx)
|
||||
{
|
||||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
var msg = await ctx.Repository.GetCommandMessage(messageId);
|
||||
if (msg == null)
|
||||
throw Errors.MessageNotFound(messageId);
|
||||
|
||||
var embeds = new List<Embed>();
|
||||
|
||||
embeds.Add(await _embeds.CreateCommandMessageInfoEmbed(msg, true));
|
||||
|
||||
await ctx.Reply(embeds: embeds.ToArray());
|
||||
}
|
||||
|
||||
public async Task DeleteMessage(InteractionContext ctx)
|
||||
{
|
||||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
|
|
|
|||
|
|
@ -32,13 +32,14 @@ public class Bot
|
|||
private readonly DiscordApiClient _rest;
|
||||
private readonly RedisService _redis;
|
||||
private readonly ILifetimeScope _services;
|
||||
private readonly RuntimeConfigService _runtimeConfig;
|
||||
|
||||
private Timer _periodicTask; // Never read, just kept here for GC reasons
|
||||
|
||||
public Bot(ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics,
|
||||
BotConfig config, RedisService redis,
|
||||
ErrorMessageService errorMessageService, CommandMessageService commandMessageService,
|
||||
Cluster cluster, DiscordApiClient rest, IDiscordCache cache)
|
||||
Cluster cluster, DiscordApiClient rest, IDiscordCache cache, RuntimeConfigService runtimeConfig)
|
||||
{
|
||||
_logger = logger.ForContext<Bot>();
|
||||
_services = services;
|
||||
|
|
@ -51,6 +52,7 @@ public class Bot
|
|||
_rest = rest;
|
||||
_redis = redis;
|
||||
_cache = cache;
|
||||
_runtimeConfig = runtimeConfig;
|
||||
}
|
||||
|
||||
private string BotStatus => $"{(_config.Prefixes ?? BotConfig.DefaultPrefixes)[0]}help"
|
||||
|
|
@ -97,13 +99,15 @@ public class Bot
|
|||
|
||||
private async Task OnEventReceived(int shardId, IGatewayEvent evt)
|
||||
{
|
||||
if (_runtimeConfig.Exists("disable_events")) return;
|
||||
|
||||
// we HandleGatewayEvent **before** getting the own user, because the own user is set in HandleGatewayEvent for ReadyEvent
|
||||
await _cache.HandleGatewayEvent(evt);
|
||||
await _cache.TryUpdateSelfMember(_config.ClientId, evt);
|
||||
await OnEventReceivedInner(shardId, evt);
|
||||
}
|
||||
|
||||
private async Task OnEventReceivedInner(int shardId, IGatewayEvent evt)
|
||||
public async Task OnEventReceivedInner(int shardId, IGatewayEvent evt)
|
||||
{
|
||||
// HandleEvent takes a type parameter, automatically inferred by the event type
|
||||
// It will then look up an IEventHandler<TypeOfEvent> in the DI container and call that object's handler method
|
||||
|
|
@ -278,7 +282,7 @@ public class Bot
|
|||
_logger.Debug("Running once-per-minute scheduled tasks");
|
||||
|
||||
// Check from a new custom status from Redis and update Discord accordingly
|
||||
if (true)
|
||||
if (!_config.DisableGateway)
|
||||
{
|
||||
var newStatus = await _redis.Connection.GetDatabase().StringGetAsync("pluralkit:botstatus");
|
||||
if (newStatus != CustomStatusMessage)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ public class BotConfig
|
|||
public string? HttpCacheUrl { get; set; }
|
||||
public bool HttpUseInnerCache { get; set; } = false;
|
||||
|
||||
public string? HttpListenerAddr { get; set; }
|
||||
public bool DisableGateway { get; set; } = false;
|
||||
public string? EventAwaiterTarget { get; set; }
|
||||
|
||||
public string? DiscordBaseUrl { get; set; }
|
||||
public string? AvatarServiceUrl { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ public partial class CommandTree
|
|||
ctx.Reply(
|
||||
$"{Emojis.Error} Parsed command {ctx.Parameters.Callback().AsCode()} not implemented in PluralKit.Bot!"),
|
||||
};
|
||||
if (ctx.Match("system", "s"))
|
||||
if (ctx.Match("system", "s", "account", "acc"))
|
||||
return HandleSystemCommand(ctx);
|
||||
if (ctx.Match("member", "m"))
|
||||
return HandleMemberCommand(ctx);
|
||||
|
|
@ -554,6 +554,8 @@ public partial class CommandTree
|
|||
case "system":
|
||||
case "systems":
|
||||
case "s":
|
||||
case "account":
|
||||
case "acc":
|
||||
await PrintCommandList(ctx, "systems", SystemCommands);
|
||||
break;
|
||||
case "member":
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ public class Context
|
|||
internal readonly ModelRepository Repository;
|
||||
internal readonly RedisService Redis;
|
||||
|
||||
public async Task<Message> Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null)
|
||||
public async Task<Message> Reply(string text = null, Embed embed = null, AllowedMentions? mentions = null, MultipartFile[]? files = null)
|
||||
{
|
||||
var botPerms = await BotPermissions;
|
||||
|
||||
|
|
@ -91,20 +91,28 @@ public class Context
|
|||
if (embed != null && !botPerms.HasFlag(PermissionSet.EmbedLinks))
|
||||
throw new PKError("PluralKit does not have permission to send embeds in this channel. Please ensure I have the **Embed Links** permission enabled.");
|
||||
|
||||
if (files != null && !botPerms.HasFlag(PermissionSet.AttachFiles))
|
||||
throw new PKError("PluralKit does not have permission to attach files in this channel. Please ensure I have the **Attach Files** permission enabled.");
|
||||
|
||||
var msg = await Rest.CreateMessage(Channel.Id, new MessageRequest
|
||||
{
|
||||
Content = text,
|
||||
Embeds = embed != null ? new[] { embed } : null,
|
||||
// Default to an empty allowed mentions object instead of null (which means no mentions allowed)
|
||||
AllowedMentions = mentions ?? new AllowedMentions()
|
||||
});
|
||||
}, files: files);
|
||||
|
||||
// if (embed != null)
|
||||
// {
|
||||
// Sensitive information that might want to be deleted by :x: reaction is typically in an embed format (member cards, for example)
|
||||
// but since we can, we just store all sent messages for possible deletion
|
||||
await _commandMessageService.RegisterMessage(msg.Id, Guild?.Id ?? 0, msg.ChannelId, Author.Id);
|
||||
// }
|
||||
// store log of sent message, so it can be queried or deleted later
|
||||
// skip DMs as DM messages can always be deleted
|
||||
if (Guild != null)
|
||||
await Repository.AddCommandMessage(new Core.CommandMessage
|
||||
{
|
||||
Mid = msg.Id,
|
||||
Guild = Guild!.Id,
|
||||
Channel = Channel.Id,
|
||||
Sender = Author.Id,
|
||||
OriginalMid = Message.Id,
|
||||
});
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -443,10 +443,11 @@ public class Groups
|
|||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group color")
|
||||
.Color(target.Color.ToDiscordColor())
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20"))
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
|
||||
.Description($"This group's color is **#{target.Color}**."
|
||||
+ (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}group {target.Reference(ctx)} color -clear`." : ""))
|
||||
.Build());
|
||||
.Build(),
|
||||
files: [MiscUtils.GenerateColorPreview(target.Color)]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -471,8 +472,9 @@ public class Groups
|
|||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title($"{Emojis.Success} Group color changed.")
|
||||
.Color(color.ToDiscordColor())
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{color}/?text=%20"))
|
||||
.Build());
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
|
||||
.Build(),
|
||||
files: [MiscUtils.GenerateColorPreview(color)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -308,10 +308,11 @@ public class MemberEdit
|
|||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member color")
|
||||
.Color(target.Color.ToDiscordColor())
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20"))
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
|
||||
.Description($"This member's color is **#{target.Color}**."
|
||||
+ (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}member {target.Reference(ctx)} color -clear`." : ""))
|
||||
.Build());
|
||||
.Build(),
|
||||
files: [MiscUtils.GenerateColorPreview(target.Color)]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -336,8 +337,9 @@ public class MemberEdit
|
|||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title($"{Emojis.Success} Member color changed.")
|
||||
.Color(color.ToDiscordColor())
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{color}/?text=%20"))
|
||||
.Build());
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
|
||||
.Build(),
|
||||
files: [MiscUtils.GenerateColorPreview(color)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ public class ProxiedMessage
|
|||
throw new PKError(error);
|
||||
}
|
||||
|
||||
var lastMessage = _lastMessageCache.GetLastMessage(ctx.Message.ChannelId);
|
||||
var lastMessage = await _lastMessageCache.GetLastMessage(ctx.Message.GuildId ?? 0, ctx.Message.ChannelId);
|
||||
|
||||
var isLatestMessage = lastMessage?.Current.Id == ctx.Message.Id
|
||||
? lastMessage?.Previous?.Id == msg.Mid
|
||||
|
|
@ -347,13 +347,8 @@ public class ProxiedMessage
|
|||
var message = await ctx.Repository.GetFullMessage(messageId.Value);
|
||||
if (message == null)
|
||||
{
|
||||
if (isDelete)
|
||||
{
|
||||
await DeleteCommandMessage(ctx, messageId.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
throw Errors.MessageNotFound(messageId.Value);
|
||||
await GetCommandMessage(ctx, messageId.Value, isDelete);
|
||||
return;
|
||||
}
|
||||
|
||||
var showContent = true;
|
||||
|
|
@ -448,20 +443,35 @@ public class ProxiedMessage
|
|||
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config));
|
||||
}
|
||||
|
||||
private async Task DeleteCommandMessage(Context ctx, ulong messageId)
|
||||
private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete)
|
||||
{
|
||||
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (cmessage == null)
|
||||
var msg = await _repo.GetCommandMessage(messageId);
|
||||
if (msg == null)
|
||||
throw Errors.MessageNotFound(messageId);
|
||||
|
||||
if (cmessage!.AuthorId != ctx.Author.Id)
|
||||
throw new PKError("You can only delete command messages queried by this account.");
|
||||
if (isDelete)
|
||||
{
|
||||
if (msg.Sender != ctx.Author.Id)
|
||||
throw new PKError("You can only delete command messages queried by this account.");
|
||||
|
||||
await ctx.Rest.DeleteMessage(cmessage.ChannelId, messageId);
|
||||
await ctx.Rest.DeleteMessage(msg.Channel, messageId);
|
||||
|
||||
if (ctx.Guild != null)
|
||||
await ctx.Rest.DeleteMessage(ctx.Message);
|
||||
else
|
||||
await ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new Emoji { Name = Emojis.Success });
|
||||
if (ctx.Guild != null)
|
||||
await ctx.Rest.DeleteMessage(ctx.Message);
|
||||
else
|
||||
await ctx.Rest.CreateReaction(ctx.Message.ChannelId, ctx.Message.Id, new Emoji { Name = Emojis.Success });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var showContent = true;
|
||||
|
||||
var channel = await _rest.GetChannelOrNull(msg.Channel);
|
||||
if (channel == null)
|
||||
showContent = false;
|
||||
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
||||
showContent = false;
|
||||
|
||||
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
|
||||
}
|
||||
}
|
||||
|
|
@ -37,10 +37,13 @@ public class System
|
|||
.Field(new Embed.Field("Getting Started",
|
||||
"New to PK? Check out our Getting Started guide on setting up members and proxies: https://pluralkit.me/start\n" +
|
||||
$"Otherwise, type `{ctx.DefaultPrefix}system` to view your system and `{ctx.DefaultPrefix}system help` for more information about commands you can use."))
|
||||
.Field(new Embed.Field($"{Emojis.Warn} Notice {Emojis.Warn}", "PluralKit is a bot meant to help you share information about your system. " +
|
||||
.Field(new Embed.Field($"{Emojis.Warn} Notice: Public By Default {Emojis.Warn}", "PluralKit is a bot meant to help you share information about your system. " +
|
||||
"Member descriptions are meant to be the equivalent to a Discord About Me. Because of this, any info you put in PK is **public by default**.\n" +
|
||||
"Note that this does **not** include message content, only member fields. For more information, check out " +
|
||||
"[the privacy section of the user guide](https://pluralkit.me/guide/#privacy). "))
|
||||
.Field(new Embed.Field($"{Emojis.Warn} Notice: Implicit Acceptance of ToS {Emojis.Warn}", "By using the PluralKit bot you implicitly agree to our " +
|
||||
"[Terms of Service](https://pluralkit.me/terms-of-service/). For questions please ask in our [support server](<https://discord.gg/PczBt78>) or " +
|
||||
"email legal@pluralkit.me"))
|
||||
.Field(new Embed.Field("System Recovery", "In the case of your Discord account getting lost or deleted, the PluralKit staff can help you recover your system. " +
|
||||
"In order to do so, we will need your **PluralKit token**. This is the *only* way you can prove ownership so we can help you recover your system. " +
|
||||
$"To get it, run `{ctx.DefaultPrefix}token` and then store it in a safe place.\n\n" +
|
||||
|
|
|
|||
|
|
@ -233,8 +233,9 @@ public class SystemEdit
|
|||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title($"{Emojis.Success} System color changed.")
|
||||
.Color(newColor.ToDiscordColor())
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{newColor}/?text=%20"))
|
||||
.Build());
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
|
||||
.Build(),
|
||||
files: [MiscUtils.GenerateColorPreview(color)]););
|
||||
}
|
||||
|
||||
public async Task ClearColor(Context ctx, PKSystem target, bool flagConfirmYes)
|
||||
|
|
@ -274,10 +275,11 @@ public class SystemEdit
|
|||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System color")
|
||||
.Color(target.Color.ToDiscordColor())
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"https://fakeimg.pl/256x256/{target.Color}/?text=%20"))
|
||||
.Thumbnail(new Embed.EmbedThumbnail($"attachment://color.gif"))
|
||||
.Description(
|
||||
$"This system's color is **#{target.Color}**." + (isOwnSystem ? $" To clear it, type `{ctx.DefaultPrefix}s color -clear`." : ""))
|
||||
.Build());
|
||||
.Build(),
|
||||
files: [MiscUtils.GenerateColorPreview(target.Color)]);
|
||||
}
|
||||
|
||||
public async Task ClearTag(Context ctx, PKSystem target, bool flagConfirmYes)
|
||||
|
|
@ -475,7 +477,7 @@ public class SystemEdit
|
|||
else
|
||||
str +=
|
||||
" Member names will now use the global system tag when proxied in the current server, if there is one set."
|
||||
+ "\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`.";
|
||||
+ $"\n\nTo check or change where your tag appears in your name use the command `{ctx.DefaultPrefix}cfg name format`.";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
public (ulong?, ulong?) ErrorChannelFor(MessageCreateEvent evt, ulong userId) => (evt.GuildId, evt.ChannelId);
|
||||
private bool IsDuplicateMessage(Message msg) =>
|
||||
// We consider a message duplicate if it has the same ID as the previous message that hit the gateway
|
||||
_lastMessageCache.GetLastMessage(msg.ChannelId)?.Current.Id == msg.Id;
|
||||
// use only the local cache here
|
||||
// http gateway sets last message before forwarding the message here, so this will always return true
|
||||
_lastMessageCache._GetLastMessage(msg.ChannelId)?.Current.Id == msg.Id;
|
||||
|
||||
public async Task Handle(int shardId, MessageCreateEvent evt)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
var guild = await _cache.TryGetGuild(channel.GuildId!.Value);
|
||||
if (guild == null)
|
||||
throw new Exception("could not find self guild in MessageEdited event");
|
||||
var lastMessage = _lastMessageCache.GetLastMessage(evt.ChannelId)?.Current;
|
||||
var lastMessage = (await _lastMessageCache.GetLastMessage(evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0, evt.ChannelId))?.Current;
|
||||
|
||||
// Only react to the last message in the channel
|
||||
if (lastMessage?.Id != evt.Id)
|
||||
|
|
@ -107,10 +107,6 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value)
|
||||
: null;
|
||||
|
||||
var messageType = lastMessage.ReferencedMessage != null
|
||||
? Message.MessageType.Reply
|
||||
: Message.MessageType.Default;
|
||||
|
||||
// TODO: is this missing anything?
|
||||
var equivalentEvt = new MessageCreateEvent
|
||||
{
|
||||
|
|
@ -123,7 +119,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
Attachments = evt.Attachments.Value ?? Array.Empty<Message.Attachment>(),
|
||||
MessageReference = messageReference,
|
||||
ReferencedMessage = referencedMessage,
|
||||
Type = messageType,
|
||||
Type = evt.Type,
|
||||
};
|
||||
return equivalentEvt;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
case "\U0001F514": // Bell
|
||||
case "\U0001F6CE": // Bellhop bell
|
||||
case "\U0001F3D3": // Ping pong paddle (lol)
|
||||
case "\U0001FAD1": // Bell pepper
|
||||
case "\u23F0": // Alarm clock
|
||||
case "\u2757": // Exclamation mark
|
||||
{
|
||||
|
|
|
|||
|
|
@ -57,16 +57,6 @@ public class Init
|
|||
|
||||
var cache = services.Resolve<IDiscordCache>();
|
||||
|
||||
if (config.Cluster == null)
|
||||
{
|
||||
// "Connect to the database" (ie. set off database migrations and ensure state)
|
||||
logger.Information("Connecting to database");
|
||||
await services.Resolve<IDatabase>().ApplyMigrations();
|
||||
|
||||
// Clear shard status from Redis
|
||||
await redis.Connection.GetDatabase().KeyDeleteAsync("pluralkit:shardstatus");
|
||||
}
|
||||
|
||||
logger.Information("Initializing bot");
|
||||
var bot = services.Resolve<Bot>();
|
||||
|
||||
|
|
@ -76,10 +66,19 @@ public class Init
|
|||
// Init the bot instance itself, register handlers and such to the client before beginning to connect
|
||||
bot.Init();
|
||||
|
||||
// Start the Discord shards themselves (handlers already set up)
|
||||
logger.Information("Connecting to Discord");
|
||||
await StartCluster(services);
|
||||
// load runtime config from redis
|
||||
await services.Resolve<RuntimeConfigService>().LoadConfig();
|
||||
|
||||
// Start HTTP server
|
||||
if (config.HttpListenerAddr != null)
|
||||
services.Resolve<HttpListenerService>().Start(config.HttpListenerAddr);
|
||||
|
||||
// Start the Discord shards themselves (handlers already set up)
|
||||
if (!config.DisableGateway)
|
||||
{
|
||||
logger.Information("Connecting to Discord");
|
||||
await StartCluster(services);
|
||||
}
|
||||
logger.Information("Connected! All is good (probably).");
|
||||
|
||||
// Lastly, we just... wait. Everything else is handled in the DiscordClient event loop
|
||||
|
|
@ -149,7 +148,7 @@ public class Init
|
|||
var builder = new ContainerBuilder();
|
||||
builder.RegisterInstance(config);
|
||||
builder.RegisterModule(new ConfigModule<BotConfig>("Bot"));
|
||||
builder.RegisterModule(new LoggingModule("bot"));
|
||||
builder.RegisterModule(new LoggingModule("dotnet-bot"));
|
||||
builder.RegisterModule(new MetricsModule());
|
||||
builder.RegisterModule<DataStoreModule>();
|
||||
builder.RegisterModule<BotModule>();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public abstract class BaseInteractive
|
|||
ButtonStyle style = ButtonStyle.Secondary, bool disabled = false)
|
||||
{
|
||||
var dispatch = _ctx.Services.Resolve<InteractionDispatchService>();
|
||||
var customId = dispatch.Register(handler, Timeout);
|
||||
var customId = dispatch.Register(_ctx.ShardId, handler, Timeout);
|
||||
|
||||
var button = new Button
|
||||
{
|
||||
|
|
@ -89,7 +89,7 @@ public abstract class BaseInteractive
|
|||
{
|
||||
var dispatch = ctx.Services.Resolve<InteractionDispatchService>();
|
||||
foreach (var button in _buttons)
|
||||
button.CustomId = dispatch.Register(button.Handler, Timeout);
|
||||
button.CustomId = dispatch.Register(_ctx.ShardId, button.Handler, Timeout);
|
||||
}
|
||||
|
||||
public abstract Task Start();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Autofac;
|
||||
|
||||
using Myriad.Cache;
|
||||
using Myriad.Gateway;
|
||||
using Myriad.Rest.Types;
|
||||
using Myriad.Types;
|
||||
|
|
@ -69,6 +70,9 @@ public class YesNoPrompt: BaseInteractive
|
|||
return true;
|
||||
}
|
||||
|
||||
// no need to reawait message
|
||||
// gateway will already have sent us only matching messages
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +92,17 @@ public class YesNoPrompt: BaseInteractive
|
|||
{
|
||||
try
|
||||
{
|
||||
// check if http gateway and set listener
|
||||
// todo: this one needs to handle options for message
|
||||
if (_ctx.Cache is HttpDiscordCache)
|
||||
await (_ctx.Cache as HttpDiscordCache).AwaitMessage(
|
||||
_ctx.Guild?.Id ?? 0,
|
||||
_ctx.Channel.Id,
|
||||
_ctx.Author.Id,
|
||||
Timeout,
|
||||
options: new[] { "yes", "y", "no", "n" }
|
||||
);
|
||||
|
||||
await queue.WaitFor(MessagePredicate, Timeout, cts.Token);
|
||||
}
|
||||
catch (TimeoutException e)
|
||||
|
|
|
|||
|
|
@ -49,8 +49,15 @@ public class BotModule: Module
|
|||
|
||||
if (botConfig.HttpCacheUrl != null)
|
||||
{
|
||||
var cache = new HttpDiscordCache(c.Resolve<ILogger>(),
|
||||
c.Resolve<HttpClient>(), botConfig.HttpCacheUrl, botConfig.Cluster?.TotalShards ?? 1, botConfig.ClientId, botConfig.HttpUseInnerCache);
|
||||
var cache = new HttpDiscordCache(
|
||||
c.Resolve<ILogger>(),
|
||||
c.Resolve<HttpClient>(),
|
||||
botConfig.HttpCacheUrl,
|
||||
botConfig.EventAwaiterTarget,
|
||||
botConfig.Cluster?.TotalShards ?? 1,
|
||||
botConfig.ClientId,
|
||||
botConfig.HttpUseInnerCache
|
||||
);
|
||||
|
||||
var metrics = c.Resolve<IMetrics>();
|
||||
|
||||
|
|
@ -153,6 +160,8 @@ public class BotModule: Module
|
|||
builder.RegisterType<CommandMessageService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<InteractionDispatchService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<AvatarHostingService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<HttpListenerService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<RuntimeConfigService>().AsSelf().SingleInstance();
|
||||
|
||||
// Sentry stuff
|
||||
builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope();
|
||||
|
|
|
|||
|
|
@ -25,5 +25,6 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="Sentry" Version="4.13.0" />
|
||||
<PackageReference Include="Watson.Lite" Version="6.3.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ public class ProxyService
|
|||
ChannelId = rootChannel.Id,
|
||||
ThreadId = threadId,
|
||||
MessageId = trigger.Id,
|
||||
Name = await FixSameName(messageChannel.Id, ctx, match.Member),
|
||||
Name = await FixSameName(trigger.GuildId!.Value, messageChannel.Id, ctx, match.Member),
|
||||
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
|
||||
Content = content,
|
||||
Attachments = trigger.Attachments,
|
||||
|
|
@ -458,11 +458,11 @@ public class ProxyService
|
|||
};
|
||||
}
|
||||
|
||||
private async Task<string> FixSameName(ulong channelId, MessageContext ctx, ProxyMember member)
|
||||
private async Task<string> FixSameName(ulong guildId, ulong channelId, MessageContext ctx, ProxyMember member)
|
||||
{
|
||||
var proxyName = member.ProxyName(ctx);
|
||||
|
||||
var lastMessage = _lastMessage.GetLastMessage(channelId)?.Previous;
|
||||
var lastMessage = (await _lastMessage.GetLastMessage(guildId, channelId))?.Previous;
|
||||
if (lastMessage == null)
|
||||
// cache is out of date or channel is empty.
|
||||
return proxyName;
|
||||
|
|
|
|||
|
|
@ -9,35 +9,35 @@ namespace PluralKit.Bot;
|
|||
public class CommandMessageService
|
||||
{
|
||||
private readonly RedisService _redis;
|
||||
private readonly ModelRepository _repo;
|
||||
private readonly ILogger _logger;
|
||||
private static readonly TimeSpan CommandMessageRetention = TimeSpan.FromHours(24);
|
||||
|
||||
public CommandMessageService(RedisService redis, IClock clock, ILogger logger)
|
||||
public CommandMessageService(RedisService redis, ModelRepository repo, IClock clock, ILogger logger)
|
||||
{
|
||||
_redis = redis;
|
||||
_repo = repo;
|
||||
_logger = logger.ForContext<CommandMessageService>();
|
||||
}
|
||||
|
||||
public async Task RegisterMessage(ulong messageId, ulong guildId, ulong channelId, ulong authorId)
|
||||
{
|
||||
if (_redis.Connection == null) return;
|
||||
|
||||
_logger.Debug(
|
||||
"Registering command response {MessageId} from author {AuthorId} in {ChannelId}",
|
||||
messageId, authorId, channelId
|
||||
);
|
||||
|
||||
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}-{guildId}", expiry: CommandMessageRetention);
|
||||
}
|
||||
|
||||
public async Task<CommandMessage?> GetCommandMessage(ulong messageId)
|
||||
{
|
||||
var repoMsg = await _repo.GetCommandMessage(messageId);
|
||||
if (repoMsg != null)
|
||||
return new CommandMessage(repoMsg.Sender, repoMsg.Channel, repoMsg.Guild);
|
||||
|
||||
var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString());
|
||||
if (str.HasValue)
|
||||
{
|
||||
var split = ((string)str).Split("-");
|
||||
return new CommandMessage(ulong.Parse(split[0]), ulong.Parse(split[1]), ulong.Parse(split[2]));
|
||||
}
|
||||
str = await _redis.Connection.GetDatabase().StringGetAsync("command_message:" + messageId.ToString());
|
||||
if (str.HasValue)
|
||||
{
|
||||
var split = ((string)str).Split("-");
|
||||
return new CommandMessage(ulong.Parse(split[0]), ulong.Parse(split[1]), ulong.Parse(split[2]));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -420,6 +420,24 @@ public class EmbedService
|
|||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateCommandMessageInfoEmbed(Core.CommandMessage msg, bool showContent)
|
||||
{
|
||||
var content = "*(command message deleted or inaccessible)*";
|
||||
if (showContent)
|
||||
{
|
||||
var discordMessage = await _rest.GetMessageOrNull(msg.Channel, msg.OriginalMid);
|
||||
if (discordMessage != null)
|
||||
content = discordMessage.Content;
|
||||
}
|
||||
|
||||
return new EmbedBuilder()
|
||||
.Title("Command response message")
|
||||
.Description(content)
|
||||
.Field(new("Original message", $"https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}", true))
|
||||
.Field(new("Sent by", $"<@{msg.Sender}>", true))
|
||||
.Build();
|
||||
}
|
||||
|
||||
public Task<Embed> CreateFrontPercentEmbed(FrontBreakdown breakdown, PKSystem system, PKGroup group,
|
||||
DateTimeZone tz, LookupContext ctx, string embedTitle,
|
||||
bool ignoreNoFronters, bool showFlat)
|
||||
|
|
|
|||
146
PluralKit.Bot/Services/HttpListenerService.cs
Normal file
146
PluralKit.Bot/Services/HttpListenerService.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using WatsonWebserver.Lite;
|
||||
using WatsonWebserver.Core;
|
||||
|
||||
using Myriad.Gateway;
|
||||
using Myriad.Serialization;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public class HttpListenerService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly RuntimeConfigService _runtimeConfig;
|
||||
private readonly Bot _bot;
|
||||
|
||||
public HttpListenerService(ILogger logger, RuntimeConfigService runtimeConfig, Bot bot)
|
||||
{
|
||||
_logger = logger.ForContext<HttpListenerService>();
|
||||
_runtimeConfig = runtimeConfig;
|
||||
_bot = bot;
|
||||
}
|
||||
|
||||
public void Start(string host)
|
||||
{
|
||||
var hosts = new[] { host };
|
||||
if (host == "allv4v6")
|
||||
{
|
||||
hosts = new[] { "[::]", "0.0.0.0" };
|
||||
}
|
||||
foreach (var h in hosts)
|
||||
{
|
||||
var server = new WebserverLite(new WebserverSettings(h, 5002), DefaultRoute);
|
||||
|
||||
server.Routes.PreAuthentication.Static.Add(WatsonWebserver.Core.HttpMethod.GET, "/runtime_config", RuntimeConfigGet);
|
||||
server.Routes.PreAuthentication.Parameter.Add(WatsonWebserver.Core.HttpMethod.POST, "/runtime_config/{key}", RuntimeConfigSet);
|
||||
server.Routes.PreAuthentication.Parameter.Add(WatsonWebserver.Core.HttpMethod.DELETE, "/runtime_config/{key}", RuntimeConfigDelete);
|
||||
|
||||
server.Routes.PreAuthentication.Parameter.Add(WatsonWebserver.Core.HttpMethod.POST, "/events/{shard_id}", GatewayEvent);
|
||||
|
||||
server.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DefaultRoute(HttpContextBase ctx)
|
||||
=> await ctx.Response.Send("hellorld");
|
||||
|
||||
private async Task RuntimeConfigGet(HttpContextBase ctx)
|
||||
{
|
||||
var config = _runtimeConfig.GetAll();
|
||||
ctx.Response.Headers.Add("content-type", "application/json");
|
||||
await ctx.Response.Send(JsonSerializer.Serialize(config));
|
||||
}
|
||||
|
||||
private async Task RuntimeConfigSet(HttpContextBase ctx)
|
||||
{
|
||||
var key = ctx.Request.Url.Parameters["key"];
|
||||
var value = ReadStream(ctx.Request.Data, ctx.Request.ContentLength);
|
||||
await _runtimeConfig.Set(key, value);
|
||||
await RuntimeConfigGet(ctx);
|
||||
}
|
||||
|
||||
private async Task RuntimeConfigDelete(HttpContextBase ctx)
|
||||
{
|
||||
var key = ctx.Request.Url.Parameters["key"];
|
||||
await _runtimeConfig.Delete(key);
|
||||
await RuntimeConfigGet(ctx);
|
||||
}
|
||||
|
||||
private JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions().ConfigureForMyriad();
|
||||
|
||||
private async Task GatewayEvent(HttpContextBase ctx)
|
||||
{
|
||||
var shardIdString = ctx.Request.Url.Parameters["shard_id"];
|
||||
if (!int.TryParse(shardIdString, out var shardId)) return;
|
||||
|
||||
var packet = JsonSerializer.Deserialize<GatewayPacket>(ReadStream(ctx.Request.Data, ctx.Request.ContentLength), _jsonSerializerOptions);
|
||||
var evt = DeserializeEvent(shardId, packet.EventType!, (JsonElement)packet.Payload!);
|
||||
if (evt != null)
|
||||
{
|
||||
await _bot.OnEventReceivedInner(shardId, evt);
|
||||
}
|
||||
await ctx.Response.Send("a");
|
||||
}
|
||||
|
||||
private IGatewayEvent? DeserializeEvent(int shardId, string eventType, JsonElement payload)
|
||||
{
|
||||
if (!IGatewayEvent.EventTypes.TryGetValue(eventType, out var clrType))
|
||||
{
|
||||
_logger.Debug("Shard {ShardId}: Received unknown event type {EventType}", shardId, eventType);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Verbose("Shard {ShardId}: Deserializing {EventType} to {ClrType}", shardId, eventType,
|
||||
clrType);
|
||||
return JsonSerializer.Deserialize(payload.GetRawText(), clrType, _jsonSerializerOptions)
|
||||
as IGatewayEvent;
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
_logger.Error(e, "Shard {ShardId}: Error deserializing event {EventType} to {ClrType}", shardId,
|
||||
eventType, clrType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//temporary re-implementation of the ReadStream function found in WatsonWebserver.Lite, but with handling for closed connections
|
||||
//https://github.com/dotnet/WatsonWebserver/issues/171
|
||||
private static string ReadStream(Stream input, long contentLength)
|
||||
{
|
||||
if (input == null) throw new ArgumentNullException(nameof(input));
|
||||
if (!input.CanRead) throw new InvalidOperationException("Input stream is not readable");
|
||||
if (contentLength < 1) return "";
|
||||
|
||||
byte[] buffer = new byte[65536];
|
||||
long bytesRemaining = contentLength;
|
||||
|
||||
using (MemoryStream ms = new MemoryStream())
|
||||
{
|
||||
int read;
|
||||
|
||||
while (bytesRemaining > 0)
|
||||
{
|
||||
read = input.Read(buffer, 0, buffer.Length);
|
||||
if (read > 0)
|
||||
{
|
||||
ms.Write(buffer, 0, read);
|
||||
bytesRemaining -= read;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IOException("Connection closed before reading end of stream.");
|
||||
}
|
||||
}
|
||||
|
||||
if (ms.Length < 1) return null;
|
||||
var str = Encoding.Default.GetString(ms.ToArray());
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
using Myriad.Cache;
|
||||
|
||||
using NodaTime;
|
||||
|
||||
using Serilog;
|
||||
|
|
@ -16,9 +18,12 @@ public class InteractionDispatchService: IDisposable
|
|||
private readonly ConcurrentDictionary<Guid, RegisteredInteraction> _handlers = new();
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public InteractionDispatchService(IClock clock, ILogger logger)
|
||||
private readonly IDiscordCache _cache;
|
||||
|
||||
public InteractionDispatchService(IClock clock, ILogger logger, IDiscordCache cache)
|
||||
{
|
||||
_clock = clock;
|
||||
_cache = cache;
|
||||
_logger = logger.ForContext<InteractionDispatchService>();
|
||||
|
||||
_cleanupWorker = CleanupLoop(_cts.Token);
|
||||
|
|
@ -50,9 +55,15 @@ public class InteractionDispatchService: IDisposable
|
|||
_handlers.TryRemove(customIdGuid, out _);
|
||||
}
|
||||
|
||||
public string Register(Func<InteractionContext, Task> callback, Duration? expiry = null)
|
||||
public string Register(int shardId, Func<InteractionContext, Task> callback, Duration? expiry = null)
|
||||
{
|
||||
var key = Guid.NewGuid();
|
||||
|
||||
// if http_cache, return RegisterRemote
|
||||
// not awaited here, it's probably fine
|
||||
if (_cache is HttpDiscordCache)
|
||||
(_cache as HttpDiscordCache).AwaitInteraction(shardId, key.ToString(), expiry);
|
||||
|
||||
var handler = new RegisteredInteraction
|
||||
{
|
||||
Callback = callback,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Myriad.Cache;
|
||||
using Myriad.Types;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
|
@ -9,9 +10,18 @@ public class LastMessageCacheService
|
|||
{
|
||||
private readonly IDictionary<ulong, CacheEntry> _cache = new ConcurrentDictionary<ulong, CacheEntry>();
|
||||
|
||||
private readonly IDiscordCache _maybeHttp;
|
||||
|
||||
public LastMessageCacheService(IDiscordCache cache)
|
||||
{
|
||||
_maybeHttp = cache;
|
||||
}
|
||||
|
||||
public void AddMessage(Message msg)
|
||||
{
|
||||
var previous = GetLastMessage(msg.ChannelId);
|
||||
if (_maybeHttp is HttpDiscordCache) return;
|
||||
|
||||
var previous = _GetLastMessage(msg.ChannelId);
|
||||
var current = ToCachedMessage(msg);
|
||||
_cache[msg.ChannelId] = new CacheEntry(current, previous?.Current);
|
||||
}
|
||||
|
|
@ -19,12 +29,26 @@ public class LastMessageCacheService
|
|||
private CachedMessage ToCachedMessage(Message msg) =>
|
||||
new(msg.Id, msg.ReferencedMessage.Value?.Id, msg.Author.Username);
|
||||
|
||||
public CacheEntry? GetLastMessage(ulong channel) =>
|
||||
_cache.TryGetValue(channel, out var message) ? message : null;
|
||||
public async Task<CacheEntry?> GetLastMessage(ulong guild, ulong channel)
|
||||
{
|
||||
if (_maybeHttp is HttpDiscordCache)
|
||||
return await (_maybeHttp as HttpDiscordCache).GetLastMessage<CacheEntry>(guild, channel);
|
||||
|
||||
return _cache.TryGetValue(channel, out var message) ? message : null;
|
||||
}
|
||||
|
||||
public CacheEntry? _GetLastMessage(ulong channel)
|
||||
{
|
||||
if (_maybeHttp is HttpDiscordCache) return null;
|
||||
|
||||
return _cache.TryGetValue(channel, out var message) ? message : null;
|
||||
}
|
||||
|
||||
public void HandleMessageDeletion(ulong channel, ulong message)
|
||||
{
|
||||
var storedMessage = GetLastMessage(channel);
|
||||
if (_maybeHttp is HttpDiscordCache) return;
|
||||
|
||||
var storedMessage = _GetLastMessage(channel);
|
||||
if (storedMessage == null)
|
||||
return;
|
||||
|
||||
|
|
@ -39,7 +63,9 @@ public class LastMessageCacheService
|
|||
|
||||
public void HandleMessageDeletion(ulong channel, List<ulong> messages)
|
||||
{
|
||||
var storedMessage = GetLastMessage(channel);
|
||||
if (_maybeHttp is HttpDiscordCache) return;
|
||||
|
||||
var storedMessage = _GetLastMessage(channel);
|
||||
if (storedMessage == null)
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public class LoggerCleanService
|
|||
private static readonly Regex _AnnabelleRegex = new("```\n(\\d{17,19})\n```");
|
||||
private static readonly Regex _AnnabelleRegexFuzzy = new("\\<t:(\\d+)\\> A message from \\*\\*[\\w.]{2,32}\\*\\* \\(`(\\d{17,19})`\\) was deleted in <#\\d{17,19}>");
|
||||
private static readonly Regex _koiraRegex = new("ID:\\*\\* (\\d{17,19})");
|
||||
private static readonly Regex _zeppelinRegex = new("🗑 Message \\(`(\\d{17,19})`\\)");
|
||||
|
||||
private static readonly Regex _VortexRegex =
|
||||
new("`\\[(\\d\\d:\\d\\d:\\d\\d)\\]` .* \\(ID:(\\d{17,19})\\).* <#\\d{17,19}>:");
|
||||
|
|
@ -83,7 +84,8 @@ public class LoggerCleanService
|
|||
new LoggerBot("Dozer", 356535250932858885, ExtractDozer),
|
||||
new LoggerBot("Skyra", 266624760782258186, ExtractSkyra),
|
||||
new LoggerBot("Annabelle", 231241068383961088, ExtractAnnabelle, fuzzyExtractFunc: ExtractAnnabelleFuzzy),
|
||||
new LoggerBot("Koira", 1247013404569239624, ExtractKoira)
|
||||
new LoggerBot("Koira", 1247013404569239624, ExtractKoira),
|
||||
new LoggerBot("Zeppelin", 473868086773153793, ExtractZeppelin) // webhook
|
||||
}.ToDictionary(b => b.Id);
|
||||
|
||||
private static Dictionary<ulong, LoggerBot> _botsByApplicationId
|
||||
|
|
@ -441,6 +443,23 @@ public class LoggerCleanService
|
|||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
private static ulong? ExtractZeppelin(Message msg)
|
||||
{
|
||||
// zeppelin uses a non-embed format by default but can be configured to use a customizable embed
|
||||
// if it's an embed, assume the footer contains the message ID
|
||||
var embed = msg.Embeds?.FirstOrDefault();
|
||||
if (embed == null)
|
||||
{
|
||||
var match = _zeppelinRegex.Match(msg.Content ?? "");
|
||||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var match = _basicRegex.Match(embed.Footer?.Text ?? "");
|
||||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public class LoggerBot
|
||||
{
|
||||
public ulong Id;
|
||||
|
|
|
|||
58
PluralKit.Bot/Services/RuntimeConfigService.cs
Normal file
58
PluralKit.Bot/Services/RuntimeConfigService.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using StackExchange.Redis;
|
||||
|
||||
using PluralKit.Core;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public class RuntimeConfigService
|
||||
{
|
||||
private readonly RedisService _redis;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private Dictionary<string, string> settings = new();
|
||||
|
||||
private string RedisKey;
|
||||
|
||||
public RuntimeConfigService(ILogger logger, RedisService redis, BotConfig config)
|
||||
{
|
||||
_logger = logger.ForContext<RuntimeConfigService>();
|
||||
_redis = redis;
|
||||
|
||||
var clusterId = config.Cluster?.NodeIndex ?? 0;
|
||||
RedisKey = $"remote_config:dotnet_bot:{clusterId}";
|
||||
}
|
||||
|
||||
public async Task LoadConfig()
|
||||
{
|
||||
var redisConfig = await _redis.Connection.GetDatabase().HashGetAllAsync(RedisKey);
|
||||
foreach (var entry in redisConfig)
|
||||
settings.Add(entry.Name, entry.Value);
|
||||
|
||||
var configStr = JsonConvert.SerializeObject(settings);
|
||||
_logger.Information($"starting with runtime config: {configStr}");
|
||||
}
|
||||
|
||||
public async Task Set(string key, string value)
|
||||
{
|
||||
await _redis.Connection.GetDatabase().HashSetAsync(RedisKey, new[] { new HashEntry(key, new RedisValue(value)) });
|
||||
settings.Add(key, value);
|
||||
_logger.Information($"updated runtime config: {key}={value}");
|
||||
}
|
||||
|
||||
public async Task Delete(string key)
|
||||
{
|
||||
await _redis.Connection.GetDatabase().HashDeleteAsync(RedisKey, key);
|
||||
settings.Remove(key);
|
||||
_logger.Information($"updated runtime config: {key} removed");
|
||||
}
|
||||
|
||||
public object? Get(string key) => settings.GetValueOrDefault(key);
|
||||
|
||||
public bool Exists(string key) => settings.ContainsKey(key);
|
||||
|
||||
public Dictionary<string, string> GetAll() => settings;
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Autofac;
|
||||
|
||||
using Myriad.Builders;
|
||||
using Myriad.Cache;
|
||||
using Myriad.Gateway;
|
||||
using Myriad.Rest.Exceptions;
|
||||
using Myriad.Rest.Types.Requests;
|
||||
|
|
@ -40,8 +41,12 @@ public static class ContextUtils
|
|||
}
|
||||
|
||||
public static async Task<MessageReactionAddEvent> AwaitReaction(this Context ctx, Message message,
|
||||
User user = null, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null)
|
||||
User user, Func<MessageReactionAddEvent, bool> predicate = null, Duration? timeout = null)
|
||||
{
|
||||
// check if http gateway and set listener
|
||||
if (ctx.Cache is HttpDiscordCache)
|
||||
await (ctx.Cache as HttpDiscordCache).AwaitReaction(ctx.Guild?.Id ?? 0, message.Id, user!.Id, timeout);
|
||||
|
||||
bool ReactionPredicate(MessageReactionAddEvent evt)
|
||||
{
|
||||
if (message.Id != evt.MessageId) return false; // Ignore reactions for different messages
|
||||
|
|
@ -57,11 +62,17 @@ public static class ContextUtils
|
|||
|
||||
public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply, bool treatAsHid = false)
|
||||
{
|
||||
var timeout = Duration.FromMinutes(1);
|
||||
|
||||
// check if http gateway and set listener
|
||||
if (ctx.Cache is HttpDiscordCache)
|
||||
await (ctx.Cache as HttpDiscordCache).AwaitMessage(ctx.Guild?.Id ?? 0, ctx.Channel.Id, ctx.Author.Id, timeout);
|
||||
|
||||
bool Predicate(MessageCreateEvent e) =>
|
||||
e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id;
|
||||
|
||||
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>()
|
||||
.WaitFor(Predicate, Duration.FromMinutes(1));
|
||||
.WaitFor(Predicate, timeout);
|
||||
|
||||
var content = msg.Content;
|
||||
if (treatAsHid)
|
||||
|
|
@ -96,11 +107,17 @@ public static class ContextUtils
|
|||
|
||||
async Task<int> PromptPageNumber()
|
||||
{
|
||||
var timeout = Duration.FromMinutes(0.5);
|
||||
|
||||
// check if http gateway and set listener
|
||||
if (ctx.Cache is HttpDiscordCache)
|
||||
await (ctx.Cache as HttpDiscordCache).AwaitMessage(ctx.Guild?.Id ?? 0, ctx.Channel.Id, ctx.Author.Id, timeout);
|
||||
|
||||
bool Predicate(MessageCreateEvent e) =>
|
||||
e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id;
|
||||
|
||||
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>()
|
||||
.WaitFor(Predicate, Duration.FromMinutes(0.5));
|
||||
.WaitFor(Predicate, timeout);
|
||||
|
||||
int.TryParse(msg.Content, out var num);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using System.Globalization;
|
||||
using Myriad.Rest.Exceptions;
|
||||
using Myriad.Rest.Types;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
@ -102,4 +103,26 @@ public static class MiscUtils
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static MultipartFile GenerateColorPreview(string color)
|
||||
{
|
||||
//generate a 128x128 solid color gif from bytes
|
||||
//image data is a 1x1 pixel, using the background color to fill the rest of the canvas
|
||||
var imgBytes = new byte[]
|
||||
{
|
||||
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // Header
|
||||
0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x00, // Logical Screen Descriptor
|
||||
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, // Global Color Table
|
||||
0x21, 0xF9, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, // Graphics Control Extension
|
||||
0x2C, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, // Image Descriptor
|
||||
0x02, 0x02, 0x4C, 0x01, 0x00, // Image Data
|
||||
0x3B // Trailer
|
||||
}; //indices 13, 14 and 15 are the R, G, and B values respectively
|
||||
|
||||
imgBytes[13] = byte.Parse(color.Substring(0, 2), NumberStyles.HexNumber);
|
||||
imgBytes[14] = byte.Parse(color.Substring(2, 2), NumberStyles.HexNumber);
|
||||
imgBytes[15] = byte.Parse(color.Substring(4, 2), NumberStyles.HexNumber);
|
||||
|
||||
return new MultipartFile("color.gif", new MemoryStream(imgBytes), null, null, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,16 @@
|
|||
"resolved": "4.13.0",
|
||||
"contentHash": "Wfw3M1WpFcrYaGzPm7QyUTfIOYkVXQ1ry6p4WYjhbLz9fPwV23SGQZTFDpdox67NHM0V0g1aoQ4YKLm4ANtEEg=="
|
||||
},
|
||||
"Watson.Lite": {
|
||||
"type": "Direct",
|
||||
"requested": "[6.3.5, )",
|
||||
"resolved": "6.3.5",
|
||||
"contentHash": "YF8+se3IVenn8YlyNeb4wSJK6QMnVD0QHIOEiZ22wS4K2wkwoSDzWS+ZAjk1MaPeB+XO5gRoENUN//pOc+wI2g==",
|
||||
"dependencies": {
|
||||
"CavemanTcp": "2.0.5",
|
||||
"Watson.Core": "6.3.5"
|
||||
}
|
||||
},
|
||||
"App.Metrics": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -107,6 +117,11 @@
|
|||
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1"
|
||||
}
|
||||
},
|
||||
"CavemanTcp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.0.5",
|
||||
"contentHash": "90wywmGpjrj26HMAkufYZwuZI8sVYB1mRwEdqugSR3kgDnPX+3l0jO86gwtFKsPvsEpsS4Dn/1EbhguzUxMU8Q=="
|
||||
},
|
||||
"Dapper": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.35",
|
||||
|
|
@ -130,6 +145,11 @@
|
|||
"System.Diagnostics.DiagnosticSource": "5.0.0"
|
||||
}
|
||||
},
|
||||
"IpMatcher": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.5",
|
||||
"contentHash": "WXNlWERj+0GN699AnMNsuJ7PfUAbU4xhOHP3nrNXLHqbOaBxybu25luSYywX1133NSlitA4YkSNmJuyPvea4sw=="
|
||||
},
|
||||
"IPNetwork2": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.667",
|
||||
|
|
@ -391,18 +411,18 @@
|
|||
"resolved": "8.5.0",
|
||||
"contentHash": "VYYMZNitZ85UEhwOKkTQI63WEMvzUqwQc74I2mm8h/DBVAMcBBxqYPni4DmuRtbCwngmuONuK2yBJfWNRKzI+A=="
|
||||
},
|
||||
"Serilog": {
|
||||
"RegexMatcher": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.2.0",
|
||||
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
|
||||
"resolved": "1.0.9",
|
||||
"contentHash": "RkQGXIrqHjD5h1mqefhgCbkaSdRYNRG5rrbzyw5zeLWiS0K1wq9xR3cNhQdzYR2MsKZ3GN523yRUsEQIMPxh3Q=="
|
||||
},
|
||||
"Serilog.Extensions.Logging": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.0",
|
||||
"contentHash": "NwSSYqPJeKNzl5AuXVHpGbr6PkZJFlNa14CdIebVjK3k/76kYj/mz5kiTRNVSsSaxM8kAIa1kpy/qyT9E4npRQ==",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging": "9.0.0",
|
||||
"Serilog": "4.2.0"
|
||||
"Microsoft.Extensions.Logging": "8.0.0",
|
||||
"Serilog": "3.1.1"
|
||||
}
|
||||
},
|
||||
"Serilog.Formatting.Compact": {
|
||||
|
|
@ -714,12 +734,36 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"Timestamps": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.0.11",
|
||||
"contentHash": "SnWhXm3FkEStQGgUTfWMh9mKItNW032o/v8eAtFrOGqG0/ejvPPA1LdLZx0N/qqoY0TH3x11+dO00jeVcM8xNQ=="
|
||||
},
|
||||
"UrlMatcher": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "hHBZVzFSfikrx4XsRsnCIwmGLgbNKtntnlqf4z+ygcNA6Y/L/J0x5GiZZWfXdTfpxhy5v7mlt2zrZs/L9SvbOA=="
|
||||
},
|
||||
"Watson.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.3.5",
|
||||
"contentHash": "Y5YxKOCSLe2KDmfwvI/J0qApgmmZR77LwyoufRVfKH7GLdHiE7fY0IfoNxWTG7nNv8knBfgwyOxdehRm+4HaCg==",
|
||||
"dependencies": {
|
||||
"IpMatcher": "1.0.5",
|
||||
"RegexMatcher": "1.0.9",
|
||||
"System.Text.Json": "8.0.5",
|
||||
"Timestamps": "1.0.11",
|
||||
"UrlMatcher": "3.0.1"
|
||||
}
|
||||
},
|
||||
"myriad": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"NodaTime": "[3.2.0, )",
|
||||
"NodaTime.Serialization.JsonNet": "[3.1.0, )",
|
||||
"Polly": "[8.5.0, )",
|
||||
"Polly.Contrib.WaitAndRetry": "[1.1.1, )",
|
||||
"Serilog": "[4.2.0, )",
|
||||
"Serilog": "[4.1.0, )",
|
||||
"StackExchange.Redis": "[2.8.22, )",
|
||||
"System.Linq.Async": "[6.0.1, )"
|
||||
}
|
||||
|
|
@ -747,8 +791,8 @@
|
|||
"NodaTime.Serialization.JsonNet": "[3.1.0, )",
|
||||
"Npgsql": "[9.0.2, )",
|
||||
"Npgsql.NodaTime": "[9.0.2, )",
|
||||
"Serilog": "[4.2.0, )",
|
||||
"Serilog.Extensions.Logging": "[9.0.0, )",
|
||||
"Serilog": "[4.1.0, )",
|
||||
"Serilog.Extensions.Logging": "[8.0.0, )",
|
||||
"Serilog.Formatting.Compact": "[3.0.0, )",
|
||||
"Serilog.NodaTime": "[3.0.0, )",
|
||||
"Serilog.Sinks.Async": "[2.1.0, )",
|
||||
|
|
@ -762,6 +806,9 @@
|
|||
"System.Interactive.Async": "[6.0.1, )",
|
||||
"ipnetwork2": "[3.0.667, )"
|
||||
}
|
||||
},
|
||||
"serilog": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue