wikidotrb 3.0.7.pre.6

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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +77 -0
  4. data/CHANGELOG.md +52 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/Rakefile +12 -0
  7. data/_config.yml +4 -0
  8. data/lib/wikidotrb/common/decorators.rb +81 -0
  9. data/lib/wikidotrb/common/exceptions.rb +88 -0
  10. data/lib/wikidotrb/common/logger.rb +25 -0
  11. data/lib/wikidotrb/connector/ajax.rb +192 -0
  12. data/lib/wikidotrb/connector/api.rb +20 -0
  13. data/lib/wikidotrb/module/auth.rb +76 -0
  14. data/lib/wikidotrb/module/client.rb +146 -0
  15. data/lib/wikidotrb/module/forum.rb +92 -0
  16. data/lib/wikidotrb/module/forum_category.rb +197 -0
  17. data/lib/wikidotrb/module/forum_group.rb +109 -0
  18. data/lib/wikidotrb/module/forum_post.rb +223 -0
  19. data/lib/wikidotrb/module/forum_thread.rb +346 -0
  20. data/lib/wikidotrb/module/page.rb +598 -0
  21. data/lib/wikidotrb/module/page_revision.rb +142 -0
  22. data/lib/wikidotrb/module/page_source.rb +17 -0
  23. data/lib/wikidotrb/module/page_votes.rb +31 -0
  24. data/lib/wikidotrb/module/private_message.rb +142 -0
  25. data/lib/wikidotrb/module/site.rb +207 -0
  26. data/lib/wikidotrb/module/site_application.rb +97 -0
  27. data/lib/wikidotrb/module/user.rb +119 -0
  28. data/lib/wikidotrb/util/parser/odate.rb +47 -0
  29. data/lib/wikidotrb/util/parser/user.rb +105 -0
  30. data/lib/wikidotrb/util/quick_module.rb +61 -0
  31. data/lib/wikidotrb/util/requestutil.rb +51 -0
  32. data/lib/wikidotrb/util/stringutil.rb +39 -0
  33. data/lib/wikidotrb/util/table/char_table.rb +477 -0
  34. data/lib/wikidotrb/version.rb +5 -0
  35. data/lib/wikidotrb.rb +41 -0
  36. data/sig/wikidotrb.rbs +4 -0
  37. metadata +197 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4b3162c75bac8240593463765275b458cb753e8da4236eb373ba987848923af5
