clash_of_clans_api 0.1.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a2b63aa46b4c6536b5c3c86729addc9e5dfa9c8071fdc890d24eed29d8768010
4
+ data.tar.gz: 4c2a7bf252282acd7f5254b22eab7fe5f273dd740c0179fb04926af0d8295fef
5
+ SHA512:
6
+ metadata.gz: b9735602b7013aa396f555b1aca1d7dfc560cd6a46b27fd290275195f1a759c3789d116076ff10fe5ef5c8762371dfa2fdbebebc8f60930adf686c80b460aafe
7
+ data.tar.gz: ccdb84b96e9c4db19c6e1bb89cd142fdd200c22b953d3f4ed6e45051bc13b137d7e23d247487842dcc915fe7c97f07550813fcbb834657add06799e967a7563f
data/README.adoc ADDED
@@ -0,0 +1,67 @@
1
+ == ClashOfClansApi
2
+
3
+ `clash_of_clans_api` is a gem to communicate with the Clash of Clans API at https://developer.clashofclans.com/.
4
+ It contains low-level methods to communicate with all existing API endpoints and aims to provide higher-level abstractions for them.
5
+
6
+ === Installation
7
+
8
+ Add this line to your application’s Gemfile:
9
+
10
+ [source,ruby]
11
+ ----
12
+ gem 'clash_of_clans_api'
13
+ ----
14
+
15
+ And then execute:
16
+
17
+ ....
18
+ $ bundle install
19
+ ....
20
+
21
+ Or install it yourself as:
22
+
23
+ ....
24
+ $ gem install clash_of_clans_api
25
+ ....
26
+
27
+ === Usage
28
+
29
+ ==== API Communication
30
+
31
+ To communicate with the Clash of Clans API, an API access token is required.
32
+ Currently, the gem is not able to create tokens itself.
33
+
34
+ The gem provides two classes for communication.
35
+ `ClashOfClansApi::Api` is a low-level interface that implements methods for all API endpoints.
36
+ If the request is successful, the API’s JSON response is parsed and returned, otherwise a `ClashOfClans::NoSucessError` is raised.
37
+ `ClashOfClansApi::Client` is a higher-level interface that exposes its `ClashOfClansApi::Api` instance through `ClashOfClansApi::Client#api`.
38
+ Both classes’ initializers take a single argument, the API token.
39
+
40
+ The method names for the endpoints are the same in both classes.
41
+ They are derived from the https://developer.clashofclans.com/#/documentation[API documentation] by the following steps.
42
+
43
+ . Take the path name from the documentation (e.g. `/clans/{clanTag}/currentwar/leaguegroup`).
44
+ . Replace slashes (`/`) with underscores (`\_`) and keep only inner ones (`clans_{clanTag}_currentwar_leaguegroup`).
45
+ . In case of path arguments, singularize the path segment referenced by the argument (`clan_{clanTag}_currentwar_leaguegroup`).
46
+ . Remove path argument segments (`clan_currentwar_leaguegroup`).
47
+
48
+ Path arguments are converted to positional arguments in the order of definition in the original path name.
49
+ Path arguments will automatically be URL-escaped.
50
+ A URL query in the form of a `Hash` can be passed as the named parameter `query:`.
51
+
52
+ ==== Clan and player tags
53
+
54
+ Tags in Clash of Clans are subject to format restrictions.
55
+ Since those restrictions are well known, `ClashOfClansApi::Tags` provides class methods for checking the format (`.sanitizable?`) and sanitizing ill-formatted tags up to a certain degree (`.sanitize`).
56
+
57
+ Even though the API seems to ignore some mistakes, e.g. using `O` (upper case letter o) instead of `0` (number zero), it also does not seem to correct them.
58
+ If a player with tag `#PY0` existed, the API would return the same information for both `#PY0` and `#PYO`, except using the tag that was requested.
59
+ This could lead to unforeseen errors like multiple database entries for the same player or clan.
60
+ Therefore, those mistakes should be catched before sending a request.
61
+
62
+
63
+ === Development
64
+
65
+ After checking out the repo, run `bin/setup` to install dependencies.
66
+ Then, run `rake spec` to run the tests.
67
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,57 @@
1
+ require_relative 'endpoint_methods'
2
+ require_relative 'no_success_error'
3
+ require_relative 'utils'
4
+
5
+ module ClashOfClansApi
6
+ class Api
7
+ include EndpointMethods
8
+
9
+ BASE_URI = URI('https://api.clashofclans.com/v1/')
10
+
11
+ def base_uri
12
+ BASE_URI
13
+ end
14
+
15
+ attr_reader :api_token
16
+
17
+ def initialize(api_token)
18
+ @api_token = api_token
19
+ end
20
+
21
+ def endpoint_headers
22
+ {
23
+ 'Authorization' => "Bearer #{api_token}",
24
+ }
25
+ end
26
+
27
+ define_endpoint :clan_currentwar_leaguegroup, method: :get, endpoint: proc { |clan_tag | "clans/#{Utils.url_escape(clan_tag)}/currentwar/leaguegroup" }
28
+ define_endpoint :clanwarleagues_war, method: :get, endpoint: proc { | war_tag | "clanwarleagues/wars/#{Utils.url_escape(war_tag)}" }
29
+ define_endpoint :clan_warlog, method: :get, endpoint: proc { |clan_tag | "clans/#{Utils.url_escape(clan_tag)}/warlog" }
30
+ define_endpoint :clans, method: :get, endpoint: 'clans'
31
+ define_endpoint :clan_currentwar, method: :get, endpoint: proc { |clan_tag | "clans/#{Utils.url_escape(clan_tag)}/currentwar" }
32
+ define_endpoint :clan, method: :get, endpoint: proc { |clan_tag | "clans/#{Utils.url_escape(clan_tag)}" }
33
+ define_endpoint :clan_members, method: :get, endpoint: proc { |clan_tag | "clans/#{Utils.url_escape(clan_tag)}/members" }
34
+
35
+ define_endpoint :player, method: :get, endpoint: proc { |player_tag | "players/#{Utils.url_escape(player_tag)}" }
36
+ define_endpoint :player_verifytoken, method: :post, endpoint: proc { |player_tag | "players/#{Utils.url_escape(player_tag)}/verifytoken" }, body: proc { |token:| %Q[{"token":"#{token.to_s}"}] }
37
+
38
+ define_endpoint :leagues, method: :get, endpoint: 'leagues'
39
+ define_endpoint :league_season, method: :get, endpoint: proc { | league_id, season_id| "leagues/#{Utils.url_escape( league_id)}/seasons/#{Utils.url_escape(season_id)}" }
40
+ define_endpoint :league, method: :get, endpoint: proc { | league_id | "leagues/#{Utils.url_escape( league_id)}" }
41
+ define_endpoint :league_seasons, method: :get, endpoint: proc { | league_id | "leagues/#{Utils.url_escape( league_id)}/seasons" }
42
+ define_endpoint :warleague, method: :get, endpoint: proc { |warleague_id | "warleagues/#{Utils.url_escape(warleague_id)}" }
43
+ define_endpoint :warleagues, method: :get, endpoint: 'warleagues'
44
+
45
+ define_endpoint :location_rankings_clans, method: :get, endpoint: proc { |location_id | "locations/#{Utils.url_escape(location_id)}/rankings/clans" }
46
+ define_endpoint :location_rankings_players, method: :get, endpoint: proc { |location_id | "locations/#{Utils.url_escape(location_id)}/rankings/players" }
47
+ define_endpoint :location_rankings_clansversus, method: :get, endpoint: proc { |location_id | "locations/#{Utils.url_escape(location_id)}/rankings/clans-versus" }
48
+ define_endpoint :location_rankings_playersversus, method: :get, endpoint: proc { |location_id | "locations/#{Utils.url_escape(location_id)}/rankings/players-versus" }
49
+ define_endpoint :locations, method: :get, endpoint: 'locations'
50
+ define_endpoint :location, method: :get, endpoint: proc { |location_id | "locations/#{Utils.url_escape(location_id)}" }
51
+
52
+ define_endpoint :goldpass_seasons_current, method: :get, endpoint: 'goldpass/seasons/current'
53
+
54
+ define_endpoint :labels_players, method: :get, endpoint: 'labels/players'
55
+ define_endpoint :labels_clans, method: :get, endpoint: 'labels/clans'
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'api'
2
+ require_relative 'models/league'
3
+
4
+ module ClashOfClansApi
5
+ class Client
6
+ attr_reader :api
7
+
8
+ def initialize(api_token)
9
+ @api = Api.new(api_token)
10
+ end
11
+
12
+ def authorized?
13
+ api.perform_get('test').code == '404'
14
+ end
15
+
16
+ def player_verifytoken(player_tag, token)
17
+ response = api.player_verifytoken(player_tag, token: token)
18
+
19
+ raise "Sent player tag #{player_tag.inspect} but received #{response['tag' ].inspect}." unless player_tag == response['tag' ]
20
+ raise "Sent token #{ token .inspect} but received #{response['token'].inspect}." unless token == response['token']
21
+
22
+ case response['status']
23
+ when 'ok'
24
+ true
25
+ when 'invalid'
26
+ false
27
+ else
28
+ raise "Unknown status #{response['status'].inspect}."
29
+ end
30
+ end
31
+
32
+ def leagues
33
+ response = api.leagues
34
+
35
+ raise NotImplementedError, "Found a paging cursor but handling it is not implemented yet." if response['paging']['cursors'].any?
36
+
37
+ response['items'].map do |league|
38
+ Models::League.new(league)
39
+ end
40
+ end
41
+
42
+ def league(id)
43
+ Models::League.new(api.league(id))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,68 @@
1
+ require 'cgi'
2
+ require 'json'
3
+ require 'net/https'
4
+
5
+ module ClashOfClansApi
6
+ module EndpointMethods
7
+ def endpoint_headers
8
+ {}
9
+ end
10
+
11
+ def perform_request(method, api_path, query: nil, body: nil, headers: nil)
12
+ uri = self.base_uri+api_path
13
+ uri.query = URI.encode_www_form(query) if query
14
+
15
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme=='https') do |http|
16
+ case method
17
+ when :get
18
+ Net::HTTP::Get
19
+ when :post
20
+ Net::HTTP::Post
21
+ else
22
+ raise ArgumentError, "Invalid method #{method.inspect}."
23
+ end.new(uri).tap do |request|
24
+ endpoint_headers.merge(headers || {}).each do |name, value|
25
+ request[name] = value
26
+ end
27
+
28
+ if body
29
+ request['Content-Type'] = 'application/json'
30
+ request.body = body
31
+ end
32
+ end.then do |request|
33
+ http.request(request)
34
+ end
35
+ end
36
+ end
37
+
38
+ def transform_response(response)
39
+ if response.is_a?(Net::HTTPSuccess)
40
+ JSON.parse(response.body)
41
+ else
42
+ raise NoSuccessError.new(response)
43
+ end
44
+ end
45
+
46
+
47
+ def self.included(klass)
48
+ klass.extend(ClassMethods)
49
+ end
50
+
51
+ module ClassMethods
52
+ def define_endpoint(name, method:, endpoint:, body: nil)
53
+ define_method(name) do |*args, **kwargs|
54
+ uri = endpoint.respond_to?(:call) ? ClashOfClansApi::Utils.call_proc_without_unknown_keywords(endpoint, *args, **kwargs) : endpoint
55
+ request_body = body .respond_to?(:call) ? ClashOfClansApi::Utils.call_proc_without_unknown_keywords(body, *args, **kwargs) : body
56
+
57
+ perform_request(method, uri, body: request_body, query: kwargs.dig(:query), headers: kwargs.dig(:headers)).then do |response|
58
+ if !kwargs.key?(:plain_response) || !kwargs.fetch(:plain_response)
59
+ transform_response(response)
60
+ else
61
+ response
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,71 @@
1
+ require_relative 'invalid_data_error'
2
+
3
+ module ClashOfClansApi
4
+ module Models
5
+ class Base
6
+ def initialize(hash)
7
+ @hash = hash
8
+
9
+ validate!
10
+ end
11
+
12
+ def to_h
13
+ @hash
14
+ end
15
+
16
+ def [](key)
17
+ @hash[key]
18
+ end
19
+
20
+ class << self
21
+ attr_reader :required_fields
22
+
23
+ def property(name, key, type: nil, required: false)
24
+ define_method(name) do
25
+ if type.nil?
26
+ self[key]
27
+ else
28
+ if property_cached?(name)
29
+ property_from_cache(name)
30
+ else
31
+ cache_property(name, self[key].then do |prop|
32
+ prop.is_a?(Array) ? prop.map {|item| type.new(item) } : type.new(prop)
33
+ end)
34
+ end
35
+ end
36
+ end
37
+
38
+ if required
39
+ @required_fields = (@required_fields || [])+[key]
40
+ end
41
+ end
42
+ end
43
+
44
+ def property_cached?(name)
45
+ @property_cache && @property_cache.key?(name)
46
+ end
47
+
48
+ def cache_property(name, obj)
49
+ @property_cache ||= {}
50
+
51
+ @property_cache[name] = obj
52
+ end
53
+
54
+ def property_from_cache(name)
55
+ @property_cache[name]
56
+ end
57
+
58
+ def validate!
59
+ if self.class.required_fields
60
+ missing = self.class.required_fields.reject do |required_field|
61
+ @hash.key?(required_field)
62
+ end
63
+
64
+ if missing.any?
65
+ raise InvalidDataError, "The following keys are required, but missing from the model data: #{missing.map(&:inspect).join(', ')}"
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'base'
2
+
3
+ module ClashOfClansApi
4
+ module Models
5
+ class IconSet < Base
6
+ property :tiny, 'tiny'
7
+ property :small, 'small'
8
+ property :medium, 'medium'
9
+ property :large, 'large'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module ClashOfClansApi
2
+ module Models
3
+ class InvalidDataError < StandardError
4
+ attr_reader :data_hash
5
+
6
+ def initialize(message, data_hash=nil)
7
+ super(message)
8
+
9
+ @data_hash = data_hash
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'base'
2
+ require_relative 'icon_set'
3
+
4
+ module ClashOfClansApi
5
+ module Models
6
+ class League < Base
7
+ property :id, 'id', required: true
8
+ property :name, 'name', required: true
9
+ property :icon_urls, 'iconUrls', type: IconSet
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'base'
2
+
3
+ module ClashOfClansApi
4
+ module Models
5
+ class Token < Base
6
+ property :id, 'id', required: true
7
+ property :developer_id, 'developerId'
8
+ property :tier, 'tier'
9
+ property :name, 'name', required: true
10
+ property :description, 'description', required: true
11
+ property :origins, 'origins'
12
+ property :scopes, 'scopes'
13
+ property :cidr_ranges, 'cidrRanges', required: true
14
+ property :valid_until, 'validUntil'
15
+ property :key, 'key', required: true
16
+
17
+ def initialize(hash, token_client:)
18
+ super(hash)
19
+
20
+ @token_client = token_client
21
+ end
22
+
23
+ def revoke
24
+ @token_client.revoke_api_key(self.id)
25
+ end
26
+
27
+ def client_from_token
28
+ ClashOfClansApi::Client.new(self.key)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ module ClashOfClansApi
2
+ class NoSuccessError < StandardError
3
+ attr_reader :response
4
+
5
+ def initialize(response)
6
+ @response = response
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module ClashOfClansApi
2
+ module Tags
3
+ def self.tag_regex
4
+ /\A#[PYLQGRJCUV0289]+\z/
5
+ end
6
+
7
+ def self.sanitizable_tag_regex
8
+ /\A\s*#?[PYLQGRJCUVO0289]+\s*\z/i
9
+ end
10
+
11
+ def self.sanitizable?(tag)
12
+ sanitizable_tag_regex.match?(tag)
13
+ end
14
+
15
+ def self.sanitize(tag)
16
+ tag.strip.upcase.gsub('O', '0').then do |t|
17
+ t.start_with?('#') ? t : "##{t}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'endpoint_methods'
2
+
3
+ module ClashOfClansApi
4
+ module TokenApi
5
+ BASE_URI = URI('https://developer.clashofclans.com/api/')
6
+
7
+ class << self
8
+ include EndpointMethods
9
+
10
+ def base_uri
11
+ BASE_URI
12
+ end
13
+
14
+ define_endpoint :login, method: :post, endpoint: 'login', body: proc { |email:, password:|
15
+ {
16
+ email: email.to_s,
17
+ password: password.to_s,
18
+ }.to_json
19
+ }
20
+ define_endpoint :logout, method: :post, endpoint: 'logout', body: proc { '{}' }
21
+ define_endpoint :apikey_list, method: :post, endpoint: 'apikey/list', body: proc { '{}' }
22
+ define_endpoint :apikey_create, method: :post, endpoint: 'apikey/create', body: proc { |name:, description:, ip_addresses:|
23
+ {
24
+ name: name,
25
+ description: description,
26
+ cidrRanges: ip_addresses,
27
+ scopes: [:clash],
28
+ }.to_json
29
+ }
30
+ define_endpoint :apikey_revoke, method: :post, endpoint: 'apikey/revoke', body: proc { |id:|
31
+ {
32
+ id: id,
33
+ }.to_json
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
1
+ require 'cgi'
2
+ require_relative 'token_api'
3
+ require_relative 'models/token'
4
+
5
+ module ClashOfClansApi
6
+ class TokenClient
7
+ attr_reader :email
8
+ attr_reader :password
9
+
10
+ def initialize(email, password)
11
+ @email = email
12
+ @password = password
13
+ end
14
+
15
+ def login!
16
+ TokenApi.login(email: email, password: password, plain_response: true).tap do |response|
17
+ if response.is_a?(Net::HTTPSuccess)
18
+ cookies = CGI::Cookie.parse(response['set-cookie'])
19
+
20
+ @session_headers = {
21
+ 'Cookie' => "session=#{cookies['session'][0]}",
22
+ }
23
+ else
24
+ raise NoSuccessError, response
25
+ end
26
+ end
27
+ end
28
+
29
+ def logout
30
+ TokenApi.logout(headers: @session_headers)
31
+ ensure
32
+ @session_headers = nil
33
+ end
34
+
35
+ def list_api_keys
36
+ response = TokenApi.apikey_list(headers: @session_headers)
37
+
38
+ response['keys'].map do |key|
39
+ Models::Token.new(key, token_client: self)
40
+ end
41
+ end
42
+
43
+ def create_api_key(name, description, ip_addresses)
44
+ response = TokenApi.apikey_create(name: name, description: description, ip_addresses: (ip_addresses.is_a?(Array) ? ip_addresses : [ip_addresses]), headers: @session_headers)
45
+
46
+ Models::Token.new(response['key'], token_client: self)
47
+ end
48
+
49
+ def revoke_api_key(id)
50
+ TokenApi.apikey_revoke(id: id, headers: @session_headers)
51
+
52
+ true
53
+ end
54
+
55
+ class << self
56
+ def create!(email, password)
57
+ new(email, password).login!
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,34 @@
1
+ require 'open-uri'
2
+
3
+ module ClashOfClansApi
4
+ module Utils
5
+ def self.url_escape(string)
6
+ if !string.nil?
7
+ CGI.escape(string.to_s)
8
+ else
9
+ raise TypeError, 'cannot escape nil'
10
+ end
11
+ end
12
+
13
+ def self.call_proc_without_unknown_keywords(proc, *args, **kwargs, &block)
14
+ params = proc.parameters.group_by(&:first).transform_values! do |m|
15
+ m.map do |s|
16
+ s[1]
17
+ end
18
+ end
19
+
20
+ proc_keys =
21
+ if params.key?(:keyrest)
22
+ kwargs
23
+ else
24
+ kwargs.slice(*(params.values_at(:key, :keyreq).compact.flatten))
25
+ end
26
+
27
+ proc.call(*args, **proc_keys, &block)
28
+ end
29
+
30
+ def self.get_current_ipv4_address
31
+ IPAddr.new(URI('https://ipv4.icanhazip.com').open.read.strip).to_s
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClashOfClansApi
4
+ VERSION = '0.1.1'
5
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'clash_of_clans_api/version'
2
+ require_relative 'clash_of_clans_api/client'
3
+ require_relative 'clash_of_clans_api/tags'
4
+ require_relative 'clash_of_clans_api/token_client'
5
+
6
+ module ClashOfClansApi
7
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clash_of_clans_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - expeehaa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - expeehaa@outlook.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.adoc
21
+ - lib/clash_of_clans_api.rb
22
+ - lib/clash_of_clans_api/api.rb
23
+ - lib/clash_of_clans_api/client.rb
24
+ - lib/clash_of_clans_api/endpoint_methods.rb
25
+ - lib/clash_of_clans_api/models/base.rb
26
+ - lib/clash_of_clans_api/models/icon_set.rb
27
+ - lib/clash_of_clans_api/models/invalid_data_error.rb
28
+ - lib/clash_of_clans_api/models/league.rb
29
+ - lib/clash_of_clans_api/models/token.rb
30
+ - lib/clash_of_clans_api/no_success_error.rb
31
+ - lib/clash_of_clans_api/tags.rb
32
+ - lib/clash_of_clans_api/token_api.rb
33
+ - lib/clash_of_clans_api/token_client.rb
34
+ - lib/clash_of_clans_api/utils.rb
35
+ - lib/clash_of_clans_api/version.rb
36
+ homepage: https://github.com/expeehaa/clash_of_clans_api
37
+ licenses: []
38
+ metadata:
39
+ allowed_push_host: https://rubygems.org
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.6.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.2.22
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: Client library for interacting with the ClashOfClans API.
59
+ test_files: []