mirror of
https://github.com/chatmail/core.git
synced 2026-04-26 18:06:35 +03:00
[still broken]* A bit progress on:
- transforming the export format to json - async - cleanup cod (*broken state, just a commit to save progress)
This commit is contained in:
@@ -11,19 +11,15 @@ use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
use zip::write::FileOptions;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExportChatResult {
|
||||
html: String,
|
||||
chat_json: String,
|
||||
// locations_geo_json: String,
|
||||
referenced_blobs: Vec<String>,
|
||||
}
|
||||
|
||||
struct ContactInfo {
|
||||
name: String,
|
||||
initial: String,
|
||||
color: String,
|
||||
profile_img: Option<String>,
|
||||
}
|
||||
|
||||
pub fn pack_exported_chat(
|
||||
context: &Context,
|
||||
artifact: ExportChatResult,
|
||||
@@ -34,11 +30,8 @@ pub fn pack_exported_chat(
|
||||
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
zip.start_file("index.html", Default::default())?;
|
||||
zip.write_all(artifact.html.as_bytes())?;
|
||||
|
||||
zip.start_file("styles.css", Default::default())?;
|
||||
zip.write_all(include_bytes!("../assets/exported-chat.css"))?;
|
||||
zip.start_file("index.json", Default::default())?;
|
||||
zip.write_all(artifact.chat_json.as_bytes())?;
|
||||
|
||||
zip.add_directory("blobs/", Default::default())?;
|
||||
|
||||
@@ -60,14 +53,58 @@ pub fn pack_exported_chat(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
#[derive(Serialize)]
|
||||
struct ChatJSON {
|
||||
name: String,
|
||||
color: String,
|
||||
profile_img: Option<String>,
|
||||
contacts: HashMap<u32, ContactJSON>,
|
||||
messages: Vec<MessageJSON>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ContactJSON {
|
||||
name: String,
|
||||
email: String,
|
||||
color: String,
|
||||
profile_img: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FileReference {
|
||||
name: String,
|
||||
filesize: String, /* todo human readable file size*/
|
||||
extension: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
enum MessageJSON {
|
||||
Message {
|
||||
id: u32,
|
||||
author_id: u32, // from_id
|
||||
viewType: Viewtype,
|
||||
timestamp_sort: i64,
|
||||
timestamp_sent: i64,
|
||||
timestamp_rcvd: i64,
|
||||
text: Option<String>,
|
||||
attachment: Option<FileReference>,
|
||||
// location
|
||||
}, // Info Message?
|
||||
}
|
||||
|
||||
impl MessageJSON {
|
||||
pub fn from_message(message: Message, context: &Context) -> MessageJSON {}
|
||||
}
|
||||
|
||||
pub async fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
let mut blobs = Vec::new();
|
||||
let mut chat_author_ids = Vec::new();
|
||||
// get all messages
|
||||
let messages: Vec<std::result::Result<Message, Error>> =
|
||||
get_chat_msgs(context, chat_id, 0, None)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|msg_id| Message::load_from_db(context, msg_id))
|
||||
.map(async move |msg_id| Message::load_from_db(context, msg_id).await)
|
||||
.collect();
|
||||
// push all referenced blobs and populate contactid list
|
||||
for message in &messages {
|
||||
@@ -82,22 +119,22 @@ pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
}
|
||||
// deduplicate contact list and load the contacts
|
||||
chat_author_ids.dedup();
|
||||
// chache information about the authors
|
||||
let mut chat_authors: HashMap<u32, ContactInfo> = HashMap::new();
|
||||
// load information about the authors
|
||||
let mut chat_authors: HashMap<u32, ContactJSON> = HashMap::new();
|
||||
chat_authors.insert(
|
||||
0,
|
||||
ContactInfo {
|
||||
ContactJSON {
|
||||
name: "Err: Contact not found".to_owned(),
|
||||
initial: "#".to_owned(),
|
||||
email: "error@localhost".to_owned(),
|
||||
profile_img: None,
|
||||
color: "grey".to_owned(),
|
||||
},
|
||||
);
|
||||
for author_id in chat_author_ids {
|
||||
let contact = Contact::get_by_id(context, author_id);
|
||||
let contact = Contact::get_by_id(context, author_id).await;
|
||||
if let Ok(c) = contact {
|
||||
let profile_img_path: String;
|
||||
if let Some(path) = c.get_profile_image(context) {
|
||||
if let Some(path) = c.get_profile_image(context).await {
|
||||
profile_img_path = path
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
@@ -111,43 +148,22 @@ pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
}
|
||||
chat_authors.insert(
|
||||
author_id,
|
||||
ContactInfo {
|
||||
ContactJSON {
|
||||
name: c.get_display_name().to_owned(),
|
||||
initial: "#".to_owned(), // TODO
|
||||
email: c.get_addr().to_owned(),
|
||||
profile_img: match profile_img_path != "" {
|
||||
true => Some(profile_img_path),
|
||||
false => None,
|
||||
},
|
||||
color: "rgb(18, 126, 208)".to_owned(), // TODO
|
||||
color: format!("{:#}", c.get_color()), // TODO
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// run message_to_html for each message and generate the html that way
|
||||
let mut html_messages: Vec<String> = Vec::new();
|
||||
for message in messages {
|
||||
if let Ok(msg) = message {
|
||||
html_messages.push(message_to_html(&chat_authors, msg, context));
|
||||
} else {
|
||||
html_messages.push(format!(
|
||||
r#"<li>
|
||||
<div class='message error'>
|
||||
<div class="msg-container">
|
||||
<div class="msg-body">
|
||||
<div dir="auto" class="text">{:?}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>"#,
|
||||
message.unwrap_err()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// todo chat image, chat name and so on..
|
||||
let chat = Chat::load_from_db(context, chat_id).unwrap();
|
||||
let chat_avatar = match chat.get_profile_image(context) {
|
||||
// Load information about the chat
|
||||
let chat: Chat = Chat::load_from_db(context, chat_id).await.unwrap();
|
||||
let chat_avatar = match chat.get_profile_image(context).await {
|
||||
Some(img) => {
|
||||
let path = img
|
||||
.file_name()
|
||||
@@ -156,190 +172,166 @@ pub fn export_chat(context: &Context, chat_id: ChatId) -> ExportChatResult {
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
blobs.push(path.clone());
|
||||
format!("<img class=\"avatar\" src=\"blobs/{}\" />", path)
|
||||
Some(format!("blobs/{}", path))
|
||||
}
|
||||
None => format!(
|
||||
"<div class=\"avatar text-avatar\" style=\"background-color:#{:#}\">{}</div>",
|
||||
chat.get_color(context),
|
||||
chat.get_name().chars().next().unwrap()
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
// todo option to export locations as kml?
|
||||
|
||||
// todo export message infos and save them to txt files
|
||||
// (those can be linked from the messages, they are stored in msg_info/[msg-id].txt)
|
||||
let chat_json = ChatJSON {
|
||||
name: chat.get_name(),
|
||||
color: format!("{:#}", chat.get_color()),
|
||||
profile_img: chat_avatar,
|
||||
contacts: chat_authors,
|
||||
messages: vec![], //todo
|
||||
};
|
||||
|
||||
blobs.dedup();
|
||||
ExportChatResult {
|
||||
html: format!(
|
||||
"<html>\
|
||||
<head>\
|
||||
<title>{chat_name}</title>\
|
||||
<link rel=\"stylesheet\" href=\"styles.css\" type=\"text/css\">\
|
||||
</head>\
|
||||
<body>\
|
||||
<div class=\"header\">\
|
||||
{chat_avatar}\
|
||||
<div class=\"name\">{chat_name}</div>\
|
||||
</div>\
|
||||
<div class=\"message-list-and-composer__message-list\">\
|
||||
<div id=\"message-list\">\
|
||||
<ul>{messages}</ul>\
|
||||
</div>\
|
||||
</div>\
|
||||
</body>\
|
||||
</html>",
|
||||
chat_name = chat.get_name(),
|
||||
chat_avatar = chat_avatar,
|
||||
messages = html_messages.join("")
|
||||
),
|
||||
chat_json: serde_json::to_string(&chat_json).unwrap(),
|
||||
referenced_blobs: blobs,
|
||||
}
|
||||
}
|
||||
|
||||
fn message_to_html(
|
||||
author_cache: &HashMap<u32, ContactInfo>,
|
||||
message: Message,
|
||||
context: &Context,
|
||||
) -> String {
|
||||
let author: &ContactInfo = {
|
||||
if let Some(c) = author_cache.get(&message.get_from_id()) {
|
||||
c
|
||||
} else {
|
||||
author_cache.get(&0).unwrap()
|
||||
}
|
||||
};
|
||||
// fn message_to_html(
|
||||
// author_cache: &HashMap<u32, ContactInfo>,
|
||||
// message: Message,
|
||||
// context: &Context,
|
||||
// ) -> String {
|
||||
// let author: &ContactInfo = {
|
||||
// if let Some(c) = author_cache.get(&message.get_from_id()) {
|
||||
// c
|
||||
// } else {
|
||||
// author_cache.get(&0).unwrap()
|
||||
// }
|
||||
// };
|
||||
|
||||
let avatar: String = {
|
||||
if let Some(profile_img) = &author.profile_img {
|
||||
format!(
|
||||
"<div class=\"author-avatar\">\
|
||||
<img \
|
||||
alt=\"{author_name}\"\
|
||||
src=\"blobs/{author_avatar_src}\"\
|
||||
/>\
|
||||
</div>",
|
||||
author_name = author.name,
|
||||
author_avatar_src = profile_img
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"<div class=\"author-avatar default\" alt=\"{name}\">\
|
||||
<div class=\"label\" style=\"background-color: {color}\">\
|
||||
{initial}\
|
||||
</div>\
|
||||
</div>",
|
||||
name = author.name,
|
||||
initial = author.initial,
|
||||
color = author.color
|
||||
)
|
||||
}
|
||||
};
|
||||
// let avatar: String = {
|
||||
// if let Some(profile_img) = &author.profile_img {
|
||||
// format!(
|
||||
// "<div class=\"author-avatar\">\
|
||||
// <img \
|
||||
// alt=\"{author_name}\"\
|
||||
// src=\"blobs/{author_avatar_src}\"\
|
||||
// />\
|
||||
// </div>",
|
||||
// author_name = author.name,
|
||||
// author_avatar_src = profile_img
|
||||
// )
|
||||
// } else {
|
||||
// format!(
|
||||
// "<div class=\"author-avatar default\" alt=\"{name}\">\
|
||||
// <div class=\"label\" style=\"background-color: {color}\">\
|
||||
// {initial}\
|
||||
// </div>\
|
||||
// </div>",
|
||||
// name = author.name,
|
||||
// initial = author.initial,
|
||||
// color = author.color
|
||||
// )
|
||||
// }
|
||||
// };
|
||||
|
||||
// save and refernce message source code somehow?
|
||||
// // save and refernce message source code somehow?
|
||||
|
||||
let has_text = message.get_text().is_some() && !message.get_text().unwrap().is_empty();
|
||||
// let has_text = message.get_text().is_some() && !message.get_text().unwrap().is_empty();
|
||||
|
||||
let attachment = match message.get_file(context) {
|
||||
None => "".to_owned(),
|
||||
Some(file) => {
|
||||
let modifier_class = if has_text { "content-below" } else { "" };
|
||||
let filename = file
|
||||
.file_name()
|
||||
.unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
match message.get_viewtype() {
|
||||
Viewtype::Audio => {
|
||||
format!("<audio \
|
||||
controls \
|
||||
class=\"message-attachment-audio {}\"> \
|
||||
<source src=\"blobs/{}\" /> \
|
||||
</audio>", modifier_class ,filename)
|
||||
},
|
||||
Viewtype::Gif | Viewtype::Image | Viewtype::Sticker => {
|
||||
format!("<a \
|
||||
href=\"blobs/{filename}\" \
|
||||
role=\"button\" \
|
||||
class=\"message-attachment-media {modifier_class}\"> \
|
||||
<img className='attachment-content' src=\"blobs/{filename}\" /> \
|
||||
</a>", modifier_class=modifier_class, filename=filename)
|
||||
},
|
||||
Viewtype::Video => {
|
||||
format!("<a \
|
||||
href=\"blobs/{filename}\" \
|
||||
role=\"button\" \
|
||||
class=\"message-attachment-media {modifier_class}\"> \
|
||||
<video className='attachment-content' src=\"blobs/{filename}\" controls=\"true\" /> \
|
||||
</a>", modifier_class=modifier_class, filename=filename)
|
||||
},
|
||||
_ => {
|
||||
format!("<div class=\"message-attachment-generic {modifier_class}\">\
|
||||
<div class=\"file-icon\">\
|
||||
<div class=\"file-extension\">\
|
||||
{extension} \
|
||||
</div>\
|
||||
</div>\
|
||||
<div className=\"text-part\">\
|
||||
<a href=\"blobs/{filename}\" className=\"name\">{filename}</a>\
|
||||
<div className=\"size\">{filesize}</div>\
|
||||
</div>\
|
||||
</div>",
|
||||
modifier_class=modifier_class,
|
||||
filename=filename,
|
||||
filesize=message.get_filebytes(&context) /* todo human readable file size*/,
|
||||
extension=file.extension().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap().to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// let attachment = match message.get_file(context) {
|
||||
// None => "".to_owned(),
|
||||
// Some(file) => {
|
||||
// let modifier_class = if has_text { "content-below" } else { "" };
|
||||
// let filename = file
|
||||
// .file_name()
|
||||
// .unwrap_or_else(|| std::ffi::OsStr::new(""))
|
||||
// .to_str()
|
||||
// .unwrap()
|
||||
// .to_owned();
|
||||
// match message.get_viewtype() {
|
||||
// Viewtype::Audio => {
|
||||
// format!("<audio \
|
||||
// controls \
|
||||
// class=\"message-attachment-audio {}\"> \
|
||||
// <source src=\"blobs/{}\" /> \
|
||||
// </audio>", modifier_class ,filename)
|
||||
// },
|
||||
// Viewtype::Gif | Viewtype::Image | Viewtype::Sticker => {
|
||||
// format!("<a \
|
||||
// href=\"blobs/{filename}\" \
|
||||
// role=\"button\" \
|
||||
// class=\"message-attachment-media {modifier_class}\"> \
|
||||
// <img className='attachment-content' src=\"blobs/{filename}\" /> \
|
||||
// </a>", modifier_class=modifier_class, filename=filename)
|
||||
// },
|
||||
// Viewtype::Video => {
|
||||
// format!("<a \
|
||||
// href=\"blobs/{filename}\" \
|
||||
// role=\"button\" \
|
||||
// class=\"message-attachment-media {modifier_class}\"> \
|
||||
// <video className='attachment-content' src=\"blobs/{filename}\" controls=\"true\" /> \
|
||||
// </a>", modifier_class=modifier_class, filename=filename)
|
||||
// },
|
||||
// _ => {
|
||||
// format!("<div class=\"message-attachment-generic {modifier_class}\">\
|
||||
// <div class=\"file-icon\">\
|
||||
// <div class=\"file-extension\">\
|
||||
// {extension} \
|
||||
// </div>\
|
||||
// </div>\
|
||||
// <div className=\"text-part\">\
|
||||
// <a href=\"blobs/{filename}\" className=\"name\">{filename}</a>\
|
||||
// <div className=\"size\">{filesize}</div>\
|
||||
// </div>\
|
||||
// </div>",
|
||||
// modifier_class=modifier_class,
|
||||
// filename=filename,
|
||||
// filesize=message.get_filebytes(&context) /* todo human readable file size*/,
|
||||
// extension=file.extension().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap().to_owned())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
format!(
|
||||
"<li>\
|
||||
<div class=\"message {direction}\">\
|
||||
{avatar}\
|
||||
<div class=\"msg-container\">\
|
||||
<span class=\"author\" style=\"color: {author_color};\">{author_name}</span>\
|
||||
<div class=\"msg-body\">\
|
||||
{attachment}
|
||||
<div dir=\"auto\" class=\"text\">\
|
||||
{content}\
|
||||
</div>\
|
||||
<div class=\"metadata {with_image_no_caption}\">\
|
||||
{encryption}\
|
||||
<span class=\"date date--{direction}\" title=\"{full_time}\">{relative_time}</span>\
|
||||
<span class=\"spacer\"></span>\
|
||||
</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
<div>\
|
||||
</li>",
|
||||
direction = match message.from_id == DC_CONTACT_ID_SELF {
|
||||
true => "outgoing",
|
||||
false => "incoming",
|
||||
},
|
||||
avatar = avatar,
|
||||
author_name = author.name,
|
||||
author_color = author.color,
|
||||
attachment = attachment,
|
||||
content = message.get_text().unwrap_or_else(|| "".to_owned()),
|
||||
with_image_no_caption = if !has_text && message.get_viewtype() == Viewtype::Image {
|
||||
"with-image-no-caption"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
encryption = match message.get_showpadlock() {
|
||||
true => r#"<div aria-label="Encryption padlock" class="padlock-icon"></div>"#,
|
||||
false => "",
|
||||
},
|
||||
full_time = "Tue, Feb 25, 2020 3:49 PM", // message.get_timestamp() ? // todo
|
||||
relative_time = "Tue 3:49 PM" // todo
|
||||
)
|
||||
// format!(
|
||||
// "<li>\
|
||||
// <div class=\"message {direction}\">\
|
||||
// {avatar}\
|
||||
// <div class=\"msg-container\">\
|
||||
// <span class=\"author\" style=\"color: {author_color};\">{author_name}</span>\
|
||||
// <div class=\"msg-body\">\
|
||||
// {attachment}
|
||||
// <div dir=\"auto\" class=\"text\">\
|
||||
// {content}\
|
||||
// </div>\
|
||||
// <div class=\"metadata {with_image_no_caption}\">\
|
||||
// {encryption}\
|
||||
// <span class=\"date date--{direction}\" title=\"{full_time}\">{relative_time}</span>\
|
||||
// <span class=\"spacer\"></span>\
|
||||
// </div>\
|
||||
// </div>\
|
||||
// </div>\
|
||||
// <div>\
|
||||
// </li>",
|
||||
// direction = match message.from_id == DC_CONTACT_ID_SELF {
|
||||
// true => "outgoing",
|
||||
// false => "incoming",
|
||||
// },
|
||||
// avatar = avatar,
|
||||
// author_name = author.name,
|
||||
// author_color = author.color,
|
||||
// attachment = attachment,
|
||||
// content = message.get_text().unwrap_or_else(|| "".to_owned()),
|
||||
// with_image_no_caption = if !has_text && message.get_viewtype() == Viewtype::Image {
|
||||
// "with-image-no-caption"
|
||||
// } else {
|
||||
// ""
|
||||
// },
|
||||
// encryption = match message.get_showpadlock() {
|
||||
// true => r#"<div aria-label="Encryption padlock" class="padlock-icon"></div>"#,
|
||||
// false => "",
|
||||
// },
|
||||
// full_time = "Tue, Feb 25, 2020 3:49 PM", // message.get_timestamp() ? // todo
|
||||
// relative_time = "Tue 3:49 PM" // todo
|
||||
// )
|
||||
|
||||
// todo link to raw message data
|
||||
// todo link to message info
|
||||
}
|
||||
|
||||
//TODO tests
|
||||
// // todo link to raw message data
|
||||
// // todo link to message info
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user