Merge remote-tracking branch 'parent/main' into upstream-20240125
This commit is contained in:
commit
9fa938eb0f
68 changed files with 824 additions and 94 deletions
16
.github/workflows/test-ruby.yml
vendored
16
.github/workflows/test-ruby.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
17
Gemfile.lock
17
Gemfile.lock
|
@ -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)
|
||||
|
|
30
app/controllers/api/v1/annual_reports_controller.rb
Normal file
30
app/controllers/api/v1/annual_reports_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
10
app/controllers/redirect/accounts_controller.rb
Normal file
10
app/controllers/redirect/accounts_controller.rb
Normal 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
|
24
app/controllers/redirect/base_controller.rb
Normal file
24
app/controllers/redirect/base_controller.rb
Normal 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
|
10
app/controllers/redirect/statuses_controller.rb
Normal file
10
app/controllers/redirect/statuses_controller.rb
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
4
app/javascript/svg-icons/repeat_active.svg
Normal file
4
app/javascript/svg-icons/repeat_active.svg
Normal 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
0
app/javascript/svg-icons/repeat_disabled.svg
Executable file → Normal file
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 415 B |
0
app/javascript/svg-icons/repeat_private.svg
Executable file → Normal file
0
app/javascript/svg-icons/repeat_private.svg
Executable file → Normal file
Before Width: | Height: | Size: 879 B After Width: | Height: | Size: 879 B |
6
app/javascript/svg-icons/repeat_private_active.svg
Normal file
6
app/javascript/svg-icons/repeat_private_active.svg
Normal 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 |
|
@ -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
43
app/lib/annual_report.rb
Normal 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
|
49
app/lib/annual_report/archetype.rb
Normal file
49
app/lib/annual_report/archetype.rb
Normal 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
|
22
app/lib/annual_report/commonly_interacted_with_accounts.rb
Normal file
22
app/lib/annual_report/commonly_interacted_with_accounts.rb
Normal 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
|
22
app/lib/annual_report/most_reblogged_accounts.rb
Normal file
22
app/lib/annual_report/most_reblogged_accounts.rb
Normal 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
|
22
app/lib/annual_report/most_used_apps.rb
Normal file
22
app/lib/annual_report/most_used_apps.rb
Normal 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
|
62
app/lib/annual_report/percentiles.rb
Normal file
62
app/lib/annual_report/percentiles.rb
Normal 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
|
16
app/lib/annual_report/source.rb
Normal file
16
app/lib/annual_report/source.rb
Normal 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
|
30
app/lib/annual_report/time_series.rb
Normal file
30
app/lib/annual_report/time_series.rb
Normal 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
|
22
app/lib/annual_report/top_hashtags.rb
Normal file
22
app/lib/annual_report/top_hashtags.rb
Normal 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
|
21
app/lib/annual_report/top_statuses.rb
Normal file
21
app/lib/annual_report/top_statuses.rb
Normal 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
|
20
app/lib/annual_report/type_distribution.rb
Normal file
20
app/lib/annual_report/type_distribution.rb
Normal 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
|
|
@ -28,7 +28,7 @@ class DeliveryFailureTracker
|
|||
end
|
||||
|
||||
def available?
|
||||
!UnavailableDomain.where(domain: @host).exists?
|
||||
!UnavailableDomain.exists?(domain: @host)
|
||||
end
|
||||
|
||||
def exhausted_deliveries_days
|
||||
|
|
|
@ -458,8 +458,8 @@ class FeedManager
|
|||
check_for_blocks = status.active_mentions.pluck(:account_id)
|
||||
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 = 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.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
37
app/models/generated_annual_report.rb
Normal file
37
app/models/generated_annual_report.rb
Normal 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
|
|
@ -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)) }
|
||||
|
|
|
@ -209,12 +209,14 @@ class MediaAttachment < ApplicationRecord
|
|||
validates :file, presence: true, if: :local?
|
||||
validates :thumbnail, absence: true, if: -> { local? && !audio_or_video? }
|
||||
|
||||
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
||||
scope :cached, -> { remote.where.not(file_file_name: nil) }
|
||||
scope :local, -> { where(remote_url: '') }
|
||||
scope :ordered, -> { order(id: :asc) }
|
||||
scope :remote, -> { where.not(remote_url: '') }
|
||||
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
23
app/presenters/annual_reports_presenter.rb
Normal file
23
app/presenters/annual_reports_presenter.rb
Normal 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
|
5
app/serializers/rest/annual_report_serializer.rb
Normal file
5
app/serializers/rest/annual_report_serializer.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::AnnualReportSerializer < ActiveModel::Serializer
|
||||
attributes :year, :data, :schema_version
|
||||
end
|
7
app/serializers/rest/annual_reports_serializer.rb
Normal file
7
app/serializers/rest/annual_reports_serializer.rb
Normal 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
|
8
app/views/redirects/show.html.haml
Normal file
8
app/views/redirects/show.html.haml
Normal 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'
|
11
app/workers/generate_annual_report_worker.rb
Normal file
11
app/workers/generate_annual_report_worker.rb
Normal 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
|
|
@ -24,6 +24,8 @@ class Scheduler::IndexingScheduler
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def indexes
|
||||
[AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
|
||||
end
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1793,6 +1793,7 @@ bg:
|
|||
failed_2fa:
|
||||
details: 'Ето подробности на опита за влизане:'
|
||||
explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване.
|
||||
further_actions_html: Ако не бяхте вие, то препоръчваме да направите %{action} незабавно, тъй като може да се злепостави.
|
||||
subject: Неуспешен втори фактор за удостоверяване
|
||||
title: Провал на втория фактор за удостоверяване
|
||||
suspicious_sign_in:
|
||||
|
|
|
@ -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:'
|
||||
|
|
|
@ -47,14 +47,19 @@ ru:
|
|||
subject: 'Mastodon: Инструкция по сбросу пароля'
|
||||
title: Сброс пароля
|
||||
two_factor_disabled:
|
||||
explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля.
|
||||
subject: 'Mastodon: Двухфакторная авторизация отключена'
|
||||
subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена.
|
||||
title: 2ФА отключена
|
||||
two_factor_enabled:
|
||||
explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP.
|
||||
subject: 'Mastodon: Настроена двухфакторная авторизация'
|
||||
subtitle: Для вашей учетной записи была включена двухфакторная аутентификация.
|
||||
title: 2ФА включена
|
||||
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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:'
|
||||
|
|
|
@ -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:'
|
||||
|
|
|
@ -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:'
|
||||
|
|
|
@ -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:'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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ý
|
||||
|
|
|
@ -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 s’qetë 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 s’ka rregullime fjalëkalimi dhe email.
|
||||
signed_in_as: 'I futur si:'
|
||||
verification:
|
||||
|
|
|
@ -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ığı
|
||||
|
|
|
@ -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:'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
17
db/migrate/20240111033014_create_generated_annual_reports.rb
Normal file
17
db/migrate/20240111033014_create_generated_annual_reports.rb
Normal 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
|
12
db/schema.rb
12
db/schema.rb
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue