tkellem 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +28 -6
  4. data/Rakefile +14 -5
  5. data/bin/tkellem +2 -107
  6. data/debian/changelog +5 -0
  7. data/debian/compat +1 -0
  8. data/debian/control +18 -0
  9. data/debian/copyright +42 -0
  10. data/debian/docs +1 -0
  11. data/debian/examples +1 -0
  12. data/debian/install +3 -0
  13. data/debian/manpages +1 -0
  14. data/debian/rules +13 -0
  15. data/debian/source/format +1 -0
  16. data/debian/tkellem.1 +11 -0
  17. data/examples/config.yml +2 -29
  18. data/lib/tkellem/bouncer.rb +196 -31
  19. data/lib/tkellem/bouncer_connection.rb +90 -85
  20. data/lib/tkellem/daemon.rb +155 -0
  21. data/lib/tkellem/irc_message.rb +58 -0
  22. data/lib/tkellem/irc_server.rb +8 -121
  23. data/lib/tkellem/migrations/001_init_db.rb +33 -0
  24. data/lib/tkellem/models/host.rb +11 -0
  25. data/lib/tkellem/models/listen_address.rb +12 -0
  26. data/lib/tkellem/models/network.rb +15 -0
  27. data/lib/tkellem/models/network_user.rb +12 -0
  28. data/lib/tkellem/models/user.rb +56 -0
  29. data/lib/tkellem/plugins/backlog.rb +160 -0
  30. data/lib/tkellem/plugins/push_service.rb +152 -0
  31. data/lib/tkellem/socket_server.rb +29 -0
  32. data/lib/tkellem/tkellem_bot.rb +318 -0
  33. data/lib/tkellem/tkellem_server.rb +114 -0
  34. data/lib/tkellem/version.rb +3 -0
  35. data/lib/tkellem.rb +7 -10
  36. data/resources/bot_command_descriptions.yml +36 -0
  37. data/spec/irc_message_spec.rb +47 -0
  38. data/spec/irc_server_spec.rb +60 -0
  39. data/spec/spec_helper.rb +0 -2
  40. data/tkellem.gemspec +20 -47
  41. metadata +118 -22
  42. data/VERSION +0 -1
  43. data/lib/tkellem/backlog.rb +0 -85
  44. data/lib/tkellem/irc_line.rb +0 -58
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ coverage/*
6
+ doc/*
7
+ .yardoc/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ruby-protocol-buffers.gemspec
4
+ gemspec
data/README.md CHANGED
@@ -4,14 +4,36 @@ tkellem is an IRC bouncer, a proxy that keeps you permanently logged on to an
4
4
  IRC server and stores all messages so that when your client next connects, you
5
5
  can see the backlog of what happened while you were gone.
6
6
 
7
- tkellem supports multiple device-independent backlogs, and connecting to
8
- multiple IRC servers all from the same process.
7
+ tkellem supports multiple users, multiple device-independent backlogs
8
+ per-user, and connecting to multiple IRC servers all from the same
9
+ process.
9
10
 
10
11
  ## IMPORTANT
11
12
 
12
- This is a very, very early version. Only the basic functionality is implemented.
13
- You probably don't want this yet.
13
+ This is still a pretty early alpha. Expect bugs and missing
14
+ functionality.
14
15
 
15
- ## Usage
16
+ ## Getting Started
16
17
 
17
- bin/tkellem examples/config.yml
18
+ This will have to do as a quickstart guide, for now:
19
+
20
+ $ git clone git://github.com/codekitchen/tkellem.git
21
+ $ cd tkellem
22
+ $ bundle install
23
+ $ bundle exec bin/tkellem start
24
+ $ bundle exec bin/tkellem admin
25
+ > help
26
+ > listen ircs://0.0.0.0:8765
27
+ > user --add <my-name> --admin
28
+ > password --user <my-name> <my-new-password>
29
+ > network --add --public freenode ircs://irc.freenode.org:7000
30
+
31
+ Then connect to tkellem with an irc client:
32
+
33
+ server: localhost
34
+ port: 8765
35
+ ssl: yes
36
+
37
+ nickname: <my-name>
38
+ login: <my-name>@freenode
39
+ server password: <my-new-password>
data/Rakefile CHANGED
@@ -1,12 +1,21 @@
1
- require 'rubygems'
2
- require 'rake'
3
- require 'rspec'
4
- require 'rspec/core/rake_task'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
5
3
 
6
4
  task :default => :spec
7
5
 
6
+ require 'rspec/core/rake_task'
8
7
  RSpec::Core::RakeTask.new(:spec) do |spec|
9
- spec.pattern = 'spec/spec_helper.rb'
8
+ spec.pattern = 'spec/*_spec.rb'
9
+ end
10
+ RSpec::Core::RakeTask.new(:rcov) do |t|
11
+ t.rcov = true
12
+ t.rcov_opts = ["--exclude", "spec,gems/,rubygems/"]
13
+ end
14
+
15
+ require 'yard'
16
+ YARD::Rake::YardocTask.new(:doc) do |t|
17
+ version = Tkellem::VERSION
18
+ t.options = ["--title", "tkellem #{version}", "--files", "LICENSE,README.md"]
10
19
  end
11
20
 
12
21
  begin
data/bin/tkellem CHANGED
@@ -1,110 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- begin
4
- require 'rubygems'
5
- rescue LoadError
6
- end
7
- require 'yaml'
8
- require 'optparse'
3
+ require 'tkellem/daemon'
9
4
 
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
5
+ Tkellem::Daemon.new(ARGV).run()
data/debian/changelog ADDED
@@ -0,0 +1,5 @@
1
+ tkellem (0.7.1-1) unstable; urgency=low
2
+
3
+ * Initial packaging
4
+
5
+ -- paul cannon <pik@debian.org> Thu, 16 Dec 2010 11:58:18 -0600
data/debian/compat ADDED
@@ -0,0 +1 @@
1
+ 7
data/debian/control ADDED
@@ -0,0 +1,18 @@
1
+ Source: tkellem
2
+ Section: net
3
+ Priority: extra
4
+ Maintainer: paul cannon <pik@debian.org>
5
+ Build-Depends: debhelper (>= 7.0.50~)
6
+ Standards-Version: 3.9.1
7
+ Homepage: https://github.com/codekitchen/tkellem
8
+
9
+ Package: tkellem
10
+ Architecture: all
11
+ Depends: ${misc:Depends}, ruby, rubygems, libeventmachine-ruby
12
+ Description: IRC bouncer with multi-client support
13
+ tkellem is an IRC bouncer, a proxy that keeps you permanently logged on to an
14
+ IRC server and stores all messages so that when your client next connects, you
15
+ can see the backlog of what happened while you were gone.
16
+ .
17
+ tkellem supports multiple device-independent backlogs, and connecting to
18
+ multiple IRC servers all from the same process.
data/debian/copyright ADDED
@@ -0,0 +1,42 @@
1
+ This work was packaged for Debian by:
2
+
3
+ paul cannon <pik@debian.org> on Thu, 16 Dec 2010 11:58:18 -0600
4
+
5
+ It was downloaded from:
6
+
7
+ https://github.com/codekitchen/tkellem
8
+
9
+ Upstream Author(s):
10
+
11
+ Brian Palmer
12
+
13
+ Copyright:
14
+
15
+ Copyright (c) 2010 Brian Palmer <brian@codekitchen.net>
16
+
17
+ License:
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a
20
+ copy of this software and associated documentation files (the "Software"),
21
+ to deal in the Software without restriction, including without limitation
22
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
23
+ and/or sell copies of the Software, and to permit persons to whom the
24
+ Software is furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
35
+ DEALINGS IN THE SOFTWARE.
36
+
37
+ The Debian packaging is:
38
+
39
+ Copyright (C) 2010 paul cannon <pik@debian.org>
40
+
41
+ and may be distributed and used under the same terms as the underlying work
42
+ (see above).
data/debian/docs ADDED
@@ -0,0 +1 @@
1
+ README.md
data/debian/examples ADDED
@@ -0,0 +1 @@
1
+ examples/config.yml
data/debian/install ADDED
@@ -0,0 +1,3 @@
1
+ bin/tkellem /usr/bin
2
+ lib/tkellem.rb /usr/lib/ruby/1.8
3
+ lib/tkellem/*.rb /usr/lib/ruby/1.8/tkellem
data/debian/manpages ADDED
@@ -0,0 +1 @@
1
+ debian/tkellem.1
data/debian/rules ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/make -f
2
+ # -*- makefile -*-
3
+ # Sample debian/rules that uses debhelper.
4
+ # This file was originally written by Joey Hess and Craig Small.
5
+ # As a special exception, when this file is copied by dh-make into a
6
+ # dh-make output file, you may use that output file without restriction.
7
+ # This special exception was added by Craig Small in version 0.37 of dh-make.
8
+
9
+ # Uncomment this to turn on verbose mode.
10
+ #export DH_VERBOSE=1
11
+
12
+ %:
13
+ dh $@
@@ -0,0 +1 @@
1
+ 3.0 (quilt)
data/debian/tkellem.1 ADDED
@@ -0,0 +1,11 @@
1
+ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.38.2.
2
+ .TH TKELLEM "1" "December 2010" "Brian Palmer" "User Commands"
3
+ .SH NAME
4
+ tkellem \- tkellem
5
+ .SH SYNOPSIS
6
+ .B tkellem
7
+ \fIPATH_TO_CONFIG\fR
8
+ .SH DESCRIPTION
9
+ Start a tkellem instance using the specified configuration file (or ~/.tkellem/config.yml if none given)
10
+ .HP
11
+ \fB\-h\fR, \fB\-\-help\fR
data/examples/config.yml CHANGED
@@ -1,29 +1,2 @@
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
1
+ # this file is deprecated, settings are now stored in ~/.tkellem/tkellem.sqlite3
2
+ # more help to come
@@ -1,52 +1,217 @@
1
- require 'eventmachine'
1
+ require 'active_support/core_ext/class/attribute_accessors'
2
2
 
3
3
  require 'tkellem/irc_server'
4
+ require 'tkellem/bouncer_connection'
4
5
 
5
6
  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)
7
+
8
+ class Bouncer
9
+ include Tkellem::EasyLogger
10
+
11
+ attr_reader :user, :network, :nick
12
+ cattr_accessor :plugins
13
+ self.plugins = []
14
+
15
+ def initialize(network_user)
16
+ @network_user = network_user
17
+ @user = network_user.user
18
+ @network = network_user.network
19
+
20
+ @nick = network_user.nick
21
+ # maps { client_conn => state_hash }
22
+ @active_conns = {}
23
+ @welcomes = []
24
+ @rooms = ['#tk']
25
+ # maps { client_conn => away_status_or_nil }
26
+ @away = {}
27
+ # plugin data
28
+ @data = {}
29
+ # clients waiting for us to connect to the irc server
30
+ @waiting_clients = []
31
+
32
+ connect!
33
+ end
34
+
35
+ def data(key)
36
+ @data[key] ||= {}
37
+ end
38
+
39
+ def active_conns
40
+ @active_conns.keys
41
+ end
42
+
43
+ def self.add_plugin(plugin)
44
+ self.plugins << plugin
45
+ end
46
+
47
+ def connected?
48
+ !!@connected
49
+ end
50
+
51
+ def connect_client(client)
52
+ @active_conns[client] = {}
53
+ @away[client] = nil
54
+
55
+ if !connected?
56
+ @waiting_clients << client
57
+ client.say_as_tkellem("Connecting you to the IRC server. Please wait...")
58
+ return
59
+ end
60
+
61
+ send_welcome(client)
62
+ # make the client join all the rooms that we're in
63
+ @rooms.each { |room| client.simulate_join(room) }
64
+
65
+ plugins.each { |plugin| plugin.new_client_connected(self, client) }
66
+ check_away_status
67
+ end
68
+
69
+ def disconnect_client(client)
70
+ @away.delete(client)
71
+ check_away_status
72
+ @active_conns.delete(client)
73
+ end
74
+
75
+ def client_msg(client, msg)
76
+ return if plugins.any? do |plugin|
77
+ !plugin.client_msg(self, client, msg)
78
+ end
79
+
80
+ forward = case msg.command
81
+ when 'PING'
82
+ client.send_msg(":tkellem!tkellem PONG tkellem :#{msg.args.last}")
83
+ false
84
+ when 'AWAY'
85
+ @away[client] = msg.args.last
86
+ check_away_status
87
+ false
88
+ else
89
+ true
12
90
  end
13
91
 
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
92
+ if forward
93
+ # send to server
94
+ send_msg(msg)
19
95
  end
96
+ end
20
97
 
21
- def remove_irc_server(name)
22
- server = @irc_servers.delete(name)
23
- if server
24
- server.close_connection(true)
25
- end
98
+ def server_msg(msg)
99
+ return if plugins.any? do |plugin|
100
+ !plugin.server_msg(self, msg)
26
101
  end
27
102
 
28
- def on_authenticate(&block)
29
- @auth_block = block
103
+ forward = case msg.command
104
+ when /0\d\d/, /2[56]\d/, /37[256]/
105
+ @welcomes << msg
106
+ ready! if msg.command == "376" # end of MOTD
107
+ false
108
+ when 'JOIN'
109
+ debug "#{msg.target_user} joined #{msg.args.last}"
110
+ @rooms << msg.args.last if msg.target_user == @nick
111
+ true
112
+ when 'PART'
113
+ debug "#{msg.target_user} left #{msg.args.last}"
114
+ @rooms.delete(msg.args.last) if msg.target_user == @nick
115
+ true
116
+ when 'PING'
117
+ send_msg("PONG tkellem!tkellem :#{msg.args.last}")
118
+ false
119
+ when 'PONG'
120
+ # swallow it, we handle ping-pong from clients separately, in
121
+ # BouncerConnection
122
+ false
123
+ else
124
+ true
30
125
  end
31
126
 
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) }
127
+ if forward
128
+ # send to clients
129
+ @active_conns.each { |c,s| c.send_msg(msg) }
35
130
  end
131
+ end
36
132
 
133
+ ## Away Statuses
37
134
 
38
- # Internal API
135
+ def check_away_status
136
+ # for now we pretty much randomly pick an away status if multiple are set
137
+ # by clients
138
+ if @away.any? { |k,v| !v }
139
+ # we have a client who isn't away
140
+ send_msg("AWAY")
141
+ else
142
+ message = @away.values.first || "Away"
143
+ send_msg("AWAY :#{message}")
144
+ end
145
+ end
146
+
147
+
148
+ def name
149
+ "#{user.name}-#{network.name}"
150
+ end
151
+ alias_method :log_name, :name
152
+
153
+ def send_msg(msg)
154
+ trace "to server: #{msg}"
155
+ @conn.send_data("#{msg}\r\n")
156
+ end
157
+
158
+ def connection_established
159
+ # TODO: support sending a real username, realname, etc
160
+ send_msg("USER #{@nick} localhost blah :#{@nick}")
161
+ change_nick(@nick, true)
162
+ check_away_status
163
+ end
164
+
165
+ def disconnected!
166
+ debug "OMG we got disconnected."
167
+ @conn = nil
168
+ @connected = false
169
+ @active_conns.each { |c,s| c.unbind }
170
+ connect!
171
+ end
39
172
 
40
- def get_irc_server(name) #:nodoc:
41
- @irc_servers[name]
173
+ protected
174
+
175
+ def change_nick(new_nick, force = false)
176
+ return if !force && new_nick == @nick
177
+ @nick = new_nick
178
+ send_msg("NICK #{new_nick}")
179
+ end
180
+
181
+ def send_welcome(bouncer_conn)
182
+ @welcomes.each { |msg| bouncer_conn.send_msg(msg) }
183
+ end
184
+
185
+ def connect!
186
+ span = @last_connect ? Time.now - @last_connect : 1000
187
+ hosts = @network.hosts(true).map { |h| h }
188
+
189
+ if span < 5 || hosts.length < 1
190
+ EM.add_timer(5) { connect! }
191
+ return
192
+ end
193
+ @last_connect = Time.now
194
+ @cur_host = (@cur_host || 0) % hosts.length
195
+ host = hosts[@cur_host]
196
+ @conn = EM.connect(host.address, host.port, IrcServerConnection, self, host.ssl)
197
+ end
198
+
199
+ def ready!
200
+ return if @joined_rooms
201
+ @joined_rooms = true
202
+ @rooms.each do |room|
203
+ send_msg("JOIN #{room}")
42
204
  end
43
205
 
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
206
+ # We're all initialized, allow connections
207
+ @connected = true
208
+ @waiting_clients.each do |client|
209
+ client.say_as_tkellem("Now connected.")
210
+ connect_client(client)
50
211
  end
212
+ @waiting_clients.clear
51
213
  end
214
+
215
+ end
216
+
52
217
  end