nadoka 0.8.3 → 0.8.4

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/.gitignore CHANGED
@@ -3,3 +3,4 @@
3
3
  Gemfile.lock
4
4
  log/
5
5
  pkg/*
6
+ nadoka_fatal_error
@@ -353,11 +353,13 @@ module Nadoka
353
353
  context.cert = OpenSSL::X509::Certificate.new(File.read(@config.client_server_ssl_cert_file))
354
354
  context.key = OpenSSL::PKey::RSA.new(File.read(@config.client_server_ssl_key_file))
355
355
  @cserver = OpenSSL::SSL::SSLServer.new(@cserver, context)
356
+ @cserver.start_immediately = false
356
357
  end
357
358
 
358
359
  while true
359
360
  # wait for client connections
360
361
  Thread.start(@cserver.accept){|cc|
362
+ cc.accept if OpenSSL::SSL::SSLSocket === cc rescue NameError
361
363
  client = nil
362
364
  begin
363
365
  if !@config.acl_object || @config.acl_object.allow_socket?(cc)
@@ -11,7 +11,7 @@
11
11
  #
12
12
 
13
13
  module Nadoka
14
- VERSION = '0.8.3'
14
+ VERSION = '0.8.4'
15
15
  NDK_Version = VERSION.dup
16
16
  NDK_Created = Time.now
17
17
 
@@ -0,0 +1,141 @@
1
+ # -*-ruby; coding: utf-8 -*- vim:set ft=ruby:
2
+ # Copyright (C) 2013 Kazuhiro NISHIYAMA
3
+ #
4
+ # This program is free software with ABSOLUTELY NO WARRANTY.
5
+ # You can re-distribute and/or modify this program under
6
+ # the same terms of the Ruby's license.
7
+ #
8
+
9
+ =begin
10
+
11
+ == Configuration:
12
+
13
+ BotConfig << {
14
+ :name => :GithubIssuesBot,
15
+ :bot_name => 'gh',
16
+ :ch => '#nadoka_check',
17
+ :tm => 30, # min
18
+ #:nkf => "--oc=CP50221 --ic=UTF-8 --fb-xml",
19
+ :owner => "nadoka",
20
+ :repo => "nadoka",
21
+ }
22
+
23
+ =end
24
+ require 'open-uri'
25
+ require 'time'
26
+ begin
27
+ require 'json'
28
+ rescue LoadError
29
+ require 'rubygems'
30
+ require 'json'
31
+ end
32
+
33
+ module GithubIssues
34
+ module_function
35
+
36
+ def issues(owner, repo, since=nil, state='open')
37
+ since = since.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
38
+ uri = URI("https://api.github.com/repos/#{owner}/#{repo}/issues?since=#{since}&state=#{state}")
39
+ issues = JSON.parse(uri.read)
40
+ issues.each do |issue|
41
+ comments_uri = URI("#{issue['comments_url']}?since=#{since}")
42
+ comments = JSON.parse(comments_uri.read)
43
+ if comments.empty?
44
+ yield [:issue, issue, issue_to_s(issue)]
45
+ else
46
+ comments.each do |comment|
47
+ yield [:comment, comment, comment_to_s(comment, issue)]
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def user_to_s(user)
54
+ return unless user
55
+ "@#{user['login']} "
56
+ end
57
+
58
+ def time_to_s(time)
59
+ return unless time
60
+ time = Time.parse(time)
61
+ time.localtime.strftime("%H:%M ")
62
+ end
63
+
64
+ def issue_to_s(issue)
65
+ return unless issue
66
+ "#{issue['html_url']} [#{issue['state']}] #{time_to_s(issue['updated_at'])}#{user_to_s(issue['user'])}#{issue['title']}"
67
+ end
68
+
69
+ def comment_to_s(comment, issue=nil)
70
+ return unless comment
71
+ if issue
72
+ info = "[#{issue['state']}] "
73
+ else
74
+ end
75
+ "#{comment['html_url']} #{info}#{time_to_s(comment['updated_at'])}#{user_to_s(comment['user'])}#{comment['body']}"
76
+ end
77
+ end
78
+
79
+ if __FILE__ == $0
80
+ owner = "rubima"
81
+ repo = "rubima"
82
+ since = Time.now - 60*60*3
83
+ GithubIssues.issues(owner, repo, since) do |_, _, s|
84
+ puts s.gsub(/\s+/, ' ')
85
+ end
86
+ GithubIssues.issues(owner, repo, since, 'close') do |_, _, s|
87
+ puts s.gsub(/\s+/, ' ')
88
+ end
89
+ exit
90
+ end
91
+
92
+ require 'nkf'
93
+
94
+ class GithubIssuesBot < Nadoka::NDK_Bot
95
+ def bot_initialize
96
+ @ch = @bot_config.fetch(:ch, '#nadoka_check')
97
+ @tm = @bot_config.fetch(:tm, 30) # min
98
+ @prevtm = Time.now
99
+ @nkf_options = @bot_config.fetch(:nkf, "--oc=CP50221 --ic=UTF-8 --fb-xml")
100
+ @owner = @bot_config.fetch(:owner, "nadoka")
101
+ @repo = @bot_config.fetch(:repo, "nadoka")
102
+ end
103
+
104
+ def bot_state
105
+ nt = Time.at(@prevtm.to_i + @tm * 60)
106
+ "<#{self.class}: next check at #{nt.asctime}@#{@ch}>"
107
+ end
108
+
109
+ def send_notice(ch, msg)
110
+ msg = msg.gsub(/\s+/, ' ')
111
+ if @nkf_options
112
+ msg = NKF.nkf(@nkf_options, msg)
113
+ end
114
+ super(ch, msg)
115
+ end
116
+
117
+ def on_timer tm
118
+ check
119
+ end
120
+
121
+ def check
122
+ tm = Time.now
123
+ if tm.to_i - @tm * 60 > @prevtm.to_i
124
+ make_notice tm
125
+ end
126
+ end
127
+
128
+ def make_notice tm
129
+ since = @prevtm
130
+ @prevtm = tm
131
+ GithubIssues.issues(@owner, @repo, since) do |_, _, s|
132
+ send_notice @ch, s
133
+ end
134
+ GithubIssues.issues(@owner, @repo, since, 'close') do |_, _, s|
135
+ send_notice @ch, s
136
+ end
137
+ rescue Exception => e
138
+ send_notice(@ch, "rss bot error: #{e}")
139
+ @manager.ndk_error e
140
+ end
141
+ end
@@ -182,7 +182,16 @@ class GoogleBot < Nadoka::NDK_Bot
182
182
  result.gsub!(/<sup>(.+?)<\/sup>/u) { "^(#{$1})" }
183
183
  result.gsub!(/<.+?>/u, '')
184
184
  result.gsub!(/&\#215;/u, "\303\227")
185
- result
185
+ return result
186
+ elsif /<[^<>]+ id="cwos"[^<>]*>([^<>]+)</u =~ html
187
+ result = $1
188
+ if /<[^<>]+ id="cwles"[^<>]*>([^<>]+)</u =~ html
189
+ result = "#{$1}#{result}"
190
+ end
191
+ #@logger.slog("google_calc>#{result.dump}")
192
+ result.gsub!(/&nbsp;/u, " ")
193
+ result.gsub!(/\s+/, " ")
194
+ return result
186
195
  else
187
196
  "response error"
188
197
  end
@@ -13,18 +13,15 @@
13
13
 
14
14
  Answer weather information using "Livedoor Weather Web Service / LWWS".
15
15
 
16
- LWWS: http://weather.livedoor.com/weather_hacks/webservice.html
16
+ LWWS: http://weather.livedoor.com/weather_hacks/webservice
17
17
 
18
18
 
19
19
  == Usage
20
20
 
21
21
  tenki> [CITY]
22
- tenki:[today|tomorrow|dayaftertomorrow]> [CITY]
23
22
 
24
23
  [CITY] should be city name in Kanji listed on following table.
25
- http://weather.livedoor.com/forecast/rss/forecastmap.xml
26
-
27
- If timing is not specified, show today's information.
24
+ http://weather.livedoor.com/forecast/rss/primary_area.xml
28
25
 
29
26
 
30
27
  == Configuration
@@ -32,7 +29,7 @@ LWWS: http://weather.livedoor.com/weather_hacks/webservice.html
32
29
  BotConfig = [
33
30
  {
34
31
  :name => :TenkiBot,
35
- :ch => /nadoka/, # default: /.*/
32
+ :ch => /nadoka/, # default: //
36
33
  }
