tkellem 0.8.11 → 0.9.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,20 +1,24 @@
1
1
  module Tkellem
2
2
 
3
- class Network < ActiveRecord::Base
4
- has_many :hosts, :dependent => :destroy
5
- accepts_nested_attributes_for :hosts
3
+ class Network < Sequel::Model
4
+ plugin :nested_attributes
5
+ plugin :validation_class_methods
6
+ plugin :serialization
6
7
 
7
- has_many :network_users, :dependent => :destroy
8
+ one_to_many :hosts, :dependent => :destroy
9
+ nested_attributes :hosts
10
+
11
+ one_to_many :network_users, :dependent => :destroy
8
12
  # networks either belong to a specific user, or they are public and any user
9
13
  # can join them.
10
- belongs_to :user
14
+ many_to_one :user
11
15
 
12
16
  validates_uniqueness_of :name, :scope => :user_id
13
17
 
14
- serialize :at_connect, Array
18
+ serialize_attributes :yaml, :at_connect
15
19
 
16
20
  def at_connect
17
- read_attribute(:at_connect) || []
21
+ super || []
18
22
  end
19
23
 
20
24
  def public?
@@ -1,17 +1,33 @@
1
1
  module Tkellem
2
2
 
3
- class NetworkUser < ActiveRecord::Base
4
- belongs_to :network
5
- belongs_to :user
3
+ class NetworkUser < Sequel::Model
4
+ plugin :serialization
6
5
 
7
- serialize :at_connect, Array
6
+ many_to_one :network
7
+ many_to_one :user
8
+
9
+ serialize_attributes :yaml, :at_connect
10
+
11
+ def at_connect
12
+ super || []
13
+ end
8
14
 
9
15
  def nick
10
- read_attribute(:nick) || user.name
16
+ super || user.name
11
17
  end
12
18
 
13
19
  def combined_at_connect
14
- network.at_connect + (at_connect || [])
20
+ network.at_connect + at_connect
21
+ end
22
+
23
+ def after_create
24
+ super
25
+ $tkellem_server.try(:after_create, self)
26
+ end
27
+
28
+ def after_destroy
29
+ super
30
+ $tkellem_server.try(:after_destroy, self)
15
31
  end
16
32
  end
17
33
 
@@ -1,13 +1,13 @@
1
1
  module Tkellem
2
2
 
3
- class Setting < ActiveRecord::Base
3
+ class Setting < Sequel::Model
4
4
  def self.get(setting_name)
5
- setting = first(:conditions => { :name => setting_name })
5
+ setting = where(:name => setting_name).first
6
6
  setting.try(:value)
7
7
  end
8
8
 
9
9
  def self.set(setting_name, new_value)
10
- setting = first(:conditions => { :name => setting_name })
10
+ setting = where(:name => setting_name).first
11
11
  setting.try(:update_attributes, :value => new_value.to_s, :unchanged => false)
12
12
  setting
13
13
  end
@@ -1,13 +1,20 @@
1
1
  module Tkellem
2
2
 
3
- class User < ActiveRecord::Base
4
- has_many :network_users, :dependent => :destroy
5
- has_many :networks, :dependent => :destroy
3
+ class User < Sequel::Model
4
+ plugin :validation_class_methods
5
+
6
+ one_to_many :network_users, :dependent => :destroy
7
+ one_to_many :networks, :dependent => :destroy
6
8
 
7
9
  validates_presence_of :username
8
10
  validates_uniqueness_of :username
9
11
  validates_presence_of :role, :in => %w(user admin)
10
12
 
13
+ def before_validation
14
+ self.role ||= 'user'
15
+ super
16
+ end
17
+
11
18
  # pluggable authentication -- add your own block, which takes |username, password|
12
19
  # parameters. Return a User object if authentication succeeded, or a
13
20
  # false/nil value if auth failed. You can create the user on-the-fly if
@@ -18,7 +25,7 @@ class User < ActiveRecord::Base
18
25
  # default database-based authentication
19
26
  # TODO: proper password hashing
20
27
  self.authentication_methods << proc do |username, password|
21
- user = find_by_username(username)
28
+ user = first(:username => username)
22
29
  user && user.valid_password?(password) && user
23
30
  end
24
31
 
