libsignal_service/websocket/
profile.rs

1use libsignal_protocol::Aci;
2use reqwest::Method;
3use serde::{Deserialize, Serialize};
4use zkgroup::profiles::{ProfileKeyCommitment, ProfileKeyVersion};
5
6use crate::{
7    content::ServiceError,
8    push_service::AvatarWrite,
9    utils::{serde_base64, serde_optional_base64},
10    websocket::{self, account::DeviceCapabilities, SignalWebSocket},
11};
12
13#[derive(Debug, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct SignalServiceProfile {
16    #[serde(default, with = "serde_optional_base64")]
17    pub identity_key: Option<Vec<u8>>,
18    #[serde(default, with = "serde_optional_base64")]
19    pub name: Option<Vec<u8>>,
20    #[serde(default, with = "serde_optional_base64")]
21    pub about: Option<Vec<u8>>,
22    #[serde(default, with = "serde_optional_base64")]
23    pub about_emoji: Option<Vec<u8>>,
24
25    // TODO: not sure whether this is via optional_base64
26    // #[serde(default, with = "serde_optional_base64")]
27    // pub payment_address: Option<Vec<u8>>,
28    pub avatar: Option<String>,
29    pub unidentified_access: Option<String>,
30
31    #[serde(default)]
32    pub unrestricted_unidentified_access: bool,
33
34    pub capabilities: DeviceCapabilities,
35}
36
37#[derive(Debug, Serialize)]
38#[serde(rename_all = "camelCase")]
39struct SignalServiceProfileWrite<'s> {
40    /// Hex-encoded
41    version: &'s str,
42    #[serde(with = "serde_base64")]
43    name: &'s [u8],
44    #[serde(with = "serde_base64")]
45    about: &'s [u8],
46    #[serde(with = "serde_base64")]
47    about_emoji: &'s [u8],
48    avatar: bool,
49    same_avatar: bool,
50    #[serde(with = "serde_base64")]
51    commitment: &'s [u8],
52}
53
54impl SignalWebSocket<websocket::Identified> {
55    pub async fn retrieve_profile_by_id(
56        &mut self,
57        address: Aci,
58        profile_key: Option<zkgroup::profiles::ProfileKey>,
59    ) -> Result<SignalServiceProfile, ServiceError> {
60        let path = if let Some(key) = profile_key {
61            let version =
62                bincode::serialize(&key.get_profile_key_version(address))?;
63            let version = std::str::from_utf8(&version)
64                .expect("hex encoded profile key version");
65            format!("/v1/profile/{}/{}", address.service_id_string(), version)
66        } else {
67            format!("/v1/profile/{}", address.service_id_string())
68        };
69        // TODO: set locale to en_US
70        self.http_request(Method::GET, path)?
71            .send()
72            .await?
73            .service_error_for_status()
74            .await?
75            .json()
76            .await
77    }
78
79    /// Writes a profile and returns the avatar URL, if one was provided.
80    ///
81    /// The name, about and emoji fields are encrypted with an [`ProfileCipher`][struct@crate::profile_cipher::ProfileCipher].
82    /// See [`AccountManager`][struct@crate::AccountManager] for a convenience method.
83    ///
84    /// Java equivalent: `writeProfile`
85    pub async fn write_profile<'s, C, S>(
86        &mut self,
87        version: &ProfileKeyVersion,
88        name: &[u8],
89        about: &[u8],
90        emoji: &[u8],
91        commitment: &ProfileKeyCommitment,
92        avatar: AvatarWrite<&mut C>,
93    ) -> Result<Option<String>, ServiceError>
94    where
95        C: std::io::Read + Send + 's,
96        S: AsRef<str>,
97    {
98        // Bincode is transparent and will return a hex-encoded string.
99        let version = bincode::serialize(version)?;
100        let version = std::str::from_utf8(&version)
101            .expect("profile_key_version is hex encoded string");
102        let commitment = bincode::serialize(commitment)?;
103
104        let command = SignalServiceProfileWrite {
105            version,
106            name,
107            about,
108            about_emoji: emoji,
109            avatar: !matches!(avatar, AvatarWrite::NoAvatar),
110            same_avatar: matches!(avatar, AvatarWrite::RetainAvatar),
111            commitment: &commitment,
112        };
113
114        // XXX this should  be a struct; cfr ProfileAvatarUploadAttributes
115        let upload_url: Result<String, _> = self
116            .http_request(Method::PUT, "/v1/profile")?
117            .send_json(&command)
118            .await?
119            .service_error_for_status()
120            .await?
121            .json()
122            .await;
123
124        match (upload_url, avatar) {
125            (_url, AvatarWrite::NewAvatar(_avatar)) => {
126                // FIXME
127                unreachable!("Uploading avatar unimplemented");
128            },
129            // FIXME cleanup when #54883 is stable and MSRV:
130            // or-patterns syntax is experimental
131            // see issue #54883 <https://github.com/rust-lang/rust/issues/54883> for more information
132            (Err(_), AvatarWrite::RetainAvatar)
133            | (Err(_), AvatarWrite::NoAvatar) => {
134                // OWS sends an empty string when there's no attachment
135                Ok(None)
136            },
137            (Ok(_resp), AvatarWrite::RetainAvatar)
138            | (Ok(_resp), AvatarWrite::NoAvatar) => {
139                tracing::warn!(
140                    "No avatar supplied but got avatar upload URL. Ignoring"
141                );
142                Ok(None)
143            },
144        }
145    }
146}
147
148impl SignalWebSocket<websocket::Unidentified> {
149    pub async fn retrieve_profile_avatar(
150        &mut self,
151        path: &str,
152    ) -> Result<impl futures::io::AsyncRead + Send + Unpin, ServiceError> {
153        self.unidentified_push_service.get_from_cdn(0, path).await
154    }
155
156    pub async fn retrieve_groups_v2_profile_avatar(
157        &mut self,
158        path: &str,
159    ) -> Result<impl futures::io::AsyncRead + Send + Unpin, ServiceError> {
160        self.unidentified_push_service.get_from_cdn(0, path).await
161    }
162}