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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +77 -0
- data/CHANGELOG.md +52 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Rakefile +12 -0
- data/_config.yml +4 -0
- data/lib/wikidotrb/common/decorators.rb +81 -0
- data/lib/wikidotrb/common/exceptions.rb +88 -0
- data/lib/wikidotrb/common/logger.rb +25 -0
- data/lib/wikidotrb/connector/ajax.rb +192 -0
- data/lib/wikidotrb/connector/api.rb +20 -0
- data/lib/wikidotrb/module/auth.rb +76 -0
- data/lib/wikidotrb/module/client.rb +146 -0
- data/lib/wikidotrb/module/forum.rb +92 -0
- data/lib/wikidotrb/module/forum_category.rb +197 -0
- data/lib/wikidotrb/module/forum_group.rb +109 -0
- data/lib/wikidotrb/module/forum_post.rb +223 -0
- data/lib/wikidotrb/module/forum_thread.rb +346 -0
- data/lib/wikidotrb/module/page.rb +598 -0
- data/lib/wikidotrb/module/page_revision.rb +142 -0
- data/lib/wikidotrb/module/page_source.rb +17 -0
- data/lib/wikidotrb/module/page_votes.rb +31 -0
- data/lib/wikidotrb/module/private_message.rb +142 -0
- data/lib/wikidotrb/module/site.rb +207 -0
- data/lib/wikidotrb/module/site_application.rb +97 -0
- data/lib/wikidotrb/module/user.rb +119 -0
- data/lib/wikidotrb/util/parser/odate.rb +47 -0
- data/lib/wikidotrb/util/parser/user.rb +105 -0
- data/lib/wikidotrb/util/quick_module.rb +61 -0
- data/lib/wikidotrb/util/requestutil.rb +51 -0
- data/lib/wikidotrb/util/stringutil.rb +39 -0
- data/lib/wikidotrb/util/table/char_table.rb +477 -0
- data/lib/wikidotrb/version.rb +5 -0
- data/lib/wikidotrb.rb +41 -0
- data/sig/wikidotrb.rbs +4 -0
- 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
|