Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
tree: 47e8b266da
Fetching contributors…

Cannot retrieve contributors at this time

executable file 707 lines (598 sloc) 19.692 kB
#!/usr/bin/env ruby
# -*- mode: ruby; coding: utf-8 -*-
$KCODE = 'u'
require 'pathname'
$LOAD_PATH.unshift(Pathname($0).dirname.parent.join('lib'))
require 'rubygems'
require 'digest'
require 'friendfeed'
require 'friendfeed/unofficial'
require 'friendfeed/v2'
require 'highline'
require 'main'
require 'mechanize'
require 'uri'
require 'tempfile'
require 'twitter'
require 'yaml'
require 'yaml/store'
MYNAME = File.basename($0)
TWITTER_URI = URI.parse('http://twitter.com/')
def ConfigDir()
$config_dir ||=
begin
config_dir = File.expand_path('~/.%s' % MYNAME)
if !File.directory?(config_dir)
Dir.mkdir(config_dir, 0700)
end
config_dir
end
end
class StatusStore
def initialize(path)
@status = YAML::Store.new(path)
end
def method_missing(name, value = nil)
key = name.to_s
if key.chomp!('=')
@status.transaction(false) {
@status[key] = value
}
else
@status.transaction(true) {
@status[key]
}
end
end
end
Status = StatusStore.new(File.join(ConfigDir(), 'status.yml'))
def putinfo(fmt, *args)
STDERR.puts sprintf(fmt, *args)
end
def agent
$agent ||= Mechanize.new
end
def parse_uri(url)
case url
when URI
url
else
begin
URI.parse(url)
rescue URI::InvalidURIError
dir, file = File.split(url)
URI.parse(File.join(dir, URI.escape(file)))
end
end
end
def get_file(url)
uri = parse_uri(url)
putinfo 'Fetching %s', uri
agent.get_file(uri)
end
class FriendFeed::Client
def change_picture_to_url(id, url)
t = Tempfile.open("picture")
t.write get_file(url)
t.close
File.open(t.path) { |f|
change_picture(id, f)
}
t.unlink
end
def follow_twitter_user(twitter_name, display_name, picture_url = nil)
id = create_imaginary_friend(display_name)
add_twitter(id, twitter_name, 'includeatreplies' => 'on')
change_picture_to_url(id, picture_url) if picture_url
end
end
def get_friendfeed_client
username, password = Status.friendfeed_userpass
begin
username && password or raise 'FriendFeed login information required'
putinfo 'Logging in to FriendFeed as %s', username
return FriendFeed::Client.new.login(username, password)
rescue => e
putinfo '%s', e.to_s
HighLine.new.tap { |hl|
username = hl.ask('Login: ') { |q|
q.default = username
}
password = hl.ask('Password: ') { |q|
q.echo = false
}
}
retry
ensure
if e
Status.friendfeed_userpass = [username, password]
end
end
end
def friendfeed_client
$ff_client ||= get_friendfeed_client
end
def friendfeedv2_client
$ffv2_client ||=
begin
httpauth = FriendFeed::V2::BasicAuth.new(friendfeed_client.nickname, friendfeed_client.remote_key)
FriendFeed::V2::Client.new(httpauth)
end
end
class Twitter::Base
def all_friends
list = []
cursor = -1
loop {
result = friends(:cursor => cursor)
break if result.users.nil?
list.concat(result.users)
cursor = result[:next_cursor]
break if cursor.zero?
}
list
end
end
def get_twitter_oauth
ckey, csecret = Status.twitter_consumer
if !ckey || !csecret
putinfo 'Twitter consumer key & secret required'
HighLine.new.tap { |hl|
ckey = hl.ask('Consumer key: ')
csecret = hl.ask('Consumer secret: ') { |q|
q.echo = false
}
}
Status.twitter_consumer = [ckey, csecret]
end
Twitter::OAuth.new(ckey, csecret)
end
def get_twitter_access(oauth)
request = oauth.consumer.get_request_token
if system('open', request.authorize_url)
putinfo 'Web browser is opened.'
else
putinfo 'Open %s with a web browser.', request.authorize_url
end
pin = HighLine.new.ask('Grant access and input the 7-digit PIN here: ')
Status.twitter_access = oauth.authorize_from_request(request.token, request.secret, pin)
end
def get_twitter_client
oauth = get_twitter_oauth
atoken, asecret = Status.twitter_access
begin
atoken && asecret or raise 'Twitter access authorization required'
oauth.authorize_from_access(atoken, asecret)
rescue => e
putinfo '%s.', e.to_s
get_twitter_access(oauth)
end
Twitter::Base.new(oauth)
end
def twitter_client
$tw_client ||= get_twitter_client
end
Main {
description 'Twitter to FriendFeed migration helper'
def run
print usage.to_s
end
mode 'follow' do
description 'Import a Twitter user to FriendFeed as an "imaginary friend"'
option('name-format') {
description 'A printf format to generate an imaginary friend\'s name from a Twitter name'
argument_required
default '(%s)'
}
argument('names') {
description 'List of Twitter user names to follow'
arity -1
}
def run
name_format = params['name-format'].value
names = params['names'].values
exit if names.empty?
names.each { |name|
twuser =
begin
twitter_client.user(name)
rescue Twitter::NotFound
putinfo 'Twitter user not found: %s', name
next
rescue => e
putinfo 'Twitter user not accessible: %s: %s', name, e
next
end
if !twuser.status
putinfo 'Twitter user may not be followable: %s', name
end
putinfo 'Creating an imaginary friend for %s', name
friendfeed_client.follow_twitter_user(name, name_format % name,
twuser.profile_image_url)
}
end
end
mode 'friends' do
description 'Import Twitter-only friends to FriendFeed as "imaginary friends"'
option('name-format') {
description 'A printf format to generate an imaginary friend\'s name from a Twitter name'
argument_required
default '(%s)'
}
def run
require 'set'
name_format = params['name-format'].value
subscribed_real = Set[]
subscribed_imag = Set[]
putinfo "Checking real friends in FriendFeed..."
friendfeed_client.get_real_friends.each { |profile|
profile['services'].each { |service|
url = service['profileUrl'] or next
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
putinfo 'Found a Twitter friend %s in FriendFeed', name
subscribed_real << name.downcase
end
}
}
putinfo "Checking imaginary friends in FriendFeed..."
friendfeed_client.get_imaginary_friends.each { |profile|
profile['services'].each { |service|
url = service['profileUrl'] or next
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
putinfo 'Found a Twitter friend %s in FriendFeed (imaginary)', name
subscribed_imag << name.downcase
end
}
}
putinfo "Checking groups in FriendFeed..."
friendfeed_client.get_profile['rooms'].each { |room|
friendfeed_client.get_services(room['nickname']).each { |service|
url = service['profileUrl'] or next
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
putinfo 'Found a Twitter friend %s in FriendFeed (group)', name
subscribed_imag << name.downcase
end
}
}
Status.friends_subscribed_real = subscribed_real.sort
Status.friends_subscribed_imag = subscribed_imag.sort
(subscribed_real & subscribed_imag).each { |name|
putinfo 'Duplicated subscription: %s', name
}
subscribed = subscribed_real + subscribed_imag
friends = Set[]
to_subscribe = Set[]
to_watch = Set[]
picture_urls = {}
twitter_client.all_friends.each { |friend|
name = friend.screen_name.downcase
friends << name
next if subscribed.include?(name)
if friend.protected
to_watch << name
else
to_subscribe << name
picture_urls[name] = friend.profile_image_url
end
}
twitter_me = twitter_client.verify_credentials.screen_name
friends << twitter_me
Status.friends = friends.sort
Status.friends_to_watch = to_watch.sort
to_watch.each { |name|
putinfo 'Skipping a protected user %s', name
}
to_subscribe.each { |name|
putinfo 'Creating an imaginary friend for %s', name
friendfeed_client.follow_twitter_user(name, name_format % name,
picture_urls[name])
}
printf <<-EOS, 'http://friendfeed.com/friends/twitter?username=' + URI.escape(twitter_me)
You may also want to check out the following page
to see if someone is joining FriendFeed:
%s
EOS
end
end
mode 'icons' do
description 'Update imaginary friends icons with those of their Twitter accounts'
def run
picture_urls = {}
twitter_client.all_friends.each { |friend|
name = friend.screen_name.downcase
picture_urls[name] = friend.profile_image_url
}
putinfo "Checking imaginary friends in FriendFeed..."
friendfeed_client.get_imaginary_friends.each { |profile|
profile['services'].each { |service|
url = service['profileUrl'] or next
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
name.downcase!
if picture_urls.key?(name)
url = picture_urls[name]
else
friend = twitter_client.user(name) rescue
begin
putinfo "Failed to get profile of %s", name
next
end
url = friend.profile_image_url
end
id = profile['id']
putinfo 'Changing the picture of %s', name
friendfeed_client.change_picture_to_url(id, url)
end
}
}
end
end
mode 'includeatreplies' do
description 'Fix imaginary friends to include @replies'
def run
putinfo "Checking imaginary friends in FriendFeed..."
friendfeed_client.get_imaginary_friends.each { |profile|
id = profile['id']
friendfeed_client.get_services(id).each { |service|
url = service['profileUrl'] or next
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
name.downcase!
putinfo 'Trying to include @replies by %s', name
begin
friendfeed_client.edit_service(id, service['serviceid'], 'includeatreplies' => 'on')
rescue => e
putinfo 'Failed, maybe due to the Twitter account removal or a network problem.'
end
end
}
}
end
end
mode 'favorites' do
description 'Synchronize Twitter favorites and FriendFeed likes as far as possible'
def run
require 'set'
ff_me = friendfeed_client.nickname
favorited = Set[]
ffim_favorited = Set[]
twitter_client.favorites.each { |favorite|
id = favorite.id
if m = favorite.text.match(%r{http://ff\.im/([A-Za-z0-9]+)})
ffim_favorited << [id, favorite.user.screen_name, m[1]]
end
favorited << id
}
liked = Set[]
tw_uri = {}
nontw_liked = Set[]
feedinfo = {}
friendfeedv2_client.get_feed([ff_me, :likes]).entries.each { |entry|
via = entry.via or next
case via.name
when 'Twitter'
uri = via.url
case TWITTER_URI.route_to(uri).to_s
when %r{\A([A-Za-z0-9_]+)/statuses/([0-9]+)\z}
id = $2.to_i
liked << id
tw_uri[id] = uri
next
end
end
# The entry is not from Twitter
user = entry.from.id
info = (feedinfo[user] ||= friendfeedv2_client.get_feedinfo(user))
if twitter = info.services.find { |service| service.id == 'twitter' }
nontw_liked << [entry, twitter.username]
end
}
# Favorite FriendFeed-liked entry in Twitter
(liked - favorited).each { |id|
putinfo "Adding a favorite in Twitter: %s", tw_uri[id]
begin
twitter_client.favorite_create(id)
rescue
# Maybe already favorited a long ago
end
}
# Find Twitter-liked entries in FriendFeed that aren't liked yet
(
friendfeed_client.get_user_friend_entries(nil, 'service' => 'twitter') +
friendfeed_client.get_user_discussed_entries(nil, 'service' => 'twitter')
# Currently imaginary friends entries are not checked
).sort_by { |entry|
entry["published"]
}.each { |entry|
# Just in case this entry is not covered by the 'liked' list
next if entry['likes'].any? { |like| like['user']['nickname'] == ff_me }
# Is the source Twitter?
url = entry['service']['profileUrl'] or next
m = TWITTER_URI.route_to(url).to_s.match(%r{\A([A-Za-z0-9_]+)/statuses/([0-9]+)\z}) or next
id = m[2].to_i
# A different entry with the same source may be liked already
next if liked.include?(id)
if favorited.include?(id)
entryid = entry['id']
putinfo "Adding a like in FriendFeed: %s", entry['link']
friendfeed_client.add_like(entryid)
liked << id
end
}
ffim_favorited.each { |id, name, short_id|
next if liked.include?(id)
next if nontw_liked.reject! { |entry, username| entry.short_id == short_id }
entry = friendfeedv2_client.decode_short(short_id) or next
user = entry.from.id
info = (feedinfo[user] ||= friendfeedv2_client.get_feedinfo(user))
twitter = info.services.find { |service| service.id == 'twitter' } or next
if twitter.username == name
putinfo "Adding a like in FriendFeed: %s", entry['link']
friendfeed_client.add_like(entry.id)
liked << id
end
}
nontw_liked.each { |entry, nickname|
search = Twitter::Search.new
search.from(nickname)
search.contains(entry.short_url.to_s)
search.each { |result|
id = result.id
uri = TWITTER_URI + '%s/statuses/%d' % [result.from_user, result.id]
putinfo "Adding a favorite in Twitter: %s", uri
begin
twitter_client.favorite_create(id)
liked << id
rescue
# Maybe already favorited
end
}
}
end
end
mode 'replies' do
description 'Produce an RSS feed of Twitter @replies to you'
option('include-friends') {
description 'Include @reples from friends'
cast :bool
default false
}
option('include-protected') {
description 'Include @reples from protected users'
cast :bool
default false
}
argument('filename') {
description 'Output RSS to this file'
}
def run
require 'nokogiri'
require 'rss'
require 'set'
require 'time'
include_friends = params['include-friends'].value
include_protected = params['include-protected'].value
filename = params['filename'].value
File.open(filename, 'w') { |w|
feed = RSS::Maker.make("2.0") { |rss|
rss.channel.title = 'Twitter replies'
rss.channel.link = 'http://twitter.com/replies'
rss.channel.description = 'Twitter replies'
friends = Status.friends.to_set
twitter_client.mentions.each { |reply|
user = reply.user
next if !include_protected && user.protected
name = user.screen_name
next if !include_friends && friends.include?(name.downcase)
text = '%s: %s' % [name, reply.text]
url = 'http://twitter.com/%s/statuses/%d' % [name, reply.id]
timestamp = Time.parse(reply.created_at)
rss.items.new_item { |item|
item.title = Nokogiri.HTML(text).inner_text
item.link = url
item.description = text
item.date = timestamp
}
}
}
w.print feed.to_s
}
end
end
mode 'protected' do
description 'Produce an RSS feed for Twitter entries from protected friends'
argument('filename') {
description 'Output RSS to this file'
}
def run
require 'nokogiri'
require 'rss'
require 'set'
require 'time'
filename = params['filename'].value
friends = Status.friends.to_set
friends_subscribed_real = Status.friends_subscribed_real.to_set
items = []
twitter_client.mentions.each { |reply|
user = reply.user
next if !user.protected
name = user.screen_name
next if friends.include?(name.downcase)
text = '[%s]: %s' % [name, reply.text]
url = 'http://twitter.com/%s/statuses/%d' % [name, reply.id]
timestamp = Time.parse(reply.created_at)
items << [timestamp, text, url]
}
twitter_client.friends_timeline.each { |status|
user = status.user
next if !user.protected
name = user.screen_name
next if friends_subscribed_real.include?(name.downcase)
text = '[%s]: %s' % [name, status.text]
url = 'http://twitter.com/%s/statuses/%d' % [name, status.id]
timestamp = Time.parse(status.created_at)
items << [timestamp, text, url]
}
File.open(filename, 'w') { |w|
feed = RSS::Maker.make("2.0") { |rss|
rss.channel.title = 'Twitter entries from protected friends'
rss.channel.link = 'http://twitter.com/home'
rss.channel.description = 'Twitter entries from protected friends'
items.sort { |a, b|
b.first <=> a.first
}.each { |timestamp, text, url|
rss.items.new_item { |item|
item.title = Nokogiri.HTML(text).inner_text
item.link = url
item.description = text
item.date = timestamp
}
}
}
w.print feed.to_s
}
end
end
mode 'retweeted' do
description 'Produce an RSS feed of your Twitter posts retweeted by others'
argument('filename') {
description 'Output RSS to this file'
}
def run
require 'nokogiri'
require 'rss'
require 'set'
require 'time'
filename = params['filename'].value
File.open(filename, 'w') { |w|
feed = RSS::Maker.make("2.0") { |rss|
rss.channel.title = 'Twitter posts retweeted by others'
rss.channel.link = 'http://twitter.com/retweeted_of_mine'
rss.channel.description = 'Twitter posts retweeted by others'
twitter_client.retweets_of_me.each { |tweet|
user = tweet.user
name = user.screen_name
text = tweet.text
url = 'http://twitter.com/%s/statuses/%d' % [name, tweet.id]
timestamp = Time.parse(tweet.created_at)
rss.items.new_item { |item|
item.title = Nokogiri.HTML(text).inner_text
item.link = url
item.description = text
item.date = timestamp
}
}
}
w.print feed.to_s
}
end
end
mode 'refresh' do
description 'Urge FriendFeed to refresh services (import feed entries)'
def run
profile = friendfeed_client.get_profile
friendfeed_client.get_services.each { |service|
putinfo "Refreshing %s..." % service['service']
friendfeed_client.refresh_service(profile['id'], service['serviceid'], service['service'])
}
end
end
}
Jump to Line
Something went wrong with that request. Please try again.