1use super::*;
2
3#[derive(Clone, Debug, Default, Serialize, Deserialize)]
4#[serde(rename_all = "camelCase", deny_unknown_fields)]
5pub struct BusinessContactProjection {
6 pub service_type: i32,
7 pub accept_type: i32,
8 pub child_type: i32,
9 pub brand_flag: u32,
10 pub home_url: String,
11 pub external_info: String,
12 pub brand_info: String,
13 pub brand_icon_url: String,
14 pub brand_list: String,
15 pub belong: String,
16}
17
18pub fn apply_contact_business_patch(
21 location: &ProfileLocation,
22 keys: &KeyFile,
23 input: &ContactProjection,
24 business: &BusinessContactProjection,
25 options: &WriteOptions,
26) -> Result<AppliedContact> {
27 ownership(location, keys, options)?;
28 identity(&input.username)?;
29 if input.username.contains('@')
30 || input.fields.contains_key("id")
31 || input.fields.contains_key("username")
32 {
33 return Err(ProfileError::InvalidMetadata);
34 }
35 let database = find_database(location, "contact.db")?;
36 let mut connection = open_write(location, &database, keys, options.allow_plaintext)?;
37 for table in ["name2id", "contact", "biz_info"] {
38 check_known_table(&connection, table)?;
39 }
40 let transaction = connection.transaction()?;
41 let id = map_id(&transaction, "name2id", "username", &input.username)?;
42 let existing = checked_identity(&transaction, "contact", id, &input.username)?;
43 checked_identity(&transaction, "biz_info", id, &input.username)?;
44 let previous = if existing {
45 transaction.query_row(
46 "SELECT local_type,verify_flag,extra_buffer FROM contact WHERE id=?1",
47 [id],
48 |row| {
49 Ok((
50 row.get::<_, Option<i64>>(0)?,
51 row.get::<_, Option<i64>>(1)?,
52 row.get::<_, Option<Vec<u8>>>(2)?,
53 ))
54 },
55 )?
56 } else {
57 (None, None, None)
58 };
59 let effective_integer = |column: &str, previous: Option<i64>| -> Result<Option<i64>> {
60 input.fields.get(column).map_or(Ok(previous), |value| {
61 if let SqlValue::Integer(value) = value {
62 value
63 .parse()
64 .map(Some)
65 .map_err(|_| ProfileError::InvalidMetadata)
66 } else {
67 Err(ProfileError::InvalidMetadata)
68 }
69 })
70 };
71 if !matches!(effective_integer("local_type", previous.0)?, Some(0..=4))
72 || effective_integer("verify_flag", previous.1)?
73 .and_then(|value| u32::try_from(value).ok())
74 .is_none_or(|value| value & 8 == 0)
75 {
76 return Err(ProfileError::InvalidMetadata);
77 }
78 let extra = match input.fields.get("extra_buffer") {
79 None => previous.2.unwrap_or_default(),
80 Some(SqlValue::Null) => Vec::new(),
81 Some(SqlValue::Blob(value)) => STANDARD
82 .decode(value)
83 .map_err(|_| ProfileError::InvalidMetadata)?,
84 _ => return Err(ProfileError::InvalidMetadata),
85 };
86 let extra = replace_service_type(&extra, business.service_type)?;
87 let mut contact_fields = if existing {
88 RawFields::new()
89 } else {
90 nullable_fields(&transaction, &database, "contact", &["id", "username"])?
91 };
92 contact_fields.extend(input.fields.clone());
93 contact_fields.insert("username".into(), SqlValue::Text(input.username.clone()));
94 contact_fields.insert(
95 "extra_buffer".into(),
96 SqlValue::Blob(STANDARD.encode(extra)),
97 );
98 let contact_changed = apply_row(
99 &transaction,
100 "contact",
101 "id",
102 SqlValue::Integer(id.to_string()),
103 &contact_fields,
104 true,
105 )?;
106 let business_fields = business_fields(&input.username, business);
107 let business_changed = apply_row(
108 &transaction,
109 "biz_info",
110 "id",
111 SqlValue::Integer(id.to_string()),
112 &business_fields,
113 true,
114 )?;
115 transaction.commit()?;
116 Ok(AppliedContact {
117 local_id: id,
118 changed: contact_changed || business_changed,
119 })
120}
121
122fn checked_identity(connection: &Connection, table: &str, id: i64, username: &str) -> Result<bool> {
123 let mut statement = connection.prepare(&format!(
124 "SELECT id,username FROM {} WHERE id=?1 OR username=?2 LIMIT 3",
125 quote_identifier(table)
126 ))?;
127 let rows = statement
128 .query_map(params![id, username], |row| {
129 Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
130 })?
131 .collect::<std::result::Result<Vec<_>, _>>()?;
132 match rows.as_slice() {
133 [] => Ok(false),
134 [(stored_id, stored_name)] if *stored_id == id && stored_name == username => Ok(true),
135 _ => Err(ProfileError::InvalidMetadata),
136 }
137}
138
139fn replace_service_type(extra: &[u8], service_type: i32) -> Result<Vec<u8>> {
140 let mut output = Vec::new();
141 for chunk in proto_chunks(extra)? {
142 if chunk.field == 13 {
143 if chunk.wire != 0 {
144 return Err(ProfileError::InvalidMetadata);
145 }
146 } else {
147 output.extend_from_slice(chunk.raw);
148 }
149 }
150 var_out(13 << 3, &mut output);
151 var_out(service_type as i64 as u64, &mut output);
152 if output.len() > 1024 * 1024 {
153 return Err(ProfileError::ResourceLimit);
154 }
155 Ok(output)
156}
157
158fn business_fields(username: &str, business: &BusinessContactProjection) -> RawFields {
159 let mut fields = RawFields::new();
160 fields.insert("username".into(), SqlValue::Text(username.to_owned()));
161 for (name, value) in [
162 ("type", i64::from(business.service_type)),
163 ("accept_type", i64::from(business.accept_type)),
164 ("child_type", i64::from(business.child_type)),
165 ("brand_flag", i64::from(business.brand_flag)),
166 ] {
167 fields.insert(name.into(), SqlValue::Integer(value.to_string()));
168 }
169 for (name, value) in [
170 ("home_url", &business.home_url),
171 ("external_info", &business.external_info),
172 ("brand_info", &business.brand_info),
173 ("brand_icon_url", &business.brand_icon_url),
174 ("brand_list", &business.brand_list),
175 ("belong", &business.belong),
176 ] {
177 fields.insert(name.into(), SqlValue::Text(value.clone()));
178 }
179 fields.insert("version".into(), SqlValue::Integer("0".into()));
180 fields.insert("sync_version".into(), SqlValue::Text(String::new()));
181 fields.insert("ext_buffer".into(), SqlValue::Blob(STANDARD.encode([])));
182 fields
183}
184
185#[cfg(test)]
186#[path = "business_projection/tests.rs"]
187mod tests;