563 lines
16 KiB
C
563 lines
16 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) {
|
|
|
|
}
|
|
|
|
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: {
|
|
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);
|
|
purple_debug_info(PLUGIN_ID, "Configure progress: %d\n", dc_event_get_data1_int(event));
|
|
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);
|
|
|
|
purple_connection_set_state(pc, PURPLE_CONNECTED);
|
|
|
|
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_display_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);
|
|
}
|