Merge branch 'main' into proxyswitch-add

This commit is contained in:
Petal Ladenson 2024-12-05 18:18:04 -07:00 committed by GitHub
commit fbebe38afe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 1000 additions and 423 deletions

289
Cargo.lock generated
View file

@ -89,7 +89,7 @@ dependencies = [
"anyhow",
"axum 0.7.5",
"fred",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-util",
"lazy_static",
"libpk",
@ -256,7 +256,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-util",
"itoa",
"matchit",
@ -398,9 +398,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.6.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "bytes-utils"
@ -637,6 +637,16 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid",
]
[[package]]
name = "der"
version = "0.7.9"
@ -815,6 +825,18 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "findshlibs"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
dependencies = [
"cc",
"lazy_static",
"libc",
"winapi",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
@ -1022,6 +1044,7 @@ dependencies = [
"metrics",
"prost",
"serde_json",
"serde_variant",
"signal-hook",
"tokio",
"tracing",
@ -1250,6 +1273,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hostname"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
dependencies = [
"cfg-if",
"libc",
"windows",
]
[[package]]
name = "http"
version = "0.2.8"
@ -1350,9 +1384,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.3.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
dependencies = [
"bytes",
"futures-channel",
@ -1391,7 +1425,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-util",
"rustls 0.22.4",
"rustls-native-certs",
@ -1409,7 +1443,7 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-util",
"rustls 0.23.10",
"rustls-pki-types",
@ -1421,20 +1455,19 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.5"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"hyper 1.3.1",
"hyper 1.5.0",
"pin-project-lite",
"socket2 0.5.7",
"tokio",
"tower",
"tower-service",
"tracing",
]
@ -1553,6 +1586,21 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json-subscriber"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d0a86fd2fba3a8721e7086b2c9fceb0994f71cdbd64ad2dfc1b202a5c062b4"
dependencies = [
"serde",
"serde_json",
"tracing",
"tracing-core",
"tracing-serde",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "json5"
version = "0.4.1"
@ -1592,13 +1640,16 @@ dependencies = [
"anyhow",
"config",
"fred",
"json-subscriber",
"lazy_static",
"metrics",
"metrics-exporter-prometheus",
"prost",
"prost-build",
"prost-types",
"sentry",
"serde",
"serde_json",
"sqlx",
"time",
"tokio",
@ -1753,7 +1804,7 @@ checksum = "b4f0c8427b39666bf970460908b213ec09b3b350f20c0c2eabcbba51704a08e6"
dependencies = [
"base64 0.22.1",
"http-body-util",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-util",
"indexmap",
"ipnet",
@ -1971,6 +2022,17 @@ dependencies = [
"hashbrown 0.13.2",
]
[[package]]
name = "os_info"
version = "3.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092"
dependencies = [
"log",
"serde",
"windows-sys 0.52.0",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -2518,12 +2580,13 @@ checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-rustls 0.27.3",
"hyper-util",
"ipnet",
@ -2692,6 +2755,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.34"
@ -2735,6 +2807,7 @@ version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
dependencies = [
"log",
"ring 0.17.8",
"rustls-pki-types",
"rustls-webpki 0.102.4",
@ -2748,6 +2821,7 @@ version = "0.23.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
dependencies = [
"log",
"once_cell",
"ring 0.17.8",
"rustls-pki-types",
@ -2881,6 +2955,115 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
[[package]]
name = "sentry"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5484316556650182f03b43d4c746ce0e3e48074a21e2f51244b648b6542e1066"
dependencies = [
"httpdate",
"reqwest 0.12.8",
"rustls 0.22.4",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio",
"ureq",
"webpki-roots 0.26.6",
]
[[package]]
name = "sentry-backtrace"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40aa225bb41e2ec9d7c90886834367f560efc1af028f1c5478a6cce6a59c463a"
dependencies = [
"backtrace",
"once_cell",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a8dd746da3d16cb8c39751619cefd4fcdbd6df9610f3310fd646b55f6e39910"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161283cfe8e99c8f6f236a402b9ccf726b201f365988b5bb637ebca0abbd4a30"
dependencies = [
"once_cell",
"rand",
"sentry-types",
"serde",
"serde_json",
]
[[package]]
name = "sentry-debug-images"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc6b25e945fcaa5e97c43faee0267eebda9f18d4b09a251775d8fef1086238a"
dependencies = [
"findshlibs",
"once_cell",
"sentry-core",
]
[[package]]
name = "sentry-panic"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74f229c7186dd971a9491ffcbe7883544aa064d1589bd30b83fb856cd22d63"
dependencies = [
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-tracing"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3c5faf2103cd01eeda779ea439b68c4ee15adcdb16600836e97feafab362ec"
dependencies = [
"sentry-backtrace",
"sentry-core",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "sentry-types"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d68cdf6bc41b8ff3ae2a9c4671e97426dcdd154cc1d4b6b72813f285d6b163f"
dependencies = [
"debugid",
"hex",
"rand",
"serde",
"serde_json",
"thiserror",
"time",
"url",
"uuid",
]
[[package]]
name = "serde"
version = "1.0.203"
@ -2963,6 +3146,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_variant"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432"
dependencies = [
"serde",
]
[[package]]
name = "sha-1"
version = "0.10.1"
@ -3584,9 +3776,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.11"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
dependencies = [
"futures-core",
"pin-project-lite",
@ -3764,12 +3956,12 @@ dependencies = [
[[package]]
name = "tracing-log"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"lazy_static",
"log",
"once_cell",
"tracing-core",
]
@ -3785,9 +3977,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.16"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
@ -3813,11 +4005,12 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "twilight-cache-inmemory"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"bitflags 2.5.0",
"dashmap",
"serde",
"tracing",
"twilight-model",
"twilight-util",
]
@ -3825,7 +4018,7 @@ dependencies = [
[[package]]
name = "twilight-gateway"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"bitflags 2.5.0",
"fastrand",
@ -3845,7 +4038,7 @@ dependencies = [
[[package]]
name = "twilight-gateway-queue"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"tokio",
"tracing",
@ -3854,12 +4047,12 @@ dependencies = [
[[package]]
name = "twilight-http"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"fastrand",
"http 1.1.0",
"http-body-util",
"hyper 1.3.1",
"hyper 1.5.0",
"hyper-rustls 0.26.0",
"hyper-util",
"percent-encoding",
@ -3875,7 +4068,7 @@ dependencies = [
[[package]]
name = "twilight-http-ratelimiting"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"tokio",
"tracing",
@ -3884,7 +4077,7 @@ dependencies = [
[[package]]
name = "twilight-model"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"bitflags 2.5.0",
"serde",
@ -3896,7 +4089,7 @@ dependencies = [
[[package]]
name = "twilight-util"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"twilight-model",
]
@ -3904,7 +4097,7 @@ dependencies = [
[[package]]
name = "twilight-validate"
version = "0.16.0-rc.1"
source = "git+https://github.com/pluralkit/twilight#41a71cc74441b54ed10e813e76ad040ce4c366e2"
source = "git+https://github.com/pluralkit/twilight#b346757b3054d461f5652b5ba0148a70eda5697e"
dependencies = [
"twilight-model",
]
@ -3921,6 +4114,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "uname"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
dependencies = [
"libc",
]
[[package]]
name = "unicode-bidi"
version = "0.3.10"
@ -3972,6 +4174,21 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a"
dependencies = [
"base64 0.22.1",
"log",
"once_cell",
"rustls 0.23.10",
"rustls-pki-types",
"url",
"webpki-roots 0.26.6",
]
[[package]]
name = "url"
version = "2.5.2"
@ -3981,6 +4198,7 @@ dependencies = [
"form_urlencoded",
"idna 0.5.0",
"percent-encoding",
"serde",
]
[[package]]
@ -3995,6 +4213,7 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"serde",
]
@ -4209,6 +4428,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.52.0"

View file

@ -18,6 +18,7 @@ futures = "0.3.30"
lazy_static = "1.4.0"
metrics = "0.23.0"
reqwest = { version = "0.12.7" , default-features = false, features = ["rustls-tls", "trust-dns"]}
sentry = { version = "0.34.0", default-features = false, features = ["backtrace", "contexts", "panic", "debug-images", "reqwest", "rustls"] } # replace native-tls with rustls
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.117"
signal-hook = "0.3.17"

View file

@ -50,7 +50,8 @@ public static class CacheExtensions
if (!channel.IsThread())
return channel;
var parent = await cache.GetChannel(guildId, 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;
}
}

View file

@ -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; }
}

View file

@ -131,6 +131,15 @@
"App.Metrics.Formatters.InfluxDB": "4.1.0"
}
},
"AppFact.SerilogOpenSearchSink": {
"type": "Transitive",
"resolved": "0.0.8",
"contentHash": "RI3lfmvAwhqrYwy5KPqsBT/tB/opSzeoVH2WUfvGKNBpl6ILCw/5wE8+19L+XMzBFVqgZ5QmkQ2PqTzG9I/ckA==",
"dependencies": {
"OpenSearch.Client": "1.4.0",
"Serilog": "2.12.0"
}
},
"Autofac": {
"type": "Transitive",
"resolved": "6.0.0",
@ -514,6 +523,24 @@
"Npgsql": "4.1.5"
}
},
"OpenSearch.Client": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "91TXm+I8PzxT0yxkf0q5Quee5stVcIFys56pU8+M3+Kiakx+aiaTAlZJHfA3Oy6dMJndNkVm37IPKYCqN3dS4g==",
"dependencies": {
"OpenSearch.Net": "1.4.0"
}
},
"OpenSearch.Net": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "xCM6m3aArN9gXIl2DvWXaDlbpjOBbgMeRPsAM7s1eDbWhu8wKNyfPNKSfep4JlYXkZ7N6Oi/+lmi+G3/SpcqlQ==",
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"System.Buffers": "4.5.1",
"System.Diagnostics.DiagnosticSource": "6.0.1"
}
},
"Pipelines.Sockets.Unofficial": {
"type": "Transitive",
"resolved": "2.2.8",
@ -797,8 +824,8 @@
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Collections": {
"type": "Transitive",
@ -851,8 +878,11 @@
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
"resolved": "6.0.1",
"contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Diagnostics.Tools": {
"type": "Transitive",
@ -1216,8 +1246,8 @@
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ=="
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Runtime.Extensions": {
"type": "Transitive",
@ -1540,6 +1570,7 @@
"dependencies": {
"App.Metrics": "[4.1.0, )",
"App.Metrics.Reporting.InfluxDB": "[4.1.0, )",
"AppFact.SerilogOpenSearchSink": "[0.0.8, )",
"Autofac": "[6.0.0, )",
"Autofac.Extensions.DependencyInjection": "[7.1.0, )",
"Dapper": "[2.0.35, )",

View file

@ -32,6 +32,7 @@ public partial class CommandTree
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 [new|add|off]", "Switching behavior when proxy tags are used");
public static Command ConfigNameFormat = new Command("config nameformat", "config nameformat [format]", "Changes your system's username formatting");
public static Command ConfigServerNameFormat = new Command("config servernameformat", "config servernameformat [format]", "Changes your system's username formatting in the current server");
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");
@ -150,7 +151,7 @@ public partial class CommandTree
{
ConfigAutoproxyAccount, ConfigAutoproxyTimeout, ConfigTimezone, ConfigPing,
ConfigMemberDefaultPrivacy, ConfigGroupDefaultPrivacy, ConfigShowPrivate,
ConfigProxySwitch, ConfigNameFormat
ConfigProxySwitch, ConfigNameFormat, ConfigServerNameFormat
};
public static Command[] ServerConfigCommands =

View file

@ -596,6 +596,8 @@ public partial class CommandTree
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));
if (ctx.MatchMultiple(new[] { "server" }, new[] { "name" }, new[] { "format" }) || ctx.MatchMultiple(new[] { "server", "servername" }, new[] { "format", "nameformat", "nf" }) || ctx.Match("snf", "servernf", "servernameformat", "snameformat"))
return ctx.Execute<Config>(null, m => m.ServerNameFormat(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.");

View file

@ -98,6 +98,15 @@ public static class ContextArgumentsExt
return ReplyFormat.Standard;
}
public static ReplyFormat PeekMatchFormat(this Context ctx)
{
int ptr1 = ctx.Parameters._ptr;
int ptr2 = ctx.Parameters._ptr;
if (ctx.PeekMatch(ref ptr1, new[] { "r", "raw" }) || ctx.MatchFlag("r", "raw")) return ReplyFormat.Raw;
if (ctx.PeekMatch(ref ptr2, new[] { "pt", "plaintext" }) || ctx.MatchFlag("pt", "plaintext")) return ReplyFormat.Plaintext;
return ReplyFormat.Standard;
}
public static bool MatchToggle(this Context ctx, bool? defaultValue = null)
{
var value = ctx.MatchToggleOrNull(defaultValue);

View file

@ -15,6 +15,10 @@ public static class ContextAvatarExt
return new ParsedImage { Url = url, Source = AvatarSource.User, SourceUser = user };
}
// If we have raw or plaintext, don't try to parse as a URL
if (ctx.PeekMatchFormat() != ReplyFormat.Standard)
return null;
// If we have a positional argument, try to parse it as a URL
var arg = ctx.RemainderOrNull();
if (arg != null)

View file

@ -1,7 +1,7 @@
using System.Text;
using Humanizer;
using Myriad.Builders;
using NodaTime;
using NodaTime.Text;
using NodaTime.TimeZones;
@ -137,6 +137,25 @@ public class Config
ProxyMember.DefaultFormat
));
if (ctx.Guild == null)
{
items.Add(new(
"Server Name Format",
"Format string used to display a member's name in the current server",
"only available in servers",
"only available in servers"
));
}
else
{
items.Add(new(
"Server Name Format",
"Format string used to display a member's name in the current server",
(await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id)).NameFormat ?? "none set",
"none set"
));
}
await ctx.Paginate<PaginatedConfigItem>(
items.ToAsyncEnumerable(),
items.Count,
@ -599,6 +618,48 @@ public class Config
await ctx.Reply($"Member names are now formatted as `{formatString}`");
}
public async Task ServerNameFormat(Context ctx)
{
ctx.CheckGuildContext();
var clearFlag = ctx.MatchClear();
var format = ctx.MatchFormat();
// if there's nothing next or what's next is raw/plaintext and we're not clearing, it's a query
if ((!ctx.HasNext() || format != ReplyFormat.Standard) && !clearFlag)
{
var guildCfg = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, ctx.System.Id);
if (guildCfg.NameFormat == null)
await ctx.Reply("You do not have a specific name format set for this server and member names are formatted with your global name format.");
else
switch (format)
{
case ReplyFormat.Raw:
await ctx.Reply($"`{guildCfg.NameFormat}`");
break;
case ReplyFormat.Plaintext:
var eb = new EmbedBuilder()
.Description($"Showing guild Name Format for system {ctx.System.DisplayHid(ctx.Config)}");
await ctx.Reply(guildCfg.NameFormat, eb.Build());
break;
default:
await ctx.Reply($"Your member names in this server are currently formatted as `{guildCfg.NameFormat}`");
break;
}
return;
}
string? formatString = null;
if (!clearFlag)
{
formatString = ctx.RemainderOrNull();
}
await ctx.Repository.UpdateSystemGuild(ctx.System.Id, ctx.Guild.Id, new() { NameFormat = formatString });
if (formatString == null)
await ctx.Reply($"Member names are now formatted with your global name format in this server.");
else
await ctx.Reply($"Member names are now formatted as `{formatString}` in this server.");
}
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 " +

View file

@ -312,21 +312,28 @@ public class Groups
ctx.CheckSystemPrivacy(target.System, target.IconPrivacy);
if ((target.Icon?.Trim() ?? "").Length > 0)
{
var eb = new EmbedBuilder()
.Title("Group icon")
.Image(new Embed.EmbedImage(target.Icon.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
eb.Description($"To clear, use `pk;group {target.Reference(ctx)} icon -clear`.");
await ctx.Reply(embed: eb.Build());
}
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.Icon.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing avatar for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.Icon.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("Group icon")
.Image(new Embed.EmbedImage(target.Icon.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `pk;group {target.Reference(ctx)} icon -clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
else
{
throw new PKSyntaxError(
"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())
@ -378,22 +385,29 @@ public class Groups
{
ctx.CheckSystemPrivacy(target.System, target.BannerPrivacy);
if ((target.BannerImage?.Trim() ?? "").Length > 0)
{
var eb = new EmbedBuilder()
.Title("Group banner image")
.Image(new Embed.EmbedImage(target.BannerImage));
if (target.System == ctx.System?.Id)
eb.Description($"To clear, use `pk;group {target.Reference(ctx)} banner clear`.");
await ctx.Reply(embed: eb.Build());
}
if ((target.Icon?.Trim() ?? "").Length > 0)
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing banner for group {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("Group banner image")
.Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `pk;group {target.Reference(ctx)} banner clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
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.");
}
"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.");
}
if (ctx.MatchClear())

View file

@ -11,7 +11,7 @@ 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 | Myriad design by @layl, art by @tedkalashnikov | 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,
};

View file

@ -86,12 +86,28 @@ public class MemberAvatar
if (location == MemberAvatarLocation.Server)
field += $" (for {ctx.Guild.Name})";
var eb = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s {field}")
.Image(new Embed.EmbedImage(currentValue?.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
eb.Description($"To clear, use `pk;member {target.Reference(ctx)} {location.Command()} clear`.");
await ctx.Reply(embed: eb.Build());
var format = ctx.MatchFormat();
if (format == ReplyFormat.Raw)
{
await ctx.Reply($"`{currentValue?.TryGetCleanCdnUrl()}`");
}
else if (format == ReplyFormat.Plaintext)
{
var eb = new EmbedBuilder()
.Description($"Showing {field} link for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply($"<{currentValue?.TryGetCleanCdnUrl()}>", embed: eb.Build());
return;
}
else if (format == ReplyFormat.Standard)
{
var eb = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s {field}")
.Image(new Embed.EmbedImage(currentValue?.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
eb.Description($"To clear, use `pk;member {target.Reference(ctx)} {location.Command()} clear`.");
await ctx.Reply(embed: eb.Build());
}
else throw new PKError("Format Not Recognized");
}
public async Task ServerAvatar(Context ctx, PKMember target)

View file

@ -133,7 +133,7 @@ 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);
@ -194,16 +194,18 @@ public class MemberEdit
public async Task BannerImage(Context ctx, PKMember target)
{
ctx.CheckOwnMember(target);
async Task ClearBannerImage()
{
ctx.CheckOwnMember(target);
await ctx.ConfirmClear("this member's banner image");
await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = null });
await ctx.Reply($"{Emojis.Success} Member banner image cleared.");
}
async Task SetBannerImage(ParsedImage img)
{
ctx.CheckOwnMember(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
@ -229,21 +231,31 @@ public class MemberEdit
async Task ShowBannerImage()
{
if ((target.BannerImage?.Trim() ?? "").Length > 0)
{
var eb = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s banner image")
.Image(new Embed.EmbedImage(target.BannerImage))
.Description($"To clear, use `pk;member {target.Reference(ctx)} banner clear`.");
await ctx.Reply(embed: eb.Build());
}
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing banner for member {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title($"{target.NameFor(ctx)}'s banner image")
.Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl()));
if (target.System == ctx.System?.Id)
ebS.Description($"To clear, use `pk;member {target.Reference(ctx)} banner clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
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.");
}
"This member does not have a banner image set." + ((target.System == ctx.System?.Id) ? " Set one by attaching an image to this command, or by passing an image URL." : ""));
}
if (ctx.MatchClear() && await ctx.ConfirmClear("this member's banner image"))
if (ctx.MatchClear())
await ClearBannerImage();
else if (await ctx.MatchImage() is { } img)
await SetBannerImage(img);

View file

@ -118,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)
@ -218,7 +219,7 @@ public class ProxiedMessage
try
{
var editedMsg =
await _webhookExecutor.EditWebhookMessage(msg.Guild ?? 0, 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 });

View file

@ -181,7 +181,7 @@ 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`."
: ""));
}
@ -351,7 +351,10 @@ public class ServerConfig
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.");
$"{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)

View file

@ -565,19 +565,28 @@ public class SystemEdit
async Task ShowIcon()
{
if ((target.AvatarUrl?.Trim() ?? "").Length > 0)
{
var eb = new EmbedBuilder()
.Title("System icon")
.Image(new Embed.EmbedImage(target.AvatarUrl.TryGetCleanCdnUrl()));
if (target.Id == ctx.System?.Id)
eb.Description("To clear, use `pk;system icon clear`.");
await ctx.Reply(embed: eb.Build());
}
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.AvatarUrl.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing icon for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.AvatarUrl.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("System icon")
.Image(new Embed.EmbedImage(target.AvatarUrl.TryGetCleanCdnUrl()));
if (target.Id == ctx.System?.Id)
ebS.Description("To clear, use `pk;system icon clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
else
{
throw new PKSyntaxError(
"This system does not have an icon set. Set one by attaching an image to this command, or by passing an image URL or @mention.");
}
}
if (target != null && target?.Id != ctx.System?.Id)
@ -639,19 +648,28 @@ public class SystemEdit
var settings = await ctx.Repository.GetSystemGuild(ctx.Guild.Id, target.Id);
if ((settings.AvatarUrl?.Trim() ?? "").Length > 0)
{
var eb = new EmbedBuilder()
.Title("System server icon")
.Image(new Embed.EmbedImage(settings.AvatarUrl.TryGetCleanCdnUrl()));
if (target.Id == ctx.System?.Id)
eb.Description("To clear, use `pk;system servericon clear`.");
await ctx.Reply(embed: eb.Build());
}
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{settings.AvatarUrl.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing icon for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{settings.AvatarUrl.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("System server icon")
.Image(new Embed.EmbedImage(settings.AvatarUrl.TryGetCleanCdnUrl()));
if (target.Id == ctx.System?.Id)
ebS.Description("To clear, use `pk;system servericon clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
else
{
throw new PKSyntaxError(
"This system does not have a icon specific to this server. Set one by attaching an image to this command, or by passing an image URL or @mention.");
}
}
ctx.CheckGuildContext();
@ -676,24 +694,31 @@ public class SystemEdit
var isOwnSystem = target.Id == ctx.System?.Id;
if (!ctx.HasNext() && ctx.Message.Attachments.Length == 0)
if ((!ctx.HasNext() && ctx.Message.Attachments.Length == 0) || ctx.PeekMatchFormat() != ReplyFormat.Standard)
{
if ((target.BannerImage?.Trim() ?? "").Length > 0)
{
var eb = new EmbedBuilder()
.Title("System banner image")
.Image(new Embed.EmbedImage(target.BannerImage));
if (isOwnSystem)
eb.Description("To clear, use `pk;system banner clear`.");
await ctx.Reply(embed: eb.Build());
}
switch (ctx.MatchFormat())
{
case ReplyFormat.Raw:
await ctx.Reply($"`{target.BannerImage.TryGetCleanCdnUrl()}`");
break;
case ReplyFormat.Plaintext:
var ebP = new EmbedBuilder()
.Description($"Showing banner for system {target.NameFor(ctx)} (`{target.DisplayHid(ctx.Config)}`)");
await ctx.Reply(text: $"<{target.BannerImage.TryGetCleanCdnUrl()}>", embed: ebP.Build());
break;
default:
var ebS = new EmbedBuilder()
.Title("System banner image")
.Image(new Embed.EmbedImage(target.BannerImage.TryGetCleanCdnUrl()));
if (target.Id == ctx.System?.Id)
ebS.Description("To clear, use `pk;system banner clear`.");
await ctx.Reply(embed: ebS.Build());
break;
}
else
{
throw new PKSyntaxError("This system does not have a banner image set."
+ (isOwnSystem ? "Set one by attaching an image to this command, or by passing an image URL or @mention." : ""));
}
return;
}
@ -842,7 +867,7 @@ public class SystemEdit
.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`, `banner`, `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`, `pronouns`, `list`, `front`, `fronthistory`, `groups`, or `all` \n- `level` is either `public` or `private`.");
return ctx.Reply(embed: eb.Build());
}

View file

@ -52,13 +52,19 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
if (!evt.Content.HasValue || !evt.Author.HasValue || !evt.Member.HasValue)
return;
var guildIdMaybe = evt.GuildId.HasValue ? evt.GuildId.Value ?? 0 : 0;
// 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.GetChannel(guildIdMaybe, evt.ChannelId); // todo: is this correct for message update?
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(guildIdMaybe, 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
@ -73,7 +79,7 @@ public class MessageEdited: IEventHandler<MessageUpdateEvent>
return;
var equivalentEvt = await GetMessageCreateEvent(evt, lastMessage, channel);
var botPermissions = await _cache.BotPermissionsIn(guildIdMaybe, channel.Id);
var botPermissions = await _cache.BotPermissionsIn(guildId, channel.Id);
try
{

View file

@ -496,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);

View file

@ -42,6 +42,8 @@ public class LoggerCleanService
private static readonly Regex _ProBotRegex = new("\\*\\*Message sent by <@(\\d{17,19})> deleted in <#\\d{17,19}>.\\*\\*");
private static readonly Regex _DozerRegex = new("Message ID: (\\d{17,19}) - (\\d{17,19})\nUserID: (\\d{17,19})");
private static readonly Regex _SkyraRegex = new("https://discord.com/channels/(\\d{17,19})/(\\d{17,19})/(\\d{17,19})");
private static readonly Regex _AnnabelleRegex = new("```\n(\\d{17,19})\n```");
private static readonly Regex _AnnabelleRegexFuzzy = new("\\<t:(\\d+)\\> A message from \\*\\*[\\w.]{2,32}\\*\\* \\(`(\\d{17,19})`\\) was deleted in <#\\d{17,19}>");
private static readonly Regex _VortexRegex =
new("`\\[(\\d\\d:\\d\\d:\\d\\d)\\]` .* \\(ID:(\\d{17,19})\\).* <#\\d{17,19}>:");
@ -79,6 +81,7 @@ public class LoggerCleanService
new LoggerBot("ProBot Prime", 567703512763334685, fuzzyExtractFunc: ExtractProBot), // webhook (?)
new LoggerBot("Dozer", 356535250932858885, ExtractDozer),
new LoggerBot("Skyra", 266624760782258186, ExtractSkyra),
new LoggerBot("Annabelle", 231241068383961088, fuzzyExtractFunc: ExtractAnnabelleFuzzy),
}.ToDictionary(b => b.Id);
private static Dictionary<ulong, LoggerBot> _botsByApplicationId
@ -119,28 +122,33 @@ public class LoggerCleanService
try
{
// We try two ways of extracting the actual message, depending on the bots
// Some bots have different log formats so we check for both types of extract function
if (bot.FuzzyExtractFunc != null)
{
// Some bots (Carl, Circle, etc) only give us a user ID and a rough timestamp, so we try our best to
// Some bots (Carl, Circle, etc) only give us a user ID, so we try our best to
// "cross-reference" those with the message DB. We know the deletion event happens *after* the message
// was sent, so we're checking for any messages sent in the same guild within 3 seconds before the
// delete event timestamp, which is... good enough, I think? Potential for false positives and negatives
// delete event log, which is... good enough, I think? Potential for false positives and negatives
// either way but shouldn't be too much, given it's constrained by user ID and guild.
var fuzzy = bot.FuzzyExtractFunc(msg);
if (fuzzy == null) return;
if (fuzzy != null)
{
_logger.Debug("Fuzzy logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}",
bot.Name, msg.Id, fuzzy);
_logger.Debug("Fuzzy logclean for {BotName} on {MessageId}: {@FuzzyExtractResult}",
bot.Name, msg.Id, fuzzy);
var exists = await _redis.HasLogCleanup(fuzzy.Value.User, msg.GuildId.Value);
var exists = await _redis.HasLogCleanup(fuzzy.Value.User, msg.GuildId.Value);
_logger.Debug(exists.ToString());
// If we didn't find a corresponding message, bail
if (!exists) return;
// If we didn't find a corresponding message, bail
if (!exists) return;
// Otherwise, we can *reasonably assume* that this is a logged deletion, so delete the log message.
await _client.DeleteMessage(msg.ChannelId, msg.Id);
// Otherwise, we can *reasonably assume* that this is a logged deletion, so delete the log message.
await _client.DeleteMessage(msg.ChannelId, msg.Id);
}
}
else if (bot.ExtractFunc != null)
if (bot.ExtractFunc != null)
{
// Other bots give us the message ID itself, and we can just extract that from the database directly.
var extractedId = bot.ExtractFunc(msg);
@ -150,10 +158,11 @@ public class LoggerCleanService
bot.Name, msg.Id, extractedId);
var mid = await _redis.GetOriginalMid(extractedId.Value);
if (mid == null) return;
// If we've gotten this far, we found a logged deletion of a trigger message. Just yeet it!
await _client.DeleteMessage(msg.ChannelId, msg.Id);
if (mid != null)
{
// If we've gotten this far, we found a logged deletion of a trigger message. Just yeet it!
await _client.DeleteMessage(msg.ChannelId, msg.Id);
}
} // else should not happen, but idk, it might
}
catch (NotFoundException)
@ -258,8 +267,8 @@ public class LoggerCleanService
private static FuzzyExtractResult? ExtractCircle(Message msg)
{
// Like Auttaja, Circle has both embed and compact modes, but the regex works for both.
// Compact: "Message from [user] ([id]) deleted in [channel]", no timestamp (use message time)
// Embed: Message Author field: "[user] ([id])", then an embed timestamp
// Compact: "Message from [user] ([id]) deleted in [channel]"
// Embed: Message Author field: "[user] ([id])"
var stringWithId = msg.Content;
if (msg.Embeds?.Length > 0)
{
@ -276,24 +285,21 @@ public class LoggerCleanService
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
User = ulong.Parse(match.Groups[1].Value)
}
: null;
}
private static FuzzyExtractResult? ExtractPancake(Message msg)
{
// Embed, author is "Message Deleted", description includes a mention, timestamp is *message send time* (but no ID)
// so we use the message timestamp to get somewhere *after* the message was proxied
// Embed, author is "Message Deleted", description includes a mention
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Description == null || embed.Author?.Name != "Message Deleted") return null;
var match = _pancakeRegex.Match(embed.Description);
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
User = ulong.Parse(match.Groups[1].Value)
}
: null;
}
@ -316,8 +322,7 @@ public class LoggerCleanService
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
User = ulong.Parse(match.Groups[1].Value)
}
: null;
}
@ -333,8 +338,7 @@ public class LoggerCleanService
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
User = ulong.Parse(match.Groups[1].Value)
}
: null;
}
@ -342,14 +346,12 @@ public class LoggerCleanService
private static FuzzyExtractResult? ExtractGearBot(Message msg)
{
// Simple text based message log.
// No message ID, but we have timestamp and author ID.
// Not using timestamp here though (seems to be same as message timestamp), might be worth implementing in the future.
// No message ID, but we have author ID.
var match = _GearBotRegex.Match(msg.Content);
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
User = ulong.Parse(match.Groups[1].Value)
}
: null;
}
@ -364,14 +366,11 @@ public class LoggerCleanService
private static FuzzyExtractResult? ExtractVortex(Message msg)
{
// timestamp is HH:MM:SS
// however, that can be set to the user's timezone, so we just use the message timestamp
var match = _VortexRegex.Match(msg.Content);
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[2].Value),
ApproxTimestamp = msg.Timestamp().ToInstant()
User = ulong.Parse(match.Groups[2].Value)
}
: null;
}
@ -379,15 +378,12 @@ public class LoggerCleanService
private static FuzzyExtractResult? ExtractProBot(Message msg)
{
// user ID and channel ID are in the embed description (we don't use channel ID)
// timestamp is in the embed footer
if (msg.Embeds.Length == 0 || msg.Embeds[0].Description == null) return null;
var match = _ProBotRegex.Match(msg.Embeds[0].Description);
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[1].Value),
ApproxTimestamp = OffsetDateTimePattern.Rfc3339
.Parse(msg.Embeds[0].Timestamp).GetValueOrThrow().ToInstant()
User = ulong.Parse(match.Groups[1].Value)
}
: null;
}
@ -407,6 +403,30 @@ public class LoggerCleanService
return match.Success ? ulong.Parse(match.Groups[3].Value) : null;
}
private static ulong? ExtractAnnabelle(Message msg)
{
// this bot has both an embed and a non-embed log format
// the embed is precise matching (this), the non-embed is fuzzy (below)
var embed = msg.Embeds?.FirstOrDefault();
if (embed?.Author?.Name == null || !embed.Author.Name.EndsWith("Deleted Message")) return null;
var match = _AnnabelleRegex.Match(embed.Fields[2].Value);
return match.Success ? ulong.Parse(match.Groups[1].Value) : null;
}
private static FuzzyExtractResult? ExtractAnnabelleFuzzy(Message msg)
{
// matching for annabelle's non-precise non-embed format
// it has a discord (unix) timestamp for the message so we use that
if (msg.Embeds.Length != 0) return null;
var match = _AnnabelleRegexFuzzy.Match(msg.Content);
return match.Success
? new FuzzyExtractResult
{
User = ulong.Parse(match.Groups[2].Value)
}
: null;
}
public class LoggerBot
{
public ulong Id;
@ -431,6 +451,5 @@ public class LoggerCleanService
public struct FuzzyExtractResult
{
public ulong User { get; set; }
public Instant ApproxTimestamp { get; set; }
}
}

View file

@ -19,7 +19,6 @@ using Newtonsoft.Json.Linq;
using Serilog;
using PluralKit.Core;
using Myriad.Utils;
namespace PluralKit.Bot;
@ -87,7 +86,8 @@ public class WebhookExecutorService
return webhookMessage;
}
public async Task<Message> EditWebhookMessage(ulong guildId, 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
{
@ -108,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);

View file

@ -116,6 +116,15 @@
"App.Metrics.Formatters.InfluxDB": "4.1.0"
}
},
"AppFact.SerilogOpenSearchSink": {
"type": "Transitive",
"resolved": "0.0.8",
"contentHash": "RI3lfmvAwhqrYwy5KPqsBT/tB/opSzeoVH2WUfvGKNBpl6ILCw/5wE8+19L+XMzBFVqgZ5QmkQ2PqTzG9I/ckA==",
"dependencies": {
"OpenSearch.Client": "1.4.0",
"Serilog": "2.12.0"
}
},
"Autofac": {
"type": "Transitive",
"resolved": "6.0.0",
@ -464,6 +473,24 @@
"Npgsql": "4.1.5"
}
},
"OpenSearch.Client": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "91TXm+I8PzxT0yxkf0q5Quee5stVcIFys56pU8+M3+Kiakx+aiaTAlZJHfA3Oy6dMJndNkVm37IPKYCqN3dS4g==",
"dependencies": {
"OpenSearch.Net": "1.4.0"
}
},
"OpenSearch.Net": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "xCM6m3aArN9gXIl2DvWXaDlbpjOBbgMeRPsAM7s1eDbWhu8wKNyfPNKSfep4JlYXkZ7N6Oi/+lmi+G3/SpcqlQ==",
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"System.Buffers": "4.5.1",
"System.Diagnostics.DiagnosticSource": "6.0.1"
}
},
"Pipelines.Sockets.Unofficial": {
"type": "Transitive",
"resolved": "2.2.8",
@ -726,8 +753,8 @@
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Collections": {
"type": "Transitive",
@ -780,8 +807,11 @@
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
"resolved": "6.0.1",
"contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Diagnostics.Tools": {
"type": "Transitive",
@ -1123,8 +1153,8 @@
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ=="
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Runtime.Extensions": {
"type": "Transitive",
@ -1459,6 +1489,7 @@
"dependencies": {
"App.Metrics": "[4.1.0, )",
"App.Metrics.Reporting.InfluxDB": "[4.1.0, )",
"AppFact.SerilogOpenSearchSink": "[0.0.8, )",
"Autofac": "[6.0.0, )",
"Autofac.Extensions.DependencyInjection": "[7.1.0, )",
"Dapper": "[2.0.35, )",

View file

@ -27,6 +27,7 @@ public class MessageContext
public string? SystemGuildTag { get; }
public bool TagEnabled { get; }
public string? NameFormat { get; }
public string? GuildNameFormat { get; }
public string? SystemAvatar { get; }
public string? SystemGuildAvatar { get; }
public bool AllowAutoproxy { get; }

View file

@ -9,7 +9,7 @@ public static class MessageContextExt
if (!ctx.TagEnabled || tag == null)
return false;
var format = ctx.NameFormat ?? ProxyMember.DefaultFormat;
var format = ctx.GuildNameFormat ?? ctx.NameFormat ?? ProxyMember.DefaultFormat;
if (!format.Contains("{tag}"))
return false;

View file

@ -45,7 +45,7 @@ public class ProxyMember
var tag = ctx.SystemGuildTag ?? ctx.SystemTag;
if (!ctx.TagEnabled) tag = null;
return FormatTag(ctx.NameFormat ?? DefaultFormat, tag, memberName);
return FormatTag(ctx.GuildNameFormat ?? ctx.NameFormat ?? DefaultFormat, tag, memberName);
}
public string? ProxyAvatar(MessageContext ctx) => ServerAvatar ?? WebhookAvatar ?? Avatar ?? ctx.SystemGuildAvatar ?? ctx.SystemAvatar;

View file

@ -16,6 +16,7 @@ create function message_context(account_id bigint, guild_id bigint, channel_id b
proxy_enabled bool,
system_guild_tag text,
system_guild_avatar text,
guild_name_format text,
last_switch int,
last_switch_members int[],
@ -51,6 +52,7 @@ as $$
coalesce(system_guild.proxy_enabled, true) as proxy_enabled,
system_guild.tag as system_guild_tag,
system_guild.avatar_url as system_guild_avatar,
system_guild.name_format as guild_name_format,
-- system_last_switch view
system_last_switch.switch as last_switch,

View file

@ -0,0 +1,6 @@
-- database version 49
-- add guild name format
alter table system_guild add column name_format text;
update info set schema_version = 49;

View file

@ -13,6 +13,7 @@ public class SystemGuildPatch: PatchObject
public Partial<bool?> TagEnabled { get; set; }
public Partial<string?> AvatarUrl { get; set; }
public Partial<string?> DisplayName { get; set; }
public Partial<string?> NameFormat { get; set; }
public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper
.With("proxy_enabled", ProxyEnabled)
@ -20,6 +21,7 @@ public class SystemGuildPatch: PatchObject
.With("tag_enabled", TagEnabled)
.With("avatar_url", AvatarUrl)
.With("display_name", DisplayName)
.With("name_format", NameFormat)
);
public new void AssertIsValid()
@ -53,6 +55,9 @@ public class SystemGuildPatch: PatchObject
if (o.ContainsKey("display_name"))
patch.DisplayName = o.Value<string>("display_name").NullIfEmpty();
if (o.ContainsKey("name_format"))
patch.NameFormat = o.Value<string>("name_format").NullIfEmpty();
return patch;
}
@ -77,6 +82,9 @@ public class SystemGuildPatch: PatchObject
if (DisplayName.IsPresent)
o.Add("display_name", DisplayName.Value);
if (NameFormat.IsPresent)
o.Add("name_format", NameFormat.Value);
return o;
}
}

View file

@ -11,6 +11,7 @@ public class SystemGuildSettings
public bool TagEnabled { get; }
public string? AvatarUrl { get; }
public string? DisplayName { get; }
public string? NameFormat { get; }
}
public static class SystemGuildExt
@ -24,6 +25,7 @@ public static class SystemGuildExt
o.Add("tag_enabled", settings.TagEnabled);
o.Add("avatar_url", settings.AvatarUrl);
o.Add("display_name", settings.DisplayName);
o.Add("name_format", settings.NameFormat);
return o;
}

View file

@ -2,6 +2,8 @@ using System.Globalization;
using Autofac;
using AppFact.SerilogOpenSearchSink;
using Microsoft.Extensions.Logging;
using NodaTime;
@ -9,7 +11,6 @@ using NodaTime;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
using Serilog.Sinks.Elasticsearch;
using Serilog.Sinks.Seq;
using Serilog.Sinks.SystemConsole.Themes;
@ -104,16 +105,12 @@ public class LoggingModule: Module
if (config.ElasticUrl != null)
{
var elasticConfig = new ElasticsearchSinkOptions(new Uri(config.ElasticUrl))
{
AutoRegisterTemplate = true,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
MinimumLogEventLevel = config.ElasticLogLevel,
IndexFormat = "pluralkit-logs-{0:yyyy.MM.dd}",
CustomFormatter = new ScalarFormatting.Elasticsearch()
};
logCfg.WriteTo.Elasticsearch(elasticConfig);
logCfg.WriteTo.OpenSearch(
uri: config.ElasticUrl,
index: "dotnet-logs",
basicAuthUser: "unused",
basicAuthPassword: "unused"
);
}
if (config.SeqLogUrl != null)

View file

@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="App.Metrics" Version="4.1.0" />
<PackageReference Include="App.Metrics.Reporting.InfluxDB" Version="4.1.0" />
<PackageReference Include="AppFact.SerilogOpenSearchSink" Version="0.0.8" />
<PackageReference Include="Autofac" Version="6.0.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.1.0" />
<PackageReference Include="Dapper" Version="2.0.35" />

View file

@ -22,6 +22,16 @@
"App.Metrics.Formatters.InfluxDB": "4.1.0"
}
},
"AppFact.SerilogOpenSearchSink": {
"type": "Direct",
"requested": "[0.0.8, )",
"resolved": "0.0.8",
"contentHash": "RI3lfmvAwhqrYwy5KPqsBT/tB/opSzeoVH2WUfvGKNBpl6ILCw/5wE8+19L+XMzBFVqgZ5QmkQ2PqTzG9I/ckA==",
"dependencies": {
"OpenSearch.Client": "1.4.0",
"Serilog": "2.12.0"
}
},
"Autofac": {
"type": "Direct",
"requested": "[6.0.0, )",
@ -551,6 +561,24 @@
"System.Xml.XDocument": "4.3.0"
}
},
"OpenSearch.Client": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "91TXm+I8PzxT0yxkf0q5Quee5stVcIFys56pU8+M3+Kiakx+aiaTAlZJHfA3Oy6dMJndNkVm37IPKYCqN3dS4g==",
"dependencies": {
"OpenSearch.Net": "1.4.0"
}
},
"OpenSearch.Net": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "xCM6m3aArN9gXIl2DvWXaDlbpjOBbgMeRPsAM7s1eDbWhu8wKNyfPNKSfep4JlYXkZ7N6Oi/+lmi+G3/SpcqlQ==",
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"System.Buffers": "4.5.1",
"System.Diagnostics.DiagnosticSource": "6.0.1"
}
},
"Pipelines.Sockets.Unofficial": {
"type": "Transitive",
"resolved": "2.2.8",
@ -692,8 +720,8 @@
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Collections": {
"type": "Transitive",
@ -746,8 +774,11 @@
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
"resolved": "6.0.1",
"contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Diagnostics.Tools": {
"type": "Transitive",
@ -1081,8 +1112,8 @@
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ=="
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Runtime.Extensions": {
"type": "Transitive",

View file

@ -108,6 +108,15 @@
"App.Metrics.Formatters.InfluxDB": "4.1.0"
}
},
"AppFact.SerilogOpenSearchSink": {
"type": "Transitive",
"resolved": "0.0.8",
"contentHash": "RI3lfmvAwhqrYwy5KPqsBT/tB/opSzeoVH2WUfvGKNBpl6ILCw/5wE8+19L+XMzBFVqgZ5QmkQ2PqTzG9I/ckA==",
"dependencies": {
"OpenSearch.Client": "1.4.0",
"Serilog": "2.12.0"
}
},
"Autofac": {
"type": "Transitive",
"resolved": "6.0.0",
@ -595,6 +604,24 @@
"resolved": "5.0.0",
"contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA=="
},
"OpenSearch.Client": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "91TXm+I8PzxT0yxkf0q5Quee5stVcIFys56pU8+M3+Kiakx+aiaTAlZJHfA3Oy6dMJndNkVm37IPKYCqN3dS4g==",
"dependencies": {
"OpenSearch.Net": "1.4.0"
}
},
"OpenSearch.Net": {
"type": "Transitive",
"resolved": "1.4.0",
"contentHash": "xCM6m3aArN9gXIl2DvWXaDlbpjOBbgMeRPsAM7s1eDbWhu8wKNyfPNKSfep4JlYXkZ7N6Oi/+lmi+G3/SpcqlQ==",
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"System.Buffers": "4.5.1",
"System.Diagnostics.DiagnosticSource": "6.0.1"
}
},
"Pipelines.Sockets.Unofficial": {
"type": "Transitive",
"resolved": "2.2.8",
@ -914,8 +941,8 @@
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Collections": {
"type": "Transitive",
@ -968,8 +995,11 @@
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw=="
"resolved": "6.0.1",
"contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Diagnostics.Tools": {
"type": "Transitive",
@ -1333,8 +1363,8 @@
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "4.7.1",
"contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ=="
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Runtime.Extensions": {
"type": "Transitive",
@ -1737,6 +1767,7 @@
"dependencies": {
"App.Metrics": "[4.1.0, )",
"App.Metrics.Reporting.InfluxDB": "[4.1.0, )",
"AppFact.SerilogOpenSearchSink": "[0.0.8, )",
"Autofac": "[6.0.0, )",
"Autofac.Extensions.DependencyInjection": "[7.1.0, )",
"Dapper": "[2.0.35, )",

View file

@ -150,6 +150,7 @@ You can have a space after `pk;`, e.g. `pk;system` and `pk; system` will do the
- `pk;config pad IDs [left|right|off]` - Toggles whether to pad (add a space) 5-character IDs in lists.
- `pk;config proxy switch [new|add|off]` - Toggles whether to log a switch whenever you proxy as a different member (or add member to recent switch in add mode).
- `pk;config name format [format]` - Changes your system's username formatting.
- `pk;config server name format [format]` - Changes your system's username formatting for the current server.
## Server owner commands
*(all commands here require Manage Server permission)*

View file

@ -24,7 +24,7 @@ You can suggest features in the [support server](https://discord.gg/PczBt78)'s `
We also track feature requests through [Github Issues](https://github.com/PluralKit/PluralKit/issues). Feel free to open issue reports or feature requests there as well.
### How can I support the bot's development?
I (the bot author, [Ske](https://twitter.com/floofstrid)) have a Patreon. The income from there goes towards server hosting, domains, infrastructure, my Monster Energy addiction, et cetera. There are no benefits. There might never be any. But nevertheless, it can be found here: [https://www.patreon.com/floofstrid](https://www.patreon.com/floofstrid)
We accept donations on [Patreon](https://patreon.com/pluralkit/) (recurring) and [Buy Me A Coffee](https://buymeacoffee.com/pluralkit/) (one-time). Any funds donated here will be used to pay for server hosting and (if anything is left over) development work.
### Can I recover my system if I lose access to my Discord account?
Yes, through one of two methods. Both require you to do preparations **before** you lose the account.

View file

@ -21,6 +21,7 @@ This requires you to have the *Manage Server* permission on the server.
### Supported bots
At the moment, log cleanup works with the following bots:
- Annabelle (precise in embed format, fuzzy in inline format)
- [Auttaja](https://auttaja.io/) (precise)
- [blargbot](https://blargbot.xyz/) (precise)
- [Carl-bot](https://carl.gg/) (precise)
@ -34,6 +35,7 @@ At the moment, log cleanup works with the following bots:
- [Mantaro](https://mantaro.site/) (precise)
- [Pancake](https://pancake.gg/) (fuzzy)
- [SafetyAtLast](https://www.safetyatlast.net/) (fuzzy)
- [Sapphire](https://sapph.xyz/) (precise, only in default format)
- [Skyra](https://www.skyra.pw/) (precise)
- [UnbelievaBoat](https://unbelievaboat.com/) (precise)
- Vanessa (fuzzy)

View file

@ -82,6 +82,7 @@ You cannot look up private members or groups of another system.
|pk;edit|-prepend|-p|Prepend the new content to the old message instead of overwriting it|
|pk;edit|-nospace|-ns|Append/prepend without adding a space|
|pk;edit|-clear-embed|-ce|Remove embeds from a message|
|pk;edit|-clear-attachments|-ca|Remove attachments from a message|
|pk;edit|-regex|-x|Edit using a C# Regex formatted like s\|X\|Y or s\|X\|Y\|F, where \| is any character, X is a Regex, Y is a substitution string, and F is a set of Regex flags|
|pk;switch edit and pk;switch add|-append|-a|Append members to the current switch or make a new switch with members appended|
|pk;switch edit and pk;switch add|-prepend|-p|Prepend members to the current switch or make a new switch with members prepended|

View file

@ -382,7 +382,7 @@ You can now set some proxy tags:
pk;member John proxy John:text
Now, oth of the following will work without needing to add multiple versions of the proxy tag:
Now, both of the following will work without needing to add multiple versions of the proxy tag:
John: Hello!
JOHN: Hello!
@ -394,6 +394,10 @@ The default proxy username formatting is "{name} {tag}", but you can customize t
pk;config nameformat {tag} {name}
pk;config nameformat {name}@{tag}
You can also do this on a per-server basis:
pk;config servernameformat {tag} {name}
pk;config servernameformat {name}@{tag}
## Interacting with proxied messages

View file

@ -1,5 +1,5 @@
go 1.19
go 1.23
use (
./services/scheduled_tasks
)
toolchain go1.23.2
use ./services/scheduled_tasks

View file

@ -1,3 +1,4 @@
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=

View file

@ -10,7 +10,9 @@ lazy_static = { workspace = true }
metrics = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }
sentry = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sqlx = { workspace = true }
time = { workspace = true }
tokio = { workspace = true }
@ -20,6 +22,7 @@ twilight-model = { workspace = true }
uuid = { workspace = true }
config = "0.14.0"
json-subscriber = { version = "0.2.2", features = ["env-filter"] }
metrics-exporter-prometheus = { version = "0.15.3", default-features = false, features = ["tokio", "http-listener", "tracing"] }
[build-dependencies]

View file

@ -102,6 +102,9 @@ pub struct PKConfig {
#[serde(default = "_json_log_default")]
pub(crate) json_log: bool,
#[serde(default)]
pub sentry_url: Option<String>,
}
impl PKConfig {

View file

@ -1,6 +1,9 @@
#![feature(let_chains)]
use std::net::SocketAddr;
use metrics_exporter_prometheus::PrometheusBuilder;
use tracing_subscriber::EnvFilter;
use sentry::IntoDsn;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
pub mod db;
pub mod proto;
@ -9,12 +12,18 @@ pub mod util;
pub mod _config;
pub use crate::_config::CONFIG as config;
// functions in this file are only used by the main function below
pub fn init_logging(component: &str) -> anyhow::Result<()> {
// todo: fix component
if config.json_log {
tracing_subscriber::fmt()
.json()
.with_env_filter(EnvFilter::from_default_env())
let mut layer = json_subscriber::layer();
layer.inner_layer_mut().add_static_field(
"component",
serde_json::Value::String(component.to_string()),
);
tracing_subscriber::registry()
.with(layer)
.with(EnvFilter::from_default_env())
.init();
} else {
tracing_subscriber::fmt()
@ -27,9 +36,46 @@ pub fn init_logging(component: &str) -> anyhow::Result<()> {
pub fn init_metrics() -> anyhow::Result<()> {
if config.run_metrics_server {
// automatically spawns a http listener at :9000
let builder = PrometheusBuilder::new();
builder.install()?;
PrometheusBuilder::new()
.with_http_listener("[::]:9000".parse::<SocketAddr>().unwrap())
.install()?;
}
Ok(())
}
pub fn init_sentry() -> sentry::ClientInitGuard {
sentry::init(sentry::ClientOptions {
dsn: config
.sentry_url
.clone()
.map(|u| u.into_dsn().unwrap())
.flatten(),
release: sentry::release_name!(),
..Default::default()
})
}
#[macro_export]
macro_rules! main {
($component:expr) => {
fn main() -> anyhow::Result<()> {
let _sentry_guard = libpk::init_sentry();
// we might also be able to use env!("CARGO_CRATE_NAME") here
libpk::init_logging($component)?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
if let Err(err) = libpk::init_metrics() {
tracing::error!("failed to init metrics collector: {err}");
};
tracing::info!("hello world");
if let Err(err) = real_main().await {
tracing::error!("failed to run service: {err}");
};
});
Ok(())
}
};
}

View file

@ -54,29 +54,9 @@ async fn rproxy(
// this function is manually formatted for easier legibility of route_services
#[rustfmt::skip]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
libpk::init_logging("api")?;
libpk::init_metrics()?;
info!("hello world");
let db = libpk::db::init_data_db().await?;
let redis = libpk::db::init_redis().await?;
let rproxy_uri = Uri::from_static(&libpk::config.api.as_ref().expect("missing api config").remote_url).to_string();
let rproxy_client = hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(HttpConnector::new());
let ctx = ApiContext {
db,
redis,
rproxy_uri: rproxy_uri[..rproxy_uri.len() - 1].to_string(),
rproxy_client,
};
fn router(ctx: ApiContext) -> Router {
// processed upside down (???) so we have to put middleware at the end
let app = Router::new()
Router::new()
.route("/v2/systems/:system_id", get(rproxy))
.route("/v2/systems/:system_id", patch(rproxy))
.route("/v2/systems/:system_id/settings", get(rproxy))
@ -133,19 +113,52 @@ async fn main() -> anyhow::Result<()> {
.route("/v2/members/:member_id/oembed.json", get(rproxy))
.route("/v2/groups/:group_id/oembed.json", get(rproxy))
.layer(axum::middleware::from_fn(middleware::logger))
.layer(middleware::ratelimit::ratelimiter(middleware::ratelimit::do_request_ratelimited)) // this sucks
.layer(axum::middleware::from_fn_with_state(ctx.clone(), middleware::authnz))
.layer(axum::middleware::from_fn(middleware::ignore_invalid_routes))
.layer(axum::middleware::from_fn(middleware::cors))
.layer(axum::middleware::from_fn(middleware::logger))
.layer(tower_http::catch_panic::CatchPanicLayer::custom(util::handle_panic))
.with_state(ctx)
.route("/", get(|| async { axum::response::Redirect::to("https://pluralkit.me/api") }));
.route("/", get(|| async { axum::response::Redirect::to("https://pluralkit.me/api") }))
}
libpk::main!("api");
async fn real_main() -> anyhow::Result<()> {
let db = libpk::db::init_data_db().await?;
let redis = libpk::db::init_redis().await?;
let rproxy_uri = Uri::from_static(
&libpk::config
.api
.as_ref()
.expect("missing api config")
.remote_url,
)
.to_string();
let rproxy_client = hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(HttpConnector::new());
let ctx = ApiContext {
db,
redis,
rproxy_uri: rproxy_uri[..rproxy_uri.len() - 1].to_string(),
rproxy_client,
};
let app = router(ctx);
let addr: &str = libpk::config
.api
.as_ref()
.expect("missing api config")
.addr
.as_ref();
let addr: &str = libpk::config.api.as_ref().expect("missing api config").addr.as_ref();
let listener = tokio::net::TcpListener::bind(addr).await?;
info!("listening on {}", addr);
axum::serve(listener, app).await?;

View file

@ -8,6 +8,8 @@ use tracing::error;
use crate::ApiContext;
use super::logger::DID_AUTHENTICATE_HEADER;
pub async fn authnz(State(ctx): State<ApiContext>, mut request: Request, next: Next) -> Response {
let headers = request.headers_mut();
headers.remove("x-pluralkit-systemid");
@ -15,6 +17,7 @@ pub async fn authnz(State(ctx): State<ApiContext>, mut request: Request, next: N
.get("authorization")
.map(|h| h.to_str().ok())
.flatten();
let mut authenticated = false;
if let Some(auth_header) = auth_header {
if let Some(system_id) =
match libpk::db::repository::legacy_token_auth(&ctx.db, auth_header).await {
@ -29,7 +32,14 @@ pub async fn authnz(State(ctx): State<ApiContext>, mut request: Request, next: N
"x-pluralkit-systemid",
HeaderValue::from_str(format!("{system_id}").as_str()).unwrap(),
);
authenticated = true;
}
}
next.run(request).await
let mut response = next.run(request).await;
if authenticated {
response
.headers_mut()
.insert(DID_AUTHENTICATE_HEADER, HeaderValue::from_static("1"));
}
response
}

View file

@ -1,7 +1,7 @@
use std::time::Instant;
use axum::{extract::MatchedPath, extract::Request, middleware::Next, response::Response};
use metrics::histogram;
use metrics::{counter, histogram};
use tracing::{info, span, warn, Instrument, Level};
use crate::util::header_or_unknown;
@ -10,11 +10,12 @@ use crate::util::header_or_unknown;
// todo: change as necessary
const MIN_LOG_TIME: u128 = 2_000;
pub const DID_AUTHENTICATE_HEADER: &'static str = "x-pluralkit-didauthenticate";
pub async fn logger(request: Request, next: Next) -> Response {
let method = request.method().clone();
let request_id = header_or_unknown(request.headers().get("Fly-Request-Id"));
let remote_ip = header_or_unknown(request.headers().get("Fly-Client-IP"));
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 endpoint = request
@ -26,10 +27,9 @@ pub async fn logger(request: Request, next: Next) -> Response {
let uri = request.uri().clone();
let request_id_span = span!(
let request_span = span!(
Level::INFO,
"request",
request_id,
remote_ip,
method = method.as_str(),
endpoint = endpoint.clone(),
@ -37,9 +37,37 @@ pub async fn logger(request: Request, next: Next) -> Response {
);
let start = Instant::now();
let response = next.run(request).instrument(request_id_span).await;
let mut response = next.run(request).instrument(request_span).await;
let elapsed = start.elapsed().as_millis();
let authenticated = {
let headers = response.headers_mut();
println!("{:#?}", headers.keys());
if headers.contains_key(DID_AUTHENTICATE_HEADER) {
headers.remove(DID_AUTHENTICATE_HEADER);
true
} else {
false
}
};
counter!(
"pluralkit_api_requests",
"method" => method.to_string(),
"endpoint" => endpoint.clone(),
"status" => response.status().to_string(),
"authenticated" => authenticated.to_string(),
)
.increment(1);
histogram!(
"pluralkit_api_requests_bucket",
"method" => method.to_string(),
"endpoint" => endpoint.clone(),
"status" => response.status().to_string(),
"authenticated" => authenticated.to_string(),
)
.record(elapsed as f64 / 1_000_f64);
info!(
"{} handled request for {} {} in {}ms",
response.status(),
@ -47,15 +75,17 @@ pub async fn logger(request: Request, next: Next) -> Response {
endpoint,
elapsed
);
histogram!(
"pk_http_requests",
"method" => method.to_string(),
"route" => endpoint.clone(),
"status" => response.status().to_string()
)
.record((elapsed as f64) / 1_000_f64);
if elapsed > MIN_LOG_TIME {
counter!(
"pluralkit_api_slow_requests_count",
"method" => method.to_string(),
"endpoint" => endpoint.clone(),
"status" => response.status().to_string(),
"authenticated" => authenticated.to_string(),
)
.increment(1);
warn!(
"request to {} full path {} (endpoint {}) took a long time ({}ms)!",
method,

View file

@ -1,88 +0,0 @@
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
ARG RUST_VERSION=1.75.0
ARG APP_NAME=pluralkit-avatars
################################################################################
# xx is a helper for cross-compilation.
# See https://github.com/tonistiigi/xx/ for more information.
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.3.0 AS xx
################################################################################
# Create a stage for building the application.
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine AS build
ARG APP_NAME
WORKDIR /app
# Copy cross compilation utilities from the xx stage.
COPY --from=xx / /
# Install host build dependencies.
RUN apk add --no-cache clang lld musl-dev git file
# This is the architecture youre building for, which is passed in by the builder.
# Placing it here allows the previous steps to be cached across architectures.
ARG TARGETPLATFORM
# Install cross compilation build dependencies.
RUN xx-apk add --no-cache musl-dev gcc
# Build the application.
# Leverage a cache mount to /usr/local/cargo/registry/
# for downloaded dependencies, a cache mount to /usr/local/cargo/git/db
# for git repository dependencies, and a cache mount to /app/target/ for
# compiled dependencies which will speed up subsequent builds.
# Leverage a bind mount to the src directory to avoid having to copy the
# source code into the container. Once built, copy the executable to an
# output directory before the cache mounted /app/target is unmounted.
# XXX: removed `id` from target mount, see: https://github.com/reproducible-containers/buildkit-cache-dance/issues/12
RUN --mount=type=bind,source=src,target=src \
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
--mount=type=cache,target=/app/target/$TARGETPLATFORM/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry/ \
<<EOF
set -e
xx-cargo build --locked --release --target-dir ./target/$TARGETPLATFORM/
cp ./target/$TARGETPLATFORM/$(xx-cargo --print-target-triple)/release/$APP_NAME /bin/server
xx-verify /bin/server
EOF
################################################################################
# Create a new stage for running the application that contains the minimal
# runtime dependencies for the application. This often uses a different base
# image from the build stage where the necessary files are copied from the build
# stage.
#
# The example below uses the alpine image as the foundation for running the app.
# By specifying the "3.18" tag, it will use version 3.18 of alpine. If
# reproducability is important, consider using a digest
# (e.g., alpine@sha256:664888ac9cfd28068e062c991ebcff4b4c7307dc8dd4df9e728bedde5c449d91).
FROM alpine:3.18 AS final
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
USER appuser
# Copy the executable from the "build" stage.
COPY --from=build /bin/server /bin/
# Expose the port that the application listens on.
EXPOSE 3000
# What the container should run when it is started.
CMD ["/bin/server"]

View file

@ -4,12 +4,8 @@ use sqlx::prelude::FromRow;
use std::{sync::Arc, time::Duration};
use tracing::{error, info};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
libpk::init_logging("avatar_cleanup")?;
libpk::init_metrics()?;
info!("hello world");
libpk::main!("avatar_cleanup");
async fn real_main() -> anyhow::Result<()> {
let config = libpk::config
.avatars
.as_ref()
@ -129,7 +125,7 @@ async fn cleanup_job(pool: sqlx::PgPool, bucket: Arc<s3::Bucket>) -> anyhow::Res
}
_ => {
let status = cf_resp.status();
println!("{:#?}", cf_resp.text().await?);
tracing::info!("raw response from cloudflare: {:#?}", cf_resp.text().await?);
anyhow::bail!("cloudflare returned bad error code {}", status);
}
}

View file

@ -142,12 +142,8 @@ pub struct AppState {
config: Arc<AvatarsConfig>,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
libpk::init_logging("avatars")?;
libpk::init_metrics()?;
info!("hello world");
libpk::main!("avatars");
async fn real_main() -> anyhow::Result<()> {
let config = libpk::config
.avatars
.as_ref()

View file

@ -19,6 +19,8 @@ use axum::{extract::State, http::Uri, routing::post, Json, Router};
mod logger;
// this package does not currently use libpk
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()

View file

@ -24,3 +24,5 @@ twilight-cache-inmemory = { workspace = true }
twilight-util = { workspace = true }
twilight-model = { workspace = true }
twilight-http = { workspace = true }
serde_variant = "0.1.3"

View file

@ -47,7 +47,6 @@ pub async fn run_server(cache: Arc<DiscordCache>) -> anyhow::Result<()> {
get(|State(cache): State<Arc<DiscordCache>>, Path(guild_id): Path<u64>| async move {
match cache.guild_permissions(Id::new(guild_id), libpk::config.discord.as_ref().expect("missing discord config").client_id).await {
Ok(val) => {
println!("hh {}", Permissions::all().bits());
status_code(StatusCode::FOUND, to_string(&val.bits()).unwrap())
},
Err(err) => {

View file

@ -1,4 +1,5 @@
use libpk::_config::ClusterSettings;
use metrics::counter;
use std::sync::{mpsc::Sender, Arc};
use tracing::{info, warn};
use twilight_gateway::{
@ -85,6 +86,18 @@ pub async fn runner(
while let Some(item) = shard.next_event(EventTypeFlags::all()).await {
match item {
Ok(event) => {
// event_type * shard_id is too many labels and prometheus fails to query it
// so we split it into two metrics
counter!(
"pluralkit_gateway_events_type",
"event_type" => serde_variant::to_variant_name(&event.kind()).unwrap(),
)
.increment(1);
counter!(
"pluralkit_gateway_events_shard",
"shard_id" => shard.id().number().to_string(),
)
.increment(1);
if let Err(error) = shard_state
.handle_event(shard.id().number(), event.clone())
.await

View file

@ -1,5 +1,6 @@
use bytes::Bytes;
use fred::{clients::RedisPool, interfaces::HashesInterface};
use metrics::{counter, gauge};
use prost::Message;
use tracing::info;
use twilight_gateway::Event;
@ -42,7 +43,7 @@ impl ShardStateManager {
async fn save_shard(&self, shard_id: u32, info: ShardState) -> anyhow::Result<()> {
self.redis
.hset(
.hset::<(), &str, (String, Bytes)>(
"pluralkit:shardstatus",
(
shard_id.to_string(),
@ -59,6 +60,13 @@ impl ShardStateManager {
shard_id,
if resumed { "resumed" } else { "ready" }
);
counter!(
"pluralkit_gateway_shard_reconnect",
"shard_id" => shard_id.to_string(),
"resumed" => resumed.to_string(),
)
.increment(1);
gauge!("pluralkit_gateway_shard_up").increment(1);
let mut info = self.get_shard(shard_id).await?;
info.last_connection = chrono::offset::Utc::now().timestamp() as i32;
info.up = true;
@ -68,6 +76,7 @@ impl ShardStateManager {
async fn socket_closed(&self, shard_id: u32) -> anyhow::Result<()> {
info!("shard {} closed", shard_id);
gauge!("pluralkit_gateway_shard_up").decrement(1);
let mut info = self.get_shard(shard_id).await?;
info.up = false;
info.disconnection_count += 1;

View file

@ -1,6 +1,9 @@
use std::time::Instant;
use axum::{extract::MatchedPath, extract::Request, middleware::Next, response::Response};
use axum::{
extract::MatchedPath, extract::Request, http::StatusCode, middleware::Next, response::Response,
};
use metrics::{counter, histogram};
use tracing::{info, span, warn, Instrument, Level};
// log any requests that take longer than 2 seconds
@ -30,13 +33,30 @@ pub async fn logger(request: Request, next: Next) -> Response {
let response = next.run(request).instrument(request_id_span).await;
let elapsed = start.elapsed().as_millis();
info!(
"{} handled request for {} {} in {}ms",
response.status(),
method,
uri.path(),
elapsed
);
counter!(
"pluralkit_gateway_cache_api_requests",
"method" => method.to_string(),
"endpoint" => endpoint.clone(),
"status" => response.status().to_string(),
)
.increment(1);
histogram!(
"pluralkit_gateway_cache_api_requests_bucket",
"method" => method.to_string(),
"endpoint" => endpoint.clone(),
"status" => response.status().to_string(),
)
.record(elapsed as f64 / 1_000_f64);
if response.status() != StatusCode::FOUND {
info!(
"{} handled request for {} {} in {}ms",
response.status(),
method,
uri.path(),
elapsed
);
}
if elapsed > MIN_LOG_TIME {
warn!(

View file

@ -18,12 +18,8 @@ mod cache_api;
mod discord;
mod logger;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
libpk::init_logging("gateway")?;
libpk::init_metrics()?;
info!("hello world");
libpk::main!("gateway");
async fn real_main() -> anyhow::Result<()> {
let (shutdown_tx, shutdown_rx) = channel::<()>();
let shutdown_tx = Arc::new(shutdown_tx);
@ -69,7 +65,7 @@ async fn main() -> anyhow::Result<()> {
let mut signals = Signals::new(&[SIGINT, SIGTERM])?;
tokio::spawn(async move {
set.spawn(tokio::spawn(async move {
for sig in signals.forever() {
info!("received signal {:?}", sig);
@ -86,7 +82,7 @@ async fn main() -> anyhow::Result<()> {
let _ = shutdown_tx.send(());
break;
}
});
}));
let _ = shutdown_rx.recv();

View file

@ -6,7 +6,7 @@ WORKDIR /build
COPY ./ /build
RUN go build .
RUN GOTOOLCHAIN=auto go build .
FROM alpine:latest

View file

@ -1,6 +1,6 @@
module scheduled_tasks
go 1.18
go 1.23
require (
github.com/getsentry/sentry-go v0.15.0

View file

@ -20,59 +20,26 @@ type httpstats struct {
func query_http_cache() []httpstats {
var values []httpstats
url := os.Getenv("CONSUL_URL")
if url == "" {
panic("missing CONSUL_URL in environment")
http_cache_url := os.Getenv("HTTP_CACHE_URL")
if http_cache_url == "" {
panic("missing HTTP_CACHE_URL in environment")
}
expected_gateway_count, err := strconv.Atoi(os.Getenv("EXPECTED_GATEWAY_COUNT"))
cluster_count, err := strconv.Atoi(os.Getenv("CLUSTER_COUNT"))
if err != nil {
panic(fmt.Sprintf("missing or invalid EXPECTED_GATEWAY_COUNT in environment"))
panic(fmt.Sprintf("missing or invalid CLUSTER_COUNT in environment"))
}
resp, err := http.Get(fmt.Sprintf("%v/v1/health/service/pluralkit-gateway", url))
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
panic(fmt.Sprintf("got status %v trying to query consul for all_gateway_instances", resp.Status))
}
var ips []string
data, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var cs []any
err = json.Unmarshal(data, &cs)
if err != nil {
panic(err)
}
if len(cs) != expected_gateway_count {
panic(fmt.Sprintf("got unexpected number of gateway instances from consul (expected %v, got %v)", expected_gateway_count, len(cs)))
}
for idx, itm := range cs {
if ip, ok := itm.(map[string]any)["Service"].(map[string]any)["Address"].(string); ok {
ips = append(ips, ip)
} else {
panic(fmt.Sprintf("got bad data from consul for all_gateway_instances, at index %v", idx))
}
}
log.Printf("querying %v gateway clusters for discord stats\n", len(ips))
for _, ip := range ips {
resp, err := http.Get("http://"+ip+":5000/stats")
for i := range cluster_count {
log.Printf("querying gateway cluster %v for discord stats\n", i)
url := fmt.Sprintf("http://cluster%v.%s:5000/stats", i, http_cache_url)
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusFound {
panic(fmt.Sprintf("got status %v trying to query %v:5000", resp.Status, ip))
panic(fmt.Sprintf("got status %v trying to query %v.%s:5000", resp.Status, i, http_cache_url))
}
var s httpstats
data, err := io.ReadAll(resp.Body)