Skip to main content

arminius_lib_roomservice_client/
requests.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Request types that can be sent over NATS
6use arminius_lib_component::{
7	MessageTokenParseError, MessagesToken,
8	error::{
9		EventNotFound, FeatureNotImplemented, Forbidden, InputJsonError, InternalServerError,
10		MatrixApiError, RoomNotFound,
11	},
12	ruma_requests::ArminiusResponse,
13};
14use arminius_lib_events::{Draft, Verified};
15use arminius_lib_macros::{FromDbError, FromNatsError};
16use arminius_lib_room_service_messages::EventProvenance;
17use axum::{http::StatusCode, response::IntoResponse};
18use ruma::{
19	MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedServerName,
20	OwnedTransactionId, OwnedUserId,
21	api::{
22		client::{
23			membership::get_member_events,
24			redact::redact_event,
25			room::create_room,
26			state::{get_state_event_for_key, get_state_events},
27		},
28		error::{ErrorBody, ErrorKind},
29	},
30	events::{
31		AnyStateEvent, AnyTimelineEvent, TimelineEventType,
32		receipt::{ReceiptThread, ReceiptType},
33		room::member::RoomMemberEventContent,
34	},
35	serde::Raw,
36};
37use serde::{Deserialize, Serialize, de::DeserializeOwned};
38
39use crate::{
40	ruma_request_wrapper::SerializingRumaRequestWrapper,
41	ruma_response_wrapper::SerializingRumaResponseWrapper,
42};
43
44/// The error type for a given API call
45pub type ErrorOf<T> = <T as APICall>::Error;
46
47#[expect(
48	missing_docs,
49	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
50)]
51#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
52#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::NotFound)]
53#[error("Unknown command: {0}")]
54pub struct UnknownCommand(pub String);
55
56/// Trait that describes a possible API call over NATS
57pub trait APICall {
58	/// The root subject to use
59	const ENDPOINT: &str;
60	/// The request type
61	type Request: Serialize + DeserializeOwned;
62	/// The response type
63	type Response: Serialize + DeserializeOwned;
64	/// The error type
65	type Error: Serialize + DeserializeOwned + IntoResponse + From<InternalServerError>;
66}
67
68/// Add an event to some room in which we are already participating
69#[derive(Debug)]
70pub struct AddNewEvent;
71
72#[expect(
73	missing_docs,
74	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
75)]
76#[derive(Debug, Serialize, Deserialize)]
77pub struct NewEvent {
78	pub event_type: TimelineEventType,
79	pub sender: OwnedUserId,
80	/// The body, encoded as JSON
81	pub body: serde_json::Value,
82	pub state_key: Option<String>,
83	/// Optionally, set a custom timestamp (only applicable for appservices)
84	pub timestamp: Option<MilliSecondsSinceUnixEpoch>,
85	/// Optionally, set a transaction ID and Device ID pair for the event
86	pub transaction_information: Option<TransactionInformation>,
87}
88
89#[expect(
90	missing_docs,
91	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
92)]
93#[derive(Debug, Serialize, Deserialize)]
94pub struct TransactionInformation {
95	pub device_id: OwnedDeviceId,
96	pub transaction_id: OwnedTransactionId,
97}
98
99/// The maximum length of any transaction ID the homeserver will accept.
100///
101/// The matrix spec itself does not set a maximum length, so we fall back to
102/// the maximum length of identifiers, see <https://gitlab.com/bundesmessenger/backend/neo/arminius/arminius/-/work_items/915>
103pub const MAX_TRANSACTIONID_LENGTH: usize = 255;
104
105#[expect(
106	missing_docs,
107	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
108)]
109#[derive(
110	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
111)]
112pub enum AddNewEventError {
113	/// Unrecoverable error.
114	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
115	#[error(transparent)]
116	InternalServerError(#[from] InternalServerError),
117	#[error(transparent)]
118	EventNotFound(#[from] EventNotFound),
119	#[error(transparent)]
120	RoomNotFound(#[from] RoomNotFound),
121	#[error(transparent)]
122	InputJsonError(#[from] InputJsonError),
123	/// The event could not be added to the room, due to authorization rules
124	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
125	#[error("Forbidden: {0}")]
126	EventForbidden(String),
127	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::Unknown)]
128	#[error("Tried creating a member event, but could not parse the Matrix ID: {0}")]
129	InvalidMxIdError(String),
130	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::Unknown)]
131	#[error("m.room.member is a state event, but no state key was provided")]
132	StateEventRequiresStateKey,
133	#[error(transparent)]
134	AddRoomMemberEventError(#[from] AddRoomMemberEventError),
135	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::TooLarge)]
136	#[error(
137		"Transaction ID too large. Arminius only accepts transaction IDS up to {MAX_TRANSACTIONID_LENGTH} characters"
138	)]
139	TransactionIdTooLarge,
140}
141
142impl APICall for AddNewEvent {
143	const ENDPOINT: &str = "add_event";
144	type Request = NewEvent;
145	type Response = OwnedEventId;
146	type Error = AddNewEventError;
147}
148
149/// Error when a room version is requested, which we do not support
150#[derive(thiserror::Error, Debug, MatrixApiError, Serialize, Deserialize)]
151#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::UnsupportedRoomVersion)]
152#[error("This server does not support creating version {0} rooms")]
153pub struct UnsupportedRoomVersionError(pub String);
154
155#[expect(
156	missing_docs,
157	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
158)]
159#[derive(
160	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
161)]
162pub enum CreateRoomError {
163	/// Unrecoverable error.
164	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
165	#[error(transparent)]
166	InternalServerError(#[from] InternalServerError),
167	#[error(transparent)]
168	EventNotFound(#[from] EventNotFound),
169	#[error(transparent)]
170	InputJsonError(#[from] InputJsonError),
171	#[error(transparent)]
172	FeatureNotImplemented(#[from] FeatureNotImplemented),
173	#[error(transparent)]
174	UnsupportedRoomVersionError(#[from] UnsupportedRoomVersionError),
175}
176
177impl From<serde_json::Error> for CreateRoomError {
178	fn from(value: serde_json::Error) -> Self {
179		InputJsonError::from(value).into()
180	}
181}
182
183/// A request to create a room
184#[derive(Debug, Serialize, Deserialize)]
185pub struct CreateRoomRequest {
186	/// The user who creates the room
187	pub creator: OwnedUserId,
188	/// The actual create_room request
189	pub request: SerializingRumaRequestWrapper<create_room::v3::Request>,
190	/// The contents of the join event
191	///
192	/// This must be populated by the client service, since it contains profile
193	/// information, to which the room service does not have access
194	pub creator_join_event_contents: RoomMemberEventContent,
195}
196
197/// POST /_matrix/client/*/createRoom
198#[derive(Debug)]
199pub struct CreateRoom;
200
201impl APICall for CreateRoom {
202	const ENDPOINT: &str = "create";
203	type Request = CreateRoomRequest;
204	type Response = OwnedRoomId;
205	type Error = CreateRoomError;
206}
207
208#[expect(
209	missing_docs,
210	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
211)]
212#[derive(thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError)]
213pub enum GetRoomEventError {
214	/// Unrecoverable error.
215	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
216	#[error(transparent)]
217	InternalServerError(#[from] InternalServerError),
218	#[error(transparent)]
219	EventNotFound(#[from] EventNotFound),
220	#[error(transparent)]
221	InputJsonError(#[from] InputJsonError),
222}
223
224/// GET /_matrix/client/*/rooms/{roomId}/event/{eventId}
225#[derive(Debug)]
226pub struct GetRoomEvent;
227
228impl APICall for GetRoomEvent {
229	const ENDPOINT: &str = "get_room_event";
230	type Request = (OwnedUserId, OwnedDeviceId, OwnedEventId);
231	type Response = Raw<AnyTimelineEvent>;
232	type Error = GetRoomEventError;
233}
234
235#[expect(
236	missing_docs,
237	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
238)]
239#[derive(
240	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
241)]
242pub enum GetRoomMembersError {
243	/// Unrecoverable error.
244	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
245	#[error(transparent)]
246	InternalServerError(#[from] InternalServerError),
247	#[error(transparent)]
248	EventNotFound(#[from] EventNotFound),
249	#[error(transparent)]
250	InputJsonError(#[from] InputJsonError),
251	#[error(transparent)]
252	RoomNotFound(#[from] RoomNotFound),
253	#[error(transparent)]
254	FeatureNotImplemented(#[from] FeatureNotImplemented),
255}
256
257/// GET /_matrix/client/v3/rooms/{roomId}/members
258#[derive(Debug)]
259pub struct GetRoomMembers;
260
261impl APICall for GetRoomMembers {
262	const ENDPOINT: &str = "room_members";
263	type Request = (SerializingRumaRequestWrapper<get_member_events::v3::Request>, OwnedUserId);
264	type Response = SerializingRumaResponseWrapper<get_member_events::v3::Response>;
265	type Error = GetRoomMembersError;
266}
267
268#[expect(
269	missing_docs,
270	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
271)]
272#[derive(
273	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
274)]
275pub enum GetStateEventsError {
276	/// Unrecoverable error.
277	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
278	#[error(transparent)]
279	InternalServerError(#[from] InternalServerError),
280	#[error(transparent)]
281	EventNotFound(#[from] EventNotFound),
282	#[error(transparent)]
283	RoomNotFound(#[from] RoomNotFound),
284}
285
286/// GET /_matrix/client/v3/rooms/{roomId}/state
287#[derive(Debug)]
288pub struct GetStateEvents;
289
290impl APICall for GetStateEvents {
291	const ENDPOINT: &str = "get_state_events";
292	type Request = (SerializingRumaRequestWrapper<get_state_events::v3::Request>, OwnedUserId);
293	type Response = SerializingRumaResponseWrapper<get_state_events::v3::Response>;
294	type Error = GetStateEventsError;
295}
296
297#[expect(
298	missing_docs,
299	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
300)]
301#[derive(
302	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
303)]
304pub enum GetStateEventsForKeyError {
305	/// Unrecoverable error.
306	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
307	#[error(transparent)]
308	InternalServerError(#[from] InternalServerError),
309	#[error(transparent)]
310	EventNotFound(#[from] EventNotFound),
311	#[error(transparent)]
312	InputJsonError(#[from] InputJsonError),
313}
314
315/// GET /_matrix/client/v3/rooms/{roomId}/state/{eventType}/{stateKey}
316#[derive(Debug)]
317pub struct GetStateEventsForKey;
318
319impl APICall for GetStateEventsForKey {
320	const ENDPOINT: &str = "get_state_event";
321	type Request =
322		(SerializingRumaRequestWrapper<get_state_event_for_key::v3::Request>, OwnedUserId);
323	type Response = SerializingRumaResponseWrapper<get_state_event_for_key::v3::Response>;
324	type Error = GetStateEventsForKeyError;
325}
326
327#[expect(
328	missing_docs,
329	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
330)]
331#[derive(thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromNatsError)]
332pub enum RedactEventError {
333	/// Unrecoverable error.
334	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
335	#[error(transparent)]
336	InternalServerError(#[from] InternalServerError),
337	#[error(transparent)]
338	EventNotFound(#[from] EventNotFound),
339	#[error(transparent)]
340	InputJsonError(#[from] InputJsonError),
341	#[error(transparent)]
342	Forbidden(#[from] Forbidden),
343	#[error(transparent)]
344	RoomNotFound(#[from] RoomNotFound),
345	#[error(transparent)]
346	FeatureNotImplemented(#[from] FeatureNotImplemented),
347}
348
349/// PUT /_matrix/client/v3/rooms/{roomId}/redact/{eventId}/{txnId}
350#[derive(Debug)]
351pub struct RedactEvent;
352
353impl APICall for RedactEvent {
354	const ENDPOINT: &str = "redact_room_event";
355	type Request = (SerializingRumaRequestWrapper<redact_event::v3::Request>, OwnedUserId);
356	type Response = SerializingRumaResponseWrapper<redact_event::v3::Response>;
357	type Error = RedactEventError;
358}
359
360/// Parameters for getting state event diffs
361#[derive(Debug, Serialize, Deserialize)]
362pub struct GetStateEventsDiffRequestParameters {
363	/// query parameter to fetch the state. `at` is the encoded message
364	/// token representing the event stream position to fetch the state at.
365	pub at: MessagesToken,
366	/// query parameter to fetch the state diffs between diff and at.
367	/// If provided, `diff` is the encoded message token representing
368	/// the event stream position to diff against.
369	pub diff: Option<MessagesToken>,
370}
371
372/// Response for getting state event diffs
373#[derive(Debug, Serialize, Deserialize)]
374pub struct GetStateEventsDiffResponse {
375	/// Array of state events representing the diff
376	/// events, if the `diff` parameter was provided.
377	/// Otherwise, the full state at the `at` event.
378	pub room_state: Vec<Raw<AnyStateEvent>>,
379}
380
381/// Command to get state event diffs
382#[derive(Debug)]
383pub struct GetStateEventsDiffs;
384
385#[expect(
386	missing_docs,
387	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
388)]
389#[derive(thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError)]
390pub enum GetStateEventsDiffError {
391	/// Unrecoverable error.
392	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
393	#[error(transparent)]
394	InternalServerError(#[from] InternalServerError),
395	#[error(transparent)]
396	EventNotFound(#[from] EventNotFound),
397	#[error(transparent)]
398	RoomNotFound(#[from] RoomNotFound),
399	#[error(transparent)]
400	MessageTokenParseError(#[from] MessageTokenParseError),
401}
402
403impl APICall for GetStateEventsDiffs {
404	const ENDPOINT: &str = "get_state_events_diffs";
405	type Request = GetStateEventsDiffRequestParameters;
406	type Response = GetStateEventsDiffResponse;
407	type Error = GetStateEventsDiffError;
408}
409
410/// Command to mark a room as forgotten
411#[derive(Debug)]
412pub struct ForgetRoom;
413
414#[expect(
415	missing_docs,
416	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
417)]
418#[derive(
419	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
420)]
421pub enum ForgetRoomError {
422	#[error(transparent)]
423	InternalServerError(#[from] InternalServerError),
424	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::Unknown)]
425	#[error("You cannot forget a room you're still in")]
426	StillInRoom,
427	#[error(transparent)]
428	RoomNotFound(#[from] RoomNotFound),
429}
430
431impl APICall for ForgetRoom {
432	const ENDPOINT: &str = "forget_room";
433	type Request = OwnedUserId;
434	type Response = ();
435	type Error = ForgetRoomError;
436}
437
438/// Command to add a new `m.room.member` event.
439///
440/// This is distinct from the [`AddNewEvent`] API, as when joining remote rooms
441/// we don't always get to control the entire contents of the event
442#[derive(Debug)]
443pub struct AddRoomMemberEvent;
444
445/// Wrapper around a matrix error we received over federation
446///
447/// In some cases, as when joining foreign rooms, if we get specific errors,
448/// such as FORBIDDEN, we just want to pass the error directly on to the client
449#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
450#[error("Custom matrix error")]
451pub struct FederatedMatrixError(pub arminius_server_federation_output::ErrorResponse);
452
453impl MatrixApiError for FederatedMatrixError {
454	fn create_matrix_error(&self) -> ruma::api::error::Error {
455		ErrorBody::Json(self.0.matrix_error.clone()).into_error(self.0.http_error)
456	}
457}
458
459impl IntoResponse for FederatedMatrixError {
460	fn into_response(self) -> axum::response::Response {
461		ArminiusResponse::from(self.create_matrix_error()).into_response()
462	}
463}
464
465#[expect(
466	missing_docs,
467	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
468)]
469#[derive(
470	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
471)]
472pub enum AddRoomMemberEventError {
473	#[error(transparent)]
474	InternalServerError(#[from] InternalServerError),
475	#[error(transparent)]
476	RoomNotFound(#[from] RoomNotFound),
477	#[error(transparent)]
478	FeatureNotImplemented(#[from] FeatureNotImplemented),
479	#[error(transparent)]
480	InputJsonError(#[from] InputJsonError),
481	/// The event could not be added to the room, due to authorization rules
482	#[error("Not allowed to add member event: {0}")]
483	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
484	EventForbidden(String),
485	#[error(transparent)]
486	UnsupportedRoomVersionError(#[from] UnsupportedRoomVersionError),
487	#[error(transparent)]
488	FederatedMatrixError(#[from] FederatedMatrixError),
489}
490
491/// A request to add a new member event to a room
492#[derive(Debug, Serialize, Deserialize)]
493pub struct AddRoomMemberEventRequest {
494	/// Foreign homeservers that may know about the room, in case we aren't participating in the
495	/// room
496	///
497	/// For instance when joining a foreign room such as
498	/// `!HyaS7FAoB2e0F9uwuePaUHki4IRo1_HwtlwElB0N78A` we need the name of a homeserver that
499	/// already knows about the room.
500	///
501	/// In some other cases when making an `AddRoomMemberEventRequest`, it can be safely assumed
502	/// that the `m.room.member` event is being added to a room which our homeserver is
503	/// already participating in. In this case, `via` can be left empty.
504	pub via: Vec<OwnedServerName>,
505	/// The content of the resulting `m.room.member` event, if any
506	pub member_event_content: RoomMemberEventContent,
507	/// The member to whom the member event applies. Will be expressed in the
508	/// state_key
509	pub member: OwnedUserId,
510	/// The user sending the event
511	pub sender: OwnedUserId,
512}
513
514impl APICall for AddRoomMemberEvent {
515	const ENDPOINT: &str = "add_room_member_event";
516	type Request = AddRoomMemberEventRequest;
517	type Response = ();
518	type Error = AddRoomMemberEventError;
519}
520
521/// Command to add an event we received over federation to the DAG
522#[derive(Debug)]
523pub struct AddFederatedEvent;
524
525/// Command to add a PDU to the DAG
526#[derive(Debug, Serialize, Deserialize)]
527pub struct AddFederatedEventCommand {
528	/// The PDU to add to the DAG
529	pub pdu: Box<serde_json::value::RawValue>,
530	/// Where the event came from.
531	///
532	/// The room service records this on the fact it publishes and does not act
533	/// on it; whether the event is then sent onward is the federation output
534	/// service's decision. That matters for
535	/// [`/send_join`](https://spec.matrix.org/latest/server-server-api/#put_matrixfederationv2send_joinroomideventid)
536	/// and friends, where the sender's homeserver does not yet know enough
537	/// about the room to distribute the event, so delivery falls to us —
538	/// [`EventProvenance::ResidentServerFor`] says so, while an event that
539	/// simply arrived over the transactions endpoint is
540	/// [`EventProvenance::Federated`] and must not be sent back out.
541	pub provenance: EventProvenance,
542}
543
544#[expect(
545	missing_docs,
546	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
547)]
548#[derive(
549	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
550)]
551pub enum AddFederatedEventError {
552	#[error(transparent)]
553	InternalServerError(#[from] InternalServerError),
554	#[error(transparent)]
555	InputJsonError(#[from] InputJsonError),
556	#[error(transparent)]
557	FeatureNotImplemented(#[from] FeatureNotImplemented),
558	#[error(transparent)]
559	RoomNotFound(#[from] RoomNotFound),
560	#[error(transparent)]
561	Forbidden(#[from] Forbidden),
562	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
563	#[error("Unauthorized: {message}")]
564	Unauthorized { message: String },
565}
566
567impl APICall for AddFederatedEvent {
568	const ENDPOINT: &str = "add_fed_event";
569
570	type Request = AddFederatedEventCommand;
571
572	type Response = OwnedEventId;
573
574	type Error = AddFederatedEventError;
575}
576
577/// Command to add a read receipt for a user in a specific room.
578#[derive(Debug)]
579pub struct ReadReceipt;
580
581#[expect(
582	missing_docs,
583	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
584)]
585#[derive(
586	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
587)]
588pub enum ReadReceiptError {
589	#[error(transparent)]
590	InternalServerError(#[from] InternalServerError),
591	#[error(transparent)]
592	EventNotFound(#[from] EventNotFound),
593	#[error(transparent)]
594	RoomNotFound(#[from] RoomNotFound),
595	/// The read receipt's thread ID is invalid. For example if the event
596	/// pointed at by it is not the head of a thread.
597	#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam)]
598	#[error("Invalid thread id: {0}")]
599	InvalidThreadId(String),
600	#[error(transparent)]
601	FeatureNotImplemented(#[from] FeatureNotImplemented),
602}
603
604/// Request to add a read receipt in a room.
605#[derive(Debug, Deserialize, Serialize)]
606pub struct ReadReceiptRequest {
607	/// The user setting the read receipt.
608	pub user_id: OwnedUserId,
609	/// The type of read receipt (public or private).
610	pub receipt_type: ReceiptType,
611	/// The event to acknowledge "up-to".
612	pub event_id: OwnedEventId,
613	/// The thread the receipt applies to.
614	///
615	/// NOTE: There is a custom `ruma` (de)serialization implemented on this
616	/// type, which makes `None` become a `ReceiptThread::Unthreaded`.
617	#[serde(default, skip_serializing_if = "ruma::serde::is_default")]
618	pub thread: ReceiptThread,
619	/// The timestamp the receipt was sent at.
620	pub ts: chrono::DateTime<chrono::Utc>,
621	/// Where the receipt came from.
622	///
623	/// Carried for the same reason as on [`AddFederatedEventCommand`]: a
624	/// receipt that reached us over federation must not be sent back out, and
625	/// that decision belongs to the federation output service rather than here.
626	pub provenance: EventProvenance,
627}
628
629impl APICall for ReadReceipt {
630	const ENDPOINT: &str = "read_receipt";
631	type Request = ReadReceiptRequest;
632	type Response = ();
633	type Error = ReadReceiptError;
634}
635
636/// Command to add a read marker for a user in a specific room.
637#[derive(Debug)]
638pub struct SetFullyReadMarker;
639
640#[expect(
641	missing_docs,
642	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
643)]
644#[derive(
645	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
646)]
647pub enum ReadMarkerError {
648	#[error(transparent)]
649	InternalServerError(#[from] InternalServerError),
650	#[error(transparent)]
651	EventNotFound(#[from] EventNotFound),
652	#[error(transparent)]
653	RoomNotFound(#[from] RoomNotFound),
654}
655
656/// Request to add a read receipt in a room.
657#[derive(Debug, Deserialize, Serialize)]
658pub struct ReadMarkerRequest {
659	/// The user setting the read marker.
660	pub user_id: OwnedUserId,
661	/// The event to acknowledge "up-to".
662	pub event_id: OwnedEventId,
663}
664
665impl APICall for SetFullyReadMarker {
666	const ENDPOINT: &str = "read_marker";
667	type Request = ReadMarkerRequest;
668	type Response = ();
669	type Error = ReadMarkerError;
670}
671
672/// Command to contextualize an event
673#[derive(Debug)]
674pub struct ContextualizeEvent;
675
676#[expect(
677	missing_docs,
678	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
679)]
680#[derive(
681	thiserror::Error, MatrixApiError, Debug, Serialize, Deserialize, FromDbError, FromNatsError,
682)]
683pub enum ContextualizeEventError {
684	/// Unrecoverable error.
685	/// See <https://bundesmessenger.gitlab.io/backend/neo/arminius/arminius/03-development/errors.html#unrecoverable-errors>
686	#[error(transparent)]
687	InternalServerError(#[from] InternalServerError),
688	#[error(transparent)]
689	RoomNotFound(#[from] RoomNotFound),
690	/// The event could not be added to the room, due to authorization rules
691	#[error("Forbidden: {0}")]
692	#[matrix_api_error(status_code = StatusCode::FORBIDDEN, error_kind = ErrorKind::Forbidden)]
693	EventForbidden(String),
694}
695
696impl APICall for ContextualizeEvent {
697	const ENDPOINT: &str = "contextualize_event";
698	type Request = arminius_lib_events::RoomEvent<Draft>;
699	type Response = arminius_lib_events::RoomEvent<Verified>;
700	type Error = ContextualizeEventError;
701}