Merge branch 'master' into skeb

This commit is contained in:
evazion
2021-03-08 03:43:15 -06:00
committed by GitHub
91 changed files with 840 additions and 438 deletions

View File

@@ -1,6 +1,7 @@
class ApplicationComponent < ViewComponent::Base
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :external_link_to, :tag_class, to: :helpers
delegate :edit_icon, :delete_icon, :undelete_icon, :flag_icon, :upvote_icon, :downvote_icon, :link_icon, to: :helpers
delegate :edit_icon, :delete_icon, :undelete_icon, :flag_icon, :upvote_icon,
:downvote_icon, :link_icon, :sticky_icon, :unsticky_icon, to: :helpers
def policy(subject)
Pundit.policy!(current_user, subject)

View File

@@ -2,7 +2,6 @@
class CommentComponent < ApplicationComponent
attr_reader :comment, :context, :dtext_data, :current_user
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :edit_icon, :delete_icon, :undelete_icon, :flag_icon, :upvote_icon, :downvote_icon, :link_icon, to: :helpers
def initialize(comment:, current_user:, context: nil, dtext_data: nil)
@comment = comment

View File

@@ -99,17 +99,31 @@
<%= menu.item do %>
<% if comment.is_deleted? %>
<%= link_to undelete_comment_path(comment.id), method: :post, remote: true do %>
<%= link_to comment_path(comment.id), "data-params": "comment[is_deleted]=false", method: :put, remote: true do %>
<%= undelete_icon %> Undelete
<% end %>
<% else %>
<%= link_to comment_path(comment.id), "data-confirm": "Are you sure you want to delete this comment?", method: :delete, remote: true do %>
<%= link_to comment_path(comment.id), "data-params": "comment[is_deleted]=true", "data-confirm": "Are you sure you want to delete this comment?", method: :put, remote: true do %>
<%= delete_icon %> Delete
<% end %>
<% end %>
<% end %>
<% end %>
<% if policy(comment).can_sticky_comment? %>
<%= menu.item do %>
<% if comment.is_sticky? %>
<%= link_to comment_path(comment.id), "data-params": "comment[is_sticky]=false", method: :put, remote: true do %>
<%= unsticky_icon %> Unsticky
<% end %>
<% else %>
<%= link_to comment_path(comment.id), "data-params": "comment[is_sticky]=true", method: :put, remote: true do %>
<%= sticky_icon %> Sticky
<% end %>
<% end %>
<% end %>
<% end %>
<% if policy(comment).reportable? %>
<%= menu.item do %>
<%= link_to new_moderation_report_path(moderation_report: { model_type: "Comment", model_id: comment.id }), remote: true do %>

View File

@@ -30,8 +30,9 @@ div.popup-menu {
display: block;
padding: 0.125em 2em 0.125em 0;
i.icon {
width: 1.5em;
.icon {
width: 1rem;
margin-right: 0.25rem;
}
}
}

View File

@@ -1,4 +1,6 @@
class PostNavbarComponent < ApplicationComponent
extend Memoist
attr_reader :post, :current_user, :search
def initialize(post:, current_user:, search: nil)
@@ -18,7 +20,9 @@ class PostNavbarComponent < ApplicationComponent
end
def favgroups
@favgroups ||= current_user.favorite_groups.for_post(post.id).sort_by do |favgroup|
favgroups = FavoriteGroup.visible(current_user).for_post(post.id)
favgroups = favgroups.where(creator: current_user).or(favgroups.where(id: favgroup_id))
favgroups.sort_by do |favgroup|
[favgroup.id == favgroup_id ? 0 : 1, favgroup.name]
end
end
@@ -38,4 +42,6 @@ class PostNavbarComponent < ApplicationComponent
def query
@query ||= PostQueryBuilder.new(search)
end
memoize :favgroups
end

View File

@@ -2,15 +2,13 @@ class ApplicationController < ActionController::Base
include Pundit
helper_method :search_params
class ApiLimitError < StandardError; end
self.responder = ApplicationResponder
skip_forgery_protection if: -> { SessionLoader.new(request).has_api_authentication? }
before_action :reset_current_user
before_action :set_current_user
before_action :normalize_search
before_action :api_check
before_action :check_rate_limit
before_action :ip_ban_check
before_action :set_variant
before_action :add_headers
@@ -71,20 +69,13 @@ class ApplicationController < ActionController::Base
response.headers["X-Git-Hash"] = Rails.application.config.x.git_hash
end
def api_check
return if CurrentUser.is_anonymous? || request.get? || request.head?
def check_rate_limit
return if request.get? || request.head?
if CurrentUser.user.token_bucket.nil?
TokenBucket.create_default(CurrentUser.user)
CurrentUser.user.reload
end
rate_limiter = RateLimiter.for_action(controller_name, action_name, CurrentUser.user, CurrentUser.ip_addr)
headers["X-Rate-Limit"] = rate_limiter.to_json
throttled = CurrentUser.user.token_bucket.throttled?
headers["X-Api-Limit"] = CurrentUser.user.token_bucket.token_count.to_s
if throttled
raise ApiLimitError, "too many requests"
end
rate_limiter.limit!
end
def rescue_exception(exception)
@@ -113,7 +104,7 @@ class ApplicationController < ActionController::Base
render_error_page(410, exception, template: "static/pagination_error", message: "You cannot go beyond page #{CurrentUser.user.page_limit}.")
when Post::SearchError
render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.")
when ApiLimitError
when RateLimiter::RateLimitError
render_error_page(429, exception)
when NotImplementedError
render_error_page(501, exception, message: "This feature isn't available: #{exception.message}")