@@ -31,7 +38,7 @@ class User < ActiveRecord::Base
31
38
  end
32
39
 
33
40
  def username=(val)
34
- write_attribute(:username, val.try(:downcase))
41
+ super(val.try(:downcase))
35
42
  end
36
43
 
37
44
  def name
@@ -44,7 +51,7 @@ class User < ActiveRecord::Base
44
51
  end
45
52
 
46
53
  def password=(password)
47
- write_attribute(:password, password ? OpenSSL::Digest::SHA1.hexdigest(password) : nil)
54
+ super(password ? OpenSSL::Digest::SHA1.hexdigest(password) : nil)
48
55
  end
49
56
 
50
57
  def admin?
@@ -15,9 +15,11 @@ module Tkellem
15
15
  # different backlog implementation. Right now, it's always loaded though.
16
16
  class Backlog
17
17
  include Tkellem::EasyLogger
18
+ include Celluloid
19
+
20
+ cattr_accessor :replay_pool
18
21
 
19
22
  Bouncer.add_plugin(self)
20
- cattr_accessor :instances
21
23
 
22
24
  def self.get_instance(bouncer)
23
25
  bouncer.data(self)[:instance] ||= self.new(bouncer)
@@ -57,7 +59,7 @@ class Backlog
57
59
  # open stream in append-only mode
58
60
  return @streams[ctx] if @streams[ctx]
59
61
  stream = @streams[ctx] = File.open(stream_filename(ctx), 'ab')
60
- stream.seek(0, IO::SEEK_END)
62
+ stream.seek(0, ::IO::SEEK_END)
61
63
  @starting_pos[ctx] = stream.pos
62
64
  stream
63
65
  end
@@ -98,7 +100,7 @@ class Backlog
98
100
  ctx = msg.prefix.split(/[!~@]/, 2).first
99
101
  end
100
102
  stream = get_stream(ctx)
101
- stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}"))
103
+ stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S") + " < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}")
102
104
  update_pos(ctx, stream.pos)
103
105
  end
104
106
  end
@@ -109,39 +111,53 @@ class Backlog
109
111
  return if msg.ctcp? && !msg.action?
110
112
  ctx = msg.args.first
111
113
  stream = get_stream(ctx)
112
- stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S > #{'* ' if msg.action?}#{msg.args.last}"))
114
+ stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S") + " > #{'* ' if msg.action?}#{msg.args.last}")
113
115
  update_pos(ctx, stream.pos)
114
116
  end
115
117
  end
116
118
 
117
119
  def send_backlog(conn, device)
118
120
  device.each do |ctx_name, pos|
119
- stream = File.open(stream_filename(ctx_name), 'rb')
120
- stream.seek(pos)
121
-
122
- while line = stream.gets
123
- timestamp, msg = parse_line(line, ctx_name)
124
- next unless msg
125
- if msg.prefix
126
- # to user
121
+ filename = stream_filename(ctx_name)
122
+ Backlog.replay_pool.async(:replay, filename, pos, @bouncer, conn, ctx_name)
123
+ device[ctx_name] = get_stream(ctx_name).pos
124
+ end
125
+ end
126
+ end
127
+
128
+ class BacklogReplay
129
+ include Celluloid
130
+
131
+ def replay(filename, pos, bouncer, conn, ctx_name)
132
+ stream = File.open(filename, 'rb')
133
+ stream.seek(pos)
134
+
135
+ while line = stream.gets
136
+ timestamp, msg = parse_line(line, ctx_name)
137
+ next unless msg
138
+ privmsg = msg.args.first[0] != '#'[0]
139
+ if msg.prefix
140
+ # to this user
141
+ if privmsg
142
+ msg.args[0] = bouncer.nick
127
143
  else
