Merge branch 'master' into skeb
20
Gemfile.lock
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class CommentVotesController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :js, :json, :xml, :html
|
||||
|
||||
def index
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class PostDisapprovalsController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :js, :html, :json, :xml
|
||||
|
||||
def create
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class PostVotesController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :js, :json, :xml, :html
|
||||
|
||||
def index
|
||||
|
||||
8
app/controllers/rate_limits_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class UsersController < ApplicationController
|
||||
respond_to :html, :xml, :json
|
||||
skip_before_action :api_check
|
||||
|
||||
def new
|
||||
@user = authorize User.new
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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! }
|
||||
|
||||
52
app/logical/rate_limiter.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/,
|
||||
|
||||
@@ -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 /,/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
5
app/policies/rate_limit_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class RateLimitPolicy < ApplicationPolicy
|
||||
def index?
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
1
app/views/comments/update.js.erb
Normal file
@@ -0,0 +1 @@
|
||||
$("#comment_<%= @comment.id %>").replaceWith("<%= j render_comment(@comment, current_user: CurrentUser.user) %>");
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
21
app/views/rate_limits/index.html.erb
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -5,6 +5,6 @@ class CreateTokenBuckets < ActiveRecord::Migration[4.2]
|
||||
end
|
||||
|
||||
def down
|
||||
raise NotImplementedError
|
||||
drop_table :token_buckets
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -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');
|
||||
|
||||
|
||||
|
||||
BIN
public/images/amazon-logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/ask-fm-logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/doujinshi-org-logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/erogamescape-logo.png
Normal file
|
After Width: | Height: | Size: 788 B |
BIN
public/images/hentai-foundry-logo.png
Normal file
|
After Width: | Height: | Size: 333 B |
BIN
public/images/ko-fi-logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/livedoor-logo.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/mangaupdates-logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/images/mihuashi-logo.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/mixi-jp-logo.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/piapro-jp-logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/picarto-logo.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/sakura-ne-jp-logo.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/stickam-logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/images/twitch-logo.png
Normal file
|
After Width: | Height: | Size: 382 B |
BIN
public/images/wikipedia-logo.png
Normal file
|
After Width: | Height: | Size: 976 B |
28
script/fixes/076_clear_inviters.rb
Executable 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
|
||||
@@ -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
|
||||
|
||||
8
test/factories/rate_limit.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
FactoryBot.define do
|
||||
factory(:rate_limit) do
|
||||
limited { false }
|
||||
points { 0 }
|
||||
action { "test" }
|
||||
key { "1234" }
|
||||
end
|
||||
end
|
||||
@@ -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" } }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
33
test/functional/rate_limits_controller_test.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
72
test/unit/rate_limit_test.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||