Files
purple-plugin-delta/delta-connection.c
Nick Thomas 6d4454f356 Fix showing duplicated messages
This commit also has some other changes sprinkled in but the main
effect is that messages are only shown precisely once. Next stage must
be to move away from serv_got_im and friends.
2021-01-12 00:04:46 +00:00

561 lines
15 KiB
C

#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'>", image_id);
}
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);
}