tkellem 0.9.0.beta3 → 0.9.0.beta4

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.
@@ -1,13 +1,13 @@
1
1
  module Tkellem
2
2
 
3
- class Setting < Sequel::Model
3
+ class Setting < ActiveRecord::Base
4
4
  def self.get(setting_name)
5
- setting = where(:name => setting_name).first
5
+ setting = first(:conditions => { :name => setting_name })
6
6
  setting.try(:value)
7
7
  end
8
8
 
9
9
  def self.set(setting_name, new_value)
10
- setting = where(:name => setting_name).first
10
+ setting = first(:conditions => { :name => setting_name })
11
11
  setting.try(:update_attributes, :value => new_value.to_s, :unchanged => false)
12
12
  setting
13
13
  end
@@ -1,20 +1,13 @@
1
1
  module Tkellem
2
2
 
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
3
+ class User < ActiveRecord::Base
4
+ has_many :network_users, :dependent => :destroy
5
+ has_many :networks, :dependent => :destroy
8
6
 
9
7
  validates_presence_of :username
10
8
  validates_uniqueness_of :username
11
9
  validates_presence_of :role, :in => %w(user admin)
12
10
 
13
- def before_validation
14
- self.role ||= 'user'
15
- super
16
- end
17
-
18
11
  # pluggable authentication -- add your own block, which takes |username, password|
19
12
  # parameters. Return a User object if authentication succeeded, or a
20
13
  # false/nil value if auth failed. You can create the user on-the-fly if
@@ -25,7 +18,7 @@ class User < Sequel::Model
25
18
  # default database-based authentication
26
19
  # TODO: proper password hashing
27
20
  self.authentication_methods << proc do |username, password|
28
- user = first(:username => username)
21
+ user = find_by_username(username)
29
22
  user && user.valid_password?(password) && user
30
23
  end
31
24
 
@@ -38,7 +31,7 @@ class User < Sequel::Model
38
31
  end
39
32
 
40
33
  def username=(val)
41
- super(val.try(:downcase))
34
+ write_attribute(:username, val.try(:downcase))
42
35
  end
43
36
 
44
37
  def name
@@ -51,7 +44,7 @@ class User < Sequel::Model
51
44
  end
52
45
 
53
46
  def password=(password)
54
- super(password ? OpenSSL::Digest::SHA1.hexdigest(password) : nil)
47
+ write_attribute(:password, password ? OpenSSL::Digest::SHA1.hexdigest(password) : nil)
55
48
  end
56
49
 
57
50
  def admin?
@@ -15,11 +15,9 @@ 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
21
18
 
22
19
  Bouncer.add_plugin(self)
20
+ cattr_accessor :instances
23
21
 
24
22
  def self.get_instance(bouncer)
25
23
  bouncer.data(self)[:instance] ||= self.new(bouncer)
@@ -59,7 +57,7 @@ class Backlog
59
57
  # open stream in append-only mode
60
58
  return @streams[ctx] if @streams[ctx]
61
59
  stream = @streams[ctx] = File.open(stream_filename(ctx), 'ab')
62
- stream.seek(0, ::IO::SEEK_END)
60
+ stream.seek(0, IO::SEEK_END)
63
61
  @starting_pos[ctx] = stream.pos
64
62
  stream
65
63
  end
@@ -118,46 +116,38 @@ class Backlog
118
116
 
119
117
  def send_backlog(conn, device)
120
118
  device.each do |ctx_name, pos|
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
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
+ privmsg = msg.args.first[0] != '#'[0]
126
+ if msg.prefix
127
+ # to this user
128
+ if privmsg
129
+ msg.args[0] = @bouncer.nick
130
+ else
131
+ # do nothing, it's good to send
132
+ end
143
133
  else
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
134
+ # from this user
135
+ if privmsg
136
+ # a one-on-one chat -- every client i've seen doesn't know how to
137
+ # display messages from themselves here, so we fake it by just
138
+ # adding an arrow and pretending the other user said it. shame.
139
+ msg.prefix = msg.args.first
140
+ msg.args[0] = @bouncer.nick
141
+ msg.args[-1] = "-> #{msg.args.last}"
142
+ else
143
+ # it's a room, we can just replay
144
+ msg.prefix = @bouncer.nick
145
+ end
158
146
  end
