tkellem 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,3 +5,4 @@ pkg/*
5
5
  coverage/*
6
6
  doc/*
7
7
  .yardoc/*
8
+ test.log
@@ -58,6 +58,8 @@ class Bouncer
58
58
  return
59
59
  end
60
60
 
61
+ # force the client nick
62
+ client.send_msg(":#{client.connecting_nick} NICK #{nick}") if client.connecting_nick != nick
61
63
  send_welcome(client)
62
64
  # make the client join all the rooms that we're in
63
65
  @rooms.each { |room| client.simulate_join(room) }
@@ -155,9 +157,10 @@ class Bouncer
155
157
  @conn.send_data("#{msg}\r\n")
156
158
  end
157
159
 
158
- def connection_established
160
+ def connection_established(conn)
161
+ @conn = conn
159
162
  # TODO: support sending a real username, realname, etc
160
- send_msg("USER #{@nick} localhost blah :#{@nick}")
163
+ send_msg("USER #{@user.username} somehost tkellem :#{@user.name}")
161
164
  change_nick(@nick, true)
162
165
  check_away_status
163
166
  end
@@ -193,7 +196,7 @@ class Bouncer
193
196
  @last_connect = Time.now
194
197
  @cur_host = (@cur_host || 0) % hosts.length
195
198
  host = hosts[@cur_host]
196
- @conn = EM.connect(host.address, host.port, IrcServerConnection, self, host.ssl)
199
+ EM.connect(host.address, host.port, IrcServerConnection, self, host.ssl)
197
200
  end
198
201
 
199
202
  def ready!
@@ -19,11 +19,11 @@ module BouncerConnection
19
19
  @name = 'new-conn'
20
20
  @data = {}
21
21
  end
22
- attr_reader :ssl, :bouncer, :name, :device_name
22
+ attr_reader :ssl, :bouncer, :name, :device_name, :connecting_nick
23
23
  alias_method :log_name, :name
24
24
 
25
25
  def nick
26
- @bouncer ? @bouncer.nick : @nick
26
+ @bouncer ? @bouncer.nick : @connecting_nick
27
27
  end
28
28
 
29
29
  def data(key)
@@ -79,16 +79,18 @@ module BouncerConnection
79
79
  if msg.args.first =~ /req/i
80
80
  send_msg("CAP NAK")
81
81
  end
82
- elsif command == 'PASS'
83
- unless @password
84
- @password = msg.args.first
85
- end
82
+ elsif command == 'PASS' && @state == :auth
83
+ @password = msg.args.first
86
84
  elsif command == 'NICK' && @state == :auth
87
- @nick = msg.args.first
85
+ @connecting_nick = msg.args.first
86
+ maybe_connect
88
87
  elsif command == 'QUIT'
89
88
  close_connection
90
- elsif command == 'USER'
91
- msg_user(msg)
89
+ elsif command == 'USER' && @state == :auth
90
+ unless @username
91
+ @username, @conn_info = msg.args.first.strip.split('@', 2).map { |a| a.downcase }
92
+ end
93
+ maybe_connect
92
94
  elsif @state == :auth
93
95
  error!("Protocol error. You must authenticate first.")
94
96
  elsif @state == :connected
@@ -106,15 +108,14 @@ module BouncerConnection
106
108
  end
107
109
  end
108
110
 
109
- def msg_user(msg)
110
- unless @user
111
- @username, rest = msg.args.first.strip.split('@', 2).map { |a| a.downcase }
111
+ def maybe_connect
112
+ if @connecting_nick && @username && @password && !@user
112
113
  @name = @username
113
114
  @user = User.authenticate(@username, @password)
114
115
  return error!("Unknown username: #{@username} or bad password.") unless @user
115
116
 
116
- if rest && !rest.empty?
117
- @conn_name, @device_name = rest.split(':', 2)
117
+ if @conn_info && !@conn_info.empty?
118
+ @conn_name, @device_name = @conn_info.split(':', 2)
118
119
  # 'default' or missing device_name to use the default backlog
119
120
  # pass a device_name to have device-independent backlogs
120
121
  @device_name = @device_name.presence || 'default'
@@ -135,7 +136,7 @@ module BouncerConnection
135
136
  end
136
137
 
137
138
  def simulate_join(room)
138
- send_msg(":#{nick}!#{name}@tkellem JOIN #{room}")
139
+ send_msg(":#{nick} JOIN #{room}")
139
140
  # TODO: intercept the NAMES response so that only this bouncer gets it
140
141
  # Otherwise other clients might show an "in this room" line.
141
142
  @bouncer.send_msg("NAMES #{room}\r\n")
@@ -1,6 +1,6 @@
1
1
  module Tkellem
2
2
 
3
- class IrcMessage < Struct.new(:prefix, :command, :args)
3
+ class IrcMessage < Struct.new(:prefix, :command, :args, :ctcp)
4
4
  RE = %r{(:[^ ]+ )?([^ ]*)(.*)}i
5
5
 
6
6
  def self.parse(line)
@@ -18,7 +18,22 @@ class IrcMessage < Struct.new(:prefix, :command, :args)
18
18
  args = args.split(' ')
19
19
  end
20
20
 
21
- self.new(prefix, command, args)
21
+ msg = self.new(prefix, command, args)
22
+
23
+ if args.last.match(%r{#{"\x01"}([^ ]+)([^\1]*)#{"\x01"}})
24
+ msg.ctcp = $1.upcase
25
+ msg.args[-1] = $2.strip
26
+ end
27
+
28
+ msg
29
+ end
30
+
31
+ def ctcp?
32
+ self.ctcp.present?
33
+ end
34
+
35
+ def action?
36
+ self.ctcp == 'ACTION'
22
37
  end
23
38
 
24
39
  def command?(cmd)
@@ -31,7 +46,11 @@ class IrcMessage < Struct.new(:prefix, :command, :args)
31
46
  line << command
32
47
  ext_arg = args.last if args.last && args.last.match(%r{\s})
33
48
  line += ext_arg ? args[0...-1] : args
34
- line << ":#{ext_arg}" unless ext_arg.nil?
49
+ if ctcp?
50
+ line << ":\x01#{ctcp} #{ext_arg}\x01"
51
+ else
52
+ line << ":#{ext_arg}" unless ext_arg.nil?
53
+ end
35
54
  line.join ' '
36
55
  end
37
56
  alias_method :to_s, :replay
@@ -50,7 +69,7 @@ class IrcMessage < Struct.new(:prefix, :command, :args)
50
69
  args = args.dup
51
70
  args[-1] = "#{timestamp.strftime("%H:%M:%S")}> #{args[-1]}"
52
71
  end
53
- IrcMessage.new(prefix, command, args)
72
+ IrcMessage.new(prefix, command, args, ctcp)
54
73
  end
55
74
 
56
75
  end
@@ -26,7 +26,7 @@ module IrcServerConnection
26
26
  end
27
27
 
28
28
  def ssl_handshake_completed
29
- EM.next_tick { @bouncer.connection_established }
29
+ EM.next_tick { @bouncer.connection_established(self) }
30
30
  end
31
31
 
32
32
  def receive_line(line)
@@ -13,7 +13,7 @@ class NetworkUser < ActiveRecord::Base
13
13
  # we use the network's at_connect until it is modified and overwritten for
14
14
  # this specific network user
15
15
  def at_connect
16
- read_attribute(:at_connect).presence || network.at_connect.presence || []
16
+ network.at_connect + (read_attribute(:at_connect) || [])
17
17
  end
18
18
  end
19
19
 
@@ -44,10 +44,14 @@ class User < ActiveRecord::Base
44
44
  end
45
45
 
46
46
  def set_password!(password)
47
- self.password = OpenSSL::Digest::SHA1.hexdigest(password)
47
+ self.password = password
48
48
  self.save!
49
49
  end
50
50
 
51
+ def password=(password)
52
+ write_attribute(:password, password ? OpenSSL::Digest::SHA1.hexdigest(password) : nil)
53
+ end
54
+
51
55
  def admin?
52
56
  role == 'admin'
53
57
  end
@@ -45,7 +45,7 @@ class Backlog
45
45
  @devices = {}
46
46
  @streams = {}
47
47
  @starting_pos = {}
48
- @dir = File.expand_path("~/.tkellem/logs/#{bouncer.user.name}/#{bouncer.network.name}")
48
+ @dir = File.expand_path("~/.tkellem/logs/#{bouncer.user.username}/#{bouncer.network.name}")
49
49
  FileUtils.mkdir_p(@dir)
50
50
  end
51
51
 
@@ -57,6 +57,7 @@ class Backlog
57
57
  # open stream in append-only mode
58
58
  return @streams[ctx] if @streams[ctx]
59
59
  stream = @streams[ctx] = File.open(stream_filename(ctx), 'ab')
60
+ stream.seek(0, IO::SEEK_END)
60
61
  @starting_pos[ctx] = stream.pos
61
62
  stream
62
63
  end
@@ -90,13 +91,14 @@ class Backlog
90
91
  # transient messages
91
92
  return
92
93
  when 'PRIVMSG'
94
+ return if msg.ctcp? && !msg.action?
93
95
  ctx = msg.args.first
94
96
  if ctx == @bouncer.nick
95
97
  # incoming pm, fake ctx to be the sender's nick
96
98
  ctx = msg.prefix.split(/[!~@]/, 2).first
97
99
  end
98
100
  stream = get_stream(ctx)
99
- stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S < #{msg.prefix}: #{msg.args.last}"))
101
+ stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S < #{'* ' if msg.action?}#{msg.prefix}: #{msg.args.last}"))
100
102
  update_pos(ctx, stream.pos)
101
103
  end
102
104
  end
@@ -104,9 +106,10 @@ class Backlog
104
106
  def client_msg(msg)
105
107
  case msg.command
106
108
  when 'PRIVMSG'
109
+ return if msg.ctcp? && !msg.action?
107
110
  ctx = msg.args.first
108
111
  stream = get_stream(ctx)
109
- stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S > #{msg.args.last}"))
112
+ stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S > #{'* ' if msg.action?}#{msg.args.last}"))
110
113
  update_pos(ctx, stream.pos)
111
114
  end
112
115
  end
@@ -145,11 +148,17 @@ class Backlog
145
148
  def parse_line(line, ctx_name)
146
149
  timestamp = Time.parse(line[0, 19])
147
150
  case line[20..-1]
148
- when %r{^> (.+)$}
149
- msg = IrcMessage.new(nil, 'PRIVMSG', [ctx_name, $1])
151
+ when %r{^> (\* )?(.+)$}
152
+ msg = IrcMessage.new(nil, 'PRIVMSG', [ctx_name, $2])
153
+ if $1 == '* '
154
+ msg.ctcp = 'ACTION'
155
+ end
150
156
  return timestamp, msg
151
- when %r{^< ([^:]+): (.+)$}
152
- msg = IrcMessage.new($1, 'PRIVMSG', [ctx_name, $2])
157
+ when %r{^< (\* )?([^:]+): (.+)$}
158
+ msg = IrcMessage.new($2, 'PRIVMSG', [ctx_name, $3])
159
+ if $1 == '* '
160
+ msg.ctcp = 'ACTION'
161
+ end
153
162
  return timestamp, msg
154
163
  else
155
164
  nil
@@ -18,15 +18,19 @@ module Tkellem
18
18
  class TkellemServer
19
19
  include Tkellem::EasyLogger
20
20
 
21
+ attr_reader :bouncers
22
+
21
23
  def initialize
22
24
  @listeners = {}
23
25
  @bouncers = {}
24
26
 
25
- ActiveRecord::Base.establish_connection({
26
- :adapter => 'sqlite3',
27
- :database => File.expand_path("~/.tkellem/tkellem.sqlite3"),
28
- })
29
- ActiveRecord::Migrator.migrate(File.expand_path("../migrations", __FILE__), nil)
27
+ unless ActiveRecord::Base.connected?
28
+ ActiveRecord::Base.establish_connection({
29
+ :adapter => 'sqlite3',
30
+ :database => File.expand_path("~/.tkellem/tkellem.sqlite3"),
31
+ })
32
+ ActiveRecord::Migrator.migrate(File.expand_path("../migrations", __FILE__), nil)
33
+ end
30
34
 
31
35
  ListenAddress.all.each { |a| listen(a) }
32
36
  NetworkUser.find_each { |nu| add_bouncer(Bouncer.new(nu)) }
@@ -1,3 +1,3 @@
1
1
  module Tkellem
2
- VERSION = "0.8.3"
2
+ VERSION = "0.8.4"
3
3
  end
@@ -0,0 +1,30 @@
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.set_password!('test123')
10
+ tk = mock(TkellemServer)
11
+ @b = mock(Bouncer)
12
+ tk.should_receive(:find_bouncer).with(u, 'testhost').and_return(@b)
13
+ @bc = em(BouncerConnection).new(tk, false)
14
+ end
15
+
16
+ it "should connect after receiving credentials" do
17
+ @bc.receive_line("NICK speccer")
18
+ @bc.receive_line("PASS test123")
19
+ @b.should_receive(:connect_client).with(@bc)
20
+ @bc.receive_line("USER speccer@testhost")
21
+ end
22
+
23
+ it "should connect when receiving user before pass" do
24
+ @bc.receive_line("USER speccer@testhost")
25
+ @bc.receive_line("PASS test123")
26
+ @b.should_receive(:connect_client).with(@bc)
27
+ @bc.receive_line("NICK speccer")
28
+ end
29
+ end
30
+
@@ -45,3 +45,14 @@ describe IrcMessage do
45
45
  line2.args.last.should == "three"
46
46
  end
47
47
  end
48
+
49
+ describe IrcMessage, "CTCP" do
50
+ it "should parse basic ACTION messages" do
51
+ msg = IrcMessage.parse(":user1 PRIVMSG #room :\1ACTION is a loser on IRC\1")
52
+ msg.command.should == 'PRIVMSG'
53
+ msg.args.should == ['#room', 'is a loser on IRC']
54
+ msg.ctcp?.should == true
55
+ msg.action?.should == true
56
+ msg.to_s.should == ":user1 PRIVMSG #room :\1ACTION is a loser on IRC\1"
57
+ end
58
+ end
@@ -3,26 +3,30 @@ require 'tkellem/irc_server'
3
3
 
4
4
  include Tkellem
5
5
 
6
- describe IrcServer, "connection" do
7
- def make_server
8
- Class.new do
9
- include IrcServer
6
+ describe Bouncer, "connection" do
7
+ before do
8
+ EM.stub!(:add_timer).and_return(nil)
9
+ end
10
10
 
11
- def send_data(*a); end
12
- end.new(nil, "spec_server", false, "speccer")
11
+ def make_server
12
+ b = Bouncer.new(NetworkUser.new(:user => User.new(:username => 'speccer'), :network => Network.new))
13
+ b
13
14
  end
14
15
 
15
16
  def send_welcome(s, &just_before_last)
16
- s.receive_line("001 blah blah")
17
- s.receive_line("002 more blah")
18
- s.receive_line("003 even more blah")
17
+ s.should_receive(:send_msg).with("USER speccer somehost tkellem :speccer")
18
+ s.should_receive(:send_msg).with("NICK speccer")
19
+ s.should_receive(:send_msg).with("AWAY :Away")
20
+ s.connection_established(nil)
21
+ s.server_msg(IrcMessage.parse("001 blah blah"))
22
+ s.server_msg(IrcMessage.parse("002 more blah"))
23
+ s.server_msg(IrcMessage.parse("003 even more blah"))
19
24
  just_before_last && just_before_last.call
20
- s.receive_line("376 :end of MOTD")
25
+ s.server_msg(IrcMessage.parse("376 :end of MOTD"))
21
26
  end
22
27
 
23
28
  def connected_server
24
29
  s = make_server
25
- s.post_init
26
30
  send_welcome(s)
27
31
  s.connected?.should be_true
28
32
  s
@@ -31,30 +35,57 @@ describe IrcServer, "connection" do
31
35
  it "should connect to the server on creation" do
32
36
  s = make_server
33
37
  s.connected?.should_not be_true
34
- s.should_receive(:send_data).with("USER speccer localhost blah :speccer\r\n")
35
- s.should_receive(:send_data).with("NICK speccer\r\n")
36
- s.post_init
38
+ s.should_receive(:send_msg).with("USER speccer somehost tkellem :speccer")
39
+ s.should_receive(:send_msg).with("NICK speccer")
40
+ s.should_receive(:send_msg).with("AWAY :Away")
41
+ s.connection_established(nil)
37
42
  end
38
43
 
39
- it "should join pending rooms once the connection is established" do
40
- s = make_server
41
- s.post_init
42
- s.connected?.should_not be_true # still haven't received the welcome
44
+ it "should pong" do
45
+ s = connected_server
46
+ s.should_receive(:send_msg).with("PONG tkellem!tkellem :HAI")
47
+ s.server_msg(IrcMessage.parse(":speccer!test@host ping :HAI"))
48
+ end
43
49
 
44
- s.join_room "#test1"
45
- # as soon as the end of MOTD is received, the IrcServer will consider itself
46
- # connected and try to join the rooms.
47
- s.should_receive(:send_data).with("JOIN #test1\r\n")
48
- s.should_receive(:send_data).with("JOIN #test2\r\n")
50
+ def tk_server
51
+ @tk_server ||= TkellemServer.new
52
+ end
49
53
 
50
- send_welcome(s) { s.join_room "#test2" }
54
+ def network_user(opts = {})
55
+ opts[:user] ||= @user ||= User.create!(:username => 'speccer', :password => 'test123')
56
+ opts[:network] ||= @network ||= Network.create!(:name => 'localhost')
57
+ @network_user ||= NetworkUser.create!(opts)
58
+ end
51
59
 
52
- s.connected?.should be_true
60
+ def bouncer(opts = {})
61
+ tk_server
62
+ network_user
63
+ @bouncer = @tk_server.bouncers.values.last
64
+ if opts[:connect]
65
+ @server_conn = em(IrcServerConnection).new(@bouncer, false)
66
+ @server_conn.stub!(:send_data)
67
+ @bouncer.connection_established(@server_conn)
68
+ @bouncer.send :ready!
69
+ end
70
+ @bouncer
53
71
  end
54
72
 
55
- it "should pong" do
56
- s = connected_server
57
- s.should_receive(:send_data).with("PONG speccer!tkellem :HAI\r\n")
58
- s.receive_line(":speccer!test@host ping :HAI")
73
+ def client_connection(opts = {})
74
+ @client ||= em(BouncerConnection).new(tk_server, false)
75
+ if opts[:connect]
76
+ end
77
+ @client
78
+ end
79
+
80
+ it "should force the client nick on connect" do
81
+ network_user(:nick => 'mynick')
82
+ bouncer(:connect => true)
83
+ @bouncer.server_msg(m ":mynick JOIN #t1")
84
+ client_connection
85
+ @client.should_receive(:send_msg).with(":some_other_nick NICK mynick")
86
+ @client.should_receive(:send_msg).with(":mynick JOIN #t1")
87
+ @client.receive_line("PASS test123")
88
+ @client.receive_line("NICK some_other_nick")
89
+ @client.receive_line("USER #{@user.username}@#{@network.name} a b :c")
59
90
  end
60
91
  end
data/spec/spec_helper.rb CHANGED
@@ -4,5 +4,31 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
4
  require 'tkellem'
5
5
  require 'rspec'
6
6
 
7
+ Tkellem::EasyLogger.logger = Logger.new("test.log")
8
+ ActiveRecord::Base.logger = Tkellem::EasyLogger.logger
9
+
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)
13
+
7
14
  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
23
+ end
24
+
25
+ def m(line)
26
+ IrcMessage.parse(line)
27
+ end
28
+
29
+ def em(mod)
30
+ c = Class.new
31
+ c.send(:include, mod)
32
+ c
33
+ end
8
34
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: tkellem
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.8.3
5
+ version: 0.8.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Brian Palmer
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-14 00:00:00 -06:00
13
+ date: 2011-06-17 00:00:00 -06:00
14
14
  default_executable: tkellem
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -149,6 +149,7 @@ files:
149
149
  - lib/tkellem/tkellem_server.rb
150
150
  - lib/tkellem/version.rb
151
151
  - resources/bot_command_descriptions.yml
152
+ - spec/bouncer_connection_spec.rb
152
153
  - spec/irc_message_spec.rb
153
154
  - spec/irc_server_spec.rb
154
155
  - spec/spec_helper.rb
@@ -182,6 +183,7 @@ signing_key:
182
183
  specification_version: 3
183
184
  summary: IRC bouncer with multi-client support
184
185
  test_files:
186
+ - spec/bouncer_connection_spec.rb
185
187
  - spec/irc_message_spec.rb
186
188
  - spec/irc_server_spec.rb
187
189
  - spec/spec_helper.rb