itest5ch 1.0.0

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.
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: []