1use axum::extract::State;
2use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::ready};
3use ruma::{
4 DeviceId, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId, TransactionId, UserId,
5 events::{
6 room::{
7 create::RoomCreateEventContent,
8 guest_access::{GuestAccess, RoomGuestAccessEventContent},
9 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
10 join_rules::{JoinRule, RoomJoinRulesEventContent},
11 member::{MembershipState, RoomMemberEventContent},
12 message::RoomMessageEventContent,
13 name::RoomNameEventContent,
14 power_levels::RoomPowerLevelsEventContent,
15 },
16 tag::TagName,
17 },
18 serde::Raw,
19};
20use synapse_admin_api::server_notices::send::{
21 by_txn,
22 v1::{self, Response},
23};
24use tuwunel_api_client::{notice_tag, room_is_notice};
25use tuwunel_core::{
26 Err, Result, err,
27 utils::{
28 BoolExt, FutureBoolExt,
29 future::ReadyBoolExt,
30 str_from_bytes,
31 stream::{IterStream, ReadyExt},
32 },
33};
34use tuwunel_matrix::pdu::PduBuilder;
35use tuwunel_service::Services;
36
37use crate::RumaAdmin;
38
39pub(crate) async fn admin_send_server_notice_route(
46 State(services): State<crate::State>,
47 body: RumaAdmin<v1::Request>,
48) -> Result<Response> {
49 let request = body.body;
50
51 send_notice(
52 &services,
53 &request.user_id,
54 request.event_type.as_deref(),
55 request.state_key.as_deref(),
56 request.content,
57 )
58 .map_ok(Response::new)
59 .await
60}
61
62pub(crate) async fn admin_send_server_notice_txn_route(
67 State(services): State<crate::State>,
68 body: RumaAdmin<by_txn::Request>,
69) -> Result<Response> {
70 let sender_user = body
71 .sender_user
72 .expect("user must be authenticated for this handler");
73
74 let sender_device = body.sender_device;
75 let request = body.body;
76
77 check_existing_txnid(&services, &sender_user, sender_device.as_deref(), &request.txn_id)
78 .await
79 .map(|response| ready(response).right_future())
80 .unwrap_or_else(|| {
81 send_notice_txn(&services, &sender_user, sender_device.as_deref(), request)
82 .left_future()
83 })
84 .await
85}
86
87async fn send_notice_txn(
91 services: &Services,
92 sender_user: &UserId,
93 sender_device: Option<&DeviceId>,
94 request: by_txn::Request,
95) -> Result<Response> {
96 let event_id = send_notice(
97 services,
98 &request.user_id,
99 request.event_type.as_deref(),
100 request.state_key.as_deref(),
101 request.content,
102 )
103 .await?;
104
105 services.transaction_ids.add_txnid(
106 sender_user,
107 sender_device,
108 &request.txn_id,
109 event_id.as_bytes(),
110 );
111
112 Ok(Response::new(event_id))
113}
114
115async fn send_notice(
120 services: &Services,
121 target: &UserId,
122 event_type: Option<&str>,
123 state_key: Option<&str>,
124 content: Raw<RoomMessageEventContent>,
125) -> Result<OwnedEventId> {
126 if !services.globals.user_is_local(target) {
127 return Err!(Request(InvalidParam("Server notices can only be sent to local users")));
128 }
129
130 if !services.users.exists(target).await {
131 return Err!(Request(NotFound("User not found")));
132 }
133
134 let room_id = find_notice_room(services, target)
135 .then(|room| {
136 room.map(|room_id| ready(Ok(room_id)).right_future())
137 .unwrap_or_else(|| {
138 create_notice_room(services, target)
139 .boxed() .left_future()
141 })
142 })
143 .await?;
144
145 let server_user = services.globals.server_user.as_ref();
146 let state_lock = services.state.mutex.lock(&room_id).await;
147
148 let is_joined = services.state_cache.is_joined(target, &room_id);
149 let is_invited = services.state_cache.is_invited(target, &room_id);
150 let needs_invite = is_joined.is_false().and(is_invited.is_false());
151
152 if needs_invite.await {
153 let pdu = PduBuilder::state(
154 target.as_str(),
155 &RoomMemberEventContent::new(MembershipState::Invite),
156 );
157
158 services
159 .timeline
160 .build_and_append_pdu(pdu, server_user, &room_id, &state_lock)
161 .boxed() .await?;
163 }
164
165 let content = Raw::from_raw_value(content.json());
166
167 let pdu = PduBuilder {
168 event_type: event_type.unwrap_or("m.room.message").into(),
169 content,
170 state_key: state_key.map(Into::into),
171 ..Default::default()
172 };
173
174 let event_id = services
175 .timeline
176 .build_and_append_pdu(pdu, server_user, &room_id, &state_lock)
177 .await?;
178
179 drop(state_lock);
180
181 Ok(event_id)
182}
183
184#[tracing::instrument(level = "trace", skip_all)]
189async fn find_notice_room(services: &Services, target: &UserId) -> Option<OwnedRoomId> {
190 let server_user = services.globals.server_user.as_ref();
191 let admin_room = services
192 .admin
193 .get_admin_room()
194 .map(Result::ok)
195 .await;
196
197 let tag = notice_tag(&services.config.admin.room.tag);
198
199 services
200 .state_cache
201 .get_shared_rooms(server_user, target)
202 .map(ToOwned::to_owned)
203 .chain(
204 services
205 .state_cache
206 .rooms_invited(target)
207 .map(ToOwned::to_owned),
208 )
209 .chain(
210 services
211 .state_cache
212 .rooms_left(target)
213 .map(ToOwned::to_owned),
214 )
215 .ready_filter(|room_id| admin_room.as_ref() != Some(room_id))
216 .filter_map(|room_id| notice_candidate(services, target, &tag, room_id))
217 .take(1)
218 .ready_fold(None, |_, room_id| Some(room_id))
219 .await
220}
221
222#[tracing::instrument(level = "trace", skip_all)]
226async fn notice_candidate(
227 services: &Services,
228 target: &UserId,
229 tag: &TagName,
230 room_id: OwnedRoomId,
231) -> Option<OwnedRoomId> {
232 room_is_notice(services, &services.globals.server_user, target, tag, &room_id)
233 .map(|notice| notice.is_ok_and(|notice| notice))
234 .await
235 .then_some(room_id)
236}
237
238#[tracing::instrument(level = "debug", skip_all)]
243async fn create_notice_room(services: &Services, target: &UserId) -> Result<OwnedRoomId> {
244 let room_id = RoomId::new_v1(services.globals.server_name());
245
246 let _short_id = services
247 .short
248 .get_or_create_shortroomid(&room_id)
249 .await;
250
251 let state_lock = services.state.mutex.lock(&room_id).await;
252 let server_user: &UserId = services.globals.server_user.as_ref();
253
254 let content = RoomCreateEventContent {
255 room_version: RoomVersionId::V11,
256 ..RoomCreateEventContent::new_v11()
257 };
258
259 [
260 PduBuilder::state(String::new(), &content),
261 PduBuilder::state(
262 server_user.as_str(),
263 &RoomMemberEventContent::new(MembershipState::Join),
264 ),
265 PduBuilder::state(String::new(), ¬ice_power_levels(server_user)),
266 PduBuilder::state(String::new(), &RoomJoinRulesEventContent::new(JoinRule::Invite)),
267 PduBuilder::state(
268 String::new(),
269 &RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
270 ),
271 PduBuilder::state(String::new(), &RoomGuestAccessEventContent::new(GuestAccess::CanJoin)),
272 PduBuilder::state(String::new(), &RoomNameEventContent::new("Server Notices".to_owned())),
273 ]
274 .into_iter()
275 .try_stream()
276 .try_for_each(|pdu| {
277 services
278 .timeline
279 .build_and_append_pdu(pdu, server_user, &room_id, &state_lock)
280 .map_ok(|_| ())
281 })
282 .await?;
283
284 drop(state_lock);
285
286 services
287 .account_data
288 .set_room_tag(target, &room_id, notice_tag(&services.config.admin.room.tag), None)
289 .await?;
290
291 Ok(room_id)
292}
293
294fn notice_power_levels(server_user: &UserId) -> RoomPowerLevelsEventContent {
298 RoomPowerLevelsEventContent {
299 users: [(server_user.into(), 100.into())].into(),
300 users_default: (-10).into(),
301 ..Default::default()
302 }
303}
304
305async fn check_existing_txnid(
310 services: &Services,
311 sender_user: &UserId,
312 sender_device: Option<&DeviceId>,
313 txn_id: &TransactionId,
314) -> Option<Result<Response>> {
315 services
316 .transaction_ids
317 .existing_txnid(sender_user, sender_device, txn_id)
318 .map_ok(|response| notice_response(&response))
319 .map(Result::ok)
320 .await
321}
322
323fn notice_response(response: &[u8]) -> Result<Response> {
327 response
328 .is_empty()
329 .is_false()
330 .ok_or_else(|| {
331 err!(Request(InvalidParam(
332 "Tried to use txn_id already used for an incompatible endpoint."
333 )))
334 })
335 .and_then(|()| {
336 str_from_bytes(response)
337 .ok()
338 .and_then(|event_id| event_id.try_into().ok())
339 .map(Response::new)
340 .ok_or_else(|| err!(Database("Invalid event_id in txn_id data: {response:?}.")))
341 })
342}
343
344#[cfg(test)]
345mod tests {
346 use ruma::{Int, user_id};
347
348 use super::notice_power_levels;
349
350 #[test]
351 fn power_levels_mute_the_target() {
352 let server = user_id!("@server:example.com");
353 let power_levels = notice_power_levels(server);
354
355 assert_eq!(power_levels.users.get(server), Some(&Int::from(100)));
356 assert_eq!(power_levels.users_default, Int::from(-10));
357 assert_eq!(power_levels.events_default, Int::from(0));
358 assert!(power_levels.users_default < power_levels.events_default);
359 }
360}