Compare commits

...

4 Commits

14 changed files with 4964 additions and 921 deletions

8
.gitignore vendored
View File

@@ -1,6 +1,2 @@
/libdelta.so
/libetpan.so
/libdeltachat.so
/libnetpgp.so
/vendor/deltachat-core-0.35.0
/vendor/libetpan-1.8
/purple
/target

3842
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "purple-plugin-delta"
version = "0.1.0"
authors = ["Nick Thomas <delta@ur.gs>"]
[lib]
name = "purple_delta"
path = "src/lib.rs"
crate-type = ["dylib"]
[dependencies]
deltachat = { git = "https://github.com/deltachat/deltachat-core-rust", tag="v1.51.0" }
lazy_static = "1.4.0"
log = "0.4.8"
purple-rs = "*"
# Keep in sync with deltachat-core-rust
[dependencies.async-std]
version = "~1.8"
features = ["unstable"]
[profile.release]
lto = true

View File

@@ -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

View File

@@ -13,24 +13,27 @@ 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:
- [~] 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
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.
## Build
@@ -47,17 +50,17 @@ 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
`~/.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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View File

@@ -0,0 +1 @@
1.50.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,
}
}
}
*/

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

@@ -0,0 +1 @@

811
src/lib.rs Normal file
View File

@@ -0,0 +1,811 @@
extern crate async_std;
extern crate lazy_static;
extern crate log;
extern crate purple_rs as purple;
use async_std::sync::Arc; // RwLock
use chat_info::{ChatInfo, PartialChatInfo}; //ChatInfoVersion
use lazy_static::lazy_static;
//use messages::{AccountInfo, ICQSystemHandle, 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};
mod chat_info;
mod delta;
pub mod logging;
//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 {
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,
// session: RwLock<Option<icq::protocol::SessionInfo>>,
}
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: ICQSystemHandle,
connections: purple::Connections<AccountDataBox>,
input_handle: Option<u32>,
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 = icq::system::spawn();
Self {
// system,
input_handle: None,
imex_command_handle: None,
connections: purple::Connections::new(),
}
}
fn register(&self, context: RegisterContext<Self>) -> RegisterContext<Self> {
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())
// Username and password are mail_user and mail_pw
.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 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_username().unwrap().into();
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 {
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),
// session: RwLock::new(None),
});
// 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);
//use std::os::unix::io::AsRawFd;
/*
self.input_handle = Some(self.enable_input(
self.system.input_rx.as_raw_fd(),
purple::PurpleInputCondition::PURPLE_INPUT_READ,
));
*/
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::InputHandler for PurpleDelta {
fn input(&mut self, _fd: i32, _cond: purple::PurpleInputCondition) {
log::debug!("Input");
/*
// Consume the byte from the input pipe.
let mut buf = [0; 1];
self.system
.input_rx
.read_exact(&mut buf)
.expect("Failed to read input pipe");
// Consume the actual message.
match self.system.rx.try_recv() {
Ok(message) => self.process_message(message),
Err(async_std::sync::TryRecvError::Empty) => log::error!("Expected message, but empty"),
Err(async_std::sync::TryRecvError::Disconnected) => {
log::error!("System disconnected");
if let Some(input_handle) = self.input_handle {
self.disable_input(input_handle);
}
}
};
*/
}
}
*/
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) {
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);

121
src/logging.rs Normal file
View File

@@ -0,0 +1,121 @@
// 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();
}
})
}