mod_spox 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +20 -0
- data/bin/mod_spox +1 -1
- data/data/mod_spox/extras/AutoKick.rb +32 -2
- data/data/mod_spox/extras/Bouncer.rb +192 -0
- data/data/mod_spox/extras/Karma.rb +132 -5
- data/data/mod_spox/extras/PhpCli.rb +13 -4
- data/data/mod_spox/extras/PhpFuncLookup.rb +6 -2
- data/data/mod_spox/extras/Roulette.rb +2 -1
- data/data/mod_spox/plugins/Authenticator.rb +1 -1
- data/data/mod_spox/plugins/Helper.rb +9 -7
- data/lib/mod_spox/Bot.rb +8 -6
- data/lib/mod_spox/ConfigurationWizard.rb +5 -5
- data/lib/mod_spox/Pipeline.rb +5 -3
- data/lib/mod_spox/PluginManager.rb +11 -5
- data/lib/mod_spox/handlers/Join.rb +5 -18
- data/lib/mod_spox/handlers/Names.rb +1 -1
- data/lib/mod_spox/handlers/Privmsg.rb +4 -14
- data/lib/mod_spox/handlers/Who.rb +9 -8
- data/lib/mod_spox/models/Auth.rb +8 -0
- data/lib/mod_spox/models/Config.rb +1 -1
- data/lib/mod_spox/models/Setting.rb +2 -2
- metadata +3 -13
- data/lib/mod_spox/migration/001_create_auths.rb +0 -13
- data/lib/mod_spox/migration/001_create_channel.rb +0 -13
- data/lib/mod_spox/migration/001_create_channel_modes.rb +0 -13
- data/lib/mod_spox/migration/001_create_config.rb +0 -13
- data/lib/mod_spox/migration/001_create_nick_channels.rb +0 -13
- data/lib/mod_spox/migration/001_create_nick_modes.rb +0 -13
- data/lib/mod_spox/migration/001_create_nicks.rb +0 -13
- data/lib/mod_spox/migration/001_create_servers.rb +0 -13
- data/lib/mod_spox/migration/001_create_settings.rb +0 -13
- data/lib/mod_spox/migration/001_create_signatures.rb +0 -13
- data/lib/mod_spox/migration/001_create_triggers.rb +0 -13
data/CHANGELOG
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
0.1.0 Release (Beta status)
|
2
|
+
Core:
|
3
|
+
* Plugins use symlinks
|
4
|
+
* Updates for sequel library compatibility
|
5
|
+
* Fixed bug in configuration wizard causing failure
|
6
|
+
* Triggerless commands usable when directly privmsging the bot
|
7
|
+
* Triggers are escaped properly before applying regex match
|
8
|
+
* Fixed trigger matching error when only trigger is provided
|
9
|
+
* Fixed output error when multiple lines are sent for output
|
10
|
+
Plugins:
|
11
|
+
* Karma plugin updates
|
12
|
+
* Added aliasing
|
13
|
+
* Auto decrement on self karma whoring
|
14
|
+
* Added a karma fight trigger
|
15
|
+
* Authentication plugin fixed to allow adding new groups to existing masks
|
16
|
+
* AutoKick ignores case during regex matching
|
17
|
+
* Fixed Roulette to have 6 chambers (was previously using only 5)
|
18
|
+
* Added option to kick on colored messages to AutoKick plugin
|
19
|
+
* PhpCli plugin properly remembers what channels it is enabled on
|
20
|
+
|
1
21
|
0.0.5 Release (Alpha status)
|
2
22
|
* Added new RAW message type for outgoing messages
|
3
23
|
* Ability to load/unload/reload individual plugins
|
data/bin/mod_spox
CHANGED
@@ -14,13 +14,38 @@ class AutoKick < ModSpox::Plugin
|
|
14
14
|
:group_id => group.pk, :description => 'Add a new autokick rule').params = [:time, :regex, :message]
|
15
15
|
Signature.find_or_create(:signature => 'autokick remove (\d+)', :plugin => name, :method => 'remove',
|
16
16
|
:group_id => group.pk, :description => 'Remove an autokick rule').params = [:id]
|
17
|
+
Signature.find_or_create(:signature => 'autokick colors ?(on|off)?', :plugin => name, :method => 'colors',
|
18
|
+
:group_id => group.pk, :description => 'Kick user for using colors', :requirement => 'public').params = [:action]
|
17
19
|
@pipeline.hook(self, :banner_watch, :Internal_PluginResponse)
|
18
20
|
@banner = nil
|
19
21
|
@map = nil
|
22
|
+
@colors = Setting[:colorkick]
|
23
|
+
@colors = Array.new if @colors.nil?
|
20
24
|
AutoKickRecord.create_table unless AutoKickRecord.table_exists?
|
21
25
|
do_listen
|
22
26
|
end
|
23
27
|
|
28
|
+
def colors(message, params)
|
29
|
+
if(params[:action])
|
30
|
+
if(params[:action] == 'on')
|
31
|
+
if(@colors.include?(message.target.pk))
|
32
|
+
reply message.replyto, 'Colored autokick is already enabled'
|
33
|
+
else
|
34
|
+
@colors << message.target.pk
|
35
|
+
Setting[:colorkick] = @colors
|
36
|
+
reply message.replyto, 'Colored autokick has been enabled'
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@colors.delete(message.target.pk)
|
40
|
+
Setting[:colorkick] = @colors
|
41
|
+
reply message.replyto, 'Colored autokick has been disabled'
|
42
|
+
end
|
43
|
+
else
|
44
|
+
status = @colors.include?(message.target.pk) ? 'on' : 'off'
|
45
|
+
reply message.replyto, "Colored autokick is currently \2#{status}\2"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
24
49
|
def list(message, params)
|
25
50
|
records = AutoKickRecord.all
|
26
51
|
unless(records.empty?)
|
@@ -61,13 +86,18 @@ class AutoKick < ModSpox::Plugin
|
|
61
86
|
def listener(message)
|
62
87
|
if(@map.keys.include?(message.target.pk))
|
63
88
|
@map[message.target.pk].each do |pattern|
|
64
|
-
reg = Regexp.new(pattern)
|
89
|
+
reg = Regexp.new(pattern, Regexp::IGNORECASE)
|
65
90
|
unless(reg.match(message.message).nil?)
|
66
91
|
record = AutoKickRecord.filter(:pattern => pattern).first
|
67
|
-
@banner.plugin.ban(message.source, message.target, record.bantime, record.message,
|
92
|
+
@banner.plugin.ban(message.source, message.target, record.bantime, record.message, false, true)
|
68
93
|
end
|
69
94
|
end
|
70
95
|
end
|
96
|
+
if(@colors.include?(message.target.pk))
|
97
|
+
if(message.is_colored?)
|
98
|
+
@banner.plugin.ban(message.source, message.target, 60, 'No color codes allowed', false, true)
|
99
|
+
end
|
100
|
+
end
|
71
101
|
end
|
72
102
|
|
73
103
|
def banner_watch(message)
|
@@ -0,0 +1,192 @@
|
|
1
|
+
class Bouncer < ModSpox::Plugin
|
2
|
+
|
3
|
+
include Models
|
4
|
+
|
5
|
+
def initialize(pipeline)
|
6
|
+
super
|
7
|
+
admin = Group.find_or_create(:name => 'bouncer')
|
8
|
+
Signature.find_or_create(:signature => 'bouncer port ?(\d+)?', :plugin => name, :method => 'port',
|
9
|
+
:group_id => admin.pk, :description => 'Show or set bouncer port').params = [:port]
|
10
|
+
Signature.find_or_create(:signature => 'bouncer (start|stop|restart)', :plugin => name, :method => 'do_service',
|
11
|
+
:group_id => admin.pk, :description => 'Start or stop the bouncer').params = [:action]
|
12
|
+
Signature.find_or_create(:signature => 'bouncer status', :plugin => name, :method => 'status',
|
13
|
+
:group_id => admin.pk, :description => 'Show current bouncer status')
|
14
|
+
Signature.find_or_create(:signature => 'bouncer disconnect', :plugin => name, :method => 'disconnect',
|
15
|
+
:group_id => admin.pk, :description => 'Disconnect all users connected to bouncer')
|
16
|
+
Signature.find_or_create(:signature => 'bouncer clients', :plugin => name, :method => 'clients',
|
17
|
+
:group_id => admin.pk, :description => 'List clients connected to bouncer')
|
18
|
+
@pipeline.hook(self, :get_msgs, :Incoming)
|
19
|
+
@listener = nil
|
20
|
+
@clients = []
|
21
|
+
@socket = nil
|
22
|
+
@processor = nil
|
23
|
+
@to_server = Queue.new
|
24
|
+
if(Config[:bouncer_port])
|
25
|
+
start_listener
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def get_msgs(message)
|
30
|
+
unless(@clients.empty?)
|
31
|
+
Logger.log("BOUNCER: Sending to #{@clients.size} clients")
|
32
|
+
@clients.each do |client|
|
33
|
+
begin
|
34
|
+
if(message.raw_content.is_a?(Array))
|
35
|
+
message.raw_content.each do |m|
|
36
|
+
client[:connection].puts(m + "\n")
|
37
|
+
end
|
38
|
+
else
|
39
|
+
client[:connection].puts(message.raw_content + "\n")
|
40
|
+
end
|
41
|
+
rescue Object => boom
|
42
|
+
client[:thread].kill if client[:thread].alive?
|
43
|
+
@clients.delete(client)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def port(message, params)
|
50
|
+
unless(params[:port])
|
51
|
+
if(Config[:bouncer_port])
|
52
|
+
reply message.replyto, "Bouncer is currently listening on port: #{Config[:bouncer_port]}"
|
53
|
+
else
|
54
|
+
reply message.replyto, "\2Warning:\2 Listening port is not currently set for bouncer"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
Config[:bouncer_port] = params[:port].to_i
|
58
|
+
reply message.replyto, "Bouncer will now listen on port: #{params[:port].to_i}"
|
59
|
+
restart_listener
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def do_service(message, params)
|
64
|
+
if(params[:action] == 'start')
|
65
|
+
if(listening?)
|
66
|
+
reply message.replyto, "\2Error:\2 Bouncer is already running"
|
67
|
+
else
|
68
|
+
start_listener
|
69
|
+
reply message.replyto, "Bouncer has been started"
|
70
|
+
end
|
71
|
+
elsif(params[:action] == 'stop')
|
72
|
+
if(listening?)
|
73
|
+
stop_listener
|
74
|
+
reply message.replyto, "Bouncer has been stopped"
|
75
|
+
else
|
76
|
+
reply message.replyto, "\2Error:\2 Bouncer is not currently running"
|
77
|
+
end
|
78
|
+
elsif(params[:action] == 'restart')
|
79
|
+
stop_listener
|
80
|
+
start_listener
|
81
|
+
reply message.replyto, "Bouncer has been restarted"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def disconnect(message, params)
|
86
|
+
unless(@clients.empty?)
|
87
|
+
@clients.each do |socket|
|
88
|
+
socket[:connection].close
|
89
|
+
@clients.delete(socket)
|
90
|
+
end
|
91
|
+
reply message.replyto, "Bouncer has disconnected all clients"
|
92
|
+
else
|
93
|
+
reply message.replyto, "\2Error:\2 No clients connected to bouncer"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def status(message, params)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def start_listener
|
103
|
+
port = Config[:bouncer_port]
|
104
|
+
if(port)
|
105
|
+
@socket = TCPServer.new(port)
|
106
|
+
@listener = Thread.new do
|
107
|
+
until(@socket.closed?)
|
108
|
+
begin
|
109
|
+
new_con = @socket.accept_nonblock
|
110
|
+
Logger.log("BOUNCER: New connection established on bouncer")
|
111
|
+
@clients << {
|
112
|
+
:connection => new_con,
|
113
|
+
:thread => Thread.new(new_con) do | con |
|
114
|
+
begin
|
115
|
+
Logger.log("CONNECTION: #{con}")
|
116
|
+
until(con.closed?)
|
117
|
+
Logger.log("WAITING FOR STUFF ON :#{con}")
|
118
|
+
Kernel.select([con], nil, nil, nil)
|
119
|
+
Logger.log("Woken up and ready to read")
|
120
|
+
string = con.gets
|
121
|
+
Logger.log("BOUNCER GOT MESSAGE: #{string}")
|
122
|
+
if(string.empty?)
|
123
|
+
raise Exception.new("EMPTY STRING")
|
124
|
+
else
|
125
|
+
@to_server << {:message => string, :socket => con}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
rescue Object => boom
|
129
|
+
Logger.log("THREAD BOUNCER ERROR: #{boom}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
}
|
133
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
|
134
|
+
IO.select([@socket])
|
135
|
+
retry
|
136
|
+
end
|
137
|
+
end
|
138
|
+
@listener = nil
|
139
|
+
end
|
140
|
+
start_processor
|
141
|
+
else
|
142
|
+
Logger.log("Error: Bouncer was not started. Failed to find set port number")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def stop_listener
|
147
|
+
@socket.close
|
148
|
+
@listener.kill unless @listener.nil?
|
149
|
+
@listener = nil
|
150
|
+
@to_server.clear
|
151
|
+
@processor.kill if !@processor.nil? && @processor.alive?
|
152
|
+
@processor = nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def start_processor
|
156
|
+
@processor.kill if !@processor.nil? && @processor.alive?
|
157
|
+
@processor = Thread.new do
|
158
|
+
begin
|
159
|
+
while(@listener) do
|
160
|
+
info = @to_server.pop
|
161
|
+
Logger.log("Processing message: #{info[:message]}")
|
162
|
+
if(info[:message] =~ /^USER\s/i)
|
163
|
+
initialize_connection(info[:socket])
|
164
|
+
else
|
165
|
+
@pipeline << Messages::Outgoing::Raw.new(info[:message])
|
166
|
+
end
|
167
|
+
end
|
168
|
+
rescue Object => boom
|
169
|
+
Logger.log("BOUNCER ERROR: #{boom}")
|
170
|
+
unless(@clients.empty?)
|
171
|
+
@clients.each do |socket|
|
172
|
+
socket[:connection].close
|
173
|
+
@clients.delete(socket)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def listening?
|
181
|
+
return !@listener.nil?
|
182
|
+
end
|
183
|
+
|
184
|
+
def initialize_connection(connection)
|
185
|
+
# send channel info and such to client
|
186
|
+
connection.puts(":localhost 001 #{me.nick} :Welcome to the network #{me.source}\n")
|
187
|
+
Models::Channel.filter(:parked => true).each do |channel|
|
188
|
+
connection.puts(":#{me.source} JOIN :#{channel.name}\n")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
@@ -5,9 +5,19 @@ class Karma < ModSpox::Plugin
|
|
5
5
|
def initialize(pipeline)
|
6
6
|
super(pipeline)
|
7
7
|
Datatype::Karma.create_table unless Datatype::Karma.table_exists?
|
8
|
+
Datatype::Alias.create_table unless Datatype::Alias.table_exists?
|
9
|
+
alias_group = Models::Group.find_or_create(:name => 'alias')
|
8
10
|
Models::Signature.find_or_create(:signature => 'karma (\S+)', :plugin => name, :method => 'score', :description => 'Returns karma for given thing').params = [:thing]
|
9
11
|
Models::Signature.find_or_create(:signature => 'karma reset (\S+)', :plugin => name, :method => 'reset',
|
10
12
|
:group_id => Models::Group.filter(:name => 'admin').first.pk, :description => 'Reset a karma score').params = [:thing]
|
13
|
+
Models::Signature.find_or_create(:signature => 'karma alias (\S+) (\S+)', :plugin => name, :method => 'aka',
|
14
|
+
:group_id => alias_group.pk, :description => 'Alias a karma object to another karma object').params = [:thing, :thang]
|
15
|
+
Models::Signature.find_or_create(:signature => 'karma dealias (\S+) (\S+)', :plugin => name, :method => 'dealias',
|
16
|
+
:group_id => alias_group.pk, :description => 'Remove a karma alias').params = [:thing, :otherthing]
|
17
|
+
Models::Signature.find_or_create(:signature => 'karma aliases (\S+)', :plugin => name, :method => 'show_aliases',
|
18
|
+
:description => 'Show all aliases for given thing').params = [:thing]
|
19
|
+
Models::Signature.find_or_create(:signature => 'karma fight (\S+) (\S+)', :plugin => name, :method => 'fight',
|
20
|
+
:description => 'Make two karma objects fight').params = [:thing, :thang]
|
11
21
|
@pipeline.hook(self, :check, :Incoming_Privmsg)
|
12
22
|
@thing_maxlen = 32
|
13
23
|
@karma_regex = /(\(.{1,#@thing_maxlen}?\)|\S{1,#@thing_maxlen})([+-]{2})(?:\s|$)/
|
@@ -20,18 +30,26 @@ class Karma < ModSpox::Plugin
|
|
20
30
|
thing.downcase!
|
21
31
|
thing = thing[1..-2] if thing[0..0] == '(' && thing[-1..1] == ')'
|
22
32
|
adj = adj == '++' ? +1 : -1
|
33
|
+
things = [thing]
|
23
34
|
karma = Datatype::Karma.find_or_create(:thing => thing, :channel_id => message.target.pk)
|
24
|
-
|
35
|
+
Datatype::Alias.get_aliases(karma.pk).each do |id|
|
36
|
+
things << Datatype::Karma[id].thing.downcase
|
37
|
+
end
|
38
|
+
if(things.include?(message.source.nick.downcase))
|
39
|
+
adj = -1
|
40
|
+
end
|
41
|
+
karma = Datatype::Karma.find_or_create(:thing => thing, :channel_id => message.target.pk)
|
42
|
+
karma.score = karma.score + adj
|
43
|
+
karma.save
|
25
44
|
end
|
26
45
|
end
|
27
46
|
end
|
28
47
|
|
29
48
|
def score(message, params)
|
30
|
-
params[:thing].downcase!
|
31
49
|
return unless message.is_public?
|
32
|
-
karma = Datatype::Karma.filter(:thing => params[:thing], :channel_id => message.target.pk).first
|
50
|
+
karma = Datatype::Karma.filter(:thing => params[:thing].downcase, :channel_id => message.target.pk).first
|
33
51
|
if(karma)
|
34
|
-
@pipeline << Privmsg.new(message.replyto, "Karma for \2#{
|
52
|
+
@pipeline << Privmsg.new(message.replyto, "Karma for \2#{params[:thing]}\2 is #{Datatype::Alias.score_object(karma.pk)}")
|
35
53
|
else
|
36
54
|
@pipeline << Privmsg.new(message.replyto, "\2Error:\2 #{params[:thing]} has no karma")
|
37
55
|
end
|
@@ -48,19 +66,128 @@ class Karma < ModSpox::Plugin
|
|
48
66
|
@pipeline << Privmsg.new(message.replyto, "\2Error:\2 #{params[:thing]} has no karma")
|
49
67
|
end
|
50
68
|
end
|
69
|
+
|
70
|
+
def fight(message, params)
|
71
|
+
thing = Datatype::Karma.find_or_create(:thing => params[:thing].downcase)
|
72
|
+
thang = Datatype::Karma.find_or_create(:thing => params[:thang].downcase)
|
73
|
+
thing_score = Datatype::Alias.score_object(thing.pk)
|
74
|
+
thang_score = Datatype::Alias.score_object(thang.pk)
|
75
|
+
winner = thing_score > thang_score ? params[:thing] : params[:thang]
|
76
|
+
loser = thing_score > thang_score ? params[:thang] : params[:thing]
|
77
|
+
distance = (thing_score - thang_score).abs
|
78
|
+
reply message.replyto, "\2KARMA FIGHT RESULTS:\2 \2#{winner}\2 has beaten \2#{loser}\2 by a #{distance} point lead"
|
79
|
+
end
|
80
|
+
|
81
|
+
def aka(message, params)
|
82
|
+
thing = Datatype::Karma.find_or_create(:thing => params[:thing].downcase, :channel_id => message.target.pk)
|
83
|
+
thang = Datatype::Karma.find_or_create(:thing => params[:thang].downcase, :channel_id => message.target.pk)
|
84
|
+
if(Datatype::Alias.filter('(thing_id = ? AND aka_id = ?) OR (thing_id = ? AND aka_id = ?)', thing.pk, thang.pk, thang.pk, thing.pk).first)
|
85
|
+
reply message.replyto, "\2Error:\2 #{params[:thing]} is already aliased to #{params[:thang]}"
|
86
|
+
else
|
87
|
+
Datatype::Alias.find_or_create(:thing_id => thing.pk, :aka_id => thang.pk)
|
88
|
+
reply message.replyto, "\2Karma Alias:\2 #{params[:thing]} is now aliased to #{params[:thang]}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def dealias(message, params)
|
93
|
+
thing = Datatype::Karma.filter(:thing => params[:thing].downcase, :channel_id => message.target.pk).first
|
94
|
+
otherthing = Datatype::Karma.filter(:thing => params[:otherthing].downcase, :channel_id => message.target.pk).first
|
95
|
+
if(thing && otherthing)
|
96
|
+
set = Datatype::Alias.filter('(thing_id = ? AND aka_id = ?) OR (thing_id = ? AND aka_id = ?)', thing.pk, otherthing.pk, otherthing.pk, thing.pk)
|
97
|
+
if(set.size < 1)
|
98
|
+
reply message.replyto, "\2Error:\2 No alias found between #{params[:thing]} and #{params[:otherthing]}"
|
99
|
+
else
|
100
|
+
set.destroy
|
101
|
+
reply message.replyto, "#{params[:thing]} has been successfully dealiased from #{params[:otherthing]}"
|
102
|
+
end
|
103
|
+
else
|
104
|
+
reply message.replyto, "\2Error:\2 No alias found between #{params[:thing]} and #{params[:otherthing]}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def show_aliases(message, params)
|
109
|
+
thing = Datatype::Karma.filter(:thing => params[:thing].downcase, :channel_id => message.target.pk).first
|
110
|
+
if(thing)
|
111
|
+
things = []
|
112
|
+
Datatype::Alias.get_aliases(thing.pk).each do |id|
|
113
|
+
things << Datatype::Karma[id].thing
|
114
|
+
end
|
115
|
+
if(things.empty?)
|
116
|
+
reply message.replyto, "#{params[:thing]} is not currently aliased"
|
117
|
+
else
|
118
|
+
reply message.replyto, "#{params[:thing]} is currently aliased to: #{things.join(', ')}"
|
119
|
+
end
|
120
|
+
else
|
121
|
+
reply message.replyto, "\2Error:\2 #{params[:thing]} has never been used and has no aliases"
|
122
|
+
end
|
123
|
+
end
|
51
124
|
|
52
125
|
module Datatype
|
53
126
|
class Karma < Sequel::Model
|
54
127
|
set_schema do
|
55
128
|
primary_key :id
|
56
|
-
text :thing, :null => false
|
129
|
+
text :thing, :null => false
|
57
130
|
integer :score, :null => false, :default => 0
|
58
131
|
foreign_key :channel_id, :table => :channels
|
132
|
+
index [:thing, :channel_id], :unique => true
|
59
133
|
end
|
60
134
|
|
61
135
|
def channel
|
62
136
|
ModSpox::Models::Channel[channel_id]
|
63
137
|
end
|
64
138
|
end
|
139
|
+
class Alias < Sequel::Model
|
140
|
+
set_schema do
|
141
|
+
primary_key :id
|
142
|
+
foreign_key :thing_id, :null => false
|
143
|
+
foreign_key :aka_id, :null => false
|
144
|
+
end
|
145
|
+
|
146
|
+
def thing
|
147
|
+
Karma[thing_id]
|
148
|
+
end
|
149
|
+
|
150
|
+
def aka
|
151
|
+
Karma[aka_id]
|
152
|
+
end
|
153
|
+
|
154
|
+
def Alias.score_object(object_id)
|
155
|
+
Alias.create_lock unless class_variable_defined?(:@@lock)
|
156
|
+
@@objects = []
|
157
|
+
score = 0
|
158
|
+
@@lock.synchronize do
|
159
|
+
score += Alias.sum_objects(object_id)
|
160
|
+
end
|
161
|
+
return score
|
162
|
+
end
|
163
|
+
|
164
|
+
def Alias.get_aliases(object_id)
|
165
|
+
Alias.score_object(object_id)
|
166
|
+
objs = @@objects.dup
|
167
|
+
objs.delete(object_id)
|
168
|
+
return objs
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def Alias.sum_objects(object_id)
|
174
|
+
return 0 if @@objects.include?(object_id)
|
175
|
+
@@objects << object_id
|
176
|
+
object = Karma[object_id]
|
177
|
+
score = object ? object.score : 0
|
178
|
+
Alias.filter(:thing_id => object_id).each do |ali|
|
179
|
+
score += Alias.sum_objects(ali.aka.pk)
|
180
|
+
end
|
181
|
+
Alias.filter(:aka_id => object_id).each do |ali|
|
182
|
+
score += Alias.sum_objects(ali.thing.pk)
|
183
|
+
end
|
184
|
+
return score
|
185
|
+
end
|
186
|
+
|
187
|
+
def Alias.create_lock
|
188
|
+
@@lock = Mutex.new
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
65
192
|
end
|
66
193
|
end
|
@@ -24,8 +24,13 @@ class PhpCli < ModSpox::Plugin
|
|
24
24
|
:description => 'Add or remove channel from allowing PHP command').params = [:action]
|
25
25
|
Signature.find_or_create(:signature => 'php (?!on|off)(.+)', :plugin => name, :method => 'execute_php', :group_id => php.pk,
|
26
26
|
:description => 'Execute PHP code').params = [:code]
|
27
|
-
Setting
|
28
|
-
@channels
|
27
|
+
@channels = Setting.find(:name => 'phpcli')
|
28
|
+
if(@channels.nil?)
|
29
|
+
Logger.log("WE ARE NIL PEOPLE")
|
30
|
+
@channels = []
|
31
|
+
else
|
32
|
+
@channels = @channels.value
|
33
|
+
end
|
29
34
|
end
|
30
35
|
|
31
36
|
def set_channel(message, params)
|
@@ -33,7 +38,9 @@ class PhpCli < ModSpox::Plugin
|
|
33
38
|
if(params[:action] == 'on')
|
34
39
|
unless(@channels.include?(message.target.pk))
|
35
40
|
@channels << message.target.pk
|
36
|
-
|
41
|
+
tmp = Setting.find_or_create(:name => 'phpcli')
|
42
|
+
tmp.value = @channels
|
43
|
+
tmp.save
|
37
44
|
end
|
38
45
|
reply message.replyto, 'PHP command now active'
|
39
46
|
else
|
@@ -41,7 +48,9 @@ class PhpCli < ModSpox::Plugin
|
|
41
48
|
reply message.replyto, 'PHP command is not currently active in this channel'
|
42
49
|
else
|
43
50
|
@channels.delete(message.target.pk)
|
44
|
-
|
51
|
+
tmp = Setting.find_or_create(:name => 'phpcli')
|
52
|
+
tmp.value = @channels
|
53
|
+
tmp.save
|
45
54
|
reply message.replyto, 'PHP command is now disabled'
|
46
55
|
end
|
47
56
|
end
|
@@ -219,8 +219,12 @@ class PhpFuncLookup < ModSpox::Plugin
|
|
219
219
|
end
|
220
220
|
end
|
221
221
|
matches.sort!
|
222
|
-
output = ["Lots of matching functions. Truncating list to 20 results."]
|
223
|
-
|
222
|
+
output = matches.size > 20 ? ["Lots of matching functions. Truncating list to 20 results."] : []
|
223
|
+
if(matches.empty?)
|
224
|
+
output = "\2Error:\2 No matches found"
|
225
|
+
else
|
226
|
+
output << matches.values_at(0..19).join(', ')
|
227
|
+
end
|
224
228
|
reply m.replyto, output
|
225
229
|
end
|
226
230
|
|
@@ -187,6 +187,7 @@ class Roulette < ModSpox::Plugin
|
|
187
187
|
rescue Banner::NotOperator => boom
|
188
188
|
reply(channel, "#{nick.nick}: *BANG*")
|
189
189
|
rescue Object => boom
|
190
|
+
reply(channel, "#{nick.nick}: *BANG*")
|
190
191
|
Logger.log("Error: Roulette ban generated an unexpected error: #{boom}")
|
191
192
|
end
|
192
193
|
else
|
@@ -200,7 +201,7 @@ class Roulette < ModSpox::Plugin
|
|
200
201
|
@pipeline << Messages::Internal::PluginRequest.new(self, 'Banner') if @banner.nil?
|
201
202
|
game = Game.filter('shots > ?', 0).filter('channel_id = ?', channel.pk).first
|
202
203
|
unless(game)
|
203
|
-
chamber = rand(
|
204
|
+
chamber = rand(6) + 1
|
204
205
|
game = Game.new(:chamber => chamber, :shots => chamber, :channel_id => channel.pk)
|
205
206
|
game.save
|
206
207
|
end
|
@@ -9,7 +9,7 @@ class Authenticator < ModSpox::Plugin
|
|
9
9
|
Models::Signature.find_or_create(:signature => 'auth mask add (\S+) (\S+)', :plugin => name, :method => 'add_mask',
|
10
10
|
:group_id => group.pk, :description => 'Add authentication mask and set initial group').params = [:mask, :group]
|
11
11
|
Models::Signature.find_or_create(:signature => 'auth mask set (\d+) (.+)', :plugin => name, :method => 'set_mask_groups',
|
12
|
-
:group_id => group.pk, :description => 'Set groups for the given mask').params = [:id, :
|
12
|
+
:group_id => group.pk, :description => 'Set groups for the given mask').params = [:id, :groups]
|
13
13
|
Models::Signature.find_or_create(:signature => 'auth mask unset (\d+) (.+)', :plugin => name, :method => 'del_mask_groups',
|
14
14
|
:group_id => group.pk, :description => 'Remove groups for the given mask').params = [:id, :groups]
|
15
15
|
Models::Signature.find_or_create(:signature => 'auth mask remove (\d+)', :plugin => name, :method => 'remove_mask',
|
@@ -20,15 +20,17 @@ class Helper < ModSpox::Plugin
|
|
20
20
|
def plugin_help(message, params)
|
21
21
|
sigs = Signature.filter(:plugin => params[:plugin])
|
22
22
|
if(sigs.count > 0)
|
23
|
-
|
23
|
+
output = []
|
24
|
+
output << "Available triggers for plugin: \2#{params[:plugin]}\2"
|
24
25
|
sigs.each do |sig|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
help = []
|
27
|
+
help << "\2Pattern:\2 #{sig.signature}"
|
28
|
+
help << "\2Parameters:\2 [#{sig.params.join(' | ')}]" if sig.params
|
29
|
+
help << "\2Auth Group:\2 #{Group[sig.group_id].name}" if sig.group_id
|
30
|
+
help << "\2Description:\2 #{sig.description}" if sig.description
|
31
|
+
output << help.join(' ')
|
31
32
|
end
|
33
|
+
reply message.source, output
|
32
34
|
else
|
33
35
|
reply message.replyto, "\2Error:\2 No triggers found for plugin named: #{params[:plugin]}"
|
34
36
|
end
|
data/lib/mod_spox/Bot.rb
CHANGED
@@ -286,13 +286,15 @@ module ModSpox
|
|
286
286
|
target = message.target.nick if message.target.is_a?(Models::Nick)
|
287
287
|
target = message.target unless target
|
288
288
|
messages = message.message.is_a?(Array) ? message.message : [message.message]
|
289
|
-
messages.each do |
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
289
|
+
messages.each do |part|
|
290
|
+
part.split("\n").each do |content|
|
291
|
+
while(content.size > 450)
|
292
|
+
output = content[0..450]
|
293
|
+
content.slice!(0, 450) #(450, content.size)
|
294
|
+
@socket << "PRIVMSG #{target} :#{message.is_action? ? "\cAACTION #{output}\cA" : output}"
|
295
|
+
end
|
296
|
+
@socket << "PRIVMSG #{target} :#{message.is_action? ? "\cAACTION #{content}\cA" : content}"
|
294
297
|
end
|
295
|
-
@socket << "PRIVMSG #{target} :#{message.is_action? ? "\cAACTION #{content}\cA" : content}"
|
296
298
|
end
|
297
299
|
end
|
298
300
|
|
@@ -1,9 +1,7 @@
|
|
1
1
|
['etc',
|
2
2
|
'mod_spox/Database',
|
3
3
|
'mod_spox/BotConfig',
|
4
|
-
'mod_spox/BaseConfig'
|
5
|
-
'mod_spox/models/Models',
|
6
|
-
'mod_spox/Helpers'].each{|f|require f}
|
4
|
+
'mod_spox/BaseConfig'].each{|f|require f}
|
7
5
|
|
8
6
|
|
9
7
|
module ModSpox
|
@@ -75,6 +73,8 @@ module ModSpox
|
|
75
73
|
}
|
76
74
|
config.write_configuration
|
77
75
|
initialize_bot
|
76
|
+
require 'mod_spox/models/Models'
|
77
|
+
require 'mod_spox/Helpers'
|
78
78
|
create_databases
|
79
79
|
#Migrators.constants.each{|m| Migrators.const_get(m).apply(Database.db, :up)}
|
80
80
|
@config.each{|value|
|
@@ -87,7 +87,7 @@ module ModSpox
|
|
87
87
|
a.password = find(:admin_password)
|
88
88
|
a.save
|
89
89
|
t = Models::Trigger.find_or_create(:trigger => find(:trigger))
|
90
|
-
t.active
|
90
|
+
t.update_with_params(:active => true)
|
91
91
|
t.save
|
92
92
|
end
|
93
93
|
|
@@ -164,7 +164,7 @@ module ModSpox
|
|
164
164
|
Database.db << "CREATE TABLE nick_modes (id serial not null primary key, mode varchar(255) not null, nick_id integer not null references nicks, channel_id integer references channels, unique (nick_id, channel_id))"
|
165
165
|
Database.db << "CREATE TABLE servers (id serial not null primary key, host varchar(255) not null, port integer not null default 6667, priority integer not null default 0, connected boolean not null default false, unique (host, port))"
|
166
166
|
Database.db << "CREATE TABLE signatures (id serial not null primary key, signature varchar(255) not null, params varchar(255), group_id integer default null references groups, method varchar(255) not null, plugin varchar(255) not null, description varchar(255), requirement varchar(255) default 'both' not null)"
|
167
|
-
Database.db << "CREATE TABLE settings (id serial not null primary key, name varchar(255) unique not null, value
|
167
|
+
Database.db << "CREATE TABLE settings (id serial not null primary key, name varchar(255) unique not null, value text)"
|
168
168
|
Database.db << "CREATE TABLE triggers (id serial not null primary key, trigger varchar(255) unique not null, active boolean not null default false)"
|
169
169
|
Database.db << "CREATE TABLE auth_groups (auth_id integer not null references auths, group_id integer not null references groups, primary key (auth_id, group_id))"
|
170
170
|
when :sqlite
|
data/lib/mod_spox/Pipeline.rb
CHANGED
@@ -153,10 +153,11 @@ module ModSpox
|
|
153
153
|
def parse(message)
|
154
154
|
return unless message.kind_of?(Messages::Incoming::Privmsg) || message.kind_of?(Messages::Incoming::Notice)
|
155
155
|
trigger = nil
|
156
|
-
@triggers.each{|t| trigger = t if message.message =~ /^#{t}/}
|
156
|
+
@triggers.each{|t| trigger = t if message.message =~ /^#{Regexp.escape(t)}/}
|
157
157
|
if(!trigger.nil? || message.addressed?)
|
158
|
+
return if !trigger.nil? && message.message.length == trigger.length
|
158
159
|
Logger.log("Message has matched against a known trigger", 15)
|
159
|
-
c = message.addressed? ? message.message[0].chr.downcase : message.message[
|
160
|
+
c = (message.addressed? && trigger.nil?) ? message.message[0].chr.downcase : message.message[trigger.length].chr.downcase
|
160
161
|
if(c =~ /^[a-z]$/)
|
161
162
|
type = c.to_sym
|
162
163
|
elsif(c =~ /^[0-9]$/)
|
@@ -167,7 +168,8 @@ module ModSpox
|
|
167
168
|
return unless @signatures[type]
|
168
169
|
@signatures[type].each do |sig|
|
169
170
|
Logger.log("Matching against: #{trigger}#{sig.signature}")
|
170
|
-
|
171
|
+
esc_trig = trigger.nil? ? '' : Regexp.escape(trigger)
|
172
|
+
res = message.message.scan(/^#{esc_trig}#{sig.signature}$/)
|
171
173
|
if(res.size > 0)
|
172
174
|
next unless message.source.auth_groups.include?(sig.group) || message.source.auth_groups.include?(@admin) ||sig.group.nil?
|
173
175
|
next if sig.requirement == 'private' && message.is_public?
|
@@ -55,7 +55,11 @@ module ModSpox
|
|
55
55
|
def load_plugin(message)
|
56
56
|
begin
|
57
57
|
path = !message.name ? "#{BotConfig[:userpluginpath]}/#{message.path.gsub(/^.+\//, '')}" : "#{BotConfig[:userpluginpath]}/#{message.name}"
|
58
|
-
|
58
|
+
begin
|
59
|
+
File.symlink(message.path, path)
|
60
|
+
rescue NotImplementedError => boom
|
61
|
+
FileUtils.copy(message.path, path)
|
62
|
+
end
|
59
63
|
do_load(path)
|
60
64
|
@pipeline << Messages::Internal::PluginLoadResponse.new(message.requester, true)
|
61
65
|
Logger.log("Loaded new plugin: #{message.path}", 10)
|
@@ -71,10 +75,12 @@ module ModSpox
|
|
71
75
|
def unload_plugin(message)
|
72
76
|
begin
|
73
77
|
do_unload(message.path)
|
74
|
-
unless(message.
|
75
|
-
|
78
|
+
unless(File.symlink?(message.path))
|
79
|
+
unless(message.name.nil?)
|
80
|
+
FileUtils.copy(message.path, "#{BotConfig[:userpluginpath]}/#{message.name}")
|
81
|
+
end
|
76
82
|
end
|
77
|
-
|
83
|
+
File.delete(message.path)
|
78
84
|
@pipeline << Messages::Internal::PluginUnloadResponse.new(message.requester, true)
|
79
85
|
Logger.log("Unloaded plugin: #{message.path}", 10)
|
80
86
|
rescue Object => boom
|
@@ -124,7 +130,7 @@ module ModSpox
|
|
124
130
|
# Destroys plugins
|
125
131
|
def unload_plugins
|
126
132
|
@plugins.each_pair do |sym, holder|
|
127
|
-
holder.plugin.destroy
|
133
|
+
holder.plugin.destroy unless holder.plugin.nil?
|
128
134
|
@pipeline.unhook_plugin(holder.plugin)
|
129
135
|
end
|
130
136
|
Models::Signature.destroy_all
|
@@ -10,25 +10,12 @@ module ModSpox
|
|
10
10
|
source = $1
|
11
11
|
chan = $2
|
12
12
|
if(source =~ /^(.+?)!(.+?)@(.+)$/)
|
13
|
-
do_save = false
|
14
13
|
nick = find_model($1)
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
nick.address = $3
|
21
|
-
do_save = true
|
22
|
-
end
|
23
|
-
unless(nick.source == source)
|
24
|
-
nick.source = source
|
25
|
-
do_save = true
|
26
|
-
end
|
27
|
-
unless(nick.visible == true)
|
28
|
-
nick.visible = true
|
29
|
-
do_save = true
|
30
|
-
end
|
31
|
-
nick.save if do_save
|
14
|
+
nick.username == $2
|
15
|
+
nick.address = $3
|
16
|
+
nick.source = source
|
17
|
+
nick.visible = true
|
18
|
+
nick.save_changes
|
32
19
|
channel = find_model(chan)
|
33
20
|
channel.nick_add(nick)
|
34
21
|
return Messages::Incoming::Join.new(string, channel, nick)
|
@@ -13,20 +13,10 @@ module ModSpox
|
|
13
13
|
base_source = $1
|
14
14
|
source = find_model(base_source.gsub(/!.+$/, ''))
|
15
15
|
if(base_source =~ /!(.+)@(.+)$/)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
unless(source.address == $2)
|
22
|
-
source.address = $2
|
23
|
-
do_save = true
|
24
|
-
end
|
25
|
-
unless(source.source == base_source)
|
26
|
-
source.source = base_source
|
27
|
-
do_save = true
|
28
|
-
end
|
29
|
-
source.save if do_save
|
16
|
+
source.username == $1
|
17
|
+
source.address = $2
|
18
|
+
source.source = base_source
|
19
|
+
source.save_changes
|
30
20
|
end
|
31
21
|
Models::NickChannel.find_or_create(:channel_id => target.pk, :nick_id => source.pk) if target.is_a?(ModSpox::Models::Channel)
|
32
22
|
return Messages::Incoming::Privmsg.new(string, source, target, message)
|
@@ -9,6 +9,8 @@ module ModSpox
|
|
9
9
|
@raw_cache = Hash.new
|
10
10
|
end
|
11
11
|
def process(string)
|
12
|
+
# :not.configured 352 mod_spox #foobar ~mod_spox 192.168.0.25 not.configured mod_spox H :0 mod_spox IRC bot
|
13
|
+
# :not.configured 352 mod_spox * ~mod_spox 192.168.0.25 not.configured mod_spox H :0 mod_spox IRC bot
|
12
14
|
if(string =~ /#{RPL_WHOREPLY}\s\S+\s(\S+|\*|\*\s\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s(\S+)\s:(\d)\s(.+)$/)
|
13
15
|
# Items matched are as follows:
|
14
16
|
# 1: location
|
@@ -19,9 +21,7 @@ module ModSpox
|
|
19
21
|
# 6: info
|
20
22
|
# 7: hops
|
21
23
|
# 8: realname
|
22
|
-
location = $1
|
23
|
-
location = $5 if $5 == '*'
|
24
|
-
location = $1.gsub(/\*\s/, '') if location.include?('* ')
|
24
|
+
location = $1 == '*' ? nil : $1
|
25
25
|
info = $6
|
26
26
|
nick = find_model($5)
|
27
27
|
nick.username = $2
|
@@ -30,11 +30,12 @@ module ModSpox
|
|
30
30
|
nick.connected_to = $4
|
31
31
|
nick.away = info =~ /G/ ? true : false
|
32
32
|
nick.save
|
33
|
-
|
34
|
-
@cache[
|
35
|
-
@
|
36
|
-
@raw_cache[
|
37
|
-
|
33
|
+
key = location.nil? ? nick.nick : location
|
34
|
+
@cache[key] = Array.new unless @cache[location]
|
35
|
+
@cache[key] << nick
|
36
|
+
@raw_cache[key] = Array.new unless @raw_cache[location]
|
37
|
+
@raw_cache[key] << string
|
38
|
+
unless(location.nil?)
|
38
39
|
channel = find_model(location)
|
39
40
|
Models::NickChannel.find_or_create(:channel_id => channel.pk, :nick_id => nick.pk)
|
40
41
|
if(info.include?('+'))
|
data/lib/mod_spox/models/Auth.rb
CHANGED
@@ -8,12 +8,20 @@ module ModSpox
|
|
8
8
|
# mask:: Mask to authenticate source against
|
9
9
|
# authed:: Nick has authenticated
|
10
10
|
class Auth < Sequel::Model(:auths)
|
11
|
+
|
12
|
+
before_destroy :clear_auth_groups
|
13
|
+
|
14
|
+
# Clear relations before destroying
|
15
|
+
def clear_auth_groups
|
16
|
+
AuthGroup.filter(:auth_id => pk).destroy
|
17
|
+
end
|
11
18
|
|
12
19
|
# Nick associated with this Auth
|
13
20
|
def nick
|
14
21
|
Nick[nick_id]
|
15
22
|
end
|
16
23
|
|
24
|
+
# Is nick identified with services
|
17
25
|
def services
|
18
26
|
s = values[:services]
|
19
27
|
if(s == 0 || s == '0' || !s)
|
@@ -9,7 +9,7 @@ module ModSpox
|
|
9
9
|
class Setting < Sequel::Model(:settings)
|
10
10
|
|
11
11
|
def value=(val)
|
12
|
-
update_values(:value => [Marshal.dump(val)].pack('m'))
|
12
|
+
update_values(:value => [Marshal.dump(val.dup)].pack('m'))
|
13
13
|
end
|
14
14
|
|
15
15
|
def value
|
@@ -31,7 +31,7 @@ module ModSpox
|
|
31
31
|
def self.[]=(key, val)
|
32
32
|
key = key.to_s if key.is_a?(Symbol)
|
33
33
|
model = Setting.find_or_create(:name => key)
|
34
|
-
model.update_with_params(:value => [Marshal.dump(val)].pack('m'))
|
34
|
+
model.update_with_params(:value => [Marshal.dump(val.dup)].pack('m'))
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
|
|
3
3
|
specification_version: 1
|
4
4
|
name: mod_spox
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.0
|
7
|
-
date: 2008-
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2008-07-11 00:00:00 -07:00
|
8
8
|
summary: The mod_spox IRC robot
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -46,17 +46,6 @@ files:
|
|
46
46
|
- lib/mod_spox/models/NickMode.rb
|
47
47
|
- lib/mod_spox/models/Group.rb
|
48
48
|
- lib/mod_spox/models/Models.rb
|
49
|
-
- lib/mod_spox/migration/001_create_channel_modes.rb
|
50
|
-
- lib/mod_spox/migration/001_create_nick_modes.rb
|
51
|
-
- lib/mod_spox/migration/001_create_nick_channels.rb
|
52
|
-
- lib/mod_spox/migration/001_create_settings.rb
|
53
|
-
- lib/mod_spox/migration/001_create_auths.rb
|
54
|
-
- lib/mod_spox/migration/001_create_signatures.rb
|
55
|
-
- lib/mod_spox/migration/001_create_config.rb
|
56
|
-
- lib/mod_spox/migration/001_create_triggers.rb
|
57
|
-
- lib/mod_spox/migration/001_create_nicks.rb
|
58
|
-
- lib/mod_spox/migration/001_create_channel.rb
|
59
|
-
- lib/mod_spox/migration/001_create_servers.rb
|
60
49
|
- lib/mod_spox/handlers/LuserClient.rb
|
61
50
|
- lib/mod_spox/handlers/Handler.rb
|
62
51
|
- lib/mod_spox/handlers/Topic.rb
|
@@ -249,6 +238,7 @@ files:
|
|
249
238
|
- data/mod_spox/extras/Logger.rb
|
250
239
|
- data/mod_spox/extras/Topten.rb
|
251
240
|
- data/mod_spox/extras/AutoMode.rb
|
241
|
+
- data/mod_spox/extras/Bouncer.rb
|
252
242
|
test_files: []
|
253
243
|
|
254
244
|
rdoc_options: []
|