Merge remote-tracking branch 'parent/main' into upstream-20240125

This commit is contained in:
KMY 2024-01-25 18:15:21 +09:00
commit 9fa938eb0f
68 changed files with 824 additions and 94 deletions

View file

@ -52,7 +52,7 @@ jobs:
run: |
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs*
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: matrix.mode == 'test'
with:
path: |-
@ -118,7 +118,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
path: './'
name: ${{ github.sha }}
@ -195,7 +195,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
path: './public'
name: ${{ github.sha }}
@ -215,14 +215,14 @@ jobs:
- run: bundle exec rake spec:system
- name: Archive logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-screenshots
@ -286,7 +286,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
path: './public'
name: ${{ github.sha }}
@ -336,14 +336,14 @@ jobs:
- run: bin/rspec --tag search
- name: Archive logs
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-search-logs-${{ matrix.ruby-version }}
path: log/
- name: Archive test screenshots
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-search-screenshots

View file

@ -80,13 +80,6 @@ Rails/WhereExists:
Exclude:
- 'app/controllers/activitypub/inboxes_controller.rb'
- 'app/controllers/admin/email_domain_blocks_controller.rb'
- 'app/lib/activitypub/activity/create.rb'
- 'app/lib/delivery_failure_tracker.rb'
- 'app/lib/feed_manager.rb'
- 'app/lib/suspicious_sign_in_detector.rb'
- 'app/models/poll.rb'
- 'app/models/session_activation.rb'
- 'app/models/status.rb'
- 'app/policies/status_policy.rb'
- 'app/serializers/rest/announcement_serializer.rb'
- 'app/workers/move_worker.rb'

View file

@ -180,7 +180,7 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
chewy (7.4.0)
chewy (7.5.0)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
@ -319,7 +319,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.53.0)
haml_lint (0.55.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@ -445,7 +445,7 @@ GEM
mime-types-data (3.2023.1205)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.20.0)
minitest (5.21.1)
msgpack (1.7.2)
multi_json (1.15.0)
multipart-post (2.3.0)
@ -504,7 +504,7 @@ GEM
orm_adapter (0.5.0)
ox (2.14.17)
parallel (1.24.0)
parser (3.2.2.4)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
parslet (2.0.0)
@ -610,7 +610,7 @@ GEM
redis (>= 4)
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.8.3)
regexp_parser (2.9.0)
reline (0.4.2)
io-console (~> 0.5)
request_store (1.5.1)
@ -652,11 +652,11 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.12.1)
rubocop (1.59.0)
rubocop (1.60.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
@ -698,7 +698,8 @@ GEM
scenic (1.7.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.16.0)
selenium-webdriver (4.17.0)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Api::V1::AnnualReportsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
before_action :require_user!
before_action :set_annual_report, except: :index
def index
with_read_replica do
@presenter = AnnualReportsPresenter.new(GeneratedAnnualReport.where(account_id: current_account.id).pending)
@relationships = StatusRelationshipsPresenter.new(@presenter.statuses, current_account.id)
end
render json: @presenter,
serializer: REST::AnnualReportsSerializer,
relationships: @relationships
end
def read
@annual_report.view!
render_empty
end
private
def set_annual_report
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id])
end
end

View file

@ -21,10 +21,19 @@ module WebAppControllerConcern
def redirect_unauthenticated_to_permalinks!
return if user_signed_in? && current_account.moved_to_account_id.nil?
redirect_path = PermalinkRedirector.new(request.path).redirect_path
return if redirect_path.blank?
permalink_redirector = PermalinkRedirector.new(request.path)
return if permalink_redirector.redirect_path.blank?
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
redirect_to(redirect_path)
respond_to do |format|
format.html do
redirect_to(permalink_redirector.redirect_confirmation_path, allow_other_host: false)
end
format.json do
redirect_to(permalink_redirector.redirect_uri, allow_other_host: true)
end
end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
class Redirect::AccountsController < ApplicationController
private
def set_resource
@resource = Account.find(params[:id])
not_found if @resource.local?
end
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
class Redirect::BaseController < ApplicationController
vary_by 'Accept-Language'
before_action :set_resource
before_action :set_app_body_class
def show
@redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
render 'redirects/show', layout: 'application'
end
private
def set_app_body_class
@body_classes = 'app-body'
end
def set_resource
raise NotImplementedError
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
class Redirect::StatusesController < Redirect::BaseController
private
def set_resource
@resource = Status.find(params[:id])
not_found if @resource.local? || !@resource.distributable?
end
end

View file

@ -19,8 +19,10 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatDisabledIcon from 'mastodon/../svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from 'mastodon/../svg-icons/repeat_private.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -444,7 +446,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;

View file

