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,598 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require_relative "../common/exceptions"
5
+ require_relative "forum_thread"
6
+ require_relative "page_revision"
7
+ require_relative "page_source"
8
+ require_relative "page_votes"
9
+ require_relative "../util/requestutil"
10
+ require_relative "../util/parser/user"
11
+ require_relative "../util/parser/odate"
12
+
13
+ module Wikidotrb
14
+ module Module
15
+ DEFAULT_MODULE_BODY = [
16
+ "fullname", # ページのフルネーム(str)
17
+ "category", # カテゴリ(str)
18
+ "name", # ページ名(str)
19
+ "title", # タイトル(str)
20
+ "created_at", # 作成日時(odate element)
21
+ "created_by_linked", # 作成者(user element)
22
+ "updated_at", # 更新日時(odate element)
23
+ "updated_by_linked", # 更新者(user element)
24
+ "commented_at", # コメント日時(odate element)
25
+ "commented_by_linked", # コメントしたユーザ(user element)
26
+ "parent_fullname", # 親ページのフルネーム(str)
27
+ "comments", # コメント数(int)
28
+ "size", # サイズ(int)
29
+ "children", # 子ページ数(int)
30
+ "rating_votes", # 投票数(int)
31
+ "rating", # レーティング(int or float)
32
+ "rating_percent", # 5つ星レーティング(%)
33
+ "revisions", # リビジョン数(int)
34
+ "tags", # タグのリスト(list of str)
35
+ "_tags" # 隠しタグのリスト(list of str)
36
+ ].freeze
37
+
38
+ class SearchPagesQuery
39
+ attr_accessor :pagetype, :category, :tags, :parent, :link_to, :created_at, :updated_at,
40
+ :created_by, :rating, :votes, :name, :fullname, :range, :order,
41
+ :offset, :limit, :perPage, :separate, :wrapper
42
+
43
+ def initialize(pagetype: "*", category: "*", tags: nil, parent: nil, link_to: nil, created_at: nil, updated_at: nil,
44
+ created_by: nil, rating: nil, votes: nil, name: nil, fullname: nil, range: nil, order: "created_at desc",
45
+ offset: 0, limit: nil, perPage: 250, separate: "no", wrapper: "no")
46
+ @pagetype = pagetype
47
+ @category = category
48
+ @tags = tags
49
+ @parent = parent
50
+ @link_to = link_to
51
+ @created_at = created_at
52
+ @updated_at = updated_at
53
+ @created_by = created_by
54
+ @rating = rating
55
+ @votes = votes
56
+ @name = name
57
+ @fullname = fullname
58
+ @range = range
59
+ @order = order
60
+ @offset = offset
61
+ @limit = limit
62
+ @perPage = perPage
63
+ @separate = separate
64
+ @wrapper = wrapper
65
+ end
66
+
67
+ def to_h
68
+ res = instance_variables.to_h { |var| [var.to_s.delete("@"), instance_variable_get(var)] }
69
+ res.compact!
70
+ res["tags"] = res["tags"].join(" ") if res["tags"].is_a?(Array)
71
+ res
72
+ end
73
+ end
74
+
75
+ class PageCollection < Array
76
+ attr_accessor :site
77
+
78
+ def initialize(site: nil, pages: [])
79
+ super(pages)
80
+ @site = site || pages.first&.site
81
+ end
82
+
83
+ def self.parse(site, html_body)
84
+ pages = []
85
+
86
+ html_body.css("div.page").each do |page_element|
87
+ page_params = {}
88
+
89
+ # レーティング方式を判定
90
+ is_5star_rating = !page_element.css("span.rating span.page-rate-list-pages-start").empty?
91
+
92
+ # 各値を取得
93
+ page_element.css("span.set").each do |set_element|
94
+ key = set_element.css("span.name").text.strip
95
+ value_element = set_element.css("span.value")
96
+
97
+ value = if value_element.empty?
98
+ nil
99
+ elsif %w[created_at updated_at commented_at].include?(key)
100
+ odate_element = value_element.css("span.odate")
101
+ odate_element.empty? ? nil : Wikidotrb::Util::Parser::ODateParser.parse(odate_element)
102
+ elsif %w[created_by_linked updated_by_linked commented_by_linked].include?(key)
103
+ printuser_element = value_element.css("span.printuser")
104
+ if printuser_element.empty?
105
+ nil
106
+ else
107
+ Wikidotrb::Util::Parser::UserParser.parse(site.client,
108
+ printuser_element)
109
+ end
110
+ elsif %w[tags _tags].include?(key)
111
+ value_element.text.split
112
+ elsif %w[rating_votes comments size revisions].include?(key)
113
+ value_element.text.strip.to_i
114
+ elsif key == "rating"
115
+ is_5star_rating ? value_element.text.strip.to_f : value_element.text.strip.to_i
116
+ elsif key == "rating_percent"
117
+ is_5star_rating ? value_element.text.strip.to_f / 100 : nil
118
+ else
119
+ value_element.text.strip
120
+ end
121
+
122
+ # keyを変換
123
+ key = key.gsub("_linked", "") if key.include?("_linked")
124
+ key = "#{key}_count" if %w[comments children revisions].include?(key)
125
+ key = "votes_count" if key == "rating_votes"
126
+
127
+ page_params[key.to_sym] = value
128
+ end
129
+
130
+ # タグのリストを統合
131
+ page_params[:tags] ||= []
132
+ page_params[:tags] += page_params.delete(:_tags) || []
133
+
134
+ # ページオブジェクトを作成
135
+ pages << Page.new(site: site, **page_params)
136
+ end
137
+
138
+ new(site: site, pages: pages)
139
+ end
140
+
141
+ def self.search_pages(site, query = SearchPagesQuery.new)
142
+ # クエリの初期化
143
+ query_dict = query.to_h
144
+ query_dict["moduleName"] = "list/ListPagesModule"
145
+ query_dict["module_body"] = %(
146
+ [[div class="page"]]
147
+ #{DEFAULT_MODULE_BODY.map do |key|
148
+ %(
149
+ [[span class="set #{key}"]]
150
+ [[span class="name"]]#{key}[[/span]]
151
+ [[span class="value"]]%%#{key}%%[[/span]]
152
+ [[/span]]
153
+ )
154
+ end.join("\n")}
155
+ [[/div]]
156
+ )
157
+
158
+ begin
159
+ # 初回リクエスト
160
+ response_data = site.amc_request(bodies: [query_dict])[0]
161
+ rescue Wikidotrb::Common::Exceptions::WikidotStatusCodeException => e
162
+ raise Wikidotrb::Common::Exceptions::ForbiddenException, "Failed to get pages, target site may be private" if e.status_code == "not_ok"
163
+
164
+ raise e
165
+ end
166
+
167
+ body = response_data["body"]
168
+ first_page_html_body = Nokogiri::HTML(body)
169
+
170
+ total = 1
171
+ html_bodies = [first_page_html_body]
172
+
173
+ # pagerの存在を確認
174
+ pager_element = first_page_html_body.css("div.pager")
175
+ unless pager_element.empty?
176
+ # 最大ページ数を取得
177
+ total = pager_element.css("span.target")[-2].css("a").text.to_i
178
+ end
179
+
180
+ # 複数ページが存在する場合はリクエストを繰り返す
181
+ if total > 1
182
+ request_bodies = []
183
+ (1...total).each do |i|
184
+ _query_dict = query_dict.dup
185
+ _query_dict["offset"] = i * query.perPage
186
+ request_bodies << _query_dict
187
+ end
188
+
189
+ responses = site.amc_request(bodies: request_bodies)
190
+ html_bodies.concat(responses.map { |response| Nokogiri::HTML(response["body"]) })
191
+ end
192
+
193
+ # 全てのHTMLボディをパースしてページコレクションを作成
194
+ pages = html_bodies.flat_map { |html_body| parse(site, html_body) }
195
+ new(site: site, pages: pages)
196
+ end
197
+
198
+ # メソッドを定義する部分の修正
199
+ def get_page_sources
200
+ PageCollection.acquire_page_sources(@site, self)
201
+ end
202
+
203
+ def get_page_ids
204
+ PageCollection.acquire_page_ids(@site, self)
205
+ end
206
+
207
+ def get_page_revisions
208
+ PageCollection.acquire_page_revisions(@site, self)
209
+ end
210
+
211
+ def get_page_votes
212
+ PageCollection.acquire_page_votes(@site, self)
213
+ end
214
+
215
+ def get_page_discuss
216
+ PageCollection.acquire_page_discuss(@site, self)
217
+ end
218
+
219
+ def self.acquire_page_sources(site, pages)
220
+ return pages if pages.empty?
221
+
222
+ responses = site.amc_request(
223
+ bodies: pages.map { |page| { "moduleName" => "viewsource/ViewSourceModule", "page_id" => page.id } }
224
+ )
225
+
226
+ pages.each_with_index do |page, index|
227
+ body = responses[index]["body"]
228
+ source = Nokogiri::HTML(body).at_css("div.page-source").text.strip
229
+ page.source = PageSource.new(page: page, wiki_text: source)
230
+ end
231
+
232
+ pages
233
+ end
234
+
235
+ def self.acquire_page_ids(site, pages)
236
+ target_pages = pages.reject(&:is_id_acquired?)
237
+ return pages if target_pages.empty?
238
+
239
+ responses = Wikidotrb::Util::RequestUtil.request(
240
+ client: site.client,
241
+ method: "GET",
242
+ urls: target_pages.map { |page| "#{page.get_url}/norender/true/noredirect/true" }
243
+ )
244
+
245
+ responses.each_with_index do |response, index|
246
+ source = response.body.to_s # Convert to string if necessary
247
+
248
+ id_match = source.match(/WIKIREQUEST\.info\.pageId = (\d+);/)
249
+ unless id_match
250
+ raise Wikidotrb::Common::Exceptions::UnexpectedException,
251
+ "Cannot find page id: #{target_pages[index].fullname}"
252
+ end
253
+
254
+ target_pages[index].id = id_match[1].to_i
255
+ end
256
+
257
+ pages
258
+ end
259
+
260
+ def self.acquire_page_revisions(site, pages)
261
+ return pages if pages.empty?
262
+
263
+ responses = site.amc_request(
264
+ bodies: pages.map do |page|
265
+ {
266
+ "moduleName" => "history/PageRevisionListModule",
267
+ "page_id" => page.id,
268
+ "options" => { "all" => true },
269
+ "perpage" => 100_000_000 # pagerを使わずに全て取得
270
+ }
271
+ end
272
+ )
273
+
274
+ responses.each_with_index do |response, index|
275
+ body = response["body"]
276
+ revs = []
277
+ body_html = Nokogiri::HTML(body)
278
+
279
+ body_html.css("table.page-history > tr[id^=revision-row-]").each do |rev_element|
280
+ rev_id = rev_element["id"].gsub("revision-row-", "").to_i
281
+
282
+ tds = rev_element.css("td")
283
+ rev_no = tds[0].text.strip.gsub(".", "").to_i
284
+ created_by = Wikidotrb::Util::Parser::UserParser.parse(site.client, tds[4].css("span.printuser").first)
285
+ created_at = Wikidotrb::Util::Parser::ODateParser.parse(tds[5].css("span.odate").first)
286
+ comment = tds[6].text.strip
287
+ revs << PageRevision.new(
288
+ page: pages[index],
289
+ id: rev_id,
290
+ rev_no: rev_no,
291
+ created_by: created_by,
292
+ created_at: created_at,
293
+ comment: comment
294
+ )
295
+ end
296
+ pages[index].revisions = revs
297
+ end
298
+
299
+ pages
300
+ end
301
+
302
+ def self.acquire_page_votes(site, pages)
303
+ return pages if pages.empty?
304
+
305
+ responses = site.amc_request(
306
+ bodies: pages.map { |page| { "moduleName" => "pagerate/WhoRatedPageModule", "pageId" => page.id } }
307
+ )
308
+
309
+ responses.each_with_index do |response, index|
310
+ body = response["body"]
311
+ html = Nokogiri::HTML(body)
312
+ user_elems = html.css("span.printuser")
313
+ value_elems = html.css('span[style^="color"]')
314
+
315
+ if user_elems.size != value_elems.size
316
+ raise Wikidotrb::Common::Exceptions::UnexpectedException,
317
+ "User and value count mismatch"
318
+ end
319
+
320
+ users = user_elems.map { |user_elem| Wikidotrb::Util::Parser::UserParser.parse(site.client, user_elem) }
321
+ values = value_elems.map do |value_elem|
322
+ value = value_elem.text.strip
323
+ if value == "+"
324
+ 1
325
+ elsif value == "-"
326
+ -1
327
+ else
328
+ value.to_i
329
+ end
330
+ end
331
+
332
+ votes = users.zip(values).map { |user, vote| PageVote.new(page: pages[index], user: user, value: vote) }
333
+ pages[index].votes = PageVoteCollection.new(page: pages[index], votes: votes)
334
+ end
335
+
336
+ pages
337
+ end
338
+
339
+ def self.acquire_page_discuss(site, pages)
340
+ target_pages = pages.reject(&:is_discuss_acquired?)
341
+ return pages if target_pages.empty?
342
+
343
+ responses = site.amc_request(
344
+ bodies: target_pages.map do |page|
345
+ {
346
+ "action" => "ForumAction",
347
+ "event" => "createPageDiscussionThread",
348
+ "page_id" => page.id,
349
+ "moduleName" => "Empty"
350
+ }
351
+ end
352
+ )
353
+
354
+ target_pages.each_with_index do |page, index|
355
+ page.discuss = ForumThread.new(site, responses[index]["thread_id"], page: page)
356
+ end
357
+ end
358
+ end
359
+
360
+ class Page
361
+ attr_accessor :site, :fullname, :name, :category, :title, :children_count,
362
+ :comments_count, :size, :rating, :votes_count, :rating_percent,
363
+ :revisions_count, :parent_fullname, :tags, :created_by, :created_at,
364
+ :updated_by, :updated_at, :commented_by, :commented_at, :_id,
365
+ :_source, :_revisions, :_votes, :_discuss
366
+
367
+ def initialize(site:, fullname:, name: "", category: "", title: "", children_count: 0, comments_count: 0, size: 0, rating: 0,
368
+ votes_count: 0, rating_percent: 0, revisions_count: 0, parent_fullname: "", tags: [], created_by: nil, created_at: nil,
369
+ updated_by: nil, updated_at: nil, commented_by: nil, commented_at: nil, _id: nil, _source: nil, _revisions: nil,
370
+ _votes: nil, _discuss: nil)
371
+ @site = site
372
+ @fullname = fullname
373
+ @name = name
374
+ @category = category
375
+ @title = title
376
+ @children_count = children_count
377
+ @comments_count = comments_count
378
+ @size = size
379
+ @rating = rating
380
+ @votes_count = votes_count
381
+ @rating_percent = rating_percent
382
+ @revisions_count = revisions_count
383
+ @parent_fullname = parent_fullname
384
+ @tags = tags
385
+ @created_by = created_by
386
+ @created_at = created_at
387
+ @updated_by = updated_by
388
+ @updated_at = updated_at
389
+ @commented_by = commented_by
390
+ @commented_at = commented_at
391
+ @_id = _id
392
+ @_source = _source
393
+ @_revisions = _revisions
394
+ @_votes = _votes
395
+ @_discuss = _discuss
396
+ end
397
+
398
+ def discuss
399
+ PageCollection.new(site: @site, pages: [self]).get_page_discuss if @_discuss.nil?
400
+ @_discuss.update
401
+ @_discuss
402
+ end
403
+
404
+ def discuss=(value)
405
+ @_discuss = value
406
+ end
407
+
408
+ def is_discuss_acquired?
409
+ !@_discuss.nil?
410
+ end
411
+
412
+ def get_url
413
+ "#{@site.get_url}/#{@fullname}"
414
+ end
415
+
416
+ def id
417
+ PageCollection.new(site: @site, pages: [self]).get_page_ids if @_id.nil?
418
+ @_id
419
+ end
420
+
421
+ def id=(value)
422
+ @_id = value
423
+ end
424
+
425
+ def is_id_acquired?
426
+ !@_id.nil?
427
+ end
428
+
429
+ def source
430
+ PageCollection.new(site: @site, pages: [self]).get_page_sources if @_source.nil?
431
+ @_source
432
+ end
433
+
434
+ def source=(value)
435
+ @_source = value
436
+ end
437
+
438
+ def revisions
439
+ PageCollection.new(site: @site, pages: [self]).get_page_revisions if @_revisions.nil?
440
+ PageRevisionCollection.new(page: self, revisions: @_revisions)
441
+ end
442
+
443
+ def revisions=(value)
444
+ @_revisions = value
445
+ end
446
+
447
+ def latest_revision
448
+ # revision_countとrev_noが一致するものを取得
449
+ @revisions.each do |revision|
450
+ return revision if revision.rev_no == @revisions_count
451
+ end
452
+
453
+ raise NotFoundException, "Cannot find latest revision"
454
+ end
455
+
456
+ def votes
457
+ PageCollection.new(site: @site, pages: [self]).get_page_votes if @_votes.nil?
458
+ @_votes
459
+ end
460
+
461
+ def votes=(value)
462
+ @_votes = value
463
+ end
464
+
465
+ def destroy
466
+ @site.client.login_check
467
+ @site.amc_request(bodies: [
468
+ {
469
+ action: "WikiPageAction",
470
+ event: "deletePage",
471
+ page_id: id,
472
+ moduleName: "Empty"
473
+ }
474
+ ])
475
+ end
476
+
477
+ def get_metas
478
+ response_data = @site.amc_request(bodies: [{ pageId: id, moduleName: "edit/EditMetaModule" }])[0]
479
+ body = response_data["body"]
480
+
481
+ metas = {}
482
+ body.scan(/&lt;meta name="([^"]+)" content="([^"]+)"/) do |meta|
483
+ metas[meta[0]] = meta[1]
484
+ end
485
+
486
+ metas
487
+ end
488
+
489
+ def set_meta(name, value)
490
+ @site.client.login_check
491
+ @site.amc_request(bodies: [
492
+ {
493
+ metaName: name,
494
+ metaContent: value,
495
+ action: "WikiPageAction",
496
+ event: "saveMetaTag",
497
+ pageId: id,
498
+ moduleName: "edit/EditMetaModule"
499
+ }
500
+ ])
501
+ end
502
+
503
+ def delete_meta(name)
504
+ @site.client.login_check
505
+ @site.amc_request(bodies: [
506
+ {
507
+ metaName: name,
508
+ action: "WikiPageAction",
509
+ event: "deleteMetaTag",
510
+ pageId: id,
511
+ moduleName: "edit/EditMetaModule"
512
+ }
513
+ ])
514
+ end
515
+
516
+ def self.create_or_edit(site:, fullname:, page_id: nil, title: "", source: "", comment: "", force_edit: false, raise_on_exists: false)
517
+ site.client.login_check
518
+
519
+ page_lock_request_body = {
520
+ mode: "page",
521
+ wiki_page: fullname,
522
+ moduleName: "edit/PageEditModule"
523
+ }
524
+ page_lock_request_body[:force_lock] = "yes" if force_edit
525
+
526
+ # `site.amc_request` returns a Hash, no need to call `.json`
527
+ page_lock_response_data = site.amc_request(bodies: [page_lock_request_body])[0]
528
+
529
+ raise Wikidotrb::Common::Exceptions::TargetErrorException, "Page #{fullname} is locked or other locks exist" if page_lock_response_data["locked"] || page_lock_response_data["other_locks"]
530
+
531
+ is_exist = page_lock_response_data.key?("page_revision_id")
532
+
533
+ raise Wikidotrb::Common::Exceptions::TargetExistsException, "Page #{fullname} already exists" if raise_on_exists && is_exist
534
+
535
+ raise ArgumentError, "page_id must be specified when editing existing page" if is_exist && page_id.nil?
536
+
537
+ lock_id = page_lock_response_data["lock_id"]
538
+ lock_secret = page_lock_response_data["lock_secret"]
539
+ page_revision_id = page_lock_response_data["page_revision_id"]
540
+
541
+ edit_request_body = {
542
+ action: "WikiPageAction",
543
+ event: "savePage",
544
+ moduleName: "Empty",
545
+ mode: "page",
546
+ lock_id: lock_id,
547
+ lock_secret: lock_secret,
548
+ revision_id: page_revision_id || "",
549
+ wiki_page: fullname,
550
+ page_id: page_id || "",
551
+ title: title,
552
+ source: source,
553
+ comments: comment
554
+ }
555
+ response_data = site.amc_request(bodies: [edit_request_body])[0]
556
+
557
+ unless response_data["status"] == "ok"
558
+ raise WikidotStatusCodeException,
559
+ "Failed to create or edit page: #{fullname}"
560
+ end
561
+
562
+ res = PageCollection.search_pages(site, Wikidotrb::Module::SearchPagesQuery.new(fullname: fullname))
563
+ raise NotFoundException, "Page creation failed: #{fullname}" if res.empty?
564
+
565
+ res[0]
566
+ end
567
+
568
+ def edit(title: nil, source: nil, comment: nil, force_edit: false)
569
+ title ||= @title
570
+ source ||= @source.wiki_text
571
+ comment ||= ""
572
+
573
+ Page.create_or_edit(
574
+ site: @site,
575
+ fullname: @fullname,
576
+ page_id: id,
577
+ title: title,
578
+ source: source,
579
+ comment: comment,
580
+ force_edit: force_edit
581
+ )
582
+ end
583
+
584
+ def set_tags(tags)
585
+ @site.client.login_check
586
+ @site.amc_request(bodies: [
587
+ {
588
+ tags: tags.join(" "),
589
+ action: "WikiPageAction",
590
+ event: "saveTags",
591
+ pageId: id,
592
+ moduleName: "Empty"
593
+ }
594
+ ])
595
+ end
596
+ end
597
+ end
598
+ end