public_appservice/
api.rs

1use axum::{Json, extract::State, http::StatusCode};
2
3use ruma::events::room::{
4    history_visibility::{HistoryVisibility, RoomHistoryVisibilityEvent},
5    member::{MembershipState, RoomMemberEvent},
6};
7use ruma::events::space::child::SpaceChildEvent;
8use std::time::Duration;
9
10use ruma::events::macros::EventContent;
11
12use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14use std::sync::Arc;
15
16use crate::AppState;
17
18use crate::cache::CacheKey;
19
20#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
21#[ruma_event(type = "commune.public.room", kind = State, state_key_type = String)]
22pub struct CommunePublicRoomEventContent {
23    pub public: bool,
24}
25
26pub async fn transactions(
27    State(state): State<Arc<AppState>>,
28    Json(payload): Json<Value>,
29) -> Result<Json<Value>, (StatusCode, String)> {
30    let events = match payload.get("events") {
31        Some(Value::Array(events)) => events,
32        Some(_) | None => {
33            tracing::info!("Events is not an array");
34            return Ok(Json(json!({})));
35        }
36    };
37
38    for event in events {
39        if cfg!(debug_assertions) {
40            tracing::info!("Event: {:#?}", event);
41        }
42
43        // If auto-join is enabled, join rooms with world_readable history visibility
44        if state.config.appservice.rules.auto_join {
45            if let Ok(event) = serde_json::from_value::<RoomHistoryVisibilityEvent>(event.clone()) {
46                if event.history_visibility() == &HistoryVisibility::WorldReadable {
47                    tracing::info!("History Visibility: World Readable");
48
49                    tokio::spawn(async move {
50                        // Join the room if history visibility is world readable
51                        // delay for a moment to allow the event to be processed
52                        tokio::time::sleep(Duration::from_secs(5)).await;
53
54                        let room_id = event.room_id().to_owned();
55                        tracing::info!("Joining room: {}", room_id);
56                        if let Err(e) = state.appservice.join_room(room_id.clone()).await {
57                            tracing::warn!("Failed to join room: {}. Error: {}", room_id, e);
58                        } else {
59                            tracing::info!("Successfully joined room: {}", room_id);
60                        }
61                    });
62
63                    return Ok(Json(json!({})));
64                }
65            }
66
67            if let Ok(event) = serde_json::from_value::<SpaceChildEvent>(event.clone()) {
68                tracing::info!("Auto joining space child room");
69
70                tokio::spawn(async move {
71                    let room_id = event.room_id().to_owned();
72                    tracing::info!("Joining room: {}", room_id);
73                    if let Err(e) = state.appservice.join_room(room_id.clone()).await {
74                        tracing::warn!("Failed to join room: {}. Error: {}", room_id, e);
75                    } else {
76                        tracing::info!("Successfully joined room: {}", room_id);
77                    }
78                });
79
80                return Ok(Json(json!({})));
81            }
82        };
83
84        let public = event["content"]["public"].as_bool();
85        if let Ok(event) = serde_json::from_value::<CommunePublicRoomEvent>(event.clone()) {
86            tracing::info!("Commune Public room event.");
87            let room_id = event.room_id().to_owned();
88            match public {
89                Some(true) => {
90                    tracing::info!("Joining room: {}", room_id);
91                    let joined = state.appservice.join_room(room_id.clone()).await;
92                    // cache the joined status
93                    if let Ok(joined) = joined {
94                        let cache_key = ("appservice:joined", room_id.as_str()).cache_key();
95                        if (state.cache.cache_data(&cache_key, &joined, 300).await).is_ok() {
96                            tracing::info!("Cached joined status for room: {}", room_id);
97                        } else {
98                            tracing::warn!("Failed to cache joined status for room: {}", room_id);
99                        }
100                    }
101                }
102                Some(false) => {
103                    tracing::info!("Leaving room: {}", room_id);
104                    if let Err(e) = state.appservice.leave_room(room_id.clone()).await {
105                        tracing::warn!("Failed to leave room: {}. Error: {}", room_id, e);
106                    } else {
107                        tracing::info!("Successfully left room: {}", room_id);
108                    }
109                    let cache_key = ("appservice:joined", room_id.as_str()).cache_key();
110                    if let Err(e) = state.cache.delete_cached_data(&cache_key).await {
111                        tracing::warn!(
112                            "Failed to delete room from cache: {}. Error: {}",
113                            room_id,
114                            e
115                        );
116                    } else {
117                        tracing::info!("Successfully removed room from cache: {}", room_id);
118                    }
119                }
120                None => {}
121            }
122        };
123
124        let member_event =
125            if let Ok(event) = serde_json::from_value::<RoomMemberEvent>(event.clone()) {
126                event
127            } else {
128                continue;
129            };
130
131        println!("Member Event: {member_event:#?}");
132
133        let room_id = member_event.room_id().to_owned();
134        let membership = member_event.membership().to_owned();
135        let server_name = member_event.room_id().server_name();
136
137        match server_name {
138            Some(server_name) => {
139                let allowed = state
140                    .config
141                    .appservice
142                    .rules
143                    .federation_domain_whitelist
144                    .iter()
145                    .any(|domain| server_name.as_str().ends_with(domain));
146
147                if server_name.as_str() != state.config.matrix.server_name && allowed {
148                    // Ignore events for rooms on other servers, if configured to local homeserver
149                    // users
150                    if state.config.appservice.rules.invite_by_local_user {
151                        tracing::info!(
152                            "Ignoring event for room on different server: {}",
153                            server_name
154                        );
155                        continue;
156                    }
157                }
158            }
159            None => {
160                tracing::info!("Ignoring event for room with no server name");
161                continue;
162            }
163        }
164
165        // Ignore membership events for other users
166        let invited_user = member_event.state_key().to_owned();
167        if invited_user != state.appservice.user_id() {
168            tracing::info!("Ignoring event for user: {}", invited_user);
169            continue;
170        }
171
172        match membership {
173            MembershipState::Invite => {
174                tracing::info!("Joining room: {}", room_id);
175                if let Err(e) = state.appservice.join_room(room_id.clone()).await {
176                    tracing::warn!("Failed to join room: {}. Error: {}", room_id, e);
177                } else {
178                    tracing::info!("Successfully joined room: {}", room_id);
179                }
180                if state
181                    .appservice
182                    .add_to_joined_rooms(room_id.clone())
183                    .is_err()
184                {
185                    tracing::warn!("Failed to add room to joined rooms list: {}", room_id);
186                }
187            }
188            MembershipState::Leave => {
189                if let Err(e) = state.appservice.leave_room(room_id.clone()).await {
190                    tracing::warn!("Failed to leave room: {}. Error: {}", room_id, e);
191                } else {
192                    tracing::info!("Successfully left room: {}", room_id);
193                }
194                if let Err(e) = state.appservice.remove_from_joined_rooms(&room_id) {
195                    tracing::warn!(
196                        "Failed to remove room from joined rooms list: {} {}",
197                        room_id,
198                        e
199                    );
200                }
201            }
202            MembershipState::Ban => {
203                tracing::info!("Banned from room: {}", room_id);
204                if let Err(e) = state.appservice.remove_from_joined_rooms(&room_id) {
205                    tracing::warn!(
206                        "Failed to remove room from joined rooms list: {} {}",
207                        room_id,
208                        e
209                    );
210                }
211            }
212            _ => {}
213        }
214    }
215
216    Ok(Json(json!({})))
217}