diff --git a/Cargo.lock b/Cargo.lock index b103416c..cb904814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 314c0cee..2c7aa9de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/Myriad/Extensions/CacheExtensions.cs b/Myriad/Extensions/CacheExtensions.cs index 0f6fb931..84045784 100644 --- a/Myriad/Extensions/CacheExtensions.cs +++ b/Myriad/Extensions/CacheExtensions.cs @@ -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; } } \ No newline at end of file diff --git a/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs b/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs index e1765a6f..512464d3 100644 --- a/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs +++ b/Myriad/Rest/Types/Requests/WebhookMessageEditRequest.cs @@ -15,4 +15,7 @@ public record WebhookMessageEditRequest [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Optional Embeds { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Optional Attachments { get; init; } } \ No newline at end of file diff --git a/PluralKit.API/packages.lock.json b/PluralKit.API/packages.lock.json index 0d41f905..5418e85c 100644 --- a/PluralKit.API/packages.lock.json +++ b/PluralKit.API/packages.lock.json @@ -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, )", diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 54c004e1..157b7edb 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -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 = diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index a458e1d4..51f8f449 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -596,6 +596,8 @@ public partial class CommandTree return ctx.Execute(null, m => m.LimitUpdate(ctx)); if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "switch" }) || ctx.Match("proxyswitch", "ps")) return ctx.Execute(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(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."); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index 1a958c8e..982eec77 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -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); diff --git a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs index ca2ff912..3501b53b 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextAvatarExt.cs @@ -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) diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index 593af15d..00da3a52 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -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( 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 " + diff --git a/PluralKit.Bot/Commands/Groups.cs b/PluralKit.Bot/Commands/Groups.cs index a6105275..0716a7c6 100644 --- a/PluralKit.Bot/Commands/Groups.cs +++ b/PluralKit.Bot/Commands/Groups.cs @@ -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()) diff --git a/PluralKit.Bot/Commands/Help.cs b/PluralKit.Bot/Commands/Help.cs index 47d18944..3894c3f5 100644 --- a/PluralKit.Bot/Commands/Help.cs +++ b/PluralKit.Bot/Commands/Help.cs @@ -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, }; diff --git a/PluralKit.Bot/Commands/MemberAvatar.cs b/PluralKit.Bot/Commands/MemberAvatar.cs index 66c17de1..b81c4754 100644 --- a/PluralKit.Bot/Commands/MemberAvatar.cs +++ b/PluralKit.Bot/Commands/MemberAvatar.cs @@ -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) diff --git a/PluralKit.Bot/Commands/MemberEdit.cs b/PluralKit.Bot/Commands/MemberEdit.cs index c9fc943b..b3c2e837 100644 --- a/PluralKit.Bot/Commands/MemberEdit.cs +++ b/PluralKit.Bot/Commands/MemberEdit.cs @@ -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 `."; + noPronounsSetMessage += $" To set some, type `pk;member {target.Reference(ctx)} 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); diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 6fc271a1..9a3b31fb 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -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 }); diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index 16cf31ee..922adcf7 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -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) diff --git a/PluralKit.Bot/Commands/SystemEdit.cs b/PluralKit.Bot/Commands/SystemEdit.cs index 264761ec..51fa3e50 100644 --- a/PluralKit.Bot/Commands/SystemEdit.cs +++ b/PluralKit.Bot/Commands/SystemEdit.cs @@ -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 `\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 `\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()); } diff --git a/PluralKit.Bot/Handlers/MessageEdited.cs b/PluralKit.Bot/Handlers/MessageEdited.cs index 468a8654..168feecb 100644 --- a/PluralKit.Bot/Handlers/MessageEdited.cs +++ b/PluralKit.Bot/Handlers/MessageEdited.cs @@ -52,13 +52,19 @@ public class MessageEdited: IEventHandler 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 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 { diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index d7d304fb..f7ce4073 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -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); diff --git a/PluralKit.Bot/Services/LoggerCleanService.cs b/PluralKit.Bot/Services/LoggerCleanService.cs index fc5855c9..37193591 100644 --- a/PluralKit.Bot/Services/LoggerCleanService.cs +++ b/PluralKit.Bot/Services/LoggerCleanService.cs @@ -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("\\ 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 _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; } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/WebhookExecutorService.cs b/PluralKit.Bot/Services/WebhookExecutorService.cs index fc23388c..999920d6 100644 --- a/PluralKit.Bot/Services/WebhookExecutorService.cs +++ b/PluralKit.Bot/Services/WebhookExecutorService.cs @@ -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 EditWebhookMessage(ulong guildId, ulong channelId, ulong messageId, string newContent, bool clearEmbeds = false) + public async Task 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.Some(new Embed[] { }) : Optional.None()), + Embeds = (clearEmbeds ? Optional.Some(new Embed[] { }) : Optional.None()), + Attachments = (clearAttachments + ? Optional.Some(new Message.Attachment[] { }) + : Optional.None()) }; return await _rest.EditWebhookMessage(webhook.Id, webhook.Token, messageId, editReq, threadId); diff --git a/PluralKit.Bot/packages.lock.json b/PluralKit.Bot/packages.lock.json index 6ef93128..c74411cc 100644 --- a/PluralKit.Bot/packages.lock.json +++ b/PluralKit.Bot/packages.lock.json @@ -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, )", diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 6de53636..ce579522 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -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; } diff --git a/PluralKit.Core/Database/Functions/MessageContextExt.cs b/PluralKit.Core/Database/Functions/MessageContextExt.cs index bca26b2d..de6d2c04 100644 --- a/PluralKit.Core/Database/Functions/MessageContextExt.cs +++ b/PluralKit.Core/Database/Functions/MessageContextExt.cs @@ -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; diff --git a/PluralKit.Core/Database/Functions/ProxyMember.cs b/PluralKit.Core/Database/Functions/ProxyMember.cs index 0df7cf35..2c97468f 100644 --- a/PluralKit.Core/Database/Functions/ProxyMember.cs +++ b/PluralKit.Core/Database/Functions/ProxyMember.cs @@ -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; diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 2e82de3f..895d064d 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -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, diff --git a/PluralKit.Core/Database/Migrations/49.sql b/PluralKit.Core/Database/Migrations/49.sql new file mode 100644 index 00000000..837c9e0b --- /dev/null +++ b/PluralKit.Core/Database/Migrations/49.sql @@ -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; \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/SystemGuildPatch.cs b/PluralKit.Core/Models/Patch/SystemGuildPatch.cs index f19ff00f..25cd8271 100644 --- a/PluralKit.Core/Models/Patch/SystemGuildPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemGuildPatch.cs @@ -13,6 +13,7 @@ public class SystemGuildPatch: PatchObject public Partial TagEnabled { get; set; } public Partial AvatarUrl { get; set; } public Partial DisplayName { get; set; } + public Partial 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("display_name").NullIfEmpty(); + if (o.ContainsKey("name_format")) + patch.NameFormat = o.Value("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; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/SystemGuildSettings.cs b/PluralKit.Core/Models/SystemGuildSettings.cs index 439712e0..231d08a3 100644 --- a/PluralKit.Core/Models/SystemGuildSettings.cs +++ b/PluralKit.Core/Models/SystemGuildSettings.cs @@ -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; } diff --git a/PluralKit.Core/Modules/LoggingModule.cs b/PluralKit.Core/Modules/LoggingModule.cs index dd026207..18ffa58b 100644 --- a/PluralKit.Core/Modules/LoggingModule.cs +++ b/PluralKit.Core/Modules/LoggingModule.cs @@ -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) diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index a11d9333..3ff78105 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -18,6 +18,7 @@ + diff --git a/PluralKit.Core/packages.lock.json b/PluralKit.Core/packages.lock.json index 7f9fef36..e9ca102e 100644 --- a/PluralKit.Core/packages.lock.json +++ b/PluralKit.Core/packages.lock.json @@ -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", diff --git a/PluralKit.Tests/packages.lock.json b/PluralKit.Tests/packages.lock.json index 122f7381..3feb41de 100644 --- a/PluralKit.Tests/packages.lock.json +++ b/PluralKit.Tests/packages.lock.json @@ -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, )", diff --git a/docs/content/command-list.md b/docs/content/command-list.md index de58dc45..fffd9073 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -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)* diff --git a/docs/content/faq.md b/docs/content/faq.md index 7e486d59..27b1ef3a 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -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. diff --git a/docs/content/staff/compatibility.md b/docs/content/staff/compatibility.md index c40fb817..5c3bcabe 100644 --- a/docs/content/staff/compatibility.md +++ b/docs/content/staff/compatibility.md @@ -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) diff --git a/docs/content/tips-and-tricks.md b/docs/content/tips-and-tricks.md index 2385ab70..7acba69a 100644 --- a/docs/content/tips-and-tricks.md +++ b/docs/content/tips-and-tricks.md @@ -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| diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index 0cf6475a..ad86343b 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -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 diff --git a/go.work b/go.work index fb60983f..a9e9d1ec 100644 --- a/go.work +++ b/go.work @@ -1,5 +1,5 @@ -go 1.19 +go 1.23 -use ( - ./services/scheduled_tasks -) +toolchain go1.23.2 + +use ./services/scheduled_tasks diff --git a/go.work.sum b/go.work.sum index 20bb40cf..72bcf761 100644 --- a/go.work.sum +++ b/go.work.sum @@ -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= diff --git a/lib/libpk/Cargo.toml b/lib/libpk/Cargo.toml index 5a4b8eb5..a21bf55f 100644 --- a/lib/libpk/Cargo.toml +++ b/lib/libpk/Cargo.toml @@ -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] diff --git a/lib/libpk/src/_config.rs b/lib/libpk/src/_config.rs index 2249b4aa..6f69399b 100644 --- a/lib/libpk/src/_config.rs +++ b/lib/libpk/src/_config.rs @@ -102,6 +102,9 @@ pub struct PKConfig { #[serde(default = "_json_log_default")] pub(crate) json_log: bool, + + #[serde(default)] + pub sentry_url: Option, } impl PKConfig { diff --git a/lib/libpk/src/lib.rs b/lib/libpk/src/lib.rs index e1a9561c..19d4e1ee 100644 --- a/lib/libpk/src/lib.rs +++ b/lib/libpk/src/lib.rs @@ -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::().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(()) + } + }; +} diff --git a/services/api/src/main.rs b/services/api/src/main.rs index dbd2d46f..bbf3c6ee 100644 --- a/services/api/src/main.rs +++ b/services/api/src/main.rs @@ -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?; diff --git a/services/api/src/middleware/authnz.rs b/services/api/src/middleware/authnz.rs index e1b44941..4544e6bf 100644 --- a/services/api/src/middleware/authnz.rs +++ b/services/api/src/middleware/authnz.rs @@ -8,6 +8,8 @@ use tracing::error; use crate::ApiContext; +use super::logger::DID_AUTHENTICATE_HEADER; + pub async fn authnz(State(ctx): State, 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, 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, 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 } diff --git a/services/api/src/middleware/logger.rs b/services/api/src/middleware/logger.rs index 7250f225..cfb84b4a 100644 --- a/services/api/src/middleware/logger.rs +++ b/services/api/src/middleware/logger.rs @@ -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, diff --git a/services/avatars/Dockerfile b/services/avatars/Dockerfile deleted file mode 100644 index 21864cc2..00000000 --- a/services/avatars/Dockerfile +++ /dev/null @@ -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/ \ - < 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) -> 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); } } diff --git a/services/avatars/src/main.rs b/services/avatars/src/main.rs index 44bbfe69..6a294f2d 100644 --- a/services/avatars/src/main.rs +++ b/services/avatars/src/main.rs @@ -142,12 +142,8 @@ pub struct AppState { config: Arc, } -#[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() diff --git a/services/dispatch/src/main.rs b/services/dispatch/src/main.rs index 1e28e7aa..a9d6d2af 100644 --- a/services/dispatch/src/main.rs +++ b/services/dispatch/src/main.rs @@ -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() diff --git a/services/gateway/Cargo.toml b/services/gateway/Cargo.toml index b1a2daa7..6b9f3e25 100644 --- a/services/gateway/Cargo.toml +++ b/services/gateway/Cargo.toml @@ -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" diff --git a/services/gateway/src/cache_api.rs b/services/gateway/src/cache_api.rs index efbca0fc..df189a31 100644 --- a/services/gateway/src/cache_api.rs +++ b/services/gateway/src/cache_api.rs @@ -47,7 +47,6 @@ pub async fn run_server(cache: Arc) -> anyhow::Result<()> { get(|State(cache): State>, Path(guild_id): Path| 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) => { diff --git a/services/gateway/src/discord/gateway.rs b/services/gateway/src/discord/gateway.rs index 15d5ac80..a50fda8e 100644 --- a/services/gateway/src/discord/gateway.rs +++ b/services/gateway/src/discord/gateway.rs @@ -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 diff --git a/services/gateway/src/discord/shard_state.rs b/services/gateway/src/discord/shard_state.rs index a301f04b..f92ff866 100644 --- a/services/gateway/src/discord/shard_state.rs +++ b/services/gateway/src/discord/shard_state.rs @@ -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; diff --git a/services/gateway/src/logger.rs b/services/gateway/src/logger.rs index aa65bc67..459aef31 100644 --- a/services/gateway/src/logger.rs +++ b/services/gateway/src/logger.rs @@ -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!( diff --git a/services/gateway/src/main.rs b/services/gateway/src/main.rs index 8ac380b1..ae9e840e 100644 --- a/services/gateway/src/main.rs +++ b/services/gateway/src/main.rs @@ -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(); diff --git a/services/scheduled_tasks/Dockerfile b/services/scheduled_tasks/Dockerfile index 9e23b7af..8f790bd1 100644 --- a/services/scheduled_tasks/Dockerfile +++ b/services/scheduled_tasks/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /build COPY ./ /build -RUN go build . +RUN GOTOOLCHAIN=auto go build . FROM alpine:latest diff --git a/services/scheduled_tasks/go.mod b/services/scheduled_tasks/go.mod index 7cea5024..74020db5 100644 --- a/services/scheduled_tasks/go.mod +++ b/services/scheduled_tasks/go.mod @@ -1,6 +1,6 @@ module scheduled_tasks -go 1.18 +go 1.23 require ( github.com/getsentry/sentry-go v0.15.0 diff --git a/services/scheduled_tasks/repo.go b/services/scheduled_tasks/repo.go index ece7e0dd..31554850 100644 --- a/services/scheduled_tasks/repo.go +++ b/services/scheduled_tasks/repo.go @@ -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)