tkellem 0.7.1 → 0.8.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.
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