Compare commits

...

9 Commits

Author SHA1 Message Date
29ea694b1e Start running delta connections. Lots of problems still 2021-10-31 21:17:42 +00:00
eec8669b88 Get delta contexts chugging 2021-10-31 16:28:46 +00:00
e99d824227 Keep hold of the Accounts struct 2021-10-31 01:31:02 +01:00
f57cdf3cdc Initialize Delta accounts on plugin load
We don't yet configure any of the accounts, but we're set up to manage
several accounts simultaneously in the same plugin instance, using
delta's built-in support for that.
2021-10-31 01:19:36 +01:00
039807bb32 Drop openssl fork, move to rust 2021 edition
The openssl-sys crate now natively supports openssl 3 on the master
branch, although there doesn't seem to be a release containing it yet.
2021-10-30 16:13:38 +01:00
fd321e02d8 Use OpenSSL 3 2021-09-09 18:18:31 +01:00
3142134360 Initial framework based off pidgin-icq 2021-04-15 00:41:54 +01:00
70b24f0e14 Add a makefile task to run a pidgin 2021-04-13 23:57:10 +01:00
77a257892a Add some more code from pidgin-icq 2021-04-13 23:57:10 +01:00
16 changed files with 2408 additions and 719 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/purple
/target

1181
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,23 +2,30 @@
name = "purple-plugin-delta"
version = "0.1.0"
authors = ["Nick Thomas <delta@ur.gs>"]
rust-version = "1.56"
edition = "2021"
[lib]
name = "delta"
path = "src/delta.rs"
name = "purple_delta"
path = "src/lib.rs"
crate-type = ["dylib"]
[dependencies]
libc = "*"
glib-sys = "*"
purple-sys = { git = "https://github.com/lupine/libpurple-rust", branch="with-flared" }
deltachat = { git = "https://github.com/deltachat/deltachat-core-rust", tag="v1.51.0" }
lazy_static = "1.4.0"
deltachat = { git = "https://github.com/deltachat/deltachat-core-rust", tag = "1.61.0" }
lazy_static = "*"
log = "*"
openssl = "*"
os_pipe = "*"
purple-rs = { git = "https://github.com/Flared/purple-rs", branch = "master" }
serde = "*"
[profile.dev]
debug = 0
## Keep in sync with deltachat-core-rust ##
[dependencies.async-std]
version = "1"
features = ["unstable"]
[profile.release]
lto = true
[patch.crates-io]
openssl-sys = { git = "https://github.com/sfackler/rust-openssl", branch = "master" }

7
Makefile Normal file
View File

@@ -0,0 +1,7 @@
run:
cargo build
rm -rf purple/plugins
mkdir -p purple/plugins
ln -s ../../target/debug/libpurple_delta.so purple/plugins/libpurple_delta.so
ldd purple/plugins/libpurple_delta.so
pidgin -d -c purple

View File

@@ -33,22 +33,15 @@ Starting again from scratch in Rust. So currently, nothing works. TODO list:
- [ ] Send/receive video messages
- [ ] Send/receive arbitrary attachments
## Build
There are some licensing issues at present, so you shouldn't build this plugin.
To get a `target/debug/libpurple_delta.so`, just run `cargo build`.
`deltachat-core-rust` uses a vendored openssl 1, unconditionally links it, and
is MPL-licensed.
`purple-plugin-delta` is GPLv3 without the [OpenSSL exemption](https://people.gnome.org/~markmc/openssl-and-the-gpl.html)
`libpurple` itself is GPLv2 without the OpenSSL exemption.
There's no point to `purple-plugin-delta` adding the OpenSSL exemption because
`libpurple` lacks it, and in any event, it will be unnecessary with the next
major version of OpenSSL. So, time should resolve this for us one way or another.
Since purple-plugin-delta is made to link against libpurple, which is GPLv2
without the "OpenSSL exemption", distributing something that linked against
OpenSSL 1 would be a licensing violation. Instead, we configure the build system
so we statically link against a vendored OpenSSL 3 instead. This has only been
possible since 2021-09-07.
Significant code using the WTFPL includes the [libpurple-rust bindings](https://github.com/sbwtw/libpurple-rust)
and the [pidgin-wechat plugin](https://github.com/sbwtw/pidgin-wechat), which
@@ -57,7 +50,7 @@ against this mess.
## Use
The easiest way to use this is to copy the `libdelta.so` file into
The easiest way to use this is to copy the `libpurple_delta.so` file into
`~/.purple/plugins`. When running pidgin, you'll now have the option to add
a "Delta Chat" account.

View File

@@ -1 +1 @@
1.50.0
1.56.0

136
src/chat_info.rs Normal file
View File

@@ -0,0 +1,136 @@
// This is a copy of https://github.com/Flared/purple-icq/blob/master/src/chat_info.rs
use super::purple;
use lazy_static::lazy_static;
use std::ffi::CString;
lazy_static! {
pub static ref SN: CString = CString::new("sn").unwrap();
pub static ref SN_NAME: CString = CString::new("Chat ID").unwrap();
pub static ref STAMP: CString = CString::new("stamp").unwrap();
pub static ref TITLE: CString = CString::new("title").unwrap();
pub static ref GROUP: CString = CString::new("group").unwrap();
pub static ref STATE: CString = CString::new("state").unwrap();
}
#[derive(Debug, Clone)]
pub struct MemberRole(String);
#[derive(Debug, Clone, Default)]
pub struct PartialChatInfo {
pub sn: String,
pub title: String,
pub group: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ChatInfo {
pub stamp: Option<String>,
pub group: Option<String>,
pub sn: String,
pub title: String,
pub about: Option<String>,
pub members_version: String,
pub info_version: String,
pub members: Vec<ChatMember>,
}
#[derive(Debug, Clone)]
pub struct ChatMember {
pub sn: String,
pub friendly_name: Option<String>,
pub role: MemberRole,
pub last_seen: Option<u64>,
pub first_name: Option<String>,
pub last_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ChatInfoVersion {
pub members_version: String,
pub info_version: String,
}
impl MemberRole {
pub fn as_flags(&self) -> purple::PurpleConvChatBuddyFlags {
match self.0.as_str() {
"admin" => purple::PurpleConvChatBuddyFlags::PURPLE_CBFLAGS_OP,
"readonly" => purple::PurpleConvChatBuddyFlags::PURPLE_CBFLAGS_NONE,
_ => purple::PurpleConvChatBuddyFlags::PURPLE_CBFLAGS_VOICE,
}
}
}
impl PartialChatInfo {
pub fn from_hashtable(table: &purple::StrHashTable) -> Option<Self> {
Some(Self {
group: table.lookup(&GROUP).map(Into::into),
sn: table.lookup(&SN)?.into(),
title: table.lookup(&TITLE)?.into(),
})
}
pub fn as_hashtable(&self) -> purple::StrHashTable {
let mut table = purple::StrHashTable::default();
table.insert(&SN, &self.sn);
if let Some(group) = &self.group {
table.insert(&GROUP, &group);
}
table.insert(&TITLE, &self.title);
table
}
}
impl ChatInfo {
pub fn as_partial(&self) -> PartialChatInfo {
PartialChatInfo {
sn: self.sn.clone(),
title: self.title.clone(),
group: self.group.clone(),
}
}
pub fn need_update(&self, new_version: &ChatInfoVersion) -> bool {
self.members_version < new_version.members_version
|| self.info_version < new_version.info_version
}
}
/*
impl From<icq::client::GetChatInfoResponseData> for ChatInfo {
fn from(info: icq::client::GetChatInfoResponseData) -> Self {
Self {
sn: info.sn,
stamp: Some(info.stamp),
title: info.name,
members_version: info.members_version,
info_version: info.info_version,
about: info.about,
members: info
.members
.into_iter()
.map(|m| ChatMember {
sn: m.sn,
role: MemberRole(m.role),
last_seen: m.user_state.last_seen.and_then(|t| match t {
0 => None,
t => Some(t),
}),
friendly_name: m.friendly,
first_name: m.anketa.first_name,
last_name: m.anketa.last_name,
})
.collect(),
..Default::default()
}
}
}
impl From<icq::client::events::HistDlgStateMChatState> for ChatInfoVersion {
fn from(info: icq::client::events::HistDlgStateMChatState) -> Self {
Self {
members_version: info.members_version,
info_version: info.info_version,
}
}
}
*/

View File

@@ -1,188 +0,0 @@
extern crate purple_sys;
extern crate glib_sys;
extern crate libc;
#[macro_use]
extern crate lazy_static;
/*
mod pointer;
mod server;
mod user;
mod chatroom;
mod message;
*/
use std::os::raw::{c_void, c_char};
use std::ptr::null_mut;
use std::boxed::Box;
use std::ffi::CString; // CStr
//use std::sync::RwLock;
use purple_sys::*;
use purple_sys::PurpleType;
//use purple_sys::PurpleConnectionState;
use purple_sys::PurpleStatusPrimitive;
//use purple_sys::PurpleRoomlistFieldType;
//use message::*;
//use pointer::Pointer;
//use server::ACCOUNT;
//use server::{send_im, send_chat, find_blist_chat, find_chat_token};
use glib_sys::{GHashTable, GList};
const TRUE: i32 = 1;
const FALSE: i32 = 0;
lazy_static!{
// static ref PLUGIN: RwLock<Pointer> = RwLock::new(Pointer::new());
static ref ICON_FILE: CString = CString::new("delta").unwrap();
// static ref DELTA_CATEGORY: CString = CString::new("Delta Chat").unwrap();
}
fn append_item(list: *mut GList, item: *mut c_void) -> *mut GList {
unsafe {
glib_sys::g_list_append(list as *mut glib_sys::GList, item as *mut libc::c_void) as
*mut GList
}
}
extern "C" fn list_icon(_: *mut PurpleAccount, _: *mut PurpleBuddy) -> *const c_char {
ICON_FILE.as_ptr()
}
extern "C" fn status_types(_: *mut PurpleAccount) -> *mut GList {
let mut list: *mut GList = null_mut();
let available = CString::new("available").unwrap();
let available_name = CString::new("Available").unwrap();
let offline = CString::new("offline").unwrap();
let offline_name = CString::new("Offline").unwrap();
let nick = CString::new("nick").unwrap();
let status = unsafe {
purple_status_type_new_with_attrs(
PurpleStatusPrimitive::PURPLE_STATUS_AVAILABLE,
available.as_ptr(),
available_name.as_ptr(),
TRUE,
TRUE,
FALSE,
nick.as_ptr(),
nick.as_ptr(),
purple_value_new(PurpleType::PURPLE_TYPE_STRING),
null_mut() as *mut c_void,
)
};
list = append_item(list, status as *mut c_void);
let status = unsafe {
purple_status_type_new_with_attrs(
PurpleStatusPrimitive::PURPLE_STATUS_OFFLINE,
offline.as_ptr(),
offline_name.as_ptr(),
TRUE,
TRUE,
FALSE,
nick.as_ptr(),
nick.as_ptr(),
purple_value_new(PurpleType::PURPLE_TYPE_STRING),
null_mut() as *mut c_void,
)
};
list = append_item(list, status as *mut c_void);
list
}
unsafe extern "C" fn login(account: *mut PurpleAccount) {
println!("account: {:?}", account);
}
extern "C" fn chat_info(_: *mut PurpleConnection) -> *mut GList {
let list: *mut GList = null_mut();
list
}
unsafe extern "C" fn join_chat(gc: *mut PurpleConnection, components: *mut GHashTable) {
println!("join_chat: {:?}, {:?}", gc, components);
}
extern "C" fn chat_info_defaults(_: *mut PurpleConnection, _: *const c_char) -> *mut GHashTable {
let table: *mut GHashTable = null_mut();
table
}
extern "C" fn close(_: *mut PurpleConnection) {}
extern "C" fn buddy_list(gc: *mut PurpleConnection) -> *mut PurpleRoomlist {
let buddies = unsafe { purple_roomlist_new(purple_connection_get_account(gc)) };
buddies
}
extern "C" fn callback(_plugin: *mut PurplePlugin) -> i32 {
TRUE
}
// extern "C" fn action_cb(_: *mut PurplePluginAction) {
//
// }
extern "C" fn actions(_: *mut PurplePlugin, _: *mut c_void) -> *mut GList {
let list: *mut GList = null_mut();
list
}
#[no_mangle]
pub extern "C" fn purple_init_plugin(plugin: *mut PurplePlugin) -> i32 {
// save plugin pointer
// PLUGIN.write().unwrap().set(plugin as *mut c_void);
let id = CString::new("prpl-delta").unwrap();
let name = CString::new("Delta Chat").unwrap();
let version = CString::new("0.1.0").unwrap();
let summary = CString::new("Delta Chat is an email-based instant messaging solution").unwrap();
let description = CString::new("See https://delta.chat for more information").unwrap();
let author = CString::new("Nick Thomas <delta@ur.gs>").unwrap();
let home_page = CString::new("https://delta.chat").unwrap();
let mut info = Box::new(PurplePluginInfo::new());
let mut extra_info = Box::new(PurplePluginProtocolInfo::new());
unsafe {
extra_info.list_icon = Some(list_icon);
extra_info.status_types = Some(status_types);
extra_info.login = Some(login);
extra_info.close = Some(close);
extra_info.roomlist_get_list = Some(buddy_list);
extra_info.chat_info = Some(chat_info);
extra_info.chat_info_defaults = Some(chat_info_defaults);
//extra_info.chat_send = Some(send_chat);
extra_info.join_chat = Some(join_chat);
//extra_info.find_blist_chat = Some(find_blist_chat);
//extra_info.send_im = Some(send_im);
info.id = id.into_raw();
info.name = name.into_raw();
info.version = version.into_raw();
info.summary = summary.into_raw();
info.description = description.into_raw();
info.author = author.into_raw();
info.homepage = home_page.into_raw();
info.load = Some(callback);
info.actions = Some(actions);
info.extra_info = Box::into_raw(extra_info) as *mut c_void;
(*plugin).info = Box::into_raw(info);
};
unsafe { purple_plugin_register(plugin) }
}

1
src/delta/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod system;

252
src/delta/system.rs Normal file
View File

@@ -0,0 +1,252 @@
// use super::poller;
// use super::protocol;
use crate::logging;
use crate::messages::{
AccountInfo, DeltaSystemHandle, FdSender, GetChatInfoMessage, GetHistoryMessage,
JoinChatMessage, PurpleMessage, SendMsgMessage, SystemMessage,
};
// use crate::{Handle, ChatInfo};
use async_std::channel::{self, Receiver};
use deltachat::accounts::Accounts;
const CHANNEL_CAPACITY: usize = 1024;
pub fn spawn() -> DeltaSystemHandle {
let (input_rx, input_tx) = os_pipe::pipe().unwrap();
let (system_tx, system_rx) = channel::bounded::<SystemMessage>(CHANNEL_CAPACITY);
let (purple_tx, purple_rx) = channel::bounded(CHANNEL_CAPACITY);
let fd_sender = FdSender::new(input_tx, system_tx);
log::debug!("Starting async thread.");
std::thread::spawn(move || run(fd_sender, purple_rx));
DeltaSystemHandle {
input_rx,
rx: system_rx,
tx: purple_tx,
}
}
pub fn run(tx: FdSender<SystemMessage>, rx: Receiver<PurpleMessage>) {
logging::set_thread_logger(logging::RemoteLogger(tx.clone()));
log::info!("Starting Delta system");
log::debug!("Performing delta accounts setup");
let mut config_dir = DeltaSystem::user_dir();
config_dir.push("purple-plugin-delta");
let accounts =
async_std::task::block_on(Accounts::new("purple-plugin-delta".into(), config_dir)).unwrap();
let mut system = DeltaSystem::new(tx, rx, accounts);
async_std::task::block_on(system.run());
}
pub struct DeltaSystem {
tx: FdSender<SystemMessage>,
rx: Receiver<PurpleMessage>,
accounts: Accounts,
}
impl DeltaSystem {
fn new(tx: FdSender<SystemMessage>, rx: Receiver<PurpleMessage>, accounts: Accounts) -> Self {
Self { tx, rx, accounts }
}
fn user_dir() -> async_std::path::PathBuf {
use std::os::unix::ffi::OsStrExt;
// SAFETY: We trust libpurple here
let slice = unsafe { std::ffi::CStr::from_ptr(crate::purple_sys::purple_user_dir()) };
let osstr = std::ffi::OsStr::from_bytes(slice.to_bytes());
let path: &async_std::path::Path = osstr.as_ref();
path.into()
}
async fn run(&mut self) {
log::info!("Looping on messages");
loop {
let purple_message = match self.rx.recv().await {
Ok(r) => r,
Err(error) => {
log::error!("Failed to receive message: {:?}", error);
break;
}
};
log::info!("Message: {:?}", purple_message);
let result = match purple_message {
PurpleMessage::Login(account_info) => self.login(account_info).await,
PurpleMessage::JoinChat(m) => self.join_chat(m).await,
PurpleMessage::SendMsg(m) => self.send_msg(m).await,
PurpleMessage::GetChatInfo(m) => self.get_chat_info(m).await,
PurpleMessage::GetHistory(m) => self.get_history(m).await,
};
if let Err(error) = result {
log::error!("Error handling message: {}", error);
}
logging::flush();
}
}
async fn login(&mut self, account_info: AccountInfo) -> std::result::Result<(), String> {
log::debug!("login");
let email_address = { account_info.protocol_data.email_address.clone() };
let password = { account_info.protocol_data.imap_pass.clone() };
let handle = &account_info.handle;
self.tx
.connection_proxy(&handle)
.set_state(purple::PurpleConnectionState::PURPLE_CONNECTING)
.await;
// TODO: make this properly async
let ctx = match self
.accounts
.get_all()
.await
.into_iter()
.map(|id| async_std::task::block_on(self.accounts.get_account(id)).unwrap())
.find(|ctx| {
async_std::task::block_on(ctx.is_self_addr(&email_address)).unwrap_or(false)
}) {
None => {
let id = self.accounts.add_account().await.unwrap();
self.accounts.get_account(id).await.unwrap()
}
Some(ctx) => ctx,
};
// Now transpose config into ctx. TODO: rest of the fields
ctx.set_config(deltachat::config::Config::Addr, Some(&email_address))
.await
.unwrap();
ctx.set_config(deltachat::config::Config::MailPw, Some(&password))
.await
.unwrap();
// FIXME: handle configuration failure nicely here. Right now we just panic.
ctx.configure().await.unwrap();
ctx.start_io().await;
async_std::task::spawn_local(DeltaSystem::deltachat_events(ctx));
// Hint from deleted code:
// self.tx.account_proxy(&handle).exec(|purple_account| ...);
Ok(())
}
async fn deltachat_events(ctx: deltachat::context::Context) {
// TODO: loop until we're out of events
let emitter = ctx.get_event_emitter();
while let Some(event) = emitter.recv().await {
println!("Received event {:?}", event);
}
// FIXME: this is back to front. We need to stop_io to interrupt the loop
ctx.stop_io().await;
}
async fn get_chat_info(&mut self, message: GetChatInfoMessage) -> Result<(), String> {
log::info!("Get chat info sn: {}", message.message_data.sn);
/*
let session = { message.protocol_data.session.read().await.clone().unwrap() };
let chat_info_response = protocol::get_chat_info_by_sn(&session, &message.message_data.sn)
.await
.map_err(|e| format!("Failed to get chat info: {:?}", e))?;
self.tx
.handle_proxy(&message.handle)
.exec_no_return(move |plugin, protocol_data| {
let chat_info = ChatInfo::from(chat_info_response);
let connection = &mut protocol_data.connection;
plugin.load_chat_info(connection, &chat_info);
})
.await;
*/
Ok(())
}
async fn join_chat(&mut self, message: JoinChatMessage) -> Result<(), String> {
log::info!("Joining stamp: {}", message.message_data.stamp);
/*
let session = { message.protocol_data.session.read().await.clone().unwrap() };
let stamp = message.message_data.stamp;
// Handle shareable URLs: https://icq.im/XXXXXXXXXXXXXX
let stamp = if stamp.contains("icq.im/") {
stamp.rsplit('/').next().unwrap().into()
} else {
stamp
};
protocol::join_chat(&session, &stamp)
.await
.map_err(|e| format!("Failed to join chat: {:?}", e))?;
let chat_info_response = protocol::get_chat_info(&session, &stamp)
.await
.map_err(|e| format!("Failed to get chat info: {:?}", e))?;
self.tx
.handle_proxy(&message.handle)
.exec_no_return(move |plugin, protocol_data| {
let chat_info = ChatInfo::from(chat_info_response);
let partial_info = chat_info.as_partial();
let connection = &mut protocol_data.connection;
plugin.chat_joined(connection, &partial_info);
plugin.conversation_joined(connection, &partial_info);
plugin.load_chat_info(connection, &chat_info);
})
.await;
*/
Ok(())
}
async fn get_history(&mut self, _get_history_message: GetHistoryMessage) -> Result<(), String> {
/*
let session = {
get_history_message
.protocol_data
.session
.read()
.await
.clone()
.unwrap()
};
let sn = &get_history_message.message_data.sn;
let from_msg_id = &get_history_message.message_data.from_msg_id;
let count = get_history_message.message_data.count;
let history = protocol::get_history(&session, sn, from_msg_id, count)
.await
.map_err(|e| format!("Failed to get history: {:?}", e))?;
super::poller::process_hist_dlg_state_messages(
self.tx.clone(),
session,
get_history_message.handle,
sn,
&history.persons,
None,
&history.messages,
)
.await;
*/
Ok(())
}
async fn send_msg(&mut self, message: SendMsgMessage) -> Result<(), String> {
log::info!("send_msg({:?})", message);
/*
let to_sn = &message.message_data.to_sn;
let message_body = &message.message_data.message;
let session = { message.protocol_data.session.read().await.clone().unwrap() };
let _msg_info = protocol::send_im(&session, to_sn, message_body)
.await
.map_err(|e| format!("Failed to send msg: {:?}", e))?;
*/
Ok(())
}
}

800
src/lib.rs Normal file
View File

@@ -0,0 +1,800 @@
extern crate async_std;
extern crate deltachat;
extern crate lazy_static;
extern crate log;
extern crate openssl;
extern crate purple_rs as purple;
use async_std::sync::Arc;
// use chat_info::ChatInfo; //PartialChatInfo, ChatInfoVersion
use lazy_static::lazy_static;
use messages::{AccountInfo, DeltaSystemHandle, PurpleMessage, SystemMessage};
use purple::*;
//use std::cell::RefCell;
use std::ffi::{CStr, CString};
//use std::io::Read;
//use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
pub mod chat_info;
pub mod delta;
pub mod logging;
pub mod messages;
pub mod status {
use lazy_static::lazy_static;
use std::ffi::CString;
lazy_static! {
pub static ref ONLINE_ID: CString = CString::new("online").unwrap();
pub static ref ONLINE_NAME: CString = CString::new("Online").unwrap();
pub static ref OFFLINE_ID: CString = CString::new("offline").unwrap();
pub static ref OFFLINE_NAME: CString = CString::new("Offline").unwrap();
}
}
lazy_static! {
static ref ICON_FILE: CString = CString::new("delta").unwrap();
}
mod blist_node {
pub const LAST_SEEN_TIMESTAMP: &str = "last_seen_timestamp";
}
mod commands {
pub const IMEX: &str = "imex";
}
pub mod chat_states {
pub const JOINED: &str = "joined";
}
pub mod conv_data {
use super::HistoryInfo;
use std::cell::RefCell;
use std::rc::Rc;
pub const CHAT_INFO: &str = "chat_info";
pub const HISTORY_INFO: &str = "history_info";
pub type HistoryInfoType = Rc<RefCell<HistoryInfo>>;
}
#[derive(Debug, Clone, Default)]
pub struct HistoryInfo {
pub oldest_message_id: Option<String>,
pub oldest_message_timestamp: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct MsgInfo {
pub chat_sn: String,
pub author_sn: String,
pub author_friendly: String,
pub text: String,
pub time: i64,
pub message_id: String,
}
#[derive(Debug, Default)]
pub struct AccountData {
email_address: String,
display_name: String,
imap_host: String,
imap_port: String,
imap_user: String,
imap_pass: String,
smtp_host: String,
smtp_port: String,
smtp_user: String,
smtp_pass: String,
bcc_self: bool,
// Not exposed: server_flags, selfstatus, e2ee_enabled
session_closed: AtomicBool,
}
impl Drop for AccountData {
fn drop(&mut self) {
log::info!("AccountData dropped");
}
}
pub type AccountDataBox = Arc<AccountData>;
pub type Handle = purple::Handle<AccountDataBox>;
pub type ProtocolData = purple::ProtocolData<AccountDataBox>;
pub struct PurpleDelta {
system: DeltaSystemHandle,
connections: purple::Connections<AccountDataBox>,
imex_command_handle: Option<PurpleCmdId>,
}
impl purple::PrplPlugin for PurpleDelta {
type Plugin = Self;
fn new() -> Self {
logging::init(log::LevelFilter::Debug).expect("Failed to initialize logging");
let system = delta::system::spawn();
Self {
system,
imex_command_handle: None,
connections: purple::Connections::new(),
}
}
fn register(&self, context: RegisterContext<Self>) -> RegisterContext<Self> {
println!("OpenSSL version: {}", openssl::version::version());
let info = purple::PrplInfo {
id: "prpl-delta".into(),
name: "Delta Chat".into(),
version: "0.1.0".into(),
summary: "Delta Chat is an email-based instant messaging solution".into(),
description: "See https://delta.chat for more information".into(),
author: "Nick Thomas <delta@ur.gs>".into(),
homepage: "https://code.ur.gs/lupine/purple-plugin-delta".into(),
};
context
.with_info(info)
.with_password()
.with_string_option("Display Name".into(), "displayname".into(), "".into())
.with_string_option("IMAP server host".into(), "mail_server".into(), "".into())
.with_string_option("IMAP server port".into(), "mail_port".into(), "".into())
.with_string_option("IMAP server username".into(), "mail_user".into(), "".into())
// Password is account password
.with_string_option("SMTP server host".into(), "send_server".into(), "".into())
.with_string_option("SMTP server port".into(), "send_port".into(), "".into())
.with_string_option("SMTP server username".into(), "send_user".into(), "".into())
.with_password_option("SMTP server password".into(), "send_pw".into(), "".into())
.with_bool_option("Copy messages to self".into(), "bcc_self".into(), false)
.enable_login()
.enable_load()
.enable_close()
//.enable_chat_info()
//.enable_chat_info_defaults()
//.enable_join_chat()
//.enable_chat_leave()
//.enable_send_im()
//.enable_chat_send()
//.enable_convo_closed()
//.enable_get_chat_name()
.enable_list_icon()
.enable_status_types()
}
}
impl purple::LoginHandler for PurpleDelta {
fn login(&mut self, account: &mut Account) {
let email_address = account.get_username().unwrap().into();
let display_name = account.get_string("displayname", "");
let imap_host = account.get_string("mail_server", "");
let imap_port = account.get_string("mail_port", "");
let imap_user = account.get_string("mail_user", "");
let imap_pass = account.get_password().unwrap().into();
let smtp_host = account.get_string("send_server", "");
let smtp_port = account.get_string("send_port", "");
let smtp_user = account.get_string("send_user", "");
let smtp_pass = account.get_string("send_pw", "");
let bcc_self = account.get_bool("bcc_self", false);
let protocol_data: AccountDataBox = Arc::new(AccountData {
email_address,
display_name,
imap_host,
imap_port,
imap_user,
imap_pass,
smtp_host,
smtp_port,
smtp_user,
smtp_pass,
bcc_self,
session_closed: AtomicBool::new(false),
});
// SAFETY:
// Safe as long as we remove the account in "close".
unsafe {
self.connections
.add(account.get_connection().unwrap(), protocol_data.clone())
};
self.system
.tx
.try_send(PurpleMessage::Login(AccountInfo {
handle: Handle::from(&mut *account),
protocol_data,
}))
.unwrap();
}
}
impl purple::CloseHandler for PurpleDelta {
fn close(&mut self, connection: &mut Connection) {
let handle = Handle::from(&mut *connection);
match self.connections.get(&handle) {
Some(protocol_data) => {
protocol_data
.data
.session_closed
.store(true, Ordering::Relaxed);
self.connections.remove(*connection);
}
None => {
log::error!("Tried closing a closed connection");
}
}
}
}
impl purple::StatusTypeHandler for PurpleDelta {
fn status_types(_account: &mut Account) -> Vec<StatusType> {
vec![
StatusType::new(
PurpleStatusPrimitive::PURPLE_STATUS_AVAILABLE,
Some(&status::ONLINE_ID),
Some(&status::ONLINE_NAME),
true,
),
StatusType::new(
PurpleStatusPrimitive::PURPLE_STATUS_OFFLINE,
Some(&status::OFFLINE_ID),
Some(&status::OFFLINE_NAME),
true,
),
]
}
}
impl purple::LoadHandler for PurpleDelta {
fn load(&mut self, _plugin: &purple::Plugin) -> bool {
logging::set_thread_logger(logging::PurpleDebugLogger);
self.imex_command_handle =
Some(self.enable_command(commands::IMEX, "w", "imex &lt;code&gt;"));
true
}
}
impl purple::ListIconHandler for PurpleDelta {
fn list_icon(_account: &mut Account) -> &'static CStr {
&ICON_FILE
}
}
/*
impl purple::ChatInfoHandler for PurpleDelta {
fn chat_info(&mut self, _connection: &mut Connection) -> Vec<purple::prpl::ChatEntry> {
vec![purple::prpl::ChatEntry {
label: &chat_info::SN_NAME,
identifier: &chat_info::SN,
required: true,
is_int: false,
min: 0,
max: 0,
secret: false,
}]
}
}
impl purple::ChatInfoDefaultsHandler for PurpleDelta {
fn chat_info_defaults(
&mut self,
_connection: &mut Connection,
chat_name: Option<&str>,
) -> purple::StrHashTable {
let mut defaults = purple::StrHashTable::default();
defaults.insert(chat_info::SN.as_c_str(), chat_name.unwrap_or(""));
defaults
}
}
impl purple::JoinChatHandler for PurpleDelta {
fn join_chat(&mut self, connection: &mut Connection, data: Option<&mut StrHashTable>) {
let data = match data {
Some(data) => data,
None => {
return;
}
};
let stamp = match Self::get_chat_name(Some(data)) {
Some(stamp) => stamp,
None => {
log::error!("No chat name provided");
return;
}
};
log::info!("Joining {}", stamp);
let handle = Handle::from(&mut *connection);
let protocol_data = self
.connections
.get(&handle)
.expect("Tried joining chat on closed connection");
if let Some(chat_states::JOINED) = data.lookup(&chat_info::STATE) {
match PartialChatInfo::from_hashtable(data) {
Some(chat_info) => {
self.conversation_joined(connection, &chat_info);
/*
self.system
.tx
.try_send(PurpleMessage::get_chat_info(
handle,
protocol_data.data.clone(),
chat_info.sn,
))
.unwrap(); */
return;
}
None => {
log::error!("Unable to load chat info");
}
}
}
/*
self.system
.tx
.try_send(PurpleMessage::join_chat(
handle,
protocol_data.data.clone(),
stamp,
))
.unwrap() */
}
}
impl purple::ChatLeaveHandler for PurpleDelta {
fn chat_leave(&mut self, connection: &mut Connection, id: i32) {
log::info!("Chat leave: {}", id);
match Conversation::find(connection, id) {
Some(mut conversation) => {
unsafe { conversation.remove_data::<ChatInfo>(conv_data::CHAT_INFO) };
}
None => {
log::warn!("Leaving chat without conversation");
}
}
}
}
impl purple::ConvoClosedHandler for PurpleDelta {
fn convo_closed(&mut self, _connection: &mut Connection, who: Option<&str>) {
log::info!("Convo closed: {:?}", who)
}
}
impl purple::GetChatNameHandler for PurpleDelta {
fn get_chat_name(data: Option<&mut purple::StrHashTable>) -> Option<String> {
data.and_then(|h| h.lookup(chat_info::SN.as_c_str()).map(Into::into))
}
}
impl purple::SendIMHandler for PurpleDelta {
fn send_im(
&mut self,
_connection: &mut Connection,
_who: &str,
_message: &str,
_flags: PurpleMessageFlags,
) -> i32 {
log::warn!("SendIM is not implemented");
-1
}
}
impl purple::ChatSendHandler for PurpleDelta {
fn chat_send(
&mut self,
connection: &mut Connection,
id: i32,
message: &str,
flags: PurpleMessageFlags,
) -> i32 {
log::info!("{}: {} [{:?}]", id, message, flags);
let mut conversation = match Conversation::find(connection, id) {
Some(c) => c,
None => {
log::error!("Conversation not found");
return -1;
}
};
let sn = match unsafe { conversation.get_data::<ChatInfo>(conv_data::CHAT_INFO) } {
Some(info) => info.sn.clone(),
None => {
log::error!("SN not found");
return -1;
}
};
let handle = Handle::from(&mut *connection);
let protocol_data = self.connections.get(&handle).expect("Connection closed");
/*
self.system
.tx
.try_send(PurpleMessage::send_msg(
handle,
protocol_data.data.clone(),
sn,
message.into(),
))
.unwrap(); */
1
}
}
*/
impl purple::CommandHandler for PurpleDelta {
fn command(
&mut self,
conversation: &mut Conversation,
command: &str,
args: &[&str],
) -> PurpleCmdRet {
log::debug!(
"command: conv={} cmd={} args={:?}",
conversation.get_title().unwrap_or("unknown"),
command,
args
);
match command {
commands::IMEX => self.command_imex(conversation, args),
_ => {
log::error!("Unknown command: {}", command);
PurpleCmdRet::PURPLE_CMD_RET_FAILED
}
}
}
}
impl PurpleDelta {
fn command_imex(&mut self, _conversation: &mut Conversation, args: &[&str]) -> PurpleCmdRet {
log::debug!("command_imex");
if args.len() != 1 {
log::error!(
"command_imex: Unsupported number of args. Got {}",
args.len()
);
return PurpleCmdRet::PURPLE_CMD_RET_FAILED;
}
/*
let count = {
let input = match args[0].parse::<u32>() {
Ok(count) => count,
Err(_) => {
log::error!("command_history: Could not parse count: {}", args[0]);
return PurpleCmdRet::PURPLE_CMD_RET_FAILED;
}
};
0 - input as i32
};
let sn = match conversation.get_name() {
Some(name) => name.to_string(),
None => {
log::error!("command_history: SN not found");
return PurpleCmdRet::PURPLE_CMD_RET_FAILED;
}
};
let from_msg_id = {
match unsafe {
conversation.get_data::<conv_data::HistoryInfoType>(conv_data::HISTORY_INFO)
} {
Some(history_info) => {
let history_info = history_info.borrow_mut();
match &history_info.oldest_message_id {
Some(oldest_message_id) => oldest_message_id.clone(),
None => {
return PurpleCmdRet::PURPLE_CMD_RET_FAILED;
}
}
}
None => {
log::error!("command_history: Can't find message id");
return PurpleCmdRet::PURPLE_CMD_RET_FAILED;
}
}
};
let handle = Handle::from(&mut conversation.get_connection());
let protocol_data = self
.connections
.get(&handle)
.expect("Tried joining chat on closed connection");
self.system
.tx
.try_send(PurpleMessage::fetch_history(
handle,
protocol_data.data.clone(),
sn,
from_msg_id,
count,
))
.unwrap();
*/
PurpleCmdRet::PURPLE_CMD_RET_OK
}
fn process_message(&mut self, message: SystemMessage) {
log::info!("received system message");
match message {
SystemMessage::ExecAccount {
handle: _,
function: _,
} => {
/*
self.connections
.get(handle)
.map(|protocol_data| function(&mut protocol_data.account))
.or_else(|| {
log::warn!("The account connection has been closed");
None
});
*/
}
SystemMessage::ExecConnection {
handle: _,
function: _,
} => {
/*
self.connections
.get(handle)
.map(|protocol_data| function(&mut protocol_data.connection))
.or_else(|| {
log::warn!("The account connection has been closed");
None
});
*/
}
SystemMessage::ExecHandle {
handle: _,
function: _,
} => {
/*
self.connections
.get(handle)
.map(|mut protocol_data| function(self, &mut protocol_data))
.or_else(|| {
log::warn!("The account connection has been closed");
None
});
*/
}
SystemMessage::FlushLogs => logging::flush(),
}
}
/*
pub fn serv_got_chat_in(&mut self, connection: &mut Connection, msg_info: MsgInfo) {
match purple::Chat::find(&mut connection.get_account(), &msg_info.chat_sn) {
Some(mut chat) => {
// Get the chat and the last seen timestamp.
let mut node = chat.as_blist_node();
let last_timestamp: i64 = node
.get_string(&blist_node::LAST_SEEN_TIMESTAMP)
.and_then(|t| t.parse::<i64>().ok())
.unwrap_or(0);
let new_timestamp = msg_info.time;
// Only trigger conversation_joined if this is a new message.
let conversation = {
if new_timestamp > last_timestamp {
node.set_string(
&blist_node::LAST_SEEN_TIMESTAMP,
&new_timestamp.to_string(),
);
Some(self.conversation_joined(
connection,
&PartialChatInfo {
sn: msg_info.chat_sn.clone(),
title: msg_info.chat_sn.clone(),
..Default::default()
},
))
} else {
None
}
};
// Get the conversation and set the oldest *displayed* messageId.
// This is the oldest message that the user can see in the chat window.
//
// If there is no conversation yet, that is okay. It means that we haven't
// seen new messages yet.
if let Some(mut conversation) = conversation {
let history_info = {
match unsafe {
conversation
.get_data::<conv_data::HistoryInfoType>(conv_data::HISTORY_INFO)
} {
Some(history_info) => history_info.clone(),
None => {
let history_info = Rc::new(RefCell::new(HistoryInfo {
oldest_message_id: None,
oldest_message_timestamp: None,
}));
unsafe {
conversation.set_data::<conv_data::HistoryInfoType>(
conv_data::HISTORY_INFO,
history_info.clone(),
)
};
history_info
}
}
};
let mut history_info = history_info.borrow_mut();
match history_info.oldest_message_timestamp {
None => {
history_info.oldest_message_id = Some(msg_info.message_id.clone());
history_info.oldest_message_timestamp = Some(msg_info.time);
}
Some(existing_timestamp) => {
if msg_info.time < existing_timestamp {
history_info.oldest_message_id = Some(msg_info.message_id.clone());
history_info.oldest_message_timestamp = Some(msg_info.time);
}
}
}
}
}
None => {
// Don't log errors for DMs because they are not yet supported.
// It happens all the time.
if msg_info.chat_sn.ends_with("@chat.agent") {
log::error!("Got message for unknown chat {}", msg_info.chat_sn);
}
}
}
connection.serv_got_chat_in(msg_info);
}
pub fn chat_joined(&mut self, connection: &mut Connection, info: &PartialChatInfo) {
log::info!("chat joined: {}", info.sn);
if info.sn.ends_with("@chat.agent") {
self.group_chat_joined(connection, info)
} else {
todo!()
};
}
fn group_chat_joined(
&mut self,
connection: &mut Connection,
info: &PartialChatInfo,
) -> purple::Chat {
let mut account = connection.get_account();
match purple::Chat::find(&mut account, &info.sn) {
Some(mut chat) => {
// The chat already exists.
// Should we replace the blist group?
if let Some(info_group) = &info.group {
let should_replace_group = {
match chat.get_group() {
Some(mut chat_group) => !chat_group.get_name().eq(info_group),
None => true,
}
};
if should_replace_group {
chat.add_to_blist(&mut self.get_or_create_group(Some(&info_group)), None);
}
}
// Replace the alias
chat.set_alias(&info.title);
chat
}
None => {
let mut components = info.as_hashtable();
components.insert(&chat_info::STATE, chat_states::JOINED);
let mut chat = purple::Chat::new(&mut account, &info.title, components);
chat.add_to_blist(&mut self.get_or_create_group(info.group.as_deref()), None);
chat
}
}
}
fn get_or_create_group(&mut self, name: Option<&str>) -> purple::Group {
let name = name.unwrap_or("ICQ");
Group::find(name).unwrap_or_else(|| {
let mut group = purple::Group::new(name);
group.add_to_blist(None);
group
})
}
pub fn conversation_joined(
&mut self,
connection: &mut Connection,
info: &PartialChatInfo,
) -> Conversation {
match connection.get_account().find_chat_conversation(&info.sn) {
Some(mut conversation) => {
if conversation.get_chat_data().unwrap().has_left() {
log::error!("Trying to join left conversation");
} else {
conversation.present();
}
conversation
}
None => {
let mut conversation = connection.serv_got_joined_chat(&info.sn).unwrap();
conversation.set_title(&info.title);
conversation
}
}
}
pub fn check_chat_info(
&mut self,
connection: &mut Connection,
sn: &str,
version: &ChatInfoVersion,
) {
match connection.get_account().find_chat_conversation(&sn) {
Some(mut conversation) => {
let chat_info = unsafe { conversation.get_data::<ChatInfo>(conv_data::CHAT_INFO) };
if chat_info
.map(|chat_info| chat_info.need_update(version))
.unwrap_or(true)
{
log::info!("Fetching chat info: {}", sn);
let handle = Handle::from(&mut *connection);
let protocol_data = self
.connections
.get(&handle)
.expect("Tried get chat info on closed connection");
self.system
.tx
.try_send(PurpleMessage::get_chat_info(
handle,
protocol_data.data.clone(),
sn.to_string(),
))
.unwrap();
}
}
None => {
log::warn!("Checking chat info for no conversation");
}
}
}
pub fn load_chat_info(&mut self, connection: &mut Connection, info: &ChatInfo) {
log::debug!("loading chat info: {:?}", info);
match connection.get_account().find_chat_conversation(&info.sn) {
Some(mut conversation) => {
conversation.set_title(&info.title);
let mut chat_conversation = conversation.get_chat_data().unwrap();
unsafe { conversation.set_data(conv_data::CHAT_INFO, info.clone()) };
chat_conversation.clear_users();
for member in &info.members {
chat_conversation.add_user(&member.sn, "", member.role.as_flags(), false);
}
if let Some(about) = &info.about {
chat_conversation.set_topic("unknown", about);
}
}
None => {
log::warn!("Loaded chat info for no conversation");
}
}
}
*/
}
purple_prpl_plugin!(PurpleDelta);

