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