1use crate::prelude::*;
27
28use tokio::time::{Duration, sleep};
29
30use prometheus::Counter;
31use prometheus::{opts, register_counter};
32
33static ROOM_INVITES: LazyLock<Counter> = LazyLock::new(|| {
34 register_counter!(opts!("room_invite_events_total", "Number of room invites",)).unwrap()
35});
36
37#[derive(Clone, Deserialize)]
39pub struct ModuleConfig {
40 pub homeservers: Vec<String>,
42 #[serde(default = "keywords_join")]
44 pub keywords_join: Vec<String>,
45 #[serde(default = "keywords_leave")]
47 pub keywords_leave: Vec<String>,
48 #[serde(default = "leave_message")]
49 pub leave_message: String,
51}
52
53fn keywords_join() -> Vec<String> {
54 vec!["join".s()]
55}
56
57fn keywords_leave() -> Vec<String> {
58 vec!["leave".s(), "part".s()]
59}
60
61fn leave_message() -> String {
62 String::from("goodbye 😿")
63}
64
65pub(crate) fn starter(mx: &Client, config: &Config) -> anyhow::Result<Vec<ModuleInfo>> {
66 info!("registering autojoiner");
67 let autojoiner_config: ModuleConfig = config.typed_module_config(module_path!())?;
68 let autojoiner_handle =
69 mx.add_event_handler(move |ev, mx, room| autojoiner(ev, mx, room, autojoiner_config));
70
71 let module_config: ModuleConfig = config.typed_module_config(module_path!())?;
72
73 let (join_tx, join_rx) = mpsc::channel::<ConsumerEvent>(1);
74 let join = ModuleInfo {
75 name: "join".s(),
76 help: "makes the bot join a channel".s(),
77 acl: vec![Acl::SpecificUsers(config.admins())],
78 trigger: TriggerType::Keyword(module_config.keywords_join.clone()),
79 channel: join_tx,
80 error_prefix: None,
81 };
82 tokio::task::spawn(join_consumer(join_rx, mx.clone(), autojoiner_handle));
83
84 let (leave_tx, leave_rx) = mpsc::channel::<ConsumerEvent>(1);
85 let leave = ModuleInfo {
86 name: "leave".s(),
87 help: "makes the bot leave a channel".s(),
88 acl: vec![Acl::SpecificUsers(config.admins())],
89 trigger: TriggerType::Keyword(module_config.keywords_leave.clone()),
90 channel: leave_tx,
91 error_prefix: Some("couldn't leave room".s()),
92 };
93 leave.spawn(leave_rx, module_config, leave_processor);
94
95 Ok(vec![join, leave])
96}
97
98pub async fn leave_processor(event: ConsumerEvent, config: ModuleConfig) -> anyhow::Result<()> {
106 let leave_room = if let Some(room_str) = event.args {
107 maybe_get_room(&event.room.client(), &room_str).await?
108 } else {
109 event.room
110 };
111
112 leave_room
113 .send(RoomMessageEventContent::text_plain(config.leave_message))
114 .await?;
115 leave_room.leave().await?;
116 Ok(())
117}
118
119pub async fn join_consumer(
125 mut rx: mpsc::Receiver<ConsumerEvent>,
126 mx: Client,
127 autojoiner_handle: EventHandlerHandle,
128) -> anyhow::Result<()> {
129 loop {
130 let Some(event) = rx.recv().await else {
131 warn!("channel closed");
132 info!("stopping the autojoiner");
133 mx.remove_event_handler(autojoiner_handle);
134 bail!("channel closed");
135 };
136
137 if let Err(e) = join_processor(mx.clone(), event.clone()).await {
138 error!("couldn't join the room: {e}");
139 if let Err(ee) = event
140 .room
141 .send(RoomMessageEventContent::text_plain(format!(
142 "couldn't join room: {e}"
143 )))
144 .await
145 {
146 error!("couldn't send error message: {ee}");
147 };
148 };
149 }
150}
151
152pub async fn join_processor(mx: Client, event: ConsumerEvent) -> anyhow::Result<()> {
160 if let Some(room_str) = event.args {
161 let room = maybe_get_room(&mx, &room_str).await?;
162 info!("joining room: {room_str} {}", room.room_id());
163 let mut delay = 2;
164 let mut joined = true;
165
166 while let Err(err) = room.join().await {
167 error!(
171 "Failed to join room {} ({err:?}), retrying in {delay}s",
172 room.room_id()
173 );
174
175 sleep(Duration::from_secs(delay)).await;
176 delay *= 2;
177
178 if delay > 3600 {
179 error!("Can't join room {} ({err:?})", room.room_id());
180 joined = false;
181 break;
182 }
183 }
184
185 trace!("Successfully joined room {}", room.room_id());
186 let response = if joined {
187 format!("joined {room_str}")
188 } else {
189 bail!("couldn't join {room_str}")
190 };
191 event
192 .room
193 .send(RoomMessageEventContent::text_plain(response))
194 .await?;
195 } else {
196 bail!("missing argument: room");
197 }
198
199 Ok(())
200}
201
202#[allow(clippy::unused_async)]
210pub async fn autojoiner(
211 room_member: StrippedRoomMemberEvent,
212 client: Client,
213 room: Room,
214 module_config: ModuleConfig,
215) -> anyhow::Result<()> {
216 if room_member.state_key
218 != client
219 .user_id()
220 .ok_or_else(|| anyhow!("missing our own userid!?"))?
221 {
222 bail!("invite not ment for us");
223 }
224
225 trace!("getting homeserver name for room");
226 let Some(room_homeserver) = &room.room_id().server_name() else {
227 bail!("retrieving room server name fails");
228 };
229
230 trace!(
231 "checking if invite is for a room on permitted homeserver: {:#?}, {:#?}",
232 &room_homeserver, &module_config.homeservers
233 );
234 if !module_config
235 .homeservers
236 .contains(&room_homeserver.to_string())
237 {
238 bail!("room is on wrong homeserver");
239 };
240
241 ROOM_INVITES.inc();
242
243 tokio::spawn(async move {
244 info!("Autojoining room {}", room.room_id());
245 let mut delay = 2;
246
247 while let Err(err) = room.join().await {
248 error!(
252 "Failed to join room {} ({err:?}), retrying in {delay}s",
253 room.room_id()
254 );
255
256 sleep(Duration::from_secs(delay)).await;
257 delay *= 2;
258
259 if delay > 3600 {
260 error!("Can't join room {} ({err:?})", room.room_id());
261 break;
262 }
263 }
264 trace!("Successfully joined room {}", room.room_id());
265 });
266
267 Ok(())
268}