Merge branch 'master' into mobile-mode-default-image-size
This commit is contained in:
3
.bundle/config
Normal file
3
.bundle/config
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
BUNDLE_BUILD__NOKOGIRI: "--use-system-libraries"
|
||||||
|
BUNDLE_BUILD__NOKOGUMBO: "--without-libxml2"
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
version: 2
|
version: 2
|
||||||
checks:
|
checks:
|
||||||
argument-count:
|
argument-count:
|
||||||
|
enabled: false
|
||||||
|
complex-logic:
|
||||||
config:
|
config:
|
||||||
threshold: 4
|
threshold: 8
|
||||||
file-lines:
|
file-lines:
|
||||||
config:
|
config:
|
||||||
threshold: 500
|
threshold: 1000
|
||||||
|
method-complexity:
|
||||||
|
config:
|
||||||
|
threshold: 15
|
||||||
method-count:
|
method-count:
|
||||||
config:
|
enabled: false
|
||||||
threshold: 40
|
|
||||||
method-lines:
|
method-lines:
|
||||||
|
enabled: false
|
||||||
|
nested-control-flow:
|
||||||
config:
|
config:
|
||||||
threshold: 100
|
threshold: 4
|
||||||
|
return-statements:
|
||||||
|
enabled: false
|
||||||
plugins:
|
plugins:
|
||||||
eslint:
|
eslint:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
1
.codecov.yml
Normal file
1
.codecov.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
comment: false
|
||||||
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[**.{js,rb,css,erb,md,json,yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[**.html.erb]
|
||||||
|
indent_size = unset
|
||||||
|
|
||||||
|
[app/javascript/vendor/**]
|
||||||
|
indent_size = unset
|
||||||
@@ -8,9 +8,13 @@ parserOptions:
|
|||||||
globals:
|
globals:
|
||||||
$: false
|
$: false
|
||||||
require: false
|
require: false
|
||||||
|
parser: babel-eslint
|
||||||
|
plugins:
|
||||||
|
- babel
|
||||||
rules:
|
rules:
|
||||||
# https://eslint.org/docs/rules/
|
# https://eslint.org/docs/rules/
|
||||||
array-callback-return: error
|
array-callback-return: error
|
||||||
|
babel/no-unused-expressions: error
|
||||||
block-scoped-var: error
|
block-scoped-var: error
|
||||||
consistent-return: error
|
consistent-return: error
|
||||||
default-case: error
|
default-case: error
|
||||||
@@ -32,7 +36,7 @@ rules:
|
|||||||
no-sequences: error
|
no-sequences: error
|
||||||
no-shadow: error
|
no-shadow: error
|
||||||
no-shadow-restricted-names: error
|
no-shadow-restricted-names: error
|
||||||
no-unused-expressions: error
|
#no-unused-expressions: error
|
||||||
no-unused-vars:
|
no-unused-vars:
|
||||||
- error
|
- error
|
||||||
- argsIgnorePattern: "^_"
|
- argsIgnorePattern: "^_"
|
||||||
|
|||||||
59
.github/workflows/test.yaml
vendored
59
.github/workflows/test.yaml
vendored
@@ -1,20 +1,40 @@
|
|||||||
name: Test
|
name: Github
|
||||||
|
|
||||||
on: [push, pull_request]
|
# Trigger on pushes to master or pull requests to master, but not both.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# https://github.com/Marr11317/ConflictAdviser
|
||||||
|
notify-merge-conflicts:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: Marr11317/ConflictAdviser@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
conflict_label: "merge conflict"
|
||||||
|
comment: 'Rebase needed'
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ubuntu:20.04
|
container: ubuntu:20.04
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEBIAN_FRONTEND: noninteractive
|
DEBIAN_FRONTEND: noninteractive
|
||||||
PARALLEL_WORKERS: 8 # number of parallel tests to run
|
PARALLEL_WORKERS: 8 # number of parallel tests to run
|
||||||
|
RUBYOPT: -W0 # silence ruby warnings
|
||||||
|
VIPS_WARNING: 0 # silence libvips warnings
|
||||||
|
|
||||||
# Code Climate configuration. https://docs.codeclimate.com/docs/finding-your-test-coverage-token
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
|
|
||||||
GIT_COMMIT_SHA: ${{ github.sha }}
|
|
||||||
GIT_BRANCH: ${{ github.ref }}
|
|
||||||
|
|
||||||
DATABASE_URL: postgresql://danbooru:danbooru@postgres/danbooru
|
DATABASE_URL: postgresql://danbooru:danbooru@postgres/danbooru
|
||||||
ARCHIVE_DATABASE_URL: postgresql://danbooru:danbooru@postgres/danbooru
|
ARCHIVE_DATABASE_URL: postgresql://danbooru:danbooru@postgres/danbooru
|
||||||
@@ -45,32 +65,25 @@ jobs:
|
|||||||
POSTGRES_PASSWORD: danbooru
|
POSTGRES_PASSWORD: danbooru
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
# - name: Save environment
|
|
||||||
# run: env | egrep "DANBOORU|DATABASE_URL" > ~/.env
|
|
||||||
# - name: Install docker-compose
|
|
||||||
# run: sudo apt-get update && sudo apt-get -y install docker-compose
|
|
||||||
# - name: Run tests
|
|
||||||
# run: docker-compose --env-file ~/.env -f config/docker/docker-compose.test.yaml -p danbooru up
|
|
||||||
- name: Install OS dependencies
|
- name: Install OS dependencies
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get -y install --no-install-recommends build-essential ruby ruby-dev ruby-bundler git nodejs yarnpkg webpack ffmpeg mkvtoolnix libvips-dev libxml2-dev postgresql-server-dev-all wget
|
apt-get -y install --no-install-recommends build-essential ruby ruby-dev ruby-bundler git nodejs yarnpkg webpack ffmpeg mkvtoolnix libvips-dev libxml2-dev libxslt-dev zlib1g-dev postgresql-server-dev-all wget curl git
|
||||||
ln -sf /usr/bin/yarnpkg /usr/bin/yarn
|
ln -sf /usr/bin/yarnpkg /usr/bin/yarn
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Ruby dependencies
|
- name: Install Ruby dependencies
|
||||||
run: BUNDLE_DEPLOYMENT=true bundle install --jobs 4
|
run: BUNDLE_DEPLOYMENT=true bundle install --jobs 4
|
||||||
|
|
||||||
- name: Install Javascript dependencies
|
- name: Install Javascript dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: Prepare database
|
- name: Prepare database
|
||||||
run: config/docker/prepare-tests.sh
|
run: config/docker/prepare-tests.sh
|
||||||
# https://docs.codeclimate.com/docs/configuring-test-coverage
|
|
||||||
- name: Prepare test coverage for Code Climate
|
|
||||||
run: |
|
|
||||||
wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64
|
|
||||||
chmod +x test-reporter-latest-linux-amd64
|
|
||||||
./test-reporter-latest-linux-amd64 before-build
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: bin/rails test
|
run: bin/rails test
|
||||||
- name: Upload test coverage to Code Climate
|
|
||||||
run: ./test-reporter-latest-linux-amd64 after-build
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,7 +2,6 @@
|
|||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
.gitconfig
|
.gitconfig
|
||||||
.git/
|
.git/
|
||||||
.bundle/
|
|
||||||
config/database.yml
|
config/database.yml
|
||||||
config/danbooru_local_config.rb
|
config/danbooru_local_config.rb
|
||||||
config/deploy/*.rb
|
config/deploy/*.rb
|
||||||
|
|||||||
19
.rubocop.yml
19
.rubocop.yml
@@ -1,8 +1,9 @@
|
|||||||
|
# some of these settings are overriden in also test/.rubocop.yml
|
||||||
|
|
||||||
require:
|
require:
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 2.7.0
|
|
||||||
NewCops: enable
|
NewCops: enable
|
||||||
Exclude:
|
Exclude:
|
||||||
- "bin/*"
|
- "bin/*"
|
||||||
@@ -41,14 +42,22 @@ Layout/SpaceInsideHashLiteralBraces:
|
|||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Max: 50
|
||||||
|
ExcludedMethods:
|
||||||
|
- concerning
|
||||||
|
- context
|
||||||
|
- should
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
|
CountBlocks: false
|
||||||
Max: 4
|
Max: 4
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 500
|
Max: 500
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 10
|
Enabled: false
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 100
|
Max: 100
|
||||||
@@ -60,7 +69,7 @@ Metrics/ParameterLists:
|
|||||||
Max: 4
|
Max: 4
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Enabled: false
|
Max: 20
|
||||||
|
|
||||||
Lint/InheritException:
|
Lint/InheritException:
|
||||||
EnforcedStyle: standard_error
|
EnforcedStyle: standard_error
|
||||||
@@ -110,7 +119,9 @@ Style/NumericPredicate:
|
|||||||
Style/PercentLiteralDelimiters:
|
Style/PercentLiteralDelimiters:
|
||||||
PreferredDelimiters:
|
PreferredDelimiters:
|
||||||
"default": "[]"
|
"default": "[]"
|
||||||
"%r": "!!"
|
|
||||||
|
Style/ParallelAssignment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Style/PerlBackrefs:
|
Style/PerlBackrefs:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|||||||
12
.simplecov
12
.simplecov
@@ -1,8 +1,12 @@
|
|||||||
SimpleCov.start "rails" do
|
SimpleCov.start "rails" do
|
||||||
add_group "Libraries", ["app/logical", "lib"]
|
add_group "Libraries", ["app/logical", "lib"]
|
||||||
add_group "Presenters", "app/presenters"
|
add_group "Presenters", "app/presenters"
|
||||||
#enable_coverage :branch
|
add_group "Policies", "app/policies"
|
||||||
#minimum_coverage line: 85, branch: 75
|
enable_coverage :branch
|
||||||
#minimum_coverage_by_file 50
|
|
||||||
#coverage_dir "tmp/coverage"
|
# https://github.com/codecov/codecov-ruby#submit-only-in-ci-example
|
||||||
|
if ENV["CODECOV_TOKEN"]
|
||||||
|
require "codecov"
|
||||||
|
SimpleCov.formatter = SimpleCov::Formatter::Codecov
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
13
Gemfile
13
Gemfile
@@ -7,7 +7,6 @@ gem "pg"
|
|||||||
gem "delayed_job"
|
gem "delayed_job"
|
||||||
gem "delayed_job_active_record"
|
gem "delayed_job_active_record"
|
||||||
gem "simple_form"
|
gem "simple_form"
|
||||||
gem "mechanize"
|
|
||||||
gem "whenever", :require => false
|
gem "whenever", :require => false
|
||||||
gem "sanitize"
|
gem "sanitize"
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
@@ -28,14 +27,11 @@ gem 'daemons'
|
|||||||
gem 'oauth2'
|
gem 'oauth2'
|
||||||
gem 'bootsnap'
|
gem 'bootsnap'
|
||||||
gem 'addressable'
|
gem 'addressable'
|
||||||
gem 'httparty'
|
|
||||||
gem 'rakismet'
|
gem 'rakismet'
|
||||||
gem 'recaptcha', require: "recaptcha/rails"
|
gem 'recaptcha', require: "recaptcha/rails"
|
||||||
gem 'activemodel-serializers-xml'
|
gem 'activemodel-serializers-xml'
|
||||||
gem 'jquery-rails'
|
|
||||||
gem 'webpacker', '>= 4.0.x'
|
gem 'webpacker', '>= 4.0.x'
|
||||||
gem 'rake'
|
gem 'rake'
|
||||||
gem 'retriable'
|
|
||||||
gem 'redis'
|
gem 'redis'
|
||||||
gem 'request_store'
|
gem 'request_store'
|
||||||
gem 'builder'
|
gem 'builder'
|
||||||
@@ -47,9 +43,7 @@ gem 'http'
|
|||||||
gem 'activerecord-hierarchical_query'
|
gem 'activerecord-hierarchical_query'
|
||||||
gem 'pundit'
|
gem 'pundit'
|
||||||
gem 'mail'
|
gem 'mail'
|
||||||
|
gem 'nokogiri'
|
||||||
# locked to 1.10.9 to workaround an incompatibility with nokogumbo 2.0.2.
|
|
||||||
gem 'nokogiri', '~> 1.10.9'
|
|
||||||
|
|
||||||
group :production, :staging do
|
group :production, :staging do
|
||||||
gem 'unicorn', :platforms => :ruby
|
gem 'unicorn', :platforms => :ruby
|
||||||
@@ -65,7 +59,6 @@ end
|
|||||||
group :development do
|
group :development do
|
||||||
gem 'rubocop'
|
gem 'rubocop'
|
||||||
gem 'rubocop-rails'
|
gem 'rubocop-rails'
|
||||||
gem 'sinatra'
|
|
||||||
gem 'meta_request'
|
gem 'meta_request'
|
||||||
gem 'rack-mini-profiler'
|
gem 'rack-mini-profiler'
|
||||||
gem 'stackprof'
|
gem 'stackprof'
|
||||||
@@ -85,11 +78,11 @@ group :test do
|
|||||||
gem "factory_bot"
|
gem "factory_bot"
|
||||||
gem "mocha", require: "mocha/minitest"
|
gem "mocha", require: "mocha/minitest"
|
||||||
gem "ffaker"
|
gem "ffaker"
|
||||||
gem "simplecov", "~> 0.17.0", require: false
|
gem "simplecov", require: false
|
||||||
gem "webmock", require: "webmock/minitest"
|
|
||||||
gem "minitest-ci"
|
gem "minitest-ci"
|
||||||
gem "minitest-reporters", require: "minitest/reporters"
|
gem "minitest-reporters", require: "minitest/reporters"
|
||||||
gem "mock_redis"
|
gem "mock_redis"
|
||||||
gem "capybara"
|
gem "capybara"
|
||||||
gem "selenium-webdriver"
|
gem "selenium-webdriver"
|
||||||
|
gem "codecov", require: false
|
||||||
end
|
end
|
||||||
|
|||||||
261
Gemfile.lock
261
Gemfile.lock
@@ -1,70 +1,70 @@
|
|||||||
GIT
|
GIT
|
||||||
remote: https://github.com/evazion/dtext_rb.git
|
remote: https://github.com/evazion/dtext_rb.git
|
||||||
revision: 507e97e0963822c20351c82620c28cc8e23423d5
|
revision: a95bf1d537cbdba4585adb8e123f03f001f56fd7
|
||||||
specs:
|
specs:
|
||||||
dtext_rb (1.10.5)
|
dtext_rb (1.10.6)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.0.3.1)
|
actioncable (6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.0.3.1)
|
actionmailbox (6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
activejob (= 6.0.3.1)
|
activejob (= 6.0.3.2)
|
||||||
activerecord (= 6.0.3.1)
|
activerecord (= 6.0.3.2)
|
||||||
activestorage (= 6.0.3.1)
|
activestorage (= 6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.0.3.1)
|
actionmailer (6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
actionview (= 6.0.3.1)
|
actionview (= 6.0.3.2)
|
||||||
activejob (= 6.0.3.1)
|
activejob (= 6.0.3.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.0.3.1)
|
actionpack (6.0.3.2)
|
||||||
actionview (= 6.0.3.1)
|
actionview (= 6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.0.8)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.0.3.1)
|
actiontext (6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
activerecord (= 6.0.3.1)
|
activerecord (= 6.0.3.2)
|
||||||
activestorage (= 6.0.3.1)
|
activestorage (= 6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.0.3.1)
|
actionview (6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
activejob (6.0.3.1)
|
activejob (6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.0.3.1)
|
activemodel (6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
activemodel-serializers-xml (1.0.2)
|
activemodel-serializers-xml (1.0.2)
|
||||||
activemodel (> 5.x)
|
activemodel (> 5.x)
|
||||||
activesupport (> 5.x)
|
activesupport (> 5.x)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
activerecord (6.0.3.1)
|
activerecord (6.0.3.2)
|
||||||
activemodel (= 6.0.3.1)
|
activemodel (= 6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
activerecord-hierarchical_query (1.2.3)
|
activerecord-hierarchical_query (1.2.3)
|
||||||
activerecord (>= 5.0, < 6.1)
|
activerecord (>= 5.0, < 6.1)
|
||||||
pg (>= 0.21, < 1.3)
|
pg (>= 0.21, < 1.3)
|
||||||
activestorage (6.0.3.1)
|
activestorage (6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
activejob (= 6.0.3.1)
|
activejob (= 6.0.3.2)
|
||||||
activerecord (= 6.0.3.1)
|
activerecord (= 6.0.3.2)
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (6.0.3.1)
|
activesupport (6.0.3.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
@@ -75,42 +75,42 @@ GEM
|
|||||||
airbrussh (1.4.0)
|
airbrussh (1.4.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
ansi (1.5.0)
|
ansi (1.5.0)
|
||||||
ast (2.4.0)
|
ast (2.4.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.326.0)
|
aws-partitions (1.341.0)
|
||||||
aws-sdk-core (3.98.0)
|
aws-sdk-core (3.103.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-sqs (1.26.0)
|
aws-sdk-sqs (1.30.0)
|
||||||
aws-sdk-core (~> 3, >= 3.71.0)
|
aws-sdk-core (~> 3, >= 3.99.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.1.4)
|
aws-sigv4 (1.2.1)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
bcrypt (3.1.13)
|
bcrypt (3.1.13)
|
||||||
bootsnap (1.4.6)
|
bootsnap (1.4.6)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
capistrano (3.14.0)
|
capistrano (3.14.1)
|
||||||
airbrussh (>= 1.0.0)
|
airbrussh (>= 1.0.0)
|
||||||
i18n
|
i18n
|
||||||
rake (>= 10.0.0)
|
rake (>= 10.0.0)
|
||||||
sshkit (>= 1.9.0)
|
sshkit (>= 1.9.0)
|
||||||
capistrano-bundler (1.6.0)
|
capistrano-bundler (2.0.0)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-deploytags (1.0.7)
|
capistrano-deploytags (1.0.7)
|
||||||
capistrano (>= 3.7.0)
|
capistrano (>= 3.7.0)
|
||||||
capistrano-rails (1.5.0)
|
capistrano-rails (1.6.1)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-bundler (~> 1.1)
|
capistrano-bundler (>= 1.1, < 3)
|
||||||
capistrano-rbenv (2.1.6)
|
capistrano-rbenv (2.2.0)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano3-unicorn (0.2.1)
|
capistrano3-unicorn (0.2.1)
|
||||||
capistrano (~> 3.1, >= 3.1.0)
|
capistrano (~> 3.1, >= 3.1.0)
|
||||||
capybara (3.32.2)
|
capybara (3.33.0)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
@@ -120,11 +120,13 @@ GEM
|
|||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
childprocess (3.0.0)
|
childprocess (3.0.0)
|
||||||
chronic (0.10.2)
|
chronic (0.10.2)
|
||||||
|
codecov (0.2.0)
|
||||||
|
colorize
|
||||||
|
json
|
||||||
|
simplecov
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
|
colorize (0.8.1)
|
||||||
concurrent-ruby (1.1.6)
|
concurrent-ruby (1.1.6)
|
||||||
connection_pool (2.2.3)
|
|
||||||
crack (0.4.3)
|
|
||||||
safe_yaml (~> 1.0.0)
|
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
daemons (1.3.1)
|
daemons (1.3.1)
|
||||||
delayed_job (4.1.8)
|
delayed_job (4.1.8)
|
||||||
@@ -132,22 +134,21 @@ GEM
|
|||||||
delayed_job_active_record (4.1.4)
|
delayed_job_active_record (4.1.4)
|
||||||
activerecord (>= 3.0, < 6.1)
|
activerecord (>= 3.0, < 6.1)
|
||||||
delayed_job (>= 3.0, < 5)
|
delayed_job (>= 3.0, < 5)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.4.4)
|
||||||
docile (1.3.2)
|
docile (1.3.2)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.7.5)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.5)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.5)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2, < 6.1)
|
railties (>= 3.2)
|
||||||
erubi (1.9.0)
|
erubi (1.9.0)
|
||||||
factory_bot (5.2.0)
|
factory_bot (6.1.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0.0)
|
||||||
faraday (1.0.1)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ffaker (2.15.0)
|
ffaker (2.15.0)
|
||||||
ffi (1.13.0)
|
ffi (1.13.1)
|
||||||
ffi (1.13.0-x64-mingw32)
|
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
@@ -156,7 +157,6 @@ GEM
|
|||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
globalid (0.4.2)
|
globalid (0.4.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
hashdiff (1.0.1)
|
|
||||||
http (4.4.1)
|
http (4.4.1)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
@@ -167,48 +167,29 @@ GEM
|
|||||||
http-form_data (2.3.0)
|
http-form_data (2.3.0)
|
||||||
http-parser (1.2.1)
|
http-parser (1.2.1)
|
||||||
ffi-compiler (>= 1.0, < 2.0)
|
ffi-compiler (>= 1.0, < 2.0)
|
||||||
httparty (0.18.0)
|
|
||||||
mime-types (~> 3.0)
|
|
||||||
multi_xml (>= 0.5.2)
|
|
||||||
i18n (1.8.3)
|
i18n (1.8.3)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
ipaddress_2 (0.13.0)
|
ipaddress_2 (0.13.0)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
jquery-rails (4.3.5)
|
json (2.3.1)
|
||||||
rails-dom-testing (>= 1, < 3)
|
|
||||||
railties (>= 4.2.0)
|
|
||||||
thor (>= 0.14, < 2.0)
|
|
||||||
json (2.3.0)
|
|
||||||
jwt (2.2.1)
|
jwt (2.2.1)
|
||||||
kgio (2.11.3)
|
kgio (2.11.3)
|
||||||
listen (3.2.1)
|
listen (3.2.1)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.5.0)
|
loofah (2.6.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
marcel (0.3.3)
|
marcel (0.3.3)
|
||||||
mimemagic (~> 0.3.2)
|
mimemagic (~> 0.3.2)
|
||||||
mechanize (2.7.6)
|
|
||||||
domain_name (~> 0.5, >= 0.5.1)
|
|
||||||
http-cookie (~> 1.0)
|
|
||||||
mime-types (>= 1.17.2)
|
|
||||||
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
|
||||||
net-http-persistent (>= 2.5.2)
|
|
||||||
nokogiri (~> 1.6)
|
|
||||||
ntlm-http (~> 0.1, >= 0.1.1)
|
|
||||||
webrobots (>= 0.0.9, < 0.2)
|
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
memory_profiler (0.9.14)
|
memory_profiler (0.9.14)
|
||||||
meta_request (0.7.2)
|
meta_request (0.7.2)
|
||||||
rack-contrib (>= 1.1, < 3)
|
rack-contrib (>= 1.1, < 3)
|
||||||
railties (>= 3.0.0, < 7)
|
railties (>= 3.0.0, < 7)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.3.1)
|
|
||||||
mime-types-data (~> 3.2015)
|
|
||||||
mime-types-data (3.2020.0512)
|
|
||||||
mimemagic (0.3.5)
|
mimemagic (0.3.5)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
mini_portile2 (2.4.0)
|
mini_portile2 (2.4.0)
|
||||||
@@ -221,42 +202,32 @@ GEM
|
|||||||
minitest (>= 5.0)
|
minitest (>= 5.0)
|
||||||
ruby-progressbar
|
ruby-progressbar
|
||||||
mocha (1.11.2)
|
mocha (1.11.2)
|
||||||
mock_redis (0.23.0)
|
mock_redis (0.25.0)
|
||||||
msgpack (1.3.3)
|
msgpack (1.3.3)
|
||||||
msgpack (1.3.3-x64-mingw32)
|
multi_json (1.15.0)
|
||||||
multi_json (1.14.1)
|
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
mustermann (1.1.1)
|
|
||||||
ruby2_keywords (~> 0.0.1)
|
|
||||||
net-http-digest_auth (1.4.1)
|
|
||||||
net-http-persistent (4.0.0)
|
|
||||||
connection_pool (~> 2.2)
|
|
||||||
net-scp (3.0.0)
|
net-scp (3.0.0)
|
||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-sftp (3.0.0)
|
net-sftp (3.0.0)
|
||||||
net-ssh (>= 5.0.0, < 7.0.0)
|
net-ssh (>= 5.0.0, < 7.0.0)
|
||||||
net-ssh (6.0.2)
|
net-ssh (6.1.0)
|
||||||
newrelic_rpm (6.11.0.365)
|
newrelic_rpm (6.11.0.365)
|
||||||
nio4r (2.5.2)
|
nio4r (2.5.2)
|
||||||
nokogiri (1.10.9)
|
nokogiri (1.10.10)
|
||||||
mini_portile2 (~> 2.4.0)
|
|
||||||
nokogiri (1.10.9-x64-mingw32)
|
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
nokogumbo (2.0.2)
|
nokogumbo (2.0.2)
|
||||||
nokogiri (~> 1.8, >= 1.8.4)
|
nokogiri (~> 1.8, >= 1.8.4)
|
||||||
ntlm-http (0.1.1)
|
|
||||||
oauth2 (1.4.4)
|
oauth2 (1.4.4)
|
||||||
faraday (>= 0.8, < 2.0)
|
faraday (>= 0.8, < 2.0)
|
||||||
jwt (>= 1.0, < 3.0)
|
jwt (>= 1.0, < 3.0)
|
||||||
multi_json (~> 1.3)
|
multi_json (~> 1.3)
|
||||||
multi_xml (~> 0.5)
|
multi_xml (~> 0.5)
|
||||||
rack (>= 1.2, < 3)
|
rack (>= 1.2, < 3)
|
||||||
parallel (1.19.1)
|
parallel (1.19.2)
|
||||||
parser (2.7.1.3)
|
parser (2.7.1.4)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.1)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pg (1.2.3-x64-mingw32)
|
|
||||||
pry (0.13.1)
|
pry (0.13.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
@@ -270,40 +241,38 @@ GEM
|
|||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
rack (2.2.2)
|
rack (2.2.3)
|
||||||
rack-contrib (2.2.0)
|
rack-contrib (2.2.0)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rack-mini-profiler (2.0.2)
|
rack-mini-profiler (2.0.2)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-protection (2.0.8.1)
|
|
||||||
rack
|
|
||||||
rack-proxy (0.6.5)
|
rack-proxy (0.6.5)
|
||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.0.3.1)
|
rails (6.0.3.2)
|
||||||
actioncable (= 6.0.3.1)
|
actioncable (= 6.0.3.2)
|
||||||
actionmailbox (= 6.0.3.1)
|
actionmailbox (= 6.0.3.2)
|
||||||
actionmailer (= 6.0.3.1)
|
actionmailer (= 6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
actiontext (= 6.0.3.1)
|
actiontext (= 6.0.3.2)
|
||||||
actionview (= 6.0.3.1)
|
actionview (= 6.0.3.2)
|
||||||
activejob (= 6.0.3.1)
|
activejob (= 6.0.3.2)
|
||||||
activemodel (= 6.0.3.1)
|
activemodel (= 6.0.3.2)
|
||||||
activerecord (= 6.0.3.1)
|
activerecord (= 6.0.3.2)
|
||||||
activestorage (= 6.0.3.1)
|
activestorage (= 6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 6.0.3.1)
|
railties (= 6.0.3.2)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.3.0)
|
rails-html-sanitizer (1.3.0)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.3)
|
||||||
railties (6.0.3.1)
|
railties (6.0.3.2)
|
||||||
actionpack (= 6.0.3.1)
|
actionpack (= 6.0.3.2)
|
||||||
activesupport (= 6.0.3.1)
|
activesupport (= 6.0.3.2)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.20.3, < 2.0)
|
thor (>= 0.20.3, < 2.0)
|
||||||
@@ -316,25 +285,24 @@ GEM
|
|||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
recaptcha (5.5.0)
|
recaptcha (5.5.0)
|
||||||
json
|
json
|
||||||
redis (4.1.4)
|
redis (4.2.1)
|
||||||
regexp_parser (1.7.1)
|
regexp_parser (1.7.1)
|
||||||
request_store (1.5.0)
|
request_store (1.5.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
retriable (3.1.2)
|
|
||||||
rexml (3.2.4)
|
rexml (3.2.4)
|
||||||
rubocop (0.85.1)
|
rubocop (0.88.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.1.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.7)
|
regexp_parser (>= 1.7)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.0.3)
|
rubocop-ast (>= 0.1.0, < 1.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-ast (0.0.3)
|
rubocop-ast (0.1.0)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.0.1)
|
||||||
rubocop-rails (2.6.0)
|
rubocop-rails (2.6.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@@ -343,10 +311,8 @@ GEM
|
|||||||
ruby-progressbar (1.10.1)
|
ruby-progressbar (1.10.1)
|
||||||
ruby-vips (2.0.17)
|
ruby-vips (2.0.17)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
ruby2_keywords (0.0.2)
|
|
||||||
rubyzip (2.3.0)
|
rubyzip (2.3.0)
|
||||||
safe_yaml (1.0.5)
|
sanitize (5.2.1)
|
||||||
sanitize (5.2.0)
|
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.8.0)
|
nokogiri (>= 1.8.0)
|
||||||
nokogumbo (~> 2.0)
|
nokogumbo (~> 2.0)
|
||||||
@@ -357,23 +323,17 @@ GEM
|
|||||||
childprocess (>= 0.5, < 4.0)
|
childprocess (>= 0.5, < 4.0)
|
||||||
rubyzip (>= 1.2.2)
|
rubyzip (>= 1.2.2)
|
||||||
semantic_range (2.3.0)
|
semantic_range (2.3.0)
|
||||||
shoulda-context (1.2.2)
|
shoulda-context (2.0.0)
|
||||||
shoulda-matchers (4.3.0)
|
shoulda-matchers (4.3.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
simple_form (5.0.2)
|
simple_form (5.0.2)
|
||||||
actionpack (>= 5.0)
|
actionpack (>= 5.0)
|
||||||
activemodel (>= 5.0)
|
activemodel (>= 5.0)
|
||||||
simplecov (0.17.1)
|
simplecov (0.18.5)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
json (>= 1.8, < 3)
|
simplecov-html (~> 0.11)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (0.12.2)
|
||||||
simplecov-html (0.10.2)
|
sprockets (4.0.2)
|
||||||
sinatra (2.0.8.1)
|
|
||||||
mustermann (~> 1.0)
|
|
||||||
rack (~> 2.0)
|
|
||||||
rack-protection (= 2.0.8.1)
|
|
||||||
tilt (~> 2.0)
|
|
||||||
sprockets (4.0.0)
|
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.1)
|
sprockets-rails (3.2.1)
|
||||||
@@ -389,13 +349,11 @@ GEM
|
|||||||
stripe (5.22.0)
|
stripe (5.22.0)
|
||||||
thor (1.0.1)
|
thor (1.0.1)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.10)
|
|
||||||
tzinfo (1.2.7)
|
tzinfo (1.2.7)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.7.7)
|
||||||
unf_ext (0.0.7.7-x64-mingw32)
|
|
||||||
unicode-display_width (1.7.0)
|
unicode-display_width (1.7.0)
|
||||||
unicorn (5.5.5)
|
unicorn (5.5.5)
|
||||||
kgio (~> 2.6)
|
kgio (~> 2.6)
|
||||||
@@ -403,28 +361,22 @@ GEM
|
|||||||
unicorn-worker-killer (0.4.4)
|
unicorn-worker-killer (0.4.4)
|
||||||
get_process_mem (~> 0)
|
get_process_mem (~> 0)
|
||||||
unicorn (>= 4, < 6)
|
unicorn (>= 4, < 6)
|
||||||
webmock (3.8.3)
|
|
||||||
addressable (>= 2.3.6)
|
|
||||||
crack (>= 0.3.2)
|
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
|
||||||
webpacker (5.1.1)
|
webpacker (5.1.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
webrobots (0.1.2)
|
websocket-driver (0.7.3)
|
||||||
websocket-driver (0.7.2)
|
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
whenever (1.0.0)
|
whenever (1.0.0)
|
||||||
chronic (>= 0.6.3)
|
chronic (>= 0.6.3)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.3.0)
|
zeitwerk (2.3.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
x64-mingw32
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
activemodel-serializers-xml
|
activemodel-serializers-xml
|
||||||
@@ -440,6 +392,7 @@ DEPENDENCIES
|
|||||||
capistrano-rbenv
|
capistrano-rbenv
|
||||||
capistrano3-unicorn
|
capistrano3-unicorn
|
||||||
capybara
|
capybara
|
||||||
|
codecov
|
||||||
daemons
|
daemons
|
||||||
delayed_job
|
delayed_job
|
||||||
delayed_job_active_record
|
delayed_job_active_record
|
||||||
@@ -450,12 +403,9 @@ DEPENDENCIES
|
|||||||
ffaker
|
ffaker
|
||||||
flamegraph
|
flamegraph
|
||||||
http
|
http
|
||||||
httparty
|
|
||||||
ipaddress_2
|
ipaddress_2
|
||||||
jquery-rails
|
|
||||||
listen
|
listen
|
||||||
mail
|
mail
|
||||||
mechanize
|
|
||||||
memoist
|
memoist
|
||||||
memory_profiler
|
memory_profiler
|
||||||
meta_request
|
meta_request
|
||||||
@@ -465,7 +415,7 @@ DEPENDENCIES
|
|||||||
mock_redis
|
mock_redis
|
||||||
net-sftp
|
net-sftp
|
||||||
newrelic_rpm
|
newrelic_rpm
|
||||||
nokogiri (~> 1.10.9)
|
nokogiri
|
||||||
oauth2
|
oauth2
|
||||||
pg
|
pg
|
||||||
pry-byebug
|
pry-byebug
|
||||||
@@ -480,7 +430,6 @@ DEPENDENCIES
|
|||||||
redis
|
redis
|
||||||
request_store
|
request_store
|
||||||
responders
|
responders
|
||||||
retriable
|
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
ruby-vips
|
ruby-vips
|
||||||
@@ -491,14 +440,12 @@ DEPENDENCIES
|
|||||||
shoulda-context
|
shoulda-context
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
simple_form
|
simple_form
|
||||||
simplecov (~> 0.17.0)
|
simplecov
|
||||||
sinatra
|
|
||||||
stackprof
|
stackprof
|
||||||
streamio-ffmpeg
|
streamio-ffmpeg
|
||||||
stripe
|
stripe
|
||||||
unicorn
|
unicorn
|
||||||
unicorn-worker-killer
|
unicorn-worker-killer
|
||||||
webmock
|
|
||||||
webpacker (>= 4.0.x)
|
webpacker (>= 4.0.x)
|
||||||
whenever
|
whenever
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ if [[ -z "$HOSTNAME" ]] ; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -n "* Enter the VLAN IP address for this server (ex: 172.16.0.1, enter nothing to skip): "
|
|
||||||
read VLAN_IP_ADDR
|
|
||||||
|
|
||||||
# Install packages
|
# Install packages
|
||||||
echo "* Installing packages..."
|
echo "* Installing packages..."
|
||||||
|
|
||||||
@@ -52,17 +49,6 @@ apt-get -y install $LIBSSL_DEV_PKG build-essential automake libxml2-dev libxslt-
|
|||||||
apt-get -y install libpq-dev postgresql-client
|
apt-get -y install libpq-dev postgresql-client
|
||||||
apt-get -y install liblcms2-dev $LIBJPEG_TURBO_DEV_PKG libexpat1-dev libgif-dev libpng-dev libexif-dev
|
apt-get -y install liblcms2-dev $LIBJPEG_TURBO_DEV_PKG libexpat1-dev libgif-dev libpng-dev libexif-dev
|
||||||
|
|
||||||
# vrack specific stuff
|
|
||||||
if [ -n "$VLAN_IP_ADDR" ] ; then
|
|
||||||
apt-get -y install vlan
|
|
||||||
modprobe 8021q
|
|
||||||
echo "8021q" >> /etc/modules
|
|
||||||
vconfig add eno2 99
|
|
||||||
ip addr add $VLAN_IP_ADDR/24 dev eno2.99
|
|
||||||
ip link set up eno2.99
|
|
||||||
curl -L -s $GITHUB_INSTALL_SCRIPTS/vrack-cfg.yaml -o /etc/netplan/01-netcfg.yaml
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||||
curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash -
|
curl -sSL https://deb.nodesource.com/setup_10.x | sudo -E bash -
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[](https://codeclimate.com/github/danbooru/danbooru/test_coverage) [](https://discord.gg/eSVKkUF)
|
[](https://codecov.io/gh/danbooru/danbooru) [](https://discord.gg/eSVKkUF)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
def rescue_exception(exception)
|
def rescue_exception(exception)
|
||||||
case exception
|
case exception
|
||||||
|
when ActionView::Template::Error
|
||||||
|
rescue_exception(exception.cause)
|
||||||
when ActiveRecord::QueryCanceled
|
when ActiveRecord::QueryCanceled
|
||||||
render_error_page(500, exception, template: "static/search_timeout", message: "The database timed out running your query.")
|
render_error_page(500, exception, template: "static/search_timeout", message: "The database timed out running your query.")
|
||||||
when ActionController::BadRequest
|
when ActionController::BadRequest
|
||||||
|
|||||||
16
app/controllers/autocomplete_controller.rb
Normal file
16
app/controllers/autocomplete_controller.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
class AutocompleteController < ApplicationController
|
||||||
|
respond_to :xml, :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
@tags = Tag.names_matches_with_aliases(params[:query], params.fetch(:limit, 10).to_i)
|
||||||
|
|
||||||
|
if request.variant.opensearch?
|
||||||
|
expires_in 1.hour
|
||||||
|
results = [params[:query], @tags.map(&:pretty_name)]
|
||||||
|
respond_with(results)
|
||||||
|
else
|
||||||
|
# XXX
|
||||||
|
respond_with(@tags.map(&:attributes))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -97,6 +97,7 @@ class CommentsController < ApplicationController
|
|||||||
|
|
||||||
if request.format.atom?
|
if request.format.atom?
|
||||||
@comments = @comments.includes(:creator, :post)
|
@comments = @comments.includes(:creator, :post)
|
||||||
|
@comments = @comments.select { |comment| comment.post.visible? }
|
||||||
elsif request.format.html?
|
elsif request.format.html?
|
||||||
@comments = @comments.includes(:creator, :updater, post: :uploader)
|
@comments = @comments.includes(:creator, :updater, post: :uploader)
|
||||||
@comments = @comments.includes(:votes) if CurrentUser.is_member?
|
@comments = @comments.includes(:votes) if CurrentUser.is_member?
|
||||||
|
|||||||
@@ -22,23 +22,25 @@ module Explore
|
|||||||
|
|
||||||
def viewed
|
def viewed
|
||||||
@date, @scale, @min_date, @max_date = parse_date(params)
|
@date, @scale, @min_date, @max_date = parse_date(params)
|
||||||
@posts = PostViewCountService.new.popular_posts(@date)
|
@posts = ReportbooruService.new.popular_posts(@date)
|
||||||
respond_with(@posts)
|
respond_with(@posts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def searches
|
def searches
|
||||||
@date, @scale, @min_date, @max_date = parse_date(params)
|
@date, @scale, @min_date, @max_date = parse_date(params)
|
||||||
@search_service = PopularSearchService.new(@date)
|
@searches = ReportbooruService.new.post_search_rankings(@date)
|
||||||
|
respond_with(@searches)
|
||||||
end
|
end
|
||||||
|
|
||||||
def missed_searches
|
def missed_searches
|
||||||
@search_service = MissedSearchService.new
|
@missed_searches = ReportbooruService.new.missed_search_rankings
|
||||||
|
respond_with(@missed_searches)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def parse_date(params)
|
def parse_date(params)
|
||||||
date = params[:date].present? ? Date.parse(params[:date]) : Time.zone.today
|
date = params[:date].present? ? Date.parse(params[:date]) : Date.today
|
||||||
scale = params[:scale].in?(["day", "week", "month"]) ? params[:scale] : "day"
|
scale = params[:scale].in?(["day", "week", "month"]) ? params[:scale] : "day"
|
||||||
min_date = date.send("beginning_of_#{scale}")
|
min_date = date.send("beginning_of_#{scale}")
|
||||||
max_date = date.send("next_#{scale}").send("beginning_of_#{scale}")
|
max_date = date.send("next_#{scale}").send("beginning_of_#{scale}")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class IqdbQueriesController < ApplicationController
|
|||||||
# XXX allow bare search params for backwards compatibility.
|
# XXX allow bare search params for backwards compatibility.
|
||||||
search_params.merge!(params.slice(:url, :image_url, :file_url, :post_id, :limit, :similarity, :high_similarity).permit!)
|
search_params.merge!(params.slice(:url, :image_url, :file_url, :post_id, :limit, :similarity, :high_similarity).permit!)
|
||||||
|
|
||||||
@high_similarity_matches, @low_similarity_matches, @matches = IqdbProxy.search(search_params)
|
@high_similarity_matches, @low_similarity_matches, @matches = IqdbProxy.new.search(search_params)
|
||||||
respond_with(@matches, template: "iqdb_queries/show")
|
respond_with(@matches, template: "iqdb_queries/show")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
48
app/controllers/mock_services_controller.rb
Normal file
48
app/controllers/mock_services_controller.rb
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
class MockServicesController < ApplicationController
|
||||||
|
skip_forgery_protection
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
raise User::PrivilegeError if Rails.env.production?
|
||||||
|
end
|
||||||
|
|
||||||
|
def recommender_recommend
|
||||||
|
@data = posts.map { |post| [post.id, rand(0.0..1.0)] }
|
||||||
|
render json: @data
|
||||||
|
end
|
||||||
|
|
||||||
|
def recommender_similar
|
||||||
|
@data = posts.map { |post| [post.id, rand(0.0..1.0)] }
|
||||||
|
render json: @data
|
||||||
|
end
|
||||||
|
|
||||||
|
def reportbooru_missed_searches
|
||||||
|
@data = tags.map { |tag| "#{tag.name} #{rand(1.0..1000.0)}" }.join("\n")
|
||||||
|
render json: @data
|
||||||
|
end
|
||||||
|
|
||||||
|
def reportbooru_post_searches
|
||||||
|
@data = tags.map { |tag| [tag.name, rand(1..1000)] }
|
||||||
|
render json: @data
|
||||||
|
end
|
||||||
|
|
||||||
|
def reportbooru_post_views
|
||||||
|
@data = posts.map { |post| [post.id, rand(1..1000)] }
|
||||||
|
render json: @data
|
||||||
|
end
|
||||||
|
|
||||||
|
def iqdbs_similar
|
||||||
|
@data = posts.map { |post| { post_id: post.id, score: rand(0..100)} }
|
||||||
|
render json: @data
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def posts(limit = 10)
|
||||||
|
Post.last(limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tags(limit = 10)
|
||||||
|
Tag.order(post_count: :desc).limit(limit)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
class StaticController < ApplicationController
|
class StaticController < ApplicationController
|
||||||
|
def privacy_policy
|
||||||
|
end
|
||||||
|
|
||||||
def terms_of_service
|
def terms_of_service
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -13,13 +16,40 @@ class StaticController < ApplicationController
|
|||||||
redirect_to wiki_page_path("help:dtext") unless request.format.js?
|
redirect_to wiki_page_path("help:dtext") unless request.format.js?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def opensearch
|
||||||
|
end
|
||||||
|
|
||||||
def site_map
|
def site_map
|
||||||
end
|
end
|
||||||
|
|
||||||
def sitemap
|
def sitemap_index
|
||||||
@popular_search_service = PopularSearchService.new(Date.yesterday)
|
@sitemap = params[:sitemap]
|
||||||
@posts = Post.where("created_at > ?", 1.week.ago).order(score: :desc).limit(200)
|
@limit = params.fetch(:limit, 10000).to_i
|
||||||
@posts = @posts.select(&:visible?)
|
|
||||||
render layout: false
|
case @sitemap
|
||||||
|
when "artists"
|
||||||
|
@relation = Artist.undeleted
|
||||||
|
@search = { is_deleted: "false" }
|
||||||
|
when "forum_topics"
|
||||||
|
@relation = ForumTopic.undeleted
|
||||||
|
@search = { is_deleted: "false" }
|
||||||
|
when "pools"
|
||||||
|
@relation = Pool.undeleted
|
||||||
|
@search = { is_deleted: "false" }
|
||||||
|
when "posts"
|
||||||
|
@relation = Post.order(id: :asc)
|
||||||
|
@serach = {}
|
||||||
|
when "tags"
|
||||||
|
@relation = Tag.nonempty
|
||||||
|
@search = {}
|
||||||
|
when "users"
|
||||||
|
@relation = User.all
|
||||||
|
@search = {}
|
||||||
|
when "wiki_pages"
|
||||||
|
@relation = WikiPage.undeleted
|
||||||
|
@search = { is_deleted: "false" }
|
||||||
|
else
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class UploadsController < ApplicationController
|
|||||||
def image_proxy
|
def image_proxy
|
||||||
authorize Upload
|
authorize Upload
|
||||||
resp = ImageProxy.get_image(params[:url])
|
resp = ImageProxy.get_image(params[:url])
|
||||||
send_data resp.body, :type => resp.content_type, :disposition => "inline"
|
send_data resp.body, type: resp.mime_type, disposition: "inline"
|
||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class UserNameChangeRequestsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@change_requests = authorize UserNameChangeRequest.visible(CurrentUser.user).order("id desc").paginate(params[:page], :limit => params[:limit])
|
@change_requests = authorize UserNameChangeRequest.visible(CurrentUser.user).paginated_search(params)
|
||||||
respond_with(@change_requests)
|
respond_with(@change_requests)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class UsersController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
if params[:name].present?
|
if params[:name].present?
|
||||||
@user = User.find_by_name!(params[:name])
|
@user = User.find_by_name!(params[:name])
|
||||||
redirect_to user_path(@user)
|
redirect_to user_path(@user, variant: params[:variant])
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,7 +42,9 @@ class UsersController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@user = authorize User.find(params[:id])
|
@user = authorize User.find(params[:id])
|
||||||
respond_with(@user, methods: @user.full_attributes)
|
respond_with(@user, methods: @user.full_attributes) do |format|
|
||||||
|
format.html.tooltip { render layout: false }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def profile
|
def profile
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ module ApplicationHelper
|
|||||||
raw content_tag(:time, duration, datetime: datetime, title: title)
|
raw content_tag(:time, duration, datetime: datetime, title: title)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def humanized_number(number)
|
||||||
|
number_to_human number, units: { thousand: "k", million: "m" }, format: "%n%u"
|
||||||
|
end
|
||||||
|
|
||||||
def time_ago_in_words_tagged(time, compact: false)
|
def time_ago_in_words_tagged(time, compact: false)
|
||||||
if time.nil?
|
if time.nil?
|
||||||
tag.em(tag.time("unknown"))
|
tag.em(tag.time("unknown"))
|
||||||
@@ -162,8 +166,10 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_ip(ip)
|
def link_to_ip(ip, shorten: false, **options)
|
||||||
link_to ip, ip_addresses_path(search: { ip_addr: ip, group_by: "user" })
|
ip_addr = IPAddr.new(ip.to_s)
|
||||||
|
ip_addr.prefix = 64 if ip_addr.ipv6? && shorten
|
||||||
|
link_to ip_addr.to_s, ip_addresses_path(search: { ip_addr: ip, group_by: "user" }), **options
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_search(search)
|
def link_to_search(search)
|
||||||
@@ -186,12 +192,13 @@ module ApplicationHelper
|
|||||||
def link_to_user(user)
|
def link_to_user(user)
|
||||||
return "anonymous" if user.blank?
|
return "anonymous" if user.blank?
|
||||||
|
|
||||||
user_class = "user-#{user.level_string.downcase}"
|
user_class = "user user-#{user.level_string.downcase}"
|
||||||
user_class += " user-post-approver" if user.can_approve_posts?
|
user_class += " user-post-approver" if user.can_approve_posts?
|
||||||
user_class += " user-post-uploader" if user.can_upload_free?
|
user_class += " user-post-uploader" if user.can_upload_free?
|
||||||
user_class += " user-banned" if user.is_banned?
|
user_class += " user-banned" if user.is_banned?
|
||||||
user_class += " with-style" if CurrentUser.user.style_usernames?
|
|
||||||
link_to(user.pretty_name, user_path(user), :class => user_class)
|
data = { "user-id": user.id, "user-name": user.name, "user-level": user.level }
|
||||||
|
link_to(user.pretty_name, user_path(user), class: user_class, data: data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mod_link_to_user(user, positive_or_negative)
|
def mod_link_to_user(user, positive_or_negative)
|
||||||
@@ -217,21 +224,8 @@ module ApplicationHelper
|
|||||||
tag.div(text, class: "prose", **options)
|
tag.div(text, class: "prose", **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def dtext_field(object, name, options = {})
|
def dtext_preview_button(preview_field)
|
||||||
options[:name] ||= name.capitalize
|
tag.input value: "Preview", type: "button", class: "dtext-preview-button", "data-preview-field": preview_field
|
||||||
options[:input_id] ||= "#{object}_#{name}"
|
|
||||||
options[:input_name] ||= "#{object}[#{name}]"
|
|
||||||
options[:value] ||= instance_variable_get("@#{object}").try(name)
|
|
||||||
options[:preview_id] ||= "dtext-preview"
|
|
||||||
options[:classes] ||= ""
|
|
||||||
options[:hint] ||= ""
|
|
||||||
options[:type] ||= "text"
|
|
||||||
|
|
||||||
render "dtext/form", options
|
|
||||||
end
|
|
||||||
|
|
||||||
def dtext_preview_button(object, name, input_id: "#{object}_#{name}", preview_id: "dtext-preview")
|
|
||||||
tag.input value: "Preview", type: "button", class: "dtext-preview-button", "data-input-id": input_id, "data-preview-id": preview_id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def quick_search_form_for(attribute, url, name, autocomplete: nil, redirect: false, &block)
|
def quick_search_form_for(attribute, url, name, autocomplete: nil, redirect: false, &block)
|
||||||
@@ -266,7 +260,7 @@ module ApplicationHelper
|
|||||||
def body_attributes(user, params, current_item = nil)
|
def body_attributes(user, params, current_item = nil)
|
||||||
current_user_data_attributes = data_attributes_for(user, "current-user", current_user_attributes)
|
current_user_data_attributes = data_attributes_for(user, "current-user", current_user_attributes)
|
||||||
|
|
||||||
if current_item.present? && current_item.respond_to?(:html_data_attributes) && current_item.respond_to?(:model_name)
|
if !current_item.nil? && current_item.respond_to?(:html_data_attributes) && current_item.respond_to?(:model_name)
|
||||||
model_name = current_item.model_name.singular.dasherize
|
model_name = current_item.model_name.singular.dasherize
|
||||||
model_attributes = current_item.html_data_attributes
|
model_attributes = current_item.html_data_attributes
|
||||||
current_item_data_attributes = data_attributes_for(current_item, model_name, model_attributes)
|
current_item_data_attributes = data_attributes_for(current_item, model_name, model_attributes)
|
||||||
@@ -353,6 +347,19 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def canonical_url(url = nil)
|
||||||
|
if url.present?
|
||||||
|
content_for(:canonical_url) { url }
|
||||||
|
elsif content_for(:canonical_url).present?
|
||||||
|
content_for(:canonical_url)
|
||||||
|
else
|
||||||
|
request_params = request.params.sort.to_h.with_indifferent_access
|
||||||
|
request_params.delete(:page) if request_params[:page].to_i == 1
|
||||||
|
request_params.delete(:limit)
|
||||||
|
url_for(**request_params, host: Danbooru.config.hostname, only_path: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def atom_feed_tag(title, url = {})
|
def atom_feed_tag(title, url = {})
|
||||||
content_for(:html_header, auto_discovery_link_tag(:atom, url, title: title))
|
content_for(:html_header, auto_discovery_link_tag(:atom, url, title: title))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ module PaginationHelper
|
|||||||
params[:page] =~ /[ab]/ || records.current_page >= Danbooru.config.max_numbered_pages
|
params[:page] =~ /[ab]/ || records.current_page >= Danbooru.config.max_numbered_pages
|
||||||
end
|
end
|
||||||
|
|
||||||
def numbered_paginator(records, switch_to_sequential = true)
|
def numbered_paginator(records)
|
||||||
if use_sequential_paginator?(records) && switch_to_sequential
|
if use_sequential_paginator?(records)
|
||||||
return sequential_paginator(records)
|
return sequential_paginator(records)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
65
app/helpers/seo_helper.rb
Normal file
65
app/helpers/seo_helper.rb
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# https://yoast.com/structured-data-schema-ultimate-guide/
|
||||||
|
# https://technicalseo.com/tools/schema-markup-generator/
|
||||||
|
# https://developers.google.com/search/docs/data-types/sitelinks-searchbox
|
||||||
|
# https://developers.google.com/search/docs/data-types/logo
|
||||||
|
# https://search.google.com/structured-data/testing-tool/u/0/
|
||||||
|
# https://search.google.com/test/rich-results
|
||||||
|
# https://schema.org/Organization
|
||||||
|
# https://schema.org/WebSite
|
||||||
|
|
||||||
|
module SeoHelper
|
||||||
|
def site_description
|
||||||
|
"#{Danbooru.config.canonical_app_name} is the original anime image booru. Search millions of anime pictures categorized by thousands of tags."
|
||||||
|
end
|
||||||
|
|
||||||
|
# https://developers.google.com/search/docs/data-types/video#video-object
|
||||||
|
def json_ld_video_data(post)
|
||||||
|
json_ld_tag({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "VideoObject",
|
||||||
|
"name": page_title,
|
||||||
|
"description": meta_description,
|
||||||
|
"uploadDate": post.created_at.iso8601,
|
||||||
|
"thumbnailUrl": post.preview_file_url,
|
||||||
|
"contentUrl": post.file_url,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_ld_website_data
|
||||||
|
urls = [
|
||||||
|
Danbooru.config.twitter_url,
|
||||||
|
Danbooru.config.discord_server_url,
|
||||||
|
Danbooru.config.source_code_url,
|
||||||
|
"https://en.wikipedia.org/wiki/Danbooru"
|
||||||
|
].compact
|
||||||
|
|
||||||
|
json_ld_tag({
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@type": "Organization",
|
||||||
|
url: root_url(host: Danbooru.config.hostname),
|
||||||
|
name: Danbooru.config.app_name,
|
||||||
|
logo: "#{root_url(host: Danbooru.config.hostname)}images/danbooru-logo-500x500.png",
|
||||||
|
sameAs: urls
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "WebSite",
|
||||||
|
"@id": root_url(anchor: "website", host: Danbooru.config.hostname),
|
||||||
|
"url": root_url(host: Danbooru.config.hostname),
|
||||||
|
"name": Danbooru.config.app_name,
|
||||||
|
"description": site_description,
|
||||||
|
"potentialAction": [{
|
||||||
|
"@type": "SearchAction",
|
||||||
|
"target": "#{posts_url(host: Danbooru.config.hostname)}?tags={search_term_string}",
|
||||||
|
"query-input": "required name=search_term_string"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_ld_tag(data)
|
||||||
|
tag.script(data.to_json.html_safe, type: "application/ld+json")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,12 +6,12 @@ function importAll(r) {
|
|||||||
|
|
||||||
require('@rails/ujs').start();
|
require('@rails/ujs').start();
|
||||||
require('hammerjs');
|
require('hammerjs');
|
||||||
require('stupid-table-plugin');
|
|
||||||
require('jquery-hotkeys');
|
require('jquery-hotkeys');
|
||||||
|
|
||||||
// should start looking for nodejs replacements
|
// should start looking for nodejs replacements
|
||||||
importAll(require.context('../vendor', true, /\.js$/));
|
importAll(require.context('../vendor', true, /\.js$/));
|
||||||
|
|
||||||
|
require('jquery');
|
||||||
require("jquery-ui/ui/effects/effect-shake");
|
require("jquery-ui/ui/effects/effect-shake");
|
||||||
require("jquery-ui/ui/widgets/autocomplete");
|
require("jquery-ui/ui/widgets/autocomplete");
|
||||||
require("jquery-ui/ui/widgets/button");
|
require("jquery-ui/ui/widgets/button");
|
||||||
@@ -33,6 +33,7 @@ require("@fortawesome/fontawesome-free/css/regular.css");
|
|||||||
importAll(require.context('../src/javascripts', true, /\.js(\.erb)?$/));
|
importAll(require.context('../src/javascripts', true, /\.js(\.erb)?$/));
|
||||||
importAll(require.context('../src/styles', true, /\.s?css(?:\.erb)?$/));
|
importAll(require.context('../src/styles', true, /\.s?css(?:\.erb)?$/));
|
||||||
|
|
||||||
|
export { default as jQuery } from "jquery";
|
||||||
export { default as Autocomplete } from '../src/javascripts/autocomplete.js.erb';
|
export { default as Autocomplete } from '../src/javascripts/autocomplete.js.erb';
|
||||||
export { default as Blacklist } from '../src/javascripts/blacklists.js';
|
export { default as Blacklist } from '../src/javascripts/blacklists.js';
|
||||||
export { default as Comment } from '../src/javascripts/comments.js';
|
export { default as Comment } from '../src/javascripts/comments.js';
|
||||||
@@ -47,5 +48,6 @@ export { default as PostVersion } from '../src/javascripts/post_version.js';
|
|||||||
export { default as RelatedTag } from '../src/javascripts/related_tag.js';
|
export { default as RelatedTag } from '../src/javascripts/related_tag.js';
|
||||||
export { default as Shortcuts } from '../src/javascripts/shortcuts.js';
|
export { default as Shortcuts } from '../src/javascripts/shortcuts.js';
|
||||||
export { default as Upload } from '../src/javascripts/uploads.js.erb';
|
export { default as Upload } from '../src/javascripts/uploads.js.erb';
|
||||||
|
export { default as UserTooltip } from '../src/javascripts/user_tooltips.js';
|
||||||
export { default as Utility } from '../src/javascripts/utility.js';
|
export { default as Utility } from '../src/javascripts/utility.js';
|
||||||
export { default as Ugoira } from '../src/javascripts/ugoira.js';
|
export { default as Ugoira } from '../src/javascripts/ugoira.js';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_
|
|||||||
Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>;
|
Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>;
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
|
Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated"];
|
||||||
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
|
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
|
||||||
Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|");
|
Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|");
|
||||||
Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i");
|
Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i");
|
||||||
@@ -37,7 +38,7 @@ Autocomplete.initialize_all = function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.initialize_tag_autocomplete();
|
this.initialize_tag_autocomplete();
|
||||||
this.initialize_mention_autocomplete($(".autocomplete-mentions textarea"));
|
this.initialize_mention_autocomplete($("form div.input.dtext textarea"));
|
||||||
this.initialize_fields($('[data-autocomplete="tag"]'), Autocomplete.tag_source);
|
this.initialize_fields($('[data-autocomplete="tag"]'), Autocomplete.tag_source);
|
||||||
this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source);
|
this.initialize_fields($('[data-autocomplete="artist"]'), Autocomplete.artist_source);
|
||||||
this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source);
|
this.initialize_fields($('[data-autocomplete="pool"]'), Autocomplete.pool_source);
|
||||||
@@ -248,9 +249,6 @@ Autocomplete.render_item = function(list, item) {
|
|||||||
} else if (item.type === "user") {
|
} else if (item.type === "user") {
|
||||||
var level_class = "user-" + item.level.toLowerCase();
|
var level_class = "user-" + item.level.toLowerCase();
|
||||||
$link.addClass(level_class);
|
$link.addClass(level_class);
|
||||||
if (CurrentUser.data("style-usernames")) {
|
|
||||||
$link.addClass("with-style");
|
|
||||||
}
|
|
||||||
} else if (item.type === "pool") {
|
} else if (item.type === "pool") {
|
||||||
$link.addClass("pool-category-" + item.category);
|
$link.addClass("pool-category-" + item.category);
|
||||||
}
|
}
|
||||||
@@ -268,9 +266,7 @@ Autocomplete.render_item = function(list, item) {
|
|||||||
|
|
||||||
Autocomplete.static_metatags = {
|
Autocomplete.static_metatags = {
|
||||||
order: Autocomplete.ORDER_METATAGS,
|
order: Autocomplete.ORDER_METATAGS,
|
||||||
status: [
|
status: ["any"].concat(Autocomplete.MISC_STATUSES),
|
||||||
"any", "deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated"
|
|
||||||
],
|
|
||||||
rating: [
|
rating: [
|
||||||
"safe", "questionable", "explicit"
|
"safe", "questionable", "explicit"
|
||||||
],
|
],
|
||||||
@@ -280,12 +276,8 @@ Autocomplete.static_metatags = {
|
|||||||
embedded: [
|
embedded: [
|
||||||
"true", "false"
|
"true", "false"
|
||||||
],
|
],
|
||||||
child: [
|
child: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
|
||||||
"any", "none"
|
parent: ["any", "none"].concat(Autocomplete.MISC_STATUSES),
|
||||||
],
|
|
||||||
parent: [
|
|
||||||
"any", "none"
|
|
||||||
],
|
|
||||||
filetype: [
|
filetype: [
|
||||||
"jpg", "png", "gif", "swf", "zip", "webm", "mp4"
|
"jpg", "png", "gif", "swf", "zip", "webm", "mp4"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -39,8 +39,11 @@ Dtext.call_edit = function(e, $button, $input, $preview) {
|
|||||||
|
|
||||||
Dtext.click_button = function(e) {
|
Dtext.click_button = function(e) {
|
||||||
var $button = $(e.target);
|
var $button = $(e.target);
|
||||||
var $input = $("#" + $button.data("input-id"));
|
var $form = $button.parents("form");
|
||||||
var $preview = $("#" + $button.data("preview-id"));
|
var fieldName = $button.data("preview-field");
|
||||||
|
var $inputContainer = $form.find(`div.input.${fieldName} .dtext-previewable`);
|
||||||
|
var $input = $inputContainer.find("> input, > textarea");
|
||||||
|
var $preview = $inputContainer.find("div.dtext-preview");
|
||||||
|
|
||||||
if ($button.val().match(/preview/i)) {
|
if ($button.val().match(/preview/i)) {
|
||||||
Dtext.call_preview(e, $button, $input, $preview);
|
Dtext.call_preview(e, $button, $input, $preview);
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ ForumPost.initialize_all = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ForumPost.initialize_edit_links = function() {
|
ForumPost.initialize_edit_links = function() {
|
||||||
$(".edit_forum_post_link").on("click.danbooru", function(e) {
|
$(document).on("click.danbooru", ".edit_forum_post_link", function(e) {
|
||||||
var link_id = $(this).attr("id");
|
let $form = $(this).parents("article.forum-post").find("form.edit_forum_post");
|
||||||
var forum_post_id = link_id.match(/^edit_forum_post_link_(\d+)$/)[1];
|
$form.fadeToggle("fast");
|
||||||
$("#edit_forum_post_" + forum_post_id).fadeToggle("fast");
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".edit_forum_topic_link").on("click.danbooru", function(e) {
|
$(document).on("click.danbooru", ".edit_forum_topic_link", function(e) {
|
||||||
var link_id = $(this).attr("id");
|
let $form = $(this).parents("article.forum-post").find("form.edit_forum_topic");
|
||||||
var forum_topic_id = link_id.match(/^edit_forum_topic_link_(\d+)$/)[1];
|
$form.fadeToggle("fast");
|
||||||
$("#edit_forum_topic_" + forum_topic_id).fadeToggle("fast");
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ Pool.initialize_add_to_pool_link = function() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$("#add-to-pool-dialog").dialog("open");
|
$("#add-to-pool-dialog").dialog("open");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#recent-pools li").on("click.danbooru", function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$("#pool_name").val($(this).attr("data-value"));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Pool.initialize_simple_edit = function() {
|
Pool.initialize_simple_edit = function() {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ PostModeMenu.initialize_edit_form = function() {
|
|||||||
|
|
||||||
$(document).on("click.danbooru", "#quick-edit-form input[type=submit]", async function(e) {
|
$(document).on("click.danbooru", "#quick-edit-form input[type=submit]", async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let post_id = $("#quick-edit-form").data("post-id");
|
let post_id = $("#quick-edit-form").attr("data-post-id");
|
||||||
await Post.update(post_id, "quick-edit", { post: { tag_string: $("#post_tag_string").val() }});
|
await Post.update(post_id, "quick-edit", { post: { tag_string: $("#post_tag_string").val() }});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,76 @@
|
|||||||
import CurrentUser from './current_user'
|
import CurrentUser from './current_user';
|
||||||
import Utility from './utility'
|
import Utility from './utility';
|
||||||
|
import { delegate, hideAll } from 'tippy.js';
|
||||||
require('qtip2');
|
import 'tippy.js/dist/tippy.css';
|
||||||
require('qtip2/dist/jquery.qtip.css');
|
|
||||||
|
|
||||||
let PostTooltip = {};
|
let PostTooltip = {};
|
||||||
|
|
||||||
PostTooltip.render_tooltip = async function (event, qtip) {
|
PostTooltip.POST_SELECTOR = "*:not(.ui-sortable-handle) > .post-preview img, .dtext-post-id-link";
|
||||||
|
PostTooltip.SHOW_DELAY = 500;
|
||||||
|
PostTooltip.HIDE_DELAY = 125;
|
||||||
|
PostTooltip.DURATION = 250;
|
||||||
|
|
||||||
|
PostTooltip.initialize = function () {
|
||||||
|
if (PostTooltip.disabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate("body", {
|
||||||
|
allowHTML: true,
|
||||||
|
appendTo: document.body,
|
||||||
|
delay: [PostTooltip.SHOW_DELAY, PostTooltip.HIDE_DELAY],
|
||||||
|
duration: PostTooltip.DURATION,
|
||||||
|
interactive: true,
|
||||||
|
maxWidth: "none",
|
||||||
|
target: PostTooltip.POST_SELECTOR,
|
||||||
|
theme: "common-tooltip post-tooltip",
|
||||||
|
touch: false,
|
||||||
|
|
||||||
|
onCreate: PostTooltip.on_create,
|
||||||
|
onShow: PostTooltip.on_show,
|
||||||
|
onHide: PostTooltip.on_hide,
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click.danbooru.postTooltip", ".post-tooltip-disable", PostTooltip.on_disable_tooltips);
|
||||||
|
};
|
||||||
|
|
||||||
|
PostTooltip.on_create = function (instance) {
|
||||||
|
let title = instance.reference.getAttribute("title");
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
instance.reference.setAttribute("data-title", title);
|
||||||
|
instance.reference.setAttribute("title", "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PostTooltip.on_show = async function (instance) {
|
||||||
let post_id = null;
|
let post_id = null;
|
||||||
let preview = false;
|
let preview = false;
|
||||||
|
let $target = $(instance.reference);
|
||||||
|
let $tooltip = $(instance.popper);
|
||||||
|
|
||||||
if ($(this).is(".dtext-post-id-link")) {
|
hideAll({ exclude: instance });
|
||||||
|
|
||||||
|
// skip if tooltip has already been rendered.
|
||||||
|
if ($tooltip.has(".post-tooltip-body").length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($target.is(".dtext-post-id-link")) {
|
||||||
preview = true;
|
preview = true;
|
||||||
post_id = /\/posts\/(\d+)/.exec($(this).attr("href"))[1];
|
post_id = /\/posts\/(\d+)/.exec($target.attr("href"))[1];
|
||||||
} else {
|
} else {
|
||||||
post_id = $(this).parents("[data-id]").data("id");
|
post_id = $target.parents("[data-id]").data("id");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
qtip.cache.request = $.get(`/posts/${post_id}`, { variant: "tooltip", preview: preview });
|
$tooltip.addClass("tooltip-loading");
|
||||||
let html = await qtip.cache.request;
|
|
||||||
|
|
||||||
qtip.set("content.text", html);
|
instance._request = $.get(`/posts/${post_id}`, { variant: "tooltip", preview: preview });
|
||||||
qtip.elements.tooltip.removeClass("post-tooltip-loading");
|
let html = await instance._request;
|
||||||
|
instance.setContent(html);
|
||||||
|
|
||||||
|
$tooltip.removeClass("tooltip-loading");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status !== 0 && error.statusText !== "abort") {
|
if (error.status !== 0 && error.statusText !== "abort") {
|
||||||
Utility.error(`Error displaying tooltip for post #${post_id} (error: ${error.status} ${error.statusText})`);
|
Utility.error(`Error displaying tooltip for post #${post_id} (error: ${error.status} ${error.statusText})`);
|
||||||
@@ -30,98 +78,19 @@ PostTooltip.render_tooltip = async function (event, qtip) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hide the tooltip the first time it is shown, while we wait on the ajax call to complete.
|
PostTooltip.on_hide = function (instance) {
|
||||||
PostTooltip.on_show = function (event, qtip) {
|
if (instance._request?.state() === "pending") {
|
||||||
if (!qtip.cache.hasBeenShown) {
|
instance._request.abort();
|
||||||
qtip.elements.tooltip.addClass("post-tooltip-loading");
|
|
||||||
qtip.cache.hasBeenShown = true;
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
PostTooltip.POST_SELECTOR = "*:not(.ui-sortable-handle) > .post-preview img, .dtext-post-id-link";
|
|
||||||
|
|
||||||
// http://qtip2.com/options
|
|
||||||
PostTooltip.QTIP_OPTIONS = {
|
|
||||||
style: {
|
|
||||||
classes: "qtip-light post-tooltip",
|
|
||||||
tip: false
|
|
||||||
},
|
|
||||||
content: PostTooltip.render_tooltip,
|
|
||||||
overwrite: false,
|
|
||||||
position: {
|
|
||||||
viewport: true,
|
|
||||||
my: "bottom left",
|
|
||||||
at: "top left",
|
|
||||||
effect: false,
|
|
||||||
adjust: {
|
|
||||||
y: -2,
|
|
||||||
method: "shift",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
show: {
|
|
||||||
solo: true,
|
|
||||||
delay: 750,
|
|
||||||
effect: false,
|
|
||||||
ready: true,
|
|
||||||
event: "mouseenter",
|
|
||||||
},
|
|
||||||
hide: {
|
|
||||||
delay: 250,
|
|
||||||
fixed: true,
|
|
||||||
effect: false,
|
|
||||||
event: "unfocus click mouseleave",
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
show: PostTooltip.on_show,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
PostTooltip.initialize = function () {
|
|
||||||
$(document).on("mouseenter.danbooru.postTooltip", PostTooltip.POST_SELECTOR, function (event) {
|
|
||||||
if (PostTooltip.disabled()) {
|
|
||||||
$(this).qtip("disable");
|
|
||||||
} else {
|
|
||||||
$(this).qtip(PostTooltip.QTIP_OPTIONS, event);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel pending ajax requests when we mouse out of the thumbnail.
|
|
||||||
$(document).on("mouseleave.danbooru.postTooltip", PostTooltip.POST_SELECTOR, function (event) {
|
|
||||||
let qtip = $(event.target).qtip("api");
|
|
||||||
|
|
||||||
if (qtip && qtip.cache && qtip.cache.request && qtip.cache.request.state() === "pending") {
|
|
||||||
qtip.cache.request.abort();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on("click.danbooru.postTooltip", ".post-tooltip-disable", PostTooltip.on_disable_tooltips);
|
|
||||||
|
|
||||||
// Hide tooltips when pressing keys or clicking thumbnails.
|
|
||||||
$(document).on("keydown.danbooru.postTooltip", PostTooltip.hide);
|
|
||||||
$(document).on("click.danbooru.postTooltip", PostTooltip.POST_SELECTOR, PostTooltip.hide);
|
|
||||||
|
|
||||||
// Disable tooltips on touch devices. https://developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent
|
|
||||||
PostTooltip.isTouching = false;
|
|
||||||
$(document).on("touchstart.danbooru.postTooltip", function (event) { PostTooltip.isTouching = true; });
|
|
||||||
$(document).on("touchend.danbooru.postTooltip", function (event) { PostTooltip.isTouching = false; });
|
|
||||||
};
|
|
||||||
|
|
||||||
PostTooltip.hide = function (event) {
|
|
||||||
// Hide on any key except control (user may be control-clicking link inside tooltip).
|
|
||||||
if (event.type === "keydown" && event.ctrlKey === true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(".post-tooltip:visible").qtip("hide");
|
|
||||||
};
|
|
||||||
|
|
||||||
PostTooltip.disabled = function (event) {
|
PostTooltip.disabled = function (event) {
|
||||||
return PostTooltip.isTouching || CurrentUser.data("disable-post-tooltips");
|
return CurrentUser.data("disable-post-tooltips");
|
||||||
};
|
};
|
||||||
|
|
||||||
PostTooltip.on_disable_tooltips = async function (event) {
|
PostTooltip.on_disable_tooltips = async function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
$(event.target).parents(".qtip").qtip("hide");
|
hideAll();
|
||||||
|
|
||||||
if (CurrentUser.data("is-anonymous")) {
|
if (CurrentUser.data("is-anonymous")) {
|
||||||
Utility.notice('You must <a href="/session/new">login</a> to disable tooltips');
|
Utility.notice('You must <a href="/session/new">login</a> to disable tooltips');
|
||||||
|
|||||||
@@ -300,10 +300,10 @@ Post.initialize_favlist = function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Post.view_original = function(e) {
|
Post.view_original = function(e = null) {
|
||||||
if (Utility.test_max_width(660)) {
|
if (Utility.test_max_width(660)) {
|
||||||
// Do the default behavior (navigate to image)
|
// Do the default behavior (navigate to image)
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var $image = $("#image");
|
var $image = $("#image");
|
||||||
@@ -316,13 +316,13 @@ Post.view_original = function(e) {
|
|||||||
});
|
});
|
||||||
Note.Box.scale_all();
|
Note.Box.scale_all();
|
||||||
$("body").attr("data-post-current-image-size", "original");
|
$("body").attr("data-post-current-image-size", "original");
|
||||||
return false;
|
e?.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
Post.view_large = function(e) {
|
Post.view_large = function(e = null) {
|
||||||
if (Utility.test_max_width(660)) {
|
if (Utility.test_max_width(660)) {
|
||||||
// Do the default behavior (navigate to image)
|
// Do the default behavior (navigate to image)
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var $image = $("#image");
|
var $image = $("#image");
|
||||||
@@ -335,7 +335,7 @@ Post.view_large = function(e) {
|
|||||||
});
|
});
|
||||||
Note.Box.scale_all();
|
Note.Box.scale_all();
|
||||||
$("body").attr("data-post-current-image-size", "large");
|
$("body").attr("data-post-current-image-size", "large");
|
||||||
return false;
|
e?.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
Post.toggle_fit_window = function(e) {
|
Post.toggle_fit_window = function(e) {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
let SavedSearch = {};
|
|
||||||
|
|
||||||
SavedSearch.initialize_all = function() {
|
|
||||||
if ($("#c-saved-searches").length) {
|
|
||||||
$("#c-saved-searches table").stupidtable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(SavedSearch.initialize_all);
|
|
||||||
|
|
||||||
export default SavedSearch
|
|
||||||
85
app/javascript/src/javascripts/user_tooltips.js
Normal file
85
app/javascript/src/javascripts/user_tooltips.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import Utility from './utility';
|
||||||
|
import { delegate } from 'tippy.js';
|
||||||
|
import 'tippy.js/dist/tippy.css';
|
||||||
|
|
||||||
|
let UserTooltip = {};
|
||||||
|
|
||||||
|
UserTooltip.SELECTOR = "*:not(.user-tooltip-name) > a.user, a.dtext-user-id-link, a.dtext-user-mention-link";
|
||||||
|
UserTooltip.SHOW_DELAY = 500;
|
||||||
|
UserTooltip.HIDE_DELAY = 125;
|
||||||
|
UserTooltip.DURATION = 250;
|
||||||
|
UserTooltip.MAX_WIDTH = 600;
|
||||||
|
|
||||||
|
UserTooltip.initialize = function () {
|
||||||
|
delegate("body", {
|
||||||
|
allowHTML: true,
|
||||||
|
appendTo: document.body,
|
||||||
|
delay: [UserTooltip.SHOW_DELAY, UserTooltip.HIDE_DELAY],
|
||||||
|
duration: UserTooltip.DURATION,
|
||||||
|
interactive: true,
|
||||||
|
maxWidth: UserTooltip.MAX_WIDTH,
|
||||||
|
target: UserTooltip.SELECTOR,
|
||||||
|
theme: "common-tooltip user-tooltip",
|
||||||
|
touch: false,
|
||||||
|
|
||||||
|
onShow: UserTooltip.on_show,
|
||||||
|
onHide: UserTooltip.on_hide,
|
||||||
|
});
|
||||||
|
|
||||||
|
delegate("body", {
|
||||||
|
allowHTML: true,
|
||||||
|
interactive: true,
|
||||||
|
theme: "common-tooltip",
|
||||||
|
target: ".user-tooltip-menu-button",
|
||||||
|
placement: "bottom",
|
||||||
|
touch: false,
|
||||||
|
trigger: "click",
|
||||||
|
content: (element) => {
|
||||||
|
return $(element).parents(".user-tooltip").find(".user-tooltip-menu").get(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
UserTooltip.on_show = async function (instance) {
|
||||||
|
let $target = $(instance.reference);
|
||||||
|
let $tooltip = $(instance.popper);
|
||||||
|
|
||||||
|
// skip if tooltip has already been rendered.
|
||||||
|
if ($tooltip.has(".user-tooltip-body").length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tooltip.addClass("tooltip-loading");
|
||||||
|
|
||||||
|
if ($target.is("a.dtext-user-id-link")) {
|
||||||
|
let user_id = /\/users\/(\d+)/.exec($target.attr("href"))[1];
|
||||||
|
instance._request = $.get(`/users/${user_id}`, { variant: "tooltip" });
|
||||||
|
} else if ($target.is("a.user")) {
|
||||||
|
let user_id = $target.attr("data-user-id");
|
||||||
|
instance._request = $.get(`/users/${user_id}`, { variant: "tooltip" });
|
||||||
|
} else if ($target.is("a.dtext-user-mention-link")) {
|
||||||
|
let user_name = $target.attr("data-user-name");
|
||||||
|
instance._request = $.get(`/users`, { name: user_name, variant: "tooltip" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = await instance._request;
|
||||||
|
instance.setContent(html);
|
||||||
|
|
||||||
|
$tooltip.removeClass("tooltip-loading");
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 0 && error.statusText !== "abort") {
|
||||||
|
Utility.error(`Error displaying tooltip (error: ${error.status} ${error.statusText})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
UserTooltip.on_hide = function (instance) {
|
||||||
|
if (instance._request?.state() === "pending") {
|
||||||
|
instance._request.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(UserTooltip.initialize);
|
||||||
|
|
||||||
|
export default UserTooltip
|
||||||
@@ -129,6 +129,14 @@ table tfoot {
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.link-plain {
|
||||||
|
color: unset;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fixed-width-container {
|
.fixed-width-container {
|
||||||
max-width: 70em;
|
max-width: 70em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
--body-background-color: white;
|
--body-background-color: white;
|
||||||
|
|
||||||
--text-color: hsl(0, 0%, 15%);
|
--text-color: hsl(0, 0%, 15%);
|
||||||
|
--inverse-text-color: white;
|
||||||
--muted-text-color: hsl(0, 0%, 55%);
|
--muted-text-color: hsl(0, 0%, 55%);
|
||||||
--header-color: hsl(0, 0%, 15%);
|
--header-color: hsl(0, 0%, 15%);
|
||||||
|
|
||||||
@@ -72,7 +73,8 @@
|
|||||||
--comment-sticky-background-color: var(--subnav-menu-background-color);
|
--comment-sticky-background-color: var(--subnav-menu-background-color);
|
||||||
|
|
||||||
--post-tooltip-background-color: var(--body-background-color);
|
--post-tooltip-background-color: var(--body-background-color);
|
||||||
--post-tooltip-border-color: #767676;
|
--post-tooltip-border-color: hsla(210, 100%, 3%, 0.15);
|
||||||
|
--post-tooltip-box-shadow: 0 4px 14px -2px hsla(210, 100%, 3%, 0.10);
|
||||||
--post-tooltip-header-background-color: var(--subnav-menu-background-color);
|
--post-tooltip-header-background-color: var(--subnav-menu-background-color);
|
||||||
--post-tooltip-info-color: #555;
|
--post-tooltip-info-color: #555;
|
||||||
--post-tooltip-scrollbar-background: #999999;
|
--post-tooltip-scrollbar-background: #999999;
|
||||||
@@ -81,6 +83,9 @@
|
|||||||
--post-tooltip-scrollbar-track-background: #EEEEEE;
|
--post-tooltip-scrollbar-track-background: #EEEEEE;
|
||||||
--post-tooltip-scrollbar-track-border: 0 none white;
|
--post-tooltip-scrollbar-track-border: 0 none white;
|
||||||
|
|
||||||
|
--user-tooltip-positive-feedback-color: orange;
|
||||||
|
--user-tooltip-negative-feedback-color: red;
|
||||||
|
|
||||||
--preview-current-post-background: rgba(0, 0, 0, 0.1);
|
--preview-current-post-background: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
--autocomplete-selected-background-color: var(--subnav-menu-background-color);
|
--autocomplete-selected-background-color: var(--subnav-menu-background-color);
|
||||||
@@ -200,6 +205,7 @@
|
|||||||
--user-platinum-color: gray;
|
--user-platinum-color: gray;
|
||||||
--user-gold-color: #00F;
|
--user-gold-color: #00F;
|
||||||
--user-member-color: var(--link-color);
|
--user-member-color: var(--link-color);
|
||||||
|
--user-banned-color: black;
|
||||||
|
|
||||||
--news-updates-background: #EEE;
|
--news-updates-background: #EEE;
|
||||||
--news-updates-border: 2px solid #666;
|
--news-updates-border: 2px solid #666;
|
||||||
@@ -260,6 +266,7 @@ body[data-current-user-theme="dark"] {
|
|||||||
|
|
||||||
/* main text colors */
|
/* main text colors */
|
||||||
--text-color: var(--grey-5);
|
--text-color: var(--grey-5);
|
||||||
|
--inverse-text-color: white;
|
||||||
--muted-text-color: var(--grey-4);
|
--muted-text-color: var(--grey-4);
|
||||||
--header-color: var(--grey-6);
|
--header-color: var(--grey-6);
|
||||||
|
|
||||||
@@ -282,6 +289,7 @@ body[data-current-user-theme="dark"] {
|
|||||||
--collection-pool-color: var(--general-tag-color);
|
--collection-pool-color: var(--general-tag-color);
|
||||||
--collection-pool-hover-color: var(--general-tag-hover-color);
|
--collection-pool-hover-color: var(--general-tag-hover-color);
|
||||||
|
|
||||||
|
--user-banned-color: var(--grey-1);
|
||||||
--user-member-color: var(--blue-1);
|
--user-member-color: var(--blue-1);
|
||||||
--user-gold-color: var(--yellow-1);
|
--user-gold-color: var(--yellow-1);
|
||||||
--user-platinum-color: var(--grey-4);
|
--user-platinum-color: var(--grey-4);
|
||||||
@@ -394,6 +402,9 @@ body[data-current-user-theme="dark"] {
|
|||||||
--post-tooltip-scrollbar-track-background: var(--grey-1);
|
--post-tooltip-scrollbar-track-background: var(--grey-1);
|
||||||
--post-tooltip-scrollbar-track-border: none;
|
--post-tooltip-scrollbar-track-border: none;
|
||||||
|
|
||||||
|
--user-tooltip-positive-feedback-color: var(--yellow-1);
|
||||||
|
--user-tooltip-negative-feedback-color: var(--red-1);
|
||||||
|
|
||||||
--preview-pending-color: var(--blue-1);
|
--preview-pending-color: var(--blue-1);
|
||||||
--preview-flagged-color: var(--red-1);
|
--preview-flagged-color: var(--red-1);
|
||||||
--preview-deleted-color: var(--grey-5);
|
--preview-deleted-color: var(--grey-5);
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ div.list-of-messages {
|
|||||||
|
|
||||||
a.message-timestamp {
|
a.message-timestamp {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--text-color);
|
font-size: 0.90em;
|
||||||
|
color: var(--muted-text-color);
|
||||||
&:hover { text-decoration: underline; }
|
&:hover { text-decoration: underline; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ form.simple_form {
|
|||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.text {
|
&.text, &.dtext {
|
||||||
.hint {
|
.hint {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
a.user-admin.with-style {
|
body[data-current-user-style-usernames="true"] {
|
||||||
color: var(--user-admin-color);
|
a.user-admin {
|
||||||
}
|
color: var(--user-admin-color);
|
||||||
|
}
|
||||||
|
|
||||||
a.user-moderator.with-style {
|
a.user-moderator {
|
||||||
color: var(--user-moderator-color);
|
color: var(--user-moderator-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.user-builder.with-style {
|
a.user-builder {
|
||||||
color: var(--user-builder-color);
|
color: var(--user-builder-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.user-platinum.with-style {
|
a.user-platinum {
|
||||||
color: var(--user-platinum-color);
|
color: var(--user-platinum-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.user-gold.with-style {
|
a.user-gold {
|
||||||
color: var(--user-gold-color);
|
color: var(--user-gold-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
a.user-member.with-style {
|
a.user-member {
|
||||||
color: var(--user-member-color);
|
color: var(--user-member-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
app/javascript/src/styles/specific/common_tooltips.scss
Normal file
51
app/javascript/src/styles/specific/common_tooltips.scss
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
div[data-tippy-root].tooltip-loading {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme~="common-tooltip"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
border: 1px solid var(--post-tooltip-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--post-tooltip-background-color);
|
||||||
|
background-clip: padding-box;
|
||||||
|
box-shadow: var(--post-tooltip-box-shadow);
|
||||||
|
|
||||||
|
/* bordered arrow styling; see https://github.com/atomiks/tippyjs/blob/master/src/scss/themes/light-border.scss */
|
||||||
|
&[data-placement^=bottom] {
|
||||||
|
> .tippy-arrow:before {
|
||||||
|
border-bottom-color: var(--post-tooltip-background-color);
|
||||||
|
bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tippy-arrow:after {
|
||||||
|
border-bottom-color: var(--post-tooltip-border-color);
|
||||||
|
border-width: 0 7px 7px;
|
||||||
|
top: -8px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-placement^=top] {
|
||||||
|
> .tippy-arrow:before {
|
||||||
|
border-top-color: var(--post-tooltip-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tippy-arrow:after {
|
||||||
|
border-top-color: var(--post-tooltip-border-color);
|
||||||
|
border-width: 7px 7px 0;
|
||||||
|
top: 17px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tippy-arrow:after {
|
||||||
|
border-color: transparent;
|
||||||
|
border-style: solid;
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
$tooltip-line-height: 16px;
|
$tooltip-line-height: 16px;
|
||||||
$tooltip-body-height: $tooltip-line-height * 6; // 6 lines high.
|
$tooltip-body-height: $tooltip-line-height * 4; // 4 lines high.
|
||||||
$tooltip-width: 164px * 3 - 10; // 3 thumbnails wide.
|
|
||||||
|
|
||||||
@mixin thin-scrollbar {
|
@mixin thin-scrollbar {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@@ -46,16 +45,13 @@ $tooltip-width: 164px * 3 - 10; // 3 thumbnails wide.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tooltip {
|
.tippy-box[data-theme~="post-tooltip"] {
|
||||||
max-width: $tooltip-width;
|
min-width: 20em;
|
||||||
min-width: $tooltip-width;
|
max-width: 40em !important;
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: $tooltip-line-height;
|
line-height: $tooltip-line-height;
|
||||||
border-color: var(--post-tooltip-border-color);
|
|
||||||
background-color: var(--post-tooltip-background-color);
|
|
||||||
|
|
||||||
.qtip-content {
|
.tippy-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@@ -85,38 +81,32 @@ $tooltip-width: 164px * 3 - 10; // 3 thumbnails wide.
|
|||||||
.post-tooltip-body-right { flex: 1; }
|
.post-tooltip-body-right { flex: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tooltip-header {
|
div.post-tooltip-header {
|
||||||
background-color: var(--post-tooltip-header-background-color);
|
background-color: var(--post-tooltip-header-background-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
align-items: center;
|
||||||
|
|
||||||
.post-tooltip-header-left {
|
.post-tooltip-info {
|
||||||
flex: 1;
|
margin-right: 0.5em;
|
||||||
|
color: var(--post-tooltip-info-color);
|
||||||
|
font-size: 10px;
|
||||||
|
flex: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tooltip-header-right {
|
a.user {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 11em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-tooltip-source {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-xs {
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-tooltip-disable {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tooltip-info {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
color: var(--post-tooltip-info-color);
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.post-tooltip-loading {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
app/javascript/src/styles/specific/privacy_policy.scss
Normal file
5
app/javascript/src/styles/specific/privacy_policy.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#c-static #a-privacy-policy {
|
||||||
|
.summary {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/javascript/src/styles/specific/user_tooltips.scss
Normal file
103
app/javascript/src/styles/specific/user_tooltips.scss
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
.tippy-box[data-theme~="user-tooltip"] {
|
||||||
|
line-height: 1.25em;
|
||||||
|
min-width: 350px;
|
||||||
|
|
||||||
|
.user-tooltip-header {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
display: grid;
|
||||||
|
grid:
|
||||||
|
"avatar header-top menu"
|
||||||
|
"avatar header-bottom menu" /
|
||||||
|
32px 1fr 32px;
|
||||||
|
column-gap: 0.25em;
|
||||||
|
|
||||||
|
.user-tooltip-avatar {
|
||||||
|
font-size: 32px;
|
||||||
|
grid-area: avatar;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tooltip-header-top {
|
||||||
|
grid-area: header-top;
|
||||||
|
|
||||||
|
.user-tooltip-badge {
|
||||||
|
color: var(--inverse-text-color);
|
||||||
|
font-size: 0.70em;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&.user-tooltip-badge-admin { background-color: var(--user-admin-color); }
|
||||||
|
&.user-tooltip-badge-moderator { background-color: var(--user-moderator-color); }
|
||||||
|
&.user-tooltip-badge-approver { background-color: var(--user-builder-color); }
|
||||||
|
&.user-tooltip-badge-contributor { background-color: var(--user-builder-color); }
|
||||||
|
&.user-tooltip-badge-builder { background-color: var(--user-builder-color); }
|
||||||
|
&.user-tooltip-badge-platinum { background-color: var(--user-platinum-color); }
|
||||||
|
&.user-tooltip-badge-gold { background-color: var(--user-gold-color); }
|
||||||
|
&.user-tooltip-badge-member { background-color: var(--user-member-color); }
|
||||||
|
&.user-tooltip-badge-banned { background-color: var(--user-banned-color); }
|
||||||
|
|
||||||
|
&.user-tooltip-badge-positive-feedback {
|
||||||
|
color: var(--user-tooltip-positive-feedback-color);
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.user-tooltip-badge-negative-feedback {
|
||||||
|
color: var(--user-tooltip-negative-feedback-color);
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tooltip-header-bottom {
|
||||||
|
grid-area: header-bottom;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.user-tooltip-menu-button {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
grid-area: menu;
|
||||||
|
align-self: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--subnav-menu-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> ul.user-tooltip-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.user-tooltip-menu {
|
||||||
|
.icon {
|
||||||
|
width: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tooltip-stats {
|
||||||
|
display: grid;
|
||||||
|
grid: auto / repeat(3, 1fr);
|
||||||
|
column-gap: 1em;
|
||||||
|
row-gap: 0.5em;
|
||||||
|
|
||||||
|
.user-tooltip-stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.user-tooltip-stat-value {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tooltip-stat-name {
|
||||||
|
font-size: 0.90em;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ class APNGInspector
|
|||||||
# if we did, file is probably maliciously formed
|
# if we did, file is probably maliciously formed
|
||||||
# fail gracefully without marking the file as corrupt
|
# fail gracefully without marking the file as corrupt
|
||||||
chunks += 1
|
chunks += 1
|
||||||
if chunks > 100000
|
if chunks > 100_000
|
||||||
iend_reached = true
|
iend_reached = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -66,7 +66,8 @@ class APNGInspector
|
|||||||
file.seek(current_pos + chunk_len + 4, IO::SEEK_SET)
|
file.seek(current_pos + chunk_len + 4, IO::SEEK_SET)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return iend_reached
|
|
||||||
|
iend_reached
|
||||||
end
|
end
|
||||||
|
|
||||||
def inspect!
|
def inspect!
|
||||||
@@ -105,6 +106,7 @@ class APNGInspector
|
|||||||
if framedata.nil? || framedata.length != 4
|
if framedata.nil? || framedata.length != 4
|
||||||
return -1
|
return -1
|
||||||
end
|
end
|
||||||
return framedata.unpack1("N".freeze)
|
|
||||||
|
framedata.unpack1("N".freeze)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module ArtistFinder
|
|||||||
SITE_BLACKLIST = [
|
SITE_BLACKLIST = [
|
||||||
"artstation.com/artist", # http://www.artstation.com/artist/serafleur/
|
"artstation.com/artist", # http://www.artstation.com/artist/serafleur/
|
||||||
"www.artstation.com", # http://www.artstation.com/serafleur/
|
"www.artstation.com", # http://www.artstation.com/serafleur/
|
||||||
%r!cdn[ab]?\.artstation\.com/p/assets/images/images!i, # https://cdna.artstation.com/p/assets/images/images/001/658/068/large/yang-waterkuma-b402.jpg?1450269769
|
%r{cdn[ab]?\.artstation\.com/p/assets/images/images}i, # https://cdna.artstation.com/p/assets/images/images/001/658/068/large/yang-waterkuma-b402.jpg?1450269769
|
||||||
"ask.fm", # http://ask.fm/mikuroko_396
|
"ask.fm", # http://ask.fm/mikuroko_396
|
||||||
"bcyimg.com",
|
"bcyimg.com",
|
||||||
"bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg
|
"bcyimg.com/drawer", # https://img9.bcyimg.com/drawer/32360/post/178vu/46229ec06e8111e79558c1b725ebc9e6.jpg
|
||||||
@@ -52,7 +52,7 @@ module ArtistFinder
|
|||||||
"hentai-foundry.com",
|
"hentai-foundry.com",
|
||||||
"hentai-foundry.com/pictures/user", # http://www.hentai-foundry.com/pictures/user/aaaninja/
|
"hentai-foundry.com/pictures/user", # http://www.hentai-foundry.com/pictures/user/aaaninja/
|
||||||
"hentai-foundry.com/user", # http://www.hentai-foundry.com/user/aaaninja/profile
|
"hentai-foundry.com/user", # http://www.hentai-foundry.com/user/aaaninja/profile
|
||||||
%r!pictures\.hentai-foundry\.com(?:/\w)?!i, # http://pictures.hentai-foundry.com/a/aaaninja/
|
%r{pictures\.hentai-foundry\.com(?:/\w)?}i, # http://pictures.hentai-foundry.com/a/aaaninja/
|
||||||
"i.imgur.com", # http://i.imgur.com/Ic9q3.jpg
|
"i.imgur.com", # http://i.imgur.com/Ic9q3.jpg
|
||||||
"instagram.com", # http://www.instagram.com/serafleur.art/
|
"instagram.com", # http://www.instagram.com/serafleur.art/
|
||||||
"iwara.tv",
|
"iwara.tv",
|
||||||
@@ -62,13 +62,15 @@ module ArtistFinder
|
|||||||
"monappy.jp",
|
"monappy.jp",
|
||||||
"monappy.jp/u", # https://monappy.jp/u/abara_bone
|
"monappy.jp/u", # https://monappy.jp/u/abara_bone
|
||||||
"mstdn.jp", # https://mstdn.jp/@oneb
|
"mstdn.jp", # https://mstdn.jp/@oneb
|
||||||
|
"www.newgrounds.com", # https://jessxjess.newgrounds.com/
|
||||||
|
"newgrounds.com/art/view/", # https://www.newgrounds.com/art/view/jessxjess/avatar-korra
|
||||||
"nicoseiga.jp",
|
"nicoseiga.jp",
|
||||||
"nicoseiga.jp/priv", # http://lohas.nicoseiga.jp/priv/2017365fb6cfbdf47ad26c7b6039feb218c5e2d4/1498430264/6820259
|
"nicoseiga.jp/priv", # http://lohas.nicoseiga.jp/priv/2017365fb6cfbdf47ad26c7b6039feb218c5e2d4/1498430264/6820259
|
||||||
"nicovideo.jp",
|
"nicovideo.jp",
|
||||||
"nicovideo.jp/user", # http://www.nicovideo.jp/user/317609
|
"nicovideo.jp/user", # http://www.nicovideo.jp/user/317609
|
||||||
"nicovideo.jp/user/illust", # http://seiga.nicovideo.jp/user/illust/29075429
|
"nicovideo.jp/user/illust", # http://seiga.nicovideo.jp/user/illust/29075429
|
||||||
"nijie.info", # http://nijie.info/members.php?id=15235
|
"nijie.info", # http://nijie.info/members.php?id=15235
|
||||||
%r!nijie\.info/nijie_picture!i, # http://pic03.nijie.info/nijie_picture/32243_20150609224803_0.png
|
%r{nijie\.info/nijie_picture}i, # http://pic03.nijie.info/nijie_picture/32243_20150609224803_0.png
|
||||||
"patreon.com", # http://patreon.com/serafleur
|
"patreon.com", # http://patreon.com/serafleur
|
||||||
"pawoo.net", # https://pawoo.net/@148nasuka
|
"pawoo.net", # https://pawoo.net/@148nasuka
|
||||||
"pawoo.net/web/accounts", # https://pawoo.net/web/accounts/228341
|
"pawoo.net/web/accounts", # https://pawoo.net/web/accounts/228341
|
||||||
@@ -120,7 +122,7 @@ module ArtistFinder
|
|||||||
|
|
||||||
SITE_BLACKLIST_REGEXP = Regexp.union(SITE_BLACKLIST.map do |domain|
|
SITE_BLACKLIST_REGEXP = Regexp.union(SITE_BLACKLIST.map do |domain|
|
||||||
domain = Regexp.escape(domain) if domain.is_a?(String)
|
domain = Regexp.escape(domain) if domain.is_a?(String)
|
||||||
%r!\Ahttps?://(?:[a-zA-Z0-9_-]+\.)*#{domain}/\z!i
|
%r{\Ahttps?://(?:[a-zA-Z0-9_-]+\.)*#{domain}/\z}i
|
||||||
end)
|
end)
|
||||||
|
|
||||||
def find_artists(url)
|
def find_artists(url)
|
||||||
@@ -128,7 +130,7 @@ module ArtistFinder
|
|||||||
artists = []
|
artists = []
|
||||||
|
|
||||||
while artists.empty? && url.size > 10
|
while artists.empty? && url.size > 10
|
||||||
u = url.sub(/\/+$/, "") + "/"
|
u = url.sub(%r{/+$}, "") + "/"
|
||||||
u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%'
|
u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%'
|
||||||
artists += Artist.joins(:urls).where(["artists.is_deleted = FALSE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all
|
artists += Artist.joins(:urls).where(["artists.is_deleted = FALSE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all
|
||||||
url = File.dirname(url) + "/"
|
url = File.dirname(url) + "/"
|
||||||
|
|||||||
@@ -148,8 +148,6 @@ class BulkUpdateRequestProcessor
|
|||||||
end.join("\n")
|
end.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def self.is_tag_move_allowed?(antecedent_name, consequent_name)
|
def self.is_tag_move_allowed?(antecedent_name, consequent_name)
|
||||||
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
|
antecedent_tag = Tag.find_by_name(Tag.normalize_name(antecedent_name))
|
||||||
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name))
|
consequent_tag = Tag.find_by_name(Tag.normalize_name(consequent_name))
|
||||||
|
|||||||
@@ -9,15 +9,6 @@ class CloudflareService
|
|||||||
api_token.present? && zone.present?
|
api_token.present? && zone.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def ips(expiry: 24.hours)
|
|
||||||
response = Danbooru::Http.cache(expiry).get("https://api.cloudflare.com/client/v4/ips")
|
|
||||||
return [] if response.code != 200
|
|
||||||
|
|
||||||
result = response.parse["result"]
|
|
||||||
ips = result["ipv4_cidrs"] + result["ipv6_cidrs"]
|
|
||||||
ips.map { |ip| IPAddr.new(ip) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def purge_cache(urls)
|
def purge_cache(urls)
|
||||||
return unless enabled?
|
return unless enabled?
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module HasBitFlags
|
|||||||
end
|
end
|
||||||
|
|
||||||
define_method("#{attribute}=") do |val|
|
define_method("#{attribute}=") do |val|
|
||||||
if val.to_s =~ /t|1|y/
|
if val.to_s =~ /[t1y]/
|
||||||
send("#{field}=", send(field) | bit_flag)
|
send("#{field}=", send(field) | bit_flag)
|
||||||
else
|
else
|
||||||
send("#{field}=", send(field) & ~bit_flag)
|
send("#{field}=", send(field) & ~bit_flag)
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ module Mentionable
|
|||||||
# - user_field
|
# - user_field
|
||||||
def mentionable(options = {})
|
def mentionable(options = {})
|
||||||
@mentionable_options = options
|
@mentionable_options = options
|
||||||
|
|
||||||
message_field = mentionable_option(:message_field)
|
|
||||||
after_save :queue_mention_messages
|
after_save :queue_mention_messages
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ module Searchable
|
|||||||
def where_array_count(attr, value)
|
def where_array_count(attr, value)
|
||||||
qualified_column = "cardinality(#{qualified_column_for(attr)})"
|
qualified_column = "cardinality(#{qualified_column_for(attr)})"
|
||||||
range = PostQueryBuilder.new(nil).parse_range(value, :integer)
|
range = PostQueryBuilder.new(nil).parse_range(value, :integer)
|
||||||
where_operator("cardinality(#{qualified_column_for(attr)})", *range)
|
where_operator(qualified_column, *range)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_boolean_attribute(attribute, params)
|
def search_boolean_attribute(attribute, params)
|
||||||
@@ -170,7 +170,7 @@ module Searchable
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def search_text_attribute(attr, params, **options)
|
def search_text_attribute(attr, params)
|
||||||
if params[attr].present?
|
if params[attr].present?
|
||||||
where(attr => params[attr])
|
where(attr => params[attr])
|
||||||
elsif params[:"#{attr}_eq"].present?
|
elsif params[:"#{attr}_eq"].present?
|
||||||
@@ -279,7 +279,8 @@ module Searchable
|
|||||||
return find_ordered(parse_ids[1])
|
return find_ordered(parse_ids[1])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return default_order
|
|
||||||
|
default_order
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_order
|
def default_order
|
||||||
|
|||||||
@@ -24,15 +24,6 @@ class CurrentUser
|
|||||||
scoped(user, &block)
|
scoped(user, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.as_system(&block)
|
|
||||||
if block_given?
|
|
||||||
scoped(::User.system, "127.0.0.1", &block)
|
|
||||||
else
|
|
||||||
self.user = User.system
|
|
||||||
self.ip_addr = "127.0.0.1"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.user
|
def self.user
|
||||||
RequestStore[:current_user]
|
RequestStore[:current_user]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class DText
|
|||||||
html = DTextRagel.parse(text, **options)
|
html = DTextRagel.parse(text, **options)
|
||||||
html = postprocess(html, *data)
|
html = postprocess(html, *data)
|
||||||
html
|
html
|
||||||
rescue DTextRagel::Error => e
|
rescue DTextRagel::Error
|
||||||
""
|
""
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ class DText
|
|||||||
fragment = Nokogiri::HTML.fragment(html)
|
fragment = Nokogiri::HTML.fragment(html)
|
||||||
|
|
||||||
titles = fragment.css("a.dtext-wiki-link").map do |node|
|
titles = fragment.css("a.dtext-wiki-link").map do |node|
|
||||||
title = node["href"][%r!\A/wiki_pages/(.*)\z!i, 1]
|
title = node["href"][%r{\A/wiki_pages/(.*)\z}i, 1]
|
||||||
title = CGI.unescape(title)
|
title = CGI.unescape(title)
|
||||||
title = WikiPage.normalize_title(title)
|
title = WikiPage.normalize_title(title)
|
||||||
title
|
title
|
||||||
@@ -163,7 +163,7 @@ class DText
|
|||||||
string = string.dup
|
string = string.dup
|
||||||
|
|
||||||
string.gsub!(/\s*\[#{tag}\](?!\])\s*/mi, "\n\n[#{tag}]\n\n")
|
string.gsub!(/\s*\[#{tag}\](?!\])\s*/mi, "\n\n[#{tag}]\n\n")
|
||||||
string.gsub!(/\s*\[\/#{tag}\]\s*/mi, "\n\n[/#{tag}]\n\n")
|
string.gsub!(%r{\s*\[/#{tag}\]\s*}mi, "\n\n[/#{tag}]\n\n")
|
||||||
string.gsub!(/(?:\r?\n){3,}/, "\n\n")
|
string.gsub!(/(?:\r?\n){3,}/, "\n\n")
|
||||||
string.strip!
|
string.strip!
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ class DText
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
text = text.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "")
|
text.gsub(/\A[[:space:]]+|[[:space:]]+\z/, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.from_html(text, inline: false, &block)
|
def self.from_html(text, inline: false, &block)
|
||||||
|
|||||||
@@ -1,17 +1,49 @@
|
|||||||
|
require "danbooru/http/html_adapter"
|
||||||
|
require "danbooru/http/xml_adapter"
|
||||||
|
require "danbooru/http/cache"
|
||||||
|
require "danbooru/http/redirector"
|
||||||
|
require "danbooru/http/retriable"
|
||||||
|
require "danbooru/http/session"
|
||||||
|
require "danbooru/http/spoof_referrer"
|
||||||
|
require "danbooru/http/unpolish_cloudflare"
|
||||||
|
|
||||||
module Danbooru
|
module Danbooru
|
||||||
class Http
|
class Http
|
||||||
DEFAULT_TIMEOUT = 3
|
class DownloadError < StandardError; end
|
||||||
|
class FileTooLargeError < StandardError; end
|
||||||
|
|
||||||
attr_writer :cache, :http
|
DEFAULT_TIMEOUT = 10
|
||||||
|
MAX_REDIRECTS = 5
|
||||||
|
|
||||||
|
attr_accessor :max_size, :http
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
delegate :get, :post, :delete, :cache, :auth, :basic_auth, :headers, to: :new
|
delegate :get, :head, :put, :post, :delete, :cache, :follow, :max_size, :timeout, :auth, :basic_auth, :headers, :cookies, :use, :public_only, :download_media, to: :new
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@http ||=
|
||||||
|
::Danbooru::Http::ApplicationClient.new
|
||||||
|
.timeout(DEFAULT_TIMEOUT)
|
||||||
|
.headers("Accept-Encoding" => "gzip")
|
||||||
|
.headers("User-Agent": "#{Danbooru.config.canonical_app_name}/#{Rails.application.config.x.git_hash}")
|
||||||
|
.use(:auto_inflate)
|
||||||
|
.use(redirector: { max_redirects: MAX_REDIRECTS })
|
||||||
|
.use(:session)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(url, **options)
|
def get(url, **options)
|
||||||
request(:get, url, **options)
|
request(:get, url, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def head(url, **options)
|
||||||
|
request(:head, url, **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put(url, **options)
|
||||||
|
request(:get, url, **options)
|
||||||
|
end
|
||||||
|
|
||||||
def post(url, **options)
|
def post(url, **options)
|
||||||
request(:post, url, **options)
|
request(:post, url, **options)
|
||||||
end
|
end
|
||||||
@@ -20,8 +52,16 @@ module Danbooru
|
|||||||
request(:delete, url, **options)
|
request(:delete, url, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache(expiry)
|
def follow(*args)
|
||||||
dup.tap { |o| o.cache = expiry.to_i }
|
dup.tap { |o| o.http = o.http.follow(*args) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_size(size)
|
||||||
|
dup.tap { |o| o.max_size = size }
|
||||||
|
end
|
||||||
|
|
||||||
|
def timeout(*args)
|
||||||
|
dup.tap { |o| o.http = o.http.timeout(*args) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth(*args)
|
def auth(*args)
|
||||||
@@ -36,36 +76,66 @@ module Danbooru
|
|||||||
dup.tap { |o| o.http = o.http.headers(*args) }
|
dup.tap { |o| o.http = o.http.headers(*args) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cookies(*args)
|
||||||
|
dup.tap { |o| o.http = o.http.cookies(*args) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def use(*args)
|
||||||
|
dup.tap { |o| o.http = o.http.use(*args) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache(expires_in)
|
||||||
|
use(cache: { expires_in: expires_in })
|
||||||
|
end
|
||||||
|
|
||||||
|
# allow requests only to public IPs, not to local or private networks.
|
||||||
|
def public_only
|
||||||
|
dup.tap do |o|
|
||||||
|
o.http = o.http.dup.tap do |http|
|
||||||
|
http.default_options = http.default_options.with_socket_class(ValidatingSocket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
concerning :DownloadMethods do
|
||||||
|
def download_media(url, file: Tempfile.new("danbooru-download-", binmode: true))
|
||||||
|
response = get(url)
|
||||||
|
|
||||||
|
raise DownloadError, "Downloading #{response.uri} failed with code #{response.status}" if response.status != 200
|
||||||
|
raise FileTooLargeError, response if @max_size && response.content_length.to_i > @max_size
|
||||||
|
|
||||||
|
size = 0
|
||||||
|
response.body.each do |chunk|
|
||||||
|
size += chunk.size
|
||||||
|
raise FileTooLargeError if @max_size && size > @max_size
|
||||||
|
file.write(chunk)
|
||||||
|
end
|
||||||
|
|
||||||
|
file.rewind
|
||||||
|
[response, MediaFile.open(file)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def request(method, url, **options)
|
def request(method, url, **options)
|
||||||
if @cache.present?
|
|
||||||
cached_request(method, url, **options)
|
|
||||||
else
|
|
||||||
raw_request(method, url, **options)
|
|
||||||
end
|
|
||||||
rescue HTTP::TimeoutError
|
|
||||||
# return a synthetic http error on connection timeouts
|
|
||||||
::HTTP::Response.new(status: 522, body: "", version: "1.1")
|
|
||||||
end
|
|
||||||
|
|
||||||
def cached_request(method, url, **options)
|
|
||||||
key = Cache.hash({ method: method, url: url, headers: http.default_options.headers.to_h, **options }.to_json)
|
|
||||||
|
|
||||||
cached_response = Cache.get(key, @cache) do
|
|
||||||
response = raw_request(method, url, **options)
|
|
||||||
{ status: response.status, body: response.to_s, headers: response.headers.to_h, version: "1.1" }
|
|
||||||
end
|
|
||||||
|
|
||||||
::HTTP::Response.new(**cached_response)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raw_request(method, url, **options)
|
|
||||||
http.send(method, url, **options)
|
http.send(method, url, **options)
|
||||||
|
rescue OpenSSL::SSL::SSLError
|
||||||
|
fake_response(590, "")
|
||||||
|
rescue ValidatingSocket::ProhibitedIpError
|
||||||
|
fake_response(591, "")
|
||||||
|
rescue HTTP::Redirector::TooManyRedirectsError
|
||||||
|
fake_response(596, "")
|
||||||
|
rescue HTTP::TimeoutError
|
||||||
|
fake_response(597, "")
|
||||||
|
rescue HTTP::ConnectionError
|
||||||
|
fake_response(598, "")
|
||||||
|
rescue HTTP::Error
|
||||||
|
fake_response(599, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
def http
|
def fake_response(status, body)
|
||||||
@http ||= ::HTTP.timeout(DEFAULT_TIMEOUT).use(:auto_inflate).headers(Danbooru.config.http_headers).headers("Accept-Encoding" => "gzip")
|
::HTTP::Response.new(status: status, version: "1.1", body: ::HTTP::Response::Body.new(body))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
31
app/logical/danbooru/http/application_client.rb
Normal file
31
app/logical/danbooru/http/application_client.rb
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# An extension to HTTP::Client that lets us write Rack-style middlewares that
|
||||||
|
# hook into the request/response cycle and override how requests are made. This
|
||||||
|
# works by extending http.rb's concept of features (HTTP::Feature) to give them
|
||||||
|
# a `perform` method that takes a http request and returns a http response.
|
||||||
|
# This can be used to intercept and modify requests and return arbitrary responses.
|
||||||
|
|
||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class ApplicationClient < HTTP::Client
|
||||||
|
# Override `perform` to call the `perform` method on features first.
|
||||||
|
def perform(request, options)
|
||||||
|
features = options.features.values.reverse.select do |feature|
|
||||||
|
feature.respond_to?(:perform)
|
||||||
|
end
|
||||||
|
|
||||||
|
perform = proc { |req| super(req, options) }
|
||||||
|
callback_chain = features.reduce(perform) do |callback_chain, feature|
|
||||||
|
proc { |req| feature.perform(req, &callback_chain) }
|
||||||
|
end
|
||||||
|
|
||||||
|
callback_chain.call(request)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Override `branch` to return an ApplicationClient instead of a
|
||||||
|
# HTTP::Client so that chaining works.
|
||||||
|
def branch(...)
|
||||||
|
ApplicationClient.new(...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
30
app/logical/danbooru/http/cache.rb
Normal file
30
app/logical/danbooru/http/cache.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class Cache < HTTP::Feature
|
||||||
|
HTTP::Options.register_feature :cache, self
|
||||||
|
|
||||||
|
attr_reader :expires_in
|
||||||
|
|
||||||
|
def initialize(expires_in:)
|
||||||
|
@expires_in = expires_in
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(request, &block)
|
||||||
|
::Cache.get(cache_key(request), expires_in) do
|
||||||
|
response = yield request
|
||||||
|
|
||||||
|
# XXX hack to remove connection state from response body so we can serialize it for caching.
|
||||||
|
response.flush
|
||||||
|
response.body.instance_variable_set(:@connection, nil)
|
||||||
|
response.body.instance_variable_set(:@stream, nil)
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_key(request)
|
||||||
|
"http:" + ::Cache.hash({ method: request.verb, url: request.uri.to_s, headers: request.headers.sort }.to_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
app/logical/danbooru/http/html_adapter.rb
Normal file
12
app/logical/danbooru/http/html_adapter.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class HtmlAdapter < HTTP::MimeType::Adapter
|
||||||
|
HTTP::MimeType.register_adapter "text/html", self
|
||||||
|
HTTP::MimeType.register_alias "text/html", :html
|
||||||
|
|
||||||
|
def decode(str)
|
||||||
|
Nokogiri::HTML5(str)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
40
app/logical/danbooru/http/redirector.rb
Normal file
40
app/logical/danbooru/http/redirector.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# A HTTP::Feature that automatically follows HTTP redirects.
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
|
||||||
|
|
||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class Redirector < HTTP::Feature
|
||||||
|
HTTP::Options.register_feature :redirector, self
|
||||||
|
|
||||||
|
attr_reader :max_redirects
|
||||||
|
|
||||||
|
def initialize(max_redirects: 5)
|
||||||
|
@max_redirects = max_redirects
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(request, &block)
|
||||||
|
response = yield request
|
||||||
|
|
||||||
|
redirects = max_redirects
|
||||||
|
while response.status.redirect?
|
||||||
|
raise HTTP::Redirector::TooManyRedirectsError if redirects <= 0
|
||||||
|
|
||||||
|
response = yield build_redirect(request, response)
|
||||||
|
redirects -= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_redirect(request, response)
|
||||||
|
location = response.headers["Location"].to_s
|
||||||
|
uri = HTTP::URI.parse(location)
|
||||||
|
|
||||||
|
verb = request.verb
|
||||||
|
verb = :get if response.status == 303 && !request.verb.in?([:get, :head])
|
||||||
|
|
||||||
|
request.redirect(uri, verb)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
54
app/logical/danbooru/http/retriable.rb
Normal file
54
app/logical/danbooru/http/retriable.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# A HTTP::Feature that automatically retries requests that return a 429 error
|
||||||
|
# or a Retry-After header. Usage: `Danbooru::Http.use(:retriable).get(url)`.
|
||||||
|
#
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
|
|
||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class Retriable < HTTP::Feature
|
||||||
|
HTTP::Options.register_feature :retriable, self
|
||||||
|
|
||||||
|
attr_reader :max_retries, :max_delay
|
||||||
|
|
||||||
|
def initialize(max_retries: 2, max_delay: 5.seconds)
|
||||||
|
@max_retries = max_retries
|
||||||
|
@max_delay = max_delay
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(request, &block)
|
||||||
|
response = yield request
|
||||||
|
|
||||||
|
retries = max_retries
|
||||||
|
while retriable?(response) && retries > 0 && retry_delay(response) <= max_delay
|
||||||
|
DanbooruLogger.info "Retrying url=#{request.uri} status=#{response.status} retries=#{retries} delay=#{retry_delay(response)}"
|
||||||
|
|
||||||
|
retries -= 1
|
||||||
|
sleep(retry_delay(response))
|
||||||
|
response = yield request
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def retriable?(response)
|
||||||
|
response.status == 429 || response.headers["Retry-After"].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def retry_delay(response, current_time: Time.zone.now)
|
||||||
|
retry_after = response.headers["Retry-After"]
|
||||||
|
|
||||||
|
if retry_after.blank?
|
||||||
|
0.seconds
|
||||||
|
elsif retry_after =~ /\A\d+\z/
|
||||||
|
retry_after.to_i.seconds
|
||||||
|
else
|
||||||
|
retry_at = Time.zone.parse(retry_after)
|
||||||
|
return 0.seconds if retry_at.blank?
|
||||||
|
|
||||||
|
[retry_at - current_time, 0].max.seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
37
app/logical/danbooru/http/session.rb
Normal file
37
app/logical/danbooru/http/session.rb
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class Session < HTTP::Feature
|
||||||
|
HTTP::Options.register_feature :session, self
|
||||||
|
|
||||||
|
attr_reader :cookie_jar
|
||||||
|
|
||||||
|
def initialize(cookie_jar: HTTP::CookieJar.new)
|
||||||
|
@cookie_jar = cookie_jar
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(request)
|
||||||
|
add_cookies(request)
|
||||||
|
response = yield request
|
||||||
|
save_cookies(response)
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_cookies(request)
|
||||||
|
cookies = cookies_for_request(request)
|
||||||
|
request.headers["Cookie"] = cookies if cookies.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cookies_for_request(request)
|
||||||
|
saved_cookies = cookie_jar.each(request.uri).map { |c| [c.name, c.value] }.to_h
|
||||||
|
request_cookies = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"].to_s)
|
||||||
|
saved_cookies.merge(request_cookies).map { |name, value| "#{name}=#{value}" }.join("; ")
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_cookies(response)
|
||||||
|
response.cookies.each do |cookie|
|
||||||
|
cookie_jar.add(cookie)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
13
app/logical/danbooru/http/spoof_referrer.rb
Normal file
13
app/logical/danbooru/http/spoof_referrer.rb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class SpoofReferrer < HTTP::Feature
|
||||||
|
HTTP::Options.register_feature :spoof_referrer, self
|
||||||
|
|
||||||
|
def perform(request, &block)
|
||||||
|
request.headers["Referer"] = request.uri.origin unless request.headers["Referer"].present?
|
||||||
|
response = yield request
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
20
app/logical/danbooru/http/unpolish_cloudflare.rb
Normal file
20
app/logical/danbooru/http/unpolish_cloudflare.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Bypass Cloudflare Polish (https://support.cloudflare.com/hc/en-us/articles/360000607372-Using-Cloudflare-Polish-to-compress-images)
|
||||||
|
|
||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class UnpolishCloudflare < HTTP::Feature
|
||||||
|
HTTP::Options.register_feature :unpolish_cloudflare, self
|
||||||
|
|
||||||
|
def perform(request, &block)
|
||||||
|
response = yield request
|
||||||
|
|
||||||
|
if response.headers["CF-Polished"].present?
|
||||||
|
request.uri.query_values = request.uri.query_values.to_h.merge(danbooru_no_polish: SecureRandom.uuid)
|
||||||
|
response = yield request
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
app/logical/danbooru/http/xml_adapter.rb
Normal file
12
app/logical/danbooru/http/xml_adapter.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module Danbooru
|
||||||
|
class Http
|
||||||
|
class XmlAdapter < HTTP::MimeType::Adapter
|
||||||
|
HTTP::MimeType.register_adapter "application/xml", self
|
||||||
|
HTTP::MimeType.register_alias "application/xml", :xml
|
||||||
|
|
||||||
|
def decode(str)
|
||||||
|
Hash.from_xml(str).with_indifferent_access
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,14 +2,13 @@ module DanbooruMaintenance
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def hourly
|
def hourly
|
||||||
|
safely { Upload.prune! }
|
||||||
end
|
end
|
||||||
|
|
||||||
def daily
|
def daily
|
||||||
safely { PostPruner.new.prune! }
|
safely { PostPruner.new.prune! }
|
||||||
safely { Upload.prune! }
|
|
||||||
safely { Delayed::Job.where('created_at < ?', 45.days.ago).delete_all }
|
safely { Delayed::Job.where('created_at < ?', 45.days.ago).delete_all }
|
||||||
safely { PostDisapproval.prune! }
|
safely { PostDisapproval.prune! }
|
||||||
safely { PostDisapproval.dmail_messages! }
|
|
||||||
safely { regenerate_post_counts! }
|
safely { regenerate_post_counts! }
|
||||||
safely { TokenBucket.prune! }
|
safely { TokenBucket.prune! }
|
||||||
safely { BulkUpdateRequestPruner.warn_old }
|
safely { BulkUpdateRequestPruner.warn_old }
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
require 'resolv'
|
|
||||||
|
|
||||||
module Downloads
|
|
||||||
class File
|
|
||||||
include ActiveModel::Validations
|
|
||||||
class Error < StandardError; end
|
|
||||||
|
|
||||||
RETRIABLE_ERRORS = [Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EIO, Errno::EHOSTUNREACH, Errno::ECONNREFUSED, Timeout::Error, IOError]
|
|
||||||
|
|
||||||
delegate :data, to: :strategy
|
|
||||||
attr_reader :url, :referer
|
|
||||||
|
|
||||||
validate :validate_url
|
|
||||||
|
|
||||||
def initialize(url, referer = nil)
|
|
||||||
@url = Addressable::URI.parse(url) rescue nil
|
|
||||||
@referer = referer
|
|
||||||
validate!
|
|
||||||
end
|
|
||||||
|
|
||||||
def size
|
|
||||||
res = HTTParty.head(uncached_url, **httparty_options, timeout: 3)
|
|
||||||
|
|
||||||
if res.success?
|
|
||||||
res.content_length
|
|
||||||
else
|
|
||||||
raise HTTParty::ResponseError.new(res)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def download!(url: uncached_url, tries: 3, **options)
|
|
||||||
Retriable.retriable(on: RETRIABLE_ERRORS, tries: tries, base_interval: 0) do
|
|
||||||
file = http_get_streaming(url, headers: strategy.headers, **options)
|
|
||||||
return [file, strategy]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_url
|
|
||||||
errors[:base] << "URL must not be blank" if url.blank?
|
|
||||||
errors[:base] << "'#{url}' is not a valid url" if !url.host.present?
|
|
||||||
errors[:base] << "'#{url}' is not a valid url. Did you mean 'http://#{url}'?" if !url.scheme.in?(%w[http https])
|
|
||||||
end
|
|
||||||
|
|
||||||
def http_get_streaming(url, file: Tempfile.new(binmode: true), headers: {}, max_size: Danbooru.config.max_file_size)
|
|
||||||
size = 0
|
|
||||||
|
|
||||||
res = HTTParty.get(url, httparty_options) do |chunk|
|
|
||||||
next if chunk.code == 302
|
|
||||||
|
|
||||||
size += chunk.size
|
|
||||||
raise Error.new("File is too large (max size: #{max_size})") if size > max_size && max_size > 0
|
|
||||||
|
|
||||||
file.write(chunk)
|
|
||||||
end
|
|
||||||
|
|
||||||
if res.success?
|
|
||||||
file.rewind
|
|
||||||
return file
|
|
||||||
else
|
|
||||||
raise Error.new("HTTP error code: #{res.code} #{res.message}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Prevent Cloudflare from potentially mangling the image. See issue #3528.
|
|
||||||
def uncached_url
|
|
||||||
return file_url unless is_cloudflare?(file_url)
|
|
||||||
|
|
||||||
url = file_url.dup
|
|
||||||
url.query_values = url.query_values.to_h.merge(danbooru_no_cache: SecureRandom.uuid)
|
|
||||||
url
|
|
||||||
end
|
|
||||||
|
|
||||||
def preview_url
|
|
||||||
@preview_url ||= Addressable::URI.parse(strategy.preview_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_url
|
|
||||||
@file_url ||= Addressable::URI.parse(strategy.image_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
def strategy
|
|
||||||
@strategy ||= Sources::Strategies.find(url.to_s, referer)
|
|
||||||
end
|
|
||||||
|
|
||||||
def httparty_options
|
|
||||||
{
|
|
||||||
timeout: 10,
|
|
||||||
stream_body: true,
|
|
||||||
headers: strategy.headers,
|
|
||||||
connection_adapter: ValidatingConnectionAdapter
|
|
||||||
}.deep_merge(Danbooru.config.httparty_options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def is_cloudflare?(url)
|
|
||||||
ip_addr = IPAddr.new(Resolv.getaddress(url.hostname))
|
|
||||||
CloudflareService.new.ips.any? { |subnet| subnet.include?(ip_addr) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.banned_ip?(ip)
|
|
||||||
ip = IPAddress.parse(ip.to_s) unless ip.is_a?(IPAddress)
|
|
||||||
|
|
||||||
if ip.ipv4?
|
|
||||||
ip.loopback? || ip.link_local? || ip.multicast? || ip.private?
|
|
||||||
elsif ip.ipv6?
|
|
||||||
ip.loopback? || ip.link_local? || ip.unique_local? || ip.unspecified?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Hook into HTTParty to validate the IP before following redirects.
|
|
||||||
# https://www.rubydoc.info/github/jnunemaker/httparty/HTTParty/ConnectionAdapter
|
|
||||||
class ValidatingConnectionAdapter < HTTParty::ConnectionAdapter
|
|
||||||
def self.call(uri, options)
|
|
||||||
ip_addr = IPAddress.parse(::Resolv.getaddress(uri.hostname))
|
|
||||||
|
|
||||||
if Downloads::File.banned_ip?(ip_addr)
|
|
||||||
raise Downloads::File::Error, "Downloads from #{ip_addr} are not allowed"
|
|
||||||
end
|
|
||||||
|
|
||||||
super(uri, options)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
30
app/logical/dtext_input.rb
Normal file
30
app/logical/dtext_input.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# A custom SimpleForm input for DText fields.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
#
|
||||||
|
# <%= f.input :body, as: :dtext %>
|
||||||
|
# <%= f.input :reason, as: :dtext, inline: true %>
|
||||||
|
#
|
||||||
|
# https://github.com/heartcombo/simple_form/wiki/Custom-inputs-examples
|
||||||
|
# https://github.com/heartcombo/simple_form/blob/master/lib/simple_form/inputs/string_input.rb
|
||||||
|
# https://github.com/heartcombo/simple_form/blob/master/lib/simple_form/inputs/text_input.rb
|
||||||
|
|
||||||
|
class DtextInput < SimpleForm::Inputs::Base
|
||||||
|
enable :placeholder, :maxlength, :minlength
|
||||||
|
|
||||||
|
def input(wrapper_options)
|
||||||
|
t = template
|
||||||
|
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||||
|
|
||||||
|
t.tag.div(class: "dtext-previewable") do
|
||||||
|
if options[:inline]
|
||||||
|
t.concat @builder.text_field(attribute_name, merged_input_options)
|
||||||
|
else
|
||||||
|
t.concat @builder.text_area(attribute_name, { rows: 20, cols: 30 }.merge(merged_input_options))
|
||||||
|
end
|
||||||
|
|
||||||
|
t.concat t.tag.div(id: "dtext-preview", class: "dtext-preview prose")
|
||||||
|
t.concat t.tag.span(t.link_to("Formatting help", t.dtext_help_path, remote: true, method: :get), class: "hint dtext-hint")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
class ImageProxy
|
class ImageProxy
|
||||||
|
class Error < StandardError; end
|
||||||
|
|
||||||
def self.needs_proxy?(url)
|
def self.needs_proxy?(url)
|
||||||
fake_referer_for(url).present?
|
fake_referer_for(url).present?
|
||||||
end
|
end
|
||||||
@@ -8,19 +10,13 @@ class ImageProxy
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.get_image(url)
|
def self.get_image(url)
|
||||||
if url.blank?
|
raise Error, "URL not present" unless url.present?
|
||||||
raise "Must specify url"
|
raise Error, "Proxy not allowed for this url (url=#{url})" unless needs_proxy?(url)
|
||||||
end
|
|
||||||
|
|
||||||
if !needs_proxy?(url)
|
referer = fake_referer_for(url)
|
||||||
raise "Proxy not allowed for this site"
|
response = Danbooru::Http.timeout(30).headers(Referer: referer).get(url)
|
||||||
end
|
raise Error, "Couldn't proxy image (code=#{response.status}, url=#{url})" unless response.status.success?
|
||||||
|
|
||||||
response = HTTParty.get(url, Danbooru.config.httparty_options.deep_merge(headers: {"Referer" => fake_referer_for(url)}))
|
response
|
||||||
if response.success?
|
|
||||||
return response
|
|
||||||
else
|
|
||||||
raise "HTTP error code: #{response.code} #{response.message}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class IpLookup
|
|||||||
end
|
end
|
||||||
|
|
||||||
def info
|
def info
|
||||||
return {} unless api_key.present?
|
return {} if api_key.blank?
|
||||||
response = Danbooru::Http.cache(cache_duration).get("https://api.ipregistry.co/#{ip_addr}?key=#{api_key}")
|
response = Danbooru::Http.cache(cache_duration).get("https://api.ipregistry.co/#{ip_addr}?key=#{api_key}")
|
||||||
return {} if response.status != 200
|
return {} if response.status != 200
|
||||||
json = response.parse.deep_symbolize_keys.with_indifferent_access
|
json = response.parse.deep_symbolize_keys.with_indifferent_access
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
class IqdbProxy
|
class IqdbProxy
|
||||||
class Error < StandardError; end
|
class Error < StandardError; end
|
||||||
|
attr_reader :http, :iqdbs_server
|
||||||
|
|
||||||
def self.enabled?
|
def initialize(http: Danbooru::Http.new, iqdbs_server: Danbooru.config.iqdbs_server)
|
||||||
Danbooru.config.iqdbs_server.present?
|
@iqdbs_server = iqdbs_server
|
||||||
|
@http = http
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.download(url, type)
|
def enabled?
|
||||||
download = Downloads::File.new(url)
|
iqdbs_server.present?
|
||||||
file, strategy = download.download!(url: download.send(type))
|
end
|
||||||
|
|
||||||
|
def download(url, type)
|
||||||
|
strategy = Sources::Strategies.find(url)
|
||||||
|
download_url = strategy.send(type)
|
||||||
|
file = strategy.download_file!(download_url)
|
||||||
file
|
file
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.search(params)
|
def search(params)
|
||||||
raise NotImplementedError, "the IQDBs service isn't configured" unless enabled?
|
|
||||||
|
|
||||||
limit = params[:limit]&.to_i&.clamp(1, 1000) || 20
|
limit = params[:limit]&.to_i&.clamp(1, 1000) || 20
|
||||||
similarity = params[:similarity]&.to_f&.clamp(0.0, 100.0) || 0.0
|
similarity = params[:similarity]&.to_f&.clamp(0.0, 100.0) || 0.0
|
||||||
high_similarity = params[:high_similarity]&.to_f&.clamp(0.0, 100.0) || 65.0
|
high_similarity = params[:high_similarity]&.to_f&.clamp(0.0, 100.0) || 65.0
|
||||||
@@ -28,7 +33,7 @@ class IqdbProxy
|
|||||||
file = download(params[:image_url], :url)
|
file = download(params[:image_url], :url)
|
||||||
results = query(file: file, limit: limit)
|
results = query(file: file, limit: limit)
|
||||||
elsif params[:file_url].present?
|
elsif params[:file_url].present?
|
||||||
file = download(params[:file_url], :file_url)
|
file = download(params[:file_url], :image_url)
|
||||||
results = query(file: file, limit: limit)
|
results = query(file: file, limit: limit)
|
||||||
elsif params[:post_id].present?
|
elsif params[:post_id].present?
|
||||||
url = Post.find(params[:post_id]).preview_file_url
|
url = Post.find(params[:post_id]).preview_file_url
|
||||||
@@ -46,15 +51,21 @@ class IqdbProxy
|
|||||||
file.try(:close)
|
file.try(:close)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.query(params)
|
def query(file: nil, url: nil, limit: 20)
|
||||||
response = HTTParty.post("#{Danbooru.config.iqdbs_server}/similar", body: params, **Danbooru.config.httparty_options)
|
raise NotImplementedError, "the IQDBs service isn't configured" unless enabled?
|
||||||
raise Error, "IQDB error: #{response.code} #{response.message}" unless response.success?
|
|
||||||
raise Error, "IQDB error: #{response.parsed_response["error"]}" if response.parsed_response.is_a?(Hash)
|
file = HTTP::FormData::File.new(file) if file
|
||||||
raise Error, "IQDB error: #{response.parsed_response.first}" if response.parsed_response.try(:first).is_a?(String)
|
form = { file: file, url: url, limit: limit }.compact
|
||||||
response.parsed_response
|
response = http.timeout(30).post("#{iqdbs_server}/similar", form: form)
|
||||||
|
|
||||||
|
raise Error, "IQDB error: #{response.status}" if response.status != 200
|
||||||
|
raise Error, "IQDB error: #{response.parse["error"]}" if response.parse.is_a?(Hash)
|
||||||
|
raise Error, "IQDB error: #{response.parse.first}" if response.parse.try(:first).is_a?(String)
|
||||||
|
|
||||||
|
response.parse
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.decorate_posts(json)
|
def decorate_posts(json)
|
||||||
post_ids = json.map { |match| match["post_id"] }
|
post_ids = json.map { |match| match["post_id"] }
|
||||||
posts = Post.where(id: post_ids).group_by(&:id).transform_values(&:first)
|
posts = Post.where(id: post_ids).group_by(&:id).transform_values(&:first)
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class MediaFile
|
|||||||
else
|
else
|
||||||
:bin
|
:bin
|
||||||
end
|
end
|
||||||
|
rescue EOFError
|
||||||
|
:bin
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.videos_enabled?
|
def self.videos_enabled?
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class MediaFile::Flash < MediaFile
|
|||||||
signature = contents[0..2]
|
signature = contents[0..2]
|
||||||
|
|
||||||
# SWF version
|
# SWF version
|
||||||
version = contents[3].unpack('C').join.to_i
|
_version = contents[3].unpack('C').join.to_i
|
||||||
|
|
||||||
# Determine the length of the uncompressed stream
|
# Determine the length of the uncompressed stream
|
||||||
length = contents[4..7].unpack('V').join.to_i
|
length = contents[4..7].unpack('V').join.to_i
|
||||||
@@ -50,7 +50,7 @@ class MediaFile::Flash < MediaFile
|
|||||||
# If we do, in fact, have compression
|
# If we do, in fact, have compression
|
||||||
if signature == 'CWS'
|
if signature == 'CWS'
|
||||||
# Decompress the body of the SWF
|
# Decompress the body of the SWF
|
||||||
body = Zlib::Inflate.inflate( contents[8..length] )
|
body = Zlib::Inflate.inflate(contents[8..length])
|
||||||
|
|
||||||
# And reconstruct the stream contents to the first 8 bytes (header)
|
# And reconstruct the stream contents to the first 8 bytes (header)
|
||||||
# Plus our decompressed body
|
# Plus our decompressed body
|
||||||
@@ -58,10 +58,10 @@ class MediaFile::Flash < MediaFile
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Determine the nbits of our dimensions rectangle
|
# Determine the nbits of our dimensions rectangle
|
||||||
nbits = contents.unpack('C'*contents.length)[8] >> 3
|
nbits = contents.unpack('C' * contents.length)[8] >> 3
|
||||||
|
|
||||||
# Determine how many bits long this entire RECT structure is
|
# Determine how many bits long this entire RECT structure is
|
||||||
rectbits = 5 + nbits * 4 # 5 bits for nbits, as well as nbits * number of fields (4)
|
rectbits = 5 + nbits * 4 # 5 bits for nbits, as well as nbits * number of fields (4)
|
||||||
|
|
||||||
# Determine how many bytes rectbits composes (ceil(rectbits/8))
|
# Determine how many bytes rectbits composes (ceil(rectbits/8))
|
||||||
rectbytes = (rectbits.to_f / 8).ceil
|
rectbytes = (rectbits.to_f / 8).ceil
|
||||||
@@ -70,11 +70,11 @@ class MediaFile::Flash < MediaFile
|
|||||||
rect = contents[8..(8 + rectbytes)].unpack("#{'B8' * rectbytes}").join
|
rect = contents[8..(8 + rectbytes)].unpack("#{'B8' * rectbytes}").join
|
||||||
|
|
||||||
# Read in nbits incremenets starting from 5
|
# Read in nbits incremenets starting from 5
|
||||||
dimensions = Array.new
|
dimensions = []
|
||||||
4.times do |n|
|
4.times do |n|
|
||||||
s = 5 + (n * nbits) # Calculate our start index
|
s = 5 + (n * nbits) # Calculate our start index
|
||||||
e = s + (nbits - 1) # Calculate our end index
|
e = s + (nbits - 1) # Calculate our end index
|
||||||
dimensions[n] = rect[s..e].to_i(2) # Read that range (binary) and convert it to an integer
|
dimensions[n] = rect[s..e].to_i(2) # Read that range (binary) and convert it to an integer
|
||||||
end
|
end
|
||||||
|
|
||||||
# The values we have here are in "twips"
|
# The values we have here are in "twips"
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# queries reportbooru to find missed post searches
|
|
||||||
class MissedSearchService
|
|
||||||
def self.enabled?
|
|
||||||
Danbooru.config.reportbooru_server.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
if !MissedSearchService.enabled?
|
|
||||||
raise NotImplementedError.new("the Reportbooru service isn't configured. Missed searches are not available.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def each_search(&block)
|
|
||||||
fetch_data.scan(/(.+?) (\d+)\.0\n/).each(&block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_data
|
|
||||||
Cache.get("ms", 1.minute) do
|
|
||||||
url = URI.parse("#{Danbooru.config.reportbooru_server}/missed_searches")
|
|
||||||
response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 6))
|
|
||||||
if response.success?
|
|
||||||
response = response.body
|
|
||||||
else
|
|
||||||
response = ""
|
|
||||||
end
|
|
||||||
response.force_encoding("utf-8")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,83 +1,102 @@
|
|||||||
class NicoSeigaApiClient
|
class NicoSeigaApiClient
|
||||||
extend Memoist
|
extend Memoist
|
||||||
BASE_URL = "http://seiga.nicovideo.jp/api"
|
XML_API = "https://seiga.nicovideo.jp/api"
|
||||||
attr_reader :illust_id
|
|
||||||
|
|
||||||
def self.agent
|
attr_reader :http
|
||||||
mech = Mechanize.new
|
|
||||||
mech.redirect_ok = false
|
|
||||||
mech.keep_alive = false
|
|
||||||
|
|
||||||
session = Cache.get("nico-seiga-session")
|
def initialize(work_id:, type:, http: Danbooru::Http.new)
|
||||||
if session
|
@work_id = work_id
|
||||||
cookie = Mechanize::Cookie.new("user_session", session)
|
@work_type = type
|
||||||
cookie.domain = ".nicovideo.jp"
|
@http = http
|
||||||
cookie.path = "/"
|
end
|
||||||
mech.cookie_jar.add(cookie)
|
|
||||||
else
|
def image_ids
|
||||||
mech.get("https://account.nicovideo.jp/login") do |page|
|
if @work_type == "illust"
|
||||||
page.form_with(:id => "login_form") do |form|
|
[api_response["id"]]
|
||||||
form["mail_tel"] = Danbooru.config.nico_seiga_login
|
elsif @work_type == "manga"
|
||||||
form["password"] = Danbooru.config.nico_seiga_password
|
manga_api_response.map do |x|
|
||||||
end.click_button
|
case x["meta"]["source_url"]
|
||||||
end
|
when %r{/thumb/(\d+)\w}i then Regexp.last_match(1)
|
||||||
session = mech.cookie_jar.cookies.select {|c| c.name == "user_session"}.first
|
when %r{nicoseiga\.cdn\.nimg\.jp/drm/image/\w+/(\d+)\w}i then Regexp.last_match(1)
|
||||||
if session
|
end
|
||||||
Cache.put("nico-seiga-session", session.value, 1.week)
|
|
||||||
else
|
|
||||||
raise "Session not found"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# This cookie needs to be set to allow viewing of adult works
|
|
||||||
cookie = Mechanize::Cookie.new("skip_fetish_warning", "1")
|
|
||||||
cookie.domain = "seiga.nicovideo.jp"
|
|
||||||
cookie.path = "/"
|
|
||||||
mech.cookie_jar.add(cookie)
|
|
||||||
|
|
||||||
mech.redirect_ok = true
|
|
||||||
mech
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(illust_id:, user_id: nil)
|
|
||||||
@illust_id = illust_id
|
|
||||||
@user_id = user_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def image_id
|
|
||||||
illust_xml["response"]["image"]["id"].to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_id
|
|
||||||
@user_id || illust_xml["response"]["image"]["user_id"].to_i
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def title
|
def title
|
||||||
illust_xml["response"]["image"]["title"]
|
api_response["title"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def desc
|
def description
|
||||||
illust_xml["response"]["image"]["description"]
|
api_response["description"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def moniker
|
def tags
|
||||||
artist_xml["response"]["user"]["nickname"]
|
api_response.dig("tag_list", "tag").to_a.map { |t| t["name"] }.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def illust_xml
|
def user_id
|
||||||
get("#{BASE_URL}/illust/info?id=#{illust_id}")
|
api_response["user_id"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def artist_xml
|
def user_name
|
||||||
get("#{BASE_URL}/user/info?id=#{user_id}")
|
if @work_type == "illust"
|
||||||
|
api_response["nickname"]
|
||||||
|
elsif @work_type == "manga"
|
||||||
|
user_api_response(user_id)["nickname"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def api_response
|
||||||
|
if @work_type == "illust"
|
||||||
|
resp = get("https://sp.seiga.nicovideo.jp/ajax/seiga/im#{@work_id}")
|
||||||
|
return {} if resp.blank? || resp.code.to_i == 404
|
||||||
|
api_response = JSON.parse(resp)["target_image"]
|
||||||
|
|
||||||
|
elsif @work_type == "manga"
|
||||||
|
resp = http.cache(1.minute).get("#{XML_API}/theme/info?id=#{@work_id}")
|
||||||
|
return {} if resp.blank? || resp.code.to_i == 404
|
||||||
|
api_response = Hash.from_xml(resp.to_s)["response"]["theme"]
|
||||||
|
end
|
||||||
|
|
||||||
|
api_response || {}
|
||||||
|
rescue JSON::ParserError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
def manga_api_response
|
||||||
|
resp = get("https://ssl.seiga.nicovideo.jp/api/v1/app/manga/episodes/#{@work_id}/frames")
|
||||||
|
return {} if resp.blank? || resp.code.to_i == 404
|
||||||
|
JSON.parse(resp)["data"]["result"]
|
||||||
|
rescue JSON::ParserError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_api_response(user_id)
|
||||||
|
resp = http.cache(1.minute).get("#{XML_API}/user/info?id=#{user_id}")
|
||||||
|
return {} if resp.blank? || resp.code.to_i == 404
|
||||||
|
Hash.from_xml(resp.to_s)["response"]["user"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def login
|
||||||
|
form = {
|
||||||
|
mail_tel: Danbooru.config.nico_seiga_login,
|
||||||
|
password: Danbooru.config.nico_seiga_password
|
||||||
|
}
|
||||||
|
|
||||||
|
# XXX should fail gracefully instead of raising exception
|
||||||
|
resp = http.cache(1.hour).post("https://account.nicovideo.jp/login/redirector?site=seiga", form: form)
|
||||||
|
raise RuntimeError, "NicoSeiga login failed (status=#{resp.status} url=#{url})" if resp.status != 200
|
||||||
|
|
||||||
|
http
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(url)
|
def get(url)
|
||||||
response = Danbooru::Http.cache(1.minute).get(url)
|
resp = login.cache(1.minute).get(url)
|
||||||
raise "nico seiga api call failed (code=#{response.code}, body=#{response.body})" if response.code != 200
|
#raise RuntimeError, "NicoSeiga get failed (status=#{resp.status} url=#{url})" if resp.status != 200
|
||||||
|
|
||||||
Hash.from_xml(response.to_s)
|
resp
|
||||||
end
|
end
|
||||||
|
|
||||||
memoize :artist_xml, :illust_xml
|
memoize :api_response, :manga_api_response, :user_api_response
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
class NicoSeigaMangaApiClient
|
|
||||||
extend Memoist
|
|
||||||
BASE_URL = "https://seiga.nicovideo.jp/api"
|
|
||||||
attr_reader :theme_id
|
|
||||||
|
|
||||||
def initialize(theme_id)
|
|
||||||
@theme_id = theme_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_id
|
|
||||||
theme_info_xml["response"]["theme"]["user_id"].to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
def title
|
|
||||||
theme_info_xml["response"]["theme"]["title"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def desc
|
|
||||||
theme_info_xml["response"]["theme"]["description"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def moniker
|
|
||||||
artist_xml["response"]["user"]["nickname"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def image_ids
|
|
||||||
images = theme_data_xml["response"]["image_list"]["image"]
|
|
||||||
images = [images] unless images.is_a?(Array)
|
|
||||||
images.map {|x| x["id"]}
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
|
||||||
theme_info_xml["response"]["theme"]["tag_list"]["tag"].map {|x| x["name"]}
|
|
||||||
end
|
|
||||||
|
|
||||||
def theme_data_xml
|
|
||||||
uri = "#{BASE_URL}/theme/data?theme_id=#{theme_id}"
|
|
||||||
body = NicoSeigaApiClient.agent.get(uri).body
|
|
||||||
Hash.from_xml(body)
|
|
||||||
end
|
|
||||||
|
|
||||||
def theme_info_xml
|
|
||||||
uri = "#{BASE_URL}/theme/info?id=#{theme_id}"
|
|
||||||
body = NicoSeigaApiClient.agent.get(uri).body
|
|
||||||
Hash.from_xml(body)
|
|
||||||
end
|
|
||||||
|
|
||||||
def artist_xml
|
|
||||||
get("#{BASE_URL}/user/info?id=#{user_id}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def get(url)
|
|
||||||
response = Danbooru::Http.cache(1.minute).get(url)
|
|
||||||
raise "nico seiga api call failed (code=#{response.code}, body=#{response.body})" if response.code != 200
|
|
||||||
|
|
||||||
Hash.from_xml(response.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
memoize :theme_data_xml, :theme_info_xml, :artist_xml
|
|
||||||
end
|
|
||||||
@@ -3,9 +3,9 @@ module PaginationExtension
|
|||||||
|
|
||||||
attr_accessor :current_page, :records_per_page, :paginator_count, :paginator_mode
|
attr_accessor :current_page, :records_per_page, :paginator_count, :paginator_mode
|
||||||
|
|
||||||
def paginate(page, limit: nil, count: nil, search_count: nil)
|
def paginate(page, limit: nil, max_limit: 1000, count: nil, search_count: nil)
|
||||||
@records_per_page = limit || Danbooru.config.posts_per_page
|
@records_per_page = limit || Danbooru.config.posts_per_page
|
||||||
@records_per_page = @records_per_page.to_i.clamp(1, 1000)
|
@records_per_page = @records_per_page.to_i.clamp(1, max_limit)
|
||||||
|
|
||||||
if count.present?
|
if count.present?
|
||||||
@paginator_count = count
|
@paginator_count = count
|
||||||
@@ -61,27 +61,35 @@ module PaginationExtension
|
|||||||
end
|
end
|
||||||
|
|
||||||
def prev_page
|
def prev_page
|
||||||
return nil if is_first_page?
|
if is_first_page?
|
||||||
|
nil
|
||||||
if paginator_mode == :numbered
|
elsif paginator_mode == :numbered
|
||||||
current_page - 1
|
current_page - 1
|
||||||
elsif paginator_mode == :sequential_before
|
elsif paginator_mode == :sequential_before && records.present?
|
||||||
"a#{records.first.id}"
|
"a#{records.first.id}"
|
||||||
elsif paginator_mode == :sequential_after
|
elsif paginator_mode == :sequential_after && records.present?
|
||||||
"b#{records.last.id}"
|
"b#{records.last.id}"
|
||||||
|
else
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
rescue ActiveRecord::QueryCanceled
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_page
|
def next_page
|
||||||
return nil if is_last_page?
|
if is_last_page?
|
||||||
|
nil
|
||||||
if paginator_mode == :numbered
|
elsif paginator_mode == :numbered
|
||||||
current_page + 1
|
current_page + 1
|
||||||
elsif paginator_mode == :sequential_before
|
elsif paginator_mode == :sequential_before && records.present?
|
||||||
"b#{records.last.id}"
|
"b#{records.last.id}"
|
||||||
elsif paginator_mode == :sequential_after
|
elsif paginator_mode == :sequential_after && records.present?
|
||||||
"a#{records.first.id}"
|
"a#{records.first.id}"
|
||||||
|
else
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
rescue ActiveRecord::QueryCanceled
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# XXX Hack: in sequential pagination we fetch one more record than we
|
# XXX Hack: in sequential pagination we fetch one more record than we
|
||||||
@@ -106,10 +114,7 @@ module PaginationExtension
|
|||||||
def total_count
|
def total_count
|
||||||
@paginator_count ||= unscoped.from(except(:offset, :limit, :order).reorder(nil)).count
|
@paginator_count ||= unscoped.from(except(:offset, :limit, :order).reorder(nil)).count
|
||||||
rescue ActiveRecord::StatementInvalid => e
|
rescue ActiveRecord::StatementInvalid => e
|
||||||
if e.to_s =~ /statement timeout/
|
raise unless e.to_s =~ /statement timeout/
|
||||||
@paginator_count ||= 1_000_000
|
@paginator_count ||= 1_000_000
|
||||||
else
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -128,15 +128,15 @@ class PawooApiClient
|
|||||||
rescue
|
rescue
|
||||||
data = {}
|
data = {}
|
||||||
end
|
end
|
||||||
return Account.new(data)
|
Account.new(data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_access_token
|
def fetch_access_token
|
||||||
raise MissingConfigurationError.new("missing pawoo client id") if Danbooru.config.pawoo_client_id.nil?
|
raise MissingConfigurationError, "missing pawoo client id" if Danbooru.config.pawoo_client_id.nil?
|
||||||
raise MissingConfigurationError.new("missing pawoo client secret") if Danbooru.config.pawoo_client_secret.nil?
|
raise MissingConfigurationError, "missing pawoo client secret" if Danbooru.config.pawoo_client_secret.nil?
|
||||||
|
|
||||||
Cache.get("pawoo-token") do
|
Cache.get("pawoo-token") do
|
||||||
result = client.client_credentials.get_token
|
result = client.client_credentials.get_token
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
require 'resolv-replace'
|
|
||||||
|
|
||||||
class PixivApiClient
|
class PixivApiClient
|
||||||
extend Memoist
|
extend Memoist
|
||||||
|
|
||||||
@@ -114,66 +112,13 @@ class PixivApiClient
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class FanboxResponse
|
|
||||||
attr_reader :json
|
|
||||||
|
|
||||||
def initialize(json)
|
|
||||||
@json = json
|
|
||||||
end
|
|
||||||
|
|
||||||
def name
|
|
||||||
json["body"]["user"]["name"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_id
|
|
||||||
json["body"]["user"]["userId"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def moniker
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
def page_count
|
|
||||||
json["body"]["body"]["images"].size
|
|
||||||
end
|
|
||||||
|
|
||||||
def artist_commentary_title
|
|
||||||
json["body"]["title"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def artist_commentary_desc
|
|
||||||
json["body"]["body"]["text"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
|
|
||||||
def pages
|
|
||||||
if json["body"]["body"]
|
|
||||||
json["body"]["body"]["images"].map {|x| x["originalUrl"]}
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def work(illust_id)
|
def work(illust_id)
|
||||||
headers = Danbooru.config.http_headers.merge(
|
params = { image_sizes: "large", include_stats: "true" }
|
||||||
"Referer" => "http://www.pixiv.net",
|
|
||||||
"Content-Type" => "application/x-www-form-urlencoded",
|
|
||||||
"Authorization" => "Bearer #{access_token}"
|
|
||||||
)
|
|
||||||
params = {
|
|
||||||
"image_sizes" => "large",
|
|
||||||
"include_stats" => "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/works/#{illust_id.to_i}.json"
|
url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/works/#{illust_id.to_i}.json"
|
||||||
response = Danbooru::Http.cache(1.minute).headers(headers).get(url, params: params)
|
response = api_client.cache(1.minute).get(url, params: params)
|
||||||
json = response.parse
|
json = response.parse
|
||||||
|
|
||||||
if response.code == 200
|
if response.status == 200
|
||||||
WorkResponse.new(json["response"][0])
|
WorkResponse.new(json["response"][0])
|
||||||
elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/
|
elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/
|
||||||
raise BadIDError.new("Pixiv ##{illust_id} not found: work was deleted, made private, or ID is invalid.")
|
raise BadIDError.new("Pixiv ##{illust_id} not found: work was deleted, made private, or ID is invalid.")
|
||||||
@@ -184,32 +129,12 @@ class PixivApiClient
|
|||||||
raise Error.new("Pixiv API call failed (status=#{response.code} body=#{response.body})")
|
raise Error.new("Pixiv API call failed (status=#{response.code} body=#{response.body})")
|
||||||
end
|
end
|
||||||
|
|
||||||
def fanbox(fanbox_id)
|
|
||||||
url = "https://www.pixiv.net/ajax/fanbox/post?postId=#{fanbox_id.to_i}"
|
|
||||||
resp = agent.get(url)
|
|
||||||
json = JSON.parse(resp.body)
|
|
||||||
if resp.code == "200"
|
|
||||||
FanboxResponse.new(json)
|
|
||||||
elsif json["status"] == "failure"
|
|
||||||
raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})")
|
|
||||||
end
|
|
||||||
rescue JSON::ParserError
|
|
||||||
raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})")
|
|
||||||
end
|
|
||||||
|
|
||||||
def novel(novel_id)
|
def novel(novel_id)
|
||||||
headers = Danbooru.config.http_headers.merge(
|
|
||||||
"Referer" => "http://www.pixiv.net",
|
|
||||||
"Content-Type" => "application/x-www-form-urlencoded",
|
|
||||||
"Authorization" => "Bearer #{access_token}"
|
|
||||||
)
|
|
||||||
|
|
||||||
url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/novels/#{novel_id.to_i}.json"
|
url = "https://public-api.secure.pixiv.net/v#{API_VERSION}/novels/#{novel_id.to_i}.json"
|
||||||
resp = HTTParty.get(url, Danbooru.config.httparty_options.deep_merge(headers: headers))
|
resp = api_client.cache(1.minute).get(url)
|
||||||
body = resp.body.force_encoding("utf-8")
|
json = resp.parse
|
||||||
json = JSON.parse(body)
|
|
||||||
|
|
||||||
if resp.success?
|
if resp.status == 200
|
||||||
NovelResponse.new(json["response"][0])
|
NovelResponse.new(json["response"][0])
|
||||||
elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/
|
elsif json["status"] == "failure" && json.dig("errors", "system", "message") =~ /対象のイラストは見つかりませんでした。/
|
||||||
raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})")
|
raise Error.new("Pixiv API call failed (status=#{resp.code} body=#{body})")
|
||||||
@@ -219,42 +144,41 @@ class PixivApiClient
|
|||||||
end
|
end
|
||||||
|
|
||||||
def access_token
|
def access_token
|
||||||
Cache.get("pixiv-papi-access-token", 3000) do
|
# truncate timestamp to 1-hour resolution so that it doesn't break caching.
|
||||||
access_token = nil
|
client_time = Time.zone.now.utc.change(min: 0).rfc3339
|
||||||
|
client_hash = Digest::MD5.hexdigest(client_time + CLIENT_HASH_SALT)
|
||||||
|
|
||||||
client_time = Time.now.rfc3339
|
headers = {
|
||||||
client_hash = Digest::MD5.hexdigest(client_time + CLIENT_HASH_SALT)
|
"Referer": "http://www.pixiv.net",
|
||||||
|
"X-Client-Time": client_time,
|
||||||
|
"X-Client-Hash": client_hash
|
||||||
|
}
|
||||||
|
|
||||||
headers = {
|
params = {
|
||||||
"Referer": "http://www.pixiv.net",
|
username: Danbooru.config.pixiv_login,
|
||||||
"X-Client-Time": client_time,
|
password: Danbooru.config.pixiv_password,
|
||||||
"X-Client-Hash": client_hash
|
grant_type: "password",
|
||||||
}
|
client_id: CLIENT_ID,
|
||||||
params = {
|
client_secret: CLIENT_SECRET
|
||||||
username: Danbooru.config.pixiv_login,
|
}
|
||||||
password: Danbooru.config.pixiv_password,
|
|
||||||
grant_type: "password",
|
|
||||||
client_id: CLIENT_ID,
|
|
||||||
client_secret: CLIENT_SECRET
|
|
||||||
}
|
|
||||||
url = "https://oauth.secure.pixiv.net/auth/token"
|
|
||||||
|
|
||||||
resp = HTTParty.post(url, Danbooru.config.httparty_options.deep_merge(body: params, headers: headers))
|
resp = http.headers(headers).cache(1.hour).post("https://oauth.secure.pixiv.net/auth/token", form: params)
|
||||||
body = resp.body.force_encoding("utf-8")
|
return nil unless resp.status == 200
|
||||||
|
|
||||||
if resp.success?
|
resp.parse.dig("response", "access_token")
|
||||||
json = JSON.parse(body)
|
|
||||||
access_token = json["response"]["access_token"]
|
|
||||||
else
|
|
||||||
raise Error.new("Pixiv API access token call failed (status=#{resp.code} body=#{body})")
|
|
||||||
end
|
|
||||||
|
|
||||||
access_token
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent
|
def api_client
|
||||||
PixivWebAgent.build
|
http.headers(
|
||||||
|
"Referer": "http://www.pixiv.net",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Authorization": "Bearer #{access_token}"
|
||||||
|
)
|
||||||
end
|
end
|
||||||
memoize :agent
|
|
||||||
|
def http
|
||||||
|
Danbooru::Http.new
|
||||||
|
end
|
||||||
|
|
||||||
|
memoize :access_token, :api_client, :http
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
class PixivWebAgent
|
|
||||||
SESSION_CACHE_KEY = "pixiv-phpsessid"
|
|
||||||
COMIC_SESSION_CACHE_KEY = "pixiv-comicsessid"
|
|
||||||
SESSION_COOKIE_KEY = "PHPSESSID"
|
|
||||||
COMIC_SESSION_COOKIE_KEY = "_pixiv-comic_session"
|
|
||||||
|
|
||||||
def self.phpsessid(agent)
|
|
||||||
agent.cookies.select { |cookie| cookie.name == SESSION_COOKIE_KEY }.first.try(:value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.build
|
|
||||||
mech = Mechanize.new
|
|
||||||
mech.keep_alive = false
|
|
||||||
|
|
||||||
phpsessid = Cache.get(SESSION_CACHE_KEY)
|
|
||||||
comicsessid = Cache.get(COMIC_SESSION_CACHE_KEY)
|
|
||||||
|
|
||||||
if phpsessid
|
|
||||||
cookie = Mechanize::Cookie.new(SESSION_COOKIE_KEY, phpsessid)
|
|
||||||
cookie.domain = ".pixiv.net"
|
|
||||||
cookie.path = "/"
|
|
||||||
mech.cookie_jar.add(cookie)
|
|
||||||
|
|
||||||
if comicsessid
|
|
||||||
cookie = Mechanize::Cookie.new(COMIC_SESSION_COOKIE_KEY, comicsessid)
|
|
||||||
cookie.domain = ".pixiv.net"
|
|
||||||
cookie.path = "/"
|
|
||||||
mech.cookie_jar.add(cookie)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
headers = {
|
|
||||||
"Origin" => "https://accounts.pixiv.net",
|
|
||||||
"Referer" => "https://accounts.pixiv.net/login?lang=en^source=pc&view_type=page&ref=wwwtop_accounts_index"
|
|
||||||
}
|
|
||||||
|
|
||||||
params = {
|
|
||||||
pixiv_id: Danbooru.config.pixiv_login,
|
|
||||||
password: Danbooru.config.pixiv_password,
|
|
||||||
captcha: nil,
|
|
||||||
g_captcha_response: nil,
|
|
||||||
source: "pc",
|
|
||||||
post_key: nil
|
|
||||||
}
|
|
||||||
|
|
||||||
mech.get("https://accounts.pixiv.net/login?lang=en&source=pc&view_type=page&ref=wwwtop_accounts_index") do |page|
|
|
||||||
json = page.search("input#init-config").first.attr("value")
|
|
||||||
if json =~ /pixivAccount\.postKey":"([a-f0-9]+)/
|
|
||||||
params[:post_key] = $1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
mech.post("https://accounts.pixiv.net/api/login?lang=en", params, headers)
|
|
||||||
if mech.current_page.body =~ /"error":false/
|
|
||||||
cookie = mech.cookies.select {|x| x.name == SESSION_COOKIE_KEY}.first
|
|
||||||
if cookie
|
|
||||||
Cache.put(SESSION_CACHE_KEY, cookie.value, 1.week)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
mech.get("https://comic.pixiv.net") do |page|
|
|
||||||
cookie = mech.cookies.select {|x| x.name == COMIC_SESSION_COOKIE_KEY}.first
|
|
||||||
if cookie
|
|
||||||
Cache.put(COMIC_SESSION_CACHE_KEY, cookie.value, 1.week)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue Net::HTTPServiceUnavailable
|
|
||||||
# ignore
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
mech
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# queries reportbooru to find popular post searches
|
|
||||||
class PopularSearchService
|
|
||||||
attr_reader :date
|
|
||||||
|
|
||||||
def self.enabled?
|
|
||||||
Danbooru.config.reportbooru_server.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(date)
|
|
||||||
if !PopularSearchService.enabled?
|
|
||||||
raise NotImplementedError.new("the Reportbooru service isn't configured. Popular searches are not available.")
|
|
||||||
end
|
|
||||||
|
|
||||||
@date = date
|
|
||||||
end
|
|
||||||
|
|
||||||
def each_search(limit = 100, &block)
|
|
||||||
JSON.parse(fetch_data.to_s).slice(0, limit).each(&block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
|
||||||
JSON.parse(fetch_data.to_s).map {|x| x[0]}
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_data
|
|
||||||
return [] unless self.class.enabled?
|
|
||||||
|
|
||||||
dates = date.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
data = Cache.get("ps-day-#{dates}", 1.minute) do
|
|
||||||
url = "#{Danbooru.config.reportbooru_server}/post_searches/rank?date=#{dates}"
|
|
||||||
response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 3))
|
|
||||||
if response.success?
|
|
||||||
response = response.body
|
|
||||||
else
|
|
||||||
response = "[]"
|
|
||||||
end
|
|
||||||
response
|
|
||||||
end.to_s.force_encoding("utf-8")
|
|
||||||
|
|
||||||
if data.blank? || data == "[]"
|
|
||||||
dates = date.yesterday.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
data = Cache.get("ps-day-#{dates}", 1.minute) do
|
|
||||||
url = "#{Danbooru.config.reportbooru_server}/post_searches/rank?date=#{dates}"
|
|
||||||
response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 3))
|
|
||||||
if response.success?
|
|
||||||
response = response.body
|
|
||||||
else
|
|
||||||
response = "[]"
|
|
||||||
end
|
|
||||||
response
|
|
||||||
end.to_s.force_encoding("utf-8")
|
|
||||||
end
|
|
||||||
|
|
||||||
data
|
|
||||||
rescue StandardError => e
|
|
||||||
DanbooruLogger.log(e)
|
|
||||||
return []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -307,6 +307,8 @@ class PostQueryBuilder
|
|||||||
Post.where(parent: nil)
|
Post.where(parent: nil)
|
||||||
when "any"
|
when "any"
|
||||||
Post.where.not(parent: nil)
|
Post.where.not(parent: nil)
|
||||||
|
when /pending|flagged|modqueue|deleted|banned|active|unmoderated/
|
||||||
|
Post.where.not(parent: nil).where(parent: status_matches(parent))
|
||||||
when /\A\d+\z/
|
when /\A\d+\z/
|
||||||
Post.where(id: parent).or(Post.where(parent: parent))
|
Post.where(id: parent).or(Post.where(parent: parent))
|
||||||
else
|
else
|
||||||
@@ -320,6 +322,8 @@ class PostQueryBuilder
|
|||||||
Post.where(has_children: false)
|
Post.where(has_children: false)
|
||||||
when "any"
|
when "any"
|
||||||
Post.where(has_children: true)
|
Post.where(has_children: true)
|
||||||
|
when /pending|flagged|modqueue|deleted|banned|active|unmoderated/
|
||||||
|
Post.where(has_children: true).where(children: status_matches(child))
|
||||||
else
|
else
|
||||||
Post.none
|
Post.none
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ module PostSets
|
|||||||
end
|
end
|
||||||
|
|
||||||
def wiki_page
|
def wiki_page
|
||||||
return nil unless tag.present? && tag.wiki_page.present?
|
return nil unless normalized_query.has_single_tag?
|
||||||
return nil unless !tag.wiki_page.is_deleted?
|
@wiki_page ||= WikiPage.undeleted.find_by(title: normalized_query.tags.first.name)
|
||||||
tag.wiki_page
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag
|
def tag
|
||||||
@@ -77,7 +76,11 @@ module PostSets
|
|||||||
end
|
end
|
||||||
|
|
||||||
def per_page
|
def per_page
|
||||||
(@per_page || query.find_metatag(:limit) || CurrentUser.user.per_page).to_i.clamp(0, MAX_PER_PAGE)
|
(@per_page || query.find_metatag(:limit) || CurrentUser.user.per_page).to_i.clamp(0, max_per_page)
|
||||||
|
end
|
||||||
|
|
||||||
|
def max_per_page
|
||||||
|
(format == "sitemap") ? 10_000 : MAX_PER_PAGE
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_random?
|
def is_random?
|
||||||
@@ -94,7 +97,7 @@ module PostSets
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_random_posts
|
def get_random_posts
|
||||||
per_page.times.inject([]) do |all, x|
|
per_page.times.inject([]) do |all, _|
|
||||||
all << ::Post.user_tag_match(tag_string).random
|
all << ::Post.user_tag_match(tag_string).random
|
||||||
end.compact.uniq
|
end.compact.uniq
|
||||||
end
|
end
|
||||||
@@ -104,15 +107,15 @@ module PostSets
|
|||||||
@post_count = get_post_count
|
@post_count = get_post_count
|
||||||
|
|
||||||
if is_random?
|
if is_random?
|
||||||
temp = get_random_posts
|
get_random_posts
|
||||||
else
|
else
|
||||||
temp = normalized_query.build.paginate(page, count: post_count, search_count: !post_count.nil?, limit: per_page)
|
normalized_query.build.paginate(page, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def hide_from_crawler?
|
def hide_from_crawler?
|
||||||
return true if current_page > 1
|
return true if current_page > 50
|
||||||
return false if query.is_empty_search? || query.is_simple_tag? || query.is_metatag?(:order, :rank)
|
return false if query.is_empty_search? || query.is_simple_tag? || query.is_metatag?(:order, :rank)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
@@ -160,20 +163,16 @@ module PostSets
|
|||||||
elsif query.is_metatag?(:search)
|
elsif query.is_metatag?(:search)
|
||||||
saved_search_tags
|
saved_search_tags
|
||||||
elsif query.is_empty_search? || query.is_metatag?(:order, :rank)
|
elsif query.is_empty_search? || query.is_metatag?(:order, :rank)
|
||||||
popular_tags
|
popular_tags.presence || frequent_tags
|
||||||
elsif query.is_single_term?
|
elsif query.is_single_term?
|
||||||
similar_tags
|
similar_tags.presence || frequent_tags
|
||||||
else
|
else
|
||||||
frequent_tags
|
frequent_tags
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def popular_tags
|
def popular_tags
|
||||||
if PopularSearchService.enabled?
|
ReportbooruService.new.popular_searches(Date.today, limit: MAX_SIDEBAR_TAGS)
|
||||||
PopularSearchService.new(Date.today).tags
|
|
||||||
else
|
|
||||||
frequent_tags
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def similar_tags
|
def similar_tags
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
class PostViewCountService
|
|
||||||
def self.enabled?
|
|
||||||
Danbooru.config.reportbooru_server.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
if !PostViewCountService.enabled?
|
|
||||||
raise NotImplementedError.new("the Reportbooru service isn't configured. Post views are not available.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_count(post_id)
|
|
||||||
url = URI.parse("#{Danbooru.config.reportbooru_server}/post_views/#{post_id}")
|
|
||||||
response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 6))
|
|
||||||
if response.success?
|
|
||||||
return JSON.parse(response.body)
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_rank(date = Date.today)
|
|
||||||
url = URI.parse("#{Danbooru.config.reportbooru_server}/post_views/rank?date=#{date}")
|
|
||||||
response = HTTParty.get(url, Danbooru.config.httparty_options.reverse_merge(timeout: 6))
|
|
||||||
if response.success?
|
|
||||||
return JSON.parse(response.body)
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
rescue JSON::ParserError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def popular_posts(date = Date.today)
|
|
||||||
ranking = fetch_rank(date) || []
|
|
||||||
ranking.slice(0, 50).map {|x| Post.find(x[0])}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
50
app/logical/reportbooru_service.rb
Normal file
50
app/logical/reportbooru_service.rb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
class ReportbooruService
|
||||||
|
attr_reader :http, :reportbooru_server
|
||||||
|
|
||||||
|
def initialize(http: Danbooru::Http.new, reportbooru_server: Danbooru.config.reportbooru_server)
|
||||||
|
@reportbooru_server = reportbooru_server
|
||||||
|
@http = http
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
reportbooru_server.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def missed_search_rankings(expires_in: 1.minutes)
|
||||||
|
return [] unless enabled?
|
||||||
|
|
||||||
|
response = http.cache(expires_in).get("#{reportbooru_server}/missed_searches")
|
||||||
|
return [] if response.status != 200
|
||||||
|
|
||||||
|
body = response.to_s.force_encoding("utf-8")
|
||||||
|
body.lines.map(&:split).map { [_1, _2.to_i] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_search_rankings(date, expires_in: 1.minutes)
|
||||||
|
request("#{reportbooru_server}/post_searches/rank?date=#{date}", expires_in)
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_view_rankings(date, expires_in: 1.minutes)
|
||||||
|
request("#{reportbooru_server}/post_views/rank?date=#{date}", expires_in)
|
||||||
|
end
|
||||||
|
|
||||||
|
def popular_searches(date, limit: 100)
|
||||||
|
ranking = post_search_rankings(date)
|
||||||
|
ranking = post_search_rankings(date.yesterday) if ranking.blank?
|
||||||
|
ranking.take(limit).map(&:first)
|
||||||
|
end
|
||||||
|
|
||||||
|
def popular_posts(date, limit: 100)
|
||||||
|
ranking = post_view_rankings(date)
|
||||||
|
ranking = post_view_rankings(date.yesterday) if ranking.blank?
|
||||||
|
ranking.take(limit).map { |x| Post.find(x[0]) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def request(url, expires_in)
|
||||||
|
return [] unless enabled?
|
||||||
|
|
||||||
|
response = http.cache(expires_in).get(url)
|
||||||
|
return [] if response.status != 200
|
||||||
|
JSON.parse(response.to_s.force_encoding("utf-8"))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
def self.all
|
def self.all
|
||||||
return [
|
[
|
||||||
Strategies::Pixiv,
|
Strategies::Pixiv,
|
||||||
Strategies::NicoSeiga,
|
Strategies::NicoSeiga,
|
||||||
Strategies::Twitter,
|
Strategies::Twitter,
|
||||||
@@ -13,7 +13,8 @@ module Sources
|
|||||||
Strategies::Pawoo,
|
Strategies::Pawoo,
|
||||||
Strategies::Moebooru,
|
Strategies::Moebooru,
|
||||||
Strategies::HentaiFoundry,
|
Strategies::HentaiFoundry,
|
||||||
Strategies::Weibo
|
Strategies::Weibo,
|
||||||
|
Strategies::Newgrounds
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
|
|
||||||
module Sources::Strategies
|
module Sources::Strategies
|
||||||
class ArtStation < Base
|
class ArtStation < Base
|
||||||
PROJECT1 = %r!\Ahttps?://www\.artstation\.com/artwork/(?<project_id>[a-z0-9-]+)/?\z!i
|
PROJECT1 = %r{\Ahttps?://www\.artstation\.com/artwork/(?<project_id>[a-z0-9-]+)/?\z}i
|
||||||
PROJECT2 = %r!\Ahttps?://(?<artist_name>[\w-]+)\.artstation\.com/projects/(?<project_id>[a-z0-9-]+)(?:/|\?[\w=-]+)?\z!i
|
PROJECT2 = %r{\Ahttps?://(?<artist_name>[\w-]+)\.artstation\.com/projects/(?<project_id>[a-z0-9-]+)(?:/|\?[\w=-]+)?\z}i
|
||||||
PROJECT = Regexp.union(PROJECT1, PROJECT2)
|
PROJECT = Regexp.union(PROJECT1, PROJECT2)
|
||||||
ARTIST1 = %r{\Ahttps?://(?<artist_name>[\w-]+)(?<!www)\.artstation\.com/?\z}i
|
ARTIST1 = %r{\Ahttps?://(?<artist_name>[\w-]+)(?<!www)\.artstation\.com/?\z}i
|
||||||
ARTIST2 = %r{\Ahttps?://www\.artstation\.com/artist/(?<artist_name>[\w-]+)/?\z}i
|
ARTIST2 = %r{\Ahttps?://www\.artstation\.com/artist/(?<artist_name>[\w-]+)/?\z}i
|
||||||
ARTIST3 = %r{\Ahttps?://www\.artstation\.com/(?<artist_name>[\w-]+)/?\z}i
|
ARTIST3 = %r{\Ahttps?://www\.artstation\.com/(?<artist_name>[\w-]+)/?\z}i
|
||||||
ARTIST = Regexp.union(ARTIST1, ARTIST2, ARTIST3)
|
ARTIST = Regexp.union(ARTIST1, ARTIST2, ARTIST3)
|
||||||
|
|
||||||
ASSET = %r!\Ahttps?://cdn\w*\.artstation\.com/p/assets/(?<type>images|covers)/images/(?<id>\d+/\d+/\d+)/(?<size>[^/]+)/(?<filename>.+)\z!i
|
ASSET = %r{\Ahttps?://cdn\w*\.artstation\.com/p/assets/(?<type>images|covers)/images/(?<id>\d+/\d+/\d+)/(?<size>[^/]+)/(?<filename>.+)\z}i
|
||||||
|
|
||||||
attr_reader :json
|
attr_reader :json
|
||||||
|
|
||||||
@@ -144,10 +144,10 @@ module Sources::Strategies
|
|||||||
|
|
||||||
urls = image_url_sizes($~[:type], $~[:id], $~[:filename])
|
urls = image_url_sizes($~[:type], $~[:id], $~[:filename])
|
||||||
if size == :smallest
|
if size == :smallest
|
||||||
urls = urls.reverse()
|
urls = urls.reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
chosen_url = urls.find { |url| http_exists?(url, headers) }
|
chosen_url = urls.find { |url| http_exists?(url) }
|
||||||
chosen_url || url
|
chosen_url || url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class Base
|
class Base
|
||||||
|
class DownloadError < StandardError; end
|
||||||
|
|
||||||
attr_reader :url, :referer_url, :urls, :parsed_url, :parsed_referer, :parsed_urls
|
attr_reader :url, :referer_url, :urls, :parsed_url, :parsed_referer, :parsed_urls
|
||||||
|
|
||||||
extend Memoist
|
extend Memoist
|
||||||
@@ -35,9 +37,9 @@ module Sources
|
|||||||
# <tt>referrer_url</tt> so the strategy can discover the HTML
|
# <tt>referrer_url</tt> so the strategy can discover the HTML
|
||||||
# page and other information.
|
# page and other information.
|
||||||
def initialize(url, referer_url = nil)
|
def initialize(url, referer_url = nil)
|
||||||
@url = url
|
@url = url.to_s
|
||||||
@referer_url = referer_url
|
@referer_url = referer_url&.to_s
|
||||||
@urls = [url, referer_url].select(&:present?)
|
@urls = [@url, @referer_url].select(&:present?)
|
||||||
|
|
||||||
@parsed_url = Addressable::URI.heuristic_parse(url) rescue nil
|
@parsed_url = Addressable::URI.heuristic_parse(url) rescue nil
|
||||||
@parsed_referer = Addressable::URI.heuristic_parse(referer_url) rescue nil
|
@parsed_referer = Addressable::URI.heuristic_parse(referer_url) rescue nil
|
||||||
@@ -58,8 +60,8 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def site_name
|
def site_name
|
||||||
Addressable::URI.heuristic_parse(url).host
|
Addressable::URI.heuristic_parse(url)&.host
|
||||||
rescue Addressable::URI::InvalidURIError => e
|
rescue Addressable::URI::InvalidURIError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -90,9 +92,7 @@ module Sources
|
|||||||
# eventually be assigned as the source for the post, but it does not
|
# eventually be assigned as the source for the post, but it does not
|
||||||
# represent what the downloader will fetch.
|
# represent what the downloader will fetch.
|
||||||
def page_url
|
def page_url
|
||||||
Rails.logger.warn "Valid page url for (#{url}, #{referer_url}) not found"
|
nil
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# This will be the url stored in posts. Typically this is the page
|
# This will be the url stored in posts. Typically this is the page
|
||||||
@@ -141,14 +141,37 @@ module Sources
|
|||||||
# Subclasses should merge in any required headers needed to access resources
|
# Subclasses should merge in any required headers needed to access resources
|
||||||
# on the site.
|
# on the site.
|
||||||
def headers
|
def headers
|
||||||
return Danbooru.config.http_headers
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the size of the image resource without actually downloading the file.
|
# Returns the size of the image resource without actually downloading the file.
|
||||||
def size
|
def remote_size
|
||||||
Downloads::File.new(image_url).size
|
response = http_downloader.head(image_url)
|
||||||
|
return nil unless response.status == 200 && response.content_length.present?
|
||||||
|
|
||||||
|
response.content_length.to_i
|
||||||
end
|
end
|
||||||
memoize :size
|
memoize :remote_size
|
||||||
|
|
||||||
|
# Download the file at the given url, or at the main image url by default.
|
||||||
|
def download_file!(download_url = image_url)
|
||||||
|
raise DownloadError, "Download failed: couldn't find download url for #{url}" if download_url.blank?
|
||||||
|
response, file = http_downloader.download_media(download_url)
|
||||||
|
raise DownloadError, "Download failed: #{download_url} returned error #{response.status}" if response.status != 200
|
||||||
|
file
|
||||||
|
end
|
||||||
|
|
||||||
|
# A http client for API requests.
|
||||||
|
def http
|
||||||
|
Danbooru::Http.new.public_only
|
||||||
|
end
|
||||||
|
memoize :http
|
||||||
|
|
||||||
|
# A http client for downloading files.
|
||||||
|
def http_downloader
|
||||||
|
http.timeout(30).max_size(Danbooru.config.max_file_size).use(:spoof_referrer).use(:unpolish_cloudflare)
|
||||||
|
end
|
||||||
|
memoize :http_downloader
|
||||||
|
|
||||||
# The url to use for artist finding purposes. This will be stored in the
|
# The url to use for artist finding purposes. This will be stored in the
|
||||||
# artist entry. Normally this will be the profile url.
|
# artist entry. Normally this will be the profile url.
|
||||||
@@ -189,7 +212,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def normalized_tags
|
def normalized_tags
|
||||||
tags.map { |tag, url| normalize_tag(tag) }.sort.uniq
|
tags.map { |tag, _url| normalize_tag(tag) }.sort.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_tag(tag)
|
def normalize_tag(tag)
|
||||||
@@ -243,7 +266,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
return {
|
{
|
||||||
:artist => {
|
:artist => {
|
||||||
:name => artist_name,
|
:name => artist_name,
|
||||||
:tag_name => tag_name,
|
:tag_name => tag_name,
|
||||||
@@ -276,9 +299,8 @@ module Sources
|
|||||||
to_h.to_json
|
to_h.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
def http_exists?(url, headers)
|
def http_exists?(url)
|
||||||
res = HTTParty.head(url, Danbooru.config.httparty_options.deep_merge(headers: headers))
|
http_downloader.head(url).status.success?
|
||||||
res.success?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convert commentary to dtext by stripping html tags. Sites can override
|
# Convert commentary to dtext by stripping html tags. Sites can override
|
||||||
|
|||||||
@@ -47,18 +47,18 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class DeviantArt < Base
|
class DeviantArt < Base
|
||||||
ASSET_SUBDOMAINS = %r{(?:fc|th|pre|img|orig|origin-orig)\d*}i
|
ASSET_SUBDOMAINS = /(?:fc|th|pre|img|orig|origin-orig)\d*/i
|
||||||
RESERVED_SUBDOMAINS = %r{\Ahttps?://(?:#{ASSET_SUBDOMAINS}|www)\.}i
|
RESERVED_SUBDOMAINS = %r{\Ahttps?://(?:#{ASSET_SUBDOMAINS}|www)\.}i
|
||||||
MAIN_DOMAIN = %r{\Ahttps?://(?:www\.)?deviantart.com}i
|
MAIN_DOMAIN = %r{\Ahttps?://(?:www\.)?deviantart.com}i
|
||||||
|
|
||||||
TITLE = %r{(?<title>[a-z0-9_-]+?)}i
|
TITLE = /(?<title>[a-z0-9_-]+?)/i
|
||||||
ARTIST = %r{(?<artist>[a-z0-9_-]+?)}i
|
ARTIST = /(?<artist>[a-z0-9_-]+?)/i
|
||||||
DEVIATION_ID = %r{(?<deviation_id>[0-9]+)}i
|
DEVIATION_ID = /(?<deviation_id>[0-9]+)/i
|
||||||
|
|
||||||
DA_FILENAME_1 = %r{[a-f0-9]{32}-d(?<base36_deviation_id>[a-z0-9]+)\.}i
|
DA_FILENAME_1 = /[a-f0-9]{32}-d(?<base36_deviation_id>[a-z0-9]+)\./i
|
||||||
DA_FILENAME_2 = %r{#{TITLE}(?:_by_#{ARTIST}(?:-d(?<base36_deviation_id>[a-z0-9]+))?)?\.}i
|
DA_FILENAME_2 = /#{TITLE}(?:_by_#{ARTIST}(?:-d(?<base36_deviation_id>[a-z0-9]+))?)?\./i
|
||||||
DA_FILENAME = Regexp.union(DA_FILENAME_1, DA_FILENAME_2)
|
DA_FILENAME = Regexp.union(DA_FILENAME_1, DA_FILENAME_2)
|
||||||
WIX_FILENAME = %r{d(?<base36_deviation_id>[a-z0-9]+)[0-9a-f-]+\.\w+(?:/\w+/\w+/[\w,]+/(?<title>[\w-]+)_by_(?<artist>[\w-]+)_d\w+-\w+\.\w+)?.+}i
|
WIX_FILENAME = %r{d(?<base36_deviation_id>[a-z0-9]+)[0-9a-f-]+\.\w+(?:/\w+/\w+/[\w,]+/(?<title>[\w-]+)_by_(?<artist>[\w-]+)_d\w+-\w+\.\w+)?.+}i
|
||||||
|
|
||||||
NOT_NORMALIZABLE_ASSET = %r{\Ahttps?://#{ASSET_SUBDOMAINS}\.deviantart\.net/.+/[0-9a-f]{32}(?:-[^d]\w+)?\.}i
|
NOT_NORMALIZABLE_ASSET = %r{\Ahttps?://#{ASSET_SUBDOMAINS}\.deviantart\.net/.+/[0-9a-f]{32}(?:-[^d]\w+)?\.}i
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ module Sources
|
|||||||
PATH_PROFILE = %r{#{MAIN_DOMAIN}/#{ARTIST}/?\z}i
|
PATH_PROFILE = %r{#{MAIN_DOMAIN}/#{ARTIST}/?\z}i
|
||||||
SUBDOMAIN_PROFILE = %r{\Ahttps?://#{ARTIST}\.deviantart\.com/?\z}i
|
SUBDOMAIN_PROFILE = %r{\Ahttps?://#{ARTIST}\.deviantart\.com/?\z}i
|
||||||
|
|
||||||
FAVME = %r{\Ahttps?://(www\.)?fav\.me/d(?<base36_deviation_id>[a-z0-9]+)\z}i
|
FAVME = %r{\Ahttps?://(?:www\.)?fav\.me/d(?<base36_deviation_id>[a-z0-9]+)\z}i
|
||||||
|
|
||||||
def domains
|
def domains
|
||||||
["deviantart.net", "deviantart.com", "fav.me"]
|
["deviantart.net", "deviantart.com", "fav.me"]
|
||||||
@@ -110,12 +110,12 @@ module Sources
|
|||||||
api_deviation[:videos].max_by { |x| x[:filesize] }[:src]
|
api_deviation[:videos].max_by { |x| x[:filesize] }[:src]
|
||||||
else
|
else
|
||||||
src = api_deviation.dig(:content, :src)
|
src = api_deviation.dig(:content, :src)
|
||||||
if deviation_id && deviation_id.to_i <= 790677560 && src =~ /^https:\/\/images-wixmp-/ && src !~ /\.gif\?/
|
if deviation_id && deviation_id.to_i <= 790_677_560 && src =~ %r{\Ahttps://images-wixmp-} && src !~ /\.gif\?/
|
||||||
src = src.sub(%r!(/f/[a-f0-9-]+/[a-f0-9-]+)!, '/intermediary\1')
|
src = src.sub(%r{(/f/[a-f0-9-]+/[a-f0-9-]+)}, '/intermediary\1')
|
||||||
src = src.sub(%r!/v1/(fit|fill)/.*\z!i, "")
|
src = src.sub(%r{/v1/(fit|fill)/.*\z}i, "")
|
||||||
end
|
end
|
||||||
src = src.sub(%r!\Ahttps?://orig\d+\.deviantart\.net!i, "http://origin-orig.deviantart.net")
|
src = src.sub(%r{\Ahttps?://orig\d+\.deviantart\.net}i, "http://origin-orig.deviantart.net")
|
||||||
src = src.gsub(%r!q_\d+,strp!, "q_100")
|
src = src.gsub(/q_\d+,strp/, "q_100")
|
||||||
src
|
src
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -191,7 +191,7 @@ module Sources
|
|||||||
|
|
||||||
# <a href="https://sa-dui.deviantart.com/journal/About-Commissions-223178193" data-sigil="thumb" class="thumb lit" ...>
|
# <a href="https://sa-dui.deviantart.com/journal/About-Commissions-223178193" data-sigil="thumb" class="thumb lit" ...>
|
||||||
if element["class"].split.include?("lit")
|
if element["class"].split.include?("lit")
|
||||||
deviation_id = element["href"][%r!-(\d+)\z!, 1].to_i
|
deviation_id = element["href"][/-(\d+)\z/, 1].to_i
|
||||||
element.content = "deviantart ##{deviation_id}"
|
element.content = "deviantart ##{deviation_id}"
|
||||||
else
|
else
|
||||||
element.content = ""
|
element.content = ""
|
||||||
@@ -199,7 +199,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
if element.name == "a" && element["href"].present?
|
if element.name == "a" && element["href"].present?
|
||||||
element["href"] = element["href"].gsub(%r!\Ahttps?://www\.deviantart\.com/users/outgoing\?!i, "")
|
element["href"] = element["href"].gsub(%r{\Ahttps?://www\.deviantart\.com/users/outgoing\?}i, "")
|
||||||
|
|
||||||
# href may be missing the `http://` bit (ex: `inprnt.com`, `//inprnt.com`). Add it if missing.
|
# href may be missing the `http://` bit (ex: `inprnt.com`, `//inprnt.com`). Add it if missing.
|
||||||
uri = Addressable::URI.heuristic_parse(element["href"]) rescue nil
|
uri = Addressable::URI.heuristic_parse(element["href"]) rescue nil
|
||||||
@@ -283,7 +283,7 @@ module Sources
|
|||||||
return nil if meta.nil?
|
return nil if meta.nil?
|
||||||
|
|
||||||
appurl = meta["content"]
|
appurl = meta["content"]
|
||||||
uuid = appurl[%r!\ADeviantArt://deviation/(.*)\z!, 1]
|
uuid = appurl[%r{\ADeviantArt://deviation/(.*)\z}, 1]
|
||||||
uuid
|
uuid
|
||||||
end
|
end
|
||||||
memoize :uuid
|
memoize :uuid
|
||||||
|
|||||||
@@ -23,11 +23,11 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class HentaiFoundry < Base
|
class HentaiFoundry < Base
|
||||||
BASE_URL = %r!\Ahttps?://(?:www\.)?hentai-foundry\.com!i
|
BASE_URL = %r{\Ahttps?://(?:www\.)?hentai-foundry\.com}i
|
||||||
PAGE_URL = %r!#{BASE_URL}/pictures/user/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:/[\w.-]*)?(\?[\w=]*)?\z!i
|
PAGE_URL = %r{#{BASE_URL}/pictures/user/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:/[\w.-]*)?(\?[\w=]*)?\z}i
|
||||||
OLD_PAGE = %r!#{BASE_URL}/pic-(?<illust_id>\d+)(?:\.html)?\z!i
|
OLD_PAGE = %r{#{BASE_URL}/pic-(?<illust_id>\d+)(?:\.html)?\z}i
|
||||||
PROFILE_URL = %r!#{BASE_URL}/(?:pictures/)?user/(?<artist_name>[\w-]+)(?:/[a-z]*)?\z!i
|
PROFILE_URL = %r{#{BASE_URL}/(?:pictures/)?user/(?<artist_name>[\w-]+)(?:/[a-z]*)?\z}i
|
||||||
IMAGE_URL = %r!\Ahttps?://pictures\.hentai-foundry\.com/+\w/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:(?:/[\w.-]+)?\.\w+)?\z!i
|
IMAGE_URL = %r{\Ahttps?://pictures\.hentai-foundry\.com/+\w/(?<artist_name>[\w-]+)/(?<illust_id>\d+)(?:(?:/[\w.-]+)?\.\w+)?\z}i
|
||||||
|
|
||||||
def domains
|
def domains
|
||||||
["hentai-foundry.com"]
|
["hentai-foundry.com"]
|
||||||
@@ -64,11 +64,10 @@ module Sources
|
|||||||
def page
|
def page
|
||||||
return nil if page_url.blank?
|
return nil if page_url.blank?
|
||||||
|
|
||||||
doc = Cache.get("hentai-foundry:#{page_url}", 1.minute) do
|
response = Danbooru::Http.new.cache(1.minute).get("#{page_url}?enterAgree=1")
|
||||||
HTTParty.get("#{page_url}?enterAgree=1").body
|
return nil unless response.status == 200
|
||||||
end
|
|
||||||
|
|
||||||
Nokogiri::HTML(doc)
|
response.parse
|
||||||
end
|
end
|
||||||
|
|
||||||
def tags
|
def tags
|
||||||
|
|||||||
@@ -32,10 +32,10 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class Moebooru < Base
|
class Moebooru < Base
|
||||||
BASE_URL = %r!\Ahttps?://(?:[^.]+\.)?(?<domain>yande\.re|konachan\.com)!i
|
BASE_URL = %r{\Ahttps?://(?:[^.]+\.)?(?<domain>yande\.re|konachan\.com)}i
|
||||||
POST_URL = %r!#{BASE_URL}/post/show/(?<id>\d+)!i
|
POST_URL = %r{#{BASE_URL}/post/show/(?<id>\d+)}i
|
||||||
URL_SLUG = %r!/(?:yande\.re%20|Konachan\.com%20-%20)?(?<id>\d+)?.*!i
|
URL_SLUG = %r{/(?:yande\.re%20|Konachan\.com%20-%20)?(?<id>\d+)?.*}i
|
||||||
IMAGE_URL = %r!#{BASE_URL}/(?<type>image|jpeg|sample)/(?<md5>\h{32})#{URL_SLUG}?\.(?<ext>jpg|jpeg|png|gif)\z!i
|
IMAGE_URL = %r{#{BASE_URL}/(?<type>image|jpeg|sample)/(?<md5>\h{32})#{URL_SLUG}?\.(?<ext>jpg|jpeg|png|gif)\z}i
|
||||||
|
|
||||||
delegate :artist_name, :profile_url, :tag_name, :artist_commentary_title, :artist_commentary_desc, :dtext_artist_commentary_title, :dtext_artist_commentary_desc, to: :sub_strategy, allow_nil: true
|
delegate :artist_name, :profile_url, :tag_name, :artist_commentary_title, :artist_commentary_desc, :dtext_artist_commentary_title, :dtext_artist_commentary_desc, to: :sub_strategy, allow_nil: true
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def preview_urls
|
def preview_urls
|
||||||
return image_urls unless post_md5.present?
|
return image_urls if post_md5.blank?
|
||||||
["https://#{file_host}/data/preview/#{post_md5[0..1]}/#{post_md5[2..3]}/#{post_md5}.jpg"]
|
["https://#{file_host}/data/preview/#{post_md5[0..1]}/#{post_md5[2..3]}/#{post_md5}.jpg"]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ module Sources
|
|||||||
|
|
||||||
# the api_response wasn't available because it's a deleted post.
|
# the api_response wasn't available because it's a deleted post.
|
||||||
elsif post_md5.present?
|
elsif post_md5.present?
|
||||||
%w[jpg png gif].find { |ext| http_exists?("https://#{site_name}/image/#{post_md5}.#{ext}", headers) }
|
%w[jpg png gif].find { |ext| http_exists?("https://#{site_name}/image/#{post_md5}.#{ext}") }
|
||||||
|
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
|
|||||||
111
app/logical/sources/strategies/newgrounds.rb
Normal file
111
app/logical/sources/strategies/newgrounds.rb
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Image Urls
|
||||||
|
# * https://art.ngfiles.com/images/1254000/1254722_natthelich_pandora.jpg
|
||||||
|
# * https://art.ngfiles.com/images/1033000/1033622_natthelich_fire-emblem-marth-plus-progress-pic.png?f1569487181
|
||||||
|
# * https://art.ngfiles.com/comments/57000/iu_57615_7115981.jpg
|
||||||
|
#
|
||||||
|
# Page URLs
|
||||||
|
# * https://www.newgrounds.com/art/view/puddbytes/costanza-at-bat
|
||||||
|
# * https://www.newgrounds.com/art/view/natthelich/fire-emblem-marth-plus-progress-pic (multiple)
|
||||||
|
#
|
||||||
|
# Profile URLs
|
||||||
|
# * https://natthelich.newgrounds.com/
|
||||||
|
|
||||||
|
module Sources
|
||||||
|
module Strategies
|
||||||
|
class Newgrounds < Base
|
||||||
|
IMAGE_URL = %r{\Ahttps?://art\.ngfiles\.com/images/\d+/\d+_(?<user_name>[0-9a-z-]+)_(?<illust_title>[0-9a-z-]+)\.\w+}i
|
||||||
|
COMMENT_URL = %r{\Ahttps?://art\.ngfiles\.com/comments/\d+/\w+\.\w+}i
|
||||||
|
|
||||||
|
PAGE_URL = %r{\Ahttps?://(?:www\.)?newgrounds\.com/art/view/(?<user_name>[0-9a-z-]+)/(?<illust_title>[0-9a-z-]+)(?:\?.*)?}i
|
||||||
|
|
||||||
|
PROFILE_URL = %r{\Ahttps?://(?<artist_name>(?!www)[0-9a-z-]+)\.newgrounds\.com(?:/.*)?}i
|
||||||
|
|
||||||
|
def domains
|
||||||
|
["newgrounds.com", "ngfiles.com"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def site_name
|
||||||
|
"NewGrounds"
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_urls
|
||||||
|
if url =~ COMMENT_URL || url =~ IMAGE_URL
|
||||||
|
[url]
|
||||||
|
else
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
urls += page&.css(".image img").to_a.map { |img| img["src"] }
|
||||||
|
urls += page&.css("#author_comments img[data-user-image='1']").to_a.map { |img| img["data-smartload-src"] || img["src"] }
|
||||||
|
|
||||||
|
urls.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_url
|
||||||
|
return nil if illust_title.blank? || user_name.blank?
|
||||||
|
|
||||||
|
"https://www.newgrounds.com/art/view/#{user_name}/#{illust_title}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def page
|
||||||
|
return nil if page_url.blank?
|
||||||
|
|
||||||
|
response = Danbooru::Http.cache(1.minute).get(page_url)
|
||||||
|
return nil if response.status == 404
|
||||||
|
|
||||||
|
response.parse
|
||||||
|
end
|
||||||
|
memoize :page
|
||||||
|
|
||||||
|
def tags
|
||||||
|
page&.css("#sidestats .tags a").to_a.map do |tag|
|
||||||
|
[tag.text, "https://www.newgrounds.com/search/conduct/art?match=tags&tags=" + tag.text]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_tag(tag)
|
||||||
|
tag = tag.tr("-", "_")
|
||||||
|
super(tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
def artist_name
|
||||||
|
name = page&.css(".item-user .item-details h4 a")&.text&.strip || user_name
|
||||||
|
name&.downcase
|
||||||
|
end
|
||||||
|
|
||||||
|
def other_names
|
||||||
|
[artist_name, user_name].compact.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def profile_url
|
||||||
|
# user names are not mutable, artist names are.
|
||||||
|
# However we need the latest name for normalization
|
||||||
|
"https://#{artist_name}.newgrounds.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
def artist_commentary_title
|
||||||
|
page&.css(".pod-head > [itemprop='name']")&.text
|
||||||
|
end
|
||||||
|
|
||||||
|
def artist_commentary_desc
|
||||||
|
page&.css("#author_comments")&.to_html
|
||||||
|
end
|
||||||
|
|
||||||
|
def dtext_artist_commentary_desc
|
||||||
|
DText.from_html(artist_commentary_desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_for_source
|
||||||
|
page_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_name
|
||||||
|
urls.map { |u| url[PROFILE_URL, :artist_name] || u[IMAGE_URL, :user_name] || u[PAGE_URL, :user_name] }.compact.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def illust_title
|
||||||
|
urls.map { |u| u[IMAGE_URL, :illust_title] || u[PAGE_URL, :illust_title] }.compact.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,25 +1,54 @@
|
|||||||
# Image Direct URL
|
# Direct URL
|
||||||
# * https://lohas.nicoseiga.jp/o/971eb8af9bbcde5c2e51d5ef3a2f62d6d9ff5552/1589933964/3583893
|
# * https://lohas.nicoseiga.jp/o/971eb8af9bbcde5c2e51d5ef3a2f62d6d9ff5552/1589933964/3583893
|
||||||
# * http://lohas.nicoseiga.jp/priv/3521156?e=1382558156&h=f2e089256abd1d453a455ec8f317a6c703e2cedf
|
# * http://lohas.nicoseiga.jp/priv/3521156?e=1382558156&h=f2e089256abd1d453a455ec8f317a6c703e2cedf
|
||||||
# * http://lohas.nicoseiga.jp/priv/b80f86c0d8591b217e7513a9e175e94e00f3c7a1/1384936074/3583893
|
# * http://lohas.nicoseiga.jp/priv/b80f86c0d8591b217e7513a9e175e94e00f3c7a1/1384936074/3583893
|
||||||
|
# * https://dcdn.cdn.nimg.jp/priv/62a56a7f67d3d3746ae5712db9cac7d465f4a339/1592186183/10466669
|
||||||
|
# * https://dcdn.cdn.nimg.jp/nicoseiga/lohas/o/8ba0a9b2ea34e1ef3b5cc50785bd10cd63ec7e4a/1592187477/10466669
|
||||||
|
#
|
||||||
|
# * http://lohas.nicoseiga.jp/material/5746c5/4459092
|
||||||
|
#
|
||||||
|
# (Manga direct url)
|
||||||
|
# * https://lohas.nicoseiga.jp/priv/f5b8966fd53bf7e06cccff9fbb2c4eef62877538/1590752727/8947170
|
||||||
|
#
|
||||||
|
# Samples
|
||||||
|
# * http://lohas.nicoseiga.jp/thumb/2163478i?
|
||||||
|
# * https://lohas.nicoseiga.jp/thumb/8947170p
|
||||||
|
#
|
||||||
|
## The direct urls and samples above can belong to both illust and manga.
|
||||||
|
## There's two ways to tell them apart:
|
||||||
|
## * visit the /source/ equivalent: illusts redirect to the /o/ intermediary page, manga redirect to /priv/ directly
|
||||||
|
## * try an api call: illusts will succeed, manga will fail
|
||||||
|
#
|
||||||
|
# Source Link
|
||||||
# * http://seiga.nicovideo.jp/image/source?id=3312222
|
# * http://seiga.nicovideo.jp/image/source?id=3312222
|
||||||
#
|
#
|
||||||
# Image Page URL
|
# Illust Page URL
|
||||||
# * https://seiga.nicovideo.jp/seiga/im3521156
|
# * https://seiga.nicovideo.jp/seiga/im3521156
|
||||||
|
# * https://seiga.nicovideo.jp/seiga/im520647 (anonymous artist)
|
||||||
#
|
#
|
||||||
# Manga Page URL
|
# Manga Page URL
|
||||||
# * http://seiga.nicovideo.jp/watch/mg316708
|
# * http://seiga.nicovideo.jp/watch/mg316708
|
||||||
|
#
|
||||||
|
# Video Page URL (not supported)
|
||||||
|
# * https://www.nicovideo.jp/watch/sm36465441
|
||||||
|
#
|
||||||
|
# Oekaki
|
||||||
|
# * https://dic.nicovideo.jp/oekaki/52833.png
|
||||||
|
|
||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class NicoSeiga < Base
|
class NicoSeiga < Base
|
||||||
URL = %r!\Ahttps?://(?:\w+\.)?nico(?:seiga|video)\.jp!
|
DIRECT = %r{\Ahttps?://lohas\.nicoseiga\.jp/(?:priv|o)/(?:\w+/\d+/)?(?<image_id>\d+)(?:\?.+)?}i
|
||||||
DIRECT1 = %r!\Ahttps?://lohas\.nicoseiga\.jp/priv/[0-9a-f]+!
|
CDN_DIRECT = %r{\Ahttps?://dcdn\.cdn\.nimg\.jp/.+/\w+/\d+/(?<image_id>\d+)}i
|
||||||
DIRECT2 = %r!\Ahttps?://lohas\.nicoseiga\.jp/o/[0-9a-f]+/\d+/\d+!
|
SOURCE = %r{\Ahttps?://seiga\.nicovideo\.jp/image/source(?:/|\?id=)(?<image_id>\d+)}i
|
||||||
DIRECT3 = %r!\Ahttps?://seiga\.nicovideo\.jp/images/source/\d+!
|
|
||||||
PAGE = %r!\Ahttps?://seiga\.nicovideo\.jp/seiga/im(\d+)!i
|
ILLUST_THUMB = %r{\Ahttps?://lohas\.nicoseiga\.jp/thumb/(?<illust_id>\d+)i}i
|
||||||
PROFILE = %r!\Ahttps?://seiga\.nicovideo\.jp/user/illust/(\d+)!i
|
MANGA_THUMB = %r{\Ahttps?://lohas\.nicoseiga\.jp/thumb/(?<image_id>\d+)p}i
|
||||||
MANGA_PAGE = %r!\Ahttps?://seiga\.nicovideo\.jp/watch/mg(\d+)!i
|
|
||||||
|
ILLUST_PAGE = %r{\Ahttps?://(?:sp\.)?seiga\.nicovideo\.jp/seiga/im(?<illust_id>\d+)}i
|
||||||
|
MANGA_PAGE = %r{\Ahttps?://(?:sp\.)?seiga\.nicovideo\.jp/watch/mg(?<manga_id>\d+)}i
|
||||||
|
|
||||||
|
PROFILE_PAGE = %r{\Ahttps?://seiga\.nicovideo\.jp/user/illust/(?<artist_id>\d+)}i
|
||||||
|
|
||||||
def domains
|
def domains
|
||||||
["nicoseiga.jp", "nicovideo.jp"]
|
["nicoseiga.jp", "nicovideo.jp"]
|
||||||
@@ -30,160 +59,136 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def image_urls
|
def image_urls
|
||||||
if url =~ DIRECT1
|
urls = []
|
||||||
return [url]
|
return urls if api_client&.api_response.blank?
|
||||||
|
|
||||||
|
if image_id.present?
|
||||||
|
urls << "https://seiga.nicovideo.jp/image/source/#{image_id}"
|
||||||
|
elsif illust_id.present?
|
||||||
|
urls << "https://seiga.nicovideo.jp/image/source/#{illust_id}"
|
||||||
|
elsif manga_id.present? && api_client.image_ids.present?
|
||||||
|
urls += api_client.image_ids.map { |id| "https://seiga.nicovideo.jp/image/source/#{id}" }
|
||||||
|
end
|
||||||
|
urls
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_url
|
||||||
|
return url if image_urls.blank? || api_client.blank?
|
||||||
|
|
||||||
|
img = case url
|
||||||
|
when DIRECT || CDN_DIRECT then "https://seiga.nicovideo.jp/image/source/#{image_id_from_url(url)}"
|
||||||
|
when SOURCE then url
|
||||||
|
else image_urls.first
|
||||||
end
|
end
|
||||||
|
|
||||||
if theme_id
|
resp = api_client.login.head(img)
|
||||||
return api_client.image_ids.map do |image_id|
|
if resp.uri.to_s =~ %r{https?://.+/(\w+/\d+/\d+)\z}i
|
||||||
"https://seiga.nicovideo.jp/image/source/#{image_id}"
|
"https://lohas.nicoseiga.jp/priv/#{$1}"
|
||||||
end
|
else
|
||||||
|
img
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
link = page.search("a#illust_link")
|
def preview_urls
|
||||||
|
if illust_id.present?
|
||||||
if link.any?
|
["https://lohas.nicoseiga.jp/thumb/#{illust_id}i"]
|
||||||
image_url = "http://seiga.nicovideo.jp" + link[0]["href"]
|
else
|
||||||
page = agent.get(image_url) # need to follow this redirect while logged in or it won't work
|
image_urls
|
||||||
|
|
||||||
if page.is_a?(Mechanize::Image)
|
|
||||||
return [page.uri.to_s]
|
|
||||||
end
|
|
||||||
|
|
||||||
images = page.search("div.illust_view_big").select {|x| x["data-src"] =~ /\/priv\//}
|
|
||||||
|
|
||||||
if images.any?
|
|
||||||
return ["http://lohas.nicoseiga.jp" + images[0]["data-src"]]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "image url not found for (#{url}, #{referer_url})"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_url
|
def page_url
|
||||||
[url, referer_url].each do |x|
|
if illust_id.present?
|
||||||
if x =~ %r!\Ahttps?://lohas\.nicoseiga\.jp/o/[a-f0-9]+/\d+/(\d+)!
|
"https://seiga.nicovideo.jp/seiga/im#{illust_id}"
|
||||||
return "http://seiga.nicovideo.jp/seiga/im#{$1}"
|
elsif manga_id.present?
|
||||||
end
|
"https://seiga.nicovideo.jp/watch/mg#{manga_id}"
|
||||||
|
elsif image_id.present?
|
||||||
if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp/priv/(\d+)\?e=\d+&h=[a-f0-9]+}i
|
"https://seiga.nicovideo.jp/image/source/#{image_id}"
|
||||||
return "http://seiga.nicovideo.jp/seiga/im#{$1}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp/priv/[a-f0-9]+/\d+/(\d+)}i
|
|
||||||
return "http://seiga.nicovideo.jp/seiga/im#{$1}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp/priv/(\d+)}i
|
|
||||||
return "http://seiga.nicovideo.jp/seiga/im#{$1}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ %r{\Ahttps?://lohas\.nicoseiga\.jp//?thumb/(\d+)i?}i
|
|
||||||
return "http://seiga.nicovideo.jp/seiga/im#{$1}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ %r{/seiga/im\d+}
|
|
||||||
return x
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ %r{/watch/mg\d+}
|
|
||||||
return x
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ %r{/image/source\?id=(\d+)}
|
|
||||||
return "http://seiga.nicovideo.jp/seiga/im#{$1}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return super
|
|
||||||
end
|
|
||||||
|
|
||||||
def canonical_url
|
|
||||||
image_url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def profile_url
|
def profile_url
|
||||||
if url =~ PROFILE
|
user_id = api_client&.user_id
|
||||||
return url
|
return if user_id.blank? # artists can be anonymous
|
||||||
end
|
|
||||||
|
|
||||||
"http://seiga.nicovideo.jp/user/illust/#{api_client.user_id}"
|
"http://seiga.nicovideo.jp/user/illust/#{api_client.user_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def artist_name
|
def artist_name
|
||||||
api_client.moniker
|
return if api_client.blank?
|
||||||
|
api_client.user_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def artist_commentary_title
|
def artist_commentary_title
|
||||||
|
return if api_client.blank?
|
||||||
api_client.title
|
api_client.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def artist_commentary_desc
|
def artist_commentary_desc
|
||||||
api_client.desc
|
return if api_client.blank?
|
||||||
|
api_client.description
|
||||||
|
end
|
||||||
|
|
||||||
|
def dtext_artist_commentary_desc
|
||||||
|
DText.from_html(artist_commentary_desc).gsub(/[^\w]im(\d+)/, ' seiga #\1 ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_for_source
|
def normalize_for_source
|
||||||
if illust_id.present?
|
# There's no way to tell apart illust from manga from the direct image url alone. What's worse,
|
||||||
"https://seiga.nicovideo.jp/seiga/im#{illust_id}"
|
# nicoseiga itself doesn't know how to normalize back to manga, so if it's not an illust type then
|
||||||
elsif theme_id.present?
|
# it's impossible to get the original manga page back from the image url alone.
|
||||||
"http://seiga.nicovideo.jp/watch/mg#{theme_id}"
|
# /source/ links on the other hand correctly redirect, hence we use them to normalize saved direct sources.
|
||||||
|
if url =~ DIRECT
|
||||||
|
"https://seiga.nicovideo.jp/image/source/#{image_id}"
|
||||||
|
else
|
||||||
|
page_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_name
|
def tag_name
|
||||||
|
return if api_client&.user_id.blank?
|
||||||
"nicoseiga#{api_client.user_id}"
|
"nicoseiga#{api_client.user_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def tags
|
def tags
|
||||||
string = page.at("meta[name=keywords]").try(:[], "content") || ""
|
return [] if api_client.blank?
|
||||||
string.split(/,/).map do |name|
|
|
||||||
[name, "https://seiga.nicovideo.jp/tag/#{CGI.escape(name)}"]
|
base_url = "https://seiga.nicovideo.jp/"
|
||||||
|
base_url += "manga/" if manga_id.present?
|
||||||
|
base_url += "tag/"
|
||||||
|
|
||||||
|
api_client.tags.map do |name|
|
||||||
|
[name, base_url + CGI.escape(name)]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
memoize :tags
|
|
||||||
|
def image_id
|
||||||
|
image_id_from_url(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_id_from_url(url)
|
||||||
|
url[DIRECT, :image_id] || url[SOURCE, :image_id] || url[MANGA_THUMB, :image_id] || url[CDN_DIRECT, :image_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
def illust_id
|
||||||
|
urls.map { |u| u[ILLUST_PAGE, :illust_id] || u[ILLUST_THUMB, :illust_id] }.compact.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def manga_id
|
||||||
|
urls.compact.map { |u| u[MANGA_PAGE, :manga_id] }.compact.first
|
||||||
|
end
|
||||||
|
|
||||||
def api_client
|
def api_client
|
||||||
if illust_id
|
if illust_id.present?
|
||||||
NicoSeigaApiClient.new(illust_id: illust_id)
|
NicoSeigaApiClient.new(work_id: illust_id, type: "illust", http: http)
|
||||||
elsif theme_id
|
elsif manga_id.present?
|
||||||
NicoSeigaMangaApiClient.new(theme_id)
|
NicoSeigaApiClient.new(work_id: manga_id, type: "manga", http: http)
|
||||||
|
elsif image_id.present?
|
||||||
|
# We default to illust to attempt getting the api anyway
|
||||||
|
NicoSeigaApiClient.new(work_id: image_id, type: "illust", http: http)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
memoize :api_client
|
memoize :api_client
|
||||||
|
|
||||||
def illust_id
|
|
||||||
if page_url =~ PAGE
|
|
||||||
return $1.to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def theme_id
|
|
||||||
if page_url =~ MANGA_PAGE
|
|
||||||
return $1.to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def page
|
|
||||||
doc = agent.get(page_url)
|
|
||||||
|
|
||||||
if doc.search("a#link_btn_login").any?
|
|
||||||
# Session cache is invalid, clear it and log in normally.
|
|
||||||
Cache.delete("nico-seiga-session")
|
|
||||||
doc = agent.get(page_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
doc
|
|
||||||
end
|
|
||||||
memoize :page
|
|
||||||
|
|
||||||
def agent
|
|
||||||
NicoSeigaApiClient.agent
|
|
||||||
end
|
|
||||||
memoize :agent
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -44,25 +44,25 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class Nijie < Base
|
class Nijie < Base
|
||||||
BASE_URL = %r!\Ahttps?://(?:[^.]+\.)?nijie\.info!i
|
BASE_URL = %r{\Ahttps?://(?:[^.]+\.)?nijie\.info}i
|
||||||
PAGE_URL = %r!#{BASE_URL}/view(?:_popup)?\.php\?id=(?<illust_id>\d+)!i
|
PAGE_URL = %r{#{BASE_URL}/view(?:_popup)?\.php\?id=(?<illust_id>\d+)}i
|
||||||
PROFILE_URL = %r!#{BASE_URL}/members(?:_illust)?\.php\?id=(?<artist_id>\d+)\z!i
|
PROFILE_URL = %r{#{BASE_URL}/members(?:_illust)?\.php\?id=(?<artist_id>\d+)\z}i
|
||||||
|
|
||||||
# https://pic03.nijie.info/nijie_picture/28310_20131101215959.jpg
|
# https://pic03.nijie.info/nijie_picture/28310_20131101215959.jpg
|
||||||
# https://pic03.nijie.info/nijie_picture/236014_20170620101426_0.png
|
# https://pic03.nijie.info/nijie_picture/236014_20170620101426_0.png
|
||||||
# http://pic.nijie.net/03/nijie_picture/829001_20190620004513_0.mp4
|
# http://pic.nijie.net/03/nijie_picture/829001_20190620004513_0.mp4
|
||||||
# https://pic05.nijie.info/nijie_picture/diff/main/559053_20180604023346_1.png
|
# https://pic05.nijie.info/nijie_picture/diff/main/559053_20180604023346_1.png
|
||||||
FILENAME1 = %r!(?<artist_id>\d+)_(?<timestamp>\d{14})(?:_\d+)?!i
|
FILENAME1 = /(?<artist_id>\d+)_(?<timestamp>\d{14})(?:_\d+)?/i
|
||||||
|
|
||||||
# https://pic01.nijie.info/nijie_picture/diff/main/218856_0_236014_20170620101329.png
|
# https://pic01.nijie.info/nijie_picture/diff/main/218856_0_236014_20170620101329.png
|
||||||
FILENAME2 = %r!(?<illust_id>\d+)_\d+_(?<artist_id>\d+)_(?<timestamp>\d{14})!i
|
FILENAME2 = /(?<illust_id>\d+)_\d+_(?<artist_id>\d+)_(?<timestamp>\d{14})/i
|
||||||
|
|
||||||
# https://pic04.nijie.info/nijie_picture/diff/main/287736_161475_20181112032855_1.png
|
# https://pic04.nijie.info/nijie_picture/diff/main/287736_161475_20181112032855_1.png
|
||||||
FILENAME3 = %r!(?<illust_id>\d+)_(?<artist_id>\d+)_(?<timestamp>\d{14})_\d+!i
|
FILENAME3 = /(?<illust_id>\d+)_(?<artist_id>\d+)_(?<timestamp>\d{14})_\d+/i
|
||||||
|
|
||||||
IMAGE_BASE_URL = %r!\Ahttps?://(?:pic\d+\.nijie\.info|pic\.nijie\.net)!i
|
IMAGE_BASE_URL = %r{\Ahttps?://(?:pic\d+\.nijie\.info|pic\.nijie\.net)}i
|
||||||
DIR = %r!(?:\d+/)?(?:__rs_\w+/)?nijie_picture(?:/diff/main)?!
|
DIR = %r{(?:\d+/)?(?:__rs_\w+/)?nijie_picture(?:/diff/main)?}
|
||||||
IMAGE_URL = %r!#{IMAGE_BASE_URL}/#{DIR}/#{Regexp.union(FILENAME1, FILENAME2, FILENAME3)}\.\w+\z!i
|
IMAGE_URL = %r{#{IMAGE_BASE_URL}/#{DIR}/#{Regexp.union(FILENAME1, FILENAME2, FILENAME3)}\.\w+\z}i
|
||||||
|
|
||||||
def domains
|
def domains
|
||||||
["nijie.info", "nijie.net"]
|
["nijie.info", "nijie.net"]
|
||||||
@@ -146,7 +146,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def to_full_image_url(x)
|
def to_full_image_url(x)
|
||||||
x.gsub(%r!__rs_\w+/!i, "").gsub(/\Ahttp:/, "https:")
|
x.gsub(%r{__rs_\w+/}i, "").gsub(/\Ahttp:/, "https:")
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_preview_url(url)
|
def to_preview_url(url)
|
||||||
@@ -178,57 +178,21 @@ module Sources
|
|||||||
def page
|
def page
|
||||||
return nil if page_url.blank?
|
return nil if page_url.blank?
|
||||||
|
|
||||||
doc = agent.get(page_url)
|
http = Danbooru::Http.new
|
||||||
|
form = { email: Danbooru.config.nijie_login, password: Danbooru.config.nijie_password }
|
||||||
|
|
||||||
if doc.search("div#header-login-container").any?
|
# XXX `retriable` must come after `cache` so that retries don't return cached error responses.
|
||||||
# Session cache is invalid, clear it and log in normally.
|
response = http.cache(1.hour).use(retriable: { max_retries: 20 }).post("https://nijie.info/login_int.php", form: form)
|
||||||
Cache.delete("nijie-session")
|
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})" if response.status != 200
|
||||||
doc = agent.get(page_url)
|
return nil unless response.status == 200
|
||||||
end
|
|
||||||
|
|
||||||
return doc
|
response = http.cookies(R18: 1).cache(1.minute).get(page_url)
|
||||||
rescue Mechanize::ResponseCodeError => e
|
return nil unless response.status == 200
|
||||||
return nil if e.response_code.to_i == 404
|
|
||||||
raise
|
response&.parse
|
||||||
end
|
end
|
||||||
|
|
||||||
memoize :page
|
memoize :page
|
||||||
|
|
||||||
def agent
|
|
||||||
mech = Mechanize.new
|
|
||||||
|
|
||||||
session = Cache.get("nijie-session")
|
|
||||||
if session
|
|
||||||
cookie = Mechanize::Cookie.new("NIJIEIJIEID", session)
|
|
||||||
cookie.domain = ".nijie.info"
|
|
||||||
cookie.path = "/"
|
|
||||||
mech.cookie_jar.add(cookie)
|
|
||||||
else
|
|
||||||
mech.get("https://nijie.info/login.php") do |page|
|
|
||||||
page.form_with(:action => "/login_int.php") do |form|
|
|
||||||
form['email'] = Danbooru.config.nijie_login
|
|
||||||
form['password'] = Danbooru.config.nijie_password
|
|
||||||
end.click_button
|
|
||||||
end
|
|
||||||
session = mech.cookie_jar.cookies.select {|c| c.name == "NIJIEIJIEID"}.first
|
|
||||||
Cache.put("nijie-session", session.value, 1.day) if session
|
|
||||||
end
|
|
||||||
|
|
||||||
# This cookie needs to be set to allow viewing of adult works while anonymous
|
|
||||||
cookie = Mechanize::Cookie.new("R18", "1")
|
|
||||||
cookie.domain = ".nijie.info"
|
|
||||||
cookie.path = "/"
|
|
||||||
mech.cookie_jar.add(cookie)
|
|
||||||
|
|
||||||
mech
|
|
||||||
rescue Mechanize::ResponseCodeError => x
|
|
||||||
if x.response_code.to_i == 429
|
|
||||||
sleep(5)
|
|
||||||
retry
|
|
||||||
else
|
|
||||||
raise
|
|
||||||
end
|
|
||||||
end
|
|
||||||
memoize :agent
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module Sources
|
|||||||
when %r{\Ahttp://p\.twpl\.jp/show/(?:large|orig)/([a-z0-9]+)}i
|
when %r{\Ahttp://p\.twpl\.jp/show/(?:large|orig)/([a-z0-9]+)}i
|
||||||
"http://p.twipple.jp/#{$1}"
|
"http://p.twipple.jp/#{$1}"
|
||||||
|
|
||||||
when %r{\Ahttps?://blog(?:(?:-imgs-)?\d*(?:-origin)?)?\.fc2\.com/(?:(?:[^/]/){3}|(?:[^/]/))([^/]+)/(?:file/)?([^\.]+\.[^\?]+)}i
|
when %r{\Ahttps?://blog(?:(?:-imgs-)?\d*(?:-origin)?)?\.fc2\.com/(?:(?:[^/]/){3}|(?:[^/]/))([^/]+)/(?:file/)?([^.]+\.[^?]+)}i
|
||||||
username = $1
|
username = $1
|
||||||
filename = $2
|
filename = $2
|
||||||
"http://#{username}.blog.fc2.com/img/#{filename}/"
|
"http://#{username}.blog.fc2.com/img/#{filename}/"
|
||||||
@@ -47,7 +47,7 @@ module Sources
|
|||||||
when %r{\Ahttps?://c(?:s|han|[1-4])\.sankakucomplex\.com/data(?:/sample)?/(?:[a-f0-9]{2}/){2}(?:sample-|preview)?([a-f0-9]{32})}i
|
when %r{\Ahttps?://c(?:s|han|[1-4])\.sankakucomplex\.com/data(?:/sample)?/(?:[a-f0-9]{2}/){2}(?:sample-|preview)?([a-f0-9]{32})}i
|
||||||
"https://chan.sankakucomplex.com/en/post/show?md5=#{$1}"
|
"https://chan.sankakucomplex.com/en/post/show?md5=#{$1}"
|
||||||
|
|
||||||
when %r{\Ahttps?://(?:www|s(?:tatic|[1-4]))\.zerochan\.net/.+(?:\.|\/)(\d+)(?:\.(?:jpe?g?))?\z}i
|
when %r{\Ahttps?://(?:www|s(?:tatic|[1-4]))\.zerochan\.net/.+(?:\.|\/)(\d+)(?:\.(?:jpe?g?|png))?\z}i
|
||||||
"https://www.zerochan.net/#{$1}#full"
|
"https://www.zerochan.net/#{$1}#full"
|
||||||
|
|
||||||
when %r{\Ahttps?://static[1-6]?\.minitokyo\.net/(?:downloads|view)/(?:\d{2}/){2}(\d+)}i
|
when %r{\Ahttps?://static[1-6]?\.minitokyo\.net/(?:downloads|view)/(?:\d{2}/){2}(\d+)}i
|
||||||
@@ -105,7 +105,7 @@ module Sources
|
|||||||
# http://img.toranoana.jp/popup_img18/04/0010/22/87/040010228714-1p.jpg
|
# http://img.toranoana.jp/popup_img18/04/0010/22/87/040010228714-1p.jpg
|
||||||
# http://img.toranoana.jp/popup_blimg/04/0030/08/30/040030083068-1p.jpg
|
# http://img.toranoana.jp/popup_blimg/04/0030/08/30/040030083068-1p.jpg
|
||||||
# https://ecdnimg.toranoana.jp/ec/img/04/0030/65/34/040030653417-6p.jpg
|
# https://ecdnimg.toranoana.jp/ec/img/04/0030/65/34/040030653417-6p.jpg
|
||||||
when %r{\Ahttps?://(\w+\.)?toranoana\.jp/(?:popup_(?:bl)?img\d*|ec/img)/\d{2}/\d{4}/\d{2}/\d{2}/(?<work_id>\d+)}i
|
when %r{\Ahttps?://(?:\w+\.)?toranoana\.jp/(?:popup_(?:bl)?img\d*|ec/img)/\d{2}/\d{4}/\d{2}/\d{2}/(?<work_id>\d+)}i
|
||||||
"https://ec.toranoana.jp/tora_r/ec/item/#{$~[:work_id]}/"
|
"https://ec.toranoana.jp/tora_r/ec/item/#{$~[:work_id]}/"
|
||||||
|
|
||||||
# https://a.hitomi.la/galleries/907838/1.png
|
# https://a.hitomi.la/galleries/907838/1.png
|
||||||
|
|||||||
@@ -16,13 +16,13 @@
|
|||||||
|
|
||||||
module Sources::Strategies
|
module Sources::Strategies
|
||||||
class Pawoo < Base
|
class Pawoo < Base
|
||||||
HOST = %r!\Ahttps?://(www\.)?pawoo\.net!i
|
HOST = %r{\Ahttps?://(www\.)?pawoo\.net}i
|
||||||
IMAGE = %r!\Ahttps?://img\.pawoo\.net/media_attachments/files/(\d+/\d+/\d+)!
|
IMAGE = %r{\Ahttps?://img\.pawoo\.net/media_attachments/files/(\d+/\d+/\d+)}
|
||||||
NAMED_PROFILE = %r!#{HOST}/@(?<artist_name>\w+)!i
|
NAMED_PROFILE = %r{#{HOST}/@(?<artist_name>\w+)}i
|
||||||
ID_PROFILE = %r!#{HOST}/web/accounts/(?<artist_id>\d+)!
|
ID_PROFILE = %r{#{HOST}/web/accounts/(?<artist_id>\d+)}
|
||||||
|
|
||||||
STATUS1 = %r!\A#{HOST}/web/statuses/(?<status_id>\d+)!
|
STATUS1 = %r{\A#{HOST}/web/statuses/(?<status_id>\d+)}
|
||||||
STATUS2 = %r!\A#{NAMED_PROFILE}/(?<status_id>\d+)!
|
STATUS2 = %r{\A#{NAMED_PROFILE}/(?<status_id>\d+)}
|
||||||
|
|
||||||
def domains
|
def domains
|
||||||
["pawoo.net"]
|
["pawoo.net"]
|
||||||
@@ -37,15 +37,13 @@ module Sources::Strategies
|
|||||||
end
|
end
|
||||||
|
|
||||||
def image_urls
|
def image_urls
|
||||||
if url =~ %r!#{IMAGE}/small/([a-z0-9]+\.\w+)\z!i
|
if url =~ %r{#{IMAGE}/small/([a-z0-9]+\.\w+)\z}i
|
||||||
return ["https://img.pawoo.net/media_attachments/files/#{$1}/original/#{$2}"]
|
["https://img.pawoo.net/media_attachments/files/#{$1}/original/#{$2}"]
|
||||||
|
elsif url =~ %r{#{IMAGE}/original/([a-z0-9]+\.\w+)\z}i
|
||||||
|
[url]
|
||||||
|
else
|
||||||
|
api_response.image_urls
|
||||||
end
|
end
|
||||||
|
|
||||||
if url =~ %r!#{IMAGE}/original/([a-z0-9]+\.\w+)\z!i
|
|
||||||
return [url]
|
|
||||||
end
|
|
||||||
|
|
||||||
return api_response.image_urls
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def page_url
|
def page_url
|
||||||
@@ -55,16 +53,17 @@ module Sources::Strategies
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def profile_url
|
def profile_url
|
||||||
if url =~ PawooApiClient::PROFILE2
|
if url =~ PawooApiClient::PROFILE2
|
||||||
return "https://pawoo.net/@#{$1}"
|
"https://pawoo.net/@#{$1}"
|
||||||
|
elsif api_response.profile_url.blank?
|
||||||
|
url
|
||||||
|
else
|
||||||
|
api_response.profile_url
|
||||||
end
|
end
|
||||||
|
|
||||||
return url if api_response.profile_url.blank?
|
|
||||||
api_response.profile_url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def artist_name
|
def artist_name
|
||||||
@@ -87,10 +86,6 @@ module Sources::Strategies
|
|||||||
urls.map { |url| url[STATUS1, :status_id] || url[STATUS2, :status_id] }.compact.first
|
urls.map { |url| url[STATUS1, :status_id] || url[STATUS2, :status_id] }.compact.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def artist_commentary_title
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def artist_commentary_desc
|
def artist_commentary_desc
|
||||||
api_response.commentary
|
api_response.commentary
|
||||||
end
|
end
|
||||||
@@ -99,18 +94,10 @@ module Sources::Strategies
|
|||||||
api_response.tags
|
api_response.tags
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalizable_for_artist_finder?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_for_artist_finder
|
|
||||||
profile_url
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_for_source
|
def normalize_for_source
|
||||||
artist_name = artist_name_from_url
|
artist_name = artist_name_from_url
|
||||||
status_id = status_id_from_url
|
status_id = status_id_from_url
|
||||||
return unless status_id.present?
|
return if status_id.blank?
|
||||||
|
|
||||||
if artist_name.present?
|
if artist_name.present?
|
||||||
"https://pawoo.net/@#{artist_name}/#{status_id}"
|
"https://pawoo.net/@#{artist_name}/#{status_id}"
|
||||||
@@ -131,7 +118,7 @@ module Sources::Strategies
|
|||||||
|
|
||||||
def api_response
|
def api_response
|
||||||
[url, referer_url].each do |x|
|
[url, referer_url].each do |x|
|
||||||
if client = PawooApiClient.new.get(x)
|
if (client = PawooApiClient.new.get(x))
|
||||||
return client
|
return client
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,37 +50,34 @@
|
|||||||
module Sources
|
module Sources
|
||||||
module Strategies
|
module Strategies
|
||||||
class Pixiv < Base
|
class Pixiv < Base
|
||||||
MONIKER = %r!(?:[a-zA-Z0-9_-]+)!
|
MONIKER = /(?:[a-zA-Z0-9_-]+)/
|
||||||
PROFILE = %r!\Ahttps?://www\.pixiv\.net/member\.php\?id=[0-9]+\z!
|
PROFILE = %r{\Ahttps?://www\.pixiv\.net/member\.php\?id=[0-9]+\z}
|
||||||
DATE = %r!(?<date>\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2})!i
|
DATE = %r{(?<date>\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2})}i
|
||||||
EXT = %r!(?:jpg|jpeg|png|gif)!i
|
EXT = /(?:jpg|jpeg|png|gif)/i
|
||||||
|
|
||||||
WEB = %r!(?:\A(?:https?://)?www\.pixiv\.net)!
|
WEB = %r{(?:\A(?:https?://)?www\.pixiv\.net)}
|
||||||
I12 = %r!(?:\A(?:https?://)?i[0-9]+\.pixiv\.net)!
|
I12 = %r{(?:\A(?:https?://)?i[0-9]+\.pixiv\.net)}
|
||||||
IMG = %r!(?:\A(?:https?://)?img[0-9]*\.pixiv\.net)!
|
IMG = %r{(?:\A(?:https?://)?img[0-9]*\.pixiv\.net)}
|
||||||
PXIMG = %r!(?:\A(?:https?://)?[^.]+\.pximg\.net)!
|
PXIMG = %r{(?:\A(?:https?://)?[^.]+\.pximg\.net)}
|
||||||
TOUCH = %r!(?:\A(?:https?://)?touch\.pixiv\.net)!
|
TOUCH = %r{(?:\A(?:https?://)?touch\.pixiv\.net)}
|
||||||
UGOIRA = %r!#{PXIMG}/img-zip-ugoira/img/#{DATE}/(?<illust_id>\d+)_ugoira1920x1080\.zip\z!i
|
UGOIRA = %r{#{PXIMG}/img-zip-ugoira/img/#{DATE}/(?<illust_id>\d+)_ugoira1920x1080\.zip\z}i
|
||||||
ORIG_IMAGE = %r!#{PXIMG}/img-original/img/#{DATE}/(?<illust_id>\d+)_p(?<page>\d+)\.#{EXT}\z!i
|
ORIG_IMAGE = %r{#{PXIMG}/img-original/img/#{DATE}/(?<illust_id>\d+)_p(?<page>\d+)\.#{EXT}\z}i
|
||||||
STACC_PAGE = %r!\A#{WEB}/stacc/#{MONIKER}/?\z!i
|
STACC_PAGE = %r{\A#{WEB}/stacc/#{MONIKER}/?\z}i
|
||||||
NOVEL_PAGE = %r!(?:\Ahttps?://www\.pixiv\.net/novel/show\.php\?id=(\d+))!
|
NOVEL_PAGE = %r{(?:\Ahttps?://www\.pixiv\.net/novel/show\.php\?id=(\d+))}
|
||||||
FANBOX_ACCOUNT = %r!(?:\Ahttps?://www\.pixiv\.net/fanbox/creator/\d+\z)!
|
|
||||||
FANBOX_IMAGE = %r!(?:\Ahttps?://fanbox\.pixiv\.net/images/post/(\d+))!
|
|
||||||
FANBOX_PAGE = %r!(?:\Ahttps?://www\.pixiv\.net/fanbox/creator/\d+/post/(\d+))!
|
|
||||||
|
|
||||||
def self.to_dtext(text)
|
def self.to_dtext(text)
|
||||||
if text.nil?
|
if text.nil?
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
text = text.gsub(%r!https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)!i) do |match|
|
text = text.gsub(%r{https?://www\.pixiv\.net/member_illust\.php\?mode=medium&illust_id=([0-9]+)}i) do |_match|
|
||||||
pixiv_id = $1
|
pixiv_id = $1
|
||||||
%(pixiv ##{pixiv_id} "»":[/posts?tags=pixiv:#{pixiv_id}])
|
%(pixiv ##{pixiv_id} "»":[/posts?tags=pixiv:#{pixiv_id}])
|
||||||
end
|
end
|
||||||
|
|
||||||
text = text.gsub(%r!https?://www\.pixiv\.net/member\.php\?id=([0-9]+)!i) do |match|
|
text = text.gsub(%r{https?://www\.pixiv\.net/member\.php\?id=([0-9]+)}i) do |_match|
|
||||||
member_id = $1
|
member_id = $1
|
||||||
profile_url = "https://www.pixiv.net/member.php?id=#{member_id}"
|
profile_url = "https://www.pixiv.net/users/#{member_id}"
|
||||||
search_params = {"search[url_matches]" => profile_url}.to_param
|
search_params = {"search[url_matches]" => profile_url}.to_param
|
||||||
|
|
||||||
%("user/#{member_id}":[#{profile_url}] "»":[/artists?#{search_params}])
|
%("user/#{member_id}":[#{profile_url}] "»":[/artists?#{search_params}])
|
||||||
@@ -127,25 +124,17 @@ module Sources
|
|||||||
return "https://www.pixiv.net/novel/show.php?id=#{novel_id}&mode=cover"
|
return "https://www.pixiv.net/novel/show.php?id=#{novel_id}&mode=cover"
|
||||||
end
|
end
|
||||||
|
|
||||||
if fanbox_id.present?
|
|
||||||
return "https://www.pixiv.net/fanbox/creator/#{metadata.user_id}/post/#{fanbox_id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if fanbox_account_id.present?
|
|
||||||
return "https://www.pixiv.net/fanbox/creator/#{fanbox_account_id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if illust_id.present?
|
if illust_id.present?
|
||||||
return "https://www.pixiv.net/artworks/#{illust_id}"
|
return "https://www.pixiv.net/artworks/#{illust_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
return url
|
url
|
||||||
rescue PixivApiClient::BadIDError
|
rescue PixivApiClient::BadIDError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def canonical_url
|
def canonical_url
|
||||||
return image_url
|
image_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def profile_url
|
def profile_url
|
||||||
@@ -155,7 +144,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
"https://www.pixiv.net/member.php?id=#{metadata.user_id}"
|
"https://www.pixiv.net/users/#{metadata.user_id}"
|
||||||
rescue PixivApiClient::BadIDError
|
rescue PixivApiClient::BadIDError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -192,17 +181,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def headers
|
def headers
|
||||||
if fanbox_id.present?
|
{ "Referer" => "https://www.pixiv.net" }
|
||||||
# need the session to download fanbox images
|
|
||||||
return {
|
|
||||||
"Referer" => "https://www.pixiv.net/fanbox",
|
|
||||||
"Cookie" => HTTP::Cookie.cookie_value(agent.cookies)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
"Referer" => "https://www.pixiv.net"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_for_source
|
def normalize_for_source
|
||||||
@@ -231,7 +210,7 @@ module Sources
|
|||||||
translated_tags = super(tag)
|
translated_tags = super(tag)
|
||||||
|
|
||||||
if translated_tags.empty? && tag.include?("/")
|
if translated_tags.empty? && tag.include?("/")
|
||||||
translated_tags = tag.split("/").flat_map { |tag| super(tag) }
|
translated_tags = tag.split("/").flat_map { |translated_tag| super(translated_tag) }
|
||||||
end
|
end
|
||||||
|
|
||||||
translated_tags
|
translated_tags
|
||||||
@@ -242,10 +221,6 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def image_urls_sub
|
def image_urls_sub
|
||||||
if url =~ FANBOX_IMAGE
|
|
||||||
return [url]
|
|
||||||
end
|
|
||||||
|
|
||||||
# there's too much normalization bullshit we have to deal with
|
# there's too much normalization bullshit we have to deal with
|
||||||
# raw urls, so just fetch the canonical url from the api every
|
# raw urls, so just fetch the canonical url from the api every
|
||||||
# time.
|
# time.
|
||||||
@@ -257,7 +232,7 @@ module Sources
|
|||||||
return [ugoira_zip_url]
|
return [ugoira_zip_url]
|
||||||
end
|
end
|
||||||
|
|
||||||
return metadata.pages
|
metadata.pages
|
||||||
end
|
end
|
||||||
|
|
||||||
# in order to prevent recursive loops, this method should not make any
|
# in order to prevent recursive loops, this method should not make any
|
||||||
@@ -265,7 +240,7 @@ module Sources
|
|||||||
# even though it makes sense to reference page_url here, it will only look
|
# even though it makes sense to reference page_url here, it will only look
|
||||||
# at (url, referer_url).
|
# at (url, referer_url).
|
||||||
def illust_id
|
def illust_id
|
||||||
return nil if novel_id.present? || fanbox_id.present?
|
return nil if novel_id.present?
|
||||||
|
|
||||||
parsed_urls.each do |url|
|
parsed_urls.each do |url|
|
||||||
# http://www.pixiv.net/member_illust.php?mode=medium&illust_id=18557054
|
# http://www.pixiv.net/member_illust.php?mode=medium&illust_id=18557054
|
||||||
@@ -276,11 +251,11 @@ module Sources
|
|||||||
return url.query_values["illust_id"].to_i
|
return url.query_values["illust_id"].to_i
|
||||||
|
|
||||||
# http://www.pixiv.net/en/artworks/46324488
|
# http://www.pixiv.net/en/artworks/46324488
|
||||||
elsif url.host == "www.pixiv.net" && url.path =~ %r!\A/(?:en/)?artworks/(?<illust_id>\d+)!i
|
elsif url.host == "www.pixiv.net" && url.path =~ %r{\A/(?:en/)?artworks/(?<illust_id>\d+)}i
|
||||||
return $~[:illust_id].to_i
|
return $~[:illust_id].to_i
|
||||||
|
|
||||||
# http://www.pixiv.net/i/18557054
|
# http://www.pixiv.net/i/18557054
|
||||||
elsif url.host == "www.pixiv.net" && url.path =~ %r!\A/i/(?<illust_id>\d+)\z!i
|
elsif url.host == "www.pixiv.net" && url.path =~ %r{\A/i/(?<illust_id>\d+)\z}i
|
||||||
return $~[:illust_id].to_i
|
return $~[:illust_id].to_i
|
||||||
|
|
||||||
# http://img18.pixiv.net/img/evazion/14901720.png
|
# http://img18.pixiv.net/img/evazion/14901720.png
|
||||||
@@ -289,8 +264,8 @@ module Sources
|
|||||||
# http://i2.pixiv.net/img18/img/evazion/14901720_s.png
|
# http://i2.pixiv.net/img18/img/evazion/14901720_s.png
|
||||||
# http://i1.pixiv.net/img07/img/pasirism/18557054_p1.png
|
# http://i1.pixiv.net/img07/img/pasirism/18557054_p1.png
|
||||||
# http://i1.pixiv.net/img07/img/pasirism/18557054_big_p1.png
|
# http://i1.pixiv.net/img07/img/pasirism/18557054_big_p1.png
|
||||||
elsif url.host =~ %r!\A(?:i\d+|img\d+)\.pixiv\.net\z!i &&
|
elsif url.host =~ /\A(?:i\d+|img\d+)\.pixiv\.net\z/i &&
|
||||||
url.path =~ %r!\A(?:/img\d+)?/img/#{MONIKER}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)!i
|
url.path =~ %r{\A(?:/img\d+)?/img/#{MONIKER}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)}i
|
||||||
return $~[:illust_id].to_i
|
return $~[:illust_id].to_i
|
||||||
|
|
||||||
# http://i1.pixiv.net/img-inf/img/2011/05/01/23/28/04/18557054_64x64.jpg
|
# http://i1.pixiv.net/img-inf/img/2011/05/01/23/28/04/18557054_64x64.jpg
|
||||||
@@ -307,13 +282,13 @@ module Sources
|
|||||||
#
|
#
|
||||||
# https://i.pximg.net/novel-cover-original/img/2019/01/14/01/15/05/10617324_d84daae89092d96bbe66efafec136e42.jpg
|
# https://i.pximg.net/novel-cover-original/img/2019/01/14/01/15/05/10617324_d84daae89092d96bbe66efafec136e42.jpg
|
||||||
# https://img-sketch.pixiv.net/uploads/medium/file/4463372/8906921629213362989.jpg
|
# https://img-sketch.pixiv.net/uploads/medium/file/4463372/8906921629213362989.jpg
|
||||||
elsif url.host =~ %r!\A(?:[^.]+\.pximg\.net|i\d+\.pixiv\.net|tc-pximg01\.techorus-cdn\.com)\z!i &&
|
elsif url.host =~ /\A(?:[^.]+\.pximg\.net|i\d+\.pixiv\.net|tc-pximg01\.techorus-cdn\.com)\z/i &&
|
||||||
url.path =~ %r!\A(/c/\w+)?/img-[a-z-]+/img/#{DATE}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)!i
|
url.path =~ %r{\A(/c/\w+)?/img-[a-z-]+/img/#{DATE}/(?<illust_id>\d+)(?:_\w+)?\.(?:jpg|jpeg|png|gif|zip)}i
|
||||||
return $~[:illust_id].to_i
|
return $~[:illust_id].to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
nil
|
||||||
end
|
end
|
||||||
memoize :illust_id
|
memoize :illust_id
|
||||||
|
|
||||||
@@ -324,89 +299,48 @@ module Sources
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
nil
|
||||||
end
|
end
|
||||||
memoize :novel_id
|
memoize :novel_id
|
||||||
|
|
||||||
def fanbox_id
|
|
||||||
[url, referer_url].each do |x|
|
|
||||||
if x =~ FANBOX_PAGE
|
|
||||||
return $1
|
|
||||||
end
|
|
||||||
|
|
||||||
if x =~ FANBOX_IMAGE
|
|
||||||
return $1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
memoize :fanbox_id
|
|
||||||
|
|
||||||
def fanbox_account_id
|
|
||||||
[url, referer_url].each do |x|
|
|
||||||
if x =~ FANBOX_ACCOUNT
|
|
||||||
return x
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
memoize :fanbox_account_id
|
|
||||||
|
|
||||||
def agent
|
|
||||||
PixivWebAgent.build
|
|
||||||
end
|
|
||||||
memoize :agent
|
|
||||||
|
|
||||||
def metadata
|
def metadata
|
||||||
if novel_id.present?
|
if novel_id.present?
|
||||||
return PixivApiClient.new.novel(novel_id)
|
return PixivApiClient.new.novel(novel_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
if fanbox_id.present?
|
PixivApiClient.new.work(illust_id)
|
||||||
return PixivApiClient.new.fanbox(fanbox_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
return PixivApiClient.new.work(illust_id)
|
|
||||||
end
|
end
|
||||||
memoize :metadata
|
memoize :metadata
|
||||||
|
|
||||||
def moniker
|
def moniker
|
||||||
# we can sometimes get the moniker from the url
|
# we can sometimes get the moniker from the url
|
||||||
if url =~ %r!#{IMG}/img/(#{MONIKER})!i
|
if url =~ %r{#{IMG}/img/(#{MONIKER})}i
|
||||||
return $1
|
$1
|
||||||
|
elsif url =~ %r{#{I12}/img[0-9]+/img/(#{MONIKER})}i
|
||||||
|
$1
|
||||||
|
elsif url =~ %r{#{WEB}/stacc/(#{MONIKER})/?$}i
|
||||||
|
$1
|
||||||
|
else
|
||||||
|
metadata.moniker
|
||||||
end
|
end
|
||||||
|
|
||||||
if url =~ %r!#{I12}/img[0-9]+/img/(#{MONIKER})!i
|
|
||||||
return $1
|
|
||||||
end
|
|
||||||
|
|
||||||
if url =~ %r!#{WEB}/stacc/(#{MONIKER})/?$!i
|
|
||||||
return $1
|
|
||||||
end
|
|
||||||
|
|
||||||
return metadata.moniker
|
|
||||||
rescue PixivApiClient::BadIDError
|
rescue PixivApiClient::BadIDError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
memoize :moniker
|
memoize :moniker
|
||||||
|
|
||||||
def data
|
def data
|
||||||
return {
|
{ ugoira_frame_data: ugoira_frame_data }
|
||||||
ugoira_frame_data: ugoira_frame_data
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ugoira_zip_url
|
def ugoira_zip_url
|
||||||
if metadata.pages.is_a?(Hash) && metadata.pages["ugoira600x600"]
|
if metadata.pages.is_a?(Hash) && metadata.pages["ugoira600x600"]
|
||||||
return metadata.pages["ugoira600x600"].sub("_ugoira600x600.zip", "_ugoira1920x1080.zip")
|
metadata.pages["ugoira600x600"].sub("_ugoira600x600.zip", "_ugoira1920x1080.zip")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
memoize :ugoira_zip_url
|
memoize :ugoira_zip_url
|
||||||
|
|
||||||
def ugoira_frame_data
|
def ugoira_frame_data
|
||||||
return metadata.json.dig("metadata", "frames")
|
metadata.json.dig("metadata", "frames")
|
||||||
rescue PixivApiClient::BadIDError
|
rescue PixivApiClient::BadIDError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -415,16 +349,14 @@ module Sources
|
|||||||
def ugoira_content_type
|
def ugoira_content_type
|
||||||
case metadata.json["image_urls"].to_s
|
case metadata.json["image_urls"].to_s
|
||||||
when /\.jpg/
|
when /\.jpg/
|
||||||
return "image/jpeg"
|
"image/jpeg"
|
||||||
|
|
||||||
when /\.png/
|
when /\.png/
|
||||||
return "image/png"
|
"image/png"
|
||||||
|
|
||||||
when /\.gif/
|
when /\.gif/
|
||||||
return "image/gif"
|
"image/gif"
|
||||||
|
else
|
||||||
|
raise Sources::Error, "content type not found for (#{url}, #{referer_url})"
|
||||||
end
|
end
|
||||||
|
|
||||||
raise Sources::Error.new("content type not found for (#{url}, #{referer_url})")
|
|
||||||
end
|
end
|
||||||
memoize :ugoira_content_type
|
memoize :ugoira_content_type
|
||||||
|
|
||||||
@@ -434,7 +366,7 @@ module Sources
|
|||||||
# http://i2.pixiv.net/img04/img/syounen_no_uta/46170939_p0.jpg
|
# http://i2.pixiv.net/img04/img/syounen_no_uta/46170939_p0.jpg
|
||||||
# http://i1.pixiv.net/c/600x600/img-master/img/2014/09/24/23/25/08/46168376_p0_master1200.jpg
|
# http://i1.pixiv.net/c/600x600/img-master/img/2014/09/24/23/25/08/46168376_p0_master1200.jpg
|
||||||
# http://i1.pixiv.net/img-original/img/2014/09/25/23/09/29/46183440_p0.jpg
|
# http://i1.pixiv.net/img-original/img/2014/09/25/23/09/29/46183440_p0.jpg
|
||||||
if url =~ %r!/\d+_p(\d+)(?:_\w+)?\.#{EXT}!i
|
if url =~ %r{/\d+_p(\d+)(?:_\w+)?\.#{EXT}}i
|
||||||
return $1.to_i
|
return $1.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -445,7 +377,7 @@ module Sources
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
nil
|
||||||
end
|
end
|
||||||
memoize :manga_page
|
memoize :manga_page
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ module Sources::Strategies
|
|||||||
class Tumblr < Base
|
class Tumblr < Base
|
||||||
SIZES = %w[1280 640 540 500h 500 400 250 100]
|
SIZES = %w[1280 640 540 500h 500 400 250 100]
|
||||||
|
|
||||||
BASE_URL = %r!\Ahttps?://(?:[^/]+\.)*tumblr\.com!i
|
BASE_URL = %r{\Ahttps?://(?:[^/]+\.)*tumblr\.com}i
|
||||||
DOMAIN = %r{(data|(\d+\.)?media)\.tumblr\.com}
|
DOMAIN = /(data|(?:\d+\.)?media)\.tumblr\.com/i
|
||||||
MD5 = %r{(?<md5>[0-9a-f]{32})}i
|
MD5 = /(?<md5>[0-9a-f]{32})/i
|
||||||
FILENAME = %r{(?<filename>(tumblr_(inline_)?)?[a-z0-9]+(_r[0-9]+)?)}i
|
FILENAME = /(?<filename>(?:tumblr_(?:inline_)?)?[a-z0-9]+(?:_r[0-9]+)?)/i
|
||||||
EXT = %r{(?<ext>\w+)}
|
EXT = /(?<ext>\w+)/
|
||||||
|
|
||||||
# old: https://66.media.tumblr.com/2c6f55531618b4335c67e29157f5c1fc/tumblr_pz4a44xdVj1ssucdno1_1280.png
|
# old: https://66.media.tumblr.com/2c6f55531618b4335c67e29157f5c1fc/tumblr_pz4a44xdVj1ssucdno1_1280.png
|
||||||
# new: https://66.media.tumblr.com/168dabd09d5ad69eb5fedcf94c45c31a/3dbfaec9b9e0c2e3-72/s640x960/bf33a1324f3f36d2dc64f011bfeab4867da62bc8.png
|
# new: https://66.media.tumblr.com/168dabd09d5ad69eb5fedcf94c45c31a/3dbfaec9b9e0c2e3-72/s640x960/bf33a1324f3f36d2dc64f011bfeab4867da62bc8.png
|
||||||
OLD_IMAGE = %r!\Ahttps?://#{DOMAIN}/(?<dir>#{MD5}/)?#{FILENAME}_(?<size>\w+)\.#{EXT}\z!i
|
OLD_IMAGE = %r{\Ahttps?://#{DOMAIN}/(?<dir>#{MD5}/)?#{FILENAME}_(?<size>\w+)\.#{EXT}\z}i
|
||||||
|
|
||||||
IMAGE = %r!\Ahttps?://#{DOMAIN}/!i
|
IMAGE = %r{\Ahttps?://#{DOMAIN}/}i
|
||||||
VIDEO = %r!\Ahttps?://(?:vtt|ve\.media)\.tumblr\.com/!i
|
VIDEO = %r{\Ahttps?://(?:vtt|ve|va\.media)\.tumblr\.com/}i
|
||||||
POST = %r!\Ahttps?://(?<blog_name>[^.]+)\.tumblr\.com/(?:post|image)/(?<post_id>\d+)!i
|
POST = %r{\Ahttps?://(?<blog_name>[^.]+)\.tumblr\.com/(?:post|image)/(?<post_id>\d+)}i
|
||||||
|
|
||||||
def self.enabled?
|
def self.enabled?
|
||||||
Danbooru.config.tumblr_consumer_key.present?
|
Danbooru.config.tumblr_consumer_key.present?
|
||||||
@@ -68,7 +68,7 @@ module Sources::Strategies
|
|||||||
|
|
||||||
def preview_urls
|
def preview_urls
|
||||||
image_urls.map do |x|
|
image_urls.map do |x|
|
||||||
x.sub(%r!_1280\.(jpg|png|gif|jpeg)\z!, '_250.\1')
|
x.sub(/_1280\.(jpg|png|gif|jpeg)\z/, '_250.\1')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ module Sources::Strategies
|
|||||||
end
|
end
|
||||||
|
|
||||||
candidates.find do |candidate|
|
candidates.find do |candidate|
|
||||||
http_exists?(candidate, headers)
|
http_exists?(candidate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
module Sources::Strategies
|
module Sources::Strategies
|
||||||
class Twitter < Base
|
class Twitter < Base
|
||||||
PAGE = %r!\Ahttps?://(?:mobile\.)?twitter\.com!i
|
PAGE = %r{\Ahttps?://(?:mobile\.)?twitter\.com}i
|
||||||
PROFILE = %r!\Ahttps?://(?:mobile\.)?twitter.com/(?<username>[a-z0-9_]+)!i
|
PROFILE = %r{\Ahttps?://(?:mobile\.)?twitter.com/(?<username>[a-z0-9_]+)}i
|
||||||
|
|
||||||
# https://pbs.twimg.com/media/EBGbJe_U8AA4Ekb.jpg
|
# https://pbs.twimg.com/media/EBGbJe_U8AA4Ekb.jpg
|
||||||
# https://pbs.twimg.com/media/EBGbJe_U8AA4Ekb?format=jpg&name=900x900
|
# https://pbs.twimg.com/media/EBGbJe_U8AA4Ekb?format=jpg&name=900x900
|
||||||
# https://pbs.twimg.com/tweet_video_thumb/ETkN_L3X0AMy1aT.jpg
|
# https://pbs.twimg.com/tweet_video_thumb/ETkN_L3X0AMy1aT.jpg
|
||||||
# https://pbs.twimg.com/ext_tw_video_thumb/1243725361986375680/pu/img/JDA7g7lcw7wK-PIv.jpg
|
# https://pbs.twimg.com/ext_tw_video_thumb/1243725361986375680/pu/img/JDA7g7lcw7wK-PIv.jpg
|
||||||
# https://pbs.twimg.com/amplify_video_thumb/1215590775364259840/img/lolCkEEioFZTb5dl.jpg
|
# https://pbs.twimg.com/amplify_video_thumb/1215590775364259840/img/lolCkEEioFZTb5dl.jpg
|
||||||
BASE_IMAGE_URL = %r!\Ahttps?://pbs\.twimg\.com/(?<media_type>media|tweet_video_thumb|ext_tw_video_thumb|amplify_video_thumb)!i
|
BASE_IMAGE_URL = %r{\Ahttps?://pbs\.twimg\.com/(?<media_type>media|tweet_video_thumb|ext_tw_video_thumb|amplify_video_thumb)}i
|
||||||
FILENAME1 = %r!(?<file_name>[a-zA-Z0-9_-]+)\.(?<file_ext>\w+)!i
|
FILENAME1 = /(?<file_name>[a-zA-Z0-9_-]+)\.(?<file_ext>\w+)/i
|
||||||
FILENAME2 = %r!(?<file_name>[a-zA-Z0-9_-]+)\?.*format=(?<file_ext>\w+)!i
|
FILENAME2 = /(?<file_name>[a-zA-Z0-9_-]+)\?.*format=(?<file_ext>\w+)/i
|
||||||
FILEPATH1 = %r!(?<file_path>\d+/[\w_-]+/img)!i
|
FILEPATH1 = %r{(?<file_path>\d+/[\w_-]+/img)}i
|
||||||
FILEPATH2 = %r!(?<file_path>\d+/img)!i
|
FILEPATH2 = %r{(?<file_path>\d+/img)}i
|
||||||
IMAGE_URL1 = %r!#{BASE_IMAGE_URL}/#{Regexp.union(FILENAME1, FILENAME2)}!i
|
IMAGE_URL1 = %r{#{BASE_IMAGE_URL}/#{Regexp.union(FILENAME1, FILENAME2)}}i
|
||||||
IMAGE_URL2 = %r!#{BASE_IMAGE_URL}/#{Regexp.union(FILEPATH1, FILEPATH2)}/#{FILENAME1}!i
|
IMAGE_URL2 = %r{#{BASE_IMAGE_URL}/#{Regexp.union(FILEPATH1, FILEPATH2)}/#{FILENAME1}}i
|
||||||
|
|
||||||
# Twitter provides a list but it's inaccurate; some names ('intent') aren't
|
# Twitter provides a list but it's inaccurate; some names ('intent') aren't
|
||||||
# included and other names in the list aren't actually reserved.
|
# included and other names in the list aren't actually reserved.
|
||||||
@@ -47,7 +47,7 @@ module Sources::Strategies
|
|||||||
return $1
|
return $1
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.artist_name_from_url(url)
|
def self.artist_name_from_url(url)
|
||||||
@@ -78,7 +78,7 @@ module Sources::Strategies
|
|||||||
elsif media[:type].in?(["video", "animated_gif"])
|
elsif media[:type].in?(["video", "animated_gif"])
|
||||||
variants = media.dig(:video_info, :variants)
|
variants = media.dig(:video_info, :variants)
|
||||||
videos = variants.select { |variant| variant[:content_type] == "video/mp4" }
|
videos = variants.select { |variant| variant[:content_type] == "video/mp4" }
|
||||||
video = videos.max_by { |video| video[:bitrate].to_i }
|
video = videos.max_by { |v| v[:bitrate].to_i }
|
||||||
video[:url]
|
video[:url]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -137,10 +137,6 @@ module Sources::Strategies
|
|||||||
api_response[:full_text].to_s
|
api_response[:full_text].to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalizable_for_artist_finder?
|
|
||||||
url =~ PAGE
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalize_for_artist_finder
|
def normalize_for_artist_finder
|
||||||
profile_url.try(:downcase).presence || url
|
profile_url.try(:downcase).presence || url
|
||||||
end
|
end
|
||||||
@@ -193,9 +189,9 @@ module Sources::Strategies
|
|||||||
|
|
||||||
desc = artist_commentary_desc.unicode_normalize(:nfkc)
|
desc = artist_commentary_desc.unicode_normalize(:nfkc)
|
||||||
desc = CGI.unescapeHTML(desc)
|
desc = CGI.unescapeHTML(desc)
|
||||||
desc = desc.gsub(%r!https?://t\.co/[a-zA-Z0-9]+!i, url_replacements)
|
desc = desc.gsub(%r{https?://t\.co/[a-zA-Z0-9]+}i, url_replacements)
|
||||||
desc = desc.gsub(%r!#([^[:space:]]+)!, '"#\\1":[https://twitter.com/hashtag/\\1]')
|
desc = desc.gsub(/#([^[:space:]]+)/, '"#\\1":[https://twitter.com/hashtag/\\1]')
|
||||||
desc = desc.gsub(%r!@([a-zA-Z0-9_]+)!, '"@\\1":[https://twitter.com/\\1]')
|
desc = desc.gsub(/@([a-zA-Z0-9_]+)/, '"@\\1":[https://twitter.com/\\1]')
|
||||||
desc.strip
|
desc.strip
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -204,7 +200,7 @@ module Sources::Strategies
|
|||||||
end
|
end
|
||||||
|
|
||||||
def api_response
|
def api_response
|
||||||
return {} if !self.class.enabled?
|
return {} unless self.class.enabled? && status_id.present?
|
||||||
api_client.status(status_id)
|
api_client.status(status_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ module Sources
|
|||||||
|
|
||||||
PAGE_URL_1 = %r{\Ahttps?://(?:www\.)?weibo\.com/(?<artist_short_id>\d+)/(?<illust_base62_id>\w+)(?:\?.*)?\z}i
|
PAGE_URL_1 = %r{\Ahttps?://(?:www\.)?weibo\.com/(?<artist_short_id>\d+)/(?<illust_base62_id>\w+)(?:\?.*)?\z}i
|
||||||
PAGE_URL_2 = %r{#{PROFILE_URL_2}/(?:wbphotos/large/mid|talbum/detail/photo_id)/(?<illust_long_id>\d+)(?:/pid/(?<image_id>\w{32}))?}i
|
PAGE_URL_2 = %r{#{PROFILE_URL_2}/(?:wbphotos/large/mid|talbum/detail/photo_id)/(?<illust_long_id>\d+)(?:/pid/(?<image_id>\w{32}))?}i
|
||||||
PAGE_URL_3 = %r{\Ahttps?://m\.weibo\.cn/(detail/(?<illust_long_id>\d+)|status/(?<illust_base62_id>\w+))}i
|
PAGE_URL_3 = %r{\Ahttps?://m\.weibo\.cn/(?:detail/(?<illust_long_id>\d+)|status/(?<illust_base62_id>\w+))}i
|
||||||
PAGE_URL_4 = %r{\Ahttps?://tw\.weibo\.com/(?:(?<artist_short_id>\d+)|\w+)/(?<illust_long_id>\d+)}i
|
PAGE_URL_4 = %r{\Ahttps?://tw\.weibo\.com/(?:(?<artist_short_id>\d+)|\w+)/(?<illust_long_id>\d+)}i
|
||||||
|
|
||||||
IMAGE_URL = %r{\Ahttps?://\w{3}\.sinaimg\.cn/\w+/(?<image_id>\w{32})\.}i
|
IMAGE_URL = %r{\Ahttps?://\w{3}\.sinaimg\.cn/\w+/(?<image_id>\w{32})\.}i
|
||||||
@@ -203,12 +203,12 @@ module Sources
|
|||||||
end
|
end
|
||||||
|
|
||||||
def api_response
|
def api_response
|
||||||
return nil if mobile_url.blank?
|
return {} if mobile_url.blank?
|
||||||
|
|
||||||
resp = Danbooru::Http.cache(1.minute).get(mobile_url)
|
resp = Danbooru::Http.cache(1.minute).get(mobile_url)
|
||||||
json_string = resp.to_s[/var \$render_data = \[(.*)\]\[0\]/m, 1]
|
json_string = resp.to_s[/var \$render_data = \[(.*)\]\[0\]/m, 1]
|
||||||
|
|
||||||
return nil if json_string.blank?
|
return {} if json_string.blank?
|
||||||
|
|
||||||
JSON.parse(json_string)["status"]
|
JSON.parse(json_string)["status"]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ class SpamDetector
|
|||||||
# if a person receives more than 10 automatic spam reports within a 1 hour
|
# if a person receives more than 10 automatic spam reports within a 1 hour
|
||||||
# window, automatically ban them forever.
|
# window, automatically ban them forever.
|
||||||
AUTOBAN_THRESHOLD = 10
|
AUTOBAN_THRESHOLD = 10
|
||||||
AUTOBAN_WINDOW = 1.hours
|
AUTOBAN_WINDOW = 1.hour
|
||||||
AUTOBAN_DURATION = 999999
|
AUTOBAN_DURATION = 999_999
|
||||||
|
|
||||||
attr_accessor :record, :user, :user_ip, :content, :comment_type
|
attr_accessor :record, :user, :user_ip, :content, :comment_type
|
||||||
|
|
||||||
rakismet_attrs author: proc { user.name },
|
rakismet_attrs author: proc { user.name },
|
||||||
author_email: proc { user.email_address&.address },
|
author_email: proc { user.email_address&.address },
|
||||||
blog_lang: "en",
|
blog_lang: "en",
|
||||||
@@ -84,8 +85,8 @@ class SpamDetector
|
|||||||
end
|
end
|
||||||
|
|
||||||
is_spam
|
is_spam
|
||||||
rescue StandardError => exception
|
rescue StandardError => e
|
||||||
DanbooruLogger.log(exception)
|
DanbooruLogger.log(e)
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user