128
- # from user, add prefix
129
- if msg.args.first[0] == '#'[0]
130
- # it's a room, we can just replay
131
- msg.prefix = @bouncer.nick
132
- else
133
- # a one-on-one chat -- every client i've seen doesn't know how to
134
- # display messages from themselves here, so we fake it by just
135
- # adding an arrow and pretending the other user said it. shame.
136
- msg.prefix = msg.args.first
137
- msg.args[0] = @bouncer.nick
138
- msg.args[-1] = "-> #{msg.args.last}"
139
- end
144
+ # do nothing, it's good to send
145
+ end
146
+ else
147
+ # from this user, maybe add prefix
148
+ if privmsg
149
+ # a one-on-one chat -- every client i've seen doesn't know how to
150
+ # display messages from themselves here, so we fake it by just
151
+ # adding an arrow and pretending the other user said it. shame.
152
+ msg.prefix = msg.args.first
153
+ msg.args[0] = bouncer.nick
154
+ msg.args[-1] = "-> #{msg.args.last}"
155
+ else
156
+ # it's a room, we can just replay
157
+ msg.prefix = bouncer.nick
140
158
  end
141
- conn.send_msg(msg.with_timestamp(timestamp))
142
159
  end
143
-
144
- device[ctx_name] = get_stream(ctx_name).pos
160
+ conn.send_msg(msg.with_timestamp(timestamp))
145
161
  end
146
162
  end
147
163
 
@@ -166,4 +182,6 @@ class Backlog
166
182
  end
167
183
  end
168
184
 
185
+ Backlog.replay_pool = BacklogReplay.pool(size: Celluloid.cores * 2)
186
+
169
187
  end
@@ -1,26 +1,30 @@
1
- require 'eventmachine'
1
+ require 'celluloid/io'
2
2
 
3
3
  require 'tkellem/tkellem_bot'
4
4
 
5
5
  module Tkellem
6
6
 
7
7
  # listens on the unix domain socket and executes admin commands
8
- module SocketServer
9
- include EM::Protocols::LineText2
8
+ # TODO: rename this class
9
+ class SocketServer
10
+ include Celluloid::IO
10
11
  include Tkellem::EasyLogger
12
+ include Tkellem::CelluloidTools::LineReader
11
13
 
12
14
  def log_name
13
15
  "admin"
14
16
  end
15
17
 
16
- def post_init
17
- set_delimiter "\n"
18
+ def initialize(socket)
19
+ @socket = socket
20
+ @delimiter = "\n"
21
+ run!
18
22
  end
19
23
 
20
24
  def receive_line(line)
21
25
  trace "admin socket: #{line}"
22
- TkellemBot.run_command(line, nil, nil) do |line|
23
- send_data("#{line}\n")
26
+ TkellemBot.run_command(line, nil, nil) do |outline|
27
+ send_data("#{outline}\n")
24
28
  end
25
29
  send_data("\0\n")
26
30
  rescue => e
@@ -28,6 +32,10 @@ module SocketServer
28
32
  e.backtrace.each { |l| send_data("#{l}\n") }
29
33
  send_data("\0\n")
30
34
  end
35
+
36
+ def send_data(dat)
37
+ @socket.write(dat)
38
+ end
31
39
  end
32
40
 
33
41
  end
@@ -156,7 +156,7 @@ class TkellemBot
156
156
  end
157
157
 
158
158
  def modify
159
- instance = model.first(:conditions => find_attributes)
159
+ instance = model.first(find_attributes)
160
160
  new_record = false
161
161
  if instance
162
162
  instance.attributes = attributes
@@ -181,7 +181,7 @@ class TkellemBot
181
181
  end
182
182
 
183
183
  def remove
184
- instance = model.first(:conditions => find_attributes)
184
+ instance = model.first(find_attributes)
185
185
  if instance
186
186
  instance.destroy
187
187
  respond "Removed #{show(instance)}"
@@ -258,7 +258,7 @@ class TkellemBot
258
258
 
259
259
  if opts['username']
260
260
  if Command.admin_user?(user)
261
- user = User.first(:conditions => { :username => opts['username'] })
261
+ user = User.where(:username => opts['username']).first
262
262
  else
263
263
  raise Command::ArgumentError, "Only admins can change other passwords"
264
264
  end
@@ -303,7 +303,7 @@ class TkellemBot
303
303
 
304
304
  def execute
305
305
  if opts['network'].present? # only settable by admins
306
- target = Network.first(:conditions => ["name = ? AND user_id IS NULL", opts['network'].downcase])
306
+ target = Network.where(:user_id => nil, :name => opts['network'].downcase).first
307
307
  else
308
308
  target = network_user
309
309
  end
@@ -339,7 +339,7 @@ class TkellemBot
339
339
  admin_option('public', '--public', "Create new public network. Once created, public/private status can't be modified.")
