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,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