mas_handlers/graphql/mutations/
matrix.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, Description, Enum, ID, InputObject, Object};
9
10use crate::graphql::{
11    UserId,
12    model::{NodeType, User},
13    state::ContextExt,
14};
15
16#[derive(Default)]
17pub struct MatrixMutations {
18    _private: (),
19}
20
21/// The input for the `addEmail` mutation
22#[derive(InputObject)]
23struct SetDisplayNameInput {
24    /// The ID of the user to add the email address to
25    user_id: ID,
26
27    /// The display name to set. If `None`, the display name will be removed.
28    display_name: Option<String>,
29}
30
31/// The status of the `setDisplayName` mutation
32#[derive(Enum, Copy, Clone, Eq, PartialEq)]
33pub enum SetDisplayNameStatus {
34    /// The display name was set
35    Set,
36    /// The display name is invalid
37    Invalid,
38}
39
40/// The payload of the `setDisplayName` mutation
41#[derive(Description)]
42enum SetDisplayNamePayload {
43    Set(User),
44    Invalid,
45}
46
47#[Object(use_type_description)]
48impl SetDisplayNamePayload {
49    /// Status of the operation
50    async fn status(&self) -> SetDisplayNameStatus {
51        match self {
52            SetDisplayNamePayload::Set(_) => SetDisplayNameStatus::Set,
53            SetDisplayNamePayload::Invalid => SetDisplayNameStatus::Invalid,
54        }
55    }
56
57    /// The user that was updated
58    async fn user(&self) -> Option<&User> {
59        match self {
60            SetDisplayNamePayload::Set(user) => Some(user),
61            SetDisplayNamePayload::Invalid => None,
62        }
63    }
64}
65
66#[Object]
67impl MatrixMutations {
68    /// Set the display name of a user
69    async fn set_display_name(
70        &self,
71        ctx: &Context<'_>,
72        input: SetDisplayNameInput,
73    ) -> Result<SetDisplayNamePayload, async_graphql::Error> {
74        let state = ctx.state();
75        let id = NodeType::User.extract_ulid(&input.user_id)?;
76        let requester = ctx.requester();
77
78        if !requester.is_owner_or_admin(&UserId(id)) {
79            return Err(async_graphql::Error::new("Unauthorized"));
80        }
81
82        // Allow non-admins to change their display name if the site config allows it
83        if !requester.is_admin() && !state.site_config().displayname_change_allowed {
84            return Err(async_graphql::Error::new("Unauthorized"));
85        }
86
87        let mut repo = state.repository().await?;
88        let user = repo
89            .user()
90            .lookup(id)
91            .await?
92            .context("Failed to lookup user")?;
93        repo.cancel().await?;
94
95        let conn = state.homeserver_connection();
96        let mxid = conn.mxid(&user.username);
97
98        if let Some(display_name) = &input.display_name {
99            // Let's do some basic validation on the display name
100            if display_name.len() > 256 {
101                return Ok(SetDisplayNamePayload::Invalid);
102            }
103
104            if display_name.is_empty() {
105                return Ok(SetDisplayNamePayload::Invalid);
106            }
107
108            conn.set_displayname(&mxid, display_name)
109                .await
110                .context("Failed to set display name")?;
111        } else {
112            conn.unset_displayname(&mxid)
113                .await
114                .context("Failed to unset display name")?;
115        }
116
117        Ok(SetDisplayNamePayload::Set(User(user.clone())))
118    }
119}