mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-08 14:57:54 +00:00
Merge branch 'main' of https://github.com/rladenson/PluralKit into logclean_annabelle
This commit is contained in:
commit
1c7f950dae
265 changed files with 10696 additions and 2964 deletions
79
PluralKit.Bot/Services/AvatarHostingService.cs
Normal file
79
PluralKit.Bot/Services/AvatarHostingService.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using PluralKit.Core;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
public class AvatarHostingService
|
||||
{
|
||||
private readonly BotConfig _config;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public AvatarHostingService(BotConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ParsedImage> TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId, PKSystem? system)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uploaded = await TryUploadAvatar(input.Url, type, userId, system);
|
||||
if (uploaded != null)
|
||||
{
|
||||
// todo: make new image type called Cdn?
|
||||
return new ParsedImage { Url = uploaded, Source = AvatarSource.HostedCdn };
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
catch (TaskCanceledException e)
|
||||
{
|
||||
// don't show an internal error to users
|
||||
if (e.Message.Contains("HttpClient.Timeout"))
|
||||
throw new PKError("Temporary error setting image, please try again later");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> TryUploadAvatar(string? avatarUrl, RehostedImageType type, ulong userId, PKSystem? system)
|
||||
{
|
||||
if (!AvatarUtils.IsDiscordCdnUrl(avatarUrl))
|
||||
return null;
|
||||
|
||||
if (_config.AvatarServiceUrl == null)
|
||||
return null;
|
||||
|
||||
var kind = type switch
|
||||
{
|
||||
RehostedImageType.Avatar => "avatar",
|
||||
RehostedImageType.Banner => "banner",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
||||
};
|
||||
|
||||
var response = await _client.PostAsJsonAsync(_config.AvatarServiceUrl + "/pull",
|
||||
new { url = avatarUrl, kind, uploaded_by = userId, system_id = system?.Uuid.ToString() });
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
|
||||
throw new PKError($"Error uploading image to CDN: {error.Error}");
|
||||
}
|
||||
|
||||
var success = await response.Content.ReadFromJsonAsync<SuccessResponse>();
|
||||
return success.Url;
|
||||
}
|
||||
|
||||
public record ErrorResponse(string Error);
|
||||
|
||||
public record SuccessResponse(string Url, bool New);
|
||||
|
||||
public enum RehostedImageType
|
||||
{
|
||||
Avatar,
|
||||
Banner,
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ public class CommandMessageService
|
|||
_logger = logger.ForContext<CommandMessageService>();
|
||||
}
|
||||
|
||||
public async Task RegisterMessage(ulong messageId, ulong channelId, ulong authorId)
|
||||
public async Task RegisterMessage(ulong messageId, ulong guildId, ulong channelId, ulong authorId)
|
||||
{
|
||||
if (_redis.Connection == null) return;
|
||||
|
||||
|
|
@ -27,17 +27,19 @@ public class CommandMessageService
|
|||
messageId, authorId, channelId
|
||||
);
|
||||
|
||||
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}", expiry: CommandMessageRetention);
|
||||
await _redis.Connection.GetDatabase().StringSetAsync(messageId.ToString(), $"{authorId}-{channelId}-{guildId}", expiry: CommandMessageRetention);
|
||||
}
|
||||
|
||||
public async Task<(ulong?, ulong?)> GetCommandMessage(ulong messageId)
|
||||
public async Task<CommandMessage?> GetCommandMessage(ulong messageId)
|
||||
{
|
||||
var str = await _redis.Connection.GetDatabase().StringGetAsync(messageId.ToString());
|
||||
if (str.HasValue)
|
||||
{
|
||||
var split = ((string)str).Split("-");
|
||||
return (ulong.Parse(split[0]), ulong.Parse(split[1]));
|
||||
return new CommandMessage(ulong.Parse(split[0]), ulong.Parse(split[1]), ulong.Parse(split[2]));
|
||||
}
|
||||
return (null, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record CommandMessage(ulong AuthorId, ulong ChannelId, ulong GuildId);
|
||||
|
|
@ -56,29 +56,18 @@ public class EmbedService
|
|||
|
||||
var memberCount = await _repo.GetSystemMemberCount(system.Id, countctx == LookupContext.ByOwner ? null : PrivacyLevel.Public);
|
||||
|
||||
uint color;
|
||||
try
|
||||
{
|
||||
color = system.Color?.ToDiscordColor() ?? DiscordUtils.Gray;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// There's no API for system colors yet, but defaulting to a blank color in advance can't be a bad idea
|
||||
color = DiscordUtils.Gray;
|
||||
}
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Title(system.NameFor(ctx))
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
$"System ID: {system.Hid} | Created on {system.Created.FormatZoned(cctx.Zone)}"))
|
||||
.Color(color)
|
||||
$"System ID: {system.DisplayHid(cctx.Config)} | Created on {system.Created.FormatZoned(cctx.Zone)}"))
|
||||
.Color(system.Color?.ToDiscordColor())
|
||||
.Url($"https://dash.pluralkit.me/profile/s/{system.Hid}");
|
||||
|
||||
var avatar = system.AvatarFor(ctx);
|
||||
if (avatar != null)
|
||||
eb.Thumbnail(new Embed.EmbedThumbnail(avatar));
|
||||
|
||||
if (system.DescriptionPrivacy.CanAccess(ctx))
|
||||
if (system.BannerPrivacy.CanAccess(ctx))
|
||||
eb.Image(new Embed.EmbedImage(system.BannerImage));
|
||||
|
||||
var latestSwitch = await _repo.GetLatestSwitch(system.Id);
|
||||
|
|
@ -90,7 +79,7 @@ public class EmbedService
|
|||
{
|
||||
var memberStr = string.Join(", ", switchMembers.Select(m => m.NameFor(ctx)));
|
||||
if (memberStr.Length > 200)
|
||||
memberStr = $"[too many to show, see `pk;system {system.Hid} fronters`]";
|
||||
memberStr = $"[too many to show, see `pk;system {system.DisplayHid(cctx.Config)} fronters`]";
|
||||
eb.Field(new Embed.Field("Fronter".ToQuantity(switchMembers.Count, ShowQuantityAs.None), memberStr));
|
||||
}
|
||||
}
|
||||
|
|
@ -137,7 +126,7 @@ public class EmbedService
|
|||
{
|
||||
if (memberCount > 0)
|
||||
eb.Field(new Embed.Field($"Members ({memberCount})",
|
||||
$"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true));
|
||||
$"(see `pk;system {system.DisplayHid(cctx.Config)} list` or `pk;system {system.DisplayHid(cctx.Config)} list full`)", true));
|
||||
else
|
||||
eb.Field(new Embed.Field($"Members ({memberCount})", "Add one with `pk;member new`!", true));
|
||||
}
|
||||
|
|
@ -175,7 +164,7 @@ public class EmbedService
|
|||
return embed.Build();
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, LookupContext ctx, DateTimeZone zone)
|
||||
public async Task<Embed> CreateMemberEmbed(PKSystem system, PKMember member, Guild guild, SystemConfig? ccfg, LookupContext ctx, DateTimeZone zone)
|
||||
{
|
||||
// string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone));
|
||||
|
||||
|
|
@ -188,19 +177,6 @@ public class EmbedService
|
|||
else
|
||||
name = $"{name}";
|
||||
|
||||
uint color;
|
||||
try
|
||||
{
|
||||
color = member.Color?.ToDiscordColor() ?? DiscordUtils.Gray;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Bad API use can cause an invalid color string
|
||||
// this is now fixed in the API, but might still have some remnants in the database
|
||||
// so we just default to a blank color, yolo
|
||||
color = DiscordUtils.Gray;
|
||||
}
|
||||
|
||||
var guildSettings = guild != null ? await _repo.GetMemberGuild(guild.Id, member.Id) : null;
|
||||
var guildDisplayName = guildSettings?.DisplayName;
|
||||
var webhook_avatar = guildSettings?.AvatarUrl ?? member.WebhookAvatarFor(ctx) ?? member.AvatarFor(ctx);
|
||||
|
|
@ -213,12 +189,12 @@ public class EmbedService
|
|||
|
||||
var eb = new EmbedBuilder()
|
||||
.Author(new Embed.EmbedAuthor(name, IconUrl: webhook_avatar.TryGetCleanCdnUrl(), Url: $"https://dash.pluralkit.me/profile/m/{member.Hid}"))
|
||||
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : DiscordUtils.Gray)
|
||||
.Color(color)
|
||||
// .WithColor(member.ColorPrivacy.CanAccess(ctx) ? color : null)
|
||||
.Color(member.Color?.ToDiscordColor())
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
$"System ID: {system.Hid} | Member ID: {member.Hid} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}"));
|
||||
$"System ID: {system.DisplayHid(ccfg)} | Member ID: {member.DisplayHid(ccfg)} {(member.MetadataPrivacy.CanAccess(ctx) ? $"| Created on {member.Created.FormatZoned(zone)}" : "")}"));
|
||||
|
||||
if (member.DescriptionPrivacy.CanAccess(ctx))
|
||||
if (member.BannerPrivacy.CanAccess(ctx))
|
||||
eb.Image(new Embed.EmbedImage(member.BannerImage));
|
||||
|
||||
var description = "";
|
||||
|
|
@ -255,7 +231,7 @@ public class EmbedService
|
|||
// More than 5 groups show in "compact" format without ID
|
||||
var content = groups.Count > 5
|
||||
? string.Join(", ", groups.Select(g => g.DisplayName ?? g.Name))
|
||||
: string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**"));
|
||||
: string.Join("\n", groups.Select(g => $"[`{g.DisplayHid(ccfg, isList: true)}`] **{g.DisplayName ?? g.Name}**"));
|
||||
eb.Field(new Embed.Field($"Groups ({groups.Count})", content.Truncate(1000)));
|
||||
}
|
||||
|
||||
|
|
@ -287,26 +263,15 @@ public class EmbedService
|
|||
else if (system.NameFor(ctx) != null)
|
||||
nameField = $"{nameField} ({system.NameFor(ctx)})";
|
||||
else
|
||||
nameField = $"{nameField} ({system.Name})";
|
||||
|
||||
uint color;
|
||||
try
|
||||
{
|
||||
color = target.Color?.ToDiscordColor() ?? DiscordUtils.Gray;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// There's no API for group colors yet, but defaulting to a blank color regardless
|
||||
color = DiscordUtils.Gray;
|
||||
}
|
||||
nameField = $"{nameField}";
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Author(new Embed.EmbedAuthor(nameField, IconUrl: target.IconFor(pctx), Url: $"https://dash.pluralkit.me/profile/g/{target.Hid}"))
|
||||
.Color(color);
|
||||
.Color(target.Color?.ToDiscordColor());
|
||||
|
||||
eb.Footer(new Embed.EmbedFooter($"System ID: {system.Hid} | Group ID: {target.Hid}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}"));
|
||||
eb.Footer(new Embed.EmbedFooter($"System ID: {system.DisplayHid(ctx.Config)} | Group ID: {target.DisplayHid(ctx.Config)}{(target.MetadataPrivacy.CanAccess(pctx) ? $" | Created on {target.Created.FormatZoned(ctx.Zone)}" : "")}"));
|
||||
|
||||
if (target.DescriptionPrivacy.CanAccess(pctx))
|
||||
if (target.BannerPrivacy.CanAccess(pctx))
|
||||
eb.Image(new Embed.EmbedImage(target.BannerImage));
|
||||
|
||||
if (target.NamePrivacy.CanAccess(pctx) && target.DisplayName != null)
|
||||
|
|
@ -324,7 +289,7 @@ public class EmbedService
|
|||
{
|
||||
var name = pctx == LookupContext.ByOwner
|
||||
? target.Reference(ctx)
|
||||
: target.Hid;
|
||||
: target.DisplayHid(ctx.Config);
|
||||
eb.Field(new Embed.Field($"Members ({memberCount})", $"(see `pk;group {name} list`)"));
|
||||
}
|
||||
}
|
||||
|
|
@ -362,16 +327,16 @@ public class EmbedService
|
|||
}
|
||||
|
||||
return new EmbedBuilder()
|
||||
.Color(members.FirstOrDefault()?.Color?.ToDiscordColor() ?? DiscordUtils.Gray)
|
||||
.Color(members.FirstOrDefault()?.Color?.ToDiscordColor())
|
||||
.Field(new Embed.Field($"Current {"fronter".ToQuantity(members.Count, ShowQuantityAs.None)}", memberStr))
|
||||
.Field(new Embed.Field("Since",
|
||||
$"{sw.Timestamp.FormatZoned(zone)} ({timeSinceSwitch.FormatDuration()} ago)"))
|
||||
.Build();
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent)
|
||||
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null)
|
||||
{
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Channel);
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel);
|
||||
var ctx = LookupContext.ByNonOwner;
|
||||
|
||||
var serverMsg = await _rest.GetMessageOrNull(msg.Message.Channel, msg.Message.Mid);
|
||||
|
|
@ -424,27 +389,29 @@ public class EmbedService
|
|||
.Field(new Embed.Field("System",
|
||||
msg.System == null
|
||||
? "*(deleted or unknown system)*"
|
||||
: msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.Hid}`)" : $"`{msg.System.Hid}`"
|
||||
: msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.DisplayHid(ccfg)}`)" : $"`{msg.System.DisplayHid(ccfg)}`"
|
||||
, true))
|
||||
.Field(new Embed.Field("Member",
|
||||
msg.Member == null
|
||||
? "*(deleted member)*"
|
||||
: $"{msg.Member.NameFor(ctx)} (`{msg.Member.Hid}`)"
|
||||
: $"{msg.Member.NameFor(ctx)} (`{msg.Member.DisplayHid(ccfg)}`)"
|
||||
, true))
|
||||
.Field(new Embed.Field("Sent by", userStr, true))
|
||||
.Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O"));
|
||||
.Timestamp(DiscordUtils.SnowflakeToInstant(msg.Message.Mid).ToDateTimeOffset().ToString("O"))
|
||||
.Footer(new Embed.EmbedFooter($"Original Message ID: {msg.Message.OriginalMid}"));
|
||||
|
||||
var roles = memberInfo?.Roles?.ToList();
|
||||
if (roles != null && roles.Count > 0 && showContent)
|
||||
{
|
||||
var rolesString = string.Join(", ", (await Task.WhenAll(roles
|
||||
.Select(async id =>
|
||||
var guild = await _cache.GetGuild(channel.GuildId!.Value);
|
||||
var rolesString = string.Join(", ", (roles
|
||||
.Select(id =>
|
||||
{
|
||||
var role = await _cache.TryGetRole(id);
|
||||
var role = Array.Find(guild.Roles, r => r.Id == id);
|
||||
if (role != null)
|
||||
return role;
|
||||
return new Role { Name = "*(unknown role)*", Position = 0 };
|
||||
})))
|
||||
}))
|
||||
.OrderByDescending(role => role.Position)
|
||||
.Select(role => role.Name));
|
||||
eb.Field(new Embed.Field($"Account roles ({roles.Count})", rolesString.Truncate(1024)));
|
||||
|
|
@ -460,19 +427,9 @@ public class EmbedService
|
|||
var color = system.Color;
|
||||
if (group != null) color = group.Color;
|
||||
|
||||
uint embedColor;
|
||||
try
|
||||
{
|
||||
embedColor = color?.ToDiscordColor() ?? DiscordUtils.Gray;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
embedColor = DiscordUtils.Gray;
|
||||
}
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Title(embedTitle)
|
||||
.Color(embedColor);
|
||||
.Color(color?.ToDiscordColor());
|
||||
|
||||
var footer =
|
||||
$"Since {breakdown.RangeStart.FormatZoned(tz)} ({(breakdown.RangeEnd - breakdown.RangeStart).FormatDuration()} ago)";
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class LogChannelService
|
|||
if (logChannelId == null)
|
||||
return;
|
||||
|
||||
var triggerChannel = await _cache.GetChannel(proxiedMessage.Channel);
|
||||
var triggerChannel = await _cache.GetChannel(proxiedMessage.Guild!.Value, proxiedMessage.Channel);
|
||||
|
||||
var member = await _repo.GetMember(proxiedMessage.Member!.Value);
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
|
@ -63,7 +63,7 @@ public class LogChannelService
|
|||
return null;
|
||||
|
||||
var guildId = proxiedMessage.Guild ?? trigger.GuildId.Value;
|
||||
var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(guildId, trigger.ChannelId);
|
||||
|
||||
// get log channel info from the database
|
||||
var guild = await _repo.GetGuild(guildId);
|
||||
|
|
@ -109,7 +109,7 @@ public class LogChannelService
|
|||
private async Task<Channel?> FindLogChannel(ulong guildId, ulong channelId)
|
||||
{
|
||||
// TODO: fetch it directly on cache miss?
|
||||
if (await _cache.TryGetChannel(channelId) is Channel channel)
|
||||
if (await _cache.TryGetChannel(guildId, channelId) is Channel channel)
|
||||
return channel;
|
||||
|
||||
if (await _rest.GetChannelOrNull(channelId) is Channel restChannel)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ public class LoggerCleanService
|
|||
private static readonly Regex _basicRegex = new("(\\d{17,19})");
|
||||
private static readonly Regex _dynoRegex = new("Message ID: (\\d{17,19})");
|
||||
private static readonly Regex _carlRegex = new("Message ID: (\\d{17,19})");
|
||||
private static readonly Regex _sapphireRegex = new("\\*\\*Message ID:\\*\\* \\[(\\d{17,19})\\]");
|
||||
private static readonly Regex _makiRegex = new("Message ID: (\\d{17,19})");
|
||||
private static readonly Regex _circleRegex = new("\\(`(\\d{17,19})`\\)");
|
||||
private static readonly Regex _loggerARegex = new("Message = (\\d{17,19})");
|
||||
private static readonly Regex _loggerBRegex = new("MessageID:(\\d{17,19})");
|
||||
|
|
@ -62,6 +64,8 @@ public class LoggerCleanService
|
|||
new LoggerBot("Dyno#8389", 470724017205149701, ExtractDyno), // webhook
|
||||
new LoggerBot("Dyno#5714", 470723870270160917, ExtractDyno), // webhook
|
||||
new LoggerBot("Dyno#1961", 347378323418251264, ExtractDyno), // webhook
|
||||
new LoggerBot("Maki", 563434444321587202, ExtractMaki), // webhook
|
||||
new LoggerBot("Sapphire", 678344927997853742, ExtractSapphire), // webhook
|
||||
new LoggerBot("Auttaja", 242730576195354624, ExtractAuttaja), // webhook
|
||||
new LoggerBot("GenericBot", 295329346590343168, ExtractGenericBot),
|
||||
new LoggerBot("blargbot", 134133271750639616, ExtractBlargBot),
|
||||
|
|
@ -101,10 +105,10 @@ public class LoggerCleanService
|
|||
|
||||
public async ValueTask HandleLoggerBotCleanup(Message msg)
|
||||
{
|
||||
var channel = await _cache.GetChannel(msg.ChannelId);
|
||||
var channel = await _cache.GetChannel(msg.GuildId!.Value, msg.ChannelId!);
|
||||
|
||||
if (channel.Type != Channel.ChannelType.GuildText) return;
|
||||
if (!(await _cache.PermissionsIn(channel.Id)).HasFlag(PermissionSet.ManageMessages)) return;
|
||||
if (!(await _cache.BotPermissionsIn(msg.GuildId!.Value, channel.Id)).HasFlag(PermissionSet.ManageMessages)) return;
|
||||
|
||||
// If this message is from a *webhook*, check if the application ID matches one of the bots we know
|
||||
// If it's from a *bot*, check the bot ID to see if we know it.
|
||||
|
|
@ -239,6 +243,26 @@ public class LoggerCleanService
|
|||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
private static ulong? ExtractMaki(Message msg)
|
||||
{
|
||||
// Embed, Message Author Name field: "Message Deleted", footer is "Message ID: [id]"
|
||||
var embed = msg.Embeds?.FirstOrDefault();
|
||||
if (embed?.Author?.Name == null || embed?.Footer == null || (!embed?.Author?.Name.StartsWith("Message Deleted") ?? false)) return null;
|
||||
var match = _makiRegex.Match(embed.Footer.Text ?? "");
|
||||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
private static ulong? ExtractSapphire(Message msg)
|
||||
{
|
||||
// Embed, Message title field: "Message deleted", description contains "**Message ID:** [[id]]"
|
||||
// Example: "**Message ID:** [1297549791927996598]"
|
||||
var embed = msg.Embeds?.FirstOrDefault();
|
||||
if (embed == null) return null;
|
||||
if (!(embed.Title?.StartsWith("Message deleted") ?? false)) return null;
|
||||
var match = _sapphireRegex.Match(embed.Description);
|
||||
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
|
||||
}
|
||||
|
||||
private static FuzzyExtractResult? ExtractCircle(Message msg)
|
||||
{
|
||||
// Like Auttaja, Circle has both embed and compact modes, but the regex works for both.
|
||||
|
|
|
|||
|
|
@ -54,33 +54,6 @@ public class PeriodicStatCollector
|
|||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
// Aggregate guild/channel stats
|
||||
var guildCount = 0;
|
||||
var channelCount = 0;
|
||||
|
||||
// No LINQ today, sorry
|
||||
await foreach (var guild in _cache.GetAllGuilds())
|
||||
{
|
||||
guildCount++;
|
||||
foreach (var channel in await _cache.GetGuildChannels(guild.Id))
|
||||
if (DiscordUtils.IsValidGuildChannel(channel))
|
||||
channelCount++;
|
||||
}
|
||||
|
||||
if (_config.UseRedisMetrics)
|
||||
{
|
||||
var db = _redis.Connection.GetDatabase();
|
||||
await db.HashSetAsync("pluralkit:cluster_stats", new StackExchange.Redis.HashEntry[] {
|
||||
new(_botConfig.Cluster.NodeIndex, JsonConvert.SerializeObject(new ClusterMetricInfo
|
||||
{
|
||||
GuildCount = guildCount,
|
||||
ChannelCount = channelCount,
|
||||
DatabaseConnectionCount = _countHolder.ConnectionCount,
|
||||
WebhookCacheSize = _webhookCache.CacheSize,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Process info
|
||||
var process = Process.GetCurrentProcess();
|
||||
_metrics.Measure.Gauge.SetValue(CoreMetrics.ProcessPhysicalMemory, process.WorkingSet64);
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ public class WebhookCacheService
|
|||
|
||||
// We don't have one, so we gotta create a new one
|
||||
// but first, make sure we haven't hit the webhook cap yet...
|
||||
if (webhooks.Length >= 10)
|
||||
if (webhooks.Length >= 15)
|
||||
throw new PKError(
|
||||
"This channel has the maximum amount of possible webhooks (10) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying.");
|
||||
"This channel has the maximum amount of possible webhooks (15) already created. A server admin must delete one or more webhooks so PluralKit can create one for proxying.");
|
||||
|
||||
return await DoCreateWebhook(channelId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using App.Metrics;
|
|||
|
||||
using Humanizer;
|
||||
|
||||
using NodaTime.Text;
|
||||
|
||||
using Myriad.Cache;
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Rest;
|
||||
|
|
@ -17,7 +19,6 @@ using Newtonsoft.Json.Linq;
|
|||
|
||||
using Serilog;
|
||||
|
||||
using PluralKit.Core;
|
||||
using Myriad.Utils;
|
||||
|
||||
namespace PluralKit.Bot;
|
||||
|
|
@ -35,6 +36,7 @@ public record ProxyRequest
|
|||
public ulong GuildId { get; init; }
|
||||
public ulong ChannelId { get; init; }
|
||||
public ulong? ThreadId { get; init; }
|
||||
public ulong MessageId { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string? AvatarUrl { get; init; }
|
||||
public string? Content { get; init; }
|
||||
|
|
@ -45,6 +47,7 @@ public record ProxyRequest
|
|||
public bool AllowEveryone { get; init; }
|
||||
public Message.MessageFlags? Flags { get; init; }
|
||||
public bool Tts { get; init; }
|
||||
public Message.MessagePoll? Poll { get; init; }
|
||||
}
|
||||
|
||||
public class WebhookExecutorService
|
||||
|
|
@ -83,7 +86,8 @@ public class WebhookExecutorService
|
|||
return webhookMessage;
|
||||
}
|
||||
|
||||
public async Task<Message> EditWebhookMessage(ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false)
|
||||
public async Task<Message> EditWebhookMessage(ulong guildId, ulong channelId, ulong messageId, string newContent,
|
||||
bool clearEmbeds = false, bool clearAttachments = false)
|
||||
{
|
||||
var allowedMentions = newContent.ParseMentions() with
|
||||
{
|
||||
|
|
@ -92,7 +96,7 @@ public class WebhookExecutorService
|
|||
};
|
||||
|
||||
ulong? threadId = null;
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, channelId);
|
||||
var channel = await _cache.GetOrFetchChannel(_rest, guildId, channelId);
|
||||
if (channel.IsThread())
|
||||
{
|
||||
threadId = channelId;
|
||||
|
|
@ -104,7 +108,10 @@ public class WebhookExecutorService
|
|||
{
|
||||
Content = newContent,
|
||||
AllowedMentions = allowedMentions,
|
||||
Embeds = (clearEmbeds == true ? Optional<Embed[]>.Some(new Embed[] { }) : Optional<Embed[]>.None()),
|
||||
Embeds = (clearEmbeds ? Optional<Embed[]>.Some(new Embed[] { }) : Optional<Embed[]>.None()),
|
||||
Attachments = (clearAttachments
|
||||
? Optional<Message.Attachment[]>.Some(new Message.Attachment[] { })
|
||||
: Optional<Message.Attachment[]>.None())
|
||||
};
|
||||
|
||||
return await _rest.EditWebhookMessage(webhook.Id, webhook.Token, messageId, editReq, threadId);
|
||||
|
|
@ -154,6 +161,26 @@ public class WebhookExecutorService
|
|||
}).ToArray();
|
||||
}
|
||||
|
||||
if (req.Poll is Message.MessagePoll poll)
|
||||
{
|
||||
int? duration = null;
|
||||
if (poll.Expiry is string expiry)
|
||||
{
|
||||
var then = OffsetDateTimePattern.ExtendedIso.Parse(expiry).Value.ToInstant();
|
||||
var now = DiscordUtils.SnowflakeToInstant(req.MessageId);
|
||||
// in theory .TotalHours should be exact, but just in case
|
||||
duration = (int)Math.Round((then - now).TotalMinutes / 60.0);
|
||||
}
|
||||
webhookReq.Poll = new ExecuteWebhookRequest.WebhookPoll
|
||||
{
|
||||
Question = poll.Question,
|
||||
Answers = poll.Answers,
|
||||
Duration = duration,
|
||||
AllowMultiselect = poll.AllowMultiselect,
|
||||
LayoutType = poll.LayoutType
|
||||
};
|
||||
}
|
||||
|
||||
Message webhookMessage;
|
||||
using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime))
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue