tkellem 0.8.3 → 0.8.4

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.
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