@ -1,3 +1,4 @@
import { createSelector } from '@reduxjs/toolkit';
import { connect } from 'react-redux';
import {
@ -12,10 +13,15 @@ import {
import Search from '../components/search';
const getRecentSearches = createSelector(
state => state.getIn(['search', 'recent']),
recent => recent.reverse(),
);
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
recent: state.getIn(['search', 'recent']).reverse(),
recent: getRecentSearches(state),
});
const mapDispatchToProps = dispatch => ({

View file

@ -18,8 +18,10 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatDisabledIcon from 'mastodon/../svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from 'mastodon/../svg-icons/repeat_private.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -356,7 +358,7 @@ class ActionBar extends PureComponent {
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
reblogIconComponent = publicStatus ? RepeatActiveIcon : RepeatPrivateActiveIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;

View file

@ -104,3 +104,59 @@
margin-inline-start: 10px;
}
}
.redirect {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 14px;
line-height: 18px;
&__logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 30px;
img {
height: 48px;
}
}
&__message {
text-align: center;
h1 {
font-size: 17px;
line-height: 22px;
font-weight: 700;
margin-bottom: 30px;
}
p {
margin-bottom: 30px;
&:last-child {
margin-bottom: 0;
}
}
a {
color: $highlight-text-color;
font-weight: 500;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}
&__link {
margin-top: 15px;
}
}

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 22L3 18L7 14L8.4 15.45L6.85 17H17V13H19V19H6.85L8.4 20.55L7 22ZM5 11V5H17.15L15.6 3.45L17 2L21 6L17 10L15.6 8.55L17.15 7H7V11H5Z"/>
<path d="M9 9H15V15H9V9Z"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

0
app/javascript/svg-icons/repeat_disabled.svg Executable file → Normal file
View file

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before After
Before After

0
app/javascript/svg-icons/repeat_private.svg Executable file → Normal file
View file

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 879 B

Before After
Before After

View file

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.4 15.45L7 14L3 18L7 22L8.4 20.55L6.85 19H13.5V18C13.5 17.6567 13.5638 17.3171 13.6988 17H6.85L8.4 15.45Z"/>
<path d="M15 14.1883C14.8435 14.443 14.7232 14.7147 14.6398 15H9V9H15V14.1883Z"/>
<path d="M5 5V11H7V7H17.15L15.6 8.55L17 10L21 6L17 2L15.6 3.45L17.15 5H5Z"/>
<path d="M16 22C15.7167 22 15.475 21.9083 15.275 21.725C15.0917 21.525 15 21.2833 15 21V18C15 17.7167 15.0917 17.4833 15.275 17.3C15.475 17.1 15.7167 17 16 17V16C16 15.45 16.1917 14.9833 16.575 14.6C16.975 14.2 17.45 14 18 14C18.55 14 19.0167 14.2 19.4 14.6C19.8 14.9833 20 15.45 20 16V17C20.2833 17 20.5167 17.1 20.7 17.3C20.9 17.4833 21 17.7167 21 18V21C21 21.2833 20.9 21.525 20.7 21.725C20.5167 21.9083 20.2833 22 20 22H16ZM17 17H19V16C19 15.7167 18.9 15.4833 18.7 15.3C18.5167 15.1 18.2833 15 18 15C17.7167 15 17.475 15.1 17.275 15.3C17.0917 15.4833 17 15.7167 17 16V17Z"/>
</svg>

After

Width:  |  Height:  |  Size: 961 B

View file

@ -357,7 +357,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
already_voted = true
with_redis_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do
already_voted = poll.votes.where(account: @account).exists?
already_voted = poll.votes.exists?(account: @account)
poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
end
@ -503,7 +503,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return false if local_usernames.empty?
Account.local.where(username: local_usernames).exists?
Account.local.exists?(username: local_usernames)
end
def tombstone_exists?

43
app/lib/annual_report.rb Normal file
View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
class AnnualReport
include DatabaseHelper
SOURCES = [
AnnualReport::Archetype,
AnnualReport::TypeDistribution,
AnnualReport::TopStatuses,
AnnualReport::MostUsedApps,
AnnualReport::CommonlyInteractedWithAccounts,
AnnualReport::TimeSeries,
AnnualReport::TopHashtags,
AnnualReport::MostRebloggedAccounts,
AnnualReport::Percentiles,
].freeze
SCHEMA = 1
def initialize(account, year)
@account = account
@year = year
end
def generate
return if GeneratedAnnualReport.exists?(account: @account, year: @year)
GeneratedAnnualReport.create(
account: @account,
year: @year,
schema_version: SCHEMA,
data: data
)
end
private
def data
with_read_replica do
SOURCES.each_with_object({}) { |klass, hsh| hsh.merge!(klass.new(@account, @year).generate) }
end
end
end

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
class AnnualReport::Archetype < AnnualReport::Source
# Average number of posts (including replies and reblogs) made by
# each active user in a single year (2023)
AVERAGE_PER_YEAR = 113
def generate
{
archetype: archetype,
}
end
private
def archetype
if (standalone_count + replies_count + reblogs_count) < AVERAGE_PER_YEAR
:lurker
elsif reblogs_count > (standalone_count * 2)
:booster
elsif polls_count > (standalone_count * 0.1) # standalone_count includes posts with polls
:pollster
elsif replies_count > (standalone_count * 2)
:replier
else
:oracle
end
end
def polls_count
@polls_count ||= base_scope.where.not(poll_id: nil).count
end
def reblogs_count
@reblogs_count ||= base_scope.where.not(reblog_of_id: nil).count
end
def replies_count
@replies_count ||= base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count
end
def standalone_count
@standalone_count ||= base_scope.without_replies.without_reblogs.count
end
def base_scope
@account.statuses.where(id: year_as_snowflake_range)
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::CommonlyInteractedWithAccounts < AnnualReport::Source
SET_SIZE = 40
def generate
{
commonly_interacted_with_accounts: commonly_interacted_with_accounts.map do |(account_id, count)|
{
account_id: account_id,
count: count,
}
end,
}
end
private
def commonly_interacted_with_accounts
@account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(in_reply_to_account_id: @account.id).group(:in_reply_to_account_id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('in_reply_to_account_id, count(*) AS total'))
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::MostRebloggedAccounts < AnnualReport::Source
SET_SIZE = 10
def generate
{
most_reblogged_accounts: most_reblogged_accounts.map do |(account_id, count)|
{
account_id: account_id,
count: count,
}
end,
}
end
private
def most_reblogged_accounts
@account.statuses.reorder(nil).where(id: year_as_snowflake_range).where.not(reblog_of_id: nil).joins(reblog: :account).group('accounts.id').having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('accounts.id, count(*) as total'))
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::MostUsedApps < AnnualReport::Source
SET_SIZE = 10
def generate
{
most_used_apps: most_used_apps.map do |(name, count)|
{
name: name,
count: count,
}
end,
}
end
private
def most_used_apps
@account.statuses.reorder(nil).where(id: year_as_snowflake_range).joins(:application).group('oauth_applications.name').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('oauth_applications.name, count(*) as total'))
end
end

