1use super::*;
2
3struct PreviewRoom {
4 name: String,
5 displays: BTreeMap<String, Option<String>>,
6 first: Vec<String>,
7 last: Option<String>,
8 count: usize,
9 valid: bool,
10}
11
12impl ProfileReader {
13 pub fn thread_pinned(&self, threads: &[String]) -> Result<BTreeMap<String, Option<bool>>> {
16 if threads.len() > 200 {
17 return Err(ProfileError::InvalidQuery);
18 }
19 if threads.is_empty() {
20 return Ok(BTreeMap::new());
21 }
22 for thread in threads {
23 validate_name(thread)?;
24 }
25 let c = self.snapshot.open(self.role("contact.db", "contacts")?)?;
26 #[cfg(test)]
27 super::batch_lookup_tests::record_batch_query();
28 let placeholders = vec!["?"; threads.len()].join(",");
29 let mut statement = c.prepare(&format!(
30 "SELECT username,flag,local_type,delete_flag,is_in_chat_room FROM contact WHERE username IN ({placeholders})"
31 ))?;
32 let rows = statement.query_map(rusqlite::params_from_iter(threads), raw)?;
33 let mut pinned = BTreeMap::new();
34 for row in rows {
35 let fields = row?;
36 let name = text(&fields, "username").ok_or(ProfileError::InvalidMetadata)?;
37 let flag = num(&fields, "flag").and_then(|flag| u32::try_from(flag).ok());
38 let ordinary = if name.ends_with("@chatroom") {
39 num(&fields, "local_type") == Some(2)
40 && num(&fields, "is_in_chat_room") == Some(1)
41 && flag.is_some_and(|flag| flag & 2 != 0)
42 } else {
43 !name.contains('@')
44 && name != "filehelper"
45 && !name.starts_with("weixin")
46 && !name.starts_with("gh_")
47 && ![
48 "brandsessionholder",
49 "brandservicesessionholder",
50 "qmessage",
51 "medianote",
52 "qqsync",
53 "lbsapp",
54 "shakeapp",
55 "floatbottle",
56 "masssendapp",
57 "appbrandcustomerservicemsg",
58 "service_officialaccounts",
59 "notifymessage",
60 "notification_messages",
61 ]
62 .contains(&name.as_str())
63 && num(&fields, "local_type") == Some(1)
64 && flag.is_some_and(|flag| flag & 1 != 0)
65 };
66 let value = (ordinary && num(&fields, "delete_flag") == Some(0))
67 .then(|| flag.map(|flag| flag & 0x800 != 0))
68 .flatten();
69 pinned.insert(name, value);
70 }
71 Ok(pinned)
72 }
73
74 pub fn thread_muted(&self, threads: &[String]) -> Result<BTreeMap<String, Option<bool>>> {
78 if threads.len() > 200 {
79 return Err(ProfileError::InvalidQuery);
80 }
81 if threads.is_empty() {
82 return Ok(BTreeMap::new());
83 }
84 for thread in threads {
85 validate_name(thread)?;
86 }
87 let c = self.snapshot.open(self.role("contact.db", "contacts")?)?;
88 #[cfg(test)]
89 super::batch_lookup_tests::record_batch_query();
90 let placeholders = vec!["?"; threads.len()].join(",");
91 let mut statement = c.prepare(&format!(
92 "SELECT username,flag,chat_room_notify,local_type,delete_flag FROM contact WHERE username IN ({placeholders})"
93 ))?;
94 let rows = statement.query_map(rusqlite::params_from_iter(threads), raw)?;
95 let mut muted = BTreeMap::new();
96 for row in rows {
97 let fields = row?;
98 let name = text(&fields, "username").ok_or(ProfileError::InvalidMetadata)?;
99 let value = if name.ends_with("@chatroom") {
100 match num(&fields, "chat_room_notify") {
101 Some(0) => Some(true),
102 Some(1) => Some(false),
103 _ => None,
104 }
105 } else if num(&fields, "local_type") == Some(1)
106 && num(&fields, "delete_flag") == Some(0)
107 {
108 num(&fields, "flag")
109 .and_then(|flag| u32::try_from(flag).ok())
110 .filter(|flag| flag & 1 != 0)
111 .map(|flag| flag & 0x200 != 0)
112 } else {
113 None
114 };
115 muted.insert(name, value);
116 }
117 Ok(muted)
118 }
119
120 pub fn group_avatar_members(
124 &self,
125 rooms: &[String],
126 ) -> Result<BTreeMap<String, Vec<GroupMember>>> {
127 if rooms.len() > 200 {
128 return Err(ProfileError::InvalidQuery);
129 }
130 if rooms.is_empty() {
131 return Ok(BTreeMap::new());
132 }
133 for room in rooms {
134 validate_name(room)?;
135 }
136 let c = self.snapshot.open(self.role("contact.db", "groups")?)?;
137 let placeholders = vec!["?"; rooms.len()].join(",");
138 let mut statement = c.prepare(&format!(
139 "SELECT id,username,ext_buffer FROM chat_room WHERE username IN ({placeholders})"
140 ))?;
141 let rows = statement.query_map(rusqlite::params_from_iter(rooms), |row| {
142 Ok((
143 row.get::<_, i64>(0)?,
144 row.get::<_, String>(1)?,
145 row.get::<_, Option<Vec<u8>>>(2)?,
146 ))
147 })?;
148 let mut selected = BTreeMap::new();
149 for row in rows {
150 let (id, name, bytes) = row?;
151 let Ok(displays) = room_displays(bytes.as_deref().unwrap_or_default()) else {
152 continue;
153 };
154 selected.insert(
155 id,
156 PreviewRoom {
157 name,
158 displays,
159 first: Vec::new(),
160 last: None,
161 count: 0,
162 valid: true,
163 },
164 );
165 }
166 if selected.is_empty() {
167 return Ok(BTreeMap::new());
168 }
169 let placeholders = vec!["?"; selected.len()].join(",");
170 let ids: Vec<_> = selected.keys().copied().collect();
171 let mut statement = c.prepare(&format!("SELECT m.room_id,c.username FROM chatroom_member m JOIN contact c ON c.id=m.member_id WHERE m.room_id IN ({placeholders}) ORDER BY m.room_id,c.username"))?;
172 let rows = statement.query_map(rusqlite::params_from_iter(ids), |row| {
173 Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
174 })?;
175 for row in rows {
176 let (id, username) = row?;
177 let room = selected.get_mut(&id).ok_or(ProfileError::InvalidMetadata)?;
178 if room.last.as_ref() == Some(&username) {
179 continue;
180 }
181 room.last = Some(username.clone());
182 room.count += 1;
183 if room.count > 100_000 {
184 return Err(ProfileError::ResourceLimit);
185 }
186 if !room.displays.is_empty() && !room.displays.contains_key(&username) {
187 room.valid = false
188 }
189 if room.first.len() < 4 {
190 room.first.push(username)
191 }
192 }
193 selected.retain(|_, room| {
194 room.valid && (room.displays.is_empty() || room.displays.len() == room.count)
195 });
196 let names: BTreeSet<_> = selected
197 .values()
198 .flat_map(|room| room.first.iter())
199 .collect();
200 let mut contacts = BTreeMap::new();
201 if !names.is_empty() {
202 let placeholders = vec!["?"; names.len()].join(",");
203 let mut statement = c.prepare(&format!(
204 "SELECT * FROM contact WHERE username IN ({placeholders})"
205 ))?;
206 let rows = statement.query_map(rusqlite::params_from_iter(names), raw)?;
207 for row in rows {
208 let value = contact(row?)?;
209 contacts.insert(value.username.clone(), value);
210 }
211 }
212 Ok(selected
213 .into_values()
214 .map(|room| {
215 let members = room
216 .first
217 .into_iter()
218 .filter_map(|name| {
219 contacts.get(&name).cloned().map(|contact| {
220 group_member(contact, room.displays.get(&name).cloned().flatten())
221 })
222 })
223 .collect();
224 (room.name, members)
225 })
226 .collect())
227 }
228}