axfive_matrix_dicebot/
bot.rs1use crate::matrix::{Event, MessageContent, RoomEvent, SyncCommand, NoticeMessage};
2use crate::commands::parse_command;
3use reqwest::{Client, Url};
4use serde::{self, Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8const USER_AGENT: &str =
9 "AxFive Matrix DiceBot/0.1.0 (+https://gitlab.com/Taywee/axfive-matrix-dicebot)";
10
11#[derive(Serialize, Deserialize, Debug)]
13pub struct MatrixConfig {
14 pub home_server: String,
16
17 #[serde(default)]
19 pub next_batch: Option<String>,
20
21 #[serde(default)]
23 pub txn_id: u64,
24
25 pub login: toml::Value,
28}
29
30#[derive(Serialize, Deserialize, Debug)]
32pub struct Config {
33 pub matrix: MatrixConfig,
34}
35
36pub struct DiceBot {
40 config_path: Option<PathBuf>,
41 config: Config,
42 access_token: String,
43 next_batch: Option<String>,
44 client: Client,
45 home_server: Url,
46 txn_id: u64,
47}
48
49#[derive(Deserialize, Debug)]
50struct LoginResponse {
51 access_token: String,
52}
53
54impl DiceBot {
55 pub async fn new(
57 config_path: Option<PathBuf>,
58 config: Config,
59 ) -> Result<Self, Box<dyn std::error::Error>> {
60 let home_server: Url = format!("https://{}", config.matrix.home_server).parse()?;
61 let client = Client::new();
62 let request = serde_json::to_string(&config.matrix.login)?;
63 let mut login_url = home_server.clone();
64 login_url.set_path("/_matrix/client/r0/login");
65 let response = client
66 .post(login_url)
67 .header("user-agent", USER_AGENT)
68 .body(request)
69 .send()
70 .await?;
71 let body: LoginResponse = serde_json::from_str(&response.text().await?)?;
72 let next_batch = config.matrix.next_batch.clone();
73 let txn_id = config.matrix.txn_id;
74 Ok(DiceBot {
75 home_server,
76 config_path,
77 client,
78 config,
79 access_token: body.access_token,
80 next_batch,
81 txn_id,
82 })
83 }
84
85 pub async fn from_path<P: Into<PathBuf>>(
87 config_path: P,
88 ) -> Result<Self, Box<dyn std::error::Error>> {
89 let config_path = config_path.into();
90 let config = {
91 let contents = fs::read_to_string(&config_path)?;
92 toml::from_str(&contents)?
93 };
94 DiceBot::new(Some(config_path), config).await
95 }
96
97 fn url<S: AsRef<str>>(&self, path: S, query: &[(&str, &str)]) -> Url {
100 let mut url = self.home_server.clone();
101 url.set_path(path.as_ref());
102 {
103 let mut query_pairs = url.query_pairs_mut();
104 query_pairs.append_pair("access_token", &self.access_token);
105
106 for pair in query.iter() {
107 query_pairs.append_pair(pair.0, pair.1);
108 }
109 }
110
111 url
112 }
113
114 pub async fn sync(&mut self) -> Result<(), Box<dyn std::error::Error>> {
116 let mut sync_url = self.url("/_matrix/client/r0/sync", &[("timeout", "30000")]);
117
118 if let Some(since) = &self.next_batch {
120 sync_url.query_pairs_mut().append_pair("since", since);
121 }
122 let body = self
123 .client
124 .get(sync_url)
125 .header("user-agent", USER_AGENT)
126 .send()
127 .await?
128 .text()
129 .await?;
130 let sync: SyncCommand = serde_json::from_str(&body).unwrap();
131 for room in sync.rooms.invite.keys() {
133 let join_url = self.url(format!("/_matrix/client/r0/rooms/{}/join", room), &[]);
134 self.client
135 .post(join_url)
136 .header("user-agent", USER_AGENT)
137 .send()
138 .await?;
139 }
140
141 for (room_id, room) in sync.rooms.join.iter() {
142 for event in &room.timeline.events {
143 if let Event::Room(RoomEvent {
144 sender,
145 event_id: _,
146 content: MessageContent::Text(message),
147 ..
148 }) = event
149 {
150 let (plain, html): (String, String) = match parse_command(message.body()) {
151 Ok(Some(command)) => {
152 let command = command.execute();
153 (command.plain().into(), command.html().into())
154 },
155 Ok(None) => continue,
156 Err(e) => {
157 let message = format!("Error parsing command: {}", e);
158 let html_message = format!("<p><strong>{}</strong></p>", message);
159 (message, html_message)
160 },
161 };
162
163 let plain = format!("{}\n{}", sender, plain);
164 let html = format!("<p>{}</p>\n{}", sender, html);
165
166 let message = NoticeMessage {
167 body: plain,
168 format: Some("org.matrix.custom.html".into()),
169 formatted_body: Some(html),
170 };
171
172 self.txn_id += 1;
173 let send_url = self.url(format!("/_matrix/client/r0/rooms/{}/send/m.room.message/{}", room_id, self.txn_id), &[]);
174 self.client
175 .put(send_url)
176 .header("user-agent", USER_AGENT)
177 .body(serde_json::to_string(&message)?)
178 .send()
179 .await?;
180 }
181 }
182 }
183 self.next_batch = Some(sync.next_batch);
184 Ok(())
185 }
186
187 pub async fn logout(mut self) -> Result<(), Box<dyn std::error::Error>> {
190 let logout_url = self.url("/_matrix/client/r0/logout", &[]);
191 self.client
192 .post(logout_url)
193 .header("user-agent", USER_AGENT)
194 .body("{}")
195 .send()
196 .await?;
197
198 self.config.matrix.next_batch = self.next_batch;
199 self.config.matrix.txn_id = self.txn_id;
200
201 if let Some(config_path) = self.config_path {
202 let config = toml::to_string_pretty(&self.config)?;
203 fs::write(config_path, config)?;
204 }
205
206 Ok(())
207 }
208}