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(Clone, Debug, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct Badge {
20 #[serde(default)]
22 pub id: String,
23 #[serde(default)]
25 pub category: String,
26 #[serde(default)]
28 pub name: String,
29 #[serde(default)]
31 pub description: String,
32 #[serde(default)]
34 pub sprites6: Vec<String>,
35 #[serde(default)]
37 pub expiration: Option<f64>,
38 #[serde(default)]
40 pub visible: bool,
41 #[serde(default)]
43 pub duration: i64,
44}
45
46#[derive(Clone, Debug, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct SignalServiceProfile {
49 #[serde(default, with = "serde_optional_base64")]
50 pub identity_key: Option<Vec<u8>>,
51 #[serde(default, with = "serde_optional_base64")]
52 pub name: Option<Vec<u8>>,
53 #[serde(default, with = "serde_optional_base64")]
54 pub about: Option<Vec<u8>>,
55 #[serde(default, with = "serde_optional_base64")]
56 pub about_emoji: Option<Vec<u8>>,
57
58 pub avatar: Option<String>,
62 pub unidentified_access: Option<String>,
63
64 #[serde(default)]
65 pub unrestricted_unidentified_access: bool,
66
67 pub capabilities: DeviceCapabilities,
68
69 #[serde(default)]
71 pub badges: Vec<Badge>,
72}
73
74#[derive(Debug, Serialize)]
75#[serde(rename_all = "camelCase")]
76struct SignalServiceProfileWrite<'s> {
77 version: &'s str,
79 #[serde(with = "serde_base64")]
80 name: &'s [u8],
81 #[serde(with = "serde_base64")]
82 about: &'s [u8],
83 #[serde(with = "serde_base64")]
84 about_emoji: &'s [u8],
85 avatar: bool,
86 same_avatar: bool,
87 #[serde(with = "serde_base64")]
88 commitment: &'s [u8],
89}
90
91impl SignalWebSocket<websocket::Identified> {
92 pub async fn retrieve_profile_by_id(
93 &mut self,
94 address: Aci,
95 profile_key: Option<zkgroup::profiles::ProfileKey>,
96 ) -> Result<SignalServiceProfile, ServiceError> {
97 let path = if let Some(key) = profile_key {
98 let version =
99 bincode::serialize(&key.get_profile_key_version(address))?;
100 let version = std::str::from_utf8(&version)
101 .expect("hex encoded profile key version");
102 format!("/v1/profile/{}/{}", address.service_id_string(), version)
103 } else {
104 format!("/v1/profile/{}", address.service_id_string())
105 };
106 self.http_request(Method::GET, path)?
108 .send()
109 .await?
110 .service_error_for_status()
111 .await?
112 .json()
113 .await
114 }
115
116 pub async fn write_profile<'s, C, S>(
123 &mut self,
124 version: &ProfileKeyVersion,
125 name: &[u8],
126 about: &[u8],
127 emoji: &[u8],
128 commitment: &ProfileKeyCommitment,
129 avatar: AvatarWrite<&mut C>,
130 ) -> Result<Option<String>, ServiceError>
131 where
132 C: std::io::Read + Send + 's,
133 S: AsRef<str>,
134 {
135 let version = bincode::serialize(version)?;
137 let version = std::str::from_utf8(&version)
138 .expect("profile_key_version is hex encoded string");
139 let commitment = bincode::serialize(commitment)?;
140
141 let command = SignalServiceProfileWrite {
142 version,
143 name,
144 about,
145 about_emoji: emoji,
146 avatar: !matches!(avatar, AvatarWrite::NoAvatar),
147 same_avatar: matches!(avatar, AvatarWrite::RetainAvatar),
148 commitment: &commitment,
149 };
150
151 let upload_url: Result<String, _> = self
153 .http_request(Method::PUT, "/v1/profile")?
154 .send_json(&command)
155 .await?
156 .service_error_for_status()
157 .await?
158 .json()
159 .await;
160
161 match (upload_url, avatar) {
162 (_url, AvatarWrite::NewAvatar(_avatar)) => {
163 unreachable!("Uploading avatar unimplemented");
165 },
166 (Err(_), AvatarWrite::RetainAvatar)
170 | (Err(_), AvatarWrite::NoAvatar) => {
171 Ok(None)
173 },
174 (Ok(_resp), AvatarWrite::RetainAvatar)
175 | (Ok(_resp), AvatarWrite::NoAvatar) => {
176 tracing::warn!(
177 "No avatar supplied but got avatar upload URL. Ignoring"
178 );
179 Ok(None)
180 },
181 }
182 }
183}
184
185impl SignalWebSocket<websocket::Unidentified> {
186 pub async fn retrieve_profile_avatar(
187 &mut self,
188 path: &str,
189 ) -> Result<impl futures::io::AsyncRead + Send + Unpin, ServiceError> {
190 self.unidentified_push_service.get_from_cdn(0, path).await
191 }
192
193 pub async fn retrieve_groups_v2_profile_avatar(
194 &mut self,
195 path: &str,
196 ) -> Result<impl futures::io::AsyncRead + Send + Unpin, ServiceError> {
197 self.unidentified_push_service.get_from_cdn(0, path).await
198 }
199}