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