147
+ conn.send_msg(msg.with_timestamp(timestamp))
159
148
  end
160
- conn.send_msg(msg.with_timestamp(timestamp))
149
+
150
+ device[ctx_name] = get_stream(ctx_name).pos
161
151
  end
162
152
  end
163
153
 
@@ -182,6 +172,4 @@ class BacklogReplay
182
172
  end
183
173
  end
184
174
 
185
- Backlog.replay_pool = BacklogReplay.pool(size: Celluloid.cores * 2)
186
-
187
175
  end
@@ -1,30 +1,26 @@
1
- require 'celluloid/io'
1
+ require 'eventmachine'
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
- # TODO: rename this class
9
- class SocketServer
10
- include Celluloid::IO
8
+ module SocketServer
9
+ include EM::Protocols::LineText2
11
10
  include Tkellem::EasyLogger
12
- include Tkellem::CelluloidTools::LineReader
13
11
 
14
12
  def log_name
15
13
  "admin"
16
14
  end
17
15
 
18
- def initialize(socket)
19
- @socket = socket
20
- @delimiter = "\n"
21
- async.run
16
+ def post_init
17
+ set_delimiter "\n"
22
18
  end
23
19
 
24
20
  def receive_line(line)
25
21
  trace "admin socket: #{line}"
26
- TkellemBot.run_command(line, nil, nil) do |outline|
27
- send_data("#{outline}\n")
22
+ TkellemBot.run_command(line, nil, nil) do |output|
23
+ send_data("#{output}\n")
28
24
  end
29
25
  send_data("\0\n")
30
26
  rescue => e
@@ -32,10 +28,6 @@ class SocketServer
32
28
  e.backtrace.each { |l| send_data("#{l}\n") }
33
29
  send_data("\0\n")
34
30
  end
35
-
36
- def send_data(dat)
37
- @socket.write(dat)
38
- end
39
31
  end
40
32
 
41
33
  end
@@ -156,7 +156,7 @@ class TkellemBot
156
156
  end
157
157
 
158
158
  def modify
159
- instance = model.first(find_attributes)
159
+ instance = model.first(:conditions => 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(find_attributes)
184
+ instance = model.first(:conditions => 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.where(:username => opts['username']).first
261
+ user = User.first(:conditions => { :username => opts['username'] })
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.where(:user_id => nil, :name => opts['network'].downcase).first
306
+ target = Network.first(:conditions => ["name = ? AND user_id IS NULL", opts['network'].downcase])
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.where(:user_id => nil).all
342
+ public_networks = Network.all(:conditions => 'user_id IS NULL')
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,14 +355,15 @@ class TkellemBot
355
355
  end
356
356
 
357
357
  def execute
358
+ # TODO: this got gross
358
359
  if args.empty? && !opts['remove']
359
360
  list
360
361
  return
361
362
  end
362
363
 
363
364
  if opts['network'].present?
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)
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)
366
367
  else
367
368
  target = network_user.try(:network)
368
369
  if target && target.public? && !self.class.admin_user?(user)
@@ -378,7 +379,7 @@ class TkellemBot
378
379
  raise(Command::ArgumentError, "No network found") unless target
379
380
  raise(Command::ArgumentError, "You must explicitly specify the network to remove") unless opts['network']
380
381
  if uri
381
- target.hosts.where(addr_args).first.try(:destroy)
382
+ target.hosts.first(:conditions => addr_args).try(:destroy)
382
383
  respond " #{show(target)}"
383
384
  else
384
385
  target.destroy
@@ -396,9 +397,16 @@ class TkellemBot
396
397
  end
397
398
  end
398
399
 
399
- Host.create(addr_args.merge(network: target))
400
- respond("updated:")
401
- respond " #{show(target)}"
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
402
410
  end
