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

@@ -102,9 +102,9 @@ GEM
sshkit (>= 1.6.1, != 1.7.0)
ansi (1.5.0)
ast (2.4.2)
aws-eventstream (1.1.0)
aws-partitions (1.429.0)
aws-sdk-core (3.112.0)
aws-eventstream (1.1.1)
aws-partitions (1.431.1)
aws-sdk-core (3.112.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
@@ -112,7 +112,7 @@ GEM
aws-sdk-sqs (1.36.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
aws-sigv4 (1.2.3)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.16)
better_errors (2.9.1)
@@ -152,7 +152,7 @@ GEM
xpath (~> 3.2)
childprocess (3.0.0)
chronic (0.10.2)
codecov (0.4.3)
codecov (0.5.1)
simplecov (>= 0.15, < 0.22)
coderay (1.1.3)
concurrent-ruby (1.1.8)
@@ -182,7 +182,7 @@ GEM
ruby2_keywords
faraday-net_http (1.0.1)
ffaker (2.18.0)
ffi (1.14.2)
ffi (1.15.0)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
@@ -244,7 +244,7 @@ GEM
net-ssh (>= 5.0.0, < 7.0.0)
net-ssh (6.1.0)
newrelic_rpm (6.15.0)
nio4r (2.5.5)
nio4r (2.5.7)
nokogiri (1.11.1)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
@@ -269,7 +269,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.2.1)
puma (5.2.2)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
@@ -324,7 +324,7 @@ GEM
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.4)
rubocop (1.10.0)
rubocop (1.11.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
@@ -354,7 +354,7 @@ GEM
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
semantic_range (2.3.1)
semantic_range (3.0.0)
shoulda-context (2.0.0)
shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)

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>

View File

@@ -220,6 +220,7 @@ Rails.application.routes.draw do
end
end
resources :artist_commentary_versions, :only => [:index, :show]
resources :rate_limits, only: [:index]
resource :related_tag, :only => [:show, :update]
resources :recommended_posts, only: [:index]
resources :robots, only: [:index]

View File

@@ -5,6 +5,6 @@ class CreateTokenBuckets < ActiveRecord::Migration[4.2]
end
def down
raise NotImplementedError
drop_table :token_buckets
end
end

View File

@@ -0,0 +1,24 @@
require_relative "20170106012138_create_token_buckets"
class ReplaceTokenBucketsWithRateLimits < ActiveRecord::Migration[6.1]
def change
revert CreateTokenBuckets
create_table :rate_limits do |t|
t.timestamps null: false
t.boolean :limited, null: false, default: false
t.float :points, null: false
t.string :action, null: false
t.string :key, null: false
t.index [:key, :action], unique: true
end
reversible do |dir|
dir.up do
execute "ALTER TABLE rate_limits SET UNLOGGED"
execute "ALTER TABLE rate_limits SET (fillfactor = 50)"
end
end
end
end

View File

