diff --git a/Cargo.lock b/Cargo.lock index be1801e..91fb98b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,7 @@ dependencies = [ "lazy_static", "lua-patterns", "pulldown-cmark", + "serde", "serde_json", "ureq", ] @@ -225,11 +226,11 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "proc-macro2" -version = "1.0.29" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -246,9 +247,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -315,9 +316,23 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.201" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", +] [[package]] name = "serde_json" @@ -347,6 +362,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tinyvec" version = "1.4.0" @@ -377,6 +403,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -460,7 +492,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 1.0.76", "wasm-bindgen-shared", ] @@ -482,7 +514,7 @@ checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 5ae8482..40d791e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ lto = true [dependencies] ureq = "2.3.0" +serde = { version = "1.0.201", features = ["serde_derive"] } serde_json = "1.0.69" fancy-regex = "0.7.1" # Used over `regex` for the look-around support pulldown-cmark = "0.9.0" diff --git a/src/command.rs b/src/command.rs index bcc5471..de0fce6 100644 --- a/src/command.rs +++ b/src/command.rs @@ -5,6 +5,7 @@ pub enum Command<'a> { Help { docs: Vec<&'a str> }, Sandwich { to: &'a str }, Url { url: &'a str }, + Gif { search: String }, } pub struct CommandParser { @@ -62,6 +63,12 @@ impl CommandParser { let args = args?; Some(Sandwich { to: args[0] }) } + "gif" => { + let args = args?; + Some(Gif { + search: args.join(" "), + }) + } x => self.url_commands_json.get(x).map(|url| Url { url: url.as_str().unwrap(), }), diff --git a/src/gif.rs b/src/gif.rs new file mode 100644 index 0000000..b910443 --- /dev/null +++ b/src/gif.rs @@ -0,0 +1,140 @@ +#![allow(unused)] // just cuz we have JSON deserialized stuff that we don't all use + // TODO(smolcK): we could do something about that idk, I like having the types +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Deserialize)] +struct Response { + next: String, + results: Vec, +} + +#[derive(Deserialize)] +struct ResponseObject { + created: f32, + hasaudio: bool, + id: String, + media_formats: HashMap, +} + +#[derive(Deserialize)] +struct MediaObject { + url: String, + dims: [i32; 2], // width, height + duration: f32, + size: i32, +} + +#[derive(PartialEq, Eq, Hash, Deserialize)] +#[serde(rename_all = "lowercase")] +// TODO(smolck): No idea if this is right lol +enum ContentFormat { + // Preview, + Gif, + #[serde(rename = "gifpreview")] + GifPreview, + + MediumGif, + #[serde(rename = "mediumgifpreview")] + MediumGifPreview, + + TinyGif, + #[serde(rename = "tinygifpreview")] + TinyGifPreview, + + NanoGif, + #[serde(rename = "nanogifpreview")] + NanoGifPreview, + + Mp4, + #[serde(rename = "mp4preview")] + Mp4Preview, + + LoopedMp4, + #[serde(rename = "loopedmp4preview")] + LoopedMp4Preview, + + TinyMp4, + #[serde(rename = "tinymp4preview")] + TinyMp4Preview, + + NanoMp4, + #[serde(rename = "nanomp4preview")] + NanoMp4Preview, + + Webm, + #[serde(rename = "webmpreview")] + WebmPreview, + + TinyWebm, + #[serde(rename = "tinywebmpreview")] + TinyWebmPreview, + + NanoWebm, + #[serde(rename = "nanowebmpreview")] + NanoWebmPreview, + + #[serde(rename = "webp_transparent")] + WebpTransparent, + #[serde(rename = "webppreview_transparent")] + WebpPreviewTransparent, + + #[serde(rename = "tinywebp_transparent")] + TinyWebpTransparent, + #[serde(rename = "tinywebppreview_transparent")] + TinyWebpPreviewTransparent, + + #[serde(rename = "nanowebp_transparent")] + NanoWebpTransparent, + #[serde(rename = "nanowebppreview_transparent")] + NanoWebpPreviewTransparent, + + #[serde(rename = "gif_transparent")] + GifTransparent, + #[serde(rename = "gifpreview_transparent")] + GifPreviewTransparent, + + #[serde(rename = "tinygif_transparent")] + TinyGifTransparent, + #[serde(rename = "tinygifpreview_transparent")] + TinyGifPreviewTransparent, + + #[serde(rename = "nanogif_transparent")] + NanoGifTransparent, + #[serde(rename = "nanogifpreview_transparent")] + NanoGifPreviewTransparent, +} + +pub struct Gif { + pub height: i32, + pub width: i32, + pub size: i32, + pub url: String, +} + +impl Gif { + pub fn search(agent: &ureq::Agent, api_key: &str, query: &str) -> Self { + let response = agent + .get("https://tenor.googleapis.com/v2/search") + .set("Accept", "application/json") + .set("Content-Type", "application/json") + .set("Charset", "utf-8") + // TODO(smolck): I hope this sanitizes this cuz it's user input lol + .query("q", query) + .query("key", api_key) + .query("limit", "1") + .call() + .unwrap(); + + let response: Response = serde_json::de::from_reader(&mut response.into_reader()).unwrap(); + let gif_info = &response.results[0].media_formats[&ContentFormat::TinyGif]; + let [width, height] = gif_info.dims; + + Self { + width, + height, + size: gif_info.size, + url: gif_info.url.clone(), + } + } +} diff --git a/src/main.rs b/src/main.rs index a827325..84d5a13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,30 +2,45 @@ #![allow(clippy::result_large_err)] mod command; +mod gif; mod help; use serde_json::Value as Json; const DEFAULT_HOMESERVER: &str = "https://matrix.org"; +#[derive(serde::Deserialize)] +struct MxcUriCreateResponse { + content_uri: String, + #[allow(unused)] + unused_expires_at: i64, +} + struct MatrixClient { pub access_token: Option, pub command_parser: command::CommandParser, pub homeserver: String, + pub agent: ureq::Agent, + /// If this is None, then gifs won't be supported + pub tenor_api_key: Option, } impl MatrixClient { - fn new(homeserver: String) -> Self { + fn new(homeserver: String, tenor_api_key: Option) -> Self { Self { access_token: None, - + // set timeouts? + agent: ureq::AgentBuilder::new().build(), homeserver, command_parser: command::CommandParser::new(), + tenor_api_key, } } fn login(&mut self, user: &str, password: &str) -> Result<(), ureq::Error> { - let response: String = ureq::post(&format!("{}/_matrix/client/r0/login", self.homeserver)) + let response: String = self + .agent + .post(&format!("{}/_matrix/client/r0/login", self.homeserver)) // TODO(smolck): These headers necessary? .set("Accept", "application/json") .set("Content-Type", "application/json") @@ -46,6 +61,77 @@ impl MatrixClient { Ok(()) } + fn send_gif_if_key_else_do_nothing( + &self, + search_query: &str, + room_id: &str, + ) -> Result<(), ureq::Error> { + let Some(key) = &self.tenor_api_key else { + return Ok(()); + }; + + let gif = gif::Gif::search(&self.agent, key, search_query); + let gif_bytes_reader = self.agent.get(&gif.url).call().unwrap().into_reader(); + + let mxc_uri = self + .agent + .post(&format!("{}/_matrix/media/v1/create", self.homeserver,)) + .set("Accept", "application/json") + .set("Charset", "utf-8") + .query("access_token", self.access_token.as_ref().unwrap()) + .call() + .unwrap(); + let mxc_uri = serde_json::from_str::(&mxc_uri.into_string().unwrap()) + .unwrap() + .content_uri; + + // TODO(smolck): Umm . . . absolutely not lmao + let mxc_uri_parts: Vec<&str> = mxc_uri.split("mxc://").collect(); + let parts = mxc_uri_parts[1].split('/').collect::>(); + let server_name = parts[0]; + let media_id = parts[1]; + + // Upload gif to mxc uri + self.agent + .put(&format!( + "{}/_matrix/media/v3/upload/{}/{}", + self.homeserver, server_name, media_id, + )) + .query("filename", "nvim-bot-gif.gif") + .query("access_token", self.access_token.as_ref().unwrap()) + .set("Content-Type", "application/octet-stream") + .send(gif_bytes_reader) + .unwrap(); + + let json = serde_json::json!({ + "msgtype": "m.image", + "info": { + "mimetype": "image/gif", + "size": gif.size, + "h": gif.height, + "w": gif.width, + }, + "url": mxc_uri, + "body": "nvim-bot-gif.gif", + }) + .to_string(); + + let _response = self + .agent + .post(&format!( + "{}/_matrix/client/r0/rooms/{}/send/m.room.message", + self.homeserver, room_id + )) + .set("Accept", "application/json") + .set("Content-Type", "application/json") + .set("Charset", "utf-8") + .query("access_token", self.access_token.as_ref().unwrap()) + .send_string(&json)? + .into_string()?; + + Ok(()) + } + fn send_message( &self, use_markdown: bool, @@ -75,19 +161,18 @@ impl MatrixClient { }; // TODO(smolck): Maybe deal with response or use it or something? - let _response: String = ureq::post(&format!( - "{}/_matrix/client/r0/rooms/{}/send/m.room.message", - self.homeserver, room_id - )) - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Charset", "utf-8") - .query("access_token", self.access_token.as_ref().unwrap()) - // .set("Authorization", &format!("Bearer {}", self.access_token.as_ref().unwrap())) - .send_string(&json)? - .into_string()?; - - // let json = serde_json::from_str::(&response).unwrap(); + let _response: String = self + .agent + .post(&format!( + "{}/_matrix/client/r0/rooms/{}/send/m.room.message", + self.homeserver, room_id + )) + .set("Accept", "application/json") + .set("Content-Type", "application/json") + .set("Charset", "utf-8") + .query("access_token", self.access_token.as_ref().unwrap()) + .send_string(&json)? + .into_string()?; Ok(()) } @@ -97,7 +182,9 @@ impl MatrixClient { next_batch: Option<&str>, filter: Option<&str>, ) -> Result { - let mut req = ureq::get(&format!("{}/_matrix/client/r0/sync", self.homeserver)) + let mut req = self + .agent + .get(&format!("{}/_matrix/client/r0/sync", self.homeserver)) .set("Accept", "application/json") .set("Content-Type", "application/json") .set("Charset", "utf-8") @@ -170,6 +257,10 @@ impl MatrixClient { Url { url } => { self.send_message(true, url, room_id).unwrap(); } + Gif { search } => { + self.send_gif_if_key_else_do_nothing(&search, room_id) + .unwrap(); + } } } @@ -253,7 +344,17 @@ fn main() -> Result<(), ureq::Error> { } }; - let mut client = MatrixClient::new(homeserver); + let tenor_api_key = { + match std::env::var("TENOR_API_KEY") { + Err(_) => { + println!("running without tenor gif functionality"); + None + } + Ok(key) => Some(key), + } + }; + + let mut client = MatrixClient::new(homeserver, tenor_api_key); client.login(&user, &password)?; client.sync()?;