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 && contact_not_deleted(&fields))
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) && contact_not_deleted(&fields) {
106 num(&fields, "flag")
107 .and_then(|flag| u32::try_from(flag).ok())
108 .filter(|flag| flag & 1 != 0)
109 .map(|flag| flag & 0x200 != 0)
110 } else {
111 None
112 };
113 muted.insert(name, value);
114 }
115 Ok(muted)
116 }
117
118 pub fn group_avatar_members(
122 &self,
123 rooms: &[String],
124 ) -> Result<BTreeMap<String, Vec<GroupMember>>> {
125 if rooms.len() > 200 {
126 return Err(ProfileError::InvalidQuery);
127 }
128 if rooms.is_empty() {
129 return Ok(BTreeMap::new());
130 }
131 for room in rooms {
132 validate_name(room)?;
133 }
134 let c = self.snapshot.open(self.role("contact.db", "groups")?)?;
135 let placeholders = vec!["?"; rooms.len()].join(",");
136 let mut statement = c.prepare(&format!(
137 "SELECT id,username,ext_buffer FROM chat_room WHERE username IN ({placeholders})"
138 ))?;
139 let rows = statement.query_map(rusqlite::params_from_iter(rooms), |row| {
140 Ok((
141 row.get::<_, i64>(0)?,
142 row.get::<_, String>(1)?,
143 row.get::<_, Option<Vec<u8>>>(2)?,
144 ))
145 })?;
146 let mut selected = BTreeMap::new();
147 for row in rows {
148 let (id, name, bytes) = row?;
149 let Ok(displays) = room_displays(bytes.as_deref().unwrap_or_default()) else {
150 continue;
151 };
152 selected.insert(
153 id,
154 PreviewRoom {
155 name,
156 displays,
157 first: Vec::new(),
158 last: None,
159 count: 0,
160 valid: true,
161 },
162 );
163 }
164 if selected.is_empty() {
165 return Ok(BTreeMap::new());
166 }
167 let placeholders = vec!["?"; selected.len()].join(",");
168 let ids: Vec<_> = selected.keys().copied().collect();
169 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"))?;
170 let rows = statement.query_map(rusqlite::params_from_iter(ids), |row| {
171 Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
172 })?;
173 for row in rows {
174 let (id, username) = row?;
175 let room = selected.get_mut(&id).ok_or(ProfileError::InvalidMetadata)?;
176 if room.last.as_ref() == Some(&username) {
177 continue;
178 }
179 room.last = Some(username.clone());
180 room.count += 1;
181 if room.count > 100_000 {
182 return Err(ProfileError::ResourceLimit);
183 }
184 if !room.displays.is_empty() && !room.displays.contains_key(&username) {
185 room.valid = false
186 }
187 if room.first.len() < 4 {
188 room.first.push(username)
189 }
190 }
191 selected.retain(|_, room| {
192 room.valid && (room.displays.is_empty() || room.displays.len() == room.count)
193 });
194 let names: BTreeSet<_> = selected
195 .values()
196 .flat_map(|room| room.first.iter())
197 .collect();
198 let mut contacts = BTreeMap::new();
199 if !names.is_empty() {
200 let placeholders = vec!["?"; names.len()].join(",");
201 let mut statement = c.prepare(&format!(
202 "SELECT * FROM contact WHERE username IN ({placeholders})"
203 ))?;
204 let rows = statement.query_map(rusqlite::params_from_iter(names), raw)?;
205 for row in rows {
206 let value = contact(row?)?;
207 contacts.insert(value.username.clone(), value);
208 }
209 }
210 Ok(selected
211 .into_values()
212 .map(|room| {
213 let members = room
214 .first
215 .into_iter()
216 .filter_map(|name| {
217 contacts.get(&name).cloned().map(|contact| {
218 group_member(contact, room.displays.get(&name).cloned().flatten())
219 })
220 })
221 .collect();
222 (room.name, members)
223 })
224 .collect())
225 }
226}