jabber_admin 0.1.1 → 1.0.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 (42) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +30 -0
  3. data/.gitignore +4 -2
  4. data/.rubocop.yml +35 -0
  5. data/.simplecov +5 -0
  6. data/.travis.yml +8 -6
  7. data/.yardopts +4 -0
  8. data/CHANGELOG.md +49 -0
  9. data/Gemfile +4 -2
  10. data/Makefile +108 -0
  11. data/README.md +145 -21
  12. data/Rakefile +5 -3
  13. data/bin/config.rb +11 -0
  14. data/bin/console +3 -3
  15. data/docker-compose.yml +15 -0
  16. data/jabber_admin.gemspec +34 -27
  17. data/lib/jabber_admin.rb +97 -34
  18. data/lib/jabber_admin/api_call.rb +110 -20
  19. data/lib/jabber_admin/commands.rb +6 -12
  20. data/lib/jabber_admin/commands/ban_account.rb +11 -10
  21. data/lib/jabber_admin/commands/create_room.rb +15 -10
  22. data/lib/jabber_admin/commands/create_room_with_opts.rb +20 -14
  23. data/lib/jabber_admin/commands/get_vcard.rb +84 -0
  24. data/lib/jabber_admin/commands/muc_register_nick.rb +26 -0
  25. data/lib/jabber_admin/commands/register.rb +16 -11
  26. data/lib/jabber_admin/commands/registered_users.rb +18 -0
  27. data/lib/jabber_admin/commands/restart.rb +8 -5
  28. data/lib/jabber_admin/commands/send_direct_invitation.rb +19 -16
  29. data/lib/jabber_admin/commands/send_stanza.rb +21 -0
  30. data/lib/jabber_admin/commands/send_stanza_c2s.rb +28 -0
  31. data/lib/jabber_admin/commands/set_nickname.rb +22 -0
  32. data/lib/jabber_admin/commands/set_presence.rb +37 -0
  33. data/lib/jabber_admin/commands/set_room_affiliation.rb +26 -0
  34. data/lib/jabber_admin/commands/set_vcard.rb +68 -0
  35. data/lib/jabber_admin/commands/subscribe_room.rb +19 -12
  36. data/lib/jabber_admin/commands/unregister.rb +13 -7
  37. data/lib/jabber_admin/commands/unsubscribe_room.rb +10 -7
  38. data/lib/jabber_admin/configuration.rb +6 -1
  39. data/lib/jabber_admin/exceptions.rb +41 -0
  40. data/lib/jabber_admin/version.rb +19 -1
  41. metadata +120 -25
  42. data/lib/jabber_admin/initializer.rb +0 -5
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ $VERBOSE = nil
4
+ require 'bundler/setup'
5
+ require 'jabber_admin'
6
+
7
+ JabberAdmin.configure do |conf|
8
+ conf.username = 'admin@jabber.local'
9
+ conf.password = 'defaultpw'
10
+ conf.url = 'http://jabber.local/api'
11
+ end
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "jabber_admin"
4
+ require_relative 'config'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "jabber_admin"
10
10
  # require "pry"
11
11
  # Pry.start
12
12
 
13
- require "irb"
13
+ require 'irb'
14
14
  IRB.start(__FILE__)
@@ -0,0 +1,15 @@
1
+ version: "3"
2
+ services:
3
+ jabber:
4
+ image: hausgold/ejabberd:18.01
5
+ network_mode: bridge
6
+ ports: ["4560", "5222", "5269", "5280", "5443"]
7
+
8
+ test:
9
+ image: ruby:2.5
10
+ network_mode: bridge
11
+ working_dir: /app
12
+ volumes:
13
+ - .:/app
14
+ links:
15
+ - jabber
@@ -1,42 +1,49 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "jabber_admin/version"
5
+ require 'jabber_admin/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "jabber_admin"
8
- spec.version = JabberAdmin::VERSION
9
- spec.authors = ["Henning Vogt"]
10
- spec.licenses = ['MIT']
11
- spec.email = ["henning.vogt@hausgold.de"]
12
-
13
- spec.summary = %q{Library for the Ejabberd RESTful admin API}
14
- spec.description = %q{Library for the Ejabberd RESTful admin API}
15
- spec.homepage = "https://github.com/hausgold/jabber_admin"
16
-
17
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
- # to allow pushing to a single host or delete this section to allow pushing to any host.
8
+ spec.name = 'jabber_admin'
9
+ spec.version = JabberAdmin::VERSION
10
+ spec.authors = ['Henning Vogt', 'Hermann Mayer']
11
+ spec.licenses = ['MIT']
12
+ spec.email = ['henning.vogt@hausgold.de', 'hermann.mayer@hausgold.de']
13
+
14
+ spec.summary = 'Library for the ejabberd RESTful admin API'
15
+ spec.description = 'Library for the ejabberd RESTful admin API'
16
+ spec.homepage = 'https://github.com/hausgold/jabber_admin'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
19
+ # 'allowed_push_host' to allow pushing to a single host or delete this
20
+ # section to allow pushing to any host.
19
21
  if spec.respond_to?(:metadata)
20
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
22
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
23
  else
22
- raise "RubyGems 2.0 or newer is required to protect against " \
23
- "public gem pushes."
24
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
25
+ 'public gem pushes.'
24
26
  end
25
27
 
26
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ spec.required_ruby_version = '~> 2.5'
29
+
30
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
27
31
  f.match(%r{^(test|spec|features)/})
28
32
  end
29
- spec.bindir = "exe"
33
+ spec.bindir = 'exe'
30
34
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
35
+ spec.require_paths = ['lib']
32
36
 
37
+ spec.add_dependency 'activesupport', '>= 5.2.0'
33
38
  spec.add_dependency 'rest-client', '~> 2.0', '>= 2.0.2'
34
- spec.add_dependency "activesupport", ">= 5"
35
-
36
- spec.required_ruby_version = '>= 2.2'
37
39
 
38
- spec.add_development_dependency "bundler", "~> 1.16"
39
- spec.add_development_dependency "rake", "~> 10.0"
40
- spec.add_development_dependency "rspec", "~> 3.0"
41
- spec.add_development_dependency "simplecov", "~> 0.15"
40
+ spec.add_development_dependency 'bundler', '>= 1.16', '< 3'
41
+ spec.add_development_dependency 'irb', '~> 1.2'
42
+ spec.add_development_dependency 'rake', '~> 13.0'
43
+ spec.add_development_dependency 'rspec', '~> 3.9'
44
+ spec.add_development_dependency 'rubocop', '~> 0.93'
45
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.43'
46
+ spec.add_development_dependency 'simplecov', '< 0.18'
47
+ spec.add_development_dependency 'vcr', '~> 6.0'
48
+ spec.add_development_dependency 'webmock', '~> 3.0'
42
49
  end
@@ -1,61 +1,124 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/inflector"
4
- require "rest-client"
3
+ require 'active_support/inflector'
4
+ require 'json'
5
+ require 'pathname'
6
+ require 'rest-client'
5
7
 
6
- require "jabber_admin/initializer"
7
- require "jabber_admin/configuration"
8
- require "jabber_admin/commands"
9
- require "jabber_admin/api_call"
10
- require "jabber_admin/version"
8
+ require 'jabber_admin/exceptions'
9
+ require 'jabber_admin/configuration'
10
+ require 'jabber_admin/commands'
11
+ require 'jabber_admin/api_call'
12
+ require 'jabber_admin/version'
11
13
 
12
- ##
13
- # Jabber Admin Library
14
+ # jabber_admin
14
15
  #
15
- # allows making API calls to the ejabberd RESTful admin backend
16
- # All supported commands are available in /lib/jabber_admin/commands/
16
+ # This gem allows making API calls to the ejabberd RESTful admin backend. We
17
+ # support a bunch of predefined commands out of the box, just have a look at
18
+ # the +lib/jabber_admin/commands/+ directory or the readme file.
17
19
  #
18
- # Usage:
19
- # All commands can be called via JabberAdmin.[method_name]!
20
- # (Do not forget the bang at the end)
20
+ # All predefined commands can be called via +JabberAdmin.COMMAND!+ or via
21
+ # +JabberAdmin.COMMAND+. The bang variant checks the result of the command
22
+ # (successful, or not) and raise a subclass of +JabberAdmin::Exception+ in case
23
+ # of issues. The non-bang variant just sends the commands in a fire and forget
24
+ # manner.
21
25
  #