120
src/logging.rs Normal file
View File

@@ -0,0 +1,120 @@
// This is a copy of https://github.com/Flared/purple-icq/blob/master/src/logging.rs
use crate::messages::{FdSender, SystemMessage};
use crate::purple;
use std::cell::RefCell;
use std::sync::Mutex;
std::thread_local! {
pub static LOGGER: RefCell<Option<Box<dyn log::Log>>> = RefCell::new(None);
}
lazy_static::lazy_static! {
static ref PURPLE_BUFFER: Mutex<Vec<(String, log::Level, String)>> = Default::default();
}
static TLS_LOGGER: ThreadLocalLogger = ThreadLocalLogger;
pub struct ThreadLocalLogger;
impl log::Log for ThreadLocalLogger {
fn enabled(&self, _metadata: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
LOGGER.with(|cell| {
if let Some(ref logger) = cell.borrow().as_ref() {
logger.log(record);
}
})
}
fn flush(&self) {
LOGGER.with(|cell| {
if let Some(ref logger) = cell.borrow().as_ref() {
logger.flush()
}
})
}
}
pub struct PurpleDebugLogger;
impl log::Log for PurpleDebugLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() < log::Level::Debug
}
fn log(&self, record: &log::Record) {
let purple_level = match record.level() {
log::Level::Error => purple::PurpleDebugLevel::PURPLE_DEBUG_ERROR,
log::Level::Warn => purple::PurpleDebugLevel::PURPLE_DEBUG_WARNING,
log::Level::Info => purple::PurpleDebugLevel::PURPLE_DEBUG_INFO,
_ => purple::PurpleDebugLevel::PURPLE_DEBUG_MISC,
};
let target = if !record.target().is_empty() {
record.target()
} else {
record.module_path().unwrap_or_default()
};
let line = format!("[{}] {}\n", target, record.args());
purple::debug(purple_level, "", &line);
}
fn flush(&self) {
let buffer = {
match PURPLE_BUFFER.lock() {
Ok(mut buffer) => buffer.split_off(0),
Err(_) => return,
}
};
for (target, level, message) in buffer {
log::log!(target: &target, level, "{}", message);
}
}
}
pub struct RemoteLogger(pub FdSender<SystemMessage>);
impl log::Log for RemoteLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() < log::Level::Debug
}
fn log(&self, record: &log::Record) {
let target = if !record.target().is_empty() {
record.target()
} else {
record.module_path().unwrap_or_default()
};
if let Ok(mut buffer) = PURPLE_BUFFER.lock() {
buffer.push((target.into(), record.level(), record.args().to_string()));
}
}
fn flush(&self) {
self.0.clone().try_send(SystemMessage::FlushLogs);
}
}
pub fn init(level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
log::set_logger(&TLS_LOGGER).map(|()| log::set_max_level(level))
}
pub fn set_thread_logger<T>(logger: T)
where
T: log::Log + 'static,
{
LOGGER.with(|cell| *cell.borrow_mut() = Some(Box::new(logger)))
}
pub fn flush() {
LOGGER.with(|cell| {
if let Some(ref logger) = cell.borrow().as_ref() {
logger.flush();
}
})
}

View File

@@ -0,0 +1,101 @@
use super::{FdSender, SystemMessage};
use crate::Handle;
use async_std::channel;
use purple::{account, Account};
pub struct AccountProxy<'a> {
pub handle: Handle,
pub sender: &'a mut FdSender<SystemMessage>,
}
impl<'a> AccountProxy<'a> {
pub async fn exec<F, T>(&mut self, f: F) -> Option<T>
where
F: FnOnce(&mut Account) -> T,
F: Send + 'static,
T: Send + 'static,
{
let (tx, rx) = channel::bounded(1);
self.exec_no_return(move |account| {
if let Err(error) = tx.try_send(f(account)) {
log::error!("Failed to send result: {:?}", error);
}
})
.await;
rx.recv().await.ok().or_else(|| {
log::error!("Failed to receive result");
None
})
}
pub async fn exec_no_return<F>(&mut self, f: F)
where
F: FnOnce(&mut Account),
F: Send + 'static,
{
self.sender
.send(SystemMessage::ExecAccount {
handle: self.handle.clone(),
function: Box::new(f),
})
.await;
}
#[allow(clippy::too_many_arguments)]
pub async fn request_input(
&mut self,
title: Option<String>,
primary: Option<String>,
secondary: Option<String>,
default_value: Option<String>,
multiline: bool,
masked: bool,
hint: Option<String>,
ok_text: String,
cancel_text: String,
who: Option<String>,
) -> Option<String> {
let (tx, rx) = channel::bounded(1);
self.exec_no_return(move |account| {
account.request_input(
title.as_deref(),
primary.as_deref(),
secondary.as_deref(),
default_value.as_deref(),
multiline,
masked,
hint.as_deref(),
&ok_text,
&cancel_text,
move |input_value| {
if let Err(error) = tx.try_send(input_value.map(|v| v.into_owned())) {
log::error!("Failed to send result: {:?}", error);
}
},
who.as_deref(),
)
})
.await;
rx.recv().await.ok().flatten()
}
pub async fn is_disconnected(&mut self) -> bool {
self.exec(move |account| account.is_disconnected())
.await
.unwrap_or(false)
}
pub async fn set_settings<T: 'static + serde::Serialize + Send>(
&mut self,
settings: T,
) -> account::settings::Result<()> {
self.exec(move |account| account.set_settings(&settings))
.await
.transpose()
.and_then(|option| {
option.ok_or_else(|| {
account::settings::Error::Message("Failed to receive result".into())
})
})
}
}