340
340
 
341
341
  def list
342
- public_networks = Network.all(:conditions => 'user_id IS NULL')
342
+ public_networks = Network.where(:user_id => nil).all
343
343
  user_networks = user.try(:reload).try(:networks) || []
344
344
  if user_networks.present? && public_networks.present?
345
345
  r "Public networks are prefixed with [P], user-specific networks with [U]."
@@ -355,15 +355,14 @@ class TkellemBot
355
355
  end
356
356
 
357
357
  def execute
358
- # TODO: this got gross
359
358
  if args.empty? && !opts['remove']
360
359
  list
361
360
  return
362
361
  end
363
362
 
364
363
  if opts['network'].present?
365
- target = Network.first(:conditions => ["name = ? AND user_id = ?", opts['network'].downcase, user.try(:id)])
366
- target ||= Network.first(:conditions => ["name = ? AND user_id IS NULL", opts['network'].downcase]) if self.class.admin_user?(user)
364
+ target = Network.where(:name => opts['network'].downcase, :user_id => user.try(:id)).first
365
+ target ||= Network.where(:user_id => nil, :name => opts['network'].downcase).first if self.class.admin_user?(user)
367
366
  else
368
367
  target = network_user.try(:network)
369
368
  if target && target.public? && !self.class.admin_user?(user)
@@ -379,7 +378,7 @@ class TkellemBot
379
378
  raise(Command::ArgumentError, "No network found") unless target
380
379
  raise(Command::ArgumentError, "You must explicitly specify the network to remove") unless opts['network']
381
380
  if uri
382
- target.hosts.first(:conditions => addr_args).try(:destroy)
381
+ target.hosts.where(addr_args).first.try(:destroy)
383
382
  respond " #{show(target)}"
384
383
  else
385
384
  target.destroy
@@ -397,16 +396,9 @@ class TkellemBot
397
396
  end
398
397
  end
399
398
 
400
- target.attributes = { :hosts_attributes => [addr_args] }
401
- target.save
402
- if target.errors.any?
403
- respond "Error:"
404
- target.errors.full_messages.each { |m| respond " #{m}" }
405
- respond " #{show(target)}"
406
- else
407
- respond("updated:")
408
- respond " #{show(target)}"
409
- end
399
+ Host.create(addr_args.merge(network: target))
400
+ respond("updated:")
401
+ respond " #{show(target)}"
410
402
  end
411
403
  end
412
404
  end
@@ -1,46 +1,62 @@
1
- require 'eventmachine'
2
- require 'active_record'
1
+ require 'active_support/core_ext'
2
+ require 'celluloid'
3
+ require 'sequel'
3
4
 
4
- require 'tkellem/bouncer_connection'
5
5
  require 'tkellem/bouncer'
6
-
7
- require 'tkellem/models/host'
8
- require 'tkellem/models/listen_address'
9
- require 'tkellem/models/network'
10
- require 'tkellem/models/network_user'
11
- require 'tkellem/models/setting'
12
- require 'tkellem/models/user'
6
+ require 'tkellem/bouncer_connection'
7
+ require 'tkellem/celluloid_tools'
13
8
 
14
9
  require 'tkellem/plugins/backlog'
15
- require 'tkellem/plugins/push_service'
10
+ #require 'tkellem/plugins/push_service'
16
11
 
17
12
  module Tkellem
18
13
 
19
14
  class TkellemServer
15
+ include Celluloid
20
16
  include Tkellem::EasyLogger
21
17
 
22
- attr_reader :bouncers
18
+ attr_reader :bouncers, :options
19
+
20
+ def self.initialize_database(path)
21
+ Sequel.extension :migration
22
+ db = Sequel.connect({
23
+ :adapter => 'sqlite',
24
+ :database => path,
25
+ })
26
+ migrations_path = File.expand_path("../migrations", __FILE__)
27
+ Sequel::Migrator.apply(db, migrations_path)
28
+
29
+ Sequel::Model.raise_on_save_failure = true
30
+ # Can't load the models until we've connected to the database and migrated
31
+ require 'tkellem/models/host'
32
+ require 'tkellem/models/listen_address'
33
+ require 'tkellem/models/network'
34
+ require 'tkellem/models/network_user'
35
+ require 'tkellem/models/setting'
36
+ require 'tkellem/models/user'
37
+
38
+ db
39
+ end
23
40
 
