diff --git a/Gemfile.lock b/Gemfile.lock index 1c700a973..874b6b95e 100644 --- a/Gemfile.lock +++ b/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) diff --git a/app/components/application_component.rb b/app/components/application_component.rb index a0b7a072b..b4d8fc1d7 100644 --- a/app/components/application_component.rb +++ b/app/components/application_component.rb @@ -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) diff --git a/app/components/comment_component.rb b/app/components/comment_component.rb index b06805c56..7f2f77ccc 100644 --- a/app/components/comment_component.rb +++ b/app/components/comment_component.rb @@ -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 diff --git a/app/components/comment_component/comment_component.html.erb b/app/components/comment_component/comment_component.html.erb index a1f6bc513..987e80783 100644 --- a/app/components/comment_component/comment_component.html.erb +++ b/app/components/comment_component/comment_component.html.erb @@ -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 %> diff --git a/app/components/popup_menu_component/popup_menu_component.scss b/app/components/popup_menu_component/popup_menu_component.scss index d4d9efc70..dcc28222f 100644 --- a/app/components/popup_menu_component/popup_menu_component.scss +++ b/app/components/popup_menu_component/popup_menu_component.scss @@ -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; } } } diff --git a/app/components/post_navbar_component.rb b/app/components/post_navbar_component.rb index 2719254f5..f277028ba 100644 --- a/app/components/post_navbar_component.rb +++ b/app/components/post_navbar_component.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5b36a3175..af9d36964 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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}") diff --git a/app/controllers/comment_votes_controller.rb b/app/controllers/comment_votes_controller.rb index c85faf4d8..8e280385a 100644 --- a/app/controllers/comment_votes_controller.rb +++ b/app/controllers/comment_votes_controller.rb @@ -1,5 +1,4 @@ class CommentVotesController < ApplicationController - skip_before_action :api_check respond_to :js, :json, :xml, :html def index diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index f655385ce..423e04df0 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -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 diff --git a/app/controllers/favorites_controller.rb b/app/controllers/favorites_controller.rb index 8449dac63..2f086b6b8 100644 --- a/app/controllers/favorites_controller.rb +++ b/app/controllers/favorites_controller.rb @@ -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 diff --git a/app/controllers/forum_posts_controller.rb b/app/controllers/forum_posts_controller.rb index 53af4054b..0a239591c 100644 --- a/app/controllers/forum_posts_controller.rb +++ b/app/controllers/forum_posts_controller.rb @@ -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) diff --git a/app/controllers/forum_topics_controller.rb b/app/controllers/forum_topics_controller.rb index f9a0ef00c..03865d1ae 100644 --- a/app/controllers/forum_topics_controller.rb +++ b/app/controllers/forum_topics_controller.rb @@ -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 diff --git a/app/controllers/moderator/post/posts_controller.rb b/app/controllers/moderator/post/posts_controller.rb index bc0603c16..ab28bd2f5 100644 --- a/app/controllers/moderator/post/posts_controller.rb +++ b/app/controllers/moderator/post/posts_controller.rb @@ -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 diff --git a/app/controllers/post_disapprovals_controller.rb b/app/controllers/post_disapprovals_controller.rb index 6dbd93f46..e27853ee9 100644 --- a/app/controllers/post_disapprovals_controller.rb +++ b/app/controllers/post_disapprovals_controller.rb @@ -1,5 +1,4 @@ class PostDisapprovalsController < ApplicationController - skip_before_action :api_check respond_to :js, :html, :json, :xml def create diff --git a/app/controllers/post_votes_controller.rb b/app/controllers/post_votes_controller.rb index 1c8f8ff2f..1f66f4cf3 100644 --- a/app/controllers/post_votes_controller.rb +++ b/app/controllers/post_votes_controller.rb @@ -1,5 +1,4 @@ class PostVotesController < ApplicationController - skip_before_action :api_check respond_to :js, :json, :xml, :html def index diff --git a/app/controllers/rate_limits_controller.rb b/app/controllers/rate_limits_controller.rb new file mode 100644 index 000000000..daaa8d899 --- /dev/null +++ b/app/controllers/rate_limits_controller.rb @@ -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 diff --git a/app/controllers/robots_controller.rb b/app/controllers/robots_controller.rb index fda10c506..1c13acaeb 100644 --- a/app/controllers/robots_controller.rb +++ b/app/controllers/robots_controller.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 387037ea1..acd68815d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,5 @@ class UsersController < ApplicationController respond_to :html, :xml, :json - skip_before_action :api_check def new @user = authorize User.new diff --git a/app/helpers/bulk_update_requests_helper.rb b/app/helpers/bulk_update_requests_helper.rb deleted file mode 100644 index cb77f0f41..000000000 --- a/app/helpers/bulk_update_requests_helper.rb +++ /dev/null @@ -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 diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 7a5e9cd9b..725e9fc93 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -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) diff --git a/app/helpers/icon_helper.rb b/app/helpers/icon_helper.rb index e300e4820..4b13dea8c 100644 --- a/app/helpers/icon_helper.rb +++ b/app/helpers/icon_helper.rb @@ -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 diff --git a/app/javascript/src/styles/base/020_base.scss b/app/javascript/src/styles/base/020_base.scss index 41ba4cd62..700ffa965 100644 --- a/app/javascript/src/styles/base/020_base.scss +++ b/app/javascript/src/styles/base/020_base.scss @@ -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; } } diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css index f7bdda518..d10c940b3 100644 --- a/app/javascript/src/styles/base/040_colors.css +++ b/app/javascript/src/styles/base/040_colors.css @@ -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); diff --git a/app/javascript/src/styles/common/dtext.scss b/app/javascript/src/styles/common/dtext.scss index e56a37ebb..0669d7fb0 100644 --- a/app/javascript/src/styles/common/dtext.scss +++ b/app/javascript/src/styles/common/dtext.scss @@ -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. diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss index 9f43d783a..22f375b2f 100644 --- a/app/javascript/src/styles/common/utilities.scss +++ b/app/javascript/src/styles/common/utilities.scss @@ -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; } diff --git a/app/javascript/src/styles/specific/related_tags.scss b/app/javascript/src/styles/specific/related_tags.scss index 03930c453..66cc8dd38 100644 --- a/app/javascript/src/styles/specific/related_tags.scss +++ b/app/javascript/src/styles/specific/related_tags.scss @@ -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; } + } } } diff --git a/app/javascript/src/styles/specific/user_upgrades.scss b/app/javascript/src/styles/specific/user_upgrades.scss index a4a9993ba..66aa57ae4 100644 --- a/app/javascript/src/styles/specific/user_upgrades.scss +++ b/app/javascript/src/styles/specific/user_upgrades.scss @@ -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); diff --git a/app/logical/danbooru_maintenance.rb b/app/logical/danbooru_maintenance.rb index c990dcfae..48acb7c0e 100644 --- a/app/logical/danbooru_maintenance.rb +++ b/app/logical/danbooru_maintenance.rb @@ -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! } diff --git a/app/logical/rate_limiter.rb b/app/logical/rate_limiter.rb new file mode 100644 index 000000000..56851f282 --- /dev/null +++ b/app/logical/rate_limiter.rb @@ -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 diff --git a/app/logical/sources/strategies.rb b/app/logical/sources/strategies.rb index a3686a37f..1215123af 100644 --- a/app/logical/sources/strategies.rb +++ b/app/logical/sources/strategies.rb @@ -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 diff --git a/app/logical/sources/strategies/base.rb b/app/logical/sources/strategies/base.rb index 11c6cfe96..a9cca0b32 100644 --- a/app/logical/sources/strategies/base.rb +++ b/app/logical/sources/strategies/base.rb @@ -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 diff --git a/app/logical/sources/strategies/mastodon.rb b/app/logical/sources/strategies/mastodon.rb index 3fef4810e..3b155e8bd 100644 --- a/app/logical/sources/strategies/mastodon.rb +++ b/app/logical/sources/strategies/mastodon.rb @@ -17,7 +17,7 @@ module Sources::Strategies class Mastodon < Base HOST = %r{\Ahttps?://(?:www\.)?(?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}/@(?\w+)}i ID_PROFILE = %r{#{HOST}/web/accounts/(?\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 diff --git a/app/logical/sources/strategies/nijie.rb b/app/logical/sources/strategies/nijie.rb index b81d1be08..bebec0720 100644 --- a/app/logical/sources/strategies/nijie.rb +++ b/app/logical/sources/strategies/nijie.rb @@ -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 diff --git a/app/logical/sources/strategies/twitter.rb b/app/logical/sources/strategies/twitter.rb index 593a049f6..55c09de30 100644 --- a/app/logical/sources/strategies/twitter.rb +++ b/app/logical/sources/strategies/twitter.rb @@ -30,6 +30,7 @@ module Sources::Strategies /(? "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| diff --git a/app/models/favorite_group.rb b/app/models/favorite_group.rb index 5a0c07190..f831172ea 100644 --- a/app/models/favorite_group.rb +++ b/app/models/favorite_group.rb @@ -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 diff --git a/app/models/rate_limit.rb b/app/models/rate_limit.rb new file mode 100644 index 000000000..dc9daee90 --- /dev/null +++ b/app/models/rate_limit.rb @@ -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 diff --git a/app/models/token_bucket.rb b/app/models/token_bucket.rb deleted file mode 100644 index 96ddf74ab..000000000 --- a/app/models/token_bucket.rb +++ /dev/null @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index d8ad0b507..03226fc26 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb index de81fa214..58bd96b3a 100644 --- a/app/models/user_upgrade.rb +++ b/app/models/user_upgrade.rb @@ -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! diff --git a/app/policies/rate_limit_policy.rb b/app/policies/rate_limit_policy.rb new file mode 100644 index 000000000..9970ac553 --- /dev/null +++ b/app/policies/rate_limit_policy.rb @@ -0,0 +1,5 @@ +class RateLimitPolicy < ApplicationPolicy + def index? + true + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index faf36bec7..fd6f93d06 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -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 diff --git a/app/views/artists/_form.html.erb b/app/views/artists/_form.html.erb index 8a72d56fb..8a1309627 100644 --- a/app/views/artists/_form.html.erb +++ b/app/views/artists/_form.html.erb @@ -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? %>
diff --git a/app/views/bulk_update_requests/_form.html.erb b/app/views/bulk_update_requests/_form.html.erb index 8e654913f..806689568 100644 --- a/app/views/bulk_update_requests/_form.html.erb +++ b/app/views/bulk_update_requests/_form.html.erb @@ -1,25 +1,26 @@ <%= edit_form_for(@bulk_update_request) do |f| %> -

- Request aliases or implications using the format shown below. An alias makes the first tag a - synonym for the second tag. An implication makes the first tag automatically add the second tag. - A rename replaces the first tag with the second tag without making it a permanent alias. - An update moves multiple tags and pools at once. -

+
+
+ Help: How to make a bulk update request -

- <% 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" %> +

- <%= f.input :title, label: "Forum Title", as: :string %> - <% end %> -

+

+ <% 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 %> +

+
+ + <%= f.input :script, label: "Request", as: :text %> <% if @bulk_update_request.new_record? %>
diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb index 01d91892c..84625bd7b 100644 --- a/app/views/comments/_form.html.erb +++ b/app/views/comments/_form.html.erb @@ -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 %> diff --git a/app/views/comments/update.js.erb b/app/views/comments/update.js.erb new file mode 100644 index 000000000..ea23b4974 --- /dev/null +++ b/app/views/comments/update.js.erb @@ -0,0 +1 @@ +$("#comment_<%= @comment.id %>").replaceWith("<%= j render_comment(@comment, current_user: CurrentUser.user) %>"); diff --git a/app/views/explore/posts/searches.html.erb b/app/views/explore/posts/searches.html.erb index a00154e61..ae4060451 100644 --- a/app/views/explore/posts/searches.html.erb +++ b/app/views/explore/posts/searches.html.erb @@ -22,7 +22,7 @@ -
+
  • <%= link_to "< Previous", searches_explore_posts_path(:date => 1.day.ago(@date).to_date), :class => "arrow" %>
  • <%= link_to "Next >", searches_explore_posts_path(:date => 1.day.since(@date).to_date), :class => "arrow" %>
  • diff --git a/app/views/explore/posts/viewed.html.erb b/app/views/explore/posts/viewed.html.erb index 407fe3691..96f6dc04f 100644 --- a/app/views/explore/posts/viewed.html.erb +++ b/app/views/explore/posts/viewed.html.erb @@ -11,7 +11,7 @@ <%= post_previews_html(@posts) %> -
    +
  • <%= link_to "< Previous", viewed_explore_posts_path(:date => 1.day.ago(@date).to_date) %>
  • <%= link_to "Next >", viewed_explore_posts_path(:date => 1.day.since(@date).to_date) %>
  • diff --git a/app/views/rate_limits/index.html.erb b/app/views/rate_limits/index.html.erb new file mode 100644 index 000000000..4ac3ffcc0 --- /dev/null +++ b/app/views/rate_limits/index.html.erb @@ -0,0 +1,21 @@ +
    +
    + <%= 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) %> +
    +
    diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb index b25b1f81e..67249de7c 100644 --- a/app/views/user_upgrades/new.html.erb +++ b/app/views/user_upgrades/new.html.erb @@ -119,7 +119,7 @@

    Frequently Asked Questions

    -
    +
    What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold? diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb index f72744ad8..4a7e2631e 100644 --- a/app/views/users/_statistics.html.erb +++ b/app/views/users/_statistics.html.erb @@ -62,14 +62,12 @@ <% end %> - - Inviter - <% if user.inviter %> + <% if user.inviter %> + + Promoter <%= link_to_user user.inviter %> <%= link_to "»", users_path(search: { inviter: { name: user.inviter.name }}) %> - <% else %> - None - <% end %> - + + <% end %> Level @@ -265,14 +263,6 @@ (<%= link_to_wiki "help", "help:api" %>) - - - API Limits - - <%= CurrentUser.user.remaining_api_limit %> - / <%= CurrentUser.user.api_burst_limit %> (may not be up to date) - - <% end %> diff --git a/config/routes.rb b/config/routes.rb index 61010d9ed..82df33b1c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] diff --git a/db/migrate/20170106012138_create_token_buckets.rb b/db/migrate/20170106012138_create_token_buckets.rb index a41a7067a..0b442ae6f 100644 --- a/db/migrate/20170106012138_create_token_buckets.rb +++ b/db/migrate/20170106012138_create_token_buckets.rb @@ -5,6 +5,6 @@ class CreateTokenBuckets < ActiveRecord::Migration[4.2] end def down - raise NotImplementedError + drop_table :token_buckets end end diff --git a/db/migrate/20210303195217_replace_token_buckets_with_rate_limits.rb b/db/migrate/20210303195217_replace_token_buckets_with_rate_limits.rb new file mode 100644 index 000000000..7002d5a75 --- /dev/null +++ b/db/migrate/20210303195217_replace_token_buckets_with_rate_limits.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index e7549407b..13c02f9c8 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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'); diff --git a/public/images/amazon-logo.png b/public/images/amazon-logo.png new file mode 100644 index 000000000..3ec9a9b32 Binary files /dev/null and b/public/images/amazon-logo.png differ diff --git a/public/images/ask-fm-logo.png b/public/images/ask-fm-logo.png new file mode 100644 index 000000000..aba849eb3 Binary files /dev/null and b/public/images/ask-fm-logo.png differ diff --git a/public/images/doujinshi-org-logo.png b/public/images/doujinshi-org-logo.png new file mode 100644 index 000000000..fa02930ea Binary files /dev/null and b/public/images/doujinshi-org-logo.png differ diff --git a/public/images/erogamescape-logo.png b/public/images/erogamescape-logo.png new file mode 100644 index 000000000..899c27a42 Binary files /dev/null and b/public/images/erogamescape-logo.png differ diff --git a/public/images/hentai-foundry-logo.png b/public/images/hentai-foundry-logo.png new file mode 100644 index 000000000..416e4017e Binary files /dev/null and b/public/images/hentai-foundry-logo.png differ diff --git a/public/images/ko-fi-logo.png b/public/images/ko-fi-logo.png new file mode 100644 index 000000000..53f8b6d83 Binary files /dev/null and b/public/images/ko-fi-logo.png differ diff --git a/public/images/livedoor-logo.png b/public/images/livedoor-logo.png new file mode 100644 index 000000000..387b050a9 Binary files /dev/null and b/public/images/livedoor-logo.png differ diff --git a/public/images/mangaupdates-logo.png b/public/images/mangaupdates-logo.png new file mode 100644 index 000000000..909203043 Binary files /dev/null and b/public/images/mangaupdates-logo.png differ diff --git a/public/images/mihuashi-logo.png b/public/images/mihuashi-logo.png new file mode 100644 index 000000000..e79939fdc Binary files /dev/null and b/public/images/mihuashi-logo.png differ diff --git a/public/images/mixi-jp-logo.png b/public/images/mixi-jp-logo.png new file mode 100644 index 000000000..2c0ef8c63 Binary files /dev/null and b/public/images/mixi-jp-logo.png differ diff --git a/public/images/piapro-jp-logo.png b/public/images/piapro-jp-logo.png new file mode 100644 index 000000000..a68069b8f Binary files /dev/null and b/public/images/piapro-jp-logo.png differ diff --git a/public/images/picarto-logo.png b/public/images/picarto-logo.png new file mode 100644 index 000000000..faa5ac18e Binary files /dev/null and b/public/images/picarto-logo.png differ diff --git a/public/images/sakura-ne-jp-logo.png b/public/images/sakura-ne-jp-logo.png new file mode 100644 index 000000000..579c3b8cb Binary files /dev/null and b/public/images/sakura-ne-jp-logo.png differ diff --git a/public/images/stickam-logo.png b/public/images/stickam-logo.png new file mode 100644 index 000000000..22393e890 Binary files /dev/null and b/public/images/stickam-logo.png differ diff --git a/public/images/twitch-logo.png b/public/images/twitch-logo.png new file mode 100644 index 000000000..7bb99fa75 Binary files /dev/null and b/public/images/twitch-logo.png differ diff --git a/public/images/wikipedia-logo.png b/public/images/wikipedia-logo.png new file mode 100644 index 000000000..b7ded5f62 Binary files /dev/null and b/public/images/wikipedia-logo.png differ diff --git a/script/fixes/076_clear_inviters.rb b/script/fixes/076_clear_inviters.rb new file mode 100755 index 000000000..3a23eef44 --- /dev/null +++ b/script/fixes/076_clear_inviters.rb @@ -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 diff --git a/test/components/post_navbar_component_test.rb b/test/components/post_navbar_component_test.rb index 4d3459018..a27024ac0 100644 --- a/test/components/post_navbar_component_test.rb +++ b/test/components/post_navbar_component_test.rb @@ -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: 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: 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 diff --git a/test/factories/rate_limit.rb b/test/factories/rate_limit.rb new file mode 100644 index 000000000..cfb37ff3f --- /dev/null +++ b/test/factories/rate_limit.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory(:rate_limit) do + limited { false } + points { 0 } + action { "test" } + key { "1234" } + end +end diff --git a/test/functional/application_controller_test.rb b/test/functional/application_controller_test.rb index bd96c4a24..0783431f6 100644 --- a/test/functional/application_controller_test.rb +++ b/test/functional/application_controller_test.rb @@ -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" } } diff --git a/test/functional/bans_controller_test.rb b/test/functional/bans_controller_test.rb index f2168b420..26ef20f40 100644 --- a/test/functional/bans_controller_test.rb +++ b/test/functional/bans_controller_test.rb @@ -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 diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb index 7e1ae2a45..ca1c7ca28 100644 --- a/test/functional/comments_controller_test.rb +++ b/test/functional/comments_controller_test.rb @@ -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 diff --git a/test/functional/rate_limits_controller_test.rb b/test/functional/rate_limits_controller_test.rb new file mode 100644 index 000000000..499a018cc --- /dev/null +++ b/test/functional/rate_limits_controller_test.rb @@ -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 diff --git a/test/functional/sessions_controller_test.rb b/test/functional/sessions_controller_test.rb index 29b3dbff6..eb9ab6fb6 100644 --- a/test/functional/sessions_controller_test.rb +++ b/test/functional/sessions_controller_test.rb @@ -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 diff --git a/test/unit/api_key_test.rb b/test/unit/api_key_test.rb index 26b68a67f..760035ae2 100644 --- a/test/unit/api_key_test.rb +++ b/test/unit/api_key_test.rb @@ -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 diff --git a/test/unit/ip_geolocation_test.rb b/test/unit/ip_geolocation_test.rb index ae4e1932d..7965e1bdc 100644 --- a/test/unit/ip_geolocation_test.rb +++ b/test/unit/ip_geolocation_test.rb @@ -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)) diff --git a/test/unit/rate_limit_test.rb b/test/unit/rate_limit_test.rb new file mode 100644 index 000000000..6a8a47725 --- /dev/null +++ b/test/unit/rate_limit_test.rb @@ -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 diff --git a/test/unit/sources/mastodon_test.rb b/test/unit/sources/mastodon_test.rb index 59d31d0f7..46ed5759d 100644 --- a/test/unit/sources/mastodon_test.rb +++ b/test/unit/sources/mastodon_test.rb @@ -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 diff --git a/test/unit/sources/nijie_test.rb b/test/unit/sources/nijie_test.rb index c90357278..ba10b6b2f 100644 --- a/test/unit/sources/nijie_test.rb +++ b/test/unit/sources/nijie_test.rb @@ -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" diff --git a/test/unit/token_bucket_test.rb b/test/unit/token_bucket_test.rb deleted file mode 100644 index 3c96b371a..000000000 --- a/test/unit/token_bucket_test.rb +++ /dev/null @@ -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 diff --git a/yarn.lock b/yarn.lock index 07de41fdf..a50c2f32d 100644 --- a/yarn.lock +++ b/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