mod_spox 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +2 -0
- data/INSTALL +9 -0
- data/README +33 -0
- data/bin/mod_spox +60 -0
- data/data/mod_spox/extras/Tester.rb +14 -0
- data/data/mod_spox/plugins/Authenticator.rb +245 -0
- data/data/mod_spox/plugins/BotNick.rb +18 -0
- data/data/mod_spox/plugins/Initializer.rb +41 -0
- data/data/mod_spox/plugins/Joiner.rb +13 -0
- data/data/mod_spox/plugins/Parter.rb +22 -0
- data/data/mod_spox/plugins/PluginLoader.rb +136 -0
- data/data/mod_spox/plugins/Ponger.rb +14 -0
- data/data/mod_spox/plugins/Quitter.rb +14 -0
- data/lib/mod_spox/Action.rb +73 -0
- data/lib/mod_spox/BaseConfig.rb +48 -0
- data/lib/mod_spox/Bot.rb +472 -0
- data/lib/mod_spox/BotConfig.rb +54 -0
- data/lib/mod_spox/ConfigurationWizard.rb +178 -0
- data/lib/mod_spox/Database.rb +25 -0
- data/lib/mod_spox/Exceptions.rb +35 -0
- data/lib/mod_spox/Helpers.rb +35 -0
- data/lib/mod_spox/Loader.rb +79 -0
- data/lib/mod_spox/Logger.rb +31 -0
- data/lib/mod_spox/MessageFactory.rb +73 -0
- data/lib/mod_spox/Monitors.rb +59 -0
- data/lib/mod_spox/Pipeline.rb +148 -0
- data/lib/mod_spox/Plugin.rb +18 -0
- data/lib/mod_spox/PluginManager.rb +105 -0
- data/lib/mod_spox/Pool.rb +50 -0
- data/lib/mod_spox/Socket.rb +171 -0
- data/lib/mod_spox/Timer.rb +138 -0
- data/lib/mod_spox/handlers/BadNick.rb +16 -0
- data/lib/mod_spox/handlers/Bounce.rb +15 -0
- data/lib/mod_spox/handlers/Created.rb +16 -0
- data/lib/mod_spox/handlers/Handler.rb +31 -0
- data/lib/mod_spox/handlers/Invite.rb +19 -0
- data/lib/mod_spox/handlers/Join.rb +30 -0
- data/lib/mod_spox/handlers/Kick.rb +24 -0
- data/lib/mod_spox/handlers/LuserChannels.rb +16 -0
- data/lib/mod_spox/handlers/LuserClient.rb +16 -0
- data/lib/mod_spox/handlers/LuserMe.rb +14 -0
- data/lib/mod_spox/handlers/LuserOp.rb +16 -0
- data/lib/mod_spox/handlers/LuserUnknown.rb +16 -0
- data/lib/mod_spox/handlers/Mode.rb +47 -0
- data/lib/mod_spox/handlers/Motd.rb +30 -0
- data/lib/mod_spox/handlers/MyInfo.rb +21 -0
- data/lib/mod_spox/handlers/Names.rb +54 -0
- data/lib/mod_spox/handlers/Nick.rb +24 -0
- data/lib/mod_spox/handlers/NickInUse.rb +16 -0
- data/lib/mod_spox/handlers/Notice.rb +32 -0
- data/lib/mod_spox/handlers/Part.rb +19 -0
- data/lib/mod_spox/handlers/Ping.rb +16 -0
- data/lib/mod_spox/handlers/Pong.rb +16 -0
- data/lib/mod_spox/handlers/Privmsg.rb +27 -0
- data/lib/mod_spox/handlers/Quit.rb +21 -0
- data/lib/mod_spox/handlers/Topic.rb +29 -0
- data/lib/mod_spox/handlers/Welcome.rb +34 -0
- data/lib/mod_spox/handlers/Who.rb +60 -0
- data/lib/mod_spox/handlers/Whois.rb +63 -0
- data/lib/mod_spox/handlers/YourHost.rb +17 -0
- data/lib/mod_spox/messages/incoming/BadNick.rb +15 -0
- data/lib/mod_spox/messages/incoming/Bounce.rb +17 -0
- data/lib/mod_spox/messages/incoming/Created.rb +14 -0
- data/lib/mod_spox/messages/incoming/Invite.rb +20 -0
- data/lib/mod_spox/messages/incoming/Join.rb +18 -0
- data/lib/mod_spox/messages/incoming/Kick.rb +25 -0
- data/lib/mod_spox/messages/incoming/LuserChannels.rb +14 -0
- data/lib/mod_spox/messages/incoming/LuserClient.rb +23 -0
- data/lib/mod_spox/messages/incoming/LuserMe.rb +17 -0
- data/lib/mod_spox/messages/incoming/LuserOp.rb +14 -0
- data/lib/mod_spox/messages/incoming/LuserUnknown.rb +14 -0
- data/lib/mod_spox/messages/incoming/Message.rb +22 -0
- data/lib/mod_spox/messages/incoming/Mode.rb +41 -0
- data/lib/mod_spox/messages/incoming/Motd.rb +17 -0
- data/lib/mod_spox/messages/incoming/MyInfo.rb +23 -0
- data/lib/mod_spox/messages/incoming/Names.rb +23 -0
- data/lib/mod_spox/messages/incoming/Nick.rb +25 -0
- data/lib/mod_spox/messages/incoming/NickInUse.rb +14 -0
- data/lib/mod_spox/messages/incoming/Notice.rb +8 -0
- data/lib/mod_spox/messages/incoming/Part.rb +20 -0
- data/lib/mod_spox/messages/incoming/Ping.rb +17 -0
- data/lib/mod_spox/messages/incoming/Pong.rb +8 -0
- data/lib/mod_spox/messages/incoming/Privmsg.rb +64 -0
- data/lib/mod_spox/messages/incoming/Quit.rb +17 -0
- data/lib/mod_spox/messages/incoming/Topic.rb +20 -0
- data/lib/mod_spox/messages/incoming/TopicInfo.rb +20 -0
- data/lib/mod_spox/messages/incoming/Welcome.rb +26 -0
- data/lib/mod_spox/messages/incoming/Who.rb +17 -0
- data/lib/mod_spox/messages/incoming/Whois.rb +47 -0
- data/lib/mod_spox/messages/incoming/YourHost.rb +17 -0
- data/lib/mod_spox/messages/internal/BotInitialized.rb +11 -0
- data/lib/mod_spox/messages/internal/ChangeNick.rb +15 -0
- data/lib/mod_spox/messages/internal/Connected.rb +20 -0
- data/lib/mod_spox/messages/internal/ConnectionFailed.rb +23 -0
- data/lib/mod_spox/messages/internal/Disconnected.rb +8 -0
- data/lib/mod_spox/messages/internal/Disconnecting.rb +8 -0
- data/lib/mod_spox/messages/internal/EstablishConnection.rb +22 -0
- data/lib/mod_spox/messages/internal/HaltBot.rb +8 -0
- data/lib/mod_spox/messages/internal/NickRequest.rb +8 -0
- data/lib/mod_spox/messages/internal/NickResponse.rb +14 -0
- data/lib/mod_spox/messages/internal/PluginLoadRequest.rb +20 -0
- data/lib/mod_spox/messages/internal/PluginLoadResponse.rb +16 -0
- data/lib/mod_spox/messages/internal/PluginModuleRequest.rb +13 -0
- data/lib/mod_spox/messages/internal/PluginModuleResponse.rb +17 -0
- data/lib/mod_spox/messages/internal/PluginReload.rb +8 -0
- data/lib/mod_spox/messages/internal/PluginRequest.rb +17 -0
- data/lib/mod_spox/messages/internal/PluginResponse.rb +20 -0
- data/lib/mod_spox/messages/internal/PluginUnloadRequest.rb +8 -0
- data/lib/mod_spox/messages/internal/PluginUnloadResponse.rb +8 -0
- data/lib/mod_spox/messages/internal/Request.rb +15 -0
- data/lib/mod_spox/messages/internal/Response.rb +15 -0
- data/lib/mod_spox/messages/internal/Shutdown.rb +8 -0
- data/lib/mod_spox/messages/internal/SignaturesUpdate.rb +8 -0
- data/lib/mod_spox/messages/internal/StatusRequest.rb +9 -0
- data/lib/mod_spox/messages/internal/StatusResponse.rb +17 -0
- data/lib/mod_spox/messages/internal/TimerAdd.rb +27 -0
- data/lib/mod_spox/messages/internal/TimerClear.rb +8 -0
- data/lib/mod_spox/messages/internal/TimerRemove.rb +15 -0
- data/lib/mod_spox/messages/internal/TimerResponse.rb +26 -0
- data/lib/mod_spox/messages/internal/TriggersUpdate.rb +8 -0
- data/lib/mod_spox/messages/outgoing/Admin.rb +15 -0
- data/lib/mod_spox/messages/outgoing/Away.rb +10 -0
- data/lib/mod_spox/messages/outgoing/ChannelMode.rb +25 -0
- data/lib/mod_spox/messages/outgoing/Connect.rb +24 -0
- data/lib/mod_spox/messages/outgoing/Die.rb +9 -0
- data/lib/mod_spox/messages/outgoing/Info.rb +15 -0
- data/lib/mod_spox/messages/outgoing/Invite.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Ison.rb +15 -0
- data/lib/mod_spox/messages/outgoing/Join.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Kick.rb +23 -0
- data/lib/mod_spox/messages/outgoing/Kill.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Links.rb +19 -0
- data/lib/mod_spox/messages/outgoing/List.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Lusers.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Motd.rb +16 -0
- data/lib/mod_spox/messages/outgoing/Names.rb +20 -0
- data/lib/mod_spox/messages/outgoing/Nick.rb +16 -0
- data/lib/mod_spox/messages/outgoing/Notice.rb +11 -0
- data/lib/mod_spox/messages/outgoing/Oper.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Part.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Pass.rb +16 -0
- data/lib/mod_spox/messages/outgoing/Ping.rb +10 -0
- data/lib/mod_spox/messages/outgoing/Pong.rb +17 -0
- data/lib/mod_spox/messages/outgoing/Privmsg.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Quit.rb +10 -0
- data/lib/mod_spox/messages/outgoing/Rehash.rb +9 -0
- data/lib/mod_spox/messages/outgoing/Restart.rb +9 -0
- data/lib/mod_spox/messages/outgoing/ServList.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Simple.rb +12 -0
- data/lib/mod_spox/messages/outgoing/Squery.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Squit.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Stats.rb +18 -0
- data/lib/mod_spox/messages/outgoing/Summon.rb +23 -0
- data/lib/mod_spox/messages/outgoing/Time.rb +15 -0
- data/lib/mod_spox/messages/outgoing/Topic.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Trace.rb +15 -0
- data/lib/mod_spox/messages/outgoing/Unaway.rb +9 -0
- data/lib/mod_spox/messages/outgoing/User.rb +23 -0
- data/lib/mod_spox/messages/outgoing/UserHost.rb +15 -0
- data/lib/mod_spox/messages/outgoing/UserMode.rb +19 -0
- data/lib/mod_spox/messages/outgoing/Users.rb +15 -0
- data/lib/mod_spox/messages/outgoing/Version.rb +16 -0
- data/lib/mod_spox/messages/outgoing/Who.rb +19 -0
- data/lib/mod_spox/messages/outgoing/WhoWas.rb +23 -0
- data/lib/mod_spox/messages/outgoing/Whois.rb +19 -0
- data/lib/mod_spox/migration/001_create_auths.rb +13 -0
- data/lib/mod_spox/migration/001_create_channel.rb +13 -0
- data/lib/mod_spox/migration/001_create_channel_modes.rb +13 -0
- data/lib/mod_spox/migration/001_create_config.rb +13 -0
- data/lib/mod_spox/migration/001_create_nick_channels.rb +13 -0
- data/lib/mod_spox/migration/001_create_nick_modes.rb +13 -0
- data/lib/mod_spox/migration/001_create_nicks.rb +13 -0
- data/lib/mod_spox/migration/001_create_servers.rb +13 -0
- data/lib/mod_spox/migration/001_create_settings.rb +13 -0
- data/lib/mod_spox/migration/001_create_signatures.rb +13 -0
- data/lib/mod_spox/migration/001_create_triggers.rb +13 -0
- data/lib/mod_spox/models/Auth.rb +79 -0
- data/lib/mod_spox/models/AuthGroup.rb +15 -0
- data/lib/mod_spox/models/Channel.rb +47 -0
- data/lib/mod_spox/models/ChannelMode.rb +14 -0
- data/lib/mod_spox/models/Config.rb +31 -0
- data/lib/mod_spox/models/Group.rb +13 -0
- data/lib/mod_spox/models/Nick.rb +110 -0
- data/lib/mod_spox/models/NickChannel.rb +43 -0
- data/lib/mod_spox/models/NickMode.rb +18 -0
- data/lib/mod_spox/models/Server.rb +12 -0
- data/lib/mod_spox/models/Setting.rb +40 -0
- data/lib/mod_spox/models/Signature.rb +30 -0
- data/lib/mod_spox/models/Trigger.rb +9 -0
- data/lib/mod_spox/rfc2812.rb +171 -0
- metadata +261 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
|
|
3
|
+
class Pipeline < Pool
|
|
4
|
+
|
|
5
|
+
# procs:: number of threads running in Pool
|
|
6
|
+
# Create a new Pipeline
|
|
7
|
+
def initialize(procs=3)
|
|
8
|
+
super(procs)
|
|
9
|
+
@messageq = Queue.new
|
|
10
|
+
Logger.log("Created queue #{@messageq} in pipeline", 10)
|
|
11
|
+
@hooks = Hash.new
|
|
12
|
+
@plugins = Hash.new
|
|
13
|
+
populate_triggers
|
|
14
|
+
populate_signatures
|
|
15
|
+
hook(self, :populate_triggers, :Internal_TriggersUpdate)
|
|
16
|
+
hook(self, :populate_signatures, :Internal_SignaturesUpdate)
|
|
17
|
+
start_pool
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# message:: Message to send down pipeline
|
|
21
|
+
# Queues a message to send down pipeline
|
|
22
|
+
def <<(message)
|
|
23
|
+
Logger.log("Message added to pipeline queue: #{message}", 5)
|
|
24
|
+
@messageq << message
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# plugin:: Plugin to hook to pipeline
|
|
28
|
+
# Hooks a plugin into the pipeline so it can be called
|
|
29
|
+
# directly when it matches a trigger
|
|
30
|
+
def hook_plugin(plugin)
|
|
31
|
+
Logger.log("Plugin #{plugin.name} hooking into pipeline", 10)
|
|
32
|
+
@plugins[plugin.name.to_sym] = plugin
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# plugin:: Plugin to unhook from pipeline
|
|
36
|
+
# Unhooks a plugin from the pipeline (This does not unhook
|
|
37
|
+
# it from the standard hooks)
|
|
38
|
+
def unhook_plugin(plugin)
|
|
39
|
+
Logger.log("Plugin #{plugin.name} unhooking from plugin", 10)
|
|
40
|
+
@plugins.delete(plugin.name.to_sym)
|
|
41
|
+
@hooks.each_pair do |type, things|
|
|
42
|
+
things.delete(plugin.name.to_sym) if things.has_key?(plugin.name.to_sym)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# plugin:: Plugin to hook to pipeline
|
|
47
|
+
# method:: Plugin method pipeline should call to process message
|
|
48
|
+
# type:: Type of message the plugin wants to process
|
|
49
|
+
# Hooks a plugin into the pipeline for a specific type of message
|
|
50
|
+
def hook(object, method, type)
|
|
51
|
+
Logger.log("Object #{object.class.to_s} hooking into messages of type: #{type}", 10)
|
|
52
|
+
type = type.gsub(/::/, '_').to_sym unless type.is_a?(Symbol)
|
|
53
|
+
method = method.to_sym unless method.is_a?(Symbol)
|
|
54
|
+
name = object.class.to_s.gsub(/^.+:/, '')
|
|
55
|
+
@hooks[type] = Hash.new unless @hooks.has_key?(type)
|
|
56
|
+
@hooks[type][name.to_sym] = Array.new unless @hooks[type][name.to_sym].is_a?(Array)
|
|
57
|
+
@hooks[type][name.to_sym] << {:object => object, :method => method}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# plugin:: Plugin to unhook from pipeline
|
|
61
|
+
# type:: Type of message the plugin no longer wants to process
|
|
62
|
+
# This will remove the hook a plugin has for a specific message type
|
|
63
|
+
def unhook(object, method, type)
|
|
64
|
+
Logger.log("Object #{object.class.to_s} unhooking from messages of type: #{type}", 10)
|
|
65
|
+
type = type.gsub(/::/, '_').to_sym unless type.is_a?(Symbol)
|
|
66
|
+
name = object.class.gsub(/^.+:/, '').to_sym
|
|
67
|
+
raise Exception::InvalidValue.new("Unknown hook type given: #{type.to_s}") unless @hooks.has_key?(type)
|
|
68
|
+
raise Exception::InvalidValue.new("Unknown object hooked: #{name.to_s}") unless @hooks[type].has_key?(name)
|
|
69
|
+
@hooks[type][name].each{|hook|
|
|
70
|
+
@hooks[type][name].delete(hook) if hook[:method] == method
|
|
71
|
+
}
|
|
72
|
+
@hooks[type].delete(name) if @hooks[type][name].empty
|
|
73
|
+
@hooks.delete(type) if @hooks[type].empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Clears all hooks from the pipeline (Commonly used when reloading plugins)
|
|
77
|
+
def clear
|
|
78
|
+
Logger.log("All hooks have been cleared from pipeline", 10)
|
|
79
|
+
@hooks.clear
|
|
80
|
+
@plugins.clear
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def populate_triggers(m=nil)
|
|
84
|
+
@triggers = []
|
|
85
|
+
Models::Trigger.filter(:active => true).each{|t|@triggers << t.trigger}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def populate_signatures(m=nil)
|
|
89
|
+
@signatures = []
|
|
90
|
+
Models::Signature.all.each{|s|@signatures << s}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Processes messages
|
|
96
|
+
def processor
|
|
97
|
+
begin
|
|
98
|
+
message = @messageq.pop
|
|
99
|
+
Logger.log("Pipeline is processing a message: #{message}", 10)
|
|
100
|
+
parse(message)
|
|
101
|
+
type = message.class.to_s.gsub(/^ModSpox::Messages::/, '').gsub(/::/, '_').to_sym
|
|
102
|
+
mod = type.to_s.gsub(/_.+$/, '').to_sym
|
|
103
|
+
Logger.log("Pipeline determines that #{message} is of type: #{type}", 10)
|
|
104
|
+
[type, mod, :all].each{|type|
|
|
105
|
+
if(@hooks.has_key?(type))
|
|
106
|
+
@hooks[type].each_value{|objects|
|
|
107
|
+
objects.each{|v| v[:object].send(v[:method].to_s, message) }
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
}
|
|
111
|
+
rescue Object => boom
|
|
112
|
+
Logger.log("Pipeline encountered an exception while processing a message: #{boom}\n#{boom.backtrace.join("\n")}", 10)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# message:: Message to parse
|
|
117
|
+
# This will parse a message to see if it matches any valid
|
|
118
|
+
# trigger signatures. If matches are found, they will be sent
|
|
119
|
+
# to the proper plugin for processing
|
|
120
|
+
def parse(message)
|
|
121
|
+
return unless message.kind_of?(Messages::Incoming::Privmsg) || message.kind_of?(Messages::Incoming::Notice)
|
|
122
|
+
trigger = nil
|
|
123
|
+
@triggers.each{|t| trigger = t if message.message =~ /^#{t}/}
|
|
124
|
+
if(!trigger.nil? || message.addressed?)
|
|
125
|
+
Logger.log("Message has matched against a known trigger", 15)
|
|
126
|
+
@signatures.each{|sig|
|
|
127
|
+
Logger.log("Matching against: #{trigger}#{sig.signature}")
|
|
128
|
+
res = message.message.scan(/^#{trigger}#{sig.signature}$/)
|
|
129
|
+
if(res.size > 0)
|
|
130
|
+
next unless message.source.auth_groups.include?(sig.group) || sig.group.nil?
|
|
131
|
+
params = Hash.new
|
|
132
|
+
sig.params.size.times do |i|
|
|
133
|
+
params[sig.params[i - 1].to_sym] = res[0][i]
|
|
134
|
+
Logger.log("Signature params: #{sig.params[i - 1].to_sym} = #{res[0][i]}")
|
|
135
|
+
end
|
|
136
|
+
if(@plugins.has_key?(sig.plugin.to_sym))
|
|
137
|
+
@plugins[sig.plugin.to_sym].send(sig.values[:method], message, params)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
}
|
|
141
|
+
else
|
|
142
|
+
Logger.log("Message failed to match any known trigger", 15)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
class Plugin
|
|
3
|
+
def initialize(pipeline)
|
|
4
|
+
@pipeline = pipeline
|
|
5
|
+
@pipeline.hook_plugin(self)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Called before the object is destroyed by the ModSpox::PluginManager
|
|
9
|
+
def destroy
|
|
10
|
+
Logger.log("Destroy method for plugin #{name} has not been defined.", 15)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns the name of the class
|
|
14
|
+
def name
|
|
15
|
+
self.class.name.to_s.gsub(/^.+:/, '')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
|
|
3
|
+
class PluginManager
|
|
4
|
+
|
|
5
|
+
# Hash of plugins. Defined by class name symbol (i.e. Trivia class: plugins[:Trivia])
|
|
6
|
+
attr_reader :plugins
|
|
7
|
+
|
|
8
|
+
# pipeline:: Pipeline for messages
|
|
9
|
+
# Create new PluginManager
|
|
10
|
+
def initialize(pipeline)
|
|
11
|
+
@plugins = Hash.new
|
|
12
|
+
@pipeline = pipeline
|
|
13
|
+
@pipeline.hook(self, :load_plugin, :Internal_PluginLoadRequest)
|
|
14
|
+
@pipeline.hook(self, :unload_plugin, :Internal_PluginUnloadRequest)
|
|
15
|
+
@pipeline.hook(self, :reload_plugins, :Internal_PluginReload)
|
|
16
|
+
@pipeline.hook(self, :send_modules, :Internal_PluginModuleRequest)
|
|
17
|
+
@plugins_module = Module.new
|
|
18
|
+
load_plugins
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# message:: Messages::Internal::PluginReload
|
|
22
|
+
# Destroys and reinitializes plugins
|
|
23
|
+
def reload_plugins(mesasge=nil)
|
|
24
|
+
unload_plugins
|
|
25
|
+
load_plugins
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Destroys plugins
|
|
29
|
+
def destroy_plugins
|
|
30
|
+
unload_plugins
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# message:: Messages::Internal::PluginLoadRequest
|
|
34
|
+
# Loads a plugin
|
|
35
|
+
def load_plugin(message)
|
|
36
|
+
begin
|
|
37
|
+
Logger.log("THE MESSAGE NAME IS: #{message.name}")
|
|
38
|
+
path = message.name.nil? ? BotConfig[:userpluginpath] : "#{BotConfig[:userpluginpath]}/#{message.name}"
|
|
39
|
+
File.copy(message.path, path)
|
|
40
|
+
reload_plugins
|
|
41
|
+
@pipeline << Messages::Internal::PluginLoadResponse.new(message.requester, true)
|
|
42
|
+
Logger.log("Loaded new plugin: #{message.path}", 10)
|
|
43
|
+
rescue Object => boom
|
|
44
|
+
Logger.log("Failed to load plugin: #{message.path}", 10)
|
|
45
|
+
@pipeline << Messages::Internal::PluginLoadResponse.new(message.requester, false)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# message:: Messages::Internal::PluginUnloadRequest
|
|
50
|
+
# Unloads a plugin
|
|
51
|
+
def unload_plugin(message)
|
|
52
|
+
begin
|
|
53
|
+
unless(message.name.nil?)
|
|
54
|
+
File.copy(message.path, "#{BotConfig[:userpluginpath]}/#{message.name}")
|
|
55
|
+
end
|
|
56
|
+
File.unlink(message.path)
|
|
57
|
+
reload_plugins
|
|
58
|
+
@pipeline << Messages::Internal::PluginUnloadResponse.new(message.requester, true)
|
|
59
|
+
Logger.log("Unloaded plugin: #{message.path}", 10)
|
|
60
|
+
rescue Object => boom
|
|
61
|
+
Logger.log("Failed to unload plugin: #{message.path} Reason: #{boom}", 10)
|
|
62
|
+
@pipeline << Messages::Internal::PluginUnloadResponse.new(message.requester, false)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# message:: Messages::Internal::PluginModuleRequest
|
|
67
|
+
# Sends the plugins module to the requester
|
|
68
|
+
def send_modules(message)
|
|
69
|
+
@pipeline << Messages::Internal::PluginModuleResponse.new(message.requester, @plugins_module)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Loads and initializes plugins
|
|
75
|
+
def load_plugins
|
|
76
|
+
@pipeline << Messages::Internal::TimerClear.new
|
|
77
|
+
[BotConfig[:pluginpath], BotConfig[:userpluginpath]].each{|path|
|
|
78
|
+
Dir.new(path).each{|file|
|
|
79
|
+
if(file =~ /^[^\.].+\.rb$/)
|
|
80
|
+
@plugins_module.module_eval(IO.readlines("#{path}/#{file}").join("\n"))
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
@plugins_module.constants.each{|const|
|
|
85
|
+
klass = @plugins_module.const_get(const)
|
|
86
|
+
if(klass < Plugin)
|
|
87
|
+
@plugins[const.to_sym] = klass.new(@pipeline)
|
|
88
|
+
Logger.log("Initialized new plugin: #{const}", 15)
|
|
89
|
+
end
|
|
90
|
+
}
|
|
91
|
+
@pipeline << Messages::Internal::SignaturesUpdate.new
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Destroys plugins
|
|
95
|
+
def unload_plugins
|
|
96
|
+
@plugins.each_pair{|sym, plugin| plugin.destroy; @pipeline.unhook_plugin(plugin)}
|
|
97
|
+
@plugins.clear
|
|
98
|
+
Models::Signature.delete_all
|
|
99
|
+
@plugins_module = Module.new
|
|
100
|
+
@pipeline << Messages::Internal::TimerClear.new
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
|
|
3
|
+
# The Pool class is used to reduce thread creation. When
|
|
4
|
+
# used in conjuntion with a PoolQueue, it provides an easy
|
|
5
|
+
# way to process many objects in an asynchronise manner
|
|
6
|
+
class Pool
|
|
7
|
+
|
|
8
|
+
# num_procs:: Number of threads to use
|
|
9
|
+
# Create a new Pool
|
|
10
|
+
def initialize(num_procs=2)
|
|
11
|
+
@num_threads = num_procs
|
|
12
|
+
@threads = Array.new
|
|
13
|
+
@kill = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Stop all the running threads
|
|
17
|
+
def destroy
|
|
18
|
+
@kill = true
|
|
19
|
+
@threads.each{|t|
|
|
20
|
+
Logger.log("Shutting down thread: #{t} in #{self.class.to_s}", 10)
|
|
21
|
+
t.exit
|
|
22
|
+
}
|
|
23
|
+
sleep(0.1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Starts the pool
|
|
27
|
+
def start_pool
|
|
28
|
+
@num_threads.times do
|
|
29
|
+
@threads << Thread.new{
|
|
30
|
+
until @kill do
|
|
31
|
+
processor
|
|
32
|
+
end
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Method the pool uses to do stuff.
|
|
38
|
+
# (It is important to note that using this can
|
|
39
|
+
# very easily eat up all your CPU time. The Processor
|
|
40
|
+
# method must yield at some point, otherwise it will
|
|
41
|
+
# just continue to loop, even if it is doing nothing.
|
|
42
|
+
# This is the reason for the PoolQueue as this Pool
|
|
43
|
+
# was created as a way to quickly process messages)
|
|
44
|
+
def processor
|
|
45
|
+
raise Exceptions::NotImplemented.new('Processor method has not been implemented')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
|
|
3
|
+
class Socket
|
|
4
|
+
|
|
5
|
+
attr_reader :sent
|
|
6
|
+
attr_reader :received
|
|
7
|
+
attr_reader :burst
|
|
8
|
+
attr_reader :burst_in
|
|
9
|
+
attr_reader :delay
|
|
10
|
+
attr_reader :server
|
|
11
|
+
attr_reader :port
|
|
12
|
+
|
|
13
|
+
# factory:: MessageFactory to parse messages
|
|
14
|
+
# server:: Server to connect to
|
|
15
|
+
# port:: Port number to connect to
|
|
16
|
+
# delay:: Number of seconds to delay between bursts
|
|
17
|
+
# burst_in:: Number of seconds allowed to burst
|
|
18
|
+
# burst:: Number of lines allowed to be sent within the burst_in time limit
|
|
19
|
+
# Create a new Socket
|
|
20
|
+
def initialize(bot, server, port, delay=2, burst_in=2, burst=4)
|
|
21
|
+
@factory = bot.factory
|
|
22
|
+
@pipeline = bot.pipeline
|
|
23
|
+
@server = server
|
|
24
|
+
@port = port.to_i
|
|
25
|
+
@sent = 0
|
|
26
|
+
@received = 0
|
|
27
|
+
@delay = delay.to_f > 0 ? delay.to_f : 2.0
|
|
28
|
+
@burst = burst.to_i > 0 ? burst.to_i : 4
|
|
29
|
+
@burst_in = 2
|
|
30
|
+
@kill = false
|
|
31
|
+
@reader_thread = nil
|
|
32
|
+
@writer_thread = nil
|
|
33
|
+
@time_check = nil
|
|
34
|
+
@check_burst = 0
|
|
35
|
+
@pause = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Connects to the IRC server
|
|
39
|
+
def connect
|
|
40
|
+
Logger.log("Establishing connection to #{@server}:#{@port}", 10)
|
|
41
|
+
@socket = TCPSocket.new(@server, @port)
|
|
42
|
+
server = Models::Server.find_or_create(:host => @server, :port => @port)
|
|
43
|
+
server.connected = true
|
|
44
|
+
server.save
|
|
45
|
+
@sendq = Queue.new
|
|
46
|
+
Logger.log("Created new send queue: #{@sendq}", 10)
|
|
47
|
+
spooler
|
|
48
|
+
reader
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# new_delay:: Seconds to delay between bursts
|
|
52
|
+
# Resets the delay
|
|
53
|
+
def delay=(new_delay)
|
|
54
|
+
raise Exceptions::InvalidValue('Send delay must be a positive number') unless new_delay.to_f > 0
|
|
55
|
+
@delay = new_delay.to_f
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# new_burst:: Number of lines allowed in burst
|
|
59
|
+
# Resets the burst
|
|
60
|
+
def burst=(new_burst)
|
|
61
|
+
raise Exceptions::InvalidValue('Burst value must be a positive number') unless new_busrt.to_i > 0
|
|
62
|
+
@burst = new_burst
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# new_burst_in:: Number of seconds allowed to burst
|
|
66
|
+
# Resets the burst_in
|
|
67
|
+
def burst_in=(new_burst_in)
|
|
68
|
+
raise Exceptions::InvalidValue('Burst in value must be positive') unless new_burst_in.to_i > 0
|
|
69
|
+
@burst_in = new_burst_in
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# message:: String to send to server
|
|
73
|
+
# Sends a string to the IRC server
|
|
74
|
+
def puts(message)
|
|
75
|
+
write(message)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# message:: String to send to server
|
|
79
|
+
# Sends a string to the IRC server
|
|
80
|
+
def write(message)
|
|
81
|
+
return if message.nil?
|
|
82
|
+
Logger.log("<< #{message}", 5)
|
|
83
|
+
@socket.send(message + "\n", 0)
|
|
84
|
+
@last_send = Time.new
|
|
85
|
+
@sent += 1
|
|
86
|
+
@check_burst += 1
|
|
87
|
+
@check_time = Time.now if @check_time.nil?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Retrieves a string from the server
|
|
91
|
+
def gets
|
|
92
|
+
read
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Retrieves a string from the server
|
|
96
|
+
def read
|
|
97
|
+
message = @socket.gets
|
|
98
|
+
if(message.nil?)
|
|
99
|
+
@pipeline << Messages::Internal::Disconnected.new
|
|
100
|
+
shutdown
|
|
101
|
+
server = Models::Server.find_or_create(:host => @server, :port => @port)
|
|
102
|
+
server.connected = false
|
|
103
|
+
server.save
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
Logger.log(">> #{message}", 5)
|
|
107
|
+
@received += 1
|
|
108
|
+
message.strip!
|
|
109
|
+
return message
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# message:: String to be sent to server
|
|
113
|
+
# Queues a message up to be sent to the IRC server
|
|
114
|
+
def <<(message)
|
|
115
|
+
queue(message)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# message:: String to be sent to server
|
|
119
|
+
# Queues a message up to be sent to the IRC server
|
|
120
|
+
def queue(message)
|
|
121
|
+
@sendq << message
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Starts the thread for sending messages to the server
|
|
125
|
+
def spooler
|
|
126
|
+
@writer_thread = Thread.new{
|
|
127
|
+
until @kill do
|
|
128
|
+
write(@sendq.pop)
|
|
129
|
+
if((Time.now - @check_time) > @burst_in && @check_burst > @burst)
|
|
130
|
+
sleep(@delay)
|
|
131
|
+
@check_time = nil
|
|
132
|
+
@check_burst = 0
|
|
133
|
+
elsif((Time.now - @check_time) > @burst_in && @check_burst < @burst)
|
|
134
|
+
@check_time = nil
|
|
135
|
+
@check_burst = 0
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Starts the thread for reading messages from the server
|
|
142
|
+
def reader
|
|
143
|
+
@reader_thread = Thread.new{
|
|
144
|
+
until @kill do
|
|
145
|
+
Kernel.select([@socket], nil, nil, nil)
|
|
146
|
+
@factory << read
|
|
147
|
+
end
|
|
148
|
+
@sendq.clear
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# restart:: Reconnect after closing connection
|
|
153
|
+
# Closes connection to IRC server
|
|
154
|
+
def shutdown(restart=false)
|
|
155
|
+
@kill = true
|
|
156
|
+
@reader_thread.join(0.1)
|
|
157
|
+
@reader_thread.kill if @reader_thread.alive?
|
|
158
|
+
@writer_thread.join(0.1)
|
|
159
|
+
@writer_thread.kill if @writer_thread.alive?
|
|
160
|
+
@reader_thread = nil
|
|
161
|
+
@writer_thread = nil
|
|
162
|
+
@socket.close
|
|
163
|
+
server = Models::Server.find_or_create(:host => @server, :port => @port)
|
|
164
|
+
server.connected = false
|
|
165
|
+
server.save
|
|
166
|
+
connect if restart
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
|
|
3
|
+
class Timer < Pool
|
|
4
|
+
|
|
5
|
+
# pipeline:: message pipeline
|
|
6
|
+
# procs:: number of threads in the pool
|
|
7
|
+
# Create a new Timer
|
|
8
|
+
def initialize(pipeline, procs=2)
|
|
9
|
+
super(procs)
|
|
10
|
+
@pipeline = pipeline
|
|
11
|
+
@timers = Array.new
|
|
12
|
+
@actions = Queue.new
|
|
13
|
+
Logger.log("Created queue: #{@actions} in timer", 10)
|
|
14
|
+
@monitor = Monitors::Timer.new
|
|
15
|
+
@thread = nil
|
|
16
|
+
@stop_timer = false
|
|
17
|
+
{:Internal_TimerAdd => :add_message,
|
|
18
|
+
:Internal_TimerRemove => :remove_message}.each_pair{|type,method|
|
|
19
|
+
@pipeline.hook(self, method, type)
|
|
20
|
+
}
|
|
21
|
+
start_pool
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Wakes the timer up early
|
|
25
|
+
def wakeup
|
|
26
|
+
Logger.log("Timer has been explicitly told to wakeup", 15)
|
|
27
|
+
@monitor.wakeup unless @thread.nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# message:: TimerAdd message
|
|
31
|
+
# Add a recurring code block
|
|
32
|
+
def add_message(message)
|
|
33
|
+
Logger.log("New block is being added to the timer", 15)
|
|
34
|
+
action = add(message.period, message.once, message.data, &message.block)
|
|
35
|
+
begin
|
|
36
|
+
@pipeline << Messages::Internal::TimerResponse.new(message.requester, action, true)
|
|
37
|
+
Logger.log("New block was successfully added to the timer", 15)
|
|
38
|
+
rescue Object => boom
|
|
39
|
+
Logger.log("Failed to add block to timer: #{boom}", 10)
|
|
40
|
+
@pipeline << Messages::Internal::TimerResponse.new(message.requester, action, false)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# message:: TimerRemove message
|
|
45
|
+
# Remove an action from the timer
|
|
46
|
+
def remove_message(message)
|
|
47
|
+
remove(message.action)
|
|
48
|
+
Logger.log("Action has been removed from the Timer", 15)
|
|
49
|
+
@pipeline << Messages::Internal::TimerResponse.new(nil, message.action, false)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# period:: seconds between running action
|
|
53
|
+
# once:: only run action once
|
|
54
|
+
# data:: data to be available
|
|
55
|
+
# &func:: data block to run
|
|
56
|
+
# Adds a new action to the timer
|
|
57
|
+
def add(period, once=false, data=nil, &func)
|
|
58
|
+
action = Action.new(self, period, data, once, &func)
|
|
59
|
+
@timers << action
|
|
60
|
+
wakeup
|
|
61
|
+
return action
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# action:: Action to add to timer's queue
|
|
65
|
+
# Adds a new action to the timer
|
|
66
|
+
def add_action(action)
|
|
67
|
+
raise Exceptions::InvalidType.new('An Action object must be supplied') unless action.is_a?(Action)
|
|
68
|
+
@timers << action
|
|
69
|
+
wakeup
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# action:: Action to remove from timer's queue
|
|
73
|
+
# Removes and action from the timer
|
|
74
|
+
def remove(action)
|
|
75
|
+
raise Exceptions::InvalidType.new('An Action object must be supplied') unless action.is_a?(Action)
|
|
76
|
+
@timers.delete(action)
|
|
77
|
+
wakeup
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Starts the timer
|
|
81
|
+
def start
|
|
82
|
+
raise Exceptions::AlreadyRunning.new('Timer is already running') unless @thread.nil?
|
|
83
|
+
@thread = Thread.new{
|
|
84
|
+
until @stop_timer do
|
|
85
|
+
to_sleep = nil
|
|
86
|
+
@timers.each do |a|
|
|
87
|
+
to_sleep = a.remaining if to_sleep.nil?
|
|
88
|
+
to_sleep = a.remaining if !a.remaining.nil? && a.remaining < to_sleep
|
|
89
|
+
end
|
|
90
|
+
Logger.log("Timer is set to sleep for #{to_sleep.nil? ? 'forever' : "#{to_sleep} seconds"}", 15)
|
|
91
|
+
actual_sleep = @monitor.wait(to_sleep)
|
|
92
|
+
tick(actual_sleep)
|
|
93
|
+
Logger.log("Timer was set to sleep for #{to_sleep.nil? ? 'forever' : "#{to_sleep} seconds"} seconds. Actual sleep time: #{actual_sleep} seconds", 15)
|
|
94
|
+
end
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Stops the timer
|
|
99
|
+
def stop
|
|
100
|
+
raise Exceptions::NotRunning.new('Timer is not running') if @thread.nil?
|
|
101
|
+
@stop_timer = true
|
|
102
|
+
wakeup
|
|
103
|
+
@thread.join
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Clears all actions in the timer's queue
|
|
107
|
+
def clear
|
|
108
|
+
@actions.clear
|
|
109
|
+
@timers.clear
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# time_passed:: time passed since last tick
|
|
115
|
+
# Decrements all Actions the given amount of time
|
|
116
|
+
def tick(time_passed)
|
|
117
|
+
for action in @timers do
|
|
118
|
+
action.tick(time_passed)
|
|
119
|
+
if(action.due?)
|
|
120
|
+
@actions << action.schedule
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Process the actions
|
|
126
|
+
def processor
|
|
127
|
+
action = @actions.pop
|
|
128
|
+
begin
|
|
129
|
+
action.run
|
|
130
|
+
remove(action) if action.is_complete?
|
|
131
|
+
rescue Object => boom
|
|
132
|
+
Logger.log("Timer block generated an exception: #{boom}\n#{boom.backtrace.join("\n")}", 5)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
module Handlers
|
|
3
|
+
class BadNick < Handler
|
|
4
|
+
def initialize(handlers)
|
|
5
|
+
handlers[ERR_ERRONEOUSNICKNAME] = self
|
|
6
|
+
end
|
|
7
|
+
def process(string)
|
|
8
|
+
if(string =~ /#{RPL_ERRORNEOUSNICK}\s\S+\s(\S+)\s:/)
|
|
9
|
+
return Messages::Incoming::BadNick.new(string, $1)
|
|
10
|
+
else
|
|
11
|
+
Logger.log('Failed to process RPL_ERRORONEOUSNICK message')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
module Handlers
|
|
3
|
+
class Bounce < Handler
|
|
4
|
+
def initialize(handlers)
|
|
5
|
+
handlers[RPL_BOUNCE] = self
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def process(string)
|
|
9
|
+
if(string =~ /:Try server (\S+), port (.+)$/)
|
|
10
|
+
return Messages::Incoming::Bounce.new(string, $1, $2)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ModSpox
|
|
2
|
+
module Handlers
|
|
3
|
+
class Created < Handler
|
|
4
|
+
def initialize(handlers)
|
|
5
|
+
handlers[RPL_CREATED] = self
|
|
6
|
+
end
|
|
7
|
+
def process(string)
|
|
8
|
+
if(string =~ /#{RPL_CREATED.to_s}.+?:created\s(.+)$/)
|
|
9
|
+
return Messages::Incoming::Created(string, $1)
|
|
10
|
+
else
|
|
11
|
+
Logger.log('Failed to parse RPL_CREATED message')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|