View File

@@ -1,5 +1,4 @@
class CommentVotesController < ApplicationController
skip_before_action :api_check
respond_to :js, :json, :xml, :html
def index

View File

@@ -1,7 +1,6 @@
class CommentsController < ApplicationController
respond_to :html, :xml, :json, :atom
respond_to :js, only: [:new, :destroy, :undelete]
skip_before_action :api_check
respond_to :js, only: [:new, :update, :destroy, :undelete]
def index
params[:group_by] ||= "comment" if params[:search].present?
@@ -32,7 +31,7 @@ class CommentsController < ApplicationController
def update
@comment = authorize Comment.find(params[:id])
@comment.update(permitted_attributes(@comment))
respond_with(@comment, :location => post_path(@comment.post_id))
respond_with(@comment)
end
def create

View File

@@ -1,6 +1,5 @@
class FavoritesController < ApplicationController
respond_to :html, :xml, :json, :js
skip_before_action :api_check
rescue_with Favorite::Error, status: 422
def index

View File

@@ -1,6 +1,5 @@
class ForumPostsController < ApplicationController
respond_to :html, :xml, :json, :js
skip_before_action :api_check
def new
@forum_post = authorize ForumPost.new_reply(params)

View File

@@ -2,7 +2,6 @@ class ForumTopicsController < ApplicationController
respond_to :html, :xml, :json
respond_to :atom, only: [:index, :show]
before_action :normalize_search, :only => :index
skip_before_action :api_check
def new
@forum_topic = authorize ForumTopic.new

View File

@@ -1,7 +1,6 @@
module Moderator
module Post
class PostsController < ApplicationController
skip_before_action :api_check
respond_to :html, :json, :xml, :js
def confirm_move_favorites

View File

@@ -1,5 +1,4 @@
class PostDisapprovalsController < ApplicationController
skip_before_action :api_check
respond_to :js, :html, :json, :xml
def create

View File

@@ -1,5 +1,4 @@
class PostVotesController < ApplicationController
skip_before_action :api_check
respond_to :js, :json, :xml, :html
def index

View File

@@ -0,0 +1,8 @@
class RateLimitsController < ApplicationController
respond_to :html, :json, :xml
def index
@rate_limits = authorize RateLimit.visible(CurrentUser.user).paginated_search(params, count_pages: true)
respond_with(@rate_limits)
end
end

View File

@@ -2,5 +2,6 @@ class RobotsController < ApplicationController
respond_to :text
def index
expires_in 1.hour, public: true unless response.cache_control.present?
end
end

View File

@@ -1,6 +1,5 @@
class UsersController < ApplicationController
respond_to :html, :xml, :json
skip_before_action :api_check
def new
@user = authorize User.new

View File

@@ -1,19 +0,0 @@
module BulkUpdateRequestsHelper
def bur_script_example
<<~BUR
create alias bunny -> rabbit
remove alias bunny -> rabbit
create implication bunny -> animal
remove implication bunny -> animal
rename bunny -> rabbit
update bunny_focus -> animal_focus bunny
nuke bunny
category touhou -> copyright
BUR
end
end

View File

@@ -7,8 +7,8 @@ module ComponentsHelper
render PostPreviewComponent.with_collection(posts, **options)
end
def render_comment(comment, **options)
render CommentComponent.new(comment: comment, **options)
def render_comment(comment, current_user:, **options)
render CommentComponent.new(comment: comment, current_user: current_user, **options)
end
def render_comment_section(post, **options)

View File

