1pub mod sync;
11
12pub use sync::{SyncPriority, ProfileSyncHandler, NoOpProfileSyncHandler};
13
14use nostr_sdk::prelude::Metadata;
15
16use crate::compact::NO_NPUB;
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub struct ProfileFlags(u8);
24
25impl ProfileFlags {
26 const MINE: u8 = 0b001;
27 const BLOCKED: u8 = 0b010;
28 const BOT: u8 = 0b100;
29
30 #[inline] pub fn is_mine(self) -> bool { self.0 & Self::MINE != 0 }
31 #[inline] pub fn is_blocked(self) -> bool { self.0 & Self::BLOCKED != 0 }
32 #[inline] pub fn is_bot(self) -> bool { self.0 & Self::BOT != 0 }
33
34 #[inline] pub fn set_mine(&mut self, v: bool) { if v { self.0 |= Self::MINE } else { self.0 &= !Self::MINE } }
35 #[inline] pub fn set_blocked(&mut self, v: bool) { if v { self.0 |= Self::BLOCKED } else { self.0 &= !Self::BLOCKED } }
36 #[inline] pub fn set_bot(&mut self, v: bool) { if v { self.0 |= Self::BOT } else { self.0 &= !Self::BOT } }
37}
38
39#[derive(Clone, Debug, PartialEq)]
46pub struct Profile {
47 pub id: u16,
48 pub name: Box<str>,
49 pub display_name: Box<str>,
50 pub nickname: Box<str>,
51 pub lud06: Box<str>,
52 pub lud16: Box<str>,
53 pub banner: Box<str>,
54 pub avatar: Box<str>,
55 pub about: Box<str>,
56 pub website: Box<str>,
57 pub nip05: Box<str>,
58 pub status_title: Box<str>,
59 pub status_purpose: Box<str>,
60 pub status_url: Box<str>,
61 pub last_updated: u32,
62 pub flags: ProfileFlags,
63 pub avatar_cached: Box<str>,
64 pub banner_cached: Box<str>,
65}
66
67impl Default for Profile {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Profile {
74 pub fn new() -> Self {
75 Self {
76 id: NO_NPUB,
77 name: Box::<str>::default(),
78 display_name: Box::<str>::default(),
79 nickname: Box::<str>::default(),
80 lud06: Box::<str>::default(),
81 lud16: Box::<str>::default(),
82 banner: Box::<str>::default(),
83 avatar: Box::<str>::default(),
84 about: Box::<str>::default(),
85 website: Box::<str>::default(),
86 nip05: Box::<str>::default(),
87 status_title: Box::<str>::default(),
88 status_purpose: Box::<str>::default(),
89 status_url: Box::<str>::default(),
90 last_updated: 0,
91 flags: ProfileFlags::default(),
92 avatar_cached: Box::<str>::default(),
93 banner_cached: Box::<str>::default(),
94 }
95 }
96
97 pub fn from_metadata(&mut self, meta: Metadata) -> bool {
99 let mut changed = false;
100
101 if let Some(name) = meta.name {
102 if *self.name != *name { self.name = name.into_boxed_str(); changed = true; }
103 }
104 if let Some(name) = meta.display_name {
105 if *self.display_name != *name { self.display_name = name.into_boxed_str(); changed = true; }
106 }
107 if let Some(lud06) = meta.lud06 {
108 if *self.lud06 != *lud06 { self.lud06 = lud06.into_boxed_str(); changed = true; }
109 }
110 if let Some(lud16) = meta.lud16 {
111 if *self.lud16 != *lud16 { self.lud16 = lud16.into_boxed_str(); changed = true; }
112 }
113 if let Some(banner) = meta.banner {
114 if *self.banner != *banner {
115 self.banner = banner.into_boxed_str();
116 self.banner_cached = Box::<str>::default();
117 changed = true;
118 }
119 }
120 if let Some(picture) = meta.picture {
121 if *self.avatar != *picture {
122 self.avatar = picture.into_boxed_str();
123 self.avatar_cached = Box::<str>::default();
124 changed = true;
125 }
126 }
127 if let Some(about) = meta.about {
128 if *self.about != *about { self.about = about.into_boxed_str(); changed = true; }
129 }
130 if let Some(website) = meta.website {
131 if *self.website != *website { self.website = website.into_boxed_str(); changed = true; }
132 }
133 if let Some(nip05) = meta.nip05 {
134 if *self.nip05 != *nip05 { self.nip05 = nip05.into_boxed_str(); changed = true; }
135 }
136 if let Some(custom) = meta.custom.get("bot") {
137 let bot_value = match custom.as_bool() {
138 Some(b) => b,
139 None => custom.as_str().map(|s| s.to_lowercase() == "true").unwrap_or(false),
140 };
141 if self.flags.is_bot() != bot_value {
142 self.flags.set_bot(bot_value);
143 changed = true;
144 }
145 }
146
147 changed
148 }
149}
150
151#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Default)]
160pub struct SlimProfile {
161 pub id: String,
162 pub name: String,
163 pub display_name: String,
164 pub nickname: String,
165 pub lud06: String,
166 pub lud16: String,
167 pub banner: String,
168 pub avatar: String,
169 pub about: String,
170 pub website: String,
171 pub nip05: String,
172 pub status: Status,
173 pub last_updated: u64,
174 pub mine: bool,
175 pub bot: bool,
176 pub is_blocked: bool,
177 pub avatar_cached: String,
178 pub banner_cached: String,
179}
180
181impl SlimProfile {
182 pub fn from_profile(profile: &Profile, interner: &crate::compact::NpubInterner) -> Self {
184 Self {
185 id: interner.resolve(profile.id).unwrap_or("").to_string(),
186 name: profile.name.to_string(),
187 display_name: profile.display_name.to_string(),
188 nickname: profile.nickname.to_string(),
189 lud06: profile.lud06.to_string(),
190 lud16: profile.lud16.to_string(),
191 banner: profile.banner.to_string(),
192 avatar: profile.avatar.to_string(),
193 about: profile.about.to_string(),
194 website: profile.website.to_string(),
195 nip05: profile.nip05.to_string(),
196 status: Status {
197 title: profile.status_title.to_string(),
198 purpose: profile.status_purpose.to_string(),
199 url: profile.status_url.to_string(),
200 },
201 last_updated: crate::compact::secs_from_compact(profile.last_updated),
202 mine: profile.flags.is_mine(),
203 bot: profile.flags.is_bot(),
204 is_blocked: profile.flags.is_blocked(),
205 avatar_cached: profile.avatar_cached.to_string(),
206 banner_cached: profile.banner_cached.to_string(),
207 }
208 }
209
210 pub fn to_profile(&self) -> Profile {
212 Profile {
213 id: NO_NPUB,
214 name: self.name.clone().into_boxed_str(),
215 display_name: self.display_name.clone().into_boxed_str(),
216 nickname: self.nickname.clone().into_boxed_str(),
217 lud06: self.lud06.clone().into_boxed_str(),
218 lud16: self.lud16.clone().into_boxed_str(),
219 banner: self.banner.clone().into_boxed_str(),
220 avatar: self.avatar.clone().into_boxed_str(),
221 about: self.about.clone().into_boxed_str(),
222 website: self.website.clone().into_boxed_str(),
223 nip05: self.nip05.clone().into_boxed_str(),
224 status_title: self.status.title.clone().into_boxed_str(),
225 status_purpose: self.status.purpose.clone().into_boxed_str(),
226 status_url: self.status.url.clone().into_boxed_str(),
227 last_updated: crate::compact::secs_to_compact(self.last_updated),
228 flags: {
229 let mut f = ProfileFlags::default();
230 f.set_mine(self.mine);
231 f.set_bot(self.bot);
232 f.set_blocked(self.is_blocked);
233 f
234 },
235 avatar_cached: self.avatar_cached.clone().into_boxed_str(),
236 banner_cached: self.banner_cached.clone().into_boxed_str(),
237 }
238 }
239}
240
241#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq)]
242pub struct Status {
243 pub title: String,
244 pub purpose: String,
245 pub url: String,
246}
247
248impl Status {
249 pub fn new() -> Self {
250 Self { title: String::new(), purpose: String::new(), url: String::new() }
251 }
252}
253
254impl Default for Status {
255 fn default() -> Self {
256 Self::new()
257 }
258}