24
- def initialize
41
+ def initialize(options)
42
+ Celluloid.logger = Tkellem::EasyLogger.logger
43
+ @options = options
25
44
  @listeners = {}
26
- @bouncers = {}
45
+ @bouncers = CelluloidTools::BackoffSupervisor.new_link({})
27
46
  $tkellem_server = self
28
47
 
29
- unless ActiveRecord::Base.connected?
30
- ActiveRecord::Base.establish_connection({
31
- :adapter => 'sqlite3',
32
- :database => File.expand_path("~/.tkellem/tkellem.sqlite3"),
33
- })
34
- ActiveRecord::Migrator.migrate(File.expand_path("../migrations", __FILE__), nil)
35
- end
36
-
37
- ListenAddress.all.each { |a| listen(a) }
38
- NetworkUser.find_each { |nu| add_bouncer(Bouncer.new(nu)) }
39
- Observer.forward_to << self
48
+ @db = self.class.initialize_database(db_file)
40
49
  end
41
50
 
42
- def stop
43
- Observer.forward_to.delete(self)
51
+ def run
52
+ start_unix_server
53
+ ListenAddress.all { |a| listen(a) }
54
+ NetworkUser.all { |nu| add_bouncer(nu) }
55
+
56
+ begin
57
+ loop { sleep 5 }
58
+ rescue Interrupt
59
+ end
44
60
  end
45
61
 
46
62
  # callbacks for AR observer events
@@ -49,7 +65,7 @@ class TkellemServer
49
65
  when ListenAddress
50
66
  listen(obj)
51
67
  when NetworkUser
52
- add_bouncer(Bouncer.new(obj))
68
+ add_bouncer(obj)
53
69
  end
54
70
  end
55
71
 
@@ -57,68 +73,75 @@ class TkellemServer
57
73
  case obj
58
74
  when ListenAddress
59
75
  stop_listening(obj)
60
- # TODO: remove bouncer on NetworkUser.destroy
61
76
  end
62
77
  end
63
78
 
64
- def listen(listen_address)
65
- address = listen_address.address
66
- port = listen_address.port
67
- ssl = listen_address.ssl
79
+ def start_unix_server
80
+ # This file relies on the models being loaded
81
+ # TODO: this is gross
82
+ require 'tkellem/socket_server'
83
+ CelluloidTools::UnixListener.start(socket_file) do |socket|
84
+ SocketServer.new(socket)
85
+ end
86
+ end
68
87
 
88
+ def listen(listen_address)
69
89
  info "Listening on #{listen_address}"
70
90
 
71
- @listeners[listen_address.id] = EM.start_server(listen_address.address,
72
- listen_address.port,
73
- BouncerConnection,
74
- self,
75
- listen_address.ssl)
91
+ if listen_address.ssl
92
+ server_class = CelluloidTools::SSLListener
93
+ else
94
+ server_class = CelluloidTools::TCPListener
95
+ end
96
+
97
+ listener = server_class.start(listen_address.address,
98
+ listen_address.port) do |socket|
99
+ BouncerConnection.new(self, socket).async.run
100
+ end
101
+
102
+ @listeners[listen_address.id] = listener
76
103
  end
77
104
 
78
105
  def stop_listening(listen_address)
79
106
  listener = @listeners[listen_address.id]
80
107
  return unless listener
81
- EM.stop_server(listener)
108
+ listener.terminate
82
109
  info "No longer listening on #{listen_address}"
83
110
  end
84
111
 
85
- def add_bouncer(bouncer)
86
- key = [bouncer.user.id, bouncer.network.name]
87
- raise("bouncer already exists: #{key}") if @bouncers.include?(key)
88
- @bouncers[key] = bouncer
112
+ def add_bouncer(network_user)
113
+ unless network_user.user && network_user.network
114
+ info "Terminating orphan network user #{network_user}"
115
+ network_user.destroy
116
+ return
117
+ end
118
+ key = [network_user.user_id, network_user.network.name]
119
+ raise("bouncer already exists: #{key}") if @bouncers.registry.key?(key)
120
+ @bouncers.supervise_as(key, Bouncer, network_user)
89
121
  end
90
122
 
91
123
  def find_bouncer(user, network_name)
92
124
  key = [user.id, network_name]
93
- bouncer = @bouncers[key]
125
+ bouncer = @bouncers.registry[key]
94
126
  if !bouncer
95
127
  # find the public network with this name, and attempt to auto-add this user to it
96
- network = Network.first(:conditions => { :user_id => nil, :name => network_name })
128
+ network = Network.first({ :user_id => nil, :name => network_name })
97
129
  if network
98
- nu = NetworkUser.create!(:user => user, :network => network)
130
+ NetworkUser.create(:user => user, :network => network)
99
131
  # AR callback should create the bouncer in sync
100
- bouncer = @bouncers[key]
132
+ bouncer = @bouncers.registry[key]
101
133
  end
102
134
  end
103
135
  bouncer
104
136
  end
105
137
 
106
- class Observer < ActiveRecord::Observer
107
- observe 'Tkellem::ListenAddress', 'Tkellem::NetworkUser'
108
- cattr_accessor :forward_to
109
- self.forward_to = []
110
-
111
- def after_create(obj)
112
- forward_to.each { |f| f.after_create(obj) }
113
- end
114
-
115
- def after_destroy(obj)
116
- forward_to.each { |f| f.after_destroy(obj) }
117
- end
138
+ def socket_file
139
+ File.join(options[:path], 'tkellem.socket')
118
140
  end
119
141
 
120
- ActiveRecord::Base.observers = Observer
121
- ActiveRecord::Base.instantiate_observers
142
+ def db_file
143
+ File.join(options[:path], 'tkellem.sqlite3')
144
+ end
122
145
  end
123
146
 
124
147
  end
@@ -1,3 +1,3 @@
1
1
  module Tkellem
2
- VERSION = "0.8.11"
2
+ VERSION = "0.9.0.beta1"
3
3
  end
data/lib/tkellem.rb CHANGED
@@ -21,7 +21,7 @@ module Tkellem
21
21
  @trace = val
22
22
  end
23
23
  def self.trace
24
- @trace || @trace = false
24
+ @trace || @trace = true
25
25
  end
26
26
 
27
27
  def log_name
@@ -32,15 +32,6 @@ module Tkellem
32
32
  puts("TRACE: #{log_name}: #{msg}") if EasyLogger.trace
33
33
  end
34
34
 
35
- def failsafe(event)
36
- yield
37
- rescue => e
38
- # if the failsafe rescue fails, we're in a really bad state and should probably just die
39
- self.error "exception while handling #{event}"
40
- self.error e.to_s
41
- (e.backtrace || []).each { |line| self.error line }
42
- end
43
-
44
35
  ::Logger::Severity.constants.each do |level|
45
36
  next if level == "UNKNOWN"
46
37
  module_eval(<<-EVAL, __FILE__, __LINE__)
data/spec/spec_helper.rb CHANGED
@@ -5,30 +5,17 @@ require 'tkellem'
5
5
  require 'rspec'
6
6
 
7
7
  Tkellem::EasyLogger.logger = Logger.new("test.log")
8
- ActiveRecord::Base.logger = Tkellem::EasyLogger.logger
9
8
 
10
- ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
11
- ActiveRecord::Migration.verbose = false
12
- ActiveRecord::Migrator.migrate(File.expand_path("../../lib/tkellem/migrations", __FILE__), nil)
9
+ TestDB = Tkellem::TkellemServer.initialize_database(':memory:')
13
10
 
14
11
  RSpec.configure do |config|
15
- config.before(:each) do
16
- ActiveRecord::Base.connection.increment_open_transactions
17
- ActiveRecord::Base.connection.begin_db_transaction
18
- end
19
-
20
- config.after(:each) do
21
- ActiveRecord::Base.connection.rollback_db_transaction
22
- ActiveRecord::Base.connection.decrement_open_transactions
12
+ config.around(:each) do |block|
13
+ TestDB.transaction(:rollback => :always) do
14
+ block.run()
15
+ end
23
16
  end
24
17
 
25
18
  def m(line)
26
19
  IrcMessage.parse(line)
27
20
  end
28
-
29
- def em(mod)
30
- c = Class.new
31
- c.send(:include, mod)
32
- c
33
- end
34
21
  end