tkellem 0.7.0
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/LICENSE +20 -0
- data/README.md +17 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/bin/tkellem +110 -0
- data/examples/config.yml +29 -0
- data/lib/tkellem/backlog.rb +85 -0
- data/lib/tkellem/bouncer.rb +52 -0
- data/lib/tkellem/bouncer_connection.rb +149 -0
- data/lib/tkellem/irc_line.rb +58 -0
- data/lib/tkellem/irc_server.rb +156 -0
- data/lib/tkellem.rb +45 -0
- data/spec/spec_helper.rb +10 -0
- metadata +89 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Brian Palmer <brian@codekitchen.net>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# tkellem
|
2
|
+
|
3
|
+
tkellem is an IRC bouncer, a proxy that keeps you permanently logged on to an
|
4
|
+
IRC server and stores all messages so that when your client next connects, you
|
5
|
+
can see the backlog of what happened while you were gone.
|
6
|
+
|
7
|
+
tkellem supports multiple device-independent backlogs, and connecting to
|
8
|
+
multiple IRC servers all from the same process.
|
9
|
+
|
10
|
+
## IMPORTANT
|
11
|
+
|
12
|
+
This is a very, very early version. Only the basic functionality is implemented.
|
13
|
+
You probably don't want this yet.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
bin/tkellem examples/config.yml
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rspec'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
9
|
+
spec.pattern = 'spec/spec_helper.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gem|
|
15
|
+
gem.name = 'tkellem'
|
16
|
+
gem.summary = 'IRC bouncer with multi-client support'
|
17
|
+
gem.email = 'brian@codekitchen.net'
|
18
|
+
gem.homepage = 'http://github.com/codekitchen/tkellem'
|
19
|
+
gem.authors = ['Brian Palmer']
|
20
|
+
gem.add_dependency 'eventmachine'
|
21
|
+
gem.executables = %w(tkellem)
|
22
|
+
end
|
23
|
+
|
24
|
+
Jeweler::GemcutterTasks.new
|
25
|
+
rescue LoadError
|
26
|
+
# om nom nom
|
27
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.7.0
|
data/bin/tkellem
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rubygems'
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
require 'yaml'
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
opts = OptionParser.new
|
11
|
+
opts.banner = <<BANNER
|
12
|
+
Usage: #{opts.program_name} PATH_TO_CONFIG
|
13
|
+
|
14
|
+
Start a tkellem instance using the specified configuration file (or ~/.tkellem/config.yml if none given)
|
15
|
+
BANNER
|
16
|
+
|
17
|
+
opts.on_tail("-h", "--help") { puts opts; exit }
|
18
|
+
|
19
|
+
rest = opts.parse(ARGV)
|
20
|
+
config_filename = rest.first || File.expand_path(ENV["HOME"]+'/.tkellem/config.yml')
|
21
|
+
|
22
|
+
config = YAML.load_file(config_filename)
|
23
|
+
|
24
|
+
$LOAD_PATH.push(File.expand_path(File.dirname(__FILE__)+'/../lib'))
|
25
|
+
require 'tkellem'
|
26
|
+
|
27
|
+
EM.run do
|
28
|
+
bouncer =
|
29
|
+
Tkellem::Bouncer.new(config['listen'] || '0.0.0.0',
|
30
|
+
config['port'] || 10001,
|
31
|
+
config['ssl'])
|
32
|
+
|
33
|
+
bouncer.max_backlog = config['max_backlog'].to_i
|
34
|
+
|
35
|
+
bouncer.on_authenticate do |username, password, irc_server|
|
36
|
+
server_config = config['connections'][irc_server.name]
|
37
|
+
if server_config && password_sha1 = server_config['password_sha1']
|
38
|
+
require 'openssl'
|
39
|
+
password_sha1 == OpenSSL::Digest::SHA1.hexdigest(password)
|
40
|
+
else
|
41
|
+
true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_irc_server(bouncer, name, conn)
|
46
|
+
server = bouncer.add_irc_server(name,
|
47
|
+
conn['host'], conn['port'], conn['ssl'],
|
48
|
+
conn['nick'])
|
49
|
+
|
50
|
+
(conn['rooms'] || []).each { |room| server.join_room(room['name']) }
|
51
|
+
conn['clients'].each { |client| server.add_client(client['name']) }
|
52
|
+
end
|
53
|
+
|
54
|
+
config['connections'].each do |name, conn|
|
55
|
+
Tkellem::EasyLogger.logger.info("adding new connection #{name}")
|
56
|
+
add_irc_server(bouncer, name, conn)
|
57
|
+
end
|
58
|
+
|
59
|
+
Signal.trap('HUP') do
|
60
|
+
Tkellem::EasyLogger.logger.warn("got HUP, reloading #{config_filename}")
|
61
|
+
new_config = YAML.load_file(config_filename)
|
62
|
+
|
63
|
+
bouncer.max_backlog = new_config['max_backlog'].to_i
|
64
|
+
|
65
|
+
# find changed connections
|
66
|
+
to_delete = config['connections'].keys
|
67
|
+
|
68
|
+
new_config['connections'].each do |name, new_conn|
|
69
|
+
to_delete.delete(name)
|
70
|
+
conn = config['connections'][name]
|
71
|
+
if !conn
|
72
|
+
Tkellem::EasyLogger.logger.info("adding new connection #{name}")
|
73
|
+
add_irc_server(bouncer, name, new_conn)
|
74
|
+
else
|
75
|
+
if new_conn['host'] != conn['host'] || new_conn['port'] != conn['port'] || new_conn['ssl'] != conn['ssl']
|
76
|
+
Tkellem::EasyLogger.logger.info("server settings changed for #{name}, dropping clients and reconnecting")
|
77
|
+
bouncer.remove_irc_server(name)
|
78
|
+
add_irc_server(bouncer, name, new_conn)
|
79
|
+
elsif conn['clients'] != new_conn['clients']
|
80
|
+
irc_server = bouncer.get_irc_server(name)
|
81
|
+
|
82
|
+
# we don't have to reconnect, but maybe clients changed. we ignore
|
83
|
+
# changes to nick and rooms on HUP, since those are dynamic once tkellem
|
84
|
+
# connects. password change will be caught on the next client
|
85
|
+
# connect.
|
86
|
+
clients_to_delete = conn['clients'].map { |c| c['name'] }
|
87
|
+
new_conn['clients'].each do |new_client|
|
88
|
+
clients_to_delete.delete(new_client['name'])
|
89
|
+
# add -- if already exists, this is a no-op. this may change in the
|
90
|
+
# future though...
|
91
|
+
Tkellem::EasyLogger.logger.info("adding client #{new_client['name']} to server #{name}")
|
92
|
+
irc_server.add_client(new_client['name'])
|
93
|
+
end
|
94
|
+
clients_to_delete.each do |client_name|
|
95
|
+
Tkellem::EasyLogger.logger.info("removing client #{client_name} from server #{name}")
|
96
|
+
irc_server.remove_client(client_name) if irc_server
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
to_delete.each do |name|
|
103
|
+
Tkellem::EasyLogger.logger.info("deleting connection #{name}")
|
104
|
+
bouncer.remove_irc_server(name)
|
105
|
+
end
|
106
|
+
|
107
|
+
config = new_config
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
data/examples/config.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# $ bin/tkellem examples/config.yml
|
2
|
+
#
|
3
|
+
# to connect: connect to localhost port 10001, ssl enabled, "real name" set to
|
4
|
+
#
|
5
|
+
# <connection_name> <client_name>
|
6
|
+
#
|
7
|
+
# e.g.:
|
8
|
+
#
|
9
|
+
# freenode laptop
|
10
|
+
---
|
11
|
+
listen: 0.0.0.0
|
12
|
+
port: 10001
|
13
|
+
ssl: true
|
14
|
+
# max_backlog: 500
|
15
|
+
connections:
|
16
|
+
freenode:
|
17
|
+
host: irc.freenode.org
|
18
|
+
port: 6667
|
19
|
+
ssl: false
|
20
|
+
nick: tkellem_r0ck
|
21
|
+
# Uncomment to enable password auth (there's no auth by default, which
|
22
|
+
# means anybody can connect). You can generate a password_sha1 like so:
|
23
|
+
# echo -n 'tkellem_r0ck' | openssl sha1
|
24
|
+
# password_sha1: a4f4a3b97c7b8a028d4e3f3fee85d6e5626baba5
|
25
|
+
rooms:
|
26
|
+
- name: "#tkellem_test"
|
27
|
+
clients:
|
28
|
+
- name: laptop
|
29
|
+
- name: iphone
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'tkellem/irc_line'
|
2
|
+
|
3
|
+
module Tkellem
|
4
|
+
|
5
|
+
# Normally there will be one client per backlog, but there can be more than one
|
6
|
+
# connection for the same backlog, if two or more IRC clients connect with the
|
7
|
+
# same client name. That situation is equivalent to how most multi-connection
|
8
|
+
# bouncers like bip work.
|
9
|
+
|
10
|
+
class Backlog
|
11
|
+
|
12
|
+
class BacklogLine < Struct.new(:irc_line, :time)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(name, max_backlog = nil)
|
16
|
+
@name = name
|
17
|
+
@backlog = []
|
18
|
+
@pm_backlogs = Hash.new { |h,k| h[k] = [] }
|
19
|
+
@active_conns = []
|
20
|
+
@max_backlog = max_backlog
|
21
|
+
end
|
22
|
+
attr_reader :name, :backlog, :active_conns, :pm_backlogs, :max_backlog
|
23
|
+
|
24
|
+
def handle_message(msg)
|
25
|
+
# TODO: only send back response messages like WHO, NAMES, etc. to the
|
26
|
+
# BouncerConnection that requested it.
|
27
|
+
if !active_conns.empty?
|
28
|
+
case msg.command
|
29
|
+
when /3\d\d/, /join/i, /part/i
|
30
|
+
# transient response -- we want to forward these, but not backlog
|
31
|
+
active_conns.each { |conn| conn.transient_response(msg) }
|
32
|
+
when /privmsg/i
|
33
|
+
active_conns.each { |conn| conn.send_msg(msg) }
|
34
|
+
else
|
35
|
+
# do nothing?
|
36
|
+
end
|
37
|
+
elsif msg.command.match(/privmsg/i)
|
38
|
+
if msg.args.first.match(/^#/)
|
39
|
+
# room privmsg always goes in a specific backlog
|
40
|
+
pm_target = msg.args.first
|
41
|
+
bl = pm_backlogs[pm_target]
|
42
|
+
else
|
43
|
+
# other messages go in the general backlog
|
44
|
+
bl = backlog
|
45
|
+
end
|
46
|
+
bl.push(BacklogLine.new(msg, Time.now))
|
47
|
+
limit_backlog(bl)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def limit_backlog(bl)
|
52
|
+
bl.shift until !max_backlog || bl.size <= max_backlog
|
53
|
+
end
|
54
|
+
|
55
|
+
def max_backlog=(new_val)
|
56
|
+
@max_backlog = new_val
|
57
|
+
limit_backlog(backlog)
|
58
|
+
pm_backlogs.each { |k,bl| limit_backlog(bl) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_conn(bouncer_conn)
|
62
|
+
active_conns << bouncer_conn
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove_conn(bouncer_conn)
|
66
|
+
active_conns.delete(bouncer_conn)
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_backlog(conn, pm_target = nil)
|
70
|
+
if pm_target
|
71
|
+
# send room-specific backlog
|
72
|
+
msgs = pm_backlogs.key?(pm_target) ? pm_backlogs[pm_target] : []
|
73
|
+
else
|
74
|
+
# send the general backlog
|
75
|
+
msgs = backlog
|
76
|
+
end
|
77
|
+
|
78
|
+
until msgs.empty?
|
79
|
+
backlog_line = msgs.shift
|
80
|
+
conn.send_msg(backlog_line.irc_line.with_timestamp(backlog_line.time))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'tkellem/irc_server'
|
4
|
+
|
5
|
+
module Tkellem
|
6
|
+
class Bouncer
|
7
|
+
def initialize(listen_address, port, do_ssl)
|
8
|
+
@irc_servers = {}
|
9
|
+
@max_backlog = nil
|
10
|
+
@server = EM.start_server(listen_address, port, BouncerConnection,
|
11
|
+
self, do_ssl)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_irc_server(name, host, port, do_ssl, nick)
|
15
|
+
server = EM.connect(host, port, IrcServer, self, name, do_ssl, nick)
|
16
|
+
@irc_servers[name] = server
|
17
|
+
server.set_max_backlog(@max_backlog) if @max_backlog
|
18
|
+
server
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove_irc_server(name)
|
22
|
+
server = @irc_servers.delete(name)
|
23
|
+
if server
|
24
|
+
server.close_connection(true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_authenticate(&block)
|
29
|
+
@auth_block = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_backlog=(max_backlog)
|
33
|
+
@max_backlog = max_backlog && max_backlog > 0 ? max_backlog : nil
|
34
|
+
@irc_servers.each { |name, server| server.set_max_backlog(@max_backlog) }
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Internal API
|
39
|
+
|
40
|
+
def get_irc_server(name) #:nodoc:
|
41
|
+
@irc_servers[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
def do_auth(username, password, irc_server) #:nodoc:
|
45
|
+
if @auth_block
|
46
|
+
@auth_block.call(username, password.to_s, irc_server)
|
47
|
+
else
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'tkellem/irc_line'
|
3
|
+
require 'tkellem/backlog'
|
4
|
+
|
5
|
+
module Tkellem
|
6
|
+
|
7
|
+
module BouncerConnection
|
8
|
+
include EM::Protocols::LineText2
|
9
|
+
include Tkellem::EasyLogger
|
10
|
+
|
11
|
+
def initialize(bouncer, do_ssl)
|
12
|
+
set_delimiter "\r\n"
|
13
|
+
|
14
|
+
@ssl = do_ssl
|
15
|
+
@bouncer = bouncer
|
16
|
+
|
17
|
+
@irc_server = nil
|
18
|
+
@backlog = nil
|
19
|
+
@nick = nil
|
20
|
+
@conn_name = nil
|
21
|
+
@name = nil
|
22
|
+
end
|
23
|
+
attr_reader :ssl, :irc_server, :backlog, :bouncer, :nick
|
24
|
+
|
25
|
+
def connected?
|
26
|
+
!!irc_server
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
@name || "new-conn"
|
31
|
+
end
|
32
|
+
|
33
|
+
def post_init
|
34
|
+
if ssl
|
35
|
+
debug "starting TLS"
|
36
|
+
start_tls :verify_peer => false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ssl_handshake_completed
|
41
|
+
debug "TLS complete"
|
42
|
+
end
|
43
|
+
|
44
|
+
def error!(msg)
|
45
|
+
info("ERROR :#{msg}")
|
46
|
+
send_msg("ERROR :#{msg}")
|
47
|
+
close_connection(true)
|
48
|
+
end
|
49
|
+
|
50
|
+
def connect(conn_name, client_name, password)
|
51
|
+
@irc_server = bouncer.get_irc_server(conn_name.downcase)
|
52
|
+
unless irc_server && irc_server.connected?
|
53
|
+
error!("unknown connection #{conn_name}")
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
unless bouncer.do_auth(conn_name, @password, irc_server)
|
58
|
+
error!("bad auth, please check your password")
|
59
|
+
@irc_server = @conn_name = @name = @backlog = nil
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
@conn_name = conn_name
|
64
|
+
@name = client_name
|
65
|
+
@backlog = irc_server.bouncer_connect(self)
|
66
|
+
unless backlog
|
67
|
+
error!("unknown client #{client_name}")
|
68
|
+
@irc_server = @conn_name = @name = nil
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
info "connected"
|
73
|
+
|
74
|
+
irc_server.send_welcome(self)
|
75
|
+
backlog.send_backlog(self)
|
76
|
+
irc_server.rooms.each { |room| simulate_join(room) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def tkellem(msg)
|
80
|
+
case msg.args.first
|
81
|
+
when /nothing_yet/i
|
82
|
+
else
|
83
|
+
send_msg(":tkellem!tkellem@tkellem PRIVMSG #{nick} :Unknown tkellem command #{msg.args.first}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive_line(line)
|
88
|
+
trace "from client: #{line}"
|
89
|
+
msg = IrcLine.parse(line)
|
90
|
+
case msg.command
|
91
|
+
when /tkellem/i
|
92
|
+
tkellem(msg)
|
93
|
+
when /pass/i
|
94
|
+
@password = msg.args.first
|
95
|
+
when /user/i
|
96
|
+
conn_name, client_name = msg.args.last.strip.split(' ')
|
97
|
+
connect(conn_name, client_name, @password)
|
98
|
+
when /nick/i
|
99
|
+
if connected?
|
100
|
+
irc_server.change_nick(msg.last)
|
101
|
+
else
|
102
|
+
@nick = msg.last
|
103
|
+
end
|
104
|
+
when /quit/i
|
105
|
+
# DENIED
|
106
|
+
close_connection
|
107
|
+
when /ping/i
|
108
|
+
send_msg(":tkellem PONG tkellem :#{msg.last}")
|
109
|
+
else
|
110
|
+
if !connected?
|
111
|
+
close_connection
|
112
|
+
else
|
113
|
+
# pay it forward
|
114
|
+
irc_server.send_msg(msg)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def simulate_join(room)
|
120
|
+
send_msg(":#{irc_server.nick}!#{name}@tkellem JOIN #{room}")
|
121
|
+
# TODO: intercept the NAMES response so that only this bouncer gets it
|
122
|
+
# Otherwise other clients might show an "in this room" line.
|
123
|
+
irc_server.send_msg("NAMES #{room}\r\n")
|
124
|
+
end
|
125
|
+
|
126
|
+
def transient_response(msg)
|
127
|
+
send_msg(msg)
|
128
|
+
if msg.command == "366"
|
129
|
+
# finished joining this room, let's backlog it
|
130
|
+
debug "got final NAMES for #{msg.args[1]}, sending backlog"
|
131
|
+
backlog.send_backlog(self, msg.args[1])
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def send_msg(msg)
|
136
|
+
trace "to client: #{msg}"
|
137
|
+
send_data("#{msg}\r\n")
|
138
|
+
end
|
139
|
+
|
140
|
+
def log_name
|
141
|
+
"#{@conn_name}-#{name}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def unbind
|
145
|
+
irc_server.bouncer_disconnect(self) if connected?
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Tkellem
|
2
|
+
|
3
|
+
class IrcLine
|
4
|
+
RE = %r{(:[^ ]+ )?([^ ]*)(.*)}i
|
5
|
+
|
6
|
+
def self.parse(line)
|
7
|
+
md = RE.match(line) or raise("invalid input: #{line.inspect}")
|
8
|
+
|
9
|
+
self.new(line, md[1], md[2], md[3])
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :prefix, :command, :args
|
13
|
+
|
14
|
+
def initialize(orig, prefix, command, args)
|
15
|
+
@orig = orig
|
16
|
+
@prefix = prefix ? prefix.strip : nil
|
17
|
+
@command = command
|
18
|
+
|
19
|
+
args.strip!
|
20
|
+
idx = args.index(":")
|
21
|
+
if idx
|
22
|
+
@args = args[0...idx].split(' ') + [args[idx+1..-1]]
|
23
|
+
else
|
24
|
+
@args = args.split(' ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def command?(cmd)
|
29
|
+
@command.downcase == cmd.downcase
|
30
|
+
end
|
31
|
+
|
32
|
+
def replay
|
33
|
+
@orig
|
34
|
+
end
|
35
|
+
alias_method :to_s, :replay
|
36
|
+
|
37
|
+
def last
|
38
|
+
args.last
|
39
|
+
end
|
40
|
+
|
41
|
+
def target_user
|
42
|
+
if prefix && md = %r{^:([^!]+)}.match(prefix)
|
43
|
+
md[1]
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_timestamp(timestamp)
|
50
|
+
new_command = [prefix, command]
|
51
|
+
new_command += args[0..-2]
|
52
|
+
new_command.push("#{timestamp.strftime("%H:%M:%S")}> #{args.last}")
|
53
|
+
new_command.join(' ')
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'tkellem/irc_line'
|
4
|
+
require 'tkellem/bouncer_connection'
|
5
|
+
require 'tkellem/backlog'
|
6
|
+
|
7
|
+
module Tkellem
|
8
|
+
|
9
|
+
module IrcServer
|
10
|
+
include EM::Protocols::LineText2
|
11
|
+
include Tkellem::EasyLogger
|
12
|
+
|
13
|
+
def initialize(bouncer, name, do_ssl, nick)
|
14
|
+
set_delimiter "\r\n"
|
15
|
+
|
16
|
+
@bouncer = bouncer
|
17
|
+
@name = name
|
18
|
+
@ssl = do_ssl
|
19
|
+
@nick = nick
|
20
|
+
|
21
|
+
@max_backlog = nil
|
22
|
+
@connected = false
|
23
|
+
@welcomes = []
|
24
|
+
@rooms = Set.new
|
25
|
+
@backlogs = {}
|
26
|
+
@active_conns = []
|
27
|
+
@joined_rooms = false
|
28
|
+
@pending_rooms = []
|
29
|
+
end
|
30
|
+
attr_reader :name, :backlogs, :welcomes, :rooms, :nick, :active_conns
|
31
|
+
alias_method :log_name, :name
|
32
|
+
|
33
|
+
def connected?
|
34
|
+
@connected
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_max_backlog(max_backlog)
|
38
|
+
@max_backlog = max_backlog
|
39
|
+
backlogs.each { |name, backlog| backlog.max_backlog = max_backlog }
|
40
|
+
end
|
41
|
+
|
42
|
+
def post_init
|
43
|
+
if @ssl
|
44
|
+
debug "starting TLS"
|
45
|
+
# TODO: support strict cert checks
|
46
|
+
start_tls :verify_peer => false
|
47
|
+
else
|
48
|
+
ssl_handshake_completed
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def ssl_handshake_completed
|
53
|
+
# TODO: support sending a real username, realname, etc
|
54
|
+
send_msg("USER #{nick} localhost blah :#{nick}")
|
55
|
+
change_nick(nick, true)
|
56
|
+
end
|
57
|
+
|
58
|
+
def receive_line(line)
|
59
|
+
trace "from server: #{line}"
|
60
|
+
msg = IrcLine.parse(line)
|
61
|
+
|
62
|
+
case msg.command
|
63
|
+
when /0\d\d/, /2[56]\d/, /37[256]/
|
64
|
+
welcomes << msg
|
65
|
+
got_welcome if msg.command == "376" # end of MOTD
|
66
|
+
when /join/i
|
67
|
+
debug "#{msg.target_user} joined #{msg.last}"
|
68
|
+
rooms << msg.last if msg.target_user == nick
|
69
|
+
when /part/i
|
70
|
+
debug "#{msg.target_user} left #{msg.last}"
|
71
|
+
rooms.delete(msg.last) if msg.target_user == nick
|
72
|
+
when /ping/i
|
73
|
+
send_msg("PONG #{nick}!tkellem #{msg.args.first}")
|
74
|
+
when /pong/i
|
75
|
+
# swallow it, we handle ping-pong from clients separately, in
|
76
|
+
# BouncerConnection
|
77
|
+
else
|
78
|
+
end
|
79
|
+
|
80
|
+
backlogs.each { |name, backlog| backlog.handle_message(msg) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def got_welcome
|
84
|
+
return if @joined_rooms
|
85
|
+
@joined_rooms = true
|
86
|
+
@pending_rooms.each do |room|
|
87
|
+
join_room(room)
|
88
|
+
end
|
89
|
+
@pending_rooms.clear
|
90
|
+
|
91
|
+
# We're all initialized, allow connections
|
92
|
+
@connected = true
|
93
|
+
end
|
94
|
+
|
95
|
+
def change_nick(new_nick, force = false)
|
96
|
+
return if !force && new_nick == @nick
|
97
|
+
@nick = new_nick
|
98
|
+
send_msg("NICK #{new_nick}")
|
99
|
+
end
|
100
|
+
|
101
|
+
def join_room(room_name)
|
102
|
+
if @joined_rooms
|
103
|
+
send_msg("JOIN #{room_name}")
|
104
|
+
else
|
105
|
+
@pending_rooms << room_name
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_client(name)
|
110
|
+
return if backlogs[name]
|
111
|
+
backlog = Backlog.new(name, @max_backlog)
|
112
|
+
backlogs[name] = backlog
|
113
|
+
end
|
114
|
+
|
115
|
+
def remove_client(name)
|
116
|
+
backlog = backlogs.delete(name)
|
117
|
+
if backlog
|
118
|
+
backlog.active_conns.each do |conn|
|
119
|
+
conn.error!("client removed")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def send_msg(msg)
|
125
|
+
trace "to server: #{msg}"
|
126
|
+
send_data("#{msg}\r\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
def send_welcome(bouncer_conn)
|
130
|
+
welcomes.each { |msg| bouncer_conn.send_msg(msg) }
|
131
|
+
end
|
132
|
+
|
133
|
+
def unbind
|
134
|
+
debug "OMG we got disconnected."
|
135
|
+
# TODO: reconnect if desired. but not if this server was explicitly shut
|
136
|
+
# down or removed.
|
137
|
+
backlogs.keys.each { |name| remove_client(name) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def bouncer_connect(bouncer_conn)
|
141
|
+
return nil unless backlogs[bouncer_conn.name]
|
142
|
+
|
143
|
+
active_conns << bouncer_conn
|
144
|
+
backlogs[bouncer_conn.name].add_conn(bouncer_conn)
|
145
|
+
backlogs[bouncer_conn.name]
|
146
|
+
end
|
147
|
+
|
148
|
+
def bouncer_disconnect(bouncer_conn)
|
149
|
+
return nil unless backlogs[bouncer_conn.name]
|
150
|
+
|
151
|
+
backlogs[bouncer_conn.name].remove_conn(bouncer_conn)
|
152
|
+
active_conns.delete(bouncer_conn)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
data/lib/tkellem.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
pathname = File.expand_path(File.dirname(__FILE__))
|
7
|
+
$LOAD_PATH.push(pathname) unless $LOAD_PATH.include?(pathname)
|
8
|
+
|
9
|
+
module Tkellem
|
10
|
+
module EasyLogger
|
11
|
+
require 'logger'
|
12
|
+
|
13
|
+
def self.logger=(new_logger)
|
14
|
+
@logger = new_logger
|
15
|
+
end
|
16
|
+
def self.logger
|
17
|
+
return @logger if @logger
|
18
|
+
@logger = Logger.new(STDERR)
|
19
|
+
@logger.datetime_format = "%Y-%m-%d"
|
20
|
+
@logger
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.trace=(val)
|
24
|
+
@trace = val
|
25
|
+
end
|
26
|
+
def self.trace
|
27
|
+
@trace || @trace = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def trace(msg)
|
31
|
+
puts("TRACE: #{log_name}: #{msg}") if EasyLogger.trace
|
32
|
+
end
|
33
|
+
|
34
|
+
::Logger::Severity.constants.each do |level|
|
35
|
+
next if level == "UNKNOWN"
|
36
|
+
module_eval(<<-EVAL, __FILE__, __LINE__)
|
37
|
+
def #{level.downcase}(msg)
|
38
|
+
EasyLogger.logger.#{level.downcase}("\#{log_name} (\#{object_id}): \#{msg}")
|
39
|
+
end
|
40
|
+
EVAL
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require 'tkellem/bouncer'
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tkellem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 7
|
8
|
+
- 0
|
9
|
+
version: 0.7.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Brian Palmer
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-17 00:00:00 -07:00
|
18
|
+
default_executable: tkellem
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: eventmachine
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description:
|
34
|
+
email: brian@codekitchen.net
|
35
|
+
executables:
|
36
|
+
- tkellem
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files:
|
40
|
+
- LICENSE
|
41
|
+
- README.md
|
42
|
+
files:
|
43
|
+
- LICENSE
|
44
|
+
- README.md
|
45
|
+
- Rakefile
|
46
|
+
- VERSION
|
47
|
+
- bin/tkellem
|
48
|
+
- examples/config.yml
|
49
|
+
- lib/tkellem.rb
|
50
|
+
- lib/tkellem/backlog.rb
|
51
|
+
- lib/tkellem/bouncer.rb
|
52
|
+
- lib/tkellem/bouncer_connection.rb
|
53
|
+
- lib/tkellem/irc_line.rb
|
54
|
+
- lib/tkellem/irc_server.rb
|
55
|
+
- spec/spec_helper.rb
|
56
|
+
has_rdoc: true
|
57
|
+
homepage: http://github.com/codekitchen/tkellem
|
58
|
+
licenses: []
|
59
|
+
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
requirements: []
|
82
|
+
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.3.7
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: IRC bouncer with multi-client support
|
88
|
+
test_files:
|
89
|
+
- spec/spec_helper.rb
|