aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile22
-rw-r--r--bitlbee.c21
-rw-r--r--bitlbee.h2
-rw-r--r--conf.c41
-rw-r--r--conf.h7
-rwxr-xr-xconfigure14
-rw-r--r--irc.c42
-rw-r--r--irc.h39
-rw-r--r--irc_cap.c1
-rw-r--r--irc_channel.c103
-rw-r--r--irc_commands.c13
-rw-r--r--irc_im.c33
-rw-r--r--irc_send.c78
-rw-r--r--irc_user.c2
-rw-r--r--lib/ssl_client.h6
-rw-r--r--lib/ssl_gnutls.c73
-rw-r--r--protocols/account.c92
-rw-r--r--protocols/jabber/jabber.c6
-rw-r--r--protocols/jabber/jabber.h18
-rw-r--r--protocols/jabber/sasl.c263
-rw-r--r--protocols/nogaim.c24
-rw-r--r--protocols/purple/purple.c24
-rw-r--r--protocols/twitter/twitter.c113
-rw-r--r--protocols/twitter/twitter.h13
-rw-r--r--protocols/twitter/twitter_lib.c245
-rw-r--r--protocols/twitter/twitter_lib.h10
-rw-r--r--root_commands.c66
-rw-r--r--set.c2
-rw-r--r--storage.c34
-rw-r--r--storage.h1
30 files changed, 1258 insertions, 150 deletions
diff --git a/Makefile b/Makefile
index ab439fb9..943c7d30 100644
--- a/Makefile
+++ b/Makefile
@@ -25,10 +25,14 @@ ifdef DOC
$(MAKE) -C doc
endif
-uninstall: uninstall-bin uninstall-doc
+ifdef DEVEL
+INSTALL_DEV = install-dev
+UNINSTALL_DEV = uninstall-dev
+endif
+uninstall: uninstall-bin uninstall-doc $(UNINSTALL_DEV)
@echo -e '\nmake uninstall does not remove files in '$(DESTDIR)$(ETCDIR)', you can use make uninstall-etc to do that.\n'
-install: install-bin install-doc install-plugins
+install: install-bin install-doc install-plugins install-etc $(INSTALL_DEV)
@echo
@echo Installed successfully
@echo
@@ -37,7 +41,9 @@ install: install-bin install-doc install-plugins
ifdef SYSTEMDSYSTEMUNITDIR
@echo If you want to start BitlBee using systemd, try \"make install-systemd\".
endif
+ifndef DEVEL
@echo To be able to compile third party plugins, run \"make install-dev\"
+endif
@echo
.PHONY: install install-bin install-etc install-doc install-plugins install-systemd install-dev \
@@ -110,12 +116,20 @@ uninstall-dev:
install-etc:
mkdir -p $(DESTDIR)$(ETCDIR)
- $(INSTALL) -m 0644 $(_SRCDIR_)motd.txt $(DESTDIR)$(ETCDIR)/motd.txt
- $(INSTALL) -m 0644 $(_SRCDIR_)bitlbee.conf $(DESTDIR)$(ETCDIR)/bitlbee.conf
+ $(INSTALL) -m 0644 $(_SRCDIR_)motd.txt $(DESTDIR)$(ETCDIR)/motd.txt.sample
+ $(INSTALL) -m 0644 $(_SRCDIR_)bitlbee.conf $(DESTDIR)$(ETCDIR)/bitlbee.conf.sample
+ @if ! [ -e $(DESTDIR)$(ETCDIR)/motd.txt ]; then \
+ $(INSTALL) -m 0644 $(_SRCDIR_)motd.txt $(DESTDIR)$(ETCDIR)/motd.txt; \
+ fi
+ @if ! [ -e $(DESTDIR)$(ETCDIR)/bitlbee.conf ]; then \
+ $(INSTALL) -m 0644 $(_SRCDIR_)bitlbee.conf $(DESTDIR)$(ETCDIR)/bitlbee.conf; \
+ fi
uninstall-etc:
rm -f $(DESTDIR)$(ETCDIR)/motd.txt
+ rm -f $(DESTDIR)$(ETCDIR)/motd.txt.sample
rm -f $(DESTDIR)$(ETCDIR)/bitlbee.conf
+ rm -f $(DESTDIR)$(ETCDIR)/bitlbee.conf.sample
-rmdir $(DESTDIR)$(ETCDIR)
install-plugins: install-plugin-otr
diff --git a/bitlbee.c b/bitlbee.c
index 75f78949..eb73a329 100644
--- a/bitlbee.c
+++ b/bitlbee.c
@@ -29,6 +29,7 @@
#include "protocols/nogaim.h"
#include "help.h"
#include "ipc.h"
+#include "lib/ssl_client.h"
#include <signal.h>
#include <stdio.h>
#include <errno.h>
@@ -175,6 +176,10 @@ int bitlbee_daemon_init()
log_link(LOGLVL_WARNING, LOGOUTPUT_SYSLOG);
}
+ if (global.conf->ssl) {
+ ssl_setup_server();
+ }
+
return(0);
}
@@ -193,7 +198,11 @@ gboolean bitlbee_io_current_client_read(gpointer data, gint fd, b_input_conditio
char line[513];
int st;
- st = read(irc->fd, line, sizeof(line) - 1);
+ if (global.conf->ssl) {
+ st = ssl_server_read(irc, line, sizeof(line) - 1);
+ } else {
+ st = read(irc->fd, line, sizeof(line) - 1);
+ }
if (st == 0) {
irc_abort(irc, 1, "Connection reset by peer");
return FALSE;
@@ -242,7 +251,11 @@ gboolean bitlbee_io_current_client_write(gpointer data, gint fd, b_input_conditi
}
size = strlen(irc->sendbuffer);
- st = write(irc->fd, irc->sendbuffer, size);
+ if (global.conf->ssl) {
+ st = ssl_server_write(irc, irc->sendbuffer, size);
+ } else {
+ st = write(irc->fd, irc->sendbuffer, size);
+ }
if (st == 0 || (st < 0 && !sockerr_again())) {
irc_abort(irc, 1, "Write error: %s", strerror(errno));
@@ -322,6 +335,10 @@ static gboolean bitlbee_io_new_client(gpointer data, gint fd, b_input_condition
/* Make the connection. */
irc = irc_new(new_socket);
+ if (global.conf->ssl) {
+ ssl_accept(irc);
+ }
+
/* We can store the IPC fd there now. */
global.listen_socket = fds[1];
global.listen_watch_source_id = b_input_add(fds[1], B_EV_IO_READ, ipc_child_read, irc);
diff --git a/bitlbee.h b/bitlbee.h
index f0bd68c8..879655c8 100644
--- a/bitlbee.h
+++ b/bitlbee.h
@@ -35,7 +35,9 @@ extern "C" {
#endif
#define PACKAGE "BitlBee"
+#ifndef BITLBEE_VERSION
#define BITLBEE_VERSION "3.5.1"
+#endif
#define VERSION BITLBEE_VERSION
#define BITLBEE_VER(a, b, c) (((a) << 16) + ((b) << 8) + (c))
#define BITLBEE_VERSION_CODE BITLBEE_VER(3, 5, 1)
diff --git a/conf.c b/conf.c
index 8c2439e7..83495d15 100644
--- a/conf.c
+++ b/conf.c
@@ -70,6 +70,11 @@ conf_t *conf_load(int argc, char *argv[])
conf->ft_listen = NULL;
conf->protocols = NULL;
conf->cafile = NULL;
+#ifdef WITH_GNUTLS
+ conf->ssl = FALSE;
+ conf->ssl_cert = NULL;
+ conf->ssl_key = NULL;
+#endif /* WITH_GNUTLS */
proxytype = 0;
i = conf_loadini(conf, global.conf_file);
@@ -171,6 +176,23 @@ conf_t *conf_load(int argc, char *argv[])
return NULL;
}
+#ifdef WITH_GNUTLS
+ if (conf->ssl && (!conf->ssl_cert || !conf->ssl_key)) {
+ fprintf(stderr, "Error: SSL enabled but cert or key is missing\n");
+ return NULL;
+ }
+
+ if (conf->ssl && conf->ssl_cert && access(conf->ssl_cert, R_OK) != 0) {
+ fprintf(stderr, "Error: Could not read SSL Cert %s: %s\n", conf->ssl_cert, strerror(errno));
+ return NULL;
+ }
+
+ if (conf->ssl && conf->ssl_key && access(conf->ssl_key, R_OK) != 0) {
+ fprintf(stderr, "Error: Could not read SSL Key %s: %s\n", conf->ssl_key, strerror(errno));
+ return NULL;
+ }
+#endif /* WITH_GNUTLS */
+
return conf;
}
@@ -238,6 +260,8 @@ static int conf_loadini(conf_t *conf, char *file)
conf->authmode = AUTHMODE_REGISTERED;
} else if (g_strcasecmp(ini->value, "closed") == 0) {
conf->authmode = AUTHMODE_CLOSED;
+ } else if (g_strcasecmp(ini->value, "sasl") == 0) {
+ conf->authmode = AUTHMODE_SASL;
} else {
conf->authmode = AUTHMODE_OPEN;
}
@@ -343,6 +367,23 @@ static int conf_loadini(conf_t *conf, char *file)
} else if (g_strcasecmp(ini->key, "cafile") == 0) {
g_free(conf->cafile);
conf->cafile = g_strdup(ini->value);
+#ifdef WITH_GNUTLS
+ } else if (g_strcasecmp(ini->key, "ssl") == 0) {
+ if (g_strcasecmp(ini->value, "true") == 0) {
+ conf->ssl = TRUE;
+ } else if (g_strcasecmp(ini->value, "false") == 0) {
+ conf->ssl = FALSE;
+ } else {
+ fprintf(stderr, "Invalid %s value: %s\n", ini->key, ini->value);
+ return 0;
+ }
+ } else if (g_strcasecmp(ini->key, "ssl_cert") == 0) {
+ g_free(conf->ssl_cert);
+ conf->ssl_cert = g_strdup(ini->value);
+ } else if (g_strcasecmp(ini->key, "ssl_key") == 0) {
+ g_free(conf->ssl_key);
+ conf->ssl_key = g_strdup(ini->value);
+#endif /* WITH_GNUTLS */
} else {
fprintf(stderr, "Error: Unknown setting `%s` in configuration file (line %d).\n",
ini->key, ini->line);
diff --git a/conf.h b/conf.h
index cd600775..3305844a 100644
--- a/conf.h
+++ b/conf.h
@@ -27,7 +27,7 @@
#define __CONF_H
typedef enum runmode { RUNMODE_DAEMON, RUNMODE_FORKDAEMON, RUNMODE_INETD } runmode_t;
-typedef enum authmode { AUTHMODE_OPEN, AUTHMODE_CLOSED, AUTHMODE_REGISTERED } authmode_t;
+typedef enum authmode { AUTHMODE_OPEN, AUTHMODE_CLOSED, AUTHMODE_REGISTERED, AUTHMODE_SASL } authmode_t;
typedef struct conf {
char *iface_in, *iface_out;
@@ -55,6 +55,11 @@ typedef struct conf {
char *ft_listen;
char **protocols;
char *cafile;
+#ifdef WITH_GNUTLS
+ int ssl;
+ char *ssl_cert;
+ char *ssl_key;
+#endif
} conf_t;
G_GNUC_MALLOC conf_t *conf_load(int argc, char *argv[]);
diff --git a/configure b/configure
index b578164f..b7730445 100755
--- a/configure
+++ b/configure
@@ -310,6 +310,10 @@ else
[ -z "$CFLAGS" ] && CFLAGS="-g -O2 -fno-strict-aliasing"
fi
+if [ "$devel" = "1" ]; then
+ echo 'DEVEL=1' >> Makefile.settings
+fi
+
if [ "$pie" = "1" ]; then
echo 'CFLAGS_BITLBEE=-fPIE' >> Makefile.settings
echo 'LDFLAGS_BITLBEE=-pie' >> Makefile.settings
@@ -655,6 +659,10 @@ if [ "$ret" = "0" ]; then
exit 1
fi;
+if [ "$ssl" = "gnutls" ]; then
+ echo '#define WITH_GNUTLS' >> config.h
+fi
+
echo 'SSL_CLIENT=ssl_'$ssl'.o' >> Makefile.settings
if detect_nameser_has_ns_types; then
@@ -860,8 +868,8 @@ EOF
[ "$jabber" = "default-on" ] && jabber=0
[ "$oscar" = "default-on" ] && oscar=0
- echo '#undef PACKAGE' >> config.h
- echo '#define PACKAGE "BitlBee-LIBPURPLE"' >> config.h
+ #echo '#undef PACKAGE' >> config.h
+ #echo '#define PACKAGE "BitlBee-LIBPURPLE"' >> config.h
if [ "$events" = "libevent" ]; then
echo 'Warning: Some libpurple modules (including msn-pecan) do their event handling'
@@ -908,6 +916,8 @@ if [ "$protocols" = "PROTOCOLS = " ]; then
echo " BitlBee will run, but you will be unable to connect to IM servers!"
fi
+echo "EFLAGS+=$(pkg-config libidn --libs)" >> Makefile.settings
+
echo "PROTOCOLS = $protocols" >> Makefile.settings
echo "PROTOOBJS = $protoobjs" >> Makefile.settings
diff --git a/irc.c b/irc.c
index a73a3111..be804c45 100644
--- a/irc.c
+++ b/irc.c
@@ -37,6 +37,7 @@ static char *set_eval_charset(set_t *set, char *value);
static char *set_eval_password(set_t *set, char *value);
static char *set_eval_bw_compat(set_t *set, char *value);
static char *set_eval_utf8_nicks(set_t *set, char *value);
+static char *set_eval_certfp(set_t *set, char *value);
irc_t *irc_new(int fd)
{
@@ -62,6 +63,8 @@ irc_t *irc_new(int fd)
irc->iconv = (GIConv) - 1;
irc->oconv = (GIConv) - 1;
+ irc->save_source_id = -1;
+
if (global.conf->ping_interval > 0 && global.conf->ping_timeout > 0) {
irc->ping_source_id = b_timeout_add(global.conf->ping_interval * 1000, irc_userping, irc);
}
@@ -73,6 +76,7 @@ irc_t *irc_new(int fd)
b->ui = &irc_ui_funcs;
s = set_add(&b->set, "allow_takeover", "true", set_eval_bool, irc);
+ s = set_add(&b->set, "auto_save", "true", set_eval_bool, irc);
s = set_add(&b->set, "away_devoice", "true", set_eval_bw_compat, irc);
s->flags |= SET_HIDDEN;
s = set_add(&b->set, "away_reply_timeout", "3600", set_eval_int, irc);
@@ -88,6 +92,7 @@ irc_t *irc_new(int fd)
s = set_add(&b->set, "nick_underscores", "false", set_eval_bool, irc);
s = set_add(&b->set, "offline_user_quits", "true", set_eval_bool, irc);
s = set_add(&b->set, "ops", "both", set_eval_irc_channel_ops, irc);
+ s = set_add(&b->set, "ops_mode", "@", set_eval_irc_channel_ops_mode, irc);
s = set_add(&b->set, "paste_buffer", "false", set_eval_bool, irc);
s->old_key = g_strdup("buddy_sendbuffer");
s = set_add(&b->set, "paste_buffer_delay", "200", set_eval_int, irc);
@@ -106,6 +111,14 @@ irc_t *irc_new(int fd)
s = set_add(&b->set, "to_char", ": ", set_eval_to_char, irc);
s = set_add(&b->set, "typing_notice", "false", set_eval_bool, irc);
s = set_add(&b->set, "utf8_nicks", "false", set_eval_utf8_nicks, irc);
+ s = set_add(&b->set, "strict_away", "false", set_eval_bool, irc);
+ s = set_add(&b->set, "only_log_tags", "false", set_eval_bool, irc);
+#ifdef WITH_GNUTLS
+ if (global.conf->ssl) {
+ s = set_add(&b->set, "_certfp", NULL, set_eval_certfp, irc);
+ s->flags = SET_NULL_OK | SET_HIDDEN;
+ }
+#endif
irc->root = iu = irc_user_new(irc, ROOT_NICK);
iu->fullname = g_strdup(ROOT_FN);
@@ -246,6 +259,11 @@ void irc_free(irc_t * irc)
log_message(LOGLVL_INFO, "Destroying connection with fd %d", irc->fd);
+ if (irc->save_source_id != -1) {
+ b_event_remove(irc->save_source_id);
+ irc->save_source_id = -1;
+ }
+
if (irc->status & USTATUS_IDENTIFIED && set_getbool(&irc->b->set, "save_on_quit")) {
if (storage_save(irc, NULL, TRUE) != STORAGE_OK) {
log_message(LOGLVL_WARNING, "Error while saving settings for user %s", irc->user->nick);
@@ -288,8 +306,14 @@ void irc_free(irc_t * irc)
b_event_remove(irc->w_watch_source_id);
}
+ if (global.conf->ssl) {
+ gnutls_bye(irc->ssl_session, GNUTLS_SHUT_WR);
+ }
closesocket(irc->fd);
irc->fd = -1;
+ if (global.conf->ssl) {
+ gnutls_deinit(irc->ssl_session);
+ }
g_hash_table_foreach_remove(irc->nick_user_hash, irc_free_hashkey, NULL);
g_hash_table_destroy(irc->nick_user_hash);
@@ -672,7 +696,12 @@ void irc_flush(irc_t *irc)
}
len = strlen(irc->sendbuffer);
- if ((n = send(irc->fd, irc->sendbuffer, len, 0)) == len) {
+ if (global.conf->ssl) {
+ n = ssl_server_write(irc, irc->sendbuffer, len);
+ } else {
+ n = send(irc->fd, irc->sendbuffer, len, 0);
+ }
+ if (n == len) {
g_free(irc->sendbuffer);
irc->sendbuffer = NULL;
@@ -747,6 +776,11 @@ int irc_check_login(irc_t *irc)
if (global.conf->authmode == AUTHMODE_CLOSED && !(irc->status & USTATUS_AUTHORIZED)) {
irc_send_num(irc, 464, ":This server is password-protected.");
return 0;
+ } else if (global.conf->authmode == AUTHMODE_SASL &&
+ (!(irc->caps & CAP_SASL) ||
+ !(irc->status & USTATUS_SASL_AUTHENTICATED))) {
+ irc_send_num(irc, 464, ":This server requires sasl.");
+ return 0;
} else {
irc_channel_t *ic;
irc_user_t *iu = irc->user;
@@ -988,6 +1022,12 @@ static char *set_eval_utf8_nicks(set_t *set, char *value)
return set_eval_bool(set, value);
}
+static char *set_eval_certfp(set_t *set, char *value)
+{
+ /* XXX: What should we do here? */
+ return value;
+}
+
void register_irc_plugin(const struct irc_plugin *p)
{
irc_plugins = g_slist_prepend(irc_plugins, (gpointer) p);
diff --git a/irc.h b/irc.h
index b2a088d0..a5bd0de2 100644
--- a/irc.h
+++ b/irc.h
@@ -26,6 +26,13 @@
#ifndef _IRC_H
#define _IRC_H
+#ifndef CONFIG
+#include "config.h"
+#endif
+
+#ifdef WITH_GNUTLS
+# include <gnutls/gnutls.h>
+#endif
#include <sys/socket.h>
#define IRC_MAX_LINE 512
@@ -53,6 +60,7 @@ typedef enum {
Currently just blocks irc_vawrite(). */
USTATUS_CAP_PENDING = 16,
USTATUS_SASL_PLAIN_PENDING = 32,
+ USTATUS_SASL_AUTHENTICATED = 64,
/* Not really status stuff, but other kinds of flags: For slightly
better password security, since the only way to send passwords
@@ -75,6 +83,7 @@ typedef enum {
CAP_EXTENDED_JOIN = (1 << 2),
CAP_AWAY_NOTIFY = (1 << 3),
CAP_USERHOST_IN_NAMES = (1 << 4),
+ CAP_SERVER_TIME = (1 << 5),
} irc_cap_flag_t;
struct irc_user;
@@ -91,6 +100,13 @@ typedef struct irc {
struct irc_user *root;
struct irc_user *user;
+#ifdef WITH_GNUTLS
+ int ssl;
+ char *certfp;
+
+ gnutls_session_t ssl_session;
+#endif
+
char *password; /* HACK: Used to save the user's password, but before
logging in, this may contain a password we should
send to identify after USER/NICK are received. */
@@ -110,6 +126,7 @@ typedef struct irc {
gint w_watch_source_id;
gint ping_source_id;
gint login_source_id; /* To slightly delay some events at login time. */
+ gint save_source_id;
struct otr *otr; /* OTR state and book keeping, used by the OTR plugin.
TODO: Some mechanism for plugindata. */
@@ -201,10 +218,12 @@ struct irc_channel_funcs {
};
typedef enum {
- IRC_CHANNEL_USER_OP = 1,
- IRC_CHANNEL_USER_HALFOP = 2,
- IRC_CHANNEL_USER_VOICE = 4,
- IRC_CHANNEL_USER_NONE = 8,
+ IRC_CHANNEL_USER_OWNER = 1,
+ IRC_CHANNEL_USER_ADMIN = 2,
+ IRC_CHANNEL_USER_OP = 4,
+ IRC_CHANNEL_USER_HALFOP = 8,
+ IRC_CHANNEL_USER_VOICE = 16,
+ IRC_CHANNEL_USER_NONE = 32,
} irc_channel_user_flags_t;
typedef struct irc_channel_user {
@@ -225,7 +244,7 @@ typedef enum {
struct irc_control_channel {
irc_control_channel_type_t type;
struct bee_group *group;
- struct account *account;
+ GSList *account;
struct prpl *protocol;
char modes[5];
};
@@ -309,6 +328,7 @@ struct irc_channel *irc_channel_with_user(irc_t *irc, irc_user_t *iu);
int irc_channel_set_topic(irc_channel_t *ic, const char *topic, const irc_user_t *who);
void irc_channel_user_set_mode(irc_channel_t *ic, irc_user_t *iu, irc_channel_user_flags_t flags);
void irc_channel_set_mode(irc_channel_t *ic, const char *s);
+struct account;
void irc_channel_auto_joins(irc_t *irc, struct account *acc);
void irc_channel_printf(irc_channel_t *ic, char *format, ...) G_GNUC_PRINTF(2, 3);
gboolean irc_channel_name_ok(const char *name);
@@ -316,9 +336,10 @@ void irc_channel_name_strip(char *name);
int irc_channel_name_cmp(const char *a_, const char *b_);
char *irc_channel_name_gen(irc_t *irc, const char *name);
gboolean irc_channel_name_hint(irc_channel_t *ic, const char *name);
-void irc_channel_update_ops(irc_channel_t *ic, char *value);
+void irc_channel_update_ops(irc_channel_t *ic, char *ops, char *ops_mode);
char irc_channel_user_get_prefix(irc_channel_user_t *icu);
char *set_eval_irc_channel_ops(struct set *set, char *value);
+char *set_eval_irc_channel_ops_mode(struct set *set, char *value);
gboolean irc_channel_wants_user(irc_channel_t *ic, irc_user_t *iu);
/* irc_commands.c */
@@ -343,6 +364,12 @@ void irc_send_who(irc_t *irc, GSList *l, const char *channel);
void irc_send_msg(irc_user_t *iu, const char *type, const char *dst, const char *msg, const char *prefix);
void irc_send_msg_raw(irc_user_t *iu, const char *type, const char *dst, const char *msg);
void irc_send_msg_f(irc_user_t *iu, const char *type, const char *dst, const char *format, ...) G_GNUC_PRINTF(4, 5);
+
+void irc_send_tagged_msg(irc_user_t *iu, const char *type, const char *dst, const char *msg, const char *prefix, const char *tags);
+void irc_send_tagged_msg_raw(irc_user_t *iu, const char *type, const char *dst, const char *msg, const char *tags);
+void irc_send_tagged_msg_f(irc_user_t *iu, const char *type, const char *dst, const char *tags, const char *format, ...) G_GNUC_PRINTF(5, 6);
+void irc_send_tagged_msg_vf(irc_user_t *iu, const char *type, const char *dst, const char *tags, const char *format, va_list params);
+
void irc_send_nick(irc_user_t *iu, const char *new_nick);
void irc_send_channel_user_mode_diff(irc_channel_t *ic, irc_user_t *iu,
irc_channel_user_flags_t old_flags, irc_channel_user_flags_t new_flags);
diff --git a/irc_cap.c b/irc_cap.c
index af1215e0..79f39256 100644
--- a/irc_cap.c
+++ b/irc_cap.c
@@ -42,6 +42,7 @@ static const cap_info_t supported_caps[] = {
{"extended-join", CAP_EXTENDED_JOIN},
{"away-notify", CAP_AWAY_NOTIFY},
{"userhost-in-names", CAP_USERHOST_IN_NAMES},
+ {"server-time", CAP_SERVER_TIME},
{NULL},
};
diff --git a/irc_channel.c b/irc_channel.c
index e2c77fc5..ee81a597 100644
--- a/irc_channel.c
+++ b/irc_channel.c
@@ -245,7 +245,8 @@ int irc_channel_add_user(irc_channel_t *ic, irc_user_t *iu)
ic->users = g_slist_insert_sorted(ic->users, icu, irc_channel_user_cmp);
if (iu == ic->irc->user || iu == ic->irc->root) {
- irc_channel_update_ops(ic, set_getstr(&ic->irc->b->set, "ops"));
+ irc_channel_update_ops(ic, set_getstr(&ic->irc->b->set, "ops"),
+ set_getstr(&ic->irc->b->set, "ops_mode"));
}
if (iu == ic->irc->user || ic->flags & IRC_CHANNEL_JOINED) {
@@ -441,7 +442,11 @@ void irc_channel_set_mode(irc_channel_t *ic, const char *s)
char irc_channel_user_get_prefix(irc_channel_user_t *icu)
{
- if (icu->flags & IRC_CHANNEL_USER_OP) {
+ if (icu->flags & IRC_CHANNEL_USER_OWNER) {
+ return '~';
+ } else if (icu->flags & IRC_CHANNEL_USER_ADMIN) {
+ return '&';
+ } else if (icu->flags & IRC_CHANNEL_USER_OP) {
return '@';
} else if (icu->flags & IRC_CHANNEL_USER_HALFOP) {
return '%';
@@ -675,14 +680,33 @@ static gint irc_channel_user_cmp(gconstpointer a_, gconstpointer b_)
return irc_user_cmp(a->iu, b->iu);
}
-void irc_channel_update_ops(irc_channel_t *ic, char *value)
+static int irc_channel_prefix_to_mode(char *value)
{
+ if (g_strcasecmp(value, "~") == 0) {
+ return IRC_CHANNEL_USER_OWNER;
+ } else if (g_strcasecmp(value, "&") == 0) {
+ return IRC_CHANNEL_USER_ADMIN;
+ } else if (g_strcasecmp(value, "@") == 0) {
+ return IRC_CHANNEL_USER_OP;
+ } else if (g_strcasecmp(value, "%") == 0) {
+ return IRC_CHANNEL_USER_HALFOP;
+ } else if (g_strcasecmp(value, "+") == 0) {
+ return IRC_CHANNEL_USER_VOICE;
+ } else {
+ return 0;
+ }
+}
+
+void irc_channel_update_ops(irc_channel_t *ic, char *ops, char *ops_mode)
+{
+ int mode = irc_channel_prefix_to_mode(ops_mode);
+
irc_channel_user_set_mode(ic, ic->irc->root,
- (strcmp(value, "both") == 0 ||
- strcmp(value, "root") == 0) ? IRC_CHANNEL_USER_OP : 0);
+ (strcmp(ops, "both") == 0 ||
+ strcmp(ops, "root") == 0) ? mode : 0);
irc_channel_user_set_mode(ic, ic->irc->user,
- (strcmp(value, "both") == 0 ||
- strcmp(value, "user") == 0) ? IRC_CHANNEL_USER_OP : 0);
+ (strcmp(ops, "both") == 0 ||
+ strcmp(ops, "user") == 0) ? mode : 0);
}
char *set_eval_irc_channel_ops(set_t *set, char *value)
@@ -696,7 +720,27 @@ char *set_eval_irc_channel_ops(set_t *set, char *value)
}
for (l = irc->channels; l; l = l->next) {
- irc_channel_update_ops(l->data, value);
+ irc_channel_update_ops(l->data, value, set_getstr(&irc->b->set, "ops_mode"));
+ }
+
+ return value;
+}
+
+char *set_eval_irc_channel_ops_mode(set_t *set, char *value)
+{
+ irc_t *irc = set->data;
+ GSList *l;
+
+ if (g_strcasecmp(value, "~") != 0 &&
+ g_strcasecmp(value, "&") != 0 &&
+ g_strcasecmp(value, "@") != 0 &&
+ g_strcasecmp(value, "%") != 0 &&
+ g_strcasecmp(value, "+") != 0) {
+ return SET_INVALID;
+ }
+
+ for (l = irc->channels; l; l = l->next) {
+ irc_channel_update_ops(l->data, set_getstr(&irc->b->set, "ops"), value);
}
return value;
@@ -822,17 +866,38 @@ static char *set_eval_by_account(set_t *set, char *value)
struct irc_channel *ic = set->data;
struct irc_control_channel *icc = ic->data;
account_t *acc;
+ GSList *new_acc = NULL;
+ char **accounts, **account;
- if (!(acc = account_get(ic->irc->b, value))) {
- return SET_INVALID;
+ if (value == NULL) {
+ goto out;
+ }
+
+ accounts = g_strsplit(value, ",", 0);
+ for (account = accounts; *account; account++) {
+ if (!(acc = account_get(ic->irc->b, *account))) {
+ goto fail;
+ } else {
+ new_acc = g_slist_append(new_acc, acc);
+ }
}
+ g_strfreev(accounts);
+
+out:
+ g_slist_free(icc->account);
+ icc->account = new_acc;
- icc->account = acc;
if ((icc->type & IRC_CC_TYPE_MASK) == IRC_CC_TYPE_ACCOUNT) {
bee_irc_channel_update(ic->irc, ic, NULL);
}
- return g_strdup(acc->tag);
+ return g_strdup(value);
+
+fail:
+ g_slist_free(new_acc);
+ g_strfreev(accounts);
+
+ return SET_INVALID;
}
static char *set_eval_fill_by(set_t *set, char *value)
@@ -920,6 +985,10 @@ static char *set_eval_show_users(set_t *set, char *value)
modechar = IRC_CHANNEL_USER_HALFOP;
} else if (last == '@') {
modechar = IRC_CHANNEL_USER_OP;
+ } else if (last == '&') {
+ modechar = IRC_CHANNEL_USER_ADMIN;
+ } else if (last == '~') {
+ modechar = IRC_CHANNEL_USER_OWNER;
}
if (strncmp(*part, "offline", 7) == 0) {
@@ -952,6 +1021,7 @@ fail:
gboolean irc_channel_wants_user(irc_channel_t *ic, irc_user_t *iu)
{
struct irc_control_channel *icc = ic->data;
+ GSList *accl;
gboolean ret = FALSE;
if (iu->bu == NULL) {
@@ -963,7 +1033,13 @@ gboolean irc_channel_wants_user(irc_channel_t *ic, irc_user_t *iu)
ret = iu->bu->group == icc->group;
break;
case IRC_CC_TYPE_ACCOUNT:
- ret = iu->bu->ic->acc == icc->account;
+ for (accl = icc->account; accl; accl = accl->next) {
+ account_t *acc = accl->data;
+ if (iu->bu->ic->acc == acc) {
+ ret = TRUE;
+ break;
+ }
+ }
break;
case IRC_CC_TYPE_PROTOCOL:
ret = iu->bu->ic->acc->prpl == icc->protocol;
@@ -991,6 +1067,7 @@ static gboolean control_channel_free(irc_channel_t *ic)
set_del(&ic->set, "protocol");
set_del(&ic->set, "show_users");
+ g_slist_free(icc->account);
g_free(icc);
ic->data = NULL;
diff --git a/irc_commands.c b/irc_commands.c
index 4b2f6b28..0bfac9ea 100644
--- a/irc_commands.c
+++ b/irc_commands.c
@@ -195,6 +195,7 @@ static void irc_cmd_authenticate(irc_t *irc, char **cmd)
/* no check_login here - wait for CAP END */
irc_setpass(irc, pass);
}
+ irc->status |= USTATUS_SASL_AUTHENTICATED;
}
g_free(user);
@@ -789,10 +790,18 @@ static void irc_cmd_away(irc_t *irc, char **cmd)
}
away[j] = '\0';
- irc_send_num(irc, 306, ":You're now away: %s", away);
+ if (set_getbool(&irc->b->set, "strict_away")) {
+ irc_send_num(irc, 306, ":You have been marked as being away");
+ } else {
+ irc_send_num(irc, 306, ":You're now away: %s", away);
+ }
set_setstr(&irc->b->set, "away", away);
} else {
- irc_send_num(irc, 305, ":Welcome back");
+ if (set_getbool(&irc->b->set, "strict_away")) {
+ irc_send_num(irc, 305, ":You are no longer marked as being away");
+ } else {
+ irc_send_num(irc, 305, ":Welcome back");
+ }
set_setstr(&irc->b->set, "away", NULL);
}
}
diff --git a/irc_im.c b/irc_im.c
index d03f39a8..f8b50853 100644
--- a/irc_im.c
+++ b/irc_im.c
@@ -113,6 +113,10 @@ static gboolean bee_irc_user_status(bee_t *bee, bee_user_t *bu, bee_user_t *old)
irc_t *irc = bee->ui_data;
irc_user_t *iu = bu->ui_data;
+ if (set_getbool(&bu->ic->acc->set, "offline_is_away") && !(bu->flags & BEE_USER_ONLINE)) {
+ bu->flags |= (BEE_USER_ONLINE | BEE_USER_AWAY);
+ }
+
/* Do this outside the if below since away state can change without
the online state changing. */
iu->flags &= ~IRC_USER_AWAY;
@@ -137,7 +141,8 @@ static gboolean bee_irc_user_status(bee_t *bee, bee_user_t *bu, bee_user_t *old)
one QUIT instead of possibly many (in case of
multiple control chans). If there's a channel that
shows offline people, a JOIN will follow. */
- if (set_getbool(&bee->set, "offline_user_quits")) {
+ if (set_getbool(&bee->set, "offline_user_quits") &&
+ set_getbool(&bu->ic->acc->set, "offline_user_quits")) {
irc_user_quit(iu, "Leaving...");
}
}
@@ -225,9 +230,16 @@ static gboolean bee_irc_user_msg(bee_t *bee, bee_user_t *bu, const char *msg_, g
char *wrapped, *ts = NULL;
char *msg = g_strdup(msg_);
char *message_type = "PRIVMSG";
+ char *tags = NULL;
GSList *l;
-
- if (sent_at > 0 && set_getbool(&irc->b->set, "display_timestamps")) {
+ struct tm msg_time;
+
+ if (sent_at > 0 && (irc->caps & CAP_SERVER_TIME)) {
+ gmtime_r(&sent_at, &msg_time);
+ tags = g_strdup_printf("time=%04d-%02d-%02dT%02d:%02d:%02d.000Z",
+ msg_time.tm_year + 1900, msg_time.tm_mon + 1, msg_time.tm_mday,
+ msg_time.tm_hour, msg_time.tm_min, msg_time.tm_sec);
+ } else if (sent_at > 0 && set_getbool(&irc->b->set, "display_timestamps")) {
ts = irc_format_timestamp(irc, sent_at);
}
@@ -300,13 +312,14 @@ static gboolean bee_irc_user_msg(bee_t *bee, bee_user_t *bu, const char *msg_, g
}
wrapped = word_wrap(msg, IRC_WORD_WRAP);
- irc_send_msg(src_iu, message_type, dst, wrapped, prefix);
+ irc_send_tagged_msg(src_iu, message_type, dst, wrapped, prefix, tags);
g_free(wrapped);
cleanup:
g_free(prefix);
g_free(msg);
g_free(ts);
+ g_free(tags);
return TRUE;
}
@@ -692,19 +705,27 @@ static gboolean bee_irc_chat_msg(bee_t *bee, struct groupchat *c, bee_user_t *bu
irc_user_t *iu = flags & OPT_SELFMESSAGE ? irc->user : bu->ui_data;
irc_channel_t *ic = c->ui_data;
char *wrapped, *ts = NULL;
+ char *tags = NULL;
+ struct tm msg_time;
if (ic == NULL) {
return FALSE;
}
- if (sent_at > 0 && set_getbool(&bee->set, "display_timestamps")) {
+ if (sent_at > 0 && (irc->caps & CAP_SERVER_TIME)) {
+ gmtime_r(&sent_at, &msg_time);
+ tags = g_strdup_printf("time=%04d-%02d-%02dT%02d:%02d:%02d.000Z",
+ msg_time.tm_year + 1900, msg_time.tm_mon + 1, msg_time.tm_mday,
+ msg_time.tm_hour, msg_time.tm_min, msg_time.tm_sec);
+ } else if (sent_at > 0 && set_getbool(&bee->set, "display_timestamps")) {
ts = irc_format_timestamp(irc, sent_at);
}
wrapped = word_wrap(msg, IRC_WORD_WRAP);
- irc_send_msg(iu, "PRIVMSG", ic->name, wrapped, ts);
+ irc_send_tagged_msg(iu, "PRIVMSG", ic->name, wrapped, ts, tags);
g_free(ts);
g_free(wrapped);
+ g_free(tags);
return TRUE;
}
diff --git a/irc_send.c b/irc_send.c
index 89f0100a..d133c530 100644
--- a/irc_send.c
+++ b/irc_send.c
@@ -44,7 +44,7 @@ void irc_send_login(irc_t *irc)
PACKAGE, BITLBEE_VERSION);
irc_send_num(irc, 3, ":%s", IRCD_INFO);
irc_send_num(irc, 4, "%s %s %s %s", irc->root->host, BITLBEE_VERSION, UMODES UMODES_PRIV, CMODES);
- irc_send_num(irc, 5, "PREFIX=(ohv)@%%+ CHANTYPES=%s CHANMODES=,,,%s NICKLEN=%d CHANNELLEN=%d "
+ irc_send_num(irc, 5, "PREFIX=(qaohv)~&@%%+ CHANTYPES=%s CHANMODES=,,,%s NICKLEN=%d CHANNELLEN=%d "
"NETWORK=BitlBee SAFELIST CASEMAPPING=rfc1459 MAXTARGETS=1 WATCH=128 "
"FLOOD=0/9999 :are supported by this server",
CTYPES, CMODES, MAX_NICK_LENGTH - 1, MAX_NICK_LENGTH - 1);
@@ -364,6 +364,11 @@ void irc_send_who(irc_t *irc, GSList *l, const char *channel)
void irc_send_msg(irc_user_t *iu, const char *type, const char *dst, const char *msg, const char *prefix)
{
+ irc_send_tagged_msg(iu, type, dst, msg, prefix, NULL);
+}
+
+void irc_send_tagged_msg(irc_user_t *iu, const char *type, const char *dst, const char *msg, const char *prefix, const char *tags)
+{
char last = 0;
const char *s = msg, *line = msg;
char raw_msg[strlen(msg) + 1024];
@@ -383,14 +388,14 @@ void irc_send_msg(irc_user_t *iu, const char *type, const char *dst, const char
strcpy(raw_msg, "\001ACTION ");
strncat(raw_msg, line + 4, s - line - 4);
strcat(raw_msg, "\001");
- irc_send_msg_raw(iu, type, dst, raw_msg);
+ irc_send_tagged_msg_raw(iu, type, dst, raw_msg, tags);
} else {
*raw_msg = '\0';
if (prefix && *prefix) {
strcpy(raw_msg, prefix);
}
strncat(raw_msg, line, s - line);
- irc_send_msg_raw(iu, type, dst, raw_msg);
+ irc_send_tagged_msg_raw(iu, type, dst, raw_msg, tags);
}
line = s + 1;
}
@@ -400,21 +405,56 @@ void irc_send_msg(irc_user_t *iu, const char *type, const char *dst, const char
void irc_send_msg_raw(irc_user_t *iu, const char *type, const char *dst, const char *msg)
{
- irc_write(iu->irc, ":%s!%s@%s %s %s :%s",
- iu->nick, iu->user, iu->host, type, dst, msg && *msg ? msg : " ");
+ irc_send_tagged_msg_raw(iu, type, dst, msg, NULL);
+}
+
+void irc_send_tagged_msg_raw(irc_user_t *iu, const char *type, const char *dst, const char *msg, const char *tags)
+{
+ if (!tags || !*tags) {
+ irc_write(iu->irc, ":%s!%s@%s %s %s :%s",
+ iu->nick, iu->user, iu->host,
+ type, dst, msg && *msg ? msg : " ");
+ } else {
+ irc_write(iu->irc, "@%s :%s!%s@%s %s %s :%s",
+ tags, iu->nick, iu->user, iu->host,
+ type, dst, msg && *msg ? msg : " ");
+ }
}
void irc_send_msg_f(irc_user_t *iu, const char *type, const char *dst, const char *format, ...)
{
- char text[IRC_MAX_LINE];
va_list params;
va_start(params, format);
- g_vsnprintf(text, IRC_MAX_LINE, format, params);
+ irc_send_tagged_msg_vf(iu, type, dst, NULL, format, params);
+ va_end(params);
+
+}
+
+void irc_send_tagged_msg_f(irc_user_t *iu, const char *type, const char *dst, const char *tags, const char *format, ...)
+{
+ va_list params;
+
+ va_start(params, format);
+ irc_send_tagged_msg_vf(iu, type, dst, tags, format, params);
va_end(params);
+}
+
+void irc_send_tagged_msg_vf(irc_user_t *iu, const char *type, const char *dst, const char *tags, const char *format, va_list params)
+{
+ char text[IRC_MAX_LINE];
- irc_write(iu->irc, ":%s!%s@%s %s %s :%s",
- iu->nick, iu->user, iu->host, type, dst, text);
+ g_vsnprintf(text, IRC_MAX_LINE, format, params);
+
+ if (!tags || !*tags) {
+ irc_write(iu->irc, ":%s!%s@%s %s %s :%s",
+ iu->nick, iu->user, iu->host,
+ type, dst, text);
+ } else {
+ irc_write(iu->irc, "@%s :%s!%s@%s %s %s :%s",
+ tags, iu->nick, iu->user, iu->host,
+ type, dst, text);
+ }
}
void irc_send_nick(irc_user_t *iu, const char *new)
@@ -427,11 +467,27 @@ void irc_send_nick(irc_user_t *iu, const char *new)
void irc_send_channel_user_mode_diff(irc_channel_t *ic, irc_user_t *iu,
irc_channel_user_flags_t old, irc_channel_user_flags_t new)
{
- char changes[3 * (5 + strlen(iu->nick))];
- char from[strlen(ic->irc->root->nick) + strlen(ic->irc->root->user) + strlen(ic->irc->root->host) + 3];
+ char changes[5 * (5 + strlen(iu->nick))];
+ char from[strlen(ic->irc->root->nick) + strlen(ic->irc->root->user) + strlen(ic->irc->root->host) + 5];
int n;
*changes = '\0'; n = 0;
+ if ((old & IRC_CHANNEL_USER_OWNER) != (new & IRC_CHANNEL_USER_OWNER)) {
+ n++;
+ if (new & IRC_CHANNEL_USER_OWNER) {
+ strcat(changes, "+q");
+ } else {
+ strcat(changes, "-q");
+ }
+ }
+ if ((old & IRC_CHANNEL_USER_ADMIN) != (new & IRC_CHANNEL_USER_ADMIN)) {
+ n++;
+ if (new & IRC_CHANNEL_USER_ADMIN) {
+ strcat(changes, "+a");
+ } else {
+ strcat(changes, "-a");
+ }
+ }
if ((old & IRC_CHANNEL_USER_OP) != (new & IRC_CHANNEL_USER_OP)) {
n++;
if (new & IRC_CHANNEL_USER_OP) {
diff --git a/irc_user.c b/irc_user.c
index 72e401d0..c6daffc7 100644
--- a/irc_user.c
+++ b/irc_user.c
@@ -198,7 +198,7 @@ const char *irc_user_get_away(irc_user_t *iu)
if (iu == irc->user) {
return set_getstr(&irc->b->set, "away");
} else if (bu) {
- if (!bu->flags & BEE_USER_ONLINE) {
+ if (!(bu->flags & BEE_USER_ONLINE)) {
return "Offline";
} else if (bu->flags & BEE_USER_AWAY) {
if (bu->status_msg) {
diff --git a/lib/ssl_client.h b/lib/ssl_client.h
index d2e12534..08debe48 100644
--- a/lib/ssl_client.h
+++ b/lib/ssl_client.h
@@ -32,6 +32,7 @@
is completed. */
#include <glib.h>
+#include "irc.h"
#include "proxy.h"
/* Some generic error codes. Especially SSL_AGAIN is important if you
@@ -71,6 +72,11 @@ G_MODULE_EXPORT void *ssl_starttls(int fd, char *hostname, gboolean verify, ssl_
G_MODULE_EXPORT int ssl_read(void *conn, char *buf, int len);
G_MODULE_EXPORT int ssl_write(void *conn, const char *buf, int len);
+G_MODULE_EXPORT gboolean ssl_setup_server(void);
+G_MODULE_EXPORT gboolean ssl_accept(irc_t *irc);
+G_MODULE_EXPORT int ssl_server_read(irc_t *irc, char *buf, int len);
+G_MODULE_EXPORT int ssl_server_write(irc_t *irc, const char *buf, int len);
+
/* Now needed by most SSL libs. See for more info:
http://www.gnu.org/software/gnutls/manual/gnutls.html#index-gnutls_005frecord_005fcheck_005fpending-209
http://www.openssl.org/docs/ssl/SSL_pending.html
diff --git a/lib/ssl_gnutls.c b/lib/ssl_gnutls.c
index c9b35fff..c37449c4 100644
--- a/lib/ssl_gnutls.c
+++ b/lib/ssl_gnutls.c
@@ -33,11 +33,14 @@
#include "sock.h"
#include "stdlib.h"
#include "bitlbee.h"
+#include "config.h"
+#include "irc.h"
int ssl_errno = 0;
static gboolean initialized = FALSE;
gnutls_certificate_credentials_t xcred;
+gnutls_certificate_credentials_t server_xcred;
#include <limits.h>
@@ -125,6 +128,40 @@ void *ssl_connect(char *host, int port, gboolean verify, ssl_input_function func
return conn;
}
+gboolean ssl_setup_server()
+{
+ gnutls_certificate_allocate_credentials(&server_xcred);
+ gnutls_certificate_set_x509_key_file(server_xcred, global.conf->ssl_cert, global.conf->ssl_key, GNUTLS_X509_FMT_PEM);
+
+ return TRUE;
+}
+
+gboolean ssl_accept(irc_t *irc)
+{
+ int ret;
+
+ gnutls_init(&irc->ssl_session, GNUTLS_SERVER);
+ gnutls_transport_set_int(irc->ssl_session, irc->fd);
+ gnutls_credentials_set(irc->ssl_session, GNUTLS_CRD_CERTIFICATE, server_xcred);
+ gnutls_certificate_server_set_request(irc->ssl_session, GNUTLS_CERT_REQUEST);
+
+ do {
+ ret = gnutls_handshake(irc->ssl_session);
+ } while (ret < 0 && gnutls_error_is_fatal(ret) == 0);
+
+ if (ret < 0) {
+ close(irc->fd);
+ gnutls_deinit(irc->ssl_session);
+
+ log_message(LOGLVL_INFO, "SSL Handshake failed (%s)", gnutls_strerror(ret)); // XXX
+
+ exit(1);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
void *ssl_starttls(int fd, char *hostname, gboolean verify, ssl_input_function func, gpointer data)
{
struct scd *conn = g_new0(struct scd, 1);
@@ -393,6 +430,24 @@ int ssl_read(void *conn, char *buf, int len)
return st;
}
+int ssl_server_read(irc_t *irc, char *buf, int len)
+{
+ int st;
+
+ st = gnutls_record_recv(irc->ssl_session, buf, len);
+
+ ssl_errno = SSL_OK;
+ if (st == GNUTLS_E_AGAIN || st == GNUTLS_E_INTERRUPTED) {
+ ssl_errno = SSL_AGAIN;
+ }
+
+ if (SSLDEBUG && getenv("BITLBEE_DEBUG") && st > 0) {
+ len = write(2, buf, st);
+ }
+
+ return st;
+}
+
int ssl_write(void *conn, const char *buf, int len)
{
int st;
@@ -416,6 +471,24 @@ int ssl_write(void *conn, const char *buf, int len)
return st;
}
+int ssl_server_write(irc_t *irc, const char *buf, int len)
+{
+ int st;
+
+ st = gnutls_record_send(irc->ssl_session, buf, len);
+
+ ssl_errno = SSL_OK;
+ if (st == GNUTLS_E_AGAIN || st == GNUTLS_E_INTERRUPTED) {
+ ssl_errno = SSL_AGAIN;
+ }
+
+ if (SSLDEBUG && getenv("BITLBEE_DEBUG") && st > 0) {
+ len = write(2, buf, st);
+ }
+
+ return st;
+}
+
int ssl_pending(void *conn)
{
if (conn == NULL) {
diff --git a/protocols/account.c b/protocols/account.c
index b5ff4e88..80b1d020 100644
--- a/protocols/account.c
+++ b/protocols/account.c
@@ -77,6 +77,8 @@ account_t *account_add(bee_t *bee, struct prpl *prpl, char *user, char *pass)
s = set_add(&a->set, "username", NULL, set_eval_account, a);
s->flags |= SET_NOSAVE | ACC_SET_OFFLINE_ONLY | ACC_SET_LOCKABLE;
set_setstr(&a->set, "username", user);
+ set_add(&a->set, "offline_user_quits", "true", set_eval_bool, a);
+ set_add(&a->set, "offline_is_away", "false", set_eval_bool, a);
if (prpl == &protocol_missing) {
s = set_add(&a->set, "server", NULL, set_eval_account, a);
@@ -130,6 +132,36 @@ account_t *account_add(bee_t *bee, struct prpl *prpl, char *user, char *pass)
return a;
}
+void account_update_channel_set(irc_channel_t *ic, char *old_tag, char *new_tag)
+{
+ gboolean found = FALSE;
+ char **account, **accounts;
+ char *saccount = set_getstr(&ic->set, "account");
+
+ if (saccount == NULL || *saccount == '\0') {
+ return;
+ }
+
+ accounts = g_strsplit(saccount, ",", 0);
+ for (account = accounts; *account; account++) {
+ if (g_strcasecmp(*account, old_tag) == 0) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (found) {
+ g_free(*account);
+ *account = g_strdup(new_tag);
+
+ saccount = g_strjoinv(",", accounts);
+ set_setstr(&ic->set, "account", saccount);
+ g_free(saccount);
+ }
+
+ g_strfreev(accounts);
+}
+
char *set_eval_account(set_t *set, char *value)
{
account_t *acc = set->data;
@@ -171,14 +203,28 @@ char *set_eval_account(set_t *set, char *value)
return NULL; /* password shouldn't be visible in plaintext! */
} else if (strcmp(set->key, "tag") == 0) {
account_t *oa;
+ irc_t *irc;
+ GSList *l;
+ char *old;
/* Enforce uniqueness. */
if ((oa = account_by_tag(acc->bee, value)) && oa != acc) {
return SET_INVALID;
}
- g_free(acc->tag);
+ old = acc->tag;
acc->tag = g_strdup(value);
+
+ irc = acc->bee->ui_data;
+ for (l = irc->channels; l; l = l->next) {
+ irc_channel_t *ic = l->data;
+
+ if (g_strcasecmp(set_getstr(&ic->set, "type"), "control") == 0) {
+ account_update_channel_set(ic, old, value);
+ }
+ }
+
+ g_free(old);
return value;
} else if (strcmp(set->key, "auto_connect") == 0) {
if (!is_bool(value)) {
@@ -299,9 +345,43 @@ account_t *account_by_tag(bee_t *bee, const char *tag)
return NULL;
}
+void account_remove_from_channel_set(irc_channel_t *ic, char *tag)
+{
+ gboolean found = FALSE;
+ char **account, **accounts;
+ char *saccount = set_getstr(&ic->set, "account");
+
+ if (saccount == NULL || *saccount == '\0') {
+ return;
+ }
+
+ accounts = g_strsplit(saccount, ",", 0);
+ for (account = accounts; *account; account++) {
+ if (g_strcasecmp(*account, tag) == 0) {
+ found = TRUE;
+ break;
+ }
+ }
+
+ if (found) {
+ g_free(*account);
+
+ do {
+ *account = *(account + 1);
+ } while (*(++account) != NULL);
+
+ saccount = g_strjoinv(",", accounts);
+ set_setstr(&ic->set, "account", saccount);
+ }
+
+ g_strfreev(accounts);
+}
+
void account_del(bee_t *bee, account_t *acc)
{
account_t *a, *l = NULL;
+ GSList *accl;
+ irc_t *irc;
if (acc->ic) {
/* Caller should have checked, accounts still in use can't be deleted. */
@@ -325,6 +405,16 @@ void account_del(bee_t *bee, account_t *acc)
}
*/
+ /* Remove from channel set account */
+ irc = acc->bee->ui_data;
+ for (accl = irc->channels; accl; accl = accl->next) {
+ irc_channel_t *ic = accl->data;
+
+ if (g_strcasecmp(set_getstr(&ic->set, "type"), "control") == 0) {
+ account_remove_from_channel_set(ic, acc->tag);
+ }
+ }
+
while (a->set) {
set_del(&a->set, a->set->key);
}
diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c
index 15b88b30..4b24bf60 100644
--- a/protocols/jabber/jabber.c
+++ b/protocols/jabber/jabber.c
@@ -116,6 +116,9 @@ static void jabber_init(account_t *acc)
s = set_add(&acc->set, "carbons", "true", set_eval_bool, acc);
s->flags |= ACC_SET_OFFLINE_ONLY;
+ s = set_add(&acc->set, "disable_scram", "false", set_eval_bool, acc);
+ s->flags |= SET_HIDDEN_DEFAULT;
+
acc->flags |= ACC_FLAG_AWAY_MESSAGE | ACC_FLAG_STATUS_MESSAGE |
ACC_FLAG_HANDLE_DOMAINS;
}
@@ -379,6 +382,9 @@ static void jabber_logout(struct im_connection *ic)
g_free(jd->muc_host);
g_free(jd->username);
g_free(jd->me);
+ g_free(jd->challenge.cnonce);
+ g_free(jd->challenge.server_signature);
+ g_free(jd->challenge.cb_header);
g_free(jd);
jabber_connections = g_slist_remove(jabber_connections, ic);
diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h
index e1087315..5100b7a9 100644
--- a/protocols/jabber/jabber.h
+++ b/protocols/jabber/jabber.h
@@ -77,6 +77,16 @@ typedef enum {
JCFLAG_ALWAYS_USE_NICKS = 2,
} jabber_chat_flags_t;
+typedef enum {
+ JCHALLENGE_DIGEST_MD5,
+ JCHALLENGE_SCRAM
+} jabber_challenge_t;
+
+typedef enum {
+ JSCRAM_SHA1 = 0x0001,
+ JSCRAM_SHA256 = 0x0002
+} jabber_scram_t;
+
struct jabber_data {
struct im_connection *ic;
@@ -94,6 +104,14 @@ struct jabber_data {
char *me; /* bare jid */
char *internal_jid;
+ struct {
+ jabber_challenge_t type;
+ int scram_algo;
+ char *cnonce;
+ char *server_signature;
+ char *cb_header;
+ } challenge;
+
const struct oauth2_service *oauth2_service;
char *oauth2_access_token;
diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c
index 77a2cee0..d4556811 100644
--- a/protocols/jabber/sasl.c
+++ b/protocols/jabber/sasl.c
@@ -22,11 +22,15 @@
\***************************************************************************/
#include <ctype.h>
+#include <gcrypt.h>
+#include <gnutls/gnutls.h>
+#include <stringprep.h>
#include "jabber.h"
#include "base64.h"
#include "oauth2.h"
#include "oauth.h"
+#include "sha1.h"
const struct oauth2_service oauth2_service_google =
{
@@ -47,7 +51,7 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
struct jabber_data *jd = ic->proto_data;
struct xt_node *c, *reply;
char *s;
- int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_hipchat_oauth = 0;
+ int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0, sup_hipchat_oauth = 0;
int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE;
GString *mechs;
@@ -74,26 +78,30 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
mechs = g_string_new("");
c = node->children;
while ((c = xt_find_node(c, "mechanism"))) {
- if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) {
- sup_plain = 1;
- } else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
- sup_digest = 1;
- } else if (c->text && g_strcasecmp(c->text, "ANONYMOUS") == 0) {
- sup_anonymous = 1;
- } else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) {
- sup_gtalk = 1;
- } else if (c->text && g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) {
- sup_hipchat_oauth = 1;
- }
-
if (c->text) {
+ if (g_strcasecmp(c->text, "PLAIN") == 0) {
+ sup_plain = 1;
+ } else if (g_strcasecmp(c->text, "DIGEST-MD5") == 0) {
+ sup_digest = 1;
+ } else if (g_strcasecmp(c->text, "ANONYMOUS") == 0) {
+ sup_anonymous = 1;
+ } else if (g_strcasecmp(c->text, "X-OAUTH2") == 0) {
+ sup_gtalk = 1;
+ } else if (g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) {
+ sup_hipchat_oauth = 1;
+ } else if (g_strcasecmp(c->text, "SCRAM-SHA-1") == 0) {
+ sup_scram |= JSCRAM_SHA1;
+ } else if (g_strcasecmp(c->text, "SCRAM-SHA-256") == 0) {
+ sup_scram |= JSCRAM_SHA256;
+ }
+
g_string_append_printf(mechs, " %s", c->text);
}
c = c->next;
}
- if (!want_oauth && !sup_plain && !sup_digest) {
+ if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) {
if (sup_gtalk || sup_hipchat_oauth) {
imcb_error(ic, "This server requires OAuth "
"(supported schemes:%s)", mechs->str);
@@ -158,11 +166,47 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
imc_logout(ic, FALSE);
xt_free_node(reply);
return XT_ABORT;
+ } else if (sup_scram && !set_getbool(&ic->acc->set, "disable_scram")) {
+ int rc;
+ unsigned char cnonce_bin[30];
+ char *puser = NULL;
+
+ if (sup_scram & JSCRAM_SHA256) {
+ jd->challenge.type = JCHALLENGE_SCRAM;
+ jd->challenge.scram_algo = GCRY_MD_SHA256;
+ xt_add_attr(reply, "mechanism", "SCRAM-SHA-256");
+ } else if (sup_scram & JSCRAM_SHA1) {
+ jd->challenge.type = JCHALLENGE_SCRAM;
+ jd->challenge.scram_algo = GCRY_MD_SHA1;
+ xt_add_attr(reply, "mechanism", "SCRAM-SHA-1");
+ } else {
+ imcb_error(ic, "Unknown scram method"); /* Just in case, but we should not get here */
+ return XT_ABORT;
+ }
+
+ rc = stringprep_profile(jd->username, &puser, "SASLprep", 0);
+ if (rc != STRINGPREP_OK) {
+ imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc));
+ return XT_ABORT;
+ }
+
+ random_bytes(cnonce_bin, sizeof(cnonce_bin));
+ jd->challenge.cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
+
+ jd->challenge.cb_header = g_strdup("n,,");
+
+ s = g_strdup_printf("%sn=%s,r=%s", jd->challenge.cb_header, puser, jd->challenge.cnonce);
+
+ reply->text = base64_encode((unsigned char *)s , strlen(s));
+ reply->text_len = strlen(reply->text);
+ g_free(puser);
+ g_free(s);
} else if (sup_digest && !(jd->ssl && sup_plain)) {
/* Only try DIGEST-MD5 if there's no SSL/TLS or if PLAIN isn't supported.
* Which in practice means "don't bother with DIGEST-MD5 most of the time".
* It's weak, pointless over TLS, and often breaks with some servers (hi openfire) */
+ jd->challenge.type = JCHALLENGE_DIGEST_MD5;
xt_add_attr(reply, "mechanism", "DIGEST-MD5");
/* The rest will be done later, when we receive a <challenge/>. */
@@ -281,7 +325,7 @@ char *sasl_get_part(char *data, char *field)
}
}
-xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
+static xt_status sasl_pkt_challenge_digest_md5(struct xt_node *node, gpointer data)
{
struct im_connection *ic = data;
struct jabber_data *jd = ic->proto_data;
@@ -400,6 +444,191 @@ silent_error:
return ret;
}
+static void hmac(int algo, unsigned char *key, size_t key_len, unsigned char *buf, size_t buf_len, unsigned char *out)
+{
+ gcry_md_hd_t digest;
+ unsigned char *tmp;
+
+ gcry_md_open(&digest, algo, GCRY_MD_FLAG_HMAC);
+ gcry_md_setkey(digest, key, key_len);
+ gcry_md_write(digest, buf, buf_len);
+
+ tmp = gcry_md_read(digest, 0);
+ memcpy(out, tmp, gcry_md_get_algo_dlen(algo));
+
+ gcry_md_close(digest);
+}
+
+static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data)
+{
+ struct im_connection *ic = data;
+ struct jabber_data *jd = ic->proto_data;
+ struct xt_node *reply_pkt = NULL;
+ xt_status ret = XT_ABORT;
+ char *random = NULL, *salt64 = NULL, *iter_str = NULL, *encoded_proof = NULL,
+ *reply = NULL, *client_first_bare = NULL, *server_first = NULL, *cb_header64 = NULL,
+ *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL, *puser = NULL, *ppass = NULL;
+
+ unsigned char *salt = NULL;
+ size_t salt_len, iter_count, i;
+
+ int algo = jd->challenge.scram_algo, rc;
+ size_t md_len = gcry_md_get_algo_dlen(algo);
+
+ unsigned char client_key[md_len];
+ unsigned char client_proof[md_len];
+ unsigned char client_signature[md_len];
+ unsigned char salted_password[md_len];
+ unsigned char server_key[md_len];
+ unsigned char server_signature[md_len];
+ unsigned char stored_key[md_len];
+
+ if (node->text_len == 0) {
+ goto error;
+ }
+
+ server_first = frombase64(node->text);
+
+ random = sasl_get_part(server_first, "r");
+ salt64 = sasl_get_part(server_first, "s");
+ iter_str = sasl_get_part(server_first, "i");
+ iter_count = atoi(iter_str);
+
+ if (strncmp(random, jd->challenge.cnonce, strlen(jd->challenge.cnonce)) != 0) {
+ imcb_error(ic, "Server nonce doesn't start with client nonce");
+ goto error;
+ }
+
+ rc = stringprep_profile(jd->username, &puser, "SASLprep", 0);
+ if (rc != STRINGPREP_OK) {
+ imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc));
+ goto error;
+ }
+ rc = stringprep_profile(ic->acc->pass, &ppass, "SASLprep", 0);
+ if (rc != STRINGPREP_OK) {
+ imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc));
+ goto error;
+ }
+
+ salt_len = base64_decode(salt64, &salt);
+
+ if (gcry_kdf_derive(ppass, strlen(ppass),
+ GCRY_KDF_PBKDF2, algo,
+ salt, salt_len, iter_count,
+ sizeof(salted_password), salted_password) != 0) {
+ imcb_error(ic, "PBKDF failed");
+ goto error;
+ }
+
+ hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Client Key", 10, client_key);
+
+ gcry_md_hash_buffer(algo, stored_key, client_key, sizeof(client_key));
+
+ cb_header64 = tobase64(jd->challenge.cb_header);
+ client_first_bare = g_strdup_printf("n=%s,r=%s", puser, jd->challenge.cnonce);
+ client_final_noproof = g_strdup_printf("c=%s,r=%s", cb_header64, random);
+
+ auth_message = g_strdup_printf("%s,%s,%s", client_first_bare, server_first, client_final_noproof);
+
+ hmac(algo, stored_key, sizeof(stored_key), (unsigned char *)auth_message, strlen(auth_message), client_signature);
+
+ for (i = 0; i < sizeof(client_key); i++) {
+ client_proof[i] = client_key[i] ^ client_signature[i];
+ }
+
+ encoded_proof = base64_encode(client_proof, sizeof(client_proof));
+
+ hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Server Key", 10, server_key);
+ hmac(algo, server_key, sizeof(server_key), (unsigned char *)auth_message, strlen(auth_message), server_signature);
+
+ jd->challenge.server_signature = base64_encode(server_signature, sizeof(server_signature));
+
+ client_final = g_strdup_printf("%s,p=%s", client_final_noproof, encoded_proof);
+ reply = tobase64(client_final);
+ reply_pkt = xt_new_node("response", reply, NULL);
+ xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL);
+
+ if (!jabber_write_packet(ic, reply_pkt)) {
+ goto silent_error;
+ }
+
+ ret = XT_HANDLED;
+ goto silent_error;
+
+error:
+ imcb_error(ic, "Incorrect SASL challenge received");
+ imc_logout(ic, FALSE);
+
+silent_error:
+ g_free(puser);
+ g_free(ppass);
+ g_free(random);
+ g_free(salt64);
+ g_free(salt);
+ g_free(iter_str);
+ g_free(encoded_proof);
+ g_free(cb_header64);
+ g_free(client_first_bare);
+ g_free(client_final_noproof);
+ g_free(client_final);
+ g_free(auth_message);
+ g_free(reply);
+ g_free(jd->challenge.cnonce);
+ jd->challenge.cnonce = NULL;
+ g_free(jd->challenge.cb_header);
+ jd->challenge.cb_header = NULL;
+ xt_free_node(reply_pkt);
+ return ret;
+}
+
+xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data)
+{
+ struct im_connection *ic = data;
+ struct jabber_data *jd = ic->proto_data;
+
+ switch (jd->challenge.type) {
+ case JCHALLENGE_DIGEST_MD5:
+ return sasl_pkt_challenge_digest_md5(node, data);
+
+ case JCHALLENGE_SCRAM:
+ return sasl_pkt_challenge_scram(node, data);
+
+ default:
+ imcb_error(ic, "Invalid challenge type");
+ return XT_ABORT;
+ }
+}
+
+static xt_status sasl_pkt_validate_scram_success(struct xt_node *node, gpointer data)
+{
+ struct im_connection *ic = data;
+ struct jabber_data *jd = ic->proto_data;
+ char *v, *dec;
+ xt_status ret = XT_HANDLED;
+
+ if (node->text_len == 0) {
+ imcb_error(ic, "Server didn't send signature");
+ imc_logout(ic, FALSE);
+ ret = XT_ABORT;
+ goto error;
+ }
+
+ dec = frombase64(node->text);
+ v = sasl_get_part(dec, "v");
+
+ if (g_strcmp0(v, jd->challenge.server_signature) != 0) {
+ imcb_error(ic, "Invalid server signature");
+ imc_logout(ic, FALSE);
+ ret = XT_ABORT;
+ goto error;
+ }
+
+error:
+ g_free(jd->challenge.server_signature);
+ jd->challenge.server_signature = NULL;
+ return ret;
+}
+
xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
{
struct im_connection *ic = data;
@@ -417,6 +646,10 @@ xt_status sasl_pkt_result(struct xt_node *node, gpointer data)
imcb_log(ic, "Authentication finished");
jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART;
+ if (jd->challenge.type == JCHALLENGE_SCRAM) {
+ return sasl_pkt_validate_scram_success(node, data);
+ }
+
if (jd->flags & JFLAG_HIPCHAT) {
return hipchat_handle_success(ic, node);
}
diff --git a/protocols/nogaim.c b/protocols/nogaim.c
index cb2d62a9..716dae9b 100644
--- a/protocols/nogaim.c
+++ b/protocols/nogaim.c
@@ -344,18 +344,22 @@ static void serv_got_crap(struct im_connection *ic, char *format, ...)
strip_html(text);
}
- /* Try to find a different connection on the same protocol. */
- for (a = ic->bee->accounts; a; a = a->next) {
- if (a->prpl == ic->acc->prpl && a->ic != ic) {
- break;
- }
- }
-
- /* If we found one, include the screenname in the message. */
- if (a) {
+ if (set_getbool(&ic->bee->set, "only_log_tags")) {
ic->bee->ui->log(ic->bee, ic->acc->tag, text);
} else {
- ic->bee->ui->log(ic->bee, ic->acc->prpl->name, text);
+ /* Try to find a different connection on the same protocol. */
+ for (a = ic->bee->accounts; a; a = a->next) {
+ if (a->prpl == ic->acc->prpl && a->ic != ic) {
+ break;
+ }
+ }
+
+ /* If we found one, include the screenname in the message. */
+ if (a) {
+ ic->bee->ui->log(ic->bee, ic->acc->tag, text);
+ } else {
+ ic->bee->ui->log(ic->bee, ic->acc->prpl->name, text);
+ }
}
g_free(text);
diff --git a/protocols/purple/purple.c b/protocols/purple/purple.c
index 8bf73331..6204614d 100644
--- a/protocols/purple/purple.c
+++ b/protocols/purple/purple.c
@@ -332,6 +332,18 @@ static void purple_init(account_t *acc)
purple_accounts_remove(pa);
}
+static void purple_save_password(account_t *acc, PurpleAccount *pa)
+{
+ char *old_password, *new_password;
+
+ old_password = set_getstr(&acc->set, "password");
+ new_password = g_strdup(purple_account_get_password(pa));
+ if (!old_password || !*old_password || g_strcmp0(old_password, new_password) != 0) {
+ set_setstr(&acc->set, "password", new_password);
+ }
+ g_free(new_password);
+}
+
static void purple_sync_settings(account_t *acc, PurpleAccount *pa)
{
PurplePlugin *prpl = purple_plugins_find_with_id(pa->protocol_id);
@@ -428,6 +440,8 @@ static void purple_logout(struct im_connection *ic)
imcb_chat_free(ic->groupchats->data);
}
+ purple_save_password(ic->acc, pd->account);
+
if (pd->filetransfers) {
purple_transfer_cancel_all(ic);
}
@@ -1909,8 +1923,8 @@ void purple_initmodule()
ret = g_memdup(&funcs, sizeof(funcs));
ret->name = ret->data = prot->info->id;
- if (strncmp(ret->name, "prpl-", 5) == 0) {
- ret->name += 5;
+ if (strncmp(ret->name, "prpl-", 5) != 0) {
+ ret->name = g_strdup_printf("prpl-%s", ret->name);
}
if (pi->options & OPT_PROTO_NO_PASSWORD) {
@@ -1927,13 +1941,13 @@ void purple_initmodule()
/* libpurple doesn't define a protocol called OSCAR, but we
need it to be compatible with normal BitlBee. */
- if (g_strcasecmp(prot->info->id, "prpl-aim") == 0) {
+ /*if (g_strcasecmp(prot->info->id, "prpl-aim") == 0) {
ret = g_memdup(&funcs, sizeof(funcs));
ret->name = "oscar";
- /* purple_get_account_prpl_id() determines the actual protocol ID (icq/aim) */
+ / * purple_get_account_prpl_id() determines the actual protocol ID (icq/aim) * /
ret->data = NULL;
register_protocol(ret);
- }
+ }*/
info = g_new0(struct plugin_info, 1);
info->abiver = BITLBEE_ABI_VERSION_CODE;
diff --git a/protocols/twitter/twitter.c b/protocols/twitter/twitter.c
index fcd76989..d43d2909 100644
--- a/protocols/twitter/twitter.c
+++ b/protocols/twitter/twitter.c
@@ -304,7 +304,7 @@ static void twitter_main_loop_start(struct im_connection *ic)
struct groupchat *twitter_groupchat_init(struct im_connection *ic)
{
- char *name_hint;
+ char *name_hint, *tmp;
struct groupchat *gc;
struct twitter_data *td = ic->proto_data;
GSList *l;
@@ -315,7 +315,14 @@ struct groupchat *twitter_groupchat_init(struct im_connection *ic)
td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline");
- name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
+ tmp = set_getstr(&ic->acc->set, "channel_name");
+
+ if (tmp == NULL || strlen(tmp) == 0) {
+ name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
+ } else {
+ name_hint = g_strdup(tmp);
+ }
+
imcb_chat_name_hint(gc, name_hint);
g_free(name_hint);
@@ -343,8 +350,10 @@ void twitter_login_finish(struct im_connection *ic)
} else if (!(td->flags & TWITTER_MODE_ONE) &&
!(td->flags & TWITTER_HAVE_FRIENDS)) {
imcb_log(ic, "Getting contact list");
+
twitter_get_friends_ids(ic, -1);
twitter_get_mutes_ids(ic, -1);
+ twitter_get_blocks_ids(ic, -1);
twitter_get_noretweets_ids(ic, -1);
} else {
twitter_main_loop_start(ic);
@@ -503,6 +512,23 @@ static char *set_eval_commands(set_t * set, char *value)
}
}
+static char *set_eval_channel_name(set_t * set, char *value)
+{
+ size_t len;
+
+ if (value == NULL) {
+ return NULL;
+ }
+
+ len = strlen(value);
+
+ if (len < MAX_NICK_LENGTH && len > 0) {
+ return value;
+ } else {
+ return NULL;
+ }
+}
+
static char *set_eval_mode(set_t * set, char *value)
{
if (g_strcasecmp(value, "one") == 0 ||
@@ -513,6 +539,15 @@ static char *set_eval_mode(set_t * set, char *value)
}
}
+static char *set_eval_id_length(set_t * set, char *value)
+{
+ int len = atoi(value);
+ if (len >= 1 || len <= 4)
+ return value;
+
+ return SET_INVALID;
+}
+
static void twitter_init(account_t * acc)
{
set_t *s;
@@ -557,6 +592,14 @@ static void twitter_init(account_t * acc)
s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
+ s = set_add(&acc->set, "id_length", "2", set_eval_id_length, acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "channel_name", NULL, set_eval_channel_name, acc);
+ s->flags |= ACC_SET_OFFLINE_ONLY;
+
+ s = set_add(&acc->set, "autofill_reply", "true", set_eval_bool, acc);
+
s = set_add(&acc->set, "_last_tweet", "0", NULL, acc);
s->flags |= SET_HIDDEN | SET_NOSAVE;
@@ -580,6 +623,7 @@ static void twitter_login(account_t * acc)
char name[strlen(acc->user) + 9];
url_t url;
char *s;
+ size_t i;
if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) ||
(url.proto != PROTO_HTTP && url.proto != PROTO_HTTPS)) {
@@ -637,8 +681,17 @@ static void twitter_login(account_t * acc)
imcb_add_buddy(ic, name, NULL);
imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
- td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH);
+ td->id_length = set_getint(&ic->acc->set, "id_length");
+ td->log_length = 0;
+ for (i = 0; i < td->id_length; i++) {
+ td->log_length = (td->log_length << 4) + 0x0F;
+ }
+ td->log_length += 1;
+
+ td->log = g_new0(struct twitter_log_data, td->log_length);
+ td->filter_log = g_new0(struct twitter_log_data, td->log_length);
td->log_id = -1;
+ td->filter_log_id = -1;
s = set_getstr(&ic->acc->set, "mode");
if (g_strcasecmp(s, "one") == 0) {
@@ -649,6 +702,10 @@ static void twitter_login(account_t * acc)
td->flags |= TWITTER_MODE_CHAT;
}
+ td->mutes_ids = g_hash_table_new_full(g_str_hash, (GEqualFunc)strcmp, (GDestroyNotify)g_free, NULL);
+ td->blocks_ids = g_hash_table_new_full(g_str_hash, (GEqualFunc)strcmp, (GDestroyNotify)g_free, NULL);
+ td->noretweets_ids = g_hash_table_new_full(g_str_hash, (GEqualFunc)strcmp, (GDestroyNotify)g_free, NULL);
+
twitter_login_finish(ic);
}
@@ -674,11 +731,9 @@ static void twitter_logout(struct im_connection *ic)
b_event_remove(td->filter_update_id);
}
- g_slist_foreach(td->mutes_ids, (GFunc) g_free, NULL);
- g_slist_free(td->mutes_ids);
-
- g_slist_foreach(td->noretweets_ids, (GFunc) g_free, NULL);
- g_slist_free(td->noretweets_ids);
+ g_hash_table_unref(td->mutes_ids);
+ g_hash_table_unref(td->blocks_ids);
+ g_hash_table_unref(td->noretweets_ids);
http_close(td->stream);
twitter_filter_remove_all(ic);
@@ -882,7 +937,7 @@ static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, cha
if (arg[0] == '#') {
arg++;
}
- if (parse_int64(arg, 16, &id) && id < TWITTER_LOG_LENGTH) {
+ if (parse_int64(arg, 16, &id) && id < td->log_length) {
bu = td->log[id].bu;
id = td->log[id].id;
} else if (parse_int64(arg, 10, &id)) {
@@ -973,7 +1028,8 @@ static void twitter_handle_command(struct im_connection *ic, char *message)
twitter_report_spam(ic, screen_name);
goto eof;
- } else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) {
+ } else if ((g_strcasecmp(cmd[0], "rt") == 0 ||
+ g_strcasecmp(cmd[0], "retweet") == 0) && cmd[1]) {
id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
td->last_status_id = 0;
@@ -992,7 +1048,11 @@ static void twitter_handle_command(struct im_connection *ic, char *message)
"post any statuses recently", cmd[1]);
goto eof;
}
- message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]);
+ if (!set_getbool(&ic->acc->set, "autofill_reply")) {
+ message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]);
+ } else {
+ message = cmd[2];
+ }
in_reply_to = id;
allow_post = TRUE;
} else if (g_strcasecmp(cmd[0], "rawreply") == 0 && cmd[1] && cmd[2]) {
@@ -1012,12 +1072,43 @@ static void twitter_handle_command(struct im_connection *ic, char *message)
twitter_status_show_url(ic, id);
}
goto eof;
+ } else if (g_strcasecmp(cmd[0], "quote") == 0 && cmd[1]) {
+ id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
+ td->last_status_id = 0;
+ if (id) {
+ twitter_status_quote_post(ic, id, cmd[2]);
+ } else {
+ twitter_log(ic, "User '%s' does not exist or didn't "
+ "post any statuses recently", cmd[1]);
+ }
+
+ goto eof;
} else if (g_strcasecmp(cmd[0], "post") == 0) {
message += 5;
allow_post = TRUE;
+ } else if (g_strcasecmp(cmd[0], "dm") == 0 && cmd[1] && cmd[2]) {
+ if ((bu = bee_user_by_handle(ic->bee, ic, cmd[1]))) {
+ twitter_direct_messages_new(ic, bu->handle, cmd[2]);
+ } else {
+ twitter_direct_messages_new(ic, cmd[1], cmd[2]);
+ }
+ goto eof;
+ } else if (g_strcasecmp(cmd[0], "rtoff") == 0 && cmd[1]) {
+ twitter_retweet_enable_disable(ic, cmd[1], 0);
+ goto eof;
+ } else if (g_strcasecmp(cmd[0], "rton") == 0 && cmd[1]) {
+ twitter_retweet_enable_disable(ic, cmd[1], 1);
+ goto eof;
+ } else if (g_strcasecmp(cmd[0], "block") == 0 && cmd[1]) {
+ twitter_block_create_destroy(ic, cmd[1], 1);
+ goto eof;
+ } else if (g_strcasecmp(cmd[0], "unblock") == 0 && cmd[1]) {
+ twitter_block_create_destroy(ic, cmd[1], 0);
+ goto eof;
}
+
if (allow_post) {
char *s;
diff --git a/protocols/twitter/twitter.h b/protocols/twitter/twitter.h
index 86c88262..bf47d4fc 100644
--- a/protocols/twitter/twitter.h
+++ b/protocols/twitter/twitter.h
@@ -60,15 +60,18 @@ struct twitter_data {
guint64 timeline_id;
GSList *follow_ids;
- GSList *mutes_ids;
- GSList *noretweets_ids;
GSList *filters;
+ GHashTable *mutes_ids;
+ GHashTable *blocks_ids;
+ GHashTable *noretweets_ids;
guint64 last_status_id; /* For undo */
gint main_loop_id;
gint filter_update_id;
struct http_request *stream;
+ time_t stream_opentime;
struct http_request *filter_stream;
+ time_t filter_stream_opentime;
struct groupchat *timeline_gc;
gint http_fails;
twitter_flags_t flags;
@@ -84,6 +87,11 @@ struct twitter_data {
/* set show_ids */
struct twitter_log_data *log;
int log_id;
+ struct twitter_log_data *filter_log;
+ int filter_log_id;
+
+ int id_length;
+ int log_length;
};
#define TWITTER_FILTER_UPDATE_WAIT 3000
@@ -99,7 +107,6 @@ struct twitter_user_data {
time_t last_time;
};
-#define TWITTER_LOG_LENGTH 256
struct twitter_log_data {
guint64 id;
/* DANGER: bu can be a dead pointer. Check it first.
diff --git a/protocols/twitter/twitter_lib.c b/protocols/twitter/twitter_lib.c
index 8425c58e..4addb650 100644
--- a/protocols/twitter/twitter_lib.c
+++ b/protocols/twitter/twitter_lib.c
@@ -240,6 +240,7 @@ static json_value *twitter_parse_response(struct im_connection *ic, struct http_
static void twitter_http_get_friends_ids(struct http_request *req);
static void twitter_http_get_mutes_ids(struct http_request *req);
+static void twitter_http_get_blocks_ids(struct http_request *req);
static void twitter_http_get_noretweets_ids(struct http_request *req);
/**
@@ -272,6 +273,20 @@ void twitter_get_mutes_ids(struct im_connection *ic, gint64 next_cursor)
}
/**
+ * Get the blocked users ids.
+ */
+void twitter_get_blocks_ids(struct im_connection *ic, gint64 next_cursor)
+{
+ char *args[2];
+
+ args[0] = "cursor";
+ args[1] = g_strdup_printf("%" G_GINT64_FORMAT, next_cursor);
+ twitter_http(ic, TWITTER_BLOCKS_IDS_URL, twitter_http_get_blocks_ids, ic, 0, args, 2);
+
+ g_free(args[1]);
+}
+
+/**
* Get the ids for users from whom we should ignore retweets.
*/
void twitter_get_noretweets_ids(struct im_connection *ic, gint64 next_cursor)
@@ -367,14 +382,15 @@ static void twitter_http_get_friends_ids(struct http_request *req)
}
/**
- * Callback for getting the mutes ids.
+ * Callback for getting the blocks and mutes ids.
*/
-static void twitter_http_get_mutes_ids(struct http_request *req)
+static void twitter_http_get_mutes_blocks_ids(struct http_request *req, int blocks)
{
struct im_connection *ic = req->data;
json_value *parsed;
struct twitter_xml_list *txl;
struct twitter_data *td;
+ GSList *elem;
// Check if the connection is stil active
if (!g_slist_find(twitter_connections, ic)) {
@@ -394,24 +410,45 @@ static void twitter_http_get_mutes_ids(struct http_request *req)
}
txl = g_new0(struct twitter_xml_list, 1);
- txl->list = td->mutes_ids;
/* mute ids API response is similar enough to friends response
to reuse this method */
twitter_xt_get_friends_id_list(parsed, txl);
json_value_free(parsed);
- td->mutes_ids = txl->list;
+ for (elem = txl->list; elem; elem = elem->next) {
+ g_hash_table_add(blocks ? td->blocks_ids : td->mutes_ids, elem->data);
+ }
+
if (txl->next_cursor) {
/* Recurse while there are still more pages */
- twitter_get_mutes_ids(ic, txl->next_cursor);
+ if (blocks) {
+ twitter_get_blocks_ids(ic, txl->next_cursor);
+ } else {
+ twitter_get_mutes_ids(ic, txl->next_cursor);
+ }
}
- txl->list = NULL;
txl_free(txl);
}
/**
+ * Callback for getting the mutes ids.
+ */
+static void twitter_http_get_mutes_ids(struct http_request *req)
+{
+ twitter_http_get_mutes_blocks_ids(req, 0);
+}
+
+/**
+ * Callback for getting the blocks ids.
+ */
+static void twitter_http_get_blocks_ids(struct http_request *req)
+{
+ twitter_http_get_mutes_blocks_ids(req, 1);
+}
+
+/**
* Callback for getting the no-retweets ids.
*/
static void twitter_http_get_noretweets_ids(struct http_request *req)
@@ -439,8 +476,6 @@ static void twitter_http_get_noretweets_ids(struct http_request *req)
}
txl = g_new0(struct twitter_xml_list, 1);
- txl->list = td->noretweets_ids;
-
// Process the retweet ids
txl->type = TXL_ID;
if (parsed->type == json_array) {
@@ -450,15 +485,12 @@ static void twitter_http_get_noretweets_ids(struct http_request *req)
if (c->type != json_integer) {
continue;
}
- txl->list = g_slist_prepend(txl->list,
- g_strdup_printf("%" PRId64, c->u.integer));
+ g_hash_table_add(td->noretweets_ids, g_strdup_printf("%"PRId64, c->u.integer));
}
}
json_value_free(parsed);
- td->noretweets_ids = txl->list;
- txl->list = NULL;
txl_free(txl);
}
@@ -816,7 +848,7 @@ static char *twitter_msg_add_id(struct im_connection *ic,
if (txs->reply_to) {
int i;
- for (i = 0; i < TWITTER_LOG_LENGTH; i++) {
+ for (i = 0; i < td->log_length; i++) {
if (td->log[i].id == txs->reply_to) {
reply_to = i;
break;
@@ -834,26 +866,39 @@ static char *twitter_msg_add_id(struct im_connection *ic,
}
}
- td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH;
- td->log[td->log_id].id = txs->id;
- td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
+ if (txs->from_filter) {
+ td->filter_log_id = (td->filter_log_id + 1) % td->log_length;
+ td->filter_log[td->filter_log_id].id = txs->id;
+ td->filter_log[td->filter_log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
+ } else {
+ td->log_id = (td->log_id + 1) % td->log_length;
+ td->log[td->log_id].id = txs->id;
+ td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
+ }
/* This is all getting hairy. :-( If we RT'ed something ourselves,
remember OUR id instead so undo will work. In other cases, the
original tweet's id should be remembered for deduplicating. */
if (g_strcasecmp(txs->user->screen_name, td->user) == 0) {
- td->log[td->log_id].id = txs->rt_id;
- /* More useful than NULL. */
- td->log[td->log_id].bu = &twitter_log_local_user;
+ if (txs->from_filter) {
+ td->filter_log[td->filter_log_id].id = txs->rt_id;
+ /* More useful than NULL. */
+ td->filter_log[td->filter_log_id].bu = &twitter_log_local_user;
+ } else {
+ td->log[td->log_id].id = txs->rt_id;
+ /* More useful than NULL. */
+ td->log[td->log_id].bu = &twitter_log_local_user;
+ }
}
if (set_getbool(&ic->acc->set, "show_ids")) {
if (reply_to != -1) {
- return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s",
- td->log_id, reply_to, prefix, txs->text);
+ return g_strdup_printf("\002[\002%0*x->%0*x\002]\002 %s%s",
+ td->id_length, td->log_id, td->id_length,
+ reply_to, prefix, txs->text);
} else {
- return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
- td->log_id, prefix, txs->text);
+ return g_strdup_printf("\002[\002%0*x\002]\002 %s%s",
+ td->id_length, td->log_id, prefix, txs->text);
}
} else {
if (*prefix) {
@@ -981,11 +1026,12 @@ static void twitter_status_show(struct im_connection *ic, struct twitter_xml_sta
/* Check this is not a tweet that should be muted */
uid_str = g_strdup_printf("%" G_GUINT64_FORMAT, status->user->uid);
- if (g_slist_find_custom(td->mutes_ids, uid_str, (GCompareFunc)strcmp)) {
+ if (g_hash_table_lookup(td->mutes_ids, uid_str) ||
+ g_hash_table_lookup(td->blocks_ids, uid_str)) {
g_free(uid_str);
return;
}
- if (status->id != status->rt_id && g_slist_find_custom(td->noretweets_ids, uid_str, (GCompareFunc)strcmp)) {
+ if (status->id != status->rt_id && g_hash_table_lookup(td->noretweets_ids, uid_str)) {
g_free(uid_str);
return;
}
@@ -1034,8 +1080,23 @@ static void twitter_http_stream(struct http_request *req)
if ((req->flags & HTTPC_EOF) || !req->reply_body) {
if (req == td->stream) {
td->stream = NULL;
+
+ if (req->status_code == 200 &&
+ td->stream_opentime + 3 < time(NULL)) {
+ debug("Reconnecting to twitter stream.");
+ twitter_open_stream(ic);
+ twitter_get_timeline(ic, -1);
+ return;
+ }
} else if (req == td->filter_stream) {
td->filter_stream = NULL;
+
+ if (req->status_code == 200 &&
+ td->filter_stream_opentime + 3 < time(NULL)) {
+ debug("Reconnecting to twitter filter stream.");
+ twitter_open_filter_stream(ic);
+ return;
+ }
}
imcb_error(ic, "Stream closed (%s)", req->status_string);
@@ -1094,8 +1155,12 @@ static gboolean twitter_stream_handle_object(struct im_connection *ic, json_valu
} else if ((c = json_o_get(o, "direct_message")) &&
(txs = twitter_xt_get_dm(c))) {
if (g_strcasecmp(txs->user->screen_name, td->user) != 0) {
- imcb_buddy_msg(ic, txs->user->screen_name,
- txs->text, 0, txs->created_at);
+ char *uid_str = g_strdup_printf("%" PRIu64, txs->user->uid);
+ if (!g_hash_table_lookup(td->blocks_ids, uid_str)) {
+ imcb_buddy_msg(ic, txs->user->screen_name,
+ txs->text, 0, txs->created_at);
+ }
+ g_free(uid_str);
}
txs_free(txs);
return TRUE;
@@ -1119,10 +1184,12 @@ static gboolean twitter_stream_handle_object(struct im_connection *ic, json_valu
static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs)
{
struct twitter_data *td = ic->proto_data;
+ struct twitter_log_data *tl;
int i;
- for (i = 0; i < TWITTER_LOG_LENGTH; i++) {
- if (td->log[i].id == txs->id) {
+ tl = txs->from_filter ? td->filter_log : td->log;
+ for (i = 0; i < td->log_length; i++) {
+ if (tl[i].id == txs->id) {
/* Got a duplicate (RT, probably). Drop it. */
return TRUE;
}
@@ -1165,33 +1232,25 @@ static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value
if (g_strcasecmp(us->screen_name, td->user) == 0) {
twitter_add_buddy(ic, ut->screen_name, ut->name);
}
- } else if (strcmp(type, "mute") == 0) {
- GSList *found;
+ } else if (strcmp(type, "mute") == 0 || strcmp(type, "block") == 0) {
char *uid_str;
+ int mute = strcmp(type, "mute") == 0;
ut = twitter_xt_get_user(target);
uid_str = g_strdup_printf("%" G_GUINT64_FORMAT, ut->uid);
- if (!(found = g_slist_find_custom(td->mutes_ids, uid_str,
- (GCompareFunc)strcmp))) {
- td->mutes_ids = g_slist_prepend(td->mutes_ids, uid_str);
- }
- twitter_log(ic, "Muted user %s", ut->screen_name);
+ g_hash_table_add(mute ? td->mutes_ids : td->blocks_ids, uid_str);
+ twitter_log(ic, "%s user %s", mute ? "Muted" : "Blocked", ut->screen_name);
if (getenv("BITLBEE_DEBUG")) {
fprintf(stderr, "New mute: %s %"G_GUINT64_FORMAT"\n",
ut->screen_name, ut->uid);
}
- } else if (strcmp(type, "unmute") == 0) {
- GSList *found;
+ } else if (strcmp(type, "unmute") == 0 || strcmp(type, "unblock") == 0) {
char *uid_str;
+ int unmute = strcmp(type, "unmute") == 0;
ut = twitter_xt_get_user(target);
uid_str = g_strdup_printf("%" G_GUINT64_FORMAT, ut->uid);
- if ((found = g_slist_find_custom(td->mutes_ids, uid_str,
- (GCompareFunc)strcmp))) {
- char *found_str = found->data;
- td->mutes_ids = g_slist_delete_link(td->mutes_ids, found);
- g_free(found_str);
- }
+ g_hash_table_remove(unmute ? td->mutes_ids : td->blocks_ids, uid_str);
g_free(uid_str);
- twitter_log(ic, "Unmuted user %s", ut->screen_name);
+ twitter_log(ic, "%s user %s", unmute ? "Unmuted" : "Blocked", ut->screen_name);
if (getenv("BITLBEE_DEBUG")) {
fprintf(stderr, "New unmute: %s %"G_GUINT64_FORMAT"\n",
ut->screen_name, ut->uid);
@@ -1214,6 +1273,7 @@ gboolean twitter_open_stream(struct im_connection *ic)
/* This flag must be enabled or we'll get no data until EOF
(which err, kind of, defeats the purpose of a streaming API). */
td->stream->flags |= HTTPC_STREAMING;
+ td->stream_opentime = time(NULL);
return TRUE;
}
@@ -1269,6 +1329,7 @@ static gboolean twitter_filter_stream(struct im_connection *ic)
/* This flag must be enabled or we'll get no data until EOF
(which err, kind of, defeats the purpose of a streaming API). */
td->filter_stream->flags |= HTTPC_STREAMING;
+ td->filter_stream_opentime = time(NULL);
ret = TRUE;
}
@@ -1683,10 +1744,12 @@ static void twitter_http_post(struct http_request *req)
*/
void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to)
{
- char *args[4] = {
+ char *args[6] = {
"status", msg,
"in_reply_to_status_id",
- g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to)
+ g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to),
+ "auto_populate_reply_metadata",
+ "true",
};
if (set_getbool(&ic->acc->set, "in_korea") && !in_reply_to) {
@@ -1697,7 +1760,7 @@ void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_t
}
twitter_http(ic, TWITTER_STATUS_UPDATE_URL, twitter_http_post, ic, 1,
- args, in_reply_to ? 4 : 2);
+ args, in_reply_to ? (set_getbool(&ic->acc->set, "autofill_reply") ? 6 : 4) : 2);
g_free(args[3]);
}
@@ -1740,6 +1803,32 @@ void twitter_mute_create_destroy(struct im_connection *ic, char *who, int create
twitter_http_post, ic, 1, args, 2);
}
+/**
+ * Block or unblock a user
+ */
+void twitter_block_create_destroy(struct im_connection *ic, char *who, int create)
+{
+ char *args[2];
+
+ args[0] = "screen_name";
+ args[1] = who;
+ twitter_http(ic, create ? TWITTER_BLOCKS_CREATE_URL : TWITTER_BLOCKS_DESTROY_URL,
+ twitter_http_post, ic, 1, args, 2);
+}
+
+/**
+ * Enable or disable retweets for user
+ */
+void twitter_retweet_enable_disable(struct im_connection *ic, char *who, int enable)
+{
+ char *args[4];
+ args[0] = "screen_name";
+ args[1] = who;
+ args[2] = "retweets";
+ args[3] = enable ? "true" : "false";
+ twitter_http(ic, TWITTER_FRIENDSHIPS_UPDATE_URL, twitter_http_post, ic, 1, args, 4);
+}
+
void twitter_status_destroy(struct im_connection *ic, guint64 id)
{
char *url;
@@ -1831,3 +1920,63 @@ void twitter_status_show_url(struct im_connection *ic, guint64 id)
twitter_http(ic, url, twitter_http_status_show_url, ic, 0, NULL, 0);
g_free(url);
}
+
+struct twitter_http_msg {
+ struct im_connection *ic;
+ char *message;
+};
+
+static void twitter_http_status_quote_post(struct http_request *req)
+{
+ struct twitter_http_msg *thm = req->data;
+ struct im_connection *ic = thm->ic;
+ struct twitter_data *td = ic->proto_data;
+ char *message;
+ const char *name;
+ json_value *parsed, *id;
+
+ if (!g_slist_find(twitter_connections, ic)) {
+ goto eof;
+ }
+
+ if (!(parsed = twitter_parse_response(ic, req))) {
+ goto eof;
+ }
+
+ name = json_o_str(json_o_get(parsed, "user"), "screen_name");
+ id = json_o_get(parsed, "id");
+
+ if (name && id && id->type == json_integer) {
+ message = g_strdup_printf("%s https://twitter.com/%s/status/%" G_GUINT64_FORMAT, thm->message, name, id->u.integer);
+
+ /*if (!twitter_length_check(ic, message)) {
+ goto eof;
+ }*/
+
+ td->last_status_id = 0;
+ twitter_post_status(ic, message, 0);
+ } else {
+ twitter_log(ic, "Error: could not fetch url for quoted tweet.");
+ }
+
+ json_value_free(parsed);
+
+eof:
+ g_free(thm->message);
+ g_free(thm);
+}
+
+void twitter_status_quote_post(struct im_connection *ic, guint64 id, char *message)
+{
+ struct twitter_http_msg *thm;
+ char *url;
+
+ thm = g_new0(struct twitter_http_msg, 1);
+
+ thm->ic = ic;
+ thm->message = g_strdup(message);
+
+ url = g_strdup_printf("%s%" G_GUINT64_FORMAT "%s", TWITTER_STATUS_SHOW_URL, id, ".json");
+ twitter_http(ic, url, twitter_http_status_quote_post, thm, 0, NULL, 0);
+ g_free(url);
+}
diff --git a/protocols/twitter/twitter_lib.h b/protocols/twitter/twitter_lib.h
index 5a4b8550..03da6421 100644
--- a/protocols/twitter/twitter_lib.h
+++ b/protocols/twitter/twitter_lib.h
@@ -57,12 +57,14 @@
#define TWITTER_FRIENDSHIPS_CREATE_URL "/friendships/create.json"
#define TWITTER_FRIENDSHIPS_DESTROY_URL "/friendships/destroy.json"
#define TWITTER_FRIENDSHIPS_SHOW_URL "/friendships/show.json"
+#define TWITTER_FRIENDSHIPS_UPDATE_URL "/friendships/update.json"
/* Social graphs URLs */
#define TWITTER_FRIENDS_IDS_URL "/friends/ids.json"
#define TWITTER_FOLLOWERS_IDS_URL "/followers/ids.json"
#define TWITTER_MUTES_IDS_URL "/mutes/users/ids.json"
#define TWITTER_NORETWEETS_IDS_URL "/friendships/no_retweets/ids.json"
+#define TWITTER_BLOCKS_IDS_URL "/blocks/ids.json"
/* Account URLs */
#define TWITTER_ACCOUNT_RATE_LIMIT_URL "/account/rate_limit_status.json"
@@ -73,8 +75,8 @@
#define TWITTER_FAVORITE_DESTROY_URL "/favorites/destroy.json"
/* Block URLs */
-#define TWITTER_BLOCKS_CREATE_URL "/blocks/create/"
-#define TWITTER_BLOCKS_DESTROY_URL "/blocks/destroy/"
+#define TWITTER_BLOCKS_CREATE_URL "/blocks/create.json"
+#define TWITTER_BLOCKS_DESTROY_URL "/blocks/destroy.json"
/* Mute URLs */
#define TWITTER_MUTES_CREATE_URL "/mutes/users/create.json"
@@ -92,6 +94,7 @@ gboolean twitter_open_filter_stream(struct im_connection *ic);
gboolean twitter_get_timeline(struct im_connection *ic, gint64 next_cursor);
void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor);
void twitter_get_mutes_ids(struct im_connection *ic, gint64 next_cursor);
+void twitter_get_blocks_ids(struct im_connection *ic, gint64 next_cursor);
void twitter_get_noretweets_ids(struct im_connection *ic, gint64 next_cursor);
void twitter_get_statuses_friends(struct im_connection *ic, gint64 next_cursor);
@@ -99,11 +102,14 @@ void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_t
void twitter_direct_messages_new(struct im_connection *ic, char *who, char *message);
void twitter_friendships_create_destroy(struct im_connection *ic, char *who, int create);
void twitter_mute_create_destroy(struct im_connection *ic, char *who, int create);
+void twitter_block_create_destroy(struct im_connection *ic, char *who, int create);
+void twitter_retweet_enable_disable(struct im_connection *ic, char *who, int enable);
void twitter_status_destroy(struct im_connection *ic, guint64 id);
void twitter_status_retweet(struct im_connection *ic, guint64 id);
void twitter_report_spam(struct im_connection *ic, char *screen_name);
void twitter_favourite_tweet(struct im_connection *ic, guint64 id);
void twitter_status_show_url(struct im_connection *ic, guint64 id);
+void twitter_status_quote_post(struct im_connection *ic, guint64 id, char *message);
#endif //_TWITTER_LIB_H
diff --git a/root_commands.c b/root_commands.c
index c89b7c6a..d04476cf 100644
--- a/root_commands.c
+++ b/root_commands.c
@@ -295,6 +295,11 @@ static void cmd_save(irc_t *irc, char **cmd)
irc_rootmsg(irc, "Please create an account first (see \x02help register\x02)");
} else if (storage_save(irc, NULL, TRUE) == STORAGE_OK) {
irc_rootmsg(irc, "Configuration saved");
+
+ if (irc->save_source_id != -1) {
+ b_event_remove(irc->save_source_id);
+ irc->save_source_id = -1;
+ }
} else {
irc_rootmsg(irc, "Configuration could not be saved!");
}
@@ -368,6 +373,10 @@ static int cmd_set_real(irc_t *irc, char **cmd, set_t **head, cmd_set_checkflags
} else {
cmd_showset(irc, head, set_name);
}
+
+ if (st /* XXX: && !(s->flags & SET_NOSAVE)*/) {
+ storage_setup_auto_save(irc);
+ }
} else if (set_name) {
cmd_showset(irc, head, set_name);
} else {
@@ -675,6 +684,23 @@ static void cmd_channel(irc_t *irc, char **cmd)
"channels you're still in cannot be deleted).",
irc->default_channel->name);
}
+ } else if (len >= 1 && g_strncasecmp(cmd[2], "rename", len) == 0) {
+ if (strlen(cmd[3]) < 1) {
+ irc_rootmsg(irc, "You have to specify new name.");
+ } else if (!(ic->flags & IRC_CHANNEL_JOINED) &&
+ ic != ic->irc->default_channel) {
+ if (irc_channel_name_hint(ic, cmd[3])) {
+ irc_rootmsg(irc, "Channel %s renamed to %s.",
+ cmd[1], cmd[3]);
+ } else {
+ irc_rootmsg(irc, "Failed to rename channel %s to %s.",
+ cmd[1], cmd[3]);
+ }
+ } else {
+ irc_rootmsg(irc, "Couldn't rename channel (main channel %s or "
+ "channels you're still in cannot be renamed).",
+ irc->default_channel->name);
+ }
} else {
irc_rootmsg(irc,
"Unknown command: %s [...] %s. Please use \x02help commands\x02 to get a list of available commands.", "channel",
@@ -1029,6 +1055,8 @@ static void cmd_set(irc_t *irc, char **cmd)
cmd_set_real(irc, cmd, &irc->b->set, NULL);
}
+#define BLIST_NICK_MAXLEN 16
+
static void cmd_blist(irc_t *irc, char **cmd)
{
int online = 0, away = 0, offline = 0, ismatch = 0;
@@ -1037,7 +1065,8 @@ static void cmd_blist(irc_t *irc, char **cmd)
GError *error = NULL;
char s[256];
char *format;
- int n_online = 0, n_away = 0, n_offline = 0;
+ char *padded;
+ int n_online = 0, n_away = 0, n_offline = 0, b_mode = 0;
if (cmd[1] && g_strcasecmp(cmd[1], "all") == 0) {
online = offline = away = 1;
@@ -1062,11 +1091,14 @@ static void cmd_blist(irc_t *irc, char **cmd)
if (strchr(irc->umode, 'b') != NULL) {
format = "%s\t%s\t%s";
+ b_mode = 1;
} else {
- format = "%-24.24s %-40.40s %s";
+ format = "%s %-40.40s %s";
}
- irc_rootmsg(irc, format, "Nick", "Handle/Account", "Status");
+ padded = b_mode ? g_strdup("Nick") : str_pad_and_truncate("Nick", BLIST_NICK_MAXLEN, NULL);
+ irc_rootmsg(irc, format, padded, "Handle/Account", "Status");
+ g_free(padded);
if (irc->root->last_channel &&
strcmp(set_getstr(&irc->root->last_channel->set, "type"), "control") != 0) {
@@ -1096,7 +1128,11 @@ static void cmd_blist(irc_t *irc, char **cmd)
}
g_snprintf(s, sizeof(s) - 1, "%s %s", bu->handle, bu->ic->acc->tag);
- irc_rootmsg(irc, format, iu->nick, s, st);
+
+ padded = b_mode ? g_strdup(iu->nick) : str_pad_and_truncate(iu->nick, BLIST_NICK_MAXLEN, NULL);
+ irc_rootmsg(irc, format, padded, s, st);
+
+ g_free(padded);
}
n_online++;
@@ -1105,7 +1141,11 @@ static void cmd_blist(irc_t *irc, char **cmd)
if ((bu->flags & BEE_USER_ONLINE) && (bu->flags & BEE_USER_AWAY)) {
if (ismatch == 1 && away == 1) {
g_snprintf(s, sizeof(s) - 1, "%s %s", bu->handle, bu->ic->acc->tag);
- irc_rootmsg(irc, format, iu->nick, s, irc_user_get_away(iu));
+
+ padded = b_mode ? g_strdup(iu->nick) : str_pad_and_truncate(iu->nick, BLIST_NICK_MAXLEN, NULL);
+ irc_rootmsg(irc, format, padded, s, irc_user_get_away(iu));
+
+ g_free(padded);
}
n_away++;
}
@@ -1113,7 +1153,11 @@ static void cmd_blist(irc_t *irc, char **cmd)
if (!(bu->flags & BEE_USER_ONLINE)) {
if (ismatch == 1 && offline == 1) {
g_snprintf(s, sizeof(s) - 1, "%s %s", bu->handle, bu->ic->acc->tag);
- irc_rootmsg(irc, format, iu->nick, s, "Offline");
+
+ padded = b_mode ? g_strdup(iu->nick) : str_pad_and_truncate(iu->nick, BLIST_NICK_MAXLEN, NULL);
+ irc_rootmsg(irc, format, padded, s, "Offline");
+
+ g_free(padded);
}
n_offline++;
}
@@ -1560,6 +1604,13 @@ static void cmd_transfer(irc_t *irc, char **cmd)
}
}
+#ifdef WITH_GNUTLS
+static void cmd_certfp(irc_t *irc, char **cmd)
+{
+ irc_rootmsg(irc, "Show current/set new certfp");
+}
+#endif
+
/* Maybe this should be a stand-alone command as well? */
static void bitlbee_whatsnew(irc_t *irc)
{
@@ -1610,6 +1661,9 @@ command_t root_commands[] = {
{ "set", 0, cmd_set, 0 },
{ "transfer", 0, cmd_transfer, 0 },
{ "yes", 0, cmd_yesno, 0 },
+#ifdef WITH_GNUTLS
+ { "certfp", 1, cmd_certfp, 0 },
+#endif
/* Not expecting too many plugins adding root commands so just make a
dumb array with some empty entried at the end. */
{ NULL },
diff --git a/set.c b/set.c
index ec18b82d..ff2470b6 100644
--- a/set.c
+++ b/set.c
@@ -63,6 +63,8 @@ set_t *set_add(set_t **head, const char *key, const char *def, set_eval eval, vo
}
if (def) {
s->def = g_strdup(def);
+ } else {
+ s->flags |= SET_NULL_OK;
}
s->eval = eval;
diff --git a/storage.c b/storage.c
index 9e0f8345..9e16c9e2 100644
--- a/storage.c
+++ b/storage.c
@@ -64,6 +64,40 @@ static storage_t *storage_init_single(const char *name)
return st;
}
+static gboolean storage_auto_save(gpointer data, gint fd, b_input_condition cond)
+{
+ irc_t *irc = data;
+ if (!(irc->status & USTATUS_IDENTIFIED) || irc->save_source_id == -1) {
+ return FALSE;
+ }
+
+ //imcb_log(irc->b->accounts->ic, "Saving config"); /* XXX: debug */
+
+ /*if (getenv("BITLBEE_DEBUG")) {
+ fprintf(stderr, "Autosave\n");
+ }*/
+
+ if (storage_save(irc, NULL, 1) != STORAGE_OK) {
+ imcb_error(irc->b->accounts->ic, "Could not autosave.");
+ }
+
+ b_event_remove(irc->save_source_id);
+ irc->save_source_id = -1;
+
+ return FALSE;
+}
+
+void storage_setup_auto_save(irc_t *irc)
+{
+ if (!set_getbool(&irc->b->set, "auto_save")) {
+ return;
+ }
+
+ if ((irc->status & USTATUS_IDENTIFIED) && irc->save_source_id == -1) {
+ irc->save_source_id = b_timeout_add(30000, storage_auto_save, irc);
+ }
+}
+
GList *storage_init(const char *primary, char **migrate)
{
GList *ret = NULL;
diff --git a/storage.h b/storage.h
index 4f40a770..1312fe90 100644
--- a/storage.h
+++ b/storage.h
@@ -59,6 +59,7 @@ storage_status_t storage_load(irc_t * irc, const char *password);
storage_status_t storage_save(irc_t *irc, char *password, int overwrite);
storage_status_t storage_remove(const char *nick);
+void storage_setup_auto_save(irc_t *irc);
void register_storage_backend(storage_t *);
G_GNUC_MALLOC GList *storage_init(const char *primary, char **migrate);