mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-04 13:06:50 +00:00
Merge branch 'main' into proxyswitch-add
This commit is contained in:
commit
fbebe38afe
59 changed files with 1000 additions and 423 deletions
289
Cargo.lock
generated
289
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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, )",
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 " +
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, )",
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
6
PluralKit.Core/Database/Migrations/49.sql
Normal file
6
PluralKit.Core/Database/Migrations/49.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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, )",
|
||||
|
|
|
|||
|
|
@ -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)*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
8
go.work
8
go.work
|
|
@ -1,5 +1,5 @@
|
|||
go 1.19
|
||||
go 1.23
|
||||
|
||||
use (
|
||||
./services/scheduled_tasks
|
||||
)
|
||||
toolchain go1.23.2
|
||||
|
||||
use ./services/scheduled_tasks
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 you’re 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"]
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ WORKDIR /build
|
|||
|
||||
COPY ./ /build
|
||||
|
||||
RUN go build .
|
||||
RUN GOTOOLCHAIN=auto go build .
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module scheduled_tasks
|
||||
|
||||
go 1.18
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/getsentry/sentry-go v0.15.0
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue