/*
* This file is part of telepathy-idle
*
* Copyright (C) 2006-2007 Collabora Limited
* Copyright (C) 2006-2007 Nokia Corporation
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* version 2.1 as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "idle-connection.h"
#include <config.h>
#include <string.h>
#include <time.h>
#define DBUS_API_SUBJECT_TO_CHANGE
#include <dbus/dbus-glib.h>
#include <telepathy-glib/enums.h>
#include <telepathy-glib/errors.h>
#include <telepathy-glib/interfaces.h>
#include <telepathy-glib/svc-connection.h>
#include <telepathy-glib/channel-manager.h>
#define IDLE_DEBUG_FLAG IDLE_DEBUG_CONNECTION
#include "idle-ctcp.h"
#include "idle-debug.h"
#include "idle-handles.h"
#include "idle-im-manager.h"
#include "idle-muc-manager.h"
#include "idle-parser.h"
#include "idle-server-connection.h"
#include "idle-server-connection-iface.h"
#include "idle-ssl-server-connection.h"
#include "extensions/extensions.h" /* Renaming */
/* From RFC 2813 :
* This in essence means that the client may send one (1) message every
* two (2) seconds without being adversely affected. Services MAY also
* be subject to this mechanism.
*/
#define MSG_QUEUE_UNLOAD_AT_A_TIME 1
#define MSG_QUEUE_TIMEOUT 2
#define SERVER_CMD_MIN_PRIORITY 0
#define SERVER_CMD_NORMAL_PRIORITY G_MAXUINT/2
#define SERVER_CMD_MAX_PRIORITY G_MAXUINT
/* FIXME use this from telepathy-glib as soon as it gets there */
#define IDLE_TP_ALIAS_PAIR_TYPE (dbus_g_type_get_struct ("GValueArray", \
G_TYPE_UINT, G_TYPE_STRING, G_TYPE_INVALID))
static void _free_alias_pair(gpointer data, gpointer user_data)
{
g_boxed_free(IDLE_TP_ALIAS_PAIR_TYPE, data);
}
static void _aliasing_iface_init(gpointer, gpointer);
static void _renaming_iface_init(gpointer, gpointer);
G_DEFINE_TYPE_WITH_CODE(IdleConnection, idle_connection, TP_TYPE_BASE_CONNECTION,
G_IMPLEMENT_INTERFACE(TP_TYPE_SVC_CONNECTION_INTERFACE_ALIASING, _aliasing_iface_init);
G_IMPLEMENT_INTERFACE(IDLE_TYPE_SVC_CONNECTION_INTERFACE_RENAMING, _renaming_iface_init));
typedef struct _IdleOutputPendingMsg IdleOutputPendingMsg;
struct _IdleOutputPendingMsg {
gchar *message;
guint priority;
guint64 id;
};
static IdleOutputPendingMsg *idle_output_pending_msg_new() {
IdleOutputPendingMsg *msg = g_slice_new(IdleOutputPendingMsg);
static guint64 last_id = 0;
msg->id = last_id++;
return msg;
}
static void idle_output_pending_msg_free(IdleOutputPendingMsg *msg) {
if (!msg)
return;
g_free(msg->message);
g_slice_free(IdleOutputPendingMsg, msg);
}
static gint pending_msg_compare(gconstpointer a, gconstpointer b, gpointer unused) {
const IdleOutputPendingMsg *msg1 = a, *msg2 = b;
if (msg1->priority == msg2->priority) {
/* prefer the message with the lower id */
return (msg1->id - msg2->id);
}
/* prefer the message with the higher priority */
return (msg2->priority - msg1->priority);
}
enum {
PROP_NICKNAME = 1,
PROP_SERVER,
PROP_PORT,
PROP_PASSWORD,
PROP_REALNAME,
PROP_USERNAME,
PROP_CHARSET,
PROP_QUITMESSAGE,
PROP_USE_SSL,
LAST_PROPERTY_ENUM
};
/* private structure */
typedef struct _IdleConnectionPrivate IdleConnectionPrivate;
struct _IdleConnectionPrivate {
/*
* network connection
*/
IdleServerConnectionIface *conn;
guint sconn_status;
/* IRC connection properties */
char *nickname;
char *server;
guint port;
char *password;
char *realname;
char *username;
char *charset;
char *quit_message;
gboolean use_ssl;
/* the string used by the a server as a prefix to any messages we send that
* it relays to other users. We need to know this so we can keep our sent
* messages short enough that they still fit in the 512-byte limit even with
* this prefix added */
char *relay_prefix;
/* output message queue */
GQueue *msg_queue;
/* UNIX time the last message was sent on */
time_t last_msg_sent;
/* GSource id for message queue unloading timeout */
guint msg_queue_timeout;
/* if we are quitting asynchronously */
gboolean quitting;
guint force_disconnect_id;
/* AliasChanged aggregation */
GPtrArray *queued_aliases;
TpHandleSet *queued_aliases_owners;
/* if idle_connection_dispose has already run once */
gboolean dispose_has_run;
};
#define IDLE_CONNECTION_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), IDLE_TYPE_CONNECTION, IdleConnectionPrivate))
static void _iface_create_handle_repos(TpBaseConnection *self, TpHandleRepoIface **repos);
static GPtrArray *_iface_create_channel_managers(TpBaseConnection *self);
static gchar *_iface_get_unique_connection_name(TpBaseConnection *self);
static void _iface_disconnected(TpBaseConnection *self);
static void _iface_shut_down(TpBaseConnection *self);
static gboolean _iface_start_connecting(TpBaseConnection *self, GError **error);
static IdleParserHandlerResult _erroneous_nickname_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static IdleParserHandlerResult _nick_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static IdleParserHandlerResult _nickname_in_use_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static IdleParserHandlerResult _ping_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static IdleParserHandlerResult _version_privmsg_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static IdleParserHandlerResult _welcome_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static IdleParserHandlerResult _whois_user_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data);
static void sconn_status_changed_cb(IdleServerConnectionIface *sconn, IdleServerConnectionState state, IdleServerConnectionStateReason reason, IdleConnection *conn);
static void sconn_received_cb(IdleServerConnectionIface *sconn, gchar *raw_msg, IdleConnection *conn);
static void irc_handshakes(IdleConnection *conn);
static void send_quit_request(IdleConnection *conn);
static void connection_connect_cb(IdleConnection *conn, gboolean success, TpConnectionStatusReason fail_reason);
static void connection_disconnect_cb(IdleConnection *conn, TpConnectionStatusReason reason);
static gboolean idle_connection_hton(IdleConnection *obj, const gchar *input, gchar **output, GError **_error);
static void idle_connection_ntoh(IdleConnection *obj, const gchar *input, gchar **output);
static void idle_connection_init(IdleConnection *obj) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(obj);
priv->sconn_status = SERVER_CONNECTION_STATE_NOT_CONNECTED;
priv->msg_queue = g_queue_new();
}
static GObject *idle_connection_constructor(GType type, guint n_params, GObjectConstructParam *params) {
IdleConnection *self = IDLE_CONNECTION(G_OBJECT_CLASS(idle_connection_parent_class)->constructor(type, n_params, params));
self->parser = g_object_new(IDLE_TYPE_PARSER, "connection", self, NULL);
return (GObject *) self;
}
static void idle_connection_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(obj);
switch (prop_id) {
case PROP_NICKNAME:
g_free(priv->nickname);
priv->nickname = g_value_dup_string(value);
break;
case PROP_SERVER:
g_free(priv->server);
priv->server = g_value_dup_string(value);
break;
case PROP_PORT:
priv->port = g_value_get_uint(value);
break;
case PROP_PASSWORD:
g_free(priv->password);
priv->password = g_value_dup_string(value);
break;
case PROP_REALNAME:
g_free(priv->realname);
priv->realname = g_value_dup_string(value);
break;
case PROP_USERNAME:
g_free(priv->username);
priv->username = g_value_dup_string(value);
break;
case PROP_CHARSET:
g_free(priv->charset);
priv->charset = g_value_dup_string(value);
break;
case PROP_QUITMESSAGE:
g_free(priv->quit_message);
priv->quit_message = g_value_dup_string(value);
break;
case PROP_USE_SSL:
priv->use_ssl = g_value_get_boolean(value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
break;
}
}
static void idle_connection_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(obj);
switch (prop_id) {
case PROP_NICKNAME:
g_value_set_string(value, priv->nickname);
break;
case PROP_SERVER:
g_value_set_string(value, priv->server);
break;
case PROP_PORT:
g_value_set_uint(value, priv->port);
break;
case PROP_PASSWORD:
g_value_set_string(value, priv->password);
break;
case PROP_REALNAME:
g_value_set_string(value, priv->realname);
break;
case PROP_USERNAME:
g_value_set_string(value, priv->username);
break;
case PROP_CHARSET:
g_value_set_string(value, priv->charset);
break;
case PROP_QUITMESSAGE:
g_value_set_string(value, priv->quit_message);
break;
case PROP_USE_SSL:
g_value_set_boolean(value, priv->use_ssl);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
break;
}
}
static void idle_connection_dispose (GObject *object) {
IdleConnection *self = IDLE_CONNECTION(object);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(self);
if (priv->dispose_has_run)
return;
priv->dispose_has_run = TRUE;
if (priv->msg_queue_timeout)
g_source_remove(priv->msg_queue_timeout);
if (priv->conn != NULL) {
g_object_unref(priv->conn);
priv->conn = NULL;
}
if (priv->queued_aliases_owners)
tp_handle_set_destroy(priv->queued_aliases_owners);
if (priv->queued_aliases)
g_ptr_array_free(priv->queued_aliases, TRUE);
g_object_unref(self->parser);
if (G_OBJECT_CLASS(idle_connection_parent_class)->dispose)
G_OBJECT_CLASS(idle_connection_parent_class)->dispose (object);
}
static void idle_connection_finalize (GObject *object) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(object);
IdleOutputPendingMsg *msg;
g_free(priv->nickname);
g_free(priv->server);
g_free(priv->password);
g_free(priv->realname);
g_free(priv->username);
g_free(priv->charset);
g_free(priv->relay_prefix);
g_free(priv->quit_message);
while ((msg = g_queue_pop_head(priv->msg_queue)) != NULL)
idle_output_pending_msg_free(msg);
g_queue_free(priv->msg_queue);
G_OBJECT_CLASS(idle_connection_parent_class)->finalize(object);
}
static void idle_connection_class_init(IdleConnectionClass *klass) {
GObjectClass *object_class = G_OBJECT_CLASS(klass);
TpBaseConnectionClass *parent_class = TP_BASE_CONNECTION_CLASS(klass);
GParamSpec *param_spec;
static const gchar *interfaces_always_present[] = {
TP_IFACE_CONNECTION_INTERFACE_ALIASING,
IDLE_IFACE_CONNECTION_INTERFACE_RENAMING,
TP_IFACE_CONNECTION_INTERFACE_REQUESTS,
NULL};
g_type_class_add_private(klass, sizeof(IdleConnectionPrivate));
object_class->constructor = idle_connection_constructor;
object_class->set_property = idle_connection_set_property;
object_class->get_property = idle_connection_get_property;
object_class->dispose = idle_connection_dispose;
object_class->finalize = idle_connection_finalize;
parent_class->create_handle_repos = _iface_create_handle_repos;
parent_class->get_unique_connection_name = _iface_get_unique_connection_name;
parent_class->create_channel_factories = NULL;
parent_class->create_channel_managers = _iface_create_channel_managers;
parent_class->connecting = NULL;
parent_class->connected = NULL;
parent_class->disconnected = _iface_disconnected;
parent_class->shut_down = _iface_shut_down;
parent_class->start_connecting = _iface_start_connecting;
parent_class->interfaces_always_present = interfaces_always_present;
param_spec = g_param_spec_string("nickname", "IRC nickname", "The nickname to be visible to others in IRC.", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB);
g_object_class_install_property(object_class, PROP_NICKNAME, param_spec);
param_spec = g_param_spec_string("server", "Hostname or IP of the IRC server to connect to", "The server used when establishing the connection.", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB);
g_object_class_install_property(object_class, PROP_SERVER, param_spec);
param_spec = g_param_spec_uint("port", "IRC server port", "The destination port used when establishing the connection.", 0, G_MAXUINT16, 0, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB | G_PARAM_CONSTRUCT);
g_object_class_install_property(object_class, PROP_PORT, param_spec);
param_spec = g_param_spec_string("password", "Server password", "Password to authenticate to the server with", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB);
g_object_class_install_property(object_class, PROP_PASSWORD, param_spec);
param_spec = g_param_spec_string("realname", "Real name", "The real name of the user connecting to IRC", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB);
g_object_class_install_property(object_class, PROP_REALNAME, param_spec);
param_spec = g_param_spec_string("username", "User name", "The username of the user connecting to IRC", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB);
g_object_class_install_property(object_class, PROP_USERNAME, param_spec);
param_spec = g_param_spec_string("charset", "Character set", "The character set to use to communicate with the outside world", "NULL", G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB | G_PARAM_CONSTRUCT);
g_object_class_install_property(object_class, PROP_CHARSET, param_spec);
param_spec = g_param_spec_string("quit-message", "Quit message", "The quit message to send to the server when leaving IRC", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB | G_PARAM_CONSTRUCT);
g_object_class_install_property(object_class, PROP_QUITMESSAGE, param_spec);
param_spec = g_param_spec_boolean("use-ssl", "Use SSL", "If the connection should use a SSL tunneled socket connection", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_BLURB | G_PARAM_CONSTRUCT);
g_object_class_install_property(object_class, PROP_USE_SSL, param_spec);
}
static GPtrArray *_iface_create_channel_managers(TpBaseConnection *self) {
GPtrArray *managers = g_ptr_array_sized_new(1);
GObject *manager;
manager = g_object_new(IDLE_TYPE_IM_MANAGER, "connection", self, NULL);
g_ptr_array_add(managers, manager);
manager = g_object_new(IDLE_TYPE_MUC_MANAGER, "connection", self, NULL);
g_ptr_array_add(managers, manager);
return managers;
}
static void _iface_create_handle_repos(TpBaseConnection *self, TpHandleRepoIface **repos) {
for (int i = 0; i < NUM_TP_HANDLE_TYPES; i++)
repos[i] = NULL;
idle_handle_repos_init(repos);
}
static gchar *_iface_get_unique_connection_name(TpBaseConnection *self) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(self);
return g_strdup_printf("%s@%s%p", priv->nickname, priv->server, self);
}
static gboolean _finish_shutdown_idle_func(gpointer data) {
TpBaseConnection *conn = TP_BASE_CONNECTION(data);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
if (priv->force_disconnect_id != 0) {
g_source_remove(priv->force_disconnect_id);
}
tp_base_connection_finish_shutdown(conn);
return FALSE;
}
static gboolean
_force_disconnect (gpointer data)
{
IdleConnection *conn = IDLE_CONNECTION(data);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
idle_server_connection_iface_disconnect(priv->conn, NULL);
return FALSE;
}
static void _iface_disconnected(TpBaseConnection *self) {
IdleConnection *conn = IDLE_CONNECTION(self);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
send_quit_request(conn);
priv->quitting = TRUE;
/* don't handle any more messages, we're quitting. See e.g. bug #19762 */
idle_parser_remove_handlers_by_data(conn->parser, conn);
/* schedule forceful disconnect for 2 seconds if the remote server doesn't
* respond or disconnect before then */
priv->force_disconnect_id = g_timeout_add_seconds(2, _force_disconnect, conn);
}
static void _iface_shut_down(TpBaseConnection *self) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(self);
GError *error = NULL;
if (priv->quitting)
return;
if (priv->sconn_status != SERVER_CONNECTION_STATE_NOT_CONNECTED) {
if (!idle_server_connection_iface_disconnect(priv->conn, &error))
g_error_free(error);
} else {
g_idle_add(_finish_shutdown_idle_func, self);;
}
}
static gboolean _iface_start_connecting(TpBaseConnection *self, GError **error) {
IdleConnection *conn = IDLE_CONNECTION(self);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
g_assert(priv->nickname != NULL);
g_assert(priv->server != NULL);
g_assert(priv->port > 0 && priv->port <= G_MAXUINT16);
if (priv->conn == NULL) {
GError *conn_error = NULL;
IdleServerConnectionIface *sconn;
GType connection_type = (priv->use_ssl) ? IDLE_TYPE_SSL_SERVER_CONNECTION : IDLE_TYPE_SERVER_CONNECTION;
if (!priv->realname || !priv->realname[0]) {
const gchar *g_realname = g_get_real_name();
g_free(priv->realname);
if (g_realname && g_realname[0] && strcmp(g_realname, "Unknown"))
priv->realname = g_strdup(g_realname);
else
priv->realname = g_strdup(priv->nickname);
}
if (!priv->username || !priv->username[0]) {
g_free(priv->username);
priv->username = g_strdup(priv->nickname);
}
sconn = IDLE_SERVER_CONNECTION_IFACE(g_object_new(connection_type, "host", priv->server, "port", priv->port, NULL));
g_signal_connect(sconn, "status-changed", (GCallback)(sconn_status_changed_cb), conn);
if (!idle_server_connection_iface_connect(sconn, &conn_error)) {
IDLE_DEBUG("server connection failed to connect: %s", conn_error->message);
g_set_error(error, TP_ERRORS, TP_ERROR_NETWORK_ERROR, "failed to open low-level network connection: %s", conn_error->message);
g_error_free(conn_error);
g_object_unref(sconn);
return FALSE;
}
priv->conn = sconn;
g_signal_connect(sconn, "received", (GCallback)(sconn_received_cb), conn);
idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_ERRONEOUSNICKNAME, _erroneous_nickname_handler, conn);
idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_NICKNAMEINUSE, _nickname_in_use_handler, conn);
idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WELCOME, _welcome_handler, conn);
idle_parser_add_handler(conn->parser, IDLE_PARSER_NUMERIC_WHOISUSER, _whois_user_handler, conn);
idle_parser_add_handler(conn->parser, IDLE_PARSER_CMD_PING, _ping_handler, conn);
idle_parser_add_handler_with_priority(conn->parser, IDLE_PARSER_PREFIXCMD_NICK, _nick_handler, conn, IDLE_PARSER_HANDLER_PRIORITY_FIRST);
idle_parser_add_handler(conn->parser, IDLE_PARSER_PREFIXCMD_PRIVMSG_USER, _version_privmsg_handler, conn);
irc_handshakes(conn);
} else {
IDLE_DEBUG("conn already open!");
g_set_error(error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, "connection already open!");
return FALSE;
}
return TRUE;
}
static gboolean msg_queue_timeout_cb(gpointer user_data);
static void sconn_status_changed_cb(IdleServerConnectionIface *sconn, IdleServerConnectionState state, IdleServerConnectionStateReason reason, IdleConnection *conn) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
TpConnectionStatusReason tp_reason;
/* cancel scheduled forced disconnect since we are now disconnected */
if (state == SERVER_CONNECTION_STATE_NOT_CONNECTED &&
priv->force_disconnect_id) {
g_source_remove(priv->force_disconnect_id);
priv->force_disconnect_id = 0;
}
IDLE_DEBUG("called with state %u", state);
switch (reason) {
case SERVER_CONNECTION_STATE_REASON_ERROR:
tp_reason = TP_CONNECTION_STATUS_REASON_NETWORK_ERROR;
break;
case SERVER_CONNECTION_STATE_REASON_REQUESTED:
tp_reason = TP_CONNECTION_STATUS_REASON_REQUESTED;
break;
default:
g_assert_not_reached();
break;
}
if (priv->quitting)
tp_reason = TP_CONNECTION_STATUS_REASON_REQUESTED;
switch (state) {
case SERVER_CONNECTION_STATE_NOT_CONNECTED:
if (conn->parent.status == TP_CONNECTION_STATUS_CONNECTING) {
connection_connect_cb(conn, FALSE, TP_CONNECTION_STATUS_REASON_NETWORK_ERROR);
connection_disconnect_cb(conn, tp_reason);
}
else {
connection_disconnect_cb(conn, tp_reason);
}
break;
case SERVER_CONNECTION_STATE_CONNECTING:
break;
case SERVER_CONNECTION_STATE_CONNECTED:
if ((priv->msg_queue_timeout == 0) && (g_queue_get_length(priv->msg_queue) > 0)) {
IDLE_DEBUG("we had messages in queue, start unloading them now");
priv->msg_queue_timeout = g_timeout_add(MSG_QUEUE_TIMEOUT, msg_queue_timeout_cb, conn);
}
break;
default:
g_assert_not_reached();
break;
}
priv->sconn_status = state;
}
static void sconn_received_cb(IdleServerConnectionIface *sconn, gchar *raw_msg, IdleConnection *conn) {
gchar *converted;
idle_connection_ntoh(conn, raw_msg, &converted);
idle_parser_receive(conn->parser, converted);
g_free(converted);
}
static gboolean msg_queue_timeout_cb(gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
int i, j;
IdleOutputPendingMsg *output_msg;
gchar msg[IRC_MSG_MAXLEN + 3];
GError *error = NULL;
IDLE_DEBUG("called");
if (priv->sconn_status != SERVER_CONNECTION_STATE_CONNECTED) {
IDLE_DEBUG("connection was not connected!");
priv->msg_queue_timeout = 0;
return FALSE;
}
output_msg = g_queue_peek_head(priv->msg_queue);
if (output_msg == NULL) {
priv->msg_queue_timeout = 0;
return FALSE;
}
g_strlcpy(msg, output_msg->message, IRC_MSG_MAXLEN + 3);
for (i = 1; i < MSG_QUEUE_UNLOAD_AT_A_TIME; i++) {
output_msg = g_queue_peek_nth(priv->msg_queue, i);
if ((output_msg != NULL) && ((strlen(msg) + strlen(output_msg->message)) < IRC_MSG_MAXLEN + 2))
strcat(msg, output_msg->message);
else
break;
}
if (idle_server_connection_iface_send(priv->conn, msg, &error)) {
for (j = 0; j < i; j++) {
output_msg = g_queue_pop_head(priv->msg_queue);
idle_output_pending_msg_free(output_msg);
}
priv->last_msg_sent = time(NULL);
} else {
IDLE_DEBUG("low-level network connection failed to send: %s", error->message);
g_error_free(error);
}
return TRUE;
}
/**
* Queue a IRC command for sending, clipping it to IRC_MSG_MAXLEN bytes and appending the required <CR><LF> to it
*/
static void _send_with_priority(IdleConnection *conn, const gchar *msg, guint priority) {
gchar cmd[IRC_MSG_MAXLEN + 3];
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
int len;
GError *error = NULL;
gchar *converted;
GError *convert_error = NULL;
time_t curr_time = time(NULL);
IdleOutputPendingMsg *output_msg;
g_assert(msg != NULL);
/* Clip the message */
g_strlcpy(cmd, msg, IRC_MSG_MAXLEN + 1);
/* Append <CR><LF> */
len = strlen(cmd);
cmd[len++] = '\r';
cmd[len++] = '\n';
cmd[len] = '\0';
if (!idle_connection_hton(conn, cmd, &converted, &convert_error)) {
IDLE_DEBUG("hton: %s", convert_error->message);
g_error_free(convert_error);
converted = g_strdup(cmd);
}
if ((priority == SERVER_CMD_MAX_PRIORITY) || ((conn->parent.status == TP_CONNECTION_STATUS_CONNECTED) && (priv->msg_queue_timeout == 0) && (curr_time - priv->last_msg_sent > MSG_QUEUE_TIMEOUT))) {
priv->last_msg_sent = curr_time;
if (!idle_server_connection_iface_send(priv->conn, converted, &error)) {
IDLE_DEBUG("server connection failed to send: %s", error->message);
g_error_free(error);
} else {
g_free(converted);
return;
}
}
output_msg = idle_output_pending_msg_new();
output_msg->message = converted;
output_msg->priority = priority;
g_queue_insert_sorted(priv->msg_queue, output_msg, pending_msg_compare, NULL);
if (!priv->msg_queue_timeout)
priv->msg_queue_timeout = g_timeout_add(MSG_QUEUE_TIMEOUT * 1000, msg_queue_timeout_cb, conn);
}
void idle_connection_send(IdleConnection *conn, const gchar *msg) {
_send_with_priority(conn, msg, SERVER_CMD_NORMAL_PRIORITY);
}
gsize
idle_connection_get_max_message_length(IdleConnection *conn)
{
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
if (priv->relay_prefix != NULL) {
/* server will add ':<relay_prefix> ' to all messages it relays on to
* other users. the +2 is for the initial : and the trailing space */
return IRC_MSG_MAXLEN - (strlen(priv->relay_prefix) + 2);
}
/* Before we've gotten our user info, we don't know how long our relay
* prefix will be, so just assume worst-case. The max possible prefix is:
* ':<15 char nick>!<? char username>@<63 char hostname> ' == 1 + 15 + 1 + ?
* + 1 + 63 + 1 == 82 + ?
* I haven't been able to find a definitive reference for the max username
* length, but the testing I've done seems to indicate that 8-10 is a
* common limit. I'll add some extra buffer to be safe.
* */
return IRC_MSG_MAXLEN - 100;
}
static IdleParserHandlerResult _erroneous_nickname_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
if (conn->parent.status == TP_CONNECTION_STATUS_CONNECTING)
connection_connect_cb(conn, FALSE, TP_CONNECTION_STATUS_REASON_AUTHENTICATION_FAILED);
return IDLE_PARSER_HANDLER_RESULT_HANDLED;
}
static IdleParserHandlerResult _nick_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
TpHandle old_handle = g_value_get_uint(g_value_array_get_nth(args, 0));
TpHandle new_handle = g_value_get_uint(g_value_array_get_nth(args, 1));
if (old_handle == new_handle)
return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
if (old_handle == conn->parent.self_handle) {
IDLE_DEBUG("Self renamed: handle was %d, now %d", old_handle, new_handle);
tp_base_connection_set_self_handle(TP_BASE_CONNECTION(conn), new_handle);
}
idle_svc_connection_interface_renaming_emit_renamed(IDLE_SVC_CONNECTION_INTERFACE_RENAMING(conn), old_handle, new_handle);
idle_connection_emit_queued_aliases_changed(conn);
return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}
static IdleParserHandlerResult _nickname_in_use_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
if (conn->parent.status == TP_CONNECTION_STATUS_CONNECTING)
connection_connect_cb(conn, FALSE, TP_CONNECTION_STATUS_REASON_NAME_IN_USE);
return IDLE_PARSER_HANDLER_RESULT_HANDLED;
}
static IdleParserHandlerResult _ping_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
gchar *reply = g_strdup_printf("PONG %s", g_value_get_string(g_value_array_get_nth(args, 0)));
_send_with_priority(conn, reply, SERVER_CMD_MAX_PRIORITY);
g_free(reply);
return IDLE_PARSER_HANDLER_RESULT_HANDLED;
}
static IdleParserHandlerResult _version_privmsg_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
const gchar *msg = g_value_get_string(g_value_array_get_nth(args, 2));
if (g_ascii_strcasecmp(msg, "\001VERSION\001"))
return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
TpHandle handle = g_value_get_uint(g_value_array_get_nth(args, 0));
const gchar *nick = tp_handle_inspect(tp_base_connection_get_handles(TP_BASE_CONNECTION(conn), TP_HANDLE_TYPE_CONTACT), handle);
gchar *reply = g_strdup_printf("VERSION telepathy-idle %s Telepathy IM/VoIP Framework http://telepathy.freedesktop.org", VERSION);
idle_ctcp_notice(nick, reply, conn);
g_free(reply);
return IDLE_PARSER_HANDLER_RESULT_HANDLED;
}
static IdleParserHandlerResult _welcome_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data) {
IdleConnection *conn = IDLE_CONNECTION(user_data);
TpHandle handle = g_value_get_uint(g_value_array_get_nth(args, 0));
tp_base_connection_set_self_handle(TP_BASE_CONNECTION(conn), handle);
connection_connect_cb(conn, TRUE, 0);
return IDLE_PARSER_HANDLER_RESULT_HANDLED;
}
static IdleParserHandlerResult
_whois_user_handler(IdleParser *parser, IdleParserMessageCode code, GValueArray *args, gpointer user_data)
{
IdleConnection *conn = IDLE_CONNECTION(user_data);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
/* message format: <nick> <user> <host> * :<real name> */
TpHandle handle = g_value_get_uint(g_value_array_get_nth(args, 0));
TpHandle self = tp_base_connection_get_self_handle(TP_BASE_CONNECTION(conn));
if (handle == self) {
if (priv->relay_prefix != NULL) {
g_free(priv->relay_prefix);
}
const char* user = g_value_get_string(g_value_array_get_nth(args, 1));
const char* host = g_value_get_string(g_value_array_get_nth(args, 2));
priv->relay_prefix = g_strdup_printf("%s!%s@%s", priv->nickname, user, host);
IDLE_DEBUG("user host prefix = %s", priv->relay_prefix);
return IDLE_PARSER_HANDLER_RESULT_HANDLED;
}
return IDLE_PARSER_HANDLER_RESULT_NOT_HANDLED;
}
static void irc_handshakes(IdleConnection *conn) {
IdleConnectionPrivate *priv;
gchar msg[IRC_MSG_MAXLEN + 1];
g_assert(conn != NULL);
g_assert(IDLE_IS_CONNECTION(conn));
priv = IDLE_CONNECTION_GET_PRIVATE(conn);
if ((priv->password != NULL) && (priv->password[0] != '\0')) {
g_snprintf(msg, IRC_MSG_MAXLEN + 1, "PASS %s", priv->password);
_send_with_priority(conn, msg, SERVER_CMD_NORMAL_PRIORITY + 1);
}
g_snprintf(msg, IRC_MSG_MAXLEN + 1, "NICK %s", priv->nickname);
idle_connection_send(conn, msg);
g_snprintf(msg, IRC_MSG_MAXLEN + 1, "USER %s %u * :%s", priv->username, 8, priv->realname);
idle_connection_send(conn, msg);
/* gather some information about ourselves */
g_snprintf(msg, IRC_MSG_MAXLEN + 1, "WHOIS %s", priv->nickname);
idle_connection_send(conn, msg);
}
static void send_quit_request(IdleConnection *conn) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
gchar cmd[IRC_MSG_MAXLEN + 1] = "QUIT";
if (priv->quit_message != NULL)
g_snprintf(cmd, IRC_MSG_MAXLEN + 1, "QUIT :%s", priv->quit_message);
_send_with_priority(conn, cmd, SERVER_CMD_MAX_PRIORITY);
}
static void connection_connect_cb(IdleConnection *conn, gboolean success, TpConnectionStatusReason fail_reason) {
TpBaseConnection *base = TP_BASE_CONNECTION(conn);
if (success)
tp_base_connection_change_status(base, TP_CONNECTION_STATUS_CONNECTED, TP_CONNECTION_STATUS_REASON_REQUESTED);
else
tp_base_connection_change_status(base, TP_CONNECTION_STATUS_DISCONNECTED, fail_reason);
}
static void connection_disconnect_cb(IdleConnection *conn, TpConnectionStatusReason reason) {
TpBaseConnection *base = TP_BASE_CONNECTION(conn);
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
if (base->status == TP_CONNECTION_STATUS_DISCONNECTED)
g_idle_add(_finish_shutdown_idle_func, base);
else
tp_base_connection_change_status(base, TP_CONNECTION_STATUS_DISCONNECTED, reason);
if (priv->msg_queue_timeout) {
g_source_remove(priv->msg_queue_timeout);
priv->msg_queue_timeout = 0;
}
}
void _queue_alias_changed(IdleConnection *conn, TpHandle handle, const gchar *alias) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
if (!priv->queued_aliases_owners) {
TpHandleRepoIface *handles = tp_base_connection_get_handles(TP_BASE_CONNECTION(conn), TP_HANDLE_TYPE_CONTACT);
priv->queued_aliases_owners = tp_handle_set_new(handles);
}
tp_handle_set_add(priv->queued_aliases_owners, handle);
if (!priv->queued_aliases)
priv->queued_aliases = g_ptr_array_new();
GValue value = {0, };
g_value_init(&value, IDLE_TP_ALIAS_PAIR_TYPE);
g_value_take_boxed(&value, dbus_g_type_specialized_construct(IDLE_TP_ALIAS_PAIR_TYPE));
dbus_g_type_struct_set(&value,
0, handle,
1, alias,
G_MAXUINT);
g_ptr_array_add(priv->queued_aliases, g_value_get_boxed(&value));
}
static GQuark _canon_nick_quark() {
static GQuark quark = 0;
if (!quark)
quark = g_quark_from_static_string("canon-nick");
return quark;
}
void idle_connection_canon_nick_receive(IdleConnection *conn, TpHandle handle, const gchar *canon_nick) {
TpHandleRepoIface *handles = tp_base_connection_get_handles(TP_BASE_CONNECTION(conn), TP_HANDLE_TYPE_CONTACT);
const gchar *old_alias = tp_handle_get_qdata(handles, handle, _canon_nick_quark());
if (!old_alias)
old_alias = tp_handle_inspect(handles, handle);
if (!strcmp(old_alias, canon_nick))
return;
tp_handle_set_qdata(handles, handle, _canon_nick_quark(), g_strdup(canon_nick), g_free);
_queue_alias_changed(conn, handle, canon_nick);
}
void idle_connection_emit_queued_aliases_changed(IdleConnection *conn) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(conn);
if (!priv->queued_aliases)
return;
tp_svc_connection_interface_aliasing_emit_aliases_changed(conn, priv->queued_aliases);
g_ptr_array_foreach(priv->queued_aliases, _free_alias_pair, NULL);
g_ptr_array_free(priv->queued_aliases, TRUE);
priv->queued_aliases = NULL;
tp_handle_set_destroy(priv->queued_aliases_owners);
priv->queued_aliases_owners = NULL;
}
static void idle_connection_get_alias_flags(TpSvcConnectionInterfaceAliasing *iface, DBusGMethodInvocation *context) {
tp_svc_connection_interface_aliasing_return_from_get_alias_flags(context, 0);
}
static void idle_connection_request_aliases(TpSvcConnectionInterfaceAliasing *iface, const GArray *handles, DBusGMethodInvocation *context) {
TpHandleRepoIface *repo = tp_base_connection_get_handles(TP_BASE_CONNECTION(iface), TP_HANDLE_TYPE_CONTACT);
GError *error = NULL;
if (!tp_handles_are_valid(repo, handles, FALSE, &error)) {
dbus_g_method_return_error(context, error);
g_error_free(error);
return;
}
const gchar **aliases = g_new0(const gchar *, handles->len + 1);
for (guint i = 0; i < handles->len; i++) {
TpHandle handle = g_array_index(handles, TpHandle, i);
const gchar *alias = tp_handle_get_qdata(repo, handle, _canon_nick_quark());
if (!alias)
alias = tp_handle_inspect(repo, handle);
aliases[i] = alias;
}
tp_svc_connection_interface_aliasing_return_from_request_aliases(context, aliases);
g_free(aliases);
}
static gboolean _send_rename_request(IdleConnection *obj, const gchar *nick, DBusGMethodInvocation *context) {
TpHandleRepoIface *handles = tp_base_connection_get_handles(TP_BASE_CONNECTION(obj), TP_HANDLE_TYPE_CONTACT);
TpHandle handle = tp_handle_ensure(handles, nick, NULL, NULL);
if (handle == 0) {
IDLE_DEBUG("failed to get handle for \"%s\"", nick);
GError error = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, "Invalid nickname requested"};
dbus_g_method_return_error(context, &error);
return FALSE;
}
tp_handle_unref(handles, handle);
gchar msg[IRC_MSG_MAXLEN + 1];
g_snprintf(msg, IRC_MSG_MAXLEN + 1, "NICK %s", nick);
idle_connection_send(obj, msg);
return TRUE;
}
static void idle_connection_request_rename(IdleSvcConnectionInterfaceRenaming *iface, const gchar *nick, DBusGMethodInvocation *context) {
IdleConnection *conn = IDLE_CONNECTION(iface);
if (_send_rename_request(conn, nick, context))
idle_svc_connection_interface_renaming_return_from_request_rename(context);
}
static void idle_connection_set_aliases(TpSvcConnectionInterfaceAliasing *iface, GHashTable *aliases, DBusGMethodInvocation *context) {
IdleConnection *conn = IDLE_CONNECTION(iface);
const gchar *requested_alias = g_hash_table_lookup(aliases, GUINT_TO_POINTER(conn->parent.self_handle));
if ((g_hash_table_size(aliases) != 1) || !requested_alias) {
GError error = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, "You can only set your own alias in IRC"};
dbus_g_method_return_error(context, &error);
return;
}
if (_send_rename_request(conn, requested_alias, context))
tp_svc_connection_interface_aliasing_return_from_set_aliases(context);
}
static gboolean idle_connection_hton(IdleConnection *obj, const gchar *input, gchar **output, GError **_error) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(obj);
GError *error = NULL;
gsize bytes_written;
gchar *ret;
if (input == NULL) {
*output = NULL;
return TRUE;
}
ret = g_convert(input, -1, priv->charset, "UTF-8", NULL, &bytes_written, &error);
if (ret == NULL) {
IDLE_DEBUG("g_convert failed: %s", error->message);
g_set_error(_error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, "character set conversion failed: %s", error->message);
g_error_free(error);
*output = NULL;
return FALSE;
}
*output = ret;
return TRUE;
}
static void idle_connection_ntoh(IdleConnection *obj, const gchar *input, gchar **output) {
IdleConnectionPrivate *priv = IDLE_CONNECTION_GET_PRIVATE(obj);
GError *error = NULL;
gsize bytes_written;
gchar *ret;
gchar *p;
if (input == NULL) {
*output = NULL;
return;
}
ret = g_convert(input, -1, "UTF-8", priv->charset, NULL, &bytes_written, &error);
if (ret == NULL) {
IDLE_DEBUG("charset conversion failed, falling back to US-ASCII: %s", error->message);
g_error_free(error);
ret = g_strdup(input);
for (p = ret; *p != '\0'; p++) {
if (*p & (1 << 7))
*p = '?';
}
}
*output = ret;
return;
}
static void _aliasing_iface_init(gpointer g_iface, gpointer iface_data) {
TpSvcConnectionInterfaceAliasingClass *klass = (TpSvcConnectionInterfaceAliasingClass *) g_iface;
#define IMPLEMENT(x) tp_svc_connection_interface_aliasing_implement_##x (\
klass, idle_connection_##x)
IMPLEMENT(get_alias_flags);
IMPLEMENT(request_aliases);
IMPLEMENT(set_aliases);
#undef IMPLEMENT
}
static void _renaming_iface_init(gpointer g_iface, gpointer iface_data) {
IdleSvcConnectionInterfaceRenamingClass *klass = (IdleSvcConnectionInterfaceRenamingClass *) g_iface;
#define IMPLEMENT(x) idle_svc_connection_interface_renaming_implement_##x (\
klass, idle_connection_##x)
IMPLEMENT(request_rename);
#undef IMPLEMENT
}