#![feature(let_chains)] use std::collections::HashSet; use serde::{ Deserialize, Serialize }; use serde_json::json; use wasm_bindgen::{ prelude::wasm_bindgen, JsValue }; use reqwest::{ Client, Url, multipart }; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace=console)] fn log (s: &str); } #[wasm_bindgen] extern "C" { #[wasm_bindgen(catch, js_namespace = ["chrome", "storage", "local"], js_name = set)] async fn storage_set (data: &JsValue) -> Result; #[wasm_bindgen(catch, js_namespace = ["chrome", "storage", "local"], js_name = get)] async fn storage_get (data: &JsValue) -> Result; } #[wasm_bindgen] pub fn post (link: &str) { } #[allow(dead_code)] #[derive(Serialize, Deserialize, Debug)] struct LocalStorage { pub token: String, pub group: String, #[serde(default)] pub header: String, #[serde(default = "default_domain")] pub domain: String, #[serde(default)] pub blacklist: String, #[serde(skip)] pub delay: u8, #[serde(default)] pub posts: bool, #[serde(default)] pub likes: bool, #[serde(default)] pub private: bool, #[serde(default)] pub screenshot: bool, } #[derive(Serialize, Deserialize, Debug)] struct LikedTweet { pub id: String, pub by: TwitterUser, pub me: Option, #[serde(default)] pub is_private: bool, } #[derive(Serialize, Deserialize, Debug)] pub struct TwitterUser { pub name: Option, pub username: String, } impl TwitterUser { fn format (&self) -> String { match &self.name { Some(name) => format!("{name} ({})", self.username.trim_start_matches("@")), None => format!("{}", self.username) } } } pub fn default_domain () -> String { String::from("x.com") } #[allow(non_upper_case_globals)] pub fn escape (mut line: String) -> String { const chars: [char; 21] = [ '\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '<', '&', '#', '+', '-', '=', '|', '{', '}', '.', '!' ]; for char in chars { line = line.replace(char, &format!("\\{}", char)); } line } pub fn format_telegram_api_url (token: &str) -> String { format!("https://api.telegram.org/bot{token}") } pub fn format_tweet_url (domain: &str, username: &str, id: &str) -> String { format!("https://{domain}/{username}/status/{id}") } pub fn format_caption (url: &str, header: &str, by: &TwitterUser, me: &Option) -> String { let by = by.format(); let me = me.as_ref().map(TwitterUser::format); // @TODO: Edge case of where header is set but we can't retrieve Me let header = escape(header .replace("{me}", &me.unwrap_or_default()) .replace("{notme}", &by)); format!("{}{header}{}{}", if !header.is_empty() { "`" } else { Default::default() }, if !header.is_empty() { "`\r\n\r\n" } else { Default::default() }, escape(url.to_owned()) ) } #[allow(unused_macros)] macro_rules! log { ($data: expr) => { log(&format!("{:?}", $data)) }; } #[wasm_bindgen] pub async fn like (tweet: JsValue, image: Option>) -> Option { let store = storage_get(&JsValue::null()).await.ok()?; let store: LocalStorage = serde_wasm_bindgen::from_value(store).ok()?; if !store.likes { return None; } let blacklist = store.blacklist.split(",").map(str::trim).collect::>(); let tweet: LikedTweet = serde_wasm_bindgen::from_value(tweet).ok()?; if !store.private && tweet.is_private { return None; } if blacklist.contains(tweet.by.username.as_str()) { return None; } let domain = if store.domain.is_empty() { default_domain() } else { store.domain }; let group = store.group; let token = store.token; let url = format_tweet_url(&domain, &tweet.by.username, &tweet.id); let caption = format_caption(&url, &store.header, &tweet.by, &tweet.me); let client = Client::new(); let link_preview_options = json!({ "is_disabled": false, "url": url, "prefer_large_media": true, "show_above_text": false, }).to_string(); let (form, endpoint) = if let Some(image) = image && store.screenshot { let endpoint = format!("{}/sendPhoto", format_telegram_api_url(&token)); let part = multipart::Part::bytes(image).file_name("test.png").mime_str("image/png").ok()?; (reqwest::multipart::Form::new() .text("chat_id", group) .text("caption", caption) .text("parse_mode", "MarkdownV2") .text("link_preview_options", link_preview_options) .part("photo", part), endpoint) } else { let endpoint = format!("{}/sendMessage", format_telegram_api_url(&token)); (reqwest::multipart::Form::new() .text("chat_id", group) .text("link_preview_options", link_preview_options) .text("parse_mode", "MarkdownV2") .text("text", caption), endpoint) }; let endpoint = Url::parse(&endpoint).ok()?; let _resp = client .post(endpoint) .multipart(form) .send().await.ok()?; log!(_resp); Some(0) }