Many fixes and tweaks

* Fix tags (again?)

* Make tag names more consistent

* Remove prefix commands (not implemented well and not worth fixing)

* Allow '-'s in tags

* Fix /joke

* Fix /members

* Fix intel_hd issue match

* Fix log analysis reply

* Clearer log analysis messages

It's weird to say the process failed when no issues were found.

* Clippy

* Final doc cleanup

* Fix link expanding

* Fix duplicate event filtering

The other code simply does not work. ChannelId does have a method to grab members but I'm not sure whether it would work either.

* Remove message resolution

It's surprisingly hard to create an bug-free implementation.

* Fix pluralkit detection

* simplify tag codegen

* commands: improve error handling in members

unwrap() bad!!!11!!

* events: use debug logs for pk checks

* Revert "Remove message resolution"

This reverts commit 0d9f224a81917212adafdeb2213f3cc11b44cf88.

* Bring back prefix commands with "."

(it's easier to type)

* Add help

* Fix messsage resolution

* utils: factor out message resolution

* Improve tag message

* Disable VC support for message resolution for now

* Improve prefix command usage

Update on edit, display additional tip with wrong usage.

* Check invoke_on_edit to display tip

* Add defer in commands which make http requests

* Apply tag sorting to slash commands too

* handlers::error: `+=` -> `writeln!`

* handlers::event: ignore own new messages

* help: remove unneeded format!

* optimize for size in release builds

* nix: cleanup deployment expressions

* nix: use treefmt

* nix: update flake.lock

Flake lock file updates:

• Updated input 'fenix':
    'github:nix-community/fenix/eb683549b7d76b12d1a009f888b91b70ed34485f' (2024-01-27)
  → 'github:nix-community/fenix/c53bb4a32f2fce7acf4e8e160a54779c4460ffdb' (2024-03-17)
• Updated input 'fenix/rust-analyzer-src':
    'github:rust-lang/rust-analyzer/596e5c77cf5b2b660b3ac2ce732fa0596c246d9b' (2024-01-26)
  → 'github:rust-lang/rust-analyzer/5ecace48f693afaa6adf8cb23086b651db3aec96' (2024-03-16)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/4fddc9be4eaf195d631333908f2a454b03628ee5' (2024-01-25)
  → 'github:nixos/nixpkgs/34ad8c9f29a18b4dd97a9ad40ceb16954f24afe7' (2024-03-17)
• Updated input 'pre-commit-hooks':
    'github:cachix/pre-commit-hooks.nix/f56597d53fd174f796b5a7d3ee0b494f9e2285cc' (2024-01-20)
  → 'github:cachix/pre-commit-hooks.nix/5df5a70ad7575f6601d91f0efec95dd9bc619431' (2024-02-15)
• Updated input 'procfile-nix':
    'github:getchoo/procfile-nix/31a33e4264e5c6214844993c5b508fb3500ef5cd' (2024-01-27)
  → 'github:getchoo/procfile-nix/7a0ab379a4ab71c9deccaca9fb463e9aaea363d8' (2024-03-14)

---------

Co-authored-by: seth <getchoo@tuta.io>
This commit is contained in:
TheKodeToad 2024-03-18 01:01:46 +00:00 committed by GitHub
parent 1ea08671fb
commit 9d0c022c68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 492 additions and 296 deletions

View file

@ -41,3 +41,9 @@ tokio = { version = "1.35.1", features = [
"signal", "signal",
] } ] }
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.5.0", features = ["serde"] }
[profile.release]
codegen-units = 1
opt-level = "s"
panic = "abort"
strip = "symbols"

View file

