1use serde::Deserialize;
4
5use crate::{
6 AppDataContext, AppDataSyncView, ApplicationDataView, AttachmentMetadata, ChatMessageDraft,
7 ChatMessageView, ChatRelationInput, Contact, DeletionView, EditView, FollowListView,
8 HttpAuthorizationPlan, LocalIdentityHandle, NostrEventId, NostrEventKind, NostrPublicKey,
9 NostrTag, ReactionDraft, ReactionView, SignedEvent, SignedNostrEvent, SoftchatError,
10 SubjectDraft, SubjectView, TypingView, UserMetadataView, create_nip98_authorization,
11 plan_nip42_authentication,
12};
13
14#[derive(Clone, Debug, Deserialize)]
16#[serde(rename_all = "camelCase", deny_unknown_fields)]
17#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
18pub struct ChatMessageInput {
19 pub created_at: i64,
21 pub participants: Vec<String>,
23 pub content: String,
25 pub relation: ChatRelationInput,
27 pub attachments: Vec<AttachmentMetadata>,
29 pub emoji_tags: Vec<Vec<String>>,
31 pub extension_tags: Vec<Vec<String>>,
33}
34
35#[derive(Clone, Debug, Deserialize)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
39pub struct SubjectInput {
40 pub created_at: i64,
42 pub participants: Vec<String>,
44 pub subject: String,
46 pub emoji_tags: Vec<Vec<String>>,
48 pub icon: Option<AttachmentMetadata>,
50 pub extension_tags: Vec<Vec<String>>,
52}
53
54#[derive(Clone, Debug, Deserialize)]
56#[serde(rename_all = "camelCase", deny_unknown_fields)]
57#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
58pub struct ReactionInput {
59 pub participants: Vec<String>,
61 pub parent_id: String,
63 pub parent_author: String,
65 pub parent_kind: i32,
67 pub reaction: String,
69 pub custom_emoji_url: String,
71 pub created_at: i64,
73}
74
75#[derive(Clone, Debug, Deserialize)]
77#[serde(rename_all = "camelCase", deny_unknown_fields)]
78#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
79pub struct UserMetadataInput {
80 pub created_at: i64,
82 pub name: Option<String>,
84 pub display_name: Option<String>,
86 pub about: Option<String>,
88 pub picture: Option<String>,
90 pub website: Option<String>,
92 pub banner: Option<String>,
94 pub bot: Option<bool>,
96 pub unknown_json: String,
98}
99
100#[cfg_attr(feature = "native-bindings", uniffi::export)]
101impl LocalIdentityHandle {
102 pub fn create_chat_message(
108 &self,
109 input: ChatMessageInput,
110 ) -> Result<ChatMessageView, SoftchatError> {
111 let draft = ChatMessageDraft {
112 created_at: timestamp(input.created_at, SoftchatError::SoftchatEventCreationFailed)?,
113 participants: public_keys(
114 input.participants,
115 SoftchatError::SoftchatEventCreationFailed,
116 )?,
117 content: input.content,
118 relation: input.relation.into_relation()?,
119 attachments: input.attachments,
120 emoji_tags: tags(input.emoji_tags, SoftchatError::SoftchatEventCreationFailed)?,
121 extension_tags: tags(
122 input.extension_tags,
123 SoftchatError::SoftchatEventCreationFailed,
124 )?,
125 };
126 self.with_identity(|identity| identity.create_chat_message(draft))
127 }
128
129 pub fn create_subject(&self, input: SubjectInput) -> Result<SubjectView, SoftchatError> {
135 let draft = SubjectDraft {
136 created_at: timestamp(input.created_at, SoftchatError::SoftchatEventCreationFailed)?,
137 participants: public_keys(
138 input.participants,
139 SoftchatError::SoftchatEventCreationFailed,
140 )?,
141 subject: input.subject,
142 emoji_tags: tags(input.emoji_tags, SoftchatError::SoftchatEventCreationFailed)?,
143 icon: input.icon,
144 extension_tags: tags(
145 input.extension_tags,
146 SoftchatError::SoftchatEventCreationFailed,
147 )?,
148 };
149 self.with_identity(|identity| identity.create_subject(draft))
150 }
151
152 pub fn create_reaction(&self, input: ReactionInput) -> Result<ReactionView, SoftchatError> {
158 let error = SoftchatError::SoftchatEventCreationFailed;
159 let draft = ReactionDraft {
160 participants: public_keys(input.participants, error)?,
161 parent_id: NostrEventId::from_hex(&input.parent_id)
162 .map_err(|_| SoftchatError::SoftchatEventCreationFailed)?,
163 parent_author: NostrPublicKey::from_hex(&input.parent_author)
164 .map_err(|_| SoftchatError::SoftchatEventCreationFailed)?,
165 parent_kind: u32::try_from(input.parent_kind)
166 .map_err(|_| SoftchatError::SoftchatEventCreationFailed)
167 .and_then(NostrEventKind::try_from_u32)
168 .map_err(|_| SoftchatError::SoftchatEventCreationFailed)?,
169 reaction: input.reaction,
170 custom_emoji_url: (!input.custom_emoji_url.is_empty())
171 .then_some(input.custom_emoji_url),
172 created_at: timestamp(input.created_at, SoftchatError::SoftchatEventCreationFailed)?,
173 };
174 self.with_identity(|identity| identity.create_reaction(draft))
175 }
176
177 pub fn create_deletion(
183 &self,
184 participants: Vec<String>,
185 event_ids: Vec<String>,
186 reason: String,
187 created_at: i64,
188 ) -> Result<DeletionView, SoftchatError> {
189 let participants = public_keys(participants, SoftchatError::SoftchatEventCreationFailed)?;
190 let event_ids = event_ids
191 .iter()
192 .map(|value| {
193 NostrEventId::from_hex(value)
194 .map_err(|_| SoftchatError::SoftchatEventCreationFailed)
195 })
196 .collect::<Result<Vec<_>, _>>()?;
197 let created_at = timestamp(created_at, SoftchatError::SoftchatEventCreationFailed)?;
198 self.with_identity(|identity| {
199 identity.create_deletion(participants, event_ids, reason, created_at)
200 })
201 }
202
203 pub fn create_edit(
209 &self,
210 participants: Vec<String>,
211 original_event_id: String,
212 content: String,
213 emoji_tags: Vec<Vec<String>>,
214 created_at: i64,
215 ) -> Result<EditView, SoftchatError> {
216 let participants = public_keys(participants, SoftchatError::SoftchatEventCreationFailed)?;
217 let original_event_id = NostrEventId::from_hex(&original_event_id)
218 .map_err(|_| SoftchatError::SoftchatEventCreationFailed)?;
219 let emoji_tags = tags(emoji_tags, SoftchatError::SoftchatEventCreationFailed)?;
220 let created_at = timestamp(created_at, SoftchatError::SoftchatEventCreationFailed)?;
221 self.with_identity(|identity| {
222 identity.create_edit(
223 participants,
224 original_event_id,
225 content,
226 emoji_tags,
227 created_at,
228 )
229 })
230 }
231
232 pub fn create_typing(
238 &self,
239 participants: Vec<String>,
240 created_at: i64,
241 ) -> Result<TypingView, SoftchatError> {
242 let participants = public_keys(participants, SoftchatError::SoftchatEventCreationFailed)?;
243 let created_at = timestamp(created_at, SoftchatError::SoftchatEventCreationFailed)?;
244 self.with_identity(|identity| identity.create_typing(participants, created_at))
245 }
246
247 pub fn create_user_metadata(
253 &self,
254 input: UserMetadataInput,
255 ) -> Result<UserMetadataView, SoftchatError> {
256 let created_at = timestamp(input.created_at, SoftchatError::InvalidUserMetadata)?;
257 self.with_identity(|identity| {
258 identity.create_user_metadata(
259 created_at,
260 input.name,
261 input.display_name,
262 input.about,
263 input.picture,
264 input.website,
265 input.banner,
266 input.bot,
267 &input.unknown_json,
268 )
269 })
270 }
271
272 pub fn create_follow_list(
278 &self,
279 created_at: i64,
280 contacts: Vec<Contact>,
281 ) -> Result<FollowListView, SoftchatError> {
282 let created_at = timestamp(created_at, SoftchatError::InvalidContactList)?;
283 self.with_identity(|identity| identity.create_follow_list(created_at, contacts))
284 }
285
286 pub fn create_application_data(
292 &self,
293 created_at: i64,
294 identifier: String,
295 content: String,
296 ) -> Result<ApplicationDataView, SoftchatError> {
297 let created_at = timestamp(created_at, SoftchatError::InvalidApplicationData)?;
298 self.with_identity(|identity| {
299 identity.create_application_data(created_at, identifier, content)
300 })
301 }
302
303 pub fn create_app_data_sync(
309 &self,
310 created_at: i64,
311 context: AppDataContext,
312 json: String,
313 ) -> Result<AppDataSyncView, SoftchatError> {
314 let created_at = timestamp(created_at, SoftchatError::InvalidApplicationData)?;
315 self.with_identity(|identity| identity.create_app_data_sync(created_at, context, &json))
316 }
317
318 pub fn decrypt_app_data_sync(
324 &self,
325 event: SignedEvent,
326 ) -> Result<AppDataSyncView, SoftchatError> {
327 let event =
328 SignedNostrEvent::try_from(event).map_err(|_| SoftchatError::InvalidApplicationData)?;
329 self.with_identity(|identity| identity.decrypt_app_data_sync(&event))
330 }
331
332 pub fn create_nip42_authentication(
338 &self,
339 challenge: String,
340 relay_url: String,
341 created_at: i64,
342 ) -> Result<SignedEvent, SoftchatError> {
343 let created_at = timestamp(created_at, SoftchatError::InvalidRelayAuthentication)?;
344 self.with_identity(|identity| {
345 plan_nip42_authentication(identity, &challenge, &relay_url, created_at)
346 .map(|event| SignedEvent::from(&event))
347 })
348 }
349
350 pub fn create_nip98_authorization(
356 &self,
357 method: String,
358 url: String,
359 payload: Option<Vec<u8>>,
360 created_at: i64,
361 ) -> Result<HttpAuthorizationPlan, SoftchatError> {
362 let created_at = timestamp(created_at, SoftchatError::InvalidHttpAuthorization)?;
363 self.with_identity(|identity| {
364 create_nip98_authorization(identity, &method, &url, payload.as_deref(), created_at)
365 })
366 }
367}
368
369fn timestamp(value: i64, error: SoftchatError) -> Result<u64, SoftchatError> {
370 u64::try_from(value).map_err(|_| error)
371}
372
373fn public_keys(
374 values: Vec<String>,
375 error: SoftchatError,
376) -> Result<Vec<NostrPublicKey>, SoftchatError> {
377 values
378 .iter()
379 .map(|value| NostrPublicKey::from_hex(value).map_err(|_| error))
380 .collect()
381}
382
383fn tags(values: Vec<Vec<String>>, error: SoftchatError) -> Result<Vec<NostrTag>, SoftchatError> {
384 values
385 .into_iter()
386 .map(|value| NostrTag::new(value).map_err(|_| error))
387 .collect()
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 const ALICE: [u8; 32] = [
395 0x5c, 0x0c, 0x52, 0x3f, 0x52, 0xa5, 0xb6, 0xfa, 0xd3, 0x9e, 0xd2, 0x40, 0x30, 0x92, 0xdf,
396 0x8c, 0xeb, 0xc3, 0x63, 0x18, 0xb3, 0x93, 0x83, 0xbc, 0xa6, 0xc0, 0x08, 0x08, 0x62, 0x6f,
397 0xab, 0x3a,
398 ];
399 const BOB_PUBLIC: &str = "4ddeb9109a8cd29ba279a637f5ec344f2479ee07df1f4043f3fe26d8948cfef9";
400
401 #[test]
402 fn retained_identity_exposes_canonical_chat_and_request_plans() -> Result<(), SoftchatError> {
403 let identity = LocalIdentityHandle::new(ALICE.to_vec())?;
404 let message = identity.create_chat_message(ChatMessageInput {
405 created_at: 1,
406 participants: vec![BOB_PUBLIC.to_owned()],
407 content: "hello".to_owned(),
408 relation: ChatRelationInput {
409 kind: crate::ChatRelationKind::None,
410 event_id: String::new(),
411 relay_hint: String::new(),
412 original_author: String::new(),
413 },
414 attachments: Vec::new(),
415 emoji_tags: Vec::new(),
416 extension_tags: Vec::new(),
417 })?;
418 assert_eq!(message.rumor.kind, 14);
419 let auth = identity.create_nip42_authentication(
420 "challenge".to_owned(),
421 "wss://relay.example".to_owned(),
422 2,
423 )?;
424 assert_eq!(auth.kind, 22_242);
425 let http = identity.create_nip98_authorization(
426 "post".to_owned(),
427 "https://relay.example/upload".to_owned(),
428 Some(b"body".to_vec()),
429 3,
430 )?;
431 assert_eq!(http.method, "POST");
432 Ok(())
433 }
434}