diff options
-rw-r--r-- | Makefile | 22 | ||||
-rw-r--r-- | bitlbee.c | 21 | ||||
-rw-r--r-- | bitlbee.h | 2 | ||||
-rw-r--r-- | conf.c | 41 | ||||
-rw-r--r-- | conf.h | 7 | ||||
-rwxr-xr-x | configure | 14 | ||||
-rw-r--r-- | irc.c | 42 | ||||
-rw-r--r-- | irc.h | 39 | ||||
-rw-r--r-- | irc_cap.c | 1 | ||||
-rw-r--r-- | irc_channel.c | 103 | ||||
-rw-r--r-- | irc_commands.c | 13 | ||||
-rw-r--r-- | irc_im.c | 33 | ||||
-rw-r--r-- | irc_send.c | 78 | ||||
-rw-r--r-- | irc_user.c | 2 | ||||
-rw-r--r-- | lib/ssl_client.h | 6 | ||||
-rw-r--r-- | lib/ssl_gnutls.c | 73 | ||||
-rw-r--r-- | protocols/account.c | 92 | ||||
-rw-r--r-- | protocols/jabber/jabber.c | 6 | ||||
-rw-r--r-- | protocols/jabber/jabber.h | 18 | ||||
-rw-r--r-- | protocols/jabber/sasl.c | 263 | ||||
-rw-r--r-- | protocols/nogaim.c | 24 | ||||
-rw-r--r-- | protocols/purple/purple.c | 24 | ||||
-rw-r--r-- | protocols/twitter/twitter.c | 113 | ||||
-rw-r--r-- | protocols/twitter/twitter.h | 13 | ||||
-rw-r--r-- | protocols/twitter/twitter_lib.c | 245 | ||||
-rw-r--r-- | protocols/twitter/twitter_lib.h | 10 | ||||
-rw-r--r-- | root_commands.c | 66 | ||||
-rw-r--r-- | set.c | 2 | ||||
-rw-r--r-- | storage.c | 34 | ||||
-rw-r--r-- | storage.h | 1 |
30 files changed, 1258 insertions, 150 deletions
@@ -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 @@ -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); @@ -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) @@ -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); @@ -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[]); @@ -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 @@ -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); @@ -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); @@ -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); } } @@ -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; } @@ -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) { @@ -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 27ef16d2..8e410340 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++; } @@ -1565,6 +1609,13 @@ static void cmd_nick(irc_t *irc, char **cmd) irc_rootmsg(irc, "This command is deprecated. Try: account %s set display_name", cmd[1]); } +#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) { @@ -1616,6 +1667,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 }, @@ -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; @@ -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; @@ -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); |