mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 04:56:49 +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
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[build]
|
||||
rustflags = ["-C", "target-cpu=native"]
|
||||
|
||||
33
.github/workflows/rust.yml
vendored
33
.github/workflows/rust.yml
vendored
|
|
@ -3,14 +3,19 @@
|
|||
# todo: don't use docker/build-push-action
|
||||
# todo: run builds on pull request
|
||||
|
||||
name: Build and push API Docker image
|
||||
name: Build and push Rust service Docker images
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'lib/pklib/**'
|
||||
- 'lib/libpk/**'
|
||||
- 'services/api/**'
|
||||
- 'services/gateway/**'
|
||||
- 'services/avatars/**'
|
||||
- '.github/workflows/rust.yml'
|
||||
- 'ci/Dockerfile.rust'
|
||||
- 'ci/rust-docker-target.sh'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
|
@ -35,21 +40,15 @@ jobs:
|
|||
with:
|
||||
# https://github.com/docker/build-push-action/issues/378
|
||||
context: .
|
||||
file: Dockerfile.rust
|
||||
file: ci/Dockerfile.rust
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
cache-from: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust
|
||||
cache-to: type=registry,ref=ghcr.io/pluralkit/docker-cache:rust,mode=max
|
||||
outputs: .docker-bin
|
||||
|
||||
# add more binaries here
|
||||
- run: |
|
||||
for binary in "api"; do
|
||||
for tag in latest ${{ env.BRANCH_NAME }} ${{ github.sha }}; do
|
||||
cat Dockerfile.bin | sed "s/__BINARY__/$binary/g" | docker build -t ghcr.io/pluralkit/$binary:$tag -f - .
|
||||
done
|
||||
if [ "${{ github.repository }}" == "PluralKit/PluralKit" ]; then
|
||||
docker push ghcr.io/pluralkit/$binary:${{ env.BRANCH_NAME }}
|
||||
docker push ghcr.io/pluralkit/$binary:${{ github.sha }}
|
||||
[ "${{ env.BRANCH_NAME }}" == "main" ] && docker push ghcr.io/pluralkit/$binary:latest
|
||||
fi
|
||||
done
|
||||
tag=${{ github.sha }} \
|
||||
branch=${{ env.BRANCH_NAME }} \
|
||||
push=$([ "${{ github.repository }}" == "PluralKit/PluralKit" ] && echo true || echo false) \
|
||||
ci/rust-docker-target.sh
|
||||
|
|
|
|||
16
.github/workflows/rustfmt.yml
vendored
Normal file
16
.github/workflows/rustfmt.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
name: "Check Rust formatting"
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: cargo fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Rustfmt Check
|
||||
uses: actions-rust-lang/rustfmt@v1
|
||||
2
.github/workflows/scheduled_tasks.yml
vendored
2
.github/workflows/scheduled_tasks.yml
vendored
|
|
@ -2,8 +2,8 @@ name: Build scheduled tasks runner Docker image
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/scheduled_tasks.yml
|
||||
- 'services/scheduled_tasks/**'
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ target/
|
|||
.idea/
|
||||
.run/
|
||||
.vscode/
|
||||
.mono/
|
||||
tags/
|
||||
.DS_Store
|
||||
mono_crash*
|
||||
|
|
|
|||
3493
Cargo.lock
generated
3493
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
43
Cargo.toml
43
Cargo.toml
|
|
@ -1,14 +1,45 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"./lib/libpk",
|
||||
"./services/api"
|
||||
"./services/api",
|
||||
"./services/dispatch",
|
||||
"./services/gateway",
|
||||
"./services/avatars"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
fred = { version = "5.2.0", default-features = false, features = ["tracing", "pool-prefer-active"] }
|
||||
axum = "0.7.5"
|
||||
axum-macros = "0.4.1"
|
||||
bytes = "1.6.0"
|
||||
chrono = "0.4"
|
||||
fred = { version = "9.3.0", default-features = false, features = ["tracing", "i-keys", "i-hashes", "i-scripts", "sha-1"] }
|
||||
futures = "0.3.30"
|
||||
lazy_static = "1.4.0"
|
||||
metrics = "0.20.1"
|
||||
serde = "1.0.152"
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
tracing = "0.1.37"
|
||||
metrics = "0.23.0"
|
||||
reqwest = { version = "0.12.7" , default-features = false, features = ["rustls-tls", "trust-dns"]}
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
signal-hook = "0.3.17"
|
||||
sqlx = { version = "0.8.2", features = ["runtime-tokio", "postgres", "time", "macros", "uuid"] }
|
||||
time = "0.3.34"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] }
|
||||
uuid = { version = "1.7.0", features = ["serde"] }
|
||||
|
||||
twilight-gateway = { git = "https://github.com/pluralkit/twilight" }
|
||||
twilight-cache-inmemory = { git = "https://github.com/pluralkit/twilight", features = ["permission-calculator"] }
|
||||
twilight-util = { git = "https://github.com/pluralkit/twilight", features = ["permission-calculator"] }
|
||||
twilight-model = { git = "https://github.com/pluralkit/twilight" }
|
||||
twilight-http = { git = "https://github.com/pluralkit/twilight", default-features = false, features = ["rustls-native-roots"] }
|
||||
|
||||
#twilight-gateway = { path = "../twilight/twilight-gateway" }
|
||||
#twilight-cache-inmemory = { path = "../twilight/twilight-cache-inmemory", features = ["permission-calculator"] }
|
||||
#twilight-util = { path = "../twilight/twilight-util", features = ["permission-calculator"] }
|
||||
#twilight-model = { path = "../twilight/twilight-model" }
|
||||
#twilight-http = { path = "../twilight/twilight-http", default-features = false, features = ["rustls-native-roots"] }
|
||||
|
||||
prost = "0.12"
|
||||
prost-types = "0.12"
|
||||
prost-build = "0.12"
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
FROM alpine:latest
|
||||
|
||||
COPY /.docker-bin/__BINARY__ /bin/__BINARY__
|
||||
|
||||
CMD ["/bin/__BINARY__"]
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
FROM alpine:latest AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apk add rustup build-base
|
||||
# todo: arm64 target
|
||||
RUN rustup-init --default-host x86_64-unknown-linux-musl --default-toolchain stable --profile default -y
|
||||
|
||||
ENV PATH=/root/.cargo/bin:$PATH
|
||||
ENV RUSTFLAGS='-C link-arg=-s'
|
||||
|
||||
RUN cargo install cargo-chef --locked
|
||||
|
||||
# build dependencies first to cache
|
||||
FROM builder AS recipe-builder
|
||||
COPY . .
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM builder AS binary-builder
|
||||
COPY --from=recipe-builder /build/recipe.json recipe.json
|
||||
RUN cargo chef cook --release --recipe-path recipe.json --target x86_64-unknown-linux-musl
|
||||
|
||||
COPY Cargo.toml /build/
|
||||
COPY Cargo.lock /build/
|
||||
|
||||
# this needs to match workspaces in Cargo.toml
|
||||
COPY lib/libpk /build/lib/libpk
|
||||
COPY services/api/ /build/services/api
|
||||
|
||||
RUN cargo build --bin api --release --target x86_64-unknown-linux-musl
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=binary-builder /build/target/x86_64-unknown-linux-musl/release/api /api
|
||||
|
|
@ -100,15 +100,19 @@ public static class DiscordCacheExtensions
|
|||
await cache.SaveChannel(thread);
|
||||
}
|
||||
|
||||
public static async Task<PermissionSet> PermissionsIn(this IDiscordCache cache, ulong channelId)
|
||||
public static async Task<PermissionSet> BotPermissionsIn(this IDiscordCache cache, ulong guildId, ulong channelId)
|
||||
{
|
||||
var channel = await cache.GetRootChannel(channelId);
|
||||
// disable this for now
|
||||
//if (cache is HttpDiscordCache)
|
||||
// return await ((HttpDiscordCache)cache).BotChannelPermissions(guildId, channelId);
|
||||
|
||||
var channel = await cache.GetRootChannel(guildId, channelId);
|
||||
|
||||
if (channel.GuildId != null)
|
||||
{
|
||||
var userId = cache.GetOwnUser();
|
||||
var member = await cache.TryGetSelfMember(channel.GuildId.Value);
|
||||
return await cache.PermissionsFor(channelId, userId, member);
|
||||
return await cache.PermissionsFor2(guildId, channelId, userId, member);
|
||||
}
|
||||
|
||||
return PermissionSet.Dm;
|
||||
|
|
|
|||
188
Myriad/Cache/HTTPDiscordCache.cs
Normal file
188
Myriad/Cache/HTTPDiscordCache.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using Serilog;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
using Myriad.Serialization;
|
||||
using Myriad.Types;
|
||||
|
||||
namespace Myriad.Cache;
|
||||
|
||||
public class HttpDiscordCache: IDiscordCache
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpClient _client;
|
||||
private readonly Uri _cacheEndpoint;
|
||||
private readonly int _shardCount;
|
||||
private readonly ulong _ownUserId;
|
||||
|
||||
private readonly MemoryDiscordCache _innerCache;
|
||||
|
||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||
|
||||
public EventHandler<(bool?, string)> OnDebug;
|
||||
|
||||
public HttpDiscordCache(ILogger logger, HttpClient client, string cacheEndpoint, int shardCount, ulong ownUserId, bool useInnerCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_client = client;
|
||||
_cacheEndpoint = new Uri(cacheEndpoint);
|
||||
_shardCount = shardCount;
|
||||
_ownUserId = ownUserId;
|
||||
_jsonSerializerOptions = new JsonSerializerOptions().ConfigureForMyriad();
|
||||
if (useInnerCache) _innerCache = new MemoryDiscordCache(ownUserId);
|
||||
}
|
||||
|
||||
public ValueTask SaveGuild(Guild guild) => _innerCache?.SaveGuild(guild) ?? default;
|
||||
public ValueTask SaveChannel(Channel channel) => _innerCache?.SaveChannel(channel) ?? default;
|
||||
public ValueTask SaveUser(User user) => default;
|
||||
public ValueTask SaveSelfMember(ulong guildId, GuildMemberPartial member) => _innerCache?.SaveSelfMember(guildId, member) ?? default;
|
||||
public ValueTask SaveRole(ulong guildId, Myriad.Types.Role role) => _innerCache?.SaveRole(guildId, role) ?? default;
|
||||
public ValueTask SaveDmChannelStub(ulong channelId) => _innerCache?.SaveDmChannelStub(channelId) ?? default;
|
||||
public ValueTask RemoveGuild(ulong guildId) => _innerCache?.RemoveGuild(guildId) ?? default;
|
||||
public ValueTask RemoveChannel(ulong channelId) => _innerCache?.RemoveChannel(channelId) ?? default;
|
||||
public ValueTask RemoveUser(ulong userId) => _innerCache?.RemoveUser(userId) ?? default;
|
||||
public ValueTask RemoveRole(ulong guildId, ulong roleId) => _innerCache?.RemoveRole(guildId, roleId) ?? default;
|
||||
|
||||
public ulong GetOwnUser() => _ownUserId;
|
||||
|
||||
private async Task<T?> QueryCache<T>(string endpoint, ulong guildId)
|
||||
{
|
||||
var cluster = _cacheEndpoint.Authority;
|
||||
// todo: there should not be infra-specific code here
|
||||
if (cluster.Contains(".service.consul") || cluster.Contains("process.pluralkit-gateway.internal"))
|
||||
// int(((guild_id >> 22) % shard_count) / 16)
|
||||
cluster = $"cluster{(int)(((guildId >> 22) % (ulong)_shardCount) / 16)}.{cluster}";
|
||||
|
||||
var response = await _client.GetAsync($"{_cacheEndpoint.Scheme}://{cluster}{endpoint}");
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return default;
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.Found)
|
||||
throw new Exception($"failed to query http cache: {response.StatusCode}");
|
||||
|
||||
var plaintext = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(plaintext, _jsonSerializerOptions);
|
||||
}
|
||||
|
||||
public async Task<Guild?> TryGetGuild(ulong guildId)
|
||||
{
|
||||
var hres = await QueryCache<Guild?>($"/guilds/{guildId}", guildId);
|
||||
if (_innerCache == null) return hres;
|
||||
var lres = await _innerCache.TryGetGuild(guildId);
|
||||
|
||||
if (lres == null && hres == null) return null;
|
||||
if (lres == null)
|
||||
{
|
||||
_logger.Warning($"TryGetGuild({guildId}) was only successful on remote cache");
|
||||
OnDebug(null, (true, "guild"));
|
||||
return hres;
|
||||
}
|
||||
if (hres == null)
|
||||
{
|
||||
_logger.Warning($"TryGetGuild({guildId}) was only successful on local cache");
|
||||
OnDebug(null, (false, "guild"));
|
||||
return lres;
|
||||
}
|
||||
return hres;
|
||||
}
|
||||
|
||||
public async Task<Channel?> TryGetChannel(ulong guildId, ulong channelId)
|
||||
{
|
||||
var hres = await QueryCache<Channel?>($"/guilds/{guildId}/channels/{channelId}", guildId);
|
||||
if (_innerCache == null) return hres;
|
||||
var lres = await _innerCache.TryGetChannel(guildId, channelId);
|
||||
if (lres == null && hres == null) return null;
|
||||
if (lres == null)
|
||||
{
|
||||
_logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on remote cache");
|
||||
OnDebug(null, (true, "channel"));
|
||||
return hres;
|
||||
}
|
||||
if (hres == null)
|
||||
{
|
||||
_logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on local cache");
|
||||
OnDebug(null, (false, "channel"));
|
||||
return lres;
|
||||
}
|
||||
return hres;
|
||||
}
|
||||
|
||||
// this should be a GetUserCached method on nirn-proxy (it's always called as GetOrFetchUser)
|
||||
// so just return nothing
|
||||
public Task<User?> TryGetUser(ulong userId)
|
||||
=> Task.FromResult<User?>(null);
|
||||
|
||||
public async Task<GuildMemberPartial?> TryGetSelfMember(ulong guildId)
|
||||
{
|
||||
var hres = await QueryCache<GuildMemberPartial?>($"/guilds/{guildId}/members/@me", guildId);
|
||||
if (_innerCache == null) return hres;
|
||||
var lres = await _innerCache.TryGetSelfMember(guildId);
|
||||
if (lres == null && hres == null) return null;
|
||||
if (lres == null)
|
||||
{
|
||||
_logger.Warning($"TryGetSelfMember({guildId}) was only successful on remote cache");
|
||||
OnDebug(null, (true, "self_member"));
|
||||
return hres;
|
||||
}
|
||||
if (hres == null)
|
||||
{
|
||||
_logger.Warning($"TryGetSelfMember({guildId}) was only successful on local cache");
|
||||
OnDebug(null, (false, "self_member"));
|
||||
return lres;
|
||||
}
|
||||
return hres;
|
||||
}
|
||||
|
||||
// public async Task<PermissionSet> BotChannelPermissions(ulong guildId, ulong channelId)
|
||||
// {
|
||||
// // todo: local cache throws rather than returning null
|
||||
// // we need to throw too, and try/catch local cache here
|
||||
// var lres = await _innerCache.BotPermissionsIn(guildId, channelId);
|
||||
// var hres = await QueryCache<PermissionSet?>($"/guilds/{guildId}/channels/{channelId}/permissions/@me", guildId);
|
||||
// if (lres == null && hres == null) return null;
|
||||
// if (lres == null)
|
||||
// {
|
||||
// _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on remote cache");
|
||||
// OnDebug(null, (true, "botchannelperms"));
|
||||
// return hres;
|
||||
// }
|
||||
// if (hres == null)
|
||||
// {
|
||||
// _logger.Warning($"TryGetChannel({guildId}, {channelId}) was only successful on local cache");
|
||||
// OnDebug(null, (false, "botchannelperms"));
|
||||
// return lres;
|
||||
// }
|
||||
//
|
||||
// // this one is easy to check, so let's check it
|
||||
// if ((int)lres != (int)hres)
|
||||
// {
|
||||
// // trust local
|
||||
// _logger.Warning($"got different permissions for {channelId} (local {(int)lres}, remote {(int)hres})");
|
||||
// OnDebug(null, (null, "botchannelperms"));
|
||||
// return lres;
|
||||
// }
|
||||
// return hres;
|
||||
// }
|
||||
|
||||
public async Task<IEnumerable<Channel>> GetGuildChannels(ulong guildId)
|
||||
{
|
||||
var hres = await QueryCache<IEnumerable<Channel>>($"/guilds/{guildId}/channels", guildId);
|
||||
if (_innerCache == null) return hres;
|
||||
var lres = await _innerCache.GetGuildChannels(guildId);
|
||||
if (lres == null && hres == null) return null;
|
||||
if (lres == null)
|
||||
{
|
||||
_logger.Warning($"GetGuildChannels({guildId}) was only successful on remote cache");
|
||||
OnDebug(null, (true, "guild_channels"));
|
||||
return hres;
|
||||
}
|
||||
if (hres == null)
|
||||
{
|
||||
_logger.Warning($"GetGuildChannels({guildId}) was only successful on local cache");
|
||||
OnDebug(null, (false, "guild_channels"));
|
||||
return lres;
|
||||
}
|
||||
return hres;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,11 +18,9 @@ public interface IDiscordCache
|
|||
|
||||
internal ulong GetOwnUser();
|
||||
public Task<Guild?> TryGetGuild(ulong guildId);
|
||||
public Task<Channel?> TryGetChannel(ulong channelId);
|
||||
public Task<Channel?> TryGetChannel(ulong guildId, ulong channelId);
|
||||
public Task<User?> TryGetUser(ulong userId);
|
||||
public Task<GuildMemberPartial?> TryGetSelfMember(ulong guildId);
|
||||
public Task<Role?> TryGetRole(ulong roleId);
|
||||
|
||||
public IAsyncEnumerable<Guild> GetAllGuilds();
|
||||
public Task<IEnumerable<Channel>> GetGuildChannels(ulong guildId);
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ public class MemoryDiscordCache: IDiscordCache
|
|||
return Task.FromResult(cg?.Guild);
|
||||
}
|
||||
|
||||
public Task<Channel?> TryGetChannel(ulong channelId)
|
||||
public Task<Channel?> TryGetChannel(ulong _, ulong channelId)
|
||||
{
|
||||
_channels.TryGetValue(channelId, out var channel);
|
||||
return Task.FromResult(channel);
|
||||
|
|
@ -155,19 +155,6 @@ public class MemoryDiscordCache: IDiscordCache
|
|||
return Task.FromResult(guildMember);
|
||||
}
|
||||
|
||||
public Task<Role?> TryGetRole(ulong roleId)
|
||||
{
|
||||
_roles.TryGetValue(roleId, out var role);
|
||||
return Task.FromResult(role);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Guild> GetAllGuilds()
|
||||
{
|
||||
return _guilds.Values
|
||||
.Select(g => g.Guild)
|
||||
.ToAsyncEnumerable();
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Channel>> GetGuildChannels(ulong guildId)
|
||||
{
|
||||
if (!_guilds.TryGetValue(guildId, out var guild))
|
||||
|
|
|
|||
|
|
@ -1,340 +0,0 @@
|
|||
using Google.Protobuf;
|
||||
|
||||
using StackExchange.Redis;
|
||||
using StackExchange.Redis.KeyspaceIsolation;
|
||||
|
||||
using Serilog;
|
||||
|
||||
using Myriad.Types;
|
||||
|
||||
namespace Myriad.Cache;
|
||||
|
||||
#pragma warning disable 4014
|
||||
public class RedisDiscordCache: IDiscordCache
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly ulong _ownUserId;
|
||||
public RedisDiscordCache(ILogger logger, ulong ownUserId)
|
||||
{
|
||||
_logger = logger;
|
||||
_ownUserId = ownUserId;
|
||||
}
|
||||
|
||||
private ConnectionMultiplexer _redis { get; set; }
|
||||
|
||||
public async Task InitAsync(string addr)
|
||||
{
|
||||
_redis = await ConnectionMultiplexer.ConnectAsync(addr);
|
||||
}
|
||||
|
||||
private IDatabase db => _redis.GetDatabase().WithKeyPrefix("discord:");
|
||||
|
||||
public async ValueTask SaveGuild(Guild guild)
|
||||
{
|
||||
_logger.Verbose("Saving guild {GuildId} to redis", guild.Id);
|
||||
|
||||
var g = new CachedGuild();
|
||||
g.Id = guild.Id;
|
||||
g.Name = guild.Name;
|
||||
g.OwnerId = guild.OwnerId;
|
||||
g.PremiumTier = (int)guild.PremiumTier;
|
||||
|
||||
var tr = db.CreateTransaction();
|
||||
|
||||
tr.HashSetAsync("guilds", guild.Id.HashWrapper(g));
|
||||
|
||||
foreach (var role in guild.Roles)
|
||||
{
|
||||
// Don't call SaveRole because that updates guild state
|
||||
// and we just got a brand new one :)
|
||||
// actually with redis it doesn't update guild state, but we're still doing it here because transaction
|
||||
tr.HashSetAsync("roles", role.Id.HashWrapper(new CachedRole()
|
||||
{
|
||||
Id = role.Id,
|
||||
Name = role.Name,
|
||||
Position = role.Position,
|
||||
Permissions = (ulong)role.Permissions,
|
||||
Mentionable = role.Mentionable,
|
||||
}));
|
||||
|
||||
tr.HashSetAsync($"guild_roles:{guild.Id}", role.Id, true, When.NotExists);
|
||||
}
|
||||
|
||||
await tr.ExecuteAsync();
|
||||
}
|
||||
|
||||
public async ValueTask SaveChannel(Channel channel)
|
||||
{
|
||||
_logger.Verbose("Saving channel {ChannelId} to redis", channel.Id);
|
||||
|
||||
await db.HashSetAsync("channels", channel.Id.HashWrapper(channel.ToProtobuf()));
|
||||
|
||||
if (channel.GuildId != null)
|
||||
await db.HashSetAsync($"guild_channels:{channel.GuildId.Value}", channel.Id, true, When.NotExists);
|
||||
|
||||
// todo: use a transaction for this?
|
||||
if (channel.Recipients != null)
|
||||
foreach (var recipient in channel.Recipients)
|
||||
await SaveUser(recipient);
|
||||
}
|
||||
|
||||
public async ValueTask SaveUser(User user)
|
||||
{
|
||||
_logger.Verbose("Saving user {UserId} to redis", user.Id);
|
||||
|
||||
var u = new CachedUser()
|
||||
{
|
||||
Id = user.Id,
|
||||
Username = user.Username,
|
||||
Discriminator = user.Discriminator,
|
||||
Bot = user.Bot,
|
||||
};
|
||||
|
||||
if (user.Avatar != null)
|
||||
u.Avatar = user.Avatar;
|
||||
|
||||
await db.HashSetAsync("users", user.Id.HashWrapper(u));
|
||||
}
|
||||
|
||||
public async ValueTask SaveSelfMember(ulong guildId, GuildMemberPartial member)
|
||||
{
|
||||
_logger.Verbose("Saving self member for guild {GuildId} to redis", guildId);
|
||||
|
||||
var gm = new CachedGuildMember();
|
||||
foreach (var role in member.Roles)
|
||||
gm.Roles.Add(role);
|
||||
|
||||
await db.HashSetAsync("members", guildId.HashWrapper(gm));
|
||||
}
|
||||
|
||||
public async ValueTask SaveRole(ulong guildId, Myriad.Types.Role role)
|
||||
{
|
||||
_logger.Verbose("Saving role {RoleId} in {GuildId} to redis", role.Id, guildId);
|
||||
|
||||
await db.HashSetAsync("roles", role.Id.HashWrapper(new CachedRole()
|
||||
{
|
||||
Id = role.Id,
|
||||
Mentionable = role.Mentionable,
|
||||
Name = role.Name,
|
||||
Permissions = (ulong)role.Permissions,
|
||||
Position = role.Position,
|
||||
}));
|
||||
|
||||
await db.HashSetAsync($"guild_roles:{guildId}", role.Id, true, When.NotExists);
|
||||
}
|
||||
|
||||
public async ValueTask SaveDmChannelStub(ulong channelId)
|
||||
{
|
||||
// Use existing channel object if present, otherwise add a stub
|
||||
// We may get a message create before channel create and we want to have it saved
|
||||
|
||||
if (await TryGetChannel(channelId) == null)
|
||||
await db.HashSetAsync("channels", channelId.HashWrapper(new CachedChannel()
|
||||
{
|
||||
Id = channelId,
|
||||
Type = (int)Channel.ChannelType.Dm,
|
||||
}));
|
||||
}
|
||||
|
||||
public async ValueTask RemoveGuild(ulong guildId)
|
||||
=> await db.HashDeleteAsync("guilds", guildId);
|
||||
|
||||
public async ValueTask RemoveChannel(ulong channelId)
|
||||
{
|
||||
var oldChannel = await TryGetChannel(channelId);
|
||||
|
||||
if (oldChannel == null)
|
||||
return;
|
||||
|
||||
await db.HashDeleteAsync("channels", channelId);
|
||||
|
||||
if (oldChannel.GuildId != null)
|
||||
await db.HashDeleteAsync($"guild_channels:{oldChannel.GuildId.Value}", oldChannel.Id);
|
||||
}
|
||||
|
||||
public async ValueTask RemoveUser(ulong userId)
|
||||
=> await db.HashDeleteAsync("users", userId);
|
||||
|
||||
public ulong GetOwnUser() => _ownUserId;
|
||||
|
||||
public async ValueTask RemoveRole(ulong guildId, ulong roleId)
|
||||
{
|
||||
await db.HashDeleteAsync("roles", roleId);
|
||||
await db.HashDeleteAsync($"guild_roles:{guildId}", roleId);
|
||||
}
|
||||
|
||||
public async Task<Guild?> TryGetGuild(ulong guildId)
|
||||
{
|
||||
var redisGuild = await db.HashGetAsync("guilds", guildId);
|
||||
if (redisGuild.IsNullOrEmpty)
|
||||
return null;
|
||||
|
||||
var guild = ((byte[])redisGuild).Unmarshal<CachedGuild>();
|
||||
|
||||
var redisRoles = await db.HashGetAllAsync($"guild_roles:{guildId}");
|
||||
|
||||
// todo: put this in a transaction or something
|
||||
var roles = await Task.WhenAll(redisRoles.Select(r => TryGetRole((ulong)r.Name)));
|
||||
|
||||
#pragma warning disable 8619
|
||||
return guild.FromProtobuf() with { Roles = roles };
|
||||
#pragma warning restore 8619
|
||||
}
|
||||
|
||||
public async Task<Channel?> TryGetChannel(ulong channelId)
|
||||
{
|
||||
var redisChannel = await db.HashGetAsync("channels", channelId);
|
||||
if (redisChannel.IsNullOrEmpty)
|
||||
return null;
|
||||
|
||||
return ((byte[])redisChannel).Unmarshal<CachedChannel>().FromProtobuf();
|
||||
}
|
||||
|
||||
public async Task<User?> TryGetUser(ulong userId)
|
||||
{
|
||||
var redisUser = await db.HashGetAsync("users", userId);
|
||||
if (redisUser.IsNullOrEmpty)
|
||||
return null;
|
||||
|
||||
return ((byte[])redisUser).Unmarshal<CachedUser>().FromProtobuf();
|
||||
}
|
||||
|
||||
public async Task<GuildMemberPartial?> TryGetSelfMember(ulong guildId)
|
||||
{
|
||||
var redisMember = await db.HashGetAsync("members", guildId);
|
||||
if (redisMember.IsNullOrEmpty)
|
||||
return null;
|
||||
|
||||
return new GuildMemberPartial()
|
||||
{
|
||||
Roles = ((byte[])redisMember).Unmarshal<CachedGuildMember>().Roles.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Myriad.Types.Role?> TryGetRole(ulong roleId)
|
||||
{
|
||||
var redisRole = await db.HashGetAsync("roles", roleId);
|
||||
if (redisRole.IsNullOrEmpty)
|
||||
return null;
|
||||
|
||||
var role = ((byte[])redisRole).Unmarshal<CachedRole>();
|
||||
|
||||
return new Myriad.Types.Role()
|
||||
{
|
||||
Id = role.Id,
|
||||
Name = role.Name,
|
||||
Position = role.Position,
|
||||
Permissions = (PermissionSet)role.Permissions,
|
||||
Mentionable = role.Mentionable,
|
||||
};
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<Guild> GetAllGuilds()
|
||||
{
|
||||
// return _guilds.Values
|
||||
// .Select(g => g.Guild)
|
||||
// .ToAsyncEnumerable();
|
||||
return new Guild[] { }.ToAsyncEnumerable();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Channel>> GetGuildChannels(ulong guildId)
|
||||
{
|
||||
var redisChannels = await db.HashGetAllAsync($"guild_channels:{guildId}");
|
||||
if (redisChannels.Length == 0)
|
||||
throw new ArgumentException("Guild not found", nameof(guildId));
|
||||
|
||||
#pragma warning disable 8619
|
||||
return await Task.WhenAll(redisChannels.Select(c => TryGetChannel((ulong)c.Name)));
|
||||
#pragma warning restore 8619
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CacheProtoExt
|
||||
{
|
||||
public static Guild FromProtobuf(this CachedGuild guild)
|
||||
=> new Guild()
|
||||
{
|
||||
Id = guild.Id,
|
||||
Name = guild.Name,
|
||||
OwnerId = guild.OwnerId,
|
||||
PremiumTier = (PremiumTier)guild.PremiumTier,
|
||||
};
|
||||
|
||||
public static CachedChannel ToProtobuf(this Channel channel)
|
||||
{
|
||||
var c = new CachedChannel();
|
||||
c.Id = channel.Id;
|
||||
c.Type = (int)channel.Type;
|
||||
if (channel.Position != null)
|
||||
c.Position = channel.Position.Value;
|
||||
c.Name = channel.Name;
|
||||
if (channel.PermissionOverwrites != null)
|
||||
foreach (var overwrite in channel.PermissionOverwrites)
|
||||
c.PermissionOverwrites.Add(new Overwrite()
|
||||
{
|
||||
Id = overwrite.Id,
|
||||
Type = (int)overwrite.Type,
|
||||
Allow = (ulong)overwrite.Allow,
|
||||
Deny = (ulong)overwrite.Deny,
|
||||
});
|
||||
if (channel.GuildId != null)
|
||||
c.GuildId = channel.GuildId.Value;
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
public static Channel FromProtobuf(this CachedChannel channel)
|
||||
=> new Channel()
|
||||
{
|
||||
Id = channel.Id,
|
||||
Type = (Channel.ChannelType)channel.Type,
|
||||
Position = channel.Position,
|
||||
Name = channel.Name,
|
||||
PermissionOverwrites = channel.PermissionOverwrites
|
||||
.Select(x => new Channel.Overwrite()
|
||||
{
|
||||
Id = x.Id,
|
||||
Type = (Channel.OverwriteType)x.Type,
|
||||
Allow = (PermissionSet)x.Allow,
|
||||
Deny = (PermissionSet)x.Deny,
|
||||
}).ToArray(),
|
||||
GuildId = channel.HasGuildId ? channel.GuildId : null,
|
||||
ParentId = channel.HasParentId ? channel.ParentId : null,
|
||||
};
|
||||
|
||||
public static User FromProtobuf(this CachedUser user)
|
||||
=> new User()
|
||||
{
|
||||
Id = user.Id,
|
||||
Username = user.Username,
|
||||
Discriminator = user.Discriminator,
|
||||
Avatar = user.HasAvatar ? user.Avatar : null,
|
||||
Bot = user.Bot,
|
||||
};
|
||||
}
|
||||
|
||||
internal static class RedisExt
|
||||
{
|
||||
// convenience method
|
||||
public static HashEntry[] HashWrapper<T>(this ulong key, T value) where T : IMessage
|
||||
=> new[] { new HashEntry(key, value.ToByteArray()) };
|
||||
}
|
||||
|
||||
public static class ProtobufExt
|
||||
{
|
||||
private static Dictionary<string, MessageParser> _parser = new();
|
||||
|
||||
public static byte[] Marshal(this IMessage message) => message.ToByteArray();
|
||||
|
||||
public static T Unmarshal<T>(this byte[] message) where T : IMessage<T>, new()
|
||||
{
|
||||
var type = typeof(T).ToString();
|
||||
if (_parser.ContainsKey(type))
|
||||
return (T)_parser[type].ParseFrom(message);
|
||||
else
|
||||
{
|
||||
_parser.Add(type, new MessageParser<T>(() => new T()));
|
||||
return Unmarshal<T>(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,27 +13,13 @@ public static class CacheExtensions
|
|||
return guild;
|
||||
}
|
||||
|
||||
public static async Task<Channel> GetChannel(this IDiscordCache cache, ulong channelId)
|
||||
public static async Task<Channel> GetChannel(this IDiscordCache cache, ulong guildId, ulong channelId)
|
||||
{
|
||||
if (!(await cache.TryGetChannel(channelId) is Channel channel))
|
||||
if (!(await cache.TryGetChannel(guildId, channelId) is Channel channel))
|
||||
throw new KeyNotFoundException($"Channel {channelId} not found in cache");
|
||||
return channel;
|
||||
}
|
||||
|
||||
public static async Task<User> GetUser(this IDiscordCache cache, ulong userId)
|
||||
{
|
||||
if (!(await cache.TryGetUser(userId) is User user))
|
||||
throw new KeyNotFoundException($"User {userId} not found in cache");
|
||||
return user;
|
||||
}
|
||||
|
||||
public static async Task<Role> GetRole(this IDiscordCache cache, ulong roleId)
|
||||
{
|
||||
if (!(await cache.TryGetRole(roleId) is Role role))
|
||||
throw new KeyNotFoundException($"Role {roleId} not found in cache");
|
||||
return role;
|
||||
}
|
||||
|
||||
public static async ValueTask<User?> GetOrFetchUser(this IDiscordCache cache, DiscordApiClient rest,
|
||||
ulong userId)
|
||||
{
|
||||
|
|
@ -47,9 +33,9 @@ public static class CacheExtensions
|
|||
}
|
||||
|
||||
public static async ValueTask<Channel?> GetOrFetchChannel(this IDiscordCache cache, DiscordApiClient rest,
|
||||
ulong channelId)
|
||||
ulong guildId, ulong channelId)
|
||||
{
|
||||
if (await cache.TryGetChannel(channelId) is { } cacheChannel)
|
||||
if (await cache.TryGetChannel(guildId, channelId) is { } cacheChannel)
|
||||
return cacheChannel;
|
||||
|
||||
var restChannel = await rest.GetChannel(channelId);
|
||||
|
|
@ -58,13 +44,14 @@ public static class CacheExtensions
|
|||
return restChannel;
|
||||
}
|
||||
|
||||
public static async Task<Channel> GetRootChannel(this IDiscordCache cache, ulong channelOrThread)
|
||||
public static async Task<Channel> GetRootChannel(this IDiscordCache cache, ulong guildId, ulong channelOrThread)
|
||||
{
|
||||
var channel = await cache.GetChannel(channelOrThread);
|
||||
var channel = await cache.GetChannel(guildId, channelOrThread);
|
||||
if (!channel.IsThread())
|
||||
return channel;
|
||||
|
||||
var parent = await cache.GetChannel(channel.ParentId!.Value);
|
||||
var parent = await cache.TryGetChannel(guildId, channel.ParentId!.Value);
|
||||
if (parent == null) throw new Exception($"failed to find parent channel for thread {channelOrThread} in cache");
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,31 +31,27 @@ public static class PermissionExtensions
|
|||
PermissionSet.AttachFiles |
|
||||
PermissionSet.EmbedLinks;
|
||||
|
||||
public static Task<PermissionSet> PermissionsFor(this IDiscordCache cache, MessageCreateEvent message) =>
|
||||
PermissionsFor(cache, message.ChannelId, message.Author.Id, message.Member, message.WebhookId != null);
|
||||
public static Task<PermissionSet> PermissionsForMCE(this IDiscordCache cache, MessageCreateEvent message) =>
|
||||
PermissionsFor2(cache, message.GuildId ?? 0, message.ChannelId, message.Author.Id, message.Member, message.WebhookId != null);
|
||||
|
||||
public static Task<PermissionSet>
|
||||
PermissionsFor(this IDiscordCache cache, ulong channelId, GuildMember member) =>
|
||||
PermissionsFor(cache, channelId, member.User.Id, member);
|
||||
PermissionsForMemberInChannel(this IDiscordCache cache, ulong guildId, ulong channelId, GuildMember member) =>
|
||||
PermissionsFor2(cache, guildId, channelId, member.User.Id, member);
|
||||
|
||||
public static async Task<PermissionSet> PermissionsFor(this IDiscordCache cache, ulong channelId, ulong userId,
|
||||
GuildMemberPartial? member, bool isWebhook = false,
|
||||
bool isThread = false)
|
||||
public static async Task<PermissionSet> PermissionsFor2(this IDiscordCache cache, ulong guildId, ulong channelId, ulong userId,
|
||||
GuildMemberPartial? member, bool isThread = false)
|
||||
{
|
||||
if (!(await cache.TryGetChannel(channelId) is Channel channel))
|
||||
if (!(await cache.TryGetChannel(guildId, channelId) is Channel channel))
|
||||
// todo: handle channel not found better
|
||||
return PermissionSet.Dm;
|
||||
|
||||
if (channel.GuildId == null)
|
||||
return PermissionSet.Dm;
|
||||
|
||||
var rootChannel = await cache.GetRootChannel(channelId);
|
||||
var rootChannel = await cache.GetRootChannel(guildId, channelId);
|
||||
|
||||
var guild = await cache.GetGuild(channel.GuildId.Value);
|
||||
|
||||
if (isWebhook)
|
||||
return EveryonePermissions(guild);
|
||||
|
||||
return PermissionsFor(guild, rootChannel, userId, member, isThread: isThread);
|
||||
}
|
||||
|
||||
|
|
@ -79,9 +75,6 @@ public static class PermissionExtensions
|
|||
return perms;
|
||||
}
|
||||
|
||||
public static PermissionSet PermissionsFor(Guild guild, Channel channel, MessageCreateEvent msg, bool isThread = false) =>
|
||||
PermissionsFor(guild, channel, msg.Author.Id, msg.Member, isThread: isThread);
|
||||
|
||||
public static PermissionSet PermissionsFor(Guild guild, Channel channel, ulong userId,
|
||||
GuildMemberPartial? member, bool isThread = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ public class ShardConnection: IAsyncDisposable
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Shard {ShardId}: Error reading from WebSocket");
|
||||
// these are never useful
|
||||
// _logger.Error(e, "Shard {ShardId}: Error reading from WebSocket");
|
||||
// force close so we can "reset"
|
||||
await CloseInner(WebSocketCloseStatus.NormalClosure, null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ public class DiscordApiClient
|
|||
public Task<Channel> CreateDm(ulong recipientId) =>
|
||||
_client.Post<Channel>("/users/@me/channels", ("CreateDM", default), new CreateDmRequest(recipientId))!;
|
||||
|
||||
public Task<RefreshedUrlsResponse> RefreshUrls(string[] urls) =>
|
||||
_client.Post<RefreshedUrlsResponse>("/attachments/refresh-urls", ("RefreshUrls", default), new RefreshUrlsRequest(urls));
|
||||
|
||||
private static string EncodeEmoji(Emoji emoji) =>
|
||||
WebUtility.UrlEncode(emoji.Id != null ? $"{emoji.Name}:{emoji.Id}" : emoji.Name) ??
|
||||
throw new ArgumentException("Could not encode emoji");
|
||||
|
|
|
|||
|
|
@ -13,4 +13,14 @@ public record ExecuteWebhookRequest
|
|||
public AllowedMentions? AllowedMentions { get; init; }
|
||||
public bool? Tts { get; init; }
|
||||
public Message.MessageFlags? Flags { get; set; }
|
||||
public WebhookPoll? Poll { get; set; }
|
||||
|
||||
public record WebhookPoll
|
||||
{
|
||||
public Message.PollMedia Question { get; init; }
|
||||
public Message.PollAnswer[] Answers { get; init; }
|
||||
public int? Duration { get; init; }
|
||||
public bool AllowMultiselect { get; init; }
|
||||
public int LayoutType { get; init; }
|
||||
}
|
||||
}
|
||||
3
Myriad/Rest/Types/Requests/RefreshUrlsRequest.cs
Normal file
3
Myriad/Rest/Types/Requests/RefreshUrlsRequest.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
namespace Myriad.Rest.Types.Requests;
|
||||
|
||||
public record RefreshUrlsRequest(string[] AttachmentUrls);
|
||||
|
|
@ -15,4 +15,7 @@ public record WebhookMessageEditRequest
|
|||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public Optional<Embed[]?> Embeds { get; init; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public Optional<Message.Attachment[]?> Attachments { get; init; }
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ namespace Myriad.Types;
|
|||
public record Activity
|
||||
{
|
||||
public string Name { get; init; }
|
||||
public string State { get; init; }
|
||||
public ActivityType Type { get; init; }
|
||||
public string? Url { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ public record Message
|
|||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public Optional<Message?> ReferencedMessage { get; init; }
|
||||
|
||||
public MessagePoll? Poll { get; init; }
|
||||
|
||||
// public MessageComponent[]? Components { get; init; }
|
||||
|
||||
public record Reference(ulong? GuildId, ulong? ChannelId, ulong? MessageId);
|
||||
|
|
@ -96,4 +98,17 @@ public record Message
|
|||
public bool Me { get; init; }
|
||||
public Emoji Emoji { get; init; }
|
||||
}
|
||||
|
||||
public record PollMedia(string? Text, Emoji? Emoji);
|
||||
|
||||
public record PollAnswer(PollMedia PollMedia);
|
||||
|
||||
public record MessagePoll
|
||||
{
|
||||
public PollMedia Question { get; init; }
|
||||
public PollAnswer[] Answers { get; init; }
|
||||
public string? Expiry { get; init; }
|
||||
public bool AllowMultiselect { get; init; }
|
||||
public int LayoutType { get; init; }
|
||||
}
|
||||
}
|
||||
11
Myriad/Types/RefreshedUrl.cs
Normal file
11
Myriad/Types/RefreshedUrl.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace Myriad.Types;
|
||||
|
||||
public record RefreshedUrlsResponse
|
||||
{
|
||||
public record RefreshedUrl
|
||||
{
|
||||
public string Original;
|
||||
public string Refreshed;
|
||||
}
|
||||
public RefreshedUrl[] RefreshedUrls;
|
||||
}
|
||||
|
|
@ -5,4 +5,5 @@ public class ApiConfig
|
|||
public int Port { get; set; } = 5000;
|
||||
public string? ClientId { get; set; }
|
||||
public string? ClientSecret { get; set; }
|
||||
public bool TrustAuth { get; set; } = false;
|
||||
}
|
||||
|
|
@ -13,19 +13,13 @@ public class AuthorizationTokenHandlerMiddleware
|
|||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext ctx, IDatabase db)
|
||||
public async Task Invoke(HttpContext ctx, IDatabase db, ApiConfig cfg)
|
||||
{
|
||||
ctx.Request.Headers.TryGetValue("authorization", out var authHeaders);
|
||||
if (authHeaders.Count > 0)
|
||||
{
|
||||
var systemId = await db.Execute(conn => conn.QuerySingleOrDefaultAsync<SystemId?>(
|
||||
"select id from systems where token = @token",
|
||||
new { token = authHeaders[0] }
|
||||
));
|
||||
|
||||
if (systemId != null)
|
||||
ctx.Items.Add("SystemId", systemId);
|
||||
}
|
||||
if (cfg.TrustAuth
|
||||
&& ctx.Request.Headers.TryGetValue("X-PluralKit-SystemId", out var sidHeaders)
|
||||
&& sidHeaders.Count > 0
|
||||
&& int.TryParse(sidHeaders[0], out var systemId))
|
||||
ctx.Items.Add("SystemId", new SystemId(systemId));
|
||||
|
||||
await _next.Invoke(ctx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ namespace PluralKit.API;
|
|||
public class PKControllerBase: ControllerBase
|
||||
{
|
||||
private readonly Guid _requestId = Guid.NewGuid();
|
||||
private readonly Regex _shortIdRegex = new("^[a-z]{5}$");
|
||||
private readonly Regex _snowflakeRegex = new("^[0-9]{17,19}$");
|
||||
|
||||
private List<PKMember>? _memberLookupCache { get; set; }
|
||||
|
|
@ -46,8 +45,8 @@ public class PKControllerBase: ControllerBase
|
|||
if (_snowflakeRegex.IsMatch(systemRef))
|
||||
return _repo.GetSystemByAccount(ulong.Parse(systemRef));
|
||||
|
||||
if (_shortIdRegex.IsMatch(systemRef))
|
||||
return _repo.GetSystemByHid(systemRef);
|
||||
if (systemRef.TryParseHid(out var hid))
|
||||
return _repo.GetSystemByHid(hid);
|
||||
|
||||
return Task.FromResult<PKSystem?>(null);
|
||||
}
|
||||
|
|
@ -71,8 +70,8 @@ public class PKControllerBase: ControllerBase
|
|||
if (Guid.TryParse(memberRef, out var guid))
|
||||
return await _repo.GetMemberByGuid(guid);
|
||||
|
||||
if (_shortIdRegex.IsMatch(memberRef))
|
||||
return await _repo.GetMemberByHid(memberRef);
|
||||
if (memberRef.TryParseHid(out var hid))
|
||||
return await _repo.GetMemberByHid(hid);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -96,8 +95,8 @@ public class PKControllerBase: ControllerBase
|
|||
if (Guid.TryParse(groupRef, out var guid))
|
||||
return await _repo.GetGroupByGuid(guid);
|
||||
|
||||
if (_shortIdRegex.IsMatch(groupRef))
|
||||
return await _repo.GetGroupByHid(groupRef);
|
||||
if (groupRef.TryParseHid(out var hid))
|
||||
return await _repo.GetGroupByHid(hid);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public class AutoproxyControllerV2: PKControllerBase
|
|||
|
||||
private async Task<IActionResult> Patch(PKSystem system, ulong? guildId, ulong? channelId, JObject data, AutoproxySettings oldData)
|
||||
{
|
||||
var updateMember = data.ContainsKey("autoproxy_member");
|
||||
var updateMember = data.ContainsKey("autoproxy_member") && data.Value<string>("autoproxy_member") != null;
|
||||
|
||||
PKMember? member = null;
|
||||
if (updateMember)
|
||||
|
|
@ -64,15 +64,37 @@ public class AutoproxyControllerV2: PKControllerBase
|
|||
}
|
||||
|
||||
var patch = AutoproxyPatch.FromJson(data, member?.Id);
|
||||
|
||||
patch.AssertIsValid();
|
||||
|
||||
var newAutoproxyMode = patch.AutoproxyMode.IsPresent ? patch.AutoproxyMode : oldData.AutoproxyMode;
|
||||
var newAutoproxyMember = patch.AutoproxyMember.IsPresent ? patch.AutoproxyMember : oldData.AutoproxyMember;
|
||||
|
||||
if (updateMember && member == null)
|
||||
{
|
||||
patch.Errors.Add(new("autoproxy_member", "Member not found."));
|
||||
if (updateMember && !(
|
||||
(patch.AutoproxyMode.IsPresent && patch.AutoproxyMode.Value == AutoproxyMode.Member)
|
||||
|| (!patch.AutoproxyMode.IsPresent && oldData.AutoproxyMode == AutoproxyMode.Member))
|
||||
)
|
||||
patch.Errors.Add(new("autoproxy_member", "Cannot update autoproxy member if autoproxy mode is set to latch"));
|
||||
}
|
||||
|
||||
if (newAutoproxyMode.Value == AutoproxyMode.Member)
|
||||
{
|
||||
if (!updateMember)
|
||||
{
|
||||
patch.Errors.Add(new("autoproxy_member", "An autoproxy member must be supplied for autoproxy mode 'member'"));
|
||||
}
|
||||
|
||||
patch.AutoproxyMode = newAutoproxyMode;
|
||||
patch.AutoproxyMember = newAutoproxyMember;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (updateMember)
|
||||
{
|
||||
patch.Errors.Add(new("autoproxy_member", "Cannot update autoproxy member if autoproxy mode is not set to 'member'"));
|
||||
}
|
||||
|
||||
patch.AutoproxyMode = newAutoproxyMode;
|
||||
patch.AutoproxyMember = null;
|
||||
}
|
||||
|
||||
if (patch.Errors.Count > 0)
|
||||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ public class GroupControllerV2: PKControllerBase
|
|||
|
||||
var ctx = ContextFor(system);
|
||||
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (!system.GroupListPrivacy.CanAccess(ContextFor(system)))
|
||||
throw Errors.UnauthorizedGroupList;
|
||||
|
||||
|
|
@ -31,20 +28,17 @@ public class GroupControllerV2: PKControllerBase
|
|||
|
||||
var j_groups = await groups
|
||||
.Where(g => g.Visibility.CanAccess(ctx))
|
||||
.Select(g => g.ToJson(ctx, needsMembersArray: with_members))
|
||||
.Select(g => g.ToJson(ctx, needsMembersArray: with_members, systemStr: system.Hid))
|
||||
.ToListAsync();
|
||||
|
||||
if (with_members && !system.MemberListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedMemberList;
|
||||
|
||||
if (with_members && j_groups.Count > 0)
|
||||
{
|
||||
var q = await _repo.GetGroupMemberInfo(await groups
|
||||
.Where(g => g.Visibility.CanAccess(ctx))
|
||||
.Where(g => g.ListPrivacy.CanAccess(ctx))
|
||||
.Select(x => x.Id)
|
||||
.ToListAsync());
|
||||
|
||||
|
||||
foreach (var row in q)
|
||||
if (row.MemberVisibility.CanAccess(ctx))
|
||||
((JArray)j_groups.Find(x => x.Value<string>("id") == row.Group)["members"]).Add(row.MemberUuid);
|
||||
|
|
@ -86,7 +80,7 @@ public class GroupControllerV2: PKControllerBase
|
|||
|
||||
await tx.CommitAsync();
|
||||
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner));
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner, system.Hid));
|
||||
}
|
||||
|
||||
[HttpGet("groups/{groupRef}")]
|
||||
|
|
@ -133,7 +127,7 @@ public class GroupControllerV2: PKControllerBase
|
|||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
var newGroup = await _repo.UpdateGroup(group.Id, patch);
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner));
|
||||
return Ok(newGroup.ToJson(LookupContext.ByOwner, system.Hid));
|
||||
}
|
||||
|
||||
[HttpDelete("groups/{groupRef}")]
|
||||
|
|
|
|||
|
|
@ -19,17 +19,19 @@ public class GroupMemberControllerV2: PKControllerBase
|
|||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
|
||||
|
||||
var ctx = ContextFor(group);
|
||||
|
||||
if (!group.ListPrivacy.CanAccess(ctx))
|
||||
throw Errors.UnauthorizedGroupMemberList;
|
||||
|
||||
var system = await _repo.GetSystem(group.System);
|
||||
var members = _repo.GetGroupMembers(group.Id).Where(m => m.MemberVisibility.CanAccess(ctx));
|
||||
|
||||
var o = new JArray();
|
||||
|
||||
await foreach (var member in members)
|
||||
o.Add(member.ToJson(ctx));
|
||||
o.Add(member.ToJson(ctx, systemStr: system.Hid));
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
|
|
@ -147,6 +149,8 @@ public class GroupMemberControllerV2: PKControllerBase
|
|||
public async Task<IActionResult> GetMemberGroups(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFoundWithRef(memberRef);
|
||||
var ctx = ContextFor(member);
|
||||
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
|
@ -158,7 +162,7 @@ public class GroupMemberControllerV2: PKControllerBase
|
|||
var o = new JArray();
|
||||
|
||||
await foreach (var group in groups)
|
||||
o.Add(group.ToJson(ctx));
|
||||
o.Add(group.ToJson(ctx, system.Hid));
|
||||
|
||||
return Ok(o);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class MemberControllerV2: PKControllerBase
|
|||
var members = _repo.GetSystemMembers(system.Id);
|
||||
return Ok(await members
|
||||
.Where(m => m.MemberVisibility.CanAccess(ctx))
|
||||
.Select(m => m.ToJson(ctx))
|
||||
.Select(m => m.ToJson(ctx, systemStr: system.Hid))
|
||||
.ToListAsync());
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ public class MemberControllerV2: PKControllerBase
|
|||
|
||||
await tx.CommitAsync();
|
||||
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner));
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, systemStr: system.Hid));
|
||||
}
|
||||
|
||||
[HttpGet("members/{memberRef}")]
|
||||
|
|
@ -111,7 +111,7 @@ public class MemberControllerV2: PKControllerBase
|
|||
throw new ModelParseError(patch.Errors);
|
||||
|
||||
var newMember = await _repo.UpdateMember(member.Id, patch);
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner));
|
||||
return Ok(newMember.ToJson(LookupContext.ByOwner, systemStr: system.Hid));
|
||||
}
|
||||
|
||||
[HttpDelete("members/{memberRef}")]
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public class SwitchControllerV2: PKControllerBase
|
|||
|
||||
var res = await _db.Execute(conn => conn.QueryAsync<SwitchesReturnNew>(
|
||||
@"select *, array(
|
||||
select members.hid from switch_members, members
|
||||
select trim(members.hid) from switch_members, members
|
||||
where switch_members.switch = switches.id and members.id = switch_members.member
|
||||
) as members from switches
|
||||
where switches.system = @System and switches.timestamp < @Before
|
||||
|
|
@ -70,7 +70,7 @@ public class SwitchControllerV2: PKControllerBase
|
|||
return Ok(new FrontersReturnNew
|
||||
{
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx)).ToListAsync(),
|
||||
Members = await members.Select(m => m.ToJson(ctx, systemStr: system.Hid)).ToListAsync(),
|
||||
Uuid = sw.Uuid,
|
||||
});
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ public class SwitchControllerV2: PKControllerBase
|
|||
{
|
||||
Uuid = newSwitch.Uuid,
|
||||
Timestamp = data.Timestamp != null ? data.Timestamp.Value : newSwitch.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner)),
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, systemStr: system.Hid)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +153,7 @@ public class SwitchControllerV2: PKControllerBase
|
|||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = await members.Select(m => m.ToJson(ctx)).ToListAsync()
|
||||
Members = await members.Select(m => m.ToJson(ctx, systemStr: system.Hid)).ToListAsync()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ public class SwitchControllerV2: PKControllerBase
|
|||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner))
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, systemStr: system.Hid))
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ public class SwitchControllerV2: PKControllerBase
|
|||
{
|
||||
Uuid = sw.Uuid,
|
||||
Timestamp = sw.Timestamp,
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner))
|
||||
Members = members.Select(x => x.ToJson(LookupContext.ByOwner, systemStr: system.Hid))
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -368,8 +368,8 @@
|
|||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.10",
|
||||
"contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -398,8 +398,8 @@
|
|||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -416,23 +416,6 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.Registry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
|
||||
"dependencies": {
|
||||
"System.Security.AccessControl": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.SystemEvents": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.6.1",
|
||||
|
|
@ -516,8 +499,8 @@
|
|||
},
|
||||
"Npgsql": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.5",
|
||||
"contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==",
|
||||
"resolved": "4.1.13",
|
||||
"contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.6.0"
|
||||
}
|
||||
|
|
@ -533,10 +516,10 @@
|
|||
},
|
||||
"Pipelines.Sockets.Unofficial": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==",
|
||||
"resolved": "2.2.8",
|
||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
||||
"dependencies": {
|
||||
"System.IO.Pipelines": "5.0.0"
|
||||
"System.IO.Pipelines": "5.0.1"
|
||||
}
|
||||
},
|
||||
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
|
||||
|
|
@ -797,11 +780,11 @@
|
|||
},
|
||||
"StackExchange.Redis": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.88",
|
||||
"contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==",
|
||||
"resolved": "2.8.16",
|
||||
"contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==",
|
||||
"dependencies": {
|
||||
"Pipelines.Sockets.Unofficial": "2.2.0",
|
||||
"System.Diagnostics.PerformanceCounter": "5.0.0"
|
||||
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
|
||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
||||
}
|
||||
},
|
||||
"System.AppContext": {
|
||||
|
|
@ -844,15 +827,6 @@
|
|||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Configuration.ConfigurationManager": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==",
|
||||
"dependencies": {
|
||||
"System.Security.Cryptography.ProtectedData": "5.0.0",
|
||||
"System.Security.Permissions": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -880,17 +854,6 @@
|
|||
"resolved": "4.7.1",
|
||||
"contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
|
||||
},
|
||||
"System.Diagnostics.PerformanceCounter": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0",
|
||||
"Microsoft.Win32.Registry": "5.0.0",
|
||||
"System.Configuration.ConfigurationManager": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.Tools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -911,14 +874,6 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Drawing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.SystemEvents": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Dynamic.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.0.11",
|
||||
|
|
@ -1058,8 +1013,8 @@
|
|||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q=="
|
||||
"resolved": "5.0.1",
|
||||
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
|
||||
},
|
||||
"System.Linq": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -1322,15 +1277,6 @@
|
|||
"System.Runtime.Extensions": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.AccessControl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Algorithms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1443,11 +1389,6 @@
|
|||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.ProtectedData": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA=="
|
||||
},
|
||||
"System.Security.Cryptography.X509Certificates": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1480,20 +1421,6 @@
|
|||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Permissions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==",
|
||||
"dependencies": {
|
||||
"System.Security.AccessControl": "5.0.0",
|
||||
"System.Windows.Extensions": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Principal.Windows": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
|
||||
},
|
||||
"System.Text.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1567,14 +1494,6 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Windows.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==",
|
||||
"dependencies": {
|
||||
"System.Drawing.Common": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Xml.ReaderWriter": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1637,7 +1556,7 @@
|
|||
"Newtonsoft.Json": "[13.0.1, )",
|
||||
"NodaTime": "[3.0.3, )",
|
||||
"NodaTime.Serialization.JsonNet": "[3.0.0, )",
|
||||
"Npgsql": "[4.1.5, )",
|
||||
"Npgsql": "[4.1.13, )",
|
||||
"Npgsql.NodaTime": "[4.1.5, )",
|
||||
"Serilog": "[2.12.0, )",
|
||||
"Serilog.Extensions.Logging": "[3.0.1, )",
|
||||
|
|
@ -1650,7 +1569,7 @@
|
|||
"Serilog.Sinks.Seq": "[5.2.2, )",
|
||||
"SqlKata": "[2.3.7, )",
|
||||
"SqlKata.Execution": "[2.3.7, )",
|
||||
"StackExchange.Redis": "[2.2.88, )",
|
||||
"StackExchange.Redis": "[2.8.16, )",
|
||||
"System.Interactive.Async": "[5.0.0, )",
|
||||
"ipnetwork2": "[2.5.381, )"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public partial class ApplicationCommandTree
|
|||
else if (ctx.Event.Data!.Name == ProxiedMessageDelete.Name)
|
||||
return ctx.Execute<ApplicationCommandProxiedMessage>(ProxiedMessageDelete, m => m.DeleteMessage(ctx));
|
||||
else if (ctx.Event.Data!.Name == ProxiedMessagePing.Name)
|
||||
return ctx.Execute<ApplicationCommandProxiedMessage>(ProxiedMessageDelete, m => m.PingMessageAuthor(ctx));
|
||||
return ctx.Execute<ApplicationCommandProxiedMessage>(ProxiedMessagePing, m => m.PingMessageAuthor(ctx));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,12 @@ public class ApplicationCommandProxiedMessage
|
|||
msg.System,
|
||||
msg.Member,
|
||||
guild,
|
||||
ctx.Config,
|
||||
LookupContext.ByNonOwner,
|
||||
DateTimeZone.Utc
|
||||
));
|
||||
|
||||
embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent));
|
||||
embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, showContent, ctx.Config));
|
||||
|
||||
await ctx.Reply(embeds: embeds.ToArray());
|
||||
}
|
||||
|
|
@ -62,14 +63,14 @@ public class ApplicationCommandProxiedMessage
|
|||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
|
||||
// check for command messages
|
||||
var (authorId, channelId) = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (authorId != null)
|
||||
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (cmessage != null)
|
||||
{
|
||||
if (authorId != ctx.User.Id)
|
||||
if (cmessage.AuthorId != ctx.User.Id)
|
||||
throw new PKError("You can only delete command messages queried by this account.");
|
||||
|
||||
var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == channelId;
|
||||
await DeleteMessageInner(ctx, channelId!.Value, messageId, isDM);
|
||||
var isDM = (await _repo.GetDmChannel(ctx.User!.Id)) == cmessage.ChannelId;
|
||||
await DeleteMessageInner(ctx, cmessage.GuildId, cmessage.ChannelId, messageId, isDM);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -77,10 +78,11 @@ public class ApplicationCommandProxiedMessage
|
|||
var message = await ctx.Repository.GetFullMessage(messageId);
|
||||
if (message != null)
|
||||
{
|
||||
if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.User.Id)
|
||||
// if user has has a system and their system sent the message, or if user sent the message, do not error
|
||||
if (!((ctx.System != null && message.System?.Id == ctx.System.Id) || message.Message.Sender == ctx.User.Id))
|
||||
throw new PKError("You can only delete your own messages.");
|
||||
|
||||
await DeleteMessageInner(ctx, message.Message.Channel, message.Message.Mid, false);
|
||||
await DeleteMessageInner(ctx, message.Message.Guild ?? 0, message.Message.Channel, message.Message.Mid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -88,9 +90,9 @@ public class ApplicationCommandProxiedMessage
|
|||
throw Errors.MessageNotFound(messageId);
|
||||
}
|
||||
|
||||
internal async Task DeleteMessageInner(InteractionContext ctx, ulong channelId, ulong messageId, bool isDM = false)
|
||||
internal async Task DeleteMessageInner(InteractionContext ctx, ulong guildId, ulong channelId, ulong messageId, bool isDM = false)
|
||||
{
|
||||
if (!((await _cache.PermissionsIn(channelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
if (!((await _cache.BotPermissionsIn(guildId, channelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
throw new PKError("PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the message."
|
||||
+ " Please contact a server administrator to remedy this.");
|
||||
|
||||
|
|
@ -100,6 +102,14 @@ public class ApplicationCommandProxiedMessage
|
|||
|
||||
public async Task PingMessageAuthor(InteractionContext ctx)
|
||||
{
|
||||
// if the command message was sent by a user account with bot usage disallowed, ignore it
|
||||
var abuse_log = await _repo.GetAbuseLogByAccount(ctx.User.Id);
|
||||
if (abuse_log != null && abuse_log.DenyBotUsage)
|
||||
{
|
||||
await ctx.Defer();
|
||||
return;
|
||||
}
|
||||
|
||||
var messageId = ctx.Event.Data!.TargetId!.Value;
|
||||
var msg = await ctx.Repository.GetFullMessage(messageId);
|
||||
if (msg == null)
|
||||
|
|
@ -109,7 +119,7 @@ public class ApplicationCommandProxiedMessage
|
|||
// (if not, PK shouldn't send messages on their behalf)
|
||||
var member = await _rest.GetGuildMember(ctx.GuildId, ctx.User.Id);
|
||||
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
|
||||
if (member == null || !(await _cache.PermissionsFor(ctx.ChannelId, member)).HasFlag(requiredPerms))
|
||||
if (member == null || !(await _cache.PermissionsForMemberInChannel(ctx.GuildId, ctx.ChannelId, member)).HasFlag(requiredPerms))
|
||||
{
|
||||
throw new PKError("You do not have permission to send messages in this channel.");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -67,8 +67,9 @@ public class Bot
|
|||
{
|
||||
new Activity
|
||||
{
|
||||
Type = ActivityType.Game,
|
||||
Name = BotStatus
|
||||
Type = ActivityType.Custom,
|
||||
Name = BotStatus,
|
||||
State = BotStatus
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -83,9 +84,16 @@ public class Bot
|
|||
// This *probably* doesn't matter in practice but I jut think it's neat, y'know.
|
||||
var timeNow = SystemClock.Instance.GetCurrentInstant();
|
||||
var timeTillNextWholeMinute = TimeSpan.FromMilliseconds(60000 - timeNow.ToUnixTimeMilliseconds() % 60000 + 250);
|
||||
_periodicTask = new Timer(_ =>
|
||||
_periodicTask = new Timer(async _ =>
|
||||
{
|
||||
var __ = UpdatePeriodic();
|
||||
try
|
||||
{
|
||||
await UpdatePeriodic();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "failed to run once-per-minute scheduled task");
|
||||
}
|
||||
}, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
|
|
@ -93,9 +101,7 @@ public class Bot
|
|||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +140,8 @@ public class Bot
|
|||
new Activity
|
||||
{
|
||||
Name = "Restarting... (please wait)",
|
||||
Type = ActivityType.Game
|
||||
State = "Restarting... (please wait)",
|
||||
Type = ActivityType.Custom
|
||||
}
|
||||
},
|
||||
Status = GatewayStatusUpdate.UserStatus.Idle
|
||||
|
|
@ -166,7 +173,16 @@ public class Bot
|
|||
}
|
||||
|
||||
using var _ = LogContext.PushProperty("EventId", Guid.NewGuid());
|
||||
using var __ = LogContext.Push(await serviceScope.Resolve<SerilogGatewayEnricherFactory>().GetEnricher(shardId, evt));
|
||||
// this fails when cache lookup fails, so put it in a try-catch
|
||||
try
|
||||
{
|
||||
using var __ = LogContext.Push(await serviceScope.Resolve<SerilogGatewayEnricherFactory>().GetEnricher(shardId, evt));
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
|
||||
await HandleError(handler, evt, serviceScope, exc);
|
||||
}
|
||||
_logger.Verbose("Received gateway event: {@Event}", evt);
|
||||
|
||||
try
|
||||
|
|
@ -234,7 +250,7 @@ public class Bot
|
|||
if (!exc.ShowToUser()) return;
|
||||
|
||||
// Once we've sent it to Sentry, report it to the user (if we have permission to)
|
||||
var reportChannel = handler.ErrorChannelFor(evt, _config.ClientId);
|
||||
var (guildId, reportChannel) = handler.ErrorChannelFor(evt, _config.ClientId);
|
||||
if (reportChannel == null)
|
||||
{
|
||||
if (evt is InteractionCreateEvent ice && ice.Type == Interaction.InteractionType.ApplicationCommand)
|
||||
|
|
@ -242,7 +258,7 @@ public class Bot
|
|||
return;
|
||||
}
|
||||
|
||||
var botPerms = await _cache.PermissionsIn(reportChannel.Value);
|
||||
var botPerms = await _cache.BotPermissionsIn(guildId ?? 0, reportChannel.Value);
|
||||
if (botPerms.HasFlag(PermissionSet.SendMessages | PermissionSet.EmbedLinks))
|
||||
await _errorMessageService.SendErrorMessage(reportChannel.Value, sentryEvent.EventId.ToString());
|
||||
}
|
||||
|
|
@ -269,7 +285,8 @@ public class Bot
|
|||
new Activity
|
||||
{
|
||||
Name = BotStatus,
|
||||
Type = ActivityType.Game
|
||||
State = BotStatus,
|
||||
Type = ActivityType.Custom,
|
||||
}
|
||||
},
|
||||
Status = GatewayStatusUpdate.UserStatus.Online
|
||||
|
|
|
|||
|
|
@ -20,11 +20,14 @@ public class BotConfig
|
|||
|
||||
public string? GatewayQueueUrl { get; set; }
|
||||
public bool UseRedisRatelimiter { get; set; } = false;
|
||||
public bool UseRedisCache { get; set; } = false;
|
||||
|
||||
public string? HttpCacheUrl { get; set; }
|
||||
public bool HttpUseInnerCache { get; set; } = false;
|
||||
|
||||
public string? RedisGatewayUrl { get; set; }
|
||||
|
||||
public string? DiscordBaseUrl { get; set; }
|
||||
public string? AvatarServiceUrl { get; set; }
|
||||
|
||||
public bool DisableErrorReporting { get; set; } = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -136,4 +136,11 @@ public static class BotMetrics
|
|||
DurationUnit = TimeUnit.Seconds,
|
||||
Context = "Bot"
|
||||
};
|
||||
|
||||
public static MeterOptions CacheDebug => new()
|
||||
{
|
||||
Name = "Bad responses to cache lookups",
|
||||
Context = "Bot",
|
||||
MeasurementUnit = Unit.Calls
|
||||
};
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ public partial class CommandTree
|
|||
public static Command ConfigShowPrivate = new Command("config show private", "config show private [on|off]", "Sets whether private information is shown to linked accounts by default");
|
||||
public static Command ConfigMemberDefaultPrivacy = new("config private member", "config private member [on|off]", "Sets whether member privacy is automatically set to private when creating a new member");
|
||||
public static Command ConfigGroupDefaultPrivacy = new("config private group", "config private group [on|off]", "Sets whether group privacy is automatically set to private when creating a new group");
|
||||
public static Command ConfigProxySwitch = new Command("config proxyswitch", "config proxyswitch [on|off]", "Sets whether to log a switch every time a proxy tag is used");
|
||||
public static Command ConfigNameFormat = new Command("config nameformat", "config nameformat [format]", "Changes your system's username formatting");
|
||||
public static Command AutoproxySet = new Command("autoproxy", "autoproxy [off|front|latch|member]", "Sets your system's autoproxy mode for the current server");
|
||||
public static Command AutoproxyOff = new Command("autoproxy off", "autoproxy off", "Disables autoproxying for your system in the current server");
|
||||
public static Command AutoproxyFront = new Command("autoproxy front", "autoproxy front", "Sets your system's autoproxy in this server to proxy the first member currently registered as front");
|
||||
|
|
@ -58,7 +60,7 @@ public partial class CommandTree
|
|||
public static Command MemberServerKeepProxy = new Command("member server keepproxy", "member <member> serverkeepproxy [on|off|clear]", "Sets whether to include a member's proxy tags when proxying in the current server.");
|
||||
public static Command MemberRandom = new Command("system random", "system [system] random", "Shows the info card of a randomly selected member in a system.");
|
||||
public static Command MemberId = new Command("member id", "member [member] id", "Prints a member's id.");
|
||||
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
|
||||
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy <name|description|birthday|pronouns|proxy|metadata|visibility|all> <public|private>", "Changes a members's privacy settings");
|
||||
public static Command GroupInfo = new Command("group", "group <name>", "Looks up information about a group");
|
||||
public static Command GroupNew = new Command("group new", "group new <name>", "Creates a new group");
|
||||
public static Command GroupList = new Command("group list", "group list", "Lists all groups in this system");
|
||||
|
|
@ -82,6 +84,7 @@ public partial class CommandTree
|
|||
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
|
||||
public static Command SwitchEdit = new Command("switch edit", "switch edit <member> [member 2] [member 3...]", "Edits the members in the latest switch");
|
||||
public static Command SwitchEditOut = new Command("switch edit out", "switch edit out", "Turns the latest switch into a switch-out");
|
||||
public static Command SwitchCopy = new Command("switch copy", "switch copy <member> [member 2] [member 3...]", "Makes a new switch with the listed members added");
|
||||
public static Command SwitchDelete = new Command("switch delete", "switch delete", "Deletes the latest switch");
|
||||
public static Command SwitchDeleteAll = new Command("switch delete", "switch delete all", "Deletes all logged switches");
|
||||
public static Command Link = new Command("link", "link <account>", "Links your system to another account");
|
||||
|
|
@ -92,19 +95,22 @@ public partial class CommandTree
|
|||
public static Command Export = new Command("export", "export", "Exports system information to a data file");
|
||||
public static Command Help = new Command("help", "help", "Shows help information about PluralKit");
|
||||
public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying");
|
||||
public static Command Dashboard = new Command("dashboard", "dashboard", "Get a link to the PluralKit dashboard");
|
||||
public static Command Message = new Command("message", "message <id|link> [delete|author]", "Looks up a proxied message");
|
||||
public static Command MessageEdit = new Command("edit", "edit [link] <text>", "Edit a previously proxied message");
|
||||
public static Command MessageReproxy = new Command("reproxy", "reproxy [link] <member>", "Reproxy a previously proxied message using a different member");
|
||||
public static Command ProxyCheck = new Command("debug proxy", "debug proxy [link|reply]", "Checks why your message has not been proxied");
|
||||
public static Command LogChannel = new Command("log channel", "log channel <channel>", "Designates a channel to post proxied messages to");
|
||||
public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel");
|
||||
public static Command LogEnable = new Command("log enable", "log enable all|<channel> [channel 2] [channel 3...]", "Enables message logging in certain channels");
|
||||
public static Command LogDisable = new Command("log disable", "log disable all|<channel> [channel 2] [channel 3...]", "Disables message logging in certain channels");
|
||||
public static Command LogShow = new Command("log show", "log show", "Displays the current list of channels where logging is disabled");
|
||||
public static Command LogClean = new Command("logclean", "logclean [on|off]", "Toggles whether to clean up other bots' log channels");
|
||||
public static Command BlacklistShow = new Command("blacklist show", "blacklist show", "Displays the current proxy blacklist");
|
||||
public static Command BlacklistAdd = new Command("blacklist add", "blacklist add all|<channel> [channel 2] [channel 3...]", "Adds certain channels to the proxy blacklist");
|
||||
public static Command BlacklistRemove = new Command("blacklist remove", "blacklist remove all|<channel> [channel 2] [channel 3...]", "Removes certain channels from the proxy blacklist");
|
||||
public static Command LogChannel = new Command("serverconfig log channel", "serverconfig log channel <channel>", "Designates a channel to post proxied messages to");
|
||||
public static Command LogChannelClear = new Command("serverconfig log channel", "serverconfig log channel -clear", "Clears the currently set log channel");
|
||||
public static Command LogEnable = new Command("serverconfig log blacklist remove", "serverconfig log blacklist remove all|<channel> [channel 2] [channel 3...]", "Enables message logging in certain channels");
|
||||
public static Command LogDisable = new Command("serverconfig log blacklist add", "serverconfig log blacklist add all|<channel> [channel 2] [channel 3...]", "Disables message logging in certain channels");
|
||||
public static Command LogShow = new Command("serverconfig log blacklist", "serverconfig log blacklist", "Displays the current list of channels where logging is disabled");
|
||||
public static Command BlacklistShow = new Command("serverconfig proxy blacklist", "serverconfig proxy blacklist", "Displays the current list of channels where message proxying is disabled");
|
||||
public static Command BlacklistAdd = new Command("serverconfig proxy blacklist add", "serverconfig proxy blacklist add all|<channel> [channel 2] [channel 3...]", "Disables message proxying in certain channels");
|
||||
public static Command BlacklistRemove = new Command("serverconfig blacklist remove", "serverconfig blacklist remove all|<channel> [channel 2] [channel 3...]", "Enables message proxying in certain channels");
|
||||
public static Command ServerConfigLogClean = new Command("serverconfig log cleanup", "serverconfig log cleanup [on|off]", "Toggles whether to clean up other bots' log channels");
|
||||
public static Command ServerConfigInvalidCommandResponse = new Command("serverconfig invalid command error", "serverconfig invalid command error [on|off]", "Sets whether to show an error message when an unknown command is sent");
|
||||
public static Command ServerConfigRequireSystemTag = new Command("serverconfig require tag", "serverconfig require tag [on|off]", "Sets whether server users are required to have a system tag on proxied messages");
|
||||
public static Command Invite = new Command("invite", "invite", "Gets a link to invite PluralKit to other servers");
|
||||
public static Command PermCheck = new Command("permcheck", "permcheck <guild>", "Checks whether a server's permission setup is correct");
|
||||
public static Command Admin = new Command("admin", "admin", "Super secret admin commands (sshhhh)");
|
||||
|
|
@ -137,13 +143,21 @@ public partial class CommandTree
|
|||
|
||||
public static Command[] SwitchCommands =
|
||||
{
|
||||
Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll
|
||||
Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut, SwitchDelete, SwitchDeleteAll, SwitchCopy
|
||||
};
|
||||
|
||||
public static Command[] ConfigCommands =
|
||||
{
|
||||
ConfigAutoproxyAccount, ConfigAutoproxyTimeout, ConfigTimezone, ConfigPing,
|
||||
ConfigMemberDefaultPrivacy, ConfigGroupDefaultPrivacy, ConfigShowPrivate
|
||||
ConfigMemberDefaultPrivacy, ConfigGroupDefaultPrivacy, ConfigShowPrivate,
|
||||
ConfigProxySwitch, ConfigNameFormat
|
||||
};
|
||||
|
||||
public static Command[] ServerConfigCommands =
|
||||
{
|
||||
ServerConfigLogClean, ServerConfigInvalidCommandResponse, ServerConfigRequireSystemTag,
|
||||
LogChannel, LogChannelClear, LogShow, LogDisable, LogEnable,
|
||||
BlacklistShow, BlacklistAdd, BlacklistRemove
|
||||
};
|
||||
|
||||
public static Command[] AutoproxyCommands =
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ public partial class CommandTree
|
|||
return CommandHelpRoot(ctx);
|
||||
if (ctx.Match("ap", "autoproxy", "auto"))
|
||||
return HandleAutoproxyCommand(ctx);
|
||||
if (ctx.Match("config", "cfg"))
|
||||
if (ctx.Match("config", "cfg", "configure"))
|
||||
return HandleConfigCommand(ctx);
|
||||
if (ctx.Match("serverconfig", "guildconfig", "scfg"))
|
||||
return HandleServerConfigCommand(ctx);
|
||||
if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls"))
|
||||
return ctx.Execute<SystemList>(SystemList, m => m.MemberList(ctx, ctx.System));
|
||||
if (ctx.Match("link"))
|
||||
|
|
@ -44,36 +46,36 @@ public partial class CommandTree
|
|||
else return ctx.Execute<Help>(Help, m => m.HelpRoot(ctx));
|
||||
if (ctx.Match("explain"))
|
||||
return ctx.Execute<Help>(Explain, m => m.Explain(ctx));
|
||||
if (ctx.Match("message", "msg"))
|
||||
if (ctx.Match("message", "msg", "messageinfo"))
|
||||
return ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx));
|
||||
if (ctx.Match("edit", "e"))
|
||||
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx));
|
||||
if (ctx.Match("reproxy", "rp", "crimes"))
|
||||
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, false));
|
||||
if (ctx.Match("x"))
|
||||
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx, true));
|
||||
if (ctx.Match("reproxy", "rp", "crimes", "crime"))
|
||||
return ctx.Execute<ProxiedMessage>(MessageReproxy, m => m.ReproxyMessage(ctx));
|
||||
if (ctx.Match("log"))
|
||||
if (ctx.Match("channel"))
|
||||
return ctx.Execute<ServerConfig>(LogChannel, m => m.SetLogChannel(ctx));
|
||||
return ctx.Execute<ServerConfig>(LogChannel, m => m.SetLogChannel(ctx), true);
|
||||
else if (ctx.Match("enable", "on"))
|
||||
return ctx.Execute<ServerConfig>(LogEnable, m => m.SetLogEnabled(ctx, true));
|
||||
return ctx.Execute<ServerConfig>(LogEnable, m => m.SetLogEnabled(ctx, true), true);
|
||||
else if (ctx.Match("disable", "off"))
|
||||
return ctx.Execute<ServerConfig>(LogDisable, m => m.SetLogEnabled(ctx, false));
|
||||
return ctx.Execute<ServerConfig>(LogDisable, m => m.SetLogEnabled(ctx, false), true);
|
||||
else if (ctx.Match("list", "show"))
|
||||
return ctx.Execute<ServerConfig>(LogShow, m => m.ShowLogDisabledChannels(ctx));
|
||||
else if (ctx.Match("commands"))
|
||||
return PrintCommandList(ctx, "message logging", LogCommands);
|
||||
else return PrintCommandExpectedError(ctx, LogCommands);
|
||||
return ctx.Execute<ServerConfig>(LogShow, m => m.ShowLogDisabledChannels(ctx), true);
|
||||
else
|
||||
return ctx.Reply($"{Emojis.Warn} Message logging commands have moved to `pk;serverconfig`.");
|
||||
if (ctx.Match("logclean"))
|
||||
return ctx.Execute<ServerConfig>(LogClean, m => m.SetLogCleanup(ctx));
|
||||
return ctx.Execute<ServerConfig>(ServerConfigLogClean, m => m.SetLogCleanup(ctx), true);
|
||||
if (ctx.Match("blacklist", "bl"))
|
||||
if (ctx.Match("enable", "on", "add", "deny"))
|
||||
return ctx.Execute<ServerConfig>(BlacklistAdd, m => m.SetBlacklisted(ctx, true));
|
||||
return ctx.Execute<ServerConfig>(BlacklistAdd, m => m.SetProxyBlacklisted(ctx, true), true);
|
||||
else if (ctx.Match("disable", "off", "remove", "allow"))
|
||||
return ctx.Execute<ServerConfig>(BlacklistRemove, m => m.SetBlacklisted(ctx, false));
|
||||
return ctx.Execute<ServerConfig>(BlacklistRemove, m => m.SetProxyBlacklisted(ctx, false), true);
|
||||
else if (ctx.Match("list", "show"))
|
||||
return ctx.Execute<ServerConfig>(BlacklistShow, m => m.ShowBlacklisted(ctx));
|
||||
else if (ctx.Match("commands"))
|
||||
return PrintCommandList(ctx, "channel blacklisting", BlacklistCommands);
|
||||
else return PrintCommandExpectedError(ctx, BlacklistCommands);
|
||||
return ctx.Execute<ServerConfig>(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true);
|
||||
else
|
||||
return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `pk;serverconfig`.");
|
||||
if (ctx.Match("proxy"))
|
||||
if (ctx.Match("debug"))
|
||||
return ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
|
||||
|
|
@ -89,7 +91,7 @@ public partial class CommandTree
|
|||
if (ctx.Match("rool")) return ctx.Execute<Fun>(null, m => m.Rool(ctx));
|
||||
if (ctx.Match("sus")) return ctx.Execute<Fun>(null, m => m.Sus(ctx));
|
||||
if (ctx.Match("error")) return ctx.Execute<Fun>(null, m => m.Error(ctx));
|
||||
if (ctx.Match("stats")) return ctx.Execute<Misc>(null, m => m.Stats(ctx));
|
||||
if (ctx.Match("stats", "status")) return ctx.Execute<Misc>(null, m => m.Stats(ctx));
|
||||
if (ctx.Match("permcheck"))
|
||||
return ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
|
||||
if (ctx.Match("proxycheck"))
|
||||
|
|
@ -98,17 +100,65 @@ public partial class CommandTree
|
|||
return HandleDebugCommand(ctx);
|
||||
if (ctx.Match("admin"))
|
||||
return HandleAdminCommand(ctx);
|
||||
if (ctx.Match("random", "r"))
|
||||
if (ctx.Match("random", "rand", "r"))
|
||||
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
|
||||
return ctx.Execute<Random>(GroupRandom, r => r.Group(ctx, ctx.System));
|
||||
else
|
||||
return ctx.Execute<Random>(MemberRandom, m => m.Member(ctx, ctx.System));
|
||||
if (ctx.Match("dashboard", "dash"))
|
||||
return ctx.Execute<Help>(Dashboard, m => m.Dashboard(ctx));
|
||||
|
||||
// don't send an "invalid command" response if the guild has those turned off
|
||||
if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true)
|
||||
return Task.CompletedTask;
|
||||
|
||||
// remove compiler warning
|
||||
return ctx.Reply(
|
||||
$"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see <https://pluralkit.me/commands>.");
|
||||
}
|
||||
|
||||
private async Task HandleAdminAbuseLogCommand(Context ctx)
|
||||
{
|
||||
ctx.AssertBotAdmin();
|
||||
|
||||
if (ctx.Match("n", "new", "create"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogCreate(ctx));
|
||||
else
|
||||
{
|
||||
AbuseLog? abuseLog = null!;
|
||||
var account = await ctx.MatchUser();
|
||||
if (account != null)
|
||||
{
|
||||
abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(ctx.PopArgument()));
|
||||
}
|
||||
|
||||
if (abuseLog == null)
|
||||
{
|
||||
await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext())
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogShow(ctx, abuseLog));
|
||||
else if (ctx.Match("au", "adduser"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogAddUser(ctx, abuseLog));
|
||||
else if (ctx.Match("ru", "removeuser"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog));
|
||||
else if (ctx.Match("desc", "description"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogDescription(ctx, abuseLog));
|
||||
else if (ctx.Match("deny", "deny-bot-usage"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog));
|
||||
else if (ctx.Match("yeet", "remove", "delete"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.AbuseLogDelete(ctx, abuseLog));
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} Unknown subcommand {ctx.PeekArgument().AsCode()}.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAdminCommand(Context ctx)
|
||||
{
|
||||
if (ctx.Match("usid", "updatesystemid"))
|
||||
|
|
@ -129,6 +179,10 @@ public partial class CommandTree
|
|||
await ctx.Execute<Admin>(Admin, a => a.SystemGroupLimit(ctx));
|
||||
else if (ctx.Match("sr", "systemrecover"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.SystemRecover(ctx));
|
||||
else if (ctx.Match("sd", "systemdelete"))
|
||||
await ctx.Execute<Admin>(Admin, a => a.SystemDelete(ctx));
|
||||
else if (ctx.Match("al", "abuselog"))
|
||||
await HandleAdminAbuseLogCommand(ctx);
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} Unknown command.");
|
||||
}
|
||||
|
|
@ -161,12 +215,6 @@ public partial class CommandTree
|
|||
else if (ctx.Match("commands", "help"))
|
||||
await PrintCommandList(ctx, "systems", SystemCommands);
|
||||
|
||||
// these are deprecated (and not accessible by other users anyway), let's leave them out of new parsing
|
||||
else if (ctx.Match("timezone", "tz"))
|
||||
await ctx.Execute<Config>(ConfigTimezone, m => m.SystemTimezone(ctx), true);
|
||||
else if (ctx.Match("ping"))
|
||||
await ctx.Execute<Config>(ConfigPing, m => m.SystemPing(ctx), true);
|
||||
|
||||
// todo: these aren't deprecated but also shouldn't be here
|
||||
else if (ctx.Match("webhook", "hook"))
|
||||
await ctx.Execute<Api>(null, m => m.SystemWebhook(ctx));
|
||||
|
|
@ -203,7 +251,7 @@ public partial class CommandTree
|
|||
// if we *still* haven't matched anything, the user entered an invalid command name or system reference
|
||||
if (ctx.Parameters._ptr == previousPtr)
|
||||
{
|
||||
if (ctx.Parameters.Peek().Length != 5 && !ctx.Parameters.Peek().TryParseMention(out _))
|
||||
if (!ctx.Parameters.Peek().TryParseHid(out _) && !ctx.Parameters.Peek().TryParseMention(out _))
|
||||
{
|
||||
await PrintCommandNotFoundError(ctx, SystemCommands);
|
||||
return;
|
||||
|
|
@ -227,9 +275,9 @@ public partial class CommandTree
|
|||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemTag, m => m.Tag(ctx, target));
|
||||
else if (ctx.Match("servertag", "st", "stag", "deer"))
|
||||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemServerTag, m => m.ServerTag(ctx, target));
|
||||
else if (ctx.Match("description", "desc", "bio", "info", "text"))
|
||||
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
|
||||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemDesc, m => m.Description(ctx, target));
|
||||
else if (ctx.Match("pronouns", "prns"))
|
||||
else if (ctx.Match("pronouns", "pronoun", "prns", "pn"))
|
||||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemPronouns, m => m.Pronouns(ctx, target));
|
||||
else if (ctx.Match("color", "colour"))
|
||||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemColor, m => m.Color(ctx, target));
|
||||
|
|
@ -267,7 +315,7 @@ public partial class CommandTree
|
|||
await ctx.CheckSystem(target).Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx, target));
|
||||
else if (ctx.Match("id"))
|
||||
await ctx.CheckSystem(target).Execute<System>(SystemId, m => m.DisplayId(ctx, target));
|
||||
else if (ctx.Match("random", "r"))
|
||||
else if (ctx.Match("random", "rand", "r"))
|
||||
if (ctx.Match("group", "g") || ctx.MatchFlag("group", "g"))
|
||||
await ctx.CheckSystem(target).Execute<Random>(GroupRandom, r => r.Group(ctx, target));
|
||||
else
|
||||
|
|
@ -297,13 +345,13 @@ public partial class CommandTree
|
|||
// Commands that have a member target (eg. pk;member <member> delete)
|
||||
if (ctx.Match("rename", "name", "changename", "setname", "rn"))
|
||||
await ctx.Execute<MemberEdit>(MemberRename, m => m.Name(ctx, target));
|
||||
else if (ctx.Match("description", "info", "bio", "text", "desc"))
|
||||
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
|
||||
await ctx.Execute<MemberEdit>(MemberDesc, m => m.Description(ctx, target));
|
||||
else if (ctx.Match("pronouns", "pronoun", "prns", "pn"))
|
||||
await ctx.Execute<MemberEdit>(MemberPronouns, m => m.Pronouns(ctx, target));
|
||||
else if (ctx.Match("color", "colour"))
|
||||
await ctx.Execute<MemberEdit>(MemberColor, m => m.Color(ctx, target));
|
||||
else if (ctx.Match("birthday", "bday", "birthdate", "cakeday", "bdate"))
|
||||
else if (ctx.Match("birthday", "birth", "bday", "birthdate", "cakeday", "bdate", "bd"))
|
||||
await ctx.Execute<MemberEdit>(MemberBirthday, m => m.Birthday(ctx, target));
|
||||
else if (ctx.Match("proxy", "tags", "proxytags", "brackets"))
|
||||
await ctx.Execute<MemberProxy>(MemberProxy, m => m.Proxy(ctx, target));
|
||||
|
|
@ -311,7 +359,7 @@ public partial class CommandTree
|
|||
await ctx.Execute<MemberEdit>(MemberDelete, m => m.Delete(ctx, target));
|
||||
else if (ctx.Match("avatar", "profile", "picture", "icon", "image", "pfp", "pic"))
|
||||
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.Avatar(ctx, target));
|
||||
else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa"))
|
||||
else if (ctx.Match("proxyavatar", "proxypfp", "webhookavatar", "webhookpfp", "pa", "pavatar", "ppfp"))
|
||||
await ctx.Execute<MemberAvatar>(MemberAvatar, m => m.WebhookAvatar(ctx, target));
|
||||
else if (ctx.Match("banner", "splash", "cover"))
|
||||
await ctx.Execute<MemberEdit>(MemberBannerImage, m => m.BannerImage(ctx, target));
|
||||
|
|
@ -319,7 +367,7 @@ public partial class CommandTree
|
|||
if (ctx.Match("add", "a"))
|
||||
await ctx.Execute<GroupMember>(MemberGroupAdd,
|
||||
m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Add));
|
||||
else if (ctx.Match("remove", "rem", "r"))
|
||||
else if (ctx.Match("remove", "rem"))
|
||||
await ctx.Execute<GroupMember>(MemberGroupRemove,
|
||||
m => m.AddRemoveGroups(ctx, target, Groups.AddRemoveOperation.Remove));
|
||||
else
|
||||
|
|
@ -346,7 +394,7 @@ public partial class CommandTree
|
|||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, null));
|
||||
else if (ctx.Match("private", "hidden", "hide"))
|
||||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Private));
|
||||
else if (ctx.Match("public", "shown", "show"))
|
||||
else if (ctx.Match("public", "shown", "show", "unhide", "unhidden"))
|
||||
await ctx.Execute<MemberEdit>(MemberPrivacy, m => m.Privacy(ctx, target, PrivacyLevel.Public));
|
||||
else if (ctx.Match("soulscream"))
|
||||
await ctx.Execute<Member>(MemberInfo, m => m.Soulscream(ctx, target));
|
||||
|
|
@ -374,17 +422,17 @@ public partial class CommandTree
|
|||
await ctx.Execute<Groups>(GroupRename, g => g.RenameGroup(ctx, target));
|
||||
else if (ctx.Match("nick", "dn", "displayname", "nickname"))
|
||||
await ctx.Execute<Groups>(GroupDisplayName, g => g.GroupDisplayName(ctx, target));
|
||||
else if (ctx.Match("description", "info", "bio", "text", "desc"))
|
||||
else if (ctx.Match("description", "desc", "describe", "d", "bio", "info", "text", "intro"))
|
||||
await ctx.Execute<Groups>(GroupDesc, g => g.GroupDescription(ctx, target));
|
||||
else if (ctx.Match("add", "a"))
|
||||
await ctx.Execute<GroupMember>(GroupAdd,
|
||||
g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Add));
|
||||
else if (ctx.Match("remove", "rem", "r"))
|
||||
else if (ctx.Match("remove", "rem"))
|
||||
await ctx.Execute<GroupMember>(GroupRemove,
|
||||
g => g.AddRemoveMembers(ctx, target, Groups.AddRemoveOperation.Remove));
|
||||
else if (ctx.Match("members", "list", "ms", "l", "ls"))
|
||||
await ctx.Execute<GroupMember>(GroupMemberList, g => g.ListGroupMembers(ctx, target));
|
||||
else if (ctx.Match("random"))
|
||||
else if (ctx.Match("random", "rand", "r"))
|
||||
await ctx.Execute<Random>(GroupMemberRandom, r => r.GroupMember(ctx, target));
|
||||
else if (ctx.Match("privacy"))
|
||||
await ctx.Execute<Groups>(GroupPrivacy, g => g.GroupPrivacy(ctx, target, null));
|
||||
|
|
@ -428,13 +476,15 @@ public partial class CommandTree
|
|||
await ctx.Execute<Switch>(SwitchEdit, m => m.SwitchEdit(ctx));
|
||||
else if (ctx.Match("delete", "remove", "erase", "cancel", "yeet"))
|
||||
await ctx.Execute<Switch>(SwitchDelete, m => m.SwitchDelete(ctx));
|
||||
else if (ctx.Match("copy", "add", "duplicate", "dupe"))
|
||||
await ctx.Execute<Switch>(SwitchCopy, m => m.SwitchEdit(ctx, true));
|
||||
else if (ctx.Match("commands", "help"))
|
||||
await PrintCommandList(ctx, "switching", SwitchCommands);
|
||||
else if (ctx.HasNext()) // there are following arguments
|
||||
await ctx.Execute<Switch>(Switch, m => m.SwitchDo(ctx));
|
||||
else
|
||||
await PrintCommandNotFoundError(ctx, Switch, SwitchOut, SwitchMove, SwitchEdit, SwitchEditOut,
|
||||
SwitchDelete, SystemFronter, SystemFrontHistory);
|
||||
SwitchDelete, SwitchCopy, SystemFronter, SystemFrontHistory);
|
||||
}
|
||||
|
||||
private async Task CommandHelpRoot(Context ctx)
|
||||
|
|
@ -482,6 +532,11 @@ public partial class CommandTree
|
|||
case "cfg":
|
||||
await PrintCommandList(ctx, "settings", ConfigCommands);
|
||||
break;
|
||||
case "serverconfig":
|
||||
case "guildconfig":
|
||||
case "scfg":
|
||||
await PrintCommandList(ctx, "server settings", ServerConfigCommands);
|
||||
break;
|
||||
case "autoproxy":
|
||||
case "ap":
|
||||
await PrintCommandList(ctx, "autoproxy", AutoproxyCommands);
|
||||
|
|
@ -500,13 +555,6 @@ public partial class CommandTree
|
|||
if (ctx.System == null)
|
||||
return ctx.Reply($"{Emojis.Error} {Errors.NoSystemError.Message}");
|
||||
|
||||
// todo: move this whole block to Autoproxy.cs when these are removed
|
||||
|
||||
if (ctx.Match("account", "ac"))
|
||||
return ctx.Execute<Config>(ConfigAutoproxyAccount, m => m.AutoproxyAccount(ctx), true);
|
||||
if (ctx.Match("timeout", "tm"))
|
||||
return ctx.Execute<Config>(ConfigAutoproxyTimeout, m => m.AutoproxyTimeout(ctx), true);
|
||||
|
||||
return ctx.Execute<Autoproxy>(AutoproxySet, m => m.SetAutoproxyMode(ctx));
|
||||
}
|
||||
|
||||
|
|
@ -536,8 +584,56 @@ public partial class CommandTree
|
|||
return ctx.Execute<Config>(null, m => m.CaseSensitiveProxyTags(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "error" }) || ctx.Match("pe"))
|
||||
return ctx.Execute<Config>(null, m => m.ProxyErrorMessageEnabled(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "split" }, new[] { "id", "ids" }) || ctx.Match("sid", "sids"))
|
||||
return ctx.Execute<Config>(null, m => m.HidDisplaySplit(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "cap", "caps", "capitalize", "capitalise" }, new[] { "id", "ids" }) || ctx.Match("capid", "capids"))
|
||||
return ctx.Execute<Config>(null, m => m.HidDisplayCaps(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "pad" }, new[] { "id", "ids" }) || ctx.MatchMultiple(new[] { "id" }, new[] { "pad", "padding" }) || ctx.Match("idpad", "padid", "padids"))
|
||||
return ctx.Execute<Config>(null, m => m.HidListPadding(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "name" }, new[] { "format" }) || ctx.Match("nameformat", "nf"))
|
||||
return ctx.Execute<Config>(null, m => m.NameFormat(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "member", "group" }, new[] { "limit" }) || ctx.Match("limit"))
|
||||
return ctx.Execute<Config>(null, m => m.LimitUpdate(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "switch" }) || ctx.Match("proxyswitch", "ps"))
|
||||
return ctx.Execute<Config>(null, m => m.ProxySwitch(ctx));
|
||||
|
||||
// todo: maybe add the list of configuration keys here?
|
||||
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings.");
|
||||
}
|
||||
|
||||
private Task HandleServerConfigCommand(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
return ctx.Execute<ServerConfig>(null, m => m.ShowConfig(ctx));
|
||||
|
||||
if (ctx.MatchMultiple(new[] { "log" }, new[] { "cleanup", "clean" }) || ctx.Match("logclean"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.SetLogCleanup(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "invalid", "unknown" }, new[] { "command" }, new[] { "error", "response" }) || ctx.Match("invalidcommanderror", "unknowncommanderror"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.InvalidCommandResponse(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "require", "enforce" }, new[] { "tag", "systemtag" }) || ctx.Match("requiretag", "enforcetag"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.RequireSystemTag(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "log" }, new[] { "channel" }))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.SetLogChannel(ctx));
|
||||
if (ctx.MatchMultiple(new[] { "log" }, new[] { "blacklist" }))
|
||||
{
|
||||
if (ctx.Match("enable", "on", "add", "deny"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.SetLogBlacklisted(ctx, true));
|
||||
else if (ctx.Match("disable", "off", "remove", "allow"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.SetLogBlacklisted(ctx, false));
|
||||
else
|
||||
return ctx.Execute<ServerConfig>(null, m => m.ShowLogDisabledChannels(ctx));
|
||||
}
|
||||
if (ctx.MatchMultiple(new[] { "proxy", "proxying" }, new[] { "blacklist" }))
|
||||
{
|
||||
if (ctx.Match("enable", "on", "add", "deny"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.SetProxyBlacklisted(ctx, true));
|
||||
else if (ctx.Match("disable", "off", "remove", "allow"))
|
||||
return ctx.Execute<ServerConfig>(null, m => m.SetProxyBlacklisted(ctx, false));
|
||||
else
|
||||
return ctx.Execute<ServerConfig>(null, m => m.ShowProxyBlacklisted(ctx));
|
||||
}
|
||||
|
||||
// todo: maybe add the list of configuration keys here?
|
||||
return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands serverconfig` for the list of possible config settings.");
|
||||
}
|
||||
}
|
||||
|
|
@ -29,11 +29,13 @@ public class Context
|
|||
private Command? _currentCommand;
|
||||
|
||||
public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message,
|
||||
int commandParseOffset, PKSystem senderSystem, SystemConfig config)
|
||||
int commandParseOffset, PKSystem senderSystem, SystemConfig config,
|
||||
GuildConfig? guildConfig)
|
||||
{
|
||||
Message = (Message)message;
|
||||
ShardId = shardId;
|
||||
Guild = guild;
|
||||
GuildConfig = guildConfig;
|
||||
Channel = channel;
|
||||
System = senderSystem;
|
||||
Config = config;
|
||||
|
|
@ -59,11 +61,12 @@ public class Context
|
|||
|
||||
public readonly Message Message;
|
||||
public readonly Guild Guild;
|
||||
public readonly GuildConfig? GuildConfig;
|
||||
public readonly int ShardId;
|
||||
public readonly Cluster Cluster;
|
||||
|
||||
public Task<PermissionSet> BotPermissions => Cache.PermissionsIn(Channel.Id);
|
||||
public Task<PermissionSet> UserPermissions => Cache.PermissionsFor((MessageCreateEvent)Message);
|
||||
public Task<PermissionSet> BotPermissions => Cache.BotPermissionsIn(Guild?.Id ?? 0, Channel.Id);
|
||||
public Task<PermissionSet> UserPermissions => Cache.PermissionsForMCE((MessageCreateEvent)Message);
|
||||
|
||||
|
||||
public readonly PKSystem System;
|
||||
|
|
@ -100,7 +103,7 @@ public class Context
|
|||
// {
|
||||
// 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, msg.ChannelId, Author.Id);
|
||||
await _commandMessageService.RegisterMessage(msg.Id, Guild?.Id ?? 0, msg.ChannelId, Author.Id);
|
||||
// }
|
||||
|
||||
return msg;
|
||||
|
|
@ -112,8 +115,7 @@ public class Context
|
|||
|
||||
if (deprecated && commandDef != null)
|
||||
{
|
||||
await Reply($"{Emojis.Warn} This command has been removed. please use `pk;{commandDef.Key}` instead.");
|
||||
return;
|
||||
await Reply($"{Emojis.Warn} Server configuration has moved to `pk;serverconfig`. The command you are trying to run is now `pk;{commandDef.Key}`.");
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -91,8 +91,12 @@ public static class ContextArgumentsExt
|
|||
public static bool MatchClear(this Context ctx)
|
||||
=> ctx.Match("clear", "reset", "default") || ctx.MatchFlag("c", "clear");
|
||||
|
||||
public static bool MatchRaw(this Context ctx) =>
|
||||
ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw");
|
||||
public static ReplyFormat MatchFormat(this Context ctx)
|
||||
{
|
||||
if (ctx.Match("r", "raw") || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
|
||||
if (ctx.Match("pt", "plaintext") || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
|
||||
return ReplyFormat.Standard;
|
||||
}
|
||||
|
||||
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
|
||||
{
|
||||
|
|
@ -184,4 +188,11 @@ public static class ContextArgumentsExt
|
|||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ReplyFormat
|
||||
{
|
||||
Standard,
|
||||
Raw,
|
||||
Plaintext
|
||||
}
|
||||
|
|
@ -33,11 +33,14 @@ public static class ContextAvatarExt
|
|||
// If we have an attachment, use that
|
||||
if (ctx.Message.Attachments.FirstOrDefault() is { } attachment)
|
||||
{
|
||||
// XXX: strip query params from attachment URLs because of new Discord CDN shenanigans
|
||||
// XXX: discord attachment URLs are unable to be validated without their query params
|
||||
// keep both the URL with query (for validation) and the clean URL (for storage) around
|
||||
var uriBuilder = new UriBuilder(attachment.ProxyUrl);
|
||||
uriBuilder.Query = "";
|
||||
|
||||
return new ParsedImage { Url = uriBuilder.Uri.AbsoluteUri, Source = AvatarSource.Attachment };
|
||||
ParsedImage img = new ParsedImage { Url = uriBuilder.Uri.AbsoluteUri, Source = AvatarSource.Attachment };
|
||||
uriBuilder.Query = "";
|
||||
img.CleanUrl = uriBuilder.Uri.AbsoluteUri;
|
||||
return img;
|
||||
}
|
||||
|
||||
// We should only get here if there are no arguments (which would get parsed as URL + throw if error)
|
||||
|
|
@ -49,6 +52,7 @@ public static class ContextAvatarExt
|
|||
public struct ParsedImage
|
||||
{
|
||||
public string Url;
|
||||
public string? CleanUrl;
|
||||
public AvatarSource Source;
|
||||
public User? SourceUser;
|
||||
}
|
||||
|
|
@ -57,5 +61,6 @@ public enum AvatarSource
|
|||
{
|
||||
Url,
|
||||
User,
|
||||
Attachment
|
||||
Attachment,
|
||||
HostedCdn
|
||||
}
|
||||
|
|
@ -14,7 +14,11 @@ public static class ContextEntityArgumentsExt
|
|||
{
|
||||
var text = ctx.PeekArgument();
|
||||
if (text.TryParseMention(out var id))
|
||||
return await ctx.Cache.GetOrFetchUser(ctx.Rest, id);
|
||||
{
|
||||
var user = await ctx.Cache.GetOrFetchUser(ctx.Rest, id);
|
||||
if (user != null) ctx.PopArgument();
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -53,8 +57,10 @@ public static class ContextEntityArgumentsExt
|
|||
return await ctx.Repository.GetSystemByAccount(id);
|
||||
|
||||
// Finally, try HID parsing
|
||||
var system = await ctx.Repository.GetSystemByHid(input);
|
||||
return system;
|
||||
if (input.TryParseHid(out var hid))
|
||||
return await ctx.Repository.GetSystemByHid(hid);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<PKMember> PeekMember(this Context ctx, SystemId? restrictToSystem = null)
|
||||
|
|
@ -83,7 +89,7 @@ public static class ContextEntityArgumentsExt
|
|||
|
||||
// Finally (or if by-HID lookup is specified), check if input is a valid HID and then try member HID parsing:
|
||||
|
||||
if (!Regex.IsMatch(input, @"^[a-zA-Z]{5}$"))
|
||||
if (!input.TryParseHid(out var hid))
|
||||
return null;
|
||||
|
||||
// For posterity:
|
||||
|
|
@ -94,21 +100,21 @@ public static class ContextEntityArgumentsExt
|
|||
PKMember memberByHid = null;
|
||||
if (restrictToSystem != null)
|
||||
{
|
||||
memberByHid = await ctx.Repository.GetMemberByHid(input, restrictToSystem);
|
||||
memberByHid = await ctx.Repository.GetMemberByHid(hid, restrictToSystem);
|
||||
if (memberByHid != null)
|
||||
return memberByHid;
|
||||
}
|
||||
// otherwise we try the querier's system and if that doesn't work we do global
|
||||
else
|
||||
{
|
||||
memberByHid = await ctx.Repository.GetMemberByHid(input, ctx.System?.Id);
|
||||
memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id);
|
||||
if (memberByHid != null)
|
||||
return memberByHid;
|
||||
|
||||
// ff ctx.System was null then this would be a duplicate of above and we don't want to run it again
|
||||
if (ctx.System != null)
|
||||
{
|
||||
memberByHid = await ctx.Repository.GetMemberByHid(input);
|
||||
memberByHid = await ctx.Repository.GetMemberByHid(hid);
|
||||
if (memberByHid != null)
|
||||
return memberByHid;
|
||||
}
|
||||
|
|
@ -148,7 +154,10 @@ public static class ContextEntityArgumentsExt
|
|||
return byDisplayName;
|
||||
}
|
||||
|
||||
if (await ctx.Repository.GetGroupByHid(input, restrictToSystem) is { } byHid)
|
||||
if (!input.TryParseHid(out var hid))
|
||||
return null;
|
||||
|
||||
if (await ctx.Repository.GetGroupByHid(hid, restrictToSystem) is { } byHid)
|
||||
return byHid;
|
||||
|
||||
return null;
|
||||
|
|
@ -164,17 +173,18 @@ public static class ContextEntityArgumentsExt
|
|||
public static string CreateNotFoundError(this Context ctx, string entity, string input)
|
||||
{
|
||||
var isIDOnlyQuery = ctx.System == null || ctx.MatchFlag("id", "by-id");
|
||||
var inputIsHid = HidUtils.ParseHid(input) != null;
|
||||
|
||||
if (isIDOnlyQuery)
|
||||
{
|
||||
if (input.Length == 5)
|
||||
if (inputIsHid)
|
||||
return $"{entity} with ID \"{input}\" not found.";
|
||||
return $"{entity} not found. Note that a {entity.ToLower()} ID is 5 characters long.";
|
||||
return $"{entity} not found. Note that a {entity.ToLower()} ID is 5 or 6 characters long.";
|
||||
}
|
||||
|
||||
if (input.Length == 5)
|
||||
if (inputIsHid)
|
||||
return $"{entity} with ID or name \"{input}\" not found.";
|
||||
return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 characters long.";
|
||||
return $"{entity} with name \"{input}\" not found. Note that a {entity.ToLower()} ID is 5 or 6 characters long.";
|
||||
}
|
||||
|
||||
public static async Task<Channel> MatchChannel(this Context ctx)
|
||||
|
|
@ -182,7 +192,8 @@ public static class ContextEntityArgumentsExt
|
|||
if (!MentionUtils.TryParseChannel(ctx.PeekArgument(), out var id))
|
||||
return null;
|
||||
|
||||
var channel = await ctx.Cache.TryGetChannel(id);
|
||||
// todo: match channels in other guilds
|
||||
var channel = await ctx.Cache.TryGetChannel(ctx.Guild!.Id, id);
|
||||
if (channel == null)
|
||||
channel = await ctx.Rest.GetChannelOrNull(id);
|
||||
if (channel == null)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ public static class ContextPrivacyExt
|
|||
{
|
||||
public static PrivacyLevel PopPrivacyLevel(this Context ctx)
|
||||
{
|
||||
if (ctx.Match("public", "show", "shown", "visible"))
|
||||
if (ctx.Match("public", "pub", "show", "shown", "visible", "unhide", "unhidden"))
|
||||
return PrivacyLevel.Public;
|
||||
|
||||
if (ctx.Match("private", "hide", "hidden"))
|
||||
if (ctx.Match("private", "priv", "hide", "hidden"))
|
||||
return PrivacyLevel.Private;
|
||||
|
||||
if (!ctx.HasNext())
|
||||
|
|
@ -33,7 +33,7 @@ public static class ContextPrivacyExt
|
|||
{
|
||||
if (!MemberPrivacyUtils.TryParseMemberPrivacy(ctx.PeekArgument(), out var subject))
|
||||
throw new PKSyntaxError(
|
||||
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `metadata`, `visibility`, or `all`).");
|
||||
$"Invalid privacy subject {ctx.PopArgument().AsCode()} (must be `name`, `description`, `avatar`, `birthday`, `pronouns`, `proxy`, `metadata`, `visibility`, or `all`).");
|
||||
|
||||
ctx.PopArgument();
|
||||
return subject;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
using Humanizer;
|
||||
using Dapper;
|
||||
using SqlKata;
|
||||
|
||||
using Myriad.Builders;
|
||||
using Myriad.Extensions;
|
||||
using Myriad.Cache;
|
||||
using Myriad.Rest;
|
||||
using Myriad.Types;
|
||||
|
||||
|
|
@ -14,11 +18,95 @@ public class Admin
|
|||
{
|
||||
private readonly BotConfig _botConfig;
|
||||
private readonly DiscordApiClient _rest;
|
||||
private readonly IDiscordCache _cache;
|
||||
|
||||
public Admin(BotConfig botConfig, DiscordApiClient rest)
|
||||
public Admin(BotConfig botConfig, DiscordApiClient rest, IDiscordCache cache)
|
||||
{
|
||||
_botConfig = botConfig;
|
||||
_rest = rest;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
private Task<(ulong Id, User? User)[]> GetUsers(IEnumerable<ulong> ids)
|
||||
{
|
||||
async Task<(ulong Id, User? User)> Inner(ulong id)
|
||||
{
|
||||
var user = await _cache.GetOrFetchUser(_rest, id);
|
||||
return (id, user);
|
||||
}
|
||||
|
||||
return Task.WhenAll(ids.Select(Inner));
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateEmbed(Context ctx, PKSystem system)
|
||||
{
|
||||
string UntilLimit(int count, int limit)
|
||||
{
|
||||
var brackets = new List<int> { 10, 25, 50, 100 };
|
||||
if (count == limit)
|
||||
return "(at limit)";
|
||||
|
||||
foreach (var x in brackets)
|
||||
{
|
||||
if (limit - x <= count)
|
||||
return $"(approx. {x} to limit)";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
var config = await ctx.Repository.GetSystemConfig(system.Id);
|
||||
|
||||
// Fetch/render info for all accounts simultaneously
|
||||
var accounts = await ctx.Repository.GetSystemAccounts(system.Id);
|
||||
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)");
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Title("System info")
|
||||
.Color(DiscordUtils.Green)
|
||||
.Field(new Embed.Field("System ID", $"`{system.Hid}`"))
|
||||
.Field(new Embed.Field("Linked accounts", string.Join("\n", users).Truncate(1000)));
|
||||
|
||||
var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
var memberCount = await ctx.Repository.GetSystemMemberCount(system.Id);
|
||||
eb.Field(new Embed.Field("Member limit", $"{memberLimit} {UntilLimit(memberCount, memberLimit)}", true));
|
||||
|
||||
var groupLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
var groupCount = await ctx.Repository.GetSystemGroupCount(system.Id);
|
||||
eb.Field(new Embed.Field("Group limit", $"{groupLimit} {UntilLimit(groupCount, groupLimit)}", true));
|
||||
|
||||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateAbuseLogEmbed(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
// Fetch/render info for all accounts simultaneously
|
||||
var accounts = await ctx.Repository.GetAbuseLogAccounts(abuseLog.Id);
|
||||
var systems = await Task.WhenAll(accounts.Select(x => ctx.Repository.GetSystemByAccount(x)));
|
||||
var users = (await GetUsers(accounts)).Select(x => x.User?.NameAndMention() ?? $"(deleted: `{x.Id}`)");
|
||||
|
||||
List<string> flagstr = new();
|
||||
if (abuseLog.DenyBotUsage)
|
||||
flagstr.Add("- bot usage denied");
|
||||
|
||||
var eb = new EmbedBuilder()
|
||||
.Title($"Abuse log: {abuseLog.Uuid.ToString()}")
|
||||
.Color(DiscordUtils.Red)
|
||||
.Footer(new Embed.EmbedFooter($"Created on {abuseLog.Created.FormatZoned(ctx.Zone)}"));
|
||||
|
||||
if (systems.Any(x => x != null))
|
||||
{
|
||||
var sysList = string.Join(", ", systems.Select(x => $"`{x.DisplayHid()}`"));
|
||||
eb.Field(new Embed.Field($"{Emojis.Warn} Accounts have registered system(s)", sysList));
|
||||
}
|
||||
|
||||
eb.Field(new Embed.Field("Accounts", string.Join("\n", users).Truncate(1000), true));
|
||||
eb.Field(new Embed.Field("Flags", flagstr.Any() ? string.Join("\n", flagstr) : "(none)", true));
|
||||
|
||||
if (abuseLog.Description != null)
|
||||
eb.Field(new Embed.Field("Description", abuseLog.Description.Truncate(1000)));
|
||||
|
||||
return eb.Build();
|
||||
}
|
||||
|
||||
public async Task UpdateSystemId(Context ctx)
|
||||
|
|
@ -29,14 +117,16 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown system.");
|
||||
|
||||
var newHid = ctx.PopArgument();
|
||||
if (!Regex.IsMatch(newHid, "^[a-z]{5}$"))
|
||||
throw new PKError($"Invalid new system ID `{newHid}`.");
|
||||
var input = ctx.PopArgument();
|
||||
if (!input.TryParseHid(out var newHid))
|
||||
throw new PKError($"Invalid new system ID `{input}`.");
|
||||
|
||||
var existingSystem = await ctx.Repository.GetSystemByHid(newHid);
|
||||
if (existingSystem != null)
|
||||
throw new PKError($"Another system already exists with ID `{newHid}`.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Change system ID of `{target.Hid}` to `{newHid}`?", "Change"))
|
||||
throw new PKError("ID change cancelled.");
|
||||
|
||||
|
|
@ -52,14 +142,17 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown member.");
|
||||
|
||||
var newHid = ctx.PopArgument();
|
||||
if (!Regex.IsMatch(newHid, "^[a-z]{5}$"))
|
||||
throw new PKError($"Invalid new member ID `{newHid}`.");
|
||||
var input = ctx.PopArgument();
|
||||
if (!input.TryParseHid(out var newHid))
|
||||
throw new PKError($"Invalid new member ID `{input}`.");
|
||||
|
||||
var existingMember = await ctx.Repository.GetMemberByHid(newHid);
|
||||
if (existingMember != null)
|
||||
throw new PKError($"Another member already exists with ID `{newHid}`.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo(
|
||||
$"Change member ID of **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`) to `{newHid}`?",
|
||||
"Change"
|
||||
|
|
@ -78,14 +171,17 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown group.");
|
||||
|
||||
var newHid = ctx.PopArgument();
|
||||
if (!Regex.IsMatch(newHid, "^[a-z]{5}$"))
|
||||
throw new PKError($"Invalid new group ID `{newHid}`.");
|
||||
var input = ctx.PopArgument();
|
||||
if (!input.TryParseHid(out var newHid))
|
||||
throw new PKError($"Invalid new group ID `{input}`.");
|
||||
|
||||
var existingGroup = await ctx.Repository.GetGroupByHid(newHid);
|
||||
if (existingGroup != null)
|
||||
throw new PKError($"Another group already exists with ID `{newHid}`.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Change group ID of **{target.Name}** (`{target.Hid}`) to `{newHid}`?",
|
||||
"Change"
|
||||
))
|
||||
|
|
@ -103,6 +199,8 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown system.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll"))
|
||||
throw new PKError("ID change cancelled.");
|
||||
|
||||
|
|
@ -124,6 +222,9 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown member.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo(
|
||||
$"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?",
|
||||
"Reroll"
|
||||
|
|
@ -148,6 +249,9 @@ public class Admin
|
|||
if (target == null)
|
||||
throw new PKError("Unknown group.");
|
||||
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?",
|
||||
"Change"
|
||||
))
|
||||
|
|
@ -176,7 +280,7 @@ public class Admin
|
|||
var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount;
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply($"Current member limit is **{currentLimit}** members.");
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +288,7 @@ public class Admin
|
|||
if (!int.TryParse(newLimitStr, out var newLimit))
|
||||
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update"))
|
||||
throw new PKError("Member limit change cancelled.");
|
||||
|
||||
|
|
@ -204,7 +309,7 @@ public class Admin
|
|||
var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply($"Current group limit is **{currentLimit}** groups.");
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +317,7 @@ public class Admin
|
|||
if (!int.TryParse(newLimitStr, out var newLimit))
|
||||
throw new PKError($"Couldn't parse `{newLimitStr}` as number.");
|
||||
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, target));
|
||||
if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update"))
|
||||
throw new PKError("Group limit change cancelled.");
|
||||
|
||||
|
|
@ -240,9 +346,10 @@ public class Admin
|
|||
|
||||
var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id);
|
||||
if (existingAccount != null)
|
||||
throw Errors.AccountInOtherSystem(existingAccount);
|
||||
throw Errors.AccountInOtherSystem(existingAccount, ctx.Config);
|
||||
|
||||
var system = await ctx.Repository.GetSystem(systemId.Value!);
|
||||
await ctx.Reply(null, await CreateEmbed(ctx, system));
|
||||
|
||||
if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account"))
|
||||
throw new PKError("System recovery cancelled.");
|
||||
|
|
@ -266,4 +373,127 @@ public class Admin
|
|||
Color = DiscordUtils.Green,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SystemDelete(Context ctx)
|
||||
{
|
||||
ctx.AssertBotAdmin();
|
||||
|
||||
var target = await ctx.MatchSystem();
|
||||
if (target == null)
|
||||
throw new PKError("Unknown system.");
|
||||
|
||||
await ctx.Reply($"To delete the following system, reply with the system's UUID: `{target.Uuid.ToString()}`",
|
||||
await CreateEmbed(ctx, target));
|
||||
if (!await ctx.ConfirmWithReply(target.Uuid.ToString()))
|
||||
throw new PKError("System deletion cancelled.");
|
||||
|
||||
await ctx.BusyIndicator(async () =>
|
||||
await ctx.Repository.DeleteSystem(target.Id));
|
||||
await ctx.Reply($"{Emojis.Success} System deletion succesful.");
|
||||
}
|
||||
|
||||
public async Task AbuseLogCreate(Context ctx)
|
||||
{
|
||||
var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage");
|
||||
var account = await ctx.MatchUser();
|
||||
if (account == null)
|
||||
throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention).");
|
||||
|
||||
string? desc = null!;
|
||||
if (ctx.HasNext(false))
|
||||
desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
|
||||
|
||||
var abuseLog = await ctx.Repository.CreateAbuseLog(desc, denyBotUsage);
|
||||
await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id);
|
||||
|
||||
await ctx.Reply(
|
||||
$"Created new abuse log with UUID `{abuseLog.Uuid.ToString()}`.",
|
||||
await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogShow(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
await ctx.Reply(null, await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogFlagDeny(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
await ctx.Reply(
|
||||
$"Bot usage is currently {(abuseLog.DenyBotUsage ? "denied" : "allowed")} "
|
||||
+ $"for accounts associated with abuse log `{abuseLog.Uuid}`.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = ctx.MatchToggle(true);
|
||||
if (abuseLog.DenyBotUsage != value)
|
||||
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value });
|
||||
|
||||
await ctx.Reply(
|
||||
$"Bot usage is now **{(value ? "denied" : "allowed")}** "
|
||||
+ $"for accounts associated with abuse log `{abuseLog.Uuid}`.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AbuseLogDescription(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
if (ctx.MatchClear() && await ctx.ConfirmClear("this abuse log description"))
|
||||
{
|
||||
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null });
|
||||
await ctx.Reply($"{Emojis.Success} Abuse log description cleared.");
|
||||
}
|
||||
else if (ctx.HasNext())
|
||||
{
|
||||
var desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
|
||||
await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = desc });
|
||||
await ctx.Reply($"{Emojis.Success} Abuse log description updated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for abuse log `{abuseLog.Uuid}`");
|
||||
await ctx.Reply(abuseLog.Description, eb.Build());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AbuseLogAddUser(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
var account = await ctx.MatchUser();
|
||||
if (account == null)
|
||||
throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention).");
|
||||
|
||||
await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id);
|
||||
await ctx.Reply(
|
||||
$"Added user {account.NameAndMention()} to the abuse log with UUID `{abuseLog.Uuid.ToString()}`.",
|
||||
await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogRemoveUser(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
var account = await ctx.MatchUser();
|
||||
if (account == null)
|
||||
throw new PKError("You must pass an account to remove from the abuse log (either ID or @mention).");
|
||||
|
||||
await ctx.Repository.UpdateAccount(account.Id, new()
|
||||
{
|
||||
AbuseLog = null,
|
||||
});
|
||||
|
||||
await ctx.Reply(
|
||||
$"Removed user {account.NameAndMention()} from the abuse log with UUID `{abuseLog.Uuid.ToString()}`.",
|
||||
await CreateAbuseLogEmbed(ctx, abuseLog));
|
||||
}
|
||||
|
||||
public async Task AbuseLogDelete(Context ctx, AbuseLog abuseLog)
|
||||
{
|
||||
if (!await ctx.PromptYesNo($"Really delete abuse log entry `{abuseLog.Uuid}`?", "Delete", matchFlag: false))
|
||||
{
|
||||
await ctx.Reply($"{Emojis.Error} Deletion cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Repository.DeleteAbuseLog(abuseLog.Id);
|
||||
await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry.");
|
||||
}
|
||||
}
|
||||
|
|
@ -143,25 +143,33 @@ public class Api
|
|||
if (_webhookRegex.IsMatch(newUrl))
|
||||
throw new PKError("PluralKit does not currently support setting a Discord webhook URL as your system's webhook URL.");
|
||||
|
||||
try
|
||||
{
|
||||
await _dispatch.DoPostRequest(ctx.System.Id, newUrl, null, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new PKError($"Could not verify that the new URL is working: {e.Message}");
|
||||
}
|
||||
|
||||
var newToken = StringUtils.GenerateToken();
|
||||
|
||||
await ctx.Reply($"{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you."
|
||||
+ " If it is exposed publicly, you **must** clear and re-set the webhook URL to get a new token."
|
||||
+ "\n\n**Please review the security requirements at <https://pluralkit.me/api/dispatch#security> before continuing.**"
|
||||
+ "\n\nWhen the server is correctly validating the token, click or reply 'yes' to continue."
|
||||
);
|
||||
if (!await ctx.PromptYesNo(newToken, "Continue", matchFlag: false))
|
||||
throw Errors.GenericCancelled();
|
||||
|
||||
var status = await _dispatch.TestUrl(ctx.System.Uuid, newUrl, newToken);
|
||||
if (status != "OK")
|
||||
{
|
||||
var message = status switch
|
||||
{
|
||||
"BadData" => "the webhook url is invalid",
|
||||
"NoIPs" => "could not find any valid IP addresses for the provided domain",
|
||||
"InvalidIP" => "could not find any valid IP addresses for the provided domain",
|
||||
"FetchFailed" => "unable to reach server",
|
||||
"TestFailed" => "server failed to validate the signing token",
|
||||
_ => $"an unknown error occurred ({status})"
|
||||
};
|
||||
throw new PKError($"Failed to validate the webhook url: {message}");
|
||||
}
|
||||
|
||||
await ctx.Repository.UpdateSystem(ctx.System.Id, new SystemPatch { WebhookUrl = newUrl, WebhookToken = newToken });
|
||||
|
||||
await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system."
|
||||
+ $"\n\n{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you."
|
||||
+ " If it leaks, you should clear and re-set the webhook URL to get a new token."
|
||||
+ "\ntodo: add link to docs or something"
|
||||
);
|
||||
|
||||
await ctx.Reply(newToken);
|
||||
await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system.");
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,7 @@ public class Autoproxy
|
|||
{
|
||||
if (relevantMember == null)
|
||||
throw new ArgumentException("Attempted to print member autoproxy status, but the linked member ID wasn't found in the database. Should be handled appropriately.");
|
||||
eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
|
||||
eb.Description($"Autoproxy is currently set to **front mode** in this server. The current (first) fronter is **{relevantMember.NameFor(ctx).EscapeMarkdown()}** (`{relevantMember.DisplayHid(ctx.Config)}`). To disable, type `pk;autoproxy off`.");
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -142,7 +142,7 @@ public class Autoproxy
|
|||
// ideally we would set it to off in the database though...
|
||||
eb.Description($"Autoproxy is currently **off** in this server. To enable it, use one of the following commands:\n{commandList}");
|
||||
else
|
||||
eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`) in this server. To disable, type `pk;autoproxy off`.");
|
||||
eb.Description($"Autoproxy is active for member **{relevantMember.NameFor(ctx)}** (`{relevantMember.DisplayHid(ctx.Config)}`) in this server. To disable, type `pk;autoproxy off`.");
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ public class Autoproxy
|
|||
if (relevantMember == null)
|
||||
eb.Description("Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. **No member is currently latched.** To disable, type `pk;autoproxy off`.");
|
||||
else
|
||||
eb.Description($"Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. The currently latched member is **{relevantMember.NameFor(ctx)}** (`{relevantMember.Hid}`). To disable, type `pk;autoproxy off`.");
|
||||
eb.Description($"Autoproxy is currently set to **latch mode**, meaning the *last-proxied member* will be autoproxied. The currently latched member is **{relevantMember.NameFor(ctx)}** (`{relevantMember.DisplayHid(ctx.Config)}`). To disable, type `pk;autoproxy off`.");
|
||||
|
||||
break;
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ public class Checks
|
|||
var error = "Channel not found or you do not have permissions to access it.";
|
||||
|
||||
// todo: this breaks if channel is not in cache and bot does not have View Channel permissions
|
||||
// with new cache it breaks if channel is not in current guild
|
||||
var channel = await ctx.MatchChannel();
|
||||
if (channel == null || channel.GuildId == null)
|
||||
throw new PKError(error);
|
||||
|
|
@ -156,7 +157,8 @@ public class Checks
|
|||
if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
||||
throw new PKError(error);
|
||||
|
||||
var botPermissions = PermissionExtensions.PermissionsFor(guild, channel, _botConfig.ClientId, guildMember);
|
||||
// todo: permcheck channel outside of guild?
|
||||
var botPermissions = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id);
|
||||
|
||||
// We use a bitfield so we can set individual permission bits
|
||||
ulong missingPermissions = 0;
|
||||
|
|
@ -231,15 +233,21 @@ public class Checks
|
|||
var channel = await _rest.GetChannelOrNull(channelId.Value);
|
||||
if (channel == null)
|
||||
throw new PKError("Unable to get the channel associated with this message.");
|
||||
|
||||
var rootChannel = await _cache.GetRootChannel(channel.Id);
|
||||
if (channel.GuildId == null)
|
||||
throw new PKError("PluralKit is not able to proxy messages in DMs.");
|
||||
|
||||
var rootChannel = await _cache.GetRootChannel(channel.GuildId!.Value, channel.Id);
|
||||
|
||||
// using channel.GuildId here since _rest.GetMessage() doesn't return the GuildId
|
||||
var context = await ctx.Repository.GetMessageContext(msg.Author.Id, channel.GuildId.Value, rootChannel.Id, msg.ChannelId);
|
||||
var members = (await ctx.Repository.GetProxyMembers(msg.Author.Id, channel.GuildId.Value)).ToList();
|
||||
|
||||
if (await ctx.Repository.GetSystemByAccount(msg.Author.Id) == null)
|
||||
{
|
||||
await ctx.Reply("Your account does not have a system registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
// for now this is just server
|
||||
var autoproxySettings = await ctx.Repository.GetAutoproxySettings(ctx.System.Id, channel.GuildId.Value, null);
|
||||
|
||||
|
|
|
|||
|
|
@ -97,11 +97,46 @@ public class Config
|
|||
|
||||
items.Add(new(
|
||||
"Proxy error",
|
||||
"Whether to send an error message when proxying fails.",
|
||||
"Whether to send an error message when proxying fails",
|
||||
EnabledDisabled(ctx.Config.ProxyErrorMessageEnabled),
|
||||
"enabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"Split IDs",
|
||||
"Whether to display 6-character IDs split with a hyphen, to ease readability",
|
||||
EnabledDisabled(ctx.Config.HidDisplaySplit),
|
||||
"disabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"Capitalize IDs",
|
||||
"Whether to display IDs as capital letters, to ease readability",
|
||||
EnabledDisabled(ctx.Config.HidDisplayCaps),
|
||||
"disabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"Pad IDs",
|
||||
"Whether to pad 5-character IDs in lists (left/right)",
|
||||
ctx.Config.HidListPadding.ToUserString(),
|
||||
"off"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"Proxy Switch",
|
||||
"Whether using a proxy tag logs a switch",
|
||||
EnabledDisabled(ctx.Config.ProxySwitch),
|
||||
"disabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"Name Format",
|
||||
"Format string used to display a member's name https://pluralkit.me/guide/#setting-a-custom-name-format",
|
||||
ctx.Config.NameFormat,
|
||||
ProxyMember.DefaultFormat
|
||||
));
|
||||
|
||||
await ctx.Paginate<PaginatedConfigItem>(
|
||||
items.ToAsyncEnumerable(),
|
||||
items.Count,
|
||||
|
|
@ -443,4 +478,115 @@ public class Config
|
|||
await ctx.Reply("Proxy error messages are now disabled. Messages that fail to proxy (due to message or attachment size) will not throw an error message.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HidDisplaySplit(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
var msg = $"Splitting of 6-character IDs with a hyphen is currently **{EnabledDisabled(ctx.Config.HidDisplaySplit)}**.";
|
||||
await ctx.Reply(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var newVal = ctx.MatchToggle(false);
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplaySplit = newVal });
|
||||
await ctx.Reply($"Splitting of 6-character IDs with a hyphen is now {EnabledDisabled(newVal)}.");
|
||||
}
|
||||
|
||||
public async Task HidDisplayCaps(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
var msg = $"Displaying IDs as capital letters is currently **{EnabledDisabled(ctx.Config.HidDisplayCaps)}**.";
|
||||
await ctx.Reply(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var newVal = ctx.MatchToggle(false);
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidDisplayCaps = newVal });
|
||||
await ctx.Reply($"Displaying IDs as capital letters is now {EnabledDisabled(newVal)}.");
|
||||
}
|
||||
|
||||
public async Task HidListPadding(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
string message;
|
||||
switch (ctx.Config.HidListPadding)
|
||||
{
|
||||
case SystemConfig.HidPadFormat.None: message = "Padding 5-character IDs in lists is currently disabled."; break;
|
||||
case SystemConfig.HidPadFormat.Left: message = "5-character IDs displayed in lists will have a padding space added to the beginning."; break;
|
||||
case SystemConfig.HidPadFormat.Right: message = "5-character IDs displayed in lists will have a padding space added to the end."; break;
|
||||
default: throw new Exception("unreachable");
|
||||
}
|
||||
await ctx.Reply(message);
|
||||
return;
|
||||
}
|
||||
|
||||
var badInputError = "Valid padding settings are `left`, `right`, or `off`.";
|
||||
|
||||
var toggleOff = ctx.MatchToggleOrNull(false);
|
||||
|
||||
switch (toggleOff)
|
||||
{
|
||||
case true: throw new PKError(badInputError);
|
||||
case false:
|
||||
{
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.None });
|
||||
await ctx.Reply("Padding 5-character IDs in lists has been disabled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.Match("left", "l"))
|
||||
{
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Left });
|
||||
await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the beginning.");
|
||||
}
|
||||
else if (ctx.Match("right", "r"))
|
||||
{
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { HidListPadding = SystemConfig.HidPadFormat.Right });
|
||||
await ctx.Reply("5-character IDs displayed in lists will now have a padding space added to the end.");
|
||||
}
|
||||
else throw new PKError(badInputError);
|
||||
}
|
||||
|
||||
public async Task ProxySwitch(Context ctx)
|
||||
{
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
var msg = $"Logging a switch every time a proxy tag is used is currently **{EnabledDisabled(ctx.Config.ProxySwitch)}**.";
|
||||
await ctx.Reply(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var newVal = ctx.MatchToggle(false);
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { ProxySwitch = newVal });
|
||||
await ctx.Reply($"Logging a switch every time a proxy tag is used is now {EnabledDisabled(newVal)}.");
|
||||
}
|
||||
|
||||
public async Task NameFormat(Context ctx)
|
||||
{
|
||||
var clearFlag = ctx.MatchClear();
|
||||
if (!ctx.HasNext() && !clearFlag)
|
||||
{
|
||||
await ctx.Reply($"Member names are currently formatted as `{ctx.Config.NameFormat ?? ProxyMember.DefaultFormat}`");
|
||||
return;
|
||||
}
|
||||
|
||||
string formatString;
|
||||
if (clearFlag)
|
||||
formatString = ProxyMember.DefaultFormat;
|
||||
else
|
||||
formatString = ctx.RemainderOrNull();
|
||||
|
||||
await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { NameFormat = formatString });
|
||||
await ctx.Reply($"Member names are now formatted as `{formatString}`");
|
||||
}
|
||||
|
||||
public Task LimitUpdate(Context ctx)
|
||||
{
|
||||
throw new PKError("You cannot update your own member or group limits. If you need a limit update, please join the " +
|
||||
"support server and ask in #bot-support: https://discord.gg/PczBt78");
|
||||
}
|
||||
}
|
||||
|
|
@ -53,41 +53,55 @@ public class GroupMember
|
|||
|
||||
public async Task ListMemberGroups(Context ctx, PKMember target)
|
||||
{
|
||||
var pctx = ctx.DirectLookupContextFor(target.System);
|
||||
var targetSystem = await ctx.Repository.GetSystem(target.System);
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
|
||||
opts.MemberFilter = target.Id;
|
||||
|
||||
var groups = await ctx.Repository.GetMemberGroups(target.Id)
|
||||
.Where(g => g.Visibility.CanAccess(pctx))
|
||||
.OrderBy(g => (g.DisplayName ?? g.Name), StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToListAsync();
|
||||
|
||||
var description = "";
|
||||
var msg = "";
|
||||
|
||||
if (groups.Count == 0)
|
||||
description = "This member has no groups.";
|
||||
else
|
||||
description = string.Join("\n", groups.Select(g => $"[`{g.Hid}`] **{g.DisplayName ?? g.Name}**"));
|
||||
|
||||
if (pctx == LookupContext.ByOwner)
|
||||
var title = new StringBuilder($"Groups containing {target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
|
||||
if (ctx.Guild != null)
|
||||
{
|
||||
msg +=
|
||||
$"\n\nTo add this member to one or more groups, use `pk;m {target.Reference(ctx)} group add <group> [group 2] [group 3...]`";
|
||||
if (groups.Count > 0)
|
||||
msg +=
|
||||
$"\nTo remove this member from one or more groups, use `pk;m {target.Reference(ctx)} group remove <group> [group 2] [group 3...]`";
|
||||
var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id);
|
||||
if (guildSettings.DisplayName != null)
|
||||
title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else if (targetSystem.NameFor(ctx) != null)
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targetSystem.NameFor(ctx) != null)
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
|
||||
}
|
||||
if (opts.Search != null)
|
||||
title.Append($" matching **{opts.Search.Truncate(100)}**");
|
||||
|
||||
await ctx.Reply(msg, new EmbedBuilder().Title($"{target.Name}'s groups").Description(description).Build());
|
||||
await ctx.RenderGroupList(ctx.LookupContextFor(target.System), target.System, title.ToString(),
|
||||
target.Color, opts);
|
||||
}
|
||||
|
||||
public async Task AddRemoveMembers(Context ctx, PKGroup target, Groups.AddRemoveOperation op)
|
||||
{
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
var members = (await ctx.ParseMemberList(ctx.System.Id))
|
||||
List<MemberId> members;
|
||||
if (ctx.MatchFlag("all", "a"))
|
||||
{
|
||||
members = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
|
||||
new DatabaseViewsExt.ListQueryOptions { })))
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
members = (await ctx.ParseMemberList(ctx.System.Id))
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var existingMembersInGroup = (await ctx.Database.Execute(conn => conn.QueryMemberList(target.System,
|
||||
new DatabaseViewsExt.ListQueryOptions { GroupFilter = target.Id })))
|
||||
|
|
@ -109,6 +123,9 @@ public class GroupMember
|
|||
toAction = members
|
||||
.Where(m => existingMembersInGroup.Contains(m.Value))
|
||||
.ToList();
|
||||
|
||||
if (ctx.MatchFlag("all", "a") && !await ctx.PromptYesNo($"Are you sure you want to remove all members from group {target.Reference(ctx)}?", "Empty Group")) throw Errors.GenericCancelled();
|
||||
|
||||
await ctx.Repository.RemoveMembersFromGroup(target.Id, toAction);
|
||||
}
|
||||
else
|
||||
|
|
@ -127,26 +144,26 @@ public class GroupMember
|
|||
var targetSystem = await GetGroupSystem(ctx, target);
|
||||
ctx.CheckSystemPrivacy(targetSystem.Id, target.ListPrivacy);
|
||||
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.System), ctx.LookupContextFor(target.System));
|
||||
opts.GroupFilter = target.Id;
|
||||
|
||||
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.Hid}`) in ");
|
||||
var title = new StringBuilder($"Members of {target.DisplayName ?? target.Name} (`{target.DisplayHid(ctx.Config)}`) in ");
|
||||
if (ctx.Guild != null)
|
||||
{
|
||||
var guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, targetSystem.Id);
|
||||
if (guildSettings.DisplayName != null)
|
||||
title.Append($"{guildSettings.DisplayName} (`{targetSystem.Hid}`)");
|
||||
title.Append($"{guildSettings.DisplayName} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else if (targetSystem.NameFor(ctx) != null)
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.Hid}`)");
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{targetSystem.Hid}`");
|
||||
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targetSystem.NameFor(ctx) != null)
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.Hid}`)");
|
||||
title.Append($"{targetSystem.NameFor(ctx)} (`{targetSystem.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{targetSystem.Hid}`");
|
||||
title.Append($"`{targetSystem.DisplayHid(ctx.Config)}`");
|
||||
}
|
||||
if (opts.Search != null)
|
||||
title.Append($" matching **{opts.Search.Truncate(100)}**");
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ public class Groups
|
|||
private readonly HttpClient _client;
|
||||
private readonly DispatchService _dispatch;
|
||||
private readonly EmbedService _embeds;
|
||||
private readonly AvatarHostingService _avatarHosting;
|
||||
|
||||
public Groups(EmbedService embeds, HttpClient client,
|
||||
DispatchService dispatch)
|
||||
DispatchService dispatch, AvatarHostingService avatarHosting)
|
||||
{
|
||||
_embeds = embeds;
|
||||
_client = client;
|
||||
_dispatch = dispatch;
|
||||
_avatarHosting = avatarHosting;
|
||||
}
|
||||
|
||||
public async Task CreateGroup(Context ctx)
|
||||
|
|
@ -44,14 +46,14 @@ public class Groups
|
|||
var groupLimit = ctx.Config.GroupLimitOverride ?? Limits.MaxGroupCount;
|
||||
if (existingGroupCount >= groupLimit)
|
||||
throw new PKError(
|
||||
$"System has reached the maximum number of groups ({groupLimit}). Please delete unused groups first in order to create new ones.");
|
||||
$"System has reached the maximum number of groups ({groupLimit}). If you need to add more groups, you can either delete existing groups, or ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>");
|
||||
|
||||
// Warn if there's already a group by this name
|
||||
var existingGroup = await ctx.Repository.GetGroupByName(ctx.System.Id, groupName);
|
||||
if (existingGroup != null)
|
||||
{
|
||||
var msg =
|
||||
$"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?";
|
||||
$"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to create another group with the same name?";
|
||||
if (!await ctx.PromptYesNo(msg, "Create"))
|
||||
throw new PKError("Group creation cancelled.");
|
||||
}
|
||||
|
|
@ -81,7 +83,7 @@ public class Groups
|
|||
|
||||
var eb = new EmbedBuilder()
|
||||
.Description(
|
||||
$"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:")
|
||||
$"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.DisplayHid(ctx.Config)}`**.\nBelow are a couple of useful commands:")
|
||||
.Field(new Embed.Field("View the group card", $"> pk;group **{reference}**"))
|
||||
.Field(new Embed.Field("Add members to the group",
|
||||
$"> pk;group **{reference}** add **MemberName**\n> pk;group **{reference}** add **Member1** **Member2** **Member3** (and so on...)"))
|
||||
|
|
@ -93,7 +95,7 @@ public class Groups
|
|||
|
||||
if (existingGroupCount >= Limits.WarnThreshold(groupLimit))
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} groups). Please review your group list for unused or duplicate groups.");
|
||||
$"{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} groups). Once you reach this limit, you will be unable to create new groups until existing groups are deleted, or you can ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>");
|
||||
}
|
||||
|
||||
public async Task RenameGroup(Context ctx, PKGroup target)
|
||||
|
|
@ -111,7 +113,7 @@ public class Groups
|
|||
if (existingGroup != null && existingGroup.Id != target.Id)
|
||||
{
|
||||
var msg =
|
||||
$"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to rename this group to that name too?";
|
||||
$"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.DisplayHid(ctx.Config)}`). Do you want to rename this group to that name too?";
|
||||
if (!await ctx.PromptYesNo(msg, "Rename"))
|
||||
throw new PKError("Group rename cancelled.");
|
||||
}
|
||||
|
|
@ -130,40 +132,47 @@ public class Groups
|
|||
|
||||
// No perms check, display name isn't covered by member privacy
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noDisplayNameSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing displayname for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
await ctx.Reply(target.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noDisplayNameSetMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Field(new Embed.Field("Name", target.Name))
|
||||
.Field(new Embed.Field("Display Name", target.DisplayName));
|
||||
var eb = new EmbedBuilder()
|
||||
.Field(new Embed.Field("Name", target.Name))
|
||||
.Field(new Embed.Field("Display Name", target.DisplayName));
|
||||
|
||||
var reference = target.Reference(ctx);
|
||||
var reference = target.Reference(ctx);
|
||||
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Description(
|
||||
$"To change display name, type `pk;group {reference} displayname <display name>`.\n"
|
||||
+ $"To clear it, type `pk;group {reference} displayname -clear`.\n"
|
||||
+ $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Description(
|
||||
$"To change display name, type `pk;group {reference} displayname <display name>`.\n"
|
||||
+ $"To clear it, type `pk;group {reference} displayname -clear`.\n"
|
||||
+ $"To print the raw display name, type `pk;group {reference} displayname -raw`.");
|
||||
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
|
||||
if (ctx.System?.Id == target.System)
|
||||
eb.Footer(new Embed.EmbedFooter($"Using {target.DisplayName.Length}/{Limits.MaxGroupNameLength} characters."));
|
||||
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -182,6 +191,8 @@ public class Groups
|
|||
else
|
||||
{
|
||||
var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing();
|
||||
if (newDisplayName.Length > Limits.MaxGroupNameLength)
|
||||
throw new PKError($"Group name too long ({newDisplayName.Length}/{Limits.MaxGroupNameLength} characters).");
|
||||
|
||||
var patch = new GroupPatch { DisplayName = Partial<string>.Present(newDisplayName) };
|
||||
await ctx.Repository.UpdateGroup(target.Id, patch);
|
||||
|
|
@ -199,30 +210,41 @@ public class Groups
|
|||
noDescriptionSetMessage +=
|
||||
$" To set one, type `pk;group {target.Reference(ctx)} description <description>`.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Description == null)
|
||||
{
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
await ctx.Reply(target.Description, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Description == null)
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -251,6 +273,7 @@ public class Groups
|
|||
{
|
||||
async Task ClearIcon()
|
||||
{
|
||||
await ctx.ConfirmClear("this group's icon");
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = null });
|
||||
|
|
@ -261,22 +284,24 @@ public class Groups
|
|||
{
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
|
||||
|
||||
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.Url });
|
||||
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url });
|
||||
|
||||
var msg = img.Source switch
|
||||
{
|
||||
AvatarSource.User =>
|
||||
$"{Emojis.Success} Group icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the group icon will need to be re-set.",
|
||||
AvatarSource.Url => $"{Emojis.Success} Group icon changed to the image at the given URL.",
|
||||
AvatarSource.HostedCdn => $"{Emojis.Success} Group icon changed to attached image.",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} Group icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the group icon will stop working.",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
|
||||
await (hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
|
||||
: ctx.Reply(msg));
|
||||
|
|
@ -300,11 +325,11 @@ public class Groups
|
|||
else
|
||||
{
|
||||
throw new PKSyntaxError(
|
||||
"This group does not have an icon set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
|
||||
"This group does not have an avatar set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.MatchClear() && await ctx.ConfirmClear("this group's icon"))
|
||||
if (ctx.MatchClear())
|
||||
await ClearIcon();
|
||||
else if (await ctx.MatchImage() is { } img)
|
||||
await SetIcon(img);
|
||||
|
|
@ -316,6 +341,7 @@ public class Groups
|
|||
{
|
||||
async Task ClearBannerImage()
|
||||
{
|
||||
await ctx.ConfirmClear("this group's banner image");
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = null });
|
||||
|
|
@ -326,13 +352,15 @@ public class Groups
|
|||
{
|
||||
ctx.CheckOwnGroup(target);
|
||||
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
|
||||
|
||||
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.Url });
|
||||
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url });
|
||||
|
||||
var msg = img.Source switch
|
||||
{
|
||||
AvatarSource.Url => $"{Emojis.Success} Group banner image changed to the image at the given URL.",
|
||||
AvatarSource.HostedCdn => $"{Emojis.Success} Group banner image changed to attached image.",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} Group banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.",
|
||||
AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."),
|
||||
|
|
@ -340,7 +368,7 @@ public class Groups
|
|||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
|
||||
await (hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
|
||||
: ctx.Reply(msg));
|
||||
|
|
@ -348,7 +376,7 @@ public class Groups
|
|||
|
||||
async Task ShowBannerImage()
|
||||
{
|
||||
ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy);
|
||||
ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy);
|
||||
|
||||
if ((target.BannerImage?.Trim() ?? "").Length > 0)
|
||||
{
|
||||
|
|
@ -364,11 +392,11 @@ public class Groups
|
|||
else
|
||||
{
|
||||
throw new PKSyntaxError(
|
||||
"This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
|
||||
"This group does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL.");
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.MatchClear() && await ctx.ConfirmClear("this group's banner image"))
|
||||
if (ctx.MatchClear())
|
||||
await ClearBannerImage();
|
||||
else if (await ctx.MatchImage() is { } img)
|
||||
await SetBannerImage(img);
|
||||
|
|
@ -379,7 +407,7 @@ public class Groups
|
|||
public async Task GroupColor(Context ctx, PKGroup target)
|
||||
{
|
||||
var isOwnSystem = ctx.System?.Id == target.System;
|
||||
var matchedRaw = ctx.MatchRaw();
|
||||
var matchedFormat = ctx.MatchFormat();
|
||||
var matchedClear = ctx.MatchClear();
|
||||
|
||||
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
|
||||
|
|
@ -387,8 +415,10 @@ public class Groups
|
|||
if (target.Color == null)
|
||||
await ctx.Reply(
|
||||
"This group does not have a color set." + (isOwnSystem ? $" To set one, type `pk;group {target.Reference(ctx)} color <color>`." : ""));
|
||||
else if (matchedRaw)
|
||||
else if (matchedFormat == ReplyFormat.Raw)
|
||||
await ctx.Reply("```\n#" + target.Color + "\n```");
|
||||
else if (matchedFormat == ReplyFormat.Plaintext)
|
||||
await ctx.Reply(target.Color);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Group color")
|
||||
|
|
@ -440,24 +470,24 @@ public class Groups
|
|||
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
|
||||
// - RenderGroupList checks the indivual privacy for each member (NameFor, etc)
|
||||
// the own system is always allowed to look up their list
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(system.Id), ctx.LookupContextFor(system.Id));
|
||||
await ctx.RenderGroupList(
|
||||
ctx.LookupContextFor(system.Id),
|
||||
system.Id,
|
||||
GetEmbedTitle(system, opts),
|
||||
GetEmbedTitle(ctx, system, opts),
|
||||
system.Color,
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
private string GetEmbedTitle(PKSystem target, ListOptions opts)
|
||||
private string GetEmbedTitle(Context ctx, PKSystem target, ListOptions opts)
|
||||
{
|
||||
var title = new StringBuilder("Groups of ");
|
||||
|
||||
if (target.Name != null)
|
||||
title.Append($"{target.Name} (`{target.Hid}`)");
|
||||
if (target.NameFor(ctx) != null)
|
||||
title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{target.Hid}`");
|
||||
title.Append($"`{target.DisplayHid(ctx.Config)}`");
|
||||
|
||||
if (opts.Search != null)
|
||||
title.Append($" matching **{opts.Search}**");
|
||||
|
|
@ -481,12 +511,13 @@ public class Groups
|
|||
.Title($"Current privacy settings for {target.Name}")
|
||||
.Field(new Embed.Field("Name", target.NamePrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Icon", target.IconPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Member list", target.ListPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Metadata (creation date)", target.MetadataPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Visibility", target.Visibility.Explanation()))
|
||||
.Description(
|
||||
$"To edit privacy settings, use the command:\n> pk;group **{target.Reference(ctx)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `name`, `description`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
|
||||
$"To edit privacy settings, use the command:\n> pk;group **{target.Reference(ctx)}** privacy **<subject>** **<level>**\n\n- `subject` is one of `name`, `description`, `banner`, `icon`, `members`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
|
@ -511,6 +542,7 @@ public class Groups
|
|||
{
|
||||
GroupPrivacySubject.Name => "name privacy",
|
||||
GroupPrivacySubject.Description => "description privacy",
|
||||
GroupPrivacySubject.Banner => "banner privacy",
|
||||
GroupPrivacySubject.Icon => "icon privacy",
|
||||
GroupPrivacySubject.List => "member list",
|
||||
GroupPrivacySubject.Metadata => "metadata",
|
||||
|
|
@ -524,6 +556,8 @@ public class Groups
|
|||
"This group's name is now hidden from other systems, and will be replaced by the group's display name.",
|
||||
(GroupPrivacySubject.Description, PrivacyLevel.Private) =>
|
||||
"This group's description is now hidden from other systems.",
|
||||
(GroupPrivacySubject.Banner, PrivacyLevel.Private) =>
|
||||
"This group's banner is now hidden from other systems.",
|
||||
(GroupPrivacySubject.Icon, PrivacyLevel.Private) =>
|
||||
"This group's icon is now hidden from other systems.",
|
||||
(GroupPrivacySubject.Visibility, PrivacyLevel.Private) =>
|
||||
|
|
@ -537,6 +571,8 @@ public class Groups
|
|||
"This group's name is no longer hidden from other systems.",
|
||||
(GroupPrivacySubject.Description, PrivacyLevel.Public) =>
|
||||
"This group's description is no longer hidden from other systems.",
|
||||
(GroupPrivacySubject.Banner, PrivacyLevel.Public) =>
|
||||
"This group's banner is no longer hidden from other systems.",
|
||||
(GroupPrivacySubject.Icon, PrivacyLevel.Public) =>
|
||||
"This group's icon is no longer hidden from other systems.",
|
||||
(GroupPrivacySubject.Visibility, PrivacyLevel.Public) =>
|
||||
|
|
@ -568,10 +604,10 @@ public class Groups
|
|||
ctx.CheckOwnGroup(target);
|
||||
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} Are you sure you want to delete this group? If so, reply to this message with the group's ID (`{target.Hid}`).\n**Note: this action is permanent.**");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid))
|
||||
$"{Emojis.Warn} Are you sure you want to delete this group? If so, reply to this message with the group's ID (`{target.DisplayHid(ctx.Config)}`).\n**Note: this action is permanent.**");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid, treatAsHid: true))
|
||||
throw new PKError(
|
||||
$"Group deletion cancelled. Note that you must reply with your group ID (`{target.Hid}`) *verbatim*.");
|
||||
$"Group deletion cancelled. Note that you must reply with your group ID (`{target.DisplayHid(ctx.Config)}`) *verbatim*.");
|
||||
|
||||
await ctx.Repository.DeleteGroup(target.Id);
|
||||
|
||||
|
|
@ -580,7 +616,7 @@ public class Groups
|
|||
|
||||
public async Task DisplayId(Context ctx, PKGroup target)
|
||||
{
|
||||
await ctx.Reply(target.Hid);
|
||||
await ctx.Reply(target.DisplayHid(ctx.Config));
|
||||
}
|
||||
|
||||
private async Task<PKSystem> GetGroupSystem(Context ctx, PKGroup target)
|
||||
|
|
|
|||
|
|
@ -11,12 +11,32 @@ public class Help
|
|||
{
|
||||
Title = "PluralKit",
|
||||
Description = "PluralKit is a bot designed for plural communities on Discord, and is open for anyone to use. It allows you to register systems, maintain system information, set up message proxying, log switches, and more.",
|
||||
Footer = new("By @Ske#6201 | Myriad design by @Layl#8888, art by https://twitter.com/sillyvizion | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"),
|
||||
Footer = new("By @ske | Myriad design by @layl, icon by @tedkalashnikov, banner by @fulmine | GitHub: https://github.com/PluralKit/PluralKit/ | Website: https://pluralkit.me/"),
|
||||
Color = DiscordUtils.Blue,
|
||||
};
|
||||
|
||||
private static Dictionary<string, Embed.Field[]> helpEmbedPages = new Dictionary<string, Embed.Field[]>
|
||||
{
|
||||
{
|
||||
"default",
|
||||
new Embed.Field[]
|
||||
{
|
||||
new
|
||||
(
|
||||
"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 `pk;token` and then store it in a safe place.\n\n"
|
||||
+ "Keep your token safe, if other people get access to it they can also use it to access your system. "
|
||||
+ "If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one."
|
||||
),
|
||||
new
|
||||
(
|
||||
"Use the buttons below to see more info!",
|
||||
""
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
"basicinfo",
|
||||
new Embed.Field[]
|
||||
|
|
@ -29,9 +49,9 @@ public class Help
|
|||
),
|
||||
new
|
||||
(
|
||||
"Why are people's names saying [BOT] next to them?",
|
||||
"These people are not actually bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation."
|
||||
),
|
||||
"Why are people's names saying [APP] or [BOT] next to them?",
|
||||
"These people are not actually apps or bots, this is just a Discord limitation. See [the documentation](https://pluralkit.me/guide#proxying) for an in-depth explanation."
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -137,7 +157,9 @@ public class Help
|
|||
public Task HelpRoot(Context ctx)
|
||||
=> ctx.Rest.CreateMessage(ctx.Channel.Id, new MessageRequest
|
||||
{
|
||||
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } },
|
||||
Content = $"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ](<https://pluralkit.me/faq/#why-do-most-of-pluralkit-s-messages-look-blank-or-empty>)",
|
||||
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description,
|
||||
Fields = helpEmbedPages.GetValueOrDefault("default") } },
|
||||
Components = new[] { helpPageButtons(ctx.Author.Id) },
|
||||
});
|
||||
|
||||
|
|
@ -151,7 +173,7 @@ public class Help
|
|||
if (ctx.Event.Message.Components.First().Components.Where(x => x.CustomId == ctx.CustomId).First().Style == ButtonStyle.Primary)
|
||||
return ctx.Respond(InteractionResponse.ResponseType.UpdateMessage, new()
|
||||
{
|
||||
Embeds = new[] { helpEmbed with { Description = helpEmbed.Description + "\n\n**Use the buttons below to see more info!**" } },
|
||||
Embeds = new[] { helpEmbed with { Fields = helpEmbedPages.GetValueOrDefault("default") } },
|
||||
Components = new[] { buttons }
|
||||
});
|
||||
|
||||
|
|
@ -168,8 +190,10 @@ public class Help
|
|||
{
|
||||
"> **About PluralKit**\nPluralKit detects messages enclosed in specific tags associated with a profile, then replaces that message under a \"pseudo-account\" of that profile using Discord webhooks.",
|
||||
"This is useful for multiple people sharing one body (aka. *systems*), people who wish to role-play as different characters without having multiple Discord accounts, or anyone else who may want to post messages under a different identity from the same Discord account.",
|
||||
"Due to Discord limitations, these messages will show up with the `[BOT]` tag - however, they are not bots."
|
||||
"Due to Discord limitations, these messages will show up with the `[APP]` or `[BOT]` tag - however, they are not apps or bots."
|
||||
});
|
||||
|
||||
public Task Explain(Context ctx) => ctx.Reply(explanation);
|
||||
|
||||
public Task Dashboard(Context ctx) => ctx.Reply("The PluralKit dashboard is at <https://dash.pluralkit.me>");
|
||||
}
|
||||
|
|
@ -45,13 +45,20 @@ public class ImportExport
|
|||
try
|
||||
{
|
||||
var response = await _client.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw Errors.InvalidImportFile;
|
||||
// hacky fix for discord api returning nonsense charsets sometimes
|
||||
response.Content.Headers.Remove("content-type");
|
||||
response.Content.Headers.Add("content-type", "application/json; charset=UTF-8");
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (content == "This content is no longer available.")
|
||||
{
|
||||
var refreshed = await ctx.Rest.RefreshUrls(new[] { url.ToString() });
|
||||
response = await _client.GetAsync(new Uri(refreshed.RefreshedUrls[0].Refreshed));
|
||||
content = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw Errors.InvalidImportFile;
|
||||
data = JsonConvert.DeserializeObject<JObject>(
|
||||
await response.Content.ReadAsStringAsync(),
|
||||
content,
|
||||
_settings
|
||||
);
|
||||
if (data == null)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace PluralKit.Bot;
|
|||
|
||||
public static class ContextListExt
|
||||
{
|
||||
public static ListOptions ParseListOptions(this Context ctx, LookupContext lookupCtx)
|
||||
public static ListOptions ParseListOptions(this Context ctx, LookupContext directLookupCtx, LookupContext lookupContext)
|
||||
{
|
||||
var p = new ListOptions();
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ public static class ContextListExt
|
|||
p.SortProperty = SortProperty.LastSwitch;
|
||||
if (ctx.MatchFlag("by-last-message", "blm", "blp")) p.SortProperty = SortProperty.LastMessage;
|
||||
if (ctx.MatchFlag("by-birthday", "by-birthdate", "bbd")) p.SortProperty = SortProperty.Birthdate;
|
||||
if (ctx.MatchFlag("random")) p.SortProperty = SortProperty.Random;
|
||||
if (ctx.MatchFlag("random", "rand")) p.SortProperty = SortProperty.Random;
|
||||
|
||||
// Sort reverse?
|
||||
if (ctx.MatchFlag("r", "rev", "reverse"))
|
||||
|
|
@ -55,10 +55,13 @@ public static class ContextListExt
|
|||
if (ctx.MatchFlag("private-only", "po")) p.PrivacyFilter = PrivacyLevel.Private;
|
||||
|
||||
// PERM CHECK: If we're trying to access non-public members of another system, error
|
||||
if (p.PrivacyFilter != PrivacyLevel.Public && lookupCtx != LookupContext.ByOwner)
|
||||
if (p.PrivacyFilter != PrivacyLevel.Public && directLookupCtx != LookupContext.ByOwner)
|
||||
// TODO: should this just return null instead of throwing or something? >.>
|
||||
throw Errors.NotOwnInfo;
|
||||
|
||||
//this is for searching
|
||||
p.Context = lookupContext;
|
||||
|
||||
// Additional fields to include in the search results
|
||||
if (ctx.MatchFlag("with-last-switch", "with-last-fronted", "with-last-front", "wls", "wlf"))
|
||||
p.IncludeLastSwitch = true;
|
||||
|
|
@ -124,11 +127,14 @@ public static class ContextListExt
|
|||
|
||||
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedMember> page)
|
||||
{
|
||||
// if there are both 5 and 6 character Hids they should be padded to align correctly.
|
||||
var shouldPad = page.Any(x => x.Hid.Length > 5);
|
||||
|
||||
// We may end up over the description character limit
|
||||
// so run it through a helper that "makes it work" :)
|
||||
eb.WithSimpleLineContent(page.Select(m =>
|
||||
{
|
||||
var ret = $"[`{m.Hid}`] **{m.NameFor(ctx)}** ";
|
||||
var ret = $"[`{m.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{m.NameFor(ctx)}** ";
|
||||
|
||||
if (opts.IncludeMessageCount && m.MessageCountFor(lookupCtx) is { } count)
|
||||
ret += $"({count} messages)";
|
||||
|
|
@ -162,7 +168,7 @@ public static class ContextListExt
|
|||
{
|
||||
foreach (var m in page)
|
||||
{
|
||||
var profile = new StringBuilder($"**ID**: {m.Hid}");
|
||||
var profile = new StringBuilder($"**ID**: {m.DisplayHid(ctx.Config)}");
|
||||
|
||||
if (m.DisplayName != null && m.NamePrivacy.CanAccess(lookupCtx))
|
||||
profile.Append($"\n**Display name**: {m.DisplayName}");
|
||||
|
|
@ -234,11 +240,14 @@ public static class ContextListExt
|
|||
|
||||
void ShortRenderer(EmbedBuilder eb, IEnumerable<ListedGroup> page)
|
||||
{
|
||||
// if there are both 5 and 6 character Hids they should be padded to align correctly.
|
||||
var shouldPad = page.Any(x => x.Hid.Length > 5);
|
||||
|
||||
// We may end up over the description character limit
|
||||
// so run it through a helper that "makes it work" :)
|
||||
eb.WithSimpleLineContent(page.Select(g =>
|
||||
{
|
||||
var ret = $"[`{g.Hid}`] **{g.NameFor(ctx)}** ";
|
||||
var ret = $"[`{g.DisplayHid(ctx.Config, isList: true, shouldPad: shouldPad)}`] **{g.NameFor(ctx)}** ";
|
||||
|
||||
switch (opts.SortProperty)
|
||||
{
|
||||
|
|
@ -308,7 +317,7 @@ public static class ContextListExt
|
|||
{
|
||||
foreach (var g in page)
|
||||
{
|
||||
var profile = new StringBuilder($"**ID**: {g.Hid}");
|
||||
var profile = new StringBuilder($"**ID**: {g.DisplayHid(ctx.Config)}");
|
||||
|
||||
if (g.DisplayName != null && g.NamePrivacy.CanAccess(lookupCtx))
|
||||
profile.Append($"\n**Display name**: {g.DisplayName}");
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ public class ListOptions
|
|||
public bool Reverse { get; set; }
|
||||
|
||||
public PrivacyLevel? PrivacyFilter { get; set; } = PrivacyLevel.Public;
|
||||
public LookupContext Context { get; set; } = LookupContext.ByNonOwner;
|
||||
public GroupId? GroupFilter { get; set; }
|
||||
public MemberId? MemberFilter { get; set; }
|
||||
public string? Search { get; set; }
|
||||
public bool SearchDescription { get; set; }
|
||||
|
||||
|
|
@ -96,8 +98,10 @@ public class ListOptions
|
|||
{
|
||||
PrivacyFilter = PrivacyFilter,
|
||||
GroupFilter = GroupFilter,
|
||||
MemberFilter = MemberFilter,
|
||||
Search = Search,
|
||||
SearchDescription = SearchDescription
|
||||
SearchDescription = SearchDescription,
|
||||
Context = Context
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,15 @@ public class Member
|
|||
private readonly HttpClient _client;
|
||||
private readonly DispatchService _dispatch;
|
||||
private readonly EmbedService _embeds;
|
||||
private readonly AvatarHostingService _avatarHosting;
|
||||
|
||||
public Member(EmbedService embeds, HttpClient client,
|
||||
DispatchService dispatch)
|
||||
DispatchService dispatch, AvatarHostingService avatarHosting)
|
||||
{
|
||||
_embeds = embeds;
|
||||
_client = client;
|
||||
_dispatch = dispatch;
|
||||
_avatarHosting = avatarHosting;
|
||||
}
|
||||
|
||||
public async Task NewMember(Context ctx)
|
||||
|
|
@ -38,7 +40,7 @@ public class Member
|
|||
var existingMember = await ctx.Repository.GetMemberByName(ctx.System.Id, memberName);
|
||||
if (existingMember != null)
|
||||
{
|
||||
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?";
|
||||
var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.DisplayHid(ctx.Config)}`). Do you want to create another member with the same name?";
|
||||
if (!await ctx.PromptYesNo(msg, "Create")) throw new PKError("Member creation cancelled.");
|
||||
}
|
||||
|
||||
|
|
@ -67,13 +69,24 @@ public class Member
|
|||
// Try to match an image attached to the message
|
||||
var avatarArg = ctx.Message.Attachments.FirstOrDefault();
|
||||
Exception imageMatchError = null;
|
||||
ParsedImage img = new();
|
||||
if (avatarArg != null)
|
||||
try
|
||||
{
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Url);
|
||||
await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = avatarArg.Url }, conn);
|
||||
// XXX: discord attachment URLs are unable to be validated without their query params
|
||||
// keep both the URL with query (for validation) and the clean URL (for storage) around
|
||||
var uriBuilder = new UriBuilder(avatarArg.ProxyUrl);
|
||||
img = new ParsedImage { Url = uriBuilder.Uri.AbsoluteUri, Source = AvatarSource.Attachment };
|
||||
|
||||
dispatchData.Add("avatar_url", avatarArg.Url);
|
||||
uriBuilder.Query = "";
|
||||
img.CleanUrl = uriBuilder.Uri.AbsoluteUri;
|
||||
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
|
||||
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
|
||||
await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = img.CleanUrl ?? img.Url }, conn);
|
||||
|
||||
dispatchData.Add("avatar_url", img.CleanUrl);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -88,34 +101,34 @@ public class Member
|
|||
|
||||
// Send confirmation and space hint
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member");
|
||||
$"{Emojis.Success} Member \"{memberName}\" (`{member.DisplayHid(ctx.Config)}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member");
|
||||
// todo: move this to ModelRepository
|
||||
if (await ctx.Database.Execute(conn => conn.QuerySingleAsync<bool>("select has_private_members(@System)",
|
||||
new { System = ctx.System.Id })) && !ctx.Config.MemberDefaultPrivate) //if has private members
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`.");
|
||||
$"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.DisplayHid(ctx.Config)} private`.");
|
||||
if (avatarArg != null)
|
||||
if (imageMatchError == null)
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} Member avatar set to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working.");
|
||||
$"{Emojis.Success} Member avatar set to attached image." + (img.Source == AvatarSource.Attachment ? $"\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working." : ""));
|
||||
else
|
||||
await ctx.Reply($"{Emojis.Error} Couldn't set avatar: {imageMatchError.Message}");
|
||||
if (memberName.Contains(" "))
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`).");
|
||||
$"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's short ID (which is `{member.DisplayHid(ctx.Config)}`).");
|
||||
if (memberCount >= memberLimit)
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} You have reached the per-system member limit ({memberLimit}). You will be unable to create additional members until existing members are deleted.");
|
||||
$"{Emojis.Warn} You have reached the per-system member limit ({memberLimit}). If you need to add more members, you can either delete existing members, or ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>");
|
||||
else if (memberCount >= Limits.WarnThreshold(memberLimit))
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Please review your member list for unused or duplicate members.");
|
||||
$"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Once you reach this limit, you will be unable to create new members until existing members are deleted, or you can ask for your limit to be raised in the PluralKit support server: <https://discord.gg/PczBt78>");
|
||||
}
|
||||
|
||||
public async Task ViewMember(Context ctx, PKMember target)
|
||||
{
|
||||
var system = await ctx.Repository.GetSystem(target.System);
|
||||
await ctx.Reply(
|
||||
embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system.Id), ctx.Zone));
|
||||
embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.Config, ctx.LookupContextFor(system.Id), ctx.Zone));
|
||||
}
|
||||
|
||||
public async Task Soulscream(Context ctx, PKMember target)
|
||||
|
|
@ -143,6 +156,6 @@ public class Member
|
|||
|
||||
public async Task DisplayId(Context ctx, PKMember target)
|
||||
{
|
||||
await ctx.Reply(target.Hid);
|
||||
await ctx.Reply(target.DisplayHid(ctx.Config));
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,12 @@ namespace PluralKit.Bot;
|
|||
public class MemberAvatar
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly AvatarHostingService _avatarHosting;
|
||||
|
||||
public MemberAvatar(HttpClient client)
|
||||
public MemberAvatar(HttpClient client, AvatarHostingService avatarHosting)
|
||||
{
|
||||
_client = client;
|
||||
_avatarHosting = avatarHosting;
|
||||
}
|
||||
|
||||
private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
|
||||
|
|
@ -121,9 +123,10 @@ public class MemberAvatar
|
|||
MemberGuildSettings? guildData)
|
||||
{
|
||||
// First, see if we need to *clear*
|
||||
if (ctx.MatchClear() && await ctx.ConfirmClear("this member's " + location.Name()))
|
||||
if (ctx.MatchClear())
|
||||
{
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
await ctx.ConfirmClear("this member's " + location.Name());
|
||||
await AvatarClear(location, ctx, target, guildData);
|
||||
return;
|
||||
}
|
||||
|
|
@ -138,8 +141,10 @@ public class MemberAvatar
|
|||
}
|
||||
|
||||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Value.Url);
|
||||
await UpdateAvatar(location, ctx, target, avatarArg.Value.Url);
|
||||
await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url);
|
||||
await PrintResponse(location, ctx, target, avatarArg.Value, guildData);
|
||||
}
|
||||
|
||||
|
|
@ -170,13 +175,15 @@ public class MemberAvatar
|
|||
$"{Emojis.Success} Member {location.Name()} changed to {avatar.SourceUser?.Username}'s avatar!{serverFrag}\n{Emojis.Warn} If {avatar.SourceUser?.Username} changes their avatar, the member's avatar will need to be re-set.",
|
||||
AvatarSource.Url =>
|
||||
$"{Emojis.Success} Member {location.Name()} changed to the image at the given URL.{serverFrag}",
|
||||
AvatarSource.HostedCdn =>
|
||||
$"{Emojis.Success} Member {location.Name()} changed to attached image.{serverFrag}",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} Member {location.Name()} changed to attached image.{serverFrag}\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working.",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = avatar.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = avatar.Source != AvatarSource.Attachment && avatar.Source != AvatarSource.HostedCdn;
|
||||
return hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(avatar.Url)).Build())
|
||||
: ctx.Reply(msg);
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ namespace PluralKit.Bot;
|
|||
public class MemberEdit
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly AvatarHostingService _avatarHosting;
|
||||
|
||||
public MemberEdit(HttpClient client)
|
||||
public MemberEdit(HttpClient client, AvatarHostingService avatarHosting)
|
||||
{
|
||||
_client = client;
|
||||
_avatarHosting = avatarHosting;
|
||||
}
|
||||
|
||||
public async Task Name(Context ctx, PKMember target)
|
||||
|
|
@ -34,7 +36,7 @@ public class MemberEdit
|
|||
if (existingMember != null && existingMember.Id != target.Id)
|
||||
{
|
||||
var msg =
|
||||
$"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?";
|
||||
$"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.DisplayHid(ctx.Config)}`). Do you want to rename this member to that name too?";
|
||||
if (!await ctx.PromptYesNo(msg, "Rename")) throw new PKError("Member renaming cancelled.");
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +47,7 @@ public class MemberEdit
|
|||
await ctx.Reply($"{Emojis.Success} Member renamed (using {newName.Length}/{Limits.MaxMemberNameLength} characters).");
|
||||
if (newName.Contains(" "))
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it.");
|
||||
$"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's short ID (which is `{target.DisplayHid(ctx.Config)}`).");
|
||||
if (target.DisplayName != null)
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Note} Note that this member has a display name set ({target.DisplayName}), and will be proxied using that name instead.");
|
||||
|
|
@ -68,30 +70,41 @@ public class MemberEdit
|
|||
noDescriptionSetMessage +=
|
||||
$" To set one, type `pk;member {target.Reference(ctx)} description <description>`.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Description == null)
|
||||
{
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
await ctx.Reply(target.Description, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Description == null)
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member description")
|
||||
.Description(target.Description)
|
||||
.Field(new Embed.Field("\u200B",
|
||||
$"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -120,30 +133,41 @@ public class MemberEdit
|
|||
{
|
||||
var noPronounsSetMessage = "This member does not have pronouns set.";
|
||||
if (ctx.System?.Id == target.System)
|
||||
noPronounsSetMessage += $"To set some, type `pk;member {target.Reference(ctx)} pronouns <pronouns>`.";
|
||||
noPronounsSetMessage += $" To set some, type `pk;member {target.Reference(ctx)} pronouns <pronouns>`.";
|
||||
|
||||
ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy);
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Pronouns == null)
|
||||
{
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing pronouns for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
await ctx.Reply(target.Pronouns, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Pronouns == null)
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply(
|
||||
$"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
await ctx.Reply(
|
||||
$"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`."
|
||||
+ (ctx.System?.Id == target.System
|
||||
? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -180,13 +204,15 @@ public class MemberEdit
|
|||
|
||||
async Task SetBannerImage(ParsedImage img)
|
||||
{
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
|
||||
|
||||
await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.Url });
|
||||
await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.CleanUrl ?? img.Url });
|
||||
|
||||
var msg = img.Source switch
|
||||
{
|
||||
AvatarSource.Url => $"{Emojis.Success} Member banner image changed to the image at the given URL.",
|
||||
AvatarSource.HostedCdn => $"{Emojis.Success} Member banner image changed to attached image.",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} Member banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.",
|
||||
AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."),
|
||||
|
|
@ -194,7 +220,7 @@ public class MemberEdit
|
|||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
|
||||
await (hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
|
||||
: ctx.Reply(msg));
|
||||
|
|
@ -207,13 +233,13 @@ public class MemberEdit
|
|||
var eb = new EmbedBuilder()
|
||||
.Title($"{target.NameFor(ctx)}'s banner image")
|
||||
.Image(new Embed.EmbedImage(target.BannerImage))
|
||||
.Description($"To clear, use `pk;member {target.Hid} banner clear`.");
|
||||
.Description($"To clear, use `pk;member {target.Reference(ctx)} banner clear`.");
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PKSyntaxError(
|
||||
"This member does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
|
||||
"This member does not have a banner image set. Set one by attaching an image to this command, or by passing an image URL.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,7 +254,7 @@ public class MemberEdit
|
|||
public async Task Color(Context ctx, PKMember target)
|
||||
{
|
||||
var isOwnSystem = ctx.System?.Id == target.System;
|
||||
var matchedRaw = ctx.MatchRaw();
|
||||
var matchedFormat = ctx.MatchFormat();
|
||||
var matchedClear = ctx.MatchClear();
|
||||
|
||||
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
|
||||
|
|
@ -236,8 +262,10 @@ public class MemberEdit
|
|||
if (target.Color == null)
|
||||
await ctx.Reply(
|
||||
"This member does not have a color set." + (isOwnSystem ? $" To set one, type `pk;member {target.Reference(ctx)} color <color>`." : ""));
|
||||
else if (matchedRaw)
|
||||
else if (matchedFormat == ReplyFormat.Raw)
|
||||
await ctx.Reply("```\n#" + target.Color + "\n```");
|
||||
else if (matchedFormat == ReplyFormat.Plaintext)
|
||||
await ctx.Reply(target.Color);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("Member color")
|
||||
|
|
@ -335,7 +363,7 @@ public class MemberEdit
|
|||
var eb = new EmbedBuilder()
|
||||
.Title("Member names")
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
$"Member ID: {target.Hid} | Active name in bold. Server name overrides display name, which overrides base name."
|
||||
$"Member ID: {target.DisplayHid(ctx.Config)} | Active name in bold. Server name overrides display name, which overrides base name."
|
||||
+ (target.DisplayName != null && ctx.System?.Id == target.System ? $" Using {target.DisplayName.Length}/{Limits.MaxMemberNameLength} characters for the display name." : "")
|
||||
+ (memberGuildConfig?.DisplayName != null ? $" Using {memberGuildConfig?.DisplayName.Length}/{Limits.MaxMemberNameLength} characters for the server name." : "")));
|
||||
|
||||
|
|
@ -384,12 +412,26 @@ public class MemberEdit
|
|||
|
||||
// No perms check, display name isn't covered by member privacy
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if what's next is "raw"/"plaintext" we need to check for null
|
||||
if (format != ReplyFormat.Standard)
|
||||
if (target.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noDisplayNameSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing displayname for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
await ctx.Reply(target.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -446,12 +488,26 @@ public class MemberEdit
|
|||
// No perms check, display name isn't covered by member privacy
|
||||
var memberGuildConfig = await ctx.Repository.GetMemberGuild(ctx.Guild.Id, target.Id);
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if what's next is "raw"/"plaintext" we need to check for null
|
||||
if (format != ReplyFormat.Standard)
|
||||
if (memberGuildConfig.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noServerNameSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{memberGuildConfig.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing servername for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
await ctx.Reply(memberGuildConfig.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -700,6 +756,7 @@ public class MemberEdit
|
|||
.Field(new Embed.Field("Name (replaces name with display name if member has one)",
|
||||
target.NamePrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Birthday", target.BirthdayPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation()))
|
||||
|
|
@ -708,7 +765,7 @@ public class MemberEdit
|
|||
target.MetadataPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Visibility", target.MemberVisibility.Explanation()))
|
||||
.Description(
|
||||
"To edit privacy settings, use the command:\n`pk;member <member> privacy <subject> <level>`\n\n- `subject` is one of `name`, `description`, `avatar`, `birthday`, `pronouns`, `proxies`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
|
||||
"To edit privacy settings, use the command:\n`pk;member <member> privacy <subject> <level>`\n\n- `subject` is one of `name`, `description`, `banner`, `avatar`, `birthday`, `pronouns`, `proxies`, `metadata`, `visibility`, or `all`\n- `level` is either `public` or `private`.")
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
|
@ -738,6 +795,7 @@ public class MemberEdit
|
|||
{
|
||||
MemberPrivacySubject.Name => "name privacy",
|
||||
MemberPrivacySubject.Description => "description privacy",
|
||||
MemberPrivacySubject.Banner => "banner privacy",
|
||||
MemberPrivacySubject.Avatar => "avatar privacy",
|
||||
MemberPrivacySubject.Pronouns => "pronoun privacy",
|
||||
MemberPrivacySubject.Birthday => "birthday privacy",
|
||||
|
|
@ -753,6 +811,8 @@ public class MemberEdit
|
|||
"This member's name is now hidden from other systems, and will be replaced by the member's display name.",
|
||||
(MemberPrivacySubject.Description, PrivacyLevel.Private) =>
|
||||
"This member's description is now hidden from other systems.",
|
||||
(MemberPrivacySubject.Banner, PrivacyLevel.Private) =>
|
||||
"This member's banner is now hidden from other systems.",
|
||||
(MemberPrivacySubject.Avatar, PrivacyLevel.Private) =>
|
||||
"This member's avatar is now hidden from other systems.",
|
||||
(MemberPrivacySubject.Birthday, PrivacyLevel.Private) =>
|
||||
|
|
@ -770,6 +830,8 @@ public class MemberEdit
|
|||
"This member's name is no longer hidden from other systems.",
|
||||
(MemberPrivacySubject.Description, PrivacyLevel.Public) =>
|
||||
"This member's description is no longer hidden from other systems.",
|
||||
(MemberPrivacySubject.Banner, PrivacyLevel.Public) =>
|
||||
"This member's banner is no longer hidden from other systems.",
|
||||
(MemberPrivacySubject.Avatar, PrivacyLevel.Public) =>
|
||||
"This member's avatar is no longer hidden from other systems.",
|
||||
(MemberPrivacySubject.Birthday, PrivacyLevel.Public) =>
|
||||
|
|
@ -812,8 +874,8 @@ public class MemberEdit
|
|||
ctx.CheckSystem().CheckOwnMember(target);
|
||||
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx)}\"? If so, reply to this message with the member's ID (`{target.Hid}`). __***This cannot be undone!***__");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid)) throw Errors.MemberDeleteCancelled;
|
||||
$"{Emojis.Warn} Are you sure you want to delete \"{target.NameFor(ctx)}\"? If so, reply to this message with the member's ID (`{target.DisplayHid(ctx.Config)}`). __***This cannot be undone!***__");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid, treatAsHid: true)) throw Errors.MemberDeleteCancelled;
|
||||
|
||||
await ctx.Repository.DeleteMember(target.Id);
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class MemberProxy
|
|||
if (!ctx.HasNext(false))
|
||||
throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`).");
|
||||
|
||||
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false));
|
||||
var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
|
||||
if (tagToAdd.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
|
||||
if (target.ProxyTags.Contains(tagToAdd))
|
||||
throw Errors.ProxyTagAlreadyExists(tagToAdd, target);
|
||||
|
|
@ -87,10 +87,17 @@ public class MemberProxy
|
|||
if (!ctx.HasNext(false))
|
||||
throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`).");
|
||||
|
||||
var tagToRemove = ParseProxyTags(ctx.RemainderOrNull(false));
|
||||
var remainder = ctx.RemainderOrNull(false);
|
||||
var tagToRemove = ParseProxyTags(remainder.NormalizeLineEndSpacing());
|
||||
if (tagToRemove.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
|
||||
if (!target.ProxyTags.Contains(tagToRemove))
|
||||
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
|
||||
{
|
||||
// Legacy support for when line endings weren't normalized
|
||||
tagToRemove = ParseProxyTags(remainder);
|
||||
if (!target.ProxyTags.Contains(tagToRemove))
|
||||
throw Errors.ProxyTagDoesNotExist(tagToRemove, target);
|
||||
}
|
||||
|
||||
|
||||
var newTags = target.ProxyTags.ToList();
|
||||
newTags.Remove(tagToRemove);
|
||||
|
|
@ -102,7 +109,7 @@ public class MemberProxy
|
|||
// Subcommand: bare proxy tag given
|
||||
else
|
||||
{
|
||||
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false));
|
||||
var requestedTag = ParseProxyTags(ctx.RemainderOrNull(false).NormalizeLineEndSpacing());
|
||||
if (requestedTag.IsEmpty) throw Errors.EmptyProxyTags(target, ctx);
|
||||
|
||||
// This is mostly a legacy command, so it's gonna warn if there's
|
||||
|
|
|
|||
|
|
@ -39,11 +39,12 @@ public class ProxiedMessage
|
|||
private readonly WebhookExecutorService _webhookExecutor;
|
||||
private readonly ProxyService _proxy;
|
||||
private readonly LastMessageCacheService _lastMessageCache;
|
||||
private readonly RedisService _redisService;
|
||||
|
||||
public ProxiedMessage(EmbedService embeds,
|
||||
DiscordApiClient rest, IMetrics metrics, ModelRepository repo, ProxyService proxy,
|
||||
WebhookExecutorService webhookExecutor, LogChannelService logChannel, IDiscordCache cache,
|
||||
LastMessageCacheService lastMessageCache)
|
||||
LastMessageCacheService lastMessageCache, RedisService redisService)
|
||||
{
|
||||
_embeds = embeds;
|
||||
_rest = rest;
|
||||
|
|
@ -54,6 +55,7 @@ public class ProxiedMessage
|
|||
_metrics = metrics;
|
||||
_proxy = proxy;
|
||||
_lastMessageCache = lastMessageCache;
|
||||
_redisService = redisService;
|
||||
}
|
||||
|
||||
public async Task ReproxyMessage(Context ctx)
|
||||
|
|
@ -91,7 +93,7 @@ public class ProxiedMessage
|
|||
}
|
||||
}
|
||||
|
||||
public async Task EditMessage(Context ctx)
|
||||
public async Task EditMessage(Context ctx, bool useRegex)
|
||||
{
|
||||
var (msg, systemId) = await GetMessageToEdit(ctx, EditTimeout, false);
|
||||
|
||||
|
|
@ -103,7 +105,7 @@ public class ProxiedMessage
|
|||
throw new PKError("Could not edit message.");
|
||||
|
||||
// Regex flag
|
||||
var useRegex = ctx.MatchFlag("regex", "x");
|
||||
useRegex = useRegex || ctx.MatchFlag("regex", "x");
|
||||
|
||||
// Check if we should append or prepend
|
||||
var mutateSpace = ctx.MatchFlag("nospace", "ns") ? "" : " ";
|
||||
|
|
@ -116,7 +118,8 @@ public class ProxiedMessage
|
|||
|
||||
// Should we clear embeds?
|
||||
var clearEmbeds = ctx.MatchFlag("clear-embed", "ce");
|
||||
if (clearEmbeds && newContent == null)
|
||||
var clearAttachments = ctx.MatchFlag("clear-attachments", "ca");
|
||||
if ((clearEmbeds || clearAttachments) && newContent == null)
|
||||
newContent = originalMsg.Content!;
|
||||
|
||||
if (newContent == null)
|
||||
|
|
@ -216,11 +219,13 @@ public class ProxiedMessage
|
|||
try
|
||||
{
|
||||
var editedMsg =
|
||||
await _webhookExecutor.EditWebhookMessage(msg.Channel, msg.Mid, newContent, clearEmbeds);
|
||||
await _webhookExecutor.EditWebhookMessage(msg.Guild ?? 0, msg.Channel, msg.Mid, newContent, clearEmbeds, clearAttachments);
|
||||
|
||||
if (ctx.Guild == null)
|
||||
await _rest.CreateReaction(ctx.Channel.Id, ctx.Message.Id, new Emoji { Name = Emojis.Success });
|
||||
|
||||
await _redisService.SetOriginalMid(ctx.Message.Id, editedMsg.Id);
|
||||
|
||||
if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages))
|
||||
await _rest.DeleteMessage(ctx.Channel.Id, ctx.Message.Id);
|
||||
|
||||
|
|
@ -348,7 +353,9 @@ public class ProxiedMessage
|
|||
else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel))
|
||||
showContent = false;
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
if (format != ReplyFormat.Standard)
|
||||
{
|
||||
var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid);
|
||||
if (discordMessage == null || !showContent)
|
||||
|
|
@ -361,21 +368,32 @@ public class ProxiedMessage
|
|||
return;
|
||||
}
|
||||
|
||||
await ctx.Reply($"```{content}```");
|
||||
|
||||
if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline))
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
await ctx.Rest.CreateMessage(
|
||||
ctx.Channel.Id,
|
||||
new MessageRequest
|
||||
{
|
||||
Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment."
|
||||
},
|
||||
new[] { new MultipartFile("message.txt", stream, null, null, null) });
|
||||
await ctx.Reply($"```{content}```");
|
||||
|
||||
if (Regex.IsMatch(content, "```.*```", RegexOptions.Singleline))
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(content));
|
||||
await ctx.Rest.CreateMessage(
|
||||
ctx.Channel.Id,
|
||||
new MessageRequest
|
||||
{
|
||||
Content = $"{Emojis.Warn} Message contains codeblocks, raw source sent as an attachment."
|
||||
},
|
||||
new[] { new MultipartFile("message.txt", stream, null, null, null) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing contents of message {message.Message.Mid}");
|
||||
await ctx.Reply(content, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDelete)
|
||||
|
|
@ -383,7 +401,8 @@ public class ProxiedMessage
|
|||
if (!showContent)
|
||||
throw new PKError(noShowContentError);
|
||||
|
||||
if (message.System?.Id != ctx.System.Id && message.Message.Sender != ctx.Author.Id)
|
||||
// if user has has a system and their system sent the message, or if user sent the message, do not error
|
||||
if (!((ctx.System != null && message.System?.Id == ctx.System.Id) || message.Message.Sender == ctx.Author.Id))
|
||||
throw new PKError("You can only delete your own messages.");
|
||||
|
||||
await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid);
|
||||
|
|
@ -414,19 +433,19 @@ public class ProxiedMessage
|
|||
return;
|
||||
}
|
||||
|
||||
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent));
|
||||
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message, showContent, ctx.Config));
|
||||
}
|
||||
|
||||
private async Task DeleteCommandMessage(Context ctx, ulong messageId)
|
||||
{
|
||||
var (authorId, channelId) = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (authorId == null)
|
||||
var cmessage = await ctx.Services.Resolve<CommandMessageService>().GetCommandMessage(messageId);
|
||||
if (cmessage == null)
|
||||
throw Errors.MessageNotFound(messageId);
|
||||
|
||||
if (authorId != ctx.Author.Id)
|
||||
if (cmessage!.AuthorId != ctx.Author.Id)
|
||||
throw new PKError("You can only delete command messages queried by this account.");
|
||||
|
||||
await ctx.Rest.DeleteMessage(channelId!.Value, messageId);
|
||||
await ctx.Rest.DeleteMessage(cmessage.ChannelId, messageId);
|
||||
|
||||
if (ctx.Guild != null)
|
||||
await ctx.Rest.DeleteMessage(ctx.Message);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ public class Random
|
|||
|
||||
var randInt = randGen.Next(members.Count);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(target, members[randInt], ctx.Guild,
|
||||
ctx.LookupContextFor(target.Id), ctx.Zone));
|
||||
ctx.Config, ctx.LookupContextFor(target.Id), ctx.Zone));
|
||||
}
|
||||
|
||||
public async Task Group(Context ctx, PKSystem target)
|
||||
|
|
@ -67,7 +67,7 @@ public class Random
|
|||
{
|
||||
ctx.CheckSystemPrivacy(group.System, group.ListPrivacy);
|
||||
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(group.System), ctx.LookupContextFor(group.System));
|
||||
opts.GroupFilter = group.Id;
|
||||
|
||||
var members = await ctx.Database.Execute(conn => conn.QueryMemberList(group.System, opts.ToQueryOptions()));
|
||||
|
|
@ -93,6 +93,6 @@ public class Random
|
|||
|
||||
var randInt = randGen.Next(ms.Count);
|
||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, ms[randInt], ctx.Guild,
|
||||
ctx.LookupContextFor(group.System), ctx.Zone));
|
||||
ctx.Config, ctx.LookupContextFor(group.System), ctx.Zone));
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,93 @@ public class ServerConfig
|
|||
_cache = cache;
|
||||
}
|
||||
|
||||
private record PaginatedConfigItem(string Key, string Description, string? CurrentValue, string DefaultValue);
|
||||
private string EnabledDisabled(bool value) => value ? "enabled" : "disabled";
|
||||
|
||||
public async Task ShowConfig(Context ctx)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
var items = new List<PaginatedConfigItem>();
|
||||
|
||||
// TODO: move log channel / blacklist into here
|
||||
|
||||
items.Add(new(
|
||||
"log cleanup",
|
||||
"Whether to clean up other bots' log channels",
|
||||
EnabledDisabled(ctx.GuildConfig!.LogCleanupEnabled),
|
||||
"disabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"invalid command error",
|
||||
"Whether to show an error message when an unknown command is sent",
|
||||
EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled),
|
||||
"enabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"require tag",
|
||||
"Whether server users are required to have a system tag on proxied messages",
|
||||
EnabledDisabled(ctx.GuildConfig!.RequireSystemTag),
|
||||
"disabled"
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"log channel",
|
||||
"Channel to log proxied messages to",
|
||||
ctx.GuildConfig!.LogChannel != null ? $"<#{ctx.GuildConfig.LogChannel}>" : "none",
|
||||
"none"
|
||||
));
|
||||
|
||||
string ChannelListMessage(int count, string cmd) => $"{count} channels, use `pk;scfg {cmd}` to view/update";
|
||||
|
||||
items.Add(new(
|
||||
"log blacklist",
|
||||
"Channels whose proxied messages will not be logged",
|
||||
ChannelListMessage(ctx.GuildConfig!.LogBlacklist.Length, "log blacklist"),
|
||||
ChannelListMessage(0, "log blacklist")
|
||||
));
|
||||
|
||||
items.Add(new(
|
||||
"proxy blacklist",
|
||||
"Channels where message proxying is disabled",
|
||||
ChannelListMessage(ctx.GuildConfig!.Blacklist.Length, "proxy blacklist"),
|
||||
ChannelListMessage(0, "proxy blacklist")
|
||||
));
|
||||
|
||||
await ctx.Paginate<PaginatedConfigItem>(
|
||||
items.ToAsyncEnumerable(),
|
||||
items.Count,
|
||||
10,
|
||||
"Current settings for this server",
|
||||
null,
|
||||
(eb, l) =>
|
||||
{
|
||||
var description = new StringBuilder();
|
||||
|
||||
foreach (var item in l)
|
||||
{
|
||||
description.Append(item.Key.AsCode());
|
||||
description.Append($" **({item.CurrentValue ?? item.DefaultValue})**");
|
||||
if (item.CurrentValue != null && item.CurrentValue != item.DefaultValue)
|
||||
description.Append("\ud83d\udd39");
|
||||
|
||||
description.AppendLine();
|
||||
description.Append(item.Description);
|
||||
description.AppendLine();
|
||||
description.AppendLine();
|
||||
}
|
||||
|
||||
eb.Description(description.ToString());
|
||||
|
||||
// using *large* blue diamond here since it's easier to see in the small footer
|
||||
eb.Footer(new("\U0001f537 means this setting was changed. Type `pk;serverconfig <setting name> clear` to reset it to the default."));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public async Task SetLogChannel(Context ctx)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
|
@ -49,7 +136,7 @@ public class ServerConfig
|
|||
if (channel.Type != Channel.ChannelType.GuildText && channel.Type != Channel.ChannelType.GuildPublicThread && channel.Type != Channel.ChannelType.GuildPrivateThread)
|
||||
throw new PKError("PluralKit cannot log messages to this type of channel.");
|
||||
|
||||
var perms = await _cache.PermissionsIn(channel.Id);
|
||||
var perms = await _cache.BotPermissionsIn(ctx.Guild.Id, channel.Id);
|
||||
if (!perms.HasFlag(PermissionSet.SendMessages))
|
||||
throw new PKError("PluralKit is missing **Send Messages** permissions in the new log channel.");
|
||||
if (!perms.HasFlag(PermissionSet.EmbedLinks))
|
||||
|
|
@ -59,6 +146,8 @@ public class ServerConfig
|
|||
await ctx.Reply($"{Emojis.Success} Proxy logging channel set to <#{channel.Id}>.");
|
||||
}
|
||||
|
||||
// legacy behaviour: enable/disable logging for commands
|
||||
// new behaviour is add/remove from log blacklist (see #LogBlacklistNew)
|
||||
public async Task SetLogEnabled(Context ctx, bool enable)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
|
@ -92,11 +181,11 @@ public class ServerConfig
|
|||
await ctx.Reply(
|
||||
$"{Emojis.Success} Message logging for the given channels {(enable ? "enabled" : "disabled")}." +
|
||||
(logChannel == null
|
||||
? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `pk;log channel #your-log-channel`."
|
||||
? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `pk;serverconfig log channel #your-log-channel`."
|
||||
: ""));
|
||||
}
|
||||
|
||||
public async Task ShowBlacklisted(Context ctx)
|
||||
public async Task ShowProxyBlacklisted(Context ctx)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
||||
|
|
@ -104,14 +193,14 @@ public class ServerConfig
|
|||
|
||||
// Resolve all channels from the cache and order by position
|
||||
var channels = (await Task.WhenAll(blacklist.Blacklist
|
||||
.Select(id => _cache.TryGetChannel(id))))
|
||||
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
|
||||
.Where(c => c != null)
|
||||
.OrderBy(c => c.Position)
|
||||
.ToList();
|
||||
|
||||
if (channels.Count == 0)
|
||||
{
|
||||
await ctx.Reply("This server has no blacklisted channels.");
|
||||
await ctx.Reply("This server has no channels where proxying is disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +210,7 @@ public class ServerConfig
|
|||
async (eb, l) =>
|
||||
{
|
||||
async Task<string> CategoryName(ulong? id) =>
|
||||
id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";
|
||||
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
|
||||
|
||||
ulong? lastCategory = null;
|
||||
|
||||
|
|
@ -153,8 +242,9 @@ public class ServerConfig
|
|||
var config = await ctx.Repository.GetGuild(ctx.Guild.Id);
|
||||
|
||||
// Resolve all channels from the cache and order by position
|
||||
// todo: GetAllChannels?
|
||||
var channels = (await Task.WhenAll(config.LogBlacklist
|
||||
.Select(id => _cache.TryGetChannel(id))))
|
||||
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
|
||||
.Where(c => c != null)
|
||||
.OrderBy(c => c.Position)
|
||||
.ToList();
|
||||
|
|
@ -171,7 +261,7 @@ public class ServerConfig
|
|||
async (eb, l) =>
|
||||
{
|
||||
async Task<string> CategoryName(ulong? id) =>
|
||||
id != null ? (await _cache.GetChannel(id.Value)).Name : "(no category)";
|
||||
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
|
||||
|
||||
ulong? lastCategory = null;
|
||||
|
||||
|
|
@ -197,14 +287,16 @@ public class ServerConfig
|
|||
}
|
||||
|
||||
|
||||
public async Task SetBlacklisted(Context ctx, bool shouldAdd)
|
||||
|
||||
public async Task SetProxyBlacklisted(Context ctx, bool shouldAdd)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
||||
var affectedChannels = new List<Channel>();
|
||||
if (ctx.Match("all"))
|
||||
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
|
||||
.Where(x => x.Type == Channel.ChannelType.GuildText).ToList();
|
||||
// All the channel types you can proxy in
|
||||
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
|
||||
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
|
||||
else
|
||||
while (ctx.HasNext())
|
||||
|
|
@ -229,6 +321,42 @@ public class ServerConfig
|
|||
$"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist.");
|
||||
}
|
||||
|
||||
public async Task SetLogBlacklisted(Context ctx, bool shouldAdd)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
||||
var affectedChannels = new List<Channel>();
|
||||
if (ctx.Match("all"))
|
||||
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
|
||||
// All the channel types you can proxy in
|
||||
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
|
||||
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
|
||||
else
|
||||
while (ctx.HasNext())
|
||||
{
|
||||
var channelString = ctx.PeekArgument();
|
||||
var channel = await ctx.MatchChannel();
|
||||
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
|
||||
affectedChannels.Add(channel);
|
||||
}
|
||||
|
||||
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
|
||||
|
||||
var blacklist = guild.LogBlacklist.ToHashSet();
|
||||
if (shouldAdd)
|
||||
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
|
||||
else
|
||||
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
|
||||
|
||||
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { LogBlacklist = blacklist.ToArray() });
|
||||
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the logging blacklist." +
|
||||
(guild.LogChannel == null
|
||||
? $"\n{Emojis.Warn} Please note that no logging channel is set, so there is nowhere to log messages to. You can set a logging channel using `pk;serverconfig log channel #your-log-channel`."
|
||||
: ""));
|
||||
}
|
||||
|
||||
public async Task SetLogCleanup(Context ctx)
|
||||
{
|
||||
var botList = string.Join(", ", LoggerCleanService.Bots.Select(b => b.Name).OrderBy(x => x.ToLowerInvariant()));
|
||||
|
|
@ -244,20 +372,16 @@ public class ServerConfig
|
|||
}
|
||||
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
||||
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
|
||||
|
||||
bool? newValue = ctx.MatchToggleOrNull();
|
||||
|
||||
if (newValue == null)
|
||||
{
|
||||
var guildCfg = await ctx.Repository.GetGuild(ctx.Guild.Id);
|
||||
if (guildCfg.LogCleanupEnabled)
|
||||
if (ctx.GuildConfig!.LogCleanupEnabled)
|
||||
eb.Description(
|
||||
"Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`.");
|
||||
"Log cleanup is currently **on** for this server. To disable it, type `pk;serverconfig logclean off`.");
|
||||
else
|
||||
eb.Description(
|
||||
"Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`.");
|
||||
"Log cleanup is currently **off** for this server. To enable it, type `pk;serverconfig logclean on`.");
|
||||
await ctx.Reply(embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
|
@ -270,4 +394,36 @@ public class ServerConfig
|
|||
else
|
||||
await ctx.Reply($"{Emojis.Success} Log cleanup has been **disabled** for this server.");
|
||||
}
|
||||
|
||||
public async Task InvalidCommandResponse(Context ctx)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**.";
|
||||
await ctx.Reply(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var newVal = ctx.MatchToggle(false);
|
||||
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = newVal });
|
||||
await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(newVal)}.");
|
||||
}
|
||||
|
||||
public async Task RequireSystemTag(Context ctx)
|
||||
{
|
||||
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
|
||||
|
||||
if (!ctx.HasNext())
|
||||
{
|
||||
var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server.";
|
||||
await ctx.Reply(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var newVal = ctx.MatchToggle(false);
|
||||
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = newVal });
|
||||
await ctx.Reply($"System tags are now **{(newVal ? "required" : "not required")}** for PluralKit users in this server.");
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ namespace PluralKit.Bot;
|
|||
|
||||
public class Switch
|
||||
{
|
||||
|
||||
public async Task SwitchDo(Context ctx)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
|
@ -103,12 +104,69 @@ public class Switch
|
|||
await ctx.Reply($"{Emojis.Success} Switch moved to <t:{newSwitchTime}> ({newSwitchDeltaStr} ago).");
|
||||
}
|
||||
|
||||
public async Task SwitchEdit(Context ctx)
|
||||
public async Task SwitchEdit(Context ctx, bool newSwitch = false)
|
||||
{
|
||||
ctx.CheckSystem();
|
||||
|
||||
var members = await ctx.ParseMemberList(ctx.System.Id);
|
||||
await DoEditCommand(ctx, members);
|
||||
var newMembers = await ctx.ParseMemberList(ctx.System.Id);
|
||||
|
||||
await using var conn = await ctx.Database.Obtain();
|
||||
var currentSwitch = await ctx.Repository.GetLatestSwitch(ctx.System.Id);
|
||||
if (currentSwitch == null)
|
||||
throw Errors.NoRegisteredSwitches;
|
||||
var currentSwitchMembers = await ctx.Repository.GetSwitchMembers(conn, currentSwitch.Id).ToListAsync().AsTask();
|
||||
|
||||
if (ctx.MatchFlag("first", "f"))
|
||||
newMembers = FirstInSwitch(newMembers[0], currentSwitchMembers);
|
||||
else if (ctx.MatchFlag("remove", "r"))
|
||||
newMembers = RemoveFromSwitch(newMembers, currentSwitchMembers);
|
||||
else if (ctx.MatchFlag("append", "a"))
|
||||
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
|
||||
else if (ctx.MatchFlag("prepend", "p"))
|
||||
newMembers = PrependToSwitch(newMembers, currentSwitchMembers);
|
||||
|
||||
if (newSwitch)
|
||||
{
|
||||
// if there's no edit flag, assume we're appending
|
||||
if (!ctx.MatchFlag("first", "f", "remove", "r", "append", "a", "prepend", "p"))
|
||||
newMembers = AppendToSwitch(newMembers, currentSwitchMembers);
|
||||
await DoSwitchCommand(ctx, newMembers);
|
||||
}
|
||||
else
|
||||
await DoEditCommand(ctx, newMembers);
|
||||
}
|
||||
|
||||
public List<PKMember> PrependToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
members.AddRange(currentSwitchMembers);
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public List<PKMember> AppendToSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
currentSwitchMembers.AddRange(members);
|
||||
members = currentSwitchMembers;
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public List<PKMember> RemoveFromSwitch(List<PKMember> members, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
var memberIds = members.Select(m => m.Id.Value);
|
||||
currentSwitchMembers = currentSwitchMembers.Where(m => !memberIds.Contains(m.Id.Value)).ToList();
|
||||
members = currentSwitchMembers;
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public List<PKMember> FirstInSwitch(PKMember member, List<PKMember> currentSwitchMembers)
|
||||
{
|
||||
currentSwitchMembers = currentSwitchMembers.Where(m => m.Id != member.Id).ToList();
|
||||
var members = new List<PKMember> { member };
|
||||
members.AddRange(currentSwitchMembers);
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
public async Task SwitchEditOut(Context ctx)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ using PluralKit.Core;
|
|||
|
||||
namespace PluralKit.Bot;
|
||||
|
||||
using Myriad.Builders;
|
||||
using Myriad.Types;
|
||||
|
||||
public class System
|
||||
{
|
||||
private readonly EmbedService _embeds;
|
||||
|
|
@ -29,9 +32,25 @@ public class System
|
|||
var system = await ctx.Repository.CreateSystem(systemName);
|
||||
await ctx.Repository.AddAccount(system.Id, ctx.Author.Id);
|
||||
|
||||
// TODO: better message, perhaps embed like in groups?
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Success} Your system has been created. Type `pk;system` to view it, and type `pk;system help` for more information about commands you can use now. Now that you have that set up, check out the getting started guide on setting up members and proxies: <https://pluralkit.me/start>");
|
||||
var eb = new EmbedBuilder()
|
||||
.Title(
|
||||
$"{Emojis.Success} Your system has been created.")
|
||||
.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 `pk;system` to view your system and `pk;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. " +
|
||||
"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("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 `pk;token` and then store it in a safe place.\n\n" +
|
||||
"Keep your token safe, if other people get access to it they can also use it to access your system. " +
|
||||
"If your token is ever compromised run `pk;token refresh` to invalidate the old token and get a new one."))
|
||||
.Field(new Embed.Field("Questions?",
|
||||
"Please join the PK server https://discord.gg/PczBt78 if you have any questions, we're happy to help"));
|
||||
await ctx.Reply($"{Emojis.Warn} If you cannot see the rest of this message see [the FAQ](<https://pluralkit.me/faq/#why-do-most-of-pluralkit-s-messages-look-blank-or-empty>)", eb.Build());
|
||||
|
||||
}
|
||||
|
||||
public async Task DisplayId(Context ctx, PKSystem target)
|
||||
|
|
@ -39,6 +58,6 @@ public class System
|
|||
if (target == null)
|
||||
throw Errors.NoSystemError;
|
||||
|
||||
await ctx.Reply(target.Hid);
|
||||
await ctx.Reply(target.DisplayHid(ctx.Config));
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,14 @@ public class SystemEdit
|
|||
private readonly HttpClient _client;
|
||||
private readonly DataFileService _dataFiles;
|
||||
private readonly PrivateChannelService _dmCache;
|
||||
private readonly AvatarHostingService _avatarHosting;
|
||||
|
||||
public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache)
|
||||
public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache, AvatarHostingService avatarHosting)
|
||||
{
|
||||
_dataFiles = dataFiles;
|
||||
_client = client;
|
||||
_dmCache = dmCache;
|
||||
_avatarHosting = avatarHosting;
|
||||
}
|
||||
|
||||
public async Task Name(Context ctx, PKSystem target)
|
||||
|
|
@ -35,24 +37,35 @@ public class SystemEdit
|
|||
if (isOwnSystem)
|
||||
noNameSetMessage += " Type `pk;system name <name>` to set one.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
if (target.Name != null)
|
||||
await ctx.Reply($"```\n{target.Name}\n```");
|
||||
else
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Name == null)
|
||||
{
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Name}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing name for system `{target.DisplayHid(ctx.Config)}`");
|
||||
await ctx.Reply(target.Name, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Name != null)
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**."
|
||||
+ (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "")
|
||||
+ $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
else
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name is currently **{target.Name}**."
|
||||
+ (isOwnSystem ? " Type `pk;system name -clear` to clear it." : "")
|
||||
+ $" Using {target.Name.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -89,24 +102,35 @@ public class SystemEdit
|
|||
|
||||
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
if (settings.DisplayName != null)
|
||||
await ctx.Reply($"```\n{settings.DisplayName}\n```");
|
||||
else
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (settings.DisplayName == null)
|
||||
{
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{settings.DisplayName}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing servername for system `{target.DisplayHid(ctx.Config)}`");
|
||||
await ctx.Reply(settings.DisplayName, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (settings.DisplayName != null)
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**."
|
||||
+ (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "")
|
||||
+ $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
else
|
||||
await ctx.Reply(noNameSetMessage);
|
||||
await ctx.Reply(
|
||||
$"{(isOwnSystem ? "Your" : "This")} system's name for this server is currently **{settings.DisplayName}**."
|
||||
+ (isOwnSystem ? " Type `pk;system servername -clear` to clear it." : "")
|
||||
+ $" Using {settings.DisplayName.Length}/{Limits.MaxSystemNameLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -141,28 +165,39 @@ public class SystemEdit
|
|||
if (isOwnSystem)
|
||||
noDescriptionSetMessage += " To set one, type `pk;s description <description>`.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Description == null)
|
||||
{
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Description}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing description for system `{target.DisplayHid(ctx.Config)}`");
|
||||
await ctx.Reply(target.Description, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Description == null)
|
||||
await ctx.Reply(noDescriptionSetMessage);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System description")
|
||||
.Description(target.Description)
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
"To print the description with formatting, type `pk;s description -raw`."
|
||||
+ (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description <new description>`." : "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System description")
|
||||
.Description(target.Description)
|
||||
.Footer(new Embed.EmbedFooter(
|
||||
"To print the description with formatting, type `pk;s description -raw`."
|
||||
+ (isOwnSystem ? " To clear it, type `pk;s description -clear`. To change it, type `pk;s description <new description>`." : "")
|
||||
+ $" Using {target.Description.Length}/{Limits.MaxDescriptionLength} characters."))
|
||||
.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +224,7 @@ public class SystemEdit
|
|||
public async Task Color(Context ctx, PKSystem target)
|
||||
{
|
||||
var isOwnSystem = ctx.System?.Id == target.Id;
|
||||
var matchedRaw = ctx.MatchRaw();
|
||||
var matchedFormat = ctx.MatchFormat();
|
||||
var matchedClear = ctx.MatchClear();
|
||||
|
||||
if (!isOwnSystem || !(ctx.HasNext() || matchedClear))
|
||||
|
|
@ -197,8 +232,10 @@ public class SystemEdit
|
|||
if (target.Color == null)
|
||||
await ctx.Reply(
|
||||
"This system does not have a color set." + (isOwnSystem ? " To set one, type `pk;system color <color>`." : ""));
|
||||
else if (matchedRaw)
|
||||
else if (matchedFormat == ReplyFormat.Raw)
|
||||
await ctx.Reply("```\n#" + target.Color + "\n```");
|
||||
else if (matchedFormat == ReplyFormat.Plaintext)
|
||||
await ctx.Reply(target.Color);
|
||||
else
|
||||
await ctx.Reply(embed: new EmbedBuilder()
|
||||
.Title("System color")
|
||||
|
|
@ -244,22 +281,33 @@ public class SystemEdit
|
|||
? "You currently have no system tag set. To set one, type `pk;s tag <tag>`."
|
||||
: "This system currently has no system tag set.";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Tag == null)
|
||||
{
|
||||
await ctx.Reply(noTagSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Tag}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Tag}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing tag for system `{target.DisplayHid(ctx.Config)}`");
|
||||
await ctx.Reply(target.Tag, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Tag == null)
|
||||
await ctx.Reply(noTagSetMessage);
|
||||
else
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}."
|
||||
+ (isOwnSystem ? "To change it, type `pk;s tag <tag>`. To clear it, type `pk;s tag -clear`." : ""));
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current system tag is {target.Tag.AsCode()}."
|
||||
+ (isOwnSystem ? "To change it, type `pk;s tag <tag>`. To clear it, type `pk;s tag -clear`." : ""));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -294,15 +342,22 @@ public class SystemEdit
|
|||
|
||||
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
|
||||
|
||||
async Task Show(bool raw = false)
|
||||
async Task Show(ReplyFormat format = ReplyFormat.Standard)
|
||||
{
|
||||
if (settings.Tag != null)
|
||||
{
|
||||
if (raw)
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```{settings.Tag}```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing servertag for system `{target.DisplayHid(ctx.Config)}`");
|
||||
await ctx.Reply(settings.Tag, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = $"Your current system tag in '{ctx.Guild.Name}' is {settings.Tag.AsCode()}";
|
||||
if (!settings.TagEnabled)
|
||||
|
|
@ -398,8 +453,8 @@ public class SystemEdit
|
|||
await EnableDisable(false);
|
||||
else if (ctx.Match("enable") || ctx.MatchFlag("enable"))
|
||||
await EnableDisable(true);
|
||||
else if (ctx.MatchRaw())
|
||||
await Show(true);
|
||||
else if (ctx.MatchFormat() != ReplyFormat.Standard)
|
||||
await Show(ctx.MatchFormat());
|
||||
else if (!ctx.HasNext(false))
|
||||
await Show();
|
||||
else
|
||||
|
|
@ -416,24 +471,35 @@ public class SystemEdit
|
|||
if (isOwnSystem)
|
||||
noPronounsSetMessage += " To set some, type `pk;system pronouns <pronouns>`";
|
||||
|
||||
if (ctx.MatchRaw())
|
||||
{
|
||||
var format = ctx.MatchFormat();
|
||||
|
||||
// if there's nothing next or what's next is "raw"/"plaintext" we're doing a query, so check for null
|
||||
if (!ctx.HasNext(false) || format != ReplyFormat.Standard)
|
||||
if (target.Pronouns == null)
|
||||
{
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == ReplyFormat.Raw)
|
||||
{
|
||||
await ctx.Reply($"```\n{target.Pronouns}\n```");
|
||||
return;
|
||||
}
|
||||
if (format == ReplyFormat.Plaintext)
|
||||
{
|
||||
var eb = new EmbedBuilder()
|
||||
.Description($"Showing pronouns for system `{target.DisplayHid(ctx.Config)}`");
|
||||
await ctx.Reply(target.Pronouns, embed: eb.Build());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx.HasNext(false))
|
||||
{
|
||||
if (target.Pronouns == null)
|
||||
await ctx.Reply(noPronounsSetMessage);
|
||||
else
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`."
|
||||
+ (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
await ctx.Reply($"{(isOwnSystem ? "Your" : "This system's")} current pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;system pronouns -raw`."
|
||||
+ (isOwnSystem ? " To clear them, type `pk;system pronouns -clear`."
|
||||
: "")
|
||||
+ $" Using {target.Pronouns.Length}/{Limits.MaxPronounsLength} characters.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -473,22 +539,24 @@ public class SystemEdit
|
|||
{
|
||||
ctx.CheckOwnSystem(target);
|
||||
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
|
||||
|
||||
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.Url });
|
||||
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.CleanUrl ?? img.Url });
|
||||
|
||||
var msg = img.Source switch
|
||||
{
|
||||
AvatarSource.User =>
|
||||
$"{Emojis.Success} System icon changed to {img.SourceUser?.Username}'s avatar!\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon will need to be re-set.",
|
||||
AvatarSource.Url => $"{Emojis.Success} System icon changed to the image at the given URL.",
|
||||
AvatarSource.HostedCdn => $"{Emojis.Success} System icon changed to attached image.",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} System icon changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon will stop working.",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
|
||||
await (hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
|
||||
: ctx.Reply(msg));
|
||||
|
|
@ -541,9 +609,10 @@ public class SystemEdit
|
|||
{
|
||||
ctx.CheckOwnSystem(target);
|
||||
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
|
||||
|
||||
await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.Url });
|
||||
await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.CleanUrl ?? img.Url });
|
||||
|
||||
var msg = img.Source switch
|
||||
{
|
||||
|
|
@ -551,13 +620,14 @@ public class SystemEdit
|
|||
$"{Emojis.Success} System icon for this server changed to {img.SourceUser?.Username}'s avatar! It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If {img.SourceUser?.Username} changes their avatar, the system icon for this server will need to be re-set.",
|
||||
AvatarSource.Url =>
|
||||
$"{Emojis.Success} System icon for this server changed to the image at the given URL. It will now be used for anything that uses system avatar in this server.",
|
||||
AvatarSource.HostedCdn => $"{Emojis.Success} System icon for this server changed to attached image.",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} System icon for this server changed to attached image. It will now be used for anything that uses system avatar in this server.\n{Emojis.Warn} If you delete the message containing the attachment, the system icon for this server will stop working.",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
|
||||
await (hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
|
||||
: ctx.Reply(msg));
|
||||
|
|
@ -602,7 +672,7 @@ public class SystemEdit
|
|||
|
||||
public async Task BannerImage(Context ctx, PKSystem target)
|
||||
{
|
||||
ctx.CheckSystemPrivacy(target.Id, target.DescriptionPrivacy);
|
||||
ctx.CheckSystemPrivacy(target.Id, target.BannerPrivacy);
|
||||
|
||||
var isOwnSystem = target.Id == ctx.System?.Id;
|
||||
|
||||
|
|
@ -638,13 +708,15 @@ public class SystemEdit
|
|||
|
||||
else if (await ctx.MatchImage() is { } img)
|
||||
{
|
||||
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
|
||||
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
|
||||
|
||||
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.Url });
|
||||
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.CleanUrl ?? img.Url });
|
||||
|
||||
var msg = img.Source switch
|
||||
{
|
||||
AvatarSource.Url => $"{Emojis.Success} System banner image changed to the image at the given URL.",
|
||||
AvatarSource.HostedCdn => $"{Emojis.Success} System banner image changed to attached image.",
|
||||
AvatarSource.Attachment =>
|
||||
$"{Emojis.Success} System banner image changed to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the banner image will stop working.",
|
||||
AvatarSource.User => throw new PKError("Cannot set a banner image to an user's avatar."),
|
||||
|
|
@ -652,7 +724,7 @@ public class SystemEdit
|
|||
};
|
||||
|
||||
// The attachment's already right there, no need to preview it.
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment;
|
||||
var hasEmbed = img.Source != AvatarSource.Attachment && img.Source != AvatarSource.HostedCdn;
|
||||
await (hasEmbed
|
||||
? ctx.Reply(msg, new EmbedBuilder().Image(new Embed.EmbedImage(img.Url)).Build())
|
||||
: ctx.Reply(msg));
|
||||
|
|
@ -662,19 +734,20 @@ public class SystemEdit
|
|||
public async Task Delete(Context ctx, PKSystem target)
|
||||
{
|
||||
ctx.CheckSystem().CheckOwnSystem(target);
|
||||
var noExport = ctx.MatchFlag("ne", "no-export");
|
||||
|
||||
await ctx.Reply(
|
||||
$"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{target.Hid}`).\n"
|
||||
+ $"**Note: this action is permanent,** but you will get a copy of your system's data that can be re-imported into PluralKit at a later date sent to you in DMs."
|
||||
+ " If you don't want this to happen, use `pk;s delete -no-export` instead.");
|
||||
if (!await ctx.ConfirmWithReply(target.Hid))
|
||||
var warnMsg = $"{Emojis.Warn} Are you sure you want to delete your system? If so, reply to this message with your system's ID (`{target.DisplayHid(ctx.Config)}`).\n";
|
||||
if (!noExport)
|
||||
warnMsg += "**Note: this action is permanent,** but you will get a copy of your system's data that can be re-imported into PluralKit at a later date sent to you in DMs."
|
||||
+ " If you don't want this to happen, use `pk;s delete -no-export` instead.";
|
||||
|
||||
await ctx.Reply(warnMsg);
|
||||
if (!await ctx.ConfirmWithReply(target.Hid, treatAsHid: true))
|
||||
throw new PKError(
|
||||
$"System deletion cancelled. Note that you must reply with your system ID (`{target.Hid}`) *verbatim*.");
|
||||
$"System deletion cancelled. Note that you must reply with your system ID (`{target.DisplayHid(ctx.Config)}`) *verbatim*.");
|
||||
|
||||
// If the user confirms the deletion, export their system and send them the export file before actually
|
||||
// deleting their system, unless they specifically tell us not to do an export.
|
||||
|
||||
var noExport = ctx.MatchFlag("ne", "no-export");
|
||||
if (!noExport)
|
||||
{
|
||||
var json = await ctx.BusyIndicator(async () =>
|
||||
|
|
@ -762,13 +835,14 @@ public class SystemEdit
|
|||
.Field(new Embed.Field("Name", target.NamePrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Avatar", target.AvatarPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Description", target.DescriptionPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Banner", target.BannerPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Pronouns", target.PronounPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Member list", target.MemberListPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Group list", target.GroupListPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Current fronter(s)", target.FrontPrivacy.Explanation()))
|
||||
.Field(new Embed.Field("Front/switch history", target.FrontHistoryPrivacy.Explanation()))
|
||||
.Description(
|
||||
"To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `name`, `avatar`, `description`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`.");
|
||||
"To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `name`, `avatar`, `description`, `banner`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`.");
|
||||
return ctx.Reply(embed: eb.Build());
|
||||
}
|
||||
|
||||
|
|
@ -788,6 +862,7 @@ public class SystemEdit
|
|||
SystemPrivacySubject.Name => "name",
|
||||
SystemPrivacySubject.Avatar => "avatar",
|
||||
SystemPrivacySubject.Description => "description",
|
||||
SystemPrivacySubject.Banner => "banner",
|
||||
SystemPrivacySubject.Pronouns => "pronouns",
|
||||
SystemPrivacySubject.Front => "front",
|
||||
SystemPrivacySubject.FrontHistory => "front history",
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public class SystemFront
|
|||
|
||||
var members = await ctx.Database.Execute(c => ctx.Repository.GetSwitchMembers(c, sw.Id)).ToListAsync();
|
||||
var membersStr = members.Any()
|
||||
? string.Join(", ", members.Select(m => $"**{m.NameFor(ctx)}**{(showMemberId ? $" (`{m.Hid}`)" : "")}"))
|
||||
? string.Join(", ", members.Select(m => $"**{m.NameFor(ctx)}**{(showMemberId ? $" (`{m.DisplayHid(ctx.Config)}`)" : "")}"))
|
||||
: "**no fronter**";
|
||||
|
||||
var switchSince = SystemClock.Instance.GetCurrentInstant() - sw.Timestamp;
|
||||
|
|
@ -138,13 +138,13 @@ public class SystemFront
|
|||
if (ctx.Guild != null)
|
||||
guildSettings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, system.Id);
|
||||
if (group != null)
|
||||
title.Append($"{group.NameFor(ctx)} (`{group.Hid}`)");
|
||||
title.Append($"{group.NameFor(ctx)} (`{group.DisplayHid(ctx.Config)}`)");
|
||||
else if (ctx.Guild != null && guildSettings.DisplayName != null)
|
||||
title.Append($"{guildSettings.DisplayName} (`{system.Hid}`)");
|
||||
title.Append($"{guildSettings.DisplayName} (`{system.DisplayHid(ctx.Config)}`)");
|
||||
else if (system.NameFor(ctx) != null)
|
||||
title.Append($"{system.NameFor(ctx)} (`{system.Hid}`)");
|
||||
title.Append($"{system.NameFor(ctx)} (`{system.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{system.Hid}`");
|
||||
title.Append($"`{system.DisplayHid(ctx.Config)}`");
|
||||
|
||||
var frontpercent = await ctx.Database.Execute(c => ctx.Repository.GetFrontBreakdown(c, system.Id, group?.Id, rangeStart.Value.ToInstant(), now));
|
||||
await ctx.Reply(embed: await _embeds.CreateFrontPercentEmbed(frontpercent, system, group, ctx.Zone,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public class SystemLink
|
|||
|
||||
var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id);
|
||||
if (existingAccount != null)
|
||||
throw Errors.AccountInOtherSystem(existingAccount);
|
||||
throw Errors.AccountInOtherSystem(existingAccount, ctx.Config);
|
||||
|
||||
var msg = $"{account.Mention()}, please confirm the link.";
|
||||
if (!await ctx.PromptYesNo(msg, "Confirm", account, false)) throw Errors.MemberLinkCancelled;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class SystemList
|
|||
// - ParseListOptions checks list access privacy and sets the privacy filter (which members show up in list)
|
||||
// - RenderMemberList checks the indivual privacy for each member (NameFor, etc)
|
||||
// the own system is always allowed to look up their list
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id));
|
||||
var opts = ctx.ParseListOptions(ctx.DirectLookupContextFor(target.Id), ctx.LookupContextFor(target.Id));
|
||||
await ctx.RenderMemberList(
|
||||
ctx.LookupContextFor(target.Id),
|
||||
target.Id,
|
||||
|
|
@ -33,11 +33,11 @@ public class SystemList
|
|||
|
||||
var systemGuildSettings = ctx.Guild != null ? await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id) : null;
|
||||
if (systemGuildSettings != null && systemGuildSettings.DisplayName != null)
|
||||
title.Append($"{systemGuildSettings.DisplayName} (`{target.Hid}`)");
|
||||
title.Append($"{systemGuildSettings.DisplayName} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
else if (target.NameFor(ctx) != null)
|
||||
title.Append($"{target.NameFor(ctx)} (`{target.Hid}`)");
|
||||
title.Append($"{target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
|
||||
else
|
||||
title.Append($"`{target.Hid}`");
|
||||
title.Append($"`{target.DisplayHid(ctx.Config)}`");
|
||||
|
||||
if (opts.Search != null)
|
||||
title.Append($" matching **{opts.Search.Truncate(100)}**");
|
||||
|
|
|
|||
|
|
@ -120,8 +120,8 @@ public static class Errors
|
|||
public static PKError UrlTooLong(string url) =>
|
||||
new($"The given URL is too long ({url.Length}/{Limits.MaxUriLength} characters).");
|
||||
|
||||
public static PKError AccountInOtherSystem(PKSystem system) =>
|
||||
new($"The mentioned account is already linked to another system (see `pk;system {system.Hid}`).");
|
||||
public static PKError AccountInOtherSystem(PKSystem system, SystemConfig config) =>
|
||||
new($"The mentioned account is already linked to another system (see `pk;system {system.DisplayHid(config)}`).");
|
||||
|
||||
public static PKError SameSwitch(ICollection<PKMember> members, LookupContext ctx)
|
||||
{
|
||||
|
|
@ -162,7 +162,7 @@ public static class Errors
|
|||
$"The webhook's name, {name.AsCode()}, is shorter than two characters, and thus cannot be proxied. Please change the member name or use a longer system tag.");
|
||||
|
||||
public static PKError ProxyNameTooLong(string name) => new(
|
||||
$"The webhook's name, {name.AsCode()}, is too long ({name.Length} > {Limits.MaxProxyNameLength} characters), and thus cannot be proxied. Please change the member name, display name or server display name, or use a shorter system tag.");
|
||||
$"The webhook's name, {name.AsCode()}, is too long ({name.Length} > {Limits.MaxProxyNameLength} characters), and thus cannot be proxied. Please change the member name, display name, server display name, system tag, or use a shorter name format");
|
||||
|
||||
public static PKError ProxyTagAlreadyExists(ProxyTag tagToAdd, PKMember member) => new(
|
||||
$"That member already has the proxy tag {tagToAdd.ProxyString.AsCode()}. The member currently has these tags: {member.ProxyTagsString()}");
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ public interface IEventHandler<in T> where T : IGatewayEvent
|
|||
{
|
||||
Task Handle(int shardId, T evt);
|
||||
|
||||
ulong? ErrorChannelFor(T evt, ulong userId) => null;
|
||||
(ulong?, ulong?) ErrorChannelFor(T evt, ulong userId) => (null, null);
|
||||
}
|
||||
|
|
@ -16,20 +16,23 @@ public class InteractionCreated: IEventHandler<InteractionCreateEvent>
|
|||
private readonly ApplicationCommandTree _commandTree;
|
||||
private readonly ILifetimeScope _services;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ModelRepository _repo;
|
||||
|
||||
public InteractionCreated(InteractionDispatchService interactionDispatch, ApplicationCommandTree commandTree,
|
||||
ILifetimeScope services, ILogger logger)
|
||||
ILifetimeScope services, ILogger logger, ModelRepository repo)
|
||||
{
|
||||
_interactionDispatch = interactionDispatch;
|
||||
_commandTree = commandTree;
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
_repo = repo;
|
||||
}
|
||||
|
||||
public async Task Handle(int shardId, InteractionCreateEvent evt)
|
||||
{
|
||||
var system = await _services.Resolve<ModelRepository>().GetSystemByAccount(evt.Member?.User.Id ?? evt.User!.Id);
|
||||
var ctx = new InteractionContext(_services, evt, system);
|
||||
var system = await _repo.GetSystemByAccount(evt.Member?.User.Id ?? evt.User!.Id);
|
||||
var config = system != null ? await _repo.GetSystemConfig(system!.Id) : null;
|
||||
var ctx = new InteractionContext(_services, evt, system, config);
|
||||
|
||||
switch (evt.Type)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
_dmCache = dmCache;
|
||||
}
|
||||
|
||||
public ulong? ErrorChannelFor(MessageCreateEvent evt, ulong userId) => evt.ChannelId;
|
||||
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;
|
||||
|
|
@ -63,7 +63,7 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
if (evt.Type != Message.MessageType.Default && evt.Type != Message.MessageType.Reply) return;
|
||||
if (IsDuplicateMessage(evt)) return;
|
||||
|
||||
var botPermissions = await _cache.PermissionsIn(evt.ChannelId);
|
||||
var botPermissions = await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId);
|
||||
if (!botPermissions.HasFlag(PermissionSet.SendMessages)) return;
|
||||
|
||||
// spawn off saving the private channel into another thread
|
||||
|
|
@ -71,8 +71,8 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
_ = _dmCache.TrySavePrivateChannel(evt);
|
||||
|
||||
var guild = evt.GuildId != null ? await _cache.GetGuild(evt.GuildId.Value) : null;
|
||||
var channel = await _cache.GetChannel(evt.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(evt.ChannelId);
|
||||
var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(evt.GuildId ?? 0, evt.ChannelId);
|
||||
|
||||
// Log metrics and message info
|
||||
_metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived);
|
||||
|
|
@ -90,7 +90,8 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
if (await TryHandleCommand(shardId, evt, guild, channel))
|
||||
return;
|
||||
|
||||
await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions);
|
||||
if (evt.GuildId != null)
|
||||
await TryHandleProxy(evt, guild, channel, rootChannel.Id, botPermissions);
|
||||
}
|
||||
|
||||
private async Task TryHandleLogClean(Channel channel, MessageCreateEvent evt)
|
||||
|
|
@ -113,6 +114,11 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
if (!HasCommandPrefix(content, _config.ClientId, out var cmdStart) || cmdStart == content.Length)
|
||||
return false;
|
||||
|
||||
// if the command message was sent by a user account with bot usage disallowed, ignore it
|
||||
var abuse_log = await _repo.GetAbuseLogByAccount(evt.Author.Id);
|
||||
if (abuse_log != null && abuse_log.DenyBotUsage)
|
||||
return false;
|
||||
|
||||
// Trim leading whitespace from command without actually modifying the string
|
||||
// This just moves the argPos pointer by however much whitespace is at the start of the post-argPos string
|
||||
var trimStartLengthDiff =
|
||||
|
|
@ -123,7 +129,9 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
{
|
||||
var system = await _repo.GetSystemByAccount(evt.Author.Id);
|
||||
var config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
|
||||
await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config));
|
||||
var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null;
|
||||
|
||||
await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig));
|
||||
}
|
||||
catch (PKError)
|
||||
{
|
||||
|
|
@ -161,6 +169,9 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
|
|||
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
||||
ctx = await _repo.GetMessageContext(evt.Author.Id, evt.GuildId ?? default, rootChannel, channel.Id != rootChannel ? channel.Id : default);
|
||||
|
||||
if (ctx.DenyBotUsage)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
return await _proxy.HandleIncomingMessage(evt, ctx, guild, channel, true, botPermissions);
|
||||
|
|
|
|||
|
|
@ -52,11 +52,19 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
|
||||
return;
|
||||
|
||||
var channel = await _cache.GetChannel(evt.ChannelId);
|
||||
// we only use message edit event for proxying, so ignore messages from DMs
|
||||
if (!evt.GuildId.HasValue || evt.GuildId.Value == null) return;
|
||||
ulong guildId = evt.GuildId!.Value!.Value;
|
||||
|
||||
var channel = await _cache.TryGetChannel(guildId, evt.ChannelId); // todo: is this correct for message update?
|
||||
if (channel == null)
|
||||
throw new Exception("could not find self channel in MessageEdited event");
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel))
|
||||
return;
|
||||
var rootChannel = await _cache.GetRootChannel(channel.Id);
|
||||
var guild = await _cache.GetGuild(channel.GuildId!.Value);
|
||||
var rootChannel = await _cache.GetRootChannel(guildId, channel.Id);
|
||||
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;
|
||||
|
||||
// Only react to the last message in the channel
|
||||
|
|
@ -67,9 +75,11 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
MessageContext ctx;
|
||||
using (_metrics.Measure.Timer.Time(BotMetrics.MessageContextQueryTime))
|
||||
ctx = await _repo.GetMessageContext(evt.Author.Value!.Id, channel.GuildId!.Value, rootChannel.Id, evt.ChannelId);
|
||||
if (ctx.DenyBotUsage)
|
||||
return;
|
||||
|
||||
var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);
|
||||
var botPermissions = await _cache.PermissionsIn(channel.Id);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildId, channel.Id);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -91,7 +101,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
private async Task<MessageCreateEvent> GetMessageCreateEvent(MessageUpdateEvent evt, CachedMessage lastMessage,
|
||||
Channel channel)
|
||||
{
|
||||
var referencedMessage = await GetReferencedMessage(evt.ChannelId, lastMessage.ReferencedMessage);
|
||||
var referencedMessage = await GetReferencedMessage(evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0, evt.ChannelId, lastMessage.ReferencedMessage);
|
||||
|
||||
var messageReference = lastMessage.ReferencedMessage != null
|
||||
? new Message.Reference(channel.GuildId, evt.ChannelId, lastMessage.ReferencedMessage.Value)
|
||||
|
|
@ -118,12 +128,12 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
|
|||
return equivalentEvt;
|
||||
}
|
||||
|
||||
private async Task<Message?> GetReferencedMessage(ulong channelId, ulong? referencedMessageId)
|
||||
private async Task<Message?> GetReferencedMessage(ulong guildId, ulong channelId, ulong? referencedMessageId)
|
||||
{
|
||||
if (referencedMessageId == null)
|
||||
return null;
|
||||
|
||||
var botPermissions = await _cache.PermissionsIn(channelId);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildId, channelId);
|
||||
if (!botPermissions.HasFlag(PermissionSet.ReadMessageHistory))
|
||||
{
|
||||
_logger.Warning(
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
// but we aren't able to get DMs from bots anyway, so it's not really needed
|
||||
if (evt.GuildId != null && (evt.Member?.User?.Bot ?? false)) return;
|
||||
|
||||
var channel = await _cache.GetChannel(evt.ChannelId);
|
||||
var channel = await _cache.GetChannel(evt.GuildId ?? 0, evt.ChannelId);
|
||||
|
||||
// check if it's a command message first
|
||||
// since this can happen in DMs as well
|
||||
|
|
@ -75,16 +75,17 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
return;
|
||||
}
|
||||
|
||||
var (authorId, _) = await _commandMessageService.GetCommandMessage(evt.MessageId);
|
||||
if (authorId != null)
|
||||
var cmessage = await _commandMessageService.GetCommandMessage(evt.MessageId);
|
||||
if (cmessage != null)
|
||||
{
|
||||
await HandleCommandDeleteReaction(evt, authorId.Value, false);
|
||||
await HandleCommandDeleteReaction(evt, cmessage.AuthorId, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Proxied messages only exist in guild text channels, so skip checking if we're elsewhere
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel)) return;
|
||||
var abuse_log = await _repo.GetAbuseLogByAccount(evt.Member!.User!.Id);
|
||||
|
||||
switch (evt.Emoji.Name.Split("\U0000fe0f", 2)[0])
|
||||
{
|
||||
|
|
@ -113,6 +114,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
case "\u23F0": // Alarm clock
|
||||
case "\u2757": // Exclamation mark
|
||||
{
|
||||
if (abuse_log != null && abuse_log.DenyBotUsage) break;
|
||||
var msg = await _repo.GetFullMessage(evt.MessageId);
|
||||
if (msg != null)
|
||||
await HandlePingReaction(evt, msg);
|
||||
|
|
@ -123,7 +125,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
private async ValueTask HandleProxyDeleteReaction(MessageReactionAddEvent evt, PKMessage msg)
|
||||
{
|
||||
if (!(await _cache.PermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
return;
|
||||
|
||||
var isSameSystem = msg.Member != null && await _repo.IsMemberOwnedByAccount(msg.Member.Value, evt.UserId);
|
||||
|
|
@ -150,7 +152,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
if (authorId != null && authorId != evt.UserId)
|
||||
return;
|
||||
|
||||
if (!((await _cache.PermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
if (!((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages) || isDM))
|
||||
return;
|
||||
|
||||
// todo: don't try to delete the user's own messages in DMs
|
||||
|
|
@ -175,6 +177,8 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
private async ValueTask HandleQueryReaction(MessageReactionAddEvent evt, FullMessage msg)
|
||||
{
|
||||
var guild = await _cache.GetGuild(evt.GuildId!.Value);
|
||||
var system = await _repo.GetSystemByAccount(evt.UserId);
|
||||
var config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
|
||||
|
||||
// Try to DM the user info about the message
|
||||
try
|
||||
|
|
@ -188,11 +192,12 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
msg.System,
|
||||
msg.Member,
|
||||
guild,
|
||||
config,
|
||||
LookupContext.ByNonOwner,
|
||||
DateTimeZone.Utc
|
||||
));
|
||||
|
||||
embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true));
|
||||
embeds.Add(await _embeds.CreateMessageInfoEmbed(msg, true, config));
|
||||
|
||||
await _rest.CreateMessage(dm, new MessageRequest { Embeds = embeds.ToArray() });
|
||||
}
|
||||
|
|
@ -203,14 +208,14 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
private async ValueTask HandlePingReaction(MessageReactionAddEvent evt, FullMessage msg)
|
||||
{
|
||||
if (!(await _cache.PermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
if (!(await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
return;
|
||||
|
||||
// Check if the "pinger" has permission to send messages in this channel
|
||||
// (if not, PK shouldn't send messages on their behalf)
|
||||
var member = await _rest.GetGuildMember(evt.GuildId!.Value, evt.UserId);
|
||||
var requiredPerms = PermissionSet.ViewChannel | PermissionSet.SendMessages;
|
||||
if (member == null || !(await _cache.PermissionsFor(evt.ChannelId, member)).HasFlag(requiredPerms)) return;
|
||||
if (member == null || !(await _cache.PermissionsForMemberInChannel(evt.GuildId ?? 0, evt.ChannelId, member)).HasFlag(requiredPerms)) return;
|
||||
|
||||
if (msg.Member == null) return;
|
||||
|
||||
|
|
@ -263,7 +268,7 @@ public class ReactionAdded: IEventHandler<MessageReactionAddEvent>
|
|||
|
||||
private async Task TryRemoveOriginalReaction(MessageReactionAddEvent evt)
|
||||
{
|
||||
if ((await _cache.PermissionsIn(evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
if ((await _cache.BotPermissionsIn(evt.GuildId ?? 0, evt.ChannelId)).HasFlag(PermissionSet.ManageMessages))
|
||||
await _rest.DeleteUserReaction(evt.ChannelId, evt.MessageId, evt.Emoji, evt.UserId);
|
||||
}
|
||||
}
|
||||
|
|
@ -42,10 +42,10 @@ public class Init
|
|||
|
||||
using var _ = SentrySdk.Init(opts =>
|
||||
{
|
||||
opts.Dsn = services.Resolve<CoreConfig>().SentryUrl;
|
||||
opts.Dsn = services.Resolve<CoreConfig>().SentryUrl ?? "";
|
||||
opts.Release = BuildInfoService.FullVersion;
|
||||
opts.AutoSessionTracking = true;
|
||||
opts.DisableTaskUnobservedTaskExceptionCapture();
|
||||
// opts.DisableTaskUnobservedTaskExceptionCapture();
|
||||
});
|
||||
|
||||
var config = services.Resolve<BotConfig>();
|
||||
|
|
@ -56,8 +56,6 @@ public class Init
|
|||
await redis.InitAsync(coreConfig);
|
||||
|
||||
var cache = services.Resolve<IDiscordCache>();
|
||||
if (cache is RedisDiscordCache)
|
||||
await (cache as RedisDiscordCache).InitAsync(coreConfig.RedisAddr);
|
||||
|
||||
if (config.Cluster == null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -84,20 +84,32 @@ public class YesNoPrompt: BaseInteractive
|
|||
|
||||
var queue = _ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>();
|
||||
|
||||
var messageDispatch = queue.WaitFor(MessagePredicate, Timeout, cts.Token);
|
||||
async Task WaitForMessage()
|
||||
{
|
||||
try
|
||||
{
|
||||
await queue.WaitFor(MessagePredicate, Timeout, cts.Token);
|
||||
}
|
||||
catch (TimeoutException e)
|
||||
{
|
||||
if (e.Message != "HandlerQueue#WaitFor timed out")
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
await Start();
|
||||
|
||||
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("Action timed out")));
|
||||
var messageDispatch = WaitForMessage();
|
||||
|
||||
cts.Token.Register(() => _tcs.TrySetException(new TimeoutException("YesNoPrompt timed out")));
|
||||
|
||||
try
|
||||
{
|
||||
var doneTask = await Task.WhenAny(_tcs.Task, messageDispatch);
|
||||
if (doneTask == messageDispatch)
|
||||
await Finish();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Finish();
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,25 @@ public class BotModule: Module
|
|||
{
|
||||
var botConfig = c.Resolve<BotConfig>();
|
||||
|
||||
if (botConfig.UseRedisCache)
|
||||
return new RedisDiscordCache(c.Resolve<ILogger>(), botConfig.ClientId);
|
||||
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 metrics = c.Resolve<IMetrics>();
|
||||
|
||||
cache.OnDebug += (_, ev) =>
|
||||
{
|
||||
var (remote, key) = ev;
|
||||
metrics.Measure.Meter.Mark(BotMetrics.CacheDebug, new MetricTags(
|
||||
new[] { "remote", "key" },
|
||||
new[] { remote.ToString(), key }
|
||||
));
|
||||
};
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
return new MemoryDiscordCache(botConfig.ClientId);
|
||||
}).AsSelf().SingleInstance();
|
||||
builder.RegisterType<PrivateChannelService>().AsSelf().SingleInstance();
|
||||
|
|
@ -136,6 +153,7 @@ public class BotModule: Module
|
|||
builder.RegisterType<ErrorMessageService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<CommandMessageService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<InteractionDispatchService>().AsSelf().SingleInstance();
|
||||
builder.RegisterType<AvatarHostingService>().AsSelf().SingleInstance();
|
||||
|
||||
// Sentry stuff
|
||||
builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope();
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@
|
|||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.32.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.47.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
|
||||
<PackageReference Include="Sentry" Version="3.11.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
|
||||
<PackageReference Include="Sentry" Version="4.12.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public class ProxyService
|
|||
public async Task<bool> HandleIncomingMessage(MessageCreateEvent message, MessageContext ctx,
|
||||
Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions)
|
||||
{
|
||||
var rootChannel = await _cache.GetRootChannel(message.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(message.GuildId!.Value, message.ChannelId);
|
||||
|
||||
if (!ShouldProxy(channel, rootChannel, message, ctx))
|
||||
return false;
|
||||
|
|
@ -102,7 +102,7 @@ public class ProxyService
|
|||
|
||||
// Check if the sender account can mention everyone/here + embed links
|
||||
// we need to "mirror" these permissions when proxying to prevent exploits
|
||||
var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, message, isThread: rootChannel.Id != channel.Id);
|
||||
var senderPermissions = PermissionExtensions.PermissionsFor(guild, rootChannel, message.Author.Id, message.Member, isThread: rootChannel.Id != channel.Id);
|
||||
var allowEveryone = senderPermissions.HasFlag(PermissionSet.MentionEveryone);
|
||||
var allowEmbeds = senderPermissions.HasFlag(PermissionSet.EmbedLinks);
|
||||
|
||||
|
|
@ -111,31 +111,10 @@ public class ProxyService
|
|||
return true;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
internal bool CanProxyInChannel(Channel ch, bool isRootChannel = false)
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
{
|
||||
// this is explicitly selecting known channel types so that when Discord add new
|
||||
// ones, users don't get flooded with error codes if that new channel type doesn't
|
||||
// support a feature we need for proxying
|
||||
return ch.Type switch
|
||||
{
|
||||
Channel.ChannelType.GuildText => true,
|
||||
Channel.ChannelType.GuildPublicThread => true,
|
||||
Channel.ChannelType.GuildPrivateThread => true,
|
||||
Channel.ChannelType.GuildNews => true,
|
||||
Channel.ChannelType.GuildNewsThread => true,
|
||||
Channel.ChannelType.GuildVoice => true,
|
||||
Channel.ChannelType.GuildStageVoice => true,
|
||||
Channel.ChannelType.GuildForum => isRootChannel,
|
||||
Channel.ChannelType.GuildMedia => isRootChannel,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
// Proxy checks that give user errors
|
||||
public async Task<string> CanProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
|
||||
{
|
||||
if (!(CanProxyInChannel(channel) && CanProxyInChannel(rootChannel, true)))
|
||||
if (!DiscordUtils.IsValidGuildChannel(channel))
|
||||
return $"PluralKit cannot proxy messages in this type of channel.";
|
||||
|
||||
// Check if the message does not go over any Discord Nitro limits
|
||||
|
|
@ -144,6 +123,21 @@ public class ProxyService
|
|||
return "PluralKit cannot proxy messages over 2000 characters in length.";
|
||||
}
|
||||
|
||||
if (ctx.RequireSystemTag)
|
||||
{
|
||||
if (!ctx.TagEnabled)
|
||||
{
|
||||
return "This server requires PluralKit users to have a system tag, but your system tag is disabled in this server. " +
|
||||
"Use `pk;s servertag -enable` to enable it for this server.";
|
||||
}
|
||||
|
||||
if (!ctx.HasProxyableTag())
|
||||
{
|
||||
return "This server requires PluralKit users to have a system tag, but you do not have one set. " +
|
||||
"A system tag can be set for all servers with `pk;s tag`, or for just this server with `pk;s servertag`.";
|
||||
}
|
||||
}
|
||||
|
||||
var guild = await _cache.GetGuild(channel.GuildId.Value);
|
||||
var fileSizeLimit = guild.FileSizeLimit();
|
||||
var bytesThreshold = fileSizeLimit * 1024 * 1024;
|
||||
|
|
@ -159,6 +153,7 @@ public class ProxyService
|
|||
return null;
|
||||
}
|
||||
|
||||
// Proxy checks that don't give user errors unless `pk;debug proxy` is used
|
||||
public bool ShouldProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
|
||||
{
|
||||
// Make sure author has a system
|
||||
|
|
@ -189,9 +184,9 @@ public class ProxyService
|
|||
throw new ProxyChecksFailedException(
|
||||
"Your system has proxying disabled in this server. Type `pk;proxy on` to enable it.");
|
||||
|
||||
// Make sure we have either an attachment or message content
|
||||
// Make sure we have an attachment, message content, or poll
|
||||
var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0;
|
||||
if (isMessageBlank && msg.Attachments.Length == 0)
|
||||
if (isMessageBlank && msg.Attachments.Length == 0 && msg.Poll == null)
|
||||
throw new ProxyChecksFailedException("Message cannot be blank.");
|
||||
|
||||
if (msg.Activity != null)
|
||||
|
|
@ -227,8 +222,8 @@ public class ProxyService
|
|||
var content = match.ProxyContent;
|
||||
if (!allowEmbeds) content = content.BreakLinkEmbeds();
|
||||
|
||||
var messageChannel = await _cache.GetChannel(trigger.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(trigger.ChannelId);
|
||||
var messageChannel = await _cache.GetChannel(trigger.GuildId!.Value, trigger.ChannelId);
|
||||
var rootChannel = await _cache.GetRootChannel(trigger.GuildId!.Value, trigger.ChannelId);
|
||||
var threadId = messageChannel.IsThread() ? messageChannel.Id : (ulong?)null;
|
||||
var guild = await _cache.GetGuild(trigger.GuildId.Value);
|
||||
var guildMember = await _rest.GetGuildMember(trigger.GuildId!.Value, trigger.Author.Id);
|
||||
|
|
@ -242,6 +237,7 @@ public class ProxyService
|
|||
GuildId = trigger.GuildId!.Value,
|
||||
ChannelId = rootChannel.Id,
|
||||
ThreadId = threadId,
|
||||
MessageId = trigger.Id,
|
||||
Name = await FixSameName(messageChannel.Id, ctx, match.Member),
|
||||
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
|
||||
Content = content,
|
||||
|
|
@ -252,6 +248,7 @@ public class ProxyService
|
|||
AllowEveryone = allowEveryone,
|
||||
Flags = trigger.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null,
|
||||
Tts = tts,
|
||||
Poll = trigger.Poll,
|
||||
});
|
||||
await HandleProxyExecutedActions(ctx, autoproxySettings, trigger, proxyMessage, match);
|
||||
}
|
||||
|
|
@ -310,6 +307,7 @@ public class ProxyService
|
|||
GuildId = guild.Id,
|
||||
ChannelId = rootChannel.Id,
|
||||
ThreadId = threadId,
|
||||
MessageId = originalMsg.Id,
|
||||
Name = match.Member.ProxyName(ctx),
|
||||
AvatarUrl = AvatarUtils.TryRewriteCdnUrl(match.Member.ProxyAvatar(ctx)),
|
||||
Content = match.ProxyContent!,
|
||||
|
|
@ -320,6 +318,7 @@ public class ProxyService
|
|||
AllowEveryone = allowEveryone,
|
||||
Flags = originalMsg.Flags.HasFlag(Message.MessageFlags.VoiceMessage) ? Message.MessageFlags.VoiceMessage : null,
|
||||
Tts = tts,
|
||||
Poll = originalMsg.Poll,
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -497,10 +496,10 @@ public class ProxyService
|
|||
async Task SaveMessageInRedis()
|
||||
{
|
||||
// logclean info
|
||||
await _redis.SetLogCleanup(triggerMessage.Author.Id, triggerMessage.GuildId.Value);
|
||||
await _redis.SetLogCleanup(triggerMessage.Author.Id, proxyMessage.GuildId!.Value);
|
||||
|
||||
// last message info (edit/reproxy)
|
||||
await _redis.SetLastMessage(triggerMessage.Author.Id, triggerMessage.ChannelId, sentMessage.Mid);
|
||||
await _redis.SetLastMessage(triggerMessage.Author.Id, proxyMessage.ChannelId, sentMessage.Mid);
|
||||
|
||||
// "by original mid" lookup
|
||||
await _redis.SetOriginalMid(triggerMessage.Id, proxyMessage.Id);
|
||||
|
|
@ -522,6 +521,10 @@ public class ProxyService
|
|||
|
||||
Task DispatchWebhook() => _dispatch.Dispatch(ctx.SystemId.Value, sentMessage);
|
||||
|
||||
Task MaybeLogSwitch() => (ctx.ProxySwitch && !Array.Exists(ctx.LastSwitchMembers, element => element == match.Member.Id))
|
||||
? _db.Execute(conn => _repo.AddSwitch(conn, (SystemId)ctx.SystemId, new[] { match.Member.Id }))
|
||||
: Task.CompletedTask;
|
||||
|
||||
async Task DeleteProxyTriggerMessage()
|
||||
{
|
||||
if (!deletePrevious)
|
||||
|
|
@ -555,7 +558,8 @@ public class ProxyService
|
|||
UpdateMemberForSentMessage(),
|
||||
LogMessageToChannel(),
|
||||
SaveLatchAutoproxy(),
|
||||
DispatchWebhook()
|
||||
DispatchWebhook(),
|
||||
MaybeLogSwitch()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -63,11 +63,23 @@ public static class AvatarUtils
|
|||
// This lets us add resizing parameters to "borrow" their media proxy server to downsize the image
|
||||
// which in turn makes it more likely to be underneath the size limit!
|
||||
private static readonly Regex DiscordCdnUrl =
|
||||
new(@"^https?://(?:cdn\.discordapp\.com|media\.discordapp\.net)/attachments/(\d{17,19})/(\d{17,19})/([^/\\&\?]+)\.(png|jpg|jpeg|webp)(\?.*)?$");
|
||||
new(@"^https?://(?:cdn\.discordapp\.com|media\.discordapp\.net)/attachments/(\d{17,19})/(\d{17,19})/([^/\\&\?]+)\.(png|jpe?g|gif|webp)(?:\?(?<query>.*))?$", RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly string DiscordMediaUrlReplacement =
|
||||
"https://media.discordapp.net/attachments/$1/$2/$3.$4?width=256&height=256";
|
||||
|
||||
public static string? TryRewriteCdnUrl(string? url) =>
|
||||
url == null ? null : DiscordCdnUrl.Replace(url, DiscordMediaUrlReplacement);
|
||||
public static string? TryRewriteCdnUrl(string? url)
|
||||
{
|
||||
if (url == null)
|
||||
return null;
|
||||
|
||||
var match = DiscordCdnUrl.Match(url);
|
||||
var newUrl = DiscordCdnUrl.Replace(url, DiscordMediaUrlReplacement);
|
||||
if (match.Groups["query"].Success)
|
||||
newUrl += "&" + match.Groups["query"].Value;
|
||||
|
||||
return newUrl;
|
||||
}
|
||||
|
||||
public static bool IsDiscordCdnUrl(string? url) => url != null && DiscordCdnUrl.Match(url).Success;
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ public static class ContextUtils
|
|||
.WaitFor(ReactionPredicate, timeout);
|
||||
}
|
||||
|
||||
public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply)
|
||||
public static async Task<bool> ConfirmWithReply(this Context ctx, string expectedReply, bool treatAsHid = false)
|
||||
{
|
||||
bool Predicate(MessageCreateEvent e) =>
|
||||
e.Author.Id == ctx.Author.Id && e.ChannelId == ctx.Channel.Id;
|
||||
|
|
@ -63,7 +63,11 @@ public static class ContextUtils
|
|||
var msg = await ctx.Services.Resolve<HandlerQueue<MessageCreateEvent>>()
|
||||
.WaitFor(Predicate, Duration.FromMinutes(1));
|
||||
|
||||
return string.Equals(msg.Content, expectedReply, StringComparison.InvariantCultureIgnoreCase);
|
||||
var content = msg.Content;
|
||||
if (treatAsHid)
|
||||
content = content.ToLower().Replace("-", null);
|
||||
|
||||
return string.Equals(content, expectedReply, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public static async Task Paginate<T>(this Context ctx, IAsyncEnumerable<T> items, int totalCount,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ public static class DiscordUtils
|
|||
public const uint Blue = 0x1f99d8;
|
||||
public const uint Green = 0x00cc78;
|
||||
public const uint Red = 0xef4b3d;
|
||||
public const uint Gray = 0x979c9f;
|
||||
|
||||
private static readonly Regex USER_MENTION = new("<@!?(\\d{17,19})>");
|
||||
private static readonly Regex ROLE_MENTION = new("<@&(\\d{17,19})>");
|
||||
|
|
@ -35,7 +34,7 @@ public static class DiscordUtils
|
|||
private static readonly Regex UNBROKEN_LINK_REGEX = new("<?(https?:\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])>?");
|
||||
|
||||
public static string NameAndMention(this User user) =>
|
||||
$"{user.Username}{(user.Discriminator == "0" ? "" : $"#{user.Discriminator}")} ({user.Mention()})";
|
||||
$"{user.Username.EscapeMarkdown()}{(user.Discriminator == "0" ? "" : $"#{user.Discriminator}")} ({user.Mention()})";
|
||||
|
||||
public static Instant SnowflakeToInstant(ulong snowflake) =>
|
||||
Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ public class InteractionContext
|
|||
private readonly ILifetimeScope _provider;
|
||||
private readonly IMetrics _metrics;
|
||||
|
||||
public InteractionContext(ILifetimeScope provider, InteractionCreateEvent evt, PKSystem system)
|
||||
public InteractionContext(ILifetimeScope provider, InteractionCreateEvent evt, PKSystem system, SystemConfig config)
|
||||
{
|
||||
Event = evt;
|
||||
System = system;
|
||||
Config = config;
|
||||
Cache = provider.Resolve<IDiscordCache>();
|
||||
Rest = provider.Resolve<DiscordApiClient>();
|
||||
Repository = provider.Resolve<ModelRepository>();
|
||||
|
|
@ -31,6 +32,7 @@ public class InteractionContext
|
|||
internal readonly DiscordApiClient Rest;
|
||||
internal readonly ModelRepository Repository;
|
||||
public readonly PKSystem System;
|
||||
public readonly SystemConfig Config;
|
||||
|
||||
public InteractionCreateEvent Event { get; }
|
||||
|
||||
|
|
@ -74,12 +76,22 @@ public class InteractionContext
|
|||
});
|
||||
}
|
||||
|
||||
public async Task Defer()
|
||||
{
|
||||
await Respond(InteractionResponse.ResponseType.DeferredChannelMessageWithSource,
|
||||
new InteractionApplicationCommandCallbackData
|
||||
{
|
||||
Components = Array.Empty<MessageComponent>(),
|
||||
Flags = Message.MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Ignore()
|
||||
{
|
||||
await Respond(InteractionResponse.ResponseType.DeferredUpdateMessage,
|
||||
new InteractionApplicationCommandCallbackData
|
||||
{
|
||||
Components = Event.Message.Components
|
||||
Components = Event.Message?.Components ?? Array.Empty<MessageComponent>()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,17 +49,17 @@ public static class MiscUtils
|
|||
if (e is WebhookExecutionErrorOnDiscordsEnd) return false;
|
||||
|
||||
// Socket errors are *not our problem*
|
||||
if (e.GetBaseException() is SocketException) return false;
|
||||
// if (e.GetBaseException() is SocketException) return false;
|
||||
|
||||
// Tasks being cancelled for whatver reason are, you guessed it, also not our problem.
|
||||
if (e is TaskCanceledException) return false;
|
||||
// if (e is TaskCanceledException) return false;
|
||||
|
||||
// Sometimes Discord just times everything out.
|
||||
if (e is TimeoutException) return false;
|
||||
// if (e is TimeoutException) return false;
|
||||
if (e is UnknownDiscordRequestException tde && tde.Message == "Request Timeout") return false;
|
||||
|
||||
// HTTP/2 streams are complicated and break sometimes.
|
||||
if (e is HttpRequestException) return false;
|
||||
// if (e is HttpRequestException) return false;
|
||||
|
||||
// This may expanded at some point.
|
||||
return true;
|
||||
|
|
@ -89,12 +89,15 @@ public static class MiscUtils
|
|||
if (e is NpgsqlException tpe && tpe.InnerException is TimeoutException)
|
||||
return false;
|
||||
|
||||
// Ignore thread pool exhaustion errors
|
||||
if (e is NpgsqlException npe && npe.Message.Contains("The connection pool has been exhausted"))
|
||||
return false;
|
||||
|
||||
// ignore "Exception while reading from stream"
|
||||
if (e is NpgsqlException npe2 && npe2.Message.Contains("Exception while reading from stream"))
|
||||
if (e is NpgsqlException npe &&
|
||||
(
|
||||
// Ignore thread pool exhaustion errors
|
||||
npe.Message.Contains("The connection pool has been exhausted")
|
||||
// ignore "Exception while reading from stream"
|
||||
|| npe.Message.Contains("Exception while reading from stream")
|
||||
// ignore "Exception while connecting"
|
||||
|| npe.Message.Contains("Exception while connecting")
|
||||
))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -24,8 +24,20 @@ public static class ModelUtils
|
|||
public static string DisplayName(this PKMember member) =>
|
||||
member.DisplayName ?? member.Name;
|
||||
|
||||
public static string Reference(this PKMember member, Context ctx) => EntityReference(member.Hid, member.NameFor(ctx));
|
||||
public static string Reference(this PKGroup group, Context ctx) => EntityReference(group.Hid, group.NameFor(ctx));
|
||||
public static string Reference(this PKMember member, Context ctx) => EntityReference(member.DisplayHid(ctx.Config), member.NameFor(ctx));
|
||||
public static string Reference(this PKGroup group, Context ctx) => EntityReference(group.DisplayHid(ctx.Config), group.NameFor(ctx));
|
||||
|
||||
|
||||
public static string DisplayHid(this PKSystem system, SystemConfig? cfg = null, bool isList = false) => HidTransform(system.Hid, cfg, isList);
|
||||
public static string DisplayHid(this PKGroup group, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(group.Hid, cfg, isList, shouldPad);
|
||||
public static string DisplayHid(this PKMember member, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) => HidTransform(member.Hid, cfg, isList, shouldPad);
|
||||
private static string HidTransform(string hid, SystemConfig? cfg = null, bool isList = false, bool shouldPad = true) =>
|
||||
HidUtils.HidTransform(
|
||||
hid,
|
||||
cfg != null && cfg.HidDisplaySplit,
|
||||
cfg != null && cfg.HidDisplayCaps,
|
||||
isList && shouldPad ? (cfg?.HidListPadding ?? SystemConfig.HidPadFormat.None) : SystemConfig.HidPadFormat.None // padding only on lists
|
||||
);
|
||||
|
||||
private static string EntityReference(string hid, string name)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -38,9 +38,11 @@ public class SerilogGatewayEnricherFactory
|
|||
{
|
||||
props.Add(new LogEventProperty("ChannelId", new ScalarValue(channel.Value)));
|
||||
|
||||
if (await _cache.TryGetChannel(channel.Value) != null)
|
||||
var guildIdForCache = guild != null ? guild.Value : 0;
|
||||
|
||||
if (await _cache.TryGetChannel(guildIdForCache, channel.Value) != null)
|
||||
{
|
||||
var botPermissions = await _cache.PermissionsIn(channel.Value);
|
||||
var botPermissions = await _cache.BotPermissionsIn(guildIdForCache, channel.Value);
|
||||
props.Add(new LogEventProperty("BotPermissions", new ScalarValue(botPermissions)));
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +54,7 @@ public class SerilogGatewayEnricherFactory
|
|||
props.Add(new LogEventProperty("UserId", new ScalarValue(user.Value)));
|
||||
|
||||
if (evt is MessageCreateEvent mce)
|
||||
props.Add(new LogEventProperty("UserPermissions", new ScalarValue(await _cache.PermissionsFor(mce))));
|
||||
props.Add(new LogEventProperty("UserPermissions", new ScalarValue(await _cache.PermissionsForMCE(mce))));
|
||||
|
||||
return new Inner(props);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@
|
|||
},
|
||||
"Sentry": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.11.1, )",
|
||||
"resolved": "3.11.1",
|
||||
"contentHash": "T/NLfs6MMkUSYsPEDajB9ad0124T18I0uUod5MNOev3iwjvcnIEQBrStEX2olbIxzqfvGXzQ/QFqTfA2ElLPlA=="
|
||||
"requested": "[4.12.1, )",
|
||||
"resolved": "4.12.1",
|
||||
"contentHash": "OLf7885OKHWLaTLTyw884mwOT4XKCWj2Hz5Wuz/TJemJqXwCIdIljkJBIoeHviRUPvtB7ulDgeYXf/Z7ScToSA=="
|
||||
},
|
||||
"SixLabors.ImageSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.1, )",
|
||||
"resolved": "3.0.1",
|
||||
"contentHash": "o0v/J6SJwp3RFrzR29beGx0cK7xcMRgOyIuw8ZNLQyNnBhiyL/vIQKn7cfycthcWUPG3XezUjFwBWzkcUUDFbg=="
|
||||
"requested": "[3.1.5, )",
|
||||
"resolved": "3.1.5",
|
||||
"contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g=="
|
||||
},
|
||||
"App.Metrics": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -337,8 +337,8 @@
|
|||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.10",
|
||||
"contentHash": "bKHbgzbGsPZbEaExRaJqBz3WQ1GfhMttM23e1nivLJ8HbA3Ad526mW2G2K350q3Dc3HG83I5W8uSZWG4Rv4IpA=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA=="
|
||||
},
|
||||
"Microsoft.Extensions.Options": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -356,8 +356,8 @@
|
|||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"Microsoft.NETCore.Targets": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -374,23 +374,6 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.Registry": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
|
||||
"dependencies": {
|
||||
"System.Security.AccessControl": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.Win32.SystemEvents": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.6.1",
|
||||
|
|
@ -466,8 +449,8 @@
|
|||
},
|
||||
"Npgsql": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.1.5",
|
||||
"contentHash": "juDlNse+SKfXRP0VSgpJkpdCcaVLZt8m37EHdRX+8hw+GG69Eat1Y0MdEfl+oetdOnf9E133GjIDEjg9AF6HSQ==",
|
||||
"resolved": "4.1.13",
|
||||
"contentHash": "p79cObfuRgS8KD5sFmQUqVlINEkJm39bCrzRclicZE1942mKcbLlc0NdoVKhBeZPv//prK/sVTUmRVxdnoPCoA==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.6.0"
|
||||
}
|
||||
|
|
@ -483,10 +466,10 @@
|
|||
},
|
||||
"Pipelines.Sockets.Unofficial": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.0",
|
||||
"contentHash": "7hzHplEIVOGBl5zOQZGX/DiJDHjq+RVRVrYgDiqXb6RriqWAdacXxp+XO9WSrATCEXyNOUOQg9aqQArsjase/A==",
|
||||
"resolved": "2.2.8",
|
||||
"contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==",
|
||||
"dependencies": {
|
||||
"System.IO.Pipelines": "5.0.0"
|
||||
"System.IO.Pipelines": "5.0.1"
|
||||
}
|
||||
},
|
||||
"Polly": {
|
||||
|
|
@ -726,11 +709,11 @@
|
|||
},
|
||||
"StackExchange.Redis": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.2.88",
|
||||
"contentHash": "JJi1jcO3/ZiamBhlsC/TR8aZmYf+nqpGzMi0HRRCy5wJkUPmMnRp0kBA6V84uhU8b531FHSdTDaFCAyCUJomjA==",
|
||||
"resolved": "2.8.16",
|
||||
"contentHash": "WaoulkOqOC9jHepca3JZKFTqndCWab5uYS7qCzmiQDlrTkFaDN7eLSlEfHycBxipRnQY9ppZM7QSsWAwUEGblw==",
|
||||
"dependencies": {
|
||||
"Pipelines.Sockets.Unofficial": "2.2.0",
|
||||
"System.Diagnostics.PerformanceCounter": "5.0.0"
|
||||
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
|
||||
"Pipelines.Sockets.Unofficial": "2.2.8"
|
||||
}
|
||||
},
|
||||
"System.AppContext": {
|
||||
|
|
@ -773,15 +756,6 @@
|
|||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Configuration.ConfigurationManager": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "aM7cbfEfVNlEEOj3DsZP+2g9NRwbkyiAv2isQEzw7pnkDg9ekCU2m1cdJLM02Uq691OaCS91tooaxcEn8d0q5w==",
|
||||
"dependencies": {
|
||||
"System.Security.Cryptography.ProtectedData": "5.0.0",
|
||||
"System.Security.Permissions": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Console": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -809,17 +783,6 @@
|
|||
"resolved": "4.7.1",
|
||||
"contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
|
||||
},
|
||||
"System.Diagnostics.PerformanceCounter": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "kcQWWtGVC3MWMNXdMDWfrmIlFZZ2OdoeT6pSNVRtk9+Sa7jwdPiMlNwb0ZQcS7NRlT92pCfmjRtkSWUW3RAKwg==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0",
|
||||
"Microsoft.Win32.Registry": "5.0.0",
|
||||
"System.Configuration.ConfigurationManager": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.Tools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -840,14 +803,6 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Drawing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==",
|
||||
"dependencies": {
|
||||
"Microsoft.Win32.SystemEvents": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Globalization": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -965,8 +920,8 @@
|
|||
},
|
||||
"System.IO.Pipelines": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "irMYm3vhVgRsYvHTU5b2gsT2CwT/SMM6LZFzuJjpIvT5Z4CshxNsaoBC1X/LltwuR3Opp8d6jOS/60WwOb7Q2Q=="
|
||||
"resolved": "5.0.1",
|
||||
"contentHash": "qEePWsaq9LoEEIqhbGe6D5J8c9IqQOUuTzzV6wn1POlfdLkJliZY3OlB0j0f17uMWlqZYjH7txj+2YbyrIA8Yg=="
|
||||
},
|
||||
"System.Linq": {
|
||||
"type": "Transitive",
|
||||
|
|
@ -1229,15 +1184,6 @@
|
|||
"System.Runtime.Extensions": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.AccessControl": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "5.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.Algorithms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1350,11 +1296,6 @@
|
|||
"System.Threading.Tasks": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.ProtectedData": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "HGxMSAFAPLNoxBvSfW08vHde0F9uh7BjASwu6JF9JnXuEPhCY3YUqURn0+bQV/4UWeaqymmrHWV+Aw9riQCtCA=="
|
||||
},
|
||||
"System.Security.Cryptography.X509Certificates": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1387,20 +1328,6 @@
|
|||
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Permissions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "uE8juAhEkp7KDBCdjDIE3H9R1HJuEHqeqX8nLX9gmYKWwsqk3T5qZlPx8qle5DPKimC/Fy3AFTdV7HamgCh9qQ==",
|
||||
"dependencies": {
|
||||
"System.Security.AccessControl": "5.0.0",
|
||||
"System.Windows.Extensions": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Principal.Windows": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
|
||||
},
|
||||
"System.Text.Encoding": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1474,14 +1401,6 @@
|
|||
"System.Runtime": "4.3.0"
|
||||
}
|
||||
},
|
||||
"System.Windows.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "c1ho9WU9ZxMZawML+ssPKZfdnrg/OjR3pe0m9v8230z3acqphwvPJqzAkH54xRYm5ntZHGG1EPP3sux9H3qSPg==",
|
||||
"dependencies": {
|
||||
"System.Drawing.Common": "5.0.0"
|
||||
}
|
||||
},
|
||||
"System.Xml.ReaderWriter": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.3.0",
|
||||
|
|
@ -1556,7 +1475,7 @@
|
|||
"Newtonsoft.Json": "[13.0.1, )",
|
||||
"NodaTime": "[3.0.3, )",
|
||||
"NodaTime.Serialization.JsonNet": "[3.0.0, )",
|
||||
"Npgsql": "[4.1.5, )",
|
||||
"Npgsql": "[4.1.13, )",
|
||||
"Npgsql.NodaTime": "[4.1.5, )",
|
||||
"Serilog": "[2.12.0, )",
|
||||
"Serilog.Extensions.Logging": "[3.0.1, )",
|
||||
|
|
@ -1569,7 +1488,7 @@
|
|||
"Serilog.Sinks.Seq": "[5.2.2, )",
|
||||
"SqlKata": "[2.3.7, )",
|
||||
"SqlKata.Execution": "[2.3.7, )",
|
||||
"StackExchange.Redis": "[2.2.88, )",
|
||||
"StackExchange.Redis": "[2.8.16, )",
|
||||
"System.Interactive.Async": "[5.0.0, )",
|
||||
"ipnetwork2": "[2.5.381, )"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ public class CoreConfig
|
|||
public string? MessagesDatabase { get; set; }
|
||||
public string? DatabasePassword { get; set; }
|
||||
public string RedisAddr { get; set; }
|
||||
public bool UseRedisMetrics { get; set; } = false;
|
||||
public string SentryUrl { get; set; }
|
||||
public string InfluxUrl { get; set; }
|
||||
public string InfluxDb { get; set; }
|
||||
public string LogDir { get; set; }
|
||||
public string? ElasticUrl { get; set; }
|
||||
public string? SeqLogUrl { get; set; }
|
||||
public string? DispatchProxyUrl { get; set; }
|
||||
public string? DispatchProxyToken { get; set; }
|
||||
|
||||
public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug;
|
||||
public LogEventLevel ElasticLogLevel { get; set; } = LogEventLevel.Information;
|
||||
|
|
|
|||
|
|
@ -83,10 +83,12 @@ internal partial class Database: IDatabase
|
|||
SqlMapper.AddTypeHandler(new NumericIdHandler<MemberId, int>(i => new MemberId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdHandler<SwitchId, int>(i => new SwitchId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdHandler<GroupId, int>(i => new GroupId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdHandler<AbuseLogId, int>(i => new AbuseLogId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SystemId, int>(i => new SystemId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<MemberId, int>(i => new MemberId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<SwitchId, int>(i => new SwitchId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<GroupId, int>(i => new GroupId(i)));
|
||||
SqlMapper.AddTypeHandler(new NumericIdArrayHandler<AbuseLogId, int>(i => new AbuseLogId(i)));
|
||||
|
||||
// Register our custom types to Npgsql
|
||||
// Without these it'll still *work* but break at the first launch + probably cause other small issues
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public class MessageContext
|
|||
public bool InBlacklist { get; }
|
||||
public bool InLogBlacklist { get; }
|
||||
public bool LogCleanupEnabled { get; }
|
||||
public bool RequireSystemTag { get; }
|
||||
public bool ProxyEnabled { get; }
|
||||
public SwitchId? LastSwitch { get; }
|
||||
public MemberId[] LastSwitchMembers { get; } = new MemberId[0];
|
||||
|
|
@ -25,10 +26,13 @@ public class MessageContext
|
|||
public string? SystemTag { get; }
|
||||
public string? SystemGuildTag { get; }
|
||||
public bool TagEnabled { get; }
|
||||
public string? NameFormat { get; }
|
||||
public string? SystemAvatar { get; }
|
||||
public string? SystemGuildAvatar { get; }
|
||||
public bool AllowAutoproxy { get; }
|
||||
public int? LatchTimeout { get; }
|
||||
public bool CaseSensitiveProxyTags { get; }
|
||||
public bool ProxyErrorMessageEnabled { get; }
|
||||
public bool ProxySwitch { get; }
|
||||
public bool DenyBotUsage { get; }
|
||||
}
|
||||
18
PluralKit.Core/Database/Functions/MessageContextExt.cs
Normal file
18
PluralKit.Core/Database/Functions/MessageContextExt.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#nullable enable
|
||||
|
||||
namespace PluralKit.Core;
|
||||
public static class MessageContextExt
|
||||
{
|
||||
public static bool HasProxyableTag(this MessageContext ctx)
|
||||
{
|
||||
var tag = ctx.SystemGuildTag ?? ctx.SystemTag;
|
||||
if (!ctx.TagEnabled || tag == null)
|
||||
return false;
|
||||
|
||||
var format = ctx.NameFormat ?? ProxyMember.DefaultFormat;
|
||||
if (!format.Contains("{tag}"))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue