vetinari 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0000606629bd97c6e36412292b86220a9279472b
4
+ data.tar.gz: 5cb4790367bb5037fa4aecd97c6058a88ba56f39
5
+ SHA512:
6
+ metadata.gz: a40248f184e7ee83ce0f6b8e12e806d5c71cf9b6f258f53c3dc774070bdf77ce02ced8ea274d241bf8adbab732cb3779b7b480dbdcc2a1080e7c98e219e21d85
7
+ data.tar.gz: 8989e04679431ca77300fb6195901a4c355e1a7047ec314b62cb14f8629afc011fecccb3d185eb0345dac6aaa9dda891129cc9b2bff39aaeb535e666b4ad696d
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Tobias Bühlmann
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Vetinari
2
+ Vetinari is a Domain Specific Language for writing IRC Bots using the [Celluloid::IO](https://github.com/celluloid/celluloid-io "Celluloid::IO") library.
3
+
4
+ ## Requirements
5
+ - Ruby >= 1.9.2
6
+ - Celluloid::IO ~> 0.14
7
+
8
+ ## Wiki
9
+ Detailed information about using Vetinari can be found in the [Project Wiki](https://github.com/tbuehlmann/vetinari/wiki).
10
+
11
+ ## Quick Setup
12
+
13
+ ### Installation
14
+ ```sh
15
+ $ gem install vetinari
16
+ ```
17
+
18
+ ### Usage
19
+ ```ruby
20
+ require 'vetinari'
21
+
22
+ bot = Vetinari::Bot.new do |config|
23
+ config.server = 'chat.freenode.org'
24
+ config.port = 6667
25
+ config.nick = 'Vetinari'
26
+ end
27
+
28
+ bot.on(:connect) do
29
+ bot.join '#vetinari'
30
+ end
31
+
32
+ bot.on(:channel, /foo/) do |env|
33
+ env[:channel].message('bar')
34
+ end
35
+
36
+ bot.connect
37
+ ```
38
+
39
+ ## Contributing
40
+
41
+ 1. Fork it
42
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
43
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
44
+ 4. Push to the branch (`git push origin my-new-feature`)
45
+ 5. Create new Pull Request
46
+
47
+ ## License
48
+ Copyright (c) 2013 Tobias Bühlmann
49
+
50
+ MIT License
51
+
52
+ Permission is hereby granted, free of charge, to any person obtaining
53
+ a copy of this software and associated documentation files (the
54
+ "Software"), to deal in the Software without restriction, including
55
+ without limitation the rights to use, copy, modify, merge, publish,
56
+ distribute, sublicense, and/or sell copies of the Software, and to
57
+ permit persons to whom the Software is furnished to do so, subject to
58
+ the following conditions:
59
+
60
+ The above copyright notice and this permission notice shall be
61
+ included in all copies or substantial portions of the Software.
62
+
63
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
64
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
65
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
66
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
67
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
68
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
69
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,24 @@
1
+ lib_dir = File.join(File.dirname(__FILE__), '..', 'lib')
2
+ $LOAD_PATH.unshift lib_dir
3
+
4
+ require 'vetinari'
5
+
6
+ echo_bot = Vetinari::Bot.new do |config|
7
+ config.server = 'chat.freenode.org'
8
+ config.port = 6667
9
+ config.nick = "Vetinari#{rand(10_000)}"
10
+ end
11
+
12
+ echo_bot.on :connect do
13
+ echo_bot.join '#vetinari'
14
+ end
15
+
16
+ echo_bot.on :channel, /\Aquit!\z/ do |env|
17
+ echo_bot.quit
18
+ end
19
+
20
+ echo_bot.on :channel, //, 1 do |env|
21
+ env[:channel].message(env[:message])
22
+ end
23
+
24
+ echo_bot.connect
@@ -0,0 +1,358 @@
1
+ module Vetinari
2
+ class Bot
3
+ include Celluloid::IO, IRC
4
+
5
+ attr_reader :config, :users, :user, :channels, :server_manager, :callbacks
6
+
7
+ def initialize(&block)
8
+ @actor = Actor.current
9
+ @config = Configuration.new(&block)
10
+ @callbacks = CallbackContainer.new(Actor.current)
11
+ @users = UserContainer.new
12
+ @channels = ChannelContainer.new
13
+ @socket = nil
14
+ @connected = false
15
+ @user = nil
16
+
17
+ setup_channel_and_user_tracking
18
+ setup_default_callbacks
19
+ setup_dcc
20
+ end
21
+
22
+ def on(event, pattern = //, worker = 0, &block)
23
+ @callbacks.add(event, pattern, worker, block)
24
+ end
25
+
26
+ exclusive :on
27
+ execute_block_on_receiver :on
28
+
29
+ def connect
30
+ @config.loggers.info '-- Starting Vetinari'
31
+ @socket = TCPSocket.open(@config.server, @config.port)
32
+ # port, ip = Socket.unpack_sockaddr_in(@socket.to_io.getpeername)
33
+ # @config.internal_port = port
34
+ # @config.internal_ip = ip
35
+ register
36
+
37
+ while message = @socket.gets do
38
+ parse message
39
+ end
40
+
41
+ disconnected
42
+ end
43
+
44
+ def raw(message, logging = true)
45
+ if @socket
46
+ @socket.puts("#{message}\r\n")
47
+ @config.loggers.info ">> #{message}" if logging
48
+ message
49
+ end
50
+ end
51
+
52
+ def connected?
53
+ @connected ? true : false
54
+ end
55
+
56
+ def inspect
57
+ nick = @user.nick rescue @config.nick
58
+ "#<Bot nick=#{nick}>"
59
+ end
60
+
61
+ def parse(message)
62
+ message.chomp!
63
+
64
+ if message =~ /^PING \S+$/
65
+ if @config.hide_ping_pongs
66
+ raw message.sub(/PING/, 'PONG'), false
67
+ else
68
+ @config.loggers.info "<< #{message}"
69
+ raw message.sub(/PING/, 'PONG')
70
+ end
71
+ else
72
+ @config.loggers.info "<< #{message}"
73
+ env = MessageParser.parse(message, @config.isupport['CHANTYPES'])
74
+ @callbacks.call(env)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def disconnected
81
+ @connected = false
82
+ @config.loggers.info '-- Vetinari disconnected'
83
+
84
+ unless @quitted
85
+ if @config.reconnect
86
+ @quitted = nil
87
+ puts "-- Reconnecting in #{@config.reconnect_interval} seconds."
88
+ after(@config.reconnect_interval) { connect }
89
+ end
90
+ end
91
+ end
92
+
93
+ def setup_dcc
94
+ if @config.dcc.external_ip && @config.dcc.ports.any?
95
+ @server_manager = Dcc::ServerManager.new(Actor.current)
96
+ end
97
+ end
98
+
99
+ def setup_default_callbacks
100
+ [376, 422].each do |raw_numeric|
101
+ on raw_numeric do |env|
102
+ unless @connected
103
+ @connected = true
104
+ @callbacks.call({:type => :connect})
105
+ end
106
+ end
107
+ end
108
+
109
+ on 005 do |env|
110
+ @config.isupport.parse(env[:params])
111
+ end
112
+
113
+ # User ping request.
114
+ on :query, /^\001PING \d+\001$/ do |env|
115
+ time = env[:message].scan(/\d+/)[0]
116
+ env[:user].notice("\001PING #{time}\001")
117
+ end
118
+
119
+ on :query, /^\001VERSION\001$/ do |env|
120
+ env[:user].notice("\001VERSION Vetinari #{Vetinari::VERSION} (https://github.com/tbuehlmann/vetinari)")
121
+ end
122
+
123
+ on :query, /^\001TIME\001$/ do |env|
124
+ env[:user].notice("\001TIME #{Time.now.strftime('%a %b %d %H:%M:%S %Y')}\001")
125
+ end
126
+ end
127
+
128
+ def setup_channel_and_user_tracking
129
+ # Add the bot user to the user container when connected.
130
+ on 001 do |env|
131
+ nick = env[:params].split(/ /).first
132
+ @user = User.new(nick, @actor) # TODO
133
+ @users.add(@user)
134
+ end
135
+
136
+ on :join do |env|
137
+ joined_user = {
138
+ :nick => env.delete(:nick),
139
+ :user => env.delete(:user),
140
+ :host => env.delete(:host)
141
+ }
142
+ channel = env.delete(:channel)
143
+
144
+ # TODO: Update existing users with user/host information.
145
+
146
+ user = @users[joined_user[:nick]]
147
+
148
+ if user
149
+ if user.bot?
150
+ channel = Channel.new(channel, @actor)
151
+ channel.get_mode
152
+ @channels.add(channel)
153
+ else
154
+ channel = @channels[channel]
155
+ end
156
+ else
157
+ channel = @channels[channel]
158
+ user = User.new(joined_user[:nick], self)
159
+ @users.add(user)
160
+ end
161
+
162
+ channel.add_user(user, [])
163
+ env[:channel] = channel
164
+ env[:user] = user
165
+ end
166
+
167
+ on 353 do |env|
168
+ channel_name = env[:params].split(/ /)[2]
169
+ channel = @channels[channel_name]
170
+ nicks_with_prefixes = env[:params].scan(/:(.*)/)[0][0].split(/ /)
171
+ nicks, prefixes = [], []
172
+ channel_prefixes = @config.isupport['PREFIX'].values.map do |p|
173
+ Regexp.escape(p)
174
+ end.join('|')
175
+
176
+ nicks_with_prefixes.each do |nick_with_prefixes|
177
+ nick = nick_with_prefixes.gsub(/#{channel_prefixes}/, '')
178
+ prefixes = nick_with_prefixes.scan(/#{channel_prefixes}/)
179
+
180
+ unless user = @users[nick]
181
+ user = User.new(nick, @actor)
182
+ @users.add(user)
183
+ end
184
+
185
+ channel.add_user(user, prefixes)
186
+ end
187
+ end
188
+
189
+ on :part do |env|
190
+ nick = env.delete(:nick)
191
+ user = env.delete(:user)
192
+ host = env.delete(:host)
193
+
194
+ # TODO: Update existing users with user/host information.
195
+
196
+ user = @users[nick]
197
+ channel = @channels[env.delete(:channel)]
198
+
199
+ if user.bot?
200
+ # Remove the channel from the channel_list.
201
+ @channels.remove(channel)
202
+
203
+ # Remove all users from the user_list that do not share channels
204
+ # with the Thaum.
205
+ all_known_users = @channels.channels.flat_map(&:users)
206
+ @users.kill_zombie_users(all_known_users)
207
+ else
208
+ channel.remove_user(user)
209
+ remove_user = @channels.channels.none? do |_channel|
210
+ _channel.has_user?(user)
211
+ end
212
+
213
+ @users.remove(user) if remove_user
214
+ end
215
+
216
+ env[:channel] = channel
217
+ env[:user] = user
218
+ end
219
+
220
+ on :kick do |env|
221
+ nick = env.delete(:nick)
222
+ user = env.delete(:user)
223
+ host = env.delete(:host)
224
+
225
+ # TODO: Update existing users with user/host information.
226
+
227
+ channel = @channels[env.delete(:channel)]
228
+ kicker = @users[nick]
229
+ kickee = @users[env.delete(:kickee)]
230
+
231
+ channel.remove_user(kickee)
232
+
233
+ if kickee.bot?
234
+ # Remove the channel from the channel_list.
235
+ @channels.remove(channel)
236
+
237
+ # Remove all users from the user_list that do not share channels
238
+ # with the Thaum.
239
+ all_known_users = @channels.channels.map(&:users).flatten
240
+ @users.kill_zombie_users(all_known_users)
241
+ else
242
+ remove_user = @channels.channels.none? do |_channel|
243
+ _channel.has_user?(kickee)
244
+ end
245
+
246
+ @users.remove(kickee) if remove_user
247
+ end
248
+
249
+ env[:kicker] = kicker
250
+ env[:kickee] = kickee
251
+ env[:channel] = channel
252
+ end
253
+
254
+ # If @config.rejoin_after_kick is set to `true`, let
255
+ # the Thaum rejoin a channel after being kicked.
256
+ on :kick do |env|
257
+ if @config.rejoin_after_kick && env[:kickee].bot?
258
+ key = env[:channel].modes['k']
259
+ env[:channel].join(key)
260
+ end
261
+ end
262
+
263
+ on :quit do |env|
264
+ nick = env.delete(:nick)
265
+ user = env.delete(:user)
266
+ host = env.delete(:host)
267
+
268
+ # TODO: Update existing users with user/host information.
269
+
270
+ user = @users[nick]
271
+
272
+ if user.bot?
273
+ channels = @channels.clear
274
+ @users.clear
275
+ else
276
+ channels = @channels.remove_user(user)
277
+ @users.remove(user)
278
+ end
279
+
280
+ env[:user] = user
281
+ env[:channels] = channels
282
+ end
283
+
284
+ on :disconnect do
285
+ @channels.clear
286
+ @users.clear
287
+ end
288
+
289
+ on :channel do |env|
290
+ nick = env[:nick]
291
+ user = env[:user]
292
+ host = env[:host]
293
+
294
+ # TODO: Update existing users with user/host information.
295
+
296
+ env[:channel] = @channels[env[:channel]]
297
+ env[:user] = @users[nick]
298
+ end
299
+
300
+ on :query do |env|
301
+ nick = env[:nick]
302
+ user = env[:user]
303
+ host = env[:host]
304
+ # TODO: Update existing users with user/host information.
305
+
306
+ env[:user] = @users[nick] || User.new(nick, @actor)
307
+ end
308
+
309
+ on :query, /\A\001DCC SEND \"?\S+\"? \d+ \d+ \d+\001\z/ do |env|
310
+ results = env[:message].scan(/\A\001DCC SEND \"?(\S+)\"? (\d+) (\d+) (\d+)\001\z/)
311
+ filename, ip, port, filesize = results.first
312
+ filename = filename.delete("/\\")
313
+ ip = IPAddr.new(ip.to_i, Socket::AF_INET)
314
+ port = Integer(port)
315
+ filesize = Integer(filesize)
316
+ file = Dcc::Incoming::File.new(env[:user], filename, ip, port, filesize, @actor)
317
+ @callbacks.call(env.merge(:type => :dcc, :file => file))
318
+ end
319
+
320
+ on :channel_mode do |env|
321
+ # TODO: Update existing users with user/host information.
322
+ # nick = env[:nick]
323
+ # user = env[:user]
324
+ # host = env[:host]
325
+
326
+ nick = env.delete(:nick)
327
+ params = env.delete(:params)
328
+ modes = env.delete(:modes)
329
+
330
+ channel = @channels[env.delete(:channel)]
331
+ env[:channel] = channel
332
+ env[:user] = @users[nick]
333
+ env[:channel_modes] = ModeParser.parse(modes, params, @config.isupport)
334
+
335
+ env[:channel_modes].each do |mode|
336
+ channel.set_mode(mode, @config.isupport)
337
+ end
338
+ end
339
+
340
+ # Response to MODE command, giving back the channel modes.
341
+ on 324 do |env|
342
+ split = env[:params].split(/ /)
343
+ channel_name = split[1]
344
+ channel = @channels[channel_name]
345
+
346
+ if channel
347
+ modes = split[2]
348
+ params = split[3..-1]
349
+ mode_changes = ModeParser.parse(modes, params, @config.isupport)
350
+
351
+ mode_changes.each do |mode_change|
352
+ channel.set_mode(mode_change, @config.isupport)
353
+ end
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,48 @@
1
+ module Vetinari
2
+ class Callback
3
+ include Celluloid
4
+
5
+ attr_reader :event
6
+ attr_writer :container
7
+
8
+ def initialize(event, pattern, proc, container, uuid)
9
+ @event = event
10
+ @pattern = pattern
11
+ @proc = proc
12
+ @container = container
13
+ @uuid = uuid
14
+ end
15
+
16
+ def call(env)
17
+ begin
18
+ @proc.call(env) if matching?(env)
19
+ rescue => e
20
+ loggers = @container.bot.config.loggers
21
+ loggers.error "-- #{e.class}: #{e.message}"
22
+ e.backtrace.each { |line| loggers.error("-- #{line}") }
23
+ end
24
+ end
25
+
26
+ def remove
27
+ @container.remove(@event, @uuid)
28
+ end
29
+
30
+ def inspect
31
+ event = @event.inspect
32
+ pattern = @pattern.inspect
33
+ uuid = @uuid.inspect
34
+ "#<Callback event=#{event} pattern=#{pattern} uuid=#{uuid}>"
35
+ end
36
+
37
+ private
38
+
39
+ def matching?(env)
40
+ case @event
41
+ when :channel, :query
42
+ env[:message] =~ @pattern
43
+ else
44
+ true
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,85 @@
1
+ module Vetinari
2
+ class CallbackContainer
3
+ attr_reader :bot
4
+
5
+ def initialize(bot)
6
+ @bot = bot
7
+ @callbacks = Hash.new { |hash, key| hash[key] = {} }
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def add(event, pattern, worker, proc)
12
+ uuid = Celluloid.uuid
13
+ args = [event, pattern, proc, self, uuid]
14
+ worker = Integer(worker)
15
+
16
+ case
17
+ when worker == 1
18
+ callback = Callback.new(*args)
19
+ synchronicity = :async
20
+ when worker > 1
21
+ callback = Callback.pool(:size => worker, :args => args)
22
+ synchronicity = :async
23
+ else
24
+ callback = Callback.new(*args)
25
+ synchronicity = :sync
26
+ end
27
+
28
+ @mutex.synchronize do
29
+ @callbacks[callback.event][uuid] = {
30
+ :callback => callback,
31
+ :synchronicity => synchronicity
32
+ }
33
+ end
34
+
35
+ callback
36
+ end
37
+
38
+ def remove(event, uuid)
39
+ @mutex.synchronize do
40
+ if @callbacks.key?(event)
41
+ hash = @callbacks[event].delete(uuid)
42
+
43
+ if hash
44
+ # https://github.com/celluloid/celluloid/issues/197
45
+ # callback.soft_terminate
46
+
47
+ # #terminate is broken for Pools:
48
+ # https://github.com/celluloid/celluloid/pull/207
49
+ #
50
+ # So, don't terminate for now.
51
+ # hash[:callback].terminate
52
+ return true
53
+ end
54
+ end
55
+ end
56
+
57
+ false
58
+ end
59
+
60
+ def call(env)
61
+ callbacks = nil
62
+
63
+ @mutex.synchronize do
64
+ if @callbacks.key?(env[:type])
65
+ callbacks = @callbacks[env[:type]].values.dup
66
+ end
67
+ end
68
+
69
+ if callbacks
70
+ callbacks.each do |hash|
71
+ case hash[:synchronicity]
72
+ when :sync
73
+ hash[:callback].call(env)
74
+ when :async
75
+ hash[:callback].async.call(env)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ def inspect
82
+ "#<CallbackContainer bot=#{@bot.inspect}>"
83
+ end
84
+ end
85
+ end