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
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "../common/exceptions"
5
+ require_relative "../util/requestutil"
6
+ require_relative "../util/stringutil"
7
+
8
+ module Wikidotrb
9
+ module Module
10
+ # ユーザーのコレクションを表すクラス
11
+ class UserCollection < Array
12
+ # ユーザー名のリストからユーザーオブジェクトのリストを取得する
13
+ # @param client [Client] クライアント
14
+ # @param names [Array<String>] ユーザー名のリスト
15
+ # @param raise_when_not_found [Boolean] ユーザーが見つからない場合に例外を送出するか
16
+ # @return [UserCollection] ユーザーオブジェクトのリスト
17
+ def self.from_names(client, names, raise_when_not_found = false)
18
+ urls = names.map { |name| "https://www.wikidot.com/user:info/#{Wikidotrb::Util::StringUtil.to_unix(name)}" }
19
+
20
+ responses = Wikidotrb::Util::RequestUtil.request(client: client, method: "GET", urls: urls)
21
+
22
+ users = []
23
+
24
+ responses.each do |response|
25
+ raise response if response.is_a?(Exception)
26
+
27
+ html = Nokogiri::HTML(response.body.to_s)
28
+
29
+ # 存在チェック
30
+ if html.at_css("div.error-block")
31
+ raise NotFoundException, "User not found: #{response.uri}" if raise_when_not_found
32
+
33
+ next
34
+ end
35
+
36
+ # idの取得
37
+ user_id = html.at_css("a.btn.btn-default.btn-xs")["href"].split("/").last.to_i
38
+
39
+ # nameの取得
40
+ name = html.at_css("h1.profile-title").text.strip
41
+
42
+ # avatar_urlの取得
43
+ avatar_url = "https://www.wikidot.com/avatar.php?userid=#{user_id}"
44
+
45
+ users << User.new(
46
+ client: client,
47
+ id: user_id,
48
+ name: name,
49
+ unix_name: Wikidotrb::Util::StringUtil.to_unix(name),
50
+ avatar_url: avatar_url
51
+ )
52
+ end
53
+
54
+ new(users)
55
+ end
56
+ end
57
+
58
+ # ユーザーオブジェクトの抽象クラス
59
+ class AbstractUser
60
+ attr_accessor :client, :id, :name, :unix_name, :avatar_url, :ip, :ip_masked
61
+
62
+ def initialize(client:, id: nil, name: nil, unix_name: nil, avatar_url: nil, ip: nil, ip_masked: nil)
63
+ @client = client
64
+ @id = id
65
+ @name = name
66
+ @unix_name = unix_name
67
+ @avatar_url = avatar_url
68
+ @ip = ip
69
+ @ip_masked = ip_masked
70
+ end
71
+ end
72
+
73
+ # 一般のユーザーオブジェクト
74
+ class User < AbstractUser
75
+ attr_accessor :client, :id, :name, :unix_name, :avatar_url, :ip
76
+
77
+ def initialize(client:, id: nil, name: nil, unix_name: nil, avatar_url: nil)
78
+ super
79
+ end
80
+
81
+ # ユーザー名からユーザーオブジェクトを取得する
82
+ # @param client [Client] クライアント
83
+ # @param name [String] ユーザー名
84
+ # @param raise_when_not_found [Boolean] ユーザーが見つからない場合に例外を送出するか
85
+ # @return [User] ユーザーオブジェクト
86
+ def self.from_name(client, name, raise_when_not_found = false)
87
+ UserCollection.from_names(client, [name], raise_when_not_found).first
88
+ end
89
+ end
90
+
91
+ # 削除されたユーザーオブジェクト
92
+ class DeletedUser < AbstractUser
93
+ def initialize(client:, id: nil)
94
+ super(client: client, id: id, name: "account deleted", unix_name: "account_deleted", avatar_url: nil)
95
+ end
96
+ end
97
+
98
+ # 匿名ユーザーオブジェクト
99
+ class AnonymousUser < AbstractUser
100
+ def initialize(client:, ip: nil, ip_masked: nil)
101
+ super(client: client, id: nil, name: "Anonymous", unix_name: "anonymous", avatar_url: nil, ip: ip, ip_masked: ip_masked)
102
+ end
103
+ end
104
+
105
+ # ゲストユーザーオブジェクト
106
+ class GuestUser < AbstractUser
107
+ def initialize(client:, name:, avatar_url:)
108
+ super(client: client, id: nil, name: name, unix_name: nil, avatar_url: avatar_url)
109
+ end
110
+ end
111
+
112
+ # Wikidotシステムユーザーオブジェクト
113
+ class WikidotUser < AbstractUser
114
+ def initialize(client:)
115
+ super(client: client, id: nil, name: "Wikidot", unix_name: "wikidot", avatar_url: nil)
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "time"
5
+
6
+ module Wikidotrb
7
+ module Util
8
+ module Parser
9
+ class ODateParser
10
+ # odate要素を解析し、Timeオブジェクトを返す
11
+ # @param odate_element [Nokogiri::XML::Element] odate要素
12
+ # @return [Time] odate要素が表す日時
13
+ # @raise [ArgumentError] odate要素が有効なunix timeを含んでいない場合
14
+ def self.parse(odate_element)
15
+ # odate_elementがNokogiri::XML::Elementでない場合はその内容をパースする
16
+ odate_element = Nokogiri::HTML(odate_element.to_s).at_css(".odate") unless odate_element.is_a?(Nokogiri::XML::Element)
17
+
18
+ # 要素がnilの場合やclass属性がない場合はエラー
19
+ raise ArgumentError, "odate element does not contain a valid unix time" if odate_element.nil? || odate_element["class"].nil?
20
+
21
+ # クラス属性を取得して処理
22
+ odate_classes = odate_element["class"].split
23
+
24
+ # "time_"が含まれるクラスを検索
25
+ odate_classes.each do |odate_class|
26
+ # "time_"が含まれるクラスを検索
27
+ next unless odate_class.start_with?("time_")
28
+
29
+ unix_time_str = odate_class.sub("time_", "")
30
+ unix_time = unix_time_str.to_i
31
+
32
+ # unix timeが有効な範囲内か確認
33
+ # Wikidotは-8640000000000から8640000000000までの範囲をサポート
34
+ min_unix_time = -8_640_000_000_000
35
+ max_unix_time = 8_640_000_000_000
36
+ raise Wikidotrb::Common::Exceptions::UnexpectedException, "Invalid unix time" if unix_time < min_unix_time || unix_time > max_unix_time
37
+
38
+ return Time.at(unix_time)
39
+ end
40
+
41
+ # "time_"を含むクラスが見つからなかった場合はエラーを発生させる
42
+ raise ArgumentError, "odate element does not contain a valid unix time"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "../../module/user"
5
+
6
+ module Wikidotrb
7
+ module Util
8
+ module Parser
9
+ class UserParser
10
+ # printuser要素をパースし、ユーザーオブジェクトを返す
11
+ # @param client [Client] クライアント
12
+ # @param elem [Nokogiri::XML::Element] パース対象の要素(printuserクラスがついた要素)
13
+ # @return [AbstractUser] パースされて得られたユーザーオブジェクト
14
+ def self.parse(client, elem)
15
+ if elem.nil?
16
+ return nil
17
+ elsif deleted_user_string?(elem)
18
+ # Handle "(user deleted)" case
19
+ return Wikidotrb::Module::DeletedUser.new(client: client)
20
+ elsif !elem.is_a?(Nokogiri::XML::Element)
21
+ # Assume it is a string and parse it using Nokogiri
22
+ parsed_doc = Nokogiri::HTML.fragment(elem.to_s)
23
+ elem = parsed_doc.children.first
24
+ end
25
+
26
+ if elem["class"]&.include?("deleted")
27
+ parse_deleted_user(client, elem)
28
+
29
+ elsif elem["class"]&.include?("anonymous")
30
+ parse_anonymous_user(client, elem)
31
+
32
+ elsif gravatar_avatar?(elem)
33
+ parse_guest_user(client, elem)
34
+
35
+ elsif elem.text.strip == "Wikidot"
36
+ parse_wikidot_user(client)
37
+
38
+ else
39
+ parse_regular_user(client, elem)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def self.parse_deleted_user(client, elem)
46
+ id = elem["data-id"].to_i
47
+ Wikidotrb::Module::DeletedUser.new(client: client, id: id)
48
+ end
49
+
50
+ def self.parse_anonymous_user(client, elem)
51
+ masked_ip = elem.at_css("span.ip").text.gsub(/[()]/, "").strip
52
+ ip = masked_ip # デフォルトはマスクされたIP
53
+
54
+ # 完全なIPが取得できる場合はそちらを使用
55
+ if (onclick_attr = elem.at_css("a")["onclick"])
56
+ match_data = onclick_attr.match(/WIKIDOT.page.listeners.anonymousUserInfo\('(.+?)'\)/)
57
+ ip = match_data[1] if match_data
58
+ end
59
+
60
+ Wikidotrb::Module::AnonymousUser.new(client: client, ip: ip, ip_masked: masked_ip)
61
+ end
62
+
63
+ def self.parse_guest_user(client, elem)
64
+ guest_name = elem.text.strip.split.first
65
+ avatar_url = elem.at_css("img")["src"]
66
+ Wikidotrb::Module::GuestUser.new(client: client, name: guest_name, avatar_url: avatar_url)
67
+ end
68
+
69
+ def self.parse_wikidot_user(client)
70
+ Wikidotrb::Module::WikidotUser.new(client: client)
71
+ end
72
+
73
+ def self.parse_regular_user(client, elem)
74
+ user_anchor = elem.css("a").last
75
+
76
+ # user_anchorがnilの場合はnilを返す
77
+ return nil if user_anchor.nil?
78
+
79
+ user_name = user_anchor.text.strip
80
+ user_unix = user_anchor["href"].to_s.gsub("http://www.wikidot.com/user:info/", "")
81
+ user_id = user_anchor["onclick"].to_s.match(/WIKIDOT.page.listeners.userInfo\((\d+)\)/)[1].to_i
82
+
83
+ Wikidotrb::Module::User.new(
84
+ client: client,
85
+ id: user_id,
86
+ name: user_name,
87
+ unix_name: user_unix,
88
+ avatar_url: "http://www.wikidot.com/avatar.php?userid=#{user_id}"
89
+ )
90
+ end
91
+
92
+ def self.gravatar_avatar?(elem)
93
+ avatar_elem = elem.at_css("img")
94
+ avatar_elem && avatar_elem["src"].include?("gravatar.com")
95
+ end
96
+
97
+ # Check if the input is specifically the string "(user deleted)"
98
+ def self.deleted_user_string?(elem)
99
+ elem.is_a?(String) && elem.strip == "(user deleted)"
100
+ end
101
+
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+ require "json"
5
+
6
+ # QMCUser構造体の定義
7
+ QMCUser = Struct.new(:id, :name, keyword_init: true)
8
+
9
+ # QMCPage構造体の定義
10
+ QMCPage = Struct.new(:title, :unix_name, keyword_init: true)
11
+
12
+ class QuickModule
13
+ # リクエストを送信する
14
+ # @param module_name [String] モジュール名
15
+ # @param site_id [Integer] サイトID
16
+ # @param query [String] クエリ
17
+ # @return [Hash] レスポンスのJSONパース結果
18
+ def self._request(module_name:, site_id:, query:)
19
+ # 有効なモジュール名か確認
20
+ raise ArgumentError, "Invalid module name" unless %w[MemberLookupQModule UserLookupQModule PageLookupQModule].include?(module_name)
21
+
22
+ # リクエストURLの構築
23
+ url = "https://www.wikidot.com/quickmodule.php?module=#{module_name}&s=#{site_id}&q=#{query}"
24
+
25
+ # HTTPリクエストの送信
26
+ response = HTTPX.get(url, timeout: { operation: 300 })
27
+
28
+ # ステータスコードのチェック
29
+ raise ArgumentError, "Site is not found" if response.status == 500
30
+
31
+ # JSONレスポンスのパース
32
+ JSON.parse(response.body.to_s)
33
+ end
34
+
35
+ # メンバーを検索する
36
+ # @param site_id [Integer] サイトID
37
+ # @param query [String] クエリ
38
+ # @return [Array<QMCUser>] ユーザーのリスト
39
+ def self.member_lookup(site_id:, query:)
40
+ users = _request(module_name: "MemberLookupQModule", site_id: site_id, query: query)["users"]
41
+ users.map { |user| QMCUser.new(id: user["user_id"].to_i, name: user["name"]) }
42
+ end
43
+
44
+ # ユーザーを検索する
45
+ # @param site_id [Integer] サイトID
46
+ # @param query [String] クエリ
47
+ # @return [Array<QMCUser>] ユーザーのリスト
48
+ def self.user_lookup(site_id:, query:)
49
+ users = _request(module_name: "UserLookupQModule", site_id: site_id, query: query)["users"]
50
+ users.map { |user| QMCUser.new(id: user["user_id"].to_i, name: user["name"]) }
51
+ end
52
+
53
+ # ページを検索する
54
+ # @param site_id [Integer] サイトID
55
+ # @param query [String] クエリ
56
+ # @return [Array<QMCPage>] ページのリスト
57
+ def self.page_lookup(site_id:, query:)
58
+ pages = _request(module_name: "PageLookupQModule", site_id: site_id, query: query)["pages"]
59
+ pages.map { |page| QMCPage.new(title: page["title"], unix_name: page["unix_name"]) }
60
+ end
61
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+ require "concurrent"
5
+
6
+ module Wikidotrb
7
+ module Util
8
+ class RequestUtil
9
+ # GETリクエストを送信する
10
+ # @param client [Client] クライアント
11
+ # @param method [String] リクエストメソッド
12
+ # @param urls [Array<String>] URLのリスト
13
+ # @param return_exceptions [Boolean] 例外を返すかどうか
14
+ # @return [Array<HTTPX::Response, Exception>] レスポンスのリスト
15
+ def self.request(client:, method:, urls:, return_exceptions: false)
16
+ config = client.amc_client.config
17
+ semaphore = Concurrent::Semaphore.new(config.semaphore_limit)
18
+
19
+ # リクエスト処理を行う非同期タスク
20
+ tasks = urls.map do |url|
21
+ Concurrent::Promises.future do
22
+ semaphore.acquire
23
+
24
+ begin
25
+ case method.upcase
26
+ when "GET"
27
+ response = HTTPX.get(url)
28
+ when "POST"
29
+ response = HTTPX.post(url)
30
+ else
31
+ raise ArgumentError, "Invalid method"
32
+ end
33
+
34
+ response
35
+ rescue StandardError => e
36
+ e
37
+ ensure
38
+ semaphore.release
39
+ end
40
+ end
41
+ end
42
+
43
+ # 全てのタスクの完了を待機
44
+ results = Concurrent::Promises.zip(*tasks).value!
45
+
46
+ # 例外を返すかどうかのオプションに応じて結果を返す
47
+ return_exceptions ? results : results.each { |r| raise r if r.is_a?(Exception) }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "table/char_table"
4
+
5
+ module Wikidotrb
6
+ module Util
7
+ class StringUtil
8
+ # Unix形式に文字列を変換する
9
+ # @param target_str [String] 変換対象の文字列
10
+ # @return [String] 変換された文字列
11
+ def self.to_unix(target_str)
12
+ # MEMO: legacy wikidotの実装に合わせている
13
+
14
+ # 特殊文字の変換を行う
15
+ special_char_map = Wikidotrb::Table::CharTable::SPECIAL_CHAR_MAP
16
+ target_str = target_str.chars.map { |char| special_char_map[char] || char }.join
17
+
18
+ # lowercaseへの変換
19
+ target_str = target_str.downcase
20
+
21
+ # ASCII以外の文字を削除し、特殊なケースを正規表現で置き換え
22
+ target_str = target_str.gsub(/[^a-z0-9\-:_]/, "-")
23
+ .gsub(/^_/, ":_")
24
+ .gsub(/(?<!:)_/, "-")
25
+ .gsub(/^-*/, "")
26
+ .gsub(/-*$/, "")
27
+ .gsub(/-{2,}/, "-")
28
+ .gsub(/:{2,}/, ":")
29
+ .gsub(":-", ":")
30
+ .gsub("-:", ":")
31
+ .gsub("_-", "_")
32
+ .gsub("-_", "_")
33
+
34
+ # 先頭と末尾の':'を削除
35
+ target_str.gsub(/^:/, "").gsub(/:$/, "")
36
+ end
37
+ end
38
+ end
39
+ end