@@ -30,6 +30,10 @@ module IconHelper
icon_tag("fas fa-thumbtack", **options)
end
def unsticky_icon(**options)
svg_icon_tag("unsticky-icon", "M306.5 186.6l-5.7-42.6H328c13.2 0 24-10.8 24-24V24c0-13.2-10.8-24-24-24H56C42.8 0 32 10.8 32 24v96c0 13.2 10.8 24 24 24h27.2l-5.7 42.6C29.6 219.4 0 270.7 0 328c0 13.2 10.8 24 24 24h144v104c0 .9.1 1.7.4 2.5l16 48c2.4 7.3 12.8 7.3 15.2 0l16-48c.3-.8.4-1.7.4-2.5V352h144c13.2 0 24-10.8 24-24 0-57.3-29.6-108.6-77.5-141.4zM50.5 304c8.3-38.5 35.6-70 71.5-87.8L138 96H80V48h224v48h-58l16 120.2c35.8 17.8 63.2 49.4 71.5 87.8z", **options)
end
def lock_icon(**options)
icon_tag("fas fa-lock", **options)
end
@@ -39,7 +43,7 @@ module IconHelper
end
def undelete_icon(**options)
icon_tag("fas fa-trash-restore_alt", **options)
icon_tag("fas fa-trash-restore-alt", **options)
end
def private_icon(**options)
@@ -172,18 +176,26 @@ module IconHelper
def external_site_icon(site_name, **options)
case site_name
when "Amazon"
image_icon_tag("amazon-logo.png", **options)
when "ArtStation"
image_icon_tag("artstation-logo.png", **options)
when "Ask.fm"
image_icon_tag("ask-fm-logo.png", **options)
when "BCY"
image_icon_tag("bcy-logo.png", **options)
when "Booth.pm"
image_icon_tag("booth-pm-logo.png", **options)
when "Circle.ms"
image_icon_tag("circle-ms-logo.png", **options)
when "DLSite"
image_icon_tag("dlsite-logo.png", **options)
when "Deviant Art"
image_icon_tag("deviantart-logo.png", **options)
when "DLSite"
image_icon_tag("dlsite-logo.png", **options)
when "Doujinshi.org"
image_icon_tag("doujinshi-org-logo.png", **options)
when "Erogamescape"
image_icon_tag("erogamescape-logo.png", **options)
when "Facebook"
image_icon_tag("facebook-logo.png", **options)
when "Fantia"
@@ -192,12 +204,24 @@ module IconHelper
image_icon_tag("fc2-logo.png", **options)
when "Gumroad"
image_icon_tag("gumroad-logo.png", **options)
when "Hentai Foundry"
image_icon_tag("hentai-foundry-logo.png", **options)
when "Instagram"
image_icon_tag("instagram-logo.png", **options)
when "Ko-fi"
image_icon_tag("ko-fi-logo.png", **options)
when "Livedoor"
image_icon_tag("livedoor-logo.png", **options)
when "Lofter"
image_icon_tag("lofter-logo.png", **options)
when "Mangaupdates"
image_icon_tag("mangaupdates-logo.png", **options)
when "Melonbooks"
image_icon_tag("melonbooks-logo.png", **options)
when "Mihuashi"
image_icon_tag("mihuashi-logo.png", **options)
when "Mixi.jp"
image_icon_tag("mixi-jp-logo.png", **options)
when "Nico Seiga"
image_icon_tag("nicoseiga-logo.png", **options)
when "Nijie"
@@ -206,6 +230,10 @@ module IconHelper
image_icon_tag("patreon-logo.png", **options)
when "pawoo.net"
image_icon_tag("pawoo-logo.png", **options)
when "Piapro.jp"
image_icon_tag("piapro-jp-logo.png", **options)
when "Picarto"
image_icon_tag("picarto-logo.png", **options)
when "Pixiv"
image_icon_tag("pixiv-logo.png", **options)
when "Pixiv Fanbox"
@@ -214,6 +242,10 @@ module IconHelper
image_icon_tag("pixiv-sketch-logo.png", **options)
when "Privatter"
image_icon_tag("privatter-logo.png", **options)
when "Sakura.ne.jp"
image_icon_tag("sakura-ne-jp-logo.png", **options)
when "Stickam"
image_icon_tag("stickam-logo.png", **options)
when "Skeb"
image_icon_tag("skeb-logo.png", **options)
when "Tinami"
@@ -224,8 +256,12 @@ module IconHelper
image_icon_tag("twitter-logo.png", **options)
when "Toranoana"
image_icon_tag("toranoana-logo.png", **options)
when "Twitch"
image_icon_tag("twitch-logo.png", **options)
when "Weibo"
image_icon_tag("weibo-logo.png", **options)
when "Wikipedia"
image_icon_tag("wikipedia-logo.png", **options)
when "Youtube"
image_icon_tag("youtube-logo.png", **options)
else

View File

@@ -112,13 +112,10 @@ table tfoot {
}
details {
border-bottom: 1px solid var(--details-border-color);
summary {
cursor: pointer;
user-select: none;
outline: none;
line-height: 2em;
}
}

View File

