net-irc 0.0.5 → 0.0.6
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/ChangeLog +7 -0
- data/Rakefile +1 -1
- data/examples/echo_bot.rb +31 -0
- data/examples/gmail.rb +201 -0
- data/examples/hatena-star-stream.rb +1 -1
- data/examples/hig.rb +733 -0
- data/examples/iig.rb +842 -0
- data/examples/lig.rb +2 -2
- data/examples/lingr.rb +1 -0
- data/examples/mixi.rb +23 -8
- data/examples/nig.rb +1 -1
- data/examples/sig.rb +5 -4
- data/examples/tig.rb +324 -124
- data/examples/wig.rb +107 -50
- data/lib/net/irc.rb +1 -1
- data/lib/net/irc/constants.rb +1 -1
- data/lib/net/irc/message.rb +1 -1
- data/lib/net/irc/pattern.rb +2 -2
- data/spec/channel_manager_spec.rb +5 -0
- data/spec/net-irc_spec.rb +15 -29
- metadata +6 -2
data/ChangeLog
CHANGED
data/Rakefile
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
|
4
|
+
$LOAD_PATH << "lib"
|
5
|
+
$LOAD_PATH << "../lib"
|
6
|
+
|
7
|
+
require "rubygems"
|
8
|
+
require "net/irc"
|
9
|
+
|
10
|
+
require "pp"
|
11
|
+
|
12
|
+
class EchoBot < Net::IRC::Client
|
13
|
+
def initialize(*args)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_rpl_welcome(m)
|
18
|
+
post JOIN, "#bot_test"
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_privmsg(m)
|
22
|
+
post NOTICE, m[0], m[1]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
EchoBot.new("foobar", "6667", {
|
27
|
+
:nick => "foobartest",
|
28
|
+
:user => "foobartest",
|
29
|
+
:real => "foobartest",
|
30
|
+
}).start
|
31
|
+
|
data/examples/gmail.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << "lib"
|
4
|
+
$LOAD_PATH << "../lib"
|
5
|
+
|
6
|
+
$KCODE = "u" # json use this
|
7
|
+
|
8
|
+
require "rubygems"
|
9
|
+
require "net/irc"
|
10
|
+
require "sdbm"
|
11
|
+
require "tmpdir"
|
12
|
+
require "uri"
|
13
|
+
require "mechanize"
|
14
|
+
require "rexml/document"
|
15
|
+
|
16
|
+
class GmailNotifier < Net::IRC::Server::Session
|
17
|
+
def server_name
|
18
|
+
"gmail"
|
19
|
+
end
|
20
|
+
|
21
|
+
def server_version
|
22
|
+
"0.0.0"
|
23
|
+
end
|
24
|
+
|
25
|
+
def main_channel
|
26
|
+
"#gmail"
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(*args)
|
30
|
+
super
|
31
|
+
@agent = WWW::Mechanize.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_user(m)
|
35
|
+
super
|
36
|
+
post @prefix, JOIN, main_channel
|
37
|
+
post server_name, MODE, main_channel, "+o", @prefix.nick
|
38
|
+
|
39
|
+
@real, *@opts = @opts.name || @real.split(/\s+/)
|
40
|
+
@opts ||= []
|
41
|
+
|
42
|
+
start_observer
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_disconnected
|
46
|
+
@observer.kill rescue nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_privmsg(m)
|
50
|
+
super
|
51
|
+
case m[1]
|
52
|
+
when 'list'
|
53
|
+
check_mail
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_ctcp(target, message)
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_whois(m)
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_who(m)
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_join(m)
|
67
|
+
end
|
68
|
+
|
69
|
+
def on_part(m)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def start_observer
|
74
|
+
@observer = Thread.start do
|
75
|
+
Thread.abort_on_exception = true
|
76
|
+
loop do
|
77
|
+
begin
|
78
|
+
@agent.auth(@real, @pass)
|
79
|
+
page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom"))
|
80
|
+
feed = REXML::Document.new page.body
|
81
|
+
db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666)
|
82
|
+
feed.get_elements('/feed/entry').reverse.each do |item|
|
83
|
+
id = item.text('id')
|
84
|
+
if db.include?(id)
|
85
|
+
next
|
86
|
+
else
|
87
|
+
db[id] = "1"
|
88
|
+
end
|
89
|
+
post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}"
|
90
|
+
post server_name, PRIVMSG, main_channel, "#{item.text('summary')}"
|
91
|
+
end
|
92
|
+
rescue Exception => e
|
93
|
+
@log.error e.inspect
|
94
|
+
ensure
|
95
|
+
db.close rescue nil
|
96
|
+
end
|
97
|
+
sleep 60 * 5
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def check_mail
|
103
|
+
begin
|
104
|
+
@agent.auth(@real, @pass)
|
105
|
+
page = @agent.get(URI.parse("https://gmail.google.com/gmail/feed/atom"))
|
106
|
+
feed = REXML::Document.new page.body
|
107
|
+
db = SDBM.open("#{Dir.tmpdir}/#{@real}.db", 0666)
|
108
|
+
feed.get_elements('/feed/entry').reverse.each do |item|
|
109
|
+
id = item.text('id')
|
110
|
+
if db.include?(id)
|
111
|
+
#next
|
112
|
+
else
|
113
|
+
db[id] = "1"
|
114
|
+
end
|
115
|
+
post server_name, PRIVMSG, main_channel, "Subject: #{item.text('title')} From: #{item.text('author/name')}"
|
116
|
+
post server_name, PRIVMSG, main_channel, "#{item.text('summary')}"
|
117
|
+
end
|
118
|
+
rescue Exception => e
|
119
|
+
@log.error e.inspect
|
120
|
+
ensure
|
121
|
+
db.close rescue nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
if __FILE__ == $0
|
127
|
+
require "optparse"
|
128
|
+
|
129
|
+
opts = {
|
130
|
+
:port => 16800,
|
131
|
+
:host => "localhost",
|
132
|
+
:log => nil,
|
133
|
+
:debug => false,
|
134
|
+
:foreground => false,
|
135
|
+
}
|
136
|
+
|
137
|
+
OptionParser.new do |parser|
|
138
|
+
parser.instance_eval do
|
139
|
+
self.banner = <<-EOB.gsub(/^\t+/, "")
|
140
|
+
Usage: #{$0} [opts]
|
141
|
+
|
142
|
+
EOB
|
143
|
+
|
144
|
+
separator ""
|
145
|
+
|
146
|
+
separator "Options:"
|
147
|
+
on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
|
148
|
+
opts[:port] = port
|
149
|
+
end
|
150
|
+
|
151
|
+
on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
|
152
|
+
opts[:host] = host
|
153
|
+
end
|
154
|
+
|
155
|
+
on("-l", "--log LOG", "log file") do |log|
|
156
|
+
opts[:log] = log
|
157
|
+
end
|
158
|
+
|
159
|
+
on("--debug", "Enable debug mode") do |debug|
|
160
|
+
opts[:log] = $stdout
|
161
|
+
opts[:debug] = true
|
162
|
+
end
|
163
|
+
|
164
|
+
on("-f", "--foreground", "run foreground") do |foreground|
|
165
|
+
opts[:log] = $stdout
|
166
|
+
opts[:foreground] = true
|
167
|
+
end
|
168
|
+
|
169
|
+
parse!(ARGV)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
opts[:logger] = Logger.new(opts[:log], "daily")
|
174
|
+
opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
|
175
|
+
|
176
|
+
def daemonize(foreground=false)
|
177
|
+
trap("SIGINT") { exit! 0 }
|
178
|
+
trap("SIGTERM") { exit! 0 }
|
179
|
+
trap("SIGHUP") { exit! 0 }
|
180
|
+
return yield if $DEBUG || foreground
|
181
|
+
Process.fork do
|
182
|
+
Process.setsid
|
183
|
+
Dir.chdir "/"
|
184
|
+
File.open("/dev/null") {|f|
|
185
|
+
STDIN.reopen f
|
186
|
+
STDOUT.reopen f
|
187
|
+
STDERR.reopen f
|
188
|
+
}
|
189
|
+
yield
|
190
|
+
end
|
191
|
+
exit! 0
|
192
|
+
end
|
193
|
+
|
194
|
+
daemonize(opts[:debug] || opts[:foreground]) do
|
195
|
+
Net::IRC::Server.new(opts[:host], opts[:port], GmailNotifier, opts).start
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Local Variables:
|
200
|
+
# coding: utf-8
|
201
|
+
# End:
|
data/examples/hig.rb
ADDED
@@ -0,0 +1,733 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
=begin
|
3
|
+
# hig.rb
|
4
|
+
|
5
|
+
## Launch
|
6
|
+
|
7
|
+
$ ruby hig.rb
|
8
|
+
|
9
|
+
If you want to help:
|
10
|
+
|
11
|
+
$ ruby hig.rb --help
|
12
|
+
|
13
|
+
## Configuration
|
14
|
+
|
15
|
+
Options specified by after irc realname.
|
16
|
+
|
17
|
+
Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ).
|
18
|
+
|
19
|
+
haiku {
|
20
|
+
host: localhost
|
21
|
+
port: 16679
|
22
|
+
name: username@example.com athack jabber=username@example.com:jabberpasswd tid=10 ratio=10:3:5
|
23
|
+
password: password on Haiku
|
24
|
+
in-encoding: utf8
|
25
|
+
out-encoding: utf8
|
26
|
+
}
|
27
|
+
|
28
|
+
### athack
|
29
|
+
|
30
|
+
If `athack` client option specified,
|
31
|
+
all nick in join message is leading with @.
|
32
|
+
|
33
|
+
So if you complemente nicks (e.g. Irssi),
|
34
|
+
it's good for Twitter like reply command (@nick).
|
35
|
+
|
36
|
+
In this case, you will see torrent of join messages after connected,
|
37
|
+
because NAMES list can't send @ leading nick (it interpreted op.)
|
38
|
+
|
39
|
+
### tid=<color>
|
40
|
+
|
41
|
+
Apply id to each message for make favorites by CTCP ACTION.
|
42
|
+
|
43
|
+
/me fav id
|
44
|
+
|
45
|
+
<color> can be
|
46
|
+
|
47
|
+
0 => white
|
48
|
+
1 => black
|
49
|
+
2 => blue navy
|
50
|
+
3 => green
|
51
|
+
4 => red
|
52
|
+
5 => brown maroon
|
53
|
+
6 => purple
|
54
|
+
7 => orange olive
|
55
|
+
8 => yellow
|
56
|
+
9 => lightgreen lime
|
57
|
+
10 => teal
|
58
|
+
11 => lightcyan cyan aqua
|
59
|
+
12 => lightblue royal
|
60
|
+
13 => pink lightpurple fuchsia
|
61
|
+
14 => grey
|
62
|
+
15 => lightgrey silver
|
63
|
+
|
64
|
+
|
65
|
+
### jabber=<jid>:<pass>
|
66
|
+
|
67
|
+
If `jabber=<jid>:<pass>` option specified,
|
68
|
+
use jabber to get friends timeline.
|
69
|
+
|
70
|
+
You must setup im notifing settings in the site and
|
71
|
+
install "xmpp4r-simple" gem.
|
72
|
+
|
73
|
+
$ sudo gem install xmpp4r-simple
|
74
|
+
|
75
|
+
Be careful for managing password.
|
76
|
+
|
77
|
+
### alwaysim
|
78
|
+
|
79
|
+
Use IM instead of any APIs (e.g. post)
|
80
|
+
|
81
|
+
### ratio=<timeline>:<friends>:<channel>
|
82
|
+
|
83
|
+
## License
|
84
|
+
|
85
|
+
Ruby's by cho45
|
86
|
+
|
87
|
+
=end
|
88
|
+
|
89
|
+
$LOAD_PATH << "lib"
|
90
|
+
$LOAD_PATH << "../lib"
|
91
|
+
|
92
|
+
$KCODE = "u" # json use this
|
93
|
+
|
94
|
+
require "rubygems"
|
95
|
+
require "net/irc"
|
96
|
+
require "net/http"
|
97
|
+
require "uri"
|
98
|
+
require "json"
|
99
|
+
require "socket"
|
100
|
+
require "time"
|
101
|
+
require "logger"
|
102
|
+
require "yaml"
|
103
|
+
require "pathname"
|
104
|
+
require "cgi"
|
105
|
+
require "digest/md5"
|
106
|
+
|
107
|
+
Net::HTTP.version_1_2
|
108
|
+
|
109
|
+
class HaikuIrcGateway < Net::IRC::Server::Session
|
110
|
+
def server_name
|
111
|
+
"haikugw"
|
112
|
+
end
|
113
|
+
|
114
|
+
def server_version
|
115
|
+
"0.0.0"
|
116
|
+
end
|
117
|
+
|
118
|
+
def main_channel
|
119
|
+
"#haiku"
|
120
|
+
end
|
121
|
+
|
122
|
+
def api_base
|
123
|
+
URI(ENV["HAIKU_BASE"] || "http://h.hatena.ne.jp/api/")
|
124
|
+
end
|
125
|
+
|
126
|
+
def api_source
|
127
|
+
"hig.rb"
|
128
|
+
end
|
129
|
+
|
130
|
+
def jabber_bot_id
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def hourly_limit
|
135
|
+
60
|
136
|
+
end
|
137
|
+
|
138
|
+
class ApiFailed < StandardError; end
|
139
|
+
|
140
|
+
def initialize(*args)
|
141
|
+
super
|
142
|
+
@channels = {}
|
143
|
+
@user_agent = "#{self.class}/#{server_version} (hig.rb)"
|
144
|
+
@map = nil
|
145
|
+
@counters = {} # for jabber fav
|
146
|
+
end
|
147
|
+
|
148
|
+
def on_user(m)
|
149
|
+
super
|
150
|
+
post @prefix, JOIN, main_channel
|
151
|
+
post server_name, MODE, main_channel, "+o", @prefix.nick
|
152
|
+
|
153
|
+
@real, *@opts = @opts.name || @real.split(/\s+/)
|
154
|
+
@opts = @opts.inject({}) {|r,i|
|
155
|
+
key, value = i.split("=")
|
156
|
+
r.update(key => value)
|
157
|
+
}
|
158
|
+
@tmap = TypableMap.new
|
159
|
+
|
160
|
+
if @opts["jabber"]
|
161
|
+
jid, pass = @opts["jabber"].split(":", 2)
|
162
|
+
@opts["jabber"].replace("jabber=#{jid}:********")
|
163
|
+
if jabber_bot_id
|
164
|
+
begin
|
165
|
+
require "xmpp4r-simple"
|
166
|
+
start_jabber(jid, pass)
|
167
|
+
rescue LoadError
|
168
|
+
log "Failed to start Jabber."
|
169
|
+
log 'Installl "xmpp4r-simple" gem or check your id/pass.'
|
170
|
+
finish
|
171
|
+
end
|
172
|
+
else
|
173
|
+
@opts.delete("jabber")
|
174
|
+
log "This gateway does not support Jabber bot."
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
log "Client Options: #{@opts.inspect}"
|
179
|
+
@log.info "Client Options: #{@opts.inspect}"
|
180
|
+
|
181
|
+
timeline_ratio, friends_ratio, channel_ratio = (@opts["ratio"] || "10:3:5").split(":").map {|ratio| ratio.to_i }
|
182
|
+
footing = (timeline_ratio + friends_ratio + channel_ratio).to_f
|
183
|
+
|
184
|
+
@timeline = []
|
185
|
+
@check_follows_thread = Thread.start do
|
186
|
+
loop do
|
187
|
+
begin
|
188
|
+
check_friends
|
189
|
+
check_keywords
|
190
|
+
rescue ApiFailed => e
|
191
|
+
@log.error e.inspect
|
192
|
+
rescue Exception => e
|
193
|
+
@log.error e.inspect
|
194
|
+
e.backtrace.each do |l|
|
195
|
+
@log.error "\t#{l}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
sleep freq(friends_ratio / footing)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
return if @opts["jabber"]
|
203
|
+
|
204
|
+
@check_timeline_thread = Thread.start do
|
205
|
+
sleep 10
|
206
|
+
loop do
|
207
|
+
begin
|
208
|
+
check_timeline
|
209
|
+
rescue ApiFailed => e
|
210
|
+
@log.error e.inspect
|
211
|
+
rescue Exception => e
|
212
|
+
@log.error e.inspect
|
213
|
+
e.backtrace.each do |l|
|
214
|
+
@log.error "\t#{l}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
sleep freq(timeline_ratio / footing)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def on_disconnected
|
223
|
+
@check_follows_thread.kill rescue nil
|
224
|
+
@check_timeline_thread.kill rescue nil
|
225
|
+
@im_thread.kill rescue nil
|
226
|
+
@im.disconnect rescue nil
|
227
|
+
end
|
228
|
+
|
229
|
+
def on_privmsg(m)
|
230
|
+
return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp?
|
231
|
+
retry_count = 3
|
232
|
+
ret = nil
|
233
|
+
target, message = *m.params
|
234
|
+
begin
|
235
|
+
channel = target.sub(/^#/, "")
|
236
|
+
reply = message[/\s+>(.+)$/, 1]
|
237
|
+
if !reply && @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post
|
238
|
+
message = "##{channel} #{message}" unless "##{channel}" == main_channel
|
239
|
+
ret = @im.deliver(jabber_bot_id, message)
|
240
|
+
post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, message
|
241
|
+
else
|
242
|
+
channel = "" if "##{channel}" == main_channel
|
243
|
+
rid = rid_for(reply) if reply
|
244
|
+
ret = api("statuses/update", {"status" => message, "in_reply_to_status_id" => rid, "keyword" => channel})
|
245
|
+
log "Status Updated via API"
|
246
|
+
end
|
247
|
+
raise ApiFailed, "API failed" unless ret
|
248
|
+
check_timeline
|
249
|
+
rescue => e
|
250
|
+
@log.error [retry_count, e.inspect].inspect
|
251
|
+
if retry_count > 0
|
252
|
+
retry_count -= 1
|
253
|
+
@log.debug "Retry to setting status..."
|
254
|
+
retry
|
255
|
+
else
|
256
|
+
log "Some Error Happened on Sending #{message}. #{e}"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def on_ctcp(target, message)
|
262
|
+
_, command, *args = message.split(/\s+/)
|
263
|
+
case command
|
264
|
+
when "list"
|
265
|
+
nick = args[0]
|
266
|
+
@log.debug([ nick, message ])
|
267
|
+
res = api("statuses/user_timeline", { "id" => nick }).reverse_each do |s|
|
268
|
+
@log.debug(s)
|
269
|
+
post nick, NOTICE, main_channel, s
|
270
|
+
end
|
271
|
+
|
272
|
+
unless res
|
273
|
+
post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
274
|
+
end
|
275
|
+
when "fav"
|
276
|
+
target = args[0]
|
277
|
+
st = @tmap[target]
|
278
|
+
id = rid_for(target)
|
279
|
+
if st || id
|
280
|
+
unless id
|
281
|
+
if @im && @im.connected?
|
282
|
+
# IM のときはいろいろめんどうなことする
|
283
|
+
nick, count = *st
|
284
|
+
pos = @counters[nick] - count
|
285
|
+
@log.debug "%p %s %d/%d => %d" % [
|
286
|
+
st,
|
287
|
+
nick,
|
288
|
+
count,
|
289
|
+
@counters[nick],
|
290
|
+
pos
|
291
|
+
]
|
292
|
+
res = api("statuses/user_timeline", { "id" => nick })
|
293
|
+
raise ApiFailed, "#{nick} may be private mode" if res.empty?
|
294
|
+
if res[pos]
|
295
|
+
id = res[pos]["id"]
|
296
|
+
else
|
297
|
+
raise ApiFailed, "#{pos} of #{nick} is not found."
|
298
|
+
end
|
299
|
+
else
|
300
|
+
id = st["id"]
|
301
|
+
end
|
302
|
+
end
|
303
|
+
res = api("favorites/create/#{id}", {})
|
304
|
+
post nil, NOTICE, main_channel, "Fav: #{res["screen_name"]}: #{res["text"].gsub(URI.regexp(%w|http https|), "http...")}"
|
305
|
+
else
|
306
|
+
post nil, NOTICE, main_channel, "No such id or status #{target}"
|
307
|
+
end
|
308
|
+
when "link"
|
309
|
+
tid = args[0]
|
310
|
+
st = @tmap[tid]
|
311
|
+
if st
|
312
|
+
st["link"] = (api_base + "/#{st["user"]["screen_name"]}/statuses/#{st["id"]}").to_s unless st["link"]
|
313
|
+
post nil, NOTICE, main_channel, st["link"]
|
314
|
+
else
|
315
|
+
post nil, NOTICE, main_channel, "No such id #{tid}"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
rescue ApiFailed => e
|
319
|
+
log e.inspect
|
320
|
+
end
|
321
|
+
|
322
|
+
def on_whois(m)
|
323
|
+
nick = m.params[0]
|
324
|
+
f = (@friends || []).find {|i| i["screen_name"] == nick }
|
325
|
+
if f
|
326
|
+
post nil, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}"
|
327
|
+
post nil, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s
|
328
|
+
post nil, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle"
|
329
|
+
post nil, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
|
330
|
+
else
|
331
|
+
post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def on_join(m)
|
336
|
+
channels = m.params[0].split(/\s*,\s*/)
|
337
|
+
channels.each do |channel|
|
338
|
+
next if channel == main_channel
|
339
|
+
begin
|
340
|
+
api("keywords/create/#{URI.escape(channel.sub(/^#/, ""))}")
|
341
|
+
@channels[channel] = {
|
342
|
+
:read => []
|
343
|
+
}
|
344
|
+
post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
|
345
|
+
rescue => e
|
346
|
+
@log.debug e.inspect
|
347
|
+
post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def on_part(m)
|
353
|
+
channel = m.params[0]
|
354
|
+
return if channel == main_channel
|
355
|
+
@channels.delete(channel)
|
356
|
+
api("keywords/destroy/#{URI.escape(channel.sub(/^#/, ""))}")
|
357
|
+
post "#{@nick}!#{@nick}@#{api_base.host}", PART, channel
|
358
|
+
end
|
359
|
+
|
360
|
+
def on_who(m)
|
361
|
+
channel = m.params[0]
|
362
|
+
case
|
363
|
+
when channel == main_channel
|
364
|
+
# "<channel> <user> <host> <server> <nick>
|
365
|
+
# ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
|
366
|
+
# :<hopcount> <real name>"
|
367
|
+
@friends.each do |f|
|
368
|
+
user = nick = f["screen_name"]
|
369
|
+
host = serv = api_base.host
|
370
|
+
real = f["name"]
|
371
|
+
post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
|
372
|
+
end
|
373
|
+
post nil, RPL_ENDOFWHO, @nick, channel
|
374
|
+
when @groups.key?(channel)
|
375
|
+
@groups[channel].each do |name|
|
376
|
+
f = @friends.find {|i| i["screen_name"] == name }
|
377
|
+
user = nick = f["screen_name"]
|
378
|
+
host = serv = api_base.host
|
379
|
+
real = f["name"]
|
380
|
+
post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
|
381
|
+
end
|
382
|
+
post nil, RPL_ENDOFWHO, @nick, channel
|
383
|
+
else
|
384
|
+
post nil, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel"
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
def check_timeline
|
390
|
+
api("statuses/friends_timeline").reverse_each do |s|
|
391
|
+
begin
|
392
|
+
id = s["id"]
|
393
|
+
next if id.nil? || @timeline.include?(id)
|
394
|
+
@timeline << id
|
395
|
+
nick = s["user"]["id"]
|
396
|
+
mesg = generate_status_message(s)
|
397
|
+
|
398
|
+
tid = @tmap.push(s)
|
399
|
+
|
400
|
+
@log.debug [id, nick, mesg]
|
401
|
+
|
402
|
+
channel = "##{s["keyword"]}"
|
403
|
+
case
|
404
|
+
when s["keyword"].match(/^id:/)
|
405
|
+
channel = main_channel
|
406
|
+
when !@channels.keys.include?(channel)
|
407
|
+
channel = main_channel
|
408
|
+
mesg = "%s = %s" % [s["keyword"], mesg]
|
409
|
+
end
|
410
|
+
|
411
|
+
if nick == @nick # 自分のときは topic に
|
412
|
+
post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, mesg
|
413
|
+
else
|
414
|
+
if @opts["tid"]
|
415
|
+
message(nick, channel, "%s \x03%s [%s]" % [mesg, @opts["tid"], tid])
|
416
|
+
else
|
417
|
+
message(nick, channel, "%s" % [mesg, tid])
|
418
|
+
end
|
419
|
+
end
|
420
|
+
rescue => e
|
421
|
+
@log.debug "Error: %p" % e
|
422
|
+
end
|
423
|
+
end
|
424
|
+
@log.debug "@timeline.size = #{@timeline.size}"
|
425
|
+
@timeline = @timeline.last(100)
|
426
|
+
end
|
427
|
+
|
428
|
+
def generate_status_message(s)
|
429
|
+
mesg = s["text"]
|
430
|
+
mesg.sub!("#{s["keyword"]}=", "") unless s["keyword"] =~ /^id:/
|
431
|
+
mesg << " > #{s["in_reply_to_user_id"]}" unless s["in_reply_to_user_id"].empty?
|
432
|
+
|
433
|
+
@log.debug(mesg)
|
434
|
+
mesg
|
435
|
+
end
|
436
|
+
|
437
|
+
def check_friends
|
438
|
+
first = true unless @friends
|
439
|
+
@friends ||= []
|
440
|
+
friends = api("statuses/friends")
|
441
|
+
if first && !@opts.key?("athack")
|
442
|
+
@friends = friends
|
443
|
+
post nil, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
|
444
|
+
post nil, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
|
445
|
+
else
|
446
|
+
prv_friends = @friends.map {|i| i["screen_name"] }
|
447
|
+
now_friends = friends.map {|i| i["screen_name"] }
|
448
|
+
|
449
|
+
# Twitter API bug?
|
450
|
+
return if !first && (now_friends.length - prv_friends.length).abs > 10
|
451
|
+
|
452
|
+
(now_friends - prv_friends).each do |join|
|
453
|
+
join = "@#{join}" if @opts.key?("athack")
|
454
|
+
post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
|
455
|
+
end
|
456
|
+
(prv_friends - now_friends).each do |part|
|
457
|
+
part = "@#{part}" if @opts.key?("athack")
|
458
|
+
post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
|
459
|
+
end
|
460
|
+
@friends = friends
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def check_keywords
|
465
|
+
keywords = api("statuses/keywords").map {|i| "##{i["title"]}" }
|
466
|
+
current = @channels.keys
|
467
|
+
current.delete main_channel
|
468
|
+
|
469
|
+
(current - keywords).each do |part|
|
470
|
+
@channels.delete(part)
|
471
|
+
post "#{@nick}!#{@nick}@#{api_base.host}", PART, part
|
472
|
+
end
|
473
|
+
|
474
|
+
(keywords - current).each do |join|
|
475
|
+
@channels[join] = {
|
476
|
+
:read => []
|
477
|
+
}
|
478
|
+
post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, join
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
def freq(ratio)
|
483
|
+
ret = 3600 / (hourly_limit * ratio).round
|
484
|
+
@log.debug "Frequency: #{ret}"
|
485
|
+
ret
|
486
|
+
end
|
487
|
+
|
488
|
+
def start_jabber(jid, pass)
|
489
|
+
@log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
|
490
|
+
@im = Jabber::Simple.new(jid, pass)
|
491
|
+
@im.add(jabber_bot_id)
|
492
|
+
@im_thread = Thread.start do
|
493
|
+
loop do
|
494
|
+
begin
|
495
|
+
@im.received_messages.each do |msg|
|
496
|
+
@log.debug [msg.from, msg.body]
|
497
|
+
if msg.from.strip == jabber_bot_id
|
498
|
+
# Haiku -> 'nick(id): msg'
|
499
|
+
body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "")
|
500
|
+
if Regexp.last_match
|
501
|
+
nick, id = Regexp.last_match.captures
|
502
|
+
body = CGI.unescapeHTML(body)
|
503
|
+
|
504
|
+
case
|
505
|
+
when nick == "投稿完了"
|
506
|
+
log "#{nick}: #{body}"
|
507
|
+
when nick == "チャンネル投稿完了"
|
508
|
+
log "#{nick}: #{body}"
|
509
|
+
when body =~ /^#([a-z_]+)\s+(.+)$/i
|
510
|
+
# channel message or not
|
511
|
+
message(id || nick, "##{Regexp.last_match[1]}", Regexp.last_match[2])
|
512
|
+
when nick == "photo" && body =~ %r|^http://haiku\.jp/user/([^/]+)/|
|
513
|
+
nick = Regexp.last_match[1]
|
514
|
+
message(nick, main_channel, body)
|
515
|
+
else
|
516
|
+
@counters[nick] ||= 0
|
517
|
+
@counters[nick] += 1
|
518
|
+
tid = @tmap.push([nick, @counters[nick]])
|
519
|
+
message(nick, main_channel, "%s \x03%s [%s]" % [body, @opts["tid"], tid])
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
rescue Exception => e
|
525
|
+
@log.error "Error on Jabber loop: #{e.inspect}"
|
526
|
+
e.backtrace.each do |l|
|
527
|
+
@log.error "\t#{l}"
|
528
|
+
end
|
529
|
+
end
|
530
|
+
sleep 1
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def require_post?(path)
|
536
|
+
[
|
537
|
+
%r|/update|,
|
538
|
+
%r|/create|,
|
539
|
+
%r|/destroy|,
|
540
|
+
].any? {|i| i === path }
|
541
|
+
end
|
542
|
+
|
543
|
+
def api(path, q={})
|
544
|
+
ret = {}
|
545
|
+
q["source"] ||= api_source
|
546
|
+
|
547
|
+
uri = api_base.dup
|
548
|
+
uri.path = "/api/#{path}.json"
|
549
|
+
uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^:,-.!~*'()\w]/n)}" : r }.join("&")
|
550
|
+
|
551
|
+
|
552
|
+
req = nil
|
553
|
+
if require_post?(path)
|
554
|
+
req = Net::HTTP::Post.new(uri.path)
|
555
|
+
req.body = uri.query
|
556
|
+
else
|
557
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
558
|
+
end
|
559
|
+
req.basic_auth(@real, @pass)
|
560
|
+
req["User-Agent"] = @user_agent
|
561
|
+
req["If-Modified-Since"] = q["since"] if q.key?("since")
|
562
|
+
|
563
|
+
@log.debug uri.inspect
|
564
|
+
ret = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
|
565
|
+
|
566
|
+
case ret
|
567
|
+
when Net::HTTPOK # 200
|
568
|
+
ret = JSON.parse(ret.body.gsub(/:'/, ':"').gsub(/',/, '",').gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"'))
|
569
|
+
raise ApiFailed, "Server Returned Error: #{ret["error"]}" if ret.kind_of?(Hash) && ret["error"]
|
570
|
+
ret
|
571
|
+
when Net::HTTPNotModified # 304
|
572
|
+
[]
|
573
|
+
when Net::HTTPBadRequest # 400
|
574
|
+
# exceeded the rate limitation
|
575
|
+
raise ApiFailed, "#{ret.code}: #{ret.message}"
|
576
|
+
else
|
577
|
+
raise ApiFailed, "Server Returned #{ret.code} #{ret.message}"
|
578
|
+
end
|
579
|
+
rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
|
580
|
+
raise ApiFailed, e.inspect
|
581
|
+
end
|
582
|
+
|
583
|
+
def message(sender, target, str)
|
584
|
+
sender = "#{sender}!#{sender}@#{api_base.host}"
|
585
|
+
post sender, PRIVMSG, target, str.gsub(/\s+/, " ")
|
586
|
+
end
|
587
|
+
|
588
|
+
def log(str)
|
589
|
+
str.gsub!(/\n/, " ")
|
590
|
+
post server_name, NOTICE, main_channel, str
|
591
|
+
end
|
592
|
+
|
593
|
+
# return rid of most recent matched status with text
|
594
|
+
def rid_for(text)
|
595
|
+
target = Regexp.new(Regexp.quote(text.strip), "i")
|
596
|
+
status = api("statuses/friends_timeline").find {|i|
|
597
|
+
next false if i["user"]["name"] == @nick # 自分は除外
|
598
|
+
i["text"] =~ target
|
599
|
+
}
|
600
|
+
|
601
|
+
@log.debug "Looking up status contains #{text.inspect} -> #{status.inspect}"
|
602
|
+
status ? status["id"] : nil
|
603
|
+
end
|
604
|
+
|
605
|
+
class TypableMap < Hash
|
606
|
+
Roma = %w|k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q|.unshift("").map {|consonant|
|
607
|
+
case
|
608
|
+
when consonant.size > 1, consonant == "y"
|
609
|
+
%w|a u o|
|
610
|
+
when consonant == "q"
|
611
|
+
%w|a i e o|
|
612
|
+
else
|
613
|
+
%w|a i u e o|
|
614
|
+
end.map {|vowel| "#{consonant}#{vowel}" }
|
615
|
+
}.flatten
|
616
|
+
|
617
|
+
def initialize(size=1)
|
618
|
+
@seq = Roma
|
619
|
+
@map = {}
|
620
|
+
@n = 0
|
621
|
+
@size = size
|
622
|
+
end
|
623
|
+
|
624
|
+
def generate(n)
|
625
|
+
ret = []
|
626
|
+
begin
|
627
|
+
n, r = n.divmod(@seq.size)
|
628
|
+
ret << @seq[r]
|
629
|
+
end while n > 0
|
630
|
+
ret.reverse.join
|
631
|
+
end
|
632
|
+
|
633
|
+
def push(obj)
|
634
|
+
id = generate(@n)
|
635
|
+
self[id] = obj
|
636
|
+
@n += 1
|
637
|
+
@n = @n % (@seq.size ** @size)
|
638
|
+
id
|
639
|
+
end
|
640
|
+
alias << push
|
641
|
+
|
642
|
+
def clear
|
643
|
+
@n = 0
|
644
|
+
super
|
645
|
+
end
|
646
|
+
|
647
|
+
private :[]=
|
648
|
+
undef update, merge, merge!, replace
|
649
|
+
end
|
650
|
+
|
651
|
+
|
652
|
+
end
|
653
|
+
|
654
|
+
if __FILE__ == $0
|
655
|
+
require "optparse"
|
656
|
+
|
657
|
+
opts = {
|
658
|
+
:port => 16679,
|
659
|
+
:host => "localhost",
|
660
|
+
:log => nil,
|
661
|
+
:debug => false,
|
662
|
+
:foreground => false,
|
663
|
+
}
|
664
|
+
|
665
|
+
OptionParser.new do |parser|
|
666
|
+
parser.instance_eval do
|
667
|
+
self.banner = <<-EOB.gsub(/^\t+/, "")
|
668
|
+
Usage: #{$0} [opts]
|
669
|
+
|
670
|
+
EOB
|
671
|
+
|
672
|
+
separator ""
|
673
|
+
|
674
|
+
separator "Options:"
|
675
|
+
on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
|
676
|
+
opts[:port] = port
|
677
|
+
end
|
678
|
+
|
679
|
+
on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
|
680
|
+
opts[:host] = host
|
681
|
+
end
|
682
|
+
|
683
|
+
on("-l", "--log LOG", "log file") do |log|
|
684
|
+
opts[:log] = log
|
685
|
+
end
|
686
|
+
|
687
|
+
on("--debug", "Enable debug mode") do |debug|
|
688
|
+
opts[:log] = $stdout
|
689
|
+
opts[:debug] = true
|
690
|
+
end
|
691
|
+
|
692
|
+
on("-f", "--foreground", "run foreground") do |foreground|
|
693
|
+
opts[:log] = $stdout
|
694
|
+
opts[:foreground] = true
|
695
|
+
end
|
696
|
+
|
697
|
+
on("-n", "--name [user name or email address]") do |name|
|
698
|
+
opts[:name] = name
|
699
|
+
end
|
700
|
+
|
701
|
+
parse!(ARGV)
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
opts[:logger] = Logger.new(opts[:log], "daily")
|
706
|
+
opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
|
707
|
+
|
708
|
+
# def daemonize(foreground=false)
|
709
|
+
# trap("SIGINT") { exit! 0 }
|
710
|
+
# trap("SIGTERM") { exit! 0 }
|
711
|
+
# trap("SIGHUP") { exit! 0 }
|
712
|
+
# return yield if $DEBUG || foreground
|
713
|
+
# Process.fork do
|
714
|
+
# Process.setsid
|
715
|
+
# Dir.chdir "/"
|
716
|
+
# File.open("/dev/null") {|f|
|
717
|
+
# STDIN.reopen f
|
718
|
+
# STDOUT.reopen f
|
719
|
+
# STDERR.reopen f
|
720
|
+
# }
|
721
|
+
# yield
|
722
|
+
# end
|
723
|
+
# exit! 0
|
724
|
+
# end
|
725
|
+
|
726
|
+
# daemonize(opts[:debug] || opts[:foreground]) do
|
727
|
+
Net::IRC::Server.new(opts[:host], opts[:port], HaikuIrcGateway, opts).start
|
728
|
+
# end
|
729
|
+
end
|
730
|
+
|
731
|
+
# Local Variables:
|
732
|
+
# coding: utf-8
|
733
|
+
# End:
|