View file

@ -0,0 +1,62 @@
# frozen_string_literal: true
class AnnualReport::Percentiles < AnnualReport::Source
def generate
{
percentiles: {
followers: (total_with_fewer_followers / (total_with_any_followers + 1.0)) * 100,
statuses: (total_with_fewer_statuses / (total_with_any_statuses + 1.0)) * 100,
},
}
end
private
def followers_gained
@followers_gained ||= @account.passive_relationships.where("date_part('year', follows.created_at) = ?", @year).count
end
def statuses_created
@statuses_created ||= @account.statuses.where(id: year_as_snowflake_range).count
end
def total_with_fewer_followers
@total_with_fewer_followers ||= Follow.find_by_sql([<<~SQL.squish, { year: @year, comparison: followers_gained }]).first.total
WITH tmp0 AS (
SELECT follows.target_account_id
FROM follows
INNER JOIN accounts ON accounts.id = follows.target_account_id
WHERE date_part('year', follows.created_at) = :year
AND accounts.domain IS NULL
GROUP BY follows.target_account_id
HAVING COUNT(*) < :comparison
)
SELECT count(*) AS total
FROM tmp0
SQL
end
def total_with_fewer_statuses
@total_with_fewer_statuses ||= Status.find_by_sql([<<~SQL.squish, { comparison: statuses_created, min_id: year_as_snowflake_range.first, max_id: year_as_snowflake_range.last }]).first.total
WITH tmp0 AS (
SELECT statuses.account_id
FROM statuses
INNER JOIN accounts ON accounts.id = statuses.account_id
WHERE statuses.id BETWEEN :min_id AND :max_id
AND accounts.domain IS NULL
GROUP BY statuses.account_id
HAVING count(*) < :comparison
)
SELECT count(*) AS total
FROM tmp0
SQL
end
def total_with_any_followers
@total_with_any_followers ||= Follow.where("date_part('year', follows.created_at) = ?", @year).joins(:target_account).merge(Account.local).count('distinct follows.target_account_id')
end
def total_with_any_statuses
@total_with_any_statuses ||= Status.where(id: year_as_snowflake_range).joins(:account).merge(Account.local).count('distinct statuses.account_id')
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AnnualReport::Source
attr_reader :account, :year
def initialize(account, year)
@account = account
@year = year
end
protected
def year_as_snowflake_range
(Mastodon::Snowflake.id_at(DateTime.new(year, 1, 1))..Mastodon::Snowflake.id_at(DateTime.new(year, 12, 31)))
end
end

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class AnnualReport::TimeSeries < AnnualReport::Source
def generate
{
time_series: (1..12).map do |month|
{
month: month,
statuses: statuses_per_month[month] || 0,
following: following_per_month[month] || 0,
followers: followers_per_month[month] || 0,
}
end,
}
end
private
def statuses_per_month
@statuses_per_month ||= @account.statuses.reorder(nil).where(id: year_as_snowflake_range).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
end
def following_per_month
@following_per_month ||= @account.active_relationships.where("date_part('year', created_at) = ?", @year).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
end
def followers_per_month
@followers_per_month ||= @account.passive_relationships.where("date_part('year', created_at) = ?", @year).group(:period).pluck(Arel.sql("date_part('month', created_at)::int AS period, count(*)")).to_h
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AnnualReport::TopHashtags < AnnualReport::Source
SET_SIZE = 40
def generate
{
top_hashtags: top_hashtags.map do |(name, count)|
{
name: name,
count: count,
}
end,
}
end
private
def top_hashtags
Tag.joins(:statuses).where(statuses: { id: @account.statuses.where(id: year_as_snowflake_range).reorder(nil).select(:id) }).group(:id).having('count(*) > 1').order(total: :desc).limit(SET_SIZE).pluck(Arel.sql('COALESCE(tags.display_name, tags.name), count(*) AS total'))
end
end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AnnualReport::TopStatuses < AnnualReport::Source
def generate
top_reblogs = base_scope.order(reblogs_count: :desc).first&.id
top_favourites = base_scope.where.not(id: top_reblogs).order(favourites_count: :desc).first&.id
top_replies = base_scope.where.not(id: [top_reblogs, top_favourites]).order(replies_count: :desc).first&.id
{
top_statuses: {
by_reblogs: top_reblogs,
by_favourites: top_favourites,
by_replies: top_replies,
},
}
end
def base_scope
@account.statuses.with_public_visibility.joins(:status_stat).where(id: year_as_snowflake_range).reorder(nil)
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AnnualReport::TypeDistribution < AnnualReport::Source
def generate
{
type_distribution: {
total: base_scope.count,
reblogs: base_scope.where.not(reblog_of_id: nil).count,
replies: base_scope.where.not(in_reply_to_id: nil).where.not(in_reply_to_account_id: @account.id).count,
standalone: base_scope.without_replies.without_reblogs.count,
},
}
end
private
def base_scope
@account.statuses.where(id: year_as_snowflake_range)
end
end

View file

@ -28,7 +28,7 @@ class DeliveryFailureTracker
end
def available?
!UnavailableDomain.where(domain: @host).exists?
!UnavailableDomain.exists?(domain: @host)
end
def exhausted_deliveries_days

View file

@ -459,7 +459,7 @@ class FeedManager
check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil?
should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
should_filter ||= status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists? # of if the account is silenced and I'm not following them
should_filter ||= status.account.silenced? && !Follow.exists?(account_id: receiver_id, target_account_id: status.account_id) # Filter if the account is silenced and I'm not following them
should_filter
end
@ -472,7 +472,7 @@ class FeedManager
if status.reply? && status.in_reply_to_account_id != status.account_id
should_filter = status.in_reply_to_account_id != list.account_id
should_filter &&= !list.show_followed?
should_filter &&= !(list.show_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
should_filter &&= !(list.show_list? && ListAccount.exists?(list_id: list.id, account_id: status.in_reply_to_account_id))
return !!should_filter
end

View file

@ -5,17 +5,46 @@ class PermalinkRedirector
def initialize(path)
@path = path
@object = nil
end
def object
@object ||= begin
if at_username_status_request? || statuses_status_request?
status = Status.find_by(id: second_segment)
status if status&.distributable? && !status&.local?
elsif at_username_request?
username, domain = first_segment.delete_prefix('@').split('@')
domain = nil if TagManager.instance.local_domain?(domain)
account = Account.find_remote(username, domain)
account unless account&.local?
elsif accounts_request? && record_integer_id_request?
account = Account.find_by(id: second_segment)
account unless account&.local?
end
end
end
def redirect_path
if at_username_status_request? || statuses_status_request?
find_status_url_by_id(second_segment)
elsif at_username_request?
find_account_url_by_name(first_segment)
elsif accounts_request? && record_integer_id_request?
find_account_url_by_id(second_segment)
elsif @path.start_with?('/deck')
@path.delete_prefix('/deck')
return ActivityPub::TagManager.instance.url_for(object) if object.present?
@path.delete_prefix('/deck') if @path.start_with?('/deck')
end
def redirect_uri
return ActivityPub::TagManager.instance.uri_for(object) if object.present?
@path.delete_prefix('/deck') if @path.start_with?('/deck')
end
def redirect_confirmation_path
case object.class.name
when 'Account'
redirect_account_path(object.id)
when 'Status'
redirect_status_path(object.id)
else
@path.delete_prefix('/deck') if @path.start_with?('/deck')
end
end
@ -56,22 +85,4 @@ class PermalinkRedirector
def path_segments
@path_segments ||= @path.delete_prefix('/deck').delete_prefix('/').split('/')
end
def find_status_url_by_id(id)
status = Status.find_by(id: id)
ActivityPub::TagManager.instance.url_for(status) if status&.distributable? && !status.account.local?
end
def find_account_url_by_id(id)
account = Account.find_by(id: id)
ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local?
end
def find_account_url_by_name(name)
username, domain = name.gsub(/\A@/, '').split('@')
domain = nil if TagManager.instance.local_domain?(domain)
account = Account.find_remote(username, domain)
ActivityPub::TagManager.instance.url_for(account) if account.present? && !account.local?
end
end

View file

@ -19,7 +19,7 @@ class SuspiciousSignInDetector
end
def previously_seen_ip?(request)
@user.ips.where('ip <<= ?', masked_ip(request)).exists?
@user.ips.exists?(['ip <<= ?', masked_ip(request)])
end
def freshly_signed_up?

View file

@ -27,11 +27,17 @@ class Vacuum::MediaAttachmentsVacuum
end
def media_attachments_past_retention_period
MediaAttachment.remote.cached.where(MediaAttachment.arel_table[:created_at].lt(@retention_period.ago)).where(MediaAttachment.arel_table[:updated_at].lt(@retention_period.ago))
MediaAttachment
.remote
.cached
.created_before(@retention_period.ago)
.updated_before(@retention_period.ago)
end
def orphaned_media_attachments
MediaAttachment.unattached.where(MediaAttachment.arel_table[:created_at].lt(TTL.ago))
MediaAttachment
.unattached
.created_before(TTL.ago)
end
def retention_period?

View file

@ -12,9 +12,11 @@
class AccountSummary < ApplicationRecord
self.primary_key = :account_id
has_many :follow_recommendation_suppressions, primary_key: :account_id, foreign_key: :account_id, inverse_of: false
scope :safe, -> { where(sensitive: false) }
scope :localized, ->(locale) { where(language: locale) }
scope :filtered, -> { joins(arel_table.join(FollowRecommendationSuppression.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:account_id].eq(FollowRecommendationSuppression.arel_table[:account_id])).join_sources).where(FollowRecommendationSuppression.arel_table[:id].eq(nil)) }
scope :filtered, -> { where.missing(:follow_recommendation_suppressions) }
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: generated_annual_reports
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# year :integer not null
# data :jsonb not null
# schema_version :integer not null
# viewed_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
class GeneratedAnnualReport < ApplicationRecord
belongs_to :account
scope :pending, -> { where(viewed_at: nil) }
def viewed?
viewed_at.present?
end
def view!
update!(viewed_at: Time.now.utc)
end
def account_ids
data['most_reblogged_accounts'].pluck('account_id') + data['commonly_interacted_with_accounts'].pluck('account_id')
end
def status_ids
data['top_statuses'].values
end
end

View file

@ -13,14 +13,14 @@ class Instance < ApplicationRecord
attr_accessor :failure_days
has_many :accounts, foreign_key: :domain, primary_key: :domain, inverse_of: false
with_options foreign_key: :domain, primary_key: :domain, inverse_of: false do
belongs_to :domain_block
belongs_to :domain_allow
belongs_to :unavailable_domain # skipcq: RB-RL1031
belongs_to :unavailable_domain
belongs_to :instance_info
belongs_to :friend_domain
has_many :accounts, dependent: nil
end
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }

View file

@ -211,10 +211,12 @@ class MediaAttachment < ApplicationRecord
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
scope :cached, -> { remote.where.not(file_file_name: nil) }
scope :created_before, ->(value) { where(arel_table[:created_at].lt(value)) }
scope :local, -> { where(remote_url: '') }
scope :ordered, -> { order(id: :asc) }
scope :remote, -> { where.not(remote_url: '') }
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
scope :updated_before, ->(value) { where(arel_table[:updated_at].lt(value)) }
attr_accessor :skip_download

View file

@ -57,7 +57,7 @@ class Poll < ApplicationRecord
end
def voted?(account)
account.id == account_id || votes.where(account: account).exists?
account.id == account_id || votes.exists?(account: account)
end
def own_votes(account)

View file

@ -41,7 +41,7 @@ class SessionActivation < ApplicationRecord
class << self
def active?(id)
id && where(session_id: id).exists?
id && exists?(session_id: id)
end
def activate(**options)

View file

@ -331,11 +331,11 @@ class Status < ApplicationRecord
end
def reported?
@reported ||= Report.where(target_account: account).unresolved.where('? = ANY(status_ids)', id).exists?
@reported ||= Report.where(target_account: account).unresolved.exists?(['? = ANY(status_ids)', id])
end
def dtl?
(%w(public public_unlisted login).include?(visibility) || (unlisted_visibility? && public_searchability?)) && tags.where(name: dtl_tag_name).exists?
(%w(public public_unlisted login).include?(visibility) || (unlisted_visibility? && public_searchability?)) && tags.exists?(name: dtl_tag_name)
end
def emojis

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class AnnualReportsPresenter
alias read_attribute_for_serialization send
attr_reader :annual_reports
def initialize(annual_reports)
@annual_reports = annual_reports
end
def accounts
@accounts ||= Account.where(id: @annual_reports.flat_map(&:account_ids)).includes(:account_stat, :moved_to_account, user: :role)
end
def statuses
@statuses ||= Status.where(id: @annual_reports.flat_map(&:status_ids)).with_includes
end
def self.model_name
@model_name ||= ActiveModel::Name.new(self)
end
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class REST::AnnualReportSerializer < ActiveModel::Serializer
attributes :year, :data, :schema_version
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class REST::AnnualReportsSerializer < ActiveModel::Serializer
has_many :annual_reports, serializer: REST::AnnualReportSerializer
has_many :accounts, serializer: REST::AccountSerializer
has_many :statuses, serializer: REST::StatusSerializer
end

View file

@ -0,0 +1,8 @@
.redirect
.redirect__logo
= link_to render_logo, root_path
.redirect__message
%h1= t('redirects.title', instance: site_hostname)
%p= t('redirects.prompt')
%p= link_to @redirect_path, @redirect_path, rel: 'noreferrer noopener'

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
class GenerateAnnualReportWorker
include Sidekiq::Worker
def perform(account_id, year)
AnnualReport.new(Account.find(account_id), year).generate
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordNotUnique
true
end
end

View file

@ -24,6 +24,8 @@ class Scheduler::IndexingScheduler
end
end
private
def indexes
[AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
end

View file

@ -1934,6 +1934,7 @@ ar:
go_to_sso_account_settings: انتقل إلى إعدادات حساب مزود الهوية الخاص بك
invalid_otp_token: رمز المصادقة بخطوتين غير صالح
otp_lost_help_html: إن فقدتَهُما ، يمكنك الاتصال بـ %{email}
rate_limited: عدد محاولات التحقق كثير جدًا، يرجى المحاولة مرة أخرى لاحقًا.
seamless_external_login: لقد قمت بتسجيل الدخول عبر خدمة خارجية، إنّ إعدادات الكلمة السرية و البريد الإلكتروني غير متوفرة.
signed_in_as: 'تم تسجيل دخولك بصفة:'
verification:

View file

@ -1793,6 +1793,7 @@ bg:
failed_2fa:
details: 'Ето подробности на опита за влизане:'
explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване.
further_actions_html: Ако не бяхте вие, то препоръчваме да направите %{action} незабавно, тъй като може да се злепостави.
subject: Неуспешен втори фактор за удостоверяване
title: Провал на втория фактор за удостоверяване
suspicious_sign_in:

View file

@ -1790,6 +1790,12 @@ da:
extra: Sikkerhedskopien kan nu downloades!
subject: Dit arkiv er klar til download
title: Arkiv download
failed_2fa:
details: 'Her er detaljerne om login-forsøget:'
explanation: Nogen har forsøgt at logge ind på kontoen, men har angivet en ugyldig anden godkendelsesfaktor.
further_actions_html: Var dette ikke dig, anbefales det straks at %{action}, da den kan være kompromitteret.
subject: Anden faktor godkendelsesfejl
title: Fejlede på anden faktor godkendelse
suspicious_sign_in:
change_password: ændrer din adgangskode
details: 'Her er nogle detaljer om login-forsøget:'

View file

@ -47,14 +47,19 @@ ru:
subject: 'Mastodon: Инструкция по сбросу пароля'
title: Сброс пароля
two_factor_disabled:
explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля.
subject: 'Mastodon: Двухфакторная авторизация отключена'
subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена.
title: А отключена
two_factor_enabled:
explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP.
subject: 'Mastodon: Настроена двухфакторная авторизация'
subtitle: Для вашей учетной записи была включена двухфакторная аутентификация.
title: А включена
two_factor_recovery_codes_changed:
explanation: Предыдущие резервные коды были аннулированы и созданы новые.
subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены'
subtitle: Предыдущие коды восстановления были аннулированы и сгенерированы новые.
title: Коды восстановления 2FA изменены
unlock_instructions:
subject: 'Mastodon: Инструкция по разблокировке'
@ -68,9 +73,13 @@ ru:
subject: 'Мастодон: Ключ Безопасности удален'
title: Один из ваших защитных ключей был удален
webauthn_disabled:
explanation: Аутентификация с помощью ключей безопасности была отключена для вашей учетной записи.
extra: Теперь вход в систему возможен только с использованием токена, сгенерированного сопряженным приложением TOTP.
subject: 'Мастодон: Аутентификация с ключами безопасности отключена'
title: Ключи безопасности отключены
webauthn_enabled:
explanation: Для вашей учетной записи включена аутентификация по ключу безопасности.
extra: Теперь ваш ключ безопасности можно использовать для входа в систему.
subject: 'Мастодон: Включена аутентификация по ключу безопасности'
title: Ключи безопасности включены
omniauth_callbacks:

View file

@ -47,14 +47,19 @@ sq:
subject: 'Mastodon: Udhëzime ricaktimi fjalëkalimi'
title: Ricaktim fjalëkalimi
two_factor_disabled:
explanation: Hyrja tanimë është e mundshme duke përdorur vetëm adresë email dhe fjalëkalim.
subject: 'Mastodon: U çaktivizua mirëfilltësimi dyfaktorësh'
subtitle: Mirëfilltësimi dyfaktorësh për llogarinë tuaj është çaktivizuar.
title: 2FA u çaktivizua
two_factor_enabled:
explanation: Për të kryer hyrjen do të kërkohet doemos një token i prodhuar nga aplikacioni TOTP i çiftuar.
subject: 'Mastodon: U aktivizua mirëfilltësimi dyfaktorësh'
subtitle: Për llogarinë tuaj është aktivizuar mirëfilltësmi dyfaktorësh.
title: 2FA u aktivizua
two_factor_recovery_codes_changed:
explanation: Kodet e dikurshëm të rikthimit janë bërë të pavlefshëm dhe janë prodhuar të rinj.
subject: 'Mastodon: U riprodhuan kode rikthimi dyfaktorësh'
subtitle: Kodet e dikurshëm të rikthimit janë bërë të pavlefshëm dhe janë prodhuar të rinj.
title: Kodet e rikthimit 2FA u ndryshuan
unlock_instructions:
subject: 'Mastodon: Udhëzime shkyçjeje'
@ -68,9 +73,13 @@ sq:
subject: 'Mastodon: Fshirje kyçi sigurie'
title: Një nga kyçet tuaj të sigurisë është fshirë
webauthn_disabled:
explanation: Mirëfilltësimi me kyçe sigurie është çaktivizuar për llogarinë tuaj.
extra: Hyrjet tani janë të mundshme vetëm duke përdorur token-in e prodhuar nga aplikacioni TOTP i çiftuar.
subject: 'Mastodon: U çaktivizua mirëfilltësimi me kyçe sigurie'
title: U çaktivizuan kyçe sigurie
webauthn_enabled:
explanation: Mirëfilltësimi me kyçe sigurie është aktivizuar për këtë llogari.
extra: Kyçi juaj i sigurisë tanimë mund të përdoret për hyrje.
subject: 'Mastodon: U aktivizua mirëfilltësim me kyçe sigurie'
title: U aktivizuan kyçe sigurie
omniauth_callbacks:

View file

@ -1761,6 +1761,9 @@ en:
duplication: Cannot react same things
limit_reached: Limit of different reactions reached
unrecognized_emoji: is not a recognized emoji
redirects:
prompt: If you trust this link, click it to continue.
title: You are leaving %{instance}.
relationships:
activity: Account activity
confirm_follow_selected_followers: Are you sure you want to follow selected followers?

View file

@ -1792,6 +1792,10 @@ es-MX:
title: Descargar archivo
failed_2fa:
details: 'Estos son los detalles del intento de inicio de sesión:'
explanation: Alguien ha intentado iniciar sesión en tu cuenta pero proporcionó un segundo factor de autenticación inválido.
further_actions_html: Si no fuiste tú, se recomienda %{action} inmediatamente ya que puede estar comprometido.
subject: Fallo de autenticación de segundo factor
title: Falló la autenticación de segundo factor
suspicious_sign_in:
change_password: cambies tu contraseña
details: 'Aquí están los detalles del inicio de sesión:'

View file

@ -1792,6 +1792,10 @@ es:
title: Descargar archivo
failed_2fa:
details: 'Estos son los detalles del intento de inicio de sesión:'
explanation: Alguien ha intentado iniciar sesión en tu cuenta pero proporcionó un segundo factor de autenticación inválido.
further_actions_html: Si no fuiste tú, se recomienda %{action} inmediatamente ya que puede estar comprometida.
subject: Fallo de autenticación del segundo factor
title: Fallo en la autenticación del segundo factor
suspicious_sign_in:
change_password: cambies tu contraseña
details: 'Aquí están los detalles del inicio de sesión:'

View file

@ -1790,6 +1790,12 @@ fy:
extra: It stiet no klear om download te wurden!
subject: Jo argyf stiet klear om download te wurden
title: Argyf ophelje
failed_2fa:
details: 'Hjir binne de details fan de oanmeldbesykjen:'
explanation: Ien hat probearre om oan te melden op jo account, mar hat in ûnjildige twaddeferifikaasjefaktor opjûn.
further_actions_html: As jo dit net wiene, rekommandearje wy jo oan daliks %{action}, omdat it kompromitearre wêze kin.
subject: Twaddefaktorautentikaasjeflater
title: Twastapsferifikaasje mislearre
suspicious_sign_in:
change_password: wizigje jo wachtwurd
details: 'Hjir binne de details fan oanmeldbesykjen:'

View file

@ -1790,6 +1790,12 @@ gl:
extra: Está preparada para descargala!
subject: O teu ficheiro xa está preparado para descargar
title: Leve o ficheiro
failed_2fa:
details: 'Detalles do intento de acceso:'
explanation: Alguén intentou acceder á túa conta mais fíxoo cun segundo factor de autenticación non válido.
further_actions_html: Se non foches ti, recomendámosche %{action} inmediatamente xa que a conta podería estar en risco.
subject: Fallo co segundo factor de autenticación
title: Fallou o segundo factor de autenticación
suspicious_sign_in:
change_password: cambia o teu contrasinal
details: 'Estos son os detalles do acceso:'

View file

@ -439,6 +439,7 @@ ru:
view: Посмотреть доменные блокировки
email_domain_blocks:
add_new: Добавить новую
allow_registrations_with_approval: Разрешить регистрацию с одобрением
attempts_over_week:
few: "%{count} попытки за последнюю неделю"
many: "%{count} попыток за последнюю неделю"
@ -1659,6 +1660,7 @@ ru:
unknown_browser: Неизвестный браузер
weibo: Weibo
current_session: Текущая сессия
date: Дата
description: "%{browser} на %{platform}"
explanation: Здесь отображаются все браузеры, с которых выполнен вход в вашу учётную запись. Авторизованные приложения находятся в секции «Приложения».
ip: IP
@ -1837,16 +1839,27 @@ ru:
webauthn: Ключи безопасности
user_mailer:
appeal_approved:
action: Настройки аккаунта
explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись снова на хорошем счету.
subject: Ваше обжалование от %{date} была одобрено
subtitle: Ваш аккаунт снова с хорошей репутацией.
title: Обжалование одобрено
appeal_rejected:
explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись восстановлена.
subject: Ваше обжалование от %{date} отклонено
subtitle: Ваша апелляция отклонена.
title: Обжалование отклонено
backup_ready:
explanation: Вы запросили полное резервное копирование вашей учетной записи Mastodon.
extra: Теперь он готов к загрузке!
subject: Ваш архив готов к загрузке
title: Архив ваших данных готов
failed_2fa:
details: 'Вот подробности попытки регистрации:'
explanation: Кто-то пытался войти в вашу учетную запись, но указал неверный второй фактор аутентификации.
further_actions_html: Если это не вы, мы рекомендуем %{action} немедленно принять меры, так как он может быть скомпрометирован.
subject: Сбой двухфакторной аутентификации
title: Сбой двухфакторной аутентификации
suspicious_sign_in:
change_password: сменить пароль
details: 'Подробности о новом входе:'
@ -1900,6 +1913,7 @@ ru:
go_to_sso_account_settings: Перейти к настройкам сторонних аккаунтов учетной записи
invalid_otp_token: Введен неверный код двухфакторной аутентификации
otp_lost_help_html: Если Вы потеряли доступ к обоим, свяжитесь с %{email}
rate_limited: Слишком много попыток аутентификации, повторите попытку позже.
seamless_external_login: Вы залогинены через сторонний сервис, поэтому настройки e-mail и пароля недоступны.
signed_in_as: 'Выполнен вход под именем:'
verification:

View file

@ -60,6 +60,7 @@ sk:
fields:
name: Označenie
value: Obsah
unlocked: Automaticky prijímaj nových nasledovateľov
account_alias:
acct: Adresa starého účtu
account_migration:

View file

@ -430,6 +430,7 @@ sk:
dashboard:
instance_accounts_dimension: Najsledovanejšie účty
instance_accounts_measure: uložené účty
instance_followers_measure: naši nasledovatelia tam
instance_follows_measure: ich sledovatelia tu
instance_languages_dimension: Najpopulárnejšie jazyky
instance_media_attachments_measure: uložené mediálne prílohy
@ -1257,6 +1258,8 @@ sk:
extra: Teraz je pripravená na stiahnutie!
subject: Tvoj archív je pripravený na stiahnutie
title: Odber archívu
failed_2fa:
details: 'Tu sú podrobnosti o pokuse o prihlásenie:'
warning:
subject:
disable: Tvoj účet %{acct} bol zamrazený

View file

@ -1604,6 +1604,7 @@ sq:
unknown_browser: Shfletues i Panjohur
weibo: Weibo
current_session: Sesioni i tanishëm
date: Datë
description: "%{browser} në %{platform}"
explanation: Këta janë shfletuesit e përdorur tani për hyrje te llogaria juaj Mastodon.
ip: IP
@ -1770,16 +1771,27 @@ sq:
webauthn: Kyçe sigurie
user_mailer:
appeal_approved:
action: Rregullime Llogarie
explanation: Apelimi i paralajmërimit kundër llogarisë tuaj më %{strike_date}, të cilin e parashtruar më %{appeal_date} është miratuar. Llogaria juaj është sërish në pozita të mira.
subject: Apelimi juaj i datës %{date} u miratua
subtitle: Llogaria juaj edhe një herë është e shëndetshme.
title: Apelimi u miratua
appeal_rejected:
explanation: Apelimi i paralajmërimit kundër llogarisë tuaj më %{strike_date}, të cilin e parashtruar më %{appeal_date}, u hodh poshtë.
subject: Apelimi juaj prej %{date} është hedhur poshtë
subtitle: Apelimi juaj është hedhur poshtë.
title: Apelimi u hodh poshtë
backup_ready:
explanation: Kërkuat një kopjeruajtje të plotë të llogarisë tuaj Mastodon.
extra: Tani është gati për shkarkim!
subject: Arkivi juaj është gati për shkarkim
title: Marrje arkivi me vete
failed_2fa:
details: 'Ja hollësitë e përpjekjes për hyrje:'
explanation: Dikush ka provuar të hyjë në llogarinë tuaj, por dha faktor të dytë mirëfilltësimi.
further_actions_html: Nëse sqetë ju, rekomandojmë të %{action} menjëherë, ngaqë mund të jetë komprometua.
subject: Dështim faktori të dytë mirëfilltësimesh
title: Dështoi mirëfilltësimi me faktor të dytë
suspicious_sign_in:
change_password: ndryshoni fjalëkalimin tuaj
details: 'Ja hollësitë për hyrjen:'
@ -1833,6 +1845,7 @@ sq:
go_to_sso_account_settings: Kaloni te rregullime llogarie te shërbimi juaj i identitetit
invalid_otp_token: Kod dyfaktorësh i pavlefshëm
otp_lost_help_html: Nëse humbët hyrjen te të dy, mund të lidheni me %{email}
rate_limited: Shumë përpjekje mirëfilltësimi, riprovoni më vonë.
seamless_external_login: Jeni futur përmes një shërbimi të jashtëm, ndaj ska rregullime fjalëkalimi dhe email.
signed_in_as: 'I futur si:'
verification:

View file

@ -1791,7 +1791,7 @@ tr:
subject: Arşiviniz indirilmeye hazır
title: Arşiv paketlemesi
failed_2fa:
details: 'Oturum açma denemesinin ayrıntıları şöyledir:'
details: 'İşte oturum açma girişiminin ayrıntıları:'
explanation: Birisi hesabınızda oturum açmaya çalıştı ancak hatalı bir iki aşamalı doğrulama kodu kullandı.
further_actions_html: Eğer bu kişi siz değilseniz, hemen %{action} yapmanızı öneriyoruz çünkü hesabınız ifşa olmuş olabilir.
subject: İki aşamalı doğrulama başarısızlığı

View file

@ -1758,6 +1758,12 @@ vi:
extra: Hiện nó đã sẵn sàng tải xuống!
subject: Dữ liệu cá nhân của bạn đã sẵn sàng để tải về
title: Nhận dữ liệu cá nhân
failed_2fa:
details: 'Chi tiết thông tin đăng nhập:'
explanation: Ai đó đã cố đăng nhập vào tài khoản của bạn nhưng cung cấp yếu tố xác thực thứ hai không hợp lệ.
further_actions_html: Nếu không phải bạn, hãy lập tức %{action} vì có thể có rủi ro.
subject: Xác minh hai bước thất bại
title: Xác minh hai bước thất bại
suspicious_sign_in:
change_password: đổi mật khẩu của bạn
details: 'Chi tiết thông tin đăng nhập:'

View file

@ -171,6 +171,11 @@ Rails.application.routes.draw do
end
end
namespace :redirect do
resources :accounts, only: :show
resources :statuses, only: :show
end
resources :media, only: [:show] do
get :player
end

View file

@ -62,6 +62,12 @@ namespace :api, format: false do
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
resources :preferences, only: [:index]
resources :annual_reports, only: [:index] do
member do
post :read
end
end
resources :announcements, only: [:index] do
scope module: :announcements do
resources :reactions, only: [:update, :destroy]

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class CreateGeneratedAnnualReports < ActiveRecord::Migration[7.1]
def change
create_table :generated_annual_reports do |t|
t.belongs_to :account, null: false, foreign_key: { on_cascade: :delete }, index: false
t.integer :year, null: false
t.jsonb :data, null: false
t.integer :schema_version, null: false
t.datetime :viewed_at
t.timestamps
end
add_index :generated_annual_reports, [:account_id, :year], unique: true
end
end

View file

@ -694,6 +694,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_21_231131) do
t.index ["inbox_url"], name: "index_friend_domains_on_inbox_url", unique: true
end
create_table "generated_annual_reports", force: :cascade do |t|
t.bigint "account_id", null: false
t.integer "year", null: false
t.jsonb "data", null: false
t.integer "schema_version", null: false
t.datetime "viewed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "year"], name: "index_generated_annual_reports_on_account_id_and_year", unique: true
end
create_table "identities", force: :cascade do |t|
t.string "provider", default: "", null: false
t.string "uid", default: "", null: false
@ -1487,6 +1498,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_21_231131) do
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
add_foreign_key "generated_annual_reports", "accounts"
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
add_foreign_key "invites", "users", on_delete: :cascade

