Compare commits

...

4 commits

Author SHA1 Message Date
alyssa
85b2b77730 chore(docs): update catalogger website link
Some checks failed
Build and push Docker image / .net docker build (push) Has been cancelled
.net checks / run .net tests (push) Has been cancelled
.net checks / dotnet-format (push) Has been cancelled
Build and push Rust service Docker images / rust docker build (push) Has been cancelled
rust checks / cargo fmt (push) Has been cancelled
2025-11-09 09:11:21 +00:00
alyssa
6a7ab2b853 feat: disable autoproxy with 3 backslashes 2025-11-09 09:09:59 +00:00
alyssa
83dd880374 chore: move app-commands script to rust 2025-11-09 09:09:50 +00:00
alyssa
c0a5bc81a0 feat(api): improve logging 2025-11-09 09:09:47 +00:00
18 changed files with 119 additions and 159 deletions

15
Cargo.lock generated
View file

@ -104,6 +104,20 @@ dependencies = [
"twilight-http",
]
[[package]]
name = "app-commands"
version = "0.1.0"
dependencies = [
"anyhow",
"futures",
"libpk",
"tokio",
"tracing",
"twilight-http",
"twilight-model",
"twilight-util",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
@ -4560,6 +4574,7 @@ version = "0.16.0"
source = "git+https://github.com/pluralkit/twilight?branch=pluralkit-7f08d95#054a2aa5d29fb46220af1cd5df568b73511cdb26"
dependencies = [
"twilight-model",
"twilight-validate",
]
[[package]]

View file

@ -27,7 +27,7 @@ axum = { git = "https://github.com/pluralkit/axum", branch = "v0.8.4-pluralkit"
twilight-gateway = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" }
twilight-cache-inmemory = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] }
twilight-util = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator"] }
twilight-util = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", features = ["permission-calculator", "builder"] }
twilight-model = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95" }
twilight-http = { git = "https://github.com/pluralkit/twilight", branch = "pluralkit-7f08d95", default-features = false, features = ["rustls-aws_lc_rs", "rustls-native-roots"] }

View file

@ -66,6 +66,15 @@ public class ProxyService
var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, guild.Id, null);
if (IsDisableAutoproxy(message))
{
await _repo.UpdateAutoproxy(ctx.SystemId.Value, guild.Id, null, new()
{
AutoproxyMode = AutoproxyMode.Off
});
return false;
}
if (autoproxySettings.AutoproxyMode == AutoproxyMode.Latch && IsUnlatch(message))
{
// "unlatch"
@ -495,6 +504,9 @@ public class ProxyService
public static bool IsUnlatch(Message message)
=> message.Content.StartsWith(@"\\") || message.Content.StartsWith("\\\u200b\\");
public static bool IsDisableAutoproxy(Message message)
=> message.Content.StartsWith(@"\\\") || message.Content.StartsWith("\\\u200b\\\u200b\\");
private async Task HandleProxyExecutedActions(MessageContext ctx, AutoproxySettings autoproxySettings,
Message triggerMessage, Message proxyMessage, ProxyMatch match,
bool deletePrevious = true)

View file

@ -127,13 +127,10 @@ fn router(ctx: ApiContext) -> Router {
.route("/v2/groups/{group_id}/oembed.json", get(rproxy))
.layer(middleware::ratelimit::ratelimiter(middleware::ratelimit::do_request_ratelimited)) // this sucks
.layer(axum::middleware::from_fn(middleware::ignore_invalid_routes::ignore_invalid_routes))
.layer(axum::middleware::from_fn(middleware::logger::logger))
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::params::params))
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::auth::auth))
.layer(axum::middleware::from_fn(middleware::logger::logger))
.layer(axum::middleware::from_fn(middleware::cors::cors))
.layer(tower_http::catch_panic::CatchPanicLayer::custom(util::handle_panic))

View file

@ -76,5 +76,10 @@ pub async fn auth(State(ctx): State<ApiContext>, mut req: Request, next: Next) -
req.extensions_mut()
.insert(AuthState::new(authed_system_id, authed_app_id, internal));
next.run(req).await
let mut res = next.run(req).await;
res.extensions_mut()
.insert(AuthState::new(authed_system_id, authed_app_id, internal));
res
}

View file

@ -12,9 +12,10 @@ const MIN_LOG_TIME: u128 = 2_000;
pub async fn logger(request: Request, next: Next) -> Response {
let method = request.method().clone();
let headers = request.headers().clone();
let remote_ip = header_or_unknown(request.headers().get("X-PluralKit-Client-IP"));
let user_agent = header_or_unknown(request.headers().get("User-Agent"));
let remote_ip = header_or_unknown(headers.get("X-PluralKit-Client-IP"));
let user_agent = header_or_unknown(headers.get("User-Agent"));
let extensions = request.extensions().clone();
@ -24,10 +25,6 @@ pub async fn logger(request: Request, next: Next) -> Response {
.map(|v| v.as_str().to_string())
.unwrap_or("unknown".to_string());
let auth = extensions
.get::<AuthState>()
.expect("should always have AuthState");
let uri = request.uri().clone();
let request_span = span!(
@ -43,15 +40,24 @@ pub async fn logger(request: Request, next: Next) -> Response {
let response = next.run(request).instrument(request_span).await;
let elapsed = start.elapsed().as_millis();
let system_id = auth
.system_id()
.map(|v| v.to_string())
.unwrap_or("none".to_string());
let rext = response.extensions().clone();
let auth = rext.get::<AuthState>();
let app_id = auth
.app_id()
.map(|v| v.to_string())
.unwrap_or("none".to_string());
let system_id = if let Some(auth) = auth {
auth.system_id()
.map(|v| v.to_string())
.unwrap_or("none".to_string())
} else {
"none".to_string()
};
let app_id = if let Some(auth) = auth {
auth.app_id()
.map(|v| v.to_string())
.unwrap_or("none".to_string())
} else {
"none".to_string()
};
counter!(
"pluralkit_api_requests",
@ -73,6 +79,14 @@ pub async fn logger(request: Request, next: Next) -> Response {
.record(elapsed as f64 / 1_000_f64);
info!(
status = response.status().as_str(),
method = method.to_string(),
endpoint,
elapsed,
user_agent,
remote_ip,
system_id,
app_id,
"{} handled request for {} {} in {}ms",
response.status(),
method,

View file

@ -0,0 +1,14 @@
[package]
name = "app-commands"
version = "0.1.0"
edition = "2024"
[dependencies]
libpk = { path = "../libpk" }
anyhow = { workspace = true }
futures = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
twilight-http = { workspace = true }
twilight-model = { workspace = true }
twilight-util = { workspace = true }

View file

@ -0,0 +1,41 @@
use twilight_model::{
application::command::{Command, CommandType},
guild::IntegrationApplication,
};
use twilight_util::builder::command::CommandBuilder;
#[libpk::main]
async fn main() -> anyhow::Result<()> {
let discord = twilight_http::Client::builder()
.token(
libpk::config
.discord
.as_ref()
.expect("missing discord config")
.bot_token
.clone(),
)
.build();
let interaction = discord.interaction(twilight_model::id::Id::new(
libpk::config
.discord
.as_ref()
.expect("missing discord config")
.client_id
.clone()
.get(),
));
let commands = vec![
// message commands
// description must be empty string
CommandBuilder::new("\u{2753} Message info", "", CommandType::Message).build(),
CommandBuilder::new("\u{274c} Delete message", "", CommandType::Message).build(),
CommandBuilder::new("\u{1f514} Ping author", "", CommandType::Message).build(),
];
interaction.set_global_commands(&commands).await?;
Ok(())
}

View file

@ -5,7 +5,7 @@ Because PluralKit deletes messages as part of proxying, this can often clutter u
## Bots with PluralKit support
Some moderation bots have official PluralKit support, and properly handle excluding proxy deletes, as well as add PK-specific information to relevant log messages:
- [**Catalogger**](https://catalogger.starshines.xyz/docs)
- [**Catalogger**](https://catalogger.app)
- [**Aero**](https://aero.bot/)
- [**CoreBot**](https://discord.gg/GAAj6DDrCJ)
- [**Quark**](https://quark.bot)

View file

@ -1,4 +0,0 @@
/commands.json
*.pyc
__pycache__/

View file

@ -1,23 +0,0 @@
# PluralKit "application command" helpers
## Adding new commands
Edit the `COMMAND_LIST` global in `commands.py`, making sure that any
command names that are specified in that file match up with the
command names used in the bot code (which will generally be in the list
in `PluralKit.Bot/ApplicationCommandMeta/ApplicationCommandList.cs`).
TODO: add helpers for slash commands to this
## Dumping application command JSON
Run `python3 commands.py` to get a JSON dump of the available application
commands - this is in a format that can be sent to Discord as a `PUT` to
`/applications/{clientId}/commands`.
## Updating Discord's list of application commands
From the root of the repository (where your `pluralkit.conf` resides),
run `python3 ./scripts/app-commands/update.py`. This will **REPLACE**
any existing application commands that Discord knows about, with the
updated list.

View file

@ -1,10 +0,0 @@
from common import *
COMMAND_LIST = [
MessageCommand("\U00002753 Message info"),
MessageCommand("\U0000274c Delete message"),
MessageCommand("\U0001f514 Ping author"),
]
if __name__ == "__main__":
print(__import__('json').dumps(COMMAND_LIST))

View file

@ -1 +0,0 @@
from .types import MessageCommand

View file

@ -1,7 +0,0 @@
class MessageCommand(dict):
COMMAND_TYPE = 3
def __init__(self, name):
super().__init__()
self["type"] = self.__class__.COMMAND_TYPE
self["name"] = name

View file

@ -1,70 +0,0 @@
from common import *
from commands import COMMAND_LIST
import io
import os
import sys
import json
from pathlib import Path
from urllib import request
from urllib.error import URLError
DISCORD_API_BASE = "https://discord.com/api/v10"
def get_config():
data = {}
# prefer token from environment if present
envbase = ["PluralKit", "Bot"]
for var in ["Token", "ClientId"]:
for sep in [':', '__']:
envvar = sep.join(envbase + [var])
if envvar in os.environ:
data[var] = os.environ[envvar]
if "Token" in data and "ClientId" in data:
return data
# else fall back to config
cfg_path = Path(os.getcwd()) / "pluralkit.conf"
if cfg_path.exists():
cfg = {}
with open(str(cfg_path), 'r') as fh:
cfg = json.load(fh)
if 'PluralKit' in cfg and 'Bot' in cfg['PluralKit']:
return cfg['PluralKit']['Bot']
return None
def main():
config = get_config()
if config is None:
raise ArgumentError("config was not loaded")
if 'Token' not in config or 'ClientId' not in config:
raise ArgumentError("config is missing 'Token' or 'ClientId'")
data = json.dumps(COMMAND_LIST)
url = DISCORD_API_BASE + f"/applications/{config['ClientId']}/commands"
req = request.Request(url, method='PUT', data=data.encode('utf-8'))
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", f"Bot {config['Token']}")
req.add_header("User-Agent", "PluralKit (app-commands updater; https://pluralkit.me)")
try:
with request.urlopen(req) as resp:
if resp.status == 200:
print("Update successful!")
return 0
except URLError as resp:
print(f"[!!!] Update not successful: status {resp.status}", file=sys.stderr)
print(f"[!!!] Response body below:\n", file=sys.stderr)
print(resp.read(), file=sys.stderr)
sys.stderr.flush()
return 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,3 +0,0 @@
#!/bin/sh
docker-compose -f "$(dirname $0)/../docker-compose.yml" exec -T -u postgres db pg_dump postgres

View file

@ -1,15 +0,0 @@
#!/bin/sh
# Usage: rclone-db.sh <remote>:<path>
# eg. rclone-db.sh b2:pluralkit
FILENAME=pluralkit-$(date -u +"%Y-%m-%dT%H:%M:%S").sql.gz
echo Dumping database to /tmp/$FILENAME...
$(dirname $0)/dump-db.sh | gzip > /tmp/$FILENAME
echo Transferring to remote $1...
rclone -P copy /tmp/$FILENAME $1
echo Cleaning up...
rm /tmp/$FILENAME

View file

@ -1,5 +0,0 @@
#!/bin/sh
# Runs a local database in the background listening on port 5432, deleting itself once stopped
# Requires Docker. May need sudo if your user isn't in the `docker` group.
docker run --rm --detach --publish 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:alpine