newton 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/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