Dave Landers

Dave’s thoughts (such as they are)

A twitter group bot for Colorado Software Summit

I have created a twitter group and an associated pair of bots for use during this year’s Colorado Software Summit.

Listening to @swsmt

The twitter group is @swsmt. If you use twitter, and want to follow tweets about the conference, then just follow @swsmt.

A few minutes later, you should also see @swsmt start following you. The main reason for this is to allow you to direct message the group. If you remove @swsmt from your followers, it should also then quit following you.

Tweeting to @swsmt

To tweet about the conference, or to send a message to everyone following @swsmt, you can do one of several things:

  • Direct message the swsmt group (For example: D swsmt blah).
  • Reply to @swsmt, or otherwise mention @swsmt in a tweet.
  • Use the #swsmt hashtag in a tweet.
  • Mention "SoftwareSummit" (one word) in your tweet – this will also pick up anyone tweeting a link to softwaresummit.com

When my bot sees any of the above, it will retweet them to the @swsmt account, so that everyone following the group will then see them.

The bots

Twitter has a pretty decent REST API, and I also found a nifty ruby library (twitter) wrapping that. Throwing together a couple of scripts was pretty easy after that.

Syncing friends

The first script I wrote syncs the group’s friends with followers. Keeping these in sync lets the follow act kind of like a subscription to the group.

The script just grabs both the followers and friends lists, and adds/removes based on the diffs between them.

The script is:

#!/usr/bin/env ruby

# synchronize followers/followed
# we do this because followers (those who 'join this group') will see
# all the messages, but we need to make them our friend so they
# can direct message the group

# http://twitter.rubyforge.org/
# sudo gem install twitter
require 'rubygems'
require 'twitter'

group_name = 'swsmt'
passwd = 'XYZZY'   # not really :)

# Log in
group = Twitter::Base.new(group_name,passwd)

# TODO only returns 100
# when we get more than 100, this will probably stop working very well
friends = group.friends.collect{ |f| f.screen_name }
followers = group.followers.collect{ |f| f.screen_name }

# followers who are not the group's friend yet (add them)
followers.each do |f| 
  unless friends.include? f then
    begin
      puts "Adding friend (following) #{f}"
      group.create_friendship f
    rescue
      puts "Unable to add friend #{f}: #{$!}"
    end
  end
end

# friends who no longer follow the group (remove them)
friends.each do |f|
  unless followers.include? f then
    begin
      puts "Removing friend (following) #{f}"
      group.destroy_friendship f
    rescue
      puts "Unable to remove friend #{f}: #{$!}"
    end
  end
end

Retweeting script

The retweet script searches recent tweets for @swsmt, #swsmt, or softwaresummit, and retweets that to @swsmt. It then grabs any recent direct messages, and retweets those.

Here’s that script:

#!/usr/bin/env ruby

# retweet messages to the @group
# we'll search for messages with a group hashtag #group or
# referencing the @group
# as well as some other keywords.
# also retweet any direct messages to the group
# run this via cron every couple of minutes to keep the group updated

# http://twitter.rubyforge.org/
# sudo gem install twitter
require 'rubygems'
require 'twitter'

group_name = 'swsmt'
passwd = 'XYZZY'   # not

# search for hashtags, group ref, or conference name
search_query = "##{group_name} OR @#{group_name} OR SoftwareSummit"

# Log in
group = Twitter::Base.new(group_name,passwd)

def retweet(group, from, text, id, lastid)
  retweet =  "Via @#{from}: #{text}"
  retweet = retweet[0,140] + '...' if retweet.length > 159
  puts retweet
  begin
    group.post(retweet)
    id.to_i > lastid.to_i ? id.to_i : lastid
  rescue
    puts "Error retweeting: #{$!}"
    lastid
  end
end
# Keep track of the last ids we retweeted
searchid_file = File.expand_path('~/.group_retweet.searchid')

last_searchid = nil
last_searchid = File.open(searchid_file){ |f| f.gets.to_i } if File.exists?(searchid_file)

begin
  # search for #group hashtag etc
  # TODO this will only return the 50 most recent - if there's too much traffic, 
  #   we will drop some.
  Twitter::Search.new(search_query).since(last_searchid).per_page(50).sort {|a,b| a['id'] <=> b['id'] }.each do |m|
    # skip messages from the group itself (probably our last retweet)
    unless  m['from_user'] == group_name then
      last_searchid = retweet group, m['from_user'], m['text'], m['id'], last_searchid
    end
  end
ensure
  File.open(searchid_file, 'w'){ |f| f.puts last_searchid } unless last_searchid.nil?
end

directid_file = File.expand_path('~/.group_retweet.directid')
last_directid = nil
last_directid = File.open(directid_file){ |f| f.gets.to_i } if File.exists?(directid_file)

begin
  # now grab any new direct messages and retweet them
  # TODO this will only return the 20 most recent - if there's too much traffic, 
  #    we will drop some
  group.direct_messages(:since_id=>last_directid).sort {|a,b| a.id <=> b.id }.each do |m|
      last_directid = retweet group, m.sender_screen_name, m.text, m.id, last_directid
  end
ensure
   File.open(directid_file, 'w'){ |f| f.puts last_directid } unless last_directid.nil?
end

I’m running both of these via cron. I just turned this on, so we’ll see how it goes.

Technorati Tags: , , , , , ,

2 comments

2 Comments so far

  1. John March 22nd, 2009 8:14 pm

    Hi Dave. I was able to setup your scripts to run a twitter group bot earlier today. The sync followers/followed part worked for a while, but now I’m getting a bunch of “Twitter is returning a 400: Bad Request (Twitter::CantConnect)” messages. Is this more of a intermittent problem with twitter? The ruby implementation of the API? Any thoughts?

  2. John March 22nd, 2009 8:23 pm

    lol. Figured that one out with a touch of searching. I suppose you had the cron jobs running every 5 minutes to prevent twitter from throttling your account and rejecting the API requests, right?