knu-friendfeed 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/tw2ff +332 -0
- data/lib/friendfeed/compat.rb +49 -0
- data/lib/friendfeed/unofficial.rb +314 -0
- data/lib/friendfeed.rb +478 -0
- metadata +8 -5
data/bin/tw2ff
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- mode: ruby -*-
|
3
|
+
|
4
|
+
$KCODE = 'u'
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(Pathname($0).dirname.parent.join('lib'))
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'digest'
|
12
|
+
require 'friendfeed'
|
13
|
+
require 'friendfeed/unofficial'
|
14
|
+
require 'main'
|
15
|
+
require 'mechanize'
|
16
|
+
require 'uri'
|
17
|
+
require 'tempfile'
|
18
|
+
require 'twitter'
|
19
|
+
require 'yaml'
|
20
|
+
require 'yaml/store'
|
21
|
+
|
22
|
+
MYNAME = File.basename($0)
|
23
|
+
|
24
|
+
TWITTER_URI = URI.parse('http://twitter.com/')
|
25
|
+
|
26
|
+
def ConfigDir()
|
27
|
+
$config_dir ||=
|
28
|
+
begin
|
29
|
+
config_dir = File.expand_path('~/.%s' % MYNAME)
|
30
|
+
if !File.directory?(config_dir)
|
31
|
+
Dir.mkdir(config_dir, 0700)
|
32
|
+
end
|
33
|
+
config_dir
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def ConfigFile()
|
38
|
+
$config_file ||= File.join(ConfigDir(), 'config.yml')
|
39
|
+
end
|
40
|
+
|
41
|
+
def Config(keypath, default = :omitted)
|
42
|
+
$config ||= YAML.load_file(ConfigFile())
|
43
|
+
|
44
|
+
keypath.split('.').inject($config) { |hash, key|
|
45
|
+
hash.is_a?(Hash) or raise TypeError
|
46
|
+
hash.fetch(key)
|
47
|
+
}
|
48
|
+
rescue => e
|
49
|
+
return default if default != :omitted
|
50
|
+
|
51
|
+
STDERR.print <<EOM
|
52
|
+
The key "#{keypath}" is missing in #{ConfigFile()}.
|
53
|
+
Please edit the file to look like the following:
|
54
|
+
|
55
|
+
---
|
56
|
+
friendfeed:
|
57
|
+
username: "username"
|
58
|
+
password: "password"
|
59
|
+
twitter:
|
60
|
+
username: "username"
|
61
|
+
password: "password"
|
62
|
+
EOM
|
63
|
+
exit 1
|
64
|
+
end
|
65
|
+
|
66
|
+
def puterror(message)
|
67
|
+
STDERR.puts MYNAME + ': ' + e.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def putinfo(fmt, *args)
|
71
|
+
STDERR.puts sprintf(fmt, *args)
|
72
|
+
end
|
73
|
+
|
74
|
+
def Status(key)
|
75
|
+
$status ||= YAML::Store.new(File.join(ConfigDir(), 'status.yml'))
|
76
|
+
if block_given?
|
77
|
+
$status.transaction(false) {
|
78
|
+
return $status[key] = yield
|
79
|
+
}
|
80
|
+
else
|
81
|
+
$status.transaction(true) {
|
82
|
+
return $status[key]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def friendfeed_client
|
88
|
+
$ff_client ||=
|
89
|
+
begin
|
90
|
+
username = Config('friendfeed.username')
|
91
|
+
password = Config('friendfeed.password')
|
92
|
+
putinfo 'Logging in to FriendFeed as %s', username
|
93
|
+
FriendFeed::Client.new.login(username, password)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Twitter::Base
|
98
|
+
def all_friends
|
99
|
+
list = []
|
100
|
+
(1..100).each { |i|
|
101
|
+
slice = friends(:page => i)
|
102
|
+
list.concat(slice)
|
103
|
+
break if slice.size < 100
|
104
|
+
}
|
105
|
+
list
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def twitter_client
|
110
|
+
$tw_client ||=
|
111
|
+
begin
|
112
|
+
username = Config('twitter.username')
|
113
|
+
password = Config('twitter.password')
|
114
|
+
putinfo 'Logging in to Twitter as %s', username
|
115
|
+
Twitter::Base.new(Twitter::HTTPAuth.new(username, password))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Main {
|
120
|
+
description 'Twitter to FriendFeed migration helper'
|
121
|
+
|
122
|
+
def run
|
123
|
+
print usage.to_s
|
124
|
+
end
|
125
|
+
|
126
|
+
mode 'sync' do
|
127
|
+
description 'Add imaginary friends for Twitter-only friends'
|
128
|
+
|
129
|
+
def run
|
130
|
+
require 'set'
|
131
|
+
|
132
|
+
ffcli = friendfeed_client()
|
133
|
+
|
134
|
+
subscribed_real = Set[]
|
135
|
+
subscribed_imag = Set[]
|
136
|
+
|
137
|
+
putinfo "Checking real friends in FriendFeed..."
|
138
|
+
ffcli.get_real_friends.each { |profile|
|
139
|
+
profile['services'].each { |service|
|
140
|
+
url = service['profileUrl'] or next
|
141
|
+
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
|
142
|
+
putinfo 'Found a Twitter friend %s in FriendFeed', name
|
143
|
+
subscribed_real << name
|
144
|
+
end
|
145
|
+
}
|
146
|
+
}
|
147
|
+
|
148
|
+
putinfo "Checking imaginary friends in FriendFeed..."
|
149
|
+
ffcli.get_imaginary_friends.each { |profile|
|
150
|
+
profile['services'].each { |service|
|
151
|
+
url = service['profileUrl'] or next
|
152
|
+
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
|
153
|
+
putinfo 'Found a Twitter friend %s in FriendFeed (imaginary)', name
|
154
|
+
subscribed_imag << name
|
155
|
+
end
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
putinfo "Checking groups in FriendFeed..."
|
160
|
+
ffcli.get_profile['rooms'].each { |room|
|
161
|
+
ffcli.get_services(room['nickname']).each { |service|
|
162
|
+
url = service['profileUrl'] or next
|
163
|
+
if (name = TWITTER_URI.route_to(url).to_s).match(/\A[A-Za-z0-9_]+\z/)
|
164
|
+
putinfo 'Found a Twitter friend %s in FriendFeed (group)', name
|
165
|
+
subscribed_imag << name
|
166
|
+
end
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
Status('friends_subscribed_real') { subscribed_real.sort }
|
171
|
+
Status('friends_subscribed_imag') { subscribed_imag.sort }
|
172
|
+
|
173
|
+
(subscribed_real & subscribed_imag).each { |name|
|
174
|
+
putinfo 'Duplicated subscription: %s', name
|
175
|
+
}
|
176
|
+
|
177
|
+
subscribed = subscribed_real + subscribed_imag
|
178
|
+
|
179
|
+
friends = Set[]
|
180
|
+
to_subscribe = Set[]
|
181
|
+
to_watch = Set[]
|
182
|
+
picture_urls = {}
|
183
|
+
|
184
|
+
twitter_client().all_friends.each { |friend|
|
185
|
+
name = friend.screen_name
|
186
|
+
friends << name
|
187
|
+
next if subscribed.include?(name)
|
188
|
+
|
189
|
+
if friend.protected
|
190
|
+
to_watch << name
|
191
|
+
else
|
192
|
+
to_subscribe << name
|
193
|
+
picture_urls[name] = friend.profile_image_url
|
194
|
+
end
|
195
|
+
}
|
196
|
+
friends << Config('twitter.username')
|
197
|
+
|
198
|
+
Status('friends') { friends.sort }
|
199
|
+
Status('friends_to_watch') { to_watch.sort }
|
200
|
+
|
201
|
+
to_watch.each { |name|
|
202
|
+
putinfo 'Skipping a protected user %s', name
|
203
|
+
}
|
204
|
+
|
205
|
+
agent = WWW::Mechanize.new
|
206
|
+
|
207
|
+
to_subscribe.each { |name|
|
208
|
+
putinfo 'Creating an imaginary friend for %s', name
|
209
|
+
id = ffcli.create_imaginary_friend('(%s)' % name)
|
210
|
+
ffcli.add_twitter(id, name)
|
211
|
+
if picture_urls.key?(name)
|
212
|
+
putinfo 'Setting the picture of %s', name
|
213
|
+
t = Tempfile.open("picture")
|
214
|
+
t.write agent.get_file(picture_urls[name])
|
215
|
+
t.close
|
216
|
+
File.open(t.path) { |f|
|
217
|
+
ffcli.change_picture(id, f)
|
218
|
+
}
|
219
|
+
end
|
220
|
+
}
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
mode 'replies' do
|
225
|
+
description 'Produce an RSS feed for Twitter replies from non-friends'
|
226
|
+
|
227
|
+
argument('filename') {
|
228
|
+
description 'Specifies a flie to write RSS to'
|
229
|
+
}
|
230
|
+
|
231
|
+
def run
|
232
|
+
require 'nokogiri'
|
233
|
+
require 'rss'
|
234
|
+
require 'set'
|
235
|
+
require 'time'
|
236
|
+
|
237
|
+
filename = params['filename'].value
|
238
|
+
|
239
|
+
File.open(filename, 'w') { |w|
|
240
|
+
feed = RSS::Maker.make("2.0") { |rss|
|
241
|
+
rss.channel.title = 'Twitter replies from non-friends'
|
242
|
+
rss.channel.link = 'http://twitter.com/replies'
|
243
|
+
rss.channel.description = 'Twitter replies from non-friends'
|
244
|
+
|
245
|
+
friends = Status('friends').to_set
|
246
|
+
|
247
|
+
twitter_client().replies.each { |reply|
|
248
|
+
user = reply.user
|
249
|
+
next if user.protected
|
250
|
+
name = user.screen_name
|
251
|
+
next if friends.include?(name)
|
252
|
+
text = '%s: %s' % [name, reply.text]
|
253
|
+
url = 'http://twitter.com/%s/statuses/%d' % [name, reply.id]
|
254
|
+
timestamp = Time.parse(reply.created_at)
|
255
|
+
rss.items.new_item { |item|
|
256
|
+
item.title = Nokogiri.HTML(text).inner_text
|
257
|
+
item.link = url
|
258
|
+
item.description = text
|
259
|
+
item.date = timestamp
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
w.print feed.to_s
|
264
|
+
}
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
mode 'protected' do
|
269
|
+
description 'Produce an RSS feed for Twitter entries from protected friends'
|
270
|
+
|
271
|
+
argument('filename') {
|
272
|
+
description 'Specifies a flie to write RSS to'
|
273
|
+
}
|
274
|
+
|
275
|
+
def run
|
276
|
+
require 'nokogiri'
|
277
|
+
require 'rss'
|
278
|
+
require 'set'
|
279
|
+
require 'time'
|
280
|
+
|
281
|
+
filename = params['filename'].value
|
282
|
+
|
283
|
+
friends = Status('friends').to_set
|
284
|
+
friends_subscribed_real = Status('friends_subscribed_real').to_set
|
285
|
+
|
286
|
+
items = []
|
287
|
+
|
288
|
+
twitter_client().replies.each { |reply|
|
289
|
+
user = reply.user
|
290
|
+
next if !user.protected
|
291
|
+
name = user.screen_name
|
292
|
+
next if friends.include?(name)
|
293
|
+
|
294
|
+
text = '[%s]: %s' % [name, reply.text]
|
295
|
+
url = 'http://twitter.com/%s/statuses/%d' % [name, reply.id]
|
296
|
+
timestamp = Time.parse(reply.created_at)
|
297
|
+
items << [timestamp, text, url]
|
298
|
+
}
|
299
|
+
|
300
|
+
twitter_client().friends_timeline.each { |status|
|
301
|
+
user = status.user
|
302
|
+
next if !user.protected
|
303
|
+
name = user.screen_name
|
304
|
+
next if friends_subscribed_real.include?(name)
|
305
|
+
text = '[%s]: %s' % [name, status.text]
|
306
|
+
url = 'http://twitter.com/%s/statuses/%d' % [name, status.id]
|
307
|
+
timestamp = Time.parse(status.created_at)
|
308
|
+
items << [timestamp, text, url]
|
309
|
+
}
|
310
|
+
|
311
|
+
File.open(filename, 'w') { |w|
|
312
|
+
feed = RSS::Maker.make("2.0") { |rss|
|
313
|
+
rss.channel.title = 'Twitter entries from protected friends'
|
314
|
+
rss.channel.link = 'http://twitter.com/home'
|
315
|
+
rss.channel.description = 'Twitter entries from protected friends'
|
316
|
+
|
317
|
+
items.sort { |a, b|
|
318
|
+
b.first <=> a.first
|
319
|
+
}.each { |timestamp, text, url|
|
320
|
+
rss.items.new_item { |item|
|
321
|
+
item.title = Nokogiri.HTML(text).inner_text
|
322
|
+
item.link = url
|
323
|
+
item.description = text
|
324
|
+
item.date = timestamp
|
325
|
+
}
|
326
|
+
}
|
327
|
+
}
|
328
|
+
w.print feed.to_s
|
329
|
+
}
|
330
|
+
end
|
331
|
+
end
|
332
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#--
|
3
|
+
# friendfeed/compat.rb - defines compatibility methods for older Ruby
|
4
|
+
#++
|
5
|
+
# Copyright (c) 2009 Akinori MUSHA <knu@iDaemons.org>
|
6
|
+
#
|
7
|
+
# All rights reserved. You can redistribute and/or modify it under the same
|
8
|
+
# terms as Ruby.
|
9
|
+
#
|
10
|
+
|
11
|
+
class Hash
|
12
|
+
def self.try_convert(obj)
|
13
|
+
return obj if obj.instance_of?(self)
|
14
|
+
return nil if !obj.respond_to?(:to_hash)
|
15
|
+
nobj = obj.to_hash
|
16
|
+
return nobj if nobj.instance_of?(self)
|
17
|
+
raise TypeError, format("can't convert %s to %s (%s#to_hash gives %s)", obj.class, self.class, obj.class, nobj.class)
|
18
|
+
end unless self.respond_to?(:try_convert)
|
19
|
+
end
|
20
|
+
|
21
|
+
class IO
|
22
|
+
def self.try_convert(obj)
|
23
|
+
return obj if obj.is_a?(self)
|
24
|
+
return nil if !obj.respond_to?(:to_io)
|
25
|
+
nobj = obj.to_io
|
26
|
+
return nobj if nobj.instance_of?(self)
|
27
|
+
raise TypeError, format("can't convert %s to %s (%s#to_io gives %s)", obj.class, self.class, obj.class, nobj.class)
|
28
|
+
end unless self.respond_to?(:try_convert)
|
29
|
+
end
|
30
|
+
|
31
|
+
class String
|
32
|
+
def self.try_convert(obj)
|
33
|
+
return obj if obj.instance_of?(self)
|
34
|
+
return nil if !obj.respond_to?(:to_str)
|
35
|
+
nobj = obj.to_str
|
36
|
+
return nobj if nobj.instance_of?(self)
|
37
|
+
raise TypeError, format("can't convert %s to %s (%s#to_str gives %s)", obj.class, self.class, obj.class, nobj.class)
|
38
|
+
end unless self.respond_to?(:try_convert)
|
39
|
+
end
|
40
|
+
|
41
|
+
class Array
|
42
|
+
def self.try_convert(obj)
|
43
|
+
return obj if obj.instance_of?(self)
|
44
|
+
return nil if !obj.respond_to?(:to_ary)
|
45
|
+
nobj = obj.to_ary
|
46
|
+
return nobj if nobj.instance_of?(self)
|
47
|
+
raise TypeError, format("can't convert %s to %s (%s#to_ary gives %s)", obj.class, self.class, obj.class, nobj.class)
|
48
|
+
end unless self.respond_to?(:try_convert)
|
49
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#--
|
3
|
+
# friendfeed/unofficial.rb - provides access to FriendFeed unofficial API's
|
4
|
+
#++
|
5
|
+
# Copyright (c) 2009 Akinori MUSHA <knu@iDaemons.org>
|
6
|
+
#
|
7
|
+
# All rights reserved. You can redistribute and/or modify it under the same
|
8
|
+
# terms as Ruby.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'friendfeed'
|
12
|
+
require 'rubygems'
|
13
|
+
require 'json'
|
14
|
+
require 'mechanize'
|
15
|
+
require 'uri'
|
16
|
+
|
17
|
+
module FriendFeed
|
18
|
+
class Client
|
19
|
+
#
|
20
|
+
# Unofficial API
|
21
|
+
#
|
22
|
+
|
23
|
+
LOGIN_URI = ROOT_URI + "/account/login"
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def get_login_agent
|
28
|
+
@login_agent or raise 'login() must be called first to use this feature'
|
29
|
+
end
|
30
|
+
|
31
|
+
public
|
32
|
+
|
33
|
+
# Performs a login with a +nickname+ and +password+ and returns
|
34
|
+
# self. This enables call of any API, including both official API
|
35
|
+
# and unofficial API.
|
36
|
+
def login(nickname, password)
|
37
|
+
@nickname = nickname
|
38
|
+
@password = password
|
39
|
+
@login_agent = WWW::Mechanize.new
|
40
|
+
|
41
|
+
page = @login_agent.get(LOGIN_URI)
|
42
|
+
|
43
|
+
login_form = page.forms.find { |form|
|
44
|
+
LOGIN_URI + form.action == LOGIN_URI
|
45
|
+
} or raise 'Cannot locate a login form'
|
46
|
+
|
47
|
+
login_form.set_fields(:email => @nickname, :password => @password)
|
48
|
+
|
49
|
+
page = login_form.submit
|
50
|
+
|
51
|
+
login_form = page.forms.find { |form|
|
52
|
+
LOGIN_URI + form.action == LOGIN_URI
|
53
|
+
} and raise 'Login failed'
|
54
|
+
|
55
|
+
page = @login_agent.get(ROOT_URI + "/account/api")
|
56
|
+
remote_key = page.parser.xpath("//td[text()='FriendFeed remote key:']/following-sibling::td[1]/text()").to_s
|
57
|
+
|
58
|
+
api_login(nickname, remote_key)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Posts a request to an internal API of FriendFeed and returns
|
62
|
+
# either a parser object for an HTML response or an object parsed
|
63
|
+
# from a JSON response). [unofficial]
|
64
|
+
def post(uri, query = {})
|
65
|
+
agent = get_login_agent()
|
66
|
+
|
67
|
+
page = agent.post(uri, {
|
68
|
+
'at' => agent.cookies.find { |cookie|
|
69
|
+
cookie.domain == 'friendfeed.com' && cookie.name == 'AT'
|
70
|
+
}.value
|
71
|
+
}.update(query))
|
72
|
+
if page.respond_to?(:parser)
|
73
|
+
parser = page.parser
|
74
|
+
messages = parser.xpath("//div[@id='errormessage']/text()")
|
75
|
+
messages.empty? or
|
76
|
+
raise messages.map { |message| message.to_s }.join(" ")
|
77
|
+
parser
|
78
|
+
else
|
79
|
+
json = JSON.parse(page.body)
|
80
|
+
message = json['error'] and
|
81
|
+
raise message
|
82
|
+
if html_frag = json['html']
|
83
|
+
html_body = '<html><body>' << html_frag << '</body></html>'
|
84
|
+
json['html_parser'] = WWW::Mechanize.html_parser.parse(html_body)
|
85
|
+
end
|
86
|
+
json
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Gets a list of services of a user or a room of a given
|
91
|
+
# +nickname+, defaulted to the authenticated user.
|
92
|
+
def get_services(nickname = @nickname)
|
93
|
+
agent = get_login_agent()
|
94
|
+
|
95
|
+
services_uri = ROOT_URI + ("/%s/services" % URI.encode(nickname))
|
96
|
+
parser = agent.get(services_uri).parser
|
97
|
+
|
98
|
+
active_servicelist = parser.xpath("//*[@class='active']//ul[@class='servicelist']")
|
99
|
+
|
100
|
+
if !active_servicelist.empty?
|
101
|
+
services = active_servicelist.xpath("./li/a").map { |a|
|
102
|
+
{
|
103
|
+
'service' => a['class'].split.find { |a_class|
|
104
|
+
a_class != 'l_editservice' && a_class != 'service'
|
105
|
+
},
|
106
|
+
'serviceid' => a['serviceid'].to_s,
|
107
|
+
}
|
108
|
+
}
|
109
|
+
profile_uri = ROOT_URI + ("/%s" % URI.encode(nickname))
|
110
|
+
agent.get(profile_uri).parser.xpath("//div[@class='servicespreview']/a").each_with_index { |a, i|
|
111
|
+
href = (profile_uri + a['href'].to_s).to_s
|
112
|
+
break if profile_uri.route_to(href).relative?
|
113
|
+
services[i]['profileUrl'] = href
|
114
|
+
}
|
115
|
+
else
|
116
|
+
services = parser.xpath("//ul[@class='servicelist']/li/a").map { |a|
|
117
|
+
{
|
118
|
+
'service' => a['class'].split.find { |a_class|
|
119
|
+
a_class != 'service'
|
120
|
+
},
|
121
|
+
'profileUrl' => a['href'].to_s,
|
122
|
+
}
|
123
|
+
}
|
124
|
+
end
|
125
|
+
services
|
126
|
+
end
|
127
|
+
|
128
|
+
# Creates a new feed of a given (unique) +nickname+ and display
|
129
|
+
# +name+, and returns a unique ID string on success. The +type+
|
130
|
+
# can be one of "group", "microblog" and "public". Like other
|
131
|
+
# methods in general, an exception is raised on
|
132
|
+
# failure. [unofficial]
|
133
|
+
def create_group(nickname, name, type = 'group')
|
134
|
+
post(ROOT_URI + '/a/createfeed', 'nickname' => nickname, 'name' => name, 'type' => type).xpath("(//a[@class='l_feedinvite'])[1]/@sid").to_s
|
135
|
+
end
|
136
|
+
|
137
|
+
EDIT_GROUP_URI = ROOT_URI + '/a/editprofile'
|
138
|
+
|
139
|
+
# Gets profile information of a group specified by a unique
|
140
|
+
# ID. [unofficial]
|
141
|
+
def get_group(id)
|
142
|
+
parser = post(ROOT_URI + '/a/profiledialog', 'stream' => id)['html_parser']
|
143
|
+
form = parser.xpath("//form[1]")
|
144
|
+
hash = { 'stream' => id }
|
145
|
+
form.xpath(".//input").each { |input|
|
146
|
+
case input['type'].downcase
|
147
|
+
when 'text'
|
148
|
+
hash[input['name']] = input['value']
|
149
|
+
when 'radio', 'checkbox'
|
150
|
+
if input['checked']
|
151
|
+
value = input['value']
|
152
|
+
if value && !value.empty?
|
153
|
+
hash[input['name']] = value
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
}
|
158
|
+
form.xpath(".//textarea").each { |input|
|
159
|
+
hash[input['name']] = input.text
|
160
|
+
}
|
161
|
+
hash
|
162
|
+
end
|
163
|
+
|
164
|
+
# Edits profile information of a group specified by a unique ID.
|
165
|
+
# Supported fields are 'nickname', 'name', 'description', 'access'
|
166
|
+
# ('private', 'semipublic' or 'public'), and 'anyoneinvite' (none
|
167
|
+
# or '1'). [unofficial]
|
168
|
+
def edit_group(id, hash)
|
169
|
+
param_hash = get_group(id)
|
170
|
+
param_hash.update(hash)
|
171
|
+
post(EDIT_GROUP_URI, param_hash)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Adds a feed to the authenticated user, a group or an imaginary
|
175
|
+
# friend specified by a unique ID. Specify 'isstatus' => 'on' to
|
176
|
+
# display entries as messages (no link), and 'importcomment' =>
|
177
|
+
# 'on' to include entry description as a comment. [unofficial]
|
178
|
+
def add_service(id, service, options = nil)
|
179
|
+
params = {
|
180
|
+
'stream' => id,
|
181
|
+
'service' => service,
|
182
|
+
}
|
183
|
+
params.update(options) if options
|
184
|
+
post(ROOT_URI + '/a/configureservice', params)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Edits a service of the authenticated user, a group or an
|
188
|
+
# imaginary friend specified by a unique ID. [unofficial]
|
189
|
+
def edit_service(id, serviceid, service, options = nil)
|
190
|
+
params = {
|
191
|
+
'stream' => id,
|
192
|
+
'service' => service,
|
193
|
+
'serviceid' => serviceid,
|
194
|
+
}
|
195
|
+
params.update(options) if options
|
196
|
+
post(ROOT_URI + '/a/configureservice', params)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Removes a service of the authenticated user, a group or an
|
200
|
+
# imaginary friend specified by a unique ID. Specify
|
201
|
+
# 'deleteentries' => 'on' to delete entries also. [unofficial]
|
202
|
+
def remove_service(id, serviceid, service, options = nil)
|
203
|
+
params = {
|
204
|
+
'stream' => id,
|
205
|
+
'service' => service,
|
206
|
+
'serviceid' => serviceid,
|
207
|
+
}
|
208
|
+
params.update(options) if options
|
209
|
+
post(ROOT_URI + '/a/removeservice', params)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Refreshes a feed of the authenticated user, a group or an
|
213
|
+
# imaginary friend specified by a unique ID. [unofficial]
|
214
|
+
def refresh_service(id, serviceid, service, options = nil)
|
215
|
+
params = {
|
216
|
+
'refresh' => 1,
|
217
|
+
}
|
218
|
+
params.update(options) if options
|
219
|
+
edit_service(id, serviceid, service, params)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Adds a feed to the authenticated user, a group or an imaginary
|
223
|
+
# friend specified by a unique ID. Specify 'isstatus' => 'on' to
|
224
|
+
# display entries as messages (no link), and 'importcomment' =>
|
225
|
+
# 'on' to include entry description as a comment. [unofficial]
|
226
|
+
def add_feed(id, url, options = nil)
|
227
|
+
params = { 'url' => url }
|
228
|
+
params.update(options) if options
|
229
|
+
add_service(id, 'feed', options)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Adds a Twitter service to the authenticated user, a group or an
|
233
|
+
# imaginary friend specified by a unique ID. [unofficial]
|
234
|
+
def add_twitter(id, twitter_name)
|
235
|
+
add_service(id, 'twitter', 'username' => twitter_name)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Edits a feed of the authenticated user, a group or an imaginary
|
239
|
+
# friend specified by a unique ID. Specify 'isstatus' => 'on' to
|
240
|
+
# display entries as messages (no link), and 'importcomment' =>
|
241
|
+
# 'on' to include entry description as a comment. [unofficial]
|
242
|
+
def edit_feed(id, serviceid, url, options = nil)
|
243
|
+
params = { 'url' => url }
|
244
|
+
params.update(options) if options
|
245
|
+
add_service(id, 'feed', options)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Edits a Twitter service of the authenticated user, a group or an
|
249
|
+
# imaginary friend specified by a unique ID. Specify 'isstatus'
|
250
|
+
# => 'on' to display entries as messages (no link), and
|
251
|
+
# 'importcomment' => 'on' to include entry description as a
|
252
|
+
# comment. [unofficial]
|
253
|
+
def edit_twitter(id, serviceid, twitter_name)
|
254
|
+
edit_service(id, serviceid, 'twitter', 'username' => twitter_name)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Removes a feed from the authenticated user, a group or an
|
258
|
+
# imaginary friend specified by a unique ID. Specify
|
259
|
+
# 'deleteentries' => 'on' to delete entries also. [unofficial]
|
260
|
+
def remove_feed(id, serviceid, url, options = nil)
|
261
|
+
params = { 'url' => url }
|
262
|
+
params.update(options) if options
|
263
|
+
remove_service(id, serviceid, 'feed', options = nil)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Removes a Twitter service from the authenticated user, a group
|
267
|
+
# or an imaginary friend specified by a unique ID. Specify
|
268
|
+
# 'deleteentries' => 'on' to delete entries also. [unofficial]
|
269
|
+
def remove_twitter(id, serviceid, twitter_name, options = nil)
|
270
|
+
params = { 'username' => twitter_name }
|
271
|
+
params.update(options) if options
|
272
|
+
remove_service(id, serviceid, 'twitter', options = nil)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Changes the picture of the authenticated user, a group or an
|
276
|
+
# imaginary friend. [unofficial]
|
277
|
+
def change_picture(id, io)
|
278
|
+
post(ROOT_URI + '/a/changepicture', 'stream' => id,
|
279
|
+
'picture' => io)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Unsubscribe from a friend, a group or an imaginary friend
|
283
|
+
# specified by a unique ID. [unofficial]
|
284
|
+
def unsubscribe_from(id)
|
285
|
+
post(ROOT_URI + '/a/unsubscribe', 'stream' => id)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Creates an imaginary friend of a given +nickname+ and returns a
|
289
|
+
# unique ID string on success. Like other methods in general, an
|
290
|
+
# exception is raised on failure. [unofficial]
|
291
|
+
def create_imaginary_friend(nickname)
|
292
|
+
post(ROOT_URI + '/a/createimaginary', 'name' => nickname).xpath("//*[@id='serviceseditor']/@streamid").to_s
|
293
|
+
end
|
294
|
+
|
295
|
+
# Renames an imaginary friend specified by a unique ID to a given
|
296
|
+
# +nickname+. [unofficial]
|
297
|
+
def rename_imaginary_friend(id, nickname)
|
298
|
+
parser = post(ROOT_URI + '/a/profiledialog', 'stream' => id)['html_parser']
|
299
|
+
form = parser.xpath("//form[1]")
|
300
|
+
hash = { 'stream' => id }
|
301
|
+
form.xpath(".//input").each { |input|
|
302
|
+
case input['type'].downcase
|
303
|
+
when 'text'
|
304
|
+
hash[input['name']] = input['value']
|
305
|
+
end
|
306
|
+
}
|
307
|
+
form.xpath(".//textarea").each { |input|
|
308
|
+
hash[input['name']] = input.text
|
309
|
+
}
|
310
|
+
hash['name'] = nickname
|
311
|
+
post(ROOT_URI + '/a/editprofile', hash)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
data/lib/friendfeed.rb
ADDED
@@ -0,0 +1,478 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#--
|
3
|
+
# friendfeed.rb - provides access to FriendFeed API's
|
4
|
+
#++
|
5
|
+
# Copyright (c) 2009 Akinori MUSHA <knu@iDaemons.org>
|
6
|
+
#
|
7
|
+
# All rights reserved. You can redistribute and/or modify it under the same
|
8
|
+
# terms as Ruby.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'rubygems'
|
12
|
+
require 'json'
|
13
|
+
require 'mechanize'
|
14
|
+
require 'uri'
|
15
|
+
require 'friendfeed/compat'
|
16
|
+
|
17
|
+
module FriendFeed
|
18
|
+
ROOT_URI = URI.parse("https://friendfeed.com/")
|
19
|
+
|
20
|
+
# Client library for FriendFeed API.
|
21
|
+
class Client
|
22
|
+
attr_reader :nickname
|
23
|
+
|
24
|
+
#
|
25
|
+
# Official API
|
26
|
+
#
|
27
|
+
|
28
|
+
API_URI = ROOT_URI + "/api/"
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def get_api_agent
|
33
|
+
@api_agent ||= WWW::Mechanize.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate
|
37
|
+
call_api('validate')
|
38
|
+
end
|
39
|
+
|
40
|
+
def require_api_login
|
41
|
+
@nickname or raise 'not logged in'
|
42
|
+
end
|
43
|
+
|
44
|
+
def call_subscription_api(path)
|
45
|
+
require_api_login
|
46
|
+
|
47
|
+
uri = API_URI + path
|
48
|
+
|
49
|
+
agent = WWW::Mechanize.new
|
50
|
+
agent.auth('username', @nickname)
|
51
|
+
JSON.parse(agent.post(uri, { 'apikey' => @remote_key }).body)
|
52
|
+
end
|
53
|
+
|
54
|
+
public
|
55
|
+
|
56
|
+
attr_reader :nickname, :remote_key
|
57
|
+
|
58
|
+
# Performs a login with a +nickname+ and +remote key+ and returns
|
59
|
+
# self. This enables call of any official API that requires
|
60
|
+
# authentication. It is not needed to call this method if you
|
61
|
+
# have called login(), which internally obtains a remote key and
|
62
|
+
# calls this method. An exception is raised if authentication
|
63
|
+
# fails.
|
64
|
+
def api_login(nickname, remote_key)
|
65
|
+
@nickname = nickname
|
66
|
+
@remote_key = remote_key
|
67
|
+
@api_agent = get_api_agent()
|
68
|
+
@api_agent.auth(@nickname, @remote_key)
|
69
|
+
validate
|
70
|
+
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# Calls an official API specified by a +path+ with optional
|
75
|
+
# +get_parameters+ and +post_parameters+, and returns an object
|
76
|
+
# parsed from a JSON response. If +post_parameters+ is given, a
|
77
|
+
# POST request is issued. A GET request is issued otherwise.
|
78
|
+
def call_api(path, get_parameters = nil, post_parameters = nil, raw = false)
|
79
|
+
api_agent = get_api_agent()
|
80
|
+
|
81
|
+
uri = API_URI + path
|
82
|
+
if get_parameters
|
83
|
+
uri.query = get_parameters.map { |key, value|
|
84
|
+
if array = Array.try_convert(value)
|
85
|
+
value = array.join(',')
|
86
|
+
end
|
87
|
+
URI.encode(key) + "=" + URI.encode(value)
|
88
|
+
}.join('&')
|
89
|
+
end
|
90
|
+
|
91
|
+
if post_parameters
|
92
|
+
body = api_agent.post(uri, post_parameters).body
|
93
|
+
else
|
94
|
+
body = api_agent.get_file(uri)
|
95
|
+
end
|
96
|
+
|
97
|
+
if raw
|
98
|
+
body
|
99
|
+
else
|
100
|
+
JSON.parse(body)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Gets profile information of a user of a given +nickname+,
|
105
|
+
# defaulted to the authenticated user, in hash.
|
106
|
+
def get_profile(nickname = @nickname)
|
107
|
+
nickname or require_api_login
|
108
|
+
call_api('user/%s/profile' % URI.encode(nickname))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Edits profile information of the authenticated user. The fields
|
112
|
+
# "name" and "picture" are supported.
|
113
|
+
def edit_profile(hash)
|
114
|
+
nickname or require_api_login
|
115
|
+
call_api('user/%s/profile' % URI.encode(nickname), nil, hash)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Gets an array of profile information of users of given
|
119
|
+
# +nicknames+.
|
120
|
+
def get_profiles(nicknames)
|
121
|
+
call_api('profiles', 'nickname' => nicknames)['profiles']
|
122
|
+
end
|
123
|
+
|
124
|
+
# Gets an array of profile information of friends of a user of a
|
125
|
+
# given +nickname+ (defaulted to the authenticated user) is
|
126
|
+
# subscribing to.
|
127
|
+
def get_real_friends(nickname = @nickname)
|
128
|
+
nickname or require_api_login
|
129
|
+
nicknames = []
|
130
|
+
get_profile(@nickname)['subscriptions'].each { |subscription|
|
131
|
+
if nickname = subscription['nickname']
|
132
|
+
nicknames << nickname
|
133
|
+
end
|
134
|
+
}
|
135
|
+
get_profiles(nicknames)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Gets an array of profile information of the authenticated user's
|
139
|
+
# imaginary friends.
|
140
|
+
def get_imaginary_friends
|
141
|
+
nickname or require_api_login
|
142
|
+
profiles = []
|
143
|
+
get_profile(@nickname)['subscriptions'].each { |subscription|
|
144
|
+
if subscription['nickname'].nil?
|
145
|
+
profiles << get_profile(subscription['id'])
|
146
|
+
end
|
147
|
+
}
|
148
|
+
profiles
|
149
|
+
end
|
150
|
+
|
151
|
+
# Gets profile information of one of the authenticated user's
|
152
|
+
# imaginary friends.
|
153
|
+
def get_imaginary_friend(id)
|
154
|
+
get_profile(id)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Gets an array of the most recent public entries.
|
158
|
+
def get_public_entries()
|
159
|
+
call_api('feed/public')['entries']
|
160
|
+
end
|
161
|
+
|
162
|
+
# Gets an array of the entries the authenticated user would see on
|
163
|
+
# their home page.
|
164
|
+
def get_home_entries()
|
165
|
+
require_api_login
|
166
|
+
call_api('feed/home')['entries']
|
167
|
+
end
|
168
|
+
|
169
|
+
# Gets an array of the entries for the authenticated user's list
|
170
|
+
# of a given +nickname+
|
171
|
+
def get_list_entries(nickname)
|
172
|
+
require_api_login
|
173
|
+
call_api('feed/list/%s' % URI.encode(nickname))['entries']
|
174
|
+
end
|
175
|
+
|
176
|
+
# Gets an array of the most recent entries from a user of a given
|
177
|
+
# +nickname+ (defaulted to the authenticated user).
|
178
|
+
def get_user_entries(nickname = @nickname)
|
179
|
+
nickname or require_api_login
|
180
|
+
call_api('feed/user/%s' % URI.encode(nickname))['entries']
|
181
|
+
end
|
182
|
+
|
183
|
+
# Gets an array of the most recent entries from users of given
|
184
|
+
# +nicknames+.
|
185
|
+
def get_multi_user_entries(nicknames)
|
186
|
+
call_api('feed/user', 'nickname' => nicknames)['entries']
|
187
|
+
end
|
188
|
+
|
189
|
+
# Gets an array of the most recent entries a user of a given
|
190
|
+
# +nickname+ (defaulted to the authenticated user) has commented
|
191
|
+
# on.
|
192
|
+
def get_user_commented_entries(nickname = @nickname)
|
193
|
+
nickname or require_api_login
|
194
|
+
call_api('feed/user/%s/comments' % URI.encode(nickname))['entries']
|
195
|
+
end
|
196
|
+
|
197
|
+
# Gets an array of the most recent entries a user of a given
|
198
|
+
# +nickname+ (defaulted to the authenticated user) has like'd.
|
199
|
+
def get_user_liked_entries(nickname = @nickname)
|
200
|
+
nickname or require_api_login
|
201
|
+
call_api('feed/user/%s/likes' % URI.encode(nickname))['entries']
|
202
|
+
end
|
203
|
+
|
204
|
+
# Gets an array of the most recent entries a user of a given
|
205
|
+
# +nickname+ (defaulted to the authenticated user) has commented
|
206
|
+
# on or like'd.
|
207
|
+
def get_user_discussed_entries(nickname = @nickname)
|
208
|
+
nickname or require_api_login
|
209
|
+
call_api('feed/user/%s/discussion' % URI.encode(nickname))['entries']
|
210
|
+
end
|
211
|
+
|
212
|
+
# Gets an array of the most recent entries from friends of a user
|
213
|
+
# of a given +nickname+ (defaulted to the authenticated user).
|
214
|
+
def get_user_friend_entries(nickname = @nickname)
|
215
|
+
nickname or require_api_login
|
216
|
+
call_api('feed/user/%s/friends' % URI.encode(nickname))['entries']
|
217
|
+
end
|
218
|
+
|
219
|
+
# Gets an array of the most recent entries in a room of a given
|
220
|
+
# +nickname+.
|
221
|
+
def get_room_entries(nickname)
|
222
|
+
call_api('feed/room/%s' % URI.encode(nickname))['entries']
|
223
|
+
end
|
224
|
+
|
225
|
+
# Gets an array of the entries the authenticated user would see on
|
226
|
+
# their rooms page.
|
227
|
+
def get_rooms_entries()
|
228
|
+
call_api('feed/rooms')['entries']
|
229
|
+
end
|
230
|
+
|
231
|
+
# Gets an entry of a given +entryid+. An exception is raised when
|
232
|
+
# it fails.
|
233
|
+
def get_entry(entryid)
|
234
|
+
call_api('feed/entry/%s' % URI.encode(entryid))['entries'].first
|
235
|
+
end
|
236
|
+
|
237
|
+
# Gets an array of entries of given +entryids+. An exception is
|
238
|
+
# raised when it fails.
|
239
|
+
def get_entries(entryids)
|
240
|
+
call_api('feed/entry', 'entry_id' => entryids)['entries']
|
241
|
+
end
|
242
|
+
|
243
|
+
# Gets an array of entries that match a given +query+.
|
244
|
+
def search(query)
|
245
|
+
call_api('feed/search', 'q' => query)['entries']
|
246
|
+
end
|
247
|
+
|
248
|
+
# Gets an array of entries that link to a given +url+.
|
249
|
+
def search_for_url(url, options = nil)
|
250
|
+
new_options = { 'url' => url }
|
251
|
+
new_options.merge!(options) if options
|
252
|
+
call_api('feed/url', new_options)['entries']
|
253
|
+
end
|
254
|
+
|
255
|
+
# Gets an array of entries that link to a given +domain+.
|
256
|
+
def search_for_domain(url, options = nil)
|
257
|
+
new_options = { 'url' => url }
|
258
|
+
new_options.merge!(options) if options
|
259
|
+
call_api('feed/domain', new_options)['entries']
|
260
|
+
end
|
261
|
+
|
262
|
+
# Publishes (shares) a given entry.
|
263
|
+
def add_entry(title, options = nil)
|
264
|
+
require_api_login
|
265
|
+
new_options = { 'title' => title }
|
266
|
+
if options
|
267
|
+
options.each { |key, value|
|
268
|
+
case key = key.to_s
|
269
|
+
when 'title', 'link', 'comment', 'room'
|
270
|
+
new_options[key] = value
|
271
|
+
when 'images'
|
272
|
+
value.each_with_index { |value, i|
|
273
|
+
if url = String.try_convert(value)
|
274
|
+
link = nil
|
275
|
+
else
|
276
|
+
if array = Array.try_convert(value)
|
277
|
+
value1, value2 = *array
|
278
|
+
elsif hash = Hash.try_convert(value)
|
279
|
+
value1, value2 = *hash.values_at('url', 'link')
|
280
|
+
else
|
281
|
+
raise TypeError, "Each image must be specified by <image URL>, [<image URL>, <link URL>], or {'url' => <image URL>, 'link' => <link URL>}."
|
282
|
+
end
|
283
|
+
url = String.try_convert(value1) or
|
284
|
+
raise TypeError, "can't convert #{value1.class} into String"
|
285
|
+
link = String.try_convert(value2) or
|
286
|
+
raise TypeError, "can't convert #{value2.class} into String"
|
287
|
+
end
|
288
|
+
new_options['image%d_url' % i] = url
|
289
|
+
new_options['image%d_link' % i] = link if link
|
290
|
+
}
|
291
|
+
when 'audios'
|
292
|
+
value.each_with_index { |value, i|
|
293
|
+
if url = String.try_convert(value)
|
294
|
+
link = nil
|
295
|
+
else
|
296
|
+
if array = Array.try_convert(value)
|
297
|
+
value1, value2 = *array
|
298
|
+
elsif hash = Hash.try_convert(value)
|
299
|
+
value1, value2 = *hash.values_at('url', 'link')
|
300
|
+
else
|
301
|
+
raise TypeError, "Each audio must be specified by <audio URL>, [<audio URL>, <link URL>], or {'url' => <audio URL>, 'link' => <link URL>}."
|
302
|
+
end
|
303
|
+
url = String.try_convert(value1) or
|
304
|
+
raise TypeError, "can't convert #{value1.class} into String"
|
305
|
+
link = String.try_convert(value2) or
|
306
|
+
raise TypeError, "can't convert #{value2.class} into String"
|
307
|
+
end
|
308
|
+
new_options['audio%d_url' % i] = url
|
309
|
+
new_options['audio%d_link' % i] = link if link
|
310
|
+
}
|
311
|
+
when 'files'
|
312
|
+
value.each_with_index { |value, i|
|
313
|
+
if file = IO.try_convert(value)
|
314
|
+
link = nil
|
315
|
+
else
|
316
|
+
if array = Array.try_convert(value)
|
317
|
+
value1, value2 = *array
|
318
|
+
elsif hash = Hash.try_convert(value)
|
319
|
+
value1, value2 = *hash.values_at('file', 'link')
|
320
|
+
else
|
321
|
+
raise TypeError, "Each file must be specified by <file IO>, [<file IO>, <link URL>], or {'file' => <file IO>, 'link' => <link URL>}."
|
322
|
+
end
|
323
|
+
file = IO.try_convert(value1) or
|
324
|
+
raise TypeError, "can't convert #{value1.class} into IO"
|
325
|
+
link = String.try_convert(value2) or
|
326
|
+
raise TypeError, "can't convert #{value2.class} into String"
|
327
|
+
end
|
328
|
+
new_options['file%d' % i] = file
|
329
|
+
new_options['file%d_link' % i] = link if link
|
330
|
+
}
|
331
|
+
end
|
332
|
+
}
|
333
|
+
end
|
334
|
+
call_api('share', nil, new_options)['entries'].first
|
335
|
+
end
|
336
|
+
|
337
|
+
alias publish add_entry
|
338
|
+
alias share add_entry
|
339
|
+
|
340
|
+
# Adds a comment to a given entry.
|
341
|
+
def add_comment(entryid, body)
|
342
|
+
require_api_login
|
343
|
+
call_api('comment', nil, {
|
344
|
+
'entry' => entryid,
|
345
|
+
'body' => body,
|
346
|
+
})
|
347
|
+
end
|
348
|
+
|
349
|
+
# Edits a given comment.
|
350
|
+
def edit_comment(entryid, commentid, body)
|
351
|
+
require_api_login
|
352
|
+
call_api('comment', nil, {
|
353
|
+
'entry' => entryid,
|
354
|
+
'comment' => commentid,
|
355
|
+
'body' => body,
|
356
|
+
})
|
357
|
+
end
|
358
|
+
|
359
|
+
# Deletes a given comment.
|
360
|
+
def delete_comment(entryid, commentid)
|
361
|
+
require_api_login
|
362
|
+
call_api('comment/delete', nil, {
|
363
|
+
'entry' => entryid,
|
364
|
+
'comment' => commentid,
|
365
|
+
})
|
366
|
+
end
|
367
|
+
|
368
|
+
# Undeletes a given comment that is already deleted.
|
369
|
+
def undelete_comment(entryid, commentid)
|
370
|
+
require_api_login
|
371
|
+
call_api('comment/delete', nil, {
|
372
|
+
'entry' => entryid,
|
373
|
+
'comment' => commentid,
|
374
|
+
'undelete' => 'on',
|
375
|
+
})
|
376
|
+
end
|
377
|
+
|
378
|
+
# Adds a "like" to a given entry.
|
379
|
+
def add_like(entryid)
|
380
|
+
require_api_login
|
381
|
+
call_api('like', nil, {
|
382
|
+
'entry' => entryid,
|
383
|
+
})
|
384
|
+
end
|
385
|
+
|
386
|
+
# Deletes an existing "like" from a given entry.
|
387
|
+
def delete_like(entryid)
|
388
|
+
require_api_login
|
389
|
+
call_api('like/delete', nil, {
|
390
|
+
'entry' => entryid,
|
391
|
+
})
|
392
|
+
end
|
393
|
+
|
394
|
+
# Deletes an existing entry of a given +entryid+.
|
395
|
+
def delete_entry(entryid)
|
396
|
+
require_api_login
|
397
|
+
call_api('entry/delete', nil, {
|
398
|
+
'entry' => entryid,
|
399
|
+
})
|
400
|
+
end
|
401
|
+
|
402
|
+
# Undeletes a given entry that is already deleted.
|
403
|
+
def undelete_entry(entryid)
|
404
|
+
require_api_login
|
405
|
+
call_api('entry/delete', nil, {
|
406
|
+
'entry' => entryid,
|
407
|
+
'undelete' => 'on',
|
408
|
+
})
|
409
|
+
end
|
410
|
+
|
411
|
+
# Hides an existing entry of a given +entryid+.
|
412
|
+
def hide_entry(entryid)
|
413
|
+
require_api_login
|
414
|
+
call_api('entry/hide', nil, {
|
415
|
+
'entry' => entryid,
|
416
|
+
})
|
417
|
+
end
|
418
|
+
|
419
|
+
# Unhides a given entry that is already hidden.
|
420
|
+
def unhide_entry(entryid)
|
421
|
+
require_api_login
|
422
|
+
call_api('entry/hide', nil, {
|
423
|
+
'entry' => entryid,
|
424
|
+
'unhide' => 'on',
|
425
|
+
})
|
426
|
+
end
|
427
|
+
|
428
|
+
# Gets a picture of a user of a given +nickname+ (defaulted to the
|
429
|
+
# authenticated user) in blob. Size can be 'small' (default),
|
430
|
+
# 'medium' or 'large',
|
431
|
+
def get_picture(nickname = @nickname, size = 'small')
|
432
|
+
nickname or require_api_login
|
433
|
+
call_api('/%s/picture' % URI.escape(nickname), { 'size' => size }, nil, true)
|
434
|
+
end
|
435
|
+
|
436
|
+
# Gets a picture of a room of a given +nickname+ in blob. Size
|
437
|
+
# can be 'small' (default), 'medium' or 'large',
|
438
|
+
def get_room_picture(nickname, size = 'small')
|
439
|
+
call_api('/rooms/%s/picture' % URI.escape(nickname), { 'size' => size }, nil, true)
|
440
|
+
end
|
441
|
+
|
442
|
+
# Gets profile information of a room of a given +nickname+ in
|
443
|
+
# hash.
|
444
|
+
def get_room_profile(nickname)
|
445
|
+
call_api('room/%s/profile' % URI.encode(nickname))
|
446
|
+
end
|
447
|
+
|
448
|
+
# Gets profile information of the authenticated user's list of a
|
449
|
+
# given +nickname+ in hash.
|
450
|
+
def get_list_profile(nickname)
|
451
|
+
call_api('list/%s/profile' % URI.encode(nickname))
|
452
|
+
end
|
453
|
+
|
454
|
+
# Subscribes to a user of a given +nickname+ and returns a status
|
455
|
+
# string.
|
456
|
+
def subscribe_to_user(nickname)
|
457
|
+
call_subscription_api('user/%s/subscribe' % URI.encode(nickname))['status']
|
458
|
+
end
|
459
|
+
|
460
|
+
# Unsubscribes from a user of a given +nickname+ and returns a
|
461
|
+
# status string.
|
462
|
+
def unsubscribe_from_user(nickname)
|
463
|
+
call_subscription_api('user/%s/subscribe?unsubscribe=1' % URI.encode(nickname))['status']
|
464
|
+
end
|
465
|
+
|
466
|
+
# Subscribes to a room of a given +nickname+ and returns a status
|
467
|
+
# string.
|
468
|
+
def subscribe_to_room(nickname)
|
469
|
+
call_subscription_api('room/%s/subscribe' % URI.encode(nickname))['status']
|
470
|
+
end
|
471
|
+
|
472
|
+
# Unsubscribes from a room of a given +nickname+ and returns a
|
473
|
+
# status string.
|
474
|
+
def unsubscribe_from_room(nickname)
|
475
|
+
call_subscription_api('room/%s/subscribe?unsubscribe=1' % URI.encode(nickname))['status']
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: knu-friendfeed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Akinori MUSHA
|
@@ -15,14 +15,17 @@ dependencies: []
|
|
15
15
|
|
16
16
|
description: A Ruby module that provides access to FriendFeed API's.
|
17
17
|
email: knu@idaemons.org
|
18
|
-
executables:
|
19
|
-
|
18
|
+
executables:
|
19
|
+
- tw2ff
|
20
20
|
extensions: []
|
21
21
|
|
22
22
|
extra_rdoc_files: []
|
23
23
|
|
24
|
-
files:
|
25
|
-
|
24
|
+
files:
|
25
|
+
- lib/friendfeed.rb
|
26
|
+
- lib/friendfeed/compat.rb
|
27
|
+
- lib/friendfeed/unofficial.rb
|
28
|
+
- bin/tw2ff
|
26
29
|
has_rdoc: true
|
27
30
|
homepage: http://github.com/knu/ruby-friendfeed
|
28
31
|
post_install_message:
|