mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +00:00
Merge remote-tracking branch 'upstream/main' into rust-command-parser
This commit is contained in:
commit
a29ed2bda0
28 changed files with 446 additions and 193 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
|
@ -98,6 +98,20 @@ dependencies = [
|
||||||
"twilight-http",
|
"twilight-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "app-commands"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"futures",
|
||||||
|
"libpk",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"twilight-http",
|
||||||
|
"twilight-model",
|
||||||
|
"twilight-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.7.1"
|
version = "1.7.1"
|
||||||
|
|
@ -4844,6 +4858,7 @@ version = "0.16.0"
|
||||||
source = "git+https://github.com/pluralkit/twilight?branch=pluralkit-7f08d95#054a2aa5d29fb46220af1cd5df568b73511cdb26"
|
source = "git+https://github.com/pluralkit/twilight?branch=pluralkit-7f08d95#054a2aa5d29fb46220af1cd5df568b73511cdb26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"twilight-model",
|
"twilight-model",
|
||||||
|
"twilight-validate",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ axum = { git = "https://github.com/pluralkit/axum", branch = "v0.8.4-pluralkit"
|
||||||
|
|
||||||
twilight-gateway = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" }
|
twilight-gateway = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" }
|
||||||
twilight-cache-inmemory = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] }
|
twilight-cache-inmemory = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] }
|
||||||
twilight-util = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] }
|
twilight-util = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator", "builder"] }
|
||||||
twilight-model = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" }
|
twilight-model = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" }
|
||||||
twilight-http = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", default-features = false, features = ["rustls-aws_lc_rs", "rustls-native-roots"] }
|
twilight-http = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", default-features = false, features = ["rustls-aws_lc_rs", "rustls-native-roots"] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ public record MessageComponent
|
||||||
public string? Url { get; init; }
|
public string? Url { get; init; }
|
||||||
public bool? Disabled { get; init; }
|
public bool? Disabled { get; init; }
|
||||||
public uint? AccentColor { get; init; }
|
public uint? AccentColor { get; init; }
|
||||||
|
public int? Spacing { get; init; }
|
||||||
public ComponentMedia? Media { get; init; }
|
public ComponentMedia? Media { get; init; }
|
||||||
public ComponentMediaItem[]? Items { get; init; }
|
public ComponentMediaItem[]? Items { get; init; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,10 @@ public class ApplicationCommandProxiedMessage
|
||||||
if (channel == null)
|
if (channel == null)
|
||||||
showContent = false;
|
showContent = false;
|
||||||
|
|
||||||
var embeds = new List<Embed>();
|
var components = new List<MessageComponent>();
|
||||||
|
|
||||||
var guild = await _cache.GetGuild(ctx.GuildId);
|
var guild = await _cache.GetGuild(ctx.GuildId);
|
||||||
if (msg.Member != null)
|
if (msg.Member != null)
|
||||||
embeds.Add(await _embeds.CreateMemberEmbed(
|
components.AddRange(await _embeds.CreateMemberMessageComponents(
|
||||||
msg.System,
|
msg.System,
|
||||||
msg.Member,
|
msg.Member,
|
||||||
guild,
|
guild,
|
||||||
|
|
@ -55,10 +54,12 @@ public class ApplicationCommandProxiedMessage
|
||||||
LookupContext.ByNonOwner,
|
LookupContext.ByNonOwner,
|
||||||
DateTimeZone.Utc
|
DateTimeZone.Utc
|
||||||
));
|
));
|
||||||
|
components.Add(new MessageComponent()
|
||||||
embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config));
|
{
|
||||||
|
Type = ComponentType.Separator
|
||||||
await ctx.Reply(embeds: embeds.ToArray());
|
});
|
||||||
|
components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, showContent, ctx.Config));
|
||||||
|
await ctx.Reply(components: components.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task QueryCommandMessage(InteractionContext ctx)
|
private async Task QueryCommandMessage(InteractionContext ctx)
|
||||||
|
|
@ -68,11 +69,7 @@ public class ApplicationCommandProxiedMessage
|
||||||
if (msg == null)
|
if (msg == null)
|
||||||
throw Errors.MessageNotFound(messageId);
|
throw Errors.MessageNotFound(messageId);
|
||||||
|
|
||||||
var embeds = new List<Embed>();
|
await ctx.Reply(components: await _embeds.CreateCommandMessageInfoMessageComponents(msg, true));
|
||||||
|
|
||||||
embeds.Add(await _embeds.CreateCommandMessageInfoEmbed(msg, true));
|
|
||||||
|
|
||||||
await ctx.Reply(embeds: embeds.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteMessage(InteractionContext ctx)
|
public async Task DeleteMessage(InteractionContext ctx)
|
||||||
|
|
|
||||||
|
|
@ -407,21 +407,33 @@ public class ProxiedMessage
|
||||||
if (author)
|
if (author)
|
||||||
{
|
{
|
||||||
var user = await _rest.GetUser(message.Message.Sender);
|
var user = await _rest.GetUser(message.Message.Sender);
|
||||||
var eb = new EmbedBuilder()
|
if (ctx.MatchFlag("show-embed", "se"))
|
||||||
.Author(new Embed.EmbedAuthor(
|
{
|
||||||
user != null
|
var eb = new EmbedBuilder()
|
||||||
? $"{user.Username}#{user.Discriminator}"
|
.Author(new Embed.EmbedAuthor(
|
||||||
: $"Deleted user ${message.Message.Sender}",
|
user != null
|
||||||
IconUrl: user != null ? user.AvatarUrl() : null))
|
? $"{user.Username}#{user.Discriminator}"
|
||||||
.Description(message.Message.Sender.ToString());
|
: $"Deleted user ${message.Message.Sender}",
|
||||||
|
IconUrl: user != null ? user.AvatarUrl() : null))
|
||||||
|
.Description(message.Message.Sender.ToString());
|
||||||
|
|
||||||
await ctx.Reply(
|
await ctx.Reply(
|
||||||
user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*",
|
user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*",
|
||||||
eb.Build());
|
eb.Build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Reply(components: await _embeds.CreateAuthorMessageComponents(user, message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config));
|
if (ctx.MatchFlag("show-embed", "se"))
|
||||||
|
{
|
||||||
|
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.Reply(components: await _embeds.CreateMessageInfoMessageComponents(message, showContent, ctx.Config));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete)
|
private async Task GetCommandMessage(Context ctx, ulong messageId, bool isDelete)
|
||||||
|
|
@ -453,6 +465,11 @@ public class ProxiedMessage
|
||||||
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
||||||
showContent = false;
|
showContent = false;
|
||||||
|
|
||||||
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
|
if (ctx.MatchFlag("show-embed", "se"))
|
||||||
|
{
|
||||||
|
await ctx.Reply(embed: await _embeds.CreateCommandMessageInfoEmbed(msg, showContent));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ctx.Reply(components: await _embeds.CreateCommandMessageInfoMessageComponents(msg, showContent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -186,10 +186,9 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
||||||
{
|
{
|
||||||
var dm = await _dmCache.GetOrCreateDmChannel(evt.UserId);
|
var dm = await _dmCache.GetOrCreateDmChannel(evt.UserId);
|
||||||
|
|
||||||
var embeds = new List<Embed>();
|
var components = new List<MessageComponent>();
|
||||||
|
|
||||||
if (msg.Member != null)
|
if (msg.Member != null)
|
||||||
embeds.Add(await _embeds.CreateMemberEmbed(
|
components.AddRange(await _embeds.CreateMemberMessageComponents(
|
||||||
msg.System,
|
msg.System,
|
||||||
msg.Member,
|
msg.Member,
|
||||||
guild,
|
guild,
|
||||||
|
|
@ -197,10 +196,12 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
||||||
LookupContext.ByNonOwner,
|
LookupContext.ByNonOwner,
|
||||||
DateTimeZone.Utc
|
DateTimeZone.Utc
|
||||||
));
|
));
|
||||||
|
components.Add(new MessageComponent()
|
||||||
embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config));
|
{
|
||||||
|
Type = ComponentType.Separator
|
||||||
await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() });
|
});
|
||||||
|
components.AddRange(await _embeds.CreateMessageInfoMessageComponents(msg, true, config));
|
||||||
|
await _rest.CreateMessage(dm, new MessageRequest { Components = components.ToArray(), Flags = Message.MessageFlags.IsComponentsV2, AllowedMentions = new AllowedMentions() });
|
||||||
}
|
}
|
||||||
catch (ForbiddenException) { } // No permissions to DM, can't check for this :(
|
catch (ForbiddenException) { } // No permissions to DM, can't check for this :(
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,15 @@ public class ProxyService
|
||||||
|
|
||||||
var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, guild.Id, null);
|
var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, guild.Id, null);
|
||||||
|
|
||||||
|
if (IsDisableAutoproxy(message))
|
||||||
|
{
|
||||||
|
await _repo.UpdateAutoproxy(ctx.SystemId.Value, guild.Id, null, new()
|
||||||
|
{
|
||||||
|
AutoproxyMode = AutoproxyMode.Off
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (autoproxySettings.AutoproxyMode == AutoproxyMode.Latch && IsUnlatch(message))
|
if (autoproxySettings.AutoproxyMode == AutoproxyMode.Latch && IsUnlatch(message))
|
||||||
{
|
{
|
||||||
// "unlatch"
|
// "unlatch"
|
||||||
|
|
@ -495,6 +504,9 @@ public class ProxyService
|
||||||
public static bool IsUnlatch(Message message)
|
public static bool IsUnlatch(Message message)
|
||||||
=> message.Content.StartsWith(@"\\") || message.Content.StartsWith("\\\u200b\\");
|
=> message.Content.StartsWith(@"\\") || message.Content.StartsWith("\\\u200b\\");
|
||||||
|
|
||||||
|
public static bool IsDisableAutoproxy(Message message)
|
||||||
|
=> message.Content.StartsWith(@"\\\") || message.Content.StartsWith("\\\u200b\\\u200b\\");
|
||||||
|
|
||||||
private async Task HandleProxyExecutedActions(MessageContext ctx, AutoproxySettings autoproxySettings,
|
private async Task HandleProxyExecutedActions(MessageContext ctx, AutoproxySettings autoproxySettings,
|
||||||
Message triggerMessage, Message proxyMessage, ProxyMatch match,
|
Message triggerMessage, Message proxyMessage, ProxyMatch match,
|
||||||
bool deletePrevious = true)
|
bool deletePrevious = true)
|
||||||
|
|
|
||||||
|
|
@ -766,6 +766,158 @@ public class EmbedService
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<MessageComponent[]> CreateMessageInfoMessageComponents(FullMessage msg, bool showContent, SystemConfig? ccfg = null)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Need this whole dance to handle cases where:
|
||||||
|
// - the user is deleted (userInfo == null)
|
||||||
|
// - the bot's no longer in the server we're querying (channel == null)
|
||||||
|
// - the member is no longer in the server we're querying (memberInfo == null)
|
||||||
|
// TODO: optimize ordering here a bit with new cache impl; and figure what happens if bot leaves server -> channel still cached -> hits this bit and 401s?
|
||||||
|
GuildMemberPartial memberInfo = null;
|
||||||
|
User userInfo = null;
|
||||||
|
if (channel != null)
|
||||||
|
{
|
||||||
|
GuildMember member = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
member = await _rest.GetGuildMember(channel.GuildId!.Value, msg.Message.Sender);
|
||||||
|
}
|
||||||
|
catch (ForbiddenException)
|
||||||
|
{
|
||||||
|
// no permission, couldn't fetch, oh well
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member != null)
|
||||||
|
// Don't do an extra request if we already have this info from the member lookup
|
||||||
|
userInfo = member.User;
|
||||||
|
memberInfo = member;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userInfo == null)
|
||||||
|
userInfo = await _cache.GetOrFetchUser(_rest, msg.Message.Sender);
|
||||||
|
|
||||||
|
// Calculate string displayed under "Sent by"
|
||||||
|
string userStr;
|
||||||
|
if (showContent && memberInfo != null && memberInfo.Nick != null)
|
||||||
|
userStr = $"**\n Username:** {userInfo.NameAndMention()}\n** Nickname:** {memberInfo.Nick}";
|
||||||
|
else if (userInfo != null) userStr = userInfo.NameAndMention();
|
||||||
|
else userStr = $"*(deleted user {msg.Message.Sender})*";
|
||||||
|
|
||||||
|
var content = serverMsg?.Content?.NormalizeLineEndSpacing();
|
||||||
|
if (content == null || !showContent)
|
||||||
|
content = "*(message contents deleted or inaccessible)*";
|
||||||
|
|
||||||
|
var systemStr = msg.System == null
|
||||||
|
? "*(deleted or unknown system)*"
|
||||||
|
: msg.System.NameFor(ctx) != null ? $"{msg.System.NameFor(ctx)} (`{msg.System.DisplayHid(ccfg)}`)" : $"`{msg.System.DisplayHid(ccfg)}`";
|
||||||
|
var memberStr = msg.Member == null
|
||||||
|
? "*(deleted member)*"
|
||||||
|
: $"{msg.Member.NameFor(ctx)} (`{msg.Member.DisplayHid(ccfg)}`)";
|
||||||
|
|
||||||
|
var roles = memberInfo?.Roles?.ToList();
|
||||||
|
var rolesContent = "";
|
||||||
|
if (roles != null && roles.Count > 0 && showContent)
|
||||||
|
{
|
||||||
|
var guild = await _cache.GetGuild(channel.GuildId!.Value);
|
||||||
|
var rolesString = string.Join(", ", (roles
|
||||||
|
.Select(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));
|
||||||
|
rolesContent = $"**Account Roles ({roles.Count})**\n{rolesString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageComponent authorData = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = $"**System:** {systemStr}\n**Member:** {memberStr}\n**Sent by:** {userStr}\n\n{rolesContent}"
|
||||||
|
};
|
||||||
|
|
||||||
|
var avatarURL = msg.Member?.AvatarFor(ctx).TryGetCleanCdnUrl();
|
||||||
|
MessageComponent header = (avatarURL == "" || avatarURL == null) ? authorData : new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Section,
|
||||||
|
Components = [authorData],
|
||||||
|
Accessory = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Thumbnail,
|
||||||
|
Media = new ComponentMedia()
|
||||||
|
{
|
||||||
|
Url = avatarURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
List<MessageComponent> body = [
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Separator,
|
||||||
|
Spacing = 2
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if (content != "")
|
||||||
|
{
|
||||||
|
body.Add(new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showContent)
|
||||||
|
{
|
||||||
|
if (serverMsg != null)
|
||||||
|
{
|
||||||
|
var media = new List<ComponentMediaItem>();
|
||||||
|
foreach (Message.Attachment attachment in serverMsg?.Attachments)
|
||||||
|
{
|
||||||
|
var url = attachment.Url;
|
||||||
|
if (url != null && url != "")
|
||||||
|
media.Add(new ComponentMediaItem()
|
||||||
|
{
|
||||||
|
Media = new ComponentMedia()
|
||||||
|
{
|
||||||
|
Url = url
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (media.Count > 0)
|
||||||
|
body.Add(new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.MediaGallery,
|
||||||
|
Items = media.ToArray()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageComponent footer = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = $"-# Original Message ID: {msg.Message.OriginalMid} · <t:{DiscordUtils.SnowflakeToTimestamp(msg.Message.Mid)}:f>"
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Container,
|
||||||
|
Components = [
|
||||||
|
header,
|
||||||
|
..body
|
||||||
|
]
|
||||||
|
},
|
||||||
|
footer
|
||||||
|
];
|
||||||
|
}
|
||||||
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null)
|
public async Task<Embed> CreateMessageInfoEmbed(FullMessage msg, bool showContent, SystemConfig? ccfg = null)
|
||||||
{
|
{
|
||||||
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel);
|
var channel = await _cache.GetOrFetchChannel(_rest, msg.Message.Guild ?? 0, msg.Message.Channel);
|
||||||
|
|
@ -852,6 +1004,106 @@ public class EmbedService
|
||||||
return eb.Build();
|
return eb.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<MessageComponent[]> CreateAuthorMessageComponents(User? user, FullMessage msg)
|
||||||
|
{
|
||||||
|
MessageComponent authorInfo;
|
||||||
|
var author = user != null
|
||||||
|
? $"{user.Username}#{user.Discriminator}"
|
||||||
|
: $"Deleted user ${msg.Message.Sender}";
|
||||||
|
var avatarUrl = user?.AvatarUrl();
|
||||||
|
var authorString = $"{author}\n**ID: **`{msg.Message.Sender.ToString()}`";
|
||||||
|
if (user != null && avatarUrl != "")
|
||||||
|
{
|
||||||
|
authorInfo = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Section,
|
||||||
|
Components = [
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = authorString
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Accessory = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Thumbnail,
|
||||||
|
Media = new ComponentMedia()
|
||||||
|
{
|
||||||
|
Url = avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
authorInfo = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = authorString
|
||||||
|
};
|
||||||
|
}
|
||||||
|
MessageComponent container = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Container,
|
||||||
|
Components = [
|
||||||
|
authorInfo,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {msg.Message.Sender})*"
|
||||||
|
},
|
||||||
|
container
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MessageComponent[]> CreateCommandMessageInfoMessageComponents(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MessageComponent> body = [
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = $"### Command response message\n**Original message:** https://discord.com/channels/{msg.Guild}/{msg.Channel}/{msg.OriginalMid}\n**Sent By:** <@{msg.Sender}>"
|
||||||
|
},
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Separator,
|
||||||
|
},
|
||||||
|
new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = content
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
MessageComponent footer = new MessageComponent()
|
||||||
|
{
|
||||||
|
Type = ComponentType.Text,
|
||||||
|
Content = $"-# Original Message ID: {msg.OriginalMid} · <t:{DiscordUtils.SnowflakeToTimestamp(msg.OriginalMid)}:f>"
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
new MessageComponent(){
|
||||||
|
Type = ComponentType.Container,
|
||||||
|
Components = [
|
||||||
|
..body
|
||||||
|
]
|
||||||
|
},
|
||||||
|
footer
|
||||||
|
];
|
||||||
|
}
|
||||||
public async Task<Embed> CreateCommandMessageInfoEmbed(Core.CommandMessage msg, bool showContent)
|
public async Task<Embed> CreateCommandMessageInfoEmbed(Core.CommandMessage msg, bool showContent)
|
||||||
{
|
{
|
||||||
var content = "*(command message deleted or inaccessible)*";
|
var content = "*(command message deleted or inaccessible)*";
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ public static class DiscordUtils
|
||||||
public static Instant SnowflakeToInstant(ulong snowflake) =>
|
public static Instant SnowflakeToInstant(ulong snowflake) =>
|
||||||
Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);
|
Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);
|
||||||
|
|
||||||
|
public static ulong SnowflakeToTimestamp(ulong snowflake) =>
|
||||||
|
((ulong)Instant.FromUtc(2015, 1, 1, 0, 0, 0).ToUnixTimeMilliseconds() + (snowflake >> 22)) / 1000;
|
||||||
|
|
||||||
public static ulong InstantToSnowflake(Instant time) =>
|
public static ulong InstantToSnowflake(Instant time) =>
|
||||||
(ulong)(time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22;
|
(ulong)(time - Instant.FromUtc(2015, 1, 1, 0, 0, 0)).TotalMilliseconds << 22;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using Autofac;
|
||||||
using Myriad.Cache;
|
using Myriad.Cache;
|
||||||
using Myriad.Gateway;
|
using Myriad.Gateway;
|
||||||
using Myriad.Rest;
|
using Myriad.Rest;
|
||||||
|
using Myriad.Rest.Types;
|
||||||
using Myriad.Types;
|
using Myriad.Types;
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
|
|
@ -76,6 +77,17 @@ public class InteractionContext
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task Reply(MessageComponent[] components = null, AllowedMentions? mentions = null)
|
||||||
|
{
|
||||||
|
await Respond(InteractionResponse.ResponseType.ChannelMessageWithSource,
|
||||||
|
new InteractionApplicationCommandCallbackData
|
||||||
|
{
|
||||||
|
Components = components,
|
||||||
|
Flags = Message.MessageFlags.Ephemeral | Message.MessageFlags.IsComponentsV2,
|
||||||
|
AllowedMentions = mentions ?? new AllowedMentions()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Defer()
|
public async Task Defer()
|
||||||
{
|
{
|
||||||
await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource,
|
await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource,
|
||||||
|
|
|
||||||
|
|
@ -127,13 +127,10 @@ fn router(ctx: ApiContext) -> Router {
|
||||||
.route("/v2/groups/{group_id}/oembed.json", get(rproxy))
|
.route("/v2/groups/{group_id}/oembed.json", get(rproxy))
|
||||||
|
|
||||||
.layer(middleware::ratelimit::ratelimiter(middleware::ratelimit::do_request_ratelimited)) // this sucks
|
.layer(middleware::ratelimit::ratelimiter(middleware::ratelimit::do_request_ratelimited)) // this sucks
|
||||||
|
|
||||||
.layer(axum::middleware::from_fn(middleware::ignore_invalid_routes::ignore_invalid_routes))
|
.layer(axum::middleware::from_fn(middleware::ignore_invalid_routes::ignore_invalid_routes))
|
||||||
.layer(axum::middleware::from_fn(middleware::logger::logger))
|
|
||||||
|
|
||||||
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::params::params))
|
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::params::params))
|
||||||
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::auth::auth))
|
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::auth::auth))
|
||||||
|
.layer(axum::middleware::from_fn(middleware::logger::logger))
|
||||||
.layer(axum::middleware::from_fn(middleware::cors::cors))
|
.layer(axum::middleware::from_fn(middleware::cors::cors))
|
||||||
.layer(tower_http::catch_panic::CatchPanicLayer::custom(util::handle_panic))
|
.layer(tower_http::catch_panic::CatchPanicLayer::custom(util::handle_panic))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,5 +76,10 @@ pub async fn auth(State(ctx): State<ApiContext>, mut req: Request, next: Next) -
|
||||||
req.extensions_mut()
|
req.extensions_mut()
|
||||||
.insert(AuthState::new(authed_system_id, authed_app_id, internal));
|
.insert(AuthState::new(authed_system_id, authed_app_id, internal));
|
||||||
|
|
||||||
next.run(req).await
|
let mut res = next.run(req).await;
|
||||||
|
|
||||||
|
res.extensions_mut()
|
||||||
|
.insert(AuthState::new(authed_system_id, authed_app_id, internal));
|
||||||
|
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ const MIN_LOG_TIME: u128 = 2_000;
|
||||||
|
|
||||||
pub async fn logger(request: Request, next: Next) -> Response {
|
pub async fn logger(request: Request, next: Next) -> Response {
|
||||||
let method = request.method().clone();
|
let method = request.method().clone();
|
||||||
|
let headers = request.headers().clone();
|
||||||
|
|
||||||
let remote_ip = header_or_unknown(request.headers().get("X-PluralKit-Client-IP"));
|
let remote_ip = header_or_unknown(headers.get("X-PluralKit-Client-IP"));
|
||||||
let user_agent = header_or_unknown(request.headers().get("User-Agent"));
|
let user_agent = header_or_unknown(headers.get("User-Agent"));
|
||||||
|
|
||||||
let extensions = request.extensions().clone();
|
let extensions = request.extensions().clone();
|
||||||
|
|
||||||
|
|
@ -24,10 +25,6 @@ pub async fn logger(request: Request, next: Next) -> Response {
|
||||||
.map(|v| v.as_str().to_string())
|
.map(|v| v.as_str().to_string())
|
||||||
.unwrap_or("unknown".to_string());
|
.unwrap_or("unknown".to_string());
|
||||||
|
|
||||||
let auth = extensions
|
|
||||||
.get::<AuthState>()
|
|
||||||
.expect("should always have AuthState");
|
|
||||||
|
|
||||||
let uri = request.uri().clone();
|
let uri = request.uri().clone();
|
||||||
|
|
||||||
let request_span = span!(
|
let request_span = span!(
|
||||||
|
|
@ -43,15 +40,24 @@ pub async fn logger(request: Request, next: Next) -> Response {
|
||||||
let response = next.run(request).instrument(request_span).await;
|
let response = next.run(request).instrument(request_span).await;
|
||||||
let elapsed = start.elapsed().as_millis();
|
let elapsed = start.elapsed().as_millis();
|
||||||
|
|
||||||
let system_id = auth
|
let rext = response.extensions().clone();
|
||||||
.system_id()
|
let auth = rext.get::<AuthState>();
|
||||||
.map(|v| v.to_string())
|
|
||||||
.unwrap_or("none".to_string());
|
|
||||||
|
|
||||||
let app_id = auth
|
let system_id = if let Some(auth) = auth {
|
||||||
.app_id()
|
auth.system_id()
|
||||||
.map(|v| v.to_string())
|
.map(|v| v.to_string())
|
||||||
.unwrap_or("none".to_string());
|
.unwrap_or("none".to_string())
|
||||||
|
} else {
|
||||||
|
"none".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let app_id = if let Some(auth) = auth {
|
||||||
|
auth.app_id()
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or("none".to_string())
|
||||||
|
} else {
|
||||||
|
"none".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
counter!(
|
counter!(
|
||||||
"pluralkit_api_requests",
|
"pluralkit_api_requests",
|
||||||
|
|
@ -73,6 +79,14 @@ pub async fn logger(request: Request, next: Next) -> Response {
|
||||||
.record(elapsed as f64 / 1_000_f64);
|
.record(elapsed as f64 / 1_000_f64);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
|
status = response.status().as_str(),
|
||||||
|
method = method.to_string(),
|
||||||
|
endpoint,
|
||||||
|
elapsed,
|
||||||
|
user_agent,
|
||||||
|
remote_ip,
|
||||||
|
system_id,
|
||||||
|
app_id,
|
||||||
"{} handled request for {} {} in {}ms",
|
"{} handled request for {} {} in {}ms",
|
||||||
response.status(),
|
response.status(),
|
||||||
method,
|
method,
|
||||||
|
|
|
||||||
14
crates/app-commands/Cargo.toml
Normal file
14
crates/app-commands/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "app-commands"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libpk = { path = "../libpk" }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
twilight-http = { workspace = true }
|
||||||
|
twilight-model = { workspace = true }
|
||||||
|
twilight-util = { workspace = true }
|
||||||
41
crates/app-commands/src/main.rs
Normal file
41
crates/app-commands/src/main.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use twilight_model::{
|
||||||
|
application::command::{Command, CommandType},
|
||||||
|
guild::IntegrationApplication,
|
||||||
|
};
|
||||||
|
use twilight_util::builder::command::CommandBuilder;
|
||||||
|
|
||||||
|
#[libpk::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let discord = twilight_http::Client::builder()
|
||||||
|
.token(
|
||||||
|
libpk::config
|
||||||
|
.discord
|
||||||
|
.as_ref()
|
||||||
|
.expect("missing discord config")
|
||||||
|
.bot_token
|
||||||
|
.clone(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let interaction = discord.interaction(twilight_model::id::Id::new(
|
||||||
|
libpk::config
|
||||||
|
.discord
|
||||||
|
.as_ref()
|
||||||
|
.expect("missing discord config")
|
||||||
|
.client_id
|
||||||
|
.clone()
|
||||||
|
.get(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let commands = vec![
|
||||||
|
// message commands
|
||||||
|
// description must be empty string
|
||||||
|
CommandBuilder::new("\u{2753} Message info", "", CommandType::Message).build(),
|
||||||
|
CommandBuilder::new("\u{274c} Delete message", "", CommandType::Message).build(),
|
||||||
|
CommandBuilder::new("\u{1f514} Ping author", "", CommandType::Message).build(),
|
||||||
|
];
|
||||||
|
|
||||||
|
interaction.set_global_commands(&commands).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
BIN
docs/content/assets/prodigi.png
Normal file
BIN
docs/content/assets/prodigi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -55,8 +55,8 @@ This includes:
|
||||||
For more information about why certain information must be public, see [this github issue](https://github.com/PluralKit/PluralKit/issues/238).
|
For more information about why certain information must be public, see [this github issue](https://github.com/PluralKit/PluralKit/issues/238).
|
||||||
|
|
||||||
### Is there a way to restrict PluralKit usage to a certain role? / Can I remove PluralKit access for specific users in my server?
|
### Is there a way to restrict PluralKit usage to a certain role? / Can I remove PluralKit access for specific users in my server?
|
||||||
This is not a feature currently available in PluralKit. It may be added in the future.
|
PluralKit does not, and *will not*, support restricting usage of the bot by role.
|
||||||
In the meantime, this feature is supported in Tupperbox (an alternative proxying bot) - ask about it in their support server: <https://discord.gg/Z4BHccHhy3>
|
This feature is supported in Tupperbox (an alternative proxying bot) - ask about it in their support server: <https://discord.gg/Z4BHccHhy3>
|
||||||
|
|
||||||
### Is it possible to block proxied messages (like blocking a user)?
|
### Is it possible to block proxied messages (like blocking a user)?
|
||||||
No. Since proxied messages are posted through webhooks, and those technically aren't real users on Discord's end, it's not possible to block them. Blocking PluralKit itself will also not block the webhook messages. Discord also does not allow you to control who can receive a specific message, so it's not possible to integrate a blocking system in the bot, either. Sorry :/
|
No. Since proxied messages are posted through webhooks, and those technically aren't real users on Discord's end, it's not possible to block them. Blocking PluralKit itself will also not block the webhook messages. Discord also does not allow you to control who can receive a specific message, so it's not possible to integrate a blocking system in the bot, either. Sorry :/
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,14 @@ This bot detects messages with certain tags associated with a profile, then repl
|
||||||
#### for example...
|
#### for example...
|
||||||

|

|
||||||
|
|
||||||
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!
|
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!
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
<div style="text-align:center;">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
PluralKit's infrastructure is generously sponsored by [Prodigi](https://prodigi.nz), a New Zealand based technology services provider.
|
||||||
|
|
@ -5,7 +5,7 @@ Because PluralKit deletes messages as part of proxying, this can often clutter u
|
||||||
## Bots with PluralKit support
|
## Bots with PluralKit support
|
||||||
Some moderation bots have official PluralKit support, and properly handle excluding proxy deletes, as well as add PK-specific information to relevant log messages:
|
Some moderation bots have official PluralKit support, and properly handle excluding proxy deletes, as well as add PK-specific information to relevant log messages:
|
||||||
|
|
||||||
- [**Catalogger**](https://catalogger.starshines.xyz/docs)
|
- [**Catalogger**](https://catalogger.app)
|
||||||
- [**Aero**](https://aero.bot/)
|
- [**Aero**](https://aero.bot/)
|
||||||
- [**CoreBot**](https://discord.gg/GAAj6DDrCJ)
|
- [**CoreBot**](https://discord.gg/GAAj6DDrCJ)
|
||||||
- [**Quark**](https://quark.bot)
|
- [**Quark**](https://quark.bot)
|
||||||
|
|
|
||||||
4
scripts/app-commands/.gitignore
vendored
4
scripts/app-commands/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
/commands.json
|
|
||||||
|
|
||||||
*.pyc
|
|
||||||
__pycache__/
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# PluralKit "application command" helpers
|
|
||||||
|
|
||||||
## Adding new commands
|
|
||||||
|
|
||||||
Edit the `COMMAND_LIST` global in `commands.py`, making sure that any
|
|
||||||
command names that are specified in that file match up with the
|
|
||||||
command names used in the bot code (which will generally be in the list
|
|
||||||
in `PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandList.cs`).
|
|
||||||
|
|
||||||
TODO: add helpers for slash commands to this
|
|
||||||
|
|
||||||
## Dumping application command JSON
|
|
||||||
|
|
||||||
Run `python3 commands.py` to get a JSON dump of the available application
|
|
||||||
commands - this is in a format that can be sent to Discord as a `PUT` to
|
|
||||||
`/applications/{clientId}/commands`.
|
|
||||||
|
|
||||||
## Updating Discord's list of application commands
|
|
||||||
|
|
||||||
From the root of the repository (where your `pluralkit.conf` resides),
|
|
||||||
run `python3 ./scripts/app-commands/update.py`. This will **REPLACE**
|
|
||||||
any existing application commands that Discord knows about, with the
|
|
||||||
updated list.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
from common import *
|
|
||||||
|
|
||||||
COMMAND_LIST = [
|
|
||||||
MessageCommand("\U00002753 Message info"),
|
|
||||||
MessageCommand("\U0000274c Delete message"),
|
|
||||||
MessageCommand("\U0001f514 Ping author"),
|
|
||||||
]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(__import__('json').dumps(COMMAND_LIST))
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from .types import MessageCommand
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
class MessageCommand(dict):
|
|
||||||
COMMAND_TYPE = 3
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
super().__init__()
|
|
||||||
self["type"] = self.__class__.COMMAND_TYPE
|
|
||||||
self["name"] = name
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
from common import *
|
|
||||||
from commands import COMMAND_LIST
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from urllib import request
|
|
||||||
from urllib.error import URLError
|
|
||||||
|
|
||||||
DISCORD_API_BASE = "https://discord.com/api/v10"
|
|
||||||
|
|
||||||
def get_config():
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# prefer token from environment if present
|
|
||||||
envbase = ["PluralKit", "Bot"]
|
|
||||||
for var in ["Token", "ClientId"]:
|
|
||||||
for sep in [':', '__']:
|
|
||||||
envvar = sep.join(envbase + [var])
|
|
||||||
if envvar in os.environ:
|
|
||||||
data[var] = os.environ[envvar]
|
|
||||||
|
|
||||||
if "Token" in data and "ClientId" in data:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# else fall back to config
|
|
||||||
cfg_path = Path(os.getcwd()) / "pluralkit.conf"
|
|
||||||
if cfg_path.exists():
|
|
||||||
cfg = {}
|
|
||||||
with open(str(cfg_path), 'r') as fh:
|
|
||||||
cfg = json.load(fh)
|
|
||||||
|
|
||||||
if 'PluralKit' in cfg and 'Bot' in cfg['PluralKit']:
|
|
||||||
return cfg['PluralKit']['Bot']
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def main():
|
|
||||||
config = get_config()
|
|
||||||
if config is None:
|
|
||||||
raise ArgumentError("config was not loaded")
|
|
||||||
if 'Token' not in config or 'ClientId' not in config:
|
|
||||||
raise ArgumentError("config is missing 'Token' or 'ClientId'")
|
|
||||||
|
|
||||||
data = json.dumps(COMMAND_LIST)
|
|
||||||
url = DISCORD_API_BASE + f"/applications/{config['ClientId']}/commands"
|
|
||||||
req = request.Request(url, method='PUT', data=data.encode('utf-8'))
|
|
||||||
req.add_header("Content-Type", "application/json")
|
|
||||||
req.add_header("Authorization", f"Bot {config['Token']}")
|
|
||||||
req.add_header("User-Agent", "PluralKit (app-commands updater; https://pluralkit.me)")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with request.urlopen(req) as resp:
|
|
||||||
if resp.status == 200:
|
|
||||||
print("Update successful!")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
except URLError as resp:
|
|
||||||
print(f"[!!!] Update not successful: status {resp.status}", file=sys.stderr)
|
|
||||||
print(f"[!!!] Response body below:\n", file=sys.stderr)
|
|
||||||
print(resp.read(), file=sys.stderr)
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
docker-compose -f "$(dirname $0)/../docker-compose.yml" exec -T -u postgres db pg_dump postgres
|
|
||||||
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Usage: rclone-db.sh <remote>:<path>
|
|
||||||
# eg. rclone-db.sh b2:pluralkit
|
|
||||||
|
|
||||||
FILENAME=pluralkit-$(date -u +"%Y-%m-%dT%H:%M:%S").sql.gz
|
|
||||||
|
|
||||||
echo Dumping database to /tmp/$FILENAME...
|
|
||||||
$(dirname $0)/dump-db.sh | gzip > /tmp/$FILENAME
|
|
||||||
|
|
||||||
echo Transferring to remote $1...
|
|
||||||
rclone -P copy /tmp/$FILENAME $1
|
|
||||||
|
|
||||||
echo Cleaning up...
|
|
||||||
rm /tmp/$FILENAME
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Runs a local database in the background listening on port 5432, deleting itself once stopped
|
|
||||||
# Requires Docker. May need sudo if your user isn't in the `docker` group.
|
|
||||||
docker run --rm --detach --publish 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:alpine
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue