tkellem 0.8.6 → 0.8.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -21,10 +21,10 @@ This will have to do as a quickstart guide, for now:
21
21
  $ tkellem start
22
22
  $ tkellem admin
23
23
  > help
24
- > listen --add ircs://0.0.0.0:8765
25
- > user --add <my-name> --admin
26
- > password --user <my-name> <my-new-password>
27
- > network --add --public freenode ircs://irc.freenode.org:7000
24
+ > listen ircs://0.0.0.0:8765
25
+ > user <my-name> --role=admin
26
+ > password --user=<my-name> <my-new-password>
27
+ > network --public --name=freenode ircs://irc.freenode.org:7000
28
28
 
29
29
  Then connect to tkellem with an irc client:
30
30
 
@@ -39,3 +39,12 @@ Then connect to tkellem with an irc client:
39
39
  Note that all config and log files are stored in ~/.tkellem of the user
40
40
  you run `tkellem start` as. You also need to run `tkellem admin` as this
41
41
  same user, in order to have access to the admin console.
42
+
43
+ ## Upgrading
44
+
45
+ Upgrading is as simple as:
46
+
47
+ $ gem install tkellem
48
+ $ tkellem restart
49
+
50
+ All active clients will be forced to re-connect.
@@ -8,7 +8,7 @@ module Tkellem
8
8
  class Bouncer
9
9
  include Tkellem::EasyLogger
10
10
 
11
- attr_reader :user, :network, :nick
11
+ attr_reader :user, :network, :nick, :network_user, :connected_at
12
12
  cattr_accessor :plugins
13
13
  self.plugins = []
14
14
 
@@ -173,15 +173,16 @@ class Bouncer
173
173
  def connection_established(conn)
174
174
  @conn = conn
175
175
  # TODO: support sending a real username, realname, etc
176
- send_msg("USER #{@user.username} somehost tkellem :#{@user.name}")
176
+ send_msg("USER #{@user.username} somehost tkellem :#{@user.name}@tkellem")
177
177
  change_nick(@nick, true)
178
- check_away_status
178
+ @connected_at = Time.now
179
179
  end
180
180
 
181
181
  def disconnected!
182
182
  debug "OMG we got disconnected."
183
183
  @conn = nil
184
184
  @connected = false
185
+ @connected_at = nil
185
186
  @active_conns.each { |c,s| c.unbind }
186
187
  connect!
187
188
  end
@@ -209,24 +210,25 @@ class Bouncer
209
210
  @last_connect = Time.now
210
211
  @cur_host = (@cur_host || 0) % hosts.length
211
212
  host = hosts[@cur_host]
212
- EM.connect(host.address, host.port, IrcServerConnection, self, host.ssl)
213
+ failsafe("connect: #{host}") do
214
+ EM.connect(host.address, host.port, IrcServerConnection, self, host.ssl)
215
+ end
213
216
  end
214
217
 
215
218
  def ready!
216
- return if @joined_rooms
217
- @joined_rooms = true
218
219
  @rooms.each do |room|
219
220
  send_msg("JOIN #{room}")
220
221
  end
221
222
 
223
+ check_away_status
224
+
222
225
  # We're all initialized, allow connections
226
+ @connected_at = Time.now
223
227
  @connected = true
224
228
 
225
- @network_user.at_connect.each do |line|
226
- cmd, args = line.split(' ', 2)
227
- next unless cmd && args && cmd[0] == '/'[0]
228
- msg = IrcMessage.parse("#{cmd[1..-1].upcase} #{args}")
229
- send_msg(msg)
229
+ @network_user.combined_at_connect.each do |line|
230
+ msg = IrcMessage.parse_client_command(line)
231
+ send_msg(msg) if msg
230
232
  end
231
233
 
232
234
  @waiting_clients.each do |client|
@@ -18,8 +18,9 @@ module BouncerConnection
18
18
  @state = :auth
19
19
  @name = 'new-conn'
20
20
  @data = {}
21
+ @connected_at = Time.now
21
22
  end
22
- attr_reader :ssl, :bouncer, :name, :device_name, :connecting_nick
23
+ attr_reader :ssl, :bouncer, :name, :device_name, :connecting_nick, :connected_at
23
24
  alias_method :log_name, :name
24
25
 
25
26
  def nick
@@ -45,6 +46,7 @@ module BouncerConnection
45
46
 
46
47
  def error!(msg)
47
48
  info("ERROR :#{msg}")
49
+ say_as_tkellem(msg)
48
50
  send_msg("ERROR :#{msg}")
49
51
  close_connection(true)
50
52
  end
@@ -58,8 +60,28 @@ module BouncerConnection
58
60
  end
59
61
 
60
62
  def msg_tkellem(msg)
61
- TkellemBot.run_command(msg.args.join(' '), @user) do |response|
62
- say_as_tkellem(response)
63
+ case @state
64
+ when :recaptcha
65
+ if @recaptcha.valid_response?(msg.args.last)
66
+ say_as_tkellem "Looks like you're human. Whew, I hate robots."
67
+ user_registration_get_password
68
+ else
69
+ say_as_tkellem "Nope, that's not right. Please try again."
70
+ end
71
+ when :password
72
+ user = User.create(:username => @username, :password => msg.args.last, :role => 'user')
73
+ if user.errors.any?
74
+ error!("There was an error creating your user account. Please try again, or contact the tkellem admin.")
75
+ else
76
+ @user = user
77
+ say_as_tkellem("Your account has been created. Set your password in your IRC client and re-connect to start using tkellem.")
78
+ end
79
+ else
80
+ if @user
81
+ TkellemBot.run_command(msg.args.join(' '), @user, @bouncer.try(:network_user)) do |response|
82
+ say_as_tkellem(response)
83
+ end
84
+ end
63
85
  end
64
86
  end
65
87
 
@@ -73,7 +95,7 @@ module BouncerConnection
73
95
  msg = IrcMessage.parse(line)
74
96
 
75
97
  command = msg.command
76
- if @user && command == 'PRIVMSG' && msg.args.first == '-tkellem'
98
+ if @state != :auth && command == 'PRIVMSG' && msg.args.first == '-tkellem'
77
99
  msg_tkellem(IrcMessage.new(nil, 'TKELLEM', [msg.args.last]))
78
100
  elsif command == 'TKELLEM'
79
101
  msg_tkellem(msg)
@@ -105,7 +127,8 @@ module BouncerConnection
105
127
  end
106
128
 
107
129
  def maybe_connect
108
- if @connecting_nick && @username && @password && !@user
130
+ return unless @connecting_nick && @username && !@user
131
+ if @password
109
132
  @name = @username
110
133
  @user = User.authenticate(@username, @password)
111
134
  return error!("Unknown username: #{@username} or bad password.") unless @user
@@ -122,9 +145,41 @@ module BouncerConnection
122
145
  @name = "#{@username}-console"
123
146
  connect_to_tkellem_console
124
147
  end
148
+ else
149
+ user = User.find_by_username(@username)
150
+ if user || user_registration == 'closed'
151
+ error!("No password given. Make sure to set your password in your IRC client config, and connect again.")
152
+ if user_registration != 'closed'
153
+ error!("If you are trying to register for a new account, this username is already taken. Please select another.")
154
+ end
155
+ else
156
+ @state = :registration
157
+ say_as_tkellem "Welcome to tkellem, #{@username}. If you already have an account and were trying to connect, please check your username, as it wasn't recognized."
158
+ say_as_tkellem "Otherwise, follow these instructions to create an account."
159
+ say_as_tkellem ' '
160
+ if recaptcha = Setting.get('recaptcha_api_key').presence
161
+ @state = :recaptcha
162
+ require 'tkellem/plugins/recaptcha'
163
+ @recaptcha = Recaptcha.new(*recaptcha.split(',', 2))
164
+ say_as_tkellem "First, you'll need to take a captcha test to verify that you aren't an evil robot bent on destroying humankind."
165
+ say_as_tkellem "Visit this URL, and tell me the code you are given after solving the captcha: #{@recaptcha.challenge_url}"
166
+ return
167
+ end
168
+ user_registration_get_password
169
+ end
125
170
  end
126
171
  end
127
172
 
173
+ def user_registration
174
+ val = Setting.get('user_registration')
175
+ %(open verified).include?(val) ? val : 'closed'
176
+ end
177
+
178
+ def user_registration_get_password
179
+ @state = :password
180
+ say_as_tkellem "You need to set an initial password for your account. Enter your password now:"
181
+ end
182
+
128
183
  def connect_to_tkellem_console
129
184
  send_msg(":tkellem 001 #{nick} :Welcome to the Tkellem admin console")
130
185
  send_msg(":tkellem 376 #{nick} :End")
@@ -28,6 +28,18 @@ class IrcMessage < Struct.new(:prefix, :command, :args, :ctcp)
28
28
  msg
29
29
  end
30
30
 
31
+ # parse a command as it'd come from a client, e.g.
32
+ # /nick newnick
33
+ # or
34
+ # /msg #someroom hey guys
35
+ def self.parse_client_command(line)
36
+ return nil unless line[0] == '/'[0]
37
+ msg = parse(line[1..-1])
38
+ return nil unless msg
39
+ msg.command = 'PRIVMSG' if msg.command == 'MSG'
40
+ msg
41
+ end
42
+
31
43
  def ctcp?
32
44
  self.ctcp.present?
33
45
  end
@@ -0,0 +1,13 @@
1
+ class Settings < ActiveRecord::Migration
2
+ def self.up
3
+ create_table 'settings' do |t|
4
+ t.string :name, :null => false
5
+ t.string :value, :null => false
6
+ t.boolean :unchanged, :null => false, :default => true
7
+ end
8
+
9
+ Tkellem::Setting.make_new('user_registration', 'closed')
10
+ Tkellem::Setting.make_new('recaptcha_api_key', '')
11
+ Tkellem::Setting.make_new('allow_user_networks', 'false')
12
+ end
13
+ end
@@ -2,11 +2,15 @@ module Tkellem
2
2
 
3
3
  class Network < ActiveRecord::Base
4
4
  has_many :hosts, :dependent => :destroy
5
+ accepts_nested_attributes_for :hosts
6
+
5
7
  has_many :network_users, :dependent => :destroy
6
8
  # networks either belong to a specific user, or they are public and any user
7
9
  # can join them.
8
10
  belongs_to :user
9
11
 
12
+ validates_uniqueness_of :name, :scope => :user_id
13
+
10
14
  serialize :at_connect, Array
11
15
 
12
16
  def at_connect
@@ -10,10 +10,8 @@ class NetworkUser < ActiveRecord::Base
10
10
  read_attribute(:nick) || user.name
11
11
  end
12
12
 
13
- # we use the network's at_connect until it is modified and overwritten for
14
- # this specific network user
15
- def at_connect
16
- network.at_connect + (read_attribute(:at_connect) || [])
13
+ def combined_at_connect
14
+ network.at_connect + (at_connect || [])
17
15
  end
18
16
  end
19
17
 
@@ -0,0 +1,24 @@
1
+ module Tkellem
2
+
3
+ class Setting < ActiveRecord::Base
4
+ def self.get(setting_name)
5
+ setting = first(:conditions => { :name => setting_name })
6
+ setting.try(:value)
7
+ end
8
+
9
+ def self.set(setting_name, new_value)
10
+ setting = first(:conditions => { :name => setting_name })
11
+ setting.try(:update_attributes, :value => new_value.to_s, :unchanged => false)
12
+ setting
13
+ end
14
+
15
+ def self.make_new(setting_name, default_value)
16
+ create!(:name => setting_name, :value => default_value.to_s, :unchanged => true)
17
+ end
18
+
19
+ def to_s
20
+ "#{name}: #{value}"
21
+ end
22
+ end
23
+
24
+ end
@@ -43,11 +43,6 @@ class User < ActiveRecord::Base
43
43
  self.password == OpenSSL::Digest::SHA1.hexdigest(password)
44
44
  end
45
45
 
46
- def set_password!(password)
47
- self.password = password
48
- self.save!
49
- end
50
-
51
46
  def password=(password)
52
47
  write_attribute(:password, password ? OpenSSL::Digest::SHA1.hexdigest(password) : nil)
53
48
  end
@@ -0,0 +1,35 @@
1
+ require 'openssl'
2
+
3
+ module Tkellem
4
+
5
+ class Recaptcha
6
+ def initialize(public_key, private_key)
7
+ @public_key = public_key
8
+ @private_key = private_key
9
+ @secret = ActiveSupport::SecureRandom.hex(16)
10
+ end
11
+
12
+ def challenge_url
13
+ "http://www.google.com/recaptcha/mailhide/d?k=#{@public_key}&c=#{url_encoded_secret}"
14
+ end
15
+
16
+ def encrypted_secret
17
+ cipher = OpenSSL::Cipher::Cipher.new('aes-128-cbc')
18
+ cipher.encrypt
19
+ cipher.iv = "\x00"*16
20
+ cipher.key = [@private_key].pack("H*") # private key is a hex string
21
+ data = cipher.update(@secret)
22
+ data << cipher.final
23
+ data
24
+ end
25
+
26
+ def url_encoded_secret
27
+ [encrypted_secret].pack('m').strip.gsub("\n", '').tr('+/', '-_')
28
+ end
29
+
30
+ def valid_response?(response)
31
+ response.strip == @secret
32
+ end
33
+ end
34
+
35
+ end
@@ -19,7 +19,7 @@ module SocketServer
19
19
 
20
20
  def receive_line(line)
21
21
  trace "admin socket: #{line}"
22
- TkellemBot.run_command(line, nil) do |line|
22
+ TkellemBot.run_command(line, nil, nil) do |line|
23
23
  send_data("#{line}\n")
24
24
  end
25
25
  send_data("\0\n")
@@ -6,8 +6,8 @@ module Tkellem
6
6
  class TkellemBot
7
7
  # careful here -- if no user is given, it's assumed the command is running as
8
8
  # an admin
9
- def self.run_command(line, user, &block)
10
- args = Shellwords.shellwords(line.downcase)
9
+ def self.run_command(line, user, network_user, &block)
10
+ args = Shellwords.shellwords(line)
11
11
  command_name = args.shift.upcase
12
12
  command = commands[command_name]
13
13
 
@@ -16,35 +16,27 @@ class TkellemBot
16
16
  return
17
17
  end
18
18
 
19
- command.run(args, user, block)
19
+ command.run(args, user, network_user, block)
20
20
  end
21
21
 
22
22
  class Command
23
- attr_accessor :args
24
-
25
- def self.options
26
- unless defined?(@options)
27
- @options = OptionParser.new
28
- class << @options
29
- attr_accessor :cmd
30
- def set(name, *args)
31
- self.on(*args) { |v| cmd.args[name] = v }
32
- end
33
- end
34
- end
35
- @options
23
+ attr_accessor :args, :user, :network_user, :opts, :options
24
+
25
+ def self.option(name, *args)
26
+ @options ||= {}
27
+ @options[name] = args
36
28
  end
37
29
 
38
- def options
39
- self.class.options
30
+ def self.admin_option(name, *args)
31
+ option(name, *args)
32
+ @admin_onlies ||= []
33
+ @admin_onlies << name
40
34
  end
41
35
 
42
36
  def self.register(cmd_name)
43
37
  cattr_accessor :name
44
38
  self.name = cmd_name
45
39
  TkellemBot.commands[name.upcase] = self
46
- self.options.banner = resources(name)['banner'] if resources(name)['banner']
47
- self.options.separator(resources(name)['help']) if resources(name)['help']
48
40
  end
49
41
 
50
42
  def self.resources(name)
@@ -55,35 +47,51 @@ class TkellemBot
55
47
  class ArgumentError < RuntimeError; end
56
48
 
57
49
  def self.admin_only?
58
- false
50
+ true
59
51
  end
60
52
 
61
- def self.run(args_arr, user, block)
53
+ def self.build_options(user, cmd = nil)
54
+ OptionParser.new.tap do |options|
55
+ @options.try(:each) { |opt_name,args|
56
+ next if !admin_user?(user) && @admin_onlies.include?(opt_name)
57
+ options.on(*args) { |v| cmd.opts[opt_name] = v }
58
+ }
59
+ resources = self.resources(name)
60
+ options.banner = resources['banner'] if resources['banner']
61
+ options.separator(resources['help']) if resources['help']
62
+ end
63
+ end
64
+
65
+ def self.run(args_arr, user, network_user, block)
62
66
  if admin_only? && !admin_user?(user)
63
67
  block.call "You can only run #{name} as an admin."
64
68
  return
65
69
  end
66
70
  cmd = self.new(block)
67
- options.cmd = cmd
68
- options.parse!(args_arr)
69
- cmd.args[:rest] = args_arr
70
- cmd.execute(cmd.args, user)
71
- rescue ArgumentError => e
71
+
72
+ cmd.args = args_arr
73
+ cmd.user = user
74
+ cmd.network_user = network_user
75
+
76
+ cmd.options = build_options(user, cmd)
77
+ cmd.options.parse!(args_arr)
78
+
79
+ cmd.execute
80
+ rescue ArgumentError, OptionParser::InvalidOption => e
72
81
  cmd.respond e.to_s
73
- cmd.show_help
74
82
  end
75
83
 
76
84
  def initialize(responder)
77
85
  @responder = responder
78
- @args = {}
86
+ @opts = {}
79
87
  end
80
88
 
81
89
  def show_help
82
- respond(options.to_s)
90
+ respond(options)
83
91
  end
84
92
 
85
93
  def respond(text)
86
- text.each_line { |l| @responder.call(l.chomp) }
94
+ text.to_s.each_line { |l| @responder.call(l.chomp) }
87
95
  end
88
96
  alias_method :r, :respond
89
97
 
@@ -98,8 +106,12 @@ class TkellemBot
98
106
  class Help < Command
99
107
  register 'help'
100
108
 
101
- def execute(args, user)
102
- name = args[:rest].first
109
+ def self.admin_only?
110
+ false
111
+ end
112
+
113
+ def execute
114
+ name = args.shift.try(:upcase)
103
115
  r "**** tkellem help ****"
104
116
  if name.nil?
105
117
  r "For more information on a command, type:"
@@ -111,12 +123,12 @@ class TkellemBot
111
123
  next if command.admin_only? && user && !user.admin?
112
124
  r "#{name}#{' ' * (25-name.length)}"
113
125
  end
114
- elsif (command = TkellemBot.commands[name.upcase])
126
+ elsif (command = TkellemBot.commands[name])
115
127
  r "Help for #{command.name}:"
116
128
  r ""
117
- r command.options.to_s
129
+ r command.build_options(user)
118
130
  else
119
- r "No help available for #{args.first.upcase}."
131
+ r "No help available for #{name}."
120
132
  end
121
133
  r "**** end of help ****"
122
134
  end
@@ -127,51 +139,64 @@ class TkellemBot
127
139
  register(name)
128
140
  cattr_accessor :model
129
141
  self.model = model
130
- options.set('add', '--add', '-a', "Add a #{model}")
131
- options.set('remove', '--remove', '-r', "Remove a #{model}")
132
- options.set('list', '--list', '-l', "List the current #{model.to_s.pluralize}")
142
+ option('remove', '--remove', '-r', "delete the specified record")
133
143
  end
134
144
 
135
145
  def show(m)
136
146
  m.to_s
137
147
  end
138
148
 
139
- def find_attributes(args, user)
140
- attributes(args, user)
149
+ def find_attributes
150
+ attributes
141
151
  end
142
152
 
143
- def list(args, user)
153
+ def list
144
154
  r "All #{self.class.name.pluralize}:"
145
155
  model.all.each { |m| r " #{show(m)}" }
146
156
  end
147
157
 
148
- def remove(args, user)
149
- instance = model.first(:conditions => find_attributes(args, user))
158
+ def modify
159
+ instance = model.first(:conditions => find_attributes)
160
+ new_record = false
150
161
  if instance
151
- instance.destroy
152
- respond "Removed #{show(instance)}"
162
+ instance.attributes = attributes
163
+ if instance.changed?
164
+ instance.save
165
+ else
166
+ respond " #{show(instance)}"
167
+ return
168
+ end
153
169
  else
154
- respond "Not found"
170
+ new_record = true
171
+ instance = model.create(attributes)
155
172
  end
156
- end
157
-
158
- def add(args, user)
159
- instance = model.create(attributes(args, user))
160
173
  if instance.errors.any?
161
- respond "Errors creating:"
174
+ respond "Error:"
162
175
  instance.errors.full_messages.each { |m| respond " #{m}" }
176
+ respond " #{show(instance)}"
163
177
  else
164
- respond "#{show(instance)} added"
178
+ respond(new_record ? "created:" : "updated:")
179
+ respond " #{show(instance)}"
165
180
  end
166
181
  end
167
182
 
168
- def execute(args, user)
169
- if args['list']
170
- list(args, user)
171
- elsif args['remove']
172
- remove(args, user)
173
- elsif args['add']
174
- add(args, user)
183
+ def remove
184
+ instance = model.first(:conditions => find_attributes)
185
+ if instance
186
+ instance.destroy
187
+ respond "Removed #{show(instance)}"
188
+ else
189
+ respond "Not found"
190
+ end
191
+ end
192
+
193
+ def execute
194
+ if opts['remove'] && args.length == 1
195
+ remove
196
+ elsif args.length == 0
197
+ list
198
+ elsif args.length == 1
199
+ modify
175
200
  else
176
201
  raise Command::ArgumentError, "Unknown sub-command"
177
202
  end
@@ -181,23 +206,19 @@ class TkellemBot
181
206
  class ListenCommand < CRUDCommand
182
207
  register_crud 'listen', ListenAddress
183
208
 
184
- def self.admin_only?
185
- true
186
- end
187
-
188
- def self.get_uri(args)
209
+ def self.get_uri(arg)
189
210
  require 'uri'
190
- uri = URI.parse(args[:rest].first)
211
+ uri = URI.parse(arg)
191
212
  unless %w(irc ircs).include?(uri.scheme)
192
213
  raise Command::ArgumentError, "Invalid URI scheme: #{uri}"
193
214
  end
194
215
  uri
195
216
  rescue URI::InvalidURIError