View File

@@ -0,0 +1,54 @@
use super::{FdSender, SystemMessage};
use crate::Handle;
use async_std::channel;
use purple::{Connection, PurpleConnectionError, PurpleConnectionState};
pub struct ConnectionProxy<'a> {
pub handle: Handle,
pub sender: &'a mut FdSender<SystemMessage>,
}
impl<'a> ConnectionProxy<'a> {
#[allow(dead_code)]
pub async fn exec<F, T>(&mut self, f: F) -> Option<T>
where
F: FnOnce(&mut Connection) -> T,
F: Send + 'static,
T: Send + 'static,
{
let (tx, rx) = channel::bounded(1);
self.exec_no_return(move |connection| {
if let Err(error) = tx.try_send(f(connection)) {
log::error!("Failed to send result: {:?}", error);
}
})
.await;
rx.recv().await.ok().or_else(|| {
log::error!("Failed to receive result");
None
})
}
pub async fn exec_no_return<F>(&mut self, f: F)
where
F: FnOnce(&mut Connection),
F: Send + 'static,
{
self.sender
.send(SystemMessage::ExecConnection {
handle: self.handle.clone(),
function: Box::new(f),
})
.await;
}
pub async fn set_state(&mut self, state: PurpleConnectionState) {
self.exec_no_return(move |connection| connection.set_state(state))
.await
}
pub async fn error_reason(&mut self, reason: PurpleConnectionError, description: String) {
self.exec_no_return(move |connection| connection.error_reason(reason, &description))
.await
}
}

View File

@@ -0,0 +1,43 @@
use super::{FdSender, SystemMessage};
use crate::{Handle, ProtocolData};
use async_std::channel;
pub struct HandleProxy<'a> {
pub handle: Handle,
pub sender: &'a mut FdSender<SystemMessage>,
}
impl<'a> HandleProxy<'a> {
#[allow(dead_code)]
pub async fn exec<F, T>(&mut self, f: F) -> Option<T>
where
F: FnOnce(&mut crate::PurpleDelta, &mut ProtocolData) -> T,
F: Send + 'static,
T: Send + 'static,
{
let (tx, rx) = channel::bounded(1);
self.exec_no_return(move |plugin, protocol_data| {
if let Err(error) = tx.try_send(f(plugin, protocol_data)) {
log::error!("Failed to send result: {:?}", error);
}
})
.await;
rx.recv().await.ok().or_else(|| {
log::error!("Failed to receive result");
None
})
}
pub async fn exec_no_return<F>(&mut self, f: F)
where
F: FnOnce(&mut crate::PurpleDelta, &mut ProtocolData),
F: Send + 'static,
{
self.sender
.send(SystemMessage::ExecHandle {
handle: self.handle.clone(),
function: Box::new(f),
})
.await;
}
}

193
src/messages/mod.rs Normal file
View File

