tkellem 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/README.md +28 -6
- data/Rakefile +14 -5
- data/bin/tkellem +2 -107
- data/debian/changelog +5 -0
- data/debian/compat +1 -0
- data/debian/control +18 -0
- data/debian/copyright +42 -0
- data/debian/docs +1 -0
- data/debian/examples +1 -0
- data/debian/install +3 -0
- data/debian/manpages +1 -0
- data/debian/rules +13 -0
- data/debian/source/format +1 -0
- data/debian/tkellem.1 +11 -0
- data/examples/config.yml +2 -29
- data/lib/tkellem/bouncer.rb +196 -31
- data/lib/tkellem/bouncer_connection.rb +90 -85
- data/lib/tkellem/daemon.rb +155 -0
- data/lib/tkellem/irc_message.rb +58 -0
- data/lib/tkellem/irc_server.rb +8 -121
- data/lib/tkellem/migrations/001_init_db.rb +33 -0
- data/lib/tkellem/models/host.rb +11 -0
- data/lib/tkellem/models/listen_address.rb +12 -0
- data/lib/tkellem/models/network.rb +15 -0
- data/lib/tkellem/models/network_user.rb +12 -0
- data/lib/tkellem/models/user.rb +56 -0
- data/lib/tkellem/plugins/backlog.rb +160 -0
- data/lib/tkellem/plugins/push_service.rb +152 -0
- data/lib/tkellem/socket_server.rb +29 -0
- data/lib/tkellem/tkellem_bot.rb +318 -0
- data/lib/tkellem/tkellem_server.rb +114 -0
- data/lib/tkellem/version.rb +3 -0
- data/lib/tkellem.rb +7 -10
- data/resources/bot_command_descriptions.yml +36 -0
- data/spec/irc_message_spec.rb +47 -0
- data/spec/irc_server_spec.rb +60 -0
- data/spec/spec_helper.rb +0 -2
- data/tkellem.gemspec +20 -47
- metadata +118 -22
- data/VERSION +0 -1
- data/lib/tkellem/backlog.rb +0 -85
- data/lib/tkellem/irc_line.rb +0 -58
@@ -0,0 +1,56 @@
|
|
1
|
+
module Tkellem
|
2
|
+
|
3
|
+
class User < ActiveRecord::Base
|
4
|
+
has_many :network_users, :dependent => :destroy
|
5
|
+
has_many :networks, :dependent => :destroy
|
6
|
+
|
7
|
+
validates_presence_of :username
|
8
|
+
validates_uniqueness_of :username
|
9
|
+
validates_presence_of :role, :in => %w(user admin)
|
10
|
+
|
11
|
+
# pluggable authentication -- add your own block, which takes |username, password|
|
12
|
+
# parameters. Return a User object if authentication succeeded, or a
|
13
|
+
# false/nil value if auth failed. You can create the user on-the-fly if
|
14
|
+
# necessary.
|
15
|
+
cattr_accessor :authentication_methods
|
16
|
+
self.authentication_methods = []
|
17
|
+
|
18
|
+
# default database-based authentication
|
19
|
+
# TODO: proper password hashing
|
20
|
+
self.authentication_methods << proc do |username, password|
|
21
|
+
user = find_by_username(username)
|
22
|
+
user && user.valid_password?(password) && user
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.authenticate(username, password)
|
26
|
+
authentication_methods.each do |m|
|
27
|
+
result = m.call(username, password)
|
28
|
+
return result if result.is_a?(self)
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def username=(val)
|
34
|
+
write_attribute(:username, val.try(:downcase))
|
35
|
+
end
|
36
|
+
|
37
|
+
def name
|
38
|
+
username
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_password?(password)
|
42
|
+
require 'openssl'
|
43
|
+
self.password == OpenSSL::Digest::SHA1.hexdigest(password)
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_password!(password)
|
47
|
+
self.password = OpenSSL::Digest::SHA1.hexdigest(password)
|
48
|
+
self.save!
|
49
|
+
end
|
50
|
+
|
51
|
+
def admin?
|
52
|
+
role == 'admin'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
5
|
+
|
6
|
+
require 'tkellem/irc_message'
|
7
|
+
|
8
|
+
module Tkellem
|
9
|
+
|
10
|
+
# The default backlog handler. Stores messages, and allows for
|
11
|
+
# device-independent backlogs (if the client sends a device_name, that device
|
12
|
+
# will get its own backlog cursor).
|
13
|
+
|
14
|
+
# This is implemented as a plugin -- in theory, it could be switched out for a
|
15
|
+
# different backlog implementation. Right now, it's always loaded though.
|
16
|
+
class Backlog
|
17
|
+
include Tkellem::EasyLogger
|
18
|
+
|
19
|
+
Bouncer.add_plugin(self)
|
20
|
+
cattr_accessor :instances
|
21
|
+
|
22
|
+
def self.get_instance(bouncer)
|
23
|
+
bouncer.data(self)[:instance] ||= self.new(bouncer)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.new_client_connected(bouncer, client)
|
27
|
+
instance = get_instance(bouncer)
|
28
|
+
instance.client_connected(client)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.client_msg(bouncer, client, msg)
|
32
|
+
instance = get_instance(bouncer)
|
33
|
+
instance.client_msg(msg)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.server_msg(bouncer, msg)
|
38
|
+
instance = get_instance(bouncer)
|
39
|
+
instance.server_msg(msg)
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(bouncer)
|
44
|
+
@bouncer = bouncer
|
45
|
+
@devices = {}
|
46
|
+
@streams = {}
|
47
|
+
@starting_pos = {}
|
48
|
+
@dir = File.expand_path("~/.tkellem/logs/#{bouncer.user.name}/#{bouncer.network.name}")
|
49
|
+
FileUtils.mkdir_p(@dir)
|
50
|
+
end
|
51
|
+
|
52
|
+
def stream_filename(ctx)
|
53
|
+
File.join(@dir, "#{ctx}.log")
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_stream(ctx)
|
57
|
+
# open stream in append-only mode
|
58
|
+
return @streams[ctx] if @streams[ctx]
|
59
|
+
stream = @streams[ctx] = File.open(stream_filename(ctx), 'ab')
|
60
|
+
@starting_pos[ctx] = stream.pos
|
61
|
+
stream
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_device(conn)
|
65
|
+
@devices[conn.device_name] ||= Hash.new { |h,k| h[k] = @starting_pos[k] }
|
66
|
+
end
|
67
|
+
|
68
|
+
def client_connected(conn)
|
69
|
+
device = get_device(conn)
|
70
|
+
if @streams.any? { |ctx_name, stream| device[ctx_name] < stream.pos }
|
71
|
+
# this device has missed messages, replay all the backlogs
|
72
|
+
send_backlog(conn, device)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_pos(ctx_name, pos)
|
77
|
+
@bouncer.active_conns.each do |conn|
|
78
|
+
device = get_device(conn)
|
79
|
+
device[ctx_name] = pos
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_name
|
84
|
+
"backlog:#{@bouncer.log_name}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def server_msg(msg)
|
88
|
+
case msg.command
|
89
|
+
when /3\d\d/, 'JOIN', 'PART'
|
90
|
+
# transient messages
|
91
|
+
return
|
92
|
+
when 'PRIVMSG'
|
93
|
+
ctx = msg.args.first
|
94
|
+
if ctx == @bouncer.nick
|
95
|
+
# incoming pm, fake ctx to be the sender's nick
|
96
|
+
ctx = msg.prefix.split(/[!~@]/, 2).first
|
97
|
+
end
|
98
|
+
stream = get_stream(ctx)
|
99
|
+
stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S < #{msg.prefix}: #{msg.args.last}"))
|
100
|
+
update_pos(ctx, stream.pos)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def client_msg(msg)
|
105
|
+
case msg.command
|
106
|
+
when 'PRIVMSG'
|
107
|
+
ctx = msg.args.first
|
108
|
+
stream = get_stream(ctx)
|
109
|
+
stream.puts(Time.now.strftime("%d-%m-%Y %H:%M:%S > #{msg.args.last}"))
|
110
|
+
update_pos(ctx, stream.pos)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def send_backlog(conn, device)
|
115
|
+
device.each do |ctx_name, pos|
|
116
|
+
stream = File.open(stream_filename(ctx_name), 'rb')
|
117
|
+
stream.seek(pos)
|
118
|
+
|
119
|
+
while line = stream.gets
|
120
|
+
timestamp, msg = parse_line(line, ctx_name)
|
121
|
+
next unless msg
|
122
|
+
if msg.prefix
|
123
|
+
# to user
|
124
|
+
else
|
125
|
+
# from user, add prefix
|
126
|
+
if msg.args.first[0] == '#'[0]
|
127
|
+
# it's a room, we can just replay
|
128
|
+
msg.prefix = @bouncer.nick
|
129
|
+
else
|
130
|
+
# a one-on-one chat -- every client i've seen doesn't know how to
|
131
|
+
# display messages from themselves here, so we fake it by just
|
132
|
+
# adding an arrow and pretending the other user said it. shame.
|
133
|
+
msg.prefix = msg.args.first
|
134
|
+
msg.args[0] = @bouncer.nick
|
135
|
+
msg.args[-1] = "-> #{msg.args.last}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
conn.send_msg(msg.with_timestamp(timestamp))
|
139
|
+
end
|
140
|
+
|
141
|
+
device[ctx_name] = get_stream(ctx_name).pos
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_line(line, ctx_name)
|
146
|
+
timestamp = Time.parse(line[0, 19])
|
147
|
+
case line[20..-1]
|
148
|
+
when %r{^> (.+)$}
|
149
|
+
msg = IrcMessage.new(nil, 'PRIVMSG', [ctx_name, $1])
|
150
|
+
return timestamp, msg
|
151
|
+
when %r{^< ([^:]+): (.+)$}
|
152
|
+
msg = IrcMessage.new($1, 'PRIVMSG', [ctx_name, $2])
|
153
|
+
return timestamp, msg
|
154
|
+
else
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'json'
|
3
|
+
require 'tkellem/irc_message'
|
4
|
+
|
5
|
+
module Tkellem
|
6
|
+
|
7
|
+
# http://colloquy.mobi/bouncers.html
|
8
|
+
class PushService
|
9
|
+
include Tkellem::EasyLogger
|
10
|
+
|
11
|
+
attr_reader :server, :port, :device_token
|
12
|
+
|
13
|
+
def self.connections
|
14
|
+
@connections || @connections = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.active_instances
|
18
|
+
# TODO: need to time these out after some period -- a week or something
|
19
|
+
@instances || @instances = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
Bouncer.add_plugin(self)
|
23
|
+
|
24
|
+
def self.new_client_connected(bouncer, client)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.client_msg(bouncer, client, msg)
|
28
|
+
# TODO: check if push services enabled
|
29
|
+
case msg.command
|
30
|
+
when 'PUSH'
|
31
|
+
if service = client.data(self)[:instance]
|
32
|
+
service.client_message(msg)
|
33
|
+
elsif msg.args.first != 'add-device'
|
34
|
+
# TODO: return error to client?
|
35
|
+
else
|
36
|
+
service = PushService.new(bouncer, msg)
|
37
|
+
# This will replace the old one for the same device, if it exists
|
38
|
+
active_instances[service.device_token] = service
|
39
|
+
client.data(self)[:instance] = service
|
40
|
+
end
|
41
|
+
false
|
42
|
+
else
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.server_msg(bouncer, msg)
|
48
|
+
active_instances.each { |token, service| service.handle_message(msg) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.stop_service(push_service)
|
52
|
+
active_instances.delete(push_service.device_token)
|
53
|
+
end
|
54
|
+
|
55
|
+
def log_name
|
56
|
+
"#{@bouncer.log_name}:#{@device_token}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(bouncer, add_device_msg)
|
60
|
+
@bouncer = bouncer
|
61
|
+
@device_token, @device_name = add_device_msg.args[1,2]
|
62
|
+
end
|
63
|
+
|
64
|
+
def client_message(msg)
|
65
|
+
raise("only push plz") unless msg.command == 'PUSH'
|
66
|
+
case msg.args.first
|
67
|
+
when 'add-device'
|
68
|
+
# shouldn't get this again
|
69
|
+
when 'service'
|
70
|
+
@server, @port = msg.args[1,2].map { |a| a.downcase }
|
71
|
+
ensure_connection
|
72
|
+
when 'connection'
|
73
|
+
# TODO: what's this for
|
74
|
+
when 'highlight-word'
|
75
|
+
# TODO: custom highlight words
|
76
|
+
when 'highlight-sound'
|
77
|
+
@highlight_sound = msg.args.last
|
78
|
+
when 'message-sound'
|
79
|
+
@message_sound = msg.args.last
|
80
|
+
when 'end-device'
|
81
|
+
when 'remove-device'
|
82
|
+
self.class.stop_service(self)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_message(msg)
|
87
|
+
return unless @connection
|
88
|
+
case msg.command
|
89
|
+
when /privmsg/i
|
90
|
+
send_message(msg) if msg.args.last =~ /#{@bouncer.nick}/
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def send_message(msg)
|
95
|
+
trace "forwarding #{msg} for #{@device_token}"
|
96
|
+
sender = msg.prefix.split('!', 2).first
|
97
|
+
room = msg.args.first
|
98
|
+
|
99
|
+
args = {
|
100
|
+
'device-token' => @device_token,
|
101
|
+
'message' => msg.args.last.to_s,
|
102
|
+
'sender' => sender,
|
103
|
+
'room' => msg.args.first,
|
104
|
+
'server' => 'blah',
|
105
|
+
'badge' => 1,
|
106
|
+
}
|
107
|
+
args['sound'] = @message_sound if @message_sound
|
108
|
+
@connection.send_data(args.to_json) if @connection
|
109
|
+
end
|
110
|
+
|
111
|
+
def ensure_connection
|
112
|
+
@connection = self.class.connections[[@server, @port]] ||=
|
113
|
+
EM.connect(@server, @port, PushServiceConnection, self, @server, @port)
|
114
|
+
end
|
115
|
+
|
116
|
+
def lost_connection
|
117
|
+
self.class.connections.delete([@server, @port])
|
118
|
+
@connection = nil
|
119
|
+
ensure_connection
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
module PushServiceConnection
|
124
|
+
include Tkellem::EasyLogger
|
125
|
+
|
126
|
+
def initialize(service, server, port)
|
127
|
+
@service = service
|
128
|
+
@server = server
|
129
|
+
@port = port
|
130
|
+
end
|
131
|
+
|
132
|
+
def post_init
|
133
|
+
start_tls :verify_peer => false
|
134
|
+
end
|
135
|
+
|
136
|
+
def log_name
|
137
|
+
"#{@server}:#{@port}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def ssl_handshake_completed
|
141
|
+
debug "connected to push service #{@server}:#{@port}"
|
142
|
+
@connected = true
|
143
|
+
end
|
144
|
+
|
145
|
+
def unbind
|
146
|
+
debug "lost connection to push service #{@server}:#{@port}"
|
147
|
+
@connected = false
|
148
|
+
@service.lost_connection
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'tkellem/tkellem_bot'
|
4
|
+
|
5
|
+
module Tkellem
|
6
|
+
|
7
|
+
# listens on the unix domain socket and executes admin commands
|
8
|
+
module SocketServer
|
9
|
+
include EM::Protocols::LineText2
|
10
|
+
include Tkellem::EasyLogger
|
11
|
+
|
12
|
+
def log_name
|
13
|
+
"admin"
|
14
|
+
end
|
15
|
+
|
16
|
+
def post_init
|
17
|
+
set_delimiter "\n"
|
18
|
+
end
|
19
|
+
|
20
|
+
def receive_line(line)
|
21
|
+
trace "admin socket: #{line}"
|
22
|
+
TkellemBot.run_command(line, nil) do |line|
|
23
|
+
send_data("#{line}\n")
|
24
|
+
end
|
25
|
+
send_data("\0\n")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Tkellem
|
5
|
+
|
6
|
+
class TkellemBot
|
7
|
+
# careful here -- if no user is given, it's assumed the command is running as
|
8
|
+
# an admin
|
9
|
+
def self.run_command(line, user, &block)
|
10
|
+
args = Shellwords.shellwords(line.downcase)
|
11
|
+
command_name = args.shift.upcase
|
12
|
+
command = commands[command_name]
|
13
|
+
|
14
|
+
unless command
|
15
|
+
yield "Invalid command. Use help for a command listing."
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
command.run(args, user, block)
|
20
|
+
end
|
21
|
+
|
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
|
36
|
+
end
|
37
|
+
|
38
|
+
def options
|
39
|
+
self.class.options
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.register(cmd_name)
|
43
|
+
cattr_accessor :name
|
44
|
+
self.name = cmd_name
|
45
|
+
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
|
+
end
|
49
|
+
|
50
|
+
def self.resources(name)
|
51
|
+
@resources ||= YAML.load_file(File.expand_path("../../../resources/bot_command_descriptions.yml", __FILE__))
|
52
|
+
@resources[name.upcase] || {}
|
53
|
+
end
|
54
|
+
|
55
|
+
class ArgumentError < RuntimeError; end
|
56
|
+
|
57
|
+
def self.admin_only?
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.run(args_arr, user, block)
|
62
|
+
if admin_only? && !admin_user?(user)
|
63
|
+
block.call "You can only run #{name} as an admin."
|
64
|
+
return
|
65
|
+
end
|
66
|
+
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
|
72
|
+
cmd.respond e.to_s
|
73
|
+
cmd.show_help
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(responder)
|
77
|
+
@responder = responder
|
78
|
+
@args = {}
|
79
|
+
end
|
80
|
+
|
81
|
+
def show_help
|
82
|
+
respond(options.to_s)
|
83
|
+
end
|
84
|
+
|
85
|
+
def respond(text)
|
86
|
+
text.each_line { |l| @responder.call(l.chomp) }
|
87
|
+
end
|
88
|
+
alias_method :r, :respond
|
89
|
+
|
90
|
+
def self.admin_user?(user)
|
91
|
+
!user || user.admin?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
cattr_accessor :commands
|
96
|
+
self.commands = {}
|
97
|
+
|
98
|
+
class Help < Command
|
99
|
+
register 'help'
|
100
|
+
|
101
|
+
def execute(args, user)
|
102
|
+
name = args[:rest].first
|
103
|
+
r "**** tkellem help ****"
|
104
|
+
if name.nil?
|
105
|
+
r "For more information on a command, type:"
|
106
|
+
r "help <command>"
|
107
|
+
r ""
|
108
|
+
r "The following commands are available:"
|
109
|
+
TkellemBot.commands.keys.sort.each do |name|
|
110
|
+
command = TkellemBot.commands[name]
|
111
|
+
next if command.admin_only? && user && !user.admin?
|
112
|
+
r "#{name}#{' ' * (25-name.length)}"
|
113
|
+
end
|
114
|
+
elsif (command = TkellemBot.commands[name.upcase])
|
115
|
+
r "Help for #{command.name}:"
|
116
|
+
r ""
|
117
|
+
r command.options.to_s
|
118
|
+
else
|
119
|
+
r "No help available for #{args.first.upcase}."
|
120
|
+
end
|
121
|
+
r "**** end of help ****"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class CRUDCommand < Command
|
126
|
+
def self.register_crud(name, model)
|
127
|
+
register(name)
|
128
|
+
cattr_accessor :model
|
129
|
+
self.model = model
|
130
|
+
options.set('add', '--add', '-a', "Add a #{model.name}")
|
131
|
+
options.set('remove', '--remove', '-r', "Remove a #{model.name}")
|
132
|
+
options.set('list', '--list', '-l', "List the current #{model.name.pluralize}")
|
133
|
+
end
|
134
|
+
|
135
|
+
def show(m)
|
136
|
+
m.to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
def find_attributes(args, user)
|
140
|
+
attributes(args, user)
|
141
|
+
end
|
142
|
+
|
143
|
+
def list(args, user)
|
144
|
+
r "All #{self.class.name.pluralize}:"
|
145
|
+
model.all.each { |m| r " #{show(m)}" }
|
146
|
+
end
|
147
|
+
|
148
|
+
def remove(args, user)
|
149
|
+
instance = model.first(:conditions => find_attributes(args, user))
|
150
|
+
if instance
|
151
|
+
instance.destroy
|
152
|
+
respond "Removed #{show(instance)}"
|
153
|
+
else
|
154
|
+
respond "Not found"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def add(args, user)
|
159
|
+
instance = model.create(attributes(args, user))
|
160
|
+
if instance.errors.any?
|
161
|
+
respond "Errors creating:"
|
162
|
+
instance.errors.full_messages.each { |m| respond " #{m}" }
|
163
|
+
else
|
164
|
+
respond "#{show(instance)} added"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
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)
|
175
|
+
else
|
176
|
+
raise Command::ArgumentError, "Unknown sub-command"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class ListenCommand < CRUDCommand
|
182
|
+
register_crud 'listen', ListenAddress
|
183
|
+
|
184
|
+
def self.admin_only?
|
185
|
+
true
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.get_uri(args)
|
189
|
+
require 'uri'
|
190
|
+
uri = URI.parse(args[:rest].first)
|
191
|
+
unless %w(irc ircs).include?(uri.scheme)
|
192
|
+
raise Command::ArgumentError, "Invalid URI scheme: #{uri}"
|
193
|
+
end
|
194
|
+
uri
|
195
|
+
rescue URI::InvalidURIError
|
196
|
+
raise Command::ArgumentError, "Invalid new address: #{args[:rest].first}"
|
197
|
+
end
|
198
|
+
|
199
|
+
def attributes(args, user)
|
200
|
+
uri = self.class.get_uri(args)
|
201
|
+
{ :address => uri.host, :port => uri.port, :ssl => (uri.scheme == 'ircs') }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class UserCommand < CRUDCommand
|
206
|
+
register_crud 'user', User
|
207
|
+
|
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')
|
214
|
+
|
215
|
+
def show(user)
|
216
|
+
"#{user.username}:#{user.role}"
|
217
|
+
end
|
218
|
+
|
219
|
+
def find_attributes(args, user)
|
220
|
+
{ :username => args[:rest].first }
|
221
|
+
end
|
222
|
+
|
223
|
+
def attributes(args, user)
|
224
|
+
find_attributes(args).merge({ :role => (args['admin'] ? 'admin' : 'user') })
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
class PasswordCommand < Command
|
229
|
+
register 'password'
|
230
|
+
|
231
|
+
options.set('username', '--user=username', '-u', 'Change password for other username')
|
232
|
+
|
233
|
+
def execute(args, user)
|
234
|
+
if args['username']
|
235
|
+
if Command.admin_user?(user)
|
236
|
+
user = User.first(:conditions => { :username => args['username'] })
|
237
|
+
else
|
238
|
+
raise Command::ArgumentError, "Only admins can change other passwords"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
unless user
|
243
|
+
raise Command::ArgumentError, "User required"
|
244
|
+
end
|
245
|
+
|
246
|
+
password = args[:rest].shift || ''
|
247
|
+
|
248
|
+
if password.size < 4
|
249
|
+
raise Command::ArgumentError, "New password too short"
|
250
|
+
end
|
251
|
+
|
252
|
+
user.set_password!(password)
|
253
|
+
respond "New password set for #{user.username}"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class NetworkCommand < CRUDCommand
|
258
|
+
register_crud 'network', Host
|
259
|
+
|
260
|
+
options.set('public', '--public', 'Set new network as public')
|
261
|
+
options.set('username', '--user=username', '-u', 'Create a user-specific network for another user')
|
262
|
+
|
263
|
+
def list(args, user)
|
264
|
+
r "All networks:"
|
265
|
+
Network.all.each { |m| r " #{show(m.hosts.first)}" if m.hosts.first }
|
266
|
+
end
|
267
|
+
|
268
|
+
def show(host)
|
269
|
+
"#{host.network.name}#{' (public)' if host.network.public?} " + host.network.hosts.map { |h| "[#{h}]" }.join(' ')
|
270
|
+
end
|
271
|
+
|
272
|
+
def get_network(args, user)
|
273
|
+
network_name = args[:rest].shift
|
274
|
+
if args['username']
|
275
|
+
if Command.admin_user?(user)
|
276
|
+
user = User.first(:conditions => { :username => args['username'] })
|
277
|
+
else
|
278
|
+
raise Command::ArgumentError, "Only admins can change other user's networks"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
network = Network.first(:conditions => { :name => network_name, :user_id => user.id }) if user
|
283
|
+
network ||= Network.first(:conditions => { :name => network_name, :user_id => nil })
|
284
|
+
if network && network.public? && !self.class.admin_user?(user)
|
285
|
+
raise Command::ArgumentError, "Only admins can modify public networks"
|
286
|
+
end
|
287
|
+
return network_name, network, user
|
288
|
+
end
|
289
|
+
|
290
|
+
def remove(args, user)
|
291
|
+
network_name, network, user = get_network(args, user)
|
292
|
+
if network
|
293
|
+
Host.all(:conditions => { :network_id => network.id }).each(&:destroy)
|
294
|
+
network.destroy
|
295
|
+
respond "Removed #{network.name} #{show(network.hosts.first) if network.hosts.first}"
|
296
|
+
else
|
297
|
+
respond "Not found"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def attributes(args, user)
|
302
|
+
network_name, network, user = get_network(args, user)
|
303
|
+
|
304
|
+
unless network
|
305
|
+
create_public = !user || (user.admin? && args['public'])
|
306
|
+
network = Network.create(:name => network_name, :user => (create_public ? nil : user))
|
307
|
+
unless create_public
|
308
|
+
NetworkUser.create(:user => user, :network => network)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
uri = ListenCommand.get_uri(args)
|
313
|
+
{ :network => network, :address => uri.host, :port => uri.port, :ssl => (uri.scheme == 'ircs') }
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|