37
34
  ]
38
35
 
@@ -42,62 +39,65 @@ BotConfig = [
42
39
  require 'open-uri'
43
40
  require 'pp'
44
41
  require 'kconv'
45
- require 'rexml/document'
46
42
  require 'date'
43
+ begin
44
+ require 'json'
45
+ rescue LoadError
46
+ require 'rubygems'
47
+ require 'json'
48
+ end
47
49
 
48
50
  module Tenki
49
51
  CityIDs = {}
50
52
 
51
53
  def init_tenki
52
- open('http://weather.livedoor.com/forecast/rss/forecastmap.xml'){|f|
53
- f.each{|line|
54
+ open('http://weather.livedoor.com/forecast/rss/primary_area.xml') do |f|
55
+ f.each_line do |line|
54
56
  if /city title="(.+?)" id="(\d+)"/ =~ line
55
- CityIDs[$1.toutf8] = $2.to_i
57
+ CityIDs[$1.toutf8] = $2
56
58
  end
57
- }
58
- }
59
+ end
60
+ end
59
61
  end
60
62
 
61
- def tenki city, timing
62
- doc = open(
63
- "http://weather.livedoor.com/forecast/webservice/rest/v1?" \
64
- "city=#{CityIDs.fetch(city)}&day=#{timing}"){|f|
65
- REXML::Document.new f.read
66
- }
67
-
68
- title = doc.elements['/lwws/title/'].text.toutf8
69
- telop = doc.elements['/lwws/telop/'].text.toutf8
70
- link = doc.elements['/lwws/link/'].text.toutf8
71
- desc = doc.elements['/lwws/description/'].text.toutf8
72
- max = doc.elements['/lwws/temperature/max/celsius/'].text
73
- min = doc.elements['/lwws/temperature/min/celsius/'].text
74
- date = Date.parse(doc.elements['/lwws/forecastdate/'].text)
75
- datestr = date.strftime('%m/%d')
76
-
77
- desc.sub!(/\.\.\..*/m, '...')
78
-
79
- celsius = []
80
- celsius << "max: #{max}" if max
81
- celsius << "min: #{min}" if min
82
- unless celsius.empty?
83
- celsius = "(#{celsius.join(', ')}) "
63
+ def tenki(city)
64
+ unless city_id = CityIDs[city]
65
+ return "Unknown city. Check city title on http://weather.livedoor.com/forecast/rss/primary_area.xml"
84
66
  end
85
- "#{title} (#{datestr}): #{telop} - #{celsius}#{desc} - #{link}"
67
+ json = open("http://weather.livedoor.com/forecast/webservice/json/v1?city=#{city_id}") do |f|
68
+ JSON.parse f.read
69
+ end
70
+
71
+ tenki = "#{json['title']}: "
72
+ tenki << json['forecasts'].map do |forecast|
73
+ max = forecast['temperature']['max']
74
+ min = forecast['temperature']['min']
75
+ celsius = []
76
+ celsius << "min:#{min['celsius']}" if min
77
+ celsius << "max:#{max['celsius']}" if max
78
+ unless celsius.empty?
79
+ temperature = "(#{celsius.join(',')})"
80
+ end
81
+ "#{forecast['dateLabel']}:#{forecast['telop']}#{temperature}"
82
+ end.join(', ')
83
+ desc = json['description']
84
+ text, = desc['text'].split(/\n\n/, 2)
85
+ text.gsub!(/\n/, '')
86
+ tenki << " - #{text}(#{desc['publicTime']})"
87
+ tenki << " - #{json['link']}"
88
+
89
+ tenki
86
90
  end
87
91
  end
88
92
 
89
93
  if __FILE__ == $0
90
94
  include Tenki
91
- city = ARGV.shift
92
- timing = ARGV.shift || 'today'
93
- if city.nil?
94
- puts "#$0 city [today|tomorrow|dayaftertomorrow]"
95
+ if ARGV.empty?
96
+ puts "#$0 city"
95
97
  else
96
98
  init_tenki
97
- begin
98
- puts tenki(city, timing)
99
- rescue IndexError
100
- puts "Unknown city. Check city title on http://weather.livedoor.com/forecast/rss/forecastmap.xml"
99
+ ARGV.each do |city|
100
+ puts tenki(city)
101
101
  end
102
102
  end
103
103
  exit
@@ -116,17 +116,14 @@ class TenkiBot < Nadoka::NDK_Bot
116
116
  return unless @available_channel === ch
117
117
  return if same_bot?(ch)
118
118
  msg = NKF.nkf('-w', msg)
119
- if /\Atenki(|:(today|tomorrow|dayaftertomorrow))>(.+)/ =~ msg
120
- city = $3.strip.toutf8
121
- timing = ($2 || 'today').strip
119
+ if /\Atenki>/ =~ msg
120
+ city = $'.strip.toutf8
122
121
  begin
123
- result = tenki(city, timing).gsub(/\n/, ' ')
124
- rescue IndexError
125
- result = "Unknown city. Check city title on http://weather.livedoor.com/forecast/rss/forecastmap.xml"
122
+ result = tenki(city)
126
123
  rescue => e
127
124
  result = "#{e}"
128
125
  end
129
- send_notice ch, NKF.nkf(@nkf, "tenki bot: #{result}")
126
+ send_notice ch, NKF.nkf(@nkf, "tenki bot: #{result}".gsub(/\s+/, ' '))
130
127
  end
131
128
  end
132
129
  end
@@ -1,4 +1,4 @@
1
- # -*-ruby-*-
1
+ # -*-ruby; coding: utf-8 -*-
2
2
  # vim:set ft=ruby:
3
3
  #
4
4
  # Copyright (c) 2009, 2011, 2012 Kazuhiro NISHIYAMA
@@ -33,6 +33,12 @@ require 'open-uri'
33
33
  require 'timeout'
34
34
  require 'tmpdir'
35
35
 
36
+ begin
37
+ require 'cgi/util'
38
+ rescue LoadError
39
+ require 'cgi'
40
+ end
41
+
36
42
  begin
37
43
  require 'rubygems'
38
44
  require 'nokogiri'
@@ -92,10 +98,10 @@ module URL2Title
92
98
  when /euc-jp/i # euc-jp, x-euc-jp
93
99
  charset = "eucjp-ms"
94
100
  end
95
- if /\Autf-8\z/i =~ charset
101
+ if /\A(?:utf-8|eucjp-ms)\z/i =~ charset
96
102
  # avoid #<ArgumentError: invalid byte sequence in UTF-8>
97
103
  # or Iconv::IllegalSequence
98
- body = NKF.nkf("-Wwm0x", body)
104
+ body = NKF.nkf("-wm0x --ic=#{charset}", body)
99
105
  elsif charset
100
106
  charset.sub!(/\Ax-?/i, '')
101
107
  begin
@@ -127,20 +133,33 @@ module URL2Title
127
133
  title = tweet unless tweet.empty?
128
134
  end
129
135
  end
130
- when /\A(?:www\.so-net\.ne\.jp)\z/
136
+ else
131
137
  if %r"<title\b(?>[^<>]*)>(.*?)</title(?>[^<>]*)>"miu =~ body
132
138
  title = $1
133
139
  end
134
- if %r!<dt id="ttl">(.*?)</dt>!miu =~ body
140
+ if defined?(::Nokogiri)
141
+ doc ||= Nokogiri::HTML(body, uri.to_s, 'utf-8')
142
+ og_title = doc.xpath("//meta[@property='og:title'][1]/@content")
143
+ unless og_title.empty?
144
+ # title is escaped string when get by regexp
145
+ title = CGI.escapeHTML(og_title.text)
146
+ end
147
+ elsif /<meta property="og:title" content="(.+?)">/ =~ body
135
148
  title = $1
136
149
  end
137
- else
138
- if %r"<title\b(?>[^<>]*)>(.*?)</title(?>[^<>]*)>"miu =~ body
150
+ if defined?(::Nokogiri)
151
+ doc ||= Nokogiri::HTML(body, uri.to_s, 'utf-8')
152
+ og_title = doc.xpath("//meta[@property='og:title']/@content")
153
+ unless og_title.empty?
154
+ # title is escaped string when get by regexp
155
+ title = CGI.escapeHTML(og_title.text)
156
+ end
157
+ elsif /<meta property="og:title" content="(.+?)">/ =~ body
139
158
  title = $1
140
159
  end
141
160
  if uri.fragment && defined?(::Nokogiri)
142
161
  begin
143
- doc = Nokogiri::HTML(body, uri.to_s, 'utf-8')
162
+ doc ||= Nokogiri::HTML(body, uri.to_s, 'utf-8')
144
163
  xpath = "//*[@id='#{uri.fragment}' or @name='#{uri.fragment}']"
145
164
  fragment_element = doc.xpath(xpath)
146
165
  # tDiary style
@@ -153,6 +172,19 @@ module URL2Title
153
172
  end
154
173
  end
155
174
  end
175
+ if defined?(::Nokogiri)
176
+ doc ||= Nokogiri::HTML(body, uri.to_s, 'utf-8')
177
+ canonical_uri = doc.xpath("//link[@rel='canonical'][1]/@href")
178
+ unless canonical_uri.empty?
179
+ info[:uri] = URI(canonical_uri.text)
180
+ end
181
+ elsif /<link rel="canonical" href="(.+?)"/i =~ body
182
+ info[:uri] = URI(CGI.unescapeHTML($1))
183
+ end
184
+ if title
185
+ title = CGI.unescapeHTML(title)
186
+ title.gsub!(/\xE3\x80\x9C/u, "\xEF\xBD\x9E") # WAVE DASH -> FULLWIDTH TILDE
187
+ end
156
188
  info[:title] = title || body
157
189
  return info
158
190
  when /\Aimage\//
@@ -211,7 +243,7 @@ if __FILE__ == $0
211
243
  ARGV.each do |url|
212
244
  info = u2t(url)
213
245
  p info
214
- puts url
246
+ puts info[:uri]
215
247
  puts info[:title]
216
248
  end
217
249
  end
@@ -232,7 +264,7 @@ class TitleBot < Nadoka::NDK_Bot
232
264
  end
233
265
 
234
266
  @same_bot = @bot_config.fetch(:same_bot, /(?!)/)
235
- @nkf_options = @bot_config.fetch(:nkf, "--oc=CP50221 --ic=UTF-8 --numchar-input --fb-xml")
267
+ @nkf_options = @bot_config.fetch(:nkf, "--oc=CP50221 --ic=UTF-8 --fb-xml")
236
268
  @timeout = @bot_config.fetch(:timeout, 10)
237
269
  @fragment_size_range = @bot_config.fetch(:fragment_size_range, 5..100)
238
270
  @headers = @bot_config.fetch(:headers, {})
@@ -95,39 +95,50 @@ class TwitterBot < Nadoka::NDK_Bot
95
95
  end
96
96
 
97
97
  @streamer = Thread.new do
98
- UserStream.client.user do |status|
99
- case # https://dev.twitter.com/docs/streaming-apis/messages
100
- when status[:delete]
101
- when status[:scrub_geo]
102
- when status[:limit]
103
- when status[:status_withheld]
104
- when status[:user_withheld]
105
- when status[:friends]
106
- when status[:event]
107
- when status[:for_user]
108
- when status[:control]
109
- when status[:warning]
110
- screen_name = status[:code]
111
- time = Time.parse(status[:created_at])
112
- send_notice @ch, "#{time.strftime('%H:%M')} #{screen_name}: #{status.text}"
113
- else
98
+ loop do
99
+ UserStream.client.user do |status|
114
100
  begin
115
- screen_name = status[:user][:screen_name]
116
- if status[:retweeted_status]
117
- status = status[:retweeted_status]
118
- screen_name << ":"
119
- screen_name << status[:user][:screen_name]
101
+ case # https://dev.twitter.com/docs/streaming-apis/messages
102
+ #when status[:delete]
103
+ #when status[:scrub_geo]
104
+ when status[:limit]
105
+ when status[:status_withheld]
106
+ when status[:user_withheld]
107
+ when status[:friends]
108
+ when status[:event]
109
+ when status[:for_user]
110
+ when status[:control]
111
+ when status[:warning]
112
+ screen_name = status[:code]
113
+ time = Time.parse(status[:created_at])
114
+ send_notice @ch, "#{time.strftime('%H:%M')} #{screen_name}: #{status.text}"
115
+ when status[:user]
116
+ screen_name = status[:user][:screen_name]
117
+ if status[:retweeted_status]
118
+ status = status[:retweeted_status]
119
+ screen_name << ":"
120
+ screen_name << status[:user][:screen_name]
121
+ end
122
+ time = Time.parse(status[:created_at])
123
+ text = status.text
124
+ text.tr!("\r\n", ' ')
125
+ text.gsub!(/&lt;/, '<')
126
+ text.gsub!(/&gt;/, '>')
127
+ text.gsub!(/&quot;/, '"')
128
+ text = NKF.nkf('--numchar-input --ic=UTF-8 --oc=' + @nkf_encoding, text) if @nkf_encoding
129
+ text.gsub!(/&amp;/, '&')
130
+ send_notice @ch, "#{time.strftime('%H:%M')} #{screen_name}: #{text}"
131
+ else
132
+ send_notice @ch, status.inspect
120
133
  end
121
- time = Time.parse(status[:created_at])
122
- text = status.text.tr("\r\n", ' ')
123
- text = NKF.nkf('--ic=UTF-8 --oc=' + @nkf_encoding, text) if @nkf_encoding
124
- send_notice @ch, "#{time.strftime('%H:%M')} #{screen_name}: #{text}"
125
134
  rescue => e
135
+ @logger.slog e.inspect
126
136
  slog e.backtrace
127
137
  slog e.inspect
128
138
  slog status.inspect
129
139
  end
130
140
  end
141
+ @logger.slog "user stream finished...and restart"
131
142
  end
132
143
  end
133
144
  end
@@ -300,7 +300,7 @@ module RICE
300
300
 
301
301
  rescue Closed
302
302
  begin
303
- @main_th.run if @main_th.alive?
303
+ @main_th.run if alive?
304
304
  rescue Closed
305
305
  end
306
306
  retry
@@ -317,7 +317,7 @@ module RICE
317
317
  def close(restart = false)
318
318
  begin
319
319
  unless restart
320
- @main_th.exit if @main_th.alive?
320
+ @main_th.exit if alive?
321
321
  @read_th.exit if @read_th.alive?
322
322
  end
323
323
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nadoka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.8.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-02-09 00:00:00.000000000 Z
13
+ date: 2013-04-29 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: Nadoka is a tool for monitoring and logging IRC conversations and responding
16
16
  to specially formatted requests. You define and customize these responses in Ruby.
@@ -52,6 +52,7 @@ files:
52
52
  - plugins/drbcl.rb
53
53
  - plugins/drbot.nb
54
54
  - plugins/evalbot.nb
55
+ - plugins/githubissuesbot.nb
55
56
  - plugins/gonzuibot.nb
56
57
  - plugins/googlebot.nb
57
58
  - plugins/identifynickserv.nb