jabber_admin 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +30 -0
  3. data/.gitignore +4 -2
  4. data/.rubocop.yml +33 -0
  5. data/.simplecov +3 -0
  6. data/.travis.yml +7 -3
  7. data/.yardopts +4 -0
  8. data/CHANGELOG.md +30 -0
  9. data/Gemfile +4 -2
  10. data/Makefile +100 -0
  11. data/README.md +142 -22
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/docker-compose.yml +15 -0
  15. data/jabber_admin.gemspec +22 -15
  16. data/lib/jabber_admin.rb +100 -34
  17. data/lib/jabber_admin/api_call.rb +106 -20
  18. data/lib/jabber_admin/commands.rb +6 -5
  19. data/lib/jabber_admin/commands/ban_account.rb +11 -10
  20. data/lib/jabber_admin/commands/create_room.rb +15 -10
  21. data/lib/jabber_admin/commands/create_room_with_opts.rb +20 -14
  22. data/lib/jabber_admin/commands/muc_register_nick.rb +26 -0
  23. data/lib/jabber_admin/commands/register.rb +16 -11
  24. data/lib/jabber_admin/commands/registered_users.rb +8 -6
  25. data/lib/jabber_admin/commands/restart.rb +8 -5
  26. data/lib/jabber_admin/commands/send_direct_invitation.rb +19 -16
  27. data/lib/jabber_admin/commands/send_stanza.rb +12 -10
  28. data/lib/jabber_admin/commands/send_stanza_c2s.rb +28 -0
  29. data/lib/jabber_admin/commands/set_nickname.rb +22 -0
  30. data/lib/jabber_admin/commands/set_presence.rb +37 -0
  31. data/lib/jabber_admin/commands/set_room_affiliation.rb +17 -12
  32. data/lib/jabber_admin/commands/subscribe_room.rb +19 -12
  33. data/lib/jabber_admin/commands/unregister.rb +13 -7
  34. data/lib/jabber_admin/commands/unsubscribe_room.rb +10 -7
  35. data/lib/jabber_admin/configuration.rb +6 -1
  36. data/lib/jabber_admin/exceptions.rb +40 -0
  37. data/lib/jabber_admin/version.rb +3 -1
  38. metadata +85 -16
  39. data/lib/jabber_admin/initializer.rb +0 -6
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
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "jabber_admin"
4
+ require 'bundler/setup'
5
+ require 'jabber_admin'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "jabber_admin"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  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.3
10
+ network_mode: bridge
11
+ working_dir: /app
12
+ volumes:
13
+ - .:/app
14
+ links:
15
+ - jabber
@@ -1,20 +1,23 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'jabber_admin/version'
4
6
 
5
7
  Gem::Specification.new do |spec|
6
- spec.name = 'jabber_admin'
7
- spec.version = JabberAdmin::VERSION
8
- spec.authors = ['Henning Vogt']
9
- spec.licenses = ['MIT']
10
- spec.email = ['henning.vogt@hausgold.de']
11
-
12
- spec.summary = %q{Library for the ejabberd RESTful admin API}
13
- spec.description = %q{Library for the ejabberd RESTful admin API}
14
- spec.homepage = 'https://github.com/hausgold/jabber_admin'
15
-
16
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
- # 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.
18
21
  if spec.respond_to?(:metadata)
19
22
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
23
  else
@@ -22,20 +25,24 @@ Gem::Specification.new do |spec|
22
25
  'public gem pushes.'
23
26
  end
24
27
 
25
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
29
  f.match(%r{^(test|spec|features)/})
27
30
  end
28
31
  spec.bindir = 'exe'
29
32
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
33
  spec.require_paths = ['lib']
31
34
 
32
- spec.add_dependency 'rest-client', '~> 2.0', '>= 2.0.2'
33
35
  spec.add_dependency 'activesupport', '>= 4.2.5'
36
+ spec.add_dependency 'rest-client', '~> 2.0', '>= 2.0.2'
34
37
 
35
38
  spec.required_ruby_version = '>= 2.2'
36
39
 
37
40
  spec.add_development_dependency 'bundler', '~> 1.16'
38
41
  spec.add_development_dependency 'rake', '~> 10.0'
39
42
  spec.add_development_dependency 'rspec', '~> 3.0'
43
+ spec.add_development_dependency 'rubocop'
44
+ spec.add_development_dependency 'rubocop-rspec'
40
45
  spec.add_development_dependency 'simplecov', '~> 0.15'
46
+ spec.add_development_dependency 'vcr', '~> 3.0'
47
+ spec.add_development_dependency 'webmock', '~> 3.0'
41
48
  end
@@ -1,61 +1,127 @@
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
+ #
88
+ # rubocop:disable Style/MethodMissing we support all given methods
89
+ def self.method_missing(method, *args)
90
+ predefined_command(method).call(predefined_callable(method), *args)
52
91
  rescue NameError
53
- super
92
+ predefined_callable(method).call(method.to_s.chomp('!'), *args)
54
93
  end