196
- raise Command::ArgumentError, "Invalid new address: #{args[:rest].first}"
217
+ raise Command::ArgumentError, "Invalid new address: #{arg}"
197
218
  end
198
219
 
199
- def attributes(args, user)
200
- uri = self.class.get_uri(args)
220
+ def attributes
221
+ uri = self.class.get_uri(args.first)
201
222
  { :address => uri.host, :port => uri.port, :ssl => (uri.scheme == 'ircs') }
202
223
  end
203
224
  end
@@ -205,33 +226,37 @@ class TkellemBot
205
226
  class UserCommand < CRUDCommand
206
227
  register_crud 'user', User
207
228
 
208
- def self.admin_only?
209
- true
210
- end
211
-
212
- options.set('user', '--user', '-u', 'Set new user as user (the default)')
213
- options.set('admin', '--admin', 'Set new user as admin')
229
+ option('role', '--role=ROLE', 'Set user role [admin|user]')
214
230
 
215
231
  def show(user)
216
232
  "#{user.username}:#{user.role}"
217
233
  end
218
234
 
219
- def find_attributes(args, user)
220
- { :username => args[:rest].first }
235
+ def find_attributes
236
+ { :username => args.first.downcase }
221
237
  end
222
238
 
223
- def attributes(args, user)
224
- find_attributes(args, user).merge({ :role => (args['admin'] ? 'admin' : 'user') })
239
+ def attributes
240
+ find_attributes.tap { |attrs|
241
+ role = opts['role'].try(:downcase)
242
+ attrs['role'] = role if %w(user admin).include?(role)
243
+ }
225
244
  end
226
245
  end
227
246
 
228
247
  class PasswordCommand < Command
229
248
  register 'password'
230
249
 
231
- options.set('username', '--user=username', '-u', 'Change password for other username')
250
+ admin_option('username', '--user=username', '-u', 'Change password for other username')
251
+
252
+ def self.admin_only?
253
+ false
254
+ end
232
255
 
233
- def execute(args, user)
234
- if args['username']
256
+ def execute
257
+ user = self.user
258
+
259
+ if opts['username']
235
260
  if Command.admin_user?(user)
236
261
  user = User.first(:conditions => { :username => args['username'] })
237
262
  else
@@ -243,111 +268,193 @@ class TkellemBot
243
268
  raise Command::ArgumentError, "User required"
244
269
  end
245
270
 
246
- password = args[:rest].shift || ''
271
+ password = args.shift || ''
247
272
 
248
273
  if password.size < 4
249
274
  raise Command::ArgumentError, "New password too short"
250
275
  end
251
276
 
252
- user.set_password!(password)
277
+ user.password = password
278
+ user.save!
253
279
  respond "New password set for #{user.username}"
254
280
  end
255
281
  end
256
282
 
257
- class AtConnectCommand < CRUDCommand
258
- register_crud 'atconnect', 'At-Connect'
283
+ class AtConnectCommand < Command
284
+ register 'atconnect'
259
285
 
260
- # options.set('network', '--network=network', '-n', 'Network to modify at-connect on')
261
- options.set('username', '--user=username', '-u', 'Modify a user-specific network for another user')
286
+ option('remove', '--remove', '-r', 'Remove previously configured command')
287
+ admin_option('network', '--network=network', '-n', 'Change atconnect for all users on a public network')
262
288
 
263
- def list(args, user, network = nil)
264
- network_name, network, user = NetworkCommand.get_network(args, user) unless network
265
- raise(Command::ArgumentError, "No network found") unless network
266
- r "At connect:"
267
- network.at_connect.each { |line| r " /#{line}" }
289
+ def self.admin_only?
290
+ false
268
291
  end
269
292
 
270
- def remove(args, user)
271
- network_name, network, user = NetworkCommand.get_network(args, user)
272
- raise(Command::ArgumentError, "No network found") unless network
273
- line = args[:rest].join(' ')
274
- network.at_connect = network.at_connect.reject { |l| l == line }
275
- network.save
276
- list(args, user, network)
293
+ def list(target)
294
+ target.reload
295
+ if target.is_a?(NetworkUser) && target.network.public?
296
+ r "Network-wide commands are prefixed with [N], user-specific commands with [U]."
297
+ r "Network-wide commands can only be modified by admins."
298
+ list(target.network)
299
+ end
300
+ prefix = target.is_a?(Network) ? 'N' : 'U'
301
+ target.at_connect.try(:each) { |line| r " [#{prefix}] #{line}" }
277
302
  end
278
303
 
279
- def add(args, user)
280
- network_name, network, user = NetworkCommand.get_network(args, user)
281
- raise(Command::ArgumentError, "No network found") unless network
282
- line = args[:rest].join(' ')
283
- raise(Command::ArgumentError, "atconnect commands must start with a /") unless line[0] == '/'[0]
284
- network.at_connect = network.at_connect + [line]
285
- network.save
286
- list(args, user, network)
304
+ def execute
305
+ if opts['network'].present? # only settable by admins
306
+ target = Network.first(:conditions => ["name = ? AND user_id IS NULL", opts['network'].downcase])
307
+ else
308
+ target = network_user
309
+ end
310
+ raise(Command::ArgumentError, "No network found") unless target
311
+
312
+ if args.size == 0
313
+ r "At connect:"
314
+ list(target)
315
+ else
316
+ line = args.join(' ')
317
+ raise(Command::ArgumentError, "atconnect commands must start with a /") unless line[0] == '/'[0]
318
+ if opts['remove']
319
+ target.at_connect = (target.at_connect || []).reject { |l| l == line }
320
+ else
321
+ target.at_connect = (target.at_connect || []) + [line]
322
+ end
323
+ target.save
324
+ r "At connect commands modified:"
325
+ list(target)
326
+ end
287
327
  end
