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.
Files changed (44) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +28 -6
  4. data/Rakefile +14 -5
  5. data/bin/tkellem +2 -107
  6. data/debian/changelog +5 -0
  7. data/debian/compat +1 -0
  8. data/debian/control +18 -0
  9. data/debian/copyright +42 -0
  10. data/debian/docs +1 -0
  11. data/debian/examples +1 -0
  12. data/debian/install +3 -0
  13. data/debian/manpages +1 -0
  14. data/debian/rules +13 -0
  15. data/debian/source/format +1 -0
  16. data/debian/tkellem.1 +11 -0
  17. data/examples/config.yml +2 -29
  18. data/lib/tkellem/bouncer.rb +196 -31
  19. data/lib/tkellem/bouncer_connection.rb +90 -85
  20. data/lib/tkellem/daemon.rb +155 -0
  21. data/lib/tkellem/irc_message.rb +58 -0
  22. data/lib/tkellem/irc_server.rb +8 -121
  23. data/lib/tkellem/migrations/001_init_db.rb +33 -0
  24. data/lib/tkellem/models/host.rb +11 -0
  25. data/lib/tkellem/models/listen_address.rb +12 -0
  26. data/lib/tkellem/models/network.rb +15 -0
  27. data/lib/tkellem/models/network_user.rb +12 -0
  28. data/lib/tkellem/models/user.rb +56 -0
  29. data/lib/tkellem/plugins/backlog.rb +160 -0
  30. data/lib/tkellem/plugins/push_service.rb +152 -0
  31. data/lib/tkellem/socket_server.rb +29 -0
  32. data/lib/tkellem/tkellem_bot.rb +318 -0
  33. data/lib/tkellem/tkellem_server.rb +114 -0
  34. data/lib/tkellem/version.rb +3 -0
  35. data/lib/tkellem.rb +7 -10
  36. data/resources/bot_command_descriptions.yml +36 -0
  37. data/spec/irc_message_spec.rb +47 -0
  38. data/spec/irc_server_spec.rb +60 -0
  39. data/spec/spec_helper.rb +0 -2
  40. data/tkellem.gemspec +20 -47
  41. metadata +118 -22
  42. data/VERSION +0 -1
  43. data/lib/tkellem/backlog.rb +0 -85
  44. 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