22
- # @example:
23
- # JabberAdmin.register!(user: 'peter', 'host': 'example.com', password: '123')
24
- # JabberAdmin.unregister!(user: 'peter', 'host': 'example.com')
26
+ # When you're missing a command you want to use, you can use the
27
+ # +JabberAdmin::ApiCall+ class directly. It allows you to easily fulfill your
28
+ # custom needs with the power of error handling (if you like).
29
+ #
30
+ # You can also use your custom command directly on the +JabberAdmin+ module, in
31
+ # both banged and non-banged versions and we pass them as a shortcut to a new
32
+ # +JabberAdmin::ApiCall+ instance.
33
+ #
34
+ # @example Configure jabber_admin gem
35
+ # JabberAdmin.configure do |config|
36
+ # # The ejabberd REST API endpoint as a full URL.
37
+ # # Take care of the path part, because this is individually
38
+ # # configured on ejabberd. (See: https://bit.ly/2rBxatJ)
39
+ # config.url = 'http://jabber.local/api'
40
+ # # Provide here the full user JID in order to authenticate as
41
+ # # a administrator.
42
+ # config.username = 'admin@jabber.local'
43
+ # # The password of the administrator account.
44
+ # config.password = 'password'
45
+ # end
46
+ #
47
+ # @example Restart the ejabberd service
25
48
  # JabberAdmin.restart!
26
49
  #
50
+ # @example Register a new user to the XMPP service
51
+ # JabberAdmin.register! user: 'peter@example.com', password: '123'
52
+ #
53
+ # @example Delete a user from the XMPP service, in fire and forget manner
54
+ # JabberAdmin.unregister user: 'peter@example.com'
27
55
  module JabberAdmin
28
56
  class << self
29
57
  attr_writer :configuration
30
58
  end
31
59
 
32
- # @return [JabberAdmin::Configuration] The global JabberAdmin configuration
60
+ # A simple getter to the global JabberAdmin configuration structure.
61
+ #
62
+ # @return [JabberAdmin::Configuration] the global JabberAdmin configuration
33
63
  def self.configuration
34
- @configuration ||= JabberAdmin::Configuration.new
64
+ @configuration ||= Configuration.new
35
65
  end
36
66
 
37
- # Class method to set and change the global configuration
67
+ # Class method to set and change the global configuration. This is just a
68
+ # tapped variant of the +.configuration+ method.
38
69
  #
39
- # @example
40
- # JabberAdmin.configure do |config|
41
- # config.api_host = 'xmpp.example.com'
42
- # config.admin = 'admin'
43
- # config.password = 'password'
44
- # end
70
+ # @yield [configuration]
71
+ # @yieldparam [JabberAdmin::Configuration] configuration
45
72
  def self.configure
46
73
  yield(configuration)
47
74
  end
48
75
 
49
- def self.method_missing(method, *args, &block)
50
- command = "jabber_admin/commands/#{method[0..-2]}".classify.constantize
51
- args.empty? ? command.call : command.call(*args)
76
+ # Allow an easy to use DSL on the +JabberAdmin+ module. We support predefined
77
+ # (known) commands and unknown ones in bang and non-bang variants. This
78
+ # allows maximum flexibility to the user. The bang versions perform the
79
+ # response checks and raise in case of issues. The non-bang versions skip
80
+ # this checks. For unknown commands the +JabberAdmin::ApiCall+ is directly
81
+ # utilized with the method name as command. (Without the trailling bang, when
82
+ # it is present)
83
+ #
84
+ # @param method [Symbol, String, #to_s] the name of the command to run
85
+ # @param args all additional payload to pass down to the API call
86
+ # @return [RestClient::Response] the actual response of the command
87
+ def self.method_missing(method, *args)
88
+ predefined_command(method).call(predefined_callable(method), *args)
52
89
  rescue NameError
53
- super
90
+ predefined_callable(method).call(method.to_s.chomp('!'), *args)
54
91
  end
55
92
 
