vines-services 0.1.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 (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