@ -1,3 +1,4 @@
use std::io::Write;
use std::path::Path; use std::path::Path;
use std::{env, fs}; use std::{env, fs};
@ -9,14 +10,15 @@ include!("src/tags.rs");
#[allow(dead_code)] #[allow(dead_code)]
fn main() { fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = env::var_os("OUT_DIR").unwrap();
let generated = Path::new(&out_dir).join("generated.rs"); let dest_file = Path::new(&out_dir).join("generated.rs");
let mut file = fs::File::create(dest_file).unwrap();
let tag_files: Vec<String> = fs::read_dir(TAG_DIR) let tag_files: Vec<String> = fs::read_dir(TAG_DIR)
.unwrap() .unwrap()
.map(|f| f.unwrap().file_name().to_string_lossy().to_string()) .map(|f| f.unwrap().file_name().to_string_lossy().to_string())
.collect(); .collect();
let tags: Vec<Tag> = tag_files let mut tags: Vec<Tag> = tag_files
.clone() .clone()
.into_iter() .into_iter()
.map(|name| { .map(|name| {
@ -39,50 +41,45 @@ fn main() {
Tag { Tag {
content, content,
file_name: name, id: name.trim_end_matches(".md").to_string(),
frontmatter: data, frontmatter: data,
} }
}) })
.collect(); .collect();
let formatted_names: Vec<String> = tags tags.sort_by(|t1, t2| t1.id.cmp(&t2.id));
let tag_names: Vec<String> = tags.iter().map(|t| format!("{},", t.id)).collect();
let tag_matches: Vec<String> = tags
.iter() .iter()
.map(|t| t.file_name.replace(".md", "").replace('-', "_")) .map(|t| format!("Self::{} => \"{}\",", t.id, t.id))
.collect(); .collect();
let tag_choice = format!( writeln!(
r#" file,
#[allow(non_camel_case_types, clippy::upper_case_acronyms)] r#"#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
#[derive(Clone, Debug, poise::ChoiceParameter)] #[derive(Clone, Debug, poise::ChoiceParameter)]
pub enum Choice {{ pub enum Choice {{
{} {}
}}"#, }}"#,
formatted_names.join(",\n") tag_names.join("\n")
); )
.unwrap();
let to_str = format!( writeln!(
r#" file,
impl Choice {{ r#"impl Choice {{
fn as_str(&self) -> &str {{ fn as_str(&self) -> &str {{
match &self {{ match &self {{
{} {}
}} }}
}} }}
}} }}"#,
"#, tag_matches.join("\n")
formatted_names )
.iter() .unwrap();
.map(|n| {
let file_name = n.replace('_', "-") + ".md";
format!("Self::{n} => \"{file_name}\",")
})
.collect::<Vec<String>>()
.join("\n")
);
let contents = Vec::from([tag_choice, to_str]).join("\n\n");
fs::write(generated, contents).unwrap();
println!( println!(
"cargo:rustc-env=TAGS={}", "cargo:rustc-env=TAGS={}",
// make sure we can deserialize with env! at runtime // make sure we can deserialize with env! at runtime

95
flake.lock generated
View file

@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1706336364, "lastModified": 1710656467,
"narHash": "sha256-mJ5i2YIVKv6jTN2+l3oOUUej2NUVjJX/H3bAq6019ks=", "narHash": "sha256-4Plj0vNP+ckWVNi6EtVojL9YV2dwSH7H4UMFCV40VE8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "eb683549b7d76b12d1a009f888b91b70ed34485f", "rev": "c53bb4a32f2fce7acf4e8e160a54779c4460ffdb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -37,6 +37,26 @@
"type": "github" "type": "github"
} }
}, },
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709336216,
"narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
@ -98,11 +118,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1706173671, "lastModified": 1710701596,
"narHash": "sha256-lciR7kQUK2FCAYuszyd7zyRRmTaXVeoZsCyK6QFpGdk=", "narHash": "sha256-v4lsAi3vE/sEWg0G8AydMjs3NTHlsNw8K01xw06cKLg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4fddc9be4eaf195d631333908f2a454b03628ee5", "rev": "34ad8c9f29a18b4dd97a9ad40ceb16954f24afe7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -128,26 +148,6 @@
"type": "github" "type": "github"
} }
}, },
"parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1704982712,
"narHash": "sha256-2Ptt+9h8dczgle2Oo6z5ni5rt/uLMG47UFTR1ry/wgg=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "07f6395285469419cf9d078f59b5b49993198c00",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"pre-commit-hooks": { "pre-commit-hooks": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
@ -159,11 +159,11 @@
"nixpkgs-stable": "nixpkgs-stable" "nixpkgs-stable": "nixpkgs-stable"
}, },
"locked": { "locked": {
"lastModified": 1705757126, "lastModified": 1708018599,
"narHash": "sha256-Eksr+n4Q8EYZKAN0Scef5JK4H6FcHc+TKNHb95CWm+c=", "narHash": "sha256-M+Ng6+SePmA8g06CmUZWi1AjG2tFBX9WCXElBHEKnyM=",
"owner": "cachix", "owner": "cachix",
"repo": "pre-commit-hooks.nix", "repo": "pre-commit-hooks.nix",
"rev": "f56597d53fd174f796b5a7d3ee0b494f9e2285cc", "rev": "5df5a70ad7575f6601d91f0efec95dd9bc619431",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -179,11 +179,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1706387387, "lastModified": 1710407041,
"narHash": "sha256-7C3HncC25yK1kvLp+/9KoBa1Iz5Ly2JtICqmCz2nvio=", "narHash": "sha256-rCHklFHPzrq341KoTgXNdknNZbjOJ+VmalqX5s5YdGM=",
"owner": "getchoo", "owner": "getchoo",
"repo": "procfile-nix", "repo": "procfile-nix",
"rev": "31a33e4264e5c6214844993c5b508fb3500ef5cd", "rev": "7a0ab379a4ab71c9deccaca9fb463e9aaea363d8",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -195,21 +195,22 @@
"root": { "root": {
"inputs": { "inputs": {
"fenix": "fenix", "fenix": "fenix",
"flake-parts": "flake-parts",
"naersk": "naersk", "naersk": "naersk",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"parts": "parts",
"pre-commit-hooks": "pre-commit-hooks", "pre-commit-hooks": "pre-commit-hooks",
"procfile-nix": "procfile-nix" "procfile-nix": "procfile-nix",
"treefmt-nix": "treefmt-nix"
} }
}, },
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1706295183, "lastModified": 1710610549,
"narHash": "sha256-VSyMaUsXfjb31B8/uT5cM5qXC1VOHLVsCi/bQuo3O/g=", "narHash": "sha256-xFIGLn5u+msUazlLbdjZ3gQgXrt7Lrlhq+XXUH0XU/0=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "596e5c77cf5b2b660b3ac2ce732fa0596c246d9b", "rev": "5ecace48f693afaa6adf8cb23086b651db3aec96",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -233,6 +234,26 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1710278050,
"narHash": "sha256-Oc6BP7soXqb8itlHI8UKkdf3V9GeJpa1S39SR5+HJys=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "35791f76524086ab4b785a33e4abbedfda64bd22",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View file

@ -3,7 +3,7 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
parts = { flake-parts = {
url = "github:hercules-ci/flake-parts"; url = "github:hercules-ci/flake-parts";
inputs.nixpkgs-lib.follows = "nixpkgs"; inputs.nixpkgs-lib.follows = "nixpkgs";
}; };
@ -27,10 +27,15 @@
url = "github:getchoo/procfile-nix"; url = "github:getchoo/procfile-nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
outputs = {parts, ...} @ inputs: outputs = {flake-parts, ...} @ inputs:
parts.lib.mkFlake {inherit inputs;} { flake-parts.lib.mkFlake {inherit inputs;} {
imports = [ imports = [
./nix/dev.nix ./nix/dev.nix
./nix/packages.nix ./nix/packages.nix
@ -38,6 +43,7 @@
inputs.pre-commit-hooks.flakeModule inputs.pre-commit-hooks.flakeModule
inputs.procfile-nix.flakeModule inputs.procfile-nix.flakeModule
inputs.treefmt-nix.flakeModule
]; ];
systems = [ systems = [

View file

@ -1,9 +1,12 @@
{ {
inputs, inputs,
self, flake-parts-lib,
withSystem,
... ...
}: { }: {
flake.nixosModules.default = import ./module.nix self; flake.nixosModules.default = flake-parts-lib.importApply ./module.nix {
inherit withSystem;
};
perSystem = { perSystem = {
lib, lib,
@ -13,8 +16,8 @@
inputs', inputs',
... ...
}: let }: let
crossPkgsFor = crossPkgs =
{ rec {
x86_64-linux = { x86_64-linux = {
x86_64 = pkgs.pkgsStatic; x86_64 = pkgs.pkgsStatic;
aarch64 = pkgs.pkgsCross.aarch64-multiplatform.pkgsStatic; aarch64 = pkgs.pkgsCross.aarch64-multiplatform.pkgsStatic;
@ -30,11 +33,13 @@
aarch64 = pkgs.pkgsCross.aarch64-multiplatform.pkgsStatic; aarch64 = pkgs.pkgsCross.aarch64-multiplatform.pkgsStatic;
}; };
aarch64-darwin = crossPkgsFor.x86_64-darwin; aarch64-darwin = x86_64-darwin;
} }
.${system}; .${system};
exeFor = arch: let refractionFor = arch: let
inherit (crossPkgs.${arch}.stdenv) cc;
target = "${arch}-unknown-linux-musl"; target = "${arch}-unknown-linux-musl";
target' = builtins.replaceStrings ["-"] ["_"] target; target' = builtins.replaceStrings ["-"] ["_"] target;
targetUpper = lib.toUpper target'; targetUpper = lib.toUpper target';
@ -52,8 +57,8 @@
}; };
refraction = self'.packages.refraction.override { refraction = self'.packages.refraction.override {
lto = true;
naersk = naersk'; naersk = naersk';
optimizeSize = true;
}; };
newAttrs = { newAttrs = {
@ -62,26 +67,26 @@
"CARGO_TARGET_${targetUpper}_RUSTFLAGS" = "-C target-feature=+crt-static"; "CARGO_TARGET_${targetUpper}_RUSTFLAGS" = "-C target-feature=+crt-static";
"CARGO_TARGET_${targetUpper}_LINKER" = newAttrs."CC_${target'}"; "CARGO_TARGET_${targetUpper}_LINKER" = newAttrs."CC_${target'}";
}; };
inherit (crossPkgsFor.${arch}.stdenv) cc;
in in
lib.getExe ( refraction.overrideAttrs newAttrs;
refraction.overrideAttrs (lib.const newAttrs)
);
containerFor = arch: containerFor = arch:
pkgs.dockerTools.buildLayeredImage { pkgs.dockerTools.buildLayeredImage {
name = "refraction"; name = "refraction";
tag = "latest-${arch}"; tag = "latest-${arch}";
contents = [pkgs.dockerTools.caCertificates]; contents = [pkgs.dockerTools.caCertificates];
config.Cmd = [(exeFor arch)]; config.Cmd = [
(lib.getExe (refractionFor arch))
];
architecture = crossPkgsFor.${arch}.go.GOARCH; architecture = crossPkgs.${arch}.go.GOARCH;
}; };
in {
legacyPackages = { mkPackagesFor = arch: {
container-x86_64 = containerFor "x86_64"; "refraction-static-${arch}" = refractionFor arch;
container-aarch64 = containerFor "aarch64"; "container-${arch}" = containerFor arch;
}; };
in {
legacyPackages = lib.attrsets.mergeAttrsList (map mkPackagesFor ["x86_64" "aarch64"]);
}; };
} }

View file

@ -2,11 +2,9 @@
lib, lib,
stdenv, stdenv,
naersk, naersk,
CoreFoundation, darwin,
Security,
SystemConfiguration,
version, version,
optimizeSize ? false, lto ? false,
}: }:
naersk.buildPackage { naersk.buildPackage {
pname = "refraction"; pname = "refraction";
@ -23,13 +21,13 @@ naersk.buildPackage {
]; ];
}; };
buildInputs = lib.optionals stdenv.hostPlatform.isDarwin [ buildInputs = lib.optionals stdenv.hostPlatform.isDarwin (with darwin.apple_sdk.frameworks; [
CoreFoundation CoreFoundation
Security Security
SystemConfiguration SystemConfiguration
]; ]);
cargoBuildFlags = lib.optionals optimizeSize ["-C" "codegen-units=1" "-C" "strip=symbols" "-C" "opt-level=z"]; cargoBuildFlags = lib.optionals lto ["-C" "lto=thin" "-C" "embed-bitcode=yes" "-Zdylib-lto"];
meta = with lib; { meta = with lib; {
mainProgram = "refraction"; mainProgram = "refraction";

View file

@ -5,24 +5,9 @@
config, config,
self', self',
... ...
}: { }: let
pre-commit.settings.hooks = { enableAll = lib.flip lib.genAttrs (lib.const {enable = true;});
actionlint.enable = true; in {
alejandra.enable = true;
deadnix.enable = true;
rustfmt.enable = true;
statix.enable = true;
nil.enable = true;
prettier = {
enable = true;
excludes = ["flake.lock"];
};
};
procfiles.daemons.processes = {
redis = lib.getExe' pkgs.redis "redis-server";
};
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
shellHook = '' shellHook = ''
${config.pre-commit.installationScript} ${config.pre-commit.installationScript}
@ -31,6 +16,7 @@
packages = with pkgs; [ packages = with pkgs; [
# general # general
actionlint actionlint
nodePackages.prettier
config.procfiles.daemons.package config.procfiles.daemons.package
# rust # rust
@ -50,6 +36,38 @@
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
}; };
formatter = pkgs.alejandra; treefmt = {
projectRootFile = "flake.nix";
programs = enableAll [
"alejandra"
"deadnix"
"prettier"
"rustfmt"
];
settings.global = {
excludes = [
"./target"
"./flake.lock"
"./Cargo.lock"
];
};
};
pre-commit.settings = {
settings.treefmt.package = config.treefmt.build.wrapper;
hooks = enableAll [
"actionlint"
"nil"
"statix"
"treefmt"
];
};
procfiles.daemons.processes = {
redis = lib.getExe' pkgs.redis "redis-server";
};
}; };
} }

