mas_handlers/graphql/mutations/
compat_session.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use anyhow::Context as _;
8use async_graphql::{Context, Enum, ID, InputObject, Object};
9use mas_storage::{
10    RepositoryAccess,
11    compat::CompatSessionRepository,
12    queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
13};
14
15use crate::graphql::{
16    model::{CompatSession, NodeType},
17    state::ContextExt,
18};
19
20#[derive(Default)]
21pub struct CompatSessionMutations {
22    _private: (),
23}
24
25/// The input of the `endCompatSession` mutation.
26#[derive(InputObject)]
27pub struct EndCompatSessionInput {
28    /// The ID of the session to end.
29    compat_session_id: ID,
30}
31
32/// The payload of the `endCompatSession` mutation.
33pub enum EndCompatSessionPayload {
34    NotFound,
35    Ended(Box<mas_data_model::CompatSession>),
36}
37
38/// The status of the `endCompatSession` mutation.
39#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)]
40enum EndCompatSessionStatus {
41    /// The session was ended.
42    Ended,
43
44    /// The session was not found.
45    NotFound,
46}
47
48#[Object]
49impl EndCompatSessionPayload {
50    /// The status of the mutation.
51    async fn status(&self) -> EndCompatSessionStatus {
52        match self {
53            Self::Ended(_) => EndCompatSessionStatus::Ended,
54            Self::NotFound => EndCompatSessionStatus::NotFound,
55        }
56    }
57
58    /// Returns the ended session.
59    async fn compat_session(&self) -> Option<CompatSession> {
60        match self {
61            Self::Ended(session) => Some(CompatSession::new(*session.clone())),
62            Self::NotFound => None,
63        }
64    }
65}
66
67/// The input of the `setCompatSessionName` mutation.
68#[derive(InputObject)]
69pub struct SetCompatSessionNameInput {
70    /// The ID of the session to set the name of.
71    compat_session_id: ID,
72
73    /// The new name of the session.
74    human_name: String,
75}
76
77/// The payload of the `setCompatSessionName` mutation.
78pub enum SetCompatSessionNamePayload {
79    /// The session was not found.
80    NotFound,
81
82    /// The session was updated.
83    Updated(mas_data_model::CompatSession),
84}
85
86/// The status of the `setCompatSessionName` mutation.
87#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)]
88enum SetCompatSessionNameStatus {
89    /// The session was updated.
90    Updated,
91
92    /// The session was not found.
93    NotFound,
94}
95
96#[Object]
97impl SetCompatSessionNamePayload {
98    /// The status of the mutation.
99    async fn status(&self) -> SetCompatSessionNameStatus {
100        match self {
101            Self::Updated(_) => SetCompatSessionNameStatus::Updated,
102            Self::NotFound => SetCompatSessionNameStatus::NotFound,
103        }
104    }
105
106    /// The session that was updated.
107    async fn oauth2_session(&self) -> Option<CompatSession> {
108        match self {
109            Self::Updated(session) => Some(CompatSession::new(session.clone())),
110            Self::NotFound => None,
111        }
112    }
113}
114
115#[Object]
116impl CompatSessionMutations {
117    async fn end_compat_session(
118        &self,
119        ctx: &Context<'_>,
120        input: EndCompatSessionInput,
121    ) -> Result<EndCompatSessionPayload, async_graphql::Error> {
122        let state = ctx.state();
123        let mut rng = state.rng();
124        let compat_session_id = NodeType::CompatSession.extract_ulid(&input.compat_session_id)?;
125        let requester = ctx.requester();
126
127        let mut repo = state.repository().await?;
128        let clock = state.clock();
129
130        let session = repo.compat_session().lookup(compat_session_id).await?;
131        let Some(session) = session else {
132            return Ok(EndCompatSessionPayload::NotFound);
133        };
134
135        if !requester.is_owner_or_admin(&session) {
136            return Ok(EndCompatSessionPayload::NotFound);
137        }
138
139        let user = repo
140            .user()
141            .lookup(session.user_id)
142            .await?
143            .context("Could not load user")?;
144
145        // Schedule a job to sync the devices of the user with the homeserver
146        repo.queue_job()
147            .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user))
148            .await?;
149
150        let session = repo.compat_session().finish(&clock, session).await?;
151
152        repo.save().await?;
153
154        Ok(EndCompatSessionPayload::Ended(Box::new(session)))
155    }
156
157    async fn set_compat_session_name(
158        &self,
159        ctx: &Context<'_>,
160        input: SetCompatSessionNameInput,
161    ) -> Result<SetCompatSessionNamePayload, async_graphql::Error> {
162        let state = ctx.state();
163        let compat_session_id = NodeType::CompatSession.extract_ulid(&input.compat_session_id)?;
164        let requester = ctx.requester();
165
166        let mut repo = state.repository().await?;
167        let homeserver = state.homeserver_connection();
168
169        let session = repo.compat_session().lookup(compat_session_id).await?;
170        let Some(session) = session else {
171            return Ok(SetCompatSessionNamePayload::NotFound);
172        };
173
174        if !requester.is_owner_or_admin(&session) {
175            return Ok(SetCompatSessionNamePayload::NotFound);
176        }
177
178        let user = repo
179            .user()
180            .lookup(session.user_id)
181            .await?
182            .context("User not found")?;
183
184        let session = repo
185            .compat_session()
186            .set_human_name(session, Some(input.human_name.clone()))
187            .await?;
188
189        // Update the device on the homeserver side
190        let mxid = homeserver.mxid(&user.username);
191        if let Some(device) = session.device.as_ref() {
192            homeserver
193                .update_device_display_name(&mxid, device.as_str(), &input.human_name)
194                .await
195                .context("Failed to provision device")?;
196        }
197
198        repo.save().await?;
199
200        Ok(SetCompatSessionNamePayload::Updated(session))
201    }
202}