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 +13 -4
- data/lib/tkellem/bouncer.rb +13 -11
- data/lib/tkellem/bouncer_connection.rb +60 -5
- data/lib/tkellem/irc_message.rb +12 -0
- data/lib/tkellem/migrations/003_settings.rb +13 -0
- data/lib/tkellem/models/network.rb +4 -0
- data/lib/tkellem/models/network_user.rb +2 -4
- data/lib/tkellem/models/setting.rb +24 -0
- data/lib/tkellem/models/user.rb +0 -5
- data/lib/tkellem/plugins/recaptcha.rb +35 -0
- data/lib/tkellem/socket_server.rb +1 -1
- data/lib/tkellem/tkellem_bot.rb +257 -150
- data/lib/tkellem/tkellem_server.rb +14 -12
- data/lib/tkellem/version.rb +1 -1
- data/resources/bot_command_descriptions.yml +42 -11
- data/resources/setting_descriptions.yml +20 -0
- data/spec/bouncer_connection_spec.rb +2 -1
- data/spec/irc_server_spec.rb +2 -3
- metadata +6 -2
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
|
25
|
-
> user
|
26
|
-
> password --user
|
27
|
-
> network --
|
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.
|
data/lib/tkellem/bouncer.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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.
|
226
|
-
|
227
|
-
|
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
|
-
|
62
|
-
|
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 @
|
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
|
-
|
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")
|
data/lib/tkellem/irc_message.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
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
|
data/lib/tkellem/models/user.rb
CHANGED
@@ -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
|
data/lib/tkellem/tkellem_bot.rb
CHANGED
@@ -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
|
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.
|
26
|
-
|
27
|
-
|
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
|
39
|
-
|
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
|
-
|
50
|
+
true
|
59
51
|
end
|
60
52
|
|
61
|
-
def self.
|
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
|
-
|
68
|
-
|
69
|
-
cmd.
|
70
|
-
cmd.
|
71
|
-
|
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
|
-
@
|
86
|
+
@opts = {}
|
79
87
|
end
|
80
88
|
|
81
89
|
def show_help
|
82
|
-
respond(options
|
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
|
102
|
-
|
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
|
126
|
+
elsif (command = TkellemBot.commands[name])
|
115
127
|
r "Help for #{command.name}:"
|
116
128
|
r ""
|
117
|
-
r command.
|
129
|
+
r command.build_options(user)
|
118
130
|
else
|
119
|
-
r "No help available for #{
|
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
|
-
|
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
|
140
|
-
attributes
|
149
|
+
def find_attributes
|
150
|
+
attributes
|
141
151
|
end
|
142
152
|
|
143
|
-
def list
|
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
|
149
|
-
instance = model.first(:conditions => find_attributes
|
158
|
+
def modify
|
159
|
+
instance = model.first(:conditions => find_attributes)
|
160
|
+
new_record = false
|
150
161
|
if instance
|
151
|
-
instance.
|
152
|
-
|
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
|
-
|
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 "
|
174
|
+
respond "Error:"
|
162
175
|
instance.errors.full_messages.each { |m| respond " #{m}" }
|
176
|
+
respond " #{show(instance)}"
|
163
177
|
else
|
164
|
-
respond "
|
178
|
+
respond(new_record ? "created:" : "updated:")
|
179
|
+
respond " #{show(instance)}"
|
165
180
|
end
|
166
181
|
end
|
167
182
|
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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.
|
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(
|
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: #{
|
217
|
+
raise Command::ArgumentError, "Invalid new address: #{arg}"
|
197
218
|
end
|
198
219
|
|
199
|
-
def attributes
|
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
|
-
|
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
|
220
|
-
{ :username => args
|
235
|
+
def find_attributes
|
236
|
+
{ :username => args.first.downcase }
|
221
237
|
end
|
222
238
|
|
223
|
-
def attributes
|
224
|
-
find_attributes
|
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
|
-
|
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
|
234
|
-
|
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
|
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.
|
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 <
|
258
|
-
|
283
|
+
class AtConnectCommand < Command
|
284
|
+
register 'atconnect'
|
259
285
|
|
260
|
-
|
261
|
-
|
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
|
264
|
-
|
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
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
network
|
286
|
-
|
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 <
|
291
|
-
|
330
|
+
class NetworkCommand < Command
|
331
|
+
register 'network'
|
292
332
|
|
293
|
-
|
294
|
-
|
333
|
+
def self.admin_only?
|
334
|
+
false
|
335
|
+
end
|
295
336
|
|
296
|
-
|
297
|
-
|
298
|
-
|
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(
|
302
|
-
"#{
|
353
|
+
def show(network)
|
354
|
+
"#{network.name} " + network.hosts.map { |h| "[#{h}]" }.join(' ')
|
303
355
|
end
|
304
356
|
|
305
|
-
def
|
306
|
-
|
307
|
-
if args['
|
308
|
-
|
309
|
-
|
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
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
-
|
324
|
-
|
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
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
-
|
441
|
+
show_help
|
335
442
|
end
|
336
443
|
end
|
444
|
+
end
|
337
445
|
|
338
|
-
|
339
|
-
|
446
|
+
class ConnectionsCommand < Command
|
447
|
+
register 'connections'
|
340
448
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
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)) }
|
data/lib/tkellem/version.rb
CHANGED
@@ -1,36 +1,67 @@
|
|
1
1
|
---
|
2
2
|
LISTEN:
|
3
|
-
banner: "Usage: LISTEN [--
|
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
|
-
|
11
|
-
LISTEN
|
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 [--
|
16
|
+
banner: "Usage: USER [--remove] <username> [--role=<admin|user>]"
|
15
17
|
help: |+
|
18
|
+
|
16
19
|
Manage users.
|
17
20
|
|
18
21
|
Example:
|
19
|
-
USER
|
22
|
+
USER joe --role=admin
|
23
|
+
USER joe --role=user
|
24
|
+
USER --remove joe
|
20
25
|
|
21
26
|
PASSWORD:
|
22
|
-
banner: "Usage: PASSWORD
|
27
|
+
banner: "Usage: PASSWORD <new-password>"
|
23
28
|
help: |+
|
24
|
-
|
29
|
+
|
30
|
+
Change password.
|
25
31
|
|
26
32
|
Example:
|
27
33
|
PASSWORD hunter2
|
28
34
|
|
29
35
|
NETWORK:
|
30
|
-
banner: "Usage: NETWORK [--
|
36
|
+
banner: "Usage: NETWORK [--remove] [--public] [--name=<network-name>] <uri>"
|
31
37
|
help: |+
|
32
|
-
|
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
|
-
|
36
|
-
|
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.
|
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)
|
data/spec/irc_server_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|