4 |
5 | require 'socket'
6 | require 'thread'
7 | require $REUDY_DIR + '/irc-client'
8 | require $REUDY_DIR + '/reudy_common'
9 |
10 | module Gimite
11 | #ボット用のIRCクライアント
12 | class BotIRCClient < IRCC
13 | include Gimite
14 |
15 | SILENT_SECOND = 20.0 #沈黙が続いたと判断する秒数。
16 |
17 | def initialize(user, logOut = STDOUT)
18 | @user = user
19 | @isExitting = false
20 | @channel = @user.settings[:channel]
21 | @infoChannel = @user.settings[:info_channel]
22 | @nick = @user.settings[:nick]
23 | @user.client = self
24 | @user.onBeginConnecting
25 | option = {
26 | 'user'=>@user.settings[:name], \
27 | 'realname'=>@user.settings[:real_name], \
28 | 'pass'=>@user.settings[:login_password].to_s, \
29 | 'nick'=>@nick, \
30 | 'channel'=>@channel, \
31 | 'channel_key'=>@user.settings[:channel_key].to_s \
32 | }
33 | super(nil, option, __ENCODING__.to_s, logOut, @user.settings[:encoding] || "ISO-2022-JP")
34 | end
35 |
36 | #IRCのメッセージをひたすら処理するループ。
37 | def processLoop
38 | loop do
39 | begin
40 | @isJoiningInfoChannel = false
41 | @prevTime = Time.now #onSilent用。
42 | @receiveQue = Queue.new #受け取った通常発言のキュー。
43 | @controlQue = Queue.new #受け取った制御発言のキュー。
44 | connect(TCPSocket.open(@user.settings[:host], @user.settings[:port].to_i,@user.settings[:localhost]))
45 | on_connect #ソケット接続時の処理。
46 | pingThread = Thread.new{ pingProcess }
47 | receiveThread = Thread.new{ receiveProcess }
48 | #受信ループ。
49 | while line = sock.gets
50 | on_recv(line.force_encoding(@user.settings[:encoding] || "ISO-2022-JP"))
51 | time = Time.now
52 | if time - @prevTime >= SILENT_SECOND
53 | @prevTime = time
54 | @user.onSilent
55 | #沈黙がしばらく続いた。
56 | #発言が何も無くてもpingProcess()のおかげで定期的にメッセージが飛んでくるので、
57 | #ここでチェックすればOK。
58 | end
59 | end
60 | puts "切断されました。"
61 | rescue SystemCallError, SocketError, IOError => ex
62 | puts "切断されました。#{ex.message}"
63 | end
64 | pingThread.exit if pingThread
65 | @receiveQue.push(nil)
66 | receiveThread.join if receiveThread
67 | break if @isExitting || @user.settings[:auto_reconnect]
68 | sleep(10)
69 | break unless queryReconnect
70 | puts "再接続中..."
71 | end
72 | end
73 |
74 | #補助情報を出力
75 | def outputInfo(s)
76 | sleep(@user.settings[:wait_before_info].to_f) if @user.settings[:wait_before_info]
77 | sendmess("NOTICE #{@infoChannel} :#{s}\n")
78 | end
79 |
80 | #発言する
81 | def speak(s)
82 | if @user.settings[:speak_with_privmsg]
83 | sendpriv(s)
84 | else
85 | sendnotice(s)
86 | end
87 | end
88 |
89 | #チャンネルを移動。接続中はこっちを使う。
90 | def moveChannel(channel)
91 | greeting = @user.settings[:leaving_message]
92 | speak(greeting) if greeting
93 | @channel = channel
94 | movechannel(@channel)
95 | end
96 |
97 | #チャンネルを変更。切断中はこっちを使う。
98 | def setChannel(channel)
99 | @channel = channel
100 | @irc_channel = channel
101 | end
102 |
103 | def status=(status)
104 | end
105 |
106 | #終了。
107 | def exit
108 | @isExitting = true
109 | greeting = @user.settings[:leaving_message]
110 | sendmess(greeting ? "QUIT :#{greeting}\r\n" : "QUIT\r\n")
111 | end
112 |
113 | #以下、IRCCのメソッドのオーバライド
114 |
115 | def on_priv(type, nick, mess)
116 | super(type, nick, mess)
117 | onPriv(type, nick, mess)
118 | end
119 |
120 | def on_external_priv(type, nick, to, mess)
121 | super(type, nick, to, mess)
122 | onExternalPriv(type, nick, to, mess)
123 | end
124 |
125 | def on_join(nick, channel)
126 | super(nick, channel)
127 | onJoin(nick, channel)
128 | end
129 |
130 | def on_myjoin(channel)
131 | #IRCC#on_myjoinの中ではon_joinが呼ばれてしまうので、
132 | #ここでsuperを呼んではいけない。
133 | onMyJoin(channel)
134 | end
135 |
136 | def on_myinvite(nick, channel)
137 | super(nick, channel)
138 | onInvite(nick, channel)
139 | end
140 |
141 | def on_error(code)
142 | onError(code)
143 | end
144 |
145 | #以下、派生クラスでオーバライド可能なメソッド
146 |
147 | #普通のメッセージ
148 | def onPriv(type, nick, mess)
149 | if nick != @nick && (@user.settings[:respond_to_notice] || type == "PRIVMSG")
150 | @prevTime= Time.now
151 | @receiveQue.push([nick, mess.strip])
152 | end
153 | end
154 |
155 | #今いるチャンネルの外からの普通のメッセージ
156 | def onExternalPriv(type, nick, to, mess)
157 | return if nick == @nick || (!@user.settings[:respond_to_notice] && type != "PRIVMSG")
158 | @prevTime = Time.now
159 | if @user.settings[:respond_to_external]
160 | #チャンネル外からの発言は制御発言、という危険な仮仕様。
161 | @controlQue.push(mess.strip)
162 | @receiveQue.push(:nop) #メッセージ処理ループのブロックを解く。
163 | else
164 | @receiveQue.push([nick, mess.strip])
165 | end
166 | end
167 |
168 | #他人がJOINした
169 | def onJoin(nick, channel)
170 | greeting = @user.settings[:private_greeting]
171 | sendmess("NOTICE #{nick} :#{greeting}\n") if greeting && !greeting.empty?
172 | @user.onOtherJoin(nick)
173 | end
174 |
175 | #自分がJOINした
176 | def onMyJoin(channel)
177 | channel.strip!
178 | channel.downcase!
179 | if channel == @channel.downcase
180 | greeting = @user.settings[:joining_message]
181 | speak(greeting) if greeting
182 | @user.onSelfJoin
183 | end
184 | unless @isJoiningInfoChannel
185 | sendmess("JOIN #{@infoChannel}\r\n")
186 | @isJoiningInfoChannel = true
187 | end
188 | end
189 |
190 | #招待された
191 | def onInvite(nick, channel)
192 | moveChannel(channel)
193 | end
194 |
195 | #再接続の前に呼び出される。
196 | #falseを返すと、再接続せずに終了する。
197 | def queryReconnect
198 | return true
199 | end
200 |
201 | #エラー
202 | def onError(code)
203 | if code == "433" #ERR_NICKNAMEINUSE ニックネームはすでに使用されている
204 | puts "Error: ニックネーム #{@nick} は、別の人に使われています。"
205 | else
206 | puts "Error: エラーコード #{code}"
207 | end
208 | sendmess("QUIT\r\n") #一度QUITして再接続。
209 | end
210 |
211 | private
212 |
213 | #受信してキューにたまっている発言を処理する。
214 | def receiveProcess
215 | while args = popMessage
216 | while args
217 | sleep(@user.settings[:wait_before_speak].to_f * (0.5 + rand)) if @user.settings[:wait_before_speak]
218 | if @receiveQue.empty?
219 | @user.onOtherSpeak(*(args+[false]))
220 | break
221 | end
222 | until @receiveQue.empty? && args
223 | #ウエイト中に他の人の発言が入った場合、前の発言は極力無視する。
224 | @user.onOtherSpeak(*(args+[true]))
225 | args = popMessage
226 | return unless args
227 | end
228 | end
229 | end
230 | end
231 |
232 | #受信してキューにたまっている発言を取り出す。
233 | #制御発言があれば優先して処理する。
234 | def popMessage
235 | loop do
236 | mess = @receiveQue.pop
237 | @user.onControlMsg(@controlQue.pop) until @controlQue.empty?
238 | return mess if mess != :nop
239 | end
240 | end
241 |
242 | #定期的に意味の無いメッセージを送り、通信が切れてないか確かめる。
243 | #通信が切れたら、sock.getsのブロック状態を解除させるためにsock.closeする。
244 | def pingProcess
245 | loop do
246 | sleep(SILENT_SECOND)
247 | begin
248 | sendmess("TOPIC #{@channel}\r\n")
249 | rescue
250 | sock.close
251 | Thread.exit
252 | end
253 | end
254 | end
255 | end
256 | end
257 |
--------------------------------------------------------------------------------
/lib/reudy/irc-client.rb:
--------------------------------------------------------------------------------
1 | #----------------------------------------------------------------------------
2 | #
3 | # IRCクライアントライブラリ
4 | #
5 | # Programed by NAKAUE.T (Meister)
6 | # Modified by Gimite 市川
7 | # Modified by Glass_saga(glass.saga@gmail.com)
8 | #
9 | # 2003.05.04 Version 1.0.0 使ってくれる人が増えたのでソースを整理
10 | # 2003.05.10 Version 1.1.0 NICK処理追加
11 | # 2003.07.24 Version 1.2.0g 中途半端にマルチチャンネル対応(Gimite)
12 | # 2003.09.27 Version 1.2.1 UltimateIRCdで認証前にPINGが来る問題に対処(Meister)
13 | # (thanks for bancho)
14 | # 2003.09.28 Version 1.2.2 文字コード変換を整理(Meister)
15 | # 外部とのやり取りを行うコードを指定する
16 | # (IRCはJISを使うことになっている)
17 | # initializeのパラメータが変更になったので注意!
18 | # 2003.09.28 Version 2.0.0 インターフェース整理(Meister)
19 | # 互換性が低くなったので一気にバージョンを上げる
20 | # 2003.10.01 Version 2.0.1 NICKのバグ修正(Meister)
21 | # 2004.01.01 Version 2.0.2 インスタンス生成後にソケットを渡せるようにした
22 | # 2004.03.03 Version 2.0.3g 接続が切れた時に、IRCC#connectで再接続できるように
23 | # IRCエラーを処理するIRCC#on_errorを追加(Gimite)
24 | # 2011.09.09 Version 2 0.4 Ruby1.9に対応(Glass_saga)
25 | #
26 | #
27 | # このソフトウェアはPublic Domain Softwareです。
28 | # 自由に利用・改変して構いません。
29 | # 改変の有無にかかわらず、自由に再配布することが出来ます。
30 | # 作者はこのソフトウェアに関して、全ての権利と全ての義務を放棄します。
31 | #
32 | #----------------------------------------------------------------------------
33 | # IRCプロトコルについてはRFC2810-2813を参照のこと。日本語訳あります。
34 | #----------------------------------------------------------------------------
35 | #----------------------------------------------------------------------------
36 |
37 | class IRCC
38 | def initialize(sock, userinfo, internal_encoding, disp=STDOUT, irc_encoding)
39 | if sock
40 | @sock = sock
41 | @sock.set_encoding(@irc_encoding, @internal_encoding) unless @irc_encoding == @internal_encoding
42 | end
43 | @userinfo = userinfo
44 | @irc_nick = @userinfo['nick']
45 | @irc_channel = @userinfo['channel'] # 自動でJOINするチャンネル。このチャンネルを抜けると終了する(仕様)
46 | @channel_key = @userinfo['channel_key'] || ''
47 |
48 | @nicklist = []
49 | @joined_channel = nil
50 |
51 | @internal_encoding = internal_encoding ? Encoding.find(internal_encoding) : Encoding::UTF_8
52 | @irc_encoding = irc_encoding ? Encoding.find(irc_encoding) : Encoding::ISO_2022_JP
53 |
54 | @disp = disp
55 | end
56 |
57 | attr_accessor :sock,:userinfo,:nicklist,:irc_nick,:joined_channel
58 |
59 | # インスタンス生成後のソケット接続
60 | def connect(sock)
61 | @sock = sock
62 | @sock.set_encoding(@irc_encoding, @internal_encoding) unless @irc_encoding == @internal_encoding
63 | @myprefix = nil
64 | end
65 |
66 | # メッセージを送信(生)
67 | def sendmess(mess)
68 | @sock.print(mess)
69 | @disp.puts(mess.chop)
70 | end
71 |
72 | # メッセージの送信(通常のPRIVMSGで)
73 | def sendpriv(mess="")
74 | dispmess(">#{@irc_nick}<", mess)
75 | buff = "PRIVMSG #{@irc_channel} :#{mess}"
76 | sendmess(buff + "\r\n")
77 | end
78 |
79 | # メッセージの送信(NOTICEで)
80 | def sendnotice(mess="")
81 | dispmess(">#{@irc_nick}<", mess)
82 | buff = "NOTICE #{@irc_channel} :#{mess}"
83 | sendmess(buff + "\r\n")
84 | end
85 |
86 | # 別のチャンネルに移動
87 | def movechannel(channel)
88 | old_channel = @irc_channel
89 | @irc_channel = channel
90 | #PARTの前にこれを書き換えておかないとQUITしてしまう
91 | sendmess("PART #{old_channel}\r\n")
92 | sendmess("JOIN #{@irc_channel} #{@channel_key}\r\n")
93 | end
94 |
95 | # 終了する(実際にはチャンネルを抜けている)
96 | def quit
97 | sendmess("PART #{@irc_channel}\r\n")
98 | end
99 |
100 | # サーバから受け取ったメッセージを処理
101 | def on_recv(s)
102 | s.chomp!
103 | @disp.puts ">#{s}"
104 |
105 | prefix = ":unknown!unknown@unknown"
106 | prefix, param = s.split(' ', 2) if s[0..0] == ':'
107 | nick, prefix = prefix.split('!', 2)
108 | nick.slice!(0)
109 | param = s unless param
110 |
111 | param, param2 = param.split(/ :/, 2)
112 | param = param.split(' ')
113 | param << param2 if param2
114 |
115 | case param[0]
116 | when 'PRIVMSG', 'NOTICE' # 通常のメッセージ(NOTICEへのBOTの反応は禁止されている)
117 | if param[2][1..1] != "\001"
118 | mess = param[-1]
119 | if param[1].downcase == @irc_channel.downcase
120 | on_priv(param[0], nick, mess)
121 | else
122 | on_external_priv(param[0], nick, param[1], mess) # 今いるチャンネルの外からの発言
123 | end
124 | end
125 | when '372', '375' # MOTD(Message Of The Day)
126 | on_motd(param[-1])
127 | when '353' # チャンネル参加メンバーのリスト
128 | @nicklist += param[-1].gsub(/@/,'').split
129 | when 'JOIN' # 誰かがチャンネルに参加した
130 | channel = param[1]
131 | if @myprefix == prefix
132 | @joined_channel = channel
133 | on_myjoin(channel)
134 | else
135 | @nicklist |= [nick]
136 | on_join(nick, channel)
137 | end
138 | when 'PART' # 誰かがチャンネルから抜けた
139 | channel = param[1]
140 | if @myprefix == prefix
141 | @nicklist = []
142 | @joined_channel = nil
143 | on_mypart(channel)
144 | # 終了シーケンスだったらQUIT
145 | sendmess("QUIT\r\n") if param[1].downcase == @irc_channel.downcase
146 | else
147 | @nicklist.delete(nick)
148 | on_part(nick,channel)
149 | end
150 | when 'QUIT' # 誰かが終了した
151 | mess = param[-1]
152 | if @myprefix == prefix
153 | @nicklist = []
154 | on_myquit(mess)
155 | else
156 | @nicklist.delete(nick)
157 | on_quit(nick,mess)
158 | end
159 | when 'KICK' # 誰かがチャンネルから蹴られた
160 | kicker = nick
161 | channel = param[1]
162 | nick = param[2]
163 | mess = param[3]||''
164 | if nick == @irc_nick
165 | if param[1].downcase == @irc_channel.downcase
166 | @nicklist = []
167 | @joined_channel=nil
168 | end
169 | on_mykick(channel,mess,kicker)
170 | sendmess("QUIT\r\n") if param[1].downcase == @irc_channel.downcase # 蹴られたのでQUIT
171 | else
172 | @nicklist.delete(nick)
173 | on_kick(nick,channel,mess,kicker)
174 | end
175 | when 'NICK' # 誰かがNICKを変更した
176 | nick_new = param[1]
177 | @irc_nick = nick_new if nick == @irc_nick
178 | @nicklist.delete(nick)
179 | @nicklist |= [nick_new]
180 | on_nick(nick,nick_new)
181 | when 'INVITE' # 誰かが自分を招待した
182 | on_myinvite(nick,param[-1]) if param[1] == @irc_nick
183 | when 'PING' # クライアントの生存確認
184 | if @myhostname
185 | sendmess("PONG #{@myhostname} #{param[1]}\r\n")
186 | else
187 | # UltimateIRCdではMOTDより前にPINGが来る
188 | # 正確なクライアントのホスト名が不明なため、適当なPONGを返す
189 | sendmess("PONG dummy #{param[1]}\r\n")
190 | end
191 | when '376','422' # MOTDの終わり=ログインシーケンスの終わり
192 | # 自分のprefixを確認するためWHOISを発行
193 | sendmess("WHOIS #{@irc_nick}\r\n")
194 | when '311' # WHOISへの応答
195 | unless @myprefix
196 | # 自分のprefixを取得
197 | @myhostname = param[4]
198 | @myprefix = "#{param[3]}@#{@myhostname}"
199 | on_login
200 | end
201 | when '433' # nickが重複した
202 | on_error('433') # 正しくは重複しないnickで再度NICKを発行
203 | when '451' # 認証されていない
204 | on_error('451')
205 | @disp.puts('unknown login sequence!!')
206 | end
207 | end
208 |
209 | # 接続確立時の処理
210 | def on_connect
211 | @disp.puts "connect"
212 | dispmess(nil, 'Login...')
213 |
214 | sendmess("PASS #{@userinfo['pass']}\r\n") if @userinfo['pass'] && !@userinfo['pass'].empty?
215 | sendmess("NICK #{@irc_nick}\r\n")
216 | sendmess("USER #{@userinfo['user']} 0 * :#{@userinfo['realname']}\r\n")
217 | end
218 |
219 | # ここから下はオーバーライドする事を想定している
220 |
221 | # メッセージを表示(文字コードは変換しない)
222 | def dispmess(nick,mess)
223 | buff = Time.now.strftime('%H:%M:%S ')
224 | if nick
225 | buff = "#{buff}#{nick} #{mess}"
226 | else
227 | buff = "#{buff}#{mess}"
228 | end
229 | @disp.puts buff
230 | end
231 |
232 | # 接続・認証が完了し、チャンネルにJOINできる
233 | def on_login
234 | sendmess("JOIN #{@irc_channel} #{@channel_key}\r\n")
235 | end
236 |
237 | # MOTD(サーバのログインメッセージ)
238 | def on_motd(mess)
239 | dispmess(nil,mess)
240 | end
241 |
242 | # 通常メッセージ受信時の処理
243 | def on_priv(type,nick,mess)
244 | dispmess("<#{nick}>",mess)
245 | end
246 |
247 | # 今いるチャンネルの外からの通常メッセージ受信時の処理
248 | def on_external_priv(type,nick,channel,mess)
249 | end
250 |
251 | # JOIN受信時の処理
252 | def on_join(nick,channel)
253 | dispmess(nick,"JOIN #{channel}")
254 | end
255 |
256 | # PART受信時の処理
257 | def on_part(nick,channel)
258 | dispmess(nick,"PART #{channel}")
259 | end
260 |
261 | # QUIT受信時の処理
262 | def on_quit(nick,mess)
263 | dispmess(nick,"QUIT #{mess}")
264 | end
265 |
266 | # KICK受信時の処理
267 | def on_kick(nick,channel,mess,kicker)
268 | dispmess(nick,"KICK #{channel} #{kicker} #{mess}")
269 | end
270 |
271 | # 自分のJOIN受信時の処理
272 | def on_myjoin(channel)
273 | on_join(@irc_nick,channel)
274 | end
275 |
276 | # 自分のPART受信時の処理
277 | def on_mypart(channel)
278 | on_part(@irc_nick,channel)
279 | end
280 |
281 | # 自分のQUIT受信時の処理
282 | def on_myquit(mess)
283 | on_quit(@irc_nick,mess)
284 | end
285 |
286 | # 自分のKICK受信時の処理
287 | def on_mykick(channel,mess,kicker)
288 | on_kick(@irc_nick,channel,mess,kicker)
289 | end
290 |
291 | # NICK受信時の処理
292 | def on_nick(nick_old,nick_new)
293 | dispmess(nick_old,"NICK #{nick_new}")
294 | end
295 |
296 | # 自分がINVITEされた時の処理
297 | def on_myinvite(nick,channel)
298 | dispmess(nick,"INVITE #{channel}")
299 | end
300 |
301 | # エラーの時の処理
302 | def on_error(code)
303 | @disp.puts "Error: #{code}"
304 | sendmess("QUIT\r\n") # 面倒なので終了にしている
305 | end
306 | end
307 |
--------------------------------------------------------------------------------
/manual/reudyman.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ロイディ Ver.3.09 マニュアル --- SSB
5 |
6 |
7 |
8 |
9 |
10 | [戻る]
11 | [ロ技研のトップ]
12 | [ロ技研掲示板]
13 |
14 |
ロイディ Ver.3.09 マニュアル
15 |
16 |
17 | 目次
18 |
19 |
35 |
36 |
37 | Windowsの人は「Windowsでの準備から実行まで」も併せてどうぞ。
38 |
39 |
40 |
41 |
42 |
43 | 以前のバージョンからバージョンアップする場合は、 reudy309up.zip を解凍して、出てきたファイルを、以前のバージョンのファイルに上書きコピーしてください。
44 |
45 |
46 |
47 | Ver.3.04.1以前からバージョンアップする時には、自動でバックアップが取られるはずです。が、心配な人は public ディレクトリを手動でバックアップしておきましょう。
48 |
49 |
50 |
51 | まだ安定してないので、色々仕様が変わってすいません(汗)。当てはまる(●が付いている)注意点をよく読んでください。
52 |
53 |
54 |
55 |
56 | | 以前のバージョン | 注意点 |
57 | | 3.01以下 | 3.02 | 3.03 | 3.04.x | 3.05〜 |
58 | | ● | ● | ● | | ログを手動で追加したい時は、 public/log.txt に直接追加すればいいようになりました。 add_log.rb を使う必要は有りません。また、 public/log.txt を編集した時に、 public/setting.txt に remake_word_to_message_list を指定する必要も無くなりました。「ログを追加/編集する」参照。 |
59 | | ● | ● | ● | ● | | Ruby/GDBM を使うようになりました。無くても動きますが、メモリ消費が激しくなります。「必要なもの」参照。 |
60 | | ● | ● | ● | ● | | ログの中で、 < > & を < > & と書く必要は無くなりました。 今までのログの中の < > & は、Ver.3.05以降での最初の起動時に、自動で < > & に変換されます。 |
61 | | ● | ● | ● | ● | | public/words.txt のフォーマットが変わりました。Ver.3.05以降での最初の起動時に、自動的にフォーマットが変更されます。「単語を追加する」参照。 |
62 | | ● | ● | ● | | | Ruby 1.8.1で動作確認するようになりました。Ruby 1.7以下では動かないかもしれません。 |
63 | | ● | | | | | public/setting.txt の必須項目に nicks が追加されました。「設定」参照。 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | - Ruby 1.8.x。必須。Ruby 1.9.xでは動作しません。Windowsでのインストール方法はこちら。
71 | - Ruby/GDBM。強く推奨。無いとメモリを大量消費します。*1*2Windowsでのインストール方法はこちら。
72 | - それなりのメモリ。*1
73 | - ロイディのソース。
74 |
75 |
76 |
77 | *1 ログ約10万行(約4MB)、単語約1万語の場合、GDBM有りなら約10MB、GDBM無しなら約90MB消費。
78 | *2 mingw版/mswin32版のRubyでは、Ruby/GDBMが無いとエラーダイアログが出ます。Ruby/GDBMを入れるか、Rubyのディレクトリの中から gdbm.so を探して削除してください。
79 |
80 |
81 |
82 |
83 |
84 | ここではIRCでロイディを動かす方法を説明します。
85 |
86 |
87 |
88 | まず、 public/setting.txt を編集します。 host 、 port にIRCサーバのホスト名とポート番号を、 channel と info_channel にロイディを動かしたいチャンネルの名前を指定します。 public/setting.txt の各項目についての詳細は「設定」を見てください。
89 |
90 |
91 |
92 | 設定が終わったら、ターミナル(やコマンドプロンプト)で
93 |
94 |
95 |
96 | ruby irc_reudy.rb public
97 |
98 |
99 |
100 | と入力してください。*5
101 |
102 |
103 |
104 |
105 | ログロード中...
106 | 単語ロード中...
107 | 接続開始...
108 |
109 |
110 |
111 |
112 | となれば、成功です。 public/setting.txt で指定したチャンネルにLimeChatやChocoaなどのIRCクライアントでアクセスして、会話をお楽しみください。といっても、最初のうちは全然まともにしゃべれませんが…。
113 |
114 |
115 |
116 | ロイディを終了するには、ターミナルでCtrl+Cを押すか、IRC上で「ロイディ、終了しなさい」と発言してください。
117 |
118 |
119 |
120 | コマンドで指定した public というのは、 settng.txt とかを含むディレクトリ名です。 public 以外のディレクトリを作って、複数の設定/記憶を使い分ける事ができます。
121 |
122 |
123 |
124 |
125 | *5 irc_reudy.rb を含むディレクトリに移動(cd)して実行しないとエラーになります。
126 |
127 |
128 |
129 |
130 |
131 |
132 | public/setting.txt には、各行に「項目名」と「値」を半角スペース(or タブ)で区切って書きます。文字コードはEUCにしてください。
133 |
134 |
135 |
136 | ★の項目は必ず設定してください。それ以外はそのままでもとりあえず動きます。
137 |
138 |
139 |
140 |
IRC専用の設定項目
141 |
142 |
143 |
144 |
145 | | 項目名 | 意味 | 値の例 |
146 | | host★ | IRCサーバ名。 | irc.fujisawa.wide.ad.jp |
147 | | port★ | IRCサーバのポート。 | 6667 |
148 | | encoding★ | IRCサーバの文字コード。LimeChatで同じサーバにログインするときに、サーバのプロパティの[文字コード]にISO-2022-JP(デフォルト)を指定している場合は JIS 、UTF-8を指定している場合は UTF8 と書いてください。 | JIS |
149 | | channel★ | ロイディが会話をするチャンネル。メインチャンネルと呼びます。 | #reudy_test |
150 | | info_channel★ | ロイディが覚えた単語が流れるチャンネル。中の人モードでも使います。Infoチャンネルと呼びます。 channel と同じでもいいです。 | #reudy_test |
151 | | login_password | IRCサーバのログインパスワード。Wideとかでは不要。 | (空欄) |
152 | | channel_key | パスワード付きのチャンネルに入る場合に必要。login_password(サーバに入るのに必要なパスワード)とは違うので注意してください。 | (空欄) |
153 | | respond_to_notice | trueなら、noticeメッセージにも反応する。 | true / false |
154 | | respond_to_external | trueなら、チャンネル外部のメッセージにも反応する。 | true / false |
155 | | speak_with_privmsg | trueなら、privmsgで話す。falseなら、noticeで話す。 | true / false |
156 | | auto_reconnect | trueなら、回線が切れたときに、自動で再接続する。 | true / false |
157 | | teacher_mode | 「中の人モード」を有効にするかどうか。 | true / false |
158 | | name | IRCの /whois で「名前」として表示されるもの。適当でいいです。 | Reudy |
159 | | real_name | IRCの /whois で「本名」として表示されるもの。適当でいいです。 | Bot Reudy |
160 |
161 |
162 |
163 |
164 |
共通の設定項目
165 |
166 |
167 |
168 |
169 | | nick | チャット用のニックネーム。IRCでは、他の人とかぶるとログインできません。 | MyReudy |
170 | | nicks | 自分の名前として認識する単語。半角カンマで区切って複数指定できます。 | Reudy,reudy,ロイディ |
171 | | default_mode | 最初のモード。0が沈黙モード、1が寡黙モード、2が通常モード、3が饒舌モード。 | 0 / 1 / 2 / 3 |
172 | | joining_message | 入室時のメッセージ。省略可。 | こんにちは。 |
173 | | leaving_message | 移動時の退室メッセージ。省略可。 | さようなら。 |
174 | | private_greeting | 入室した他人に個人宛てで送るメッセージ。省略可。 | こんにちは。 |
175 | | disable_commands | trueなら、「設定を更新」以外のコマンドを無効化。 | true / false |
176 | | disable_studying | trueなら、反応するだけで学習はしない。 | true / false |
177 | | target_nick | ニックネーム(Nick)を指定すると、その人の物まねをする。*6 | Gimite |
178 | | forbidden_nick | ここでニックネーム(Nick)を指定した人の発言は使わない。*6 | tobocchi|ProzaKc|hAnE |
179 | | wait_before_speak | 反応するまでに何秒 間(ま)を置くかの目安。小数も可。あまり短くするとサーバに蹴られる事が有ります。 | 2 |
180 | | wait_before_info | 単語記憶メッセージを流す間隔(秒)。小数も可。あまり短くするとサーバに蹴られる事が有ります。 | 0.2 |
181 |
182 |
183 |
184 |
185 | *6 正規表現を使えます。「正規表現について」参照。
186 |
187 |
188 |
189 |
190 |
191 | ロイディがいるチャンネルで発言すると、ロイディにコマンドを送れます。
192 |
193 |
194 |
195 |
196 | | コマンド(発言) | 効果 |
197 | | ロイディ、設定を更新。 | settings.txtを読み直します。ただし、更新できない設定も有ります。 |
198 | | ロイディ、沈黙モード。 | 沈黙モードになります。ほとんどしゃべりません。 |
199 | | ロイディ、寡黙モード。 | 寡黙モードになります。時々しゃべります。 |
200 | | ロイディ、通常モード。 | 通常モードになります。普通にしゃべります。 |
201 | | ロイディ、饒舌モード。 | 饒舌モードになります。しゃべりまくります。 |
202 | | ロイディ、「〜」を覚えさせたのは誰? | その単語を誰に教わったかを答えます。 |
203 | | ロイディ、〜のものまねをしなさい。 | その人のものまねを始めます。 |
204 | | ロイディ、終了しなさい。 | プログラムを終了します。 |
205 |
206 |
207 |
208 |
209 | 設定で nicks を書き換えた場合は、「ロイディ」の部分を読み替えてください。
210 |
211 |
212 |
213 | 設定で disable_commands を true にすると、「設定を更新」以外のコマンドが無効になります。
214 |
215 |
216 |
217 |
218 |
219 | ロイディのログは public/log.txt に入っています。各行が
220 |
221 |
222 |
223 | 発言者 タブ 発言内容
224 |
225 |
226 |
227 | となっています。このフォーマットを守れば、自由にログを追加、編集できます。文字コードはEUCにしてください。EUC以外で保存すると、データが壊れます。
228 |
229 |
230 |
231 | ロイディ実行中に public/log.txt をいじらないでください。ややこしい事態になります。
232 |
233 |
234 |
235 | ログをファイルの最後に追加すると、次の起動の時に
236 |
237 |
238 |
239 | public/log.txt に追加されたログを読み込み中...
240 |
241 |
242 |
243 | と表示されます。追加分について単語の抽出とかをするので、しばらく時間がかかります。
244 |
245 |
246 |
247 | public/log.txt の途中を変更すると、
248 |
249 |
250 |
251 | public/log.txt の途中が変更されています。内部データを作り直します...
252 |
253 |
254 |
255 | と表示されます。この場合は内部データを一から作り直すので、かなり時間がかかります。
256 |
257 |
258 |
259 |
260 |
261 | ロイディが覚えた単語は public/words.txt に入っています。Ver.3.05からフォーマットが変わって、単純に1行に1個の単語を書き並べたものになりました。
262 |
263 |
264 |
265 | ロイディは自動で単語を覚えますが、 public/words.txt に手動で書き足すのもOKです。文字コードはEUCにしてください。EUC以外で保存すると、データが壊れます。
266 |
267 |
268 |
269 | ロイディ実行中に public/words.txt をいじらないでください。ややこしい事態になります。
270 |
271 |
272 |
273 | public/words.txt を変更すると、
274 |
275 |
276 |
277 | public/words.txt が変更されたようです。単語を読み込み中...
278 |
279 |
280 |
281 | と表示されます。追加された単語を探しにいくので、しばらく時間がかかります。
282 |
283 |
284 |
285 | 現バージョンでは、単語の削除はできません。( public/words.txt から削除しても、内部のデータからは削除されずに残ります)。
286 |
287 |
288 |
289 |
290 |
291 | ロイディは起動時に public/log.txt が手動で変更されてないかどうかをチェックします。ログが大きくなってくると、これはそれなりに時間がかかります。
292 |
293 |
294 |
295 |
296 | ruby irc_reudy.rb -f public
297 |
298 |
299 |
300 |
301 | のように -f をつけると、このチェックを省略できます。
302 |
303 |
304 |
305 |
306 |
307 | この機能はVer.3.07で追加されました。また、今のところIRC版でのみ有効です。
308 |
309 |
310 |
311 | ロイディは通常のモードでは、人間同士の会話や、ロイディと人間との会話から、自動的に反応を学習します。
312 |
313 |
314 |
315 | これに対して「中の人モード」では、特定の「中の人」が、ロイディの発言を「修正」していくことで、ロイディに反応を覚えこませることができます。
316 |
317 |
318 |
319 | 中の人モードを有効にするには、設定の teacher_mode を true にします。
320 |
321 |
322 |
323 | 中の人がロイディの発言を修正するには、Infoチャンネルで発言します。例えば、こんな感じです。 channel が #reudyroom 、 info_channel が #reudyinfo になってるとします。
324 |
325 |
326 |
327 |
328 | <#reudyroom:Human1> こんちわ〜。
329 | <#reudyroom:RReudy> さようなら。
330 | <#reudyinfo:Teacher> こんにちは。
331 |
332 |
333 |
334 | これでロイディは「こんちわ〜。」→「こんにちは。」という反応のパターンを覚えました。
335 |
336 |
337 |
338 | この方法では、中の人はロイディの発言を「修正」するだけです。ロイディが反応しなかった発言に対する反応は登録できません。
339 |
340 |
341 |
342 | メインチャンネルの会話とは関係なく、ロイディに反応を覚えさせるには、次のようにします。
343 |
344 |
345 |
346 |
347 | <#reudyinfo:Teacher> こんちわ〜。→→こんにちは。
348 |
349 |
350 |
351 | 「→」記号を2つ続ける所に注意してください。
352 |
353 |
354 |
355 | teacher_mode を true にすると、
356 |
357 |
358 |
359 | - メインチャンネルの会話からの学習は無効になります。
360 | - ロイディの発言には、できるだけ中の人から教わった反応を使うようになります。過去にメインチャンネルで学習したデータは、原則として使いません。
361 |
362 |
363 |
364 |
365 |
366 | 設定の target_nick と forbidden_nick には、正規表現を使えます。詳しくはぐぐったりしてみてください。ここでは、簡単な使い方だけ。
367 |
368 |
369 |
370 | 複数のニックネームを指定するには、 | で区切ります。例えば、
371 |
372 |
373 |
374 | forbidden_nick RuThemis|ProzaKc
375 |
376 |
377 |
378 | は、「RuThemisとProzaKcの発言を使わない」という意味になります。
379 |
380 |
381 |
382 | 記号には、(上の | 以外にも)特別な意味を持つものが有ります。普通の記号として使うには、記号の前に \ を入れてください。例えば、 [DIABL0] さんの物まねをさせるには、
383 |
384 |
385 |
386 | target_nick \[DIABL0\]
387 |
388 |
389 |
390 | とします。
391 |
392 |
393 |
394 |
395 |
396 | ロイディのターミナルへの出力文字コードは、標準ではShift-JISです。これを変えるには、 irc_reudy.rb をエディタで開いて、6行目の
397 |
398 |
399 |
400 | $OUT_KCODE= "SJIS" #出力文字コード
401 |
402 |
403 |
404 | の "SJIS" の部分を書き換えます。 "SJIS" 以外に、 "EUC" と "UTF-8" と "JIS" を指定できます。
405 |
406 |
407 |
408 |
409 |
410 |
411 | | ファイル名 | 説明 |
412 | | irc_reudy.rb | このファイルを使ってIRC版ロイディを起動します。「IRC版の使い方」参照。 |
413 | | lib/* | ロイディの内部処理で使われるもの。直接は使いません。 |
414 | | public/setting.txt | ロイディの設定をここに書いてください。「設定」参照。 |
415 | | public/log.txt | ロイディが覚えた発言のログ。追加/編集できます。「ログを追加/編集する」参照。 |
416 | | public/words.txt | ロイディが覚えた単語のリスト。追加もできます。「単語を追加する」参照。 |
417 | | public/log.dat | 内部使用のファイル。中身は log.txt とほとんど同じですが、このファイルをいじってはいけません。データが壊れます。 |
418 | public/words.dat public/similar.gdbm public/version.dat | 内部使用のファイル。いじったり、消したりしないでください。 |
419 |
420 |
421 |
422 |
423 |
424 |
425 | 修正BSDライセンスです。つまり、著作権表示さえすれば、改造、再配布、これを使ったソフトの配布などは全て自由です。ただし、何も保証しないし、仮に何か起きても僕は責任を取りません。詳しくはLICENCE.txtをどうぞ。
426 |
427 |
428 |
429 | ただし、irc-client.rbだけはPublic Domain Softwareです。
430 |
431 |
432 |
433 |
434 |
435 | 質問、バグ、要望など有りましたら、掲示板までどうぞ。
436 |
437 |
438 |
439 | 元祖ロイディの居場所とロイディの解説は、こちら。
440 | http://www.rogiken.org/SSB/
441 |
442 |
443 |
444 |
Since 2003, Society for the Study of Botics
445 |
446 |
447 |
448 |
--------------------------------------------------------------------------------
/lib/reudy/reudy.rb:
--------------------------------------------------------------------------------
1 | #encoding:utf-8
2 | #Copyright (C) 2003 Gimite 市川
3 | #Modified by Glass_saga
4 |
5 | require $REUDY_DIR+'/wordset'
6 | require $REUDY_DIR+'/word_searcher'
7 | require $REUDY_DIR+'/message_log'
8 | require $REUDY_DIR+'/similar_searcher'
9 | require $REUDY_DIR+'/word_associator'
10 | require $REUDY_DIR+'/wtml_manager'
11 | require $REUDY_DIR+'/attention_decider'
12 | require $REUDY_DIR+'/response_estimator'
13 | require $REUDY_DIR+'/reudy_common'
14 | require 'yaml'
15 |
16 | unless Encoding.default_external == __ENCODING__
17 | STDOUT.set_encoding(Encoding.default_external, __ENCODING__)
18 | STDERR.set_encoding(Encoding.default_external, __ENCODING__)
19 | STDIN.set_encoding(Encoding.default_external, __ENCODING__)
20 | end
21 |
22 | module Gimite
23 | class Reudy
24 | include Gimite
25 |
26 | def initialize(dir, fixedSettings = {},db="pstore",mecab=nil)
27 | @attention = nil
28 |
29 | #設定を読み込む。
30 | @db = db #使用するDBの名前
31 | if mecab
32 | begin
33 | require $REUDY_DIR+'/tango-mecab' #単語の抽出にmecabを使用する
34 | rescue => ex
35 | warn ex.message
36 | require $REUDY_DIR+'/tango-mgm'
37 | end
38 | else
39 | require $REUDY_DIR+'/tango-mgm'
40 | end
41 | @fixedSettings = fixedSettings
42 | @settingPath = dir + '/setting.yml'
43 | @settings = {}
44 | loadSettings
45 | @autoSave = !@settings[:disable_auto_saving]
46 |
47 | #働き者のオブジェクト達を作る。
48 | @log = MessageLog.new(dir + '/log.yml')
49 | @log.addObserver(self)
50 | warn "ログロード終了"
51 | @wordSet = WordSet.new(dir + '/words.yml')
52 | @wordSearcher = WordSearcher.new(@wordSet)
53 | @wtmlManager = WordToMessageListManager.new(@wordSet, @log, @wordSearcher)
54 | @extractor = WordExtractor.new(14, method(:onAddWord))
55 | @simSearcher = SimilarSearcher.new(dir + '/db', @log,@db)
56 | @associator = WordAssociator.new(dir + '/assoc.txt')
57 | @attention = AttentionDecider.new
58 | @attention.setParameter(attentionParameters)
59 | @resEst = ResponseEstimator.new(@log, @wordSearcher, method(:isUsableBaseMsg), method(:canAdoptWord))
60 | warn "単語ロード終了"
61 | #その他インスタンス変数の初期化。
62 | @client = nil
63 | @lastSpeachInput = nil
64 | @lastSpeach = nil
65 | @inputWords = []
66 | @newInputWords = []
67 | @recentUnusedCt = 100 #最近n個の発言は対象としない
68 | @repeatProofCt = 50 #過去n発言で使ったベース発言は再利用しない
69 | @recentBaseMsgNs = Array.new(@repeatProofCt) #最近使ったベース発言番号
70 | @thoughtFile = open(dir + "/thought.txt", "a") #思考過程を記録するファイル
71 | @thoughtFile.sync = true
72 |
73 | setWordAdoptBorder
74 | end
75 |
76 | #設定をファイルからロード
77 | def loadSettings
78 | File.open(@settingPath) do |file|
79 | @settings = YAML.load(file)
80 | end
81 | @settings.merge!(@fixedSettings)
82 | #メンバ変数を更新
83 | @targetNickReg = Regexp.new(@settings[:target_nick] || "", Regexp::IGNORECASE)
84 | #これにマッチしないNickの発言は、ベース発言として使用不能
85 | if @settings[:forbidden_nick] && !@settings[:forbidden_nick].empty?
86 | @forbiddenNickReg = Regexp.new(@settings[:forbidden_nick], Regexp::IGNORECASE)
87 | else
88 | @forbiddenNickReg = /(?!)/o #何にもマッチしない正規表現
89 | end
90 | @myNicks = @settings[:nicks] #これにマッチするNickの発言は、ベース発言として使用不能
91 | @my_nicks_regexp = Regexp.new(@myNicks.map{|n| Regexp.escape(n) }.join("|"))
92 | changeMode(@settings[:default_mode].to_i) #デフォルトのモードに変更
93 | end
94 |
95 | #チャットクライアントの指定
96 | attr_accessor :client,:settings
97 |
98 | #モードを変更
99 | def changeMode(mode)
100 | return false if mode == @mode
101 | @mode = mode
102 | @attention.setParameter(attentionParameters) if @attention
103 | updateStatus
104 | return true
105 | end
106 |
107 | def updateStatus
108 | @client.status = ["沈黙", "寡黙", nil, "饒舌"][@mode] if @client
109 | end
110 |
111 | #注目判定器に与えるパラメータ。
112 | def attentionParameters
113 | case @mode
114 | when 0 #沈黙モード。
115 | return { \
116 | :min => 0.001, \
117 | :max => 0.001, \
118 | :default => 0.001, \
119 | :called => 0.001, \
120 | :self => 0.0, \
121 | :ignored => 0.0 \
122 | }
123 | when 1 #寡黙モード。
124 | return { \
125 | :min => 0.1, \
126 | :max => 0.3, \
127 | :default => 0.1, \
128 | :called => 1.1, \
129 | :self => 0.005, \
130 | :ignored => 0.002 \
131 | }
132 | when 2 #通常モード。
133 | return { \
134 | :min => 0.5, \
135 | :max => 1.1, \
136 | :default => 0.5, \
137 | :called => 1.1, \
138 | :self => 0.3, \
139 | :ignored => 0.002 \
140 | }
141 | when 3 #饒舌モード。
142 | return { \
143 | :min => 0.8, \
144 | :max => 1.1, \
145 | :default => 0.8, \
146 | :called => 1.1, \
147 | :self => 0.8, \
148 | :ignored => 0.01 \
149 | }
150 | when 4 #必ず応答するモード。
151 | return { \
152 | :min => 1.1, \
153 | :max => 1.1, \
154 | :default => 1.1, \
155 | :called => 1.1, \
156 | :self => 0.8, \
157 | :ignored => 0.003 \
158 | }
159 | end
160 | end
161 |
162 | #単語がこれより多く出現してたら置換などの対象にしない、という
163 | #ボーダを求めて@wordAdoptBorderに代入。
164 | def setWordAdoptBorder
165 | if @wordSet.words.empty?
166 | @wordAdoptBorder = 0
167 | else
168 | msgCts = @wordSet.words.map{|w| w.mids.size }
169 | msgCts.sort!
170 | msgCts.reverse!
171 | @wordAdoptBorder = msgCts[msgCts.size / 50]
172 | end
173 | end
174 |
175 | #その単語が置換などの対象になるか
176 | def canAdoptWord(word)
177 | word.mids.size < @wordAdoptBorder
178 | end
179 |
180 | #発言をベース発言として使用可能か。
181 | def isUsableBaseMsg(msgN)
182 | size = @log.size
183 | return false if msgN >= size #存在しない発言。
184 | msg = @log[msgN]
185 | return unless msg #空行。削除された発言など。
186 | return false if !@settings[:teacher_mode] && size > @recentUnusedCt && msgN >= size - @recentUnusedCt #発言が新しすぎる。(中の人モードでは無効)
187 | nick = msg.fromNick
188 | return false if nick == "!" #自分自身の発言。
189 | return false if !(nick =~ @targetNickReg) || nick =~ @forbiddenNickReg #この発言者の発言は使えない。
190 | return false if @recentBaseMsgNs.include?(msgN) #最近そのベース発言を使った。
191 | return true
192 | end
193 |
194 | #mid番目の発言への返事(と思われる発言)について、[発言番号,返事らしさ]を返す。
195 | #ただし、ベース発言として使用できるものだけが対象。
196 | #該当するものが無ければ[nil,0]を返す。
197 | def responseTo(mid, debug = false)
198 | if @settings[:teacher_mode]
199 | if isUsableBaseMsg(mid+1) && @log[mid].fromNick == "!input"
200 | return [mid+1, 20]
201 | else
202 | return [nil, 0]
203 | end
204 | else
205 | return @resEst.responseTo(mid, debug)
206 | end
207 | end
208 |
209 | #類似発言検索用のフィルタ
210 | def similarSearchFilter(msgN)
211 | !responseTo(msgN).first.nil?
212 | end
213 |
214 | #sentence中の自分のNickをtargetに置き換える。
215 | def replaceMyNicks(sentence, target)
216 | sentence.gsub(@my_nicks_regexp, target)
217 | end
218 |
219 | #入力文章から既知単語を拾う。
220 | def pickUpInputWords(input)
221 | input = replaceMyNicks(input, " ")
222 | @newInputWords = @wordSearcher.searchWords(input).select{ |w| canAdoptWord(w) } #入力に含まれる単語を列挙
223 | #入力に単語が無い場合は、時々入力語をランダムに変更
224 | if @newInputWords.empty? && rand(50).zero?
225 | word = @wordSet.words.sample
226 | @newInputWords.push(word) if canAdoptWord(word)
227 | end
228 | #連想される単語を追加
229 | assoc_words = @newInputWords.map{|w| @associator.associate(w.str) }
230 | assoc_words.compact!
231 | assoc_words.map!{|s| Word.new(s) }
232 | @newInputWords.concat(assoc_words)
233 | #入力語の更新
234 | unless @newInputWords.empty?
235 | if rand(5).nonzero?
236 | @inputWords.replace(@newInputWords)
237 | else
238 | @inputWords.concat(@newInputWords)
239 | end
240 | end
241 | end
242 |
243 | #「単語を除く文字数」から発言を採用するかを決める。
244 | #「単語だけ」に等しい発言は採用されにくいようにする。
245 | #単語が無い発言は確実に採用され、このメソッドは使われない。
246 | def shouldAdoptSaying(additionalLen)
247 | case additionalLen
248 | when 0
249 | false
250 | when 1
251 | rand < 0.125
252 | when 2, 3
253 | rand < 0.25
254 | when 4...7
255 | rand < 0.75
256 | else
257 | true
258 | end
259 | end
260 |
261 | #inputWords中の単語を含む各発言について、ブロックを繰り返す。
262 | #ブロックは発言番号を引数に取る。
263 | #発言の順序はランダム。
264 | def eachMsgContainingWords(input_words)
265 | input_words.shuffle.each do |word|
266 | word.mids.shuffle.each do |mid|
267 | yield(mid)
268 | end
269 | end
270 | end
271 |
272 | #共通の単語を持つ発言と、その返事の発言番号を返す。
273 | #適切なものが無ければ、[nil, nil]。
274 | def getBaseMsgUsingKeyword(inputWords)
275 | maxMid = maxResMid = nil
276 | maxProb = 0
277 | i = 0
278 | eachMsgContainingWords(inputWords) do |mid|
279 | resMid, prob = responseTo(mid, true)
280 | if resMid
281 | if prob > maxProb
282 | maxMid = mid
283 | maxResMid = resMid
284 | maxProb = prob
285 | end
286 | i += 1
287 | break if i >= 5
288 | end
289 | end
290 | dprint("共通単語発言", @log[maxMid].body) if maxMid
291 | return [maxMid, maxResMid]
292 | end
293 |
294 | #類似発言と、その返事の発言番号を返す。
295 | #適切なものが無ければ、[nil, nil]。
296 | def getBaseMsgUsingSimilarity(sentence)
297 | maxMid = maxResMid = nil
298 | maxProb = 0
299 | i = 0
300 | @simSearcher.eachSimilarMsg(sentence) do |mid|
301 | resMid, prob = responseTo(mid, true)
302 | if resMid
303 | if prob > maxProb
304 | maxMid = mid
305 | maxResMid = resMid
306 | maxProb = prob
307 | end
308 | i += 1
309 | break if i >= 5
310 | end
311 | end
312 | dprint("類似発言", @log[maxMid].body, maxProb) if maxMid
313 | return [maxMid, maxResMid]
314 | end
315 |
316 | #msgN番の発言を使ったベース発言の文字列。
317 | def getBaseMsgStr(msg_n)
318 | str = @log[msg_n].body
319 | str.replace($1) if str =~ /^(.*)[<>]/ && $1.size >= str.size / 2 #文の後半に[<>]が有れば、その後ろはカット。
320 | str
321 | end
322 |
323 | #base内の既知単語をnewWordsで置換したものを返す。
324 | #toForceがfalseの場合、短すぎる文章になってしまった場合はnilを返す。
325 | def replaceWords(base, new_words, toForce)
326 | #baseを単語の前後で分割してpartsにする。
327 | parts = [base]
328 | @wordSet.words.each do |word|
329 | next if word.str.empty?
330 | if @wordSearcher.hasWord(base, word) && canAdoptWord(word)
331 | newParts = []
332 | parts.each_with_index do |part,i|
333 | if (i % 2).zero?
334 | word_regexp = /^(.*?)#{Regexp.escape(word.str)}(.*)$/
335 | while part =~ word_regexp
336 | newParts.push($1, word.str)
337 | part = $2
338 | end
339 | end
340 | newParts.push(part)
341 | end
342 | parts = newParts
343 | end
344 | end
345 | #先頭から2番目以降の単語の直前でカットしたりしなかったり。
346 | wordCt = (parts.size-1) / 2
347 | if parts.size > 1
348 | cutPos = rand(wordCt) * 2 + 1
349 | parts.replace(parts[cutPos..-1].unshift("")) if cutPos > 1
350 | end
351 | #単語を除いた文章が短すぎるものはある確率で却下。
352 | if wordCt.nonzero? && !toForce && !shouldAdoptSaying(sigma(0...parts.size){|i| (i % 2).zero? ? parts[i].size : 0 })
353 | return nil
354 | end
355 | #単語を置換。
356 | new_words.shuffle.each do |new_word|
357 | new_word_str = new_word.str
358 | old_word_str = parts[rand(wordCt)*2+1]
359 | 0.upto(wordCt-1) do |j|
360 | parts[j*2+1] = new_word_str if parts[j*2+1] == old_word_str
361 | end
362 | break if rand < 0.5
363 | end
364 | output = parts.join
365 | #閉じ括弧が残った場合に開き括弧を補う。
366 | #入れ子になってたりしたら知らない。
367 | case output
368 | when /^[^「」]*」/
369 | output.replace("「#{output}")
370 | when /^[^()]*)/
371 | output.replace("(#{output}")
372 | when /^[^()]*\)/
373 | output.replace("(#{output}")
374 | end
375 | return output
376 | end
377 |
378 | #自由発言の選び方を記録する。
379 | def recordThought(pattern, simMid, resMid, words, output)
380 | @thoughtFile.puts [@log.size-1, pattern, simMid, resMid, words.map{ |w| w.str }.join(","), output].join("\t")
381 | end
382 |
383 | #自由に発言する。
384 | def speakFreely(fromNick, origInput, mustRespond)
385 | input = replaceMyNicks(origInput, " ")
386 | output = nil
387 | simMsgN, baseMsgN = getBaseMsgUsingSimilarity(input) #まず、類似性を使ってベース発言を求める。
388 | if !@newInputWords.empty?
389 | if baseMsgN
390 | output = replaceWords(getBaseMsgStr(baseMsgN), @inputWords, mustRespond)
391 | recordThought(1, simMsgN, baseMsgN, @newInputWords, output) if output
392 | else
393 | simMsgN, baseMsgN = getBaseMsgUsingKeyword(@newInputWords)
394 | output = getBaseMsgStr(baseMsgN) if baseMsgN
395 | recordThought(2, simMsgN, baseMsgN, @newInputWords, output) if output
396 | end
397 | else
398 | if baseMsgN
399 | output = getBaseMsgStr(baseMsgN)
400 | unless @wordSearcher.searchWords(output).empty?
401 | if mustRespond
402 | output = replaceWords(output, @inputWords, true)
403 | else
404 | output = nil
405 | end
406 | end
407 | recordThought(3, simMsgN, baseMsgN, @inputWords, output) if output
408 | else
409 | if mustRespond && !@inputWords.empty?
410 | simMsgN, baseMsgN = getBaseMsgUsingKeyword(@inputWords) #最新でない入力語も使ってキーワード検索。
411 | output = getBaseMsgStr(baseMsgN) if baseMsgN
412 | recordThought(4, simMsgN, baseMsgN, @inputWords, output) if output
413 | end
414 | end
415 | end
416 | if mustRespond && !output
417 | log_size = @log.size
418 | 2000.times do
419 | msgN = rand(log_size)
420 | if isUsableBaseMsg(msgN)
421 | baseMsgN = msgN
422 | output = getBaseMsgStr(baseMsgN)
423 | break
424 | end
425 | end
426 | end
427 | if output
428 | #最近使ったベース発言を更新
429 | @recentBaseMsgNs.shift
430 | @recentBaseMsgNs.push(baseMsgN)
431 | output = replaceMyNicks(output, fromNick) #発言中の自分のNickを相手のNickに変換
432 | speak(origInput, output) #実際に発言。
433 | end
434 | end
435 |
436 | #自由発話として発言する。
437 | def speak(input, output)
438 | @lastSpeachInput = input
439 | @lastSpeach = output
440 | studyMsg("!", output) #自分の発言を記憶する。
441 | @client.outputInfo("「#{input}」に反応した。") if @settings[:teacher_mode]
442 | @attention.onSelfSpeak(@wordSearcher.searchWords(output))
443 | @client.speak(output)
444 | end
445 |
446 | #定型コマンドを処理。
447 | #入力が定型コマンドであれば応答メッセージを返す。
448 | #そうでなければnilを返す。ただし、終了コマンドだったら:exitを返す。
449 | def processCommand(input)
450 | if input =~ /設定を更新/
451 | loadSettings
452 | return "設定を更新しました。"
453 | end
454 | return nil if @settings[:disable_commands] #コマンドが禁止されている場合
455 | case input
456 | when /黙れ|黙りなさい|黙ってろ|沈黙モード/
457 | return changeMode(0) ? "沈黙モードに切り替える。" : ""
458 | when /寡黙モード/
459 | return changeMode(1) ? "寡黙モードに切り替える。" : ""
460 | when /通常モード/
461 | return changeMode(2) ? "通常モードに切り替える。" : ""
462 | when /饒舌モード/
463 | return changeMode(3) ? "饒舌モードに切り替える。" : ""
464 | when /休んで良いよ|終了しなさい/
465 | save
466 | @client.exit
467 | return :exit
468 | when /([\x21-\x7e]+)の(?:もの|モノ|物)(?:まね|真似)/ #半角文字を抽出する正規表現
469 | begin
470 | @targetNickReg = Regexp.new($1, Regexp::IGNORECASE)
471 | return "#{$1}のものまねを開始する。"
472 | rescue RegexpError
473 | return "正規表現が間違っている。"
474 | end
475 | when /(?:もの|モノ|物)(?:まね|真似).*(?:解除|中止|終了|やめろ|やめて)/
476 | @targetNickReg = /(?!)/
477 | return "物まねを解除する。"
478 | end
479 | if input =~ /覚えさせた|教わった/ && input.include?("誰") && input =~ /「(.+?)」/
480 | wordStr = $1
481 | if wordIdx = @wordSet.words.index(Word.new(wordStr))
482 | author = @wordSet.words[wordIdx].author
483 | if !author.empty?
484 | return "#{author}さんに。>#{wordStr}"
485 | else
486 | return "不確定だ。>#{wordStr}"
487 | end
488 | else
489 | return "その単語は記憶していない。"
490 | end
491 | end
492 | return nil #定型コマンドではない。
493 | end
494 |
495 | #通常の発言を学習。
496 | def studyMsg(fromNick, input)
497 | return if @settings[:disable_studying]
498 | if @settings[:teacher_mode]
499 | @fromNick = fromNick
500 | @extractor.processLine(input) #単語の抽出のみ。
501 | else
502 | @log.addMsg(fromNick, input)
503 | end
504 | end
505 |
506 | #学習内容を手動保存
507 | def save
508 | @wordSet.save
509 | end
510 |
511 | #ログに発言が追加された。
512 | def onAddMsg
513 | msg = @log[-1]
514 | @fromNick = msg.fromNick unless msg.fromNick == "!"
515 | @extractor.processLine(msg.body) unless @settings[:teacher_mode] #中の人モードでは、単語の抽出は別にやる。
516 | #@extractor以外のオブジェクトは自力で@logを監視しているので、
517 | #ここで何かする必要は無い。
518 | end
519 |
520 | #ログがクリアされた。
521 | def onClearLog
522 | end
523 |
524 | #単語が追加された
525 | def onAddWord(wordStr)
526 | if @wordSet.addWord(wordStr, @fromNick)
527 | if @client
528 | @client.outputInfo("単語「#{wordStr}」を記憶した。")
529 | else
530 | puts "単語「#{wordStr}」を記憶した。"
531 | end
532 | @wordSet.save if @autoSave
533 | end
534 | end
535 |
536 | #接続を開始した
537 | def onBeginConnecting
538 | warn "接続開始..."
539 | end
540 |
541 | #自分が入室した
542 | def onSelfJoin
543 | updateStatus
544 | end
545 |
546 | #他人が入室した
547 | def onOtherJoin(fromNick)
548 | end
549 |
550 | #他人が発言した。
551 | def onOtherSpeak(from_nick, input, should_ignore = false)
552 | output = nil #発言。
553 | called = @myNicks.any?{|n| input.include?(n) }
554 | output = called ? processCommand(input) : nil
555 | if output
556 | @client.speak(output) if output != :exit && !output.empty?
557 | else #定型コマンドではない。
558 | @lastSpeach = input
559 | studyMsg(from_nick, input)
560 | pickUpInputWords(input)
561 | prob = @attention.onOtherSpeak(from_nick, input, called)
562 | dprint("発言率", prob, @attention.to_s) #発言率を求める。
563 | speakFreely(from_nick, input, prob > 1.0) if (!should_ignore && rand < prob) || prob > 1.0 #自由発話。
564 | end
565 | end
566 |
567 | #制御発言(infoでの発言)があった。
568 | def onControlMsg(str)
569 | return if @settings[:disable_studying] || !@settings[:teacher_mode]
570 | if str =~ /^(.+)→→(.+)$/
571 | input = $1
572 | output = $2
573 | else
574 | input = @lastSpeachInput
575 | output = str
576 | end
577 | if input
578 | @log.addMsg("!input", input)
579 | @log.addMsg("!teacher", output)
580 | @client.outputInfo("反応「#{input}→→#{output}」を学習した。" ) if @client
581 | end
582 | end
583 |
584 | #沈黙がしばらく続いた。
585 | def onSilent
586 | prob = @attention.onSilent
587 | if rand < prob && @lastSpeach
588 | speakFreely(@fromNick, @lastSpeach, prob > rand * 1.1) #自発発言。
589 | #自発発言では、発言が無い限り、同じ発言を対象にしつづける。
590 | #このせいで全然しゃべらなくなるのを防ぐため、時々mustRespondをONにする。
591 | end
592 | end
593 | end
594 | end
595 |
--------------------------------------------------------------------------------