403
411
  end
404
412
  end
@@ -1,62 +1,47 @@
1
- require 'active_support/core_ext'
2
- require 'celluloid'
3
- require 'sequel'
1
+ require 'eventmachine'
2
+ require 'active_record'
3
+ require 'rails/observers/activerecord/active_record'
4
4
 
5
- require 'tkellem/bouncer'
6
5
  require 'tkellem/bouncer_connection'
7
- require 'tkellem/celluloid_tools'
6
+ require 'tkellem/bouncer'
7
+
8
+ require 'tkellem/models/host'
9
+ require 'tkellem/models/listen_address'
10
+ require 'tkellem/models/network'
11
+ require 'tkellem/models/network_user'
12
+ require 'tkellem/models/setting'
13
+ require 'tkellem/models/user'
8
14
 
9
15
  require 'tkellem/plugins/backlog'
10
- #require 'tkellem/plugins/push_service'
16
+ require 'tkellem/plugins/push_service'
11
17
 
12
18
  module Tkellem
13
19
 
14
20
  class TkellemServer
15
- include Celluloid
16
21
  include Tkellem::EasyLogger
17
22
 
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
+ attr_reader :bouncers
40
24
 
41
- def initialize(options)
42
- Celluloid.logger = Tkellem::EasyLogger.logger
43
- @options = options
25
+ def initialize
44
26
  @listeners = {}
45
- @bouncers = CelluloidTools::BackoffSupervisor.new_link({})
27
+ @bouncers = {}
46
28
  $tkellem_server = self
47
29
 
48
- @db = self.class.initialize_database(db_file)
49
- end
30
+ unless ActiveRecord::Base.connected?
31
+ ActiveRecord::Base.establish_connection({
32
+ :adapter => 'sqlite3',
33
+ :database => File.expand_path("~/.tkellem/tkellem.sqlite3"),
34
+ })
35
+ ActiveRecord::Migrator.migrate(File.expand_path("../migrations", __FILE__), nil)
36
+ end
50
37
 
51
- def run
52
- start_unix_server
53
- ListenAddress.all { |a| listen(a) }
54
- NetworkUser.all { |nu| add_bouncer(nu) }
38
+ ListenAddress.all.each { |a| listen(a) }
39
+ NetworkUser.find_each { |nu| add_bouncer(nu) }
40
+ Observer.forward_to << self
41
+ end
55
42
 
56
- begin
57
- loop { sleep 5 }
58
- rescue Interrupt
59
- end
43
+ def stop
44
+ Observer.forward_to.delete(self)
60
45
  end
61
46
 
62
47
  # callbacks for AR observer events
@@ -73,75 +58,83 @@ class TkellemServer
73
58
  case obj
74
59
  when ListenAddress
75
60
  stop_listening(obj)
76
- end
77
- end
78
-
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)
61
+ when NetworkUser
62
+ stop_bouncer(obj)
85
63
  end
86
64
  end
87
65
 
88
66
  def listen(listen_address)
89
67
  info "Listening on #{listen_address}"
90
68
 
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
69
+ @listeners[listen_address.id] = EM.start_server(listen_address.address,
70
+ listen_address.port,
71
+ BouncerConnection,
72
+ self,
73
+ listen_address.ssl)
103
74
  end
104
75
 
105
76
  def stop_listening(listen_address)
106
77
  listener = @listeners[listen_address.id]
107
78
  return unless listener
108
- listener.terminate
79
+ EM.stop_server(listener)
109
80
  info "No longer listening on #{listen_address}"
110
81
  end
111
82
 
112
83
  def add_bouncer(network_user)
113
84
  unless network_user.user && network_user.network
114
- info "Terminating orphan network user #{network_user}"
85
+ info "Terminating orphan network user #{network_user.inspect}"
115
86
  network_user.destroy
116
87
  return
117
88
  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
