vines-services 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +19 -0
  2. data/README +40 -0
  3. data/Rakefile +130 -0
  4. data/bin/vines-services +95 -0
  5. data/conf/config.rb +25 -0
  6. data/lib/vines/services/command/init.rb +209 -0
  7. data/lib/vines/services/command/restart.rb +14 -0
  8. data/lib/vines/services/command/start.rb +30 -0
  9. data/lib/vines/services/command/stop.rb +20 -0
  10. data/lib/vines/services/command/views.rb +26 -0
  11. data/lib/vines/services/component.rb +26 -0
  12. data/lib/vines/services/config.rb +105 -0
  13. data/lib/vines/services/connection.rb +120 -0
  14. data/lib/vines/services/controller/attributes_controller.rb +19 -0
  15. data/lib/vines/services/controller/base_controller.rb +99 -0
  16. data/lib/vines/services/controller/disco_info_controller.rb +61 -0
  17. data/lib/vines/services/controller/labels_controller.rb +17 -0
  18. data/lib/vines/services/controller/members_controller.rb +44 -0
  19. data/lib/vines/services/controller/messages_controller.rb +66 -0
  20. data/lib/vines/services/controller/probes_controller.rb +45 -0
  21. data/lib/vines/services/controller/services_controller.rb +81 -0
  22. data/lib/vines/services/controller/subscriptions_controller.rb +39 -0
  23. data/lib/vines/services/controller/systems_controller.rb +45 -0
  24. data/lib/vines/services/controller/transfers_controller.rb +58 -0
  25. data/lib/vines/services/controller/uploads_controller.rb +62 -0
  26. data/lib/vines/services/controller/users_controller.rb +127 -0
  27. data/lib/vines/services/core_ext/blather.rb +46 -0
  28. data/lib/vines/services/core_ext/couchrest.rb +33 -0
  29. data/lib/vines/services/indexer.rb +195 -0
  30. data/lib/vines/services/priority_queue.rb +94 -0
  31. data/lib/vines/services/roster.rb +70 -0
  32. data/lib/vines/services/storage/couchdb/fragment.rb +23 -0
  33. data/lib/vines/services/storage/couchdb/service.rb +170 -0
  34. data/lib/vines/services/storage/couchdb/system.rb +141 -0
  35. data/lib/vines/services/storage/couchdb/upload.rb +66 -0
  36. data/lib/vines/services/storage/couchdb/user.rb +137 -0
  37. data/lib/vines/services/storage/couchdb/vcard.rb +13 -0
  38. data/lib/vines/services/storage/couchdb.rb +157 -0
  39. data/lib/vines/services/storage.rb +33 -0
  40. data/lib/vines/services/throttle.rb +26 -0
  41. data/lib/vines/services/version.rb +7 -0
  42. data/lib/vines/services/vql/compiler.rb +94 -0
  43. data/lib/vines/services/vql/vql.citrus +115 -0
  44. data/lib/vines/services/vql/vql.rb +186 -0
  45. data/lib/vines/services.rb +71 -0
  46. data/test/config_test.rb +242 -0
  47. data/test/priority_queue_test.rb +23 -0
  48. data/test/storage/couchdb_test.rb +30 -0
  49. data/test/vql/compiler_test.rb +96 -0
  50. data/test/vql/vql_test.rb +233 -0
  51. data/web/coffeescripts/api.coffee +51 -0
  52. data/web/coffeescripts/commands.coffee +18 -0
  53. data/web/coffeescripts/files.coffee +315 -0
  54. data/web/coffeescripts/init.coffee +21 -0
  55. data/web/coffeescripts/services.coffee +356 -0
  56. data/web/coffeescripts/setup.coffee +503 -0
  57. data/web/coffeescripts/systems.coffee +371 -0
  58. data/web/images/default-service.png +0 -0
  59. data/web/images/linux.png +0 -0
  60. data/web/images/mac.png +0 -0
  61. data/web/images/run.png +0 -0
  62. data/web/images/windows.png +0 -0
  63. data/web/index.html +17 -0
  64. data/web/stylesheets/common.css +52 -0
  65. data/web/stylesheets/files.css +218 -0
  66. data/web/stylesheets/services.css +181 -0
  67. data/web/stylesheets/setup.css +117 -0
  68. data/web/stylesheets/systems.css +142 -0
  69. metadata +230 -0
@@ -0,0 +1,105 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ # A Config object is passed to the xmpp connections to give them access
6
+ # to server configuration information like component subdomain host names,
7
+ # storage systems, etc. This class provides the DSL methods used in the
8
+ # conf/config.rb file.
9
+ class Config
10
+ LOG_LEVELS = %w[debug info warn error fatal].freeze
11
+
12
+ attr_reader :vhosts
13
+
14
+ @@instance = nil
15
+ def self.configure(&block)
16
+ @@instance = self.new(&block)
17
+ end
18
+
19
+ def self.instance
20
+ @@instance
21
+ end
22
+
23
+ def initialize(&block)
24
+ @vhosts = {}
25
+ instance_eval(&block)
26
+ raise "must define at least one virtual host" if @vhosts.empty?
27
+ end
28
+
29
+ def host(*names, &block)
30
+ names = names.flatten.map {|name| name.downcase }
31
+ dupes = names.uniq.size != names.size || (@vhosts.keys & names).any?
32
+ raise "one host definition per domain allowed" if dupes
33
+ names.each do |name|
34
+ @vhosts[name] = Host.new(name, &block)
35
+ end
36
+ end
37
+
38
+ def log(level)
39
+ const = Logger.const_get(level.to_s.upcase) rescue nil
40
+ unless LOG_LEVELS.include?(level.to_s) && const
41
+ raise "log level must be one of: #{LOG_LEVELS.join(', ')}"
42
+ end
43
+ log = Class.new.extend(Vines::Log).log
44
+ log.progname = 'vines-service'
45
+ log.level = const
46
+ Blather.logger.level = const
47
+ end
48
+
49
+ def hosts
50
+ @vhosts.values
51
+ end
52
+
53
+ class Host
54
+ attr_reader :name
55
+
56
+ def initialize(name, &block)
57
+ @name, @storage, @uploads, @upstream = name, nil, nil, []
58
+ instance_eval(&block)
59
+ raise "storage required for #{@name}" unless @storage
60
+ raise "upstream connection required for #{@name}" if @upstream.empty?
61
+ unless @uploads
62
+ @uploads = File.expand_path('data/upload')
63
+ FileUtils.mkdir_p(@uploads)
64
+ end
65
+ end
66
+
67
+ def uploads(dir=nil)
68
+ return @uploads unless dir
69
+ @uploads = File.expand_path(dir)
70
+ begin
71
+ FileUtils.mkdir_p(@uploads)
72
+ rescue
73
+ raise "can't create #{@uploads}"
74
+ end
75
+ end
76
+
77
+ def storage(name=nil, &block)
78
+ if name
79
+ raise "one storage mechanism per host allowed" if @storage
80
+ @storage = Storage.from_name(name, &block)
81
+ else
82
+ @storage
83
+ end
84
+ end
85
+
86
+ def upstream(host, port, password)
87
+ raise 'host, port, and password required for upstream connections' unless
88
+ host && port && password
89
+ @upstream << {host: host, port: port, password: password}
90
+ end
91
+
92
+ def start
93
+ @upstream.each do |info|
94
+ stream = Vines::Services::Connection.new(
95
+ host: info[:host],
96
+ port: info[:port],
97
+ password: info[:password],
98
+ vhost: self)
99
+ stream.start
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,120 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ # Connects the component process to the chat server and provides the protocol
6
+ # used by the web user interface and the agents.
7
+ class Connection
8
+ include Vines::Log
9
+
10
+ @@controllers = []
11
+ def self.register(*args, klass)
12
+ @@controllers << [args, klass]
13
+ end
14
+
15
+ def initialize(options)
16
+ host, port, password, @vhost = *options.values_at(:host, :port, :password, :vhost)
17
+ @stream = Blather::Client.setup(@vhost.name, password, host, port)
18
+ @throttle = Throttle.new(@stream)
19
+ @queues = {}
20
+
21
+ @stream.register_handler(:disconnected) do
22
+ log.info("Stream disconnected, reconnecting . . .")
23
+ EM::Timer.new(10) do
24
+ self.class.new(options).start
25
+ end
26
+ true # prevent EM.stop
27
+ end
28
+
29
+ @stream.register_handler(:ready) do
30
+ log.info("Connected #{@stream.jid} component to #{host}:#{port}")
31
+ Fiber.new do
32
+ broadcast_presence
33
+ end.resume
34
+ end
35
+
36
+ @stream.register_handler(:iq, '/iq[@type="get"]/ns:query', :ns => 'jabber:iq:version') do |node|
37
+ if node.to == @stream.jid
38
+ iq = Blather::Stanza::Iq::Query.new(:result)
39
+ iq.id, iq.to = node.id, node.from
40
+ iq.query.add_child("<name>Vines Services</name>")
41
+ iq.query.add_child("<version>#{VERSION}</version>")
42
+ @stream.write(iq)
43
+ end
44
+ end
45
+
46
+ @@controllers.each do |args, klass|
47
+ @stream.register_handler(*args) do |node|
48
+ jid = node.from.to_s
49
+ start = !@queues.key?(jid)
50
+ queue = (@queues[jid] ||= EM::Queue.new)
51
+ queue.push([node, klass])
52
+ process_node_queue(jid) if start
53
+ end
54
+ end
55
+ end
56
+
57
+ def start
58
+ @stream.run
59
+ end
60
+
61
+ private
62
+
63
+ # Send initial presence from each service JID to its users when the
64
+ # component starts.
65
+ def broadcast_presence
66
+ services = CouchModels::Service.find_all
67
+ users = services.map {|service| service.users }.flatten.uniq
68
+ nodes = users.map {|jid| available(@stream.jid, jid) }
69
+ nodes << services.map do |service|
70
+ service.users.map {|jid| available(service.jid, jid) }
71
+ end
72
+ nodes.flatten!
73
+ @throttle.async_send(nodes)
74
+ end
75
+
76
+ def available(from, to)
77
+ Blather::Stanza::Presence::Status.new.tap do |node|
78
+ node.from = from
79
+ node.to = to
80
+ end
81
+ end
82
+
83
+ # We must process or deliver stanzas in the order they are received, so
84
+ # we create a node queue for each sending JID and process it in this loop.
85
+ #
86
+ # The process loop continues until the queue is empty for this JID, then
87
+ # the queue is deleted. JID's come and go frequently, without notifying the
88
+ # component, so we must delete their queue as soon as it's empty to avoid
89
+ # maintaining a growing list of queues for JID's we will never see again.
90
+ #
91
+ # Each node is wrapped its own Fiber so it can be paused and resumed
92
+ # during asynchronous IO.
93
+ def process_node_queue(jid)
94
+ if @queues[jid].empty?
95
+ @queues.delete(jid)
96
+ else
97
+ @queues[jid].pop do |pair|
98
+ Fiber.new do
99
+ process_node(*pair)
100
+ process_node_queue(jid)
101
+ end.resume
102
+ end
103
+ end
104
+ end
105
+
106
+ # Create a new controller instance to process the node.
107
+ def process_node(node, klass)
108
+ begin
109
+ klass.new(node, @stream, @vhost).process
110
+ rescue Forbidden
111
+ log.warn("Authorization failed for #{node.from}:\n#{node}")
112
+ @stream.write(Blather::StanzaError.new(node, 'forbidden', :auth).to_node)
113
+ rescue Exception => e
114
+ log.error("Error processing node: #{e.message}")
115
+ @stream.write(Blather::StanzaError.new(node, 'internal-server-error', :cancel).to_node)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class AttributesController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/systems/attributes'
9
+
10
+ private
11
+
12
+ def get
13
+ forbidden! unless current_user.manage_services?
14
+ send_result(rows: System.find_attributes)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,99 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class BaseController
7
+ include Vines::Log
8
+ include CouchModels
9
+ include Nokogiri::XML
10
+
11
+ def self.register(*args)
12
+ Connection.register(*args, self)
13
+ end
14
+
15
+ attr_reader :storage, :node, :stream, :uploads
16
+
17
+ def initialize(node, stream, vhost)
18
+ @node, @stream, @storage = node, stream, vhost.storage
19
+ @uploads = vhost.uploads
20
+ @current_user = nil
21
+ end
22
+
23
+ def process
24
+ # must be addressed to component, not a service
25
+ return unless to_component?
26
+ if node.get?
27
+ get
28
+ elsif node.set? && node.elements.first['action'] == 'delete'
29
+ delete
30
+ elsif node.set?
31
+ save
32
+ end
33
+ rescue Forbidden
34
+ raise
35
+ rescue
36
+ send_error('not-acceptable')
37
+ end
38
+
39
+ private
40
+
41
+ def get
42
+ send_error('feature-not-implemented')
43
+ end
44
+
45
+ def save
46
+ send_error('feature-not-implemented')
47
+ end
48
+
49
+ def delete
50
+ send_error('feature-not-implemented')
51
+ end
52
+
53
+ def send_result(obj=nil)
54
+ iq = Blather::Stanza::Iq::Query.new(:result).tap do |result|
55
+ result.id, result.to, result.from = node.id, node.from, node.to
56
+ result.query.content = obj.to_json if obj
57
+ result.query.namespace = node.elements.first.namespace.href
58
+ end
59
+ stream.write(iq)
60
+ end
61
+
62
+ def send_error(condition, obj=nil)
63
+ err = Blather::StanzaError.new(node, condition, :modify).tap do |error|
64
+ error.extras << Blather::XMPPNode.new('vines-error').tap do |verr|
65
+ verr.namespace = 'http://getvines.com/error'
66
+ verr.content = obj.to_json
67
+ end if obj
68
+ end
69
+ stream.write(err.to_node)
70
+ end
71
+
72
+ def send_doc(doc)
73
+ if doc
74
+ send_result(doc.to_result)
75
+ else
76
+ send_error('item-not-found')
77
+ end
78
+ end
79
+
80
+ # Return true if the stanza is addressed to the component's JID rather
81
+ # than to a service JID.
82
+ def to_component?
83
+ node.to == stream.jid
84
+ end
85
+
86
+ def forbidden!
87
+ raise Forbidden
88
+ end
89
+
90
+ # Return the User object for the user that sent this stanza. Useful for
91
+ # permission authorization checks before performing server actions.
92
+ def current_user
93
+ jid = node.from.stripped.to_s.downcase
94
+ @current_user ||= User.find_by_jid(jid)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class DiscoInfoController < BaseController
7
+ register :disco_info, :get?
8
+
9
+ def process
10
+ reply = (node.to == stream.jid) ? component : service
11
+ stream.write(reply) if reply
12
+ end
13
+
14
+ private
15
+
16
+ # Return the discovery reply node for a query addressed to the
17
+ # component JID itself (e.g. vines.wonderland.lit), rather than to a
18
+ # service JID. Advertise the http://getvines.com/protocol feature
19
+ # so agents can discover the component JID with which to communicate.
20
+ def component
21
+ disco_node.tap do |disco|
22
+ disco.identities = {
23
+ name: 'Vines Services',
24
+ type: 'bot',
25
+ category: 'component'
26
+ }
27
+ disco.features = %w[
28
+ http://jabber.org/protocol/bytestreams
29
+ http://jabber.org/protocol/disco#info
30
+ http://jabber.org/protocol/si
31
+ http://jabber.org/protocol/si/profile/file-transfer
32
+ http://jabber.org/protocol/xhtml-im
33
+ http://getvines.com/protocol
34
+ jabber:iq:version
35
+ ]
36
+ end
37
+ end
38
+
39
+ # Return the discovery reply node for a query addressed to a service
40
+ # JID (e.g. web_servers@vines.wonderland.lit). Ignore, rather than send
41
+ # an error, for queries to service JID's that don't exist or to which
42
+ # the user doesn't have access.
43
+ def service
44
+ found = Service.find_by_jid(node.to.to_s)
45
+ return unless found && found.users.include?(node.from.stripped.to_s)
46
+ disco_node.tap do |disco|
47
+ disco.features = %w[http://jabber.org/protocol/xhtml-im]
48
+ end
49
+ end
50
+
51
+ def disco_node
52
+ Blather::Stanza::DiscoInfo.new(:result).tap do |disco|
53
+ disco.id = node.id
54
+ disco.to = node.from
55
+ disco.from = node.to
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class LabelsController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/files/labels'
9
+
10
+ def get
11
+ forbidden! unless current_user.manage_files?
12
+ send_result(rows: Upload.find_labels)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class MembersController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/services/members'
9
+
10
+ private
11
+
12
+ def get
13
+ forbidden! unless current_user.manage_services?
14
+ if id = node.elements.first['id']
15
+ rows = Service.find(id).members rescue []
16
+ send_result(rows: rows)
17
+ else
18
+ find_members_by_vql
19
+ end
20
+ end
21
+
22
+ # Compile the VQL syntax into a SQL query to find matching member
23
+ # systems. Return a list of members or the parser error so the user
24
+ # can debug their query syntax.
25
+ def find_members_by_vql
26
+ begin
27
+ code = node.elements.first.content
28
+ sql, params = VQL::Compiler.new.to_sql(code)
29
+ query(sql, params)
30
+ rescue Exception => e
31
+ send_result(ok: false, error: e.message)
32
+ end
33
+ end
34
+
35
+ def query(sql, params)
36
+ storage.query(sql, params) do |rows|
37
+ members = rows.map {|row| {name: row[0], os: row[1]} }
38
+ send_result(ok: true, rows: members)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ # Broadcast messages from a user to a group of systems (a.k.a. a service).
7
+ # Responses from the agents are routed back through the component so the
8
+ # user appears to be talking to just the service's JID, not each and every
9
+ # agent that belongs to the service.
10
+ class MessagesController < BaseController
11
+ register :message, :chat?, :body
12
+
13
+ NS = 'http://getvines.com/protocol'.freeze
14
+
15
+ def process
16
+ current_user.system? ? forward_to_user : forward_to_service
17
+ end
18
+
19
+ private
20
+
21
+ # Forward the agent's response message to the user that sent it the
22
+ # command.
23
+ def forward_to_user
24
+ jid = node.xpath('/message/ns:jid', 'ns' => NS).first
25
+ return unless jid
26
+ jid.remove
27
+ node.from = node.to
28
+ node.to = jid.content
29
+ stream.write(node)
30
+ end
31
+
32
+ # Forward the user's message to the members of the service. Tag the
33
+ # message with a jid element, identifying the user executing the command
34
+ # like: <jid xmlns="http://getvines.com/protocol">alice@wonderland.lit/tea</jid>
35
+ # When the agent receives a message from one of its services, it checks
36
+ # this jid element for the user permissions with which to run the command.
37
+ #
38
+ # Ignore the message, rather than send an error, if the user lacks
39
+ # privilege to access the service to avoid directory harvesting.
40
+ def forward_to_service
41
+ service = Service.find_by_jid(node.to.stripped)
42
+ if service && service.user?(node.from.stripped)
43
+ service.members.each do |member|
44
+ to = Blather::JID.new(member['name'], node.from.domain)
45
+ stream.write(create_message(to))
46
+ end
47
+ else
48
+ log.warn("#{node.from} denied access to #{node.to}")
49
+ end
50
+ end
51
+
52
+ # Copy the message's content into a new message destined for the given
53
+ # agent JID. Identify this command/response exchange with the message's
54
+ # thread value. Tag the message with a jid element, identifying the
55
+ # original user that sent the command.
56
+ def create_message(to)
57
+ Blather::Stanza::Message.new(to, node.body).tap do |msg|
58
+ msg.thread = node.thread || Kit.uuid
59
+ msg.from = node.to
60
+ msg << msg.document.create_element('jid', node.from, xmlns: NS)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,45 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ # Reply to presence probes if the user has privilege to access the
7
+ # requested service JID. Reply to all probes to the component itself.
8
+ class ProbesController < BaseController
9
+ register :presence, :probe?
10
+
11
+ def process
12
+ from, to = node.from.stripped, node.to.stripped
13
+ if approved?
14
+ stream.write(available(to, from))
15
+ else
16
+ stream.write(unsubscribed(to, from))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def approved?
23
+ return true if to_component?
24
+ found = Service.find_by_jid(node.to)
25
+ found && found.user?(node.from.stripped)
26
+ end
27
+
28
+ def available(from, to)
29
+ Blather::Stanza::Presence::Status.new.tap do |stanza|
30
+ stanza.from = from
31
+ stanza.to = to
32
+ end
33
+ end
34
+
35
+ def unsubscribed(from, to)
36
+ Blather::Stanza::Presence::Subscription.new.tap do |stanza|
37
+ stanza.type = :unsubscribed
38
+ stanza.from = from
39
+ stanza.to = to
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,81 @@
1
+ # encoding: UTF-8
2
+
3
+ module Vines
4
+ module Services
5
+ module Controller
6
+ class ServicesController < BaseController
7
+ register :iq, "/iq[@type='get' or @type='set']/ns:query",
8
+ 'ns' => 'http://getvines.com/protocol/services'
9
+
10
+ private
11
+
12
+ def get
13
+ forbidden! unless current_user.manage_services?
14
+
15
+ if id = node.elements.first['id']
16
+ send_doc(Service.find(id))
17
+ elsif name = node.elements.first['name']
18
+ send_doc(Service.find_by_name(name))
19
+ else
20
+ rows = Service.find_all.map do |doc|
21
+ {id: doc.id, name: doc.name, size: doc.size}
22
+ end
23
+ send_result(rows: rows)
24
+ end
25
+ end
26
+
27
+ def save
28
+ forbidden! unless current_user.manage_services?
29
+
30
+ obj = JSON.parse(node.elements.first.content)
31
+ raise 'name required' unless obj['name']
32
+ obj['jid'] = to_jid(obj['name'])
33
+ obj['users'] = validate_users(obj['users'])
34
+
35
+ begin
36
+ compiled = VQL::Compiler.new.to_js(obj['code'])
37
+ rescue Exception => e
38
+ send_error('not-acceptable', {error: e.message})
39
+ return
40
+ end
41
+
42
+ service = Service.find(obj['id']) || Service.new
43
+ members = service.members
44
+ if service.update_attributes(obj)
45
+ send_result(service.to_result)
46
+ members << Service.find(service.id).members
47
+ System.notify_members(stream, node.from, members.flatten.uniq)
48
+ else
49
+ send_error('not-acceptable')
50
+ end
51
+ end
52
+
53
+ # Ensure the JID's that are given access to this service actually exist
54
+ # and are not system accounts. System users may never have access to
55
+ # services. Return the list of JID's that pass validation.
56
+ def validate_users(jids)
57
+ return [] unless jids
58
+ users = User.by_jid.keys(jids)
59
+ users.map {|u| u.system? ? nil : u.jid }.compact
60
+ end
61
+
62
+ # Create a JID for the service from its given name so we can address
63
+ # stanzas to the service.
64
+ def to_jid(name)
65
+ Blather::JID.new(CGI.escape(name), stream.jid.domain).to_s.downcase
66
+ end
67
+
68
+ def delete
69
+ forbidden! unless current_user.manage_services?
70
+
71
+ if service = Service.find(node.elements.first['id'])
72
+ service.destroy
73
+ send_result
74
+ else
75
+ send_error('item-not-found')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end