newton 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/newton/bot.rb ADDED
@@ -0,0 +1,361 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'socket'
3
+ require "thread"
4
+ require "newton/rubyext/module"
5
+ require "newton/rubyext/queue"
6
+ require "newton/rubyext/string"
7
+ require "newton/rubyext/infinity"
8
+
9
+ require "newton/exceptions"
10
+
11
+ require "newton/formatted_logger"
12
+ require "newton/syncable"
13
+ require "newton/message"
14
+ require "newton/message_queue"
15
+ require "newton/irc"
16
+ require "newton/channel"
17
+ require "newton/user"
18
+ require "newton/constants"
19
+ require "newton/callback"
20
+ require "newton/ban"
21
+ require "newton/mask"
22
+ require "newton/isupport"
23
+
24
+ Thread.abort_on_exception = true
25
+ module Newton
26
+ VERSION = '0.0.1'
27
+
28
+ # @member [String] server The address of the server to connnect to
29
+ # @member [Number] port The port of the server to connect to
30
+ # @member [Boolean] ssl Whether to use SSL or not
31
+ # @member [String] nick The nick to use
32
+ # @member [String] realname The realname to use
33
+ # @member [Boolean] verbose If true, every incoming and outgoing message will be logged
34
+ # @member [Float] messages_per_second How many messages the server processes per second
35
+ # @member [Number] server_queue_size The size of the message queue of the server
36
+ # @member [Symbol] strictness The strictness of Newton. Allowed values: :strict and :forgiving
37
+ class Config < Struct.new(:server, :port, :ssl, :password, :nick, :realname, :verbose, :messages_per_second, :server_queue_size, :strictness)
38
+ end
39
+
40
+ class Bot
41
+ # @return [Config]
42
+ attr_accessor :config
43
+ # @return [IRC]
44
+ attr_accessor :irc
45
+
46
+ # The store is used for storing state and information bot-wide,
47
+ # mainly because the use of instance variables in bots is not
48
+ # possible.
49
+ #
50
+ # @example
51
+ # configure do |c|
52
+ # …
53
+ # store[:message_counter] = 0
54
+ # end
55
+ #
56
+ # on :message do
57
+ # store[:message_counter] += 1
58
+ # channel.send "This was message ##{store[:message_counter]}"
59
+ # end
60
+ #
61
+ # @return [Hash]
62
+ attr_reader :store
63
+
64
+ # Helper method for turning a String into a {Channel} object.
65
+ #
66
+ # @param [String] channel a channel name
67
+ # @return [Channel] a {Channel} object
68
+ # @example
69
+ # on :message, /^please join (#.+)$/ do |target|
70
+ # Channel(target).join
71
+ # end
72
+ def Channel(channel)
73
+ return channel if channel.is_a?(Channel)
74
+ Channel.find_ensured(channel, self)
75
+ end
76
+
77
+ # Helper method for turning a String into an {User} object.
78
+ #
79
+ # @param [String] user a user's nickname
80
+ # @return [User] an {User} object
81
+ # @example
82
+ # on :message, /^tell me everything about (.+)$/ do |target|
83
+ # user = User(target)
84
+ # reply "%s is named %s and connects from %s" % [user.nick, user.name, user.host]
85
+ # end
86
+ def User(user)
87
+ return user if user.is_a?(User)
88
+ User.find_ensured(user, self)
89
+ end
90
+
91
+ # @return [void]
92
+ # @see FormattedLogger#debug
93
+ def debug(msg)
94
+ FormattedLogger.debug(msg)
95
+ end
96
+
97
+ # @return [Boolean]
98
+ def strict?
99
+ @config.strictness == :strict
100
+ end
101
+
102
+ # @yield
103
+ def initialize(&b)
104
+ @events = {}
105
+ @config = Config.new("localhost", 6667, false, nil, "newton", "Newton", false, 0.5, 10, :forgiving)
106
+
107
+ @store = {}
108
+ @semaphores = {}
109
+ instance_eval(&b) if block_given?
110
+ end
111
+
112
+ # This method is used to set a bot's options. It indeed does
113
+ # nothing else but yielding {Bot#config}, but it makes for a nice DSL.
114
+ #
115
+ # @yieldparam [Struct] config the bot's config
116
+ # @return [void]
117
+ def configure
118
+ yield @config
119
+ end
120
+
121
+ # Since Newton uses threads, all handlers can be run
122
+ # simultaneously, even the same handler multiple times. This also
123
+ # means, that your code has to be thread-safe. Most of the time,
124
+ # this is not a problem, but if you are accessing stored data, you
125
+ # will most likely have to synchronize access to it. Instead of
126
+ # managing all mutexes yourself, Newton provides a synchronize
127
+ # method, which takes a name and block.
128
+ #
129
+ # @param [String, Symbol] name a name for the synchronize block.
130
+ # Synchronize blocks with the same name share the same mutex,
131
+ # which means that only one of them will be executed at a time.
132
+ # @return [void]
133
+ # @yield
134
+ #
135
+ # @example
136
+ # configure do |c|
137
+ # …
138
+ # store[:i] = 0
139
+ # end
140
+ #
141
+ # on :channel, /^start counting!/ do
142
+ # synchronize(:my_counter) do
143
+ # 10.times do
144
+ # val = store[:i]
145
+ # # at this point, another thread might've incremented :i already.
146
+ # # this thread wouldn't know about it, though.
147
+ # store[:i] = val + 1
148
+ # end
149
+ # end
150
+ # end
151
+ def synchronize(name, &block)
152
+ semaphore = (@semaphores[name] ||= Mutex.new)
153
+ semaphore.synchronize(&block)
154
+ end
155
+
156
+ # Registers a handler.
157
+ #
158
+ # @param [String, Symbol, Integer] event the event to match. Available
159
+ # events are all IRC commands in lowercase as symbols, all numeric
160
+ # replies, and the following:
161
+ # - :channel (a channel message)
162
+ # - :private (a private message)
163
+ # - :message (both channel and private messages)
164
+ # - :error (handling errors, use a numeric error code as `match`)
165
+ # - :ctcp (ctcp requests, use a ctcp command as `match`)
166
+ #
167
+ # @param [Regexp, String, Integer] match every message of the
168
+ # right event will be checked against this argument and the event
169
+ # will only be called if it matches
170
+ #
171
+ # @yieldparam [String] *args each capture group of the regex will
172
+ # be one argument to the block. It is optional to accept them,
173
+ # though
174
+ #
175
+ # @return [void]
176
+ def on(event, *regexps, &block)
177
+ regexps = [//] if regexps.empty?
178
+
179
+ event = event.to_sym
180
+
181
+ regexps.map! do |regexp|
182
+ case regexp
183
+ when String, Integer
184
+ /^#{Regexp.escape(regexp.to_s)}$/
185
+ else
186
+ regexp
187
+ end
188
+ end
189
+ (@events[event] ||= []) << [regexps, block]
190
+ end
191
+
192
+ # Define helper methods in the context of the bot.
193
+ #
194
+ # @yield Expects a block containing method definitions
195
+ # @return [void]
196
+ def helpers(&b)
197
+ Callback.class_eval(&b)
198
+ end
199
+
200
+ # Stop execution of the current {#on} handler.
201
+ #
202
+ # @return [void]
203
+ def halt
204
+ throw :halt
205
+ end
206
+
207
+ # Sends a raw message to the server.
208
+ #
209
+ # @param [String] command The message to send.
210
+ # @return [void]
211
+ # @see IRC#message
212
+ def raw(command)
213
+ @irc.message(command)
214
+ end
215
+
216
+ # Sends a PRIVMSG to a recipient (a channel or user).
217
+ # You should be using {Channel#send} and {User#send} instead.
218
+ #
219
+ # @param [String] recipient the recipient
220
+ # @param [String] text the message to send
221
+ # @return [void]
222
+ # @see Channel#send
223
+ # @see User#send
224
+ def msg(recipient, text)
225
+ text = text.to_s
226
+ text.split("\n").each do |line|
227
+ # 512 is the overall max length. command, recipients and stuff account to it
228
+ line.split("").each_slice(400) do |chars|
229
+ string = chars.join
230
+ raw("PRIVMSG #{recipient} :#{string}")
231
+ end
232
+ end
233
+ end
234
+
235
+ # Invoke an action (/me) in/to a recipient (a channel or user).
236
+ # You should be using {Channel#action} and {User#action} instead.
237
+ #
238
+ # @param [String] recipient the recipient
239
+ # @param [String] text the message to send
240
+ # @return [void]
241
+ # @see Channel#action
242
+ # @see User#action
243
+ def action(recipient, text)
244
+ raw("PRIVMSG #{recipient} :\001ACTION #{text}\001")
245
+ end
246
+
247
+ # Joins a list of channels.
248
+ #
249
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
250
+ # @param [String] key optionally the key of the channel
251
+ # @return [void]
252
+ # @see Channel#join
253
+ def join(channel, key = nil)
254
+ Channel(channel).join(key)
255
+ end
256
+
257
+ # Parts a list of channels.
258
+ #
259
+ # @param [String, Channel] channel either the name of a channel or a {Channel} object
260
+ # @param [String] reason an optional reason/part message
261
+ # @return [void]
262
+ # @see Channel#part
263
+ def part(channel, reason = nil)
264
+ Channel(channel).part(reason)
265
+ end
266
+
267
+ # @return [String]
268
+ attr_accessor :nick
269
+ def nick
270
+ @config.nick
271
+ end
272
+
273
+ # Sets the bot's nick.
274
+ #
275
+ # @param [String] new_nick
276
+ # @raise [Exceptions::NickTooLong]
277
+ def nick=(new_nick)
278
+ if new_nick.size > @irc.isupport["NICKLEN"] && strict?
279
+ raise Exceptions::NickTooLong, new_nick
280
+ end
281
+
282
+ raw "NICK #{new_nick}"
283
+ end
284
+
285
+ # Disconnects from the server.
286
+ #
287
+ # @return [void]
288
+ def quit(message = nil)
289
+ command = message ? "QUIT :#{message}" : "QUIT"
290
+ raw command
291
+ end
292
+
293
+ # Connects the bot to an server.
294
+ #
295
+ # @return [void]
296
+ def start
297
+ FormattedLogger.debug "Connecting to #{@config.server}:#{@config.port}"
298
+ @irc = IRC.new(self, @config)
299
+ @irc.connect
300
+ end
301
+
302
+ # @api private
303
+ # @return [void]
304
+ def dispatch(event, msg = nil)
305
+ if handlers = find(event, msg)
306
+ handlers.each do |handler|
307
+ regexps, block = *handler
308
+
309
+ # calling Message#match multiple times is not a problem
310
+ # because we cache the result
311
+ if msg
312
+ regexp = regexps.find { |rx| msg.match(rx, event) }
313
+ captures = msg.match(regexp, event).captures
314
+ else
315
+ captures = []
316
+ end
317
+
318
+ invoke(block, msg, captures)
319
+ end
320
+ end
321
+ end
322
+
323
+ private
324
+ def find(type, msg = nil)
325
+ if events = @events[type]
326
+ if msg.nil?
327
+ return events
328
+ end
329
+
330
+ events.select { |regexps,_|
331
+ regexps.any? { |regexp|
332
+ msg.match(regexp, type)
333
+ }
334
+ }
335
+ end
336
+ end
337
+
338
+ def invoke(block, msg, match)
339
+ # -1 splat arg, send everything
340
+ # 0 no args, send nothing
341
+ # 1 defined number of args, send only those
342
+ bargs = case block.arity <=> 0
343
+ when -1; match
344
+ when 0; []
345
+ when 1; match[0..block.arity-1]
346
+ end
347
+ Thread.new do
348
+ begin
349
+ catch(:halt) do
350
+ Callback.new(block, msg, self).call(*bargs)
351
+ end
352
+ rescue => e
353
+ FormattedLogger.debug "#{e.backtrace.first}: #{e.message} (#{e.class})"
354
+ e.backtrace[1..-1].each do |line|
355
+ FormattedLogger.debug "\t" + line
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,24 @@
1
+ module Newton
2
+ # @api private
3
+ class Callback
4
+ def initialize(block, msg, bot)
5
+ @block, @msg, @bot, @bargs = block, msg, bot
6
+ end
7
+
8
+ # @return [void]
9
+ def call(*bargs)
10
+ instance_exec(*bargs, &@block)
11
+ end
12
+
13
+ # Forwards method calls to the current message and bot instance.
14
+ def method_missing(m, *args, &blk)
15
+ if @msg.respond_to?(m)
16
+ @msg.__send__(m, *args, &blk)
17
+ elsif @bot.respond_to?(m)
18
+ @bot.__send__(m, *args, &blk)
19
+ else
20
+ super
21
+ end
22
+ end
23
+ end
24
+ end