+
90
+ key = bouncers_key(network_user)
91
+ raise("bouncer already exists: #{key}") if @bouncers.include?(key)
92
+ @bouncers[key] = Bouncer.new(bouncer)
93
+ end
94
+
95
+ def stop_bouncer(obj)
96
+ key = bouncers_key(network_user)
97
+ bouncer = @bouncers.delete(key)
98
+ if bouncer
99
+ bouncer.kill!
100
+ end
121
101
  end
122
102
 
123
103
  def find_bouncer(user, network_name)
124
104
  key = [user.id, network_name]
125
- bouncer = @bouncers.registry[key]
105
+ bouncer = @bouncers[key]
126
106
  if !bouncer
127
107
  # find the public network with this name, and attempt to auto-add this user to it
128
- network = Network.first({ :user_id => nil, :name => network_name })
108
+ network = Network.first(:conditions => { :user_id => nil, :name => network_name })
129
109
  if network
130
- NetworkUser.create(:user => user, :network => network)
110
+ NetworkUser.create!(:user => user, :network => network)
131
111
  # AR callback should create the bouncer in sync
132
- bouncer = @bouncers.registry[key]
112
+ bouncer = @bouncers[key]
133
113
  end
134
114
  end
135
115
  bouncer
136
116
  end
137
117
 
138
- def socket_file
139
- File.join(options[:path], 'tkellem.socket')
118
+ def bouncers_key(network_user)
119
+ [network_user.user_id, network_user.network.name]
140
120
  end
141
121
 
142
- def db_file
143
- File.join(options[:path], 'tkellem.sqlite3')
122
+ class Observer < ActiveRecord::Observer
123
+ observe 'Tkellem::ListenAddress', 'Tkellem::NetworkUser'
124
+ cattr_accessor :forward_to
125
+ self.forward_to = []
126
+
127
+ def after_create(obj)
128
+ forward_to.each { |f| f.after_create(obj) }
129
+ end
130
+
131
+ def after_destroy(obj)
132
+ forward_to.each { |f| f.after_destroy(obj) }
133
+ end
144
134
  end
135
+
136
+ ActiveRecord::Base.observers = Observer
137
+ ActiveRecord::Base.instantiate_observers
145
138
  end
146
139
 
147
140
  end
@@ -1,3 +1,3 @@
1
1
  module Tkellem
2
- VERSION = "0.9.0.beta3"
2
+ VERSION = "0.9.0.beta4"
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 = true
24
+ @trace || @trace = false
25
25
  end
26
26
 
27
27
  def log_name
@@ -32,6 +32,15 @@ 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
+
35
44
  ::Logger::Severity.constants.each do |level|
36
45
  next if level == "UNKNOWN"
37
46
  module_eval(<<-EVAL, __FILE__, __LINE__)
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'tkellem/bouncer_connection'
3
+
4
+ include Tkellem
5
+
6
+ describe BouncerConnection, "connect" do
7
+ before do
8
+ @u = User.create(:username => 'speccer')
9
+ @u.password = 'test123'
10
+ @u.save
11
+ @tk = mock(TkellemServer)
12
+ @b = mock(Bouncer)
13
+ @bc = em(BouncerConnection).new(@tk, false)
14
+ end
15
+
16
+ it "should ignore blank lines" do
17
+ @bc.should_receive(:error!).never
18
+ @bc.receive_line("")
19
+ end
20
+
21
+ it "should connect after receiving credentials" do
22
+ @tk.should_receive(:find_bouncer).with(@u, 'testhost').and_return(@b)
23
+ @bc.receive_line("NICK speccer")
24
+ @bc.receive_line("PASS test123")
25
+ @b.should_receive(:connect_client).with(@bc)
26
+ @bc.receive_line("USER speccer@testhost")
27
+ end
28
+
29
+ it "should connect when receiving user before pass" do
30
+ @tk.should_receive(:find_bouncer).with(@u, 'testhost').and_return(@b)
31
+ @bc.receive_line("USER speccer@testhost")
32
+ @bc.receive_line("PASS test123")
33
+ @b.should_receive(:connect_client).with(@bc)
34
+ @bc.receive_line("NICK speccer")
35
+ end
36
+ end
37
+