aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--bitlbee.c4
-rw-r--r--doc/user-guide/commands.xml71
-rw-r--r--lib/misc.c69
-rw-r--r--protocols/account.c14
-rw-r--r--protocols/bee_chat.c15
-rw-r--r--protocols/jabber/iq.c4
-rw-r--r--protocols/jabber/jabber.c21
-rw-r--r--protocols/jabber/jabber.h1
-rw-r--r--protocols/jabber/jabber_util.c4
-rw-r--r--protocols/nogaim.c11
-rw-r--r--protocols/nogaim.h3
-rw-r--r--protocols/oscar/chat.c5
-rw-r--r--protocols/oscar/tlv.c6
-rw-r--r--protocols/twitter/twitter.c276
-rw-r--r--protocols/twitter/twitter.h18
-rw-r--r--protocols/twitter/twitter_lib.c287
-rw-r--r--protocols/twitter/twitter_lib.h3
-rw-r--r--unix.c2
19 files changed, 702 insertions, 116 deletions
diff --git a/.travis.yml b/.travis.yml
index 69336d67..d1d8ece5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
language: c
-script: dpkg-buildpackage -uc -us
+script: ./configure && make check && dpkg-buildpackage -uc -us
before_install:
- sudo apt-get update -qq
- - sudo apt-get install --no-install-recommends -qq asciidoc xsltproc xmlto lynx check libevent-dev libpurple-dev
+ - sudo apt-get install --no-install-recommends -qq asciidoc xsltproc xmlto lynx check libevent-dev libpurple-dev check
- wget http://dump.dequis.org/indexed/bitlbee-travis-libs/libotr5{,-dev}_4.1.0-2~bpo70+1_amd64.deb
- sudo dpkg -i *.deb
diff --git a/bitlbee.c b/bitlbee.c
index 90df135a..dae57f93 100644
--- a/bitlbee.c
+++ b/bitlbee.c
@@ -328,10 +328,6 @@ static gboolean bitlbee_io_new_client( gpointer data, gint fd, b_input_condition
{
irc_t *irc;
- /* Since we're fork()ing here, let's make sure we won't
- get the same random numbers as the parent/siblings. */
- srand( time( NULL ) ^ getpid() );
-
b_main_init();
/* Close the listening socket, we're a client. */
diff --git a/doc/user-guide/commands.xml b/doc/user-guide/commands.xml
index e7d56ba1..b840350c 100644
--- a/doc/user-guide/commands.xml
+++ b/doc/user-guide/commands.xml
@@ -846,6 +846,7 @@
<varlistentry><term>undo #[&lt;id&gt;]</term><listitem><para>Delete your last Tweet (or one with the given ID)</para></listitem></varlistentry>
<varlistentry><term>rt &lt;screenname|#id&gt;</term><listitem><para>Retweet someone's last Tweet (or one with the given ID)</para></listitem></varlistentry>
<varlistentry><term>reply &lt;screenname|#id&gt;</term><listitem><para>Reply to a Tweet (with a reply-to reference)</para></listitem></varlistentry>
+ <varlistentry><term>rawreply &lt;screenname|#id&gt;</term><listitem><para>Reply to a Tweet (with no reply-to reference)</para></listitem></varlistentry>
<varlistentry><term>report &lt;screenname|#id&gt;</term><listitem><para>Report the given user (or the user who posted the tweet with the given ID) for sending spam. This will also block them.</para></listitem></varlistentry>
<varlistentry><term>follow &lt;screenname&gt;</term><listitem><para>Start following a person</para></listitem></varlistentry>
<varlistentry><term>unfollow &lt;screenname&gt;</term><listitem><para>Stop following a person</para></listitem></varlistentry>
@@ -887,7 +888,13 @@
<bitlbee-setting name="display_name" type="string" scope="account">
<description>
<para>
- Currently only available for MSN connections. This setting allows you to read and change your "friendly name" for this connection. Since this is a server-side setting, it can't be changed when the account is off-line.
+ Currently only available for MSN connections, and for jabber groupchats.
+ </para>
+ <para>
+ For MSN: This setting allows you to read and change your "friendly name" for this connection. Since this is a server-side setting, it can't be changed when the account is off-line.
+ </para>
+ <para>
+ For jabber groupchats: this sets the default value of 'nick' for newly created groupchats. There is no way to set an account-wide nick like MSN.
</para>
</description>
</bitlbee-setting>
@@ -1083,6 +1090,68 @@
</bitlbee-setting>
+ <bitlbee-setting name="format_string" type="string" scope="account">
+ <default>^B[^B%i^B]^B %c</default>
+
+ <description>
+ <para>
+ This setting controls how tweets are displayed. Below are listed the two replacement attributes you may use in the string. If you wish to use a percent symbol in your string, ensure you escape it with a second percent symbol (i.e. "%%").
+ </para>
+
+ <para>
+ The default setting contains ^B as a control code for toggling of the bold property. Other similar properties (including color codes) are also functional in this setting. See the documentation for your IRC client for details on such control codes.
+ </para>
+
+ <variablelist>
+ <varlistentry><term>%i</term><listitem><para>The ID of the tweet</para></listitem></varlistentry>
+ <varlistentry><term>%c</term><listitem><para>The text of the tweet</para></listitem></varlistentry>
+ </variablelist>
+ </description>
+
+ </bitlbee-setting>
+
+ <bitlbee-setting name="reply_format_string" type="string" scope="account">
+ <default>^B[^B%i->%t^B]^B %c</default>
+
+ <description>
+ <para>
+ This setting controls how replies to tweets are displayed. Below are listed the three replacement attributes you may use in the string. If you wish to use a percent symbol in your string, ensure you escape it with a second percent symbol (i.e. "%%").
+ </para>
+
+ <para>
+ The default setting contains ^B as a control code for toggling of the bold property. Other similar properties (including color codes) are also functional in this setting. See the documentation for your IRC client for details on such control codes.
+ </para>
+
+ <variablelist>
+ <varlistentry><term>%i</term><listitem><para>The ID of this tweet</para></listitem></varlistentry>
+ <varlistentry><term>%r</term><listitem><para>The ID of the tweet being replied to</para></listitem></varlistentry>
+ <varlistentry><term>%c</term><listitem><para>The text of the tweet</para></listitem></varlistentry>
+ </variablelist>
+ </description>
+
+ </bitlbee-setting>
+
+ <bitlbee-setting name="retweet_format_string" type="string" scope="account">
+ <default>^B[^B%i^B]^B RT @%a: %c</default>
+
+ <description>
+ <para>
+ This setting controls how retweets are displayed. Below are listed the three replacement attributes you may use in the string. If you wish to use a percent symbol in your string, ensure you escape it with a second percent symbol (i.e. "%%").
+ </para>
+
+ <para>
+ The default setting contains ^B as a control code for toggling of the bold property. Other similar properties (including color codes) are also functional in this setting. See the documentation for your IRC client for details on such control codes.
+ </para>
+
+ <variablelist>
+ <varlistentry><term>%i</term><listitem><para>The ID of the tweet</para></listitem></varlistentry>
+ <varlistentry><term>%a</term><listitem><para>The author of the original tweet</para></listitem></varlistentry>
+ <varlistentry><term>%c</term><listitem><para>The text of the tweet</para></listitem></varlistentry>
+ </variablelist>
+ </description>
+
+ </bitlbee-setting>
+
<bitlbee-setting name="mobile_is_away" type="boolean" scope="global">
<default>false</default>
diff --git a/lib/misc.c b/lib/misc.c
index 6fedb48f..43322cab 100644
--- a/lib/misc.c
+++ b/lib/misc.c
@@ -413,69 +413,20 @@ signed int do_iconv( char *from_cs, char *to_cs, char *src, char *dst, size_t si
return outbuf - dst;
}
-/* A pretty reliable random number generator. Tries to use the /dev/random
- devices first, and falls back to the random number generator from libc
- when it fails. Opens randomizer devices with O_NONBLOCK to make sure a
- lack of entropy won't halt BitlBee. */
+/* A wrapper for /dev/urandom.
+ * If /dev/urandom is not present or not usable, it calls abort()
+ * to prevent bitlbee from working without a decent entropy source */
void random_bytes( unsigned char *buf, int count )
{
- static int use_dev = -1;
-
- /* Actually this probing code isn't really necessary, is it? */
- if( use_dev == -1 )
+ int fd;
+ if( ( ( fd = open( "/dev/urandom", O_RDONLY ) ) == -1 ) ||
+ ( read( fd, buf, count ) == -1 ) )
{
- if( access( "/dev/random", R_OK ) == 0 || access( "/dev/urandom", R_OK ) == 0 )
- use_dev = 1;
- else
- {
- use_dev = 0;
- srand( ( getpid() << 16 ) ^ time( NULL ) );
- }
- }
-
- if( use_dev )
- {
- int fd;
-
- /* At least on Linux, /dev/random can block if there's not
- enough entropy. We really don't want that, so if it can't
- give anything, use /dev/urandom instead. */
- if( ( fd = open( "/dev/random", O_RDONLY | O_NONBLOCK ) ) >= 0 )
- if( read( fd, buf, count ) == count )
- {
- close( fd );
- return;
- }
- close( fd );
-
- /* urandom isn't supposed to block at all, but just to be
- sure. If it blocks, we'll disable use_dev and use the libc
- randomizer instead. */
- if( ( fd = open( "/dev/urandom", O_RDONLY | O_NONBLOCK ) ) >= 0 )
- if( read( fd, buf, count ) == count )
- {
- close( fd );
- return;
- }
- close( fd );
-
- /* If /dev/random blocks once, we'll still try to use it
- again next time. If /dev/urandom also fails for some
- reason, stick with libc during this session. */
-
- use_dev = 0;
- srand( ( getpid() << 16 ) ^ time( NULL ) );
- }
-
- if( !use_dev )
- {
- int i;
-
- /* Possibly the LSB of rand() isn't very random on some
- platforms. Seems okay on at least Linux and OSX though. */
- for( i = 0; i < count; i ++ )
- buf[i] = rand() & 0xff;
+ log_message( LOGLVL_ERROR, "/dev/urandom not present - aborting" );
+ abort();
}
+
+ close( fd );
}
int is_bool( char *value )
diff --git a/protocols/account.c b/protocols/account.c
index 188e362e..234b9de2 100644
--- a/protocols/account.c
+++ b/protocols/account.c
@@ -28,7 +28,7 @@
#include "account.h"
static const char* account_protocols_local[] = {
- "gg", NULL
+ "gg", "whatsapp", NULL
};
static char *set_eval_nick_source( set_t *set, char *value );
@@ -350,9 +350,6 @@ static gboolean account_on_timeout( gpointer d, gint fd, b_input_condition cond
void account_on( bee_t *bee, account_t *a )
{
- GHashTableIter nicks;
- gpointer k, v;
-
if( a->ic )
{
/* Trying to enable an already-enabled account */
@@ -366,15 +363,6 @@ void account_on( bee_t *bee, account_t *a )
if( a->ic && !( a->ic->flags & ( OPT_SLOW_LOGIN | OPT_LOGGED_IN ) ) )
a->ic->keepalive = b_timeout_add( 120000, account_on_timeout, a->ic );
-
- if( a->flags & ACC_FLAG_LOCAL )
- {
- g_hash_table_iter_init(&nicks, a->nicks);
- while( g_hash_table_iter_next( &nicks, &k, &v ) )
- {
- a->prpl->add_buddy( a->ic, (char*) k, NULL );
- }
- }
}
void account_off( bee_t *bee, account_t *a )
diff --git a/protocols/bee_chat.c b/protocols/bee_chat.c
index 39110a10..e1d07925 100644
--- a/protocols/bee_chat.c
+++ b/protocols/bee_chat.c
@@ -79,6 +79,13 @@ void imcb_chat_free( struct groupchat *c )
g_free( c );
}
+static gboolean handle_is_self( struct im_connection *ic, const char *handle )
+{
+ return ( ic->acc->prpl->handle_is_self ) ?
+ ic->acc->prpl->handle_is_self( ic, handle ) :
+ ( ic->acc->prpl->handle_cmp( ic->acc->user, handle ) == 0 );
+}
+
void imcb_chat_msg( struct groupchat *c, const char *who, char *msg, uint32_t flags, time_t sent_at )
{
struct im_connection *ic = c->ic;
@@ -88,7 +95,7 @@ void imcb_chat_msg( struct groupchat *c, const char *who, char *msg, uint32_t fl
char *s;
/* Gaim sends own messages through this too. IRC doesn't want this, so kill them */
- if( g_strcasecmp( who, ic->acc->user ) == 0 )
+ if( handle_is_self( ic, who ) )
return;
bu = bee_user_by_handle( bee, ic, who );
@@ -138,7 +145,7 @@ void imcb_chat_topic( struct groupchat *c, char *who, char *topic, time_t set_at
if( who == NULL)
bu = NULL;
- else if( g_strcasecmp( who, ic->acc->user ) == 0 )
+ else if( handle_is_self( ic, who ) )
bu = bee->user;
else
bu = bee_user_by_handle( bee, ic, who );
@@ -160,7 +167,7 @@ void imcb_chat_add_buddy( struct groupchat *c, const char *handle )
if( set_getbool( &c->ic->bee->set, "debug" ) )
imcb_log( c->ic, "User %s added to conversation %p", handle, c );
- me = ic->acc->prpl->handle_cmp( handle, ic->acc->user ) == 0;
+ me = handle_is_self( ic, handle );
/* Most protocols allow people to join, even when they're not in
your contact list. Try to handle that here */
@@ -188,7 +195,7 @@ void imcb_chat_remove_buddy( struct groupchat *c, const char *handle, const char
imcb_log( ic, "User %s removed from conversation %p (%s)", handle, c, reason ? reason : "" );
/* It might be yourself! */
- if( g_strcasecmp( handle, ic->acc->user ) == 0 )
+ if( handle_is_self( ic, handle ) )
{
if( c->joined == 0 )
return;
diff --git a/protocols/jabber/iq.c b/protocols/jabber/iq.c
index 61417bcc..cf1ff298 100644
--- a/protocols/jabber/iq.c
+++ b/protocols/jabber/iq.c
@@ -357,10 +357,6 @@ xt_status jabber_pkt_bind_sess( struct im_connection *ic, struct xt_node *node,
if( s )
*s = '\0';
jabber_set_me( ic, c->text );
- imcb_log( ic, "Server claims your JID is `%s' instead of `%s'. "
- "This mismatch may cause problems with groupchats "
- "and possibly other things.",
- c->text, ic->acc->user );
if( s )
*s = '/';
}
diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c
index 4b5cb3a1..30e55159 100644
--- a/protocols/jabber/jabber.c
+++ b/protocols/jabber/jabber.c
@@ -62,6 +62,8 @@ static void jabber_init( account_t *acc )
s = set_add( &acc->set, "oauth", "false", set_eval_oauth, acc );
+ s = set_add( &acc->set, "display_name", NULL, NULL, acc );
+
g_snprintf( str, sizeof( str ), "%d", jabber_port_list[0] );
s = set_add( &acc->set, "port", str, set_eval_int, acc );
s->flags |= ACC_SET_OFFLINE_ONLY;
@@ -317,6 +319,7 @@ static void jabber_logout( struct im_connection *ic )
g_free( jd->oauth2_access_token );
g_free( jd->away_message );
+ g_free( jd->internal_jid );
g_free( jd->username );
g_free( jd->me );
g_free( jd );
@@ -472,14 +475,22 @@ static void jabber_remove_buddy( struct im_connection *ic, char *who, char *grou
static struct groupchat *jabber_chat_join_( struct im_connection *ic, const char *room, const char *nick, const char *password, set_t **sets )
{
struct jabber_data *jd = ic->proto_data;
+ char *final_nick;
+ /* Ignore the passed nick parameter if we have our own default */
+ if ( !( final_nick = set_getstr( sets, "nick" ) ) &&
+ !( final_nick = set_getstr( &ic->acc->set, "display_name" ) ) ) {
+ /* Well, whatever, actually use the provided default, then */
+ final_nick = (char *) nick;
+ }
+
if( strchr( room, '@' ) == NULL )
imcb_error( ic, "%s is not a valid Jabber room name. Maybe you mean %s@conference.%s?",
room, room, jd->server );
else if( jabber_chat_by_jid( ic, room ) )
imcb_error( ic, "Already present in chat `%s'", room );
else
- return jabber_chat_join( ic, room, nick, set_getstr( sets, "password" ) );
+ return jabber_chat_join( ic, room, final_nick, set_getstr( sets, "password" ) );
return NULL;
}
@@ -620,6 +631,13 @@ void *jabber_buddy_action( struct bee_user *bu, const char *action, char * const
return NULL;
}
+gboolean jabber_handle_is_self( struct im_connection *ic, const char *who ) {
+ struct jabber_data *jd = ic->proto_data;
+ return ( ( g_strcasecmp( who, ic->acc->user ) == 0 ) ||
+ ( jd->internal_jid &&
+ g_strcasecmp( who, jd->internal_jid ) == 0 ) );
+}
+
void jabber_initmodule()
{
struct prpl *ret = g_new0( struct prpl, 1 );
@@ -647,6 +665,7 @@ void jabber_initmodule()
ret->keepalive = jabber_keepalive;
ret->send_typing = jabber_send_typing;
ret->handle_cmp = g_strcasecmp;
+ ret->handle_is_self = jabber_handle_is_self;
ret->transfer_request = jabber_si_transfer_request;
ret->buddy_action_list = jabber_buddy_action_list;
ret->buddy_action = jabber_buddy_action;
diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h
index eb99f9ca..a5882767 100644
--- a/protocols/jabber/jabber.h
+++ b/protocols/jabber/jabber.h
@@ -96,6 +96,7 @@ struct jabber_data
char *username; /* USERNAME@server */
char *server; /* username@SERVER -=> server/domain, not hostname */
char *me; /* bare jid */
+ char *internal_jid;
const struct oauth2_service *oauth2_service;
char *oauth2_access_token;
diff --git a/protocols/jabber/jabber_util.c b/protocols/jabber/jabber_util.c
index d6396802..1a3d9fd4 100644
--- a/protocols/jabber/jabber_util.c
+++ b/protocols/jabber/jabber_util.c
@@ -823,6 +823,10 @@ gboolean jabber_set_me( struct im_connection *ic, const char *me )
jd->server = strchr( jd->me, '@' );
jd->username = g_strndup( jd->me, jd->server - jd->me );
jd->server ++;
+
+ /* Set the "internal" account username, for groupchats */
+ g_free( jd->internal_jid );
+ jd->internal_jid = g_strdup( jd->me );
return TRUE;
}
diff --git a/protocols/nogaim.c b/protocols/nogaim.c
index ff1c9a85..0a674b40 100644
--- a/protocols/nogaim.c
+++ b/protocols/nogaim.c
@@ -293,6 +293,17 @@ void imcb_connected( struct im_connection *ic )
function should be handled correctly. (IOW, ignored) */
if( ic->flags & OPT_LOGGED_IN )
return;
+
+ if( ic->acc->flags & ACC_FLAG_LOCAL )
+ {
+ GHashTableIter nicks;
+ gpointer k, v;
+ g_hash_table_iter_init( &nicks, ic->acc->nicks );
+ while( g_hash_table_iter_next( &nicks, &k, &v ) )
+ {
+ ic->acc->prpl->add_buddy( ic, (char*) k, NULL );
+ }
+ }
imcb_log( ic, "Logged in" );
diff --git a/protocols/nogaim.h b/protocols/nogaim.h
index c236a0b5..d1711cde 100644
--- a/protocols/nogaim.h
+++ b/protocols/nogaim.h
@@ -262,6 +262,9 @@ struct prpl {
GList *(* buddy_action_list) (struct bee_user *bu);
void *(* buddy_action) (struct bee_user *bu, const char *action, char * const args[], void *data);
+ /* If null, equivalent to handle_cmp( ic->acc->user, who ) */
+ gboolean (* handle_is_self) (struct im_connection *, const char *who);
+
/* Some placeholders so eventually older plugins may cooperate with newer BitlBees. */
void *resv1;
void *resv2;
diff --git a/protocols/oscar/chat.c b/protocols/oscar/chat.c
index d8399563..6c8d8998 100644
--- a/protocols/oscar/chat.c
+++ b/protocols/oscar/chat.c
@@ -66,7 +66,8 @@ int aim_chat_send_im(aim_session_t *sess, aim_conn_t *conn, guint16 flags, const
*
*/
for (i = 0; i < sizeof(ckstr); i++)
- aimutil_put8(ckstr+i, (guint8) rand());
+ (void) aimutil_put8(ckstr+i, (guint8) rand());
+
cookie = aim_mkcookie(ckstr, AIM_COOKIETYPE_CHAT, NULL);
cookie->data = NULL; /* XXX store something useful here */
@@ -227,7 +228,7 @@ int aim_chat_invite(aim_session_t *sess, aim_conn_t *conn, const char *sn, const
* Cookie
*/
for (i = 0; i < sizeof(ckstr); i++)
- aimutil_put8(ckstr, (guint8) rand());
+ (void) aimutil_put8(ckstr, (guint8) rand());
/* XXX should be uncached by an unwritten 'invite accept' handler */
if ((priv = g_malloc(sizeof(struct aim_invite_priv)))) {
diff --git a/protocols/oscar/tlv.c b/protocols/oscar/tlv.c
index 89ef6f26..e7e3a7be 100644
--- a/protocols/oscar/tlv.c
+++ b/protocols/oscar/tlv.c
@@ -180,7 +180,7 @@ int aim_addtlvtochain8(aim_tlvlist_t **list, const guint16 t, const guint8 v)
{
guint8 v8[1];
- aimutil_put8(v8, v);
+ (void) aimutil_put8(v8, v);
return aim_addtlvtochain_raw(list, t, 1, v8);
}
@@ -198,7 +198,7 @@ int aim_addtlvtochain16(aim_tlvlist_t **list, const guint16 t, const guint16 v)
{
guint8 v16[2];
- aimutil_put16(v16, v);
+ (void) aimutil_put16(v16, v);
return aim_addtlvtochain_raw(list, t, 2, v16);
}
@@ -216,7 +216,7 @@ int aim_addtlvtochain32(aim_tlvlist_t **list, const guint16 t, const guint32 v)
{
guint8 v32[4];
- aimutil_put32(v32, v);
+ (void) aimutil_put32(v32, v);
return aim_addtlvtochain_raw(list, t, 4, v32);
}
diff --git a/protocols/twitter/twitter.c b/protocols/twitter/twitter.c
index d2aafcb4..8f5dc168 100644
--- a/protocols/twitter/twitter.c
+++ b/protocols/twitter/twitter.c
@@ -30,6 +30,215 @@
#include "url.h"
GSList *twitter_connections = NULL;
+
+static int twitter_filter_cmp(struct twitter_filter *tf1,
+ struct twitter_filter *tf2)
+{
+ int i1 = 0;
+ int i2 = 0;
+ int i;
+
+ static const twitter_filter_type_t types[] = {
+ /* Order of the types */
+ TWITTER_FILTER_TYPE_FOLLOW,
+ TWITTER_FILTER_TYPE_TRACK
+ };
+
+ for (i = 0; i < G_N_ELEMENTS(types); i++) {
+ if (types[i] == tf1->type) {
+ i1 = i + 1;
+ break;
+ }
+ }
+
+ for (i = 0; i < G_N_ELEMENTS(types); i++) {
+ if (types[i] == tf2->type) {
+ i2 = i + 1;
+ break;
+ }
+ }
+
+ if (i1 != i2) {
+ /* With different types, return their difference */
+ return i1 - i2;
+ }
+
+ /* With the same type, return the text comparison */
+ return g_strcasecmp(tf1->text, tf2->text);
+}
+
+static gboolean twitter_filter_update(gpointer data, gint fd,
+ b_input_condition cond)
+{
+ struct im_connection *ic = data;
+ struct twitter_data *td = ic->proto_data;
+
+ if (td->filters) {
+ twitter_open_filter_stream(ic);
+ } else if (td->filter_stream) {
+ http_close(td->filter_stream);
+ td->filter_stream = NULL;
+ }
+
+ td->filter_update_id = 0;
+ return FALSE;
+}
+
+static struct twitter_filter *twitter_filter_get(struct groupchat *c,
+ twitter_filter_type_t type,
+ const char *text)
+{
+ struct twitter_data *td = c->ic->proto_data;
+ struct twitter_filter *tf = NULL;
+ struct twitter_filter tfc = {type, (char*) text};
+ GSList *l;
+
+ for (l = td->filters; l; l = g_slist_next(l)) {
+ tf = l->data;
+
+ if (twitter_filter_cmp(tf, &tfc) == 0)
+ break;
+
+ tf = NULL;
+ }
+
+ if (!tf) {
+ tf = g_new0(struct twitter_filter, 1);
+ tf->type = type;
+ tf->text = g_strdup(text);
+ td->filters = g_slist_prepend(td->filters, tf);
+ }
+
+ if (!g_slist_find(tf->groupchats, c))
+ tf->groupchats = g_slist_prepend(tf->groupchats, c);
+
+ if (td->filter_update_id > 0)
+ b_event_remove(td->filter_update_id);
+
+ /* Wait for other possible filter changes to avoid request spam */
+ td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
+ twitter_filter_update, c->ic);
+ return tf;
+}
+
+static void twitter_filter_free(struct twitter_filter *tf)
+{
+ g_slist_free(tf->groupchats);
+ g_free(tf->text);
+ g_free(tf);
+}
+
+static void twitter_filter_remove(struct groupchat *c)
+{
+ struct twitter_data *td = c->ic->proto_data;
+ struct twitter_filter *tf;
+ GSList *l = td->filters;
+ GSList *p;
+
+ while (l != NULL) {
+ tf = l->data;
+ tf->groupchats = g_slist_remove(tf->groupchats, c);
+
+ p = l;
+ l = g_slist_next(l);
+
+ if (!tf->groupchats) {
+ twitter_filter_free(tf);
+ td->filters = g_slist_delete_link(td->filters, p);
+ }
+ }
+
+ if (td->filter_update_id > 0)
+ b_event_remove(td->filter_update_id);
+
+ /* Wait for other possible filter changes to avoid request spam */
+ td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
+ twitter_filter_update, c->ic);}
+
+static void twitter_filter_remove_all(struct im_connection *ic)
+{
+ struct twitter_data *td = ic->proto_data;
+ GSList *chats = NULL;
+ struct twitter_filter *tf;
+ GSList *l = td->filters;
+ GSList *p;
+
+ while (l != NULL) {
+ tf = l->data;
+
+ /* Build up a list of groupchats to be freed */
+ for (p = tf->groupchats; p; p = g_slist_next(p)) {
+ if (!g_slist_find(chats, p->data))
+ chats = g_slist_prepend(chats, p->data);
+ }
+
+ p = l;
+ l = g_slist_next(l);
+ twitter_filter_free(p->data);
+ td->filters = g_slist_delete_link(td->filters, p);
+ }
+
+ l = chats;
+
+ while (l != NULL) {
+ p = l;
+ l = g_slist_next(l);
+
+ /* Freed each remaining groupchat */
+ imcb_chat_free(p->data);
+ chats = g_slist_delete_link(chats, p);
+ }
+
+ if (td->filter_stream) {
+ http_close(td->filter_stream);
+ td->filter_stream = NULL;
+ }
+}
+
+static GSList *twitter_filter_parse(struct groupchat *c, const char *text)
+{
+ char **fs = g_strsplit(text, ";", 0);
+ GSList *ret = NULL;
+ struct twitter_filter *tf;
+ char **f;
+ char *v;
+ int i;
+ int t;
+
+ static const twitter_filter_type_t types[] = {
+ TWITTER_FILTER_TYPE_FOLLOW,
+ TWITTER_FILTER_TYPE_TRACK
+ };
+
+ static const char *typestrs[] = {
+ "follow",
+ "track"
+ };
+
+ for (f = fs; *f; f++) {
+ if ((v = strchr(*f, ':')) == NULL)
+ continue;
+
+ *(v++) = 0;
+
+ for (t = -1, i = 0; i < G_N_ELEMENTS(types); i++) {
+ if (g_strcasecmp(typestrs[i], *f) == 0) {
+ t = i;
+ break;
+ }
+ }
+
+ if (t < 0 || strlen(v) == 0)
+ continue;
+
+ tf = twitter_filter_get(c, types[t], v);
+ ret = g_slist_prepend(ret, tf);
+ }
+
+ g_strfreev(fs);
+ return ret;
+}
+
/**
* Main loop function
*/
@@ -329,6 +538,10 @@ static void twitter_init(account_t * acc)
s = set_add(&acc->set, "show_old_mentions", "0", set_eval_int, acc);
s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
+
+ s = set_add(&acc->set, "format_string", "\002[\002%i\002]\002 %c", NULL, acc);
+ s = set_add(&acc->set, "retweet_format_string", "\002[\002%i\002]\002 RT @%a: %c", NULL, acc);
+ s = set_add(&acc->set, "reply_format_string", "\002[\002%i->%r\002]\002 %c", NULL, acc);
s = set_add(&acc->set, "_last_tweet", "0", NULL, acc);
s->flags |= SET_HIDDEN | SET_NOSAVE;
@@ -435,7 +648,11 @@ static void twitter_logout(struct im_connection *ic)
imcb_chat_free(td->timeline_gc);
if (td) {
+ if (td->filter_update_id > 0)
+ b_event_remove(td->filter_update_id);
+
http_close(td->stream);
+ twitter_filter_remove_all(ic);
oauth_info_free(td->oauth_info);
g_free(td->user);
g_free(td->prefix);
@@ -508,12 +725,57 @@ static void twitter_chat_invite(struct groupchat *c, char *who, char *message)
{
}
+static struct groupchat *twitter_chat_join(struct im_connection *ic,
+ const char *room, const char *nick,
+ const char *password, set_t **sets)
+{
+ struct groupchat *c = imcb_chat_new(ic, room);
+ GSList *fs = twitter_filter_parse(c, room);
+ GString *topic = g_string_new("");
+ struct twitter_filter *tf;
+ GSList *l;
+
+ fs = g_slist_sort(fs, (GCompareFunc) twitter_filter_cmp);
+
+ for (l = fs; l; l = g_slist_next(l)) {
+ tf = l->data;
+
+ if (topic->len > 0)
+ g_string_append(topic, ", ");
+
+ if (tf->type == TWITTER_FILTER_TYPE_FOLLOW)
+ g_string_append_c(topic, '@');
+
+ g_string_append(topic, tf->text);
+ }
+
+ if (topic->len > 0)
+ g_string_prepend(topic, "Twitter Filter: ");
+
+ imcb_chat_topic(c, NULL, topic->str, 0);
+ imcb_chat_add_buddy(c, ic->acc->user);
+
+ if (topic->len == 0) {
+ imcb_error(ic, "Failed to handle any filters");
+ imcb_chat_free(c);
+ c = NULL;
+ }
+
+ g_string_free(topic, TRUE);
+ g_slist_free(fs);
+
+ return c;
+}
+
static void twitter_chat_leave(struct groupchat *c)
{
struct twitter_data *td = c->ic->proto_data;
- if (c != td->timeline_gc)
- return; /* WTF? */
+ if (c != td->timeline_gc) {
+ twitter_filter_remove(c);
+ imcb_chat_free(c);
+ return;
+ }
/* If the user leaves the channel: Fine. Rejoin him/her once new
tweets come in. */
@@ -673,6 +935,15 @@ static void twitter_handle_command(struct im_connection *ic, char *message)
message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]);
in_reply_to = id;
allow_post = TRUE;
+ } else if (g_strcasecmp(cmd[0], "rawreply") == 0 && cmd[1] && cmd[2]) {
+ id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
+ if (!id) {
+ twitter_log(ic, "Tweet `%s' does not exist", cmd[1]);
+ goto eof;
+ }
+ message = cmd[2];
+ in_reply_to = id;
+ allow_post = TRUE;
} else if (g_strcasecmp(cmd[0], "post") == 0) {
message += 5;
allow_post = TRUE;
@@ -747,6 +1018,7 @@ void twitter_initmodule()
ret->remove_buddy = twitter_remove_buddy;
ret->chat_msg = twitter_chat_msg;
ret->chat_invite = twitter_chat_invite;
+ ret->chat_join = twitter_chat_join;
ret->chat_leave = twitter_chat_leave;
ret->keepalive = twitter_keepalive;
ret->add_permit = twitter_add_permit;
diff --git a/protocols/twitter/twitter.h b/protocols/twitter/twitter.h
index 8792b7c9..00230cc0 100644
--- a/protocols/twitter/twitter.h
+++ b/protocols/twitter/twitter.h
@@ -44,6 +44,12 @@ typedef enum
TWITTER_GOT_MENTIONS = 0x40000,
} twitter_flags_t;
+typedef enum
+{
+ TWITTER_FILTER_TYPE_FOLLOW = 0,
+ TWITTER_FILTER_TYPE_TRACK
+} twitter_filter_type_t;
+
struct twitter_log_data;
struct twitter_data
@@ -57,10 +63,13 @@ struct twitter_data
guint64 timeline_id;
GSList *follow_ids;
+ GSList *filters;
guint64 last_status_id; /* For undo */
gint main_loop_id;
+ gint filter_update_id;
struct http_request *stream;
+ struct http_request *filter_stream;
struct groupchat *timeline_gc;
gint http_fails;
twitter_flags_t flags;
@@ -78,6 +87,15 @@ struct twitter_data
int log_id;
};
+#define TWITTER_FILTER_UPDATE_WAIT 3000
+struct twitter_filter
+{
+ twitter_filter_type_t type;
+ char *text;
+ guint64 uid;
+ GSList *groupchats;
+};
+
struct twitter_user_data
{
guint64 last_id;
diff --git a/protocols/twitter/twitter_lib.c b/protocols/twitter/twitter_lib.c
index 718867a7..b827a139 100644
--- a/protocols/twitter/twitter_lib.c
+++ b/protocols/twitter/twitter_lib.c
@@ -50,6 +50,7 @@ struct twitter_xml_list {
};
struct twitter_xml_user {
+ guint64 uid;
char *name;
char *screen_name;
};
@@ -60,6 +61,8 @@ struct twitter_xml_status {
struct twitter_xml_user *user;
guint64 id, rt_id; /* Usually equal, with RTs id == *original* id */
guint64 reply_to;
+ gboolean from_filter;
+ struct twitter_xml_status *rt;
};
/**
@@ -85,6 +88,7 @@ static void txs_free(struct twitter_xml_status *txs)
g_free(txs->text);
txu_free(txs->user);
+ txs_free(txs->rt);
g_free(txs);
}
@@ -391,11 +395,15 @@ static void twitter_http_get_users_lookup(struct http_request *req)
struct twitter_xml_user *twitter_xt_get_user(const json_value *node)
{
struct twitter_xml_user *txu;
+ json_value *jv;
txu = g_new0(struct twitter_xml_user, 1);
txu->name = g_strdup(json_o_str(node, "name"));
txu->screen_name = g_strdup(json_o_str(node, "screen_name"));
+ jv = json_o_get(node, "id");
+ txu->uid = jv->u.integer;
+
return txu;
}
@@ -482,9 +490,9 @@ static struct twitter_xml_status *twitter_xt_get_status(const json_value *node)
struct twitter_xml_status *rtxs = twitter_xt_get_status(rt);
if (rtxs) {
g_free(txs->text);
- txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text);
+ txs->text = g_strdup(rtxs->text);
txs->id = rtxs->id;
- txs_free(rtxs);
+ txs->rt = rtxs;
}
} else if (entities) {
txs->text = expand_entities(txs->text, entities);
@@ -602,6 +610,49 @@ static gboolean twitter_xt_get_status_list(struct im_connection *ic, const json_
return TRUE;
}
+/**
+ * Function to properly format a tweet as per the users configuration.
+ */
+static char *twitter_msg_get_text(struct im_connection *ic, int log_id, int reply_to,
+ struct twitter_xml_status *txs, const char *prefix) {
+ gchar * format = set_getstr(&ic->acc->set, "format_string");
+ GString * text = g_string_new(NULL);
+
+ gchar *c;
+ if (reply_to != -1)
+ format = set_getstr(&ic->acc->set, "reply_format_string");
+ if (txs->rt)
+ format = set_getstr(&ic->acc->set, "retweet_format_string");
+
+ for (c = format; *c ; c++) {
+ if (!(*c == '%' && *(c+1))) {
+ text = g_string_append_c(text, *c);
+ continue;
+ }
+ c++; // Move past the %
+ switch (*c) {
+ case 'i':
+ g_string_append_printf(text, "%02x", log_id);
+ break;
+ case 'r':
+ if (reply_to != -1) // In case someone does put %r in the wrong format_string
+ g_string_append_printf(text, "%02x", reply_to);
+ break;
+ case 'a':
+ if (txs->rt) // In case someone does put %a in the wrong format_string
+ text = g_string_append(text, txs->rt->user->screen_name);
+ break;
+ case 'c':
+ text = g_string_append(text, txs->text);
+ break;
+ default:
+ text = g_string_append_c(text, *c);
+ }
+ }
+ text = g_string_prepend(text, prefix);
+ return g_string_free(text, FALSE);
+}
+
/* Will log messages either way. Need to keep track of IDs for stream deduping.
Plus, show_ids is on by default and I don't see why anyone would disable it. */
static char *twitter_msg_add_id(struct im_connection *ic,
@@ -640,19 +691,45 @@ static char *twitter_msg_add_id(struct im_connection *ic,
if (g_strcasecmp(txs->user->screen_name, td->user) == 0)
td->log[td->log_id].id = txs->rt_id;
- 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);
- else
- return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
- td->log_id, prefix, txs->text);
- } else {
- if (*prefix)
- return g_strconcat(prefix, txs->text, NULL);
- else
- return NULL;
+ return twitter_msg_get_text(ic, td->log_id, reply_to, txs, prefix);
+}
+
+/**
+ * Function that is called to see the filter statuses in groupchat windows.
+ */
+static void twitter_status_show_filter(struct im_connection *ic, struct twitter_xml_status *status)
+{
+ struct twitter_data *td = ic->proto_data;
+ char *msg = twitter_msg_add_id(ic, status, "");
+ struct twitter_filter *tf;
+ GSList *f;
+ GSList *l;
+
+ for (f = td->filters; f; f = g_slist_next(f)) {
+ tf = f->data;
+
+ switch (tf->type) {
+ case TWITTER_FILTER_TYPE_FOLLOW:
+ if (status->user->uid != tf->uid)
+ continue;
+ break;
+
+ case TWITTER_FILTER_TYPE_TRACK:
+ if (strcasestr(status->text, tf->text) == NULL)
+ continue;
+ break;
+
+ default:
+ continue;
+ }
+
+ for (l = tf->groupchats; l; l = g_slist_next(l)) {
+ imcb_chat_msg(l->data, status->user->screen_name,
+ msg ? msg : status->text, 0, 0);
+ }
}
+
+ g_free(msg);
}
/**
@@ -730,7 +807,9 @@ static void twitter_status_show(struct im_connection *ic, struct twitter_xml_sta
if (set_getbool(&ic->acc->set, "strip_newlines"))
strip_newlines(status->text);
- if (td->flags & TWITTER_MODE_CHAT)
+ if (status->from_filter)
+ twitter_status_show_filter(ic, status);
+ else if (td->flags & TWITTER_MODE_CHAT)
twitter_status_show_chat(ic, status);
else
twitter_status_show_msg(ic, status);
@@ -744,7 +823,7 @@ static void twitter_status_show(struct im_connection *ic, struct twitter_xml_sta
g_free(last_id_str);
}
-static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o);
+static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o, gboolean from_filter);
static void twitter_http_stream(struct http_request *req)
{
@@ -753,6 +832,7 @@ static void twitter_http_stream(struct http_request *req)
json_value *parsed;
int len = 0;
char c, *nl;
+ gboolean from_filter;
if (!g_slist_find(twitter_connections, ic))
return;
@@ -761,7 +841,11 @@ static void twitter_http_stream(struct http_request *req)
td = ic->proto_data;
if ((req->flags & HTTPC_EOF) || !req->reply_body) {
- td->stream = NULL;
+ if (req == td->stream)
+ td->stream = NULL;
+ else if (req == td->filter_stream)
+ td->filter_stream = NULL;
+
imcb_error(ic, "Stream closed (%s)", req->status_string);
imc_logout(ic, TRUE);
return;
@@ -778,7 +862,8 @@ static void twitter_http_stream(struct http_request *req)
req->reply_body[len] = '\0';
if ((parsed = json_parse(req->reply_body, req->body_size))) {
- twitter_stream_handle_object(ic, parsed);
+ from_filter = (req == td->filter_stream);
+ twitter_stream_handle_object(ic, parsed, from_filter);
}
json_value_free(parsed);
req->reply_body[len] = c;
@@ -794,13 +879,14 @@ static void twitter_http_stream(struct http_request *req)
static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o);
static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs);
-static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o)
+static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o, gboolean from_filter)
{
struct twitter_data *td = ic->proto_data;
struct twitter_xml_status *txs;
json_value *c;
if ((txs = twitter_xt_get_status(o))) {
+ txs->from_filter = from_filter;
gboolean ret = twitter_stream_handle_status(ic, txs);
txs_free(txs);
return ret;
@@ -898,6 +984,169 @@ gboolean twitter_open_stream(struct im_connection *ic)
return FALSE;
}
+static gboolean twitter_filter_stream(struct im_connection *ic)
+{
+ struct twitter_data *td = ic->proto_data;
+ char *args[4] = {"follow", NULL, "track", NULL};
+ GString *followstr = g_string_new("");
+ GString *trackstr = g_string_new("");
+ gboolean ret = FALSE;
+ struct twitter_filter *tf;
+ GSList *l;
+
+ for (l = td->filters; l; l = g_slist_next(l)) {
+ tf = l->data;
+
+ switch (tf->type) {
+ case TWITTER_FILTER_TYPE_FOLLOW:
+ if (followstr->len > 0)
+ g_string_append_c(followstr, ',');
+
+ g_string_append_printf(followstr, "%" G_GUINT64_FORMAT,
+ tf->uid);
+ break;
+
+ case TWITTER_FILTER_TYPE_TRACK:
+ if (trackstr->len > 0)
+ g_string_append_c(trackstr, ',');
+
+ g_string_append(trackstr, tf->text);
+ break;
+
+ default:
+ continue;
+ }
+ }
+
+ args[1] = followstr->str;
+ args[3] = trackstr->str;
+
+ if (td->filter_stream)
+ http_close(td->filter_stream);
+
+ if ((td->filter_stream = twitter_http(ic, TWITTER_FILTER_STREAM_URL,
+ twitter_http_stream, ic, 0,
+ args, 4))) {
+ /* 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;
+ ret = TRUE;
+ }
+
+ g_string_free(followstr, TRUE);
+ g_string_free(trackstr, TRUE);
+
+ return ret;
+}
+
+static void twitter_filter_users_post(struct http_request *req)
+{
+ struct im_connection *ic = req->data;
+ struct twitter_data *td;
+ struct twitter_filter *tf;
+ GList *users = NULL;
+ json_value *parsed;
+ json_value *id;
+ const char *name;
+ GString *fstr;
+ GSList *l;
+ GList *u;
+ int i;
+
+ // Check if the connection is still active.
+ if (!g_slist_find(twitter_connections, ic))
+ return;
+
+ td = ic->proto_data;
+
+ if (!(parsed = twitter_parse_response(ic, req)))
+ return;
+
+ for (l = td->filters; l; l = g_slist_next(l)) {
+ tf = l->data;
+
+ if (tf->type == TWITTER_FILTER_TYPE_FOLLOW)
+ users = g_list_prepend(users, tf);
+ }
+
+ if (parsed->type != json_array)
+ goto finish;
+
+ for (i = 0; i < parsed->u.array.length; i++) {
+ id = json_o_get(parsed->u.array.values[i], "id");
+ name = json_o_str(parsed->u.array.values[i], "screen_name");
+
+ if (!name || !id || id->type != json_integer)
+ continue;
+
+ for (u = users; u; u = g_list_next(u)) {
+ tf = u->data;
+
+ if (g_strcasecmp(tf->text, name) == 0) {
+ tf->uid = id->u.integer;
+ users = g_list_delete_link(users, u);
+ break;
+ }
+ }
+ }
+
+finish:
+ json_value_free(parsed);
+ twitter_filter_stream(ic);
+
+ if (!users)
+ return;
+
+ fstr = g_string_new("");
+
+ for (u = users; u; u = g_list_next(u)) {
+ if (fstr->len > 0)
+ g_string_append(fstr, ", ");
+
+ g_string_append(fstr, tf->text);
+ }
+
+ imcb_error(ic, "Failed UID acquisitions: %s", fstr->str);
+
+ g_string_free(fstr, TRUE);
+ g_list_free(users);
+}
+
+gboolean twitter_open_filter_stream(struct im_connection *ic)
+{
+ struct twitter_data *td = ic->proto_data;
+ char *args[2] = {"screen_name", NULL};
+ GString *ustr = g_string_new("");
+ struct twitter_filter *tf;
+ struct http_request *req;
+ GSList *l;
+
+ for (l = td->filters; l; l = g_slist_next(l)) {
+ tf = l->data;
+
+ if (tf->type != TWITTER_FILTER_TYPE_FOLLOW || tf->uid != 0)
+ continue;
+
+ if (ustr->len > 0)
+ g_string_append_c(ustr, ',');
+
+ g_string_append(ustr, tf->text);
+ }
+
+ if (ustr->len == 0) {
+ g_string_free(ustr, TRUE);
+ return twitter_filter_stream(ic);
+ }
+
+ args[1] = ustr->str;
+ req = twitter_http(ic, TWITTER_USERS_LOOKUP_URL,
+ twitter_filter_users_post,
+ ic, 0, args, 2);
+
+ g_string_free(ustr, TRUE);
+ return req != NULL;
+}
+
static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor);
static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor);
diff --git a/protocols/twitter/twitter_lib.h b/protocols/twitter/twitter_lib.h
index 7fd3b808..ee103100 100644
--- a/protocols/twitter/twitter_lib.h
+++ b/protocols/twitter/twitter_lib.h
@@ -78,9 +78,12 @@
/* Report spam */
#define TWITTER_REPORT_SPAM_URL "/users/report_spam.json"
+/* Stream URLs */
#define TWITTER_USER_STREAM_URL "https://userstream.twitter.com/1.1/user.json"
+#define TWITTER_FILTER_STREAM_URL "https://stream.twitter.com/1.1/statuses/filter.json"
gboolean twitter_open_stream(struct im_connection *ic);
+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_statuses_friends(struct im_connection *ic, gint64 next_cursor);
diff --git a/unix.c b/unix.c
index e1ebb19d..7be82509 100644
--- a/unix.c
+++ b/unix.c
@@ -86,8 +86,6 @@ int main( int argc, char *argv[] )
otr_init();
#endif
- srand( time( NULL ) ^ getpid() );
-
global.helpfile = g_strdup( HELP_FILE );
if( help_init( &global.help, global.helpfile ) == NULL )
log_message( LOGLVL_WARNING, "Error opening helpfile %s.", HELP_FILE );