tsumanne 0.0.3 → 0.0.4
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 +4 -4
- data/.dockerignore +8 -0
- data/.mdlrc +6 -0
- data/.rubocop.yml +20 -12
- data/Dockerfile +24 -0
- data/README.md +0 -4
- data/Rakefile +19 -16
- data/exe/tsumanne +2 -1
- data/lib/tsumanne/api.rb +22 -21
- data/lib/tsumanne/cli.rb +153 -2
- data/lib/tsumanne/models/get_threads.rb +8 -3
- data/lib/tsumanne/models/register_thread.rb +5 -1
- data/lib/tsumanne/models/search_indexes.rb +7 -2
- data/lib/tsumanne/models/search_thread_from_uri.rb +14 -2
- data/lib/tsumanne/version.rb +1 -1
- data/mise.toml +42 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/{ast@2.4.2.rbi → ast@2.4.3.rbi} +45 -79
- data/sorbet/rbi/gems/benchmark@0.5.0.rbi +621 -0
- data/sorbet/rbi/gems/{chef-utils@18.2.7.rbi → chef-utils@18.8.54.rbi} +1 -0
- data/sorbet/rbi/gems/{concurrent-ruby@1.2.2.rbi → concurrent-ruby@1.3.5.rbi} +1 -0
- data/sorbet/rbi/gems/{diff-lcs@1.5.0.rbi → diff-lcs@1.6.2.rbi} +183 -199
- data/sorbet/rbi/gems/docile@1.4.0.rbi +147 -147
- data/sorbet/rbi/gems/{erubi@1.12.0.rbi → erubi@1.13.1.rbi} +36 -26
- data/sorbet/rbi/gems/{json@2.6.3.rbi → json@2.19.4.rbi} +920 -251
- data/sorbet/rbi/gems/kramdown-parser-gfm@1.1.0.rbi +32 -63
- data/sorbet/rbi/gems/{kramdown@2.4.0.rbi → kramdown@2.5.1.rbi} +896 -711
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.5.rbi +9 -0
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +189 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +896 -0
- data/sorbet/rbi/gems/{mdl@0.12.0.rbi → mdl@0.15.0.rbi} +109 -113
- data/sorbet/rbi/gems/mixlib-cli@2.1.8.rbi +35 -41
- data/sorbet/rbi/gems/mixlib-config@3.0.27.rbi +147 -142
- data/sorbet/rbi/gems/{mixlib-shellout@3.2.7.rbi → mixlib-shellout@3.3.9.rbi} +127 -150
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +45 -56
- data/sorbet/rbi/gems/optimist@3.2.1.rbi +895 -0
- data/sorbet/rbi/gems/parallel@1.28.0.rbi +270 -0
- data/sorbet/rbi/gems/parser@3.3.11.1.rbi +5229 -0
- data/sorbet/rbi/gems/prism@1.9.0.rbi +42224 -0
- data/sorbet/rbi/gems/{racc@1.7.1.rbi → racc@1.8.1.rbi} +48 -44
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +101 -141
- data/sorbet/rbi/gems/{rake@13.0.6.rbi → rake@13.4.2.rbi} +867 -761
- data/sorbet/rbi/gems/rbi@0.3.10.rbi +5520 -0
- data/sorbet/rbi/gems/rbs@4.0.2.rbi +6908 -0
- data/sorbet/rbi/gems/regexp_parser@2.12.0.rbi +3398 -0
- data/sorbet/rbi/gems/require-hooks@0.4.0.rbi +152 -0
- data/sorbet/rbi/gems/{rexml@3.2.6.rbi → rexml@3.4.4.rbi} +1032 -908
- data/sorbet/rbi/gems/{rspec-core@3.12.2.rbi → rspec-core@3.13.6.rbi} +2860 -4206
- data/sorbet/rbi/gems/rspec-expectations@3.13.5.rbi +6108 -0
- data/sorbet/rbi/gems/{rspec-mocks@3.12.6.rbi → rspec-mocks@3.13.7.rbi} +1264 -1787
- data/sorbet/rbi/gems/rspec-support@3.13.6.rbi +1274 -0
- data/sorbet/rbi/gems/rspec@3.13.2.rbi +15 -0
- data/sorbet/rbi/gems/rubocop-ast@1.49.1.rbi +7212 -0
- data/sorbet/rbi/gems/rubocop-performance@1.26.1.rbi +3403 -0
- data/sorbet/rbi/gems/{rubocop-rake@0.6.0.rbi → rubocop-rake@0.7.1.rbi} +89 -103
- data/sorbet/rbi/gems/rubocop-rspec@3.9.0.rbi +7606 -0
- data/sorbet/rbi/gems/rubocop-sorbet@0.12.0.rbi +2448 -0
- data/sorbet/rbi/gems/rubocop@1.86.1.rbi +62232 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +326 -655
- data/sorbet/rbi/gems/rubydex@0.1.0.beta14.rbi +655 -0
- data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +24 -158
- data/sorbet/rbi/gems/simplecov@0.22.0.rbi +450 -606
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +32 -166
- data/sorbet/rbi/gems/spoom@1.7.13.rbi +6151 -0
- data/sorbet/rbi/gems/{tapioca@0.11.8.rbi → tapioca@0.19.1.rbi} +1609 -1379
- data/sorbet/rbi/gems/{thor@1.2.2.rbi → thor@1.5.0.rbi} +1075 -1170
- data/sorbet/rbi/gems/tomlrb@2.0.3.rbi +1 -0
- data/sorbet/rbi/gems/tsort@0.2.0.rbi +389 -0
- data/sorbet/rbi/gems/unicode-display_width@3.2.0.rbi +130 -0
- data/sorbet/rbi/gems/unicode-emoji@4.2.0.rbi +332 -0
- metadata +65 -62
- data/.ruby-version +0 -1
- data/sorbet/rbi/gems/base64@0.1.1.rbi +0 -172
- data/sorbet/rbi/gems/ffi-compiler@1.0.1.rbi +0 -206
- data/sorbet/rbi/gems/ffi@1.15.5.rbi +0 -1994
- data/sorbet/rbi/gems/http-parser@1.2.3.rbi +0 -422
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +0 -14237
- data/sorbet/rbi/gems/mhtml@0.1.6.rbi +0 -353
- data/sorbet/rbi/gems/parallel@1.23.0.rbi +0 -273
- data/sorbet/rbi/gems/parser@3.2.2.3.rbi +0 -7253
- data/sorbet/rbi/gems/prettier_print@1.2.1.rbi +0 -951
- data/sorbet/rbi/gems/rbi@0.0.17.rbi +0 -2967
- data/sorbet/rbi/gems/regexp_parser@2.8.1.rbi +0 -3749
- data/sorbet/rbi/gems/rspec-expectations@3.12.3.rbi +0 -8100
- data/sorbet/rbi/gems/rspec-support@3.12.1.rbi +0 -1609
- data/sorbet/rbi/gems/rspec@3.12.0.rbi +0 -82
- data/sorbet/rbi/gems/rubocop-ast@1.29.0.rbi +0 -7144
- data/sorbet/rbi/gems/rubocop-capybara@2.18.0.rbi +0 -827
- data/sorbet/rbi/gems/rubocop-factory_bot@2.23.1.rbi +0 -812
- data/sorbet/rbi/gems/rubocop-performance@1.19.0.rbi +0 -3157
- data/sorbet/rbi/gems/rubocop-rspec@2.23.2.rbi +0 -7691
- data/sorbet/rbi/gems/rubocop-sorbet@0.7.3.rbi +0 -1226
- data/sorbet/rbi/gems/rubocop@1.56.2.rbi +0 -56541
- data/sorbet/rbi/gems/spoom@1.2.3.rbi +0 -3199
- data/sorbet/rbi/gems/syntax_tree@6.1.1.rbi +0 -22855
- data/sorbet/rbi/gems/unicode-display_width@2.4.2.rbi +0 -65
- data/sorbet/rbi/gems/unparser@0.6.8.rbi +0 -4525
- data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +0 -425
- data/sorbet/rbi/gems/yard@0.9.34.rbi +0 -18219
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 364315dfc400885784277475e8cfe19af407d5b1d27daee8adeb9bfc942d18cd
|
|
4
|
+
data.tar.gz: 4e4aba0c9982cb4b8c93979abba3a144c7a8eed87a7852d0d07050596200e0ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3fd6ad688af2126ed9f44cce4a03dd21c0448266326ce338117794bf285967d2d0f8fd671e6e26d2bb8d8d33cd86ff2cb80360d89bcff3873c8881d51a6c554
|
|
7
|
+
data.tar.gz: 9fb34ceda1df2f7f3af6d28b96811ff1326cba98c0d4de947d8221d58e66948237ee9dc76d6968d007c4f69f1958e27273fbcf67fe297f8fa9577f4ad5f34cd1
|
data/.dockerignore
ADDED
data/.mdlrc
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# This file is a Mixlib::Config file, not a style file: `all` / `exclude_rule` are
|
|
2
|
+
# only understood inside a style file passed with `style`. Prefix a rule with `~`
|
|
3
|
+
# to exclude it here.
|
|
4
|
+
#
|
|
5
|
+
# MD013 (line length): badge definitions and links do not wrap.
|
|
6
|
+
rules "~MD013"
|
data/.rubocop.yml
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
rubocop-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
- rubocop-rspec
|
|
7
|
-
- rubocop-performance
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-performance
|
|
3
|
+
- rubocop-rake
|
|
4
|
+
- rubocop-rspec
|
|
5
|
+
- rubocop-sorbet
|
|
8
6
|
|
|
9
7
|
AllCops:
|
|
10
|
-
TargetRubyVersion: 3.
|
|
8
|
+
TargetRubyVersion: 3.3
|
|
11
9
|
NewCops: enable
|
|
10
|
+
Exclude:
|
|
11
|
+
# Generated by `tapioca gem`; linting them only creates churn on regeneration.
|
|
12
|
+
- 'sorbet/rbi/gems/**/*.rbi'
|
|
13
|
+
# Bundler binstub.
|
|
14
|
+
- 'bin/tapioca'
|
|
15
|
+
|
|
16
|
+
# This gem keeps its development dependencies in the gemspec, not the Gemfile.
|
|
17
|
+
Gemspec/DevelopmentDependencies:
|
|
18
|
+
EnforcedStyle: gemspec
|
|
12
19
|
|
|
13
20
|
Style/StringLiterals:
|
|
14
21
|
Enabled: true
|
|
@@ -24,9 +31,10 @@ Layout/LineLength:
|
|
|
24
31
|
Metrics/MethodLength:
|
|
25
32
|
Max: 15
|
|
26
33
|
|
|
27
|
-
Lint/EmptyFile:
|
|
28
|
-
Exclude:
|
|
29
|
-
- 'sorbet/rbi/gems/*.rbi'
|
|
30
|
-
|
|
31
34
|
Gemspec:
|
|
32
35
|
Enabled: true
|
|
36
|
+
|
|
37
|
+
# `CLI` is a flat dispatcher: one short method per subcommand, which reads best in a single class.
|
|
38
|
+
Metrics/ClassLength:
|
|
39
|
+
Exclude:
|
|
40
|
+
- 'lib/tsumanne/cli.rb'
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FROM --platform=$BUILDPLATFORM ruby:4.0@sha256:c400c1e41e8ad2276c82529ff9fd552cdb339a84e761c9d57d2d7f582122fa6e AS builder
|
|
2
|
+
|
|
3
|
+
WORKDIR /src
|
|
4
|
+
COPY . .
|
|
5
|
+
RUN gem build tsumanne.gemspec --output /tmp/tsumanne.gem
|
|
6
|
+
|
|
7
|
+
FROM ruby:4.0-slim@sha256:607bf92fa7ecebb4a0c6654b62cb44c48d94b36b6f5a754611ddbbe3dc5b6135
|
|
8
|
+
|
|
9
|
+
LABEL org.opencontainers.image.title="tsumanne" \
|
|
10
|
+
org.opencontainers.image.description="Unofficial API wrapper and CLI for tsumanne.net" \
|
|
11
|
+
org.opencontainers.image.source="https://github.com/eggplants/tsumanne" \
|
|
12
|
+
org.opencontainers.image.licenses="MIT"
|
|
13
|
+
|
|
14
|
+
RUN --mount=type=bind,from=builder,source=/tmp/tsumanne.gem,target=/tmp/tsumanne.gem \
|
|
15
|
+
gem install /tmp/tsumanne.gem --no-document
|
|
16
|
+
|
|
17
|
+
RUN tsumanne --version
|
|
18
|
+
|
|
19
|
+
RUN useradd --create-home --user-group tsumanne
|
|
20
|
+
USER tsumanne
|
|
21
|
+
WORKDIR /home/tsumanne
|
|
22
|
+
|
|
23
|
+
ENTRYPOINT ["tsumanne"]
|
|
24
|
+
CMD ["--help"]
|
data/README.md
CHANGED
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
[![Rake]](https://github.com/eggplants/tsumanne/actions/workflows/rake.yml)
|
|
4
4
|
[![Release Gem]](https://github.com/eggplants/tsumanne/actions/workflows/release.yml)
|
|
5
5
|
[![Gem Version]](https://badge.fury.io/rb/tsumanne)
|
|
6
|
-
[![Maintainability]](https://codeclimate.com/github/eggplants/tsumanne/maintainability)
|
|
7
|
-
[![Test Coverage]](https://codeclimate.com/github/eggplants/tsumanne/test_coverage)
|
|
8
6
|
|
|
9
7
|
[Rake]: <https://github.com/eggplants/tsumanne/actions/workflows/rake.yml/badge.svg>
|
|
10
8
|
[Release Gem]: <https://github.com/eggplants/tsumanne/actions/workflows/release.yml/badge.svg>
|
|
11
9
|
[Gem Version]: <https://badge.fury.io/rb/tsumanne.svg>
|
|
12
|
-
[Maintainability]: <https://api.codeclimate.com/v1/badges/673df4b2c0e15b80f06c/maintainability>
|
|
13
|
-
[Test Coverage]: <https://api.codeclimate.com/v1/badges/673df4b2c0e15b80f06c/test_coverage>
|
|
14
10
|
|
|
15
11
|
API Wrapper for tsumanne.net
|
|
16
12
|
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
+
require "English"
|
|
6
|
+
|
|
5
7
|
require "bundler/gem_tasks"
|
|
6
8
|
require "rspec/core/rake_task"
|
|
7
9
|
require "rubocop/rake_task"
|
|
@@ -12,48 +14,49 @@ require "rubocop/rake_task"
|
|
|
12
14
|
RSpec::Core::RakeTask.new(:spec)
|
|
13
15
|
|
|
14
16
|
RuboCop::RakeTask.new do |task|
|
|
15
|
-
task.requires <<
|
|
17
|
+
task.requires << "rubocop-sorbet"
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
namespace :mdl do
|
|
19
|
-
desc
|
|
21
|
+
desc "Format markdown files with markdownlint"
|
|
20
22
|
task :format do
|
|
21
|
-
system("bundle exec mdl #{Dir.glob("**/*.md") *
|
|
22
|
-
puts "#{
|
|
23
|
+
system("bundle exec mdl -c .mdlrc #{Dir.glob("**/*.md") * "\s"}")
|
|
24
|
+
puts "#{$CHILD_STATUS.success? ? :no : :some} errors found"
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
namespace :sorbet do
|
|
27
|
-
desc
|
|
29
|
+
desc "Type-checking with sorbet"
|
|
28
30
|
task :check do
|
|
29
31
|
system("bundle exec srb tc")
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
desc
|
|
34
|
+
desc "Type-checking and auto-fix correctable errors with sorbet"
|
|
33
35
|
task :autocorrect do
|
|
34
36
|
system("bundle exec srb tc --autocorrect")
|
|
35
37
|
end
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
namespace :tapioca do
|
|
39
|
-
desc
|
|
41
|
+
desc "Generate RBI from gems with tapioca"
|
|
40
42
|
task :gen do
|
|
41
43
|
system("bundle exec tapioca gem --all")
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
desc
|
|
46
|
+
desc "Check duplicated definitions exists in shim RBIs with tapioca"
|
|
45
47
|
task :check_shims do
|
|
46
48
|
system("bundle exec tapioca check-shims")
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
task :default do
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
%i[mdl:format rubocop:autocorrect_all sorbet:autocorrect tapioca:check_shims spec].each do |task_name|
|
|
54
|
+
padding = "=" * task_name.size
|
|
55
|
+
puts <<~BANNER
|
|
56
|
+
.。*゚+.*.。 #{padding} ゚+..。*゚+
|
|
57
|
+
.。*゚+.*.。 #{task_name} ゚+..。*゚+
|
|
58
|
+
.。*゚+.*.。 #{padding} ゚+..。*゚+
|
|
59
|
+
BANNER
|
|
60
|
+
Rake::Task[task_name].execute
|
|
61
|
+
end
|
|
59
62
|
end
|
data/exe/tsumanne
CHANGED
data/lib/tsumanne/api.rb
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
require "net/http"
|
|
6
6
|
require "uri"
|
|
7
7
|
require "json"
|
|
8
|
-
require "zlib"
|
|
9
8
|
|
|
10
|
-
require "mhtml"
|
|
11
9
|
require "sorbet-runtime"
|
|
12
10
|
|
|
13
11
|
require_relative "models/get_threads"
|
|
@@ -17,10 +15,11 @@ require_relative "models/register_thread"
|
|
|
17
15
|
|
|
18
16
|
# API module for tsumanne.net includes knowledge as const.
|
|
19
17
|
module Tsumanne
|
|
18
|
+
# Client for the tsumanne.net endpoints of a single board.
|
|
20
19
|
class API
|
|
21
20
|
extend T::Sig
|
|
22
21
|
|
|
23
|
-
sig{ returns(String) }
|
|
22
|
+
sig { returns(String) }
|
|
24
23
|
attr_reader :board_id
|
|
25
24
|
|
|
26
25
|
sig { params(board_id: Symbol).void }
|
|
@@ -35,25 +34,27 @@ module Tsumanne
|
|
|
35
34
|
GetThreadsResponse.from_hash(fetch_json(paths: [index, page.to_s]))
|
|
36
35
|
end
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
# Returns the merged HTML export of an archived thread, as served by the site (Shift_JIS encoded).
|
|
38
|
+
#
|
|
39
|
+
# `mht.php` used to answer with JSON pointing at a gzipped MHTML file; it now redirects straight
|
|
40
|
+
# to a single self-contained HTML document, so no MHTML parsing is involved anymore.
|
|
41
|
+
sig { params(thread_id: String).returns(String) }
|
|
39
42
|
def get_thread_mht(thread_id)
|
|
40
|
-
# https://tsumanne.net/si/mht.php?id=129691
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
zstream.finish
|
|
48
|
-
zstream.close
|
|
49
|
-
Mhtml::RootDocument.new(buf)
|
|
43
|
+
# https://tsumanne.net/si/mht.php?id=129691 -> 302 ./mhttmp/86279902.html
|
|
44
|
+
uri = join_paths(BASE_URL, [@board_id, "mht.php"])
|
|
45
|
+
uri.query = URI.encode_www_form({ id: thread_id })
|
|
46
|
+
res = Net::HTTP.get_response(uri)
|
|
47
|
+
raise Error, "thread #{thread_id} is not archived (HTTP #{res.code})" unless res.is_a?(Net::HTTPRedirection)
|
|
48
|
+
|
|
49
|
+
T.must(Net::HTTP.get(URI.join(uri, res["location"])))
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
sig { params(thread_path: String).returns(T.nilable(
|
|
52
|
+
sig { params(thread_path: String).returns(T.nilable(String)) }
|
|
53
53
|
def get_thread_from_path(thread_path)
|
|
54
54
|
# https://tsumanne.net/si/data/2023/08/30/8883354/
|
|
55
55
|
match_data = %r{^\d{4}/\d{2}/\d{2}/(?<thread_id>\d+)$}.match(thread_path)
|
|
56
56
|
return if match_data.nil?
|
|
57
|
+
|
|
57
58
|
get_thread_mht(T.must(match_data[:thread_id]))
|
|
58
59
|
end
|
|
59
60
|
|
|
@@ -78,7 +79,8 @@ module Tsumanne
|
|
|
78
79
|
def register_thread(uri, indexes: nil)
|
|
79
80
|
# post, https://tsumanne.net/si/input.php?format=json&url=...&category=...
|
|
80
81
|
RegisterThreadResponse.from_hash(
|
|
81
|
-
fetch_json(paths: ["input.php?format=json"], query: { url: uri, category: (indexes || []).join(",") },
|
|
82
|
+
fetch_json(paths: ["input.php?format=json"], query: { url: uri, category: (indexes || []).join(",") },
|
|
83
|
+
method: :post)
|
|
82
84
|
)
|
|
83
85
|
end
|
|
84
86
|
|
|
@@ -97,11 +99,10 @@ module Tsumanne
|
|
|
97
99
|
when :get
|
|
98
100
|
uri.query = query
|
|
99
101
|
Net::HTTP.get(uri)
|
|
100
|
-
when :get_response
|
|
101
|
-
uri.query = query
|
|
102
|
-
Net::HTTP.get_response(uri).body
|
|
103
102
|
when :post
|
|
104
|
-
|
|
103
|
+
# Without an explicit form content type the server does not populate `$_POST`,
|
|
104
|
+
# and answers with a PHP warning followed by a "not permitted URL" error.
|
|
105
|
+
Net::HTTP.post(uri, query, "Content-Type" => "application/x-www-form-urlencoded").body
|
|
105
106
|
end
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -118,7 +119,7 @@ module Tsumanne
|
|
|
118
119
|
|
|
119
120
|
sig { params(base: String, paths: T::Array[String]).returns(URI::Generic) }
|
|
120
121
|
def join_paths(base, *paths)
|
|
121
|
-
URI.parse(T.must(([base] + paths).reduce { File.join(
|
|
122
|
+
URI.parse(T.must(([base] + paths).reduce { |joined, path| File.join(joined, path) }))
|
|
122
123
|
end
|
|
123
124
|
end
|
|
124
125
|
end
|
data/lib/tsumanne/cli.rb
CHANGED
|
@@ -2,12 +2,163 @@
|
|
|
2
2
|
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
+
require "json"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require "optimist"
|
|
9
|
+
require "sorbet-runtime"
|
|
10
|
+
|
|
11
|
+
require_relative "../tsumanne"
|
|
12
|
+
|
|
5
13
|
module Tsumanne
|
|
14
|
+
# Command line entry point for the `tsumanne` executable.
|
|
6
15
|
class CLI
|
|
7
16
|
extend T::Sig
|
|
8
|
-
|
|
17
|
+
|
|
18
|
+
# Subcommands, in the order `--help` lists them.
|
|
19
|
+
COMMANDS = T.let({
|
|
20
|
+
"threads" => "List the archived threads of a board",
|
|
21
|
+
"thread" => "Print an archived thread, by id or by archive path",
|
|
22
|
+
"search" => "Find the archived thread of a 2chan.net thread URI",
|
|
23
|
+
"indexes" => "List the indexes, or search them by keyword",
|
|
24
|
+
"register" => "Ask the site to archive a 2chan.net thread"
|
|
25
|
+
}.freeze, T::Hash[String, String])
|
|
26
|
+
|
|
27
|
+
# Runs `args` as a command line and returns the exit status the executable should exit with.
|
|
28
|
+
sig { params(args: T::Array[String]).returns(Integer) }
|
|
9
29
|
def self.start(args)
|
|
10
|
-
|
|
30
|
+
new(args).run
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(args: T::Array[String]).void }
|
|
34
|
+
def initialize(args)
|
|
35
|
+
@args = T.let(args.dup, T::Array[String])
|
|
36
|
+
# Reassigned to the subcommand parser, so that `--help` educates about the command at hand.
|
|
37
|
+
@parser = T.let(global_parser, Optimist::Parser)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sig { returns(Integer) }
|
|
41
|
+
def run
|
|
42
|
+
execute
|
|
43
|
+
0
|
|
44
|
+
rescue Optimist::HelpNeeded
|
|
45
|
+
@parser.educate($stdout)
|
|
46
|
+
0
|
|
47
|
+
rescue Optimist::VersionNeeded
|
|
48
|
+
$stdout.puts(@parser.version)
|
|
49
|
+
0
|
|
50
|
+
rescue Optimist::CommandlineError, URI::InvalidURIError, Error => e
|
|
51
|
+
warn("Error: #{e.message}")
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
sig { void }
|
|
58
|
+
def execute
|
|
59
|
+
options = @parser.parse(@args)
|
|
60
|
+
# `stop_on` leaves the command and everything after it in `@args`.
|
|
61
|
+
command = @args.shift
|
|
62
|
+
raise Optimist::HelpNeeded if command.nil?
|
|
63
|
+
raise Error, "unknown command `#{command}`" unless COMMANDS.key?(command)
|
|
64
|
+
|
|
65
|
+
run_command(command, API.new(board_id: options[:board].to_sym))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { params(command: String, api: API).void }
|
|
69
|
+
def run_command(command, api)
|
|
70
|
+
case command
|
|
71
|
+
when "threads" then threads(api)
|
|
72
|
+
when "thread" then thread(api)
|
|
73
|
+
when "search" then search(api)
|
|
74
|
+
when "indexes" then indexes(api)
|
|
75
|
+
else register(api)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { params(api: API).void }
|
|
80
|
+
def threads(api)
|
|
81
|
+
options = parse_command("threads [options]") do |parser|
|
|
82
|
+
parser.opt(:index, "Index (category) to list", type: :string, default: "all")
|
|
83
|
+
parser.opt(:page, "Page to list", type: :integer, default: 1)
|
|
84
|
+
end
|
|
85
|
+
print_json(api.get_threads(index: options[:index], page: options[:page]))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
sig { params(api: API).void }
|
|
89
|
+
def thread(api)
|
|
90
|
+
parse_command("thread [options] <thread id | archive path>")
|
|
91
|
+
target = operand("`thread` needs a thread id or an archive path")
|
|
92
|
+
$stdout.puts(/\A\d+\z/.match?(target) ? api.get_thread_mht(target) : thread_from_path(api, target))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { params(api: API, target: String).returns(String) }
|
|
96
|
+
def thread_from_path(api, target)
|
|
97
|
+
api.get_thread_from_path(target) ||
|
|
98
|
+
raise(Error, "`#{target}` is not an archive path (expected `YYYY/MM/DD/<thread id>`)")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(api: API).void }
|
|
102
|
+
def search(api)
|
|
103
|
+
parse_command("search [options] <thread uri>")
|
|
104
|
+
print_json(api.search_thread_from_uri(URI.parse(operand("`search` needs a 2chan.net thread URI"))))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { params(api: API).void }
|
|
108
|
+
def indexes(api)
|
|
109
|
+
options = parse_command("indexes [options]") do |parser|
|
|
110
|
+
parser.opt(:keyword, "Keyword to search the indexes for", type: :string)
|
|
111
|
+
parser.opt(:order, "Order to list the indexes in",
|
|
112
|
+
type: :string, default: "newer", permitted: INDEXES_ORDERS.keys.map(&:to_s))
|
|
113
|
+
parser.opt(:page, "Page to list", type: :integer, default: 1)
|
|
114
|
+
end
|
|
115
|
+
print_json(api.search_indexes(keyword: options[:keyword], order: options[:order].to_sym, page: options[:page]))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
sig { params(api: API).void }
|
|
119
|
+
def register(api)
|
|
120
|
+
options = parse_command("register [options] <thread uri>") do |parser|
|
|
121
|
+
parser.opt(:index, "Index (category) to file the thread under, repeatable", type: :string, multi: true)
|
|
122
|
+
end
|
|
123
|
+
uri = URI.parse(operand("`register` needs a 2chan.net thread URI"))
|
|
124
|
+
print_json(api.register_thread(uri, indexes: options[:index]))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
sig { returns(Optimist::Parser) }
|
|
128
|
+
def global_parser
|
|
129
|
+
parser = Optimist::Parser.new
|
|
130
|
+
parser.usage("[global options] <command> [options] [arguments]")
|
|
131
|
+
parser.version("tsumanne #{VERSION}")
|
|
132
|
+
parser.opt(:board, "Board to talk to",
|
|
133
|
+
type: :string, default: "img", permitted: BOARD_IDS.keys.map(&:to_s))
|
|
134
|
+
# Declared here so that `--help` lists them above the command list, instead of below it.
|
|
135
|
+
parser.opt(:version, "Print version and exit")
|
|
136
|
+
parser.opt(:help, "Show this message")
|
|
137
|
+
parser.stop_on(COMMANDS.keys)
|
|
138
|
+
parser.text("")
|
|
139
|
+
parser.text("Commands:")
|
|
140
|
+
COMMANDS.each { |name, description| parser.text(format(" %-8<name>s %<description>s", name:, description:)) }
|
|
141
|
+
parser
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Parses the options of the command at hand, leaving its operands in `@args`.
|
|
145
|
+
sig { params(usage: String, blk: T.nilable(T.proc.params(parser: Optimist::Parser).void)).returns(T.untyped) }
|
|
146
|
+
def parse_command(usage, &blk)
|
|
147
|
+
parser = Optimist::Parser.new
|
|
148
|
+
parser.usage("[global options] #{usage}")
|
|
149
|
+
blk&.call(parser)
|
|
150
|
+
@parser = parser
|
|
151
|
+
parser.parse(@args)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sig { params(message: String).returns(String) }
|
|
155
|
+
def operand(message)
|
|
156
|
+
@args.shift || raise(Error, message)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { params(response: T::Struct).void }
|
|
160
|
+
def print_json(response)
|
|
161
|
+
$stdout.puts(JSON.generate(response.serialize))
|
|
11
162
|
end
|
|
12
163
|
end
|
|
13
164
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "sorbet-runtime"
|
|
4
5
|
|
|
6
|
+
# Response of `GET /<board_id>/<index>/<page>`, a page of archived threads.
|
|
5
7
|
class GetThreadsResponse < T::Struct
|
|
6
8
|
extend T::Sig
|
|
7
9
|
|
|
10
|
+
# A single archived thread listed on the page.
|
|
8
11
|
class Log < T::Struct
|
|
9
12
|
extend T::Sig
|
|
10
13
|
|
|
@@ -14,11 +17,13 @@ class GetThreadsResponse < T::Struct
|
|
|
14
17
|
const :close, T::Boolean
|
|
15
18
|
const :res, Integer
|
|
16
19
|
const :files, Integer
|
|
17
|
-
|
|
18
|
-
const :
|
|
20
|
+
# Only some boards report this one.
|
|
21
|
+
const :access, T.nilable(Integer)
|
|
19
22
|
const :text, String
|
|
20
23
|
const :thumb, String
|
|
21
|
-
|
|
24
|
+
# `public` and `del` are no longer part of the response; kept for older archives.
|
|
25
|
+
const :public, T.nilable(T::Boolean)
|
|
26
|
+
const :del, T.nilable(Integer)
|
|
22
27
|
const :atid, T::Boolean
|
|
23
28
|
const :last, String # Time
|
|
24
29
|
const :path, String
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "sorbet-runtime"
|
|
4
5
|
|
|
6
|
+
# Response of `POST /<board_id>/input.php`, the thread registration endpoint.
|
|
5
7
|
class RegisterThreadResponse < T::Struct
|
|
6
8
|
extend T::Sig
|
|
7
9
|
|
|
10
|
+
# Result of attaching the requested categories to the registered thread.
|
|
8
11
|
class Tags < T::Struct
|
|
9
12
|
extend T::Sig
|
|
10
13
|
|
|
@@ -14,5 +17,6 @@ class RegisterThreadResponse < T::Struct
|
|
|
14
17
|
|
|
15
18
|
const :success, T::Boolean
|
|
16
19
|
const :messages, T::Array[String]
|
|
17
|
-
|
|
20
|
+
# Only returned when the request actually attached categories.
|
|
21
|
+
const :tags, T.nilable(Tags)
|
|
18
22
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "sorbet-runtime"
|
|
4
5
|
|
|
6
|
+
# Response of `GET /<board_id>/indexes.php`, a keyword search over the indexes.
|
|
5
7
|
class SearchIndexesResponse < T::Struct
|
|
6
8
|
extend T::Sig
|
|
7
9
|
|
|
10
|
+
# A single index (category) tag matched by the search.
|
|
8
11
|
class Tag < T::Struct
|
|
9
12
|
extend T::Sig
|
|
10
13
|
|
|
@@ -12,7 +15,9 @@ class SearchIndexesResponse < T::Struct
|
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
const :success, T::Boolean
|
|
15
|
-
const :messages, T::Array[String]
|
|
16
|
-
|
|
18
|
+
const :messages, T.nilable(T::Array[String])
|
|
19
|
+
# `null` when listing every index instead of searching for one.
|
|
20
|
+
const :count, T.nilable(Integer)
|
|
21
|
+
const :lastpage, Integer
|
|
17
22
|
const :tags, T::Array[Tag]
|
|
18
23
|
end
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require "sorbet-runtime"
|
|
4
5
|
|
|
6
|
+
# Response of `GET /<board_id>/indexes.php?sbmt=URL`, a lookup of archived threads by their source URI.
|
|
5
7
|
class SearchThreadFromUriResponse < T::Struct
|
|
6
8
|
extend T::Sig
|
|
7
9
|
|
|
10
|
+
# An archived thread whose source URI matched the query.
|
|
11
|
+
class Log < T::Struct
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
const :id, Integer
|
|
15
|
+
const :url, String # URI
|
|
16
|
+
const :path, String
|
|
17
|
+
const :category, T::Array[String]
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
const :success, T::Boolean
|
|
9
|
-
const :
|
|
10
|
-
const :
|
|
21
|
+
const :logs, T::Array[Log]
|
|
22
|
+
const :messages, T.nilable(T::Array[String])
|
|
11
23
|
end
|
data/lib/tsumanne/version.rb
CHANGED
data/mise.toml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[tools]
|
|
2
|
+
"aqua:azu/dockerfile-pin" = "latest"
|
|
3
|
+
claude = "latest"
|
|
4
|
+
pinact = "latest"
|
|
5
|
+
ruby = "latest"
|
|
6
|
+
|
|
7
|
+
[tasks.install]
|
|
8
|
+
description = "Install gem dependencies"
|
|
9
|
+
run = "bundle install"
|
|
10
|
+
|
|
11
|
+
[tasks.lint]
|
|
12
|
+
description = "Run all linters and the type checker (no auto-fix)"
|
|
13
|
+
depends = ["install"]
|
|
14
|
+
run = [
|
|
15
|
+
"bundle exec rubocop",
|
|
16
|
+
"bundle exec mdl -c .mdlrc $(git ls-files '*.md')",
|
|
17
|
+
"bundle exec srb tc",
|
|
18
|
+
"bundle exec tapioca check-shims",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tasks.test]
|
|
22
|
+
description = "Run the RSpec suite"
|
|
23
|
+
depends = ["install"]
|
|
24
|
+
run = "bundle exec rspec"
|
|
25
|
+
|
|
26
|
+
[tasks."lint:fix"]
|
|
27
|
+
description = "Auto-fix what the linters and the type checker can fix"
|
|
28
|
+
depends = ["install"]
|
|
29
|
+
run = [
|
|
30
|
+
"bundle exec rubocop --autocorrect-all",
|
|
31
|
+
"bundle exec srb tc --autocorrect",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tasks.pre-commit]
|
|
35
|
+
description = "Auto-fix, then verify everything before committing"
|
|
36
|
+
depends = ["lint:fix"]
|
|
37
|
+
run = ["mise run lint", "mise run test"]
|
|
38
|
+
|
|
39
|
+
[tasks.release]
|
|
40
|
+
description = "Build and push the gem to RubyGems"
|
|
41
|
+
depends = ["install"]
|
|
42
|
+
run = "bundle exec rake release"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
**/*.rbi linguist-generated=true
|