94
+ # rubocop:enable Style/MethodMissing
55
95
 
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
96
+ # Try to find the given name as a predefined command. When there is no such
97
+ # predefined command, we raise a +NameError+.
98
+ #
99
+ # @param name [Symbol, String, #to_s] the command name to lookup
100
+ # @return [Class] the predefined command class constant
101
+ def self.predefined_command(name)
102
+ # Remove bangs and build the camel case variant
103
+ "JabberAdmin::Commands::#{name.to_s.chomp('!').camelize}".constantize
104
+ end
105
+
106
+ # Generate a matching API call wrapper for the given command name. When we
107
+ # have to deal with a bang version, we pass the bang down to the API call
108
+ # instance. Otherwise we just run the regular +#perform+ method on the API
109
+ # call instance.
110
+ #
111
+ # @param name [Symbol, String, #to_s] the command name to match
112
+ # @return [Proc] the API call wrapper
113
+ def self.predefined_callable(name)
114
+ method = name.to_s.end_with?('!') ? 'perform!' : 'perform'
115
+ proc { |*args| ApiCall.send(method, *args) }
116
+ end
117
+
118
+ # We support all methods if you ask for. This is our dynamic command approach
119
+ # here to support predefined and custom commands in the same namespace.
120
+ #
121
+ # @param method [String] the method to lookup
122
+ # @param include_private [Boolean] allow the lookup of private methods
123
+ # @return [Boolean] always +true+
124
+ def self.respond_to_missing?(_method, _include_private = false)
125
+ true
60
126
  end
61
127
  end
@@ -1,42 +1,128 @@
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 => err
45
+ @response = err.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
+ def check_response
56
+ # The REST API responds a 404 status code when the command is not known.
57
+ raise UnknownCommandError, "Command '#{command}' is not known", response \
58
+ if response.code == 404
59
+
60
+ # In case we send commands with missing data or any other validation
61
+ # issues, the REST API will respond with a 400 Bad Request status
62
+ # code.
63
+ raise CommandError, 'Invalid arguments for command', response \
64
+ if response.code == 400
65
+
66
+ # Looks like the ejabberd REST API is returning 200 OK in case the
67
+ # request was valid and permitted. But it does not indicate that the
68
+ # request was successful handled. This is indicated on the response body
69
+ # as a one (1) or a zero (0). (0 on success, 1 otherwise)
70
+ raise RequestError, 'Response code was not 200', response \
71
+ unless response.code == 200
72
+
73
+ # Stop the check, when we should not check the response body
74
+ return unless check_res_body
75
+
76
+ # The command was not successful, for some reason. Unfortunately we do
77
+ # not get any further information here, which makes error debugging a
78
+ # struggle.
79
+ raise CommandError, 'Command was not successful', response \
80
+ unless response.body == '0'
81
+ end
82
+ # rubocop:enable Metrics/AbcSize
83
+
84
+ # Just a simple DSL wrapper for the response method.
85
+ #
86
+ # @return [RestClient::Response] the API call response
87
+ def perform
88
+ response
89
+ end
90
+
91
+ # Just a simple DSL wrapper for the response method. But this variant
92
+ # perfoms a response check which will raise excpetions when there are
93
+ # issues.
94
+ #
95
+ # @raise JabberAdmin::ApiError
96
+ # @raise JabberAdmin::CommandError
97
+ # @return [RestClient::Response] the API call response
98
+ def perform!
99
+ check_response
100
+ response
101
+ end
102
+
103
+ # A simple class level shortcut of the +perform+ method. This is just DSL
104
+ # code which accepts the same arguments as the instance initialize method.
105
+ # (+#new+)
106
+ #
107
+ # @param command [String] the command to execute
108
+ # @param payload [Hash] the request payload, empty by default
109
+ # @return [RestClient::Response] the API call response
110
+ def self.perform(*args)
111
+ new(*args).perform
112
+ end
113
+
114
+ # A simple class level shortcut of the +perform!+ method. This is just DSL
115
+ # code which accepts the same arguments as the instance initialize method.
116
+ # (+#new+)
117
+ #
118
+ # @param command [String] the command to execute
119
+ # @param payload [Hash] the request payload, empty by default
120
+ # @return [RestClient::Response] the API call response
121
+ #
122
+ # @raise JabberAdmin::ApiError
123
+ # @raise JabberAdmin::CommandError
124
+ def self.perform!(*args)
125
+ new(*args).perform!
40
126
  end
41
127
  end
42
128
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Require all commands from the commands subfolder
4
- Dir["#{File.dirname(__FILE__)}/commands/**/*.rb"].each {|file| require file }
5
-
6
- ##
7
- # Contains alle commands that are supported
8
3
  module JabberAdmin
4
+ # Contains all predefined commands that are supported.
9
5
  module Commands; end
10
6
  end
7
+
8
+ # Require all commands from the commands subfolder
9
+ Dir[Pathname.new(__dir__).join('commands', '**', '*.rb')].each do |file|
10
+ require file
11
+ end
@@ -2,17 +2,18 @@
2
2
 
3
3
  module JabberAdmin
4
4
  module Commands
5
- ##
6
- # Ban an account: kick sessions and set random password
7
- # https://docs.ejabberd.im/developer/ejabberd-api/admin-api/#ban-account-ban-an-account-kick-sessions-and-set-random-password
5
+ # Ban an account by kicking sessions and set random password.
6
+ #
7
+ # @see https://bit.ly/2KW9xVt
8
8
  class BanAccount
9
- # @param [user] The user
10
- # @param [host] Server name
11
- # @param [reason] Reason for banning user
12
- def self.call(user:, host:, reason:)
13
- JabberAdmin::ApiCall.perform(
14
- 'ban_account', user: user, host: host, reason: reason
15
- )
9
+ # Pass the correct data to the given callable.
10
+ #
11
+ # @param callable [Proc, #call] the callable to call
12
+ # @param user [String] user JID wo/ resource (eg. +tom@localhost+)
13
+ # @param reason [String] the banning reason (eg. +Spamming other users+)
14
+ def self.call(callable, user:, reason:)
15
+ uid, host = user.split('@')
16
+ callable.call('ban_account', user: uid, host: host, reason: reason)
16
17
  end
17
18
  end
18
19
  end