@@ -255,7 +255,7 @@ html {
--login-link-color: var(--red-5);
--footer-border-color: var(--grey-1);
--details-border-color: var(--grey-2);
--divider-border-color: var(--grey-2);
--jquery-ui-widget-content-background: var(--body-background-color);
--jquery-ui-widget-content-text-color: var(--text-color);
@@ -446,7 +446,7 @@ body[data-current-user-theme="dark"] {
--login-link-color: var(--red-4);
--footer-border-color: var(--grey-7);
--details-border-color: var(--grey-7);
--divider-border-color: var(--grey-7);
--jquery-ui-widget-content-text-color: var(--text-color);
--jquery-ui-widget-content-background: var(--grey-8);

View File

@@ -131,6 +131,14 @@ div.prose {
.spoiler {
background: var(--dtext-spoiler-background-color);
}
details {
margin-bottom: 1em;
summary {
margin-bottom: 1em;
}
}
}
// avoid empty gaps beneath dtext blocks in table rows.

View File

@@ -42,6 +42,8 @@ $spacer: 0.25rem; /* 4px */
.space-x-4 > * + * { margin-left: 4 * $spacer; }
.space-y-4 > * + * { margin-top: 4 * $spacer; }
.divide-y-1 > * + * { border-top: 1px solid var(--divider-border-color); }
.align-top { vertical-align: top; }
.flex-auto { flex: 1 1 auto; }

View File

@@ -15,11 +15,6 @@ div.related-tags {
width: 15em;
}
/* Hide the related tag checkbox unless it's checked or hovered. */
input[type="checkbox"] {
visibility: hidden;
}
li.selected a {
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-text-stroke */
/* https://caniuse.com/text-stroke */
@@ -27,7 +22,14 @@ div.related-tags {
text-stroke: 0.5px;
}
li.selected, li:hover {
input[type="checkbox"] { visibility: visible; }
/* On computers with a mouse, hide the related tag checkbox unless it's checked or hovered. */
@media (hover: hover) {
input[type="checkbox"] {
visibility: hidden;
}
li.selected, li:hover {
input[type="checkbox"] { visibility: visible; }
}
}
}

View File

@@ -1,5 +1,9 @@
div#c-user-upgrades {
div#a-new {
summary {
padding: 1rem 0;
}
table#feature-comparison {
colgroup#basic {
background-color: var(--user-upgrade-basic-background-color);

View File

@@ -5,13 +5,13 @@ module DanbooruMaintenance
safely { Upload.prune! }
safely { PostPruner.prune! }
safely { PostAppealForumUpdater.update_forum! }
safely { RateLimit.prune! }
safely { regenerate_post_counts! }
end
def daily
safely { Delayed::Job.where('created_at < ?', 45.days.ago).delete_all }
safely { PostDisapproval.prune! }
safely { TokenBucket.prune! }
safely { BulkUpdateRequestPruner.warn_old }
safely { BulkUpdateRequestPruner.reject_expired }
safely { Ban.prune! }

View File

@@ -0,0 +1,52 @@
class RateLimiter
class RateLimitError < StandardError; end
attr_reader :action, :keys, :cost, :rate, :burst
def initialize(action, keys = ["*"], cost: 1, rate: 1, burst: 1)
@action = action
@keys = keys
@cost = cost
@rate = rate
@burst = burst
end
def self.for_action(controller_name, action_name, user, ip_addr)
action = "#{controller_name}:#{action_name}"
keys = [(user.cache_key unless user.is_anonymous?), "ip/#{ip_addr.to_s}"].compact
case action
when "users:create"
rate, burst = 1.0/5.minutes, 10
when "emails:update", "sessions:create", "moderation_reports:create"
rate, burst = 1.0/1.minute, 10
when "dmails:create", "comments:create", "forum_posts:create", "forum_topics:create"
rate, burst = 1.0/1.minute, 50
when "comment_votes:create", "comment_votes:destroy", "post_votes:create",
"post_votes:destroy", "favorites:create", "favorites:destroy", "post_disapprovals:create"
rate, burst = 1.0/1.second, 200
else
rate = user.api_regen_multiplier
burst = 200
end
RateLimiter.new(action, keys, rate: rate, burst: burst)
end
def limit!
raise RateLimitError if limited?
end
def limited?
rate_limits.any?(&:limited?)
end
def as_json(options = {})
hash = rate_limits.map { |limit| [limit.key, limit.points] }.to_h
super(options).except("keys", "rate_limits").merge(limits: hash)
end
def rate_limits
@rate_limits ||= RateLimit.create_or_update!(action: action, keys: keys, cost: cost, rate: rate, burst: burst)
end
end

View File

@@ -3,17 +3,17 @@ module Sources
def self.all
[
Strategies::Pixiv,
Strategies::Fanbox,
Strategies::NicoSeiga,
Strategies::Twitter,
Strategies::Tumblr,
Strategies::NicoSeiga,
Strategies::Stash, # must come before DeviantArt
Strategies::DeviantArt,
Strategies::Tumblr,
Strategies::ArtStation,
Strategies::Nijie,
Strategies::Mastodon,
Strategies::Moebooru,
Strategies::Nijie,
Strategies::ArtStation,
Strategies::HentaiFoundry,
Strategies::Fanbox,
Strategies::Mastodon,
Strategies::Weibo,
Strategies::Newgrounds,
Strategies::Skeb
@@ -21,7 +21,7 @@ module Sources
end
def self.find(url, referer = nil, default: Strategies::Null)
strategy = all.map { |strategy| strategy.new(url, referer) }.detect(&:match?)
strategy = all.lazy.map { |s| s.new(url, referer) }.detect(&:match?)
strategy || default&.new(url, referer)
end

View File

@@ -64,6 +64,10 @@ module Sources
# XXX should go in dedicated strategies.
case host
when /amazon\.(com|jp|co\.jp)\z/i
"Amazon"
when /ask\.fm\z/i
"Ask.fm"
when /bcy\.net\z/i
"BCY"
when /booth\.pm\z/i
@@ -72,6 +76,10 @@ module Sources
"Circle.ms"
when /dlsite\.(com|net)\z/i
"DLSite"
when /doujinshi\.mugimugi\.org\z/i, /doujinshi\.org\z/i
"Doujinshi.org"
when /erogamescape\.dyndns\.org\z/i
"Erogamescape"
when /facebook\.com\z/i
"Facebook"
when /fantia\.jp\z/i
@@ -82,18 +90,42 @@ module Sources
"Gumroad"
when /instagram\.com\z/i
"Instagram"
when /ko-fi\.com\z/i
"Ko-fi"
when /livedoor\.(jp|com)\z/i
"Livedoor"
when /lofter\.com\z/i
"Lofter"
when /mangaupdates\.com\z/i
"Mangaupdates"
when /melonbooks\.co\.jp\z/i
"Melonbooks"
when /mihuashi\.com\z/i
"Mihuashi"
when /mixi\.jp\z/i
"Mixi.jp"
when /patreon\.com\z/i
"Patreon"
when /piapro\.jp\z/i
"Piapro.jp"
when /picarto\.tv\z/i
"Picarto"
when /privatter\.net\z/i
"Privatter"
when /sakura\.ne\.jp\z/i
"Sakura.ne.jp"
when /stickam\.jp\z/i
"Stickam"
when /skeb\.jp\z/i
"Skeb"
when /tinami\.com\z/i
"Tinami"
when /toranoana\.(jp|shop)\z/i
"Toranoana"
when /twitch\.tv\z/i
"Twitch"
when /wikipedia\.org\z/i
"Wikipedia"
when /youtube\.com\z/i
"Youtube"
else

View File

@@ -17,7 +17,7 @@
module Sources::Strategies
class Mastodon < Base
HOST = %r{\Ahttps?://(?:www\.)?(?<domain>pawoo\.net|baraag\.net)}i
IMAGE = %r{\Ahttps?://(?:img\.pawoo\.net|baraag\.net)/media_attachments/files/(\d+/\d+/\d+)}
IMAGE = %r{\Ahttps?://(?:img\.pawoo\.net|baraag\.net(?:/system(?:/cache)?)?)/media_attachments/files/((?:\d+/)+\d+)}
NAMED_PROFILE = %r{#{HOST}/@(?<artist_name>\w+)}i
ID_PROFILE = %r{#{HOST}/web/accounts/(?<account_id>\d+)}
@@ -35,6 +35,7 @@ module Sources::Strategies
def file_host
case site_name
when "pawoo.net" then "img.pawoo.net"
when "baraag.net" then "baraag.net/system"
else site_name
end
end
@@ -85,7 +86,7 @@ module Sources::Strategies
end
def artist_name_from_url
url[NAMED_PROFILE, :artist_name]
urls.map { |url| url[NAMED_PROFILE, :artist_name] }.compact.first
end
def other_names
@@ -93,7 +94,7 @@ module Sources::Strategies
end
def account_id
url[ID_PROFILE, :account_id] || api_response.account_id
urls.map { |url| url[ID_PROFILE, :account_id] }.compact.first || api_response.account_id
end
def status_id_from_url

View File

@@ -212,34 +212,57 @@ module Sources
return nil if page_url.blank? || client.blank?
response = client.cache(1.minute).get(page_url)
return nil unless response.status == 200
response&.parse
if response.status != 200 || response.parse.search("#login_illust").present?
clear_cached_session_cookie!
else
response.parse
end
end
memoize :page
def client
nijie = http.timeout(60).use(retriable: { max_retries: 20 })
cookie = Cache.get("nijie-session-cookie", 1.week) do
login_page = nijie.get("https://nijie.info/login.php").parse
form = {
email: Danbooru.config.nijie_login,
password: Danbooru.config.nijie_password,
url: login_page.at("input[name='url']")["value"],
save: "on",
ticket: ""
}
response = nijie.post("https://nijie.info/login_int.php", form: form)
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})" if response.status != 200
return nil unless response.status == 200
response.cookies.select { |c| c.name == "NIJIEIJIEID" }.compact.first
end
nijie.cookies(NIJIEIJIEID: cookie, R18: 1)
return nil if cached_session_cookie.nil?
http.cookies(NIJIEIJIEID: cached_session_cookie, R18: 1)
end
memoize :client
def http
super.timeout(60).use(retriable: { max_retries: 20 })
end
def cached_session_cookie
Cache.get("nijie-session-cookie", 60.minutes, skip_nil: true) do
session_cookie
end
end
def clear_cached_session_cookie!
flush_cache # clear memoized session cookie
Cache.delete("nijie-session-cookie")
end
def session_cookie
login_page = http.get("https://nijie.info/login.php").parse
form = {
email: Danbooru.config.nijie_login,
password: Danbooru.config.nijie_password,
url: login_page.at("input[name='url']")["value"],
save: "on",
ticket: ""
}
response = http.post("https://nijie.info/login_int.php", form: form)
if response.status == 200
response.cookies.select { |c| c.name == "NIJIEIJIEID" }.compact.first
else
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})"
nil
end
end
memoize :client, :cached_session_cookie
end
end
end

View File

@@ -30,6 +30,7 @@ module Sources::Strategies
/(?<!\A)誕生祭(?:\d*)\z/,
/(?<!\A)版もうひとつの深夜の真剣お絵描き60分一本勝負(?:_\d+)?\z/,
/(?<!\A)版深夜の真剣お絵描き60分一本勝負(?:_\d+)?\z/,
/(?<!\A)版深夜の真剣お絵かき60分一本勝負(?:_\d+)?\z/,
/(?<!\A)深夜の真剣お絵描き60分一本勝負(?:_\d+)?\z/,
/(?<!\A)版深夜のお絵描き60分一本勝負(?:_\d+)?\z/,
/(?<!\A)版真剣お絵描き60分一本勝(?:_\d+)?\z/,

View File

@@ -10,7 +10,7 @@ class TagNameValidator < ActiveModel::EachValidator
case value
when /\A_*\z/
record.errors.add(attribute, "'#{value}' cannot be blank")
record.errors.add(attribute, "cannot be blank")
when /\*/
record.errors.add(attribute, "'#{value}' cannot contain asterisks ('*')")
when /,/

View File

@@ -18,7 +18,6 @@ class UserPromotion
user.level = new_level
user.can_upload_free = can_upload_free unless can_upload_free.nil?
user.can_approve_posts = can_approve_posts unless can_approve_posts.nil?
user.inviter = promoter
create_user_feedback
create_dmail

View File

@@ -12,6 +12,7 @@ class Artist < ApplicationRecord
validate :validate_tag_category
validates :name, tag_name: true, uniqueness: true
before_save :update_tag_category
after_save :create_version
after_save :clear_url_string_changed
@@ -155,7 +156,7 @@ class Artist < ApplicationRecord
return unless !is_deleted? && name_changed? && tag.present?
if tag.category_name != "Artist" && !tag.empty?
errors.add(:base, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
errors.add(:name, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
end
end

View File

@@ -97,6 +97,8 @@ class ArtistUrl < ApplicationRecord
true
when %r!www\.artstation\.com!i
true
when %r!blogimg\.jp!i, %r!image\.blog\.livedoor\.jp!i
true
else
false
end
@@ -106,9 +108,9 @@ class ArtistUrl < ApplicationRecord
def priority
sites = %w[
Pixiv Twitter
ArtStation Deviant\ Art Nico\ Seiga Nijie pawoo.net Pixiv\ Fanbox Pixiv\ Sketch Tinami Tumblr
Booth.pm Facebook Fantia FC2 Gumroad Instagram Lofter Patreon Privatter Skeb Weibo Youtube
Circle.ms DLSite Melonbooks Toranoana
ArtStation BCY Deviant\ Art Hentai\ Foundry Nico\ Seiga Nijie pawoo.net Pixiv\ Fanbox Pixiv\ Sketch Tinami Tumblr
Ask.fm Booth.pm Facebook Fantia FC2 Gumroad Instagram Ko-fi Livedoor Lofter Mihuashi Mixi.jp Patreon Piapro.jp Picarto Privatter Sakura.ne.jp Stickam Skeb Twitch Weibo Youtube
Amazon Circle.ms DLSite Doujinshi.org Erogamescape Mangaupdates Melonbooks Toranoana Wikipedia
]
sites.index(site_name) || 1000

View File

@@ -43,7 +43,7 @@ class Ban < ApplicationRecord
end
def validate_user_is_bannable
errors.add(:user, "is already banned") if user.is_banned?
errors.add(:user, "is already banned") if user&.is_banned?
end
def update_user_on_create

View File

@@ -1,11 +1,13 @@
class Comment < ApplicationRecord
validates_presence_of :body, :message => "has no content"
belongs_to :post
belongs_to :creator, class_name: "User"
belongs_to_updater
has_many :moderation_reports, as: :model
has_many :votes, :class_name => "CommentVote", :dependent => :destroy
validates :body, presence: true
before_create :autoreport_spam
after_create :update_last_commented_at_on_create
after_update(:if => ->(rec) {(!rec.is_deleted? || !rec.saved_change_to_is_deleted?) && CurrentUser.id != rec.creator_id}) do |rec|

View File

@@ -1,9 +1,12 @@
class FavoriteGroup < ApplicationRecord
validates_uniqueness_of :name, :case_sensitive => false, :scope => :creator_id
validates_format_of :name, :with => /\A[^,]+\Z/, :message => "cannot have commas"
belongs_to :creator, class_name: "User"
before_validation :normalize_name
before_validation :strip_name
validates :name, presence: true
validates :name, uniqueness: { case_sensitive: false, scope: :creator_id }
validates :name, format: { without: /,/, message: "cannot have commas" }
validate :creator_can_create_favorite_groups, :on => :create
validate :validate_number_of_posts
validate :validate_posts

76
app/models/rate_limit.rb Normal file
View File

@@ -0,0 +1,76 @@
class RateLimit < ApplicationRecord
scope :expired, -> { where("updated_at < ?", 1.hour.ago) }
def self.prune!
expired.delete_all
end
def self.visible(user)
if user.is_owner?
all
elsif user.is_anonymous?
none
else
where(key: [user.cache_key])
end
end
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :limited, :points, :action, :key)
q = q.apply_default_order(params)
q
end
# `action` is the action being limited. Usually a controller endpoint.
# `keys` is who is being limited. Usually a [user, ip] pair, meaning the action is limited both by the user's ID and their IP.
# `cost` is the number of points the action costs.
# `rate` is the number of points per second that are refilled.
# `burst` is the maximum number of points that can be saved up.
def self.create_or_update!(action:, keys:, cost:, rate:, burst:, minimum_points: -30)
# { key0: keys[0], ..., keyN: keys[N] }
key_params = keys.map.with_index { |key, i| [:"key#{i}", key] }.to_h
# (created_at, updated_at, action, keyN, points)
values = keys.map.with_index { |key, i| "(:now, :now, :action, :key#{i}, :points)" }
# Do an upsert, creating a new rate limit object for each key that doesn't
# already exist, and updating the limit for each limit that already exists.
#
# If the current point count is negative, then we're limited. Penalize the
# caller 1 second (1 rate unit), up to a maximum penalty of 30 seconds (by default).
#
# Otherwise, if the point count is positive, then we're not limited. Update
# the point count and subtract the cost of the call.
#
# https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT
sql = <<~SQL
INSERT INTO rate_limits (created_at, updated_at, action, key, points)
VALUES #{values.join(", ")}
ON CONFLICT (action, key) DO UPDATE SET
updated_at = :now,
limited = rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at)) < 0,
points =
CASE
WHEN rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at)) < 0 THEN
GREATEST(:minimum_points, LEAST(:burst, rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at))) - :rate)
ELSE
GREATEST(:minimum_points, LEAST(:burst, rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at))) - :cost)
END
RETURNING *
SQL
sql_params = {
now: Time.zone.now,
action: action,
rate: rate,
burst: burst,
cost: cost,
points: burst - cost,
minimum_points: minimum_points,
**key_params
}
rate_limits = RateLimit.find_by_sql([sql, sql_params])
rate_limits
end
end

View File

@@ -1,41 +0,0 @@
class TokenBucket < ApplicationRecord
self.primary_key = "user_id"
belongs_to :user
def self.prune!
where("last_touched_at < ?", 1.day.ago).delete_all
end
def self.create_default(user)
TokenBucket.create(user_id: user.id, token_count: user.api_burst_limit, last_touched_at: Time.now)
end
def accept?
token_count >= 1
end
def add!
now = Time.now
TokenBucket.where(user_id: user_id).update_all(["token_count = least(token_count + (? * extract(epoch from ? - last_touched_at)), ?), last_touched_at = ?", user.api_regen_multiplier, now, user.api_burst_limit, now])
# estimate the token count to avoid reloading
self.token_count += user.api_regen_multiplier * (now - last_touched_at)
self.token_count = user.api_burst_limit if token_count > user.api_burst_limit
end
def consume!
TokenBucket.where(user_id: user_id).update_all("token_count = greatest(0, token_count - 1)")
self.token_count -= 1
end
def throttled?
add!
if accept?
consume!
return false
else
return true
end
end
end

View File

@@ -134,7 +134,6 @@ class User < ApplicationRecord
has_many :user_events, dependent: :destroy
has_one :recent_ban, -> {order("bans.id desc")}, :class_name => "Ban"
has_one :token_bucket
has_one :email_address, dependent: :destroy
has_many :api_keys, dependent: :destroy
has_many :note_versions, :foreign_key => "updater_id"
@@ -465,26 +464,12 @@ class User < ApplicationRecord
# regen this amount per second
def api_regen_multiplier(level)
if level >= User::Levels::PLATINUM
if level >= User::Levels::GOLD
4
elsif level == User::Levels::GOLD
2
else
1
end
end
# can make this many api calls at once before being bound by
# api_regen_multiplier refilling your pool
def api_burst_limit(level)
if level >= User::Levels::PLATINUM
60
elsif level == User::Levels::GOLD
30
else
10
end
end
end
def max_saved_searches
@@ -535,14 +520,6 @@ class User < ApplicationRecord
User.api_regen_multiplier(level)
end
def api_burst_limit
User.api_burst_limit(level)
end
def remaining_api_limit
token_bucket.try(:token_count) || api_burst_limit
end
def statement_timeout
User.statement_timeout(level)
end