@@ -2929,6 +2929,41 @@ CREATE SEQUENCE public.posts_id_seq
ALTER SEQUENCE public.posts_id_seq OWNED BY public.posts.id;
--
-- Name: rate_limits; Type: TABLE; Schema: public; Owner: -
--
CREATE UNLOGGED TABLE public.rate_limits (
id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
limited boolean DEFAULT false NOT NULL,
points double precision NOT NULL,
action character varying NOT NULL,
key character varying NOT NULL
)
WITH (fillfactor='50');
--
-- Name: rate_limits_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.rate_limits_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: rate_limits_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.rate_limits_id_seq OWNED BY public.rate_limits.id;
--
-- Name: saved_searches; Type: TABLE; Schema: public; Owner: -
--
@@ -3085,17 +3120,6 @@ CREATE SEQUENCE public.tags_id_seq
ALTER SEQUENCE public.tags_id_seq OWNED BY public.tags.id;
--
-- Name: token_buckets; Type: TABLE; Schema: public; Owner: -
--
CREATE UNLOGGED TABLE public.token_buckets (
user_id integer,
last_touched_at timestamp without time zone NOT NULL,
token_count real NOT NULL
);
--
-- Name: uploads; Type: TABLE; Schema: public; Owner: -
--
@@ -4352,6 +4376,13 @@ ALTER TABLE ONLY public.post_votes ALTER COLUMN id SET DEFAULT nextval('public.p
ALTER TABLE ONLY public.posts ALTER COLUMN id SET DEFAULT nextval('public.posts_id_seq'::regclass);
--
-- Name: rate_limits id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.rate_limits ALTER COLUMN id SET DEFAULT nextval('public.rate_limits_id_seq'::regclass);
--
-- Name: saved_searches id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -4739,6 +4770,14 @@ ALTER TABLE ONLY public.posts
ADD CONSTRAINT posts_pkey PRIMARY KEY (id);
--
-- Name: rate_limits rate_limits_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.rate_limits
ADD CONSTRAINT rate_limits_pkey PRIMARY KEY (id);
--
-- Name: saved_searches saved_searches_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -7280,6 +7319,13 @@ CREATE INDEX index_posts_on_uploader_id ON public.posts USING btree (uploader_id
CREATE INDEX index_posts_on_uploader_ip_addr ON public.posts USING btree (uploader_ip_addr);
--
-- Name: index_rate_limits_on_key_and_action; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_rate_limits_on_key_and_action ON public.rate_limits USING btree (key, action);
--
-- Name: index_saved_searches_on_labels; Type: INDEX; Schema: public; Owner: -
--
@@ -7385,13 +7431,6 @@ CREATE INDEX index_tags_on_name_trgm ON public.tags USING gin (name public.gin_t
CREATE INDEX index_tags_on_post_count ON public.tags USING btree (post_count);
--
-- Name: index_token_buckets_on_user_id; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_token_buckets_on_user_id ON public.token_buckets USING btree (user_id);
--
-- Name: index_uploads_on_referer_url; Type: INDEX; Schema: public; Owner: -
--
@@ -7964,6 +8003,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20210127000201'),
('20210127012303'),
('20210214095121'),
('20210214101614');
('20210214101614'),
('20210303195217');

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env ruby
require_relative "../../config/environment"
User.transaction do
# Clear inviter for users who were listed as invited by Albert. Most of these
# are very old Gold upgrades. Others are old accounts who probably weren't
# invited by Albert himself.
p User.where(inviter_id: 1).count
User.where(inviter_id: 1).update_all(inviter_id: nil)
# Clear inviter for older Gold and Platinum upgrades where the user was listed as having invited themselves.
p User.where("inviter_id = id").count
User.where("inviter_id = id").update_all(inviter_id: nil)
# Clear inviter for newer Gold and Platinum upgrades where the user was listed as being invited by DanbooruBot.
p User.where(inviter_id: User.system.id).count
User.where(inviter_id: User.system.id).update_all(inviter_id: nil)
# Clear inviter for users where there is a promotion feedback from the inviter.
p User.joins(:feedback).where.not(inviter_id: nil).where_regex(:body, "^(You have been promoted|You gained the ability|Promoted from|Promoted by)").where("inviter_id = user_feedback.creator_id").count
User.joins(:feedback).where.not(inviter_id: nil).where_regex(:body, "^(You have been promoted|You gained the ability|Promoted from|Promoted by)").where("inviter_id = user_feedback.creator_id").update_all(inviter_id: nil)
# Clear inviter for users where there is a promotion modaction from the inviter.
sql = "JOIN (SELECT (regexp_matches(description, '/users/([0-9]+)'))[1]::integer as user_id, * FROM mod_actions) AS subquery ON subquery.user_id = users.id"
p User.joins(sql).where("subquery.category": [7, 8, 9, 19]).where("users.inviter_id = subquery.creator_id").count
User.joins(sql).where("subquery.category": [7, 8, 9, 19]).where("users.inviter_id = subquery.creator_id").update_all(inviter_id: nil)
end

View File

@@ -46,8 +46,8 @@ class PostNavbarComponentTest < ViewComponent::TestCase
context "for a post with favgroups" do
setup do
as(@user) do
@favgroup1 = create(:favorite_group, creator: @user)
@favgroup2 = create(:favorite_group, creator: @user)
@favgroup1 = create(:favorite_group, creator: @user, is_public: true)
@favgroup2 = create(:favorite_group, creator: @user, is_public: false)
@post.update(tag_string: "favgroup:#{@favgroup1.id} favgroup:#{@favgroup2.id}")
end
end
@@ -65,6 +65,18 @@ class PostNavbarComponentTest < ViewComponent::TestCase
assert_css(".favgroup-navbar[data-selected=true] .favgroup-name", text: "Favgroup: #{@favgroup1.pretty_name}")
assert_css(".favgroup-navbar[data-selected=false] .favgroup-name", text: "Favgroup: #{@favgroup2.pretty_name}")
end
should "show public favgroups that belong to another user when doing a favgroup:<id> search" do
render_post_navbar(@post, current_user: create(:user), search: "favgroup:#{@favgroup1.id}")
assert_css(".favgroup-navbar[data-selected=true] .favgroup-name", text: "Favgroup: #{@favgroup1.pretty_name}")
end
should "not show private favgroups that belong to another user when doing a favgroup:<id> search" do
render_post_navbar(@post, current_user: create(:user), search: "favgroup:#{@favgroup2.id}")
assert_css(".favgroup-navbar .favgroup-name", count: 0)
end
end
end
end

View File

@@ -0,0 +1,8 @@
FactoryBot.define do
factory(:rate_limit) do
limited { false }
points { 0 }
action { "test" }
key { "1234" }
end
end

View File

@@ -227,7 +227,7 @@ class ApplicationControllerTest < ActionDispatch::IntegrationTest
should "fail with a 429 error" do
user = create(:user)
post = create(:post, rating: "s")
TokenBucket.any_instance.stubs(:throttled?).returns(true)
RateLimit.any_instance.stubs(:limited?).returns(true)
put_auth post_path(post), user, params: { post: { rating: "e" } }

View File

@@ -100,6 +100,11 @@ class BansControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
end
should "not raise an exception on a blank username" do
post_auth bans_path, @mod, params: {}
assert_response :success
end
end
context "update action" do

View File

@@ -142,14 +142,14 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest
context "when updating another user's comment" do
should "succeed if updater is a moderator" do
put_auth comment_path(@comment.id), @user, params: {comment: {body: "abc"}}
put_auth comment_path(@comment.id), @user, params: {comment: {body: "abc"}}, xhr: true
assert_equal("abc", @comment.reload.body)
assert_redirected_to post_path(@comment.post)
assert_response :success
end
should "fail if updater is not a moderator" do
@mod_comment = as(@mod) { create(:comment, post: @post) }
put_auth comment_path(@mod_comment.id), @user, params: {comment: {body: "abc"}}
put_auth comment_path(@mod_comment.id), @user, params: {comment: {body: "abc"}}, xhr: true
assert_not_equal("abc", @mod_comment.reload.body)
assert_response 403
end
@@ -157,13 +157,13 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest
context "when stickying a comment" do
should "succeed if updater is a moderator" do
put_auth comment_path(@comment.id), @mod, params: {comment: {is_sticky: true}}
put_auth comment_path(@comment.id), @mod, params: {comment: {is_sticky: true}}, xhr: true
assert_equal(true, @comment.reload.is_sticky)
assert_redirected_to @comment.post
assert_response :success
end
should "fail if updater is not a moderator" do
put_auth comment_path(@comment.id), @user, params: {comment: {is_sticky: true}}
put_auth comment_path(@comment.id), @user, params: {comment: {is_sticky: true}}, xhr: true
assert_response 403
assert_equal(false, @comment.reload.is_sticky)
end
@@ -172,7 +172,7 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest
context "for a deleted comment" do
should "not allow the creator to edit the comment" do
@comment.update!(is_deleted: true)
put_auth comment_path(@comment.id), @user, params: { comment: { body: "blah" }}
put_auth comment_path(@comment.id), @user, params: { comment: { body: "blah" }}, xhr: true
assert_response 403
assert_not_equal("blah", @comment.reload.body)
@@ -180,16 +180,16 @@ class CommentsControllerTest < ActionDispatch::IntegrationTest
end
should "update the body" do
put_auth comment_path(@comment.id), @user, params: {comment: {body: "abc"}}
put_auth comment_path(@comment.id), @user, params: {comment: {body: "abc"}}, xhr: true
assert_equal("abc", @comment.reload.body)
assert_redirected_to post_path(@comment.post)
assert_response :success
end
should "allow changing the body and is_deleted" do
put_auth comment_path(@comment.id), @user, params: {comment: {body: "herp derp", is_deleted: true}}
put_auth comment_path(@comment.id), @user, params: {comment: {body: "herp derp", is_deleted: true}}, xhr: true
assert_equal("herp derp", @comment.reload.body)
assert_equal(true, @comment.is_deleted)
assert_redirected_to post_path(@post)
assert_response :success
end
should "not allow changing do_not_bump_post or post_id" do

View File

@@ -0,0 +1,33 @@
require 'test_helper'
class RateLimitsControllerTest < ActionDispatch::IntegrationTest
context "The rate limits controller" do
context "index action" do
setup do
@user = create(:user)
create(:rate_limit, key: @user.cache_key)
end
should "show all rate limits to the owner" do
get_auth rate_limits_path, create(:owner_user)
assert_response :success
assert_select "tbody tr", count: 2 # 2 because the login action creates a second rate limit.
end
should "show the user their own rate limits" do
get_auth rate_limits_path, @user
assert_response :success
assert_select "tbody tr", count: 1
end
should "not show users rate limits belonging to other users" do
get_auth rate_limits_path, create(:user)
assert_response :success
assert_select "tbody tr", count: 0
end
end
end
end

View File

@@ -72,6 +72,31 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
assert_equal(0, @ip_ban.reload.hit_count)
assert_nil(@ip_ban.last_hit_at)
end
should "rate limit logins to 10 per minute per IP" do
freeze_time
11.times do
post session_path, params: { name: @user.name, password: "password" }, headers: { REMOTE_ADDR: "1.2.3.4" }
assert_redirected_to posts_path
assert_equal(@user.id, session[:user_id])
delete_auth session_path, @user
end
post session_path, params: { name: @user.name, password: "password" }, headers: { REMOTE_ADDR: "1.2.3.4" }
assert_response 429
assert_not_equal(@user.id, session[:user_id])
travel 59.seconds
post session_path, params: { name: @user.name, password: "password" }, headers: { REMOTE_ADDR: "1.2.3.4" }
assert_response 429
assert_not_equal(@user.id, session[:user_id])
travel 10.seconds
post session_path, params: { name: @user.name, password: "password" }, headers: { REMOTE_ADDR: "1.2.3.4" }
assert_redirected_to posts_path
assert_equal(@user.id, session[:user_id])
end
end
context "destroy action" do

View File

@@ -48,11 +48,5 @@ class ApiKeyTest < ActiveSupport::TestCase
should "not authenticate with the wrong name" do
assert_equal(false, create(:user).authenticate_api_key(@api_key.key))
end
should "have the same limits whether or not they have an api key" do
assert_no_difference(["@user.reload.api_regen_multiplier", "@user.reload.api_burst_limit"]) do
@api_key.destroy
end
end
end
end

View File

@@ -33,7 +33,7 @@ class IpGeolocationTest < ActiveSupport::TestCase
should "work for a residential IP" do
@ip = IpGeolocation.create_or_update!("2a01:0e35:2f22:e3d0::1")
assert_equal(26, @ip.network.prefix)
assert_equal(28, @ip.network.prefix)
assert_equal(false, @ip.is_proxy?)
assert_equal(49, @ip.latitude.round(0))
assert_equal(2, @ip.longitude.round(0))

View File

@@ -0,0 +1,72 @@
require 'test_helper'
class RateLimitTest < ActiveSupport::TestCase
context "RateLimit: " do
context "#limit! method" do
should "create a new rate limit object if none exists, or update it if it already exists" do
assert_difference("RateLimit.count", 1) do
RateLimiter.new("write", ["users/1"]).limited?
end
assert_difference("RateLimit.count", 0) do
RateLimiter.new("write", ["users/1"]).limited?
end
assert_difference("RateLimit.count", 1) do
RateLimiter.new("write", ["users/1", "ip/1.2.3.4"]).limited?
end
assert_difference("RateLimit.count", 0) do
RateLimiter.new("write", ["users/1", "ip/1.2.3.4"]).limited?
end
end
should "include the cost of the first action when initializing the limit" do
limiter = RateLimiter.new("write", ["users/1"], burst: 10, cost: 1)
assert_equal(9, limiter.rate_limits.first.points)
end
should "be limited and penalize the caller 1 second if the point count is negative" do
freeze_time
create(:rate_limit, action: "write", key: "users/1", points: -1)
limiter = RateLimiter.new("write", ["users/1"], cost: 1)
assert_equal(true, limiter.limited?)
assert_equal(-2, limiter.rate_limits.first.points)
end
should "not be limited if the point count was positive before the action" do
freeze_time
create(:rate_limit, action: "write", key: "users/1", points: 0.01)
limiter = RateLimiter.new("write", ["users/1"], cost: 1)
assert_equal(false, limiter.limited?)
assert_equal(-0.99, limiter.rate_limits.first.points)
end
should "refill the points at the correct rate" do
freeze_time
create(:rate_limit, action: "write", key: "users/1", points: -2)
limiter = RateLimiter.new("write", ["users/1"], cost: 1, rate: 1, burst: 10)
assert_equal(true, limiter.limited?)
assert_equal(-3, limiter.rate_limits.first.points)
travel 1.second
limiter = RateLimiter.new("write", ["users/1"], cost: 1, rate: 1, burst: 10)
assert_equal(true, limiter.limited?)
assert_equal(-3, limiter.rate_limits.first.points)
travel 5.second
limiter = RateLimiter.new("write", ["users/1"], cost: 1, rate: 1, burst: 10)
assert_equal(false, limiter.limited?)
assert_equal(1, limiter.rate_limits.first.points)
travel 60.second
limiter = RateLimiter.new("write", ["users/1"], cost: 1, rate: 1, burst: 10)
assert_equal(false, limiter.limited?)
assert_equal(9, limiter.rate_limits.first.points)
end
end
end
end

View File

@@ -95,20 +95,30 @@ module Sources
should "fetch the source data" do
assert_equal("evazion", @site.artist_name)
end
should "correctly get the page url" do
assert_equal(@ref, @site.page_url)
end
end
context "A baraag url" do
setup do
skip "Baraag keys not set" unless Danbooru.config.baraag_client_id
@url = "https://baraag.net/@bardbot/105732813175612920"
@site = Sources::Strategies.find(@url)
@site1 = Sources::Strategies.find(@url)
@img = "https://baraag.net/system/media_attachments/files/105/803/948/862/719/091/original/54e1cb7ca33ec449.png"
@ref = "https://baraag.net/@Nakamura/105803949565505009"
@site2 = Sources::Strategies.find(@img, @ref)
end
should "work" do
assert_equal("https://baraag.net/@bardbot", @site.profile_url)
assert_equal(["https://baraag.net/system/media_attachments/files/105/732/803/241/495/700/original/556e1eb7f5ca610f.png"], @site.image_urls)
assert_equal("bardbot", @site.artist_name)
assert_equal("🍌", @site.dtext_artist_commentary_desc)
assert_equal("https://baraag.net/@bardbot", @site1.profile_url)
assert_equal(["https://baraag.net/system/media_attachments/files/105/732/803/241/495/700/original/556e1eb7f5ca610f.png"], @site1.image_urls)
assert_equal("bardbot", @site1.artist_name)
assert_equal("🍌", @site1.dtext_artist_commentary_desc)
assert_equal([@img], @site2.image_urls)
end
end

View File

@@ -305,6 +305,18 @@ module Sources
end
end
context "when the cached session cookie is invalid" do
should "clear the cached cookie after failing to fetch the data" do
site = Sources::Strategies.find("https://nijie.info/view.php?id=203688")
Cache.put("nijie-session-cookie", HTTP::Cookie.new(name: "NIJIEIJIEID", value: "fake", domain: "nijie.info", path: "/"))
assert_equal("fake", site.cached_session_cookie.value)
assert_equal([], site.image_urls)
assert_nil(Cache.get("nijie-session-cookie"))
end
end
context "a doujin post" do
should "work" do
image = "https://pic.nijie.net/01/dojin_main/dojin_sam/20120213044700%E3%82%B3%E3%83%94%E3%83%BC%20%EF%BD%9E%200011%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC.jpg"

View File

@@ -1,45 +0,0 @@
require 'test_helper'
class TokenBucketTest < ActiveSupport::TestCase
context "#add!" do
setup do
@user = FactoryBot.create(:user)
TokenBucket.create(user_id: @user.id, last_touched_at: 1.minute.ago, token_count: 0)
end
should "work" do
@user.token_bucket.add!
assert_operator(@user.token_bucket.token_count, :>, 0)
@user.reload
assert_operator(@user.token_bucket.token_count, :>, 0)
end
end
context "#consume!" do
setup do
@user = FactoryBot.create(:user)
TokenBucket.create(user_id: @user.id, last_touched_at: 1.minute.ago, token_count: 1)
end
should "work" do
@user.token_bucket.consume!
assert_operator(@user.token_bucket.token_count, :<, 1)
@user.reload
assert_operator(@user.token_bucket.token_count, :<, 1)
end
end
context "#throttled?" do
setup do
@user = FactoryBot.create(:user)
TokenBucket.create(user_id: @user.id, last_touched_at: 1.minute.ago, token_count: 0)
end
should "work" do
assert(!@user.token_bucket.throttled?)
assert_operator(@user.token_bucket.token_count, :<, 60)
@user.reload
assert_operator(@user.token_bucket.token_count, :<, 60)
end
end
end

226
yarn.lock
View File

@@ -83,13 +83,13 @@ __metadata:
linkType: hard
"@babel/generator@npm:^7.13.0":
version: 7.13.0
resolution: "@babel/generator@npm:7.13.0"
version: 7.13.9
resolution: "@babel/generator@npm:7.13.9"
dependencies:
"@babel/types": ^7.13.0
jsesc: ^2.5.1
source-map: ^0.5.0
checksum: d406238edc9e967e5a5013b9c7cf02d9eb4ea0160cd209cb63edb39a095d392b007e6762acb65ae79958a8bc0cf94945155b34dbcb2dfc93df1159881c217148
checksum: d9cf7db910dd703a55c3ba147a8024564d51de06f5e3e61aef6ca197bcd80a6cb0a633fe4688c8c9f6226c70ee6f32a747050a8e420972b45cc98a6b3fc5ae66
languageName: node
linkType: hard
@@ -153,9 +153,9 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-define-polyfill-provider@npm:^0.1.4":
version: 0.1.4
resolution: "@babel/helper-define-polyfill-provider@npm:0.1.4"
"@babel/helper-define-polyfill-provider@npm:^0.1.5":
version: 0.1.5
resolution: "@babel/helper-define-polyfill-provider@npm:0.1.5"
dependencies:
"@babel/helper-compilation-targets": ^7.13.0
"@babel/helper-module-imports": ^7.12.13
@@ -167,7 +167,7 @@ __metadata:
semver: ^6.1.2
peerDependencies:
"@babel/core": ^7.4.0-0
checksum: 268ad963d95dd22c2fab0822a42b9a5bf7d0d2909bbaacf7377326c70c0071e0423c0092085a7e6531bbaf4ae917f8fa86f15de4da395add99cca900b95a7498
checksum: 41a3bf1b016cd94cece5eec1aa7fcc868ca32e0b630735e2be934d1ff7145226633b8c7d67884c18d7a090a5465a94bb8c4b01160ed8ea240f952d6aa1057ef0
languageName: node
linkType: hard
@@ -360,11 +360,11 @@ __metadata:
linkType: hard
"@babel/parser@npm:^7.12.13, @babel/parser@npm:^7.13.0, @babel/parser@npm:^7.13.4, @babel/parser@npm:^7.7.0":
version: 7.13.4
resolution: "@babel/parser@npm:7.13.4"
version: 7.13.9
resolution: "@babel/parser@npm:7.13.9"
bin:
parser: ./bin/babel-parser.js
checksum: 3aac62adbd1fd91798751a09b385ed3810acffb7bd637066bea65acf16670fdc8c7c39bab2148c57b4d6606355344de01922c9aba86405c771eaabc58701077a
checksum: de61d40db87a09a2bf230b06cd33121e25a650cf82efb3af7d348e9e5d5ca9426fa76f264eb7c9c5f16a11d17cf66adbe2f807d5a6126c370017ea4ca506fcea
languageName: node
linkType: hard
@@ -983,8 +983,8 @@ __metadata:
linkType: hard
"@babel/plugin-transform-runtime@npm:^7.12.1":
version: 7.13.8
resolution: "@babel/plugin-transform-runtime@npm:7.13.8"
version: 7.13.9
resolution: "@babel/plugin-transform-runtime@npm:7.13.9"
dependencies:
"@babel/helper-module-imports": ^7.12.13
"@babel/helper-plugin-utils": ^7.13.0
@@ -994,7 +994,7 @@ __metadata:
semver: ^6.3.0
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 481c3cdcd500eb29fe2cb5410570c63dbd06b909e66985c3e5b5f6b2d38b59fb013fd64a0f48f4f50bff866eddde1c17f7c0a5733835ba1176db76e2aa05fde4
checksum: 79fd9e6ff154c6005acd1478a5e5c44b4d33ef9d96f67b83dabba59ad816a7ea5b77800ff222fd565c4e6178831165d4bc848bc07c1d88c5deaa67609af58682
languageName: node
linkType: hard
@@ -1078,8 +1078,8 @@ __metadata:
linkType: hard
"@babel/preset-env@npm:^7.12.11":
version: 7.13.8
resolution: "@babel/preset-env@npm:7.13.8"
version: 7.13.9
resolution: "@babel/preset-env@npm:7.13.9"
dependencies:
"@babel/compat-data": ^7.13.8
"@babel/helper-compilation-targets": ^7.13.8
@@ -1151,7 +1151,7 @@ __metadata:
semver: ^6.3.0
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: fa41587bdb31900499a3d8661e251e93e2b075590660985de3b63efdf72f1a4c1588a25c9148d0f91046639d324b7730ebf8c4fb0230b3dfcc70ac7937fd6a7e
checksum: 55ef45c648da2cf98d703a3f5128eeb883285580f02717059c1ac708ac8cb291e40705838dfdd4f4c59da3c96b816c13e2d2d0d9a7490e3bace4cf41ec8ba151
languageName: node
linkType: hard
@@ -1171,11 +1171,11 @@ __metadata:
linkType: hard
"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.8.4":
version: 7.13.8
resolution: "@babel/runtime@npm:7.13.8"
version: 7.13.9
resolution: "@babel/runtime@npm:7.13.9"
dependencies:
regenerator-runtime: ^0.13.4
checksum: d8db125d3629c4000322e8264d7510640ac671136f416d1fcd4b74d26d7e703bf671820fab81a29a5ee0bd11e981de518666619398ec29b1ab5e155ab8131812
checksum: e6f79d20e10c2921520c499f3cf295a9ee5c137e73f77f77eedde9f9073bc3541c1fc7fa6c97b0613f4140303ac00d08506e9f090068d219c58781d2b62c662d
languageName: node
linkType: hard
@@ -1250,58 +1250,58 @@ __metadata:
linkType: hard
"@fontsource/anton@npm:^4.0.0":
version: 4.2.2
resolution: "@fontsource/anton@npm:4.2.2"
checksum: 16d73f93da0896db663e1533588091191f20df5c45a85507b7dab4b5dc79e2e68e1c58620108baa2ea519aab26ead550a03a28476275ed5c6ab36528536a6d00
version: 4.2.3
resolution: "@fontsource/anton@npm:4.2.3"
checksum: 38d0b351da330c124736e5d9b97cc33916369d64de913f773b4987c85278ab26d6756f203d091121e710a0bc8a4a90d51510e0c8b892a99e820fba46e0f99564
languageName: node
linkType: hard
"@fontsource/archivo-narrow@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/archivo-narrow@npm:4.2.1"
checksum: f6be53f4e451e3c1fe4478385204f42da274c124dfbe4c6d384686299179c11885a7a64b2759d0b3ea876c04f25f93b5f7879e6c6cd69b7120b162f08f41175d
version: 4.2.2
resolution: "@fontsource/archivo-narrow@npm:4.2.2"
checksum: 2e466c137ad902ae95c0dc35a20030e1ded8d855b53fadb8bc82084a54da74664708d29292d1e248b2af18060360d638947b9325ee3855257f300f2da222c620
languageName: node
linkType: hard
"@fontsource/ibm-plex-mono@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/ibm-plex-mono@npm:4.2.1"
checksum: e2df398d11f7202eb1b0830cd39a36c7bd90dcea0f07622db36ab14131089aed2ecba335f31a22f3d898ad5e936545adc9b2e2aa6abeb3281d2cc80e0078242d
version: 4.2.2
resolution: "@fontsource/ibm-plex-mono@npm:4.2.2"
checksum: 6fd8c8425e57b6fe705fe3bbf98fd834f49093c54f6d73e70693daa9efc82862a07dd873fd6ce9ecfff31ad0609dee55610fe8c6a14ba0bec0cd81036450bbd1
languageName: node
linkType: hard
"@fontsource/indie-flower@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/indie-flower@npm:4.2.1"
checksum: 2d4906cfbe5e6b42a2fc69a034a36b700e6e9f296d0a23c94e5659949f6480224c3d9c5bb1201bc59992b8f0dbd8bc5fd9fde58cdb5ed9c6c6847b3aeb5bb5de
version: 4.2.2
resolution: "@fontsource/indie-flower@npm:4.2.2"
checksum: 86a1fc85e6d45a553f4f864233d1ddbec5d50f50f3ec06ba60238fb46c2cd8475fe7c1c47ed0e49e776756540f37a6d1f659466940b1e88d61daf69bcbd8fce6
languageName: node
linkType: hard
"@fontsource/lora@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/lora@npm:4.2.1"
checksum: e77425c60daba9e411421d8ebbbe285dfce8f8f1606123c144d81d56cf6bc0a0b426df6aa883a093985f263d9b81d849042656c30473cfef1b99281f7957dc1e
version: 4.2.2
resolution: "@fontsource/lora@npm:4.2.2"
checksum: a4b46c5b1999ad95c2714583a7c2ff72e773197b16f1a51bc623ce2143b1507cf91c6f1b87465042de2f90a710de5718bd1b74b2435ad913d17a531a401f1c47
languageName: node
linkType: hard
"@fontsource/petit-formal-script@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/petit-formal-script@npm:4.2.1"
checksum: 0ad6d99530791913e52e91bf3df523d0540e64136ddfa0bbd9874253305f9cc69eb467b9be5ccacdecfefb6bf538bbbb2216c77d85067c4a31b5bf86ddfb8baa
version: 4.2.2
resolution: "@fontsource/petit-formal-script@npm:4.2.2"
checksum: 9901321861d9c244d788b5d9a44cb22111fe35b7722ea07586ddf10e64c8e0c0494719a38399c1cc2d30668f0b588533496d991b053cfa2935280262b0ab8d9a
languageName: node
linkType: hard
"@fontsource/rokkitt@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/rokkitt@npm:4.2.1"
checksum: 48f620a73048c52621edb8134d8eb402f415937bd3bdcdeb1ac5a637b572fe8116f9190245f5bc9c07a39f4b909543cef54c09cad14506c83c32c2a2fcdf892d
version: 4.2.2
resolution: "@fontsource/rokkitt@npm:4.2.2"
checksum: c97cd17023c5a74774ba395e61505a75e03b2722b5e65ebc8a47e65ee0b4f37d41924caa55cb102968988ccc182b6377f8bc2ecc06d7d491e0d1568255388c7b
languageName: node
linkType: hard
"@fontsource/unifrakturmaguntia@npm:^4.0.0":
version: 4.2.1
resolution: "@fontsource/unifrakturmaguntia@npm:4.2.1"
checksum: 0051457de0cbad78d41a64378cdde228a227e756e05b015bec43ca22b0f9c66cace9b6d5a2f521b1f7b49a8e823eb94fdd057ad4c0dc33bcf3e462704eba2f71
version: 4.2.2
resolution: "@fontsource/unifrakturmaguntia@npm:4.2.2"
checksum: f11299dc562353c19e7a0140b796ba993c8c5644c45d659563772047bc4cb9891dd14856d3d6221485f5b47f314721e88845415b8890b90e4c337d6ca755811d
languageName: node
linkType: hard
@@ -1359,9 +1359,9 @@ __metadata:
linkType: hard
"@popperjs/core@npm:^2.8.3":
version: 2.8.6
resolution: "@popperjs/core@npm:2.8.6"
checksum: 1e3259c2aa915dbd5721d7b056e71425fda5103b9c449448d3801a05a94ec0c19d88109a5ded98bfebc887c4151ef073b3074a0b638bc7c1ba71c554678374af
version: 2.9.0
resolution: "@popperjs/core@npm:2.9.0"
checksum: 9b4a2ae8906faf56f1612e2ded9f27cb3771c3684cf9be90f7527d41cf263584b385399be0a2a99eb89ff89788c37d18773d2c4a6b6a51db3d0f9c4c5a0839b9
languageName: node
linkType: hard
@@ -1465,12 +1465,12 @@ __metadata:
linkType: hard
"@types/eslint@npm:*, @types/eslint@npm:^7.2.6":
version: 7.2.6
resolution: "@types/eslint@npm:7.2.6"
version: 7.2.7
resolution: "@types/eslint@npm:7.2.7"
dependencies:
"@types/estree": "*"
"@types/json-schema": "*"
checksum: 3a89e63d02c447b7182a30b80925ccfbd46661aa3e7262a61aa8de9b90d1de4b0b722f681f7473b35258e110ef726d73df406374abdac8435358dd13ca74251d
checksum: cb7b820d887b51c9d620005a74cf223ec834565c366035cfaf4cc31d5681cc72d2fc421465f648019145e50f11222e35f0910139a13c2e46ff399e72cad0e431
languageName: node
linkType: hard
@@ -1538,9 +1538,9 @@ __metadata:
linkType: hard
"@types/node@npm:*":
version: 14.14.31
resolution: "@types/node@npm:14.14.31"
checksum: 635dc8a0898a923621e02ca179e17baa39fdfa44f0096fcc1b7046c9b32317e74a99956a7b45ca0e8069874f51f4e7873a418239a318a4b6e7936f6510ac5992
version: 14.14.32
resolution: "@types/node@npm:14.14.32"
checksum: ae73f3b668242da660b4f4f2047114fc047f5a6a92b8f9f4ab2f8ca1325c914c08cf13b6f90e6e88016fafb7a662d1963b075a6220d2208651a98a1cb3407221
languageName: node
linkType: hard
@@ -1847,14 +1847,14 @@ __metadata:
linkType: hard
"ajv@npm:^7.0.2":
version: 7.1.1
resolution: "ajv@npm:7.1.1"
version: 7.2.1
resolution: "ajv@npm:7.2.1"
dependencies:
fast-deep-equal: ^3.1.1
json-schema-traverse: ^1.0.0
require-from-string: ^2.0.2
uri-js: ^4.2.2
checksum: fe4e138529363bf1c8c429e1f3e88480918b538fe4a44660b989cea863714715af75e874aad129ccd5cbcf6647fa457e20b735bb3279a3bca08f11193bae5d19
checksum: 34044f60ca45ef8ec850f5d09e4db340bb870639efc1694d54bec7ff4b06b12b077872949223f703d1507bbf3d553b59752f4c327ffcd25726ee27919d586037
languageName: node
linkType: hard
@@ -2211,38 +2211,38 @@ __metadata:
linkType: hard
"babel-plugin-polyfill-corejs2@npm:^0.1.4":
version: 0.1.8
resolution: "babel-plugin-polyfill-corejs2@npm:0.1.8"
version: 0.1.10
resolution: "babel-plugin-polyfill-corejs2@npm:0.1.10"
dependencies:
"@babel/compat-data": ^7.13.0
"@babel/helper-define-polyfill-provider": ^0.1.4
"@babel/helper-define-polyfill-provider": ^0.1.5
semver: ^6.1.1
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 937b94eb1850471f705b0bdb0bf2635494a7ea67d9b252aeb9fe5ae18728f18fa92b1be60856bb0d7b08eaa5bca10b2b87b54b720ef00040a50e161543210b05
checksum: b11a01d9d3a078de5f26eeef8216f29b104239eee3ae93767dccdff9df558d07d159a35941ce5d77d6c658b9017475922831a232f8e60d94056412ba6ef2692b
languageName: node
linkType: hard
"babel-plugin-polyfill-corejs3@npm:^0.1.3":
version: 0.1.6
resolution: "babel-plugin-polyfill-corejs3@npm:0.1.6"
version: 0.1.7
resolution: "babel-plugin-polyfill-corejs3@npm:0.1.7"
dependencies:
"@babel/helper-define-polyfill-provider": ^0.1.4
"@babel/helper-define-polyfill-provider": ^0.1.5
core-js-compat: ^3.8.1
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: d855c4db2b34b0bc3706961322738e83707a0ac13b8f88c2c3529f0848192aa8eb9a8d96d3f9448bf8927225ac24324a1e2253591102b127157720292abc4d1c
checksum: d6f94262fbcfbfcffdb526abd20b49bdd730d646df3709b06536248b72c7b4c53a4f75f755c9041f249bf8486bd4eb1e79fdfb0796e4795cef64942b51123b50
languageName: node
linkType: hard
"babel-plugin-polyfill-regenerator@npm:^0.1.2":
version: 0.1.5
resolution: "babel-plugin-polyfill-regenerator@npm:0.1.5"
version: 0.1.6
resolution: "babel-plugin-polyfill-regenerator@npm:0.1.6"
dependencies:
"@babel/helper-define-polyfill-provider": ^0.1.4
"@babel/helper-define-polyfill-provider": ^0.1.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 06839aec3b847ce9c2d488c9a245f8e608bdabbb2e5a0263ea227b648f93eb91d593bda21c60a099482231dc416bf6e6f5b8838b45534b695056e23c23073c46
checksum: 49b98a19015074d3466e8b020928b7dc09ff2c1a62d8d8ba2f02f6e7e0cc99e3ac5e7624a7611acf0a8073d363c2d6aa6a0a6e7508b85f63982150164f1d7e25
languageName: node
linkType: hard
@@ -2573,9 +2573,9 @@ __metadata:
linkType: hard
"caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001181":
version: 1.0.30001192
resolution: "caniuse-lite@npm:1.0.30001192"
checksum: d2e3bc901b0cde3cd4522fa7f813565a4559284648b5c54f1049e6f991fc55d7d93b907e739270516e0207dd09b7c2ed8b8ca4469dceeb515eb1ec09798dff16
version: 1.0.30001196
resolution: "caniuse-lite@npm:1.0.30001196"
checksum: 42c38418062cd00c43793679c1a8766f98127c3cb99c9775156b8d7843e2e12fd188791360e890c0c25a2c492ff30d9072d1727cb65a860e87a9d9fd11b0d917
languageName: node
linkType: hard
@@ -2805,7 +2805,7 @@ __metadata:
languageName: node
linkType: hard
"colorette@npm:^1.2.1":
"colorette@npm:^1.2.1, colorette@npm:^1.2.2":
version: 1.2.2
resolution: "colorette@npm:1.2.2"
checksum: e240f0c94b8d9f34b52bd17b50fc13a3b74f9e662edeaa2b0c65e06ec6b1fc6367fb42b834ec5a1d819d68b74a3d850f3bd3e284f9e614d6c4ffa122f83c6ec5
@@ -3054,8 +3054,8 @@ __metadata:
linkType: hard
"css-loader@npm:^5.0.1":
version: 5.1.0
resolution: "css-loader@npm:5.1.0"
version: 5.1.1
resolution: "css-loader@npm:5.1.1"
dependencies:
camelcase: ^6.2.0
cssesc: ^3.0.0
@@ -3071,7 +3071,7 @@ __metadata:
semver: ^7.3.4
peerDependencies:
webpack: ^4.27.0 || ^5.0.0
checksum: af700ed732a4c032522390421d6092ee86b20488e2fe91f794ad78a368dbf46bb09454f48c61a24e0bbe5825bff4066ba1c607f8c4c296f7f163be6abcd9c001
checksum: 620fae3cace4a251fec723da2a4549a3874dd3459774451f834b19c4f38051aa76323d500d5d3408244b11fb40dbfc2fb30722be7b5e87ba7c6bed65396a898b
languageName: node
linkType: hard
@@ -3139,14 +3139,14 @@ __metadata:
linkType: hard
"debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1":
version: 4.3.1
resolution: "debug@npm:4.3.1"
version: 4.3.2
resolution: "debug@npm:4.3.2"
dependencies:
ms: 2.1.2
peerDependenciesMeta:
supports-color:
optional: true
checksum: 0d41ba5177510e8b388dfd7df143ab0f9312e4abdaba312595461511dac88e9ef8101939d33b4e6d37e10341af6a5301082e4d7d6f3deb4d57bc05fc7d296fad
checksum: 5543570879e2274f6725d4285a034d6e0822d35faefc6f55965933fb440e8c21eb3a0bef934e66f4b6b491f898ee2de37cab980e9d4fd61372136c19d3ce4527
languageName: node
linkType: hard
@@ -3466,9 +3466,9 @@ __metadata:
linkType: hard
"dropzone@npm:^5.5.1":
version: 5.7.6
resolution: "dropzone@npm:5.7.6"
checksum: dd56e57c030d2559d26aba853ec6f7a1067f30b516d9ecb916efa7db0de5c05c0fc476519a973e543771d15b6f2661f719c0b75416248234d775968b0e475eb9
version: 5.8.0
resolution: "dropzone@npm:5.8.0"
checksum: 9641ad9f76356161d7a0b8d7c593e21d4949984712b93a224efc12a5f39c09525fbe5873c427e7f7e572de084c019513d57848b722f2e98651e9c35ed1472830
languageName: node
linkType: hard
@@ -3497,9 +3497,9 @@ __metadata:
linkType: hard
"electron-to-chromium@npm:^1.3.649":
version: 1.3.675
resolution: "electron-to-chromium@npm:1.3.675"
checksum: 32bc34084af5f6eb5e52500de8282302b0d19e85e93201e4544f91520d80cc262ccdcc800d23a67b9995e9abb59a82d3dfb31313388e4d2c50bf6838f9ddcfc5
version: 1.3.682
resolution: "electron-to-chromium@npm:1.3.682"
checksum: 9ceb3a72a4ff0c239889d7d1aba0a02ee3d19540e6e0cc63906f03dae931b01f390e54a723f52f94fc35835b1352b746850315a56057dd596c3f8d43635c0c44
languageName: node
linkType: hard
@@ -3610,9 +3610,9 @@ __metadata:
linkType: hard
"es-module-lexer@npm:^0.4.0":
version: 0.4.0
resolution: "es-module-lexer@npm:0.4.0"
checksum: 41d29cc3eb7e648c535bba09e6511834cbb9d06ed4747972ad102e605e1134567a39a3b3f482b9148693a5a95caf7ffe828db9d8f1ede833b836a5d9593f1933
version: 0.4.1
resolution: "es-module-lexer@npm:0.4.1"
checksum: 0c634ce62d3a77b04aa56b9ca2af2b58ff73a834afc76ac6747b25173e97d9050a28451b6ed39b54b84b8498d887ac8bd5bcf2c9aa9ba948ca0aee0acd613618
languageName: node
linkType: hard
@@ -4095,7 +4095,7 @@ __metadata:
languageName: node
linkType: hard
"file-entry-cache@npm:^6.0.0, file-entry-cache@npm:^6.0.1":
"file-entry-cache@npm:^6.0.1":
version: 6.0.1
resolution: "file-entry-cache@npm:6.0.1"
dependencies:
@@ -4490,11 +4490,11 @@ fsevents@~2.3.1:
linkType: hard
"glob-parent@npm:^5.0.0, glob-parent@npm:^5.1.0, glob-parent@npm:~5.1.0":
version: 5.1.1
resolution: "glob-parent@npm:5.1.1"
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
dependencies:
is-glob: ^4.0.1
checksum: 2af6e196fba4071fb07ba261366e446ba2b320e6db0a2069cf8e12117c5811abc6721f08546148048882d01120df47e56aa5a965517a6e5ba19bfeb792655119
checksum: 82fcaa4ce102a0ae01370ed8fd5299ca32184af0418e1c1b613ed851240160558c0cc9712868eb9ca1924f687b07cd9c70c25f303f39f9f376d9a32f94f28e76
languageName: node
linkType: hard
@@ -7303,8 +7303,8 @@ fsevents@~2.3.1:
linkType: hard
"postcss-loader@npm:^5.0.0":
version: 5.0.0
resolution: "postcss-loader@npm:5.0.0"
version: 5.1.0
resolution: "postcss-loader@npm:5.1.0"
dependencies:
cosmiconfig: ^7.0.0
klona: ^2.0.4
@@ -7312,7 +7312,7 @@ fsevents@~2.3.1:
peerDependencies:
postcss: ^7.0.0 || ^8.0.1
webpack: ^5.0.0
checksum: 51cdba32b86ed793812b12905f9944631e5bcfe271b20ca04e7c63babde672f79a25061e57a0b424328be243049958c9396289e62ce39e49f8d4da25825ebdd5
checksum: 266feaa1d958871a00f3eb5d1e1d54d7cac105e85d7c7f494404161a3d77d3edbb128c32ea2f263c76baeff015e56e9067e5c0bd44f63b52976bd30c973909e1
languageName: node
linkType: hard
@@ -7603,13 +7603,13 @@ fsevents@~2.3.1:
linkType: hard
"postcss@npm:^8.2.4, postcss@npm:^8.2.6":
version: 8.2.6
resolution: "postcss@npm:8.2.6"
version: 8.2.7
resolution: "postcss@npm:8.2.7"
dependencies:
colorette: ^1.2.1
colorette: ^1.2.2
nanoid: ^3.1.20
source-map: ^0.6.1
checksum: 31dcc6632589e5b0d06ae3854122073a38952f3a25280511fa4bd8c2bc254c05d1d1db0913848ed3171c84d41b233ec3fe5d7349de4700f624a16fa93553ccac
checksum: cd2a7a8b9c1950783afd05e906d65a6433b13bcce5a52f3c2802c524479e7ba6893f516338ed1bc755b3a8545eb5a181daf0fcf25af694ad2e33e9d9fe7d4254
languageName: node
linkType: hard
@@ -8921,7 +8921,7 @@ fsevents@~2.3.1:
languageName: node
linkType: hard
"string-width@npm:^4.1.0, string-width@npm:^4.2.0":
"string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2":
version: 4.2.2
resolution: "string-width@npm:4.2.2"
dependencies:
@@ -9053,8 +9053,8 @@ fsevents@~2.3.1:
linkType: hard
"stylelint@npm:^13.0.0":
version: 13.11.0
resolution: "stylelint@npm:13.11.0"
version: 13.12.0
resolution: "stylelint@npm:13.12.0"
dependencies:
"@stylelint/postcss-css-in-js": ^0.37.2
"@stylelint/postcss-markdown": ^0.36.2
@@ -9066,7 +9066,7 @@ fsevents@~2.3.1:
execall: ^2.0.0
fast-glob: ^3.2.5
fastest-levenshtein: ^1.0.12
file-entry-cache: ^6.0.0
file-entry-cache: ^6.0.1
get-stdin: ^8.0.0
global-modules: ^2.0.0
globby: ^11.0.2
@@ -9076,7 +9076,7 @@ fsevents@~2.3.1:
import-lazy: ^4.0.0
imurmurhash: ^0.1.4
known-css-properties: ^0.21.0
lodash: ^4.17.20
lodash: ^4.17.21
log-symbols: ^4.0.0
mathml-tag-names: ^2.1.3
meow: ^9.0.0
@@ -9096,7 +9096,7 @@ fsevents@~2.3.1:
resolve-from: ^5.0.0
slash: ^3.0.0
specificity: ^0.4.1
string-width: ^4.2.0
string-width: ^4.2.2
strip-ansi: ^6.0.0
style-search: ^0.1.0
sugarss: ^2.0.0
@@ -9106,7 +9106,7 @@ fsevents@~2.3.1:
write-file-atomic: ^3.0.3
bin:
stylelint: bin/stylelint.js
checksum: ea646e0cc8940b14b984fa0b6286a0653d4af46af394a457dbe4afe376ceeda44a4f869c2addff1b73758d58d5547922a63d63ae23d55edd130ed9e3b7012d05
checksum: bd841f8a4c0d4f921bafb917230cf4495b88de878591d3b0e283d933de05accbafd8ca9b93049dde40e07a1de6f0d337d1efff36cec946d2c4ed710d7325809c
languageName: node
linkType: hard
@@ -9546,9 +9546,9 @@ fsevents@~2.3.1:
linkType: hard
"unist-util-is@npm:^4.0.0":
version: 4.0.4
resolution: "unist-util-is@npm:4.0.4"
checksum: 4a3561644e3c7eda33726a0e5d3da9279e50618cad2c0b81e9315b1826244b4c3815d8a2b079fb220b552c456b83406f718fdd5c6f42f43d5996f5daa856ca0f
version: 4.1.0
resolution: "unist-util-is@npm:4.1.0"
checksum: 08f19f4ff12c78de81356af8cfdaeb4c93a36ae0647de4fdbd191efa061aaf222d8a0a3a848d29472df3c65327b12ee9ab213f80e0602944a53d7229e72f8faa
languageName: node
linkType: hard
@@ -9652,9 +9652,9 @@ fsevents@~2.3.1:
linkType: hard
"v8-compile-cache@npm:^2.0.3, v8-compile-cache@npm:^2.2.0":
version: 2.2.0
resolution: "v8-compile-cache@npm:2.2.0"
checksum: 1efc9946401fcad7a67619b520d8d12e31c7138090ffd9f98af9b919461fa23d947ecef0eab89cca4037c01d29d25a389ab6c0fac70ee4ed030443b08cdf6cff
version: 2.3.0
resolution: "v8-compile-cache@npm:2.3.0"
checksum: b56f83d9ff14187562badc4955dadeef53ff3abde478ce60759539dd8d5472a91fce9db6083fc2450e54cef6f2110c1a28d8c12162dbf575a6cfcb846986904b
languageName: node
linkType: hard
@@ -9886,8 +9886,8 @@ fsevents@~2.3.1:
linkType: hard
"webpack@npm:^5.11.0":
version: 5.24.2
resolution: "webpack@npm:5.24.2"
version: 5.24.3
resolution: "webpack@npm:5.24.3"
dependencies:
"@types/eslint-scope": ^3.7.0
"@types/estree": ^0.0.46
@@ -9917,7 +9917,7 @@ fsevents@~2.3.1:
optional: true
bin:
webpack: bin/webpack.js
checksum: 18408ddd3e5d5849179d6f5ebb44bad28783315da22a19a7f5ad9901e0ef96157a1747db52745fbdb25081f1f73162313088e4895b42f725e2b1221bdcec8a6f
checksum: 025181b6a4ba57caabd81047ab5751835a1b00e04cd15d3dca36e33f2034a194f73135b468da402001e5f9042bdad41d38587aec2a747b9fd7846044092655bb
languageName: node
linkType: hard