4
+ data.tar.gz: 2a9123f682563a8f8b7035cbd19cb5f8e6dadc7d50f39c8421fcd083d8a4a52a
5
+ SHA512:
6
+ metadata.gz: 40833f07667655fe0dcaa8fdeb89856c48e5390eef2c67701bd02371e79e0209c7addcfaf6e130bb37a9b6e7849ba3f86fe7a41715daf42313da170237c98520
7
+ data.tar.gz: 41e5dc7aa06fbade1a3220f3ccbbb847d27047b2090c4d5fdaf1533eddd529108d9714cd90bffbb0a58b17f2a52751a9460ecdb5aa6dfa2787cd59d2bfdba57c
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,77 @@
1
+ # Target Ruby Version
2
+ AllCops:
3
+ TargetRubyVersion: 3.0
4
+ NewCops: enable
5
+ SuggestExtensions: false
6
+
7
+ Style/StringLiterals:
8
+ EnforcedStyle: double_quotes
9
+
10
+ Style/StringLiteralsInInterpolation:
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/Documentation:
14
+ Enabled: false
15
+
16
+ Metrics/ClassLength:
17
+ Max: 500
18
+
19
+ Metrics/MethodLength:
20
+ Max: 70
21
+
22
+ Metrics/CyclomaticComplexity:
23
+ Max: 25
24
+
25
+ Metrics/ModuleLength:
26
+ Max: 500
27
+
28
+ Metrics/PerceivedComplexity:
29
+ Max: 25
30
+
31
+ Metrics/AbcSize:
32
+ Max: 80
33
+
34
+ Metrics/BlockLength:
35
+ Max: 200
36
+
37
+ Metrics/ParameterLists:
38
+ Max: 25
39
+
40
+ Layout/LineLength:
41
+ Max: 200
42
+
43
+ Metrics/CollectionLiteralLength:
44
+ Enabled: false
45
+
46
+ Naming/MethodParameterName:
47
+ Enabled: false
48
+
49
+ Naming/AccessorMethodName:
50
+ Enabled: false
51
+
52
+ Naming/MethodName:
53
+ Enabled: false
54
+
55
+ Naming/PredicateName:
56
+ Enabled: false
57
+
58
+ Naming/VariableName:
59
+ Enabled: false
60
+
61
+ Style/OptionalBooleanParameter:
62
+ Enabled: false
63
+
64
+ Lint/EmptyFile:
65
+ Enabled: false
66
+
67
+ Lint/DuplicateMethods:
68
+ Enabled: false
69
+
70
+ Lint/UnusedMethodArgument:
71
+ Enabled: false
72
+
73
+ Lint/UnderscorePrefixedVariableName:
74
+ Enabled: false
75
+
76
+ Gemspec/DevelopmentDependencies:
77
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [released]
9
+
10
+ ## [3.0.7.pre] - 2024-10-06
11
+
12
+ ### Added
13
+ - Initial release of `wikidotrb`, a Ruby library inspired by `wikidot.py`.
14
+
15
+ ## [3.0.7.pre.1] - 2024-10-08
16
+
17
+ ### Changed
18
+ - Improved `UserParser.parse` method to convert non-`Nokogiri::XML::Element` elements properly before processing.
19
+
20
+ ## [3.0.7.pre.2] - 2024-10-08
21
+
22
+ ### Fixed
23
+ - Corrected the instantiation of `PageRevisionCollection` by ensuring it properly passes `page` and `revisions` as named parameters.
24
+
25
+ ## [3.0.7.pre.3] - 2024-10-08
26
+
27
+ ### Fixed
28
+ - Remove debug `puts` statement from `PageCollection` instantiation.
29
+
30
+ ## [3.0.7.pre.4] - 2024-10-08
31
+
32
+ ### Fixed
33
+ - Fixed an issue where the `acquire_sources` and `acquire_htmls` methods incorrectly parsed the `response` object. Now directly access `response["body"]` without unnecessary parsing.
34
+
35
+ ## [3.0.7.pre.5] - 2024-10-08
36
+
37
+ ### Fixed
38
+ - Remove debug `puts` statement from `PageCollection` instantiation.
39
+
40
+ ## [3.0.7.pre.6] - 2024-10-08
41
+
42
+ ### Changed
43
+ - Refactored `UserParser.parse` to better handle the string `"(user deleted)"` as input.
44
+
45
+ ### Added
46
+ - Private helper method `deleted_user_string?` to clearly separate the logic for parsing `"(user deleted)"`.
47
+
48
+ ### Improved
49
+ - Made parsing helper methods (`parse_deleted_user`, `parse_anonymous_user`, `parse_guest_user`, `parse_wikidot_user`, `parse_regular_user`) private to encapsulate their functionality.
50
+
51
+ ### Fixed
52
+ - Added `nil` check for `user_anchor` in `parse_regular_user` to prevent errors when user information is missing.
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/_config.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ username: 'testuser'
3
+ password: 'password'
4
+ site: 'test'
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikidotrb
4
+ module Common
5
+ module Decorators
6
+ def login_required(*methods)
7
+ methods.each do |method|
8
+ if singleton_methods.include?(method)
9
+ # クラスメソッドにデコレータを適用
10
+ singleton_class.class_eval do
11
+ alias_method "#{method}_without_login_check", method
12
+
13
+ define_method(method) do |*args, **kwargs|
14
+ client = nil
15
+
16
+ # インスタンス変数として存在するかを最初にチェック
17
+ client ||= self.client if respond_to?(:client)
18
+
19
+ # キーワード引数からclientを探す
20
+ client = kwargs[:client] if kwargs.key?(:client)
21
+
22
+ # 引数からClientインスタンスを探す
23
+ unless client
24
+ args.each do |arg|
25
+ if arg.is_a?(Wikidotrb::Module::Client)
26
+ client = arg
27
+ break
28
+ end
29
+ end
30
+ end
31
+
32
+ # clientが見つからない場合はエラーを発生させる
33
+ raise ArgumentError, "Client is not found" if client.nil?
34
+
35
+ # clientのログインチェック
36
+ client.login_check
37
+
38
+ # 元のメソッドの呼び出し
39
+ send("#{method}_without_login_check", *args, **kwargs)
40
+ end
41
+ end
42
+ elsif instance_methods.include?(method)
43
+ # インスタンスメソッドにデコレータを適用
44
+ alias_method "#{method}_without_login_check", method
45
+
46
+ define_method(method) do |*args, **kwargs|
47
+ client = nil
48
+
49
+ # インスタンス変数として存在するかを最初にチェック
50
+ client ||= self.client if respond_to?(:client)
51
+
52
+ # キーワード引数からclientを探す
53
+ client = kwargs[:client] if kwargs.key?(:client)
54
+
55
+ # 引数からClientインスタンスを探す
56
+ unless client
57
+ args.each do |arg|
58
+ if arg.is_a?(Wikidotrb::Module::Client)
59
+ client = arg
60
+ break
61
+ end
62
+ end
63
+ end
64
+
65
+ # clientが見つからない場合はエラーを発生させる
66
+ raise ArgumentError, "Client is not found" if client.nil?
67
+
68
+ # clientのログインチェック
69
+ client.login_check
70
+
71
+ # 元のメソッドの呼び出し
72
+ send("#{method}_without_login_check", *args, **kwargs)
73
+ end
74
+ else
75
+ raise NameError, "Undefined method `#{method}` for class `#{self}`"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikidotrb
4
+ module Common
5
+ module Exceptions
6
+ # ---
7
+ # 基底クラス
8
+ # ---
9
+
10
+ class WikidotException < StandardError
11
+ # 独自例外の基底クラス
12
+ end
13
+
14
+ # ---
15
+ # ワイルドカード
16
+ # ---
17
+
18
+ class UnexpectedException < WikidotException
19
+ # 予期せぬ例外が発生したときの例外
20
+ end
21
+
22
+ # ---
23
+ # セッション関連
24
+ # ---
25
+
26
+ class SessionCreateException < WikidotException
27
+ # セッションの作成に失敗したときの例外
28
+ end
29
+
30
+ class LoginRequiredException < WikidotException
31
+ # ログインが必要なメソッドのときの例外
32
+ end
33
+
34
+ # ---
35
+ # AMC関連
36
+ # ---
37
+
38
+ class AjaxModuleConnectorException < WikidotException
39
+ # ajax-module-connector.phpへのリクエストに失敗したときの例外
40
+ end
41
+
42
+ class AMCHttpStatusCodeException < AjaxModuleConnectorException
43
+ # AMCから返却されたHTTPステータスが200以外だったときの例外
44
+ attr_reader :status_code
45
+
46
+ def initialize(message, status_code)
47
+ super(message)
48
+ @status_code = status_code
49
+ end
50
+ end
51
+
52
+ class WikidotStatusCodeException < AjaxModuleConnectorException
53
+ # AMCから返却されたデータ内のステータスがokではなかったときの例外
54
+ # HTTPステータスが200以外の場合はAMCHttpStatusCodeExceptionを投げる
55
+ attr_reader :status_code
56
+
57
+ def initialize(message, status_code)
58
+ super(message)
59
+ @status_code = status_code
60
+ end
61
+ end
62
+
63
+ class ResponseDataException < AjaxModuleConnectorException
64
+ # AMCから返却されたデータが不正だったときの例外
65
+ end
66
+
67
+ # ---
68
+ # ターゲットエラー関連
69
+ # ---
70
+
71
+ class NotFoundException < WikidotException
72
+ # サイトやページ・ユーザが見つからなかったときの例外
73
+ end
74
+
75
+ class TargetExistsException < WikidotException
76
+ # 対象が既に存在しているときの例外
77
+ end
78
+
79
+ class TargetErrorException < WikidotException
80
+ # メソッドの対象としたオブジェクトに操作が適用できないときの例外
81
+ end
82
+
83
+ class ForbiddenException < WikidotException
84
+ # 権限がないときの例外
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Wikidotrb
6
+ module Common
7
+ # Logger設定
8
+ def self.setup_logger(name = "wikidot", level = Logger::INFO)
9
+ # ロガーの作成
10
+ _logger = Logger.new($stdout)
11
+ _logger.progname = name
12
+ _logger.level = level
13
+
14
+ # ログフォーマット
15
+ _logger.formatter = proc do |severity, datetime, progname, msg|
16
+ "#{datetime} [#{progname}/#{severity}] #{msg}\n"
17
+ end
18
+
19
+ _logger
20
+ end
21
+
22
+ # ロガーの初期化
23
+ Logger = setup_logger
24
+ end
25
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+ require "json"
5
+ require "logger"
6
+ require "concurrent"
7
+
8
+ require_relative "../common/exceptions"
9
+ require_relative "../common/logger"
10
+
11
+ module Wikidotrb
12
+ module Connector
13
+ class AjaxRequestHeader
14
+ # AjaxRequestHeaderオブジェクトの初期化
15
+ # @param content_type [String] Content-Type
16
+ # @param user_agent [String] User-Agent
17
+ # @param referer [String] Referer
18
+ # @param cookie [Hash] Cookie
19
+ def initialize(content_type: nil, user_agent: nil, referer: nil, cookie: nil)
20
+ @content_type = content_type || "application/x-www-form-urlencoded; charset=UTF-8"
21
+ @user_agent = user_agent || "WikidotRb"
22
+ @referer = referer || "https://www.wikidot.com/"
23
+ @cookie = { "wikidot_token7" => 123_456 }.merge(cookie || {})
24
+ end
25
+
26
+ # Cookieを設定
27
+ # @param name [String] Cookie名
28
+ # @param value [String] Cookie値
29
+ def set_cookie(name, value)
30
+ @cookie[name] = value
31
+ end
32
+
33
+ # Cookieを取得
34
+ # @param name [String] Cookie名
35
+ # @return [String, nil] Cookie値
36
+ def get_cookie(name)
37
+ @cookie[name]
38
+ end
39
+
40
+ # Cookieを削除
41
+ # @param name [String] Cookie名
42
+ def delete_cookie(name)
43
+ @cookie.delete(name)
44
+ end
45
+
46
+ # ヘッダを構築して返す
47
+ # @return [Hash] ヘッダのハッシュ
48
+ def get_header
49
+ {
50
+ "Content-Type" => @content_type,
51
+ "User-Agent" => @user_agent,
52
+ "Referer" => @referer,
53
+ "Cookie" => @cookie.map { |name, value| "#{name}=#{value};" }.join
54
+ }
55
+ end
56
+ end
57
+
58
+ # AjaxModuleConnectorConfigの定義
59
+ AjaxModuleConnectorConfig = Struct.new(
60
+ :request_timeout, :attempt_limit, :retry_interval, :semaphore_limit,
61
+ keyword_init: true
62
+ ) do
63
+ def initialize(**args)
64
+ super
65
+ self.request_timeout ||= 20
66
+ self.attempt_limit ||= 3
67
+ self.retry_interval ||= 5
68
+ self.semaphore_limit ||= 10
69
+ end
70
+ end
71
+
72
+ class AjaxModuleConnectorClient
73
+ attr_reader :header, :config, :site_name
74
+
75
+ # AjaxModuleConnectorClientオブジェクトの初期化
76
+ # @param site_name [String] サイト名
77
+ # @param config [AjaxModuleConnectorConfig] クライアントの設定
78
+
79
+ def initialize(site_name:, config: nil)
80
+ raise ArgumentError, "site_name cannot be nil" if site_name.nil?
81
+
82
+ @site_name = site_name
83
+ @config = config || AjaxModuleConnectorConfig.new
84
+ @header = AjaxRequestHeader.new
85
+ @logger = Wikidotrb::Common::Logger
86
+ end
87
+
88
+ # サイトの存在とSSLの対応をチェック
89
+ # @return [Boolean] SSL対応しているか
90
+ # @raise [NotFoundException] サイトが見つからない場合
91
+ def check_existence_and_ssl(site_name)
92
+ http_url = "http://#{site_name}.wikidot.com"
93
+ https_url = "https://#{site_name}.wikidot.com"
94
+
95
+ http_response = HTTPX.get(http_url)
96
+ raise NotFoundException, "Site is not found: #{site_name}.wikidot.com" if http_response.status == 404
97
+
98
+ https_response = HTTPX.get(https_url)
99
+ https_response.status == 200 || (https_response.status == 301 && https_response.headers["location"].start_with?("https"))
100
+ end
101
+
102
+ # ajax-module-connector.phpへのリクエストを行う
103
+ # @param bodies [Array<Hash>] リクエストボディのリスト
104
+ # @param return_exceptions [Boolean] 例外を返すかどうか
105
+ # @param site_name [String] サイト名
106
+ # @param site_ssl_supported [Boolean] サイトがSSL対応しているかどうか
107
+ # @return [Array<Hash>] レスポンスボディのリスト
108
+ # @raise [AMCHttpStatusCodeException, WikidotStatusCodeException, ResponseDataException]
109
+ def request(bodies:, return_exceptions: false, site_name: nil, site_ssl_supported: nil)
110
+ semaphore = Concurrent::Semaphore.new(@config.semaphore_limit)
111
+ site_name ||= @site_name
112
+ site_ssl_supported ||= check_existence_and_ssl(site_name)
113
+
114
+ tasks = bodies.map do |body|
115
+ Concurrent::Promises.future do
116
+ retry_count = 0
117
+
118
+ loop do
119
+ semaphore.acquire
120
+
121
+ begin
122
+ protocol = site_ssl_supported ? "https" : "http"
123
+ url = "#{protocol}://#{site_name}.wikidot.com/ajax-module-connector.php"
124
+ body["wikidot_token7"] = 123_456
125
+ @logger.debug("Ajax Request: #{url} -> #{body}")
126
+ response = HTTPX.post(
127
+ url,
128
+ headers: @header.get_header,
129
+ form: body,
130
+ timeout: { operation: @config.request_timeout }
131
+ )
132
+
133
+ if response.status != 200
134
+ retry_count += 1
135
+ if retry_count >= @config.attempt_limit
136
+ @logger.error("AMC is respond HTTP error code: #{response.status} -> #{body}")
137
+ raise AMCHttpStatusCodeException.new("AMC is respond HTTP error code: #{response.status}",
138
+ response.status)
139
+ end
140
+
141
+ @logger.info("AMC is respond status: #{response.status} (retry: #{retry_count}) -> #{body}")
142
+ sleep @config.retry_interval
143
+ next
144
+ end
145
+
146
+ response_body = JSON.parse(response.body.to_s)
147
+
148
+ if response_body.nil? || response_body.empty?
149
+ @logger.error("AMC is respond empty data -> #{body}")
150
+ raise ResponseDataException, "AMC is respond empty data"
151
+ end
152
+
153
+ if response_body["status"]
154
+ if response_body["status"] == "try_again"
155
+ retry_count += 1
156
+ if retry_count >= @config.attempt_limit
157
+ @logger.error("AMC is respond status: \"try_again\" -> #{body}")
158
+ raise Wikidotrb::Common::Exceptions::WikidotStatusCodeException.new(
159
+ 'AMC is respond status: "try_again"', "try_again"
160
+ )
161
+ end
162
+
163
+ @logger.info("AMC is respond status: \"try_again\" (retry: #{retry_count})")
164
+ sleep @config.retry_interval
165
+ next
166
+ elsif response_body["status"] != "ok"
167
+ @logger.error("AMC is respond error status: \"#{response_body["status"]}\" -> #{body}")
168
+ raise Wikidotrb::Common::Exceptions::WikidotStatusCodeException.new(
169
+ "AMC is respond error status: \"#{response_body["status"]}\"", response_body["status"]
170
+ )
171
+ end
172
+ end
173
+
174
+ break response_body
175
+ rescue JSON::ParserError
176
+ @logger.error("AMC is respond non-json data: \"#{response.body}\" -> #{body}")
177
+ raise Wikidotrb::Common::Exceptions::ResponseDataException,
178
+ "AMC is respond non-json data: \"#{response.body}\""
179
+ ensure
180
+ semaphore.release
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ results = Concurrent::Promises.zip(*tasks).value!
187
+
188
+ return_exceptions ? results : results.each { |r| raise r if r.is_a?(Exception) }
189
+ end
190
+ end
191
+ end
192
+ end