Merge remote-tracking branch 'origin/master' into rails-7

This commit is contained in:
Earlopain 2022-10-23 19:35:57 +02:00
commit dd3fb80b8c
No known key found for this signature in database
GPG Key ID: 6CFB948E15246897
96 changed files with 1270 additions and 897 deletions

View File

@ -68,3 +68,10 @@
# export DANBOORU_VERSION=
# export DANBOORU_HOSTNAME=
# export DANBOORU_IQDBS_SERVER=
#
# Development Only
#
# Start the integrated solargraph service from the compose file. Requires a rebuild when changed.
# export COMPOSE_PROFILES=solargraph

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.env.*
.env
.bundle
.yardoc
config/database.yml

View File

@ -4,8 +4,8 @@ require:
AllCops:
NewCops: enable
Exclude:
- "bin/*"
- "node_modules/**/*"
- bin/*
- node_modules/**/*
Bundler/OrderedGems:
Enabled: false
@ -13,23 +13,30 @@ Bundler/OrderedGems:
Layout/EmptyLineAfterGuardClause:
Enabled: false
Layout/EmptyLineBetweenDefs:
Enabled: false
Layout/FirstArrayElementIndentation:
EnforcedStyle: consistent
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Layout/LineLength:
Enabled: false
Lint/InheritException:
Enabled: false
Lint/RescueException:
Enabled: false
Lint/SymbolConversion:
EnforcedStyle: consistent
Metrics/AbcSize:
Enabled: false
Metrics/BlockLength:
Enabled: false
AllowedMethods:
- class_methods
- concerning
- context
- create_table
- should
Exclude:
- config/routes.rb
Metrics/ClassLength:
Enabled: false
@ -49,15 +56,28 @@ Metrics/PerceivedComplexity:
Naming/PredicateName:
Enabled: false
Rails/BulkChangeTable:
Enabled: false
Rails/HasManyOrHasOneDependent:
Enabled: false
Rails/HttpStatus:
EnforcedStyle: numeric
Rails/I18nLocaleTexts:
Enabled: false
Rails/InverseOf:
Enabled: false
Rails/Output:
Exclude:
- db/*.rb
Rails/ReversibleMigration:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
@ -85,14 +105,17 @@ Style/FloatDivision:
Style/FrozenStringLiteralComment:
Enabled: false
Style/GuardClause:
Enabled: false
Style/HashSyntax:
EnforcedShorthandSyntax: never
Style/IfUnlessModifier:
Enabled: false
Style/MutableConstant:
Enabled: false
Style/NumericPredicate:
Enabled: false
EnforcedStyle: comparison
Style/PerlBackrefs:
Enabled: false
@ -100,17 +123,14 @@ Style/PerlBackrefs:
Style/QuotedSymbols:
Enabled: false
Style/RescueStandardError:
Enabled: false
Style/StringLiterals:
Enabled: false
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
Style/TrailingCommaInArguments:
EnforcedStyleForMultiline: comma
Style/SymbolArray:
Enabled: false
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/WordArray:
Enabled: false
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: consistent_comma

View File

@ -1,31 +1,12 @@
---
include:
- "**/*.rb"
exclude:
- spec/**/*
- test/**/*
- vendor/**/*
- ".bundle/**/*"
require:
- actioncable
- actionmailer
- actionpack
- actionview
- activejob
- activemodel
- activerecord
- activestorage
- activesupport
domains: []
- actioncable
- actionmailer
- actionpack
- actionview
- activejob
- activemodel
- activerecord
- activestorage
- activesupport
reporters:
- rubocop
- require_not_found
formatter:
rubocop:
cops: safe
except: []
only: []
extra_args: []
require_paths: []
plugins: []
max_files: 5000
- rubocop

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"solargraph.checkGemVersion": false,
"solargraph.diagnostics": true,
"solargraph.externalServer": {
"host": "localhost",
"port": 7658
},
"solargraph.formatting": true,
"solargraph.transport": "external",
}

View File

@ -26,5 +26,15 @@ RUN gem install bundler:2.3.12 && \
RUN wget -O /usr/bin/shoreman https://github.com/chrismytton/shoreman/raw/master/shoreman.sh \
&& chmod +x /usr/bin/shoreman
# Only setup solargraph stuff when the profile is selected
ARG COMPOSE_PROFILES
RUN if [[ $COMPOSE_PROFILES == *"solargraph"* ]]; then \
solargraph download-core && bundle exec yard gems && solargraph bundle; \
fi
# Stop bin/rails console from offering autocomplete
RUN echo "IRB.conf[:USE_AUTOCOMPLETE] = false" > ~/.irbrc
WORKDIR /app
CMD [ "shoreman" ]

View File

@ -53,6 +53,12 @@ group :development, :test do
gem 'puma'
end
group :docker do
gem "rubocop", require: false
gem "rubocop-rails", require: false
gem "solargraph", require: false
end
group :test do
gem "shoulda-context"
gem "shoulda-matchers"

View File

@ -85,7 +85,10 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
backport (1.2.0)
bcrypt (3.1.18)
benchmark (0.2.0)
bootsnap (1.13.0)
msgpack (~> 1.2)
brpoplpush-redis_script (0.1.2)
@ -118,6 +121,7 @@ GEM
activesupport (>= 5.0)
request_store (>= 1.0)
ruby2_keywords
e2mmap (0.1.0)
elasticsearch (7.17.1)
elasticsearch-api (= 7.17.1)
elasticsearch-transport (= 7.17.1)
@ -174,9 +178,14 @@ GEM
multi_xml (>= 0.5.2)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.4)
json (2.6.2)
jsonapi-renderer (0.2.2)
kgio (2.11.4)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@ -217,6 +226,9 @@ GEM
nokogiri (1.13.8)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
parallel (1.22.1)
parser (3.1.2.1)
ast (~> 2.4.1)
pg (1.4.3)
pry (0.14.1)
coderay (~> 1.1)
@ -259,6 +271,7 @@ GEM
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
raindrops (0.20.0)
rake (13.0.6)
rb-fsevent (0.11.2)
@ -267,6 +280,7 @@ GEM
recaptcha (5.12.3)
json
redis (4.8.0)
regexp_parser (2.6.0)
request_store (1.5.1)
rack (>= 1.4)
resolv (0.2.1)
@ -285,7 +299,26 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.5)
rubocop (1.37.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.22.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.23.0)
parser (>= 3.1.1.0)
rubocop-rails (2.17.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
ruby-progressbar (1.11.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
@ -308,9 +341,25 @@ GEM
simple_form (5.1.0)
actionpack (>= 5.2)
activemodel (>= 5.2)
solargraph (0.47.2)
backport (~> 1.2)
benchmark
bundler (>= 1.17.2)
diff-lcs (~> 1.4)
e2mmap
jaro_winkler (~> 1.5)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1)
parser (~> 3.0)
reverse_markdown (>= 1.0.5, < 3)
rubocop (>= 0.52)
thor (~> 1.0)
tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
thor (1.2.1)
tilt (2.0.11)
timecop (0.9.5)
timeout (0.3.0)
tzinfo (2.0.5)
@ -318,6 +367,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.3.0)
unicorn (6.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@ -333,11 +383,14 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.7.0)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
whenever (1.0.0)
chronic (>= 0.6.3)
yard (0.9.28)
webrick (~> 1.7.0)
zeitwerk (2.6.1)
PLATFORMS
@ -377,6 +430,8 @@ DEPENDENCIES
resolv
responders
retriable
rubocop
rubocop-rails
ruby-vips
sanitize
shoulda-context
@ -384,6 +439,7 @@ DEPENDENCIES
sidekiq
sidekiq-unique-jobs
simple_form
solargraph
streamio-ffmpeg
timecop
unicorn

View File