View File

@@ -119,7 +119,7 @@ class UserUpgrade < ApplicationRecord
end
def upgrade_recipient!
recipient.update!(level: level, inviter: User.system)
recipient.update!(level: level)
end
def create_mod_action!

View File

@@ -0,0 +1,5 @@
class RateLimitPolicy < ApplicationPolicy
def index?
true
end
end

View File

@@ -61,10 +61,8 @@ class UserPolicy < ApplicationPolicy
updated_at last_logged_in_at last_forum_read_at
comment_threshold default_image_size
favorite_tags blacklisted_tags time_zone per_page
custom_style favorite_count api_regen_multiplier
api_burst_limit remaining_api_limit statement_timeout
favorite_group_limit favorite_limit tag_query_limit
max_saved_searches theme
custom_style favorite_count statement_timeout favorite_group_limit
favorite_limit tag_query_limit max_saved_searches theme
]
end

View File

@@ -5,7 +5,7 @@
<%= f.input :group_name %>
<%= f.input :url_string, label: "URLs", as: :text, input_html: { value: params.dig(:artist, :url_string) || @artist.urls.join("\n")}, hint: "You can prefix a URL with - to mark it as dead." %>
<% if @artist.wiki_page.present? %>
<% if @artist.tag&.artist? && @artist.wiki_page.present? %>
<div class="input">
<label>Wiki (<%= link_to "Edit", edit_wiki_page_path(@artist.wiki_page) %>)</label>
</div>

