newton 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +189 -0
- data/Rakefile +6 -0
- data/examples/autovoice.rb +45 -0
- data/examples/echo_bot.rb +22 -0
- data/examples/excess_flood.rb +23 -0
- data/examples/memo.rb +50 -0
- data/examples/schema.rb +41 -0
- data/examples/secure_eval.rb +46 -0
- data/lib/newton/ban.rb +40 -0
- data/lib/newton/bot.rb +361 -0
- data/lib/newton/callback.rb +24 -0
- data/lib/newton/channel.rb +362 -0
- data/lib/newton/constants.rb +123 -0
- data/lib/newton/exceptions.rb +25 -0
- data/lib/newton/formatted_logger.rb +64 -0
- data/lib/newton/irc.rb +261 -0
- data/lib/newton/isupport.rb +96 -0
- data/lib/newton/mask.rb +46 -0
- data/lib/newton/message.rb +162 -0
- data/lib/newton/message_queue.rb +62 -0
- data/lib/newton/rubyext/infinity.rb +1 -0
- data/lib/newton/rubyext/module.rb +18 -0
- data/lib/newton/rubyext/queue.rb +19 -0
- data/lib/newton/rubyext/string.rb +24 -0
- data/lib/newton/syncable.rb +55 -0
- data/lib/newton/user.rb +226 -0
- data/lib/newton.rb +1 -0
- data/test/helper.rb +60 -0
- data/test/test_commands.rb +85 -0
- data/test/test_events.rb +89 -0
- data/test/test_helpers.rb +14 -0
- data/test/test_irc.rb +38 -0
- data/test/test_message.rb +117 -0
- data/test/test_parse.rb +153 -0
- data/test/test_queue.rb +49 -0
- data/test/tests.rb +9 -0
- metadata +100 -0
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
|