mirror of
https://github.com/chatmail/core.git
synced 2026-04-22 16:06:30 +03:00
with this PR, when an `.xdc` with `request_integration = map` in the manifest is added to the "Saved Messages" chat, it is used _locally_ as an replacement for the shipped maps.xdc (other devices will see the `.xdc` but not use it) this allows easy development and adapting the map to use services that work better in some area. there are lots of known discussions and ideas about adding more barriers of safety. however, after internal discussions, we decided to move forward and also to allow internet, if requested by an integration (as discussed at https://github.com/deltachat/deltachat-core-rust/pull/3516). the gist is to ease development and to make users who want to adapt, actionable _now_, without making things too hard and adding too high barriers or stressing our own resources/power too much. note, that things are still experimental and will be the next time - without the corresponding switch being enabled, nothing will work at all, so we can be quite relaxed here :) for android/ios, things will work directly. for desktop, allow_internet needs to be accepted unconditionally from core. for the future, we might add a question before using an integration and/or add signing. or sth. completely different - but for now, the thing is to get started. nb: "integration" field in the webxdc-info is experimental as well and should not be used in UIs at all currently, it may vanish again and is there mainly for simplicity of the code; therefore, no need to document that. successor of https://github.com/deltachat/deltachat-core-rust/pull/5461 this is how it looks like currently - again, please note that all that is an experiment! <img width=320 src=https://github.com/deltachat/deltachat-core-rust/assets/9800740/f659c891-f46a-4e28-9d0a-b6783d69be8d> <img width=320 src=https://github.com/deltachat/deltachat-core-rust/assets/9800740/54549b3c-a894-4568-9e27-d5f1caea2d22> ... when going out of experimental, there are loots of ideas, eg. changing "Start" to "integrate"
277 lines
10 KiB
Rust
277 lines
10 KiB
Rust
//! # Maps Webxdc Integration.
|
|
//!
|
|
//! A Maps Webxdc Integration uses `sendUpdate()` and `setUpdateListener()` as usual,
|
|
//! however, it agrees with the core on the following update format:
|
|
//!
|
|
//! ## Setting POIs via `sendUpdate()`
|
|
//!
|
|
//! ```json
|
|
//! payload: {
|
|
//! action: "pos",
|
|
//! lat: 53.550556,
|
|
//! lng: 9.993333,
|
|
//! label: "my poi"
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! Just sent POI are received via `setUpdateListener()`, as well as old POI.
|
|
//!
|
|
//! ## Receiving Locations via `setUpdateListener()`
|
|
//!
|
|
//! ```json
|
|
//! payload: {
|
|
//! action: "pos",
|
|
//! lat: 47.994828,
|
|
//! lng: 7.849881,
|
|
//! timestamp: 1712928222,
|
|
//! contactId: 123, // can be used as a unique ID to differ tracks etc
|
|
//! name: "Alice",
|
|
//! color: "#ff8080",
|
|
//! independent: false, // false: current or past position of contact, true: a POI
|
|
//! label: "" // used for POI only
|
|
//! }
|
|
//! ```
|
|
|
|
use crate::{chat, location};
|
|
use std::collections::{hash_map, HashMap};
|
|
|
|
use crate::context::Context;
|
|
use crate::message::{Message, MsgId};
|
|
|
|
use crate::chat::ChatId;
|
|
use crate::color::color_int_to_hex_string;
|
|
use crate::contact::{Contact, ContactId};
|
|
use crate::tools::time;
|
|
use crate::webxdc::{StatusUpdateItem, StatusUpdateItemAndSerial, StatusUpdateSerial};
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct MapsActionPayload {
|
|
action: String,
|
|
lat: Option<f64>,
|
|
lng: Option<f64>,
|
|
label: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct LocationItem {
|
|
action: String,
|
|
#[serde(rename = "contactId")]
|
|
contact_id: u32,
|
|
lat: f64,
|
|
lng: f64,
|
|
independent: bool,
|
|
timestamp: i64,
|
|
label: String,
|
|
name: String,
|
|
color: String,
|
|
}
|
|
|
|
pub(crate) async fn intercept_send_update(
|
|
context: &Context,
|
|
chat_id: Option<ChatId>,
|
|
status_update: StatusUpdateItem,
|
|
) -> Result<()> {
|
|
let payload = serde_json::from_value::<MapsActionPayload>(status_update.payload)?;
|
|
let lat = payload.lat.unwrap_or_default();
|
|
let lng = payload.lng.unwrap_or_default();
|
|
let label = payload.label.unwrap_or_default();
|
|
|
|
if payload.action == "pos" && !label.is_empty() {
|
|
let chat_id = if let Some(chat_id) = chat_id {
|
|
chat_id
|
|
} else {
|
|
ChatId::create_for_contact(context, ContactId::SELF).await?
|
|
};
|
|
|
|
let mut poi_msg = Message::new_text(label);
|
|
poi_msg.set_location(lat, lng);
|
|
chat::send_msg(context, chat_id, &mut poi_msg).await?;
|
|
} else {
|
|
warn!(context, "unknown maps integration action");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn intercept_get_updates(
|
|
context: &Context,
|
|
chat_id: Option<ChatId>,
|
|
last_known_serial: StatusUpdateSerial,
|
|
) -> Result<String> {
|
|
let mut json = String::default();
|
|
let mut contact_data: HashMap<ContactId, (String, String)> = HashMap::new();
|
|
|
|
let begin = time() - 24 * 60 * 60;
|
|
let locations = location::get_range(context, chat_id, None, begin, 0).await?;
|
|
for location in locations.iter().rev() {
|
|
if location.location_id > last_known_serial.to_u32() {
|
|
let (name, color) = match contact_data.entry(location.contact_id) {
|
|
hash_map::Entry::Vacant(e) => {
|
|
let contact = Contact::get_by_id(context, location.contact_id).await?;
|
|
let name = contact.get_display_name().to_string();
|
|
let color = color_int_to_hex_string(contact.get_color());
|
|
e.insert((name, color)).clone()
|
|
}
|
|
hash_map::Entry::Occupied(e) => e.get().clone(),
|
|
};
|
|
|
|
let mut label = String::new();
|
|
if location.independent != 0 {
|
|
if let Some(marker) = &location.marker {
|
|
label = marker.to_string() // marker contains one-char labels only
|
|
} else if location.msg_id != 0 {
|
|
if let Some(msg) =
|
|
Message::load_from_db_optional(context, MsgId::new(location.msg_id)).await?
|
|
{
|
|
label = msg.get_text()
|
|
}
|
|
}
|
|
}
|
|
|
|
let location_item = LocationItem {
|
|
action: "pos".to_string(),
|
|
contact_id: location.contact_id.to_u32(),
|
|
lat: location.latitude,
|
|
lng: location.longitude,
|
|
independent: location.independent != 0,
|
|
timestamp: location.timestamp,
|
|
label,
|
|
name,
|
|
color,
|
|
};
|
|
|
|
let update_item = StatusUpdateItemAndSerial {
|
|
item: StatusUpdateItem {
|
|
payload: serde_json::to_value(location_item)?,
|
|
info: None,
|
|
href: None,
|
|
document: None,
|
|
summary: None,
|
|
uid: None,
|
|
notify: None,
|
|
},
|
|
serial: StatusUpdateSerial(location.location_id),
|
|
max_serial: StatusUpdateSerial(location.location_id),
|
|
};
|
|
|
|
if !json.is_empty() {
|
|
json.push_str(",\n");
|
|
}
|
|
json.push_str(&serde_json::to_string(&update_item)?);
|
|
}
|
|
}
|
|
|
|
Ok(format!("[{json}]"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::chat::{create_group_chat, ChatId, ProtectionStatus};
|
|
use crate::chatlist::Chatlist;
|
|
use crate::contact::Contact;
|
|
use crate::message::Message;
|
|
use crate::test_utils::TestContext;
|
|
use crate::webxdc::StatusUpdateSerial;
|
|
use crate::{location, EventType};
|
|
use anyhow::Result;
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_maps_integration() -> Result<()> {
|
|
let t = TestContext::new_alice().await;
|
|
|
|
let bytes = include_bytes!("../../test-data/webxdc/mapstest-integration-set.xdc");
|
|
let file = t.get_blobdir().join("maps.xdc");
|
|
tokio::fs::write(&file, bytes).await.unwrap();
|
|
t.set_webxdc_integration(file.to_str().unwrap()).await?;
|
|
|
|
let chatlist = Chatlist::try_load(&t, 0, None, None).await?;
|
|
let summary = chatlist.get_summary(&t, 0, None).await?;
|
|
assert_eq!(summary.text, "No messages.");
|
|
|
|
// Integrate Webxdc into a chat with Bob;
|
|
// sending updates is intercepted by integrations and results in setting a POI in core
|
|
let bob_id = Contact::create(&t, "", "bob@example.net").await?;
|
|
let bob_chat_id = ChatId::create_for_contact(&t, bob_id).await?;
|
|
let integration_id = t.init_webxdc_integration(Some(bob_chat_id)).await?.unwrap();
|
|
assert!(!integration_id.is_special());
|
|
|
|
let integration = Message::load_from_db(&t, integration_id).await?;
|
|
let info = integration.get_webxdc_info(&t).await?;
|
|
assert_eq!(info.name, "Maps Test 2");
|
|
assert_eq!(info.internet_access, true);
|
|
|
|
t.send_webxdc_status_update(
|
|
integration_id,
|
|
r#"{"payload": {"action": "pos", "lat": 11.0, "lng": 12.0, "label": "poi #1"}}"#,
|
|
)
|
|
.await?;
|
|
t.evtracker
|
|
.get_matching(|evt| matches!(evt, EventType::WebxdcStatusUpdate { .. }))
|
|
.await;
|
|
let updates = t
|
|
.get_webxdc_status_updates(integration_id, StatusUpdateSerial(0))
|
|
.await?;
|
|
assert!(updates.contains(r#""lat":11"#));
|
|
assert!(updates.contains(r#""lng":12"#));
|
|
assert!(updates.contains(r#""label":"poi #1""#));
|
|
assert!(updates.contains(r#""contactId":"#)); // checking for sth. that is not in the sent update make sure integration is called
|
|
assert!(updates.contains(r#""name":"Me""#));
|
|
assert!(updates.contains(r##""color":"#"##));
|
|
let locations = location::get_range(&t, Some(bob_chat_id), None, 0, 0).await?;
|
|
assert_eq!(locations.len(), 1);
|
|
let location = locations.last().unwrap();
|
|
assert_eq!(location.latitude, 11.0);
|
|
assert_eq!(location.longitude, 12.0);
|
|
assert_eq!(location.independent, 1);
|
|
let msg = t.get_last_msg().await;
|
|
assert_eq!(msg.text, "poi #1");
|
|
assert_eq!(msg.chat_id, bob_chat_id);
|
|
|
|
// Integrate Webxdc into another group
|
|
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
|
let integration_id = t.init_webxdc_integration(Some(group_id)).await?.unwrap();
|
|
|
|
let locations = location::get_range(&t, Some(group_id), None, 0, 0).await?;
|
|
assert_eq!(locations.len(), 0);
|
|
t.send_webxdc_status_update(
|
|
integration_id,
|
|
r#"{"payload": {"action": "pos", "lat": 22.0, "lng": 23.0, "label": "poi #2"}}"#,
|
|
)
|
|
.await?;
|
|
let updates = t
|
|
.get_webxdc_status_updates(integration_id, StatusUpdateSerial(0))
|
|
.await?;
|
|
assert!(!updates.contains(r#""lat":11"#));
|
|
assert!(!updates.contains(r#""label":"poi #1""#));
|
|
assert!(updates.contains(r#""lat":22"#));
|
|
assert!(updates.contains(r#""lng":23"#));
|
|
assert!(updates.contains(r#""label":"poi #2""#));
|
|
let locations = location::get_range(&t, Some(group_id), None, 0, 0).await?;
|
|
assert_eq!(locations.len(), 1);
|
|
let location = locations.last().unwrap();
|
|
assert_eq!(location.latitude, 22.0);
|
|
assert_eq!(location.longitude, 23.0);
|
|
assert_eq!(location.independent, 1);
|
|
let msg = t.get_last_msg().await;
|
|
assert_eq!(msg.text, "poi #2");
|
|
assert_eq!(msg.chat_id, group_id);
|
|
|
|
// In global map, both POI are visible
|
|
let integration_id = t.init_webxdc_integration(None).await?.unwrap();
|
|
|
|
let updates = t
|
|
.get_webxdc_status_updates(integration_id, StatusUpdateSerial(0))
|
|
.await?;
|
|
assert!(updates.contains(r#""lat":11"#));
|
|
assert!(updates.contains(r#""label":"poi #1""#));
|
|
assert!(updates.contains(r#""lat":22"#));
|
|
assert!(updates.contains(r#""label":"poi #2""#));
|
|
let locations = location::get_range(&t, None, None, 0, 0).await?;
|
|
assert_eq!(locations.len(), 2);
|
|
|
|
Ok(())
|
|
}
|
|
}
|