sonicbot_matrix/
lib.rs

1use uuid::Uuid;
2use ruma::{DeviceKeyAlgorithm, RoomId, UserId, UInt, RoomAliasId,  presence::PresenceState, serde::Raw, api::client::r0::sync::sync_events::*};
3use std::collections::BTreeMap;
4use ureq::{Agent, AgentBuilder, OrAnyStatus};
5use serde::{Serialize, Deserialize};
6use serde_json::{json, Value};
7use std::time::{Instant, Duration};
8use linewrapper::LineWrapper;
9use std::sync::mpsc::{channel, Sender, Receiver};
10#[cfg(target_os = "android")]
11use macroquad::prelude::*;
12
13pub mod macros;
14pub mod essentials;
15pub mod plugins;
16mod instruction_generators;
17use instruction_generators::RoomTypeData;
18const BASE: &str = "/_matrix/client/r0/";
19const TIME_TO_IDLE: Duration = Duration::from_secs(3 * 60);
20#[derive(Clone, Debug, Serialize, Deserialize, Default)]
21pub struct StorageData {
22    pub auth_token: String,
23    pub room_to_aliases: BTreeMap<RoomId, Vec<RoomAliasId>>,
24}
25
26#[allow(dead_code)]
27pub struct SonicBotEventModule<'a> {
28    pub name: String,
29    pub essential: bool,
30    pub main: Box<dyn Fn(EventArgs<'a>) -> Vec<crate::Instructions>>,
31    help: String,
32}
33#[derive(Debug, Clone)]
34pub struct MessageArgs<'a> {
35    pub message_info: MessageInfo,
36    pub owner: UserId,
37    pub ctrlc_handler: &'a ctrlc_handler::CtrlCHandler,
38    pub cleanup_on_ctrlc: bool,
39    pub prefix: String,
40}
41
42impl<'a> MessageArgs<'a> {
43    pub fn new(message_info: MessageInfo, owner: UserId, ctrlc_handler: &'a ctrlc_handler::CtrlCHandler, cleanup_on_ctrlc: bool, prefix: String) -> Self {
44        Self {
45            message_info: message_info,
46            owner: owner,
47            ctrlc_handler: ctrlc_handler,
48            cleanup_on_ctrlc: cleanup_on_ctrlc,
49            prefix: prefix,
50        }
51    }
52}
53
54
55#[derive(Debug, Clone)]
56pub struct EventArgs<'a> {
57    pub room_data: crate::instruction_generators::RoomTypeData,
58    pub starting: bool,
59    pub ctrlc_handler: &'a ctrlc_handler::CtrlCHandler,
60    pub cleanup_on_ctrlc: bool,
61    pub owner: UserId,
62    pub prefix: String,
63    pub me: UserId,
64    pub tx: Sender<String>,
65    pub room_to_aliases: BTreeMap<RoomId, Vec<RoomAliasId>>,
66}
67
68impl<'a> EventArgs<'a> {
69    pub fn new(room_data: crate::instruction_generators::RoomTypeData, starting: bool, ctrlc_handler: &'a ctrlc_handler::CtrlCHandler, cleanup_on_ctrlc: bool, owner: UserId, prefix: String, me: UserId, tx: Sender<String>, room_to_aliases: BTreeMap<RoomId, Vec<RoomAliasId>>) -> Self {
70        Self {
71            room_data: room_data,
72            starting: starting,
73            ctrlc_handler: ctrlc_handler,
74            cleanup_on_ctrlc: cleanup_on_ctrlc,
75            owner: owner,
76            prefix: prefix,
77            me: me,
78            tx: tx.clone(),
79            room_to_aliases: room_to_aliases,
80        }
81    }
82}
83
84pub struct SonicBotMessageModule<'a> {
85    pub name: String,
86    pub essential: bool,
87    pub main: Box<dyn Fn(MessageArgs<'a>) -> Vec<crate::Instructions>>,
88    pub help: String,
89}
90
91pub fn generate_module_names(glob_results: glob::Paths) -> Vec<String> {
92    let mut module_names: Vec<String> = Vec::new();
93    for r in glob_results {
94        let file_name: String = r.unwrap().as_path().file_name().unwrap().to_str().unwrap().to_string();
95        let module_name = file_name.split(".").collect::<Vec<&str>>()[0];
96        if module_name != "mod" {
97            module_names.push(module_name.to_string());
98        }
99    }
100    module_names.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
101    module_names
102}
103
104#[derive(Clone, Debug, Serialize, Deserialize, Default)]
105pub struct EventResponse {
106    /// The batch token to supply in the `since` param of the next `/sync` request.
107    pub next_batch: String,
108
109    /// Updates to rooms.
110    #[serde(default, skip_serializing_if = "Rooms::is_empty")]
111    pub rooms: Rooms,
112
113    /// Updates to the presence status of other users.
114    #[serde(default, skip_serializing_if = "Presence::is_empty")]
115    pub presence: Presence,
116
117    /// The global private data created by this user.
118    #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")]
119    pub account_data: GlobalAccountData,
120
121    /// Messages sent directly between devices.
122    #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
123    pub to_device: ToDevice,
124
125    /// Information on E2E device updates.
126    ///
127    /// Only present on an incremental sync.
128    #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
129    pub device_lists: DeviceLists,
130
131    /// For each key algorithm, the number of unclaimed one-time keys
132    /// currently held on the server for a device.
133    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
134    pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, UInt>,
135
136    /// For each key algorithm, the number of unclaimed one-time keys
137    /// currently held on the server for a device.
138    ///
139    /// The presence of this field indicates that the server supports
140    /// fallback keys.
141    #[serde(rename = "org.matrix.msc2732.device_unused_fallback_key_types")]
142    pub device_unused_fallback_key_types: Option<Vec<DeviceKeyAlgorithm>>,
143}
144
145#[derive(Debug)]
146pub struct SonicBot {
147    data: StorageData,
148    host: String,
149    me: UserId,
150    agent: Agent,
151    joined_rooms: Vec<ruma::RoomId>,
152    since: Option<String>,
153    last_response_time: Instant,
154    starting: bool,
155    ctrlc_handler: ctrlc_handler::CtrlCHandler,
156    cleanup_on_ctrlc: bool,
157    prefix: String,
158    owner: UserId,
159    line_wrapper: Option<Sender<String>>,
160}
161#[derive(PartialEq, Eq, Debug, Clone)]
162pub enum Instructions {
163    UpdateLastResponseTime(Instant),
164    AddRoom(ruma::RoomId),
165    Quit(bool),
166    DelRoom(ruma::RoomId),
167    SetSince(String),
168    SendMessage(RoomId, String),
169    SaveRoomAlias(RoomId, RoomAliasId)
170}
171
172#[derive(PartialEq, Eq, Debug, Clone)]
173pub struct MessageInfo {
174    pub message: String,
175    pub words: Vec<String>,
176    pub args: Vec<String>,
177    pub sender: UserId,
178    pub room_id: RoomId,
179    pub room_aliases: Vec<RoomAliasId>
180}
181impl SonicBot {
182    pub fn new(host: impl Into<String>, username: impl Into<String>, server_name: impl Into<String>, cleanup_on_ctrlc: bool, prefix: impl Into<String>, owner: impl Into<String>) -> Self {
183        Self {
184            data: StorageData::default(),
185            host: host.into().trim_end_matches("/").to_string(),
186            me: UserId::try_from(format!("@{}:{}",  username.into(), server_name.into()).as_str()).unwrap(),
187            agent: AgentBuilder::new().timeout_read(Duration::from_secs(3)).build(),
188            joined_rooms: Vec::new(),
189            since: None,
190            //last_event: EventResponse::default(),
191            last_response_time: Instant::now(),
192            starting: true,
193            ctrlc_handler: ctrlc_handler::CtrlCHandler::new(),
194            cleanup_on_ctrlc: cleanup_on_ctrlc,
195            prefix: prefix.into(),
196            owner: UserId::try_from(owner.into()).unwrap(),
197            line_wrapper: None,
198        }
199    }
200    fn login(&self, password: String) -> String {
201        let content = json!({
202            "type": "m.login.password",
203            "identifier": {
204                "type": "m.id.user",
205                "user": self.me.localpart()
206            },
207            "password": password
208        });
209        let val: Value = self.post("login", content, None).unwrap().into_json().unwrap();
210        self.get_tx().send(format!("{:?}", val)).unwrap();
211        val["access_token"].as_str().unwrap().to_string()
212    }
213    fn post(&self, url_ext: impl Into<String>, content: Value, query_pairs: Option<Vec<(String, String)>>) -> Result<ureq::Response, ureq::Transport> {
214        let mut r = self.agent.post(format!("{}{}{}", self.host, BASE, url_ext.into()).as_str()).set("Authorization", format!("Bearer {}", self.data.auth_token).as_str());
215        if let Some(pairs) = query_pairs {
216            for pair in pairs {
217                r = r.query(pair.0.as_str(), pair.1.as_str());
218            }
219        }
220        r.send_json(content).or_any_status()
221    }
222    fn put(&self, url_ext: impl Into<String>, content: Value, query_pairs: Option<Vec<(String, String)>>) -> Result<ureq::Response, ureq::Transport> {
223        let mut r = self.agent.put(format!("{}{}{}", self.host, BASE, url_ext.into()).as_str()).set("Authorization", format!("Bearer {}", self.data.auth_token).as_str());
224        if let Some(pairs) = query_pairs {
225            for pair in pairs {
226                r = r.query(pair.0.as_str(), pair.1.as_str());
227            }
228        }
229        r.send_json(content).or_any_status()
230    }
231    fn get(&self, url_ext: impl Into<String>, query_pairs: Option<Vec<(String, String)>>) -> Result<ureq::Response, ureq::Transport> {
232        let mut r = self.agent.get(format!("{}{}{}", self.host, BASE, url_ext.into()).as_str()).set("Authorization", format!("Bearer {}", self.data.auth_token).as_str());
233        if let Some(pairs) = query_pairs {
234            for pair in pairs {
235                r = r.query(pair.0.as_str(), pair.1.as_str());
236            }
237        }
238        let mut resp = r.clone().call().or_any_status();
239        while let Err(_e) = resp {
240            resp = r.clone().call().or_any_status();
241        }
242        resp
243    }
244    fn join_room_id(&self, room_id: ruma::RoomId) -> Value {
245        //let room_string = room_id_or_alias.into();
246        //let serv_name = server_name.into();
247        let rid = room_id.to_string();
248        let room = urlencoding::encode(rid.as_str());
249        let content = json!({});
250        self.post(format!("rooms/{}/join", room).as_str(), content, None).unwrap().into_json().unwrap()
251    }
252    fn sync(&self) -> Result<EventResponse, Value> {
253        //let mut r = self.get("sync")
254        let presence = self.calculate_presence();
255        let req: ureq::Response;
256        if let Some(start) = self.since.clone() {
257            req = self.get("sync", Some(vec![("since".to_string(), start), ("full_state".to_string(), "false".to_string()), ("set_presence".to_string(), format!("{}", presence)), ("timeout".to_string(), "3000".to_string())])).unwrap();
258        } else {
259            req = self.get("sync", Some(vec![("full_state".to_string(), "true".to_string()), ("set_presence".to_string(), format!("{}", presence)), ("timeout".to_string(), "3000".to_string())])).unwrap();
260        }
261        let val: Value = req.into_json().unwrap();
262        //self.get_tx().send(format!("{:#?}", val.to_string())).unwrap();
263        //sm_println!(self, "{:#?}", val);
264        if val.clone().as_object().unwrap().contains_key("error") {
265            Err(val)
266        } else {
267            Ok(serde_json::from_str::<Raw<EventResponse>>(val.to_string().as_str()).unwrap().deserialize().unwrap())
268        }
269    }
270    fn calculate_presence(&self) -> PresenceState {
271        if self.last_response_time.elapsed() > TIME_TO_IDLE {
272            PresenceState::Unavailable
273        } else {
274            PresenceState::Online
275        }
276    }
277    fn process_instructions(&mut self, instructions: Vec<Instructions>) -> Option<String> {
278        if instructions.contains(&Instructions::Quit(false)) {
279            std::process::exit(0);
280        }
281        for instruction in instructions {
282            match instruction {
283                Instructions::Quit(_x) => {
284                    return Some("QUIT".to_string());
285                },
286                Instructions::DelRoom(x) => {
287                    self.joined_rooms.retain(|z| z == &x);
288                },
289                Instructions::AddRoom(x) => {
290                    if !self.joined_rooms.contains(&x) {
291                        self.joined_rooms.push(x.clone());
292                        self.join_room_id(x);
293                    }
294                },
295                Instructions::UpdateLastResponseTime(x) => {
296                    self.last_response_time = x;
297                },
298                Instructions::SetSince(x) => {
299                    self.since = Some(x);
300                },
301                Instructions::SendMessage(room_id, message) => {
302                    self.send_message(room_id, message);
303                },
304                Instructions::SaveRoomAlias(room_id, alias) => {
305                    if !self.data.room_to_aliases.contains_key(&room_id) {
306                        self.data.room_to_aliases.insert(room_id.clone(), Vec::new()).unwrap();
307                    }
308                    if !self.data.room_to_aliases[&room_id].contains(&alias) {
309                        let mut aliases = self.data.room_to_aliases[&room_id].clone();
310                        aliases.push(alias);
311                        self.data.room_to_aliases.insert(room_id.clone(), aliases);
312                    }
313                    sm_println!(self, "{:#?}", self.data.room_to_aliases[&room_id]);
314                }
315            }
316        }
317        None
318    }
319    #[cfg(not(target_os = "android"))]
320    pub fn start(&mut self, password: impl Into<String>, room_ids: Vec<impl Into<String>>) {
321        let (tx, rx) = channel::<String>();
322        self.line_wrapper = Some(tx.clone());
323        let mut line_wrapper = LineWrapper::new();
324        let pass = password.into();
325        self.data.auth_token = self.login(pass);
326        let mut instructions: Vec<Instructions> = self.generate_instructions(self.sync().unwrap());
327        let mut processed_instructions = self.process_instructions(instructions);
328        self.starting = false;
329        for this_room in room_ids {
330            let this_room_string = this_room.into();
331            let this_room_id_val: Value = self.get(format!("directory/room/{}", urlencoding::encode(this_room_string.as_str()).to_string().as_str()), None).unwrap().into_json().unwrap();
332            let room_id = ruma::RoomId::try_from(this_room_id_val["room_id"].as_str().unwrap()).unwrap();
333            if !self.joined_rooms.contains(&room_id) {
334                self.join_room_id(room_id);
335            }
336        }
337        while processed_instructions.is_none() && self.ctrlc_handler.should_continue() {
338            instructions = self.generate_instructions(self.sync().unwrap());
339            processed_instructions = self.process_instructions(instructions);
340            Self::check_line_wrapper(&rx, &mut line_wrapper);
341        }
342    }
343    fn get_tx(&self) -> Sender<String> {
344        self.line_wrapper.clone().unwrap()
345    }
346    //#[cfg(target_os = "android")]
347    //async fn generate_and_process_instructions(&mut self) {
348    //    self.process_instructions(self.generate_instructions(self.sync().unwrap()));
349    //}
350    #[cfg(target_os = "android")]
351    fn respond(&mut self, room_ids: Vec<impl Into<String>>) {
352        //self.generate_and_process_instructions().await;
353        //info!("[sonicbot-matrix] in _future");
354        //self.get_tx().send("test".to_string()).unwrap();
355        self.process_instructions(self.generate_instructions(self.sync().unwrap()));
356        //info!("[sonicbot-matrix] got past first instructions");
357        self.starting = false;
358        for this_room in room_ids {
359            let this_room_string = this_room.into();
360            let this_room_id_val: Value = self.get(format!("directory/room/{}", urlencoding::encode(this_room_string.as_str()).to_string().as_str()), None).unwrap().into_json().unwrap();
361            let room_id = ruma::RoomId::try_from(this_room_id_val["room_id"].as_str().unwrap()).unwrap();
362            if !self.joined_rooms.contains(&room_id) {
363                self.join_room_id(room_id);
364            }
365        }
366        loop {
367            self.process_instructions(self.generate_instructions(self.sync().unwrap()));
368        }
369    } 
370    #[cfg(target_os = "android")]
371    pub async fn start(mut self, password: impl Into<String>, room_ids: Vec<String>) {
372        let (tx, rx) = channel::<String>();
373        self.line_wrapper = Some(tx.clone());
374        let mut line_wrapper = LineWrapper::new();
375        let pass = password.into();
376        self.data.auth_token = self.login(pass);
377        std::thread::spawn(move || {
378            self.respond(room_ids); 
379        });
380        //info!("[sonicbot-matrix] got past _future");
381        //self.generate_and_process_instructions().await;
382        //self.starting = false;
383        
384        loop {
385            //instructions = self.generate_instructions();
386            //processed_instructions = self.process_instructions(instructions);
387            //info!("[sonicbot-matrix] checking line_wrapper");
388            Self::check_line_wrapper(&rx, &mut line_wrapper);
389            //info!("[sonicbot-matrix] checked line_wrapper");
390            line_wrapper.show_lines();
391            //info!("[sonicbot-matrix] got past show_lines");
392            next_frame().await;
393        }
394    }
395    #[allow(unused_variables)]
396    fn check_line_wrapper(rx: &Receiver<String>, line_wrapper: &mut LineWrapper) {
397        if let Ok(message) = rx.try_recv() {
398            linewrapper::lw_println!(line_wrapper, "{}", message);
399        }
400    }
401    fn send_message(&self, room_id: RoomId, message: impl Into<String>) {
402        let msg = message.into();
403        let txid = Uuid::new_v4().to_simple().encode_lower(&mut Uuid::encode_buffer()).to_string();
404        self.put(format!("rooms/{}/send/m.room.message/{}", room_id.clone(), txid).as_str(), json!({"body": msg, "msgtype": "m.text"}), None).unwrap();
405    }
406    fn generate_instructions(&self, event: EventResponse) -> Vec<Instructions> {
407        let mut instructions: Vec<Instructions> = Vec::new();
408        instructions.push(Instructions::SetSince(event.next_batch));
409        //instructions.append()
410        //if let Some(rooms) = event.rooms {
411        handle_these_rooms!(self, instructions, RoomTypeData::Joined(event.rooms.join), RoomTypeData::Left(event.rooms.leave), RoomTypeData::Invited(event.rooms.invite));
412        instructions
413    }
414}