@ -15,22 +15,30 @@ To mitigate this you can install a WSL distribution and clone the project inside
#### Installation
1. Download and install the prerequisites.
2. Clone the repo with `git clone https://github.com/zwagoth/e621ng.git`.
3. `cd` into the repo.
4. Run the following commands:
1. Clone the repo with `git clone https://github.com/zwagoth/e621ng.git`.
1. `cd` into the repo.
1. Copy the sample environment file with `cp .env.sample .env`.
1. Uncomment the `COMPOSE_PROFILES` variable if you wish to use solargraph. Doesn't work on Windows without WSL.
1. Run the following commands:
```
docker-compose run -e DANBOORU_DISABLE_THROTTLES=true -e SEED_POST_COUNT=100 e621 /app/bin/setup
docker-compose up
```
After running the commands once only `docker-compose up` is needed to bring up the containers.
5. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.<br>
1. This would be a good time to rewatch your favorite TV series installment, cook & have breakfast/lunch/dinner, walk the dog, clean your room, etc.<br>
By the time you get back the install will surely have completed.<sup>1</sup>
6. To confirm the installation worked, open the web browser of your choice and enter `http://localhost:3000` into the address bar and see if the website loads correctly. An admin account has been created automatically, the username and password are `admin` and `e621test` respectively.
1. To confirm the installation worked, open the web browser of your choice and enter `http://localhost:3000` into the address bar and see if the website loads correctly. An admin account has been created automatically, the username and password are `admin` and `e621test` respectively.
Note: When gems or js packages were updated you need to execute `docker-compose build` to reflect them in the container.
<sub><sup>1</sup> If the install did not finish by the time an activity is complete please select another activity to avoid crippling boredom.</sub>
#### Useful docker services
`docker-compose run --rm tests` to execute the test suite.
`docker-compose run --rm rubocop` to run the linter. Run it against changed files only, there are too many existing violations at the moment.
#### Development Database
The postgres server accepts outside connections which you can use to connect with a local client. Use `localhost:34517` to connect to a database named `danbooru2` with the user `danbooru`. Leave the password blank, anything will work.
@ -66,9 +74,3 @@ debug your Nginx configuration file.
### IQDB Service
IQDB integration is delegated to the [IQDBS service](https://github.com/zwagoth/iqdbs).
### Cropped Thumbnails
There's optional support for cropped thumbnails. This relies on installing
`libvips-8.6` or higher and setting `Danbooru.config.enable_image_cropping?`
to true.

View File

@ -28,6 +28,8 @@ ORDER BY u1.id DESC, u2.last_logged_in_at DESC;")
def update
@user = User.find(params[:id])
@user.validate_email_format = true
@user.skip_email_blank_check = true
@user.update!(user_params)
if @user.saved_change_to_profile_about || @user.saved_change_to_profile_artinfo
ModAction.log(:user_text_change, { user_id: @user.id })
@ -37,8 +39,6 @@ ORDER BY u1.id DESC, u2.last_logged_in_at DESC;")
end
@user.mark_verified! if params[:user][:verified] == 'true'
@user.mark_unverified! if params[:user][:verified] == 'false'
params[:user][:is_upgrade] = true
params[:user][:skip_dmail] = true
@user.promote_to!(params[:user][:level], params[:user])
old_username = @user.name

View File

@ -32,12 +32,6 @@ class ApplicationController < ActionController::Base
protected
def self.rescue_with(*klasses, status: 500)
rescue_from *klasses do |exception|
render_error_page(status, exception)
end
end
def api_check
if !CurrentUser.is_anonymous? && !request.get? && !request.head?
throttled = CurrentUser.user.token_bucket.throttled?
@ -55,6 +49,9 @@ class ApplicationController < ActionController::Base
def rescue_exception(exception)
@exception = exception
# If InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to do it here manually.
CurrentUser.user ||= User.anonymous
if Rails.env.test? && ENV["DEBUG"]
puts "---"
STDERR.puts("#{exception.class} exception thrown: #{exception.message}")
@ -120,9 +117,8 @@ class ApplicationController < ActionController::Base
def render_expected_error(status, message, format: request.format.symbol)
format = :html unless format.in?(%i[html json atom])
layout = CurrentUser.user.present? ? "default" : "blank"
@message = message
render "static/error", layout: layout, status: status, formats: format
render "static/error", status: status, formats: format
end
def render_error_page(status, exception, message: exception.message, format: request.format.symbol)
@ -132,17 +128,14 @@ class ApplicationController < ActionController::Base
@backtrace = Rails.backtrace_cleaner.clean(@exception.backtrace)
format = :html unless format.in?(%i[html json atom])
# if InvalidAuthenticityToken was raised, CurrentUser isn't set so we have to use the blank layout.
layout = CurrentUser.user.present? ? "default" : "blank"
if !CurrentUser.user&.try(:is_janitor?) && message == exception.message
if !CurrentUser.user.is_janitor? && message == exception.message
@message = "An unexpected error occurred."
end
DanbooruLogger.log(@exception, expected: @expected)
log = ExceptionLog.add(exception, CurrentUser.id, request) if !@expected
@log_code = log&.code
render "static/error", layout: layout, status: status, formats: format
render "static/error", status: status, formats: format
end
def access_denied(exception = nil)

View File

@ -8,19 +8,19 @@ class CommentVotesController < ApplicationController
def create
@comment = Comment.find(params[:comment_id])
@comment_vote = VoteManager.comment_vote!(comment: @comment, user: CurrentUser.user, score: params[:score])
if @comment_vote == :need_unvote
if @comment_vote == :need_unvote && params[:no_unvote] != "true"
VoteManager.comment_unvote!(comment: @comment, user: CurrentUser.user)
end
@comment.reload
render json: {score: @comment.score, our_score: @comment_vote != :need_unvote ? @comment_vote.score : 0}
rescue CommentVote::Error, ActiveRecord::RecordInvalid => x
rescue UserVote::Error, ActiveRecord::RecordInvalid => x
render_expected_error(422, x)
end
def destroy
@comment = Comment.find(params[:comment_id])
VoteManager.comment_unvote!(comment: @comment, user: CurrentUser.user)
rescue CommentVote::Error => x
rescue UserVote::Error => x
render_expected_error(422, x)
end

View File

@ -0,0 +1,43 @@
class MascotsController < ApplicationController
respond_to :html, :json
before_action :admin_only, except: [:index]
def index
@mascots = Mascot.search(search_params).paginate(params[:page], limit: 75)
respond_with(@mascots)
end
def new
@mascot = Mascot.new
end
def create
@mascot = Mascot.create(mascot_params.merge(creator: CurrentUser.user))
ModAction.log(:mascot_create, { id: @mascot.id }) if @mascot.valid?
respond_with(@mascot, location: mascots_path)
end
def edit
@mascot = Mascot.find(params[:id])
end
def update
@mascot = Mascot.find(params[:id])
@mascot.update(mascot_params)
ModAction.log(:mascot_update, { id: @mascot.id }) if @mascot.valid?
respond_with(@mascot, location: mascots_path)
end
def destroy
@mascot = Mascot.find(params[:id])
@mascot.destroy
ModAction.log(:mascot_delete, { id: @mascot.id })
respond_with(@mascot)
end
private
def mascot_params
params.fetch(:mascot, {}).permit(%i[mascot_file display_name background_color artist_url artist_name safe_mode_only active])
end
end

View File

@ -4,7 +4,7 @@ class PostVersionsController < ApplicationController
respond_to :js, only: [:undo]
def index
@post_versions = PostVersion.__elasticsearch__.search(PostVersion.build_query(search_params)).paginate(params[:page], :limit => params[:limit], :search_count => params[:search], includes: [:updater, post: [:versions]])
@post_versions = PostVersion.__elasticsearch__.search(PostVersion.build_query(search_params)).paginate(params[:page], limit: params[:limit], max_count: 10_000, search_count: params[:search], includes: [:updater, post: [:versions]])
respond_with(@post_versions)
end

View File

@ -10,14 +10,14 @@ class PostVotesController < ApplicationController
VoteManager.unvote!(post: @post, user: CurrentUser.user)
end
render json: {score: @post.score, up: @post.up_score, down: @post.down_score, our_score: @post_vote != :need_unvote ? @post_vote.score : 0}
rescue PostVote::Error, ActiveRecord::RecordInvalid => x
rescue UserVote::Error, ActiveRecord::RecordInvalid => x
render_expected_error(422, x)
end
def destroy
@post = Post.find(params[:post_id])
VoteManager.unvote!(post: @post, user: CurrentUser.user)
rescue PostVote::Error => x
rescue UserVote::Error => x
render_expected_error(422, x)
end

View File

@ -10,6 +10,7 @@ class TakedownsController < ApplicationController
def destroy
@takedown = Takedown.find(params[:id])
@takedown.destroy
ModAction.log(:takedown_delete, { takedown_id: @takedown.id })
respond_with(@takedown)
end

View File

@ -19,12 +19,7 @@ class UsersController < ApplicationController
def index
if params[:name].present?
@user = User.find_by_name(params[:name])
if @user.nil?
raise "No user found with name: #{params[:name]}"
else
redirect_to user_path(@user)
end
redirect_to user_path(id: params[:name])
else
@users = User.search(search_params).includes(:user_status).paginate(params[:page], limit: params[:limit], search_count: params[:search])
respond_with(@users) do |format|
@ -63,6 +58,7 @@ class UsersController < ApplicationController
raise User::PrivilegeError.new("Signups are disabled") unless Danbooru.config.enable_signups?
User.transaction do
@user = User.new(user_params(:create).merge({last_ip_addr: request.remote_ip}))
@user.validate_email_format = true
@user.email_verification_key = '1' if Danbooru.config.enable_email_verification?
if !Danbooru.config.enable_recaptcha? || verify_recaptcha(model: @user)
@user.save
@ -92,6 +88,7 @@ class UsersController < ApplicationController
def update
@user = User.find(CurrentUser.id)
@user.validate_email_format = true
check_privilege(@user)
@user.update(user_params(:update))
if @user.errors.any?

View File

@ -23,6 +23,8 @@ class ModActionDecorator < ApplicationDecorator
### Takedowns ###
when "takedown_process"
"Completed takedown ##{vals['takedown_id']}"
when "takedown_delete"
"Deleted takedown ##{vals['takedown_id']}"
### IP Ban ###
when "ip_ban_create"
@ -292,6 +294,14 @@ class ModActionDecorator < ApplicationDecorator
when "wiki_page_rename"
"Renamed wiki page ([[#{vals['old_title']}]] → [[#{vals['new_title']}]])"
### Mascots ###
when "mascot_create"
"Created mascot ##{vals['id']}"
when "mascot_update"
"Updated mascot ##{vals['id']}"
when "mascot_delete"
"Deleted mascot ##{vals['id']}"
when "bulk_revert"
"Processed bulk revert for #{vals['constraints']} by #{user}"

View File

@ -1,3 +1,3 @@
class PaginatedDecorator < Draper::CollectionDecorator
delegate :current_page, :total_pages, :is_first_page?, :is_last_page?, :sequential_paginator_mode, :records, :total_count
delegate :current_page, :total_pages, :is_first_page?, :is_last_page?, :sequential_paginator_mode, :max_numbered_pages, :records, :total_count
end

View File

@ -116,14 +116,6 @@ module ApplicationHelper
content_tag(:time, content || datetime, :datetime => datetime, :title => time.to_formatted_s)
end
def humanized_duration(from, to)
duration = distance_of_time_in_words(from, to)
datetime = from.iso8601 + "/" + to.iso8601
title = "#{from.strftime("%Y-%m-%d %H:%M")} to #{to.strftime("%Y-%m-%d %H:%M")}"
raw content_tag(:time, duration, datetime: datetime, title: title)
end
def time_ago_in_words_tagged(time, compact: false)
if time.past?
text = time_ago_in_words(time) + " ago"

View File

@ -3,7 +3,7 @@ module PaginationHelper
html = '<div class="paginator"><menu>'
if records.respond_to?(:any?) && records.any?
if params[:page] =~ /[ab]/ && !records.is_first_page?
if !records.is_first_page?
html << '<li>' + link_to("< Previous", nav_params_for("a#{records[0].id}"), rel: "prev", id: "paginator-prev", "data-shortcut": "a left") + '</li>'
end
@ -17,7 +17,7 @@ module PaginationHelper
end
def use_sequential_paginator?(records)
params[:page] =~ /\A[ab]\d+\z/ || records.current_page >= Danbooru.config.max_numbered_pages
params[:page] =~ /\A[ab]\d+\z/ || records.current_page >= records.max_numbered_pages
end
def numbered_paginator(records, switch_to_sequential = true)
@ -36,35 +36,35 @@ module PaginationHelper
if records.total_pages <= (window * 2) + 5
1.upto(records.total_pages) do |page|
html << numbered_paginator_item(page, records.current_page)
html << numbered_paginator_item(page, records)
end
elsif records.current_page <= window + 2
1.upto(records.current_page + window) do |page|
html << numbered_paginator_item(page, records.current_page)
html << numbered_paginator_item(page, records)
end
html << numbered_paginator_item("...", records.current_page)
html << numbered_paginator_final_item(records.total_pages, records.current_page)
html << numbered_paginator_item("...", records)
html << numbered_paginator_final_item(records)
elsif records.current_page >= records.total_pages - (window + 1)
html << numbered_paginator_item(1, records.current_page)
html << numbered_paginator_item("...", records.current_page)
html << numbered_paginator_item(1, records)
html << numbered_paginator_item("...", records)
(records.current_page - window).upto(records.total_pages) do |page|
html << numbered_paginator_item(page, records.current_page)
html << numbered_paginator_item(page, records)
end
else
html << numbered_paginator_item(1, records.current_page)
html << numbered_paginator_item("...", records.current_page)
html << numbered_paginator_item(1, records)
html << numbered_paginator_item("...", records)
if records.size > 0
right_window = records.current_page + window
else
right_window = records.current_page
end
(records.current_page - window).upto(right_window) do |page|
html << numbered_paginator_item(page, records.current_page)
html << numbered_paginator_item(page, records)
end
if records.size > 0
html << numbered_paginator_item("...", records.current_page)
html << numbered_paginator_final_item(records.total_pages, records.current_page)
html << numbered_paginator_item("...", records)
html << numbered_paginator_final_item(records)
end
end
@ -78,23 +78,23 @@ module PaginationHelper
html.html_safe
end
def numbered_paginator_final_item(total_pages, current_page)
if total_pages <= Danbooru.config.max_numbered_pages
numbered_paginator_item(total_pages, current_page)
def numbered_paginator_final_item(records)
if records.total_pages <= records.max_numbered_pages
numbered_paginator_item(records.total_pages, records)
else
numbered_paginator_item(Danbooru.config.max_numbered_pages, current_page)
numbered_paginator_item(records.max_numbered_pages, records)
end
end
def numbered_paginator_item(page, current_page)
return "" if page.to_i > Danbooru.config.max_numbered_pages
def numbered_paginator_item(page, records)
return "" if page.to_i > records.max_numbered_pages
html = []
if page == "..."
html << "<li class='more'>"
html << content_tag(:i, nil, class: "fas fa-ellipsis-h")
html << "</li>"
elsif page == current_page
elsif page == records.current_page
html << "<li class='current-page'>"
html << '<span>' + page.to_s + '</span>'
html << "</li>"

View File

@ -22,7 +22,7 @@ export default {
watch: {
addToList(value) {
const maxEntries = 50;
const entries = new Set([value, ...this.currentEntries()]);
const entries = new Set([value.trim(), ...this.currentEntries()]);
LS.putObject(`autocomplete-${this.listId}`, [...entries].slice(0, maxEntries));
}
},

View File

@ -4,28 +4,24 @@ const Mascots = {
current: 0
};
function showMascot(cur) {
const mascots = window.mascots;
function showMascot(mascot) {
$('body').css("background-image", "url(" + mascot.background_url + ")");
$('body').css("background-color", mascot.background_color);
$('.mascotbox').css("background-image", "url(" + mascot.background_url + ")");
$('.mascotbox').css("background-color", mascot.background_color);
const blurred = mascots[cur][0].substr(0, mascots[cur][0].lastIndexOf(".")) + "_blur" + mascots[cur][0].slice(mascots[cur][0].lastIndexOf("."));
$('body').css("background-image", "url(" + mascots[cur][0] + ")");
$('body').css("background-color", mascots[cur][1]);
$('.mascotbox').css("background-image", "url(" + blurred + ")");
$('.mascotbox').css("background-color", mascots[cur][1]);
if (mascots[cur][2])
$('#mascot_artist').html("Mascot by " + mascots[cur][2]);
else
$('#mascot_artist').html("&nbsp;");
const artistLink = $("<span>").text("Mascot by ").append($("<a>").text(mascot.artist_name).attr("href", mascot.artist_url));
$("#mascot_artist").empty().append(artistLink);
}
function changeMascot() {
const mascots = window.mascots;
Mascots.current += 1;
Mascots.current = Mascots.current % mascots.length;
showMascot(Mascots.current);
const availableMascotIds = Object.keys(mascots);
const currentMascotIndex = availableMascotIds.indexOf(Mascots.current);
Mascots.current = availableMascotIds[(currentMascotIndex + 1) % availableMascotIds.length];
showMascot(mascots[Mascots.current]);
LS.put('mascot', Mascots.current);
}
@ -33,10 +29,13 @@ function changeMascot() {
function initMascots() {
$('#change-mascot').on('click', changeMascot);
const mascots = window.mascots;
Mascots.current = parseInt(LS.get("mascot"));
if (isNaN(Mascots.current) || Mascots.current < 0 || Mascots.current >= mascots.length)
Mascots.current = Math.floor(Math.random() * mascots.length);
showMascot(Mascots.current);
Mascots.current = LS.get("mascot");
if (!mascots[Mascots.current]) {
const availableMascotIds = Object.keys(mascots);
const mascotIndex = Math.floor(Math.random() * availableMascotIds.length);
Mascots.current = availableMascotIds[mascotIndex];
}
showMascot(mascots[Mascots.current]);
}
$(function () {

View File

@ -3,28 +3,6 @@
width: 15em; // Match width to that of the
}
@keyframes heartbeat {
0% {
transform:scale(1);
}
50% {
transform:scale(1.3);
}
100% {
transform:scale(1);
}
}
@keyframes sharpen {
from {
filter: blur(8px);
}
to {
filter: none;
}
}
article.post-preview {
box-sizing: border-box;
height: 154px;

View File

@ -45,9 +45,7 @@ module Danbooru
end
# taken from kaminari (https://github.com/amatsuda/kaminari)
def total_count
return option_for(:count) if option_for(:count)
def real_count
c = except(:offset, :limit, :order)
c = c.reorder(nil)
c = c.count

View File

@ -1,7 +1,7 @@
module Danbooru
module Paginator
module BaseExtension
def paginate_base(page, options)
def paginate_base(page, options = {})
@paginator_options = options
if use_numbered_paginator?(page)
@ -16,8 +16,16 @@ module Danbooru
end
def validate_numbered_page(page)
return if page.to_i <= Danbooru.config.max_numbered_pages
raise Danbooru::Paginator::PaginationError, "You cannot go beyond page #{Danbooru.config.max_numbered_pages}. Please narrow your search terms."
return if page.to_i <= max_numbered_pages
raise Danbooru::Paginator::PaginationError, "You cannot go beyond page #{max_numbered_pages}. Please narrow your search terms."
end
def max_numbered_pages
if @paginator_options[:max_count]
[Danbooru.config.max_numbered_pages, @paginator_options[:max_count] / records_per_page].min
else
Danbooru.config.max_numbered_pages
end
end
def use_numbered_paginator?(page)
@ -39,7 +47,8 @@ module Danbooru
end
def records_per_page
option_for(:limit).to_i
limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page
[limit.to_i, 320].min
end
# When paginating large tables, we want to avoid doing an expensive count query
@ -48,24 +57,11 @@ module Danbooru
# exist, then assume we're doing a search and don't override the default count
# behavior. Otherwise, just return some large number so the paginator skips the
# count.
def option_for(key)
case key
when :limit
limit = @paginator_options.try(:[], :limit) || Danbooru.config.posts_per_page
if limit.to_i > 320
limit = 320
end
limit
def total_count
return 1_000_000 if @paginator_options.key?(:search_count) && @paginator_options[:search_count].blank?
return @paginator_options[:exact_count] if @paginator_options[:exact_count]
when :count
if @paginator_options.key?(:search_count) && @paginator_options[:search_count].blank?
1_000_000
elsif @paginator_options[:count]
@paginator_options[:count]
else
nil
end
end
real_count
end
end
end

View File

@ -8,6 +8,7 @@ module Danbooru
@_current_page = options[:current_page]
@_records_per_page = options[:per_page]
@_total_count = options[:total]
@_max_numbered_pages = options[:max_numbered_pages] || Danbooru.config.max_numbered_pages
real_array = orig_array || []
@_orig_size = real_array.size
if options[:mode] == :sequential
@ -41,6 +42,10 @@ module Danbooru
end
end
def max_numbered_pages
@_max_numbered_pages
end
def total_pages
if records_per_page > 0
(total_count.to_f / records_per_page).ceil
@ -55,10 +60,10 @@ module Danbooru
attr_reader :current_page, :sequential_paginator_mode
def paginate(page, options = {})
def paginate(page, options)
paginated, mode = paginate_base(page, options)
new_opts = {mode: mode, seq_mode: sequential_paginator_mode,
new_opts = {mode: mode, seq_mode: sequential_paginator_mode, max_numbered_pages: max_numbered_pages,
per_page: records_per_page, total: total_count, current_page: current_page}
if options[:results] == :results
PaginatedArray.new(paginated.results, new_opts)
@ -94,7 +99,7 @@ module Danbooru
end
def paginate_numbered(page)
search.definition.update(size: records_per_page, from: (page - 1) * records_per_page, track_total_hits: Danbooru.config.max_numbered_pages * records_per_page + 1)
search.definition.update(size: records_per_page, from: (page - 1) * records_per_page, track_total_hits: (max_numbered_pages * records_per_page) + 1)
@current_page = page
self
@ -105,13 +110,7 @@ module Danbooru
self
end
def total_count
return option_for(:count) if option_for(:count)
response_hits_total
end
def response_hits_total
def real_count
if response['hits']['total'].respond_to?(:keys)
response['hits']['total']['value']
else
@ -122,13 +121,13 @@ module Danbooru
def exists?
search.definition[:body]&.delete(:sort)
search.definition.update(from: 0, size: 1, terminate_after: 1, sort: '_doc', _source: false, track_total_hits: false)
response_hits_total > 0
real_count > 0
end
def count_only
search.definition[:body]&.delete(:sort)
search.definition.update(from: 0, size: 0, sort: '_doc', _source: false, track_total_hits: true)
response_hits_total
real_count
end
end
end

View File

@ -0,0 +1,18 @@
class DummyTicket
def initialize(accused, post_id)
@ticket = Ticket.new(
id: 0,
created_at: Time.now,
updated_at: Time.now,
creator_id: User.system.id,
disp_id: 0,
status: "pending",
qtype: "user",
reason: "User ##{accused.id} (#{accused.name}) tried to reupload destroyed post ##{post_id}",
)
end
def notify
@ticket.push_pubsub("create")
end
end

View File

@ -45,6 +45,26 @@ module FileMethods
end
end
def is_ai_generated?(file_path)
return false if !is_image?
image = Vips::Image.new_from_file(file_path)
fetch = lambda do |key|
value = image.get(key)
value.encode("ASCII", invalid: :replace, undef: :replace).gsub("\u0000", "")
rescue Vips::Error
""
end
return true if fetch.call("png-comment-0-parameters").present?
return true if fetch.call("png-comment-0-Dream").present?
return true if fetch.call("exif-ifd0-Software").include?("NovelAI") || fetch.call("png-comment-2-Software").include?("NovelAI")
return true if ["exif-ifd0-ImageDescription", "exif-ifd2-UserComment", "png-comment-4-Comment"].any? { |field| fetch.call(field).include?('"sampler": "') }
exif_data = fetch.call("exif-data")
return true if ["Model hash", "OpenAI", "NovelAI"].any? { |marker| exif_data.include?(marker) }
false
end
def file_header_to_file_ext(file_path)
File.open file_path do |bin|
mime_type = Marcel::MimeType.for(bin)

View File

@ -6,13 +6,13 @@ class FileValidator
@file_path = file_path
end
def validate
validate_file_ext
validate_file_size
def validate(max_file_sizes: Danbooru.config.max_file_sizes, max_width: Danbooru.config.max_image_width, max_height: Danbooru.config.max_image_height)
validate_file_ext(max_file_sizes)
validate_file_size(max_file_sizes)
validate_file_integrity
validate_video_container_format
validate_video_duration
validate_resolution
validate_resolution(max_width, max_height)
end
def validate_file_integrity
@ -21,35 +21,35 @@ class FileValidator
end
end
def validate_file_ext
if Danbooru.config.max_file_sizes.keys.exclude? record.file_ext
record.errors.add(:file_ext, "#{record.file_ext} is invalid (only JPEG, PNG, GIF, and WebM files are allowed")
def validate_file_ext(max_file_sizes)
if max_file_sizes.keys.exclude? record.file_ext
record.errors.add(:file_ext, "#{record.file_ext} is invalid (only #{max_file_sizes.keys.to_sentence} files are allowed")
throw :abort
end
end
def validate_file_size
def validate_file_size(max_file_sizes)
if record.file_size <= 16
record.errors.add(:file_size, "is too small")
end
max_size = Danbooru.config.max_file_sizes.fetch(record.file_ext, 0)
max_size = max_file_sizes.fetch(record.file_ext, 0)
if record.file_size > max_size
record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{max_size / (1024 * 1024)} MiB")
record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{ApplicationController.helpers.number_to_human_size(max_size)}")
end
if record.is_animated_png?(file_path) && record.file_size > Danbooru.config.max_apng_file_size
record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{Danbooru.config.max_apng_file_size / (1024*1024)} MiB")
record.errors.add(:file_size, "is too large. Maximum allowed for this file type is #{ApplicationController.helpers.number_to_human_size(Danbooru.config.max_apng_file_size)}")
end
end
def validate_resolution
def validate_resolution(max_width, max_height)
resolution = record.image_width.to_i * record.image_height.to_i
if resolution > Danbooru.config.max_image_resolution
record.errors.add(:base, "image resolution is too large (resolution: #{(resolution / 1_000_000.0).round(1)} megapixels (#{record.image_width}x#{record.image_height}); max: #{Danbooru.config.max_image_resolution / 1_000_000} megapixels)")
elsif record.image_width > Danbooru.config.max_image_width
record.errors.add(:image_width, "is too large (width: #{record.image_width}; max width: #{Danbooru.config.max_image_width})")
elsif record.image_height > Danbooru.config.max_image_height
record.errors.add(:image_height, "is too large (height: #{record.image_height}; max height: #{Danbooru.config.max_image_height})")
elsif record.image_width > max_width
record.errors.add(:image_width, "is too large (width: #{record.image_width}; max width: #{max_width})")
elsif record.image_height > max_height
record.errors.add(:image_height, "is too large (height: #{record.image_height}; max height: #{max_height})")
end
end

View File

@ -13,7 +13,12 @@ module Moderator
elsif params[:user_name].present?
search_by_user_name(params[:user_name].split(/,/).map(&:strip), with_history)
elsif params[:ip_addr].present?
search_by_ip_addr(params[:ip_addr].split(/,/).map(&:strip), with_history)
ip_addrs = params[:ip_addr].split(/,/).map(&:strip)
if params[:add_ip_mask].to_s.truthy? && ip_addrs.count == 1 && ip_addrs[0].exclude?("/")
mask = IPAddr.new(ip_addrs[0]).ipv4? ? 24 : 64
ip_addrs[0] = "#{ip_addrs[0]}/#{mask}"
end
search_by_ip_addr(ip_addrs, with_history)
else
[]
end

View File

@ -12,14 +12,6 @@ module PostSets
nil
end
def unknown_post_count?
false
end
def use_sequential_paginator?
false
end
def fill_tag_types(posts)
tag_array = []
posts.each do |p|

View File

@ -20,7 +20,7 @@ module PostSets
def posts
@post_count ||= ::Post.tag_match("fav:#{@user.name} status:any").count_only
@posts ||= begin
favs = ::Favorite.for_user(@user.id).includes(:post).order(created_at: :desc).paginate(page, count: @post_count, limit: @limit)
favs = ::Favorite.for_user(@user.id).includes(:post).order(created_at: :desc).paginate(page, exact_count: @post_count, limit: @limit)
new_opts = {mode: :numbered, per_page: favs.records_per_page, total: @post_count, current_page: current_page}
::Danbooru::Paginator::PaginatedArray.new(favs.map {|f| f.post},
new_opts

View File

@ -1,9 +1,7 @@
require_relative "../danbooru/paginator/elasticsearch_extensions"
module PostSets
class Pool < PostSets::Base
module ActiveRecordExtension
attr_accessor :total_pages, :current_page
end
attr_reader :pool, :page
def initialize(pool, page = 1)
@ -25,11 +23,9 @@ module PostSets
def posts
@posts ||= begin
x = pool.posts(:offset => offset, :limit => limit)
x.extend(ActiveRecordExtension)
x.total_pages = total_pages
x.current_page = current_page
x
posts = pool.posts(offset: offset, limit: limit)
options = { mode: :numbered, per_page: limit, total: pool.post_count, current_page: current_page }
Danbooru::Paginator::PaginatedArray.new(posts, options)
end
end
@ -45,14 +41,6 @@ module PostSets
@presenter ||= PostSetPresenters::Pool.new(self)
end
def total_pages
(pool.post_count.to_f / limit).ceil
end
def size
posts.size
end
def current_page
[page.to_i, 1].max
end

View File

@ -1,16 +1,9 @@
module PostSets
class PoolGallery < PostSets::Base
attr_reader :page, :per_page, :pools
attr_reader :pools
def initialize(pools, page = 1, per_page = nil)
def initialize(pools)
@pools = pools
@page = page
@per_page = (per_page || CurrentUser.per_page).to_i
@per_page = 200 if @per_page > 200
end
def current_page
[page.to_i, 1].max
end
def presenter

View File

@ -66,10 +66,6 @@ module PostSets
random || (Tag.has_metatag?(tag_array, :order) == "random" && !Tag.has_metatag?(tag_array, :randseed))
end
def use_sequential_paginator?
unknown_post_count? && !CurrentUser.is_privileged?
end
def posts
@posts ||= begin
temp = ::Post.tag_match(tag_string).paginate(page, limit: per_page, includes: [:uploader])
@ -86,10 +82,6 @@ module PostSets
_posts
end
def unknown_post_count?
post_count == Danbooru.config.blank_tag_search_fast_count
end
def hide_from_crawler?
!is_empty_tag?
end

View File

@ -96,11 +96,9 @@ private
end
def authenticate_api_key(name, api_key)
CurrentUser.user = User.authenticate_api_key(name, api_key)
if CurrentUser.user.nil?
raise AuthenticationFailure.new
end
user = User.authenticate_api_key(name, api_key)
raise AuthenticationFailure if user.nil?
CurrentUser.user = user
end
def load_session_user

View File

@ -3,6 +3,7 @@ class StorageManager
DEFAULT_BASE_DIR = "#{Rails.root}/public/data"
IMAGE_TYPES = %i[preview large crop original]
MASCOT_PREFIX = "mascots"
attr_reader :base_url, :base_dir, :hierarchical, :large_image_prefix, :protected_prefix, :base_path, :replacement_prefix
@ -185,6 +186,24 @@ class StorageManager
"#{base_dir}/#{replacement_prefix}/#{subdir}#{file}"
end
def store_mascot(io, mascot)
store(io, mascot_path(mascot.md5, mascot.file_ext))
end
def mascot_path(md5, file_ext)
file = "#{md5}.#{file_ext}"
"#{base_dir}/#{MASCOT_PREFIX}/#{file}"
end
def mascot_url(mascot)
file = "#{mascot.md5}.#{mascot.file_ext}"
"#{base_url}#{base_path}/#{MASCOT_PREFIX}/#{file}"
end
def delete_mascot(md5, file_ext)
delete(mascot_path(md5, file_ext))
end
def subdir_for(md5)
hierarchical ? "#{md5[0..1]}/#{md5[2..3]}/" : ""
end

View File

@ -71,6 +71,7 @@ class UploadService
tags = []
tags += ["animated_gif", "animated"] if upload.is_animated_gif?(file.path)
tags += ["animated_png", "animated"] if upload.is_animated_png?(file.path)
tags += ["ai_generated"] if upload.is_ai_generated?(file.path)
tags.join(" ")
end

View File

@ -30,16 +30,19 @@ class UserDeletion
end
def clear_user_settings
user.update_columns(recent_tags: '',
favorite_tags: '',
blacklisted_tags: '',
time_zone: "Eastern Time (US & Canada)",
email: '',
email_verification_key: '1',
avatar_id: nil,
profile_about: '',
profile_artinfo: '',
custom_style: '')
user.update_columns(
recent_tags: '',
favorite_tags: '',
blacklisted_tags: '',
time_zone: "Eastern Time (US & Canada)",
email: '',
email_verification_key: '1',
avatar_id: nil,
profile_about: '',
profile_artinfo: '',
custom_style: '',
level: User::Levels::MEMBER,
)
end
def reset_password

View File

@ -20,6 +20,7 @@ class UserEmailChange
if User.authenticate(user.name, password).nil?
user.errors.add(:base, "Password was incorrect")
else
user.validate_email_format = true
user.email = new_email
user.email_verification_key = '1' if Danbooru.config.enable_email_verification?
user.save

View File

@ -1,6 +1,6 @@
class UserNameValidator < ActiveModel::EachValidator
def validate_each(rec, attr, value)
name = value
name = value
rec.errors.add(attr, "already exists") if User.find_by_name(name).present?
rec.errors.add(attr, "must be 2 to 20 characters long") if !name.length.between?(2, 20)
@ -8,6 +8,7 @@ class UserNameValidator < ActiveModel::EachValidator
rec.errors.add(attr, "must not begin with a special character") if name =~ /\A[_\-~']/
rec.errors.add(attr, "must not contain consecutive special characters") if name =~ /_{2}|-{2}|~{2}|'{2}/
rec.errors.add(attr, "cannot begin or end with an underscore") if name =~ /\A_|_\z/
rec.errors.add(attr, "cannot consist of numbers only") if name =~ /\A[0-9]+\z/
rec.errors.add(attr, "cannot be the string 'me'") if name.downcase == 'me'
end
end

View File

@ -4,8 +4,8 @@ class VoteManager
retries = 5
score = score.to_i
begin
raise PostVote::Error.new("Invalid vote") unless [1, -1].include?(score)
raise PostVote::Error.new("You do not have permission to vote") unless user.is_voter?
raise UserVote::Error.new("Invalid vote") unless [1, -1].include?(score)
raise UserVote::Error.new("You do not have permission to vote") unless user.is_voter?
target_isolation = !Rails.env.test? ? { isolation: :serializable } : {}
PostVote.transaction(**target_isolation) do
PostVote.uncached do
@ -13,7 +13,7 @@ class VoteManager
score_modifier = score
old_vote = PostVote.where(user_id: user.id, post_id: post.id).first
if old_vote
raise PostVote::Error.new("Vote is locked") if old_vote.score == 0
raise UserVote::Error.new("Vote is locked") if old_vote.score == 0
if old_vote.score == score
return :need_unvote
else
@ -38,9 +38,9 @@ class VoteManager
rescue ActiveRecord::SerializationFailure
retries -= 1
retry if retries > 0
raise PostVote::Error.new("Failed to vote, please try again later")
raise UserVote::Error.new("Failed to vote, please try again later")
rescue ActiveRecord::RecordNotUnique
raise PostVote::Error.new("You have already voted for this post")
raise UserVote::Error.new("You have already voted for this post")
end
post.update_index
@vote
@ -55,7 +55,7 @@ class VoteManager
post.with_lock do
vote = PostVote.where(user_id: user.id, post_id: post.id).first
return unless vote
raise PostVote::Error.new "You can't remove locked votes" if vote.score == 0 && !force
raise UserVote::Error.new "You can't remove locked votes" if vote.score == 0 && !force
post.votes.where(user: user).delete_all
subtract_vote(post, vote)
post.reload
@ -65,7 +65,7 @@ class VoteManager
rescue ActiveRecord::SerializationFailure
retries -= 1
retry if retries > 0
raise PostVote::Error.new("Failed to unvote, please try again later")
raise UserVote::Error.new("Failed to unvote, please try again later")
end
post.update_index
end
@ -93,8 +93,8 @@ class VoteManager
@vote = nil
score = score.to_i
begin
raise CommentVote::Error.new("Invalid vote") unless [1, -1].include?(score)
raise CommentVote::Error.new("You do not have permission to vote") unless user.is_voter?
raise UserVote::Error.new("Invalid vote") unless [1, -1].include?(score)
raise UserVote::Error.new("You do not have permission to vote") unless user.is_voter?
target_isolation = !Rails.env.test? ? { isolation: :serializable } : {}
CommentVote.transaction(**target_isolation) do
CommentVote.uncached do
@ -102,7 +102,7 @@ class VoteManager
score_modifier = score
old_vote = CommentVote.where(user_id: user.id, comment_id: comment.id).first
if old_vote
raise CommentVote::Error.new("Vote is locked") if old_vote.score == 0
raise UserVote::Error.new("Vote is locked") if old_vote.score == 0
if old_vote.score == score
return :need_unvote
else
@ -118,9 +118,9 @@ class VoteManager
rescue ActiveRecord::SerializationFailure
retries -= 1
retry if retries > 0
raise PostVote::Error.new("Failed to vote, please try again later.")
raise UserVote::Error.new("Failed to vote, please try again later.")
rescue ActiveRecord::RecordNotUnique
raise CommentVote::Error.new("You have already voted for this comment")
raise UserVote::Error.new("You have already voted for this comment")
end
@vote
end
@ -132,7 +132,7 @@ class VoteManager
comment.with_lock do
vote = CommentVote.where(user_id: user.id, comment_id: comment.id).first
return unless vote
raise CommentVote::Error.new("You can't remove locked votes") if vote.score == 0 && !force
raise UserVote::Error.new("You can't remove locked votes") if vote.score == 0 && !force
CommentVote.where(user_id: user.id, comment_id: comment.id).delete_all
Comment.where(id: comment.id).update_all("score = score - #{vote.score}")
end

View File

@ -25,12 +25,6 @@ class ApplicationRecord < ActiveRecord::Base
where.not("lower(#{qualified_column_for(attr)}) LIKE ? ESCAPE E'\\\\'", value.downcase.to_escaped_for_sql_like)
end
# https://www.postgresql.org/docs/current/static/functions-matching.html#FUNCTIONS-POSIX-REGEXP
# "(?e)" means force use of ERE syntax; see sections 9.7.3.1 and 9.7.3.4.
def where_regex(attr, value)
where("#{qualified_column_for(attr)} ~ ?", "(?e)" + value)
end
def attribute_exact_matches(attribute, value, **options)
return all unless value.present?
@ -312,7 +306,7 @@ class ApplicationRecord < ActiveRecord::Base
define_method "#{name}=" do |value|
if value.respond_to?(:to_str)
super value.to_str.scan(parse).map(&cast)
super value.to_str.scan(parse).flatten.map(&cast)
elsif value.respond_to?(:to_a)
super value.to_a
else

View File

@ -182,7 +182,7 @@ class Artist < ApplicationRecord
while artists.empty? && url.length > 10
u = url.sub(/\/+$/, "") + "/"
u = u.to_escaped_for_sql_like.gsub(/\*/, '%') + '%'
artists += Artist.joins(:urls).where(["artists.is_active = TRUE AND artist_urls.normalized_url LIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all
artists += Artist.joins(:urls).where(["artists.is_active = TRUE AND artist_urls.normalized_url ILIKE ? ESCAPE E'\\\\'", u]).limit(10).order("artists.name").all
url = File.dirname(url) + "/"
break if url =~ SITE_BLACKLIST_REGEXP
@ -423,24 +423,16 @@ class Artist < ApplicationRecord
end
def any_name_matches(query)
if query =~ %r!\A/(.*)/\z!
where_regex(:name, $1).or(any_other_name_matches($1)).or(where_regex(:group_name, $1))
else
normalized_name = normalize_name(query)
normalized_name = "*#{normalized_name}*" unless normalized_name.include?("*")
where_like(:name, normalized_name).or(any_other_name_like(normalized_name)).or(where_like(:group_name, normalized_name))
end
normalized_name = normalize_name(query)
normalized_name = "*#{normalized_name}*" unless normalized_name.include?("*")
where_like(:name, normalized_name).or(any_other_name_like(normalized_name)).or(where_like(:group_name, normalized_name))
end
def url_matches(query)
if query =~ %r!\A/(.*)/\z!
where(id: ArtistUrl.where_regex(:url, $1).select(:artist_id))
elsif query.include?("*")
where(id: ArtistUrl.where_like(:url, query).select(:artist_id))
elsif query =~ %r!\Ahttps?://!i
if query =~ %r!\Ahttps?://!i
find_artists(query)
else
where(id: ArtistUrl.where_like(:url, "*#{query}*").select(:artist_id))
where(id: ArtistUrl.search(url_matches: query).select(:artist_id))
end
end

View File

@ -65,12 +65,9 @@ class ArtistUrl < ApplicationRecord
def self.url_attribute_matches(attr, url)
if url.blank?
all
elsif url =~ %r!\A/(.*)/\z!
where_regex(attr, $1)
elsif url.include?("*")
where_ilike(attr, url)
else
where(attr => normalize(url))
url = "*#{url}*" if url.exclude?("*")
where_ilike(attr, url)
end
end

View File

@ -1,24 +1,16 @@
class CommentVote < ApplicationRecord
class Error < Exception;
end
belongs_to :comment
belongs_to :user
before_validation :initialize_user, :on => :create
validates :user_id, :comment_id, :score, presence: true
# validates :user_id, uniqueness: { :scope => :comment_id, :message => "have already voted for this comment" }
class CommentVote < UserVote
validate :validate_user_can_vote
validate :validate_comment_can_be_voted
validates :score, inclusion: { :in => [-1, 0, 1], :message => "must be 1 or -1" }
scope :for_user, ->(uid) {where("user_id = ?", uid)}
def self.for_comments_and_user(comment_ids, user_id)
return {} unless user_id
CommentVote.where(comment_id: comment_ids, user_id: user_id).index_by(&:comment_id)
end
def self.model_creator_column
:creator
end
def validate_user_can_vote
allowed = user.can_comment_vote_with_reason
if allowed != true
@ -36,83 +28,4 @@ class CommentVote < ApplicationRecord
errors.add :base, "You cannot vote on sticky comments"
end
end
def is_positive?
score == 1
end
def is_negative?
score == -1
end
def is_locked?
score == 0
end
def initialize_user
self.user_id ||= CurrentUser.user.id
self.user_ip_addr ||= CurrentUser.ip_addr
end
module SearchMethods
def search(params)
q = super
if params[:comment_id].present?
q = q.where("comment_id = ?", params[:comment_id].to_i)
end
if params[:user_name].present?
user_id = User.name_to_id(params[:user_name])
if user_id
q = q.where('user_id = ?', user_id)
else
q = q.none
end
end
if params[:user_id].present?
q = q.where('user_id = ?', params[:user_id].to_i)
end
allow_complex_parameters = (params.keys & %w[comment_id user_name user_id]).any?
if allow_complex_parameters
if params[:timeframe].present?
q = q.where("comment_votes.updated_at >= ?", params[:timeframe].to_i.days.ago)
end
if params[:user_ip_addr].present?
q = q.where("user_ip_addr <<= ?", params[:user_ip_addr])
end
if params[:score].present?
q = q.where("comment_votes.score = ?", params[:score])
end
if params[:comment_creator_name].present?
comment_creator_id = User.name_to_id(params[:comment_creator_name])
if comment_creator_id
q = q.joins(:comment).where("comments.creator_id = ?", comment_creator_id)
else
q = q.none
end
end
if params[:duplicates_only] == "1"
subselect = CommentVote.search(params.except("duplicates_only")).select(:user_ip_addr).group(:user_ip_addr).having("count(user_ip_addr) > 1").reorder("")
q = q.where(user_ip_addr: subselect)
end
end
if params[:order] == "ip_addr" && allow_complex_parameters
q = q.order(:user_ip_addr)
else
q = q.apply_default_order(params)
end
q
end
end
extend SearchMethods
end

89
app/models/mascot.rb Normal file
View File

@ -0,0 +1,89 @@
class Mascot < ApplicationRecord
belongs_to_creator
attr_accessor :mascot_file
validates :display_name, :background_color, :artist_url, :artist_name, presence: true
validates :artist_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" }
validates :mascot_file, presence: true, on: :create
validate :set_file_properties
validates :md5, uniqueness: true
validate if: :mascot_file do |mascot|
max_file_sizes = { "jpg" => 500.kilobytes, "png" => 500.kilobytes }
FileValidator.new(mascot, mascot_file.path).validate(max_file_sizes: max_file_sizes, max_width: 1_000, max_height: 1_000)
end
after_commit :invalidate_cache
after_save_commit :write_storage_file
after_destroy_commit :remove_storage_file
def set_file_properties
return if mascot_file.blank?
self.file_ext = file_header_to_file_ext(mascot_file.path)
self.md5 = Digest::MD5.file(mascot_file.path).hexdigest
end
def write_storage_file
return if mascot_file.blank?
Danbooru.config.storage_manager.delete_mascot(md5_previously_was, file_ext_previously_was)
Danbooru.config.storage_manager.store_mascot(mascot_file, self)
end
def self.active_for_browser
Cache.get("active_mascots", 1.day) do
query = Mascot.where(active: true)
query = query.where(safe_mode_only: false) if !Danbooru.config.safe_mode?
mascots = query.map do |mascot|
mascot.slice(:id, :background_color, :artist_url, :artist_name).merge(background_url: mascot.url_path)
end
mascots.index_by { |mascot| mascot["id"] }
end
end
def invalidate_cache
Cache.delete("active_mascots")
end
def remove_storage_file
Danbooru.config.storage_manager.delete_mascot(md5, file_ext)
end
def url_path
Danbooru.config.storage_manager.mascot_url(self)
end
def file_path
Danbooru.config.storage_manager.mascot_path(self)
end
concerning :ValidationMethods do
def dimensions
@dimensions ||= calculate_dimensions(mascot_file.path)
end
def image_width
dimensions[0]
end
def image_height
dimensions[1]
end
def file_size
@file_size ||= Danbooru.config.storage_manager.open(mascot_file.path).size
end
end
def self.search(params)
q = super
q.order("lower(artist_name)")
end
def method_attributes
super + [:url_path]
end
include FileMethods
end

View File

@ -39,6 +39,9 @@ class ModAction < ApplicationRecord
:help_update,
:ip_ban_create,
:ip_ban_delete,
:mascot_create,
:mascot_update,
:mascot_delete,
:pool_delete,
:report_reason_create,
:report_reason_delete,
@ -76,7 +79,8 @@ class ModAction < ApplicationRecord
:mass_update,
:nuke_tag,
:takedown_process
:takedown_delete,
:takedown_process,
]
def self.search(params)

View File

@ -6,12 +6,12 @@ class NewsUpdate < ApplicationRecord
after_destroy :invalidate_cache
def self.recent
@recent_news ||= Cache.get('recent_news', 1.day) do
self.order('id desc').first(1)
Cache.get("recent_news_v2", 1.day) do
order("id desc").first
end
end
def invalidate_cache
Cache.delete('recent_news')
Cache.delete("recent_news_v2")
end
end

View File

@ -2,7 +2,7 @@ class Pool < ApplicationRecord
class RevertError < Exception;
end
array_attribute :post_ids, parse: /\d+/, cast: :to_i
array_attribute :post_ids, parse: %r{(?:https://(?:e621|e926)\.net/posts/)?(\d+)}i, cast: :to_i
belongs_to_creator
validates :name, uniqueness: { case_sensitive: false, if: :name_changed? }

View File

@ -42,6 +42,13 @@ class PostReplacement < ApplicationRecord
def no_pending_duplicates
return true if is_backup
if (destroyed_post = DestroyedPost.find_by(md5: md5))
errors.add(:base, "An unexpected errror occured")
DummyTicket.new(creator, destroyed_post.post_id).notify
return
end
post = Post.where(md5: md5).first
if post
self.errors.add(:md5, "duplicate of existing post ##{post.id}")

View File

@ -1,6 +1,5 @@
# -*- encoding : utf-8 -*-
class PostSet < ApplicationRecord
array_attribute :post_ids, parse: /\d+/, cast: :to_i
array_attribute :post_ids, parse: %r{(?:https://(?:e621|e926)\.net/posts/)?(\d+)}i, cast: :to_i
has_many :post_set_maintainers, dependent: :destroy do
def in_cooldown(user)

View File

@ -1,19 +1,8 @@
class PostVote < ApplicationRecord
class Error < Exception ; end
belongs_to :post
belongs_to :user
after_initialize :initialize_attributes, if: :new_record?
class PostVote < UserVote
validate :validate_user_can_vote
validates :post_id, :user_id, :score, presence: true
validates :score, inclusion: { :in => [1, 0, -1] }
scope :for_user, ->(uid) {where("user_id = ?", uid)}
def initialize_attributes
self.user_id ||= CurrentUser.user.id
self.user_ip_addr ||= CurrentUser.ip_addr
def self.model_creator_column
:uploader
end
def validate_user_can_vote
@ -28,57 +17,4 @@ class PostVote < ApplicationRecord
end
true
end
module SearchMethods
def search(params)
q = super
if params[:post_id].present?
q = q.where('post_id = ?', params[:post_id])
end
if params[:user_name].present?
user_id = User.name_to_id(params[:user_name])
if user_id
q = q.where('user_id = ?', user_id)
else
q = q.none
end
end
if params[:user_id].present?
q = q.where('user_id = ?', params[:user_id].to_i)
end
allow_complex_parameters = (params.keys & %w[post_id user_name user_id]).any?
if allow_complex_parameters
if params[:timeframe].present?
q = q.where("updated_at >= ?", params[:timeframe].to_i.days.ago)
end
if params[:user_ip_addr].present?
q = q.where("user_ip_addr <<= ?", params[:user_ip_addr])
end
if params[:score].present?
q = q.where("score = ?", params[:score])
end
if params[:duplicates_only] == "1"
subselect = PostVote.search(params.except("duplicates_only")).select(:user_ip_addr).group(:user_ip_addr).having("count(user_ip_addr) > 1").reorder("")
q = q.where(user_ip_addr: subselect)
end
end
if params[:order] == "ip_addr" && allow_complex_parameters
q = q.order(:user_ip_addr)
else
q = q.apply_default_order(params)
end
q
end
end
extend SearchMethods
end

View File

@ -88,8 +88,7 @@ class Takedown < ApplicationRecord
def add_posts_by_ids!(ids)
added_ids = []
with_lock do
ids = ids.gsub(/(https?:\/\/)?(e621|e926)\.net\/posts\/(\d+)/i, '\3')
self.post_ids = (post_array + ids.scan(/\d+/).map(&:to_i)).uniq.join(' ')
self.post_ids = (post_array + matching_post_ids(ids)).uniq.join(' ')
added_ids = self.post_array - self.post_array_was
save!
end
@ -107,10 +106,14 @@ class Takedown < ApplicationRecord
def remove_posts_by_ids!(ids)
with_lock do
self.post_ids = (post_array - ids.scan(/\d+/).map(&:to_i)).uniq.join(' ')
self.post_ids = (post_array - matching_post_ids(ids)).uniq.join(' ')
save!
end
end
def matching_post_ids(input)
input.scan(%r{(?:https://(?:e621|e926)\.net/posts/)?(\d+)}i).flatten.map(&:to_i).uniq
end
end
module PostMethods
@ -119,12 +122,12 @@ class Takedown < ApplicationRecord
end
def normalize_post_ids
self.post_ids = post_ids.scan(/\d+/).uniq.join(' ')
self.post_ids = matching_post_ids(post_ids).join(' ')
end
def normalize_deleted_post_ids
posts = post_ids.scan(/\d+/).uniq
del_posts = del_post_ids.scan(/\d+/).uniq
posts = matching_post_ids(post_ids)
del_posts = matching_post_ids(del_post_ids)
del_posts = del_posts & posts # ensure that all deleted posts are also posts
self.del_post_ids = del_posts.join(' ')
end
@ -135,7 +138,7 @@ class Takedown < ApplicationRecord
end
def del_post_array
del_post_ids.scan(/\d+/).map(&:to_i)
matching_post_ids(del_post_ids)
end
def actual_deleted_posts
@ -143,11 +146,11 @@ class Takedown < ApplicationRecord
end
def post_array
post_ids.scan(/\d+/).map(&:to_i)
matching_post_ids(post_ids)
end
def post_array_was
post_ids_was.scan(/\d+/).map(&:to_i)
matching_post_ids(post_ids_was)
end
def actual_posts

View File

@ -62,7 +62,7 @@ class Ticket < ApplicationRecord
def can_see_details?(user)
if content
content.visible?(user)
content.visible?(user) || (user.id == creator_id)
else
true
end

View File

@ -179,6 +179,12 @@ class Upload < ApplicationRecord
return
end
if (destroyed_post = DestroyedPost.find_by(md5: md5))
errors.add(:base, "An unexpected errror occured")
DummyTicket.new(uploader, destroyed_post.post_id).notify
return
end
replacements = PostReplacement.pending.where(md5: md5)
replacements = replacements.where.not(id: replacement_id) if replacement_id

View File

@ -67,25 +67,16 @@ class User < ApplicationRecord
include Danbooru::HasBitFlags
has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs"
attr_accessor :password, :old_password
attr_accessor :password, :old_password, :validate_email_format, :skip_email_blank_check
after_initialize :initialize_attributes, if: :new_record?
before_validation :normalize_email
if Danbooru.config.enable_email_verification?
validates :email, presence: { on: :create }
validates :email, presence: { on: :update, if: ->(rec) { rec.email_changed? } }
validates :email, uniqueness: { case_sensitive: false, on: :update, if: ->(rec) { rec.email.present? && rec.saved_change_to_email? } }
validates :email, uniqueness: { case_sensitive: false, on: :update, if: ->(rec) { rec.email.present? && rec.email_changed? } }
validates :email, uniqueness: { case_sensitive: false, on: :create }
validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, on: :create }
validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, on: :update, if: ->(rec) { rec.email_changed? } }
else
validates :email, uniqueness: { case_sensitive: false, on: :create, if: ->(rec) { rec.email.present?} }
end
validates :email, presence: { if: :enable_email_verification?, unless: :skip_email_blank_check }
validates :email, uniqueness: { case_sensitive: false, if: :enable_email_verification? }
validates :email, uniqueness: { case_sensitive: false, on: :create, if: ->(rec) { rec.email.present? && !Danbooru.config.enable_email_verification? } }
validates :email, format: { with: /\A.+@[^ ,;@]+\.[^ ,;@]+\z/, if: :enable_email_verification?, unless: ->(rec) { rec.email.blank? && !rec.email_changed? && rec.skip_email_blank_check } }
validate :validate_email_address_allowed, on: [:create, :update], if: ->(rec) { (rec.new_record? && rec.email.present?) || (rec.email.present? && rec.email_changed?) }
validates :name, user_name: true, on: :create
validates :default_image_size, inclusion: { :in => %w(large fit fitv original) }
validates :per_page, inclusion: { :in => 1..320 }
@ -402,8 +393,8 @@ class User < ApplicationRecord
update_attribute(:email_verification_key, nil)
end
def normalize_email
self.email = nil if email.blank?
def enable_email_verification?
Danbooru.config.enable_email_verification? && validate_email_format
end
def validate_email_address_allowed

101
app/models/user_vote.rb Normal file
View File

@ -0,0 +1,101 @@
class UserVote < ApplicationRecord
class Error < Exception; end
self.abstract_class = true
belongs_to :user
validates :score, inclusion: { in: [-1, 0, 1], message: "must be 1 or -1" }
after_initialize :initialize_attributes, if: :new_record?
scope :for_user, ->(uid) { where("user_id = ?", uid) }
def self.inherited(child_class)
super
child_class.class_eval do
belongs_to model_type
end
end
# PostVote => :post
def self.model_type
model_name.singular.delete_suffix("_vote").to_sym
end
def initialize_attributes
self.user_id ||= CurrentUser.user.id
self.user_ip_addr ||= CurrentUser.ip_addr
end
def is_positive?
score == 1
end
def is_negative?
score == -1
end
def is_locked?
score == 0
end
module SearchMethods
def search(params)
q = super
if params["#{model_type}_id"].present?
q = q.where("#{model_type}_id = ?", params["#{model_type}_id"])
end
if params[:user_name].present?
user_id = User.name_to_id(params[:user_name])
if user_id
q = q.where("user_id = ?", user_id)
else
q = q.none
end
end
if params[:user_id].present?
q = q.where("user_id = ?", params[:user_id].to_i)
end
allow_complex_params = (params.keys & ["#{model_type}_id", "user_name", "user_id"]).any?
if allow_complex_params
if params[:"#{model_type}_creator_name"].present?
creator_id = User.name_to_id(params[:"#{model_type}_creator_name"])
if creator_id
q = q.joins(model_type).where(model_type => { "#{model_creator_column}_id": creator_id })
else
q = q.none
end
end
if params[:timeframe].present?
q = q.where("#{table_name}.updated_at >= ?", params[:timeframe].to_i.days.ago)
end
if params[:user_ip_addr].present?
q = q.where("user_ip_addr <<= ?", params[:user_ip_addr])
end
if params[:score].present?
q = q.where("#{table_name}.score = ?", params[:score])
end
if params[:duplicates_only] == "1"
subselect = search(params.except("duplicates_only")).select(:user_ip_addr).group(:user_ip_addr).having("count(user_ip_addr) > 1").reorder("")
q = q.where(user_ip_addr: subselect)
end
end
if params[:order] == "ip_addr" && allow_complex_params
q = q.order(:user_ip_addr)
else
q = q.apply_default_order(params)
end
q
end
end
extend SearchMethods
end

View File

@ -1,74 +1 @@
<div id="c-comment-votes">
<div id="a-index">
<%# path is a string here because of duplicate routes %>
<%= form_search(path: "comment_votes") do |f| %>
<%= f.input :user_name, label: "Voter Username", autocomplete: "user" %>
<%= f.input :comment_id, label: "Comment ID" %>
<br>
<%= f.input :comment_creator_name, label: "Comment Creator Username", autocomplete: "user" %>
<%= f.input :timeframe, label: "Timeframe", include_blank: true, collection: [["Last Week", "7"], ["Last Month", "30"], ["Last Three Months", "90"], ["Last Year", "360"]] %>
<%= f.input :score, label: "Type", include_blank: true, collection: [["Upvote", "1"], ["Locked", "0"], ["Downvote", "-1"]] %>
<%= f.input :user_ip_addr, label: "IP Address" %>
<%= f.input :duplicates_only, label: "Duplicates Only", as: :boolean %>
<%= f.input :order, collection: [["Created", "id"], ["IP Address", "ip_addr"]] %>
<%= f.submit "Search" %>
<% end %>
<table class="striped" id='votes'>
<thead>
<tr>
<th>ID</th>
<th>Comment</th>
<th>Comment Creator</th>
<th>Voter</th>
<th>Email</th>
<th>Signed Up</th>
<th>Vote</th>
<th>Created</th>
<th>Updated</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<% @comment_votes.each do |vote| %>
<tr id="r<%= vote.id %>">
<td><%= vote.id %></td>
<td><%= link_to vote.comment_id, comment_path(vote.comment) %></td>
<td><%= mod_link_to_user vote.comment.creator, :negative %></td>
<td><%= mod_link_to_user vote.user, :negative %></td>
<td><%= vote.user.email %>
<td title="Signed up at <%= vote.user.created_at.strftime("%c") %>"><%= time_ago_in_words(vote.user.created_at) %> ago
<td>
<% if vote.score == 1 %><span class='greentext'>Up</span>
<% elsif vote.score == 0 %><span class='yellowtext'>Locked</span>
<% elsif vote.score == nil %>Unrecorded
<% else %><span class='redtext'>Down</span>
<% end %></td>
<td title="Created at <%= vote.created_at.strftime("%c") %>"><%= time_ago_in_words(vote.created_at) %> ago
</td>
<td title="Updated at <%= vote.updated_at.strftime("%c") %>"><%= time_ago_in_words(vote.updated_at) %> ago
</td>
<td><%= link_to_ip vote.user_ip_addr %></td>
</tr>
<% end %>
</tbody>
</table>
<br/>
<%= tag.button "Select All", id: "select-all-votes" %><br/>
<%= tag.button "Lock Votes", id: "lock-votes" %> Set the votes to 0, preventing the user
from voting on the image again<br/>
<%= tag.button "Delete Votes", id: "delete-votes" %> Remove the votes
<%= javascript_tag nonce: true do -%>
new Danbooru.VoteManager('comment');
<% end -%>
<div id="paginator">
<%= numbered_paginator(@comment_votes) %>
</div>
</div>
</div>
<% content_for(:page_title) do %>
Comment Votes
<% end %>
<%= render "user_votes/common_index", type: CommentVote, votes: @comment_votes %>

View File

@ -3,13 +3,7 @@
<% if comment.post.present? && (CurrentUser.is_moderator? || !comment.is_hidden?) %>
<div class="comment-post">
<div class="post-container">
<%= content_tag(:div, { id: "post_#{comment.post.id}", class: ["post", *PostPresenter.preview_class(comment.post)].join(" ") }.merge(PostPresenter.data_attributes(comment.post))) do %>
<div class="preview">
<% if comment.post.visible? %>
<%= link_to(image_tag(comment.post.preview_file_url), post_path(comment.post)) %>
<% end %>
</div>
<% end %>
<%= PostPresenter.preview(comment.post, inline: true, show_deleted: true) %>
</div>
<div class="comments list-of-comments">
<%= render "comments/partials/show/comment", comment: comment, post: nil %>

View File

@ -13,13 +13,7 @@
<% if CurrentUser.is_moderator? || post.comments.undeleted.exists? %>
<div class="comment-post">
<div class="post-container">
<%= content_tag(:div, { id: "post_#{post.id}", class: ["post", *PostPresenter.preview_class(post)].join(" ") }.merge(PostPresenter.data_attributes(post))) do %>
<div class="preview">
<% if post.visible? %>
<%= link_to(image_tag(post.preview_file_url), post_path(post)) %>
<% end %>
</div>
<% end %>
<%= PostPresenter.preview(post, inline: true, show_deleted: true) %>
<div class="post-information">
<%= render "comments/partials/index/header", :post => post %>
</div>

View File

@ -1,14 +1,6 @@
<div id="c-comments">
<div id="a-show">
<% if @comment.post %>
<%= content_tag(:div, { id: "post_#{@comment.post_id}", class: ["post", *PostPresenter.preview_class(@comment.post)].join(" ") }.merge(PostPresenter.data_attributes(@comment.post))) do %>
<div class="preview">
<% if @comment.post.visible? %>
<%= link_to(image_tag(@comment.post.preview_file_url), post_path(@comment.post)) %>
<% end %>
</div>
<% end %>
<% end %>
<%= PostPresenter.preview(@comment.post, inline: true, show_deleted: true) %>
<div class="comments-for-post">
<div class="list-of-comments">
<%= render "comments/partials/show/comment", comment: @comment, post: nil %>

View File

@ -0,0 +1,91 @@
<title><%= get_title %></title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00549e">
<meta name="theme-color" content="#00549e">
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
<link rel="top" title="<%= Danbooru.config.app_name %>" href="/">
<%= csrf_meta_tag %>
<% unless disable_mobile_mode? %>
<meta name="viewport" content="width=device-width,initial-scale=1">
<% end %>
<meta name="current-user-name" content="<%= CurrentUser.name %>">
<meta name="current-user-id" content="<%= CurrentUser.id %>">
<meta name="current-user-can-approve-posts" content="<%= CurrentUser.can_approve_posts? %>">
<meta name="user-comment-threshold" content="<%= CurrentUser.comment_threshold %>">
<% if CurrentUser.user.blacklisted_tags.present? %>
<meta name="blacklisted-tags" content="<%= CurrentUser.user.blacklisted_tags.split(/(?:\r|\n)+/).to_json %>">
<meta name="blacklist-users" content="<%= CurrentUser.blacklist_users? %>">
<% end %>
<meta name="enable-js-navigation" content="<%= CurrentUser.user.enable_keyboard_navigation %>">
<meta name="enable-auto-complete" content="<%= CurrentUser.user.enable_auto_complete %>">
<meta name="style-usernames" content="<%= CurrentUser.user.style_usernames? %>">
<meta name="last-forum-read-at" content="<%= CurrentUser.user.last_forum_read_at %>">
<% if CurrentUser.user.custom_style.present? %>
<%= stylesheet_link_tag custom_style_users_path(md5: Digest::MD5.hexdigest(CurrentUser.user.custom_style)), media: "screen", nonce: true %>
<% end %>
<% if flash[:notice] =~ /error/i %>
<meta name="errors" content="true">
<% end %>
<%= auto_discovery_link_tag :atom, posts_path(:format => "atom", :tags => params[:tags]) %>
<%= javascript_include_tag "/vendor/jquery-3.5.0.min.js", nonce: true, integrity: "sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=", crossorigin: "anonymous" %>
<%= stylesheet_link_tag "/vendor/fontawesome/css/all.min.css", nonce: true, integrity: "sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf", crossorigin: "anonymous" %>
<%= stylesheet_pack_tag "application", nonce: true %>
<%= javascript_pack_tag "application", nonce: true, defer: false %>
<% if Danbooru.config.twitter_site %>
<script type="application/ld+json">
{
"@context" : "http://schema.org",
"@type" : "Organization",
"name" : "<%= Danbooru.config.app_name %>",
"url" : "<%= root_url %>",
"sameAs" : [
"https://twitter.com/<%= Danbooru.config.twitter_site[1..-1] %>"
]
}
</script>
<% end %>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"url" : "<%= root_url %>",
"potentialAction": [{
"@type": "SearchAction",
"target": "<%= posts_url %>?tags={search_term_string}",
"query-input": "required name=search_term_string"
}]
}
</script>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"name": "<%= Danbooru.config.app_name %>",
"alternateName": "<%= Danbooru.config.description %>",
"url" : "<%= root_url %>"
}
</script>
<style id="blacklisted-hider">
.post-preview, #image-container, #c-comments .post, .post-thumbnail {
visibility: hidden !important;
}
</style>
<noscript>
<style>
.post-preview, #image-container, #c-comments .post, .post-thumbnail {
visibility: visible !important;
}
</style>
</noscript>
<%= raw Danbooru.config.custom_html_header_content %>
<%= yield :html_header %>

View File

@ -1,25 +1,12 @@
<!doctype html>
<html>
<head>
<title><%= get_title %></title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="top" title="<%= Danbooru.config.app_name %>" href="/">
<%= csrf_meta_tag %>
<% unless disable_mobile_mode? %>
<meta name="viewport" content="width=device-width,initial-scale=1">
<% end %>
<%= auto_discovery_link_tag :atom, posts_path(:format => "atom", :tags => params[:tags]) %>
<%= raw Danbooru.config.custom_html_header_content %>
<script src="/vendor/jquery-3.5.0.min.js" integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<%= stylesheet_pack_tag "application", nonce: true %>
<%= javascript_pack_tag "application", nonce: true %>
<%= yield :html_header %>
<%= render "layouts/head" %>
</head>
<body lang="en">
<%= tag.body **body_attributes(CurrentUser.user) do %>
<%= render "layouts/theme_include" %>
<div id="page">
<%= yield :layout %>
</div>
</body>
<% end %>
</html>

View File

@ -1,91 +1,7 @@
<!doctype html>
<html>
<head>
<title><%= get_title %></title>
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00549e">
<meta name="theme-color" content="#00549e">
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
<link rel="top" title="<%= Danbooru.config.app_name %>" href="/">
<%= csrf_meta_tag %>
<% unless disable_mobile_mode? %>
<meta name="viewport" content="width=device-width,initial-scale=1">
<% end %>
<meta name="current-user-name" content="<%= CurrentUser.name %>">
<meta name="current-user-id" content="<%= CurrentUser.id %>">
<meta name="current-user-can-approve-posts" content="<%= CurrentUser.can_approve_posts? %>">
<meta name="user-comment-threshold" content="<%= CurrentUser.comment_threshold %>">
<% if CurrentUser.user.blacklisted_tags.present? %>
<meta name="blacklisted-tags" content="<%= CurrentUser.user.blacklisted_tags.split(/(?:\r|\n)+/).to_json %>">
<meta name="blacklist-users" content="<%= CurrentUser.blacklist_users? %>">
<% end %>
<% if flash[:notice] =~ /error/i %>
<meta name="errors" content="true">
<% end %>
<meta name="enable-js-navigation" content="<%= CurrentUser.user.enable_keyboard_navigation %>">
<meta name="enable-auto-complete" content="<%= CurrentUser.user.enable_auto_complete %>">
<meta name="style-usernames" content="<%= CurrentUser.user.style_usernames? %>">
<meta name="last-forum-read-at" content="<%= CurrentUser.user.last_forum_read_at %>">
<%= auto_discovery_link_tag :atom, posts_path(:format => "atom", :tags => params[:tags]) %>
<%= javascript_include_tag "/vendor/jquery-3.5.0.min.js", nonce: true, integrity: "sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=", crossorigin: "anonymous" %>
<%= stylesheet_link_tag "/vendor/fontawesome/css/all.min.css", nonce: true, integrity: "sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf", crossorigin: "anonymous" %>
<%= stylesheet_pack_tag "application", nonce: true %>
<%= javascript_pack_tag "application", nonce: true, defer: false %>
<% if CurrentUser.user.custom_style.present? %>
<%= stylesheet_link_tag custom_style_users_path(md5: Digest::MD5.hexdigest(CurrentUser.user.custom_style)), media: "screen", nonce: true %>
<% end %>
<% if Danbooru.config.twitter_site %>
<script type="application/ld+json">
{
"@context" : "http://schema.org",
"@type" : "Organization",
"name" : "<%= Danbooru.config.app_name %>",
"url" : "<%= root_url %>",
"sameAs" : [
"https://twitter.com/<%= Danbooru.config.twitter_site[1..-1] %>"
]
}
</script>
<% end %>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"url" : "<%= root_url %>",
"potentialAction": [{
"@type": "SearchAction",
"target": "<%= posts_url %>?tags={search_term_string}",
"query-input": "required name=search_term_string"
}]
}
</script>
<script type="application/ld+json">
{
"@context": "http://schema.org",
"@type": "WebSite",
"name": "<%= Danbooru.config.app_name %>",
"alternateName": "<%= Danbooru.config.description %>",
"url" : "<%= root_url %>"
}
</script>
<%= yield :html_header %>
<%= raw Danbooru.config.custom_html_header_content %>
<style id="blacklisted-hider">
.post-preview, #image-container, #c-comments .post, .post-thumbnail {
visibility: hidden !important;
}
</style>
<noscript>
<style>
.post-preview, #image-container, #c-comments .post, .post-thumbnail {
visibility: visible !important;
}
</style>
</noscript>
<%= render "layouts/head" %>
</head>
<%= tag.body **body_attributes(CurrentUser.user) do %>
<%= render "layouts/theme_include" %>
@ -120,7 +36,7 @@
</div>
<% end %>
<%= render "news_updates/listing" %>
<%= render "news_updates/notice", news_update: NewsUpdate.recent %>
<% if CurrentUser.user.is_banned? %>
<%= render "users/ban_notice" %>

View File

@ -0,0 +1,11 @@
<%= custom_form_for(mascot) do |f| %>
<%= error_messages_for "mascot" %>
<%= f.input :mascot_file, as: :file, input_html: { accept: "image/png,image/jpeg,.png,.jpg,.jpeg" } %>
<%= f.input :display_name %>
<%= f.input :background_color %>
<%= f.input :artist_url %>
<%= f.input :artist_name %>
<%= f.input :safe_mode_only, label: "E9 Only" %>
<%= f.input :active %>
<%= f.submit %>
<% end %>

View File

@ -0,0 +1,8 @@
<% content_for(:secondary_links) do %>
<menu>
<%= subnav_link_to "Listing", mascots_path %>
<% if CurrentUser.user.is_admin? %>
<%= subnav_link_to "New", new_mascot_path %>
<% end %>
</menu>
<% end %>

View File

@ -0,0 +1,12 @@
<div class="c-mascots">
<div class="a-edit">
<h1>Edit Mascot</h1>
<%= render "form", mascot: @mascot %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Edit Mascot
<% end %>

View File

@ -0,0 +1,49 @@
<div id="c-mascots">
<div id="a-index">
<h1>Mascots</h1>
<table class="striped">
<thead>
<tr>
<th>Name</th>
<th>Background Color</th>
<th>Artist Name</th>
<th>Artist URL</th>
<th>Active</th>
<th>E9 Only</th>
<th>Created</th>
<% if CurrentUser.user.is_admin? %>
<th></th>
<% end %>
</tr>
</thead>
<tbody>
<% @mascots.each do |mascot| %>
<tr>
<td><%= link_to mascot.display_name, mascot.url_path %></td>
<td><%= mascot.background_color %></td>
<td><%= mascot.artist_name %></td>
<td><%= mascot.artist_url %></td>
<td><%= mascot.active %></td>
<td><%= mascot.safe_mode_only %></td>
<td><%= compact_time mascot.created_at %></td>
<% if CurrentUser.user.is_admin? %>
<td>
<%= link_to "Edit", edit_mascot_path(mascot) %>
| <%= link_to "Delete", mascot_path(mascot), method: :delete, data: { confirm: "Are you sure you want to delete this mascot?" } %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
<%= numbered_paginator(@mascots) %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
Mascots
<% end %>

View File

@ -0,0 +1,12 @@
<div class="c-mascots">
<div class="a-new">
<h1>New Mascot</h1>
<%= render "form", mascot: @mascot %>
</div>
</div>
<%= render "secondary_links" %>
<% content_for(:page_title) do %>
New Mascot
<% end %>

View File

@ -5,6 +5,9 @@
<%= f.input :user_id, label: "User IDs" %>
<%= f.input :user_name, label: "User Names", autocomplete: "user" %>
<%= f.input :ip_addr, label: "IP Addresses" %>
<%= f.input :with_history, label: "With History", as: :boolean %>
<% if (params.dig(:search, :ip_addr)&.split(",")&.count || 0) == 1 %>
<%= f.input :add_ip_mask, label: "Add IP Mask?", as: :boolean, hint: "/24 for IPv4, /64 for IPv6" %>
<% end %>
<%= f.input :with_history, label: "With History?", as: :boolean %>
<%= f.submit "Search" %>
<% end %>

View File

@ -1,10 +0,0 @@
<% if NewsUpdate.recent.present? %>
<div class="ui-state-highlight site-notice" style="display: none;" id="news" data-id="<%= NewsUpdate.recent[0].id %>">
<div id="news-closebutton" class="closebutton">Dismiss</div>
<h6>News - <%= NewsUpdate.recent[0].created_at.strftime("%b %d, %Y") %>
(<%= time_ago_in_words NewsUpdate.recent[0].created_at %> ago)
<span id="news-showtext" class="showtext">Click to show.</span>
</h6>
<div class="newsbody dtext-container"><%= format_text(NewsUpdate.recent[0].message) %></div>
</div>
<% end %>

View File

@ -1,3 +0,0 @@
<% if NewsUpdate.recent.present? %>
<div class="dtext-container"><%= format_text(NewsUpdate.recent[0].message) %></div>
<% end %>

View File

@ -0,0 +1,10 @@
<% if news_update.present? %>
<div class="ui-state-highlight site-notice" style="display: none;" id="news" data-id="<%= news_update.id %>">
<div id="news-closebutton" class="closebutton">Dismiss</div>
<h6>News - <%= news_update.created_at.strftime("%b %d, %Y") %>
(<%= time_ago_in_words news_update.created_at %> ago)
<span id="news-showtext" class="showtext">Click to show.</span>
</h6>
<div class="newsbody dtext-container"><%= format_text(news_update.message) %></div>
</div>
<% end %>

View File

@ -16,7 +16,7 @@
<%= @post_set.presenter.post_previews_html(self) %>
</div>
<%= numbered_paginator(@post_set) %>
<%= numbered_paginator(@post_set.posts) %>
</div>
</div>
</div>

View File

@ -47,7 +47,7 @@
</div>
</div>
<% elsif post_version.description_changed && post_version.version != 1%>
Cleared
<em>Cleared</em>
<% end %>
</div>
<div class="pv-tags-locked pv-content">

View File

@ -1,70 +1 @@
<div id="c-post-votes">
<div id="a-index">
<%# path is a string here because of duplicate routes %>
<%= form_search(path: "post_votes") do |f| %>
<%= f.input :user_name, label: "Username", autocomplete: "user" %>
<%= f.input :post_id, label: "Post ID" %>
<br>
<%= f.input :timeframe, label: "Timeframe", include_blank: true, collection: [["Last Week", "7"], ["Last Month", "30"], ["Last Three Months", "90"], ["Last Year", "360"]] %>
<%= f.input :score, label: "Type", include_blank: true, collection: [["Upvote", "1"], ["Locked", "0"], ["Downvote", "-1"]] %>
<%= f.input :user_ip_addr, label: "IP Address" %>
<%= f.input :duplicates_only, label: "Duplicates Only", as: :boolean %>
<%= f.input :order, collection: [["Created", "id"], ["IP Address", "ip_addr"]] %>
<%= f.submit "Search" %>
<% end %>
<table class='striped' id='votes'>
<thead>
<tr>
<th>ID</th>
<th>Post</th>
<th>Voter</th>
<th>Email</th>
<th>Signed Up</th>
<th>Vote</th>
<th>Created</th>
<th>Updated</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<% @post_votes.each do |vote| %>
<tr id="r<%= vote.id %>">
<td><%= vote.id %></td>
<td><%= link_to vote.post_id, post_path(id: vote.post_id) %></td>
<td><%= mod_link_to_user vote.user, :negative %></td>
<td><%= vote.user.email %>
<td title="Signed up at <%= vote.user.created_at.strftime("%c") %>"><%= time_ago_in_words(vote.user.created_at) %> ago
<td>
<% if vote.score == 1 %><span class='greentext'>Up</span>
<% elsif vote.score == 0 %><span class='yellowtext'>Locked</span>
<% else %><span class='redtext'>Down</span>
<% end %></td>
<td title="Created at <%= vote.created_at.strftime("%c") %>"><%= time_ago_in_words(vote.created_at) %> ago
</td>
<td title="Updated at <%= vote.updated_at.strftime("%c") %>"><%= time_ago_in_words(vote.updated_at) %> ago
</td>
<td><%= link_to_ip vote.user_ip_addr %></td>
</tr>
</tbody>
<% end %>
</table>
<br/>
<%= tag.button "Select All", id: "select-all-votes" %><br/>
<%= tag.button "Lock Votes", id: "lock-votes" %> Set the votes to 0, preventing the user from
voting on the image again<br/>
<%= tag.button "Delete Votes", id: "delete-votes" %> Remove the votes
<%= javascript_tag nonce: true do -%>
new Danbooru.VoteManager('post');
<% end -%>
<div id="paginator">
<%= numbered_paginator(@post_votes) %>
</div>
</div>
</div>
<% content_for(:page_title) do %>
Post Votes
<% end %>
<%= render "user_votes/common_index", type: PostVote, votes: @post_votes %>

View File

@ -32,6 +32,10 @@
}
.mascotbox {
z-index: 1;
overflow: hidden;
position: relative;
background-repeat:no-repeat;
background-attachment:fixed;
background-position:50% 0;
@ -44,6 +48,23 @@
text-shadow: 0 0 2px black, 0 0 6px black;
}
.mascotbox:before {
z-index: -1;
content: "";
position: absolute;
width: 200vw;
height: 200vh;
top: -100px;
left: -100px;
background-image: inherit;
background-color: inherit;
background-repeat: inherit;
background-attachment: inherit;
background-position: inherit;
filter: blur(8px);
}
#searchbox {
padding-bottom:5px;
}
@ -66,7 +87,7 @@
<div id="c-static">
<div id="a-home">
<%= javascript_tag nonce: true do -%>
var mascots = <%= Danbooru.config.mascots.to_json.html_safe %>;
var mascots = <%= Mascot.active_for_browser.to_json.html_safe %>;
<% end -%>
<div id="searchbox" class='mascotbox'>
@ -81,7 +102,7 @@
<%= link_to "Posts", posts_path, title: "A paginated list of every post" %>
<%= link_to "Comments", comments_path, title: "A paginated list of every comment" %>
<%= link_to "Tags", tags_path, title: "A paginated list of every tag" %>
<%= link_to "Wiki", wiki_pages_path, title: "Wiki" %>
<%= link_to "Wiki", wiki_pages_path(title: 'help:home'), title: "Wiki" %>
<%= link_to "Forum", forum_topics_path, title: "Forum" %>
<%= link_to "&raquo;".html_safe, site_map_path, title: "A site map" %>
</div>
@ -104,9 +125,9 @@
<% end %>
</div>
<% if NewsUpdate.recent.present? %>
<% if news_update = NewsUpdate.recent %>
<div id="news-excerpt-box" class="mascotbox">
<div class="news-excerpt dtext-container"><%= format_text(NewsUpdate.recent[0].message.lines.first, inline: true) %></div>
<div class="news-excerpt dtext-container"><%= format_text(news_update.message.lines.first, inline: true) %></div>
<div class="previous-news-link"><%= link_to "Click here for previous news", news_updates_path %></div>
</div>
<% end %>

View File

@ -30,6 +30,7 @@
<ul>
<li><h1>Tools</h1></li>
<li><%= link_to("News Updates", news_updates_path) %></li>
<li><%= link_to("Mascots", mascots_path) %></li>
<li><%= link_to("Source Code", Danbooru.config.source_code_url) %></li>
<li><%= link_to("Keyboard Shortcuts", keyboard_shortcuts_path) %></li>
<li><%= link_to("API Documentation", help_page_path(id: "api")) %></li>

View File

@ -0,0 +1,70 @@
<div id="c-<%= type.model_name.plural %>">
<div id="a-index">
<%# path is a string here because of duplicate routes %>
<%= form_search(path: type.model_name.route_key) do |f| %>
<%= f.input :user_name, label: "Voter Username", autocomplete: "user" %>
<%= f.input :"#{type.model_type}_id", label: "#{type.model_type.capitalize} ID" %>
<br>
<%= f.input :"#{type.model_type}_creator_name", label: "#{type.model_type.capitalize} Creator Username", autocomplete: "user" %>
<%= f.input :timeframe, label: "Timeframe", include_blank: true, collection: [["Last Week", "7"], ["Last Month", "30"], ["Last Three Months", "90"], ["Last Year", "360"]] %>
<%= f.input :score, label: "Type", include_blank: true, collection: [["Upvote", "1"], ["Locked", "0"], ["Downvote", "-1"]] %>
<%= f.input :user_ip_addr, label: "IP Address" %>
<%= f.input :duplicates_only, label: "Duplicates Only", as: :boolean %>
<%= f.input :order, collection: [["Created", "id"], ["IP Address", "ip_addr"]] %>
<%= f.submit "Search" %>
<% end %>
<table class="striped" id='votes'>
<thead>
<tr>
<th>ID</th>
<th><%= type.model_type.capitalize %></th>
<th><%= type.model_type.capitalize %> Creator</th>
<th>Voter</th>
<th>Email</th>
<th>Signed Up</th>
<th>Vote</th>
<th>Created</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<% votes.each do |vote| %>
<tr id="r<%= vote.id %>">
<td><%= vote.id %></td>
<td><%= link_to vote.send("#{type.model_type}_id"), vote.send(type.model_type) %></td>
<td><%= mod_link_to_user vote.send(type.model_type).send(type.model_creator_column), :negative %></td>
<td><%= mod_link_to_user vote.user, :negative %></td>
<td><%= vote.user.email %>
<td title="Signed up at <%= vote.user.created_at.strftime("%c") %>"><%= time_ago_in_words(vote.user.created_at) %> ago
<td>
<% if vote.is_positive? %><span class='greentext'>Up</span>
<% elsif vote.is_locked? %><span class='yellowtext'>Locked</span>
<% else %><span class='redtext'>Down</span>
<% end %></td>
<td title="Created at <%= vote.created_at.strftime("%c") %>"><%= time_ago_in_words(vote.created_at) %> ago
</td>
<td><%= link_to_ip vote.user_ip_addr %></td>
</tr>
<% end %>
</tbody>
</table>
<br/>
<%= tag.button "Select All", id: "select-all-votes" %><br/>
<%= tag.button "Lock Votes", id: "lock-votes" %> Set the votes to 0, preventing the user
from voting on the <%= type.model_type %> again<br/>
<%= tag.button "Delete Votes", id: "delete-votes" %> Remove the votes
<%= javascript_tag nonce: true do -%>
new Danbooru.VoteManager('<%= type.model_type %>');
<% end -%>
<div id="paginator">
<%= numbered_paginator(votes) %>
</div>
</div>
</div>
<% content_for(:page_title) do %>
<%= type.model_name.plural.titleize %>
<% end %>

View File

@ -352,8 +352,8 @@ fart'
def max_file_sizes
{
'jpg' => 100.megabytes,
'gif' => 20.megabytes,
'png' => 100.megabytes,
'gif' => 20.megabytes,
'webm' => 100.megabytes
}
end
@ -600,6 +600,8 @@ fart'
"Previously deleted (post #%PARENT_ID%)",
"Excessive same base image set",
"Colored base",
"Advertisment",
"Underage artist",
"",
"Does not meet minimum quality standards (Artistic)",
"Does not meet minimum quality standards (Resolution)",
@ -614,6 +616,7 @@ fart'
"Irrelevant to site (Human only)",
"Irrelevant to site (Screencap)",
"Irrelevant to site (Zero pictured)",
"Irrelevant to site (AI assisted/generated)",
"Irrelevant to site (%OTHER_ID%)",
"",
"Paysite/commercial content",
@ -783,19 +786,6 @@ fart'
{zone: nil, revive_id: nil, checksum: nil}
end
def mascots
[
["https://static1.e621.net/data/mascot_bg/esix1.jpg", "#012e56", "<a href='http://www.furaffinity.net/user/keishinkae'>Keishinkae</a>"],
["https://static1.e621.net/data/mascot_bg/esix2.jpg", "#012e56", "<a href='http://www.furaffinity.net/user/keishinkae'>Keishinkae</a>"],
["https://static1.e621.net/data/mascot_bg/raptor1.jpg", "#012e56", "<a href='http://nowhereincoming.net/'>darkdoomer</a>"],
["https://static1.e621.net/data/mascot_bg/hexerade.jpg", "#002d55", "<a href='http://www.furaffinity.net/user/chizi'>chizi</a>"],
["https://static1.e621.net/data/mascot_bg/wiredhooves.jpg", "#012e56", "<a href='http://www.furaffinity.net/user/wiredhooves'>wiredhooves</a>"],
["https://static1.e621.net/data/mascot_bg/ecmajor.jpg", "#012e57", "<a href='http://www.horsecore.org/'>ECMajor</a>"],
["https://static1.e621.net/data/mascot_bg/evalionfix.jpg", "#012e57", "<a href='http://www.furaffinity.net/user/evalion'>evalion</a>"],
["https://static1.e621.net/data/mascot_bg/peacock.png", "#012e57", "<a href='http://www.furaffinity.net/user/ratte'>Ratte</a>"]
]
end
# Additional video samples will be generated in these dimensions if it makes sense to do so
# They will be available as additional scale options on applicable posts in the order they appear here
def video_rescales

View File

@ -341,6 +341,7 @@ Rails.application.routes.draw do
get :resend_confirmation
end
end
resources :mascots, only: [:index, :new, :create, :edit, :update, :destroy]
options "*all", to: "application#enable_cors"

View File

@ -0,0 +1,16 @@
class AddMascotTable < ActiveRecord::Migration[6.1]
def change
create_table :mascots do |t|
t.references :creator, foreign_key: { to_table: :users }, null: false
t.string :display_name, null: false
t.string :md5, index: { unique: true }, null: false
t.string :file_ext, null: false
t.string :background_color, null: false
t.string :artist_url, null: false
t.string :artist_name, null: false
t.boolean :safe_mode_only, default: false, null: false
t.boolean :active, default: true, null: false
t.timestamps
end
end
end

View File

@ -13,7 +13,7 @@ end
puts "== Seeding database with sample content ==\n"
# Uncomment to see detailed logs
#ActiveRecord::Base.logger = ActiveSupport::Logger.new($stdout)
# ActiveRecord::Base.logger = ActiveSupport::Logger.new($stdout)
admin = User.find_or_create_by!(name: "admin") do |user|
user.created_at = 2.weeks.ago
@ -41,41 +41,57 @@ ForumCategory.find_or_create_by!(id: Danbooru.config.alias_implication_forum_cat
category.can_view = 0
end
unless Rails.env.test?
CurrentUser.user = admin
CurrentUser.ip_addr = "127.0.0.1"
resources = YAML.load_file Rails.root.join("db", "seeds.yml")
url = "https://e621.net/posts.json?limit=#{ENV.fetch("SEED_POST_COUNT", 100)}&tags=id:#{resources["post_ids"].join(",")}"
response = HTTParty.get(url, {
headers: {"User-Agent" => "e621ng/seeding"}
def api_request(path)
response = HTTParty.get("https://e621.net#{path}", {
headers: { "User-Agent" => "e621ng/seeding" },
})
json = JSON.parse(response.body)
JSON.parse(response.body)
end
def import_posts
resources = YAML.load_file Rails.root.join("db/seeds.yml")
json = api_request("/posts.json?limit=#{ENV.fetch('SEED_POST_COUNT', 100)}&tags=id:#{resources['post_ids'].join(',')}")
json["posts"].each do |post|
puts post["file"]["url"]
data = Net::HTTP.get(URI(post["file"]["url"]))
file = Tempfile.new.binmode
file.write data
post["tags"].each do |category, tags|
Tag.find_or_create_by_name_list(tags.map {|tag| category + ":" + tag})
Tag.find_or_create_by_name_list(tags.map { |tag| "#{category}:#{tag}" })
end
md5 = Digest::MD5.hexdigest(data)
service = UploadService.new({
uploader_id: CurrentUser.id,
uploader_ip_addr: CurrentUser.ip_addr,
file: file,
tag_string: post["tags"].values.flatten.join(" "),
source: post["sources"].join("\n"),
description: post["description"],
rating: post["rating"],
md5: md5,
md5_confirmation: md5
})
uploader: CurrentUser.user,
uploader_ip_addr: CurrentUser.ip_addr,
direct_url: post["file"]["url"],
tag_string: post["tags"].values.flatten.join(" "),
source: post["sources"].join("\n"),
description: post["description"],
rating: post["rating"],
})
service.start!
end
end
def import_mascots
api_request("/mascots.json").each do |mascot|
puts mascot["url_path"]
Mascot.create!(
creator: CurrentUser.user,
mascot_file: Downloads::File.new(mascot["url_path"]).download!,
display_name: mascot["display_name"],
background_color: mascot["background_color"],
artist_url: mascot["artist_url"],
artist_name: mascot["artist_name"],
safe_mode_only: mascot["safe_mode_only"],
active: mascot["active"],
)
end
end
unless Rails.env.test?
CurrentUser.user = admin
CurrentUser.ip_addr = "127.0.0.1"
import_posts
import_mascots
end

View File

@ -1034,6 +1034,45 @@ CREATE SEQUENCE public.janitor_trials_id_seq
ALTER SEQUENCE public.janitor_trials_id_seq OWNED BY public.janitor_trials.id;
--
-- Name: mascots; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.mascots (
id bigint NOT NULL,
creator_id bigint NOT NULL,
display_name character varying NOT NULL,
md5 character varying NOT NULL,
file_ext character varying NOT NULL,
background_color character varying NOT NULL,
artist_url character varying NOT NULL,
artist_name character varying NOT NULL,
safe_mode_only boolean DEFAULT false NOT NULL,
active boolean DEFAULT true NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: mascots_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.mascots_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: mascots_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.mascots_id_seq OWNED BY public.mascots.id;
--
-- Name: mod_actions_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
@ -2623,6 +2662,13 @@ ALTER TABLE ONLY public.ip_bans ALTER COLUMN id SET DEFAULT nextval('public.ip_b
ALTER TABLE ONLY public.janitor_trials ALTER COLUMN id SET DEFAULT nextval('public.janitor_trials_id_seq'::regclass);
--
-- Name: mascots id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.mascots ALTER COLUMN id SET DEFAULT nextval('public.mascots_id_seq'::regclass);
--
-- Name: news_updates id; Type: DEFAULT; Schema: public; Owner: -
--
@ -3076,6 +3122,14 @@ ALTER TABLE ONLY public.janitor_trials
ADD CONSTRAINT janitor_trials_pkey PRIMARY KEY (id);
--
-- Name: mascots mascots_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.mascots
ADD CONSTRAINT mascots_pkey PRIMARY KEY (id);
--
-- Name: mod_actions mod_actions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -3765,6 +3819,20 @@ CREATE UNIQUE INDEX index_ip_bans_on_ip_addr ON public.ip_bans USING btree (ip_a
CREATE INDEX index_janitor_trials_on_user_id ON public.janitor_trials USING btree (user_id);
--
-- Name: index_mascots_on_creator_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_mascots_on_creator_id ON public.mascots USING btree (creator_id);
--
-- Name: index_mascots_on_md5; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_mascots_on_md5 ON public.mascots USING btree (md5);
--
-- Name: index_mod_actions_on_action; Type: INDEX; Schema: public; Owner: -
--
@ -4452,6 +4520,14 @@ ALTER TABLE ONLY public.staff_audit_logs
ADD CONSTRAINT fk_rails_02329e5ef9 FOREIGN KEY (user_id) REFERENCES public.users(id);
--
-- Name: mascots fk_rails_9901e810fa; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.mascots
ADD CONSTRAINT fk_rails_9901e810fa FOREIGN KEY (creator_id) REFERENCES public.users(id);
--
-- Name: favorites fk_rails_a7668ef613; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -4734,6 +4810,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20220316162257'),
('20220516103329'),
('20220710133556'),
('20220810131625');
('20220810131625'),
('20221014085948');

View File

@ -13,7 +13,10 @@ x-environment: &common-env
services:
e621:
build: ./
build:
context: ./
args:
COMPOSE_PROFILES: ${COMPOSE_PROFILES-}
image: e621
volumes:
- .:/app
@ -31,24 +34,6 @@ services:
- iqdb
tty: true
tests:
image: e621
environment:
<<: *common-env
# Hide annoying output from libvips on corrupt files
VIPS_WARNING: "0"
volumes:
- .:/app
- node_modules:/app/node_modules
depends_on:
- postgres
- redis
- memcached
- elastic
entrypoint: ["docker/test_runner.sh"]
profiles:
- test
nginx:
image: nginx:stable-alpine
volumes:
@ -111,6 +96,45 @@ services:
- post_data:/data
- iqdb_data:/home/vagrant/iqdbs
# Useful for development
tests:
image: e621
environment:
<<: *common-env
# Hide annoying output from libvips on corrupt files
VIPS_WARNING: "0"
volumes:
- .:/app
- node_modules:/app/node_modules
depends_on:
- postgres
- redis
- memcached
- elastic
entrypoint: ["docker/test_runner.sh"]
profiles:
- tests
rubocop:
image: e621
volumes:
- .:/app
entrypoint: bundle exec rubocop
profiles:
- rubocop
solargraph:
image: e621
entrypoint: solargraph socket -h 0.0.0.0
working_dir: $PWD
volumes:
- .:$PWD
ports:
- 7658:7658
profiles:
- solargraph
volumes:
post_data:
iqdb_data:

View File

@ -42,6 +42,26 @@ class Admin::UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(50, @admin.level)
end
end
context "on an user with a blank email" do
setup do
@user = create(:user, email: "")
Danbooru.config.stubs(:enable_email_verification?).returns(true)
end
should "succeed" do
put_auth admin_user_path(@user), @mod, params: { user: { level: "20", email: "" } }
assert_redirected_to(user_path(@user))
@user.reload
assert_equal(20, @user.level)
end
should "prevent invalid emails" do
put_auth admin_user_path(@user), @mod, params: { user: { level: "10", email: "invalid" } }
@user.reload
assert_equal("", @user.email)
end
end
end
end
end

View File

@ -5,6 +5,7 @@ module Maintenance
class EmailChangesControllerTest < ActionDispatch::IntegrationTest
context "in all cases" do
setup do
Danbooru.config.stubs(:enable_email_verification?).returns(true)
@user = create(:user, email: "bob@ogres.net")
end
@ -32,6 +33,20 @@ module Maintenance
assert_equal("bob@ogres.net", @user.email)
end
end
should "not work with an invalid email" do
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "" } }
@user.reload
assert_not_equal("", @user.email)
assert_match(/Email can't be blank/, flash[:notice])
end
should "work with a valid email when the users current email is invalid" do
@user = create(:user, email: "")
post_auth maintenance_user_email_change_path, @user, params: { email_change: { password: "password", email: "abc@ogres.net" } }
@user.reload
assert_equal("abc@ogres.net", @user.email)
end
end
end
end

View File

@ -12,14 +12,9 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
should "list all users for /users?name=<name>" do
get users_path, params: { name: @user.name }
assert_redirected_to(@user)
end
should "raise error for /users?name=<nonexistent>" do
get users_path, params: { name: "nobody" }
assert_response :error
should "redirect for /users?name=<name>" do
get users_path, params: { name: "some_username" }
assert_redirected_to(user_path(id: "some_username"))
end
should "list all users (with search)" do
@ -96,6 +91,43 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end
end
end
context "with a duplicate username" do
setup do
create(:user, name: "test123")
end
should "prevent creation" do
assert_no_difference(-> { User.count }) do
post users_path, params: { user: { name: "TEst123", password: "xxxxx1", password_confirmation: "xxxxx1" } }
assert_match(/Name already exists/, flash[:notice])
end
end
end
context "with email validation" do
setup do
Danbooru.config.stubs(:enable_email_verification?).returns(true)
end
should "reject invalid emails" do
assert_no_difference(-> { User.count }) do
post users_path, params: { user: { name: "test", password: "xxxxxx", password_confirmation: "xxxxxx" } }
assert_match(/Email can't be blank/, flash[:notice])
post users_path, params: { user: { name: "test", password: "xxxxxx", password_confirmation: "xxxxxx", email: "invalid" } }
assert_match(/Email is invalid/, flash[:notice])
end
end
should "reject duplicate emails" do
create(:user, email: "valid@e621.net")
assert_no_difference(-> { User.count }) do
post users_path, params: { user: { name: "test2", password: "xxxxxx", password_confirmation: "xxxxxx", email: "VaLid@E621.net" } }
assert_match(/Email has already been taken/, flash[:notice])
end
end
end
end
context "edit action" do
@ -131,6 +163,18 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(20, @user.level)
end
end
context "for an user with blank email" do
setup do
@user = create(:user, email: "")
Danbooru.config.stubs(:enable_email_verification?).returns(true)
end
should "force them to update their email" do
put_auth user_path(@user), @user, params: { user: { comment_threshold: "-100" } }
assert_match(/Email can't be blank/, flash[:notice])
end
end
end
end
end

View File

@ -197,33 +197,33 @@ class ArtistTest < ActiveSupport::TestCase
end
should "search on its name should return results" do
artist = FactoryBot.create(:artist, :name => "artist")
FactoryBot.create(:artist, name: "artist")
assert_not_nil(Artist.search(:name => "artist").first)
assert_not_nil(Artist.search(:name_like => "artist").first)
assert_not_nil(Artist.search(:any_name_matches => "artist").first)
assert_not_nil(Artist.search(:any_name_matches => "/art/").first)
assert_not_nil(Artist.search(name: "artist").first)
assert_not_nil(Artist.search(name_like: "artist").first)
assert_not_nil(Artist.search(any_name_matches: "artist").first)
assert_not_nil(Artist.search(any_name_matches: "*art*").first)
end
should "search on other names should return matches" do
artist = FactoryBot.create(:artist, :name => "artist", :other_names_string => "aaa ccc_ddd")
FactoryBot.create(:artist, name: "artist", other_names_string: "aaa ccc_ddd")
assert_nil(Artist.search(any_other_name_like: "*artist*").first)
assert_not_nil(Artist.search(any_other_name_like: "*aaa*").first)
assert_not_nil(Artist.search(any_other_name_like: "*ccc_ddd*").first)
assert_not_nil(Artist.search(name: "artist").first)
assert_not_nil(Artist.search(:any_name_matches => "aaa").first)
assert_not_nil(Artist.search(:any_name_matches => "/a/").first)
assert_not_nil(Artist.search(any_name_matches: "aaa").first)
assert_not_nil(Artist.search(any_name_matches: "*a*").first)
end
should "search on group name and return matches" do
cat_or_fish = FactoryBot.create(:artist, :name => "cat_or_fish")
yuu = FactoryBot.create(:artist, :name => "yuu", :group_name => "cat_or_fish")
cat_or_fish = FactoryBot.create(:artist, name: "cat_or_fish")
FactoryBot.create(:artist, name: "yuu", group_name: "cat_or_fish")
assert_equal("yuu", cat_or_fish.member_names)
assert_not_nil(Artist.search(:group_name => "cat_or_fish").first)
assert_not_nil(Artist.search(:any_name_matches => "cat_or_fish").first)
assert_not_nil(Artist.search(:any_name_matches => "/cat/").first)
assert_not_nil(Artist.search(group_name: "cat_or_fish").first)
assert_not_nil(Artist.search(any_name_matches: "cat_or_fish").first)
assert_not_nil(Artist.search(any_name_matches: "*cat*").first)
end
should "search on url and return matches" do
@ -231,7 +231,7 @@ class ArtistTest < ActiveSupport::TestCase
assert_equal([bkub.id], Artist.search(url_matches: "bkub").map(&:id))
assert_equal([bkub.id], Artist.search(url_matches: "*bkub*").map(&:id))
assert_equal([bkub.id], Artist.search(url_matches: "/rifyu|bkub/").map(&:id))
assert_equal([], Artist.search(url_matches: "*rifyu*").map(&:id))
assert_equal([bkub.id], Artist.search(url_matches: "http://bkub.com/test.jpg").map(&:id))
end

View File

@ -93,11 +93,9 @@ class ArtistUrlTest < ActiveSupport::TestCase
assert_search_equals([@bkub_url], artist: { name: "bkub" })
assert_search_equals([@bkub_url], url_matches: "*bkub*")
assert_search_equals([@bkub_url], url_matches: "/^https?://bkub\.com$/")
assert_search_equals([@bkub_url], normalized_url_matches: "*bkub*")
assert_search_equals([@bkub_url], normalized_url_matches: "/^https?://bkub\.com/$/")
assert_search_equals([@bkub_url], normalized_url_matches: "https://bkub.com")
assert_search_equals([@bkub_url], normalized_url_matches: "http://bkub.com")
assert_search_equals([@bkub_url], url: "https://bkub.com")
assert_search_equals([@bkub_url], url_eq: "https://bkub.com")

View File

@ -34,11 +34,11 @@ module PostSets
end
should "know the total number of pages" do
assert_equal(3, @set.total_pages)
assert_equal(3, @set.posts.total_pages)
end
should "know the current page" do
assert_equal(2, @set.current_page)
assert_equal(2, @set.posts.current_page)
end
end

View File

@ -1988,15 +1988,6 @@ class PostTest < ActiveSupport::TestCase
end
context "Voting:" do
# TODO: What the heck is this about?
# should "not allow members to vote" do
# @user = FactoryBot.create(:user)
# @post = FactoryBot.create(:post)
# as_user do
# assert_raises(PostVote::Error) { VoteManager.vote!(user: @user, post: @post, score: 1) }
# end
# end
should "not allow duplicate votes" do
user = FactoryBot.create(:privileged_user)
post = FactoryBot.create(:post)

View File

@ -23,7 +23,7 @@ class PostVoteTest < ActiveSupport::TestCase
end
should "not accept any other scores" do
error = assert_raises(PostVote::Error) { VoteManager.vote!(user: @user, post: @post, score: 'xxx') }
error = assert_raises(UserVote::Error) { VoteManager.vote!(user: @user, post: @post, score: 'xxx') }
assert_equal("Invalid vote", error.message)
end

View File

@ -2,11 +2,11 @@ require 'test_helper'
class UserDeletionTest < ActiveSupport::TestCase
setup do
Sidekiq::Testing::inline!
Sidekiq::Testing.inline!
end
teardown do
Sidekiq::Testing::fake!
Sidekiq::Testing.fake!
end
context "an invalid user deletion" do
@ -43,7 +43,7 @@ class UserDeletionTest < ActiveSupport::TestCase
context "a valid user deletion" do
setup do
@user = FactoryBot.create(:user, created_at: 2.weeks.ago)
@user = FactoryBot.create(:privileged_user, created_at: 2.weeks.ago)
CurrentUser.user = @user
CurrentUser.ip_addr = "127.0.0.1"
@ -69,6 +69,10 @@ class UserDeletionTest < ActiveSupport::TestCase
assert_nil(User.authenticate(@user.name, "password"))
end
should "reset the level" do
assert_equal(User::Levels::MEMBER, @user.level)
end
should "remove any favorites" do
@post.reload
assert_equal(0, Favorite.count)