beerbot 0.1.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.
@@ -0,0 +1,234 @@
1
+ # The files in this directory are part of BeerBot, a a ruby irc bot library.
2
+ # Copyright (C) 2014 Daniel Bush
3
+ # This program is distributed under the terms of the GNU
4
+ # General Public License. A copy of the license should be
5
+ # enclosed with this project in the file LICENSE. If not
6
+ # see <http://www.gnu.org/licenses/>.
7
+
8
+ require_relative '../01.protocols/botmsg.rb'
9
+
10
+ module BeerBot
11
+
12
+ BotMsg = BeerBot::Protocol::BotMsg
13
+
14
+ # Represents a bot module and may contain a reference to the loaded
15
+ # ruby module or any errors associated with loading it.
16
+
17
+ class BotModule < Hash
18
+ def initialize name,status:false,mod:nil,modname:nil,errors:[]
19
+ self[:status] = status
20
+ self[:name] = name # The module name.
21
+ self[:mod] = mod # The loaded ruby module.
22
+ self[:modname] = modname
23
+ self[:errors] = errors
24
+ end
25
+ end
26
+
27
+ # Represents a sequence of BotModule instances.
28
+
29
+ class Bot < Array
30
+
31
+ attr_accessor :module_path,:module_names
32
+
33
+ def initialize module_path,module_names
34
+ super()
35
+ @module_path = module_path
36
+ @module_names = module_names
37
+ self.load!
38
+ end
39
+
40
+ def load!
41
+ self.reject!{true} unless self.empty? # ick :)
42
+ Dir.chdir(@module_path) {
43
+ @module_names.each {|name|
44
+ initfile = "#{name}/init.rb"
45
+ modname = "::BeerBot::Modules::#{name}"
46
+ mod = nil
47
+ err = [
48
+ [:nodir,!File.directory?(name)],
49
+ [:badname,name !~ /^[A-Z]/ ],
50
+ [:noinit,!File.exists?(initfile)],
51
+ ].select{|e| e[1]}
52
+ ok = (err.size == 0)
53
+
54
+ if ok then
55
+ puts "loading #{initfile}..."
56
+ load(initfile)
57
+ mod = Object.const_get(modname)
58
+ if mod.respond_to?(:instance) then
59
+ mod = mod.instance
60
+ end
61
+ else
62
+ p [initfile,err]
63
+ raise "Can't load (some) modules."
64
+ end
65
+
66
+ bm = BotModule.new(
67
+ name,status:ok,mod:mod,
68
+ modname:modname,errors:err
69
+ )
70
+ self.push(bm)
71
+ }
72
+ }
73
+ end
74
+
75
+ def has_errors
76
+ if self.find{|bot_module| !bot_module[:status]} then
77
+ true
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ # Return list of valid (loaded) modules.
84
+
85
+ def valid_modules
86
+ self.select{|bot_module| bot_module[:status]}
87
+ end
88
+
89
+ # Call :meth on valid (loaded) modules and maybe accumulate result
90
+ # or return first valid response...
91
+ #
92
+ # Returns array-based botmsg. The array could be empty, which
93
+ # means nothing was returned (or we couldn't interpret the output
94
+ # of the bot modules).
95
+ #
96
+ # We expect each bot_module to return nil or a botmsg (Hash, Array
97
+ # or Proc that returns the first two).
98
+ #
99
+ # At the moment, we use inject and break on first valid
100
+ # response...
101
+
102
+ def run meth,*args,**kargs
103
+ self.valid_modules.inject([]) {|arr,bot_module|
104
+ name,mod = bot_module.values_at(:name,:mod)
105
+ next arr unless mod.respond_to?(meth)
106
+ botmsg = mod.send(meth,*args,**kargs)
107
+ if botmsg then
108
+ #arr << [name,botmsg]
109
+ arr += BotMsg.to_a(botmsg)
110
+ break arr # TODO allow multi-module response?
111
+ else
112
+ arr
113
+ end
114
+ }
115
+ end
116
+
117
+ def cmd msg,from:nil,to:nil,me:false,world:nil
118
+ if @cmd then
119
+ botmsg = @cmd.call(msg,from:from,to:to,world:world,me:me)
120
+ return botmsg
121
+ else
122
+ self.run(:cmd,msg,from:from,to:to,world:world,me:me)
123
+ end
124
+ end
125
+
126
+ def hear msg,from:nil,to:nil,me:false,world:nil
127
+ if @hear then
128
+ botmsg = @hear.call(msg,from:from,to:to,me:me,world:world)
129
+ return botmsg
130
+ else
131
+ self.run(:hear,msg,from:from,to:to,me:me,world:world)
132
+ end
133
+ end
134
+
135
+ def help arr,from:nil,to:nil,world:nil,me:false
136
+ m = []
137
+ modname,*topics = arr
138
+
139
+ # "help"
140
+
141
+ if arr.empty? then
142
+ helplist = self.valid_modules.select {|bot_module|
143
+ bot_module[:mod].respond_to?(:help)
144
+ }.map{|bot_module|
145
+ bot_module[:name]
146
+ }
147
+ reply = [
148
+ {
149
+ to:from,
150
+ msg:"To issue commands to the bot over a channel, you need to start with a command prefix like ','."
151
+ },
152
+ {
153
+ to:from,
154
+ msg:"Modules (type: help <module-name>): "+helplist.join('; ')
155
+ }
156
+ ]
157
+
158
+ # "help modname [subtopic [... [... etc]]]"
159
+
160
+ else
161
+
162
+ bot_module = self.valid_modules.find{|bot_module|
163
+ bot_module[:name]==modname
164
+ }
165
+
166
+ # Can't find module...
167
+ if bot_module.nil? then
168
+ reply = [to:to,msg:"Don't know this topic #{from}"]
169
+
170
+ # Can find module...
171
+ else
172
+
173
+ mod = bot_module[:mod]
174
+ reply = []
175
+
176
+
177
+ # Module has help...
178
+ if mod.respond_to?(:help) then
179
+ arr = mod.help(topics)
180
+
181
+ if !arr || arr.empty? then
182
+ reply += [to:from,msg:"hmmm, the module didn't say anything..."]
183
+ else
184
+ if topics.empty? then
185
+ reply += [to:from,msg:"Note: modules should list topics which you can access like: help #{modname} <topic>"]
186
+ end
187
+ reply += arr.map{|a| {to:from,msg:a}}
188
+ end
189
+
190
+ # Module doesn't have help...
191
+ else
192
+ reply += [to:from,msg:"type: #{modname} doesn't seem to have any help"]
193
+ end
194
+ end
195
+
196
+ end
197
+
198
+ if not me then
199
+ reply += [to:to,msg:"pm'ing you #{from}"]
200
+ end
201
+ return reply
202
+ end
203
+
204
+
205
+ # Override normal cmd processing with Proc.
206
+ #
207
+ # You might want to do this to temporarily stop normal bot command
208
+ # behaviour in order for the bot to go into some sort of exclusive
209
+ # mode.
210
+ #
211
+ # To disable normal response behaviour do:
212
+ # bot.set_cmd {|msg,**kargs| nil }
213
+ # bot.set_hear {|msg,**kargs| nil }
214
+ #
215
+ #
216
+ # To unset,
217
+ # bot.set_cmd
218
+ # bot.set_hear
219
+
220
+ def set_cmd &block
221
+ @cmd = block
222
+ end
223
+
224
+ # Override normal hear-processing with Proc.
225
+ #
226
+ # See set_cmd.
227
+
228
+ def set_hear &block
229
+ @hear = block
230
+ end
231
+
232
+ end
233
+
234
+ end
@@ -0,0 +1,34 @@
1
+ # The files in this directory are part of BeerBot, a a ruby irc bot library.
2
+ # Copyright (C) 2014 Daniel Bush
3
+ # This program is distributed under the terms of the GNU
4
+ # General Public License. A copy of the license should be
5
+ # enclosed with this project in the file LICENSE. If not
6
+ # see <http://www.gnu.org/licenses/>.
7
+
8
+ require_relative '../00.utils/More'
9
+
10
+ module BeerBot
11
+
12
+ # More-based system (see More).
13
+ #
14
+ # #filter expects a botmsg and returns an array botmsg.
15
+
16
+ class BotMsgMore < ::BeerBot::Utils::More
17
+ def filter botmsg
18
+ return nil unless botmsg
19
+ replies = []
20
+ by_to = Hash.new{|h,k| h[k]=[]}
21
+ arr = BeerBot::Protocol::BotMsg.to_a(botmsg)
22
+
23
+ arr.inject(by_to){|h,v| h[v[:to]].push(v); h}
24
+ by_to.each_pair{|to,a|
25
+ replies += super(a,to)
26
+ if replies.size < a.size then
27
+ replies += [msg:"Type: more",to:to]
28
+ end
29
+ }
30
+ return replies
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,160 @@
1
+ # The files in this directory are part of BeerBot, a a ruby irc bot library.
2
+ # Copyright (C) 2013,2014 Daniel Bush
3
+ # This program is distributed under the terms of the GNU
4
+ # General Public License. A copy of the license should be
5
+ # enclosed with this project in the file LICENSE. If not
6
+ # see <http://www.gnu.org/licenses/>.
7
+
8
+ require_relative '../00.utils/utils'
9
+ require_relative '../01.protocols/irc'
10
+ require_relative '../03.more/BotMsgMore'
11
+
12
+ module BeerBot
13
+
14
+ # Dispatchers receive incoming messages from a connection and
15
+ # decide what to do with them.
16
+
17
+ module Dispatchers
18
+
19
+ # This dispatcher calls BeerBot::Protocol::IRC.parse on what it
20
+ # receives and then processes the response.
21
+ #
22
+ # There are several ways to specify how the result of parse is
23
+ # processed. You can:
24
+ # 1) pass in a block at instantiation time
25
+ # 2) set a block using #set_receive
26
+ # 3) subclass this class and write your own #receive
27
+
28
+ class IRCDispatcher
29
+
30
+ IRC = BeerBot::Protocol::IRC
31
+ Utils = BeerBot::Utils
32
+ BotMsgMore = BeerBot::BotMsgMore
33
+
34
+ attr_accessor :bot,:nick,:prefix,:world,:more
35
+
36
+ def initialize bot,nick,prefix:',',world:nil,&block
37
+ @bot = bot
38
+ @more = BotMsgMore.new
39
+
40
+ @nick = nick
41
+ @get_nick_cmd = Utils.make_prefix_parser(nick)
42
+ @nickrx = Regexp.new("^#{nick}$",'i')
43
+
44
+ @prefix = prefix
45
+ @get_prefix_cmd = Utils.make_prefix_parser(prefix)
46
+
47
+ @world = world
48
+
49
+ if block_given? then
50
+ @block = block
51
+ end
52
+ end
53
+
54
+ # Set a receiving proc.
55
+ #
56
+ # If no block given, @block is set to nil and #receive is used.
57
+
58
+ def set_receive &block
59
+ if block_given? then
60
+ @block = block
61
+ else
62
+ @block = nil
63
+ end
64
+ end
65
+
66
+ def parse irc_str
67
+ IRC.parse(irc_str)
68
+ end
69
+
70
+ def receive irc_str
71
+
72
+ event,*args = self.parse(irc_str)
73
+
74
+ if @block then
75
+ return self.instance_exec(event,*args,&@block)
76
+ end
77
+
78
+ # Otherwise, here is the default behaviour...
79
+
80
+ case event
81
+ when :unknown
82
+ #puts "protocol/irc :unknown"
83
+ when :default
84
+ #puts "protocol/irc :default"
85
+ when :nick
86
+ old,nick = args
87
+ @world.nick(old,nick) if @world
88
+ when :part
89
+ nick,channel = args
90
+ @world.part(nick,channel) if @world
91
+ when :join
92
+ nick,channel = args
93
+ @world.join(nick,channel) if @world
94
+ when :chanlist
95
+ #p "[dispatcher] :chanlist"
96
+ channel,users = args
97
+ if @world then
98
+ users.each {|user|
99
+ @world.join(user,channel)
100
+ }
101
+ end
102
+
103
+ when :privmsg
104
+
105
+ from,to,msg = args
106
+
107
+ # Somebody messaging us privately:
108
+ me = (@nickrx === to)
109
+
110
+ # Somebody talking to us on channel: "Beerbot: ..."
111
+ cmd = @get_nick_cmd.call(msg)
112
+ if not cmd then
113
+ # Somebody commanding us on channel: ",command ..."
114
+ cmd = @get_prefix_cmd.call(msg)
115
+ end
116
+
117
+ if cmd then
118
+ case cmd
119
+ # dispatch more-filtering...
120
+ when /^more!*|moar!*$/i
121
+ replies = @more.more(to)
122
+ # dispatch help...
123
+ when /^\s*help(?:\s+(.*))?$/
124
+ if $1.nil? then
125
+ args = []
126
+ else
127
+ args = $1.strip.split(/\s+/)
128
+ end
129
+ replies = @bot.help(args,from:from,to:to,me:me,world:world)
130
+ # dispatch cmd...
131
+ else
132
+ replies = @bot.cmd(cmd,from:from,to:to,me:me,world:world)
133
+ end
134
+ else
135
+ # We're just hearing something on a channel...
136
+ replies = @bot.hear(msg,from:from,to:to,me:me,world:world)
137
+ end
138
+
139
+ else
140
+ puts "protocol/irc unrecognised event: '#{event}'"
141
+ end
142
+
143
+ case replies
144
+ when String # assume irc string
145
+ replies
146
+ when Hash,Array,Proc
147
+ # more-filter the reply...
148
+ replies = @more.filter(replies)
149
+ IRC.to_irc(replies)
150
+ else
151
+ nil
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+
160
+ end
@@ -0,0 +1,22 @@
1
+ # The files in this directory are part of BeerBot, a a ruby irc bot library.
2
+ # Copyright (C) 2014 Daniel Bush
3
+ # This program is distributed under the terms of the GNU
4
+ # General Public License. A copy of the license should be
5
+ # enclosed with this project in the file LICENSE. If not
6
+ # see <http://www.gnu.org/licenses/>.
7
+
8
+ module BeerBot
9
+ module Scheduler
10
+ def self.instance timezone=nil
11
+ @@instance ||= CronR::Cron.new
12
+ if timezone then
13
+ @@instance.time {
14
+ Time.use_zone(timezone) {
15
+ Time.zone.now
16
+ }
17
+ }
18
+ end
19
+ @@instance
20
+ end
21
+ end
22
+ end