Skip to main content

wechat_profile/
business_projection.rs

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
18/// Apply a received business contact and its ordinary extra metadata in one
19/// transaction. This performs no refresh or other account operation.
20pub 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;