@@ -0,0 +1,193 @@
use self::account_proxy::AccountProxy;
use self::connection_proxy::ConnectionProxy;
use self::handle_proxy::HandleProxy;
use crate::{AccountDataBox, Handle, ProtocolData, PurpleDelta};
use async_std::channel::{Receiver, Sender};
use purple::{Account, Connection};
mod account_proxy;
mod connection_proxy;
mod handle_proxy;
pub struct FdSender<T> {
os_sender: os_pipe::PipeWriter,
channel_sender: Sender<T>,
}
impl<T> FdSender<T> {
pub fn new(os_sender: os_pipe::PipeWriter, channel_sender: Sender<T>) -> Self {
Self {
os_sender,
channel_sender,
}
}
pub async fn send(&mut self, item: T) {
match self.channel_sender.send(item).await {
Ok(()) => {
use std::io::Write;
self.os_sender.write_all(&[0]).unwrap();
}
Err(error) => log::error!("Failed to send message: {}", error),
}
}
pub fn try_send(&mut self, item: T) {
self.channel_sender.try_send(item).unwrap();
use std::io::Write;
self.os_sender.write_all(&[0]).unwrap();
}
}
impl FdSender<SystemMessage> {
pub fn connection_proxy<'a>(&'a mut self, handle: &Handle) -> ConnectionProxy<'a> {
ConnectionProxy {
handle: handle.clone(),
sender: self,
}
}
pub fn account_proxy<'a>(&'a mut self, handle: &Handle) -> AccountProxy<'a> {
AccountProxy {
handle: handle.clone(),
sender: self,
}
}
pub fn handle_proxy<'a>(&'a mut self, handle: &Handle) -> HandleProxy<'a> {
HandleProxy {
handle: handle.clone(),
sender: self,
}
}
}
impl<T> Clone for FdSender<T> {
fn clone(&self) -> Self {
Self {
os_sender: self.os_sender.try_clone().unwrap(),
channel_sender: self.channel_sender.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct AccountInfo {
pub handle: Handle,
pub protocol_data: AccountDataBox,
}
#[derive(Debug, Clone)]
pub struct PurpleMessageWithHandle<T> {
pub handle: Handle,
pub protocol_data: AccountDataBox,
pub message_data: T,
}
#[derive(Debug, Clone)]
pub struct JoinChatMessageData {
pub stamp: String,
}
#[derive(Debug, Clone)]
pub struct SendMsgMessageData {
pub to_sn: String,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct GetChatInfoMessageData {
pub sn: String,
}
#[derive(Debug, Clone)]
pub struct GetHistoryMessageData {
pub sn: String,
pub from_msg_id: String,
pub count: i32,
}
#[derive(Debug)]
pub enum PurpleMessage {
Login(AccountInfo),
JoinChat(JoinChatMessage),
SendMsg(SendMsgMessage),
GetChatInfo(GetChatInfoMessage),
GetHistory(GetHistoryMessage),
}
pub type JoinChatMessage = PurpleMessageWithHandle<JoinChatMessageData>;
pub type GetHistoryMessage = PurpleMessageWithHandle<GetHistoryMessageData>;
pub type SendMsgMessage = PurpleMessageWithHandle<SendMsgMessageData>;
pub type GetChatInfoMessage = PurpleMessageWithHandle<GetChatInfoMessageData>;
impl PurpleMessage {
pub fn join_chat(handle: Handle, protocol_data: AccountDataBox, stamp: String) -> Self {
Self::JoinChat(JoinChatMessage {
handle,
protocol_data,
message_data: JoinChatMessageData { stamp },
})
}
pub fn fetch_history(
handle: Handle,
protocol_data: AccountDataBox,
sn: String,
from_msg_id: String,
count: i32,
) -> Self {
Self::GetHistory(GetHistoryMessage {
handle,
protocol_data,
message_data: GetHistoryMessageData {
sn,
from_msg_id,
count,
},
})
}
pub fn send_msg(
handle: Handle,
protocol_data: AccountDataBox,
to_sn: String,
message: String,
) -> Self {
Self::SendMsg(SendMsgMessage {
handle,
protocol_data,
message_data: SendMsgMessageData { to_sn, message },
})
}
pub fn get_chat_info(handle: Handle, protocol_data: AccountDataBox, sn: String) -> Self {
Self::GetChatInfo(GetChatInfoMessage {
handle,
protocol_data,
message_data: GetChatInfoMessageData { sn },
})
}
}
pub enum SystemMessage {
ExecAccount {
handle: Handle,
function: Box<dyn FnOnce(&mut Account) + Send + 'static>,
},
ExecConnection {
handle: Handle,
function: Box<dyn FnOnce(&mut Connection) + Send + 'static>,
},
ExecHandle {
handle: Handle,
function: Box<dyn FnOnce(&mut PurpleDelta, &mut ProtocolData) + Send + 'static>,
},
FlushLogs,
}
pub struct DeltaSystemHandle {
pub input_rx: os_pipe::PipeReader,
pub rx: Receiver<SystemMessage>,
pub tx: Sender<PurpleMessage>,
}