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