knu-friendfeed 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|