tgx2/rs/src/lib.rs
2024-08-31 06:18:16 +03:00

199 lines
5.1 KiB
Rust

#![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<JsValue, JsValue>;
#[wasm_bindgen(catch, js_namespace = ["chrome", "storage", "local"], js_name = get)]
async fn storage_get (data: &JsValue) -> Result<JsValue, JsValue>;
}
#[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<TwitterUser>,
#[serde(default)]
pub is_private: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TwitterUser {
pub name: Option<String>,
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<TwitterUser>) -> 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<Vec<u8>>) -> Option<u8> {
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::<HashSet<&str>>();
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)
}