Merge branch 'master' into skeb
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
class ApplicationComponent < ViewComponent::Base
|
||||
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :external_link_to, :tag_class, to: :helpers
|
||||
delegate :edit_icon, :delete_icon, :undelete_icon, :flag_icon, :upvote_icon, :downvote_icon, :link_icon, to: :helpers
|
||||
delegate :edit_icon, :delete_icon, :undelete_icon, :flag_icon, :upvote_icon,
|
||||
:downvote_icon, :link_icon, :sticky_icon, :unsticky_icon, to: :helpers
|
||||
|
||||
def policy(subject)
|
||||
Pundit.policy!(current_user, subject)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
class CommentComponent < ApplicationComponent
|
||||
attr_reader :comment, :context, :dtext_data, :current_user
|
||||
delegate :link_to_user, :time_ago_in_words_tagged, :format_text, :edit_icon, :delete_icon, :undelete_icon, :flag_icon, :upvote_icon, :downvote_icon, :link_icon, to: :helpers
|
||||
|
||||
def initialize(comment:, current_user:, context: nil, dtext_data: nil)
|
||||
@comment = comment
|
||||
|
||||
@@ -99,17 +99,31 @@
|
||||
|
||||
<%= menu.item do %>
|
||||
<% if comment.is_deleted? %>
|
||||
<%= link_to undelete_comment_path(comment.id), method: :post, remote: true do %>
|
||||
<%= link_to comment_path(comment.id), "data-params": "comment[is_deleted]=false", method: :put, remote: true do %>
|
||||
<%= undelete_icon %> Undelete
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to comment_path(comment.id), "data-confirm": "Are you sure you want to delete this comment?", method: :delete, remote: true do %>
|
||||
<%= link_to comment_path(comment.id), "data-params": "comment[is_deleted]=true", "data-confirm": "Are you sure you want to delete this comment?", method: :put, remote: true do %>
|
||||
<%= delete_icon %> Delete
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if policy(comment).can_sticky_comment? %>
|
||||
<%= menu.item do %>
|
||||
<% if comment.is_sticky? %>
|
||||
<%= link_to comment_path(comment.id), "data-params": "comment[is_sticky]=false", method: :put, remote: true do %>
|
||||
<%= unsticky_icon %> Unsticky
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to comment_path(comment.id), "data-params": "comment[is_sticky]=true", method: :put, remote: true do %>
|
||||
<%= sticky_icon %> Sticky
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if policy(comment).reportable? %>
|
||||
<%= menu.item do %>
|
||||
<%= link_to new_moderation_report_path(moderation_report: { model_type: "Comment", model_id: comment.id }), remote: true do %>
|
||||
|
||||
@@ -30,8 +30,9 @@ div.popup-menu {
|
||||
display: block;
|
||||
padding: 0.125em 2em 0.125em 0;
|
||||
|
||||
i.icon {
|
||||
width: 1.5em;
|
||||
.icon {
|
||||
width: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class PostNavbarComponent < ApplicationComponent
|
||||
extend Memoist
|
||||
|
||||
attr_reader :post, :current_user, :search
|
||||
|
||||
def initialize(post:, current_user:, search: nil)
|
||||
@@ -18,7 +20,9 @@ class PostNavbarComponent < ApplicationComponent
|
||||
end
|
||||
|
||||
def favgroups
|
||||
@favgroups ||= current_user.favorite_groups.for_post(post.id).sort_by do |favgroup|
|
||||
favgroups = FavoriteGroup.visible(current_user).for_post(post.id)
|
||||
favgroups = favgroups.where(creator: current_user).or(favgroups.where(id: favgroup_id))
|
||||
favgroups.sort_by do |favgroup|
|
||||
[favgroup.id == favgroup_id ? 0 : 1, favgroup.name]
|
||||
end
|
||||
end
|
||||
@@ -38,4 +42,6 @@ class PostNavbarComponent < ApplicationComponent
|
||||
def query
|
||||
@query ||= PostQueryBuilder.new(search)
|
||||
end
|
||||
|
||||
memoize :favgroups
|
||||
end
|
||||
|
||||
@@ -2,15 +2,13 @@ class ApplicationController < ActionController::Base
|
||||
include Pundit
|
||||
helper_method :search_params
|
||||
|
||||
class ApiLimitError < StandardError; end
|
||||
|
||||
self.responder = ApplicationResponder
|
||||
|
||||
skip_forgery_protection if: -> { SessionLoader.new(request).has_api_authentication? }
|
||||
before_action :reset_current_user
|
||||
before_action :set_current_user
|
||||
before_action :normalize_search
|
||||
before_action :api_check
|
||||
before_action :check_rate_limit
|
||||
before_action :ip_ban_check
|
||||
before_action :set_variant
|
||||
before_action :add_headers
|
||||
@@ -71,20 +69,13 @@ class ApplicationController < ActionController::Base
|
||||
response.headers["X-Git-Hash"] = Rails.application.config.x.git_hash
|
||||
end
|
||||
|
||||
def api_check
|
||||
return if CurrentUser.is_anonymous? || request.get? || request.head?
|
||||
def check_rate_limit
|
||||
return if request.get? || request.head?
|
||||
|
||||
if CurrentUser.user.token_bucket.nil?
|
||||
TokenBucket.create_default(CurrentUser.user)
|
||||
CurrentUser.user.reload
|
||||
end
|
||||
rate_limiter = RateLimiter.for_action(controller_name, action_name, CurrentUser.user, CurrentUser.ip_addr)
|
||||
headers["X-Rate-Limit"] = rate_limiter.to_json
|
||||
|
||||
throttled = CurrentUser.user.token_bucket.throttled?
|
||||
headers["X-Api-Limit"] = CurrentUser.user.token_bucket.token_count.to_s
|
||||
|
||||
if throttled
|
||||
raise ApiLimitError, "too many requests"
|
||||
end
|
||||
rate_limiter.limit!
|
||||
end
|
||||
|
||||
def rescue_exception(exception)
|
||||
@@ -113,7 +104,7 @@ class ApplicationController < ActionController::Base
|
||||
render_error_page(410, exception, template: "static/pagination_error", message: "You cannot go beyond page #{CurrentUser.user.page_limit}.")
|
||||
when Post::SearchError
|
||||
render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.")
|
||||
when ApiLimitError
|
||||
when RateLimiter::RateLimitError
|
||||
render_error_page(429, exception)
|
||||
when NotImplementedError
|
||||
render_error_page(501, exception, message: "This feature isn't available: #{exception.message}")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class CommentVotesController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :js, :json, :xml, :html
|
||||
|
||||
def index
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class CommentsController < ApplicationController
|
||||
respond_to :html, :xml, :json, :atom
|
||||
respond_to :js, only: [:new, :destroy, :undelete]
|
||||
skip_before_action :api_check
|
||||
respond_to :js, only: [:new, :update, :destroy, :undelete]
|
||||
|
||||
def index
|
||||
params[:group_by] ||= "comment" if params[:search].present?
|
||||
@@ -32,7 +31,7 @@ class CommentsController < ApplicationController
|
||||
def update
|
||||
@comment = authorize Comment.find(params[:id])
|
||||
@comment.update(permitted_attributes(@comment))
|
||||
respond_with(@comment, :location => post_path(@comment.post_id))
|
||||
respond_with(@comment)
|
||||
end
|
||||
|
||||
def create
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class FavoritesController < ApplicationController
|
||||
respond_to :html, :xml, :json, :js
|
||||
skip_before_action :api_check
|
||||
rescue_with Favorite::Error, status: 422
|
||||
|
||||
def index
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class ForumPostsController < ApplicationController
|
||||
respond_to :html, :xml, :json, :js
|
||||
skip_before_action :api_check
|
||||
|
||||
def new
|
||||
@forum_post = authorize ForumPost.new_reply(params)
|
||||
|
||||
@@ -2,7 +2,6 @@ class ForumTopicsController < ApplicationController
|
||||
respond_to :html, :xml, :json
|
||||
respond_to :atom, only: [:index, :show]
|
||||
before_action :normalize_search, :only => :index
|
||||
skip_before_action :api_check
|
||||
|
||||
def new
|
||||
@forum_topic = authorize ForumTopic.new
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
module Moderator
|
||||
module Post
|
||||
class PostsController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :html, :json, :xml, :js
|
||||
|
||||
def confirm_move_favorites
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class PostDisapprovalsController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :js, :html, :json, :xml
|
||||
|
||||
def create
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class PostVotesController < ApplicationController
|
||||
skip_before_action :api_check
|
||||
respond_to :js, :json, :xml, :html
|
||||
|
||||
def index
|
||||
|
||||
8
app/controllers/rate_limits_controller.rb
Normal file
8
app/controllers/rate_limits_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class RateLimitsController < ApplicationController
|
||||
respond_to :html, :json, :xml
|
||||
|
||||
def index
|
||||
@rate_limits = authorize RateLimit.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||
respond_with(@rate_limits)
|
||||
end
|
||||
end
|
||||
@@ -2,5 +2,6 @@ class RobotsController < ApplicationController
|
||||
respond_to :text
|
||||
|
||||
def index
|
||||
expires_in 1.hour, public: true unless response.cache_control.present?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
class UsersController < ApplicationController
|
||||
respond_to :html, :xml, :json
|
||||
skip_before_action :api_check
|
||||
|
||||
def new
|
||||
@user = authorize User.new
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
module BulkUpdateRequestsHelper
|
||||
def bur_script_example
|
||||
<<~BUR
|
||||
create alias bunny -> rabbit
|
||||
remove alias bunny -> rabbit
|
||||
|
||||
create implication bunny -> animal
|
||||
remove implication bunny -> animal
|
||||
|
||||
rename bunny -> rabbit
|
||||
|
||||
update bunny_focus -> animal_focus bunny
|
||||
|
||||
nuke bunny
|
||||
|
||||
category touhou -> copyright
|
||||
BUR
|
||||
end
|
||||
end
|
||||
@@ -7,8 +7,8 @@ module ComponentsHelper
|
||||
render PostPreviewComponent.with_collection(posts, **options)
|
||||
end
|
||||
|
||||
def render_comment(comment, **options)
|
||||
render CommentComponent.new(comment: comment, **options)
|
||||
def render_comment(comment, current_user:, **options)
|
||||
render CommentComponent.new(comment: comment, current_user: current_user, **options)
|
||||
end
|
||||
|
||||
def render_comment_section(post, **options)
|
||||
|
||||
@@ -30,6 +30,10 @@ module IconHelper
|
||||
icon_tag("fas fa-thumbtack", **options)
|
||||
end
|
||||
|
||||
def unsticky_icon(**options)
|
||||
svg_icon_tag("unsticky-icon", "M306.5 186.6l-5.7-42.6H328c13.2 0 24-10.8 24-24V24c0-13.2-10.8-24-24-24H56C42.8 0 32 10.8 32 24v96c0 13.2 10.8 24 24 24h27.2l-5.7 42.6C29.6 219.4 0 270.7 0 328c0 13.2 10.8 24 24 24h144v104c0 .9.1 1.7.4 2.5l16 48c2.4 7.3 12.8 7.3 15.2 0l16-48c.3-.8.4-1.7.4-2.5V352h144c13.2 0 24-10.8 24-24 0-57.3-29.6-108.6-77.5-141.4zM50.5 304c8.3-38.5 35.6-70 71.5-87.8L138 96H80V48h224v48h-58l16 120.2c35.8 17.8 63.2 49.4 71.5 87.8z", **options)
|
||||
end
|
||||
|
||||
def lock_icon(**options)
|
||||
icon_tag("fas fa-lock", **options)
|
||||
end
|
||||
@@ -39,7 +43,7 @@ module IconHelper
|
||||
end
|
||||
|
||||
def undelete_icon(**options)
|
||||
icon_tag("fas fa-trash-restore_alt", **options)
|
||||
icon_tag("fas fa-trash-restore-alt", **options)
|
||||
end
|
||||
|
||||
def private_icon(**options)
|
||||
@@ -172,18 +176,26 @@ module IconHelper
|
||||
|
||||
def external_site_icon(site_name, **options)
|
||||
case site_name
|
||||
when "Amazon"
|
||||
image_icon_tag("amazon-logo.png", **options)
|
||||
when "ArtStation"
|
||||
image_icon_tag("artstation-logo.png", **options)
|
||||
when "Ask.fm"
|
||||
image_icon_tag("ask-fm-logo.png", **options)
|
||||
when "BCY"
|
||||
image_icon_tag("bcy-logo.png", **options)
|
||||
when "Booth.pm"
|
||||
image_icon_tag("booth-pm-logo.png", **options)
|
||||
when "Circle.ms"
|
||||
image_icon_tag("circle-ms-logo.png", **options)
|
||||
when "DLSite"
|
||||
image_icon_tag("dlsite-logo.png", **options)
|
||||
when "Deviant Art"
|
||||
image_icon_tag("deviantart-logo.png", **options)
|
||||
when "DLSite"
|
||||
image_icon_tag("dlsite-logo.png", **options)
|
||||
when "Doujinshi.org"
|
||||
image_icon_tag("doujinshi-org-logo.png", **options)
|
||||
when "Erogamescape"
|
||||
image_icon_tag("erogamescape-logo.png", **options)
|
||||
when "Facebook"
|
||||
image_icon_tag("facebook-logo.png", **options)
|
||||
when "Fantia"
|
||||
@@ -192,12 +204,24 @@ module IconHelper
|
||||
image_icon_tag("fc2-logo.png", **options)
|
||||
when "Gumroad"
|
||||
image_icon_tag("gumroad-logo.png", **options)
|
||||
when "Hentai Foundry"
|
||||
image_icon_tag("hentai-foundry-logo.png", **options)
|
||||
when "Instagram"
|
||||
image_icon_tag("instagram-logo.png", **options)
|
||||
when "Ko-fi"
|
||||
image_icon_tag("ko-fi-logo.png", **options)
|
||||
when "Livedoor"
|
||||
image_icon_tag("livedoor-logo.png", **options)
|
||||
when "Lofter"
|
||||
image_icon_tag("lofter-logo.png", **options)
|
||||
when "Mangaupdates"
|
||||
image_icon_tag("mangaupdates-logo.png", **options)
|
||||
when "Melonbooks"
|
||||
image_icon_tag("melonbooks-logo.png", **options)
|
||||
when "Mihuashi"
|
||||
image_icon_tag("mihuashi-logo.png", **options)
|
||||
when "Mixi.jp"
|
||||
image_icon_tag("mixi-jp-logo.png", **options)
|
||||
when "Nico Seiga"
|
||||
image_icon_tag("nicoseiga-logo.png", **options)
|
||||
when "Nijie"
|
||||
@@ -206,6 +230,10 @@ module IconHelper
|
||||
image_icon_tag("patreon-logo.png", **options)
|
||||
when "pawoo.net"
|
||||
image_icon_tag("pawoo-logo.png", **options)
|
||||
when "Piapro.jp"
|
||||
image_icon_tag("piapro-jp-logo.png", **options)
|
||||
when "Picarto"
|
||||
image_icon_tag("picarto-logo.png", **options)
|
||||
when "Pixiv"
|
||||
image_icon_tag("pixiv-logo.png", **options)
|
||||
when "Pixiv Fanbox"
|
||||
@@ -214,6 +242,10 @@ module IconHelper
|
||||
image_icon_tag("pixiv-sketch-logo.png", **options)
|
||||
when "Privatter"
|
||||
image_icon_tag("privatter-logo.png", **options)
|
||||
when "Sakura.ne.jp"
|
||||
image_icon_tag("sakura-ne-jp-logo.png", **options)
|
||||
when "Stickam"
|
||||
image_icon_tag("stickam-logo.png", **options)
|
||||
when "Skeb"
|
||||
image_icon_tag("skeb-logo.png", **options)
|
||||
when "Tinami"
|
||||
@@ -224,8 +256,12 @@ module IconHelper
|
||||
image_icon_tag("twitter-logo.png", **options)
|
||||
when "Toranoana"
|
||||
image_icon_tag("toranoana-logo.png", **options)
|
||||
when "Twitch"
|
||||
image_icon_tag("twitch-logo.png", **options)
|
||||
when "Weibo"
|
||||
image_icon_tag("weibo-logo.png", **options)
|
||||
when "Wikipedia"
|
||||
image_icon_tag("wikipedia-logo.png", **options)
|
||||
when "Youtube"
|
||||
image_icon_tag("youtube-logo.png", **options)
|
||||
else
|
||||
|
||||
@@ -112,13 +112,10 @@ table tfoot {
|
||||
}
|
||||
|
||||
details {
|
||||
border-bottom: 1px solid var(--details-border-color);
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ html {
|
||||
|
||||
--login-link-color: var(--red-5);
|
||||
--footer-border-color: var(--grey-1);
|
||||
--details-border-color: var(--grey-2);
|
||||
--divider-border-color: var(--grey-2);
|
||||
|
||||
--jquery-ui-widget-content-background: var(--body-background-color);
|
||||
--jquery-ui-widget-content-text-color: var(--text-color);
|
||||
@@ -446,7 +446,7 @@ body[data-current-user-theme="dark"] {
|
||||
|
||||
--login-link-color: var(--red-4);
|
||||
--footer-border-color: var(--grey-7);
|
||||
--details-border-color: var(--grey-7);
|
||||
--divider-border-color: var(--grey-7);
|
||||
|
||||
--jquery-ui-widget-content-text-color: var(--text-color);
|
||||
--jquery-ui-widget-content-background: var(--grey-8);
|
||||
|
||||
@@ -131,6 +131,14 @@ div.prose {
|
||||
.spoiler {
|
||||
background: var(--dtext-spoiler-background-color);
|
||||
}
|
||||
|
||||
details {
|
||||
margin-bottom: 1em;
|
||||
|
||||
summary {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// avoid empty gaps beneath dtext blocks in table rows.
|
||||
|
||||
@@ -42,6 +42,8 @@ $spacer: 0.25rem; /* 4px */
|
||||
.space-x-4 > * + * { margin-left: 4 * $spacer; }
|
||||
.space-y-4 > * + * { margin-top: 4 * $spacer; }
|
||||
|
||||
.divide-y-1 > * + * { border-top: 1px solid var(--divider-border-color); }
|
||||
|
||||
.align-top { vertical-align: top; }
|
||||
|
||||
.flex-auto { flex: 1 1 auto; }
|
||||
|
||||
@@ -15,11 +15,6 @@ div.related-tags {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
/* Hide the related tag checkbox unless it's checked or hovered. */
|
||||
input[type="checkbox"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
li.selected a {
|
||||
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-text-stroke */
|
||||
/* https://caniuse.com/text-stroke */
|
||||
@@ -27,7 +22,14 @@ div.related-tags {
|
||||
text-stroke: 0.5px;
|
||||
}
|
||||
|
||||
li.selected, li:hover {
|
||||
input[type="checkbox"] { visibility: visible; }
|
||||
/* On computers with a mouse, hide the related tag checkbox unless it's checked or hovered. */
|
||||
@media (hover: hover) {
|
||||
input[type="checkbox"] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
li.selected, li:hover {
|
||||
input[type="checkbox"] { visibility: visible; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
div#c-user-upgrades {
|
||||
div#a-new {
|
||||
summary {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
table#feature-comparison {
|
||||
colgroup#basic {
|
||||
background-color: var(--user-upgrade-basic-background-color);
|
||||
|
||||
@@ -5,13 +5,13 @@ module DanbooruMaintenance
|
||||
safely { Upload.prune! }
|
||||
safely { PostPruner.prune! }
|
||||
safely { PostAppealForumUpdater.update_forum! }
|
||||
safely { RateLimit.prune! }
|
||||
safely { regenerate_post_counts! }
|
||||
end
|
||||
|
||||
def daily
|
||||
safely { Delayed::Job.where('created_at < ?', 45.days.ago).delete_all }
|
||||
safely { PostDisapproval.prune! }
|
||||
safely { TokenBucket.prune! }
|
||||
safely { BulkUpdateRequestPruner.warn_old }
|
||||
safely { BulkUpdateRequestPruner.reject_expired }
|
||||
safely { Ban.prune! }
|
||||
|
||||
52
app/logical/rate_limiter.rb
Normal file
52
app/logical/rate_limiter.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
class RateLimiter
|
||||
class RateLimitError < StandardError; end
|
||||
|
||||
attr_reader :action, :keys, :cost, :rate, :burst
|
||||
|
||||
def initialize(action, keys = ["*"], cost: 1, rate: 1, burst: 1)
|
||||
@action = action
|
||||
@keys = keys
|
||||
@cost = cost
|
||||
@rate = rate
|
||||
@burst = burst
|
||||
end
|
||||
|
||||
def self.for_action(controller_name, action_name, user, ip_addr)
|
||||
action = "#{controller_name}:#{action_name}"
|
||||
keys = [(user.cache_key unless user.is_anonymous?), "ip/#{ip_addr.to_s}"].compact
|
||||
|
||||
case action
|
||||
when "users:create"
|
||||
rate, burst = 1.0/5.minutes, 10
|
||||
when "emails:update", "sessions:create", "moderation_reports:create"
|
||||
rate, burst = 1.0/1.minute, 10
|
||||
when "dmails:create", "comments:create", "forum_posts:create", "forum_topics:create"
|
||||
rate, burst = 1.0/1.minute, 50
|
||||
when "comment_votes:create", "comment_votes:destroy", "post_votes:create",
|
||||
"post_votes:destroy", "favorites:create", "favorites:destroy", "post_disapprovals:create"
|
||||
rate, burst = 1.0/1.second, 200
|
||||
else
|
||||
rate = user.api_regen_multiplier
|
||||
burst = 200
|
||||
end
|
||||
|
||||
RateLimiter.new(action, keys, rate: rate, burst: burst)
|
||||
end
|
||||
|
||||
def limit!
|
||||
raise RateLimitError if limited?
|
||||
end
|
||||
|
||||
def limited?
|
||||
rate_limits.any?(&:limited?)
|
||||
end
|
||||
|
||||
def as_json(options = {})
|
||||
hash = rate_limits.map { |limit| [limit.key, limit.points] }.to_h
|
||||
super(options).except("keys", "rate_limits").merge(limits: hash)
|
||||
end
|
||||
|
||||
def rate_limits
|
||||
@rate_limits ||= RateLimit.create_or_update!(action: action, keys: keys, cost: cost, rate: rate, burst: burst)
|
||||
end
|
||||
end
|
||||
@@ -3,17 +3,17 @@ module Sources
|
||||
def self.all
|
||||
[
|
||||
Strategies::Pixiv,
|
||||
Strategies::Fanbox,
|
||||
Strategies::NicoSeiga,
|
||||
Strategies::Twitter,
|
||||
Strategies::Tumblr,
|
||||
Strategies::NicoSeiga,
|
||||
Strategies::Stash, # must come before DeviantArt
|
||||
Strategies::DeviantArt,
|
||||
Strategies::Tumblr,
|
||||
Strategies::ArtStation,
|
||||
Strategies::Nijie,
|
||||
Strategies::Mastodon,
|
||||
Strategies::Moebooru,
|
||||
Strategies::Nijie,
|
||||
Strategies::ArtStation,
|
||||
Strategies::HentaiFoundry,
|
||||
Strategies::Fanbox,
|
||||
Strategies::Mastodon,
|
||||
Strategies::Weibo,
|
||||
Strategies::Newgrounds,
|
||||
Strategies::Skeb
|
||||
@@ -21,7 +21,7 @@ module Sources
|
||||
end
|
||||
|
||||
def self.find(url, referer = nil, default: Strategies::Null)
|
||||
strategy = all.map { |strategy| strategy.new(url, referer) }.detect(&:match?)
|
||||
strategy = all.lazy.map { |s| s.new(url, referer) }.detect(&:match?)
|
||||
strategy || default&.new(url, referer)
|
||||
end
|
||||
|
||||
|
||||
@@ -64,6 +64,10 @@ module Sources
|
||||
|
||||
# XXX should go in dedicated strategies.
|
||||
case host
|
||||
when /amazon\.(com|jp|co\.jp)\z/i
|
||||
"Amazon"
|
||||
when /ask\.fm\z/i
|
||||
"Ask.fm"
|
||||
when /bcy\.net\z/i
|
||||
"BCY"
|
||||
when /booth\.pm\z/i
|
||||
@@ -72,6 +76,10 @@ module Sources
|
||||
"Circle.ms"
|
||||
when /dlsite\.(com|net)\z/i
|
||||
"DLSite"
|
||||
when /doujinshi\.mugimugi\.org\z/i, /doujinshi\.org\z/i
|
||||
"Doujinshi.org"
|
||||
when /erogamescape\.dyndns\.org\z/i
|
||||
"Erogamescape"
|
||||
when /facebook\.com\z/i
|
||||
"Facebook"
|
||||
when /fantia\.jp\z/i
|
||||
@@ -82,18 +90,42 @@ module Sources
|
||||
"Gumroad"
|
||||
when /instagram\.com\z/i
|
||||
"Instagram"
|
||||
when /ko-fi\.com\z/i
|
||||
"Ko-fi"
|
||||
when /livedoor\.(jp|com)\z/i
|
||||
"Livedoor"
|
||||
when /lofter\.com\z/i
|
||||
"Lofter"
|
||||
when /mangaupdates\.com\z/i
|
||||
"Mangaupdates"
|
||||
when /melonbooks\.co\.jp\z/i
|
||||
"Melonbooks"
|
||||
when /mihuashi\.com\z/i
|
||||
"Mihuashi"
|
||||
when /mixi\.jp\z/i
|
||||
"Mixi.jp"
|
||||
when /patreon\.com\z/i
|
||||
"Patreon"
|
||||
when /piapro\.jp\z/i
|
||||
"Piapro.jp"
|
||||
when /picarto\.tv\z/i
|
||||
"Picarto"
|
||||
when /privatter\.net\z/i
|
||||
"Privatter"
|
||||
when /sakura\.ne\.jp\z/i
|
||||
"Sakura.ne.jp"
|
||||
when /stickam\.jp\z/i
|
||||
"Stickam"
|
||||
when /skeb\.jp\z/i
|
||||
"Skeb"
|
||||
when /tinami\.com\z/i
|
||||
"Tinami"
|
||||
when /toranoana\.(jp|shop)\z/i
|
||||
"Toranoana"
|
||||
when /twitch\.tv\z/i
|
||||
"Twitch"
|
||||
when /wikipedia\.org\z/i
|
||||
"Wikipedia"
|
||||
when /youtube\.com\z/i
|
||||
"Youtube"
|
||||
else
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
module Sources::Strategies
|
||||
class Mastodon < Base
|
||||
HOST = %r{\Ahttps?://(?:www\.)?(?<domain>pawoo\.net|baraag\.net)}i
|
||||
IMAGE = %r{\Ahttps?://(?:img\.pawoo\.net|baraag\.net)/media_attachments/files/(\d+/\d+/\d+)}
|
||||
IMAGE = %r{\Ahttps?://(?:img\.pawoo\.net|baraag\.net(?:/system(?:/cache)?)?)/media_attachments/files/((?:\d+/)+\d+)}
|
||||
NAMED_PROFILE = %r{#{HOST}/@(?<artist_name>\w+)}i
|
||||
ID_PROFILE = %r{#{HOST}/web/accounts/(?<account_id>\d+)}
|
||||
|
||||
@@ -35,6 +35,7 @@ module Sources::Strategies
|
||||
def file_host
|
||||
case site_name
|
||||
when "pawoo.net" then "img.pawoo.net"
|
||||
when "baraag.net" then "baraag.net/system"
|
||||
else site_name
|
||||
end
|
||||
end
|
||||
@@ -85,7 +86,7 @@ module Sources::Strategies
|
||||
end
|
||||
|
||||
def artist_name_from_url
|
||||
url[NAMED_PROFILE, :artist_name]
|
||||
urls.map { |url| url[NAMED_PROFILE, :artist_name] }.compact.first
|
||||
end
|
||||
|
||||
def other_names
|
||||
@@ -93,7 +94,7 @@ module Sources::Strategies
|
||||
end
|
||||
|
||||
def account_id
|
||||
url[ID_PROFILE, :account_id] || api_response.account_id
|
||||
urls.map { |url| url[ID_PROFILE, :account_id] }.compact.first || api_response.account_id
|
||||
end
|
||||
|
||||
def status_id_from_url
|
||||
|
||||
@@ -212,34 +212,57 @@ module Sources
|
||||
return nil if page_url.blank? || client.blank?
|
||||
|
||||
response = client.cache(1.minute).get(page_url)
|
||||
return nil unless response.status == 200
|
||||
|
||||
response&.parse
|
||||
if response.status != 200 || response.parse.search("#login_illust").present?
|
||||
clear_cached_session_cookie!
|
||||
else
|
||||
response.parse
|
||||
end
|
||||
end
|
||||
memoize :page
|
||||
|
||||
def client
|
||||
nijie = http.timeout(60).use(retriable: { max_retries: 20 })
|
||||
|
||||
cookie = Cache.get("nijie-session-cookie", 1.week) do
|
||||
login_page = nijie.get("https://nijie.info/login.php").parse
|
||||
form = {
|
||||
email: Danbooru.config.nijie_login,
|
||||
password: Danbooru.config.nijie_password,
|
||||
url: login_page.at("input[name='url']")["value"],
|
||||
save: "on",
|
||||
ticket: ""
|
||||
}
|
||||
response = nijie.post("https://nijie.info/login_int.php", form: form)
|
||||
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})" if response.status != 200
|
||||
return nil unless response.status == 200
|
||||
|
||||
response.cookies.select { |c| c.name == "NIJIEIJIEID" }.compact.first
|
||||
end
|
||||
|
||||
nijie.cookies(NIJIEIJIEID: cookie, R18: 1)
|
||||
return nil if cached_session_cookie.nil?
|
||||
http.cookies(NIJIEIJIEID: cached_session_cookie, R18: 1)
|
||||
end
|
||||
memoize :client
|
||||
|
||||
def http
|
||||
super.timeout(60).use(retriable: { max_retries: 20 })
|
||||
end
|
||||
|
||||
def cached_session_cookie
|
||||
Cache.get("nijie-session-cookie", 60.minutes, skip_nil: true) do
|
||||
session_cookie
|
||||
end
|
||||
end
|
||||
|
||||
def clear_cached_session_cookie!
|
||||
flush_cache # clear memoized session cookie
|
||||
Cache.delete("nijie-session-cookie")
|
||||
end
|
||||
|
||||
def session_cookie
|
||||
login_page = http.get("https://nijie.info/login.php").parse
|
||||
|
||||
form = {
|
||||
email: Danbooru.config.nijie_login,
|
||||
password: Danbooru.config.nijie_password,
|
||||
url: login_page.at("input[name='url']")["value"],
|
||||
save: "on",
|
||||
ticket: ""
|
||||
}
|
||||
|
||||
response = http.post("https://nijie.info/login_int.php", form: form)
|
||||
|
||||
if response.status == 200
|
||||
response.cookies.select { |c| c.name == "NIJIEIJIEID" }.compact.first
|
||||
else
|
||||
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
memoize :client, :cached_session_cookie
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@ module Sources::Strategies
|
||||
/(?<!\A)誕生祭(?:\d*)\z/,
|
||||
/(?<!\A)版もうひとつの深夜の真剣お絵描き60分一本勝負(?:_\d+)?\z/,
|
||||
/(?<!\A)版深夜の真剣お絵描き60分一本勝負(?:_\d+)?\z/,
|
||||
/(?<!\A)版深夜の真剣お絵かき60分一本勝負(?:_\d+)?\z/,
|
||||
/(?<!\A)深夜の真剣お絵描き60分一本勝負(?:_\d+)?\z/,
|
||||
/(?<!\A)版深夜のお絵描き60分一本勝負(?:_\d+)?\z/,
|
||||
/(?<!\A)版真剣お絵描き60分一本勝(?:_\d+)?\z/,
|
||||
|
||||
@@ -10,7 +10,7 @@ class TagNameValidator < ActiveModel::EachValidator
|
||||
|
||||
case value
|
||||
when /\A_*\z/
|
||||
record.errors.add(attribute, "'#{value}' cannot be blank")
|
||||
record.errors.add(attribute, "cannot be blank")
|
||||
when /\*/
|
||||
record.errors.add(attribute, "'#{value}' cannot contain asterisks ('*')")
|
||||
when /,/
|
||||
|
||||
@@ -18,7 +18,6 @@ class UserPromotion
|
||||
user.level = new_level
|
||||
user.can_upload_free = can_upload_free unless can_upload_free.nil?
|
||||
user.can_approve_posts = can_approve_posts unless can_approve_posts.nil?
|
||||
user.inviter = promoter
|
||||
|
||||
create_user_feedback
|
||||
create_dmail
|
||||
|
||||
@@ -12,6 +12,7 @@ class Artist < ApplicationRecord
|
||||
|
||||
validate :validate_tag_category
|
||||
validates :name, tag_name: true, uniqueness: true
|
||||
|
||||
before_save :update_tag_category
|
||||
after_save :create_version
|
||||
after_save :clear_url_string_changed
|
||||
@@ -155,7 +156,7 @@ class Artist < ApplicationRecord
|
||||
return unless !is_deleted? && name_changed? && tag.present?
|
||||
|
||||
if tag.category_name != "Artist" && !tag.empty?
|
||||
errors.add(:base, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
|
||||
errors.add(:name, "'#{name}' is a #{tag.category_name.downcase} tag; artist entries can only be created for artist tags")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -97,6 +97,8 @@ class ArtistUrl < ApplicationRecord
|
||||
true
|
||||
when %r!www\.artstation\.com!i
|
||||
true
|
||||
when %r!blogimg\.jp!i, %r!image\.blog\.livedoor\.jp!i
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
@@ -106,9 +108,9 @@ class ArtistUrl < ApplicationRecord
|
||||
def priority
|
||||
sites = %w[
|
||||
Pixiv Twitter
|
||||
ArtStation Deviant\ Art Nico\ Seiga Nijie pawoo.net Pixiv\ Fanbox Pixiv\ Sketch Tinami Tumblr
|
||||
Booth.pm Facebook Fantia FC2 Gumroad Instagram Lofter Patreon Privatter Skeb Weibo Youtube
|
||||
Circle.ms DLSite Melonbooks Toranoana
|
||||
ArtStation BCY Deviant\ Art Hentai\ Foundry Nico\ Seiga Nijie pawoo.net Pixiv\ Fanbox Pixiv\ Sketch Tinami Tumblr
|
||||
Ask.fm Booth.pm Facebook Fantia FC2 Gumroad Instagram Ko-fi Livedoor Lofter Mihuashi Mixi.jp Patreon Piapro.jp Picarto Privatter Sakura.ne.jp Stickam Skeb Twitch Weibo Youtube
|
||||
Amazon Circle.ms DLSite Doujinshi.org Erogamescape Mangaupdates Melonbooks Toranoana Wikipedia
|
||||
]
|
||||
|
||||
sites.index(site_name) || 1000
|
||||
|
||||
@@ -43,7 +43,7 @@ class Ban < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_user_is_bannable
|
||||
errors.add(:user, "is already banned") if user.is_banned?
|
||||
errors.add(:user, "is already banned") if user&.is_banned?
|
||||
end
|
||||
|
||||
def update_user_on_create
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
class Comment < ApplicationRecord
|
||||
validates_presence_of :body, :message => "has no content"
|
||||
belongs_to :post
|
||||
belongs_to :creator, class_name: "User"
|
||||
belongs_to_updater
|
||||
|
||||
has_many :moderation_reports, as: :model
|
||||
has_many :votes, :class_name => "CommentVote", :dependent => :destroy
|
||||
|
||||
validates :body, presence: true
|
||||
|
||||
before_create :autoreport_spam
|
||||
after_create :update_last_commented_at_on_create
|
||||
after_update(:if => ->(rec) {(!rec.is_deleted? || !rec.saved_change_to_is_deleted?) && CurrentUser.id != rec.creator_id}) do |rec|
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
class FavoriteGroup < ApplicationRecord
|
||||
validates_uniqueness_of :name, :case_sensitive => false, :scope => :creator_id
|
||||
validates_format_of :name, :with => /\A[^,]+\Z/, :message => "cannot have commas"
|
||||
belongs_to :creator, class_name: "User"
|
||||
|
||||
before_validation :normalize_name
|
||||
before_validation :strip_name
|
||||
|
||||
validates :name, presence: true
|
||||
validates :name, uniqueness: { case_sensitive: false, scope: :creator_id }
|
||||
validates :name, format: { without: /,/, message: "cannot have commas" }
|
||||
validate :creator_can_create_favorite_groups, :on => :create
|
||||
validate :validate_number_of_posts
|
||||
validate :validate_posts
|
||||
|
||||
76
app/models/rate_limit.rb
Normal file
76
app/models/rate_limit.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
class RateLimit < ApplicationRecord
|
||||
scope :expired, -> { where("updated_at < ?", 1.hour.ago) }
|
||||
|
||||
def self.prune!
|
||||
expired.delete_all
|
||||
end
|
||||
|
||||
def self.visible(user)
|
||||
if user.is_owner?
|
||||
all
|
||||
elsif user.is_anonymous?
|
||||
none
|
||||
else
|
||||
where(key: [user.cache_key])
|
||||
end
|
||||
end
|
||||
|
||||
def self.search(params)
|
||||
q = search_attributes(params, :id, :created_at, :updated_at, :limited, :points, :action, :key)
|
||||
q = q.apply_default_order(params)
|
||||
q
|
||||
end
|
||||
|
||||
# `action` is the action being limited. Usually a controller endpoint.
|
||||
# `keys` is who is being limited. Usually a [user, ip] pair, meaning the action is limited both by the user's ID and their IP.
|
||||
# `cost` is the number of points the action costs.
|
||||
# `rate` is the number of points per second that are refilled.
|
||||
# `burst` is the maximum number of points that can be saved up.
|
||||
def self.create_or_update!(action:, keys:, cost:, rate:, burst:, minimum_points: -30)
|
||||
# { key0: keys[0], ..., keyN: keys[N] }
|
||||
key_params = keys.map.with_index { |key, i| [:"key#{i}", key] }.to_h
|
||||
|
||||
# (created_at, updated_at, action, keyN, points)
|
||||
values = keys.map.with_index { |key, i| "(:now, :now, :action, :key#{i}, :points)" }
|
||||
|
||||
# Do an upsert, creating a new rate limit object for each key that doesn't
|
||||
# already exist, and updating the limit for each limit that already exists.
|
||||
#
|
||||
# If the current point count is negative, then we're limited. Penalize the
|
||||
# caller 1 second (1 rate unit), up to a maximum penalty of 30 seconds (by default).
|
||||
#
|
||||
# Otherwise, if the point count is positive, then we're not limited. Update
|
||||
# the point count and subtract the cost of the call.
|
||||
#
|
||||
# https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT
|
||||
sql = <<~SQL
|
||||
INSERT INTO rate_limits (created_at, updated_at, action, key, points)
|
||||
VALUES #{values.join(", ")}
|
||||
ON CONFLICT (action, key) DO UPDATE SET
|
||||
updated_at = :now,
|
||||
limited = rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at)) < 0,
|
||||
points =
|
||||
CASE
|
||||
WHEN rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at)) < 0 THEN
|
||||
GREATEST(:minimum_points, LEAST(:burst, rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at))) - :rate)
|
||||
ELSE
|
||||
GREATEST(:minimum_points, LEAST(:burst, rate_limits.points + :rate * EXTRACT(epoch FROM (:now - rate_limits.updated_at))) - :cost)
|
||||
END
|
||||
RETURNING *
|
||||
SQL
|
||||
|
||||
sql_params = {
|
||||
now: Time.zone.now,
|
||||
action: action,
|
||||
rate: rate,
|
||||
burst: burst,
|
||||
cost: cost,
|
||||
points: burst - cost,
|
||||
minimum_points: minimum_points,
|
||||
**key_params
|
||||
}
|
||||
|
||||
rate_limits = RateLimit.find_by_sql([sql, sql_params])
|
||||
rate_limits
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
class TokenBucket < ApplicationRecord
|
||||
self.primary_key = "user_id"
|
||||
belongs_to :user
|
||||
|
||||
def self.prune!
|
||||
where("last_touched_at < ?", 1.day.ago).delete_all
|
||||
end
|
||||
|
||||
def self.create_default(user)
|
||||
TokenBucket.create(user_id: user.id, token_count: user.api_burst_limit, last_touched_at: Time.now)
|
||||
end
|
||||
|
||||
def accept?
|
||||
token_count >= 1
|
||||
end
|
||||
|
||||
def add!
|
||||
now = Time.now
|
||||
TokenBucket.where(user_id: user_id).update_all(["token_count = least(token_count + (? * extract(epoch from ? - last_touched_at)), ?), last_touched_at = ?", user.api_regen_multiplier, now, user.api_burst_limit, now])
|
||||
|
||||
# estimate the token count to avoid reloading
|
||||
self.token_count += user.api_regen_multiplier * (now - last_touched_at)
|
||||
self.token_count = user.api_burst_limit if token_count > user.api_burst_limit
|
||||
end
|
||||
|
||||
def consume!
|
||||
TokenBucket.where(user_id: user_id).update_all("token_count = greatest(0, token_count - 1)")
|
||||
self.token_count -= 1
|
||||
end
|
||||
|
||||
def throttled?
|
||||
add!
|
||||
|
||||
if accept?
|
||||
consume!
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -134,7 +134,6 @@ class User < ApplicationRecord
|
||||
has_many :user_events, dependent: :destroy
|
||||
has_one :recent_ban, -> {order("bans.id desc")}, :class_name => "Ban"
|
||||
|
||||
has_one :token_bucket
|
||||
has_one :email_address, dependent: :destroy
|
||||
has_many :api_keys, dependent: :destroy
|
||||
has_many :note_versions, :foreign_key => "updater_id"
|
||||
@@ -465,26 +464,12 @@ class User < ApplicationRecord
|
||||
|
||||
# regen this amount per second
|
||||
def api_regen_multiplier(level)
|
||||
if level >= User::Levels::PLATINUM
|
||||
if level >= User::Levels::GOLD
|
||||
4
|
||||
elsif level == User::Levels::GOLD
|
||||
2
|
||||
else
|
||||
1
|
||||
end
|
||||
end
|
||||
|
||||
# can make this many api calls at once before being bound by
|
||||
# api_regen_multiplier refilling your pool
|
||||
def api_burst_limit(level)
|
||||
if level >= User::Levels::PLATINUM
|
||||
60
|
||||
elsif level == User::Levels::GOLD
|
||||
30
|
||||
else
|
||||
10
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def max_saved_searches
|
||||
@@ -535,14 +520,6 @@ class User < ApplicationRecord
|
||||
User.api_regen_multiplier(level)
|
||||
end
|
||||
|
||||
def api_burst_limit
|
||||
User.api_burst_limit(level)
|
||||
end
|
||||
|
||||
def remaining_api_limit
|
||||
token_bucket.try(:token_count) || api_burst_limit
|
||||
end
|
||||
|
||||
def statement_timeout
|
||||
User.statement_timeout(level)
|
||||
end
|
||||
|
||||
@@ -119,7 +119,7 @@ class UserUpgrade < ApplicationRecord
|
||||
end
|
||||
|
||||
def upgrade_recipient!
|
||||
recipient.update!(level: level, inviter: User.system)
|
||||
recipient.update!(level: level)
|
||||
end
|
||||
|
||||
def create_mod_action!
|
||||
|
||||
5
app/policies/rate_limit_policy.rb
Normal file
5
app/policies/rate_limit_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class RateLimitPolicy < ApplicationPolicy
|
||||
def index?
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -61,10 +61,8 @@ class UserPolicy < ApplicationPolicy
|
||||
updated_at last_logged_in_at last_forum_read_at
|
||||
comment_threshold default_image_size
|
||||
favorite_tags blacklisted_tags time_zone per_page
|
||||
custom_style favorite_count api_regen_multiplier
|
||||
api_burst_limit remaining_api_limit statement_timeout
|
||||
favorite_group_limit favorite_limit tag_query_limit
|
||||
max_saved_searches theme
|
||||
custom_style favorite_count statement_timeout favorite_group_limit
|
||||
favorite_limit tag_query_limit max_saved_searches theme
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%= f.input :group_name %>
|
||||
<%= f.input :url_string, label: "URLs", as: :text, input_html: { value: params.dig(:artist, :url_string) || @artist.urls.join("\n")}, hint: "You can prefix a URL with - to mark it as dead." %>
|
||||
|
||||
<% if @artist.wiki_page.present? %>
|
||||
<% if @artist.tag&.artist? && @artist.wiki_page.present? %>
|
||||
<div class="input">
|
||||
<label>Wiki (<%= link_to "Edit", edit_wiki_page_path(@artist.wiki_page) %>)</label>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
<%= edit_form_for(@bulk_update_request) do |f| %>
|
||||
<p>
|
||||
Request aliases or implications using the format shown below. An <b>alias</b> makes the first tag a
|
||||
synonym for the second tag. An <b>implication</b> makes the first tag automatically add the second tag.
|
||||
A <b>rename</b> replaces the first tag with the second tag without making it a permanent alias.
|
||||
An <b>update</b> moves multiple tags and pools at once.
|
||||
</p>
|
||||
<div class="prose">
|
||||
<details>
|
||||
<summary>Help: How to make a bulk update request</summary>
|
||||
|
||||
<p>
|
||||
<% if @bulk_update_request.new_record? && @bulk_update_request.forum_topic.present? %>
|
||||
This request will be attached to
|
||||
<%= link_to "topic ##{@bulk_update_request.forum_topic_id}: #{@bulk_update_request.forum_topic.title}", @bulk_update_request.forum_topic %>.
|
||||
<%= f.input :forum_topic_id, as: :hidden, input_html: { value: params.dig(:bulk_update_request, :forum_topic_id) } %>
|
||||
<% elsif @bulk_update_request.new_record? && @bulk_update_request.forum_topic.blank? %>
|
||||
This request will create a new forum topic. To attach this request to an existing topic, find
|
||||
the forum topic and click "Request alias/implication" at the top of the page.
|
||||
<%= embed_wiki "help:bur_notice" %>
|
||||
</details>
|
||||
|
||||
<%= f.input :title, label: "Forum Title", as: :string %>
|
||||
<% end %>
|
||||
</p>
|
||||
<p>
|
||||
<% if @bulk_update_request.new_record? && @bulk_update_request.forum_topic.present? %>
|
||||
This request will be attached to
|
||||
<%= link_to "topic ##{@bulk_update_request.forum_topic_id}: #{@bulk_update_request.forum_topic.title}", @bulk_update_request.forum_topic %>.
|
||||
<%= f.input :forum_topic_id, as: :hidden, input_html: { value: params.dig(:bulk_update_request, :forum_topic_id) } %>
|
||||
<% elsif @bulk_update_request.new_record? && @bulk_update_request.forum_topic.blank? %>
|
||||
This request will create a new forum topic. To attach this request to an existing topic, find
|
||||
the forum topic and click "Request alias/implication" at the top of the page.
|
||||
|
||||
<%= f.input :script, label: "Request", as: :text, placeholder: bur_script_example %>
|
||||
<%= f.input :title, label: "Forum Title", as: :string %>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%= f.input :script, label: "Request", as: :text %>
|
||||
|
||||
<% if @bulk_update_request.new_record? %>
|
||||
<div class="input">
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
<%= f.button :submit, "Submit" %>
|
||||
<%= dtext_preview_button "comment_body" %>
|
||||
<% if comment.new_record? %>
|
||||
<%= f.input :do_not_bump_post, :label => "No bump" %>
|
||||
<% end %>
|
||||
<% if policy(comment).can_sticky_comment? %>
|
||||
<%= f.input :is_sticky, label: "Sticky", for: "comment_is_sticky" %>
|
||||
<%= f.input :do_not_bump_post, label: "No bump" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
1
app/views/comments/update.js.erb
Normal file
1
app/views/comments/update.js.erb
Normal file
@@ -0,0 +1 @@
|
||||
$("#comment_<%= @comment.id %>").replaceWith("<%= j render_comment(@comment, current_user: CurrentUser.user) %>");
|
||||
@@ -22,7 +22,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="paginator">
|
||||
<div class="paginator text-center mt-4">
|
||||
<menu>
|
||||
<li><%= link_to "< Previous", searches_explore_posts_path(:date => 1.day.ago(@date).to_date), :class => "arrow" %></li>
|
||||
<li><%= link_to "Next >", searches_explore_posts_path(:date => 1.day.since(@date).to_date), :class => "arrow" %></li>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<%= post_previews_html(@posts) %>
|
||||
|
||||
<div class="paginator">
|
||||
<div class="paginator text-center mt-4">
|
||||
<menu>
|
||||
<li><%= link_to "< Previous", viewed_explore_posts_path(:date => 1.day.ago(@date).to_date) %></li>
|
||||
<li><%= link_to "Next >", viewed_explore_posts_path(:date => 1.day.since(@date).to_date) %></li>
|
||||
|
||||
21
app/views/rate_limits/index.html.erb
Normal file
21
app/views/rate_limits/index.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<div id="c-rate-limits">
|
||||
<div id="a-index">
|
||||
<%= table_for @rate_limits, class: "striped autofit" do |t| %>
|
||||
<% t.column :action %>
|
||||
|
||||
<% t.column :key %>
|
||||
|
||||
<% t.column :points do |rate_limit| %>
|
||||
<%= rate_limit.points.round(2) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column :limited? %>
|
||||
|
||||
<% t.column :updated_at do |rate_limit| %>
|
||||
<%= time_ago_in_words_tagged rate_limit.updated_at %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@rate_limits) %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
<h2 class="mb-4">Frequently Asked Questions</h2>
|
||||
|
||||
<div id="frequently-asked-questions">
|
||||
<div id="frequently-asked-questions" class="divide-y-1">
|
||||
<details>
|
||||
<summary>What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold?</summary>
|
||||
|
||||
|
||||
@@ -62,14 +62,12 @@
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
<tr>
|
||||
<th>Inviter</th>
|
||||
<% if user.inviter %>
|
||||
<% if user.inviter %>
|
||||
<tr>
|
||||
<th>Promoter</th>
|
||||
<td><%= link_to_user user.inviter %> <%= link_to "»", users_path(search: { inviter: { name: user.inviter.name }}) %></td>
|
||||
<% else %>
|
||||
<td>None</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
</tr>
|
||||
<% end %>
|
||||
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
@@ -265,14 +263,6 @@
|
||||
(<%= link_to_wiki "help", "help:api" %>)
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>API Limits</th>
|
||||
<td>
|
||||
<%= CurrentUser.user.remaining_api_limit %>
|
||||
/ <%= CurrentUser.user.api_burst_limit %> <span class="fineprint">(may not be up to date)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user