nadoka 0.8.0
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 +5 -0
- data/ChangeLog.old +1553 -0
- data/Gemfile +4 -0
- data/README.org +31 -0
- data/Rakefile +1 -0
- data/bin/nadoka +13 -0
- data/lib/rss_check.rb +206 -0
- data/lib/tagparts.rb +206 -0
- data/nadoka.gemspec +29 -0
- data/nadoka.rb +123 -0
- data/nadokarc +267 -0
- data/ndk/bot.rb +241 -0
- data/ndk/client.rb +288 -0
- data/ndk/config.rb +571 -0
- data/ndk/error.rb +61 -0
- data/ndk/logger.rb +311 -0
- data/ndk/server.rb +784 -0
- data/ndk/server_state.rb +324 -0
- data/ndk/version.rb +44 -0
- data/plugins/autoawaybot.nb +66 -0
- data/plugins/autodumpbot.nb +227 -0
- data/plugins/autoop.nb +56 -0
- data/plugins/backlogbot.nb +88 -0
- data/plugins/checkbot.nb +64 -0
- data/plugins/cronbot.nb +20 -0
- data/plugins/dictbot.nb +53 -0
- data/plugins/drbcl.rb +39 -0
- data/plugins/drbot.nb +93 -0
- data/plugins/evalbot.nb +49 -0
- data/plugins/gonzuibot.nb +41 -0
- data/plugins/googlebot.nb +345 -0
- data/plugins/identifynickserv.nb +43 -0
- data/plugins/mailcheckbot.nb +0 -0
- data/plugins/marldiabot.nb +99 -0
- data/plugins/messagebot.nb +96 -0
- data/plugins/modemanager.nb +150 -0
- data/plugins/opensearchbot.nb +156 -0
- data/plugins/opshop.nb +23 -0
- data/plugins/pastebot.nb +46 -0
- data/plugins/roulettebot.nb +33 -0
- data/plugins/rss_checkbot.nb +121 -0
- data/plugins/samplebot.nb +24 -0
- data/plugins/sendpingbot.nb +17 -0
- data/plugins/shellbot.nb +59 -0
- data/plugins/sixamobot.nb +77 -0
- data/plugins/tenkibot.nb +111 -0
- data/plugins/timestampbot.nb +62 -0
- data/plugins/titlebot.nb +226 -0
- data/plugins/translatebot.nb +301 -0
- data/plugins/twitterbot.nb +138 -0
- data/plugins/weba.nb +209 -0
- data/plugins/xibot.nb +113 -0
- data/rice/irc.rb +780 -0
- metadata +102 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
# -*-ruby-*-
|
2
|
+
# nadoka-twit
|
3
|
+
#
|
4
|
+
# = Usage
|
5
|
+
#
|
6
|
+
# == Get consumer key
|
7
|
+
#
|
8
|
+
# 1. access https://twitter.com/apps/new
|
9
|
+
# 2. register it
|
10
|
+
# 3. memo 'Consumer key' and 'Consumer secret'
|
11
|
+
#
|
12
|
+
# == Get access token
|
13
|
+
#
|
14
|
+
# 1. run this script with consumer key and consumer secret like:
|
15
|
+
# ruby twitterbot.nb <consumer_key> <consumer_secret>
|
16
|
+
# 2. memo access_token and access_token_secret
|
17
|
+
#
|
18
|
+
# == Setting nadokarc
|
19
|
+
#
|
20
|
+
# 1. set :consumer_key, :consumer_secret, :access_token,
|
21
|
+
# and :acccess_token_secret
|
22
|
+
#
|
23
|
+
# = Configuration
|
24
|
+
#
|
25
|
+
# == :ch
|
26
|
+
#
|
27
|
+
# target channel
|
28
|
+
#
|
29
|
+
# == :pattern
|
30
|
+
#
|
31
|
+
# pattern for messages to send twitter
|
32
|
+
#
|
33
|
+
# == :nkf_encoding
|
34
|
+
#
|
35
|
+
# the encoding of messages
|
36
|
+
#
|
37
|
+
# == :consumer_key, :consumer_secret
|
38
|
+
#
|
39
|
+
# Consumer key and consumer secret
|
40
|
+
#
|
41
|
+
# == :access_token, :acccess_token_secret
|
42
|
+
#
|
43
|
+
# Access token and access token secret
|
44
|
+
#
|
45
|
+
require 'time'
|
46
|
+
require 'rubygems'
|
47
|
+
require 'rubytter'
|
48
|
+
require 'json'
|
49
|
+
|
50
|
+
if __FILE__ == $0
|
51
|
+
key = ARGV.shift
|
52
|
+
secret = ARGV.shift
|
53
|
+
unless key && secret
|
54
|
+
puts "Usage: #$0 <consumer_key> <consumer_secret>"
|
55
|
+
end
|
56
|
+
|
57
|
+
oauth = Rubytter::OAuth.new(key, secret)
|
58
|
+
request_token = oauth.get_request_token
|
59
|
+
system('open', request_token.authorize_url) || puts("Access here: #{request_token.authorize_url}\nand...")
|
60
|
+
|
61
|
+
print "Enter PIN: "
|
62
|
+
pin = gets.strip
|
63
|
+
|
64
|
+
access_token = request_token.get_access_token(
|
65
|
+
:oauth_token => request_token.token,
|
66
|
+
:oauth_verifier => pin
|
67
|
+
)
|
68
|
+
puts ":access_token => '#{access_token.token}',"
|
69
|
+
puts ":access_token_secret => '#{access_token.secret}',"
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
|
73
|
+
class TwitterBot < Nadoka::NDK_Bot
|
74
|
+
def bot_initialize
|
75
|
+
@ch = @bot_config.fetch(:ch, nil)
|
76
|
+
@pattern = @bot_config.fetch(:pattern, />tw$/)
|
77
|
+
@nkf_encoding = @bot_config.fetch(:nkf_encoding, nil)
|
78
|
+
|
79
|
+
consumer = OAuth::Consumer.new(
|
80
|
+
@bot_config.fetch(:consumer_key, nil),
|
81
|
+
@bot_config.fetch(:consumer_secret, nil),
|
82
|
+
:site => 'https://api.twitter.com')
|
83
|
+
access_token = OAuth::AccessToken.new(consumer,
|
84
|
+
@bot_config.fetch(:access_token, nil),
|
85
|
+
@bot_config.fetch(:access_token_secret, nil))
|
86
|
+
@rt = OAuthRubytter.new(access_token)
|
87
|
+
@current_id = -1
|
88
|
+
end
|
89
|
+
|
90
|
+
def on_timer(t)
|
91
|
+
@rt.friends_timeline.each do |status|
|
92
|
+
id = status.id.to_i
|
93
|
+
next unless @current_id < id
|
94
|
+
@current_id = id
|
95
|
+
time = Time.parse(status.created_at)
|
96
|
+
next if time + 5 * 60 < Time.now
|
97
|
+
text = status.text.tr("\r\n", ' ')
|
98
|
+
text = NKF.nkf('--ic=UTF-8 --oc=' + @nkf_encoding, text) if @nkf_encoding
|
99
|
+
send_notice @ch, "#{time.strftime('%H:%M')} #{status.user.screen_name}: #{text}"
|
100
|
+
end
|
101
|
+
rescue Errno::ETIMEDOUT, Timeout::Error, SocketError
|
102
|
+
rescue Exception => err
|
103
|
+
puts_error_message(err)
|
104
|
+
end
|
105
|
+
|
106
|
+
def on_client_privmsg(client, ch, message)
|
107
|
+
return unless @ch.nil? or @ch.upcase == ch.upcase
|
108
|
+
unless @pattern =~ message
|
109
|
+
slog 'pattern unmatch, ignored'
|
110
|
+
return
|
111
|
+
end
|
112
|
+
text = message.sub(@pattern, '')
|
113
|
+
text = NKF.nkf('--oc=UTF-8 --ic=' + @nkf_encoding, text) if @nkf_encoding
|
114
|
+
slog((@rt.update(text) ? 'sent to twitter: ' : 'twitter send faild: ') + message)
|
115
|
+
rescue Exception => err
|
116
|
+
puts_error_message(err)
|
117
|
+
end
|
118
|
+
|
119
|
+
def slog(msg, nostamp = false)
|
120
|
+
current_method = caller.first[/:in \`(.*?)\'/, 1].to_s
|
121
|
+
msg.each do |line|
|
122
|
+
@logger.slog "#{self.class.to_s}##{current_method} #{line}", nostamp
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def puts_error_message(err)
|
128
|
+
if err.is_a?(Rubytter::APIError)
|
129
|
+
@logger.slog "%s: %s (%s) %s" % [err.backtrace[0], err.message, err.class, err.response]
|
130
|
+
elsif err.is_a?(JSON::ParserError)
|
131
|
+
sleep 180
|
132
|
+
return
|
133
|
+
else
|
134
|
+
@logger.slog "%s: %s (%s)" % [err.backtrace[0], err.message, err.class]
|
135
|
+
end
|
136
|
+
@logger.slog err.backtrace.select{|l|/\A\/home/=~l}
|
137
|
+
end
|
138
|
+
end
|
data/plugins/weba.nb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# -*-ruby-*-
|
2
|
+
#
|
3
|
+
# Copyright (c) 2004-2005 SASADA Koichi <ko1 at atdot.net>
|
4
|
+
#
|
5
|
+
# This program is free software with ABSOLUTELY NO WARRANTY.
|
6
|
+
# You can re-distribute and/or modify this program under
|
7
|
+
# the same terms of the Ruby's license.
|
8
|
+
#
|
9
|
+
#
|
10
|
+
# $Id$
|
11
|
+
#
|
12
|
+
|
13
|
+
=begin
|
14
|
+
|
15
|
+
== Abstract
|
16
|
+
|
17
|
+
WebA: Web Accessor
|
18
|
+
http interface for irc
|
19
|
+
|
20
|
+
You can access IRC via ((<http://host:port/weba>))
|
21
|
+
(by default).
|
22
|
+
|
23
|
+
|
24
|
+
== Configuration
|
25
|
+
|
26
|
+
BotConfig = [
|
27
|
+
{
|
28
|
+
:name => :WebA,
|
29
|
+
:passwd => 'WebAPassWord', # if passwd is specified, use Basic Access Authentication with id
|
30
|
+
:id => 'weba',
|
31
|
+
:port => 12345,
|
32
|
+
:entry => 'weba',
|
33
|
+
# :message_format => ... (see source)
|
34
|
+
}
|
35
|
+
]
|
36
|
+
|
37
|
+
=end
|
38
|
+
|
39
|
+
require 'webrick'
|
40
|
+
require 'lib/tagparts'
|
41
|
+
|
42
|
+
class WebA < Nadoka::NDK_Bot
|
43
|
+
class WebAlet < WEBrick::HTTPServlet::AbstractServlet
|
44
|
+
def initialize server, bot, authorizer
|
45
|
+
super
|
46
|
+
@bot = bot
|
47
|
+
@auth = authorizer
|
48
|
+
end
|
49
|
+
|
50
|
+
def do_GET req, res
|
51
|
+
@auth.authenticate(req, res) if @auth
|
52
|
+
begin
|
53
|
+
res.body = @bot.htmlpage(req.query).to_s
|
54
|
+
res['content-type'] = 'text/html; charset=Shift_JIS'
|
55
|
+
rescue WebARedirect => e
|
56
|
+
res.set_redirect(WEBrick::HTTPStatus::Found, "#{req.path}?ch=#{URI.encode(e.ch.tosjis)}")
|
57
|
+
res.body = 'moved'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class WebARedirect < Exception
|
63
|
+
attr_reader :ch
|
64
|
+
def initialize ch
|
65
|
+
@ch = ch
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
include HTMLParts
|
70
|
+
|
71
|
+
def htmlpage query
|
72
|
+
rch = (query['ch'] || '').tojis
|
73
|
+
ch = ccn(rch)
|
74
|
+
ch = !ch.empty? && (@state.channels.include?(ch) || ch == 'all') && ch
|
75
|
+
|
76
|
+
ttl = ch ? " - #{rch.tosjis}" : ''
|
77
|
+
|
78
|
+
if ch && (msg = query['message']) && !msg.empty?
|
79
|
+
msg = msg.tojis + ' (from WebA)'
|
80
|
+
send_privmsg(ch, msg)
|
81
|
+
raise WebARedirect.new(ch)
|
82
|
+
end
|
83
|
+
|
84
|
+
_html(
|
85
|
+
_head(_title("WebA: IRC Web Accessor#{ttl}")),
|
86
|
+
_body(
|
87
|
+
_h1("WebA#{ttl}"),
|
88
|
+
_p(
|
89
|
+
_a({:href => "./#{@entry}?ch="+URI.encode((rch || '').tosjis)}, 'reload'),
|
90
|
+
_a({:href => "./#{@entry}"}, 'top')
|
91
|
+
),
|
92
|
+
|
93
|
+
case ch
|
94
|
+
when 'command'
|
95
|
+
command_ch
|
96
|
+
else
|
97
|
+
view_ch(rch, ch)
|
98
|
+
select_ch(rch, ch)
|
99
|
+
end
|
100
|
+
))
|
101
|
+
end
|
102
|
+
|
103
|
+
def select_ch rch, ch
|
104
|
+
_p({:class => 'channel-list'},
|
105
|
+
(@state.channel_raw_names.sort + ['all']).map{|e|
|
106
|
+
e = e.tosjis
|
107
|
+
[_a({:href => "./#{@entry}?ch="+ URI.encode(e)}, e), ' ']
|
108
|
+
}
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def view_ch rch, ch
|
113
|
+
return unless ch
|
114
|
+
|
115
|
+
chs = ch.tosjis
|
116
|
+
|
117
|
+
if ch == 'all'
|
118
|
+
msgs = []
|
119
|
+
@stores.pools.each{|_, store|
|
120
|
+
msgs.concat store.pool
|
121
|
+
}
|
122
|
+
msgs = msgs.sort_by{|msg| msg[:time]}
|
123
|
+
else
|
124
|
+
msgs = (@stores.pools[ch] && @stores.pools[ch].pool) || []
|
125
|
+
end
|
126
|
+
|
127
|
+
_div({:class => 'irc-accessor'},
|
128
|
+
if(ch != 'all')
|
129
|
+
_form({:method => 'get', :action => "./#{@entry}", :class => 'sayform'},
|
130
|
+
"msg: ",
|
131
|
+
_input({:type => 'text', :name => 'message', :class => 'msgbox'}),
|
132
|
+
_input({:type => 'submit', :name => 'say', :value => 'say'}),
|
133
|
+
_input({:type => 'hidden', :name => 'ch', :value => ch})
|
134
|
+
)
|
135
|
+
end,
|
136
|
+
_h2("channel #{ch.tosjis}"),
|
137
|
+
_div({:class => 'messages'},
|
138
|
+
msgs.map{|m|
|
139
|
+
if ch == 'all' && m[:ch]
|
140
|
+
chn = _a({:href => "./#{@entry}?ch=#{URI.encode(m[:ch])}"}, m[:ch].tosjis)
|
141
|
+
msg = @config.log_format_message(@all_message_format, m)
|
142
|
+
elsif m[:type] == 'PRIVMSG'
|
143
|
+
chn = _a({:href => "./#{@entry}?user=#{URI.encode(m[:user])}"}, "<#{m[:user].tosjis}>")
|
144
|
+
msg = @config.log_format_message(@message_format, m)
|
145
|
+
else
|
146
|
+
msg = @config.log_format_message(@message_format, m)
|
147
|
+
chn = ''
|
148
|
+
end
|
149
|
+
|
150
|
+
_div({:class=>'msg'},
|
151
|
+
"#{m[:time].strftime('%H:%M')} ", chn, "#{msg}".tosjis)
|
152
|
+
}.reverse
|
153
|
+
)
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
def bot_initialize
|
158
|
+
@stores = @logger.message_stores
|
159
|
+
@server = WEBrick::HTTPServer.new({
|
160
|
+
:Port => @bot_config.fetch(:port, 12345),
|
161
|
+
})
|
162
|
+
@entry = @bot_config.fetch(:entry, 'weba')
|
163
|
+
|
164
|
+
auth = nil
|
165
|
+
if passwd = @bot_config.fetch(:passwd, 'WebAPassWord')
|
166
|
+
id = @bot_config.fetch(:id, 'weba')
|
167
|
+
|
168
|
+
userdb = Hash.new
|
169
|
+
userdb.extend(WEBrick::HTTPAuth::UserDB)
|
170
|
+
userdb.auth_type = WEBrick::HTTPAuth::BasicAuth
|
171
|
+
userdb.set_passwd("WebA Authentication", id, passwd)
|
172
|
+
|
173
|
+
auth = WEBrick::HTTPAuth::BasicAuth.new({
|
174
|
+
:Realm => "WebA Authentication",
|
175
|
+
:UserDB => userdb,
|
176
|
+
:Algorithm => 'MD5-sess',
|
177
|
+
:Qop => [ 'auth' ],
|
178
|
+
:UseOpaque => true,
|
179
|
+
:NonceExpirePeriod => 60,
|
180
|
+
:NonceExpireDelta => 5,
|
181
|
+
})
|
182
|
+
end
|
183
|
+
|
184
|
+
@server.mount("/#{@entry}", WebAlet, self, auth)
|
185
|
+
|
186
|
+
@server_thread = Thread.new{
|
187
|
+
begin
|
188
|
+
@server.start
|
189
|
+
rescue Exception => e
|
190
|
+
@manager.ndk_error e
|
191
|
+
end
|
192
|
+
}
|
193
|
+
@message_format = @config.default_log[:message_format].merge(
|
194
|
+
@bot_config.fetch(:message_format, {
|
195
|
+
'PRIVMSG' => '{msg}',
|
196
|
+
'NOTICE' => '{{user}} {msg}',
|
197
|
+
}))
|
198
|
+
|
199
|
+
@all_message_format = @config.default_log[:message_format].merge(
|
200
|
+
@bot_config.fetch(:all_message_format, {}))
|
201
|
+
end
|
202
|
+
|
203
|
+
def bot_destruct
|
204
|
+
@server_thread.kill
|
205
|
+
@server.shutdown
|
206
|
+
sleep 1
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
data/plugins/xibot.nb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
#
|
2
|
+
# Xi Bot
|
3
|
+
#
|
4
|
+
# No rights reserved.
|
5
|
+
#
|
6
|
+
# Synopsis:
|
7
|
+
# xi> 2d10 (two dice of ten)
|
8
|
+
# [2d10] 13 = 7 + 6
|
9
|
+
# xi> 5d
|
10
|
+
# [5d6] 14 = 3 + 1 + 3 + 1 + 6
|
11
|
+
# xi>100
|
12
|
+
# [1d100] 26
|
13
|
+
#
|
14
|
+
|
15
|
+
class XiBot < Nadoka::NDK_Bot
|
16
|
+
def bot_initialize
|
17
|
+
@available_channel = @bot_config[:ch] || /.*/
|
18
|
+
end
|
19
|
+
|
20
|
+
def dice(count=1, max=6)
|
21
|
+
count.times{ count += rand(max) }
|
22
|
+
count
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_privmsg prefix, ch, msg
|
26
|
+
return unless @available_channel === ch
|
27
|
+
return unless /\Axi\s*>\s*/ =~ msg
|
28
|
+
case $~.post_match.downcase
|
29
|
+
when /character/
|
30
|
+
%w/STR DEX CON INT WIS CHA/.each do |name|
|
31
|
+
values = (1..3).map{|i|rand(6)+1}
|
32
|
+
sum = values.inject(0){|s, i|s += i}
|
33
|
+
send_notice(ch, '%s: %2d = %s' % [name, sum, values.join(' + ')])
|
34
|
+
end
|
35
|
+
when /char/
|
36
|
+
values = %w/STR DEX CON INT WIS CHA/.map do |name|
|
37
|
+
'%s: %2d' % [name, (1..4).map{|i|rand(6)+1}.sort.last(3).inject(0){|s, i|s += i}]
|
38
|
+
end
|
39
|
+
send_notice(ch, "#{prefix.nick}: #{values.join(', ')}")
|
40
|
+
when /san/
|
41
|
+
int = dice(2, 6) + 6
|
42
|
+
pow = dice(3, 6)
|
43
|
+
san0 = pow * 5
|
44
|
+
current = san0
|
45
|
+
result = 'int:%d pow:%d san0:%d' % [int, pow, san0]
|
46
|
+
|
47
|
+
case rand(10)
|
48
|
+
when 9
|
49
|
+
result <<= ' you saw Great Cthulhu.'
|
50
|
+
losts = [dice(1, 10), dice(1, 100)]
|
51
|
+
when 7, 8
|
52
|
+
result <<= ' you saw a living dead.'
|
53
|
+
losts = [1, dice(1, 10)]
|
54
|
+
when 4, 5, 6
|
55
|
+
result <<= ' you saw a Dimension-Shambler.'
|
56
|
+
losts = [0, dice(1, 10)]
|
57
|
+
when 2, 3, 4
|
58
|
+
result <<= ' you woke up in the grave.'
|
59
|
+
losts = [0, dice(1, 6)]
|
60
|
+
else
|
61
|
+
result <<= ' you find a dead body.'
|
62
|
+
losts = [0, dice(1, 3)]
|
63
|
+
end
|
64
|
+
|
65
|
+
check = dice(1, 100)
|
66
|
+
result << " check:#{check}"
|
67
|
+
lost = losts[check > current ? 1 : 0]
|
68
|
+
|
69
|
+
insane = false
|
70
|
+
if lost > 0
|
71
|
+
result << " you lost #{lost} SAN point."
|
72
|
+
if lost >= current
|
73
|
+
# eternal insanity
|
74
|
+
result << ' you went mad. (eternal)'
|
75
|
+
insane = true
|
76
|
+
elsif lost * 5 > current
|
77
|
+
# indefinite insanity
|
78
|
+
r = %w/緊張症・痴呆症 記憶喪失 サンチョ・パンザ症、ドンキホーテ症 偏執症
|
79
|
+
恐怖症、フェティッシュ 強迫観念、中毒、けいれん発作 誇大妄想 精神分裂症
|
80
|
+
犯罪性精神異常 多重人格/[rand(10)]
|
81
|
+
result << ' you went mad. (indefinite %s)' % NKF.nkf('-jW', r)
|
82
|
+
insane = true
|
83
|
+
elsif lost >= 5
|
84
|
+
idearoll = dice(1, 100)
|
85
|
+
result << " idearoll:#{idearoll}"
|
86
|
+
if idearoll <= int * 5
|
87
|
+
# temporary insanity
|
88
|
+
result << ' you went mad. (temporary)'
|
89
|
+
insane = true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
result << ' you kept sanity.' unless insane
|
94
|
+
#message = '%s: current: %d check: %d result: %s' % [prefix.nick, current, check, result]
|
95
|
+
message = "#{prefix.nick}: #{result}"
|
96
|
+
send_notice(ch, message)
|
97
|
+
when /(?:(\d+)d)?(\d+)?(?:\*([1-9]))?/
|
98
|
+
count = $1.to_i
|
99
|
+
count = 1 unless (1..100).include? count
|
100
|
+
max = $2.to_i
|
101
|
+
max = 6 unless (1..1_000_000_000).include? max
|
102
|
+
($3 ? $3.to_i : 1).times do
|
103
|
+
values = (1..count).map{|i|rand(max)+1}
|
104
|
+
sum = values.inject(0){|s, i|s += i}
|
105
|
+
if count == 1
|
106
|
+
send_notice(ch, '%s: [%dd%d] %d' % [prefix.nick,count, max, sum])
|
107
|
+
else
|
108
|
+
send_notice(ch, '%s: [%dd%d] %d = %s' % [prefix.nick,count, max, sum, values.join(' + ')])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|