199 lines
5.1 KiB
Rust
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)
|
|
|
|
}
|