56
- def self.respond_to_missing?(method, include_private = false)
57
- "jabber_admin/commands/#{method[0..-2]}".classify.constantize && true
58
- rescue NameError
59
- super
93
+ # Try to find the given name as a predefined command. When there is no such
94
+ # predefined command, we raise a +NameError+.
95
+ #
96
+ # @param name [Symbol, String, #to_s] the command name to lookup
97
+ # @return [Class] the predefined command class constant
98
+ def self.predefined_command(name)
99
+ # Remove bangs and build the camel case variant
100
+ "JabberAdmin::Commands::#{name.to_s.chomp('!').camelize}".constantize
101
+ end
102
+
103
+ # Generate a matching API call wrapper for the given command name. When we
104
+ # have to deal with a bang version, we pass the bang down to the API call
105
+ # instance. Otherwise we just run the regular +#perform+ method on the API
106
+ # call instance.
107
+ #
108
+ # @param name [Symbol, String, #to_s] the command name to match
109
+ # @return [Proc] the API call wrapper
110
+ def self.predefined_callable(name)
111
+ method = name.to_s.end_with?('!') ? 'perform!' : 'perform'
112
+ proc { |*args| ApiCall.send(method, *args) }
113
+ end
114
+
115
+ # We support all methods if you ask for. This is our dynamic command approach
116
+ # here to support predefined and custom commands in the same namespace.
117
+ #
118
+ # @param method [String] the method to lookup
119
+ # @param include_private [Boolean] allow the lookup of private methods
120
+ # @return [Boolean] always +true+
121
+ def self.respond_to_missing?(_method, _include_private = false)
122
+ true
60
123
  end
61
124
  end
@@ -1,42 +1,132 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JabberAdmin
4
- # Error that will be raised when API call is not successful
5
- class ApiError < StandardError; end
6
-
7
- # Handles the communication with the API
4
+ # Handles a single communication with the API. An instance persists the
5
+ # response when performed once. So you can get the response multiple times,
6
+ # without repeating the request.
8
7
  class ApiCall
9
- attr_reader :command, :payload
10
-
11
- def self.perform(command, payload = {})
12
- new(command, payload).perform
13
- end
8
+ attr_reader :command, :payload, :check_res_body
14
9
 
15
- def initialize(command, payload)
10
+ # Setup a new API call instance with the given command and the given
11
+ # request payload.
12
+ #
13
+ # @param command [String] the command to execute
14
+ # @param check_res_body [Boolean] whenever to check the response body
15
+ # @param payload the request payload, empty by default
16
+ def initialize(command, check_res_body: true, **payload)
16
17
  @command = command
17
18
  @payload = payload
19
+ @check_res_body = check_res_body
18
20
  end
19
21
 
20
- def perform
21
- raise(ApiError, response.to_s) if response.code != 200
22
-
23
- response
22
+ # The resulting URL of the API call. This URL is constructed with the
23
+ # +JabberAdmin.configuration.url+ as base and the command name as the
24
+ # suffix. The configuration is allowed to end with trailling slash, or not.
25
+ #
26
+ # @return [String] the API call URL
27
+ def url
28
+ "#{JabberAdmin.configuration.url.strip.chomp('/')}/#{@command}"
24
29
  end
25
30
 
26
- private
27
-
31
+ # This method compose the actual request, perfoms it and stores the
32
+ # response to the instance. Additional calls to this method will not
33
+ # repeat the request, but will deliver the response directly.
34
+ #
35
+ # @return [RestClient::Response] the response of the API call
28
36
  def response
