From b26bfd50fab820447154f1a1a50f551a3fd261bb Mon Sep 17 00:00:00 2001 From: Tranquillity Codes Date: Wed, 8 Nov 2023 17:11:20 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.toml | 12 ++ src/main.rs | 429 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 442 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6a03bd4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mxclient" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = {version = "1.0.191", features = ["derive"]} +serde_json = {version = "1.0.108", features = ["raw_value"]} +serde_with = {version = "3.4.0", features = ["json"]} +ureq = {version = "2.8.0", features = ["json"]} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2ad4777 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,429 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +use std::collections::HashMap; +use std::time::Instant; +use std::fs::read_to_string; +use std::fs; + +//use serde_json::Result; +// {"m.homeserver": {"base_url": "https://matrix.itycodes.org", "onion_url": "http://matrix.gnozq2oxulflviccmdjt3aour42jgahycazoy3mdpuebzi2s4kktpwqd.onion"}} +#[derive(Serialize, Deserialize, Debug)] +struct HomeServer { + base_url: String, + onion_url: Option, +} +#[derive(Serialize, Deserialize, Debug)] +struct WellKnownClient { + #[serde(rename(deserialize = "m.homeserver"))] + m_homeserver: HomeServer, +} + +#[derive(Eq, Hash, PartialEq, Debug, Clone)] +struct MatrixUser { + name: String, + server: String +} +#[derive(Debug)] +struct MatrixClient { + homeserver: String, + user: MatrixUser, + token: Option +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize)] +struct Config { + homeserver: String, + user: String, + password: String, + token: Option, + device: Option +} + +#[derive(Serialize, Deserialize, Debug)] +struct LoginIdentifier { + #[serde(rename(serialize = "type"))] + ident_type: String, + user: String +} +#[derive(Serialize, Deserialize, Debug)] +struct LoginDetails { + identifier: LoginIdentifier, + #[serde(rename(serialize = "initial_device_display_name"))] + initial_name: String, + password: String, + #[serde(rename(serialize = "type"))] + login_type: String +} +#[derive(Serialize, Deserialize, Debug, Clone)] +struct LoginToken { + #[serde(rename(deserialize = "access_token"))] + token: String, + device_id: String +} +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +struct StateFilter { + limit: Option, + contains_url: Option, + include_redudant_members: Option, + unread_thread_notifications: Option, + lazy_load_members: Option, + not_rooms: Option>, + not_types: Option>, + not_senders: Option>, + rooms: Option>, + senders: Option>, + types: Option>, +} +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +struct RoomEventFilter { + limit: Option, + contains_url: Option, + include_redudant_members: Option, + unread_thread_notifications: Option, + lazy_load_members: Option, + not_rooms: Option>, + not_types: Option>, + not_senders: Option>, + rooms: Option>, + senders: Option>, + types: Option>, +} +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +struct EventFilter { + limit: Option, + not_senders: Option>, + not_types: Option>, + senders: Option>, + types: Option>, +} +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +struct RoomFilter { + account_data: RoomEventFilter, + ephemeral: RoomEventFilter, + include_leave: Option, + not_rooms: Option>, + rooms: Option>, + state: StateFilter, + timeline: RoomEventFilter +} +#[derive(Serialize, Deserialize, Debug)] +enum EventFormat { + #[serde(rename = "client")] + Client, + #[serde(rename = "federation")] + Federation +} +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Default)] +struct FilterConfig { + account_data: EventFilter, + event_fields: Option>, + event_format: Option, + presence: EventFilter, + room: RoomFilter +} +#[derive(Serialize, Deserialize, Debug)] +struct Filter { + filter_id: String, +} + +#[derive(Serialize, Deserialize, Debug)] +enum SetPresence { + #[serde(rename = "offline")] + Offline, + #[serde(rename = "online")] + Online, + #[serde(rename = "unavailable")] + Unavailable +} +#[derive(Serialize, Deserialize, Debug, Default)] +struct SyncConfig { + filter: String, // Does Rust have proper unions? + full_state: Option, + set_presence: Option, + since: Option, + timeout: Option +} +#[derive(Serialize, Deserialize, Debug)] +struct Event { + content: HashMap, + #[serde(rename = "type")] + event_type: String +} +#[derive(Serialize, Deserialize, Debug)] +struct AccountData { + events: Vec +} +#[derive(Serialize, Deserialize, Debug)] +struct StrippedStateEvent { + //content: EventContent, + sender: String, + state_key: String, + #[serde(rename = "type")] + event_type: String +} +//#[derive(Serialize, Deserialize, Debug)] +//struct InviteState { +// events: Vec +//} +//#[derive(Serialize, Deserialize, Debug)] +//struct InvitedRooms { +// invite_state: InviteState +//} +#[derive(Serialize, Deserialize, Debug)] +struct ClientEventWithoutRoomID { + content: HashMap, + event_id: String, + origin_server_ts: u64, + sender: String, + state_key: Option, + #[serde(rename = "type")] + event_type: String, + // unsigned: UnsignedData +} +#[derive(Serialize, Deserialize, Debug)] +struct State { + events: Vec, +} +#[derive(Serialize, Deserialize, Debug)] +struct Timeline { + events: Vec, + limited: bool, + prev_batch: String +} +#[derive(Serialize, Deserialize, Debug)] +struct JoinedRooms { + account_data: AccountData, + state: State, + timeline: Timeline +} +#[derive(Serialize, Deserialize, Debug)] +struct Rooms { + //invite: , + join: HashMap, + //knock: , + //leave: , +} +#[derive(Serialize, Deserialize, Debug)] +struct SyncData { + account_data: AccountData, + rooms: Rooms, +} + +#[derive(Default, Debug, Clone)] +enum Priority { + Low, + #[default] + Normal, + High +} + +#[derive(Default, Debug, Clone)] +struct Room { + id: String, + name: Option, + alias: Option, + is_dm: bool, + priority: Priority, + prev_batch: String +} +#[derive(Default, Debug)] +struct RoomIndex { + high_priority: Vec, + normal_priority: Vec, + low_priority: Vec, +} +#[derive(Default, Debug)] +struct DMIndex { + high_priority: HashMap>, + normal_priority: HashMap>, + low_priority: HashMap>, +} + +fn well_known(homeserver_url: &str) -> WellKnownClient { + let body: String = ureq::get(format!("{homeserver_url}/.well-known/matrix/client").as_str()) + //.set("Example-Header", "header value") + .call().unwrap() + .into_string().unwrap(); + let well_known: WellKnownClient = serde_json::from_str(body.as_str()).unwrap(); + well_known +} +fn login(config: &MatrixClient, details: LoginDetails) -> LoginToken { + let body: String = ureq::post(format!("{0}/_matrix/client/v3/login", config.homeserver).as_str()) + .send_json(details).unwrap() + .into_string().unwrap(); + let token: LoginToken = serde_json::from_str(body.as_str()).unwrap(); + token +} +fn make_filter(config: &MatrixClient, filter: FilterConfig) -> Filter { + let body: String = ureq::post(format!("{0}/_matrix/client/v3/user/{1}/filter", config.homeserver, format!("@{0}:{1}", config.user.name.as_str(), config.user.server.as_str())).as_str()) + .set("Authorization", format!("Bearer {0}", config.token.as_ref().unwrap()).as_str()) + .send_json(filter).unwrap() + .into_string().unwrap(); + let filter: Filter = serde_json::from_str(body.as_str()).unwrap(); + filter +} +fn do_sync(config: &MatrixClient, params: SyncConfig) -> SyncData { + let body: String = ureq::get(format!("{0}/_matrix/client/v3/sync", config.homeserver).as_str()) + .set("Authorization", format!("Bearer {0}", config.token.as_ref().unwrap()).as_str()) + .query("filter", params.filter.as_str()) + .send_json(params).unwrap() + .into_string().unwrap(); + //println!("{}", body); + let synced: SyncData = serde_json::from_str(body.as_str()).unwrap(); + synced +} + +fn main() { + let mut cfg: Config = serde_json::from_str(&read_to_string("./config.json").unwrap()).unwrap(); + let homeserver = cfg.homeserver.clone(); + let mut client = MatrixClient { homeserver: well_known(format!("https://{homeserver}").as_str()).m_homeserver.base_url.to_owned(), user: MatrixUser { name: cfg.user.clone(), server: homeserver.to_string() }, token: None }; + let login_details = LoginDetails { + identifier: LoginIdentifier { user: cfg.user.clone(), ident_type: String::from("m.id.user") }, + initial_name: String::from("rsmx"), + password: cfg.password.clone(), + login_type: String::from("m.login.password") + }; + let before_sync = Instant::now(); + let mut opt_token: Option = None; + if cfg.token == None || cfg.device == None { + let logged = login(&client, login_details); + + opt_token = Some(logged.clone()); + + cfg.token = Some(logged.token); + cfg.device = Some(logged.device_id); + fs::write("./config.json", serde_json::to_string(&cfg).unwrap()).unwrap(); + } + //let token = LoginToken { token: String::from("syt_aXR5_pZMxSUrXpalVqsEVWOco_4BFwQl"), device_id: "KIYJVNPCYP".to_string() }; + let token = opt_token.unwrap_or( LoginToken { token: cfg.token.unwrap(), device_id: cfg.device.unwrap() } ); + println!("Device ID: {0}", token.device_id); + println!("Token: {0}", token.token); + client.token = Some(token.token); + let filter_conf = FilterConfig { + room: RoomFilter { + timeline: RoomEventFilter { + types: Some(Vec::from(["m.room.member".to_string()])), + senders: Some(Vec::from([format!("@{0}:{1}", client.user.name.as_str(), client.user.server.as_str())])), + ..Default::default() + }, + ephemeral: RoomEventFilter { + types: Some(Vec::new()), + ..Default::default() + }, + state: StateFilter { + types: Some(Vec::from(["m.room.name".to_string(), "m.room.canonical_alias".to_string()])), + ..Default::default() + }, + ..Default::default() + }, + presence: EventFilter { + types: Some(Vec::new()), + ..Default::default() + }, + ..Default::default() + }; + //println!("{:?}", filter_conf); + //println!("{}", serde_json::to_string(&filter_conf).unwrap()); + let filter = make_filter(&client, filter_conf); + //println!("{:?}", &filter); + let initial = do_sync(&client, SyncConfig {filter: filter.filter_id, ..Default::default() } ); + + println!("Sync: {0}ms", before_sync.elapsed().as_millis()); + + //println!("{}", serde_json::to_string(&initial).unwrap()); + let joined = initial.rooms.join; + //let mut tmp_dms: HashMap> = Default::default(); + + + let before_parse = Instant::now(); + + let mut room_index: RoomIndex = Default::default(); + for (room_id, room_data) in joined { + let mut name: Option = None; + let mut alias: Option = None; + for state_ev in room_data.state.events { + if state_ev.event_type == "m.room.name" { + name = Some(state_ev.content.get("name").unwrap().as_str().unwrap().to_string()); + } + if state_ev.event_type == "m.room.canonical_alias" { + alias = Some(state_ev.content.get("alias").unwrap().as_str().unwrap().to_string()); + } + } + let is_direct = false; + let prev_batch = room_data.timeline.prev_batch; + for ev in room_data.account_data.events { + if ev.event_type == "m.tag" { + if ev.content.get("tags").unwrap().as_object().unwrap().contains_key("m.lowpriority") { + room_index.low_priority.push(Room { id: room_id.clone(), name: name.clone(), alias: alias.clone(), priority: Priority::Low, is_dm: is_direct, prev_batch: prev_batch.clone() }); + } else if ev.content.get("tags").unwrap().as_object().unwrap().contains_key("m.favourite") { + room_index.high_priority.push(Room { id: room_id.clone(), name: name.clone(), alias: alias.clone(), priority: Priority::High, is_dm: is_direct, prev_batch: prev_batch.clone() }); + } else { + let weird_res = ev.content.get("tags").unwrap().as_object().unwrap(); + panic!("Unreachable: {}", serde_json::to_string(&weird_res).unwrap()); + } + break; + } + } + room_index.normal_priority.push(Room { id: room_id.clone(), name: name.clone(), alias: alias.clone(), priority: Priority::Normal, is_dm: is_direct, prev_batch: prev_batch.clone() }); + } + let account_events = initial.account_data.events; + let mut dms: DMIndex = Default::default(); + for ev in account_events { + if ev.event_type == "m.direct" { + for (user_name, room_ids) in ev.content { + let mut low_dms: Vec = Vec::new(); + let mut normal_dms: Vec = Vec::new(); + let mut high_dms: Vec = Vec::new(); + for room_id in room_ids.as_array().unwrap() { + // why is a clone needed here if the iterator is not mutable? + for room in &room_index.low_priority { + if room.id == room_id.as_str().unwrap() { + low_dms.push(room.clone()); + break; + } + } + for room in &room_index.normal_priority { + if room.id == room_id.as_str().unwrap() { + normal_dms.push(room.clone()); + break; + } + } + for room in &room_index.high_priority { + if room.id == room_id.as_str().unwrap() { + high_dms.push(room.clone()); + break; + } + } + } + let split = user_name.split(":").collect::>(); + let name = split.get(0).unwrap()[1..].to_string(); + let server = split.get(1).unwrap().to_string(); + let user = MatrixUser { name: name, server: server }; + if low_dms.len() > 0 { + dms.low_priority.insert(user.clone(), low_dms); + } + if normal_dms.len() > 0 { + dms.normal_priority.insert(user.clone(), normal_dms); + } + if high_dms.len() > 0 { + dms.high_priority.insert(user.clone(), high_dms); + } + } + } + } + + println!("Parse: {0}ms", before_parse.elapsed().as_millis()); + //for room in dms.high_priority { + // println!("{:?}", room); + //} +}