tkellem 0.8.6 → 0.8.7

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