29
- @res ||= RestClient::Request.execute(
37
+ @response ||= RestClient::Request.execute(
30
38
  method: :post,
31
39
  url: url,
32
- user: JabberAdmin.configuration.admin,
40
+ user: JabberAdmin.configuration.username,
33
41
  password: JabberAdmin.configuration.password,
34
42
  payload: payload.to_json
35
43
  )
44
+ rescue RestClient::Exception => e
45
+ @response = e.response
36
46
  end
37
47
 
38
- def url
39
- "#{JabberAdmin.configuration.api_host}/api/#{@command}"
48
+ # Check if the response was successful. Otherwise raise exceptions with
49
+ # +JabberAdmin::Exception+ as base type.
50
+ #
51
+ # @raise JabberAdmin::ApiError
52
+ # @raise JabberAdmin::CommandError
53
+ #
54
+ # rubocop:disable Metrics/AbcSize because its the bundled check logic
55
+ # rubocop:disable Metrics/MethodLength dito
56
+ def check_response
57
+ # The REST API responds a 404 status code when the command is not known.
58
+ if response.code == 404
59
+ raise UnknownCommandError.new("Command '#{command}' is not known",
60
+ response)
61
+ end
62
+
63
+ # In case we send commands with missing data or any other validation
64
+ # issues, the REST API will respond with a 400 Bad Request status
65
+ # code.
66
+ raise CommandError.new('Invalid arguments for command', response) \
67
+ if response.code == 400
68
+
69
+ # Looks like the ejabberd REST API is returning 200 OK in case the
70
+ # request was valid and permitted. But it does not indicate that the
71
+ # request was successful handled. This is indicated on the response body
72
+ # as a one (1) or a zero (0). (0 on success, 1 otherwise)
73
+ raise RequestError.new('Response code was not 200', response) \
74
+ unless response.code == 200
75
+
76
+ # Stop the check, when we should not check the response body
77
+ return unless check_res_body
78
+
79
+ # The command was not successful, for some reason. Unfortunately we do
80
+ # not get any further information here, which makes error debugging a
81
+ # struggle.
82
+ raise CommandError.new('Command was not successful', response) \
83
+ unless response.body == '0'
84
+ end
85
+ # rubocop:enable Metrics/AbcSize
86
+ # rubocop:enable Metrics/MethodLength
87
+
88
+ # Just a simple DSL wrapper for the response method.
89
+ #
90
+ # @return [RestClient::Response] the API call response
91
+ def perform
92
+ response
93
+ end
94
+
95
+ # Just a simple DSL wrapper for the response method. But this variant
96
+ # perfoms a response check which will raise excpetions when there are
97
+ # issues.
98
+ #
99
+ # @raise JabberAdmin::ApiError
100
+ # @raise JabberAdmin::CommandError
101
+ # @return [RestClient::Response] the API call response
102
+ def perform!
103
+ check_response
104
+ response
105
+ end
106
+
107
+ # A simple class level shortcut of the +perform+ method. This is just DSL
108
+ # code which accepts the same arguments as the instance initialize method.
109
+ # (+#new+)
110
+ #
111
+ # @param command [String] the command to execute
112
+ # @param payload [Hash] the request payload, empty by default
113
+ # @return [RestClient::Response] the API call response
114
+ def self.perform(*args)
115
+ new(*args).perform
116
+ end
117
+
118
+ # A simple class level shortcut of the +perform!+ method. This is just DSL
119
+ # code which accepts the same arguments as the instance initialize method.
120
+ # (+#new+)
121
+ #
122
+ # @param command [String] the command to execute
123
+ # @param payload [Hash] the request payload, empty by default
124
+ # @return [RestClient::Response] the API call response
125
+ #
126
+ # @raise JabberAdmin::ApiError
127
+ # @raise JabberAdmin::CommandError
128
+ def self.perform!(*args)
129
+ new(*args).perform!
40
130
  end
41
131
  end
42
132
  end
@@ -1,17 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jabber_admin/commands/ban_account'
4
- require 'jabber_admin/commands/create_room'
5
- require 'jabber_admin/commands/create_room_with_opts'
6
- require 'jabber_admin/commands/register'
7
- require 'jabber_admin/commands/restart'
8
- require 'jabber_admin/commands/send_direct_invitation'
9
- require 'jabber_admin/commands/subscribe_room'
10
- require 'jabber_admin/commands/unsubscribe_room'
11
- require 'jabber_admin/commands/unregister'
12
-
13
- ##
14
- # Contains alle commands that are supported
15
3
  module JabberAdmin
4
+ # Contains all predefined commands that are supported.
16
5
  module Commands; end
17
6
  end
7
+
8
+ # Require all commands from the commands subfolder
9
+ Dir[Pathname.new(__dir__).join('commands', '**', '*.rb')].sort.each do |file|
10
+ require file
11
+ end