288
328
  end
289
329
 
290
- class NetworkCommand < CRUDCommand
291
- register_crud 'network', Host
330
+ class NetworkCommand < Command
331
+ register 'network'
292
332
 
293
- options.set('public', '--public', 'Set new network as public')
294
- options.set('username', '--user=username', '-u', 'Create a user-specific network for another user')
333
+ def self.admin_only?
334
+ false
335
+ end
295
336
 
296
- def list(args, user)
297
- r "All networks:"
298
- Network.all.each { |m| r " #{show(m.hosts.first)}" if m.hosts.first }
337
+ option('remove', '--remove', '-r', "Remove a hostname for a network, or the entire network if no host is given.")
338
+ option('network', '--name=NETWORK', '-n', "Operate on a different network than the current connection.")
339
+ admin_option('public', '--public', "Create new public network. Once created, public/private status can't be modified.")
340
+
341
+ def list
342
+ public_networks = Network.all(:conditions => 'user_id IS NULL')
343
+ user_networks = user.reload.try(:networks) || []
344
+ if user_networks.present? && public_networks.present?
345
+ r "Public networks are prefixed with [P], user-specific networks with [U]."
346
+ end
347
+ (public_networks + user_networks).each do |net|
348
+ prefix = net.public? ? 'P' : 'U'
349
+ r " [#{prefix}] #{show(net)}"
350
+ end
299
351
  end
300
352
 
301
- def show(host)
302
- "#{host.network.name}#{' (public)' if host.network.public?} " + host.network.hosts.map { |h| "[#{h}]" }.join(' ')
353
+ def show(network)
354
+ "#{network.name} " + network.hosts.map { |h| "[#{h}]" }.join(' ')
303
355
  end
304
356
 
305
- def self.get_network(args, user)
306
- network_name = args[:rest].shift
307
- if args['username']
308
- if Command.admin_user?(user)
309
- user = User.first(:conditions => { :username => args['username'] })
310
- else
311
- raise Command::ArgumentError, "Only admins can change other user's networks"
312
- end
357
+ def execute
358
+ # TODO: this got gross
359
+ if args.empty? && !opts['remove']
360
+ list
361
+ return
313
362
  end
314
363
 
315
- network = Network.first(:conditions => { :name => network_name, :user_id => user.id }) if user
316
- network ||= Network.first(:conditions => { :name => network_name, :user_id => nil })
317
- if network && network.public? && !admin_user?(user)
318
- raise Command::ArgumentError, "Only admins can modify public networks"
364
+ if opts['network'].present?
365
+ target = Network.first(:conditions => ["name = ? AND user_id = ?", opts['network'].downcase, user.try(:id)])
366
+ target ||= Network.first(:conditions => ["name = ? AND user_id IS NULL", opts['network'].downcase]) if self.class.admin_user?(user)
367
+ else
368
+ target = network_user.try(:network)
369
+ if target && target.public? && !self.class.admin_user?(user)
370
+ raise(Command::ArgumentError, "Only admins can modify public networks")
371
+ end
372
+ raise(Command::ArgumentError, "No network found") unless target
319
373
  end
320
- return network_name, network, user
321
- end
322
374
 
323
- def get_network(args, user)
324
- self.class.get_network(args, user)
375
+ uri = ListenCommand.get_uri(args.shift) unless args.empty?
376
+ addr_args = { :address => uri.host, :port => uri.port, :ssl => (uri.scheme == 'ircs') } if uri
377
+
378
+ if opts['remove']
379
+ raise(Command::ArgumentError, "No network found") unless target
380
+ raise(Command::ArgumentError, "You must explicitly specify the network to remove") unless opts['network']
381
+ if uri
382
+ target.hosts.first(:conditions => addr_args).try(:destroy)
383
+ respond " #{show(target)}"
384
+ else
385
+ target.destroy
386
+ r "Network #{target.name} removed"
387
+ end
388
+ else
389
+ unless target
390
+ create_public = (self.class.admin_user?(user) && opts['public'])
391
+ raise(Command::ArgumentError, "Only public networks can be created without a user") unless create_public || user
392
+ raise(Command::ArgumentError, "Creating user networks has been disabled by the admins") unless Setting.get('allow_user_networks') == 'true'
393
+ target = Network.create(:name => opts['network'], :user => (create_public ? nil : user))
394
+ unless create_public
395
+ NetworkUser.create(:user => user, :network => target)
396
+ end
397
+ end
398
+
399
+ target.attributes = { :hosts_attributes => [addr_args] }
400
+ target.save
401
+ if target.errors.any?
402
+ respond "Error:"
403
+ target.errors.full_messages.each { |m| respond " #{m}" }
404
+ respond " #{show(target)}"
405
+ else
406
+ respond("updated:")
407
+ respond " #{show(target)}"
408
+ end
409
+ end
325
410
  end
411
+ end
326
412
 
327
- def remove(args, user)
328
- network_name, network, user = get_network(args, user)
329
- if network
330
- Host.all(:conditions => { :network_id => network.id }).each(&:destroy)
331
- network.destroy
332
- respond "Removed #{network.name} #{show(network.hosts.first) if network.hosts.first}"
413
+ class SettingCommand < Command
414
+ register 'setting'
415
+
416
+ def self.setting_resources(name)
417
+ @setting_resources ||= YAML.load_file(File.expand_path("../../../resources/setting_descriptions.yml", __FILE__))
418
+ @setting_resources[name] || {}
419
+ end
420
+
421
+ def execute
422
+ case args.size
423
+ when 0
424
+ r "Settings:"
425
+ Setting.all.each { |s| r " #{s}" }
426
+ when 1
427
+ setting = Setting.find_by_name(args.first)
428
+ if setting
429
+ r(setting.to_s)
430
+ desc = self.class.setting_resources(setting.name)
431
+ if desc['help']
432
+ desc['help'].each_line { |l| r l }
433
+ end
434
+ else
435
+ r("No setting with that name")
436
+ end
437
+ when 2
438
+ setting = Setting.set(args[0], args[1])
439
+ setting ? r(setting.to_s) : r("No setting with that name")
333
440
  else
