wikidotrb 3.0.7.pre.6

Sign up to get free protection for your applications and to get access to all the features.
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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikidotrb
4
+ module Connector
5
+ # APIキーのオブジェクト
6
+ class APIKeys
7
+ # 読み取り専用の属性
8
+ attr_reader :ro_key, :rw_key
9
+
10
+ # 初期化
11
+ # @param ro_key [String] Read Only Key
12
+ # @param rw_key [String] Read-Write Key
13
+ def initialize(ro_key:, rw_key:)
14
+ @ro_key = ro_key
15
+ @rw_key = rw_key
16
+ freeze
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+ require "uri"
5
+ require "json"
6
+ require_relative "../common/exceptions"
7
+
8
+ module Wikidotrb
9
+ module Module
10
+ class HTTPAuthentication
11
+ # ユーザー名とパスワードでログインする
12
+ # @param client [Client] クライアント
13
+ # @param username [String] ユーザー名
14
+ # @param password [String] パスワード
15
+ # @raise [SessionCreateException] セッション作成に失敗した場合
16
+ def self.login(client, username, password)
17
+ url = "https://www.wikidot.com/default--flow/login__LoginPopupScreen"
18
+
19
+ # ログインリクエストのデータを作成
20
+ request_data = {
21
+ "login" => username,
22
+ "password" => password,
23
+ "action" => "Login2Action",
24
+ "event" => "login"
25
+ }
26
+
27
+ response = HTTPX.post(
28
+ url,
29
+ headers: client.amc_client.header.get_header,
30
+ form: request_data,
31
+ timeout: { operation: 20 }
32
+ )
33
+
34
+ # ステータスコードのチェック
35
+ unless response.status == 200
36
+ raise Wikidotrb::Common::Exceptions::SessionCreateException,
37
+ "Login attempt is failed due to HTTP status code: #{response.status}"
38
+ end
39
+
40
+ # レスポンスボディのチェック
41
+ if response.body.to_s.include?("The login and password do not match")
42
+ raise Wikidotrb::Common::Exceptions::SessionCreateException,
43
+ "Login attempt is failed due to invalid username or password"
44
+ end
45
+
46
+ # クッキーのチェックとパース
47
+ set_cookie_header = response.headers["set-cookie"]
48
+
49
+ # `set-cookie` が存在しない、または `nil` の場合にエラーを発生
50
+ raise Wikidotrb::Common::Exceptions::SessionCreateException, "Login attempt is failed due to invalid cookies" if set_cookie_header.nil? || set_cookie_header.empty?
51
+
52
+ # セッションクッキーを取得
53
+ session_cookie = set_cookie_header.match(/WIKIDOT_SESSION_ID=([^;]+)/)
54
+
55
+ raise Wikidotrb::Common::Exceptions::SessionCreateException, "Login attempt is failed due to invalid cookies" unless session_cookie
56
+
57
+ # セッションクッキーの設定
58
+ client.amc_client.header.set_cookie("WIKIDOT_SESSION_ID", session_cookie[1])
59
+ end
60
+
61
+ # ログアウトする
62
+ # @param client [Client] クライアント
63
+ def self.logout(client)
64
+ begin
65
+ client.amc_client.request(
66
+ [{ "action" => "Login2Action", "event" => "logout", "moduleName" => "Empty" }]
67
+ )
68
+ rescue StandardError
69
+ # 例外を無視してログアウト処理を続ける
70
+ end
71
+
72
+ client.amc_client.header.delete_cookie("WIKIDOT_SESSION_ID")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../common/logger"
4
+ require_relative "../common/exceptions"
5
+ require_relative "../connector/ajax"
6
+ require_relative "auth"
7
+ require_relative "private_message"
8
+ require_relative "site"
9
+ require_relative "user"
10
+
11
+ module Wikidotrb
12
+ module Module
13
+ class ClientUserMethods
14
+ attr_reader :client
15
+
16
+ def initialize(client)
17
+ @client = client
18
+ end
19
+
20
+ # ユーザー名からユーザーオブジェクトを取得する
21
+ # @param name [String] ユーザー名
22
+ # @param raise_when_not_found [Boolean] ユーザーが見つからない場合に例外を送出するか
23
+ # @return [User] ユーザーオブジェクト
24
+ def get(name, raise_when_not_found: false)
25
+ Wikidotrb::Module::User.from_name(@client, name, raise_when_not_found)
26
+ end
27
+
28
+ # ユーザー名からユーザーオブジェクトを取得する
29
+ # @param names [Array<String>] ユーザー名のリスト
30
+ # @param raise_when_not_found [Boolean] ユーザーが見つからない場合に例外を送出するか
31
+ # @return [Array<User>] ユーザーオブジェクトのリスト
32
+ def get_bulk(names, raise_when_not_found: false)
33
+ Wikidotrb::Module::UserCollection.from_names(@client, names, raise_when_not_found)
34
+ end
35
+ end
36
+
37
+ class ClientPrivateMessageMethods
38
+ attr_reader :client
39
+
40
+ def initialize(client)
41
+ @client = client
42
+ end
43
+
44
+ # メッセージを送信する
45
+ # @param recipient [User] 受信者
46
+ # @param subject [String] 件名
47
+ # @param body [String] 本文
48
+ def send_message(recipient, subject, body)
49
+ Wikidotrb::Module::PrivateMessage.send_message(
50
+ client: @client, recipient: recipient, subject: subject, body: body
51
+ )
52
+ end
53
+
54
+ # 受信箱を取得する
55
+ # @return [PrivateMessageInbox] 受信箱
56
+ def get_inbox
57
+ Wikidotrb::Module::PrivateMessageInbox.acquire(client: @client)
58
+ end
59
+
60
+ # 送信箱を取得する
61
+ # @return [PrivateMessageSentBox] 送信箱
62
+ def get_sentbox
63
+ Wikidotrb::Module::PrivateMessageSentBox.acquire(client: @client)
64
+ end
65
+
66
+ # メッセージを取得する
67
+ # @param message_ids [Array<Integer>] メッセージIDのリスト
68
+ # @return [PrivateMessageCollection] メッセージのリスト
69
+ def get_messages(message_ids)
70
+ Wikidotrb::Module::PrivateMessageCollection.from_ids(client: @client, message_ids: message_ids)
71
+ end
72
+
73
+ # メッセージを取得する
74
+ # @param message_id [Integer] メッセージID
75
+ # @return [PrivateMessage] メッセージ
76
+ def get_message(message_id)
77
+ Wikidotrb::Module::PrivateMessage.from_id(client: @client, message_id: message_id)
78
+ end
79
+ end
80
+
81
+ class ClientSiteMethods
82
+ attr_reader :client
83
+
84
+ def initialize(client)
85
+ @client = client
86
+ end
87
+
88
+ # UNIX名からサイトオブジェクトを取得する
89
+ # @param unix_name [String] サイトのUNIX名
90
+ # @return [Site] サイトオブジェクト
91
+ def get(unix_name)
92
+ Wikidotrb::Module::Site.from_unix_name(client: client, unix_name: unix_name)
93
+ end
94
+ end
95
+
96
+ class Client
97
+ attr_accessor :amc_client, :is_logged_in, :username
98
+ attr_reader :user, :private_message, :site
99
+
100
+ # 基幹クライアント
101
+ def initialize(username: nil, password: nil, amc_config: nil, logging_level: "WARN")
102
+ # 最初にロギングレベルを決定する
103
+ Wikidotrb::Common::Logger.level = logging_level
104
+
105
+ # AMCClientを初期化
106
+ @amc_client = Wikidotrb::Connector::AjaxModuleConnectorClient.new(site_name: "www", config: amc_config)
107
+
108
+ # セッション関連変数の初期化
109
+ @is_logged_in = false
110
+ @username = nil
111
+
112
+ # usernameとpasswordが指定されていればログインする
113
+ if username && password
114
+ Wikidotrb::Module::HTTPAuthentication.login(self, username, password)
115
+ @is_logged_in = true
116
+ @username = username
117
+ end
118
+
119
+ # メソッドの定義
120
+ @user = ClientUserMethods.new(self)
121
+ @private_message = ClientPrivateMessageMethods.new(self)
122
+ @site = ClientSiteMethods.new(self)
123
+ end
124
+
125
+ # デストラクタ
126
+ def finalize
127
+ return unless @is_logged_in
128
+
129
+ Wikidotrb::Module::HTTPAuthentication.logout(self)
130
+ @is_logged_in = false
131
+ @username = nil
132
+ end
133
+
134
+ def to_s
135
+ "Client(username=#{@username}, is_logged_in=#{@is_logged_in})"
136
+ end
137
+
138
+ # ログインチェック
139
+ def login_check
140
+ raise Wikidotrb::Common::Exceptions::LoginRequiredException, "Login is required to execute this function" unless @is_logged_in
141
+
142
+ nil
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "forum_category"
4
+ require_relative "forum_group"
5
+ require_relative "forum_thread"
6
+
7
+ module Wikidotrb
8
+ module Module
9
+ class ForumCategoryMethods
10
+ # 初期化メソッド
11
+ # @param forum [Forum] フォーラムオブジェクト
12
+ def initialize(forum)
13
+ @forum = forum
14
+ end
15
+
16
+ # カテゴリをIDから取得
17
+ # @param id [Integer] カテゴリID
18
+ # @return [ForumCategory] 更新されたカテゴリ
19
+ def get(id)
20
+ category = ForumCategory.new(
21
+ site: @forum.site,
22
+ id: id,
23
+ forum: @forum
24
+ )
25
+ category.update
26
+ end
27
+ end
28
+
29
+ class ForumThreadMethods
30
+ # 初期化メソッド
31
+ # @param forum [Forum] フォーラムオブジェクト
32
+ def initialize(forum)
33
+ @forum = forum
34
+ end
35
+
36
+ # スレッドをIDから取得
37
+ # @param id [Integer] スレッドID
38
+ # @return [ForumThread] 更新されたスレッド
39
+ def get(id)
40
+ thread = ForumThread.new(
41
+ site: @forum.site,
42
+ id: id,
43
+ forum: @forum
44
+ )
45
+ thread.update
46
+ end
47
+ end
48
+
49
+ class Forum
50
+ attr_accessor :site, :_groups, :_categories
51
+
52
+ # 初期化メソッド
53
+ # @param site [Site] サイトオブジェクト
54
+ def initialize(site:)
55
+ @site = site
56
+ @name = "Forum"
57
+ @_groups = nil
58
+ @_categories = nil
59
+ @category = ForumCategoryMethods.new(self)
60
+ @thread = ForumThreadMethods.new(self)
61
+ end
62
+
63
+ # カテゴリメソッドオブジェクトを取得
64
+ # @return [ForumCategoryMethods] カテゴリメソッド
65
+ attr_reader :category
66
+
67
+ # スレッドメソッドオブジェクトを取得
68
+ # @return [ForumThreadMethods] スレッドメソッド
69
+ attr_reader :thread
70
+
71
+ # フォーラムのURLを取得
72
+ # @return [String] フォーラムのURL
73
+ def get_url
74
+ "#{@site.get_url}/forum/start"
75
+ end
76
+
77
+ # グループのプロパティ
78
+ # @return [ForumGroupCollection] グループコレクション
79
+ def groups
80
+ ForumGroupCollection.get_groups(site: @site, forum: self) if @_groups.nil?
81
+ @_groups
82
+ end
83
+
84
+ # カテゴリのプロパティ
85
+ # @return [ForumCategoryCollection] カテゴリコレクション
86
+ def categories
87
+ ForumCategoryCollection.get_categories(site: @site, forum: self) if @_categories.nil?
88
+ @_categories
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "date"
5
+ require_relative "forum_thread"
6
+
7
+ module Wikidotrb
8
+ module Module
9
+ class ForumCategoryCollection < Array
10
+ attr_accessor :forum
11
+
12
+ # 初期化メソッド
13
+ # @param forum [Forum] フォーラムオブジェクト
14
+ # @param categories [Array<ForumCategory>] カテゴリーのリスト
15
+ def initialize(forum:, categories: [])
16
+ super(categories)
17
+ @forum = forum
18
+ end
19
+
20
+ # サイトとフォーラムからカテゴリを取得
21
+ # @param site [Site] サイトオブジェクト
22
+ # @param forum [Forum] フォーラムオブジェクト
23
+ def self.get_categories(site:, forum:)
24
+ categories = []
25
+
26
+ forum.groups.each do |group|
27
+ categories.concat(group.categories)
28
+ end
29
+
30
+ forum._categories = ForumCategoryCollection.new(forum: forum, categories: categories)
31
+ end
32
+
33
+ # IDまたはタイトルからカテゴリを検索
34
+ # @param id [Integer] カテゴリID
35
+ # @param title [String] カテゴリのタイトル
36
+ # @return [ForumCategory, nil] 一致するカテゴリ
37
+ def find(id: nil, title: nil)
38
+ find do |category|
39
+ (id.nil? || category.id == id) && (title.nil? || category.title == title)
40
+ end
41
+ end
42
+
43
+ # カテゴリ情報を取得して更新する
44
+ # @param forum [Forum] フォーラムオブジェクト
45
+ # @param categories [Array<ForumCategory>] カテゴリーのリスト
46
+ # @return [Array<ForumCategory>] 更新されたカテゴリのリスト
47
+ def self.acquire_update(forum:, categories:)
48
+ return categories if categories.empty?
49
+
50
+ responses = forum.site.amc_request(
51
+ bodies: categories.map { |category| { "c" => category.id, "moduleName" => "forum/ForumViewCategoryModule" } }
52
+ )
53
+
54
+ responses.each_with_index do |response, index|
55
+ category = categories[index]
56
+ html = Nokogiri::HTML(response.body.to_s)
57
+ statistics = html.at_css("div.statistics").text
58
+ description = html.at_css("div.description-block").text.strip
59
+ info = html.at_css("div.forum-breadcrumbs").text.match(%r{([ \S]*) / ([ \S]*)})
60
+ counts = statistics.scan(/\d+/).map(&:to_i)
61
+
62
+ category.last = nil if category.posts_counts != counts[1]
63
+ category.description = description.match(/[ \S]*$/)[0]
64
+ category.threads_counts, category.posts_counts = counts
65
+ category.group = category.forum.groups.find(info[1])
66
+ category.title = info[2]
67
+ pager_no = html.at_css("span.pager-no")
68
+ category.pagerno = pager_no.nil? ? 1 : pager_no.text.match(/of (\d+)/)[1].to_i
69
+ end
70
+
71
+ categories
72
+ end
73
+
74
+ # カテゴリ情報を更新
75
+ def update
76
+ ForumCategoryCollection.acquire_update(forum: @forum, categories: self)
77
+ end
78
+ end
79
+
80
+ class ForumCategory
81
+ attr_accessor :site, :id, :forum, :title, :description, :group, :threads_counts, :posts_counts, :pagerno
82
+ attr_reader :last
83
+
84
+ def initialize(site:, id:, forum:, title: nil, description: nil, group: nil, threads_counts: nil,
85
+ posts_counts: nil, pagerno: nil, last_thread_id: nil, last_post_id: nil)
86
+ @site = site
87
+ @id = id
88
+ @forum = forum
89
+ @title = title
90
+ @description = description
91
+ @group = group
92
+ @threads_counts = threads_counts
93
+ @posts_counts = posts_counts
94
+ @pagerno = pagerno
95
+ @_last_thread_id = last_thread_id
96
+ @_last_post_id = last_post_id
97
+ @_last = nil
98
+ end
99
+
100
+ # カテゴリのURLを取得
101
+ def get_url
102
+ "#{@site.get_url}/forum/c-#{@id}"
103
+ end
104
+
105
+ # カテゴリを更新
106
+ def update
107
+ ForumCategoryCollection.new(forum: @forum, categories: [self]).update.first
108
+ end
109
+
110
+ # 最後の投稿を取得
111
+ def last
112
+ @_last = @forum.thread.get(@_last_thread_id).get(@_last_post_id) if @_last_thread_id && @_last_post_id && @_last.nil?
113
+ @_last
114
+ end
115
+
116
+ # 最後の投稿を設定
117
+ def last=(value)
118
+ @_last = value
119
+ end
120
+
121
+ # スレッドのコレクションを取得する
122
+ # @return [ForumThreadCollection] スレッドオブジェクトのコレクション
123
+ def threads
124
+ client = @site.client
125
+ update
126
+ responses = @site.amc_request(
127
+ bodies: (1..@pagerno).map { |no| { "p" => no, "c" => @id, "moduleName" => "forum/ForumViewCategoryModule" } }
128
+ )
129
+
130
+ threads = []
131
+
132
+ responses.each do |response|
133
+ html = Nokogiri::HTML(response.body.to_s)
134
+ html.css("table.table tr.head~tr").each do |info|
135
+ title = info.at_css("div.title a")
136
+ thread_id = title["href"].match(/t-(\d+)/)[1].to_i
137
+ description = info.at_css("div.description").text.strip
138
+ user = info.at_css("span.printuser")
139
+ odate = info.at_css("span.odate")
140
+ posts_count = info.at_css("td.posts").text.to_i
141
+ last_id = info.at_css("td.last>a")
142
+ post_id = last_id.nil? ? nil : last_id["href"].match(/post-(\d+)/)[1].to_i
143
+
144
+ thread = ForumThread.new(
145
+ site: @site,
146
+ id: thread_id,
147
+ forum: @forum,
148
+ title: title.text.strip,
149
+ description: description,
150
+ created_by: user_parser(client, user),
151
+ created_at: odate_parser(odate),
152
+ posts_counts: posts_count,
153
+ _last_post_id: post_id
154
+ )
155
+
156
+ threads << thread
157
+ end
158
+ end
159
+
160
+ ForumThreadCollection.new(forum: @forum, threads: threads)
161
+ end
162
+
163
+ # 新しいスレッドを作成
164
+ def new_thread(title:, source:, description: "")
165
+ client = @site.client
166
+ client.login_check
167
+
168
+ response = @site.amc_request(
169
+ bodies: [
170
+ {
171
+ "category_id" => @id,
172
+ "title" => title,
173
+ "description" => description,
174
+ "source" => source,
175
+ "action" => "ForumAction",
176
+ "event" => "newThread"
177
+ }
178
+ ]
179
+ ).first
180
+
181
+ body = JSON.parse(response.body.to_s)
182
+
183
+ ForumThread.new(
184
+ site: @site,
185
+ id: body["threadId"].to_i,
186
+ forum: @forum,
187
+ category: self,
188
+ title: title,
189
+ description: description,
190
+ created_by: client.user.get(client.username),
191
+ created_at: DateTime.parse(body["CURRENT_TIMESTAMP"]),
192
+ posts_counts: 1
193
+ )
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "json"
5
+ require_relative "forum_category"
6
+
7
+ module Wikidotrb
8
+ module Module
9
+ class ForumGroupCollection < Array
10
+ attr_accessor :forum
11
+
12
+ # 初期化メソッド
13
+ # @param forum [Forum] フォーラムオブジェクト
14
+ # @param groups [Array<ForumGroup>] グループのリスト
15
+ def initialize(forum:, groups: [])
16
+ super(groups)
17
+ @forum = forum
18
+ end
19
+
20
+ # サイトとフォーラムからグループを取得
21
+ # @param site [Site] サイトオブジェクト
22
+ # @param forum [Forum] フォーラムオブジェクト
23
+ def self.get_groups(site:, forum:)
24
+ groups = []
25
+
26
+ response = site.amc_request(bodies: [{ "moduleName" => "forum/ForumStartModule", "hidden" => "true" }]).first
27
+ body = JSON.parse(response.body)["body"]
28
+ html = Nokogiri::HTML(body)
29
+
30
+ html.css("div.forum-group").each do |group_info|
31
+ group = ForumGroup.new(
32
+ site: site,
33
+ forum: forum,
34
+ title: group_info.at_css("div.title").text.strip,
35
+ description: group_info.at_css("div.description").text.strip
36
+ )
37
+
38
+ categories = []
39
+
40
+ group_info.css("table tr.head~tr").each do |info|
41
+ name = info.at_css("td.name")
42
+ thread_count = info.at_css("td.threads").text.strip.to_i
43
+ post_count = info.at_css("td.posts").text.strip.to_i
44
+ last_id = info.at_css("td.last>a")
45
+ if last_id.nil?
46
+ thread_id = nil
47
+ post_id = nil
48
+ else
49
+ thread_id, post_id = last_id["href"].match(/t-(\d+).+post-(\d+)/).captures.map(&:to_i)
50
+ end
51
+
52
+ category = ForumCategory.new(
53
+ site: site,
54
+ id: name.at_css("a")["href"].match(/c-(\d+)/)[1].to_i,
55
+ description: name.at_css("div.description").text.strip,
56
+ forum: forum,
57
+ title: name.at_css("a").text.strip,
58
+ group: group,
59
+ threads_counts: thread_count,
60
+ posts_counts: post_count,
61
+ last_thread_id: thread_id,
62
+ last_post_id: post_id
63
+ )
64
+
65
+ categories << category
66
+ end
67
+
68
+ group.categories = ForumCategoryCollection.new(forum: forum, categories: categories)
69
+
70
+ groups << group
71
+ end
72
+
73
+ forum._groups = ForumGroupCollection.new(forum: forum, groups: groups)
74
+ end
75
+
76
+ # グループをタイトルと説明から検索
77
+ # @param title [String] グループのタイトル
78
+ # @param description [String] グループの説明
79
+ # @return [ForumGroup, nil] 見つかったグループ
80
+ def find(title: nil, description: nil)
81
+ find do |group|
82
+ (title.nil? || group.title == title) && (description.nil? || group.description == description)
83
+ end
84
+ end
85
+
86
+ # 条件に一致するすべてのグループを検索
87
+ # @param title [String] グループのタイトル
88
+ # @param description [String] グループの説明
89
+ # @return [Array<ForumGroup>] 見つかったグループのリスト
90
+ def findall(title: nil, description: nil)
91
+ select do |group|
92
+ (title.nil? || group.title == title) && (description.nil? || group.description == description)
93
+ end
94
+ end
95
+ end
96
+
97
+ class ForumGroup
98
+ attr_accessor :site, :forum, :title, :description, :categories
99
+
100
+ def initialize(site:, forum:, title:, description:, categories: nil)
101
+ @site = site
102
+ @forum = forum
103
+ @title = title
104
+ @description = description
105
+ @categories = categories || ForumCategoryCollection.new(forum: forum)
106
+ end
107
+ end
108
+ end
109
+ end