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
|
@ -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?
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue