Merge remote-tracking branch 'parent/main' into upstream-20240122
This commit is contained in:
commit
a4cc73438e
65 changed files with 1150 additions and 707 deletions
|
@ -6,7 +6,7 @@ module Admin
|
|||
|
||||
def index
|
||||
authorize :audit_log, :index?
|
||||
@auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username)
|
||||
@auditable_accounts = Account.auditable.select(:id, :username)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
|
||||
scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
|
||||
scope = scope.not_excluded_by_account(current_account) unless current_account.nil? || current_account.id == @account.id
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
|
|||
@domains = InstancesIndex.query(function_score: {
|
||||
query: {
|
||||
prefix: {
|
||||
domain: TagManager.instance.normalize_domain(params[:q].strip),
|
||||
domain: normalized_domain,
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -37,11 +37,18 @@ class Api::V1::Peers::SearchController < Api::BaseController
|
|||
},
|
||||
}).limit(10).pluck(:domain)
|
||||
else
|
||||
domain = params[:q].strip
|
||||
domain = TagManager.instance.normalize_domain(domain)
|
||||
@domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain)
|
||||
domain = normalized_domain
|
||||
@domains = Instance.searchable.domain_starts_with(domain).limit(10).pluck(:domain)
|
||||
end
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
@domains = []
|
||||
end
|
||||
|
||||
def normalized_domain
|
||||
TagManager.instance.normalize_domain(query_value)
|
||||
end
|
||||
|
||||
def query_value
|
||||
params[:q].strip
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::SessionsController < Devise::SessionsController
|
||||
include Redisable
|
||||
|
||||
MAX_2FA_ATTEMPTS_PER_HOUR = 10
|
||||
|
||||
layout 'auth'
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
|
@ -130,9 +134,23 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
session.delete(:attempt_user_updated_at)
|
||||
end
|
||||
|
||||
def clear_2fa_attempt_from_user(user)
|
||||
redis.del(second_factor_attempts_key(user))
|
||||
end
|
||||
|
||||
def check_second_factor_rate_limits(user)
|
||||
attempts, = redis.multi do |multi|
|
||||
multi.incr(second_factor_attempts_key(user))
|
||||
multi.expire(second_factor_attempts_key(user), 1.hour)
|
||||
end
|
||||
|
||||
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
|
||||
end
|
||||
|
||||
def on_authentication_success(user, security_measure)
|
||||
@on_authentication_success_called = true
|
||||
|
||||
clear_2fa_attempt_from_user(user)
|
||||
clear_attempt_from_session
|
||||
|
||||
user.update_sign_in!(new_sign_in: true)
|
||||
|
@ -164,4 +182,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
|
||||
def second_factor_attempts_key(user)
|
||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,11 @@ module Auth::TwoFactorAuthenticationConcern
|
|||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if check_second_factor_rate_limits(user)
|
||||
flash.now[:alert] = I18n.t('users.rate_limited')
|
||||
return prompt_for_two_factor(user)
|
||||
end
|
||||
|
||||
if valid_otp_attempt?(user)
|
||||
on_authentication_success(user, :otp)
|
||||
else
|
||||
|
|
|
@ -163,7 +163,7 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||
def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
|
||||
unless id
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
|
||||
|
@ -172,14 +172,14 @@ module JsonLdHelper
|
|||
uri = json['id']
|
||||
end
|
||||
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
||||
json.present? && json['id'] == uri ? json : nil
|
||||
end
|
||||
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
||||
on_behalf_of ||= Account.representative
|
||||
|
||||
build_request(uri, on_behalf_of).perform do |response|
|
||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||
|
||||
body_to_json(response.body_with_limit) if response.code == 200
|
||||
|
@ -212,8 +212,8 @@ module JsonLdHelper
|
|||
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
||||
end
|
||||
|
||||
def build_request(uri, on_behalf_of = nil)
|
||||
Request.new(:get, uri).tap do |request|
|
||||
def build_request(uri, on_behalf_of = nil, options: {})
|
||||
Request.new(:get, uri, **options).tap do |request|
|
||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||
end
|
||||
|
|
|
@ -179,6 +179,11 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
|
||||
export const clickSearchResult = (q, type) => (dispatch, getState) => {
|
||||
const previous = getState().getIn(['search', 'recent']);
|
||||
|
||||
if (previous.some(x => x.get('q') === q && x.get('type') === type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const current = previous.add(fromJS({ type, q })).takeLast(4);
|
||||
|
||||
|
@ -207,4 +212,4 @@ export const hydrateSearch = () => (dispatch, getState) => {
|
|||
if (history !== null) {
|
||||
dispatch(updateSearchHistory(history));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -62,17 +62,17 @@ class Search extends PureComponent {
|
|||
};
|
||||
|
||||
defaultOptions = [
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
|
||||
{ label: <><mark>my:</mark> <FormattedList type='disjunction' value={['favourited', 'bookmarked', 'boosted']} /></>, action: e => { e.preventDefault(); this._insertText('my:'); } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
|
||||
{ label: <><mark>domain:</mark> <FormattedMessage id='search_popout.domain' defaultMessage='domain' /></>, action: e => { e.preventDefault(); this._insertText('domain:'); } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } },
|
||||
{ label: <><mark>order:</mark> <FormattedList type='disjunction' value={['desc', 'asc']} /></>, action: e => { e.preventDefault(); this._insertText('order:'); } },
|
||||
{ key: 'prompt-has', label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:'); } },
|
||||
{ key: 'prompt-is', label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:'); } },
|
||||
{ key: 'prompt-my', label: <><mark>my:</mark> <FormattedList type='disjunction' value={['favourited', 'bookmarked', 'boosted']} /></>, action: e => { e.preventDefault(); this._insertText('my:'); } },
|
||||
{ key: 'prompt-language', label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:'); } },
|
||||
{ key: 'prompt-from', label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:'); } },
|
||||
{ key: 'prompt-domain', label: <><mark>domain:</mark> <FormattedMessage id='search_popout.domain' defaultMessage='domain' /></>, action: e => { e.preventDefault(); this._insertText('domain:'); } },
|
||||
{ key: 'prompt-before', label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:'); } },
|
||||
{ key: 'prompt-during', label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:'); } },
|
||||
{ key: 'prompt-after', label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:'); } },
|
||||
{ key: 'prompt-in', label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library', 'public']} /></>, action: e => { e.preventDefault(); this._insertText('in:'); } },
|
||||
{ key: 'prompt-order', label: <><mark>order:</mark> <FormattedList type='disjunction' value={['desc', 'asc']} /></>, action: e => { e.preventDefault(); this._insertText('order:'); } },
|
||||
];
|
||||
|
||||
setRef = c => {
|
||||
|
@ -265,6 +265,8 @@ class Search extends PureComponent {
|
|||
const { recent } = this.props;
|
||||
|
||||
return recent.toArray().map(search => ({
|
||||
key: `${search.get('type')}/${search.get('q')}`,
|
||||
|
||||
label: labelForRecentSearch(search),
|
||||
|
||||
action: () => this.handleRecentSearchClick(search),
|
||||
|
@ -349,8 +351,8 @@ class Search extends PureComponent {
|
|||
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
|
||||
|
||||
<div className='search__popout__menu'>
|
||||
{recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => (
|
||||
<button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
|
||||
{recent.size > 0 ? this._getOptions().map(({ label, key, action, forget }, i) => (
|
||||
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
|
||||
<span>{label}</span>
|
||||
<button className='icon-button' onMouseDown={forget}><Icon id='times' icon={CloseIcon} /></button>
|
||||
</button>
|
||||
|
|
|
@ -358,7 +358,7 @@
|
|||
"keyboard_shortcuts.my_profile": "mở hồ sơ của bạn",
|
||||
"keyboard_shortcuts.notifications": "mở thông báo",
|
||||
"keyboard_shortcuts.open_media": "mở ảnh hoặc video",
|
||||
"keyboard_shortcuts.pinned": "mở những tút đã ghim",
|
||||
"keyboard_shortcuts.pinned": "Open pinned posts list",
|
||||
"keyboard_shortcuts.profile": "mở trang của người đăng tút",
|
||||
"keyboard_shortcuts.reply": "trả lời",
|
||||
"keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi",
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import type { TypedUseSelectorHook } from 'react-redux';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import type { AppDispatch, RootState } from './store';
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
|
||||
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
||||
state: RootState;
|
||||
|
|
|
@ -100,9 +100,8 @@ table + p {
|
|||
border-top-right-radius: 12px;
|
||||
height: 140px;
|
||||
vertical-align: bottom;
|
||||
background-color: #f3f2f5;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-position: center !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
|
||||
.email-account-banner-inner-td {
|
||||
|
|
|
@ -28,11 +28,11 @@ class StatusCacheHydrator
|
|||
|
||||
def hydrate_non_reblog_payload(empty_payload, account_id, account)
|
||||
empty_payload.tap do |payload|
|
||||
payload[:favourited] = Favourite.where(account_id: account_id, status_id: @status.id).exists?
|
||||
payload[:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.id).exists?
|
||||
payload[:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.conversation_id).exists?
|
||||
payload[:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.id).exists?
|
||||
payload[:pinned] = StatusPin.where(account_id: account_id, status_id: @status.id).exists? if @status.account_id == account_id
|
||||
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.id)
|
||||
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.id)
|
||||
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.conversation_id)
|
||||
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id)
|
||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
|
||||
payload[:emoji_reactions] = @status.emoji_reactions_grouped_by_name(account)
|
||||
|
||||
|
@ -43,7 +43,7 @@ class StatusCacheHydrator
|
|||
end
|
||||
end
|
||||
|
||||
def hydrate_reblog_payload(empty_payload, account_id, account) # rubocop:disable Metrics/AbcSize
|
||||
def hydrate_reblog_payload(empty_payload, account_id, account)
|
||||
empty_payload.tap do |payload|
|
||||
payload[:muted] = false
|
||||
payload[:bookmarked] = false
|
||||
|
@ -54,11 +54,11 @@ class StatusCacheHydrator
|
|||
# used to create the status, we need to hydrate it here too
|
||||
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
|
||||
|
||||
payload[:reblog][:favourited] = Favourite.where(account_id: account_id, status_id: @status.reblog_of_id).exists?
|
||||
payload[:reblog][:reblogged] = Status.where(account_id: account_id, reblog_of_id: @status.reblog_of_id).exists?
|
||||
payload[:reblog][:muted] = ConversationMute.where(account_id: account_id, conversation_id: @status.reblog.conversation_id).exists?
|
||||
payload[:reblog][:bookmarked] = Bookmark.where(account_id: account_id, status_id: @status.reblog_of_id).exists?
|
||||
payload[:reblog][:pinned] = StatusPin.where(account_id: account_id, status_id: @status.reblog_of_id).exists? if @status.reblog.account_id == account_id
|
||||
payload[:reblog][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_id)
|
||||
payload[:reblog][:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.reblog_of_id)
|
||||
payload[:reblog][:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.reblog.conversation_id)
|
||||
payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id)
|
||||
payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id
|
||||
payload[:reblog][:filtered] = payload[:filtered]
|
||||
payload[:reblog][:emoji_reactions] = @status.reblog.emoji_reactions_grouped_by_name(account)
|
||||
|
||||
|
|
|
@ -130,10 +130,10 @@ class Account < ApplicationRecord
|
|||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
scope :groups, -> { where(actor_type: 'Group') }
|
||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||
scope :matches_username, ->(value) { where('lower((username)::text) ~ lower(?)', value.to_s) }
|
||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches_regexp(value.to_s)) }
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") }
|
||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
||||
scope :without_unapproved, -> { left_outer_joins(:user).merge(User.approved.confirmed).or(remote) }
|
||||
scope :auditable, -> { where(id: Admin::ActionLog.select(:account_id).distinct) }
|
||||
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
|
||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) }
|
||||
scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }
|
||||
|
|
|
@ -72,7 +72,7 @@ class Admin::ActionLogFilter
|
|||
end
|
||||
|
||||
def results
|
||||
scope = latest_action_logs.includes(:target)
|
||||
scope = latest_action_logs.includes(:target, :account)
|
||||
|
||||
params.each do |key, value|
|
||||
next if key.to_s == 'page'
|
||||
|
|
|
@ -20,8 +20,11 @@ class Appeal < ApplicationRecord
|
|||
|
||||
belongs_to :account
|
||||
belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id', inverse_of: :appeal
|
||||
belongs_to :approved_by_account, class_name: 'Account', optional: true
|
||||
belongs_to :rejected_by_account, class_name: 'Account', optional: true
|
||||
|
||||
with_options class_name: 'Account', optional: true do
|
||||
belongs_to :approved_by_account
|
||||
belongs_to :rejected_by_account
|
||||
end
|
||||
|
||||
validates :text, presence: true, length: { maximum: 2_000 }
|
||||
validates :account_warning_id, uniqueness: true
|
||||
|
|
|
@ -183,7 +183,7 @@ module Account::Interactions
|
|||
end
|
||||
|
||||
def following?(other_account)
|
||||
active_relationships.where(target_account: other_account).exists?
|
||||
active_relationships.exists?(target_account: other_account)
|
||||
end
|
||||
|
||||
def following_anyone?
|
||||
|
@ -212,63 +212,63 @@ module Account::Interactions
|
|||
end
|
||||
|
||||
def blocking?(other_account)
|
||||
block_relationships.where(target_account: other_account).exists?
|
||||
block_relationships.exists?(target_account: other_account)
|
||||
end
|
||||
|
||||
def domain_blocking?(other_domain)
|
||||
domain_blocks.where(domain: other_domain).exists?
|
||||
domain_blocks.exists?(domain: other_domain)
|
||||
end
|
||||
|
||||
def muting?(other_account)
|
||||
mute_relationships.where(target_account: other_account).exists?
|
||||
mute_relationships.exists?(target_account: other_account)
|
||||
end
|
||||
|
||||
def muting_conversation?(conversation)
|
||||
conversation_mutes.where(conversation: conversation).exists?
|
||||
conversation_mutes.exists?(conversation: conversation)
|
||||
end
|
||||
|
||||
def muting_notifications?(other_account)
|
||||
mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
|
||||
mute_relationships.exists?(target_account: other_account, hide_notifications: true)
|
||||
end
|
||||
|
||||
def muting_reblogs?(other_account)
|
||||
active_relationships.where(target_account: other_account, show_reblogs: false).exists?
|
||||
active_relationships.exists?(target_account: other_account, show_reblogs: false)
|
||||
end
|
||||
|
||||
def requested?(other_account)
|
||||
follow_requests.where(target_account: other_account).exists?
|
||||
follow_requests.exists?(target_account: other_account)
|
||||
end
|
||||
|
||||
def favourited?(status)
|
||||
status.proper.favourites.where(account: self).exists?
|
||||
status.proper.favourites.exists?(account: self)
|
||||
end
|
||||
|
||||
def emoji_reacted?(status, shortcode = nil, domain = nil, domain_force: false)
|
||||
if shortcode.present?
|
||||
if domain.present? || domain_force
|
||||
status.proper.emoji_reactions.joins(:custom_emoji).where(account: self, name: shortcode, custom_emoji: { domain: domain }).exists?
|
||||
status.proper.emoji_reactions.joins(:custom_emoji).exists?(account: self, name: shortcode, custom_emoji: { domain: domain })
|
||||
else
|
||||
status.proper.emoji_reactions.where(account: self, name: shortcode).exists?
|
||||
status.proper.emoji_reactions.exists?(account: self, name: shortcode)
|
||||
end
|
||||
else
|
||||
status.proper.emoji_reactions.where(account: self).exists?
|
||||
status.proper.emoji_reactions.exists?(account: self)
|
||||
end
|
||||
end
|
||||
|
||||
def bookmarked?(status)
|
||||
status.proper.bookmarks.where(account: self).exists?
|
||||
status.proper.bookmarks.exists?(account: self)
|
||||
end
|
||||
|
||||
def reblogged?(status)
|
||||
status.proper.reblogs.where(account: self).exists?
|
||||
status.proper.reblogs.exists?(account: self)
|
||||
end
|
||||
|
||||
def pinned?(status)
|
||||
status_pins.where(status: status).exists?
|
||||
status_pins.exists?(status: status)
|
||||
end
|
||||
|
||||
def endorsed?(account)
|
||||
account_pins.where(target_account: account).exists?
|
||||
account_pins.exists?(target_account: account)
|
||||
end
|
||||
|
||||
def status_matches_filters(status)
|
||||
|
|
|
@ -17,8 +17,6 @@ class DomainAllow < ApplicationRecord
|
|||
|
||||
validates :domain, presence: true, uniqueness: true, domain: true
|
||||
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
|
||||
def to_log_human_identifier
|
||||
domain
|
||||
end
|
||||
|
|
|
@ -39,7 +39,6 @@ class DomainBlock < ApplicationRecord
|
|||
has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false, dependent: nil
|
||||
delegate :count, to: :accounts, prefix: true
|
||||
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :with_user_facing_limitations, -> { where(hidden: false) }
|
||||
scope :with_limitations, lambda {
|
||||
where(severity: [:silence, :suspend])
|
||||
|
|
|
@ -21,8 +21,10 @@ class EmailDomainBlock < ApplicationRecord
|
|||
include DomainNormalizable
|
||||
include Paginable
|
||||
|
||||
belongs_to :parent, class_name: 'EmailDomainBlock', optional: true
|
||||
has_many :children, class_name: 'EmailDomainBlock', foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||
with_options class_name: 'EmailDomainBlock' do
|
||||
belongs_to :parent, optional: true
|
||||
has_many :children, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
|
||||
end
|
||||
|
||||
validates :domain, presence: true, uniqueness: true, domain: true
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ class FeaturedTag < ApplicationRecord
|
|||
end
|
||||
|
||||
def decrement(deleted_status_id)
|
||||
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at)
|
||||
update(statuses_count: [0, statuses_count - 1].max, last_status_at: visible_tagged_account_statuses.where.not(id: deleted_status_id).select(:created_at).first&.created_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -55,8 +55,8 @@ class FeaturedTag < ApplicationRecord
|
|||
end
|
||||
|
||||
def reset_data
|
||||
self.statuses_count = account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag).count
|
||||
self.last_status_at = account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag).select(:created_at).first&.created_at
|
||||
self.statuses_count = visible_tagged_account_statuses.count
|
||||
self.last_status_at = visible_tagged_account_statuses.select(:created_at).first&.created_at
|
||||
end
|
||||
|
||||
def validate_featured_tags_limit
|
||||
|
@ -66,6 +66,14 @@ class FeaturedTag < ApplicationRecord
|
|||
end
|
||||
|
||||
def validate_tag_uniqueness
|
||||
errors.add(:name, :taken) if FeaturedTag.by_name(name).where(account_id: account_id).exists?
|
||||
errors.add(:name, :taken) if tag_already_featured_for_account?
|
||||
end
|
||||
|
||||
def tag_already_featured_for_account?
|
||||
FeaturedTag.by_name(name).exists?(account_id: account_id)
|
||||
end
|
||||
|
||||
def visible_tagged_account_statuses
|
||||
account.statuses.where(visibility: %i(public unlisted public_unlisted login)).tagged_with(tag)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,7 @@ class Instance < ApplicationRecord
|
|||
|
||||
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :domain_starts_with, ->(value) { where(arel_table[:domain].matches("#{sanitize_sql_like(value)}%", false, true)) }
|
||||
scope :by_domain_and_subdomains, ->(domain) { where("reverse('.' || domain) LIKE reverse(?)", "%.#{domain}") }
|
||||
|
||||
def self.refresh
|
||||
|
|
|
@ -27,8 +27,11 @@ class Poll < ApplicationRecord
|
|||
belongs_to :status
|
||||
|
||||
has_many :votes, class_name: 'PollVote', inverse_of: :poll, dependent: :delete_all
|
||||
has_many :voters, -> { group('accounts.id') }, through: :votes, class_name: 'Account', source: :account
|
||||
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }, through: :votes, class_name: 'Account', source: :account
|
||||
|
||||
with_options class_name: 'Account', source: :account, through: :votes do
|
||||
has_many :voters, -> { group('accounts.id') }
|
||||
has_many :local_voters, -> { group('accounts.id').merge(Account.local) }
|
||||
end
|
||||
|
||||
has_many :notifications, as: :activity, dependent: :destroy
|
||||
|
||||
|
|
|
@ -1,66 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrivacyPolicy < ActiveModelSerializers::Model
|
||||
DEFAULT_PRIVACY_POLICY = <<~TXT
|
||||
This privacy policy describes how %{domain} ("%{domain}", "we", "us") collects, protects and uses the personally identifiable information you may provide through the %{domain} website or its API. The policy also describes the choices available to you regarding our use of your personal information and how you can access and update this information. This policy does not apply to the practices of companies that %{domain} does not own or control, or to individuals that %{domain} does not employ or manage.
|
||||
|
||||
# What information do we collect?
|
||||
|
||||
- **Basic account information**: If you register on this server, you may be asked to enter a username, an e-mail address and a password. You may also enter additional profile information such as a display name and biography, and upload a profile picture and header image. The username, display name, biography, profile picture and header image are always listed publicly.
|
||||
- **Posts, following and other public information**: The list of people you follow is listed publicly, the same is true for your followers. When you submit a message, the date and time is stored as well as the application you submitted the message from. Messages may contain media attachments, such as pictures and videos. Public and unlisted posts are available publicly. When you feature a post on your profile, that is also publicly available information. Your posts are delivered to your followers, in some cases it means they are delivered to different servers and copies are stored there. When you delete posts, this is likewise delivered to your followers. The action of reblogging or favouriting another post is always public.
|
||||
- **Direct and followers-only posts**: All posts are stored and processed on the server. Followers-only posts are delivered to your followers and users who are mentioned in them, and direct posts are delivered only to users mentioned in them. In some cases it means they are delivered to different servers and copies are stored there. We make a good faith effort to limit the access to those posts only to authorized persons, but other servers may fail to do so. Therefore it's important to review servers your followers belong to. You may toggle an option to approve and reject new followers manually in the settings. **Please keep in mind that the operators of the server and any receiving server may view such messages**, and that recipients may screenshot, copy or otherwise re-share them. **Do not share any sensitive information over Mastodon.**
|
||||
- **IPs and other metadata**: When you log in, we record the IP address you log in from, as well as the name of your browser application. All the logged in sessions are available for your review and revocation in the settings. The latest IP address used is stored for up to 12 months. We also may retain server logs which include the IP address of every request to our server.
|
||||
|
||||
# What do we use your information for?
|
||||
|
||||
Any of the information we collect from you may be used in the following ways:
|
||||
|
||||
- To provide the core functionality of Mastodon. You can only interact with other people's content and post your own content when you are logged in. For example, you may follow other people to view their combined posts in your own personalized home timeline.
|
||||
- To aid moderation of the community, for example comparing your IP address with other known ones to determine ban evasion or other violations.
|
||||
- The email address you provide may be used to send you information, notifications about other people interacting with your content or sending you messages, and to respond to inquiries, and/or other requests or questions.
|
||||
|
||||
# How do we protect your information?
|
||||
|
||||
We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. Among other things, your browser session, as well as the traffic between your applications and the API, are secured with SSL, and your password is hashed using a strong one-way algorithm. You may enable two-factor authentication to further secure access to your account.
|
||||
|
||||
# What is our data retention policy?
|
||||
|
||||
We will make a good faith effort to:
|
||||
|
||||
- Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more than 90 days.
|
||||
- Retain the IP addresses associated with registered users no more than 12 months.
|
||||
|
||||
You can request and download an archive of your content, including your posts, media attachments, profile picture, and header image.
|
||||
|
||||
You may irreversibly delete your account at any time.
|
||||
|
||||
# Do we use cookies?
|
||||
|
||||
Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.
|
||||
|
||||
We use cookies to understand and save your preferences for future visits.
|
||||
|
||||
# Do we disclose any information to outside parties?
|
||||
|
||||
We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety.
|
||||
|
||||
Your public content may be downloaded by other servers in the network. Your public and followers-only posts are delivered to the servers where your followers reside, and direct messages are delivered to the servers of the recipients, in so far as those followers or recipients reside on a different server than this.
|
||||
|
||||
When you authorize an application to use your account, depending on the scope of permissions you approve, it may access your public profile information, your following list, your followers, your lists, all your posts, and your favourites. Applications can never access your e-mail address or password.
|
||||
|
||||
# Site usage by children
|
||||
|
||||
If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (General Data Protection Regulation) do not use this site.
|
||||
|
||||
If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.
|
||||
|
||||
Law requirements can be different if this server is in another jurisdiction.
|
||||
|
||||
___
|
||||
|
||||
This document is CC-BY-SA. Originally adapted from the [Discourse privacy policy](https://github.com/discourse/discourse).
|
||||
TXT
|
||||
|
||||
DEFAULT_PRIVACY_POLICY = Rails.root.join('config', 'templates', 'privacy-policy.md').read
|
||||
DEFAULT_UPDATED_AT = DateTime.new(2022, 10, 7).freeze
|
||||
|
||||
attributes :updated_at, :text
|
||||
|
|
|
@ -29,9 +29,12 @@ class Report < ApplicationRecord
|
|||
rate_limit by: :account, family: :reports
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :target_account, class_name: 'Account'
|
||||
belongs_to :action_taken_by_account, class_name: 'Account', optional: true
|
||||
belongs_to :assigned_account, class_name: 'Account', optional: true
|
||||
|
||||
with_options class_name: 'Account' do
|
||||
belongs_to :target_account
|
||||
belongs_to :action_taken_by_account, optional: true
|
||||
belongs_to :assigned_account, optional: true
|
||||
end
|
||||
|
||||
has_many :notes, class_name: 'ReportNote', inverse_of: :report, dependent: :destroy
|
||||
has_many :notifications, as: :activity, dependent: :destroy
|
||||
|
|
|
@ -70,9 +70,11 @@ class Status < ApplicationRecord
|
|||
has_one :owned_conversation, class_name: 'Conversation', foreign_key: 'ancestor_status_id', dependent: :nullify, inverse_of: false
|
||||
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true, inverse_of: false
|
||||
|
||||
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
|
||||
belongs_to :quote, foreign_key: 'quote_of_id', class_name: 'Status', inverse_of: :quotes, optional: true
|
||||
with_options class_name: 'Status', optional: true do
|
||||
belongs_to :thread, foreign_key: 'in_reply_to_id', inverse_of: :replies
|
||||
belongs_to :reblog, foreign_key: 'reblog_of_id', inverse_of: :reblogs
|
||||
belongs_to :quote, foreign_key: 'quote_of_id', inverse_of: :quotes
|
||||
end
|
||||
|
||||
has_many :favourites, inverse_of: :status, dependent: :destroy
|
||||
has_many :emoji_reactions, inverse_of: :status, dependent: :destroy
|
||||
|
|
|
@ -436,7 +436,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def sign_up_from_ip_requires_approval?
|
||||
!sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
|
||||
sign_up_ip.present? && IpBlock.sign_up_requires_approval.exists?(['ip >>= ?', sign_up_ip.to_s])
|
||||
end
|
||||
|
||||
def sign_up_email_requires_approval?
|
||||
|
|
|
@ -19,7 +19,7 @@ class REST::TagSerializer < ActiveModel::Serializer
|
|||
if instance_options && instance_options[:relationships]
|
||||
instance_options[:relationships].following_map[object.id] || false
|
||||
else
|
||||
TagFollow.where(tag_id: object.id, account_id: current_user.account_id).exists?
|
||||
TagFollow.exists?(tag_id: object.id, account_id: current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -23,9 +23,9 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -26,9 +26,9 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -37,7 +37,20 @@ class ActivityPub::FetchRepliesService < BaseService
|
|||
return unless @allow_synchronous_requests
|
||||
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
|
||||
# queries incorrectly by default.
|
||||
#
|
||||
# While this is relevant for all URLs with query strings, this is
|
||||
# the only code path where this happens in practice.
|
||||
#
|
||||
# Therefore, retry with correct signatures if this fails.
|
||||
begin
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true)
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
|
||||
|
||||
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { with_query_string: true })
|
||||
end
|
||||
end
|
||||
|
||||
def filtered_replies
|
||||
|
|
|
@ -59,9 +59,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
|
|||
|
||||
case collection['type']
|
||||
when 'Collection', 'CollectionPage'
|
||||
collection['items']
|
||||
as_array(collection['items'])
|
||||
when 'OrderedCollection', 'OrderedCollectionPage'
|
||||
collection['orderedItems']
|
||||
as_array(collection['orderedItems'])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ class Keys::QueryService < BaseService
|
|||
|
||||
return if json['items'].blank?
|
||||
|
||||
@devices = json['items'].map do |device|
|
||||
@devices = as_array(json['items']).map do |device|
|
||||
Device.new(device_id: device['id'], name: device['name'], identity_key: device.dig('identityKey', 'publicKeyBase64'), fingerprint_key: device.dig('fingerprintKey', 'publicKeyBase64'), claim_url: device['claim'])
|
||||
end
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error => e
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue