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