net-irc2 0.0.10

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/examples/gmail.rb ADDED
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:encoding=UTF-8:
3
+
4
+ $LOAD_PATH << "lib"
5
+ $LOAD_PATH << "../lib"
6
+
7
+ $KCODE = "u" unless defined? ::Encoding
8
+
9
+ require "rubygems"
10
+ require "net/irc"
11
+ require "sdbm"
12
+ require "tmpdir"
13
+ require "uri"
14
+ require "mechanize"
15
+ require "rexml/document"
16
+
17
+ class GmailNotifier < Net::IRC::Server::Session
18
+ def server_name
19
+ "gmail"
20
+ end
21
+
22
+ def server_version
23
+ "0.0.0"
24
+ end
25
+
26
+ def main_channel
27
+ "#gmail"
28
+ end
29
+
30
+ def initialize(*args)
31
+ super
32
+ @agent = WWW::Mechanize.new
33
+ end
34
+
35
+ def on_user(m)
36
+ super
37
+ post @prefix, JOIN, main_channel
38
+ post server_name, MODE, main_channel, "+o", @prefix.nick
39
+
40
+ @real, *@opts = @opts.name || @real.split(/\s+/)
41
+ @opts ||= []
42
+
43
+ start_observer
44
+ end
45
+
46
+ def on_disconnected
47
+ @observer.kill rescue nil
48
+ end
49
+
50
+ def on_privmsg(m)
51
+ super
52
+ case m[1]
53
+ when 'list'
54
+ check_mail
55
+ end
56
+ end
57
+
58
+ def on_ctcp(target, message)
59
+ end
60
+
61
+ def on_whois(m)
62
+ end
63
+
64
+ def on_who(m)
65
+ end
66
+
67
+ def on_join(m)
68
+ end
69
+
70
+ def on_part(m)
71
+ end
72
+
73
+ private
74
+ def start_observer
75
+ @observer = Thread.start do
76
+ Thread.abort_on_exception = true
77
+ loop do
78
+ begin
79
+ @agent.auth(@real, @pass)
80
+ page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom"))
81
+ feed = REXML::Document.new page.body
82
+ db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666)
83
+ feed.get_elements('/feed/entry').reverse.each do |item|
84
+ id = item.text('id')
85
+ if db.include?(id)
86
+ next
87
+ else
88
+ db[id] = "1"
89
+ end
90
+ post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}"
91
+ post server_name, PRIVMSG, main_channel, "#{item.text('summary')}"
92
+ end
93
+ rescue Exception => e
94
+ @log.error e.inspect
95
+ ensure
96
+ db.close rescue nil
97
+ end
98
+ sleep 60 * 5
99
+ end
100
+ end
101
+ end
102
+
103
+ def check_mail
104
+ begin
105
+ @agent.auth(@real, @pass)
106
+ page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom"))
107
+ feed = REXML::Document.new page.body
108
+ db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666)
109
+ feed.get_elements('/feed/entry').reverse.each do |item|
110
+ id = item.text('id')
111
+ if db.include?(id)
112
+ #next
113
+ else
114
+ db[id] = "1"
115
+ end
116
+ post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}"
117
+ post server_name, PRIVMSG, main_channel, "#{item.text('summary')}"
118
+ end
119
+ rescue Exception => e
120
+ @log.error e.inspect
121
+ ensure
122
+ db.close rescue nil
123
+ end
124
+ end
125
+ end
126
+
127
+ if __FILE__ == $0
128
+ require "optparse"
129
+
130
+ opts = {
131
+ :port => 16800,
132
+ :host => "localhost",
133
+ :log => nil,
134
+ :debug => false,
135
+ :foreground => false,
136
+ }
137
+
138
+ OptionParser.new do |parser|
139
+ parser.instance_eval do
140
+ self.banner = <<-EOB.gsub(/^\t+/, "")
141
+ Usage: #{$0} [opts]
142
+
143
+ EOB
144
+
145
+ separator ""
146
+
147
+ separator "Options:"
148
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
149
+ opts[:port] = port
150
+ end
151
+
152
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
153
+ opts[:host] = host
154
+ end
155
+
156
+ on("-l", "--log LOG", "log file") do |log|
157
+ opts[:log] = log
158
+ end
159
+
160
+ on("--debug", "Enable debug mode") do |debug|
161
+ opts[:log] = $stdout
162
+ opts[:debug] = true
163
+ end
164
+
165
+ on("-f", "--foreground", "run foreground") do |foreground|
166
+ opts[:log] = $stdout
167
+ opts[:foreground] = true
168
+ end
169
+
170
+ parse!(ARGV)
171
+ end
172
+ end
173
+
174
+ opts[:logger] = Logger.new(opts[:log], "daily")
175
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
176
+
177
+ def daemonize(foreground=false)
178
+ trap("SIGINT") { exit! 0 }
179
+ trap("SIGTERM") { exit! 0 }
180
+ trap("SIGHUP") { exit! 0 }
181
+ return yield if $DEBUG || foreground
182
+ Process.fork do
183
+ Process.setsid
184
+ Dir.chdir "/"
185
+ File.open("/dev/null") {|f|
186
+ STDIN.reopen f
187
+ STDOUT.reopen f
188
+ STDERR.reopen f
189
+ }
190
+ yield
191
+ end
192
+ exit! 0
193
+ end
194
+
195
+ daemonize(opts[:debug] || opts[:foreground]) do
196
+ Net::IRC::Server.new(opts[:host], opts[:port], GmailNotifier, opts).start
197
+ end
198
+ end
199
+
200
+ # Local Variables:
201
+ # coding: utf-8
202
+ # End:
data/examples/gtig.rb ADDED
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env ruby
2
+ =begin
3
+
4
+ タスク:
5
+ 10:30 21:00 にタスクを表示
6
+
7
+ カレンダー:
8
+ 予定の10分前に予定を表示
9
+ 07:30 今日のタスクを表示
10
+ 23:30 明日のタスクを表示
11
+
12
+ =end
13
+
14
+ $LOAD_PATH << "lib"
15
+ $LOAD_PATH << "../lib"
16
+
17
+ $KCODE = "u" unless defined? ::Encoding
18
+
19
+ require "pp"
20
+ require "rubygems"
21
+ require "net/irc"
22
+ require "logger"
23
+ require "pathname"
24
+ require "yaml"
25
+ require 'pit'
26
+ require 'google/api_client'
27
+
28
+ class GoogleTasksIrcGateway < Net::IRC::Server::Session
29
+ CONFIG = Pit.get('google tasks', :require => {
30
+ 'CLIENT_ID' => 'OAuth Client ID',
31
+ 'CLIENT_SECRET' => 'OAuth Client Secret',
32
+ })
33
+
34
+ CONFIG_DIR = Pathname.new("~/.gtig.rb").expand_path
35
+
36
+ @@ctcp_action_commands = []
37
+
38
+ class << self
39
+ def ctcp_action(*commands, &block)
40
+ name = "+ctcp_action_#{commands.inspect}"
41
+ define_method(name, block)
42
+ commands.each do |command|
43
+ @@ctcp_action_commands << [command, name]
44
+ end
45
+ end
46
+ end
47
+
48
+ def server_name
49
+ "tasks"
50
+ end
51
+
52
+ def server_version
53
+ "0.0.0"
54
+ end
55
+
56
+ def main_channel
57
+ "#tasks"
58
+ end
59
+
60
+ def config(&block)
61
+ # merge local (user) config and global config
62
+ merged = {}
63
+ global = {}
64
+ local = {}
65
+
66
+ global_config = CONFIG_DIR + "config"
67
+ begin
68
+ global = eval(global_config.read) || {}
69
+ rescue Errno::ENOENT
70
+ end
71
+
72
+ local_config = @real ? CONFIG_DIR + "#{@real}/config" : nil
73
+ if local_config
74
+ begin
75
+ local = eval(local_config.read) || {}
76
+ rescue Errno::ENOENT
77
+ end
78
+ end
79
+
80
+ merged.update(global)
81
+ merged.update(local)
82
+
83
+ if block
84
+ merged.instance_eval(&block)
85
+ merged.each do |k, v|
86
+ unless global[k] == v
87
+ local[k] = v
88
+ end
89
+ end
90
+
91
+ if local_config
92
+ local_config.parent.mkpath
93
+ local_config.open('w') do |f|
94
+ PP.pp(local, f)
95
+ end
96
+ end
97
+ end
98
+
99
+ merged
100
+ end
101
+
102
+ COLORS = {
103
+ "navy" => 2,
104
+ "aqua" => 11,
105
+ "teal" => 10,
106
+ "blue" => 2,
107
+ "olive" => 7,
108
+ "purple" => 6,
109
+ "lightcyan" => 11,
110
+ "grey" => 14,
111
+ "royal" => 12,
112
+ "white" => 0,
113
+ "red" => 4,
114
+ "orange" => 7,
115
+ "lightpurple" => 13,
116
+ "pink" => 13,
117
+ "yellow" => 8,
118
+ "black" => 1,
119
+ "cyan" => 11,
120
+ "maroon" => 5,
121
+ "silver" => 15,
122
+ "lime" => 9,
123
+ "lightgreen" => 9,
124
+ "fuchsia" => 13,
125
+ "lightblue" => 12,
126
+ "lightgrey" => 15,
127
+ "green" => 3,
128
+ "brown" => 5
129
+ }
130
+
131
+ def color(color, string)
132
+ "\003%.2d%s\017" % [COLORS[color.to_s], string]
133
+ end
134
+
135
+ def initialize(*args)
136
+ super
137
+ @channels = {}
138
+ end
139
+
140
+ def on_disconnected
141
+ end
142
+
143
+ def on_user(m)
144
+ super
145
+ @real, *@opts = @real.split(/\s+/)
146
+ @opts ||= []
147
+ post @prefix, JOIN, main_channel
148
+ init_channel(main_channel)
149
+ end
150
+
151
+ def on_join(m)
152
+ channels = m.params[0].split(/ *, */)
153
+ channels.each do |channel|
154
+ channel = channel.split(" ", 2).first
155
+ init_channel(channel)
156
+ end
157
+ end
158
+
159
+ def on_part(m)
160
+ channel = m.params[0]
161
+ destroy_channel(channel)
162
+ end
163
+
164
+ def on_privmsg(m)
165
+ target, mesg = *m.params
166
+ m.ctcps.each {|ctcp| on_ctcp(target, ctcp) } if m.ctcp?
167
+ return if mesg.empty?
168
+ return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "")
169
+ end
170
+
171
+ def on_ctcp(target, mesg)
172
+ type, mesg = mesg.split(" ", 2)
173
+ method = "on_ctcp_#{type.downcase}".to_sym
174
+ send(method, target, mesg) if respond_to? method, true
175
+ end
176
+
177
+ def on_ctcp_action(target, mesg)
178
+ command, *args = mesg.split(" ")
179
+ if command
180
+ command.downcase!
181
+
182
+ @@ctcp_action_commands.each do |define, name|
183
+ if define === command
184
+ send(name, target, mesg, Regexp.last_match || command, args)
185
+ break
186
+ end
187
+ end
188
+ else
189
+ commands = @@ctcp_action_commands.map {|define, name|
190
+ define
191
+ }.select {|define|
192
+ define.is_a? String
193
+ }
194
+
195
+ commands.each_slice(5) do |c|
196
+ post server_name, NOTICE, target, c.join(" ")
197
+ end
198
+ end
199
+
200
+ rescue Exception => e
201
+ post server_name, NOTICE, target, e.inspect
202
+ e.backtrace.each do |l|
203
+ @log.error "\t#{l}"
204
+ end
205
+ end
206
+
207
+ ctcp_action "oauth" do |target, mesg, command, args|
208
+ if args.length == 1
209
+ auth_channel(target, args.first)
210
+ else
211
+ init_channel(target)
212
+ end
213
+ end
214
+
215
+ def init_channel(channel)
216
+ destroy_channel(channel) if @channels[channel]
217
+
218
+ client = Google::APIClient.new
219
+ client.authorization.update_token!(Marshal.load(config["token_#{channel}"])) if config["token_#{channel}"]
220
+ client.authorization.client_id = CONFIG['CLIENT_ID']
221
+ client.authorization.client_secret = CONFIG['CLIENT_SECRET']
222
+ client.authorization.scope = 'https://www.googleapis.com/auth/tasks.readonly https://www.googleapis.com/auth/calendar.readonly'
223
+ client.authorization.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
224
+
225
+ @channels[channel] = {
226
+ :client => client,
227
+ }
228
+
229
+ if client.authorization.access_token
230
+ auth_channel(channel)
231
+ else
232
+ post server_name, NOTICE, channel, 'Access following URL: %s' % client.authorization.authorization_uri.to_s
233
+ post server_name, NOTICE, channel, 'and send /me oauth <CODE>'
234
+ end
235
+ end
236
+
237
+ def auth_channel(channel, code=nil)
238
+ post server_name, NOTICE, channel, 'Authenticating...'
239
+ client = @channels[channel][:client]
240
+
241
+ client.authorization.code = code if code
242
+ client.authorization.fetch_access_token!
243
+
244
+ post server_name, NOTICE, channel, 'Authenticating... done'
245
+
246
+ config {
247
+ self["token_#{channel}"] = Marshal.dump({
248
+ 'refresh_token' => client.authorization.refresh_token,
249
+ 'access_token' => client.authorization.access_token,
250
+ 'expires_in' => client.authorization.expires_in
251
+ })
252
+ }
253
+
254
+ @channels[channel] = {
255
+ :client => client,
256
+ :tasks => client.discovered_api('tasks'),
257
+ :calendar => client.discovered_api('calendar', 'v3'),
258
+ }
259
+
260
+ observe_channel(channel)
261
+ end
262
+
263
+ def observe_channel(channel)
264
+ check_tasks
265
+ check_calendars
266
+
267
+ @channels[channel][:thread] = Thread.start(channel) do |channel|
268
+ loop do
269
+ @log.info :loop
270
+
271
+ now = Time.now.strftime("%H:%M")
272
+
273
+ case now
274
+ when "07:30"
275
+ check_calendars(60 * 60 * 24)
276
+ when "10:30"
277
+ check_tasks
278
+ check_calendars
279
+ when "21:00"
280
+ check_tasks
281
+ check_calendars
282
+ when "23:30"
283
+ check_calendars(60 * 60 * 24)
284
+ when /..:.0/
285
+ check_calendars
286
+ end
287
+
288
+ sleep 60
289
+ end
290
+ end
291
+ end
292
+
293
+ def destroy_channel(channel)
294
+ @channels[channel][:thread].kill rescue nil
295
+ @channels.delete(channel)
296
+ end
297
+
298
+ def check_tasks
299
+ @channels.each do |channel, info|
300
+ @log.info "check_tasks[#{channel}]"
301
+ client = info[:client]
302
+ client.authorization.fetch_access_token! if client.authorization.refresh_token && client.authorization.expired?
303
+
304
+ result = client.execute( info[:tasks].tasks.list, { 'tasklist' => '@default' })
305
+
306
+ now = Time.now
307
+ result.data.items.sort_by {|i| i.due ? -i.due.to_i : -(1/0.0) }.each do |task|
308
+ next if task.status == 'completed'
309
+ if task.due
310
+ diff = task.due - now
311
+ due = diff < 0 ? color(:red, "overdue") : color(:green, "#{(diff / (60 * 60 * 24)).floor} days")
312
+ post server_name, NOTICE, channel, "%s %s" % [due, task.title]
313
+ else
314
+ post server_name, NOTICE, channel, task.title
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ def check_calendars(range=60*10)
321
+ @channels.each do |channel, info|
322
+ @log.info "check_calendars[#{channel}]"
323
+ client = info[:client]
324
+ client.authorization.fetch_access_token! if client.authorization.refresh_token && client.authorization.expired?
325
+
326
+ result = client.execute( info[:calendar].calendar_list.list, {})
327
+ calendars = result.data.items.select {|i| i.accessRole == 'owner' }
328
+
329
+ calendars.each do |calendar|
330
+ calendar_color = COLORS.invert[ calendar.color_id.to_i % COLORS.size ]
331
+ result = client.execute( info[:calendar].events.list, {
332
+ 'calendarId' => calendar.id,
333
+ 'maxResults' => 100,
334
+ 'timeMin' => Time.now.xmlschema,
335
+ 'timeMax' => (Time.now + range).xmlschema,
336
+ 'singleEvents' => 'true',
337
+ 'orderBy' => 'startTime',
338
+ 'fields' => 'items(description,end,etag,iCalUID,id,kind,location,originalStartTime,reminders,start,status,summary,transparency,updated),nextPageToken,summary',
339
+ })
340
+ result.data.items.each do |item|
341
+ post server_name, NOTICE, channel, "%s: %s~ %s" % [ color(calendar_color, calendar.summary), item.start.date || item.start.date_time.strftime('%m/%d %H:%M'), item.summary ]
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
347
+
348
+ if __FILE__ == $0
349
+ require "optparse"
350
+
351
+ opts = {
352
+ :port => 16720,
353
+ :host => "localhost",
354
+ :log => $stdout,
355
+ :debug => true,
356
+ :foreground => true,
357
+ }
358
+
359
+ OptionParser.new do |parser|
360
+ parser.instance_eval do
361
+ self.banner = <<-EOB.gsub(/^\t+/, "")
362
+ Usage: #{$0} [opts]
363
+
364
+ EOB
365
+
366
+ separator ""
367
+
368
+ separator "Options:"
369
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
370
+ opts[:port] = port
371
+ end
372
+
373
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
374
+ opts[:host] = host
375
+ end
376
+
377
+ on("-l", "--log LOG", "log file") do |log|
378
+ opts[:log] = log
379
+ end
380
+
381
+ on("--debug", "Enable debug mode") do |debug|
382
+ opts[:log] = $stdout
383
+ opts[:debug] = true
384
+ end
385
+
386
+ on("-f", "--foreground", "run foreground") do |foreground|
387
+ opts[:log] = $stdout
388
+ opts[:foreground] = true
389
+ end
390
+
391
+ parse!(ARGV)
392
+ end
393
+ end
394
+
395
+ opts[:logger] = Logger.new(opts[:log], "daily")
396
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
397
+
398
+ def daemonize(foreground=false)
399
+ trap("SIGINT") { exit! 0 }
400
+ trap("SIGTERM") { exit! 0 }
401
+ trap("SIGHUP") { exit! 0 }
402
+ return yield if $DEBUG || foreground
403
+ Process.fork do
404
+ Process.setsid
405
+ Dir.chdir "/"
406
+ File.open("/dev/null") {|f|
407
+ STDIN.reopen f
408
+ STDOUT.reopen f
409
+ STDERR.reopen f
410
+ }
411
+ yield
412
+ end
413
+ exit! 0
414
+ end
415
+
416
+ daemonize(opts[:debug] || opts[:foreground]) do
417
+ Net::IRC::Server.new(opts[:host], opts[:port], GoogleTasksIrcGateway, opts).start
418
+ end
419
+ end
420
+