334
- respond "Not found"
441
+ show_help
335
442
  end
336
443
  end
444
+ end
337
445
 
338
- def attributes(args, user)
339
- network_name, network, user = get_network(args, user)
446
+ class ConnectionsCommand < Command
447
+ register 'connections'
340
448
 
341
- unless network
342
- create_public = !user || (user.admin? && args['public'])
343
- network = Network.create(:name => network_name, :user => (create_public ? nil : user))
344
- unless create_public
345
- NetworkUser.create(:user => user, :network => network)
449
+ def execute
450
+ require 'socket'
451
+ $tkellem_server.bouncers.each do |k, bouncer|
452
+ respond "#{bouncer.user.username}@#{bouncer.network.name} (#{bouncer.connected? ? 'connected' : 'connecting'}) #{"since #{bouncer.connected_at}" if bouncer.connected?}"
453
+ bouncer.active_conns.each do |conn|
454
+ port, addr = Socket.unpack_sockaddr_in(conn.get_peername)
455
+ respond " #{addr} device=#{conn.device_name} since #{conn.connected_at}"
346
456
  end
347
457
  end
348
-
349
- uri = ListenCommand.get_uri(args)
350
- { :network => network, :address => uri.host, :port => uri.port, :ssl => (uri.scheme == 'ircs') }
351
458
  end
352
459
  end
353
460
  end
@@ -4,17 +4,26 @@ require 'active_record'
4
4
  require 'tkellem/bouncer_connection'
5
5
  require 'tkellem/bouncer'
6
6
 
7
- require 'tkellem/models/user'
8
- require 'tkellem/models/network'
9
7
  require 'tkellem/models/host'
10
- require 'tkellem/models/network_user'
11
8
  require 'tkellem/models/listen_address'
9
+ require 'tkellem/models/network'
10
+ require 'tkellem/models/network_user'
11
+ require 'tkellem/models/setting'
12
+ require 'tkellem/models/user'
12
13
 
13
- require 'tkellem/plugins/push_service'
14
14
  require 'tkellem/plugins/backlog'
15
+ require 'tkellem/plugins/push_service'
15
16
 
16
17
  module Tkellem
17
18
 
19
+ unless ActiveRecord::Base.connected?
20
+ ActiveRecord::Base.establish_connection({
21
+ :adapter => 'sqlite3',
22
+ :database => File.expand_path("~/.tkellem/tkellem.sqlite3"),
23
+ })
24
+ ActiveRecord::Migrator.migrate(File.expand_path("../migrations", __FILE__), nil)
25
+ end
26
+
18
27
  class TkellemServer
19
28
  include Tkellem::EasyLogger
20
29
 
@@ -23,14 +32,7 @@ class TkellemServer
23
32
  def initialize
24
33
  @listeners = {}
25
34
  @bouncers = {}
26
-
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
35
+ $tkellem_server = self
34
36
 
35
37
  ListenAddress.all.each { |a| listen(a) }
36
38
  NetworkUser.find_each { |nu| add_bouncer(Bouncer.new(nu)) }
@@ -1,3 +1,3 @@
1
1
  module Tkellem
2
- VERSION = "0.8.6"
2
+ VERSION = "0.8.7"
3
3
  end
@@ -1,36 +1,67 @@
1
1
  ---
2
2
  LISTEN:
3
- banner: "Usage: LISTEN [--add|--remove|--list] <uri>"
3
+ banner: "Usage: LISTEN [--remove] <uri>"
4
4
  help: |+
5
+
5
6
  Manage the address/port combinations for tkellem to accept client
6
7
  connections on.
7
8
 
8
9
  Protocol is either `irc`, or `ircs` for an SSL listener.
9
10
 
10
- Example:
11
- LISTEN --add ircs://0.0.0.0:10001
11
+ Examples:
12
+ LISTEN ircs://0.0.0.0:10001
13
+ LISTEN --remove ircs://0.0.0.0:10001
12
14
 
13
15
  USER:
14
- banner: "Usage: USER [--add|--remove|--list] <username> [--admin]"
16
+ banner: "Usage: USER [--remove] <username> [--role=<admin|user>]"
15
17
  help: |+
18
+
16
19
  Manage users.
17
20
 
18
21
  Example:
19
- USER --add joe admin
22
+ USER joe --role=admin
23
+ USER joe --role=user
24
+ USER --remove joe
20
25
 
21
26
  PASSWORD:
22
- banner: "Usage: PASSWORD [--user <username>] <new-password>"
27
+ banner: "Usage: PASSWORD <new-password>"
23
28
  help: |+
24
- Change password. If you are an admin, you can change other users passwords as well.
29
+
30
+ Change password.
25
31
 
26
32
  Example:
27
33
  PASSWORD hunter2
28
34
 
29
35
  NETWORK:
30
- banner: "Usage: NETWORK [--add|--remove|--list] [--public] <network-name> <uri>"
36
+ banner: "Usage: NETWORK [--remove] [--public] [--name=<network-name>] <uri>"
31
37
  help: |+
