Merge commit '7a1f087659204e9d0cbba2de37e45b1921cefe20' into glitch-soc/merge-upstream
This commit is contained in:
commit
70de52c297
|
@ -81,9 +81,6 @@ Rails/WhereExists:
|
||||||
- 'app/lib/delivery_failure_tracker.rb'
|
- 'app/lib/delivery_failure_tracker.rb'
|
||||||
- 'app/lib/feed_manager.rb'
|
- 'app/lib/feed_manager.rb'
|
||||||
- 'app/lib/suspicious_sign_in_detector.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/policies/status_policy.rb'
|
||||||
- 'app/serializers/rest/announcement_serializer.rb'
|
- 'app/serializers/rest/announcement_serializer.rb'
|
||||||
- 'app/workers/move_worker.rb'
|
- 'app/workers/move_worker.rb'
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -27,11 +27,17 @@ class Vacuum::MediaAttachmentsVacuum
|
||||||
end
|
end
|
||||||
|
|
||||||
def media_attachments_past_retention_period
|
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
|
end
|
||||||
|
|
||||||
def orphaned_media_attachments
|
def orphaned_media_attachments
|
||||||
MediaAttachment.unattached.where(MediaAttachment.arel_table[:created_at].lt(TTL.ago))
|
MediaAttachment
|
||||||
|
.unattached
|
||||||
|
.created_before(TTL.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
def retention_period?
|
def retention_period?
|
||||||
|
|
|
@ -12,9 +12,11 @@
|
||||||
class AccountSummary < ApplicationRecord
|
class AccountSummary < ApplicationRecord
|
||||||
self.primary_key = :account_id
|
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 :safe, -> { where(sensitive: false) }
|
||||||
scope :localized, ->(locale) { where(language: locale) }
|
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
|
def self.refresh
|
||||||
Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
|
Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
|
||||||
|
|
|
@ -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
|
|
@ -206,10 +206,12 @@ class MediaAttachment < ApplicationRecord
|
||||||
|
|
||||||
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
scope :attached, -> { where.not(status_id: nil).or(where.not(scheduled_status_id: nil)) }
|
||||||
scope :cached, -> { remote.where.not(file_file_name: 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 :local, -> { where(remote_url: '') }
|
||||||
scope :ordered, -> { order(id: :asc) }
|
scope :ordered, -> { order(id: :asc) }
|
||||||
scope :remote, -> { where.not(remote_url: '') }
|
scope :remote, -> { where.not(remote_url: '') }
|
||||||
scope :unattached, -> { where(status_id: nil, scheduled_status_id: nil) }
|
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
|
attr_accessor :skip_download
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ class Poll < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def voted?(account)
|
def voted?(account)
|
||||||
account.id == account_id || votes.where(account: account).exists?
|
account.id == account_id || votes.exists?(account: account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def own_votes(account)
|
def own_votes(account)
|
||||||
|
|
|
@ -41,7 +41,7 @@ class SessionActivation < ApplicationRecord
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def active?(id)
|
def active?(id)
|
||||||
id && where(session_id: id).exists?
|
id && exists?(session_id: id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def activate(**options)
|
def activate(**options)
|
||||||
|
|
|
@ -270,7 +270,7 @@ class Status < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def reported?
|
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
|
end
|
||||||
|
|
||||||
def emojis
|
def emojis
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::AnnualReportSerializer < ActiveModel::Serializer
|
||||||
|
attributes :year, :data, :schema_version
|
||||||
|
end
|
|
@ -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
|
|
@ -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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def indexes
|
def indexes
|
||||||
[AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
|
[AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
|
||||||
end
|
end
|
||||||
|
|
|
@ -1934,6 +1934,7 @@ ar:
|
||||||
go_to_sso_account_settings: انتقل إلى إعدادات حساب مزود الهوية الخاص بك
|
go_to_sso_account_settings: انتقل إلى إعدادات حساب مزود الهوية الخاص بك
|
||||||
invalid_otp_token: رمز المصادقة بخطوتين غير صالح
|
invalid_otp_token: رمز المصادقة بخطوتين غير صالح
|
||||||
otp_lost_help_html: إن فقدتَهُما ، يمكنك الاتصال بـ %{email}
|
otp_lost_help_html: إن فقدتَهُما ، يمكنك الاتصال بـ %{email}
|
||||||
|
rate_limited: عدد محاولات التحقق كثير جدًا، يرجى المحاولة مرة أخرى لاحقًا.
|
||||||
seamless_external_login: لقد قمت بتسجيل الدخول عبر خدمة خارجية، إنّ إعدادات الكلمة السرية و البريد الإلكتروني غير متوفرة.
|
seamless_external_login: لقد قمت بتسجيل الدخول عبر خدمة خارجية، إنّ إعدادات الكلمة السرية و البريد الإلكتروني غير متوفرة.
|
||||||
signed_in_as: 'تم تسجيل دخولك بصفة:'
|
signed_in_as: 'تم تسجيل دخولك بصفة:'
|
||||||
verification:
|
verification:
|
||||||
|
|
|
@ -1793,6 +1793,7 @@ bg:
|
||||||
failed_2fa:
|
failed_2fa:
|
||||||
details: 'Ето подробности на опита за влизане:'
|
details: 'Ето подробности на опита за влизане:'
|
||||||
explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване.
|
explanation: Някой се опита да влезе в акаунта ви, но предостави невалиден втори фактор за удостоверяване.
|
||||||
|
further_actions_html: Ако не бяхте вие, то препоръчваме да направите %{action} незабавно, тъй като може да се злепостави.
|
||||||
subject: Неуспешен втори фактор за удостоверяване
|
subject: Неуспешен втори фактор за удостоверяване
|
||||||
title: Провал на втория фактор за удостоверяване
|
title: Провал на втория фактор за удостоверяване
|
||||||
suspicious_sign_in:
|
suspicious_sign_in:
|
||||||
|
|
|
@ -1790,6 +1790,12 @@ da:
|
||||||
extra: Sikkerhedskopien kan nu downloades!
|
extra: Sikkerhedskopien kan nu downloades!
|
||||||
subject: Dit arkiv er klar til download
|
subject: Dit arkiv er klar til download
|
||||||
title: Arkiv 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:
|
suspicious_sign_in:
|
||||||
change_password: ændrer din adgangskode
|
change_password: ændrer din adgangskode
|
||||||
details: 'Her er nogle detaljer om login-forsøget:'
|
details: 'Her er nogle detaljer om login-forsøget:'
|
||||||
|
|
|
@ -47,14 +47,19 @@ ru:
|
||||||
subject: 'Mastodon: Инструкция по сбросу пароля'
|
subject: 'Mastodon: Инструкция по сбросу пароля'
|
||||||
title: Сброс пароля
|
title: Сброс пароля
|
||||||
two_factor_disabled:
|
two_factor_disabled:
|
||||||
|
explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля.
|
||||||
subject: 'Mastodon: Двухфакторная авторизация отключена'
|
subject: 'Mastodon: Двухфакторная авторизация отключена'
|
||||||
|
subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена.
|
||||||
title: 2ФА отключена
|
title: 2ФА отключена
|
||||||
two_factor_enabled:
|
two_factor_enabled:
|
||||||
|
explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP.
|
||||||
subject: 'Mastodon: Настроена двухфакторная авторизация'
|
subject: 'Mastodon: Настроена двухфакторная авторизация'
|
||||||
|
subtitle: Для вашей учетной записи была включена двухфакторная аутентификация.
|
||||||
title: 2ФА включена
|
title: 2ФА включена
|
||||||
two_factor_recovery_codes_changed:
|
two_factor_recovery_codes_changed:
|
||||||
explanation: Предыдущие резервные коды были аннулированы и созданы новые.
|
explanation: Предыдущие резервные коды были аннулированы и созданы новые.
|
||||||
subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены'
|
subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены'
|
||||||
|
subtitle: Предыдущие коды восстановления были аннулированы и сгенерированы новые.
|
||||||
title: Коды восстановления 2FA изменены
|
title: Коды восстановления 2FA изменены
|
||||||
unlock_instructions:
|
unlock_instructions:
|
||||||
subject: 'Mastodon: Инструкция по разблокировке'
|
subject: 'Mastodon: Инструкция по разблокировке'
|
||||||
|
@ -68,9 +73,13 @@ ru:
|
||||||
subject: 'Мастодон: Ключ Безопасности удален'
|
subject: 'Мастодон: Ключ Безопасности удален'
|
||||||
title: Один из ваших защитных ключей был удален
|
title: Один из ваших защитных ключей был удален
|
||||||
webauthn_disabled:
|
webauthn_disabled:
|
||||||
|
explanation: Аутентификация с помощью ключей безопасности была отключена для вашей учетной записи.
|
||||||
|
extra: Теперь вход в систему возможен только с использованием токена, сгенерированного сопряженным приложением TOTP.
|
||||||
subject: 'Мастодон: Аутентификация с ключами безопасности отключена'
|
subject: 'Мастодон: Аутентификация с ключами безопасности отключена'
|
||||||
title: Ключи безопасности отключены
|
title: Ключи безопасности отключены
|
||||||
webauthn_enabled:
|
webauthn_enabled:
|
||||||
|
explanation: Для вашей учетной записи включена аутентификация по ключу безопасности.
|
||||||
|
extra: Теперь ваш ключ безопасности можно использовать для входа в систему.
|
||||||
subject: 'Мастодон: Включена аутентификация по ключу безопасности'
|
subject: 'Мастодон: Включена аутентификация по ключу безопасности'
|
||||||
title: Ключи безопасности включены
|
title: Ключи безопасности включены
|
||||||
omniauth_callbacks:
|
omniauth_callbacks:
|
||||||
|
|
|
@ -47,14 +47,19 @@ sq:
|
||||||
subject: 'Mastodon: Udhëzime ricaktimi fjalëkalimi'
|
subject: 'Mastodon: Udhëzime ricaktimi fjalëkalimi'
|
||||||
title: Ricaktim fjalëkalimi
|
title: Ricaktim fjalëkalimi
|
||||||
two_factor_disabled:
|
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'
|
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
|
title: 2FA u çaktivizua
|
||||||
two_factor_enabled:
|
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'
|
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
|
title: 2FA u aktivizua
|
||||||
two_factor_recovery_codes_changed:
|
two_factor_recovery_codes_changed:
|
||||||
explanation: Kodet e dikurshëm të rikthimit janë bërë të pavlefshëm dhe janë prodhuar të rinj.
|
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'
|
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
|
title: Kodet e rikthimit 2FA u ndryshuan
|
||||||
unlock_instructions:
|
unlock_instructions:
|
||||||
subject: 'Mastodon: Udhëzime shkyçjeje'
|
subject: 'Mastodon: Udhëzime shkyçjeje'
|
||||||
|
@ -68,9 +73,13 @@ sq:
|
||||||
subject: 'Mastodon: Fshirje kyçi sigurie'
|
subject: 'Mastodon: Fshirje kyçi sigurie'
|
||||||
title: Një nga kyçet tuaj të sigurisë është fshirë
|
title: Një nga kyçet tuaj të sigurisë është fshirë
|
||||||
webauthn_disabled:
|
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'
|
subject: 'Mastodon: U çaktivizua mirëfilltësimi me kyçe sigurie'
|
||||||
title: U çaktivizuan kyçe sigurie
|
title: U çaktivizuan kyçe sigurie
|
||||||
webauthn_enabled:
|
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'
|
subject: 'Mastodon: U aktivizua mirëfilltësim me kyçe sigurie'
|
||||||
title: U aktivizuan kyçe sigurie
|
title: U aktivizuan kyçe sigurie
|
||||||
omniauth_callbacks:
|
omniauth_callbacks:
|
||||||
|
|
|
@ -1792,6 +1792,10 @@ es-MX:
|
||||||
title: Descargar archivo
|
title: Descargar archivo
|
||||||
failed_2fa:
|
failed_2fa:
|
||||||
details: 'Estos son los detalles del intento de inicio de sesión:'
|
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:
|
suspicious_sign_in:
|
||||||
change_password: cambies tu contraseña
|
change_password: cambies tu contraseña
|
||||||
details: 'Aquí están los detalles del inicio de sesión:'
|
details: 'Aquí están los detalles del inicio de sesión:'
|
||||||
|
|
|
@ -1792,6 +1792,10 @@ es:
|
||||||
title: Descargar archivo
|
title: Descargar archivo
|
||||||
failed_2fa:
|
failed_2fa:
|
||||||
details: 'Estos son los detalles del intento de inicio de sesión:'
|
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:
|
suspicious_sign_in:
|
||||||
change_password: cambies tu contraseña
|
change_password: cambies tu contraseña
|
||||||
details: 'Aquí están los detalles del inicio de sesión:'
|
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!
|
extra: It stiet no klear om download te wurden!
|
||||||
subject: Jo argyf stiet klear om download te wurden
|
subject: Jo argyf stiet klear om download te wurden
|
||||||
title: Argyf ophelje
|
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:
|
suspicious_sign_in:
|
||||||
change_password: wizigje jo wachtwurd
|
change_password: wizigje jo wachtwurd
|
||||||
details: 'Hjir binne de details fan oanmeldbesykjen:'
|
details: 'Hjir binne de details fan oanmeldbesykjen:'
|
||||||
|
|
|
@ -1790,6 +1790,12 @@ gl:
|
||||||
extra: Está preparada para descargala!
|
extra: Está preparada para descargala!
|
||||||
subject: O teu ficheiro xa está preparado para descargar
|
subject: O teu ficheiro xa está preparado para descargar
|
||||||
title: Leve o ficheiro
|
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:
|
suspicious_sign_in:
|
||||||
change_password: cambia o teu contrasinal
|
change_password: cambia o teu contrasinal
|
||||||
details: 'Estos son os detalles do acceso:'
|
details: 'Estos son os detalles do acceso:'
|
||||||
|
|
|
@ -439,6 +439,7 @@ ru:
|
||||||
view: Посмотреть доменные блокировки
|
view: Посмотреть доменные блокировки
|
||||||
email_domain_blocks:
|
email_domain_blocks:
|
||||||
add_new: Добавить новую
|
add_new: Добавить новую
|
||||||
|
allow_registrations_with_approval: Разрешить регистрацию с одобрением
|
||||||
attempts_over_week:
|
attempts_over_week:
|
||||||
few: "%{count} попытки за последнюю неделю"
|
few: "%{count} попытки за последнюю неделю"
|
||||||
many: "%{count} попыток за последнюю неделю"
|
many: "%{count} попыток за последнюю неделю"
|
||||||
|
@ -1659,6 +1660,7 @@ ru:
|
||||||
unknown_browser: Неизвестный браузер
|
unknown_browser: Неизвестный браузер
|
||||||
weibo: Weibo
|
weibo: Weibo
|
||||||
current_session: Текущая сессия
|
current_session: Текущая сессия
|
||||||
|
date: Дата
|
||||||
description: "%{browser} на %{platform}"
|
description: "%{browser} на %{platform}"
|
||||||
explanation: Здесь отображаются все браузеры, с которых выполнен вход в вашу учётную запись. Авторизованные приложения находятся в секции «Приложения».
|
explanation: Здесь отображаются все браузеры, с которых выполнен вход в вашу учётную запись. Авторизованные приложения находятся в секции «Приложения».
|
||||||
ip: IP
|
ip: IP
|
||||||
|
@ -1837,16 +1839,27 @@ ru:
|
||||||
webauthn: Ключи безопасности
|
webauthn: Ключи безопасности
|
||||||
user_mailer:
|
user_mailer:
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
|
action: Настройки аккаунта
|
||||||
explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись снова на хорошем счету.
|
explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись снова на хорошем счету.
|
||||||
subject: Ваше обжалование от %{date} была одобрено
|
subject: Ваше обжалование от %{date} была одобрено
|
||||||
|
subtitle: Ваш аккаунт снова с хорошей репутацией.
|
||||||
title: Обжалование одобрено
|
title: Обжалование одобрено
|
||||||
appeal_rejected:
|
appeal_rejected:
|
||||||
explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись восстановлена.
|
explanation: Апелляция на разблокировку против вашей учетной записи %{strike_date}, которую вы подали на %{appeal_date}, была одобрена. Ваша учетная запись восстановлена.
|
||||||
subject: Ваше обжалование от %{date} отклонено
|
subject: Ваше обжалование от %{date} отклонено
|
||||||
|
subtitle: Ваша апелляция отклонена.
|
||||||
title: Обжалование отклонено
|
title: Обжалование отклонено
|
||||||
backup_ready:
|
backup_ready:
|
||||||
|
explanation: Вы запросили полное резервное копирование вашей учетной записи Mastodon.
|
||||||
|
extra: Теперь он готов к загрузке!
|
||||||
subject: Ваш архив готов к загрузке
|
subject: Ваш архив готов к загрузке
|
||||||
title: Архив ваших данных готов
|
title: Архив ваших данных готов
|
||||||
|
failed_2fa:
|
||||||
|
details: 'Вот подробности попытки регистрации:'
|
||||||
|
explanation: Кто-то пытался войти в вашу учетную запись, но указал неверный второй фактор аутентификации.
|
||||||
|
further_actions_html: Если это не вы, мы рекомендуем %{action} немедленно принять меры, так как он может быть скомпрометирован.
|
||||||
|
subject: Сбой двухфакторной аутентификации
|
||||||
|
title: Сбой двухфакторной аутентификации
|
||||||
suspicious_sign_in:
|
suspicious_sign_in:
|
||||||
change_password: сменить пароль
|
change_password: сменить пароль
|
||||||
details: 'Подробности о новом входе:'
|
details: 'Подробности о новом входе:'
|
||||||
|
@ -1900,6 +1913,7 @@ ru:
|
||||||
go_to_sso_account_settings: Перейти к настройкам сторонних аккаунтов учетной записи
|
go_to_sso_account_settings: Перейти к настройкам сторонних аккаунтов учетной записи
|
||||||
invalid_otp_token: Введен неверный код двухфакторной аутентификации
|
invalid_otp_token: Введен неверный код двухфакторной аутентификации
|
||||||
otp_lost_help_html: Если Вы потеряли доступ к обоим, свяжитесь с %{email}
|
otp_lost_help_html: Если Вы потеряли доступ к обоим, свяжитесь с %{email}
|
||||||
|
rate_limited: Слишком много попыток аутентификации, повторите попытку позже.
|
||||||
seamless_external_login: Вы залогинены через сторонний сервис, поэтому настройки e-mail и пароля недоступны.
|
seamless_external_login: Вы залогинены через сторонний сервис, поэтому настройки e-mail и пароля недоступны.
|
||||||
signed_in_as: 'Выполнен вход под именем:'
|
signed_in_as: 'Выполнен вход под именем:'
|
||||||
verification:
|
verification:
|
||||||
|
|
|
@ -60,6 +60,7 @@ sk:
|
||||||
fields:
|
fields:
|
||||||
name: Označenie
|
name: Označenie
|
||||||
value: Obsah
|
value: Obsah
|
||||||
|
unlocked: Automaticky prijímaj nových nasledovateľov
|
||||||
account_alias:
|
account_alias:
|
||||||
acct: Adresa starého účtu
|
acct: Adresa starého účtu
|
||||||
account_migration:
|
account_migration:
|
||||||
|
|
|
@ -430,6 +430,7 @@ sk:
|
||||||
dashboard:
|
dashboard:
|
||||||
instance_accounts_dimension: Najsledovanejšie účty
|
instance_accounts_dimension: Najsledovanejšie účty
|
||||||
instance_accounts_measure: uložené účty
|
instance_accounts_measure: uložené účty
|
||||||
|
instance_followers_measure: naši nasledovatelia tam
|
||||||
instance_follows_measure: ich sledovatelia tu
|
instance_follows_measure: ich sledovatelia tu
|
||||||
instance_languages_dimension: Najpopulárnejšie jazyky
|
instance_languages_dimension: Najpopulárnejšie jazyky
|
||||||
instance_media_attachments_measure: uložené mediálne prílohy
|
instance_media_attachments_measure: uložené mediálne prílohy
|
||||||
|
@ -1257,6 +1258,8 @@ sk:
|
||||||
extra: Teraz je pripravená na stiahnutie!
|
extra: Teraz je pripravená na stiahnutie!
|
||||||
subject: Tvoj archív je pripravený na stiahnutie
|
subject: Tvoj archív je pripravený na stiahnutie
|
||||||
title: Odber archívu
|
title: Odber archívu
|
||||||
|
failed_2fa:
|
||||||
|
details: 'Tu sú podrobnosti o pokuse o prihlásenie:'
|
||||||
warning:
|
warning:
|
||||||
subject:
|
subject:
|
||||||
disable: Tvoj účet %{acct} bol zamrazený
|
disable: Tvoj účet %{acct} bol zamrazený
|
||||||
|
|
|
@ -1604,6 +1604,7 @@ sq:
|
||||||
unknown_browser: Shfletues i Panjohur
|
unknown_browser: Shfletues i Panjohur
|
||||||
weibo: Weibo
|
weibo: Weibo
|
||||||
current_session: Sesioni i tanishëm
|
current_session: Sesioni i tanishëm
|
||||||
|
date: Datë
|
||||||
description: "%{browser} në %{platform}"
|
description: "%{browser} në %{platform}"
|
||||||
explanation: Këta janë shfletuesit e përdorur tani për hyrje te llogaria juaj Mastodon.
|
explanation: Këta janë shfletuesit e përdorur tani për hyrje te llogaria juaj Mastodon.
|
||||||
ip: IP
|
ip: IP
|
||||||
|
@ -1770,16 +1771,27 @@ sq:
|
||||||
webauthn: Kyçe sigurie
|
webauthn: Kyçe sigurie
|
||||||
user_mailer:
|
user_mailer:
|
||||||
appeal_approved:
|
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.
|
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
|
subject: Apelimi juaj i datës %{date} u miratua
|
||||||
|
subtitle: Llogaria juaj edhe një herë është e shëndetshme.
|
||||||
title: Apelimi u miratua
|
title: Apelimi u miratua
|
||||||
appeal_rejected:
|
appeal_rejected:
|
||||||
explanation: Apelimi i paralajmërimit kundër llogarisë tuaj më %{strike_date}, të cilin e parashtruar më %{appeal_date}, u hodh poshtë.
|
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ë
|
subject: Apelimi juaj prej %{date} është hedhur poshtë
|
||||||
|
subtitle: Apelimi juaj është hedhur poshtë.
|
||||||
title: Apelimi u hodh poshtë
|
title: Apelimi u hodh poshtë
|
||||||
backup_ready:
|
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
|
subject: Arkivi juaj është gati për shkarkim
|
||||||
title: Marrje arkivi me vete
|
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:
|
suspicious_sign_in:
|
||||||
change_password: ndryshoni fjalëkalimin tuaj
|
change_password: ndryshoni fjalëkalimin tuaj
|
||||||
details: 'Ja hollësitë për hyrjen:'
|
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
|
go_to_sso_account_settings: Kaloni te rregullime llogarie te shërbimi juaj i identitetit
|
||||||
invalid_otp_token: Kod dyfaktorësh i pavlefshëm
|
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}
|
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.
|
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:'
|
signed_in_as: 'I futur si:'
|
||||||
verification:
|
verification:
|
||||||
|
|
|
@ -1791,7 +1791,7 @@ tr:
|
||||||
subject: Arşiviniz indirilmeye hazır
|
subject: Arşiviniz indirilmeye hazır
|
||||||
title: Arşiv paketlemesi
|
title: Arşiv paketlemesi
|
||||||
failed_2fa:
|
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ı.
|
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.
|
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ığı
|
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!
|
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ề
|
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
|
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:
|
suspicious_sign_in:
|
||||||
change_password: đổi mật khẩu của bạn
|
change_password: đổi mật khẩu của bạn
|
||||||
details: 'Chi tiết thông tin đăng nhập:'
|
details: 'Chi tiết thông tin đăng nhập:'
|
||||||
|
|
|
@ -52,6 +52,12 @@ namespace :api, format: false do
|
||||||
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
||||||
resources :preferences, only: [:index]
|
resources :preferences, only: [:index]
|
||||||
|
|
||||||
|
resources :annual_reports, only: [:index] do
|
||||||
|
member do
|
||||||
|
post :read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
resources :announcements, only: [:index] do
|
resources :announcements, only: [:index] do
|
||||||
scope module: :announcements do
|
scope module: :announcements do
|
||||||
resources :reactions, only: [:update, :destroy]
|
resources :reactions, only: [:update, :destroy]
|
||||||
|
|
|
@ -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
|
14
db/schema.rb
14
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do
|
ActiveRecord::Schema[7.1].define(version: 2024_01_11_033014) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -516,6 +516,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do
|
||||||
t.index ["target_account_id"], name: "index_follows_on_target_account_id"
|
t.index ["target_account_id"], name: "index_follows_on_target_account_id"
|
||||||
end
|
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|
|
create_table "identities", force: :cascade do |t|
|
||||||
t.string "provider", default: "", null: false
|
t.string "provider", default: "", null: false
|
||||||
t.string "uid", default: "", null: false
|
t.string "uid", default: "", null: false
|
||||||
|
@ -1229,6 +1240,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do
|
||||||
add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade
|
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", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade
|
||||||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", 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 "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
||||||
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
||||||
add_foreign_key "invites", "users", 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...')
|
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
|
processed = 0
|
||||||
removed = 0
|
removed = 0
|
||||||
progress = create_progress_bar(scope.count)
|
progress = create_progress_bar(scope.count)
|
||||||
|
|
|
@ -12,7 +12,7 @@ describe Api::BaseController do
|
||||||
head 200
|
head 200
|
||||||
end
|
end
|
||||||
|
|
||||||
def error
|
def failure
|
||||||
FakeService.new
|
FakeService.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -30,7 +30,7 @@ describe Api::BaseController do
|
||||||
|
|
||||||
it 'does not protect from forgery' do
|
it 'does not protect from forgery' do
|
||||||
ActionController::Base.allow_forgery_protection = true
|
ActionController::Base.allow_forgery_protection = true
|
||||||
post 'success'
|
post :success
|
||||||
expect(response).to have_http_status(200)
|
expect(response).to have_http_status(200)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -50,47 +50,55 @@ describe Api::BaseController do
|
||||||
|
|
||||||
it 'returns http forbidden for unconfirmed accounts' do
|
it 'returns http forbidden for unconfirmed accounts' do
|
||||||
user.update(confirmed_at: nil)
|
user.update(confirmed_at: nil)
|
||||||
post 'success'
|
post :success
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http forbidden for pending accounts' do
|
it 'returns http forbidden for pending accounts' do
|
||||||
user.update(approved: false)
|
user.update(approved: false)
|
||||||
post 'success'
|
post :success
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http forbidden for disabled accounts' do
|
it 'returns http forbidden for disabled accounts' do
|
||||||
user.update(disabled: true)
|
user.update(disabled: true)
|
||||||
post 'success'
|
post :success
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns http forbidden for suspended accounts' do
|
it 'returns http forbidden for suspended accounts' do
|
||||||
user.account.suspend!
|
user.account.suspend!
|
||||||
post 'success'
|
post :success
|
||||||
expect(response).to have_http_status(403)
|
expect(response).to have_http_status(403)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'error handling' do
|
describe 'error handling' do
|
||||||
before do
|
before do
|
||||||
routes.draw { get 'error' => 'api/base#error' }
|
routes.draw { get 'failure' => 'api/base#failure' }
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
ActiveRecord::RecordInvalid => 422,
|
ActiveRecord::RecordInvalid => 422,
|
||||||
Mastodon::ValidationError => 422,
|
|
||||||
ActiveRecord::RecordNotFound => 404,
|
ActiveRecord::RecordNotFound => 404,
|
||||||
Mastodon::UnexpectedResponseError => 503,
|
ActiveRecord::RecordNotUnique => 422,
|
||||||
|
Date::Error => 422,
|
||||||
HTTP::Error => 503,
|
HTTP::Error => 503,
|
||||||
OpenSSL::SSL::SSLError => 503,
|
Mastodon::InvalidParameterError => 400,
|
||||||
Mastodon::NotPermittedError => 403,
|
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|
|
}.each do |error, code|
|
||||||
it "Handles error class of #{error}" do
|
it "Handles error class of #{error}" do
|
||||||
allow(FakeService).to receive(:new).and_raise(error)
|
allow(FakeService).to receive(:new).and_raise(error)
|
||||||
|
|
||||||
get 'error'
|
get :failure
|
||||||
|
|
||||||
expect(response).to have_http_status(code)
|
expect(response).to have_http_status(code)
|
||||||
expect(FakeService).to have_received(:new)
|
expect(FakeService).to have_received(:new)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue