libsignal_service/websocket/
profile.rs1use 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 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 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 self.http_request(Method::GET, path)?
71 .send()
72 .await?
73 .service_error_for_status()
74 .await?
75 .json()
76 .await
77 }
78
79 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 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 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 unreachable!("Uploading avatar unimplemented");
128 },
129 (Err(_), AvatarWrite::RetainAvatar)
133 | (Err(_), AvatarWrite::NoAvatar) => {
134 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}