View file

@ -120,7 +120,7 @@ module Mastodon::CLI
say('Beginning removal of now-orphaned media attachments to free up disk space...')
scope = MediaAttachment.unattached.where('created_at < ?', options[:days].pred.days.ago)
scope = MediaAttachment.unattached.created_before(options[:days].pred.days.ago)
processed = 0
removed = 0
progress = create_progress_bar(scope.count)

View file

@ -12,7 +12,7 @@ describe Api::BaseController do
head 200
end
def error
def failure
FakeService.new
end
end
@ -30,7 +30,7 @@ describe Api::BaseController do
it 'does not protect from forgery' do
ActionController::Base.allow_forgery_protection = true
post 'success'
post :success
expect(response).to have_http_status(200)
end
end
@ -50,47 +50,55 @@ describe Api::BaseController do
it 'returns http forbidden for unconfirmed accounts' do
user.update(confirmed_at: nil)
post 'success'
post :success
expect(response).to have_http_status(403)
end
it 'returns http forbidden for pending accounts' do
user.update(approved: false)
post 'success'
post :success
expect(response).to have_http_status(403)
end
it 'returns http forbidden for disabled accounts' do
user.update(disabled: true)
post 'success'
post :success
expect(response).to have_http_status(403)
end
it 'returns http forbidden for suspended accounts' do
user.account.suspend!
post 'success'
post :success
expect(response).to have_http_status(403)
end
end
describe 'error handling' do
before do
routes.draw { get 'error' => 'api/base#error' }
routes.draw { get 'failure' => 'api/base#failure' }
end
{
ActiveRecord::RecordInvalid => 422,
Mastodon::ValidationError => 422,
ActiveRecord::RecordNotFound => 404,
Mastodon::UnexpectedResponseError => 503,
ActiveRecord::RecordNotUnique => 422,
Date::Error => 422,
HTTP::Error => 503,
OpenSSL::SSL::SSLError => 503,
Mastodon::InvalidParameterError => 400,
Mastodon::NotPermittedError => 403,
Mastodon::RaceConditionError => 503,
Mastodon::RateLimitExceededError => 429,
Mastodon::UnexpectedResponseError => 503,
Mastodon::ValidationError => 422,
OpenSSL::SSL::SSLError => 503,
Seahorse::Client::NetworkingError => 503,
Stoplight::Error::RedLight => 503,
}.each do |error, code|
it "Handles error class of #{error}" do
allow(FakeService).to receive(:new).and_raise(error)
get 'error'
get :failure
expect(response).to have_http_status(code)
expect(FakeService).to have_received(:new)
end