Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
29ea694b1e | |||
eec8669b88 | |||
e99d824227 | |||
f57cdf3cdc | |||
039807bb32 | |||
fd321e02d8 | |||
3142134360 | |||
70b24f0e14 | |||
77a257892a | |||
6237b9421d |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,2 @@
|
||||
/libdelta.so
|
||||
/libetpan.so
|
||||
/libdeltachat.so
|
||||
/libnetpgp.so
|
||||
/vendor/deltachat-core-0.35.0
|
||||
/vendor/libetpan-1.8
|
||||
/purple
|
||||
/target
|
||||
|
3909
Cargo.lock
generated
Normal file
3909
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
Normal file
31
Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "purple-plugin-delta"
|
||||
version = "0.1.0"
|
||||
authors = ["Nick Thomas <delta@ur.gs>"]
|
||||
rust-version = "1.56"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "purple_delta"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["dylib"]
|
||||
|
||||
[dependencies]
|
||||
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 = "*"
|
||||
|
||||
## 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" }
|
34
Makefile
34
Makefile
@@ -1,27 +1,7 @@
|
||||
CC ?= gcc
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
PKG_CONFIG ?= pkg-config
|
||||
LIB_TARGET = libdelta.so
|
||||
LIB_DEST = $(DESTDIR)`$(PKG_CONFIG) --variable=plugindir purple`
|
||||
|
||||
$(LIB_TARGET): *.c *.h Makefile
|
||||
$(CC) -C \
|
||||
-Wall -Wextra -Werror \
|
||||
-std=c11 \
|
||||
-shared \
|
||||
-fpic \
|
||||
$(shell $(PKG_CONFIG) --cflags purple deltachat) \
|
||||
-o $(LIB_TARGET) \
|
||||
*.c \
|
||||
-shared \
|
||||
$(shell $(PKG_CONFIG) --libs purple deltachat) \
|
||||
|
||||
install:
|
||||
install -D $(LIB_TARGET) $(LIB_DEST)
|
||||
|
||||
uninstall:
|
||||
rm -f $(LIB_DEST)/$(LIB_TARGET)
|
||||
|
||||
clean:
|
||||
rm $(LIB_TARGET)
|
||||
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
|
||||
|
54
README.md
54
README.md
@@ -13,51 +13,47 @@ Delta has:
|
||||
* An electron [desktop application](https://github.com/deltachat/deltachat-desktop)
|
||||
|
||||
This project is a [libpurple](https://developer.pidgin.im/wiki/WhatIsLibpurple)
|
||||
plugin that wraps `deltachat-core`, allowing a number of existing desktop and
|
||||
plugin that wraps `deltachat-core-rust`, allowing a number of existing desktop and
|
||||
mobile clients to send and receive IMs over SMTP+IMAP. It may be useful for
|
||||
[Linux-based mobile devices](https://source.puri.sm/Librem5/chatty), for
|
||||
GUI desktop usage **without** an Electron dependency, or console desktop usage.
|
||||
GUI desktop usage **without** an Electron dependency, or desktop usage.
|
||||
|
||||
Current status is probably best described as "skunkworks", although connecting
|
||||
to an account and sending / receiving text and image messages should work
|
||||
reliably in pidgin. Chatty supports text messages, and can be coaxed into using
|
||||
this plugin, but there's a long way to go with that yet.
|
||||
## Current status
|
||||
|
||||
A big refactoring to use "proper" purple IM structures is necessary to make
|
||||
further progress, I think.
|
||||
Starting again from scratch in Rust. So currently, nothing works. TODO list:
|
||||
|
||||
I also need to implement support for the buddy list.
|
||||
|
||||
We currrently build against deltachat v1.50.0. You'll need to build and install
|
||||
deltachat-ffi separately and ensure that it's available via `pkg-config` for
|
||||
deltachat to install.
|
||||
- [~] Connect to email account
|
||||
- [ ] Full settings support
|
||||
- [ ] Show buddy list
|
||||
- [ ] Send/receive text messages to single contact
|
||||
- [ ] Send/receive text messages to group chat
|
||||
- [ ] IMEX setup
|
||||
- [ ] Send/receive image messages
|
||||
- [ ] Send/receive audio messages
|
||||
- [ ] 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.
|
||||
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.
|
||||
|
||||
`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.
|
||||
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
|
||||
I'm taking a lot of inspiration from. WTF I like happens to include building it
|
||||
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.
|
||||
|
||||
If it doesn't show up, chances are pidgin can't find the various shared
|
||||
libraries the .so depends on. You can run `ldd ~/.purple/plugins/libdelta.so`
|
||||
to confirm. I'll document fixing this after the build and install system is
|
||||
settled.
|
||||
|
||||
At present, the "Username" and "Password" account fields correspond to email
|
||||
address and password, respectively. Many important settings also show up on the
|
||||
"Advanced" tab - if left blank, the plugin will attempt to automatically detect
|
||||
|
@@ -1,560 +0,0 @@
|
||||
#include <connection.h>
|
||||
#include <debug.h>
|
||||
#include <eventloop.h>
|
||||
#include <imgstore.h>
|
||||
#include <util.h>
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "delta-connection.h"
|
||||
#include "libdelta.h"
|
||||
|
||||
#define IMEX_RECEIVED_MESSAGE "Setup message received. To apply it, reply with:\nIMEX: %d nnnn-nnnn-nnnn-nnnn-nnnn-nnnn-nnnn-nnnn-nnnn\nNo whitespace in the setup-code!"
|
||||
|
||||
void delta_recv_im(DeltaConnectionData *conn, dc_msg_t *msg);
|
||||
|
||||
void
|
||||
_transpose_config(dc_context_t *mailbox, PurpleAccount *acct)
|
||||
{
|
||||
const char *addr = acct->username;
|
||||
const char *display = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_DISPLAY_NAME, NULL);
|
||||
|
||||
const char *imap_host = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_HOST, NULL);
|
||||
const char *imap_user = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_IMAP_USER, NULL);
|
||||
const char *imap_pass = purple_account_get_password(acct);
|
||||
const char *imap_port = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_PORT, NULL);
|
||||
|
||||
const char *smtp_host = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_HOST, NULL);
|
||||
const char *smtp_user = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_USER, NULL);
|
||||
const char *smtp_pass = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_PASS, NULL);
|
||||
const char *smtp_port = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_PORT, NULL);
|
||||
|
||||
gboolean bcc_self = purple_account_get_bool(acct, PLUGIN_ACCOUNT_OPT_BCC_SELF, FALSE);
|
||||
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_ADDR, addr);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_DISPLAY_NAME, display);
|
||||
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_HOST, imap_host);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_USER, imap_user);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_PASS, imap_pass);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_PORT, imap_port);
|
||||
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_HOST, smtp_host);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_USER, smtp_user);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_PASS, smtp_pass);
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_PORT, smtp_port);
|
||||
|
||||
if (bcc_self) {
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_BCC_SELF, "1");
|
||||
} else {
|
||||
dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_BCC_SELF, "0");
|
||||
};
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
DeltaConnectionData *conn;
|
||||
|
||||
// Used by delta_process_incoming_message
|
||||
uint32_t msg_id;
|
||||
gboolean msg_changed;
|
||||
|
||||
// Used by delta_process_connection_state
|
||||
int connection_state;
|
||||
} ProcessRequest;
|
||||
|
||||
gboolean
|
||||
delta_process_incoming_message(void *data)
|
||||
{
|
||||
ProcessRequest *pr = (ProcessRequest *)data;
|
||||
g_assert(pr != NULL);
|
||||
g_assert(pr->conn != NULL);
|
||||
|
||||
dc_msg_t *msg = dc_get_msg(pr->conn->mailbox, pr->msg_id);
|
||||
delta_recv_im(pr->conn, msg);
|
||||
dc_msg_unref(msg);
|
||||
g_free(data);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
delta_process_connection_state(void *data)
|
||||
{
|
||||
ProcessRequest *pr = (ProcessRequest *)data;
|
||||
g_assert(pr != NULL);
|
||||
g_assert(pr->conn != NULL);
|
||||
|
||||
purple_connection_update_progress(
|
||||
pr->conn->pc,
|
||||
"Connecting...",
|
||||
pr->connection_state,
|
||||
MAX_DELTA_CONFIGURE
|
||||
);
|
||||
|
||||
if (pr->connection_state == MAX_DELTA_CONFIGURE) {
|
||||
purple_connection_set_state(pr->conn->pc, PURPLE_CONNECTED);
|
||||
}
|
||||
|
||||
g_free(data);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
delta_process_fresh_messages(void *data)
|
||||
{
|
||||
ProcessRequest *pr = (ProcessRequest *)data;
|
||||
g_assert(pr != NULL);
|
||||
g_assert(pr->conn != NULL);
|
||||
|
||||
dc_context_t *mailbox = pr->conn->mailbox;
|
||||
g_assert(mailbox != NULL);
|
||||
|
||||
// Spot any messages received while offline
|
||||
dc_array_t *fresh_msgs = dc_get_fresh_msgs(mailbox);
|
||||
size_t fresh_count = dc_array_get_cnt(fresh_msgs);
|
||||
|
||||
purple_debug_info(PLUGIN_ID, "fresh_count: %zu\n", fresh_count);
|
||||
|
||||
for(size_t i = 0; i < fresh_count; i++) {
|
||||
uint32_t msg_id = dc_array_get_id(fresh_msgs, i);
|
||||
dc_msg_t *msg = dc_get_msg(mailbox, msg_id);
|
||||
|
||||
delta_recv_im(pr->conn, msg);
|
||||
}
|
||||
|
||||
dc_array_unref(fresh_msgs);
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
ProcessRequest *
|
||||
delta_build_process_request(DeltaConnectionData *conn)
|
||||
{
|
||||
g_assert(conn != NULL);
|
||||
|
||||
ProcessRequest *pr = g_malloc(sizeof(ProcessRequest));
|
||||
g_assert(pr != NULL);
|
||||
|
||||
pr->conn = conn;
|
||||
|
||||
return pr;
|
||||
}
|
||||
|
||||
// Do not call any libpurple or delta functions in here, as it is not
|
||||
// thread-safe and events may be dispatched from any delta thread. Use
|
||||
// purple_timeout_add(0, callback, data) to run on the main thread instead
|
||||
void *
|
||||
delta_event_handler(void *context)
|
||||
{
|
||||
DeltaConnectionData *conn = (DeltaConnectionData *)context;
|
||||
|
||||
g_assert(conn != NULL);
|
||||
|
||||
dc_context_t *mailbox = conn->mailbox;
|
||||
dc_event_emitter_t* emitter = dc_get_event_emitter(mailbox);
|
||||
dc_event_t* event;
|
||||
|
||||
// FIXME: do we still need runthreads?
|
||||
while (conn->runthreads && (event = dc_get_next_event(emitter)) != NULL) {
|
||||
ProcessRequest *pr = NULL;
|
||||
int event_id = dc_event_get_id(event);
|
||||
|
||||
purple_debug_info(PLUGIN_ID, "Event %d received from Delta.\n", event_id);
|
||||
|
||||
switch (event_id) {
|
||||
case DC_EVENT_SMTP_MESSAGE_SENT:
|
||||
case DC_EVENT_IMAP_CONNECTED:
|
||||
case DC_EVENT_SMTP_CONNECTED:
|
||||
case DC_EVENT_IMAP_MESSAGE_DELETED:
|
||||
case DC_EVENT_IMAP_MESSAGE_MOVED:
|
||||
case DC_EVENT_INFO: {
|
||||
char *info = dc_event_get_data2_str(event);
|
||||
purple_debug_info(PLUGIN_ID, "INFO from Delta: %s\n", info);
|
||||
dc_str_unref(info);
|
||||
break;
|
||||
}
|
||||
case DC_EVENT_WARNING: {
|
||||
char *warn = dc_event_get_data2_str(event);
|
||||
purple_debug_info(PLUGIN_ID, "WARNING from Delta: %s\n", warn);
|
||||
dc_str_unref(warn);
|
||||
break;
|
||||
}
|
||||
case DC_EVENT_ERROR:
|
||||
case DC_EVENT_ERROR_NETWORK: {
|
||||
int errcode = dc_event_get_data1_int(event);
|
||||
char *err = dc_event_get_data2_str(event);
|
||||
purple_debug_info(PLUGIN_ID, "ERROR from Delta: %d: %s\n", errcode, err);
|
||||
dc_str_unref(err);
|
||||
break;
|
||||
}
|
||||
|
||||
case DC_EVENT_MSGS_CHANGED: {
|
||||
// This event may be issued for a single message, in which case the
|
||||
// message ID is in data2 and we should treat it as an incoming msg
|
||||
// FIXME: this leads to duplicate messages when it's an outgoing
|
||||
// message we just sent
|
||||
uint32_t msg_id = dc_event_get_data2_int(event);
|
||||
|
||||
pr = delta_build_process_request(conn);
|
||||
if (msg_id) {
|
||||
// FIXME: for now, only display IMEX setup messages to avoid duplicates
|
||||
pr->msg_id = msg_id;
|
||||
pr->msg_changed = TRUE;
|
||||
|
||||
purple_timeout_add(0, delta_process_incoming_message, pr);
|
||||
} else {
|
||||
purple_timeout_add(0, delta_process_fresh_messages, pr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case DC_EVENT_INCOMING_MSG:
|
||||
// data1 is chat_id, which we don't seem to need yet.
|
||||
// TODO: It may be needed for group chats
|
||||
pr = delta_build_process_request(conn);
|
||||
pr->msg_id = (uint32_t)dc_event_get_data2_int(event);
|
||||
purple_timeout_add(0, delta_process_incoming_message, pr);
|
||||
break;
|
||||
|
||||
// Things left to do
|
||||
case DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED:
|
||||
case DC_EVENT_NEW_BLOB_FILE:
|
||||
case DC_EVENT_DELETED_BLOB_FILE:
|
||||
case DC_EVENT_MSG_DELIVERED:
|
||||
case DC_EVENT_MSG_READ:
|
||||
case DC_EVENT_MSG_FAILED:
|
||||
case DC_EVENT_CHAT_MODIFIED:
|
||||
case DC_EVENT_CONTACTS_CHANGED:
|
||||
case DC_EVENT_ERROR_SELF_NOT_IN_GROUP:
|
||||
case DC_EVENT_IMEX_FILE_WRITTEN:
|
||||
case DC_EVENT_IMEX_PROGRESS:
|
||||
case DC_EVENT_LOCATION_CHANGED:
|
||||
case DC_EVENT_MSGS_NOTICED:
|
||||
case DC_EVENT_SECUREJOIN_INVITER_PROGRESS:
|
||||
case DC_EVENT_SECUREJOIN_JOINER_PROGRESS:
|
||||
purple_debug_info(PLUGIN_ID, "Event %d is TODO\n", event_id);
|
||||
break;
|
||||
|
||||
case DC_EVENT_CONFIGURE_PROGRESS:
|
||||
pr = delta_build_process_request(conn);
|
||||
pr->connection_state = dc_event_get_data1_int(event);
|
||||
purple_timeout_add(0, delta_process_connection_state, pr);
|
||||
break;
|
||||
|
||||
default:
|
||||
purple_debug_info(PLUGIN_ID, "Unknown Delta event: %d\n", event_id);
|
||||
}
|
||||
|
||||
dc_event_unref(event);
|
||||
}
|
||||
dc_event_emitter_unref(emitter);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
delta_connection_new(PurpleConnection *pc)
|
||||
{
|
||||
DeltaConnectionData *conn = NULL;
|
||||
|
||||
g_assert(purple_connection_get_protocol_data(pc) == NULL);
|
||||
|
||||
conn = g_new0(DeltaConnectionData, 1);
|
||||
conn->pc = pc;
|
||||
purple_connection_set_protocol_data(pc, conn);
|
||||
}
|
||||
|
||||
void
|
||||
delta_connection_free(PurpleConnection *pc)
|
||||
{
|
||||
DeltaConnectionData *conn = purple_connection_get_protocol_data(pc);
|
||||
|
||||
g_assert(conn != NULL);
|
||||
|
||||
conn->runthreads = 0;
|
||||
|
||||
if (conn->mailbox != NULL) {
|
||||
dc_stop_ongoing_process(conn->mailbox);
|
||||
dc_stop_io(conn->mailbox);
|
||||
|
||||
// TODO: correctly handle join failing
|
||||
purple_debug_info(PLUGIN_ID, "Joining event thread\n");
|
||||
if (pthread_join(conn->event_thread, NULL) != 0) {
|
||||
purple_debug_info(PLUGIN_ID, "joining event thread failed\n");
|
||||
}
|
||||
|
||||
dc_context_unref(conn->mailbox);
|
||||
}
|
||||
|
||||
purple_connection_set_protocol_data(pc, NULL);
|
||||
|
||||
// TODO: free resources as they are added to DeltaConnectionData
|
||||
conn->pc = NULL;
|
||||
conn->mailbox = NULL;
|
||||
|
||||
g_free(conn);
|
||||
}
|
||||
|
||||
void
|
||||
delta_connection_start_login(PurpleConnection *pc)
|
||||
{
|
||||
char dbname[1024];
|
||||
PurpleAccount *acct = pc->account;
|
||||
DeltaConnectionData *conn = purple_connection_get_protocol_data(pc);
|
||||
dc_context_t *mailbox = NULL;
|
||||
|
||||
g_snprintf(
|
||||
dbname, 1024, "%s%sdelta_db-%s",
|
||||
purple_user_dir(), G_DIR_SEPARATOR_S, acct->username
|
||||
);
|
||||
|
||||
mailbox = dc_context_new(PLUGIN_ID, dbname, NULL);
|
||||
|
||||
conn->mailbox = mailbox;
|
||||
_transpose_config(mailbox, acct);
|
||||
|
||||
conn->runthreads = 1;
|
||||
pthread_create(&conn->event_thread, NULL, delta_event_handler, conn);
|
||||
|
||||
dc_configure(mailbox);
|
||||
dc_start_io(mailbox);
|
||||
dc_maybe_network(mailbox);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
gboolean delta_try_process_imex(dc_context_t *mailbox, char *text) {
|
||||
if (!g_str_has_prefix(text, "IMEX: ")) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gchar **parts = g_strsplit(text, " ", 3);
|
||||
if (g_strv_length(parts) != 3) {
|
||||
g_strfreev(parts);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int msg_id = atoi(parts[1]);
|
||||
gboolean success = dc_continue_key_transfer(mailbox, msg_id, parts[2]);
|
||||
|
||||
g_strfreev(parts);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
int
|
||||
delta_send_im(PurpleConnection *pc, const char *who, const char *message, PurpleMessageFlags flags)
|
||||
{
|
||||
UNUSED(flags);
|
||||
|
||||
DeltaConnectionData *conn = (DeltaConnectionData *)purple_connection_get_protocol_data(pc);
|
||||
g_assert(conn != NULL);
|
||||
|
||||
dc_context_t *mailbox = conn->mailbox;
|
||||
g_assert(mailbox != NULL);
|
||||
|
||||
uint32_t contact_id = dc_create_contact(mailbox, NULL, who);
|
||||
uint32_t chat_id = dc_create_chat_by_contact_id(mailbox, contact_id);
|
||||
|
||||
GData *attrs;
|
||||
const char *msg_ptr, *start, *end;
|
||||
msg_ptr = message;
|
||||
|
||||
// Send each image included in the message.
|
||||
while (purple_markup_find_tag("img", msg_ptr, &start, &end, &attrs) == TRUE) {
|
||||
char *id_str = g_datalist_id_get_data(&attrs, g_quark_from_string("id"));
|
||||
purple_debug_info(PLUGIN_ID, "In a loop, got %s\n", id_str);
|
||||
|
||||
msg_ptr = end + 1;
|
||||
|
||||
if (id_str == NULL || strlen(id_str) == 0) {
|
||||
g_datalist_clear(&attrs);
|
||||
continue;
|
||||
}
|
||||
|
||||
int id = atoi(id_str);
|
||||
g_datalist_clear(&attrs);
|
||||
|
||||
if (id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GError *err = NULL;
|
||||
char *tempdir = g_dir_make_tmp(NULL, &err);
|
||||
if (err != NULL) {
|
||||
purple_debug_info(PLUGIN_ID, "Couldn't get a temporary dir for image %d: %s", id, err->message);
|
||||
g_free(err);
|
||||
continue;
|
||||
}
|
||||
|
||||
PurpleStoredImage *img = purple_imgstore_find_by_id(id);
|
||||
const char *filename = purple_imgstore_get_filename(img);
|
||||
const char *extension = purple_imgstore_get_extension(img);
|
||||
gconstpointer data = purple_imgstore_get_data(img);
|
||||
|
||||
char *path = g_strdup_printf("%s/%s", tempdir, filename);
|
||||
|
||||
g_file_set_contents(path, data, purple_imgstore_get_size(img), &err);
|
||||
if (err != NULL) {
|
||||
purple_debug_info(PLUGIN_ID, "failed to write %s to temporary file: %s\n", filename, err->message);
|
||||
g_free(err);
|
||||
goto next;
|
||||
}
|
||||
|
||||
purple_debug_info(PLUGIN_ID, "Sending image %s from imgstore: %d\n", filename, id);
|
||||
|
||||
dc_msg_t *img_msg = dc_msg_new(mailbox, DC_MSG_IMAGE);
|
||||
dc_msg_set_file(img_msg, path, extension);
|
||||
dc_send_msg(mailbox, chat_id, img_msg);
|
||||
dc_msg_unref(img_msg);
|
||||
|
||||
next:
|
||||
remove(path);
|
||||
remove(tempdir);
|
||||
g_free(path);
|
||||
}
|
||||
|
||||
// Send any text left
|
||||
char *stripped_message = purple_markup_strip_html(message);
|
||||
g_assert(stripped_message != NULL);
|
||||
if (strlen(stripped_message) > 0) {
|
||||
if (!delta_try_process_imex(mailbox, stripped_message)) {
|
||||
dc_send_text_msg(mailbox, chat_id, stripped_message);
|
||||
}
|
||||
}
|
||||
g_free(stripped_message);
|
||||
|
||||
return 0; // success; don't echo the message to the chat window since we display it anyway
|
||||
}
|
||||
|
||||
void
|
||||
delta_recv_im(DeltaConnectionData *conn, dc_msg_t *msg)
|
||||
{
|
||||
dc_context_t *mailbox = conn->mailbox;
|
||||
g_assert(mailbox != NULL);
|
||||
|
||||
PurpleConnection *pc = conn->pc;
|
||||
g_assert(pc != NULL);
|
||||
|
||||
uint32_t msg_id = dc_msg_get_id(msg);
|
||||
int viewtype = dc_msg_get_viewtype(msg);
|
||||
time_t timestamp = dc_msg_get_timestamp(msg);
|
||||
char *text = dc_msg_get_text(msg);
|
||||
uint32_t chat_id = dc_msg_get_chat_id(msg);
|
||||
dc_contact_t *from = dc_get_contact(mailbox, dc_msg_get_from_id(msg));
|
||||
dc_chat_t *chat = dc_get_chat(mailbox, chat_id);
|
||||
dc_array_t *contacts = dc_get_chat_contacts(mailbox, chat_id);
|
||||
int num_contacts = dc_array_get_cnt(contacts);
|
||||
|
||||
if (chat == NULL) {
|
||||
purple_debug_info(PLUGIN_ID, "Receiving IM: unknown chat: %d\n", chat_id);
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (dc_chat_get_type(chat) == DC_CHAT_TYPE_GROUP) {
|
||||
purple_debug_info(PLUGIN_ID, "Receiving IM: group chat with ID %d! Not yet supported\n", chat_id);
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (num_contacts != 1) {
|
||||
purple_debug_info(PLUGIN_ID, "Receiving IM: 1-1 chat %d with %d contacts instead of 1!\n", chat_id, num_contacts);
|
||||
goto out;
|
||||
}
|
||||
|
||||
// FIXME: using dc_array_get_contact_id fails here, complaining that it's not an array of locations
|
||||
dc_contact_t *contact = dc_get_contact(mailbox, dc_array_get_id(contacts, 0));
|
||||
char *who = NULL;
|
||||
|
||||
// In the current architecture, delta_send_im and delta_recv_im must agree
|
||||
// on the value for 'who'. Using the email address is an easy cheat for this
|
||||
// but gets shaky in the long term.
|
||||
if (contact != NULL) {
|
||||
who = dc_contact_get_addr(contact);
|
||||
} else {
|
||||
who = dc_chat_get_name(chat);
|
||||
}
|
||||
|
||||
int flags = 0;
|
||||
int state = dc_msg_get_state(msg);
|
||||
|
||||
if (state == DC_STATE_IN_FRESH || state == DC_STATE_IN_NOTICED || state == DC_STATE_IN_SEEN) {
|
||||
flags |= PURPLE_MESSAGE_RECV;
|
||||
} else {
|
||||
flags |= PURPLE_MESSAGE_SEND;
|
||||
}
|
||||
|
||||
// FIXME: as a massive hack, convert IMEX setup messages into a text message
|
||||
// prompting the user how to trigger the IMEX filter in outgoing messages.
|
||||
if (dc_msg_is_setupmessage(msg)) {
|
||||
purple_debug_info(PLUGIN_ID, "Receiving IMEX: ID=%d\n", msg_id);
|
||||
viewtype = DC_MSG_TEXT;
|
||||
dc_str_unref(text);
|
||||
text = g_strndup("", 1024);
|
||||
g_assert(text != NULL);
|
||||
|
||||
g_snprintf(text, 1024, IMEX_RECEIVED_MESSAGE, msg_id);
|
||||
}
|
||||
|
||||
switch(viewtype) {
|
||||
case DC_MSG_GIF:
|
||||
case DC_MSG_IMAGE:
|
||||
case DC_MSG_STICKER:
|
||||
flags = flags | PURPLE_MESSAGE_IMAGES;
|
||||
break;
|
||||
case DC_MSG_TEXT:
|
||||
flags = flags | PURPLE_MESSAGE_RAW;
|
||||
break;
|
||||
case DC_MSG_VIDEO: // Pidgin only supports these as files for download
|
||||
case DC_MSG_FILE:
|
||||
case DC_MSG_AUDIO: // Sound to play
|
||||
case DC_MSG_VOICE:
|
||||
break;
|
||||
default:
|
||||
purple_debug_info(PLUGIN_ID, "Message %d: unknown message type: %d\n", msg_id, viewtype);
|
||||
}
|
||||
|
||||
int image_id = 0;
|
||||
|
||||
if ((flags & PURPLE_MESSAGE_IMAGES) > 0) {
|
||||
char *filename = dc_msg_get_file(msg);
|
||||
gchar *data;
|
||||
gsize length;
|
||||
GError *err = NULL;
|
||||
|
||||
g_file_get_contents(filename, &data, &length, &err);
|
||||
if (err != NULL) {
|
||||
purple_debug_info(PLUGIN_ID, "Failed to read image %s: %s\n", filename, err->message);
|
||||
g_error_free(err);
|
||||
goto out;
|
||||
}
|
||||
|
||||
image_id = purple_imgstore_add_with_id(data, length, filename);
|
||||
text = g_strdup_printf("<img id='%d'><br/>%s", image_id, text);
|
||||
}
|
||||
|
||||
char *name = dc_contact_get_name(from);
|
||||
int msglen = strlen(name) + 3 + strlen(text);
|
||||
|
||||
char *msgtext = malloc(msglen);
|
||||
g_snprintf(msgtext, msglen, "%s: %s", name, text);
|
||||
|
||||
serv_got_im(pc, who, msgtext, flags, timestamp);
|
||||
|
||||
if (image_id > 0) {
|
||||
purple_imgstore_unref_by_id(image_id);
|
||||
}
|
||||
|
||||
dc_markseen_msgs(mailbox, &msg_id, 1);
|
||||
g_free(msgtext);
|
||||
dc_str_unref(who);
|
||||
dc_str_unref(name);
|
||||
dc_contact_unref(contact);
|
||||
out:
|
||||
dc_str_unref(text);
|
||||
dc_chat_unref(chat);
|
||||
dc_array_unref(contacts);
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
#ifndef DELTA_CONNECTION_H
|
||||
#define DELTA_CONNECTION_H
|
||||
|
||||
#include <glib.h>
|
||||
#include <deltachat/deltachat.h>
|
||||
#include <pthread.h>
|
||||
|
||||
struct _PurpleConnection;
|
||||
|
||||
typedef struct _DeltaConnectionData {
|
||||
struct _PurpleConnection *pc;
|
||||
dc_context_t *mailbox;
|
||||
|
||||
// Set to 0 to convince threads to exit
|
||||
int runthreads;
|
||||
|
||||
pthread_t event_thread;
|
||||
} DeltaConnectionData;
|
||||
|
||||
#define MAX_DELTA_CONFIGURE 1000
|
||||
|
||||
void delta_connection_new(struct _PurpleConnection *pc);
|
||||
void delta_connection_free(struct _PurpleConnection *pc);
|
||||
|
||||
void delta_connection_start_login(PurpleConnection *pc);
|
||||
|
||||
int delta_send_im(PurpleConnection *pc, const char *who, const char *message, PurpleMessageFlags flags);
|
||||
|
||||
#endif
|
||||
|
246
libdelta.c
246
libdelta.c
@@ -1,246 +0,0 @@
|
||||
#define PURPLE_PLUGINS
|
||||
|
||||
#include "libdelta.h"
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
// All from libpurple
|
||||
#include <accountopt.h>
|
||||
#include <connection.h>
|
||||
#include <notify.h>
|
||||
#include <plugin.h>
|
||||
#include <prpl.h>
|
||||
#include <version.h>
|
||||
|
||||
#include "delta-connection.h"
|
||||
#include "util.h"
|
||||
|
||||
static GList *
|
||||
delta_status_types(PurpleAccount *acct)
|
||||
{
|
||||
UNUSED(acct);
|
||||
|
||||
GList *types = NULL;
|
||||
|
||||
types = g_list_append(types, purple_status_type_new(PURPLE_STATUS_OFFLINE, "Offline", NULL, TRUE));
|
||||
types = g_list_append(types, purple_status_type_new(PURPLE_STATUS_AVAILABLE, "Online", NULL, TRUE));
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
static void
|
||||
delta_login(PurpleAccount *acct)
|
||||
{
|
||||
PurpleConnection *pc = purple_account_get_connection(acct);
|
||||
|
||||
|
||||
delta_connection_new(pc);
|
||||
delta_connection_start_login(pc);
|
||||
|
||||
pc->flags |= PURPLE_CONNECTION_NO_BGCOLOR;
|
||||
}
|
||||
|
||||
static void
|
||||
delta_close(PurpleConnection *pc)
|
||||
{
|
||||
// TODO: actually disconnect!
|
||||
purple_connection_set_state(pc, PURPLE_DISCONNECTED);
|
||||
delta_connection_free(pc);
|
||||
}
|
||||
|
||||
static const char *
|
||||
delta_list_icon(PurpleAccount *acct, PurpleBuddy *buddy)
|
||||
{
|
||||
UNUSED(acct);
|
||||
UNUSED(buddy);
|
||||
|
||||
return "delta";
|
||||
}
|
||||
|
||||
static PurpleAccountOption *
|
||||
str_opt(const char *text, const char *name, const char *def)
|
||||
{
|
||||
return purple_account_option_string_new(text, name, def);
|
||||
}
|
||||
|
||||
static PurpleAccountOption *
|
||||
pwd_opt(const char *text, const char *name, const char *def)
|
||||
{
|
||||
PurpleAccountOption* option = str_opt(text, name, def);
|
||||
purple_account_option_set_masked(option, TRUE);
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
static PurpleAccountOption *
|
||||
bool_opt(const char *text, const char *name, const gboolean def)
|
||||
{
|
||||
return purple_account_option_bool_new(text, name, def);
|
||||
}
|
||||
|
||||
static void
|
||||
delta_init_plugin(PurplePlugin *plugin)
|
||||
{
|
||||
PurplePluginProtocolInfo *extra = (PurplePluginProtocolInfo *)plugin->info->extra_info;
|
||||
GList *opts = NULL;
|
||||
|
||||
opts = g_list_prepend(opts, str_opt("Display Name", PLUGIN_ACCOUNT_OPT_DISPLAY_NAME, NULL));
|
||||
|
||||
opts = g_list_prepend(opts, str_opt("IMAP Server Host", PLUGIN_ACCOUNT_OPT_IMAP_SERVER_HOST, NULL));
|
||||
opts = g_list_prepend(opts, str_opt("IMAP Server Port", PLUGIN_ACCOUNT_OPT_IMAP_SERVER_PORT, NULL));
|
||||
opts = g_list_prepend(opts, str_opt("IMAP Username", PLUGIN_ACCOUNT_OPT_IMAP_USER, NULL));
|
||||
|
||||
|
||||
// These are pidgin's built-in username & password options
|
||||
// FIXME: it's not super-obvious or pleasant :/
|
||||
// opts = g_list_prepend(opts, str_opt("Email Address", PLUGIN_ACCOUNT_OPT_EMAIL_ADDRESS, ""));
|
||||
// opts = g_list_prepend(opts, pwd_opt("IMAP Password", PLUGIN_ACCOUNT_OPT_IMAP_PASS, ""));
|
||||
|
||||
opts = g_list_prepend(opts, str_opt("SMTP Server Host", PLUGIN_ACCOUNT_OPT_SMTP_SERVER_HOST, NULL));
|
||||
opts = g_list_prepend(opts, str_opt("SMTP Server Port", PLUGIN_ACCOUNT_OPT_SMTP_SERVER_PORT, NULL));
|
||||
opts = g_list_prepend(opts, str_opt("SMTP Username", PLUGIN_ACCOUNT_OPT_SMTP_USER, NULL));
|
||||
opts = g_list_prepend(opts, pwd_opt("SMTP Password", PLUGIN_ACCOUNT_OPT_SMTP_PASS, NULL));
|
||||
|
||||
// Not exposed: server_flags, selfstatus, e2ee_enabled
|
||||
// https://deltachat.github.io/api/classmrmailbox__t.html
|
||||
|
||||
opts = g_list_prepend(opts, bool_opt("Send copy to self", PLUGIN_ACCOUNT_OPT_BCC_SELF, FALSE));
|
||||
|
||||
extra->protocol_options = g_list_reverse(opts);
|
||||
}
|
||||
|
||||
static void
|
||||
delta_destroy_plugin(PurplePlugin *plugin) {
|
||||
UNUSED(plugin);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
delta_offline_message(const PurpleBuddy *buddy)
|
||||
{
|
||||
UNUSED(buddy);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static PurplePluginProtocolInfo extra_info =
|
||||
{
|
||||
DELTA_PROTOCOL_OPTS, /* options */
|
||||
NULL, /* user_splits */
|
||||
NULL, /* protocol_options, initialized in delta_init_plugin() */
|
||||
{ /* icon_spec, a PurpleBuddyIconSpec */
|
||||
"svg,png,jpg,gif", /* format */
|
||||
0, /* min_width */
|
||||
0, /* min_height */
|
||||
128, /* max_width */
|
||||
128, /* max_height */
|
||||
10000, /* max_filesize */
|
||||
PURPLE_ICON_SCALE_DISPLAY, /* scale_rules */
|
||||
},
|
||||
delta_list_icon, /* list_icon */
|
||||
NULL, /* list_emblem */
|
||||
NULL, /* status_text */
|
||||
NULL, /* tooltip_text */
|
||||
delta_status_types, /* status_types */
|
||||
NULL, /* blist_node_menu */
|
||||
NULL, /* chat_info */
|
||||
NULL, /* chat_info_defaults */
|
||||
delta_login, /* login */
|
||||
delta_close, /* close */
|
||||
delta_send_im, /* send_im */
|
||||
NULL, /* set_info */
|
||||
NULL, /* send_typing */
|
||||
NULL, /* get_info */
|
||||
NULL, /* set_status */
|
||||
NULL, /* set_idle */
|
||||
NULL, /* change_passwd */
|
||||
NULL, /* add_buddy */
|
||||
NULL, /* add_buddies */
|
||||
NULL, /* remove_buddy */
|
||||
NULL, /* remove_buddies */
|
||||
NULL, /* add_permit */
|
||||
NULL, /* add_deny */
|
||||
NULL, /* rem_permit */
|
||||
NULL, /* rem_deny */
|
||||
NULL, /* set_permit_deny */
|
||||
NULL, /* join_chat */
|
||||
NULL, /* reject_chat */
|
||||
NULL, /* get_chat_name */
|
||||
NULL, /* chat_invite */
|
||||
NULL, /* chat_leave */
|
||||
NULL, /* chat_whisper */
|
||||
NULL, /* chat_send */
|
||||
NULL, /* keepalive */
|
||||
NULL, /* register_user */
|
||||
NULL, /* get_cb_info */
|
||||
NULL, /* get_cb_away */
|
||||
NULL, /* alias_buddy */
|
||||
NULL, /* group_buddy */
|
||||
NULL, /* rename_group */
|
||||
NULL, /* buddy_free */
|
||||
NULL, /* convo_closed */
|
||||
NULL, /* normalize */
|
||||
NULL, /* set_buddy_icon */
|
||||
NULL, /* remove_group */
|
||||
NULL, /* get_cb_real_name */
|
||||
NULL, /* set_chat_topic */
|
||||
NULL, /* find_blist_chat */
|
||||
NULL, /* roomlist_get_list */
|
||||
NULL, /* roomlist_cancel */
|
||||
NULL, /* roomlist_expand_category */
|
||||
NULL, /* can_receive_file */
|
||||
NULL, /* send_file */
|
||||
NULL, /* new_xfer */
|
||||
delta_offline_message, /* offline_message */
|
||||
NULL, /* whiteboard_prpl_ops */
|
||||
NULL, /* send_raw */
|
||||
NULL, /* roomlist_room_serialize */
|
||||
NULL, /* unregister_user */
|
||||
NULL, /* send_attention */
|
||||
NULL, /* get_attention_types */
|
||||
sizeof(PurplePluginProtocolInfo), /* struct_size */
|
||||
NULL, /* get_account_text_table */
|
||||
NULL, /* initiate_media */
|
||||
NULL, /* get_media_caps */
|
||||
NULL, /* get_moods */
|
||||
NULL, /* set_public_alias */
|
||||
NULL, /* get_public_alias */
|
||||
NULL, /* add_buddy_with_invite */
|
||||
NULL /* add_buddies_with_invite */
|
||||
};
|
||||
|
||||
|
||||
static PurplePluginInfo info = {
|
||||
PURPLE_PLUGIN_MAGIC,
|
||||
PURPLE_MAJOR_VERSION,
|
||||
PURPLE_MINOR_VERSION,
|
||||
PURPLE_PLUGIN_PROTOCOL,
|
||||
NULL, // UI requirements
|
||||
0, // flags
|
||||
NULL, // dependencies
|
||||
PURPLE_PRIORITY_DEFAULT,
|
||||
|
||||
PLUGIN_ID,
|
||||
"Delta Chat",
|
||||
"0.0.0",
|
||||
|
||||
"Delta Chat is an email-based instant messaging solution",
|
||||
"See https://delta.chat for more information",
|
||||
"Nick Thomas <delta@ur.gs>",
|
||||
"https://delta.chat",
|
||||
|
||||
NULL, // plugin_load
|
||||
NULL, // plugin_unload
|
||||
delta_destroy_plugin, // plugin_destroy
|
||||
|
||||
NULL, // ui_info
|
||||
&extra_info, // extra_info
|
||||
NULL, // prefs_info
|
||||
NULL, // actions
|
||||
|
||||
NULL, // reserved1
|
||||
NULL, // reserved2
|
||||
NULL, // reserved3
|
||||
NULL // reserved4
|
||||
};
|
||||
|
||||
PURPLE_INIT_PLUGIN(delta, delta_init_plugin, info)
|
35
libdelta.h
35
libdelta.h
@@ -1,35 +0,0 @@
|
||||
#ifndef LIBDELTA_H
|
||||
#define LIBDELTA_H
|
||||
|
||||
#define PLUGIN_ID "prpl-delta"
|
||||
|
||||
#define PLUGIN_CHAT_INFO_CHAT_ID "chat_id"
|
||||
|
||||
#define DELTA_PROTOCOL_OPTS \
|
||||
OPT_PROTO_UNIQUE_CHATNAME | \
|
||||
OPT_PROTO_CHAT_TOPIC | \
|
||||
OPT_PROTO_IM_IMAGE | \
|
||||
OPT_PROTO_MAIL_CHECK
|
||||
|
||||
// These two will instead be the pidgin "username" and "password" options that
|
||||
// I can't seem to get rid of.
|
||||
#define PLUGIN_ACCOUNT_OPT_ADDR "addr"
|
||||
#define PLUGIN_ACCOUNT_OPT_IMAP_PASS "mail_pw"
|
||||
|
||||
// Share the remaining keys between purple and delta
|
||||
#define PLUGIN_ACCOUNT_OPT_DISPLAY_NAME "displayname"
|
||||
|
||||
#define PLUGIN_ACCOUNT_OPT_IMAP_SERVER_HOST "mail_server"
|
||||
#define PLUGIN_ACCOUNT_OPT_IMAP_SERVER_PORT "mail_port"
|
||||
#define PLUGIN_ACCOUNT_OPT_IMAP_USER "mail_user"
|
||||
|
||||
#define PLUGIN_ACCOUNT_OPT_SMTP_SERVER_HOST "send_server"
|
||||
#define PLUGIN_ACCOUNT_OPT_SMTP_SERVER_PORT "send_port"
|
||||
#define PLUGIN_ACCOUNT_OPT_SMTP_USER "send_user"
|
||||
#define PLUGIN_ACCOUNT_OPT_SMTP_PASS "send_pw"
|
||||
|
||||
#define PLUGIN_ACCOUNT_OPT_BCC_SELF "bcc_self"
|
||||
|
||||
#define UNUSED(x) (void)(x)
|
||||
|
||||
#endif
|
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
@@ -0,0 +1 @@
|
||||
1.56.0
|
136
src/chat_info.rs
Normal file
136
src/chat_info.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
1
src/delta/mod.rs
Normal file
1
src/delta/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod system;
|
252
src/delta/system.rs
Normal file
252
src/delta/system.rs
Normal 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
800
src/lib.rs
Normal 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 <code>"));
|
||||
|
||||
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
120
src/logging.rs
Normal 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();
|
||||
}
|
||||
})
|
||||
}
|
101
src/messages/account_proxy.rs
Normal file
101
src/messages/account_proxy.rs
Normal 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())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
54
src/messages/connection_proxy.rs
Normal file
54
src/messages/connection_proxy.rs
Normal 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
|
||||
}
|
||||
}
|
43
src/messages/handle_proxy.rs
Normal file
43
src/messages/handle_proxy.rs
Normal 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
193
src/messages/mod.rs
Normal 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>,
|
||||
}
|
Reference in New Issue
Block a user