tjplurker 1.0.4 → 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/lib/tjplurker.rb +1 -351
- data/lib/tjplurker/core.rb +296 -0
- data/lib/tjplurker/legacy.rb +489 -0
- data/lib/tjplurker/robot.rb +142 -0
- metadata +18 -4
data/lib/tjplurker.rb
CHANGED
@@ -1,351 +1 @@
|
|
1
|
-
|
2
|
-
require 'logger'
|
3
|
-
require 'json'
|
4
|
-
require 'net/http'
|
5
|
-
|
6
|
-
$tjpLog = Logger.new(STDOUT)
|
7
|
-
$tjpLog.level = Logger::INFO
|
8
|
-
$tjpLog.formatter = proc{ |severity, datetime, progname, msg|
|
9
|
-
"#{severity} [#{datetime}] in #{progname} -- #{msg}\n"
|
10
|
-
}
|
11
|
-
# It's a wrapper of Plurk API.
|
12
|
-
# Note that every methods in this class returns a JSON object
|
13
|
-
# which was converted from Plurk server's response.
|
14
|
-
# All mehtods will retry again when request timeout occurs.
|
15
|
-
class TJPlurker
|
16
|
-
# Plurk user ID.
|
17
|
-
attr_reader :user
|
18
|
-
|
19
|
-
# If +auto_login+ sets +true+, it will autologin while constructing.
|
20
|
-
def initialize api_key, user, passwd, auto_login=true
|
21
|
-
@user, @passwd, @api_key = user, passwd, api_key
|
22
|
-
@cookie = nil
|
23
|
-
@user_channel = nil
|
24
|
-
@id_buffer = nil
|
25
|
-
login if auto_login
|
26
|
-
end
|
27
|
-
|
28
|
-
# If no_data sets true, the successful login message was simplified as follow:
|
29
|
-
#
|
30
|
-
# {"success_text": "ok"}
|
31
|
-
#
|
32
|
-
# Otherwise, more details of user's profile were retured.
|
33
|
-
def login no_data=true
|
34
|
-
method = "/API/Users/login"
|
35
|
-
attr = {:username=>@user, :password=>@passwd, :api_key=>@api_key}
|
36
|
-
attr[:no_data] = '1' if no_data
|
37
|
-
api(method, attr)
|
38
|
-
end
|
39
|
-
|
40
|
-
def plurk_add content, qualifier=':', limited_to=nil, no_comments=nil, lang='tr_ch'
|
41
|
-
method = "/API/Timeline/plurkAdd"
|
42
|
-
attr = {:api_key=>@api_key, :content=>content, :qualifier=>qualifier,:limited_to=>limited_to, :no_comments=>no_comments, :lang=>lang}
|
43
|
-
api(method, attr)
|
44
|
-
end
|
45
|
-
|
46
|
-
def response_add plurk_id, content, qualifier=':'
|
47
|
-
method = "/API/Responses/responseAdd"
|
48
|
-
attr = {:api_key=>@api_key, :plurk_id=>plurk_id, :content=>content, :qualifier=>qualifier}
|
49
|
-
api(method, attr)
|
50
|
-
end
|
51
|
-
|
52
|
-
def become_fan fan_id
|
53
|
-
method = "/API/FriendsFans/becomeFan"
|
54
|
-
attr = {:api_key=>@api_key, :fan_id=>fan_id}
|
55
|
-
api(method, attr)
|
56
|
-
end
|
57
|
-
|
58
|
-
def add_all_as_friends
|
59
|
-
method = "/API/Alerts/addAllAsFriends"
|
60
|
-
attr = {:api_key=>@api_key}
|
61
|
-
api(method, attr)
|
62
|
-
end
|
63
|
-
|
64
|
-
# This method is provided by TJPlurker.
|
65
|
-
# +max+:: The maximum number of your request
|
66
|
-
# +return+:: An array contains friends
|
67
|
-
def get_friend_list user_id, max=nil
|
68
|
-
list = []
|
69
|
-
offset = 0;
|
70
|
-
loop{
|
71
|
-
set = get_friends_by_offset(user_id, 100, offset)
|
72
|
-
size = set.size
|
73
|
-
puts size
|
74
|
-
break if size == 0
|
75
|
-
list |= set
|
76
|
-
break if max && list.size>max
|
77
|
-
offset += size
|
78
|
-
}
|
79
|
-
return list
|
80
|
-
end
|
81
|
-
|
82
|
-
# This maximum limit is set 100 officially, this method is not that useful.
|
83
|
-
# We suggest you use get_friend_list instead of get_friends_by_offset.
|
84
|
-
def get_friends_by_offset user_id, limit=100, offset='0'
|
85
|
-
method = "/API/FriendsFans/getFriendsByOffset"
|
86
|
-
attr = {:api_key=>@api_key, :user_id=>user_id, :offset=>offset, :limit=>limit}
|
87
|
-
api(method, attr)
|
88
|
-
end
|
89
|
-
|
90
|
-
def get_public_profile user_id
|
91
|
-
method = "/API/Profile/getPublicProfile"
|
92
|
-
attr = {:api_key=>@api_key, :user_id=>user_id}
|
93
|
-
api(method, attr)
|
94
|
-
end
|
95
|
-
|
96
|
-
# Warning:: It has bugs in offecial Plurk API.
|
97
|
-
def get_plurks limit=nil, offset=nil
|
98
|
-
method = "/API/Polling/getPlurks"
|
99
|
-
attr = {:api_key=>@api_key, :limit=>limit, :offset=>offset}
|
100
|
-
api(method, attr)
|
101
|
-
end
|
102
|
-
|
103
|
-
def get_user_channel
|
104
|
-
method = "/API/Realtime/getUserChannel"
|
105
|
-
attr = {:api_key=>@api_key}
|
106
|
-
api(method, attr)
|
107
|
-
end
|
108
|
-
|
109
|
-
def comet_channel
|
110
|
-
#Assign new channel if @user_channel is nil
|
111
|
-
@user_channel ||= get_user_channel
|
112
|
-
begin
|
113
|
-
res = Net::HTTP.get_response(URI.parse(@user_channel['comet_server']))
|
114
|
-
json = JSON.parse(res.body.sub!(/CometChannel\.scriptCallback\((.*)\);/){|match| $1})
|
115
|
-
rescue Timeout::Error => ex
|
116
|
-
$tjpLog.warn(self.class){"Request timeout, retry."}
|
117
|
-
retry
|
118
|
-
rescue Errno::ECONNRESET => ex
|
119
|
-
$tjpLog.warn(self.class){"Connection reset, retry."}
|
120
|
-
retry
|
121
|
-
rescue => ex
|
122
|
-
$tjpLog.fatal(self.class){"Unknown error, skip."}
|
123
|
-
puts ex.class, ex.message, ex.backtrace
|
124
|
-
return
|
125
|
-
end
|
126
|
-
#Update the offset
|
127
|
-
@user_channel['comet_server'].sub!(/[\d]+$/, json["new_offset"].to_s)
|
128
|
-
if json['data']
|
129
|
-
json['data'].each{|member|
|
130
|
-
notification = NotificationData.new(member)
|
131
|
-
return if notification.id == @id_buffer
|
132
|
-
$tjpLog.info(self.class){"Notification: #{notification.user_id}: \"#{notification.content}\""}
|
133
|
-
@id_buffer = notification.id
|
134
|
-
yield(notification)
|
135
|
-
}
|
136
|
-
end
|
137
|
-
json
|
138
|
-
end
|
139
|
-
|
140
|
-
private
|
141
|
-
def api method, attr
|
142
|
-
#Build POST Request
|
143
|
-
req = Net::HTTP::Post.new(method)
|
144
|
-
req.set_form_data(attr)
|
145
|
-
req["Cookie"] = @cookie
|
146
|
-
#Build GET Request
|
147
|
-
#path = method + "?" + URI.encode_www_form(attr)
|
148
|
-
#req2 = Net::HTTP::Get.new(path)
|
149
|
-
#req2["Cookie"] = @cookie
|
150
|
-
|
151
|
-
#Build HTTP connection
|
152
|
-
http = Net::HTTP.new('www.plurk.com', 80)
|
153
|
-
begin
|
154
|
-
resp = http.start{
|
155
|
-
$tjpLog.debug(self.class){"Request: #{method}, #{attr}"}
|
156
|
-
http.request(req)
|
157
|
-
}
|
158
|
-
@cookie ||= resp['set-cookie']
|
159
|
-
json = JSON.parse(resp.body)
|
160
|
-
$tjpLog.debug(self.class){"Response: #{json}"}
|
161
|
-
$tjpLog.error(self.class){json["error_text"]} if json.is_a?(Hash) && json["error_text"]
|
162
|
-
rescue Timeout::Error => ex
|
163
|
-
$tjpLog.warn(self.class){"Request timeout, retrying."}
|
164
|
-
retry
|
165
|
-
rescue Errno::ECONNRESET => ex
|
166
|
-
$tjpLog.warn(self.class){"Connection reset, retry."}
|
167
|
-
retry
|
168
|
-
rescue JSON::ParserError => ex
|
169
|
-
$tjpLog.error(self.class){"JSON parse error, request failed."}
|
170
|
-
rescue => ex
|
171
|
-
$tjpLog.fatal(self.class){"Unknown error, skip."}
|
172
|
-
puts ex.class, ex.message, ex.backtrace
|
173
|
-
return
|
174
|
-
end
|
175
|
-
return json
|
176
|
-
end
|
177
|
-
|
178
|
-
# Format the json from the comet server. See TJPlurker#comet_channel
|
179
|
-
class NotificationData
|
180
|
-
# Can be +new_plurk+ or +new_response+.
|
181
|
-
attr_reader :type
|
182
|
-
attr_reader :response_count
|
183
|
-
attr_reader :plurk_id
|
184
|
-
attr_reader :lang
|
185
|
-
attr_reader :user_id
|
186
|
-
attr_reader :qualifier
|
187
|
-
attr_reader :content
|
188
|
-
# Notification ID.
|
189
|
-
attr_reader :id
|
190
|
-
# Time of the listened post.
|
191
|
-
attr_reader :posted
|
192
|
-
attr_reader :owner_id
|
193
|
-
# The original JSON object from server response.
|
194
|
-
attr_reader :origin
|
195
|
-
|
196
|
-
def initialize member
|
197
|
-
@type = member['type']
|
198
|
-
@response_count = member['response_count']
|
199
|
-
@plurk_id = member['plurk_id']
|
200
|
-
|
201
|
-
@lang = @type=="new_plurk" ? member['lang'] : member['response']['lang']
|
202
|
-
@user_id = @type=="new_plurk" ? member['user_id'] : member['response']['user_id']
|
203
|
-
@qualifier = @type=="new_plurk" ? member['qualifier'] : member['response']['qualifier']
|
204
|
-
@content = @type=="new_plurk" ? member['content'] : member['response']['content']
|
205
|
-
@id = @type=="new_plurk" ? member['id'] : member['response']['id']
|
206
|
-
@posted = @type=="new_plurk" ? member['posted'] : member['response']['posted']
|
207
|
-
|
208
|
-
@owner_id = @type=="new_plurk" ? member['owner_id'] : member['plurk']['owner_id']
|
209
|
-
|
210
|
-
@origin = member
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
class Robot
|
215
|
-
def initialize *params
|
216
|
-
case params.size
|
217
|
-
when 1
|
218
|
-
@tjplurker = params[0]
|
219
|
-
when 3
|
220
|
-
api_key, user, passwd = params
|
221
|
-
@tjplurker = TJPlurker.new(api_key, user, passwd)
|
222
|
-
when 4
|
223
|
-
api_key, user, passwd, auto_login = params
|
224
|
-
@tjplurker = TJPlurker.new(api_key, user, passwd, auto_login)
|
225
|
-
end
|
226
|
-
@services = []
|
227
|
-
end
|
228
|
-
|
229
|
-
# Starting a loop and invokes services when a notification was listened.
|
230
|
-
def start
|
231
|
-
$tjpLog.info(self.class){"Start robot."}
|
232
|
-
Thread.new{
|
233
|
-
loop{
|
234
|
-
@tjplurker.add_all_as_friends
|
235
|
-
sleep(60)
|
236
|
-
}
|
237
|
-
}
|
238
|
-
loop{
|
239
|
-
@tjplurker.comet_channel{|data|
|
240
|
-
@services.each{|service|
|
241
|
-
begin
|
242
|
-
service.serve(@tjplurker, data)
|
243
|
-
rescue => ex
|
244
|
-
$tjpLog.error(self.class){"An error occured in #{service.class}, class: #{ex.class}, message: #{ex.message}, backtrace: #{ex.backtrace}"}
|
245
|
-
end
|
246
|
-
}
|
247
|
-
}
|
248
|
-
}
|
249
|
-
end
|
250
|
-
|
251
|
-
# +service+:: Can be instance TJPlurker::Service or class TJPlurker::Service or it's descendant classes.
|
252
|
-
def add_service service
|
253
|
-
if service.is_a? Service
|
254
|
-
@services << service
|
255
|
-
$tjpLog.info(self.class){"Add service: #{service.name}"}
|
256
|
-
elsif service <= Service
|
257
|
-
tmp = service.new
|
258
|
-
@services << tmp
|
259
|
-
$tjpLog.info(self.class){"Add service: #{tmp.name}"}
|
260
|
-
else
|
261
|
-
$tjpLog.warn(self.class){"Unrecognized service, ignored."}
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
class Service
|
267
|
-
attr_reader :name
|
268
|
-
def initialize name=self.class.to_s, &proc
|
269
|
-
@name = name
|
270
|
-
if block_given?
|
271
|
-
@serve_proc = proc
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
# Invoke when listened a new plurk.
|
276
|
-
def decide tjplurker, data
|
277
|
-
true
|
278
|
-
end
|
279
|
-
|
280
|
-
def action tjplurker, data
|
281
|
-
if @serve_proc
|
282
|
-
@serve_proc[tjplurker, data]
|
283
|
-
else
|
284
|
-
p data
|
285
|
-
end
|
286
|
-
end
|
287
|
-
|
288
|
-
def serve tjplurker, data
|
289
|
-
action(tjplurker, data) if decide(tjplurker, data)
|
290
|
-
end
|
291
|
-
end
|
292
|
-
|
293
|
-
class ThreadService < Service
|
294
|
-
# +lasting+:: How long in second will a thread lives. If one plurk just be responsed, then it will live another +lasting+ time.
|
295
|
-
# +refresh+:: Interval that checks if thread should terminate.
|
296
|
-
def initialize name=self.class.to_s, lasting=60, refresh=1, &proc
|
297
|
-
super name
|
298
|
-
@lasting = lasting
|
299
|
-
@refresh = refresh
|
300
|
-
@plurk_id_hash = Hash.new
|
301
|
-
@mutex = Mutex.new
|
302
|
-
end
|
303
|
-
|
304
|
-
# Invoke after thread termination.
|
305
|
-
# You can use this to tell users this service just terminate.
|
306
|
-
def final tjplurker, data
|
307
|
-
|
308
|
-
end
|
309
|
-
|
310
|
-
def serve tjplurker, data
|
311
|
-
if data.type=='new_plurk' && decide(tjplurker, data)
|
312
|
-
@plurk_id_hash[data.plurk_id] = Thread.new(data.plurk_id){|plurk_id|
|
313
|
-
$tjpLog.debug(self.class){"Create thread in plurk ##{plurk_id}"}
|
314
|
-
Thread.current[:last_time] = Time.now.to_i
|
315
|
-
loop{
|
316
|
-
sleep(@refresh)
|
317
|
-
$tjpLog.debug(self.class){"Plurk ##{plurk_id} remain #{Thread.current[:last_time]+@lasting-Time.now.to_i} seconds."}
|
318
|
-
@mutex.lock
|
319
|
-
if Time.now.to_i - Thread.current[:last_time] > @lasting
|
320
|
-
@mutex.unlock
|
321
|
-
break
|
322
|
-
end
|
323
|
-
@mutex.unlock
|
324
|
-
}
|
325
|
-
$tjpLog.debug(self.class){"Terminate thread in plurk ##{plurk_id}"}
|
326
|
-
@mutex.lock
|
327
|
-
@plurk_id_hash.delete(plurk_id)
|
328
|
-
@mutex.unlock
|
329
|
-
final(tjplurker, data)
|
330
|
-
}
|
331
|
-
else
|
332
|
-
@mutex.lock
|
333
|
-
if data.type=='new_response' && @plurk_id_hash.has_key?(data.plurk_id)
|
334
|
-
@plurk_id_hash[data.plurk_id][:last_time] = Time.now.to_i
|
335
|
-
action(tjplurker, data)
|
336
|
-
end
|
337
|
-
@mutex.unlock
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
if __FILE__ == $0
|
344
|
-
pk = Plurker.new("you api key", "your id", "your password")
|
345
|
-
puts pk.login
|
346
|
-
loop{
|
347
|
-
pk.comet_channel{|date|
|
348
|
-
puts data
|
349
|
-
}
|
350
|
-
}
|
351
|
-
end
|
1
|
+
require 'tjplurker/core'
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# :title:TJPlurker API Documentation
|
2
|
+
# encoding: utf-8
|
3
|
+
require 'oauth'
|
4
|
+
require 'json'
|
5
|
+
require 'logger'
|
6
|
+
require 'open-uri'
|
7
|
+
|
8
|
+
$tjpLog = Logger.new(STDOUT)
|
9
|
+
$tjpLog.level = Logger::INFO
|
10
|
+
$tjpLog.formatter = proc{ |severity, datetime, progname, msg|
|
11
|
+
"#{severity} [#{datetime}] in #{progname} -- #{msg}\n"
|
12
|
+
}
|
13
|
+
|
14
|
+
module TJP
|
15
|
+
# It's core class of TJPlurker based on {Plurk API 2.0}[http://www.plurk.com/API].
|
16
|
+
# Note that every API wrapped methods in this class returns a JSON object
|
17
|
+
# All mehtods will retry again when request timeout occurs.
|
18
|
+
class TJPlurker
|
19
|
+
# It will automatically authorize if +access_token+ and +access_token_secret+ are both given.
|
20
|
+
# If you do not have access token and secret, leave the parameters blank,
|
21
|
+
# and you will be asked to enter verification code to get access token and secret.
|
22
|
+
# key:: consumer key
|
23
|
+
# secret:: consumer secret
|
24
|
+
# return:: OAuth::AccessToken
|
25
|
+
def initialize(key, secret, token=nil, token_secret=nil)
|
26
|
+
@key, @secret, @token, @token_secret = key, secret, token, token_secret
|
27
|
+
@consumer = OAuth::Consumer.new(@key, @secret, {
|
28
|
+
:site => 'http://www.plurk.com',
|
29
|
+
:scheme => :header,
|
30
|
+
:http_method => :post,
|
31
|
+
:request_token_path => '/OAuth/request_token',
|
32
|
+
:access_token_path => '/OAuth/access_token',
|
33
|
+
:authorize_path => '/OAuth/authorize'
|
34
|
+
})
|
35
|
+
end
|
36
|
+
|
37
|
+
# return:: authorize URL
|
38
|
+
def get_authorize_url
|
39
|
+
@request_token = @consumer.get_request_token
|
40
|
+
return @request_token.authorize_url
|
41
|
+
end
|
42
|
+
|
43
|
+
# Change or get access token and secret.
|
44
|
+
# If parameters are not both given, user will be asked to enter verfication code,
|
45
|
+
# otherwise, replace the access token and secret.
|
46
|
+
# return:: access token and access token secret in Array
|
47
|
+
def authorize(access_token=nil, access_token_secret=nil)
|
48
|
+
if access_token && access_token_secret
|
49
|
+
#@access_token = OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
|
50
|
+
@token, @token_secret = access_token, access_token_secret
|
51
|
+
else
|
52
|
+
puts "Authorize URL: #{get_authorize_url}"
|
53
|
+
print "Please Enter your verification code: "
|
54
|
+
verify = gets.chomp!
|
55
|
+
access_token = @request_token.get_access_token :oauth_verifier=>verify
|
56
|
+
@token, @token_secret = access_token.token, access_token.secret
|
57
|
+
end
|
58
|
+
$tjpLog.info(self.class){"Access Token: #{@token}, Access Token Secret: #{@token_secret}"}
|
59
|
+
return @token, @token_secret
|
60
|
+
end
|
61
|
+
|
62
|
+
# url:: plurk APP url in String
|
63
|
+
# body:: options in Hash
|
64
|
+
# return:: result in JSON
|
65
|
+
def api(url, body=nil, headers=nil)
|
66
|
+
# clone consumer for supporting thread
|
67
|
+
access_token = OAuth::AccessToken.new(@consumer.clone, @token, @token_secret)
|
68
|
+
resp = access_token.post(url, body, headers).body
|
69
|
+
json = JSON.parse(resp)
|
70
|
+
$tjpLog.debug(self.class){"Response: #{json}"}
|
71
|
+
$tjpLog.error(self.class){json["error_text"]} if json.is_a?(Hash) && json["error_text"]
|
72
|
+
return json
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
#:section: Offical API wrapping
|
77
|
+
|
78
|
+
|
79
|
+
# qualifier:: The Plurk's qualifier, must be in English. Can be following:
|
80
|
+
# loves, likes, shares, gives, hates, wants, has, will, asks, wishes, was,
|
81
|
+
# feels, thinks, says, is, :, freestyle, hopes, needs, wonders
|
82
|
+
# limited_to:: Limit the plurk only to some users (also known as private plurking).
|
83
|
+
# Should can be a JSON list of friend ids in String formatting or Ruby Array, e.g.
|
84
|
+
# \[3,4,66,34] or "\[3,4,66,34]" will only be plurked to these user ids.
|
85
|
+
# If it's [0] then the Plurk is privatley posted to the poster's friends.
|
86
|
+
# no_comments:: If set to 1 or "1", then responses are disabled for this plurk.
|
87
|
+
# If set to 2 or "2", then only friends can respond to this plurk.
|
88
|
+
# lang:: The plurk's language. Can be following:
|
89
|
+
# 'en': 'English', 'pt_BR': 'Português', 'cn': '中文 (中国)', 'ca': 'Català', 'el': 'Ελληνικά'
|
90
|
+
# 'dk': 'Dansk', 'de': 'Deutsch', 'es': 'Español', 'sv': 'Svenska', 'nb': 'Norsk bokmål'
|
91
|
+
# 'hi': 'Hindi', 'ro': 'Română', 'hr': 'Hrvatski', 'fr': 'Français', 'ru': 'Pусский'
|
92
|
+
# 'it': 'Italiano', 'ja': '日本語', 'he': 'עברית', 'hu': 'Magyar', 'ne': 'Nederlands', 'th': 'ไทย'
|
93
|
+
# 'ta_fp': 'Filipino', 'in': 'Bahasa Indonesia', 'pl': 'Polski', 'ar': 'العربية', 'fi': 'Finnish'
|
94
|
+
# 'tr_ch': '中文 (繁體中文)', 'tr': 'Türkçe', 'ga': 'Gaeilge', 'sk': 'Slovenský'
|
95
|
+
# 'uk': 'українська', 'fa': 'فارسی
|
96
|
+
def plurk_add content, qualifier=':', limited_to=nil, no_comments=nil, lang='tr_ch'
|
97
|
+
method = "/APP/Timeline/plurkAdd"
|
98
|
+
limited_to = limited_to.to_s if limited_to.is_a? Array
|
99
|
+
attr = {:content=>content, :qualifier=>qualifier, :limited_to=>limited_to, :no_comments=>no_comments, :lang=>lang}
|
100
|
+
api(method, attr)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Adds a responses to +plurk_id+. Language is inherited from the plurk.
|
104
|
+
def response_add plurk_id, content, qualifier=':'
|
105
|
+
method = "/APP/Responses/responseAdd"
|
106
|
+
attr = {:plurk_id=>plurk_id, :content=>content, :qualifier=>qualifier}
|
107
|
+
api(method, attr)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Accept all friendship requests as friends.
|
111
|
+
def add_all_as_friends
|
112
|
+
method = "/APP/Alerts/addAllAsFriends"
|
113
|
+
api(method)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Become fan of +fan_id+. To stop being a fan of someone, use set_following
|
117
|
+
def become_fan fan_id
|
118
|
+
method = "/APP/FriendsFans/becomeFan"
|
119
|
+
attr = {:fan_id=>fan_id}
|
120
|
+
api(method, attr)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns user_id's friend list in chucks of 100 friends at a time.
|
124
|
+
# limit:: The max number of friends to be returned.
|
125
|
+
# offset:: Can be Numeric or String, e.g. 10, "20", 30, "40" etc.
|
126
|
+
# return:: An Array
|
127
|
+
def get_friends_by_offset user_id, limit=100, offset=0
|
128
|
+
method = "/APP/FriendsFans/getFriendsByOffset"
|
129
|
+
attr = {:user_id=>user_id, :offset=>offset, :limit=>limit}
|
130
|
+
api(method, attr)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Fetches public information such as a user's public plurks and basic information.
|
134
|
+
# Fetches also if the current user is following the user, are friends with or is a fan.
|
135
|
+
def get_public_profile user_id
|
136
|
+
method = "/APP/Profile/getPublicProfile"
|
137
|
+
attr = {:user_id=>user_id}
|
138
|
+
api(method, attr)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get plurks from user's timeline
|
142
|
+
# Warning:: It has bugs in offecial Plurk API.
|
143
|
+
# offset:: Return plurks newer than offset, formatted as 2009-6-20T21:55:34.
|
144
|
+
# limit:: The max number of plurks to be returned (default: 20)
|
145
|
+
# filter:: Can be only_user, only_responded, only_private or only_favorite
|
146
|
+
def get_plurks offset=nil, limit=nil, filter=nil
|
147
|
+
method = "/APP/Timeline/getPlurks"
|
148
|
+
attr = {:limit=>limit, :offset=>offset}
|
149
|
+
api(method, attr)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Get instant notifications when there are new plurks and responses on a user's timeline.
|
153
|
+
# Generally you don't need call this because comet_channel will automatically do that for you.
|
154
|
+
# For details, see http://www.plurk.com/API#realtime
|
155
|
+
# return:: channel name and comet server in Hash
|
156
|
+
def get_user_channel
|
157
|
+
method = "/APP/Realtime/getUserChannel"
|
158
|
+
api(method)
|
159
|
+
end
|
160
|
+
|
161
|
+
# It will do GET requests to user's comet server to get new notification data.
|
162
|
+
# The request will sleep for about 50 seconds before returning a response if there is
|
163
|
+
# no new data added to your channel.
|
164
|
+
# It will automatically get comet server URL from /APP/Realtime/getUserChannel
|
165
|
+
# if there is no comet server yet, and renew +offset+ each time it was called.
|
166
|
+
# You don't need to worry about URL and +offset+.
|
167
|
+
# For details, see http://www.plurk.com/API#realtime
|
168
|
+
# return:: plurk notification in Hash
|
169
|
+
def comet_channel
|
170
|
+
#Assign new channel if @user_channel is nil
|
171
|
+
@user_channel ||= get_user_channel
|
172
|
+
begin
|
173
|
+
res = Net::HTTP.get_response(URI.parse(@user_channel['comet_server']))
|
174
|
+
json = JSON.parse(res.body.sub!(/CometChannel\.scriptCallback\((.*)\);/){|match| $1})
|
175
|
+
rescue Timeout::Error => ex
|
176
|
+
$tjpLog.warn(self.class){"Request timeout, retry."}
|
177
|
+
retry
|
178
|
+
rescue Errno::ECONNRESET => ex
|
179
|
+
$tjpLog.warn(self.class){"Connection reset, retry."}
|
180
|
+
retry
|
181
|
+
rescue => ex
|
182
|
+
$tjpLog.fatal(self.class){"Unknown error, skip."}
|
183
|
+
puts ex.class, ex.message, ex.backtrace
|
184
|
+
return
|
185
|
+
end
|
186
|
+
#Update the offset
|
187
|
+
@user_channel['comet_server'].sub!(/[\d]+$/, json["new_offset"].to_s)
|
188
|
+
if json['data']
|
189
|
+
json['data'].each{|member|
|
190
|
+
notification = NotificationData.new(member)
|
191
|
+
# Prevent duplicate notification
|
192
|
+
return if notification.id == @id_buffer
|
193
|
+
$tjpLog.info(self.class){"Notification: #{notification.user_id}: \"#{notification.content}\""}
|
194
|
+
@id_buffer = notification.id
|
195
|
+
yield(notification)
|
196
|
+
}
|
197
|
+
end
|
198
|
+
json
|
199
|
+
end
|
200
|
+
|
201
|
+
# :section: TJPlurker provided
|
202
|
+
# Those methos were all provided by TJPlukrer instead of Plurk
|
203
|
+
|
204
|
+
# Improved from get_friends_by_offset for getting all friends from a user
|
205
|
+
# return:: An Array
|
206
|
+
def get_friends user_id, max=nil
|
207
|
+
list = []
|
208
|
+
offset = 0;
|
209
|
+
loop{
|
210
|
+
set = get_friends_by_offset(user_id, 100, offset)
|
211
|
+
size = set.size
|
212
|
+
break if size == 0
|
213
|
+
list |= set
|
214
|
+
break if max && list.size>max
|
215
|
+
offset += size
|
216
|
+
}
|
217
|
+
return list
|
218
|
+
end
|
219
|
+
|
220
|
+
# Used to follow a lot of users.
|
221
|
+
# level:: e.g. 1 for following user's all friends, 2 for following user's all friends and all friends of all friends.
|
222
|
+
def super_become_fan fan_id, level=1
|
223
|
+
return if level<=0
|
224
|
+
become_fan(fan_id)
|
225
|
+
list = []
|
226
|
+
offset = 0;
|
227
|
+
loop{
|
228
|
+
set = get_friends_by_offset(fan_id, 100, offset)
|
229
|
+
size = set.size
|
230
|
+
break if size == 0
|
231
|
+
tg = ThreadGroup.new
|
232
|
+
set.each{|i|
|
233
|
+
tg.add Thread.new{
|
234
|
+
wait_sec = 1
|
235
|
+
until become_fan(i["uid"]).key?("success_text")
|
236
|
+
$tjpLog.info(self.class){"Trying to become #{i["uid"]}'s fan in #{wait_sec} seconds"}
|
237
|
+
sleep(wait_sec)
|
238
|
+
wait_sec *= 2
|
239
|
+
end
|
240
|
+
$tjpLog.info(self.class){"Become #{i["uid"]}'s fan successfully"}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
tg.list.each{|i| i.join }
|
244
|
+
list |= set
|
245
|
+
offset += size
|
246
|
+
}
|
247
|
+
list.each{|i|
|
248
|
+
super_become_fan(i["uid"], level-1)
|
249
|
+
}
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Format plurk notification. See TJPlurker#comet_channel
|
254
|
+
class NotificationData
|
255
|
+
# Can be +new_plurk+ or +new_response+.
|
256
|
+
attr_reader :type
|
257
|
+
attr_reader :response_count
|
258
|
+
attr_reader :plurk_id
|
259
|
+
attr_reader :lang
|
260
|
+
attr_reader :user_id
|
261
|
+
attr_reader :qualifier
|
262
|
+
attr_reader :content
|
263
|
+
# Notification ID.
|
264
|
+
attr_reader :id
|
265
|
+
# Time of the listened post.
|
266
|
+
attr_reader :posted
|
267
|
+
attr_reader :owner_id
|
268
|
+
# The original JSON object from server response.
|
269
|
+
attr_reader :origin
|
270
|
+
|
271
|
+
def initialize member
|
272
|
+
@type = member['type']
|
273
|
+
@response_count = member['response_count']
|
274
|
+
@plurk_id = member['plurk_id']
|
275
|
+
|
276
|
+
@lang = @type=="new_plurk" ? member['lang'] : member['response']['lang']
|
277
|
+
@user_id = @type=="new_plurk" ? member['user_id'] : member['response']['user_id']
|
278
|
+
@qualifier = @type=="new_plurk" ? member['qualifier'] : member['response']['qualifier']
|
279
|
+
@content = @type=="new_plurk" ? member['content'] : member['response']['content']
|
280
|
+
@id = @type=="new_plurk" ? member['id'] : member['response']['id']
|
281
|
+
@posted = @type=="new_plurk" ? member['posted'] : member['response']['posted']
|
282
|
+
|
283
|
+
@owner_id = @type=="new_plurk" ? member['owner_id'] : member['plurk']['owner_id']
|
284
|
+
|
285
|
+
@origin = member
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
module_function
|
290
|
+
# Convert user name into user_id, e.g. Given "tonytonyjan", return "5874158"
|
291
|
+
def get_uid user_name
|
292
|
+
open("http://www.plurk.com/#{user_name}").read =~ /"user_id": (\d*)/
|
293
|
+
raise "User \"#{user_name}\" not found." unless $1
|
294
|
+
return $1
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,489 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'json'
|
3
|
+
require 'oauth'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
$tjpLog = Logger.new(STDOUT)
|
7
|
+
$tjpLog.level = Logger::INFO
|
8
|
+
$tjpLog.formatter = proc{ |severity, datetime, progname, msg|
|
9
|
+
"#{severity} [#{datetime}] in #{progname} -- #{msg}\n"
|
10
|
+
}
|
11
|
+
|
12
|
+
# == 注意
|
13
|
+
# 這是舊的函式庫,如果你想學習如何使用最新的TJPlurker,建議你應該參考 {此頁}[link:GETSTART.html]
|
14
|
+
#
|
15
|
+
# 如果你堅持享用舊版本,你應該在程式中如下引用以支援舊式的寫法:
|
16
|
+
# require "tjplurker/legacy"
|
17
|
+
# include Legacy
|
18
|
+
# 而不是
|
19
|
+
# require "tjplurker"
|
20
|
+
# include TJP
|
21
|
+
module Legacy
|
22
|
+
# == 注意
|
23
|
+
# 這是舊的函式庫,如果你想學習如何使用最新的TJPlurker,建議你應該參考 {此頁}[link:GETSTART.html]
|
24
|
+
#
|
25
|
+
# 如果你堅持享用舊版本,你應該在程式中如下引用以支援舊式的寫法:
|
26
|
+
# require "tjplurker/legacy"
|
27
|
+
# include Legacy
|
28
|
+
# 而不是
|
29
|
+
# require "tjplurker"
|
30
|
+
# include TJP
|
31
|
+
#
|
32
|
+
# 這是TJPlurker的核心類別,大部分的情況下此類別的方法都會回傳一個由Plurk Server產生的JSON物件。
|
33
|
+
# 所有的方法會在timeout發生的時候重新呼叫
|
34
|
+
#
|
35
|
+
# It's core class of TJPlurker.
|
36
|
+
# Note that every methods in this class returns a JSON object
|
37
|
+
# which was converted from Plurk server's response.
|
38
|
+
# All mehtods will retry again when request timeout occurs.
|
39
|
+
class TJPlurker
|
40
|
+
# 噗浪帳號
|
41
|
+
# Plurk user ID.
|
42
|
+
attr_reader :user
|
43
|
+
|
44
|
+
=begin
|
45
|
+
[auto_login] 是否在建構時自動登入
|
46
|
+
If +auto_login+ sets +true+, it will autologin while constructing.
|
47
|
+
=end
|
48
|
+
def initialize api_key, user, passwd, auto_login=true
|
49
|
+
@user, @passwd, @api_key = user, passwd, api_key
|
50
|
+
@cookie = nil
|
51
|
+
@user_channel = nil
|
52
|
+
@id_buffer = nil
|
53
|
+
login if auto_login
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
=begin
|
58
|
+
:section: Offical API wrapping
|
59
|
+
對Plurk官方API做包裹
|
60
|
+
|
61
|
+
Plurk API wrapping
|
62
|
+
=end
|
63
|
+
|
64
|
+
=begin
|
65
|
+
登入
|
66
|
+
[no_data] 是否將登入成功後的訊息簡化成:
|
67
|
+
{"success_text": "ok"}
|
68
|
+
否則將回傳使用者的詳細資料
|
69
|
+
|
70
|
+
If no_data sets true, the successful login message was simplified as follow:
|
71
|
+
{"success_text": "ok"}
|
72
|
+
Otherwise, more details of user's profile were retured.
|
73
|
+
=end
|
74
|
+
def login no_data=true
|
75
|
+
method = "/API/Users/login"
|
76
|
+
attr = {:username=>@user, :password=>@passwd, :api_key=>@api_key}
|
77
|
+
attr[:no_data] = '1' if no_data
|
78
|
+
api(method, attr)
|
79
|
+
end
|
80
|
+
|
81
|
+
=begin
|
82
|
+
發噗
|
83
|
+
|
84
|
+
範例:
|
85
|
+
require 'tjplurker'
|
86
|
+
tjp = TJPlurker.new("API Key", "帳號", "密碼")
|
87
|
+
tjp.plurk_add("Hello TJPlurker")
|
88
|
+
=end
|
89
|
+
def plurk_add content, qualifier=':', limited_to=nil, no_comments=nil, lang='tr_ch'
|
90
|
+
method = "/API/Timeline/plurkAdd"
|
91
|
+
attr = {:api_key=>@api_key, :content=>content, :qualifier=>qualifier,:limited_to=>limited_to, :no_comments=>no_comments, :lang=>lang}
|
92
|
+
api(method, attr)
|
93
|
+
end
|
94
|
+
|
95
|
+
=begin
|
96
|
+
回噗
|
97
|
+
[qualifier] 可以是如下的字串:
|
98
|
+
|
99
|
+
Can be the following string:
|
100
|
+
loves, likes, shares, gives, hates, wants, has, will,
|
101
|
+
asks, wishes, was, feels, thinks, says, is, :, freestyle, hopes, needs, wonders
|
102
|
+
範例:
|
103
|
+
require 'tjplurker'
|
104
|
+
tjp = TJPlurker.new("API Key", "帳號", "密碼")
|
105
|
+
json_obj = tjp.plurk_add("Hello TJPlurker")
|
106
|
+
tjp.response_add(json_obj["plurk_id"], "This is a reply.", "says")
|
107
|
+
=end
|
108
|
+
def response_add plurk_id, content, qualifier=':'
|
109
|
+
method = "/API/Responses/responseAdd"
|
110
|
+
attr = {:api_key=>@api_key, :plurk_id=>plurk_id, :content=>content, :qualifier=>qualifier}
|
111
|
+
api(method, attr)
|
112
|
+
end
|
113
|
+
|
114
|
+
=begin
|
115
|
+
成為粉絲
|
116
|
+
[fan_id] 你想追蹤的人的ID
|
117
|
+
|
118
|
+
The ID of the user you want to become fan of
|
119
|
+
=end
|
120
|
+
def become_fan fan_id
|
121
|
+
method = "/API/FriendsFans/becomeFan"
|
122
|
+
attr = {:api_key=>@api_key, :fan_id=>fan_id}
|
123
|
+
api(method, attr)
|
124
|
+
end
|
125
|
+
|
126
|
+
=begin
|
127
|
+
接受所有的好友邀請
|
128
|
+
|
129
|
+
Accept all friendship requests as friends.
|
130
|
+
Successful return:
|
131
|
+
{"success_text": "ok"}
|
132
|
+
=end
|
133
|
+
def add_all_as_friends
|
134
|
+
method = "/API/Alerts/addAllAsFriends"
|
135
|
+
attr = {:api_key=>@api_key}
|
136
|
+
api(method, attr)
|
137
|
+
end
|
138
|
+
|
139
|
+
=begin
|
140
|
+
取得使用者的好友名單
|
141
|
+
|
142
|
+
Returns user_id's friend list
|
143
|
+
[limit] The max number of friends to be returned (default 10).
|
144
|
+
[Successful return] 好友名單陣列
|
145
|
+
|
146
|
+
Returns a list of JSON objects users, e.g.
|
147
|
+
[{"id": 3, "nick_name": "alvin", ...}, ...]
|
148
|
+
[warnning] +limit+的最大值被官方設定為100,這個方法沒有很好用,建議你用tjp_get_friends取代
|
149
|
+
This maximum of +limit+ is set 100 officially, this method is not that useful.
|
150
|
+
We suggest you use get_friend_list instead of get_friends_by_offset.
|
151
|
+
=end
|
152
|
+
def get_friends_by_offset user_id, limit=100, offset='0'
|
153
|
+
method = "/API/FriendsFans/getFriendsByOffset"
|
154
|
+
attr = {:api_key=>@api_key, :user_id=>user_id, :offset=>offset, :limit=>limit}
|
155
|
+
api(method, attr)
|
156
|
+
end
|
157
|
+
|
158
|
+
=begin
|
159
|
+
Fetches public information such as a user's public plurks and basic information.
|
160
|
+
Fetches also if the current user is following the user, are friends with or is a fan.
|
161
|
+
[Successful return] Returns a JSON object with a lot of information
|
162
|
+
that can be used to construct a user's public profile and timeline.
|
163
|
+
[Error returns] HTTP 400 BAD REQUEST with {"error_text": "Invalid user_id"} as body
|
164
|
+
HTTP 400 BAD REQUEST with {"error_text": "User not found"} as body
|
165
|
+
=end
|
166
|
+
def get_public_profile user_id
|
167
|
+
method = "/API/Profile/getPublicProfile"
|
168
|
+
attr = {:api_key=>@api_key, :user_id=>user_id}
|
169
|
+
api(method, attr)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Warning:: It has bugs in offecial Plurk API.
|
173
|
+
def get_plurks limit=nil, offset=nil
|
174
|
+
method = "/API/Polling/getPlurks"
|
175
|
+
attr = {:api_key=>@api_key, :limit=>limit, :offset=>offset}
|
176
|
+
api(method, attr)
|
177
|
+
end
|
178
|
+
|
179
|
+
def get_user_channel
|
180
|
+
method = "/API/Realtime/getUserChannel"
|
181
|
+
attr = {:api_key=>@api_key}
|
182
|
+
api(method, attr)
|
183
|
+
end
|
184
|
+
|
185
|
+
def comet_channel
|
186
|
+
#Assign new channel if @user_channel is nil
|
187
|
+
@user_channel ||= get_user_channel
|
188
|
+
begin
|
189
|
+
res = Net::HTTP.get_response(URI.parse(@user_channel['comet_server']))
|
190
|
+
json = JSON.parse(res.body.sub!(/CometChannel\.scriptCallback\((.*)\);/){|match| $1})
|
191
|
+
rescue Timeout::Error => ex
|
192
|
+
$tjpLog.warn(self.class){"Request timeout, retry."}
|
193
|
+
retry
|
194
|
+
rescue Errno::ECONNRESET => ex
|
195
|
+
$tjpLog.warn(self.class){"Connection reset, retry."}
|
196
|
+
retry
|
197
|
+
rescue => ex
|
198
|
+
$tjpLog.fatal(self.class){"Unknown error, skip."}
|
199
|
+
puts ex.class, ex.message, ex.backtrace
|
200
|
+
return
|
201
|
+
end
|
202
|
+
#Update the offset
|
203
|
+
@user_channel['comet_server'].sub!(/[\d]+$/, json["new_offset"].to_s)
|
204
|
+
if json['data']
|
205
|
+
json['data'].each{|member|
|
206
|
+
notification = NotificationData.new(member)
|
207
|
+
return if notification.id == @id_buffer
|
208
|
+
$tjpLog.info(self.class){"Notification: #{notification.user_id}: \"#{notification.content}\""}
|
209
|
+
@id_buffer = notification.id
|
210
|
+
yield(notification)
|
211
|
+
}
|
212
|
+
end
|
213
|
+
json
|
214
|
+
end
|
215
|
+
|
216
|
+
=begin
|
217
|
+
:section: TJPlurker provided
|
218
|
+
由TJPlurker提供的工具,非官方有的方法
|
219
|
+
|
220
|
+
Those methos were all provided by TJPlukrer instead of plurk
|
221
|
+
=end
|
222
|
+
|
223
|
+
=begin
|
224
|
+
這個方法游TJPlurker提供的非官方功能,改良自get_friends_by_offset,用於取得使用者所有的好友
|
225
|
+
|
226
|
+
This method is provided by TJPlurker. Improved from get_friends_by_offset for getting all friends of a user
|
227
|
+
[max] 取得多少好友,nil則無限制
|
228
|
+
|
229
|
+
The maximum number of your request. Set +nil+ for unlimited.
|
230
|
+
[return] 陣列
|
231
|
+
|
232
|
+
An array contains friends
|
233
|
+
範例:
|
234
|
+
require 'tjplurker'
|
235
|
+
tjp = TJPlurker.new("API Key", "帳號", "密碼")
|
236
|
+
friend_list = tjp.tjp_get_friends("5874158")
|
237
|
+
friend_list.each{|friend| puts friend["uid"]}
|
238
|
+
=end
|
239
|
+
def tjp_get_friends user_id, max=nil
|
240
|
+
list = []
|
241
|
+
offset = 0;
|
242
|
+
loop{
|
243
|
+
set = get_friends_by_offset(user_id, 100, offset)
|
244
|
+
size = set.size
|
245
|
+
break if size == 0
|
246
|
+
list |= set
|
247
|
+
break if max && list.size>max
|
248
|
+
offset += size
|
249
|
+
}
|
250
|
+
return list
|
251
|
+
end
|
252
|
+
|
253
|
+
=begin
|
254
|
+
用於追蹤大量使用者的河道
|
255
|
+
[level] 深度,如等於1會追蹤使用者的所有好友,等於2則會追蹤使用者所有的好友以及好友的好友
|
256
|
+
=end
|
257
|
+
def tjp_super_become_fan fan_id, level=1
|
258
|
+
return if level<=0
|
259
|
+
become_fan(fan_id)
|
260
|
+
list = []
|
261
|
+
offset = 0;
|
262
|
+
loop{
|
263
|
+
set = get_friends_by_offset(fan_id, 100, offset)
|
264
|
+
size = set.size
|
265
|
+
break if size == 0
|
266
|
+
tg = ThreadGroup.new
|
267
|
+
set.each{|i|
|
268
|
+
tg.add Thread.new{
|
269
|
+
wait_sec = 1
|
270
|
+
until become_fan(i["uid"]).key?("success_text")
|
271
|
+
$tjpLog.info(self.class){"Trying to become #{i["uid"]}'s fan in #{wait_sec} seconds"}
|
272
|
+
sleep(wait_sec)
|
273
|
+
wait_sec *= 2
|
274
|
+
end
|
275
|
+
$tjpLog.info(self.class){"Become #{i["uid"]}'s fan successfully"}
|
276
|
+
}
|
277
|
+
}
|
278
|
+
tg.list.each{|i| i.join }
|
279
|
+
list |= set
|
280
|
+
offset += size
|
281
|
+
}
|
282
|
+
list.each{|i|
|
283
|
+
super_become_fan(i["uid"], level-1)
|
284
|
+
}
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
def api method, attr
|
289
|
+
#Build POST Request
|
290
|
+
req = Net::HTTP::Post.new(method)
|
291
|
+
req.set_form_data(attr)
|
292
|
+
req["Cookie"] = @cookie
|
293
|
+
#Build GET Request
|
294
|
+
#path = method + "?" + URI.encode_www_form(attr)
|
295
|
+
#req2 = Net::HTTP::Get.new(path)
|
296
|
+
#req2["Cookie"] = @cookie
|
297
|
+
|
298
|
+
#Build HTTP connection
|
299
|
+
http = Net::HTTP.new('www.plurk.com', 80)
|
300
|
+
begin
|
301
|
+
resp = http.start{
|
302
|
+
$tjpLog.debug(self.class){"Request: #{method}, #{attr}"}
|
303
|
+
http.request(req)
|
304
|
+
}
|
305
|
+
@cookie ||= resp['set-cookie']
|
306
|
+
json = JSON.parse(resp.body)
|
307
|
+
$tjpLog.debug(self.class){"Response: #{json}"}
|
308
|
+
$tjpLog.error(self.class){json["error_text"]} if json.is_a?(Hash) && json["error_text"]
|
309
|
+
rescue Timeout::Error => ex
|
310
|
+
$tjpLog.warn(self.class){"Request timeout, retrying."}
|
311
|
+
retry
|
312
|
+
rescue Errno::ECONNRESET => ex
|
313
|
+
$tjpLog.warn(self.class){"Connection reset, retry."}
|
314
|
+
retry
|
315
|
+
rescue JSON::ParserError => ex
|
316
|
+
$tjpLog.error(self.class){"JSON parse error, request failed."}
|
317
|
+
rescue => ex
|
318
|
+
$tjpLog.fatal(self.class){"Unknown error, skip."}
|
319
|
+
puts ex.class, ex.message, ex.backtrace
|
320
|
+
return
|
321
|
+
end
|
322
|
+
return json
|
323
|
+
end
|
324
|
+
|
325
|
+
# Format the json from the comet server. See TJPlurker#comet_channel
|
326
|
+
class NotificationData
|
327
|
+
# Can be +new_plurk+ or +new_response+.
|
328
|
+
attr_reader :type
|
329
|
+
attr_reader :response_count
|
330
|
+
attr_reader :plurk_id
|
331
|
+
attr_reader :lang
|
332
|
+
attr_reader :user_id
|
333
|
+
attr_reader :qualifier
|
334
|
+
attr_reader :content
|
335
|
+
# Notification ID.
|
336
|
+
attr_reader :id
|
337
|
+
# Time of the listened post.
|
338
|
+
attr_reader :posted
|
339
|
+
attr_reader :owner_id
|
340
|
+
# The original JSON object from server response.
|
341
|
+
attr_reader :origin
|
342
|
+
|
343
|
+
def initialize member
|
344
|
+
@type = member['type']
|
345
|
+
@response_count = member['response_count']
|
346
|
+
@plurk_id = member['plurk_id']
|
347
|
+
|
348
|
+
@lang = @type=="new_plurk" ? member['lang'] : member['response']['lang']
|
349
|
+
@user_id = @type=="new_plurk" ? member['user_id'] : member['response']['user_id']
|
350
|
+
@qualifier = @type=="new_plurk" ? member['qualifier'] : member['response']['qualifier']
|
351
|
+
@content = @type=="new_plurk" ? member['content'] : member['response']['content']
|
352
|
+
@id = @type=="new_plurk" ? member['id'] : member['response']['id']
|
353
|
+
@posted = @type=="new_plurk" ? member['posted'] : member['response']['posted']
|
354
|
+
|
355
|
+
@owner_id = @type=="new_plurk" ? member['owner_id'] : member['plurk']['owner_id']
|
356
|
+
|
357
|
+
@origin = member
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
class Robot
|
362
|
+
def initialize *params
|
363
|
+
case params.size
|
364
|
+
when 1
|
365
|
+
@tjplurker = params[0]
|
366
|
+
when 3
|
367
|
+
api_key, user, passwd = params
|
368
|
+
@tjplurker = TJPlurker.new(api_key, user, passwd)
|
369
|
+
when 4
|
370
|
+
api_key, user, passwd, auto_login = params
|
371
|
+
@tjplurker = TJPlurker.new(api_key, user, passwd, auto_login)
|
372
|
+
end
|
373
|
+
@services = []
|
374
|
+
end
|
375
|
+
|
376
|
+
# Starting a loop and invokes services when a notification was listened.
|
377
|
+
def start
|
378
|
+
$tjpLog.info(self.class){"Start robot."}
|
379
|
+
Thread.new{
|
380
|
+
loop{
|
381
|
+
@tjplurker.add_all_as_friends
|
382
|
+
sleep(60)
|
383
|
+
}
|
384
|
+
}
|
385
|
+
loop{
|
386
|
+
@tjplurker.comet_channel{|data|
|
387
|
+
@services.each{|service|
|
388
|
+
begin
|
389
|
+
service.serve(@tjplurker, data)
|
390
|
+
rescue => ex
|
391
|
+
$tjpLog.error(self.class){"An error occured in #{service.class}, class: #{ex.class}, message: #{ex.message}, backtrace: #{ex.backtrace}"}
|
392
|
+
end
|
393
|
+
}
|
394
|
+
}
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
# +service+:: Can be instance TJPlurker::Service or class TJPlurker::Service or it's descendant classes.
|
399
|
+
def add_service service
|
400
|
+
if service.is_a? Service
|
401
|
+
@services << service
|
402
|
+
$tjpLog.info(self.class){"Add service: #{service.name}"}
|
403
|
+
elsif service <= Service
|
404
|
+
tmp = service.new
|
405
|
+
@services << tmp
|
406
|
+
$tjpLog.info(self.class){"Add service: #{tmp.name}"}
|
407
|
+
else
|
408
|
+
$tjpLog.warn(self.class){"Unrecognized service, ignored."}
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
class Service
|
414
|
+
attr_reader :name
|
415
|
+
def initialize name=self.class.to_s, &proc
|
416
|
+
@name = name
|
417
|
+
if block_given?
|
418
|
+
@serve_proc = proc
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Invoke when listened a new plurk.
|
423
|
+
def decide tjplurker, data
|
424
|
+
true
|
425
|
+
end
|
426
|
+
|
427
|
+
def action tjplurker, data
|
428
|
+
if @serve_proc
|
429
|
+
@serve_proc[tjplurker, data]
|
430
|
+
else
|
431
|
+
p data
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def serve tjplurker, data
|
436
|
+
action(tjplurker, data) if decide(tjplurker, data)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
class ThreadService < Service
|
441
|
+
# +lasting+:: How long in second will a thread lives. If one plurk just be responsed, then it will live another +lasting+ time.
|
442
|
+
# +refresh+:: Interval that checks if thread should terminate.
|
443
|
+
def initialize name=self.class.to_s, lasting=60, refresh=1, &proc
|
444
|
+
super name
|
445
|
+
@lasting = lasting
|
446
|
+
@refresh = refresh
|
447
|
+
@plurk_id_hash = Hash.new
|
448
|
+
@mutex = Mutex.new
|
449
|
+
end
|
450
|
+
|
451
|
+
# Invoke after thread termination.
|
452
|
+
# You can use this to tell users this service just terminate.
|
453
|
+
def final tjplurker, data
|
454
|
+
|
455
|
+
end
|
456
|
+
|
457
|
+
def serve tjplurker, data
|
458
|
+
if data.type=='new_plurk' && decide(tjplurker, data)
|
459
|
+
@plurk_id_hash[data.plurk_id] = Thread.new(data.plurk_id){|plurk_id|
|
460
|
+
$tjpLog.debug(self.class){"Create thread in plurk ##{plurk_id}"}
|
461
|
+
Thread.current[:last_time] = Time.now.to_i
|
462
|
+
loop{
|
463
|
+
sleep(@refresh)
|
464
|
+
$tjpLog.debug(self.class){"Plurk ##{plurk_id} remain #{Thread.current[:last_time]+@lasting-Time.now.to_i} seconds."}
|
465
|
+
@mutex.lock
|
466
|
+
if Time.now.to_i - Thread.current[:last_time] > @lasting
|
467
|
+
@mutex.unlock
|
468
|
+
break
|
469
|
+
end
|
470
|
+
@mutex.unlock
|
471
|
+
}
|
472
|
+
$tjpLog.debug(self.class){"Terminate thread in plurk ##{plurk_id}"}
|
473
|
+
@mutex.lock
|
474
|
+
@plurk_id_hash.delete(plurk_id)
|
475
|
+
@mutex.unlock
|
476
|
+
final(tjplurker, data)
|
477
|
+
}
|
478
|
+
else
|
479
|
+
@mutex.lock
|
480
|
+
if data.type=='new_response' && @plurk_id_hash.has_key?(data.plurk_id)
|
481
|
+
@plurk_id_hash[data.plurk_id][:last_time] = Time.now.to_i
|
482
|
+
action(tjplurker, data)
|
483
|
+
end
|
484
|
+
@mutex.unlock
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module TJP
|
2
|
+
class Robot
|
3
|
+
# :call-seq: new(tjplurker)
|
4
|
+
# new(key, screte)
|
5
|
+
# new(key, secret, access_token, access_token_screte)
|
6
|
+
# key:: consumer key
|
7
|
+
# secret:: consumer secret
|
8
|
+
attr_reader :tjplurker
|
9
|
+
def initialize *params
|
10
|
+
case params.size
|
11
|
+
when 1
|
12
|
+
@tjplurker = params[0]
|
13
|
+
when 2
|
14
|
+
key, screte = params
|
15
|
+
@tjplurker = TJPlurker.new(key, screte)
|
16
|
+
when 4
|
17
|
+
key, secret, access_token, access_token_screte = params
|
18
|
+
@tjplurker = TJPlurker.new(key, secret, access_token, access_token_screte)
|
19
|
+
end
|
20
|
+
@services = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Starting a loop and invokes services when a notification was listened.
|
24
|
+
def start
|
25
|
+
$tjpLog.info(self.class){"Start robot."}
|
26
|
+
Thread.new{
|
27
|
+
loop{
|
28
|
+
@tjplurker.add_all_as_friends
|
29
|
+
sleep(60)
|
30
|
+
}
|
31
|
+
}
|
32
|
+
loop{
|
33
|
+
@tjplurker.comet_channel{|data|
|
34
|
+
@services.each{|service|
|
35
|
+
begin
|
36
|
+
service.serve(@tjplurker, data)
|
37
|
+
rescue => ex
|
38
|
+
$tjpLog.error(self.class){"An error occured in #{service.class}, class: #{ex.class}, message: #{ex.message}, backtrace: #{ex.backtrace}"}
|
39
|
+
end
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# service:: Can be instance TJPlurker::Service or class TJPlurker::Service or it's descendant classes.
|
46
|
+
def add_service service
|
47
|
+
if service.is_a? Service
|
48
|
+
@services << service
|
49
|
+
$tjpLog.info(self.class){"Add service: #{service.name}"}
|
50
|
+
elsif service <= Service
|
51
|
+
tmp = service.new
|
52
|
+
@services << tmp
|
53
|
+
$tjpLog.info(self.class){"Add service: #{tmp.name}"}
|
54
|
+
else
|
55
|
+
$tjpLog.warn(self.class){"Unrecognized service, ignored."}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Service
|
61
|
+
attr_reader :name
|
62
|
+
def initialize name=self.class.to_s, &proc
|
63
|
+
@name = name
|
64
|
+
if block_given?
|
65
|
+
@serve_proc = proc
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Invoke when listened a new plurk.
|
70
|
+
# data:: notification data
|
71
|
+
def decide tjplurker, data
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
# Invoke when decide return true
|
76
|
+
# data:: notification data
|
77
|
+
def action tjplurker, data
|
78
|
+
if @serve_proc
|
79
|
+
@serve_proc[tjplurker, data]
|
80
|
+
else
|
81
|
+
p data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# It has higher priority than decide and action
|
86
|
+
# data:: notification data
|
87
|
+
def serve tjplurker, data
|
88
|
+
action(tjplurker, data) if decide(tjplurker, data)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class ThreadService < Service
|
93
|
+
# lasting:: How long in second will a thread lives. If one plurk just be responsed,
|
94
|
+
# then it will live another +lasting+ time.
|
95
|
+
# refresh:: Interval that checks if thread should terminate.
|
96
|
+
def initialize name=self.class.to_s, lasting=60, refresh=1, &proc
|
97
|
+
super name
|
98
|
+
@lasting = lasting
|
99
|
+
@refresh = refresh
|
100
|
+
@plurk_id_hash = Hash.new
|
101
|
+
@mutex = Mutex.new
|
102
|
+
end
|
103
|
+
|
104
|
+
# Invoke after thread termination.
|
105
|
+
# You can use this to tell users this service just terminate.
|
106
|
+
# data:: notification data
|
107
|
+
def final tjplurker, data
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
def serve tjplurker, data
|
112
|
+
if data.type=='new_plurk' && decide(tjplurker, data)
|
113
|
+
@plurk_id_hash[data.plurk_id] = Thread.new(data.plurk_id){|plurk_id|
|
114
|
+
$tjpLog.debug(self.class){"Create thread in plurk ##{plurk_id}"}
|
115
|
+
Thread.current[:last_time] = Time.now.to_i
|
116
|
+
loop{
|
117
|
+
sleep(@refresh)
|
118
|
+
$tjpLog.debug(self.class){"Plurk ##{plurk_id} remain #{Thread.current[:last_time]+@lasting-Time.now.to_i} seconds."}
|
119
|
+
@mutex.lock
|
120
|
+
if Time.now.to_i - Thread.current[:last_time] > @lasting
|
121
|
+
@mutex.unlock
|
122
|
+
break
|
123
|
+
end
|
124
|
+
@mutex.unlock
|
125
|
+
}
|
126
|
+
$tjpLog.debug(self.class){"Terminate thread in plurk ##{plurk_id}"}
|
127
|
+
@mutex.lock
|
128
|
+
@plurk_id_hash.delete(plurk_id)
|
129
|
+
@mutex.unlock
|
130
|
+
final(tjplurker, data)
|
131
|
+
}
|
132
|
+
else
|
133
|
+
@mutex.lock
|
134
|
+
if data.type=='new_response' && @plurk_id_hash.has_key?(data.plurk_id)
|
135
|
+
@plurk_id_hash[data.plurk_id][:last_time] = Time.now.to_i
|
136
|
+
action(tjplurker, data)
|
137
|
+
end
|
138
|
+
@mutex.unlock
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tjplurker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: '1.2'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-07-
|
12
|
+
date: 2011-07-15 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: json
|
16
|
-
requirement: &
|
16
|
+
requirement: &18195672 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,7 +21,18 @@ dependencies:
|
|
21
21
|
version: 1.4.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *18195672
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: oauth
|
27
|
+
requirement: &18195384 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.4.5
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *18195384
|
25
36
|
description: A safe Plurk API wrapper based on Ruby and mainly designed for easily
|
26
37
|
making plurk robot.
|
27
38
|
email: tonytonyjan.cs97@g2.nctu.edu.tw
|
@@ -30,6 +41,9 @@ extensions: []
|
|
30
41
|
extra_rdoc_files: []
|
31
42
|
files:
|
32
43
|
- lib/tjplurker.rb
|
44
|
+
- lib/tjplurker/core.rb
|
45
|
+
- lib/tjplurker/legacy.rb
|
46
|
+
- lib/tjplurker/robot.rb
|
33
47
|
homepage: http://code.google.com/p/tjplurker
|
34
48
|
licenses: []
|
35
49
|
post_install_message:
|