View File

@@ -1,25 +1,26 @@
<%= edit_form_for(@bulk_update_request) do |f| %>
<p>
Request aliases or implications using the format shown below. An <b>alias</b> makes the first tag a
synonym for the second tag. An <b>implication</b> makes the first tag automatically add the second tag.
A <b>rename</b> replaces the first tag with the second tag without making it a permanent alias.
An <b>update</b> moves multiple tags and pools at once.
</p>
<div class="prose">
<details>
<summary>Help: How to make a bulk update request</summary>
<p>
<% if @bulk_update_request.new_record? && @bulk_update_request.forum_topic.present? %>
This request will be attached to
<%= link_to "topic ##{@bulk_update_request.forum_topic_id}: #{@bulk_update_request.forum_topic.title}", @bulk_update_request.forum_topic %>.
<%= f.input :forum_topic_id, as: :hidden, input_html: { value: params.dig(:bulk_update_request, :forum_topic_id) } %>
<% elsif @bulk_update_request.new_record? && @bulk_update_request.forum_topic.blank? %>
This request will create a new forum topic. To attach this request to an existing topic, find
the forum topic and click "Request alias/implication" at the top of the page.
<%= embed_wiki "help:bur_notice" %>
</details>
<%= f.input :title, label: "Forum Title", as: :string %>
<% end %>
</p>
<p>
<% if @bulk_update_request.new_record? && @bulk_update_request.forum_topic.present? %>
This request will be attached to
<%= link_to "topic ##{@bulk_update_request.forum_topic_id}: #{@bulk_update_request.forum_topic.title}", @bulk_update_request.forum_topic %>.
<%= f.input :forum_topic_id, as: :hidden, input_html: { value: params.dig(:bulk_update_request, :forum_topic_id) } %>
<% elsif @bulk_update_request.new_record? && @bulk_update_request.forum_topic.blank? %>
This request will create a new forum topic. To attach this request to an existing topic, find
the forum topic and click "Request alias/implication" at the top of the page.
<%= f.input :script, label: "Request", as: :text, placeholder: bur_script_example %>
<%= f.input :title, label: "Forum Title", as: :string %>
<% end %>
</p>
</div>
<%= f.input :script, label: "Request", as: :text %>
<% if @bulk_update_request.new_record? %>
<div class="input">

