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 pub next_batch: String,
108
109 #[serde(default, skip_serializing_if = "Rooms::is_empty")]
111 pub rooms: Rooms,
112
113 #[serde(default, skip_serializing_if = "Presence::is_empty")]
115 pub presence: Presence,
116
117 #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")]
119 pub account_data: GlobalAccountData,
120
121 #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
123 pub to_device: ToDevice,
124
125 #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
129 pub device_lists: DeviceLists,
130
131 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
134 pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, UInt>,
135
136 #[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_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 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 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 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")]
351 fn respond(&mut self, room_ids: Vec<impl Into<String>>) {
352 self.process_instructions(self.generate_instructions(self.sync().unwrap()));
356 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 loop {
385 Self::check_line_wrapper(&rx, &mut line_wrapper);
389 line_wrapper.show_lines();
391 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 handle_these_rooms!(self, instructions, RoomTypeData::Joined(event.rooms.join), RoomTypeData::Left(event.rooms.leave), RoomTypeData::Invited(event.rooms.invite));
412 instructions
413 }
414}