tkellem 0.9.0.beta3 → 0.9.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+