View File

@@ -6,9 +6,6 @@
<%= f.button :submit, "Submit" %>
<%= dtext_preview_button "comment_body" %>
<% if comment.new_record? %>
<%= f.input :do_not_bump_post, :label => "No bump" %>
<% end %>
<% if policy(comment).can_sticky_comment? %>
<%= f.input :is_sticky, label: "Sticky", for: "comment_is_sticky" %>
<%= f.input :do_not_bump_post, label: "No bump" %>
<% end %>
<% end %>

View File

@@ -0,0 +1 @@
$("#comment_<%= @comment.id %>").replaceWith("<%= j render_comment(@comment, current_user: CurrentUser.user) %>");

View File

@@ -22,7 +22,7 @@
</tbody>
</table>
<div class="paginator">
<div class="paginator text-center mt-4">
<menu>
<li><%= link_to "< Previous", searches_explore_posts_path(:date => 1.day.ago(@date).to_date), :class => "arrow" %></li>
<li><%= link_to "Next >", searches_explore_posts_path(:date => 1.day.since(@date).to_date), :class => "arrow" %></li>

View File

@@ -11,7 +11,7 @@
<%= post_previews_html(@posts) %>
<div class="paginator">
<div class="paginator text-center mt-4">
<menu>
<li><%= link_to "< Previous", viewed_explore_posts_path(:date => 1.day.ago(@date).to_date) %></li>
<li><%= link_to "Next >", viewed_explore_posts_path(:date => 1.day.since(@date).to_date) %></li>

View File

@@ -0,0 +1,21 @@
<div id="c-rate-limits">
<div id="a-index">
<%= table_for @rate_limits, class: "striped autofit" do |t| %>
<% t.column :action %>
<% t.column :key %>
<% t.column :points do |rate_limit| %>
<%= rate_limit.points.round(2) %>
<% end %>
<% t.column :limited? %>
<% t.column :updated_at do |rate_limit| %>
<%= time_ago_in_words_tagged rate_limit.updated_at %>
<% end %>
<% end %>
<%= numbered_paginator(@rate_limits) %>
</div>
</div>

View File

@@ -119,7 +119,7 @@
<h2 class="mb-4">Frequently Asked Questions</h2>
<div id="frequently-asked-questions">
<div id="frequently-asked-questions" class="divide-y-1">
<details>
<summary>What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold?</summary>

View File

@@ -62,14 +62,12 @@
</tr>
<% end %>
<tr>
<th>Inviter</th>
<% if user.inviter %>
<% if user.inviter %>
<tr>
<th>Promoter</th>
<td><%= link_to_user user.inviter %> <%= link_to "»", users_path(search: { inviter: { name: user.inviter.name }}) %></td>
<% else %>
<td>None</td>
<% end %>
</tr>
</tr>
<% end %>
<tr>
<th>Level</th>
@@ -265,14 +263,6 @@
(<%= link_to_wiki "help", "help:api" %>)
</td>
</tr>
<tr>
<th>API Limits</th>
<td>
<%= CurrentUser.user.remaining_api_limit %>
/ <%= CurrentUser.user.api_burst_limit %> <span class="fineprint">(may not be up to date)</span>
</td>
</tr>
<% end %>
</tbody>
</table>