32
- Manage networks. --add will add a new network, or a new connection host to an existing network. Public networks can only be created by admins, and can be joined by anybody.
38
+
39
+ Manage networks. Will add a new network, or a new connection host to an existing network. Public networks can only be created by admins, and can be joined by anybody.
40
+
41
+ To connect to a network, change the server username in your irc client to reflect what network you want to connect to, in the form of username@network. For instance, to connect to a network called "freenode" as the user "joe", you would set your username to joe@freenode
42
+
43
+ Examples:
44
+ NETWORK ircs://irc.freenode.org:7000 # operate on the current network
45
+ NETWORK --name=freenode ircs://irc.freenode.org:7000
46
+ NETWORK --remove --name=freenode ircs://irc.freenode.org:7000
47
+ NETWORK --remove --name=freenode
48
+
49
+ ATCONNECT:
50
+ banner: "Usage: ATCONNECT [--remove] <command>"
51
+ help: |+
52
+
53
+ Add or remove an IRC command to run at connect.
54
+
55
+ Examples:
56
+ ATCONNECT /join #tkellem
57
+ ATCONNECT --remove /join #tkellem
58
+
59
+ SETTING:
60
+ banner: "SETTING [<name>] [<new_value>]"
61
+ help: |+
62
+
63
+ View and modify global settings.
33
64
 
34
65
  Examples:
35
- NETWORK --add --public freenode irc://irc.freenode.org:6667
36
- NETWORK --add freenode ircs://irc.freenode.org:7000
66
+ SETTING
67
+ SETTING public_registration true
@@ -0,0 +1,20 @@
1
+ ---
2
+ user_registration:
3
+ help: |+
4
+ Allow new users to register themselves with this instance of tkellem, rather than having to create all users through the tkellem console. Set to 'open' to allow self-registration, 'verified' to allow self-registration but require admin approval before activation, any other value means closed registration.
5
+
6
+ If you enable this, you should configure recaptcha as well, see the help for the recaptcha_api_key setting.
7
+
8
+ New users will register themselves by connecting to tkellem with a username but no password and no network specified. The tkellem bot will walk them through the process.
9
+
10
+ recaptcha_api_key:
11
+ help: |+
12
+ Configure the recaptcha API key for public registrations. Generate a key by visiting http://www.google.com/recaptcha/mailhide/apikey . Then set this setting to the value
13
+
14
+ <public key>,<private key>
15
+
16
+ That is, the public key, then a comma, then the private key. This will protect user self-signups behind a recaptcha check.
17
+
18
+ allow_user_networks:
19
+ help: |+
20
+ If 'true', allow users to create their own networks with the `/tkellem network` command. Otherwise, users can only join admin-created public networks.
@@ -6,7 +6,8 @@ include Tkellem
6
6
  describe BouncerConnection, "connect" do
7
7
  before do
8
8
  u = User.create(:username => 'speccer')
9
- u.set_password!('test123')
9
+ u.password = 'test123'
10
+ u.save
10
11
  tk = mock(TkellemServer)
11
12
  @b = mock(Bouncer)
12
13
  tk.should_receive(:find_bouncer).with(u, 'testhost').and_return(@b)
@@ -14,7 +14,7 @@ describe Bouncer, "connection" do
14
14
  end
15
15
 
16
16
  def send_welcome(s, &just_before_last)
17
- s.should_receive(:send_msg).with("USER speccer somehost tkellem :speccer")
17
+ s.should_receive(:send_msg).with("USER speccer somehost tkellem :speccer@tkellem")
18
18
  s.should_receive(:send_msg).with("NICK speccer")
19
19
  s.should_receive(:send_msg).with("AWAY :Away")
20
20
  s.connection_established(nil)
@@ -35,9 +35,8 @@ describe Bouncer, "connection" do
35
35
  it "should connect to the server on creation" do
36
36
  s = make_server
37
37
  s.connected?.should_not be_true
38
- s.should_receive(:send_msg).with("USER speccer somehost tkellem :speccer")
38
+ s.should_receive(:send_msg).with("USER speccer somehost tkellem :speccer@tkellem")
39
39
  s.should_receive(:send_msg).with("NICK speccer")
40
- s.should_receive(:send_msg).with("AWAY :Away")
41
40
  s.connection_established(nil)
42
41
  end
43
42
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: tkellem
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.8.6
5
+ version: 0.8.7
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-18 00:00:00 -06:00
13
+ date: 2011-06-20 00:00:00 -06:00
14
14
  default_executable: tkellem
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -137,18 +137,22 @@ files:
137
137
  - lib/tkellem/irc_server.rb
138
138
  - lib/tkellem/migrations/001_init_db.rb
139
139
  - lib/tkellem/migrations/002_at_connect_columns.rb
140
+ - lib/tkellem/migrations/003_settings.rb
140
141
  - lib/tkellem/models/host.rb
141
142
  - lib/tkellem/models/listen_address.rb
142
143
  - lib/tkellem/models/network.rb
143
144
  - lib/tkellem/models/network_user.rb
145
+ - lib/tkellem/models/setting.rb
144
146
  - lib/tkellem/models/user.rb
145
147
  - lib/tkellem/plugins/backlog.rb
146
148
  - lib/tkellem/plugins/push_service.rb
149
+ - lib/tkellem/plugins/recaptcha.rb
147
150
  - lib/tkellem/socket_server.rb
148
151
  - lib/tkellem/tkellem_bot.rb
149
152
  - lib/tkellem/tkellem_server.rb
150
153
  - lib/tkellem/version.rb
151
154
  - resources/bot_command_descriptions.yml
155
+ - resources/setting_descriptions.yml
152
156
  - spec/bouncer_connection_spec.rb
153
157
  - spec/irc_message_spec.rb
154
158
  - spec/irc_server_spec.rb