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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

After

Width:  |  Height:  |  Size: 275 B

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

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

Before After
Before After

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

Before

Width:  |  Height:  |  Size: 879 B

After

Width:  |  Height:  |  Size: 879 B

Before After
Before After

View file

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

After

Width:  |  Height:  |  Size: 961 B

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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