itest5ch 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0c541a98e77de8fb2fbdf1e8053fe17701115e8b2ffb20a46a56d3927a5e02cb
4
+ data.tar.gz: 98be41ebe60e8d3cc30c2334d1dd05003f10864e039d8511413d32d8ccb84ad6
5
+ SHA512:
6
+ metadata.gz: b7133eda1d48f3a1fc852ae5d4b3e4567e442e50afe4f441578fb7376371945643a682949c36decfe41e78699edf5a603d01fee4fa6bcc563e5ab6bcd1d0a32a
7
+ data.tar.gz: dcf94763f4d7cbc7850263e1207ccfcfcdcb365f1a6f90a8053c82e546bf70a11ec78fd4244d6ecb365f74c53b08ca7e4a77e0dd19ecd9a53fff6406be638e0f
@@ -0,0 +1,127 @@
1
+ # Ruby CircleCI 2.0 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
+ #
5
+ version: 2
6
+
7
+ default: &default
8
+ docker:
9
+ - image: ruby
10
+ environment:
11
+ # c.f. https://github.com/ffaker/ffaker/issues/277#issuecomment-263519146
12
+ LANG: en_US.UTF-8
13
+ LANGUAGE: en_US.UTF-8
14
+ LC_ALL: C.UTF-8
15
+
16
+ BUNDLE_PATH: vendor/bundle
17
+ BUNDLE_JOBS: 4
18
+
19
+ working_directory: ~/app
20
+
21
+ restore_repo_cache_option: &restore_repo_cache_option
22
+ key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
23
+
24
+ save_repo_cache_option: &save_repo_cache_option
25
+ key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
26
+ paths:
27
+ - ~/app
28
+
29
+ restore_bundle_cache_option: &restore_bundle_cache_option
30
+ keys:
31
+ - v3-bundle-{{ .Environment.CIRCLE_JOB }}-{{ checksum "itest5ch.gemspec" }}
32
+ - v3-bundle-{{ .Environment.CIRCLE_JOB }}
33
+
34
+ save_bundle_cache_option: &save_bundle_cache_option
35
+ key: v3-bundle-{{ .Environment.CIRCLE_JOB }}-{{ checksum "itest5ch.gemspec" }}
36
+ paths:
37
+ - ~/app/vendor/bundle
38
+ - ~/app/Gemfile.lock
39
+
40
+ rspec_steps: &rspec_steps
41
+ - restore_cache:
42
+ <<: *restore_repo_cache_option
43
+ - restore_cache:
44
+ <<: *restore_bundle_cache_option
45
+ - run: ./.circleci/setup.sh
46
+ - save_cache:
47
+ <<: *save_bundle_cache_option
48
+
49
+ - run: bundle exec rspec
50
+
51
+ build_jobs: &build_jobs
52
+ - checkout_code
53
+ - rspec:2.3:
54
+ requires:
55
+ - checkout_code
56
+ - rspec:2.4:
57
+ requires:
58
+ - checkout_code
59
+ - rspec:2.5:
60
+ requires:
61
+ - checkout_code
62
+ - rubocop:
63
+ requires:
64
+ - checkout_code
65
+
66
+ jobs:
67
+ checkout_code:
68
+ <<: *default
69
+
70
+ steps:
71
+ - checkout
72
+
73
+ - save_cache:
74
+ <<: *save_repo_cache_option
75
+
76
+ rspec:2.3:
77
+ <<: *default
78
+
79
+ docker:
80
+ - image: ruby:2.3
81
+
82
+ steps: *rspec_steps
83
+
84
+ rspec:2.4:
85
+ <<: *default
86
+
87
+ docker:
88
+ - image: ruby:2.4
89
+
90
+ steps: *rspec_steps
91
+
92
+ rspec:2.5:
93
+ <<: *default
94
+
95
+ docker:
96
+ - image: ruby:2.5
97
+
98
+ steps: *rspec_steps
99
+
100
+ rubocop:
101
+ <<: *default
102
+
103
+ steps:
104
+ - restore_cache:
105
+ <<: *restore_repo_cache_option
106
+ - restore_cache:
107
+ <<: *restore_bundle_cache_option
108
+ - run: ./.circleci/setup.sh
109
+ - save_cache:
110
+ <<: *save_bundle_cache_option
111
+
112
+ - run: bundle exec rubocop
113
+
114
+ workflows:
115
+ version: 2
116
+
117
+ build:
118
+ jobs: *build_jobs
119
+
120
+ weekly_build:
121
+ triggers:
122
+ - schedule:
123
+ cron: "00 19 * * 5" # JST 4:00 (Sat)
124
+ filters:
125
+ branches:
126
+ only: master
127
+ jobs: *build_jobs
@@ -0,0 +1,8 @@
1
+ #!/bin/bash -xe
2
+
3
+ ruby --version
4
+ bundle --version
5
+ gem --version
6
+
7
+ bundle update
8
+ bundle clean
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ repo_token: ENHg6l26Ivgty7gR2Hjh5vEqOg2gi2Dzk
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ inherit_gem:
2
+ onkcop:
3
+ - "config/rubocop.yml"
4
+ - "config/rspec.yml"
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 2.3
8
+ # uncomment if use rails cops
9
+ # TargetRailsVersion: 5.1
10
+
11
+ require: rubocop-rspec
12
+
13
+ RSpec/FilePath:
14
+ Exclude:
15
+ - spec/integration_spec.rb
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup markdown
2
+ --no-private
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## master
2
+ [full changelog](http://github.com/sue445/itest5ch/compare/v1.0.0...master)
3
+
4
+ ## v1.0.0
5
+ * first release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in itest5ch.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 sue445
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # Itest5ch
2
+
3
+ 5ch (a.k.a. 2ch) reader via http://itest.5ch.net/
4
+
5
+ [![CircleCI](https://circleci.com/gh/sue445/itest5ch/tree/master.svg?style=svg&circle-token=6424acab43b6dd71c3e09619ff7bb2e9b5611aa0)](https://circleci.com/gh/sue445/itest5ch/tree/master)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'itest5ch'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install itest5ch
22
+
23
+ ## Usage
24
+
25
+ ### [Itest5ch::Board](lib/itest5ch/board.rb)
26
+
27
+ #### Get all boards
28
+
29
+ ```ruby
30
+ category_boards = Itest5ch::Board.all
31
+ #=> {"地震"=>
32
+ [#<Itest5ch::Board:0x00007f821c5bb4b8 @name="地震headline", @url="http://itest.5ch.net/subback/bbynamazu">,
33
+ #<Itest5ch::Board:0x00007f821c5b9f78 @name="地震速報", @url="http://itest.5ch.net/subback/namazuplus">,
34
+ #<Itest5ch::Board:0x00007f821c5b8a38 @name="臨時地震", @url="http://itest.5ch.net/subback/eq">,
35
+ #<Itest5ch::Board:0x00007f821c5c37d0 @name="臨時地震+", @url="http://itest.5ch.net/subback/eqplus">,
36
+ #<Itest5ch::Board:0x00007f821c5c2290 @name="緊急自然災害", @url="http://itest.5ch.net/subback/lifeline">],
37
+ "おすすめ"=>
38
+ [#<Itest5ch::Board:0x00007f821c5d3dd8 @name="スマートフォン", @url="http://itest.5ch.net/subback/smartphone">,
39
+ #<Itest5ch::Board:0x00007f821c5d2898 @name="ハワイ州", @url="http://itest.5ch.net/subback/hawaii">,
40
+ #<Itest5ch::Board:0x00007f821c5d1358 @name="大学生活", @url="http://itest.5ch.net/subback/campus">,
41
+ #<Itest5ch::Board:0x00007f821c5dbf38 @name="恋愛サロン", @url="http://itest.5ch.net/subback/lovesaloon">,
42
+ #<Itest5ch::Board:0x00007f821c5da9f8 @name="アレルギー", @url="http://itest.5ch.net/subback/allergy">,
43
+ #<Itest5ch::Board:0x00007f821c5d94b8 @name="ラブライブ!", @url="http://itest.5ch.net/subback/lovelive">,
44
+ #<Itest5ch::Board:0x00007f821ca0e8d0 @name="Apple", @url="http://itest.5ch.net/subback/apple2">,
45
+ #<Itest5ch::Board:0x00007f821c5e2ba8 @name="初心者の質問", @url="http://itest.5ch.net/subback/qa">],
46
+ ```
47
+
48
+ #### Get boards of a category
49
+
50
+ ```ruby
51
+ boards = Itest5ch::Board.find_category_boards("おすすめ")
52
+ #=> [#<Itest5ch::Board:0x00007f821c639840 @name="スマートフォン", @url="http://itest.5ch.net/subback/smartphone">,
53
+ #<Itest5ch::Board:0x00007f821c628dd8 @name="ハワイ州", @url="http://itest.5ch.net/subback/hawaii">,
54
+ #<Itest5ch::Board:0x00007f821c6185c8 @name="大学生活", @url="http://itest.5ch.net/subback/campus">,
55
+ #<Itest5ch::Board:0x00007f821c67bf10 @name="恋愛サロン", @url="http://itest.5ch.net/subback/lovesaloon">,
56
+ #<Itest5ch::Board:0x00007f821c66b728 @name="アレルギー", @url="http://itest.5ch.net/subback/allergy">,
57
+ #<Itest5ch::Board:0x00007f821c65add8 @name="ラブライブ!", @url="http://itest.5ch.net/subback/lovelive">,
58
+ #<Itest5ch::Board:0x00007f821c64a258 @name="Apple", @url="http://itest.5ch.net/subback/apple2">,
59
+ #<Itest5ch::Board:0x00007f821c6b9658 @name="初心者の質問", @url="http://itest.5ch.net/subback/qa">]
60
+ ```
61
+
62
+ #### Get a board
63
+ ```ruby
64
+ board = Itest5ch::Board.find("スマートフォン")
65
+
66
+ # or
67
+
68
+ board = Itest5ch::Board.find("smartphone")
69
+ ```
70
+
71
+ ### [Itest5ch::Thread](lib/itest5ch/thread.rb)
72
+ #### Get threads
73
+ ```ruby
74
+ threads = board.threads
75
+ ```
76
+
77
+ #### Get a thread
78
+ ```ruby
79
+ # with PC url
80
+ thread = Itest5ch::Thread.new("http://egg.5ch.net/test/read.cgi/smartphone/0000000000")
81
+
82
+ # or
83
+
84
+ # with Smartphone url
85
+ thread = Itest5ch::Thread.new("http://itest.5ch.net/egg/test/read.cgi/smartphone/0000000000")
86
+ ```
87
+
88
+ ### [Itest5ch::Comment](lib/itest5ch/comment.rb)
89
+ ```ruby
90
+ comments = thread.comments
91
+ ```
92
+
93
+ ### [Itest5ch::Config](lib/itest5ch/config.rb)
94
+ ```ruby
95
+ Itest5ch.config.user_agent = "XXXX"
96
+ ```
97
+
98
+ * `user_agent` : User Agent
99
+
100
+ ## ProTip
101
+ When `Time.zone` is initialized, `Itest5ch::Comment#date` returns `ActiveSupport::TimeWithZone` instead of `Time` (requirements `activesupport`)
102
+
103
+ ```ruby
104
+ comment.date
105
+ #=> 2018-03-06 12:34:56 +0900
106
+ comment.date.class
107
+ #=> Time
108
+ ```
109
+
110
+ ```ruby
111
+ require "active_support/time"
112
+ Time.zone = "Tokyo"
113
+
114
+ comment.date
115
+ #=> Tue, 06 Mar 2018 12:34:56 JST +09:00
116
+ comment.date.class
117
+ #=> ActiveSupport::TimeWithZone
118
+ ```
119
+
120
+ ## Development
121
+
122
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
123
+
124
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
125
+
126
+ ## Contributing
127
+
128
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sue445/itest5ch.
129
+
130
+ ## License
131
+
132
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ desc "Run RSpec code examples (unit test)"
5
+ RSpec::Core::RakeTask.new("spec:unit") do |task|
6
+ task.rspec_opts = "--tag ~type:integration"
7
+ end
8
+
9
+ desc "Run RSpec code examples (integration test)"
10
+ RSpec::Core::RakeTask.new("spec:integration") do |task|
11
+ task.rspec_opts = "--tag type:integration"
12
+ end
13
+
14
+ task :spec => ["spec:unit", "spec:integration"]
15
+
16
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "itest5ch"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/itest5ch.gemspec ADDED
@@ -0,0 +1,42 @@
1
+
2
+ lib = File.expand_path("lib", __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "itest5ch/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "itest5ch"
8
+ spec.version = Itest5ch::VERSION
9
+ spec.authors = ["sue445"]
10
+ spec.email = ["sue445@sue445.net"]
11
+
12
+ spec.summary = "5ch (a.k.a. 2ch) reader via itest.5ch.net"
13
+ spec.description = "5ch (a.k.a. 2ch) reader via itest.5ch.net"
14
+ spec.homepage = "https://github.com/sue445/itest5ch"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "hpricot"
25
+ spec.add_dependency "htmlentities"
26
+
27
+ spec.add_development_dependency "activesupport"
28
+ spec.add_development_dependency "bundler", "~> 1.16"
29
+ spec.add_development_dependency "coveralls"
30
+ spec.add_development_dependency "onkcop", "0.53.0.0"
31
+ spec.add_development_dependency "pry-byebug"
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "retryable"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "rspec-its"
36
+ spec.add_development_dependency "rspec-parameterized"
37
+ spec.add_development_dependency "rubocop", "0.53.0"
38
+ spec.add_development_dependency "rubocop-rspec", "1.25.1"
39
+ spec.add_development_dependency "simplecov"
40
+ spec.add_development_dependency "webmock"
41
+ spec.add_development_dependency "yard"
42
+ end
data/lib/itest5ch.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "itest5ch/version"
2
+ require "hpricot"
3
+ require "htmlentities"
4
+
5
+ module Itest5ch
6
+ autoload :AssertMethods, "itest5ch/assert_methods"
7
+ autoload :Board, "itest5ch/board"
8
+ autoload :BoardListPage, "itest5ch/board_list_page"
9
+ autoload :Comment, "itest5ch/comment"
10
+ autoload :Config, "itest5ch/config"
11
+ autoload :HttpMethods, "itest5ch/http_methods"
12
+ autoload :Thread, "itest5ch/thread"
13
+
14
+ # @return [Itest5ch::Config]
15
+ def self.config
16
+ @config ||= Config.new
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module Itest5ch
2
+ module AssertMethods
3
+ # @param hash [Hash]
4
+ # @param keys [Array<Symbol>]
5
+ #
6
+ # @raise [ArgumentError]
7
+ def assert_required_keys!(hash, *keys)
8
+ keys.each do |key|
9
+ assert_required!(key, hash[key])
10
+ end
11
+ end
12
+
13
+ # @param name [Symbol]
14
+ # @param value [Object]
15
+ #
16
+ # @raise [ArgumentError]
17
+ def assert_required!(name, value)
18
+ raise ArgumentError, "#{name} is required" unless value
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,76 @@
1
+ module Itest5ch
2
+ class Board
3
+ include HttpMethods
4
+
5
+ # @!attribute [rw] url
6
+ # @return [String]
7
+ attr_accessor :url
8
+
9
+ # @!attribute [rw] name
10
+ # @return [String]
11
+ attr_accessor :name
12
+
13
+ # @param url [String]
14
+ # @param name [String]
15
+ def initialize(url, name: nil)
16
+ @url = url
17
+ @name = name
18
+ end
19
+
20
+ # @param other [Itest5ch::Board]
21
+ #
22
+ # @return [Boolean]
23
+ def ==(other)
24
+ other.is_a?(Board) && url == other.url && name == other.name
25
+ end
26
+
27
+ # @return [Array<Itest5ch::Thread>]
28
+ def threads
29
+ hash = get_json(json_url, referer: url)
30
+ hash["threads"].map do |thread|
31
+ board, dat = thread[3].split("/", 2)
32
+ Itest5ch::Thread.new(
33
+ subdomain: thread[2],
34
+ board: board,
35
+ dat: dat.to_i,
36
+ name: thread[5],
37
+ )
38
+ end
39
+ end
40
+
41
+ # @return [String]
42
+ def json_url
43
+ if (m = url.match(%r{^https?://itest\.5ch\.net/subback/(.+?)/?$}))
44
+ return "http://itest.5ch.net/subbacks/#{m[1]}.json"
45
+ end
46
+
47
+ if (m = url.match(%r{^https?://.+\.5ch\.net/(.+?)/?$}))
48
+ return "http://itest.5ch.net/subbacks/#{m[1]}.json"
49
+ end
50
+
51
+ raise "Unknown url: #{url}"
52
+ end
53
+
54
+ # Get all boards
55
+ #
56
+ # @return [Hash<String, Array<Itest5ch::Board>>] key: category name, value: boards
57
+ def self.all
58
+ BoardListPage.new.all
59
+ end
60
+
61
+ # @param category_name [String]
62
+ #
63
+ # @return [Array<Itest5ch::Board>]
64
+ def self.find_category_boards(category_name)
65
+ all[category_name]
66
+ end
67
+
68
+ # @param board_name [String] name or id
69
+ #
70
+ # @return [Itest5ch::Board]
71
+ def self.find(board_name)
72
+ url = "#{Itest5ch::BoardListPage::BOARDS_URL}subback/#{board_name}"
73
+ all.values.flatten.find {|board| board_name == board.name || url == board.url }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ module Itest5ch
2
+ class BoardListPage
3
+ include HttpMethods
4
+
5
+ BOARDS_URL = "http://itest.5ch.net/".freeze
6
+
7
+ # Get all boards
8
+ #
9
+ # @return [Hash<String, Array<Itest5ch::Board>>] key: category name, value: boards
10
+ def all
11
+ doc = Hpricot(get_html(BOARDS_URL))
12
+
13
+ doc.search("//div[@id='bbsmenu']//ul[@class='pure-menu-list']").
14
+ reject {|ul| ul["id"] == "history" }.each_with_object({}) do |ul, categories|
15
+
16
+ category_name = ul.at("/li[@class='pure-menu-item pure-menu-selected']").inner_text.strip
17
+ categories[category_name] = get_boards(ul)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def get_boards(ul)
24
+ ul.search("/li").select {|li| board_element?(li) }.each_with_object([]) do |li, boards|
25
+ url = URI.join(BOARDS_URL, li.at("/a")["href"]).to_s
26
+ name = li.inner_text.strip
27
+
28
+ boards << Board.new(url, name: name)
29
+ end
30
+ end
31
+
32
+ def board_element?(li)
33
+ return false unless li["class"].include?("pure-menu-item")
34
+ return false if li["class"].include?("pure-menu-selected")
35
+
36
+ true
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,91 @@
1
+ module Itest5ch
2
+ require "base64"
3
+
4
+ class Comment
5
+ # @!attribute number
6
+ # @return [Integer]
7
+ attr_accessor :number
8
+
9
+ # @!attribute name
10
+ # @return [String]
11
+ attr_accessor :name
12
+
13
+ # @!attribute mail
14
+ # @return [String]
15
+ attr_accessor :mail
16
+
17
+ # @!attribute date
18
+ # @return [Time]
19
+ attr_accessor :date
20
+
21
+ # @!attribute id
22
+ # @return [String]
23
+ attr_accessor :id
24
+
25
+ # @!attribute message
26
+ # @return [String]
27
+ attr_accessor :message
28
+
29
+ # @!attribute thread
30
+ # @return [Itest5ch::Thread]
31
+ attr_accessor :thread
32
+
33
+ # @param number [Integer]
34
+ # @param name [String]
35
+ # @param mail [String]
36
+ # @param date [Time]
37
+ # @param id [String]
38
+ # @param message [String]
39
+ # @param thread [Itest5ch::Thread]
40
+ def initialize(number:, name:, mail:, date:, id:, message:, thread:) # rubocop:disable Metrics/ParameterLists
41
+ @number = number
42
+ @name = name
43
+ @mail = mail
44
+ @date = date
45
+ @id = id
46
+ @message = message
47
+ @thread = thread
48
+ end
49
+
50
+ # @return [Array<Integer>]
51
+ def anchor_numbers
52
+ numbers =
53
+ message.scan(/[>>]+([0-90-9\-]+)/).map do |result|
54
+ str = result.first.tr("0-9", "0-9")
55
+ if (m = str.match(/([0-9]+)-([0-9]+)/))
56
+ (m[1].to_i..m[2].to_i).to_a
57
+ else
58
+ str.to_i
59
+ end
60
+ end
61
+
62
+ numbers.flatten.compact
63
+ end
64
+
65
+ alias_method :reply_numbers, :anchor_numbers
66
+
67
+ # @return [String]
68
+ def pc_url
69
+ "#{thread.pc_url}/#{number}"
70
+ end
71
+
72
+ # @return [String]
73
+ def smartphone_url
74
+ "#{thread.smartphone_url}/#{number}"
75
+ end
76
+
77
+ # Get Id checker url for http://hissi.org
78
+ #
79
+ # @return [String]
80
+ # @return [nil] {#id} is empty
81
+ #
82
+ # @see http://hissi.org
83
+ def id_checker_url
84
+ return nil if !id || id.empty?
85
+
86
+ ymd = date.strftime("%Y%m%d")
87
+ encoded_id = Base64.strict_encode64(id).delete("=")
88
+ "http://hissi.org/read.php/#{thread.board}/#{ymd}/#{encoded_id}.html"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,7 @@
1
+ module Itest5ch
2
+ class Config
3
+ # @!attribute [rw] user_agent
4
+ # @return [String]
5
+ attr_accessor :user_agent
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ module Itest5ch
2
+ module HttpMethods
3
+ require "open-uri"
4
+ require "json"
5
+
6
+ # @param url [String]
7
+ # @param referer [String]
8
+ #
9
+ # @return [String]
10
+ def get_html(url, referer: nil)
11
+ options = {}
12
+ options["User-Agent"] = Itest5ch.config.user_agent if Itest5ch.config.user_agent
13
+ options["Referer"] = referer if referer
14
+
15
+ open(url, options).read # rubocop:disable Security/Open
16
+ end
17
+
18
+ # @param url [String]
19
+ # @param referer [String]
20
+ #
21
+ # @return [Hash]
22
+ def get_json(url, referer: nil)
23
+ JSON.parse(get_html(url, referer: referer))
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,173 @@
1
+ module Itest5ch
2
+ require "securerandom"
3
+ require "cgi"
4
+
5
+ class Thread
6
+ include HttpMethods
7
+ include AssertMethods
8
+
9
+ # @!attribute [rw] subdomain
10
+ # @return [String]
11
+ attr_accessor :subdomain
12
+
13
+ # @!attribute [rw] board
14
+ # @return [String]
15
+ attr_accessor :board
16
+
17
+ # @!attribute [rw] dat
18
+ # @return [Integer]
19
+ attr_accessor :dat
20
+
21
+ # @!attribute [w] name
22
+ attr_writer :name
23
+
24
+ # @overload initialize(subdomain:, board:, dat:, name: nil)
25
+ # Set attributes
26
+ #
27
+ # @param subdomain [String]
28
+ # @param board [String]
29
+ # @param dat [Integer]
30
+ # @param name [String]
31
+ #
32
+ # @example
33
+ # thread = Itest5ch::Thread.new(subdomain: "egg", board: "applism", dat: 1234567890)
34
+ #
35
+ # @overload initialize(url)
36
+ # Set thread url (PC or SmartPhone)
37
+ #
38
+ # @param url [String] thread url
39
+ #
40
+ # @example with SmartPhone url
41
+ # thread = Itest5ch::Thread.new("http://itest.5ch.net/egg/test/read.cgi/applism/1234567890")
42
+ #
43
+ # @example with PC url
44
+ # thread = Itest5ch::Thread.new("http://egg.5ch.net/test/read.cgi/applism/1234567890")
45
+ #
46
+ # @raise [ArgumentError] `arg` is not either Hash and String
47
+ def initialize(args)
48
+ case args
49
+ when Hash
50
+ initialize_with_hash(args)
51
+ when String
52
+ initialize_with_string(args)
53
+ else
54
+ raise ArgumentError, "args is either Hash or String is required"
55
+ end
56
+ end
57
+
58
+ # @param other [Itest5ch::Thread]
59
+ #
60
+ # @return [Boolean]
61
+ def ==(other)
62
+ other.is_a?(Thread) && subdomain == other.subdomain && board == other.board &&
63
+ dat == other.dat && name == other.name
64
+ end
65
+
66
+ # @return [Array<Itest5ch::Comment>]
67
+ def comments
68
+ fetch_data["comments"].map do |comment|
69
+ Comment.new(
70
+ number: comment[0].to_i,
71
+ name: comment[1],
72
+ mail: comment[2],
73
+ date: time_at(comment[3].to_i),
74
+ id: comment[4],
75
+ message: self.class.normalize_message(comment[6]),
76
+ thread: self,
77
+ )
78
+ end
79
+ end
80
+
81
+ # @param message [String]
82
+ #
83
+ # @return [String]
84
+ def self.normalize_message(message)
85
+ message = coder.decode(message).scrub("")
86
+ message = CGI.unescapeHTML(message)
87
+ message.gsub(/\s*<br>\s*/i, "\n").strip
88
+ end
89
+
90
+ def self.coder
91
+ @coder ||= HTMLEntities.new
92
+ end
93
+ private_class_method :coder
94
+
95
+ # @return [String]
96
+ def smartphone_url
97
+ "http://itest.5ch.net/#{subdomain}/test/read.cgi/#{board}/#{dat}"
98
+ end
99
+
100
+ # @return [String]
101
+ def pc_url
102
+ "http://#{subdomain}.5ch.net/test/read.cgi/#{board}/#{dat}"
103
+ end
104
+
105
+ # @return [String]
106
+ def name
107
+ @name ||= fetch_name
108
+ end
109
+
110
+ # @return [String] thread name
111
+ def fetch_name
112
+ fetch_data["thread"][5]
113
+ end
114
+
115
+ # @return [String]
116
+ def json_url
117
+ "http://itest.5ch.net/public/newapi/client.php?subdomain=#{subdomain}&board=#{board}&dat=#{dat}&rand=#{rand}"
118
+ end
119
+
120
+ private
121
+
122
+ # @param hash [Hash]
123
+ def initialize_with_hash(hash)
124
+ assert_required_keys!(hash, :subdomain, :board, :dat)
125
+
126
+ @subdomain = hash[:subdomain]
127
+ @board = hash[:board]
128
+ @dat = hash[:dat]
129
+ @name = hash[:name]
130
+ end
131
+
132
+ # @param url [String]
133
+ def initialize_with_string(url)
134
+ if (m = url.match(%r{https?://itest\.5ch\.net/(.+)/test/read\.cgi/([^/]+)/([0-9]+)}))
135
+ @subdomain = m[1]
136
+ @board = m[2]
137
+ @dat = m[3].to_i
138
+ return
139
+ end
140
+
141
+ if (m = url.match(%r{https?://(.+)\.5ch\.net/test/read\.cgi/([^/]+)/([0-9]+)}))
142
+ @subdomain = m[1]
143
+ @board = m[2]
144
+ @dat = m[3].to_i
145
+ return
146
+ end
147
+
148
+ raise ArgumentError, "'#{url}' is invalid url format"
149
+ end
150
+
151
+ # @return [Hash]
152
+ def fetch_data
153
+ get_json(json_url, referer: smartphone_url)
154
+ end
155
+
156
+ # @return [String] random 10 char string
157
+ def rand
158
+ SecureRandom.hex(5)
159
+ end
160
+
161
+ # @param unixtime [Integer]
162
+ #
163
+ # @return [ActiveSupport::TimeWithZone] When `Time.zone` is initialized
164
+ # @return [Time] When `Time.zone` is not initialized or without activesupport
165
+ def time_at(unixtime)
166
+ if Time.respond_to?(:zone) && Time.zone.respond_to?(:at)
167
+ Time.zone.at(unixtime)
168
+ else
169
+ Time.at(unixtime)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,3 @@
1
+ module Itest5ch
2
+ VERSION = "1.0.0".freeze
3
+ end
metadata ADDED
@@ -0,0 +1,306 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: itest5ch
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - sue445
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-05-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hpricot
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: htmlentities
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: coveralls
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: onkcop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.53.0.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.53.0.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: retryable
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec-its
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec-parameterized
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - '='
186
+ - !ruby/object:Gem::Version
187
+ version: 0.53.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - '='
193
+ - !ruby/object:Gem::Version
194
+ version: 0.53.0
195
+ - !ruby/object:Gem::Dependency
196
+ name: rubocop-rspec
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - '='
200
+ - !ruby/object:Gem::Version
201
+ version: 1.25.1
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - '='
207
+ - !ruby/object:Gem::Version
208
+ version: 1.25.1
209
+ - !ruby/object:Gem::Dependency
210
+ name: simplecov
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: webmock
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: yard
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ description: 5ch (a.k.a. 2ch) reader via itest.5ch.net
252
+ email:
253
+ - sue445@sue445.net
254
+ executables: []
255
+ extensions: []
256
+ extra_rdoc_files: []
257
+ files:
258
+ - ".circleci/config.yml"
259
+ - ".circleci/setup.sh"
260
+ - ".coveralls.yml"
261
+ - ".gitignore"
262
+ - ".rspec"
263
+ - ".rubocop.yml"
264
+ - ".yardopts"
265
+ - CHANGELOG.md
266
+ - Gemfile
267
+ - LICENSE.txt
268
+ - README.md
269
+ - Rakefile
270
+ - bin/console
271
+ - bin/setup
272
+ - itest5ch.gemspec
273
+ - lib/itest5ch.rb
274
+ - lib/itest5ch/assert_methods.rb
275
+ - lib/itest5ch/board.rb
276
+ - lib/itest5ch/board_list_page.rb
277
+ - lib/itest5ch/comment.rb
278
+ - lib/itest5ch/config.rb
279
+ - lib/itest5ch/http_methods.rb
280
+ - lib/itest5ch/thread.rb
281
+ - lib/itest5ch/version.rb
282
+ homepage: https://github.com/sue445/itest5ch
283
+ licenses:
284
+ - MIT
285
+ metadata: {}
286
+ post_install_message:
287
+ rdoc_options: []
288
+ require_paths:
289
+ - lib
290
+ required_ruby_version: !ruby/object:Gem::Requirement
291
+ requirements:
292
+ - - ">="
293
+ - !ruby/object:Gem::Version
294
+ version: '0'
295
+ required_rubygems_version: !ruby/object:Gem::Requirement
296
+ requirements:
297
+ - - ">="
298
+ - !ruby/object:Gem::Version
299
+ version: '0'
300
+ requirements: []
301
+ rubyforge_project:
302
+ rubygems_version: 2.7.6
303
+ signing_key:
304
+ specification_version: 4
305
+ summary: 5ch (a.k.a. 2ch) reader via itest.5ch.net
306
+ test_files: []