View file

@ -1,4 +1,4 @@
self: { {withSystem, ...}: {
config, config,
lib, lib,
pkgs, pkgs,
@ -22,7 +22,9 @@ self: {
in { in {
options.services.refraction = { options.services.refraction = {
enable = mkEnableOption "refraction"; enable = mkEnableOption "refraction";
package = mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "refraction" {}; package = mkPackageOption (
withSystem pkgs.stdenv.hostPlatform.system ({pkgs, ...}: pkgs)
) "refraction" {};
user = mkOption { user = mkOption {
description = mdDoc '' description = mdDoc ''
@ -102,7 +104,7 @@ in {
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
Restart = "always"; Restart = "on-failure";
EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
@ -122,8 +124,14 @@ in {
ProtectKernelModules = true; ProtectKernelModules = true;
ProtectKernelTunables = true; ProtectKernelTunables = true;
ProtectSystem = "strict"; ProtectSystem = "strict";
RestrictNamespaces = "uts ipc pid user cgroup"; RestrictNamespaces = true;
RestrictSUIDSGID = true; RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
"~@privileged"
];
}; };
}; };

View file

@ -12,14 +12,6 @@
packages = { packages = {
refraction = pkgs.callPackage ./derivation.nix { refraction = pkgs.callPackage ./derivation.nix {
version = builtins.substring 0 7 self.rev or "dirty"; version = builtins.substring 0 7 self.rev or "dirty";
inherit
(pkgs.darwin.apple_sdk.frameworks)
CoreFoundation
Security
SystemConfiguration
;
naersk = inputs.naersk.lib.${system}; naersk = inputs.naersk.lib.${system};
}; };

View file

@ -7,7 +7,10 @@ use reqwest::StatusCode;
const DADJOKE: &str = "https://icanhazdadjoke.com"; const DADJOKE: &str = "https://icanhazdadjoke.com";
pub async fn get_joke() -> Result<String> { pub async fn get_joke() -> Result<String> {
let req = REQWEST_CLIENT.get(DADJOKE).build()?; let req = REQWEST_CLIENT
.get(DADJOKE)
.header("Accept", "text/plain")
.build()?;
debug!("Making request to {}", req.url()); debug!("Making request to {}", req.url());
let resp = REQWEST_CLIENT.execute(req).await?; let resp = REQWEST_CLIENT.execute(req).await?;

View file

@ -0,0 +1,23 @@
use eyre::Result;
use poise::{builtins, samples::HelpConfiguration};
use crate::Context;
/// View the help menu
#[poise::command(slash_command, prefix_command, track_edits = true)]
pub async fn help(
ctx: Context<'_>,
#[description = "provide information about a specific command"] command: Option<String>,
) -> Result<()> {
builtins::help(
ctx,
command.as_deref(),
HelpConfiguration {
extra_text_at_bottom: "Use /help for more information about a specific command!",
..HelpConfiguration::default()
},
)
.await?;
Ok(())
}

View file

@ -5,11 +5,14 @@ use eyre::Result;
use log::trace; use log::trace;
/// It's a joke /// It's a joke
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command, track_edits = true)]
pub async fn joke(ctx: Context<'_>) -> Result<()> { pub async fn joke(ctx: Context<'_>) -> Result<()> {
trace!("Running joke command"); trace!("Running joke command");
ctx.defer().await?;
let joke = dadjoke::get_joke().await?; let joke = dadjoke::get_joke().await?;
ctx.reply(joke).await?; ctx.say(joke).await?;
Ok(()) Ok(())
} }

View file

@ -1,26 +1,34 @@
use crate::{consts, Context}; use crate::{consts, Context};
use eyre::{OptionExt, Result}; use eyre::{eyre, Context as _, OptionExt, Result};
use log::trace; use log::trace;
use poise::serenity_prelude::CreateEmbed; use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply; use poise::CreateReply;
/// Returns the number of members in the server /// Returns the number of members in the server
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command, guild_only = true, track_edits = true)]
pub async fn members(ctx: Context<'_>) -> Result<()> { pub async fn members(ctx: Context<'_>) -> Result<()> {
trace!("Running members command"); trace!("Running members command");
let guild = ctx.guild().ok_or_eyre("Couldn't fetch guild!")?.to_owned();
let count = guild.member_count; ctx.defer().await?;
let online = if let Some(count) = guild.approximate_presence_count {
count.to_string() let guild_id = ctx.guild_id().ok_or_eyre("Couldn't get guild ID!")?;
} else { let guild = ctx
"Undefined".to_string() .http()
}; .get_guild_with_counts(guild_id)
.await
.wrap_err_with(|| format!("Couldn't fetch guild {guild_id} with counts!"))?;
let member_count = guild
.approximate_member_count
.ok_or_else(|| eyre!("Couldn't get member count for guild {guild_id}!"))?;
let online_count = guild
.approximate_presence_count
.ok_or_else(|| eyre!("Couldn't get online count for guild {guild_id}!"))?;
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title(format!("{count} total members!")) .title(format!("{member_count} total members!",))
.description(format!("{online} online members")) .description(format!("{online_count} online members",))
.color(consts::COLORS["blue"]); .color(consts::COLORS["blue"]);
let reply = CreateReply::default().embed(embed); let reply = CreateReply::default().embed(embed);

View file

@ -1,3 +1,4 @@
mod help;
mod joke; mod joke;
mod members; mod members;
mod ping; mod ping;
@ -6,6 +7,7 @@ mod say;
mod stars; mod stars;
mod tag; mod tag;
pub use help::help;
pub use joke::joke; pub use joke::joke;
pub use members::members; pub use members::members;
pub use ping::ping; pub use ping::ping;

View file

@ -4,9 +4,9 @@ use eyre::Result;
use log::trace; use log::trace;
/// Replies with pong! /// Replies with pong!
#[poise::command(slash_command, prefix_command, ephemeral)] #[poise::command(slash_command, prefix_command, track_edits = true, ephemeral)]
pub async fn ping(ctx: Context<'_>) -> Result<()> { pub async fn ping(ctx: Context<'_>) -> Result<()> {
trace!("Running ping command!"); trace!("Running ping command!");
ctx.reply("Pong!").await?; ctx.say("Pong!").await?;
Ok(()) Ok(())
} }

View file

@ -7,12 +7,15 @@ use poise::serenity_prelude::{CreateEmbed, CreateEmbedFooter};
use poise::CreateReply; use poise::CreateReply;
/// Gets a Rory photo! /// Gets a Rory photo!
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command, track_edits = true)]
pub async fn rory( pub async fn rory(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "specify a Rory ID"] id: Option<u64>, #[description = "specify a Rory ID"] id: Option<u64>,
) -> Result<()> { ) -> Result<()> {
trace!("Running rory command"); trace!("Running rory command");
ctx.defer().await?;
let rory = rory::get(id).await?; let rory = rory::get(id).await?;
let embed = { let embed = {

View file

@ -9,18 +9,30 @@ use poise::serenity_prelude::{CreateEmbed, CreateEmbedAuthor, CreateMessage};
prefix_command, prefix_command,
ephemeral, ephemeral,
default_member_permissions = "MODERATE_MEMBERS", default_member_permissions = "MODERATE_MEMBERS",
required_permissions = "MODERATE_MEMBERS" required_permissions = "MODERATE_MEMBERS",
guild_only = true
)] )]
pub async fn say(ctx: Context<'_>, #[description = "Just content?"] content: String) -> Result<()> { pub async fn say(
ctx: Context<'_>,
#[description = "the message content"] content: String,
) -> Result<()> {
let guild = ctx.guild().ok_or_eyre("Couldn't get guild!")?.to_owned(); let guild = ctx.guild().ok_or_eyre("Couldn't get guild!")?.to_owned();
let channel = ctx let channel = ctx
.guild_channel() .guild_channel()
.await .await
.ok_or_eyre("Couldn't get channel!")?; .ok_or_eyre("Couldn't get channel!")?;
if let Context::Prefix(prefix) = ctx {
// ignore error, we might not have perm
let _ = prefix.msg.delete(ctx).await;
}
ctx.defer_ephemeral().await?; ctx.defer_ephemeral().await?;
channel.say(ctx, &content).await?; channel.say(ctx, &content).await?;
ctx.say("I said what you said!").await?;
if let Context::Application(_) = ctx {
ctx.say("I said what you said!").await?;
}
if let Some(channel_id) = ctx.data().config.discord.channels().say_log_channel_id() { if let Some(channel_id) = ctx.data().config.discord.channels().say_log_channel_id() {
let log_channel = guild let log_channel = guild
@ -29,8 +41,11 @@ pub async fn say(ctx: Context<'_>, #[description = "Just content?"] content: Str
.find(|c| c.0 == &channel_id) .find(|c| c.0 == &channel_id)
.ok_or_eyre("Couldn't get log channel from guild!")?; .ok_or_eyre("Couldn't get log channel from guild!")?;
let author = CreateEmbedAuthor::new(ctx.author().tag()) let author = CreateEmbedAuthor::new(ctx.author().tag()).icon_url(
.icon_url(ctx.author().avatar_url().unwrap_or("Undefined".to_string())); ctx.author()
.avatar_url()
.unwrap_or_else(|| ctx.author().default_avatar_url()),
);
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.title("Say command used!") .title("Say command used!")

View file

@ -6,10 +6,12 @@ use poise::serenity_prelude::CreateEmbed;
use poise::CreateReply; use poise::CreateReply;
/// Returns GitHub stargazer count /// Returns GitHub stargazer count
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command, track_edits = true)]
pub async fn stars(ctx: Context<'_>) -> Result<()> { pub async fn stars(ctx: Context<'_>) -> Result<()> {
trace!("Running stars command"); trace!("Running stars command");
ctx.defer().await?;
let prismlauncher = ctx let prismlauncher = ctx
.data() .data()
.octocrab .octocrab

View file

@ -13,19 +13,24 @@ include!(concat!(env!("OUT_DIR"), "/generated.rs"));
static TAGS: Lazy<Vec<Tag>> = Lazy::new(|| serde_json::from_str(env!("TAGS")).unwrap()); static TAGS: Lazy<Vec<Tag>> = Lazy::new(|| serde_json::from_str(env!("TAGS")).unwrap());
/// Send a tag /// Send a tag
#[poise::command(slash_command, prefix_command)] #[poise::command(
slash_command,
prefix_command,
track_edits = true,
help_text_fn = help
)]
pub async fn tag( pub async fn tag(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "the copypasta you want to send"] name: Choice, #[description = "the tag to send"] name: Choice,
user: Option<User>, #[description = "a user to mention"] user: Option<User>,
) -> Result<()> { ) -> Result<()> {
trace!("Running tag command"); trace!("Running tag command");
let tag_file = name.as_str(); let tag_id = name.as_str();
let tag = TAGS let tag = TAGS
.iter() .iter()
.find(|t| t.file_name == tag_file) .find(|t| t.id == tag_id)
.ok_or_else(|| eyre!("Tried to get non-existent tag: {tag_file}"))?; .ok_or_else(|| eyre!("Tried to get non-existent tag: {tag_id}"))?;
let frontmatter = &tag.frontmatter; let frontmatter = &tag.frontmatter;
@ -49,6 +54,9 @@ pub async fn tag(
} }
} }
e = e.title(&frontmatter.title);
e = e.description(&tag.content);
e e
}; };
@ -66,3 +74,13 @@ pub async fn tag(
Ok(()) Ok(())
} }
fn help() -> String {
let tag_list = TAGS
.iter()
.map(|tag| format!("`{}`", tag.id))
.collect::<Vec<String>>()
.join(", ");
format!("Available tags: {tag_list}")
}

View file

@ -14,5 +14,6 @@ pub fn get() -> Vec<Command<Data, Report>> {
general::say(), general::say(),
general::stars(), general::stars(),
general::tag(), general::tag(),
general::help(),
] ]
} }

View file

@ -1,11 +1,21 @@
use crate::consts; use crate::consts;
use crate::Data; use crate::Data;
use std::fmt::Write;
use eyre::Report; use eyre::Report;
use log::error; use log::error;
use poise::serenity_prelude::{CreateEmbed, Timestamp}; use poise::serenity_prelude::{CreateEmbed, Timestamp};
use poise::{CreateReply, FrameworkError}; use poise::{CreateReply, FrameworkError};
// getchoo: i like writeln! and don't like
macro_rules! writelne {
($dst:expr, $($arg:tt)*) => {
if let Err(why) = writeln!($dst, $($arg)*) {
error!("We somehow cannot write to what should be on the heap. What are you using this macro with? Anyways, here's the error:\n{why:#?}");
}
}
}
pub async fn handle(error: FrameworkError<'_, Data, Report>) { pub async fn handle(error: FrameworkError<'_, Data, Report>) {
match error { match error {
FrameworkError::Setup { FrameworkError::Setup {
@ -44,6 +54,42 @@ pub async fn handle(error: FrameworkError<'_, Data, Report>) {
); );
} }
FrameworkError::ArgumentParse {
error, input, ctx, ..
} => {
let mut response = String::new();
if let Some(input) = input {
writelne!(
&mut response,
"**Cannot parse `{input}` as argument: {error}**\n"
);
} else {
writelne!(&mut response, "**{error}**\n");
}
if let Some(help_text) = ctx.command().help_text.as_ref() {
writelne!(&mut response, "{help_text}\n");
}
if ctx.command().invoke_on_edit {
writelne!(
&mut response,
"**Tip:** Edit your message to update the response."
);
}
writelne!(
&mut response,
"For more information, refer to /help {}.",
ctx.command().name
);
if let Err(why) = ctx.say(response).await {
error!("Unhandled error displaying ArgumentParse error\n{why:#?}");
}
}
error => { error => {
if let Err(e) = poise::builtins::on_error(error).await { if let Err(e) = poise::builtins::on_error(error).await {
error!("Unhandled error occurred:\n{e:#?}"); error!("Unhandled error occurred:\n{e:#?}");

View file

@ -95,7 +95,7 @@ fn intel_hd(log: &str) -> Issue {
See https://prismlauncher.org/wiki/getting-started/installing-java/#a-note-about-intel-hd-20003000-on-windows-10 for more info".to_string() See https://prismlauncher.org/wiki/getting-started/installing-java/#a-note-about-intel-hd-20003000-on-windows-10 for more info".to_string()
); );
let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.<init>(Ljava/util/jar/Manifest;)V"); let found = log.contains("org.lwjgl.LWJGLException: Pixel format not accelerated");
found.then_some(issue) found.then_some(issue)
} }
@ -204,7 +204,7 @@ async fn outdated_launcher(log: &str, data: &Data) -> Result<Issue> {
if version_from_log < latest_version { if version_from_log < latest_version {
let issue = ( let issue = (
"Outdated Prism Launcher".to_string(), "Outdated Prism Launcher".to_string(),
format!("Your installed version is {version_from_log}, while the newest version is {latest_version}.\nPlease update, for more info see https://prismlauncher.org/download/") format!("Your installed version is {version_from_log}, while the newest version is {latest_version}.\nPlease update, for more info see https://prismlauncher.org/download/")
); );

View file

@ -19,7 +19,7 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()>
if log.is_err() { if log.is_err() {
let embed = CreateEmbed::new() let embed = CreateEmbed::new()
.title("Analyze failed!") .title("Analysis failed!")
.description("Couldn't download log"); .description("Couldn't download log");
let allowed_mentions = CreateAllowedMentions::new().replied_user(true); let allowed_mentions = CreateAllowedMentions::new().replied_user(true);
let our_message = CreateMessage::new() let our_message = CreateMessage::new()
@ -43,11 +43,9 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()>
let mut e = CreateEmbed::new().title("Log analysis"); let mut e = CreateEmbed::new().title("Log analysis");
if issues.is_empty() { if issues.is_empty() {
e = e.color(COLORS["green"]).field( e = e
"Analyze failed!", .color(COLORS["green"])
"No issues found automatically", .description("No issues found automatically");
false,
);
} else { } else {
e = e.color(COLORS["red"]); e = e.color(COLORS["red"]);
@ -61,6 +59,7 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()>
let allowed_mentions = CreateAllowedMentions::new().replied_user(true); let allowed_mentions = CreateAllowedMentions::new().replied_user(true);
let message = CreateMessage::new() let message = CreateMessage::new()
.reference_message(message)
.allowed_mentions(allowed_mentions) .allowed_mentions(allowed_mentions)
.embed(embed); .embed(embed);

View file

@ -4,13 +4,12 @@ use poise::serenity_prelude::{Context, CreateAllowedMentions, CreateMessage, Mes
use crate::utils; use crate::utils;
pub async fn handle(ctx: &Context, message: &Message) -> Result<()> { pub async fn handle(ctx: &Context, message: &Message) -> Result<()> {
let embeds = utils::resolve_message(ctx, message).await?; let embeds = utils::resolve_message::from_message(ctx, message).await?;
// TODO getchoo: actually reply to user
// ...not sure why Message doesn't give me a builder in reply() or equivalents
if !embeds.is_empty() { if !embeds.is_empty() {
let allowed_mentions = CreateAllowedMentions::new().replied_user(false); let allowed_mentions = CreateAllowedMentions::new().replied_user(false);
let reply = CreateMessage::new() let reply = CreateMessage::new()
.reference_message(message)
.embeds(embeds) .embeds(embeds)
.allowed_mentions(allowed_mentions); .allowed_mentions(allowed_mentions);

View file

@ -15,7 +15,7 @@ mod support_onboard;
pub async fn handle( pub async fn handle(
ctx: &Context, ctx: &Context,
event: &FullEvent, event: &FullEvent,
framework: FrameworkContext<'_, Data, Report>, _framework: FrameworkContext<'_, Data, Report>,
data: &Data, data: &Data,
) -> Result<()> { ) -> Result<()> {
match event { match event {
@ -31,8 +31,10 @@ pub async fn handle(
FullEvent::Message { new_message } => { FullEvent::Message { new_message } => {
// ignore new messages from bots // ignore new messages from bots
// NOTE: the webhook_id check allows us to still respond to PK users // note: the webhook_id check allows us to still respond to PK users
if new_message.author.bot && new_message.webhook_id.is_none() { if (new_message.author.bot && new_message.webhook_id.is_none())
|| new_message.is_own(ctx)
{
trace!("Ignoring message {} from bot", new_message.id); trace!("Ignoring message {} from bot", new_message.id);
return Ok(()); return Ok(());
} }
@ -57,7 +59,7 @@ pub async fn handle(
} }
FullEvent::ThreadCreate { thread } => { FullEvent::ThreadCreate { thread } => {
support_onboard::handle(ctx, thread, framework).await?; support_onboard::handle(ctx, thread).await?;
} }
_ => {} _ => {}

View file

@ -2,18 +2,18 @@ use crate::{api, Data};
use std::time::Duration; use std::time::Duration;
use eyre::Result; use eyre::Result;
use log::trace; use log::{debug, trace};
use poise::serenity_prelude::{Context, Message}; use poise::serenity_prelude::{Context, Message};
use tokio::time::sleep; use tokio::time::sleep;
const PK_DELAY_SEC: Duration = Duration::from_secs(1000); const PK_DELAY: Duration = Duration::from_secs(1);
pub async fn is_message_proxied(message: &Message) -> Result<bool> { pub async fn is_message_proxied(message: &Message) -> Result<bool> {
trace!( trace!(
"Waiting on PluralKit API for {} seconds", "Waiting on PluralKit API for {} seconds",
PK_DELAY_SEC.as_secs() PK_DELAY.as_secs()
); );
sleep(PK_DELAY_SEC).await; sleep(PK_DELAY).await;
let proxied = api::pluralkit::get_sender(message.id).await.is_ok(); let proxied = api::pluralkit::get_sender(message.id).await.is_ok();
@ -21,21 +21,23 @@ pub async fn is_message_proxied(message: &Message) -> Result<bool> {
} }
pub async fn handle(_: &Context, msg: &Message, data: &Data) -> Result<()> { pub async fn handle(_: &Context, msg: &Message, data: &Data) -> Result<()> {
if msg.webhook_id.is_some() { if msg.webhook_id.is_none() {
trace!( return Ok(());
"Message {} has a webhook ID. Checking if it was sent through PluralKit", }
msg.id
);
trace!( debug!(
"Waiting on PluralKit API for {} seconds", "Message {} has a webhook ID. Checking if it was sent through PluralKit",
PK_DELAY_SEC.as_secs() msg.id
); );
sleep(PK_DELAY_SEC).await;
if let Ok(sender) = api::pluralkit::get_sender(msg.id).await { trace!(
data.storage.store_user_plurality(sender).await?; "Waiting on PluralKit API for {} seconds",
} PK_DELAY.as_secs()
);
sleep(PK_DELAY).await;
if let Ok(sender) = api::pluralkit::get_sender(msg.id).await {
data.storage.store_user_plurality(sender).await?;
} }
Ok(()) Ok(())

View file

@ -1,36 +1,17 @@
use crate::Data; use eyre::{eyre, OptionExt, Result};
use eyre::{eyre, Context as _, OptionExt, Report, Result};
use log::{debug, trace}; use log::{debug, trace};
use poise::serenity_prelude::{ use poise::serenity_prelude::{
ChannelType, Context, CreateAllowedMentions, CreateMessage, GuildChannel, ChannelType, Context, CreateAllowedMentions, CreateMessage, GuildChannel,
}; };
use poise::FrameworkContext;
pub async fn handle( pub async fn handle(ctx: &Context, thread: &GuildChannel) -> Result<()> {
ctx: &Context,
thread: &GuildChannel,
framework: FrameworkContext<'_, Data, Report>,
) -> Result<()> {
if thread.kind != ChannelType::PublicThread { if thread.kind != ChannelType::PublicThread {
trace!("Not doing support onboard in non-public thread channel"); trace!("Not doing support onboard in non-public thread channel");
return Ok(()); return Ok(());
} }
// TODO @getchoo: it seems like we can get multiple ThreadCreate events if thread.last_message_id.is_some() {
// should probably figure out a better way to not repeat ourselves here debug!("Ignoring duplicate thread creation event");
if thread
.members(ctx)
.wrap_err_with(|| {
format!(
"Couldn't fetch members from thread {}! Not sending a support onboard message.",
thread.id
)
})?
.iter()
.any(|member| member.user.id == framework.bot_id)
{
debug!("Not sending support onboard message...I think i've been here before :p");
return Ok(()); return Ok(());
} }

View file

@ -2,8 +2,7 @@
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use std::time::Duration;
use eyre::{eyre, Context as _, Report, Result}; use eyre::{eyre, Context as _, Report, Result};
use log::{info, trace, warn}; use log::{info, trace, warn};
@ -111,7 +110,7 @@ async fn main() -> Result<()> {
}, },
prefix_options: PrefixFrameworkOptions { prefix_options: PrefixFrameworkOptions {
prefix: Some("r".into()), prefix: Some(".".into()),
edit_tracker: Some(Arc::from(EditTracker::for_timespan(Duration::from_secs( edit_tracker: Some(Arc::from(EditTracker::for_timespan(Duration::from_secs(
3600, 3600,
)))), )))),

View file

@ -15,6 +15,6 @@ pub struct TagFrontmatter {
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Tag { pub struct Tag {
pub content: String, pub content: String,
pub file_name: String, pub id: String,
pub frontmatter: TagFrontmatter, pub frontmatter: TagFrontmatter,
} }

View file

@ -1,3 +1 @@
mod resolve_message; pub mod resolve_message;
pub use resolve_message::resolve as resolve_message;

View file

@ -4,17 +4,14 @@ use eyre::{eyre, Context as _, Result};
use log::{debug, trace}; use log::{debug, trace};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use poise::serenity_prelude::{ use poise::serenity_prelude::{
ChannelId, ChannelType, Colour, Context, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Cache, CacheHttp, ChannelId, ChannelType, Colour, Context, CreateEmbed, CreateEmbedAuthor,
Message, MessageId, CreateEmbedFooter, GuildChannel, Member, Message, MessageId, Permissions,
}; };
use regex::Regex; use regex::Regex;
static MESSAGE_PATTERN: Lazy<Regex> = Lazy::new(|| { fn find_first_image(message: &Message) -> Option<String> {
Regex::new(r"/(https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?<serverId>\d+)\/(?<channelId>\d+)\/(?<messageId>\d+)/g;").unwrap() message
}); .attachments
fn find_first_image(msg: &Message) -> Option<String> {
msg.attachments
.iter() .iter()
.find(|a| { .find(|a| {
a.content_type a.content_type
@ -25,86 +22,130 @@ fn find_first_image(msg: &Message) -> Option<String> {
.map(|res| res.url.clone()) .map(|res| res.url.clone())
} }
pub async fn resolve(ctx: &Context, msg: &Message) -> Result<Vec<CreateEmbed>> { async fn member_can_view_channel(
ctx: impl CacheHttp + AsRef<Cache>,
member: &Member,
channel: &GuildChannel,
) -> Result<bool> {
static REQUIRED_PERMISSIONS: Lazy<Permissions> =
Lazy::new(|| Permissions::VIEW_CHANNEL | Permissions::READ_MESSAGE_HISTORY);
let guild = ctx.http().get_guild(channel.guild_id).await?;
let channel_to_check = match &channel.kind {
ChannelType::PublicThread => {
let parent_id = channel
.parent_id
.ok_or_else(|| eyre!("Couldn't get parent of thread {}", channel.id))?;
parent_id
.to_channel(ctx)
.await?
.guild()
.ok_or_else(|| eyre!("Couldn't get GuildChannel from ChannelID {parent_id}!"))?
}
ChannelType::Text | ChannelType::News => channel.to_owned(),
_ => return Ok(false),
};
let can_view = guild
.user_permissions_in(&channel_to_check, member)
.contains(*REQUIRED_PERMISSIONS);
Ok(can_view)
}
pub async fn to_embed(
ctx: impl CacheHttp + AsRef<Cache>,
message: &Message,
) -> Result<CreateEmbed> {
let author = CreateEmbedAuthor::new(message.author.tag()).icon_url(
message
.author
.avatar_url()
.unwrap_or_else(|| message.author.default_avatar_url()),
);
let footer = CreateEmbedFooter::new(format!(
"#{}",
message.channel(ctx).await?.guild().unwrap_or_default().name
));
let mut embed = CreateEmbed::new()
.author(author)
.color(Colour::BLITZ_BLUE)
.timestamp(message.timestamp)
.footer(footer)
.description(format!(
"{}\n\n[Jump to original message]({})",
message.content,
message.link()
));
if !message.attachments.is_empty() {
embed = embed.fields(message.attachments.iter().map(|a| {
(
"Attachments".to_string(),
format!("[{}]({})", a.filename, a.url),
false,
)
}));
if let Some(image) = find_first_image(message) {
embed = embed.image(image);
}
}
Ok(embed)
}
pub async fn from_message(ctx: &Context, msg: &Message) -> Result<Vec<CreateEmbed>> {
static MESSAGE_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?:https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?<server_id>\d+)\/(?<channel_id>\d+)\/(?<message_id>\d+)").unwrap()
});
let Some(guild_id) = msg.guild_id else {
debug!("Not resolving message in DM");
return Ok(Vec::new());
};
let author = guild_id.member(ctx, msg.author.id).await?;
let matches = MESSAGE_PATTERN let matches = MESSAGE_PATTERN
.captures_iter(&msg.content) .captures_iter(&msg.content)
.map(|capture| capture.extract()); .map(|capture| capture.extract());
let mut embeds: Vec<CreateEmbed> = vec![]; let mut embeds: Vec<CreateEmbed> = vec![];
for (url, [_server_id, channel_id, message_id]) in matches { for (url, [target_guild_id, target_channel_id, target_message_id]) in matches {
trace!("Attempting to resolve message {message_id} from URL {url}"); if target_guild_id != guild_id.to_string() {
debug!("Not resolving message from other server");
let channel = ChannelId::from_str(channel_id) continue;
.wrap_err_with(|| format!("Couldn't parse channel ID {channel_id}!"))?
.to_channel_cached(ctx.as_ref())
.ok_or_else(|| eyre!("Couldn't find Guild Channel from {channel_id}!"))?
.to_owned();
let author_can_view = if channel.kind == ChannelType::PublicThread
|| channel.kind == ChannelType::PrivateThread
{
let thread_members = channel
.id
.get_thread_members(ctx)
.await
.wrap_err("Couldn't get members from thread!")?;
thread_members
.iter()
.any(|member| member.user_id == msg.author.id)
} else {
channel
.members(ctx)
.wrap_err_with(|| format!("Couldn't get members for channel {channel_id}!"))?
.iter()
.any(|member| member.user.id == msg.author.id)
};
if !author_can_view {
debug!("Not resolving message for author who can't see it");
} }
trace!("Attempting to resolve message {target_message_id} from URL {url}");
let original_message = channel let target_channel = ChannelId::from_str(target_channel_id)?
.message( .to_channel(ctx)
ctx, .await?
MessageId::from_str(message_id) .guild()
.wrap_err_with(|| format!("Couldn't parse message ID {message_id}!"))?, .ok_or_else(|| {
) eyre!("Couldn't find GuildChannel from ChannelId {target_channel_id}!")
.await
.wrap_err_with(|| {
format!("Couldn't get message from ID {message_id} in channel {channel_id}!")
})?; })?;
let author = CreateEmbedAuthor::new(original_message.author.tag()) if !member_can_view_channel(ctx, &author, &target_channel).await? {
.icon_url(original_message.author.default_avatar_url()); debug!("Not resolving message for author who can't see it");
let footer = CreateEmbedFooter::new(format!("#{}", channel.name)); continue;
let mut embed = CreateEmbed::new()
.author(author)
.color(Colour::BLITZ_BLUE)
.timestamp(original_message.timestamp)
.footer(footer)
.description(format!(
"{}\n\n[Jump to original message]({})",
original_message.content,
original_message.link()
));
if !original_message.attachments.is_empty() {
embed = embed.fields(original_message.attachments.iter().map(|a| {
(
"Attachments".to_string(),
format!("[{}]({})", a.filename, a.url),
false,
)
}));
if let Some(image) = find_first_image(msg) {
embed = embed.image(image);
}
} }
let target_message_id = MessageId::from_str(target_message_id)?;
let target_message = target_channel
.message(ctx, target_message_id)
.await
.wrap_err_with(|| {
eyre!("Couldn't find channel message from ID {target_message_id}!")
})?;
let embed = to_embed(ctx, &target_message).await?;
embeds.push(embed); embeds.push(embed);
} }