Skip to content

Commit

Permalink
Add support for language preferences for trending statuses and links (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
Gargron committed Oct 8, 2022
1 parent 678fc4d commit 45ebdb7
Show file tree
Hide file tree
Showing 29 changed files with 274 additions and 121 deletions.
1 change: 1 addition & 0 deletions app/controllers/admin/trends/links_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Admin::Trends::LinksController < Admin::BaseController
def index
authorize :preview_card, :review?

@locales = PreviewCardTrend.pluck('distinct language')
@preview_cards = filtered_preview_cards.page(params[:page])
@form = Trends::PreviewCardBatch.new
end
Expand Down
1 change: 1 addition & 0 deletions app/controllers/admin/trends/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Admin::Trends::StatusesController < Admin::BaseController
def index
authorize :status, :review?

@locales = StatusTrend.pluck('distinct language')
@statuses = filtered_statuses.page(params[:page])
@form = Trends::StatusBatch.new
end
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/api/v1/trends/links_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ def set_links
end

def links_from_trends
Trends.links.query.allowed.in_locale(content_locale)
scope = Trends.links.query.allowed.in_locale(content_locale)
scope = scope.filtered_for(current_account) if user_signed_in?
scope
end

def insert_pagination_headers
Expand Down
4 changes: 1 addition & 3 deletions app/mailers/admin_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class AdminMailer < ApplicationMailer
layout 'plain_mailer'

helper :accounts
helper :languages

def new_report(recipient, report)
@report = report
Expand Down Expand Up @@ -37,11 +38,8 @@ def new_pending_account(recipient, user)

def new_trends(recipient, links, tags, statuses)
@links = links
@lowest_trending_link = Trends.links.query.allowed.limit(Trends.links.options[:review_threshold]).last
@tags = tags
@lowest_trending_tag = Trends.tags.query.allowed.limit(Trends.tags.options[:review_threshold]).last
@statuses = statuses
@lowest_trending_status = Trends.statuses.query.allowed.limit(Trends.statuses.options[:review_threshold]).last
@me = recipient
@instance = Rails.configuration.x.local_domain

Expand Down
1 change: 1 addition & 0 deletions app/models/preview_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class PreviewCard < ApplicationRecord
enum link_type: [:unknown, :article]

has_and_belongs_to_many :statuses
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy

has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 80 -strip' }, validate_media_type: false

Expand Down
17 changes: 17 additions & 0 deletions app/models/preview_card_trend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: preview_card_trends
#
# id :bigint(8) not null, primary key
# preview_card_id :bigint(8) not null
# score :float default(0.0), not null
# rank :integer default(0), not null
# allowed :boolean default(FALSE), not null
# language :string
#
class PreviewCardTrend < ApplicationRecord
belongs_to :preview_card
scope :allowed, -> { where(allowed: true) }
end
1 change: 1 addition & 0 deletions app/models/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Status < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy
has_one :status_stat, inverse_of: :status
has_one :poll, inverse_of: :status, dependent: :destroy
has_one :trend, class_name: 'StatusTrend', inverse_of: :status

validates :uri, uniqueness: true, presence: true, unless: :local?
validates :text, presence: true, unless: -> { with_media? || reblog? }
Expand Down
21 changes: 21 additions & 0 deletions app/models/status_trend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: status_trends
#
# id :bigint(8) not null, primary key
# status_id :bigint(8) not null
# account_id :bigint(8) not null
# score :float default(0.0), not null
# rank :integer default(0), not null
# allowed :boolean default(FALSE), not null
# language :string
#

class StatusTrend < ApplicationRecord
belongs_to :status
belongs_to :account

scope :allowed, -> { where(allowed: true) }
end
6 changes: 5 additions & 1 deletion app/models/trends.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def self.refresh!
end

def self.request_review!
return unless enabled?
return if skip_review? || !enabled?

links_requiring_review = links.request_review
tags_requiring_review = tags.request_review
Expand All @@ -43,6 +43,10 @@ def self.enabled?
Setting.trends
end

def skip_review?
Setting.trendable_by_default
end

def self.available_locales
@available_locales ||= I18n.available_locales.map { |locale| locale.to_s.split(/[_-]/).first }.uniq
end
Expand Down
4 changes: 4 additions & 0 deletions app/models/trends/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,8 @@ def rename_set(pipeline, from_key, to_key, set_items)
pipeline.rename(from_key, to_key)
end
end

def skip_review?
Setting.trendable_by_default
end
end
98 changes: 68 additions & 30 deletions app/models/trends/links.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,40 @@ class Trends::Links < Trends::Base
decay_threshold: 1,
}

class Query < Trends::Query
def filtered_for!(account)
@account = account
self
end

def filtered_for(account)
clone.filtered_for!(account)
end

def to_arel
scope = PreviewCard.joins(:trend).reorder(score: :desc)
scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present?
scope = scope.merge(PreviewCardTrend.allowed) if @allowed
scope = scope.offset(@offset) if @offset.present?
scope = scope.limit(@limit) if @limit.present?
scope
end

private

def language_order_clause
Arel::Nodes::Case.new.when(PreviewCardTrend.arel_table[:language].in(preferred_languages)).then(1).else(0)
end

def preferred_languages
if @account&.chosen_languages.present?
@account.chosen_languages
else
@locale
end
end
end

def register(status, at_time = Time.now.utc)
original_status = status.proper

Expand All @@ -28,24 +62,33 @@ def add(preview_card, account_id, at_time = Time.now.utc)
record_used_id(preview_card.id, at_time)
end

def query
Query.new(key_prefix, klass)
end

def refresh(at_time = Time.now.utc)
preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + currently_trending_ids(false, -1)).uniq)
preview_cards = PreviewCard.where(id: (recently_used_ids(at_time) + PreviewCardTrend.pluck(:preview_card_id)).uniq)
calculate_scores(preview_cards, at_time)
end

def request_review
preview_cards = PreviewCard.where(id: currently_trending_ids(false, -1))
PreviewCardTrend.pluck('distinct language').flat_map do |language|
score_at_threshold = PreviewCardTrend.where(language: language, allowed: true).order(rank: :desc).where('rank <= ?', options[:review_threshold]).first&.score || 0
preview_card_trends = PreviewCardTrend.where(language: language, allowed: false).joins(:preview_card)

preview_cards.filter_map do |preview_card|
next unless would_be_trending?(preview_card.id) && !preview_card.trendable? && preview_card.requires_review_notification?
preview_card_trends.filter_map do |trend|
preview_card = trend.preview_card

if preview_card.provider.nil?
preview_card.provider = PreviewCardProvider.create(domain: preview_card.domain, requested_review_at: Time.now.utc)
else
preview_card.provider.touch(:requested_review_at)
end
next unless trend.score > score_at_threshold && !preview_card.trendable? && preview_card.requires_review_notification?

if preview_card.provider.nil?
preview_card.provider = PreviewCardProvider.create(domain: preview_card.domain, requested_review_at: Time.now.utc)
else
preview_card.provider.touch(:requested_review_at)
end

preview_card
preview_card
end
end
end

Expand All @@ -62,10 +105,7 @@ def klass
private

def calculate_scores(preview_cards, at_time)
global_items = []
locale_items = Hash.new { |h, key| h[key] = [] }

preview_cards.each do |preview_card|
items = preview_cards.map do |preview_card|
expected = preview_card.history.get(at_time - 1.day).accounts.to_f
expected = 1.0 if expected.zero?
observed = preview_card.history.get(at_time).accounts.to_f
Expand All @@ -89,26 +129,24 @@ def calculate_scores(preview_cards, at_time)
preview_card.update_columns(max_score: max_score, max_score_at: max_time)
end

decaying_score = max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f))

next unless decaying_score >= options[:decay_threshold]
decaying_score = begin
if max_score.zero? || !valid_locale?(preview_card.language)
0
else
max_score * (0.5**((at_time.to_f - max_time.to_f) / options[:max_score_halflife].to_f))
end
end

global_items << { score: decaying_score, item: preview_card }
locale_items[preview_card.language] << { score: decaying_score, item: preview_card } if valid_locale?(preview_card.language)
[decaying_score, preview_card]
end

replace_items('', global_items)
to_insert = items.filter { |(score, _)| score >= options[:decay_threshold] }
to_delete = items.filter { |(score, _)| score < options[:decay_threshold] }

Trends.available_locales.each do |locale|
replace_items(":#{locale}", locale_items[locale])
PreviewCardTrend.transaction do
PreviewCardTrend.upsert_all(to_insert.map { |(score, preview_card)| { preview_card_id: preview_card.id, score: score, language: preview_card.language, allowed: preview_card.trendable? || false } }, unique_by: :preview_card_id) if to_insert.any?
PreviewCardTrend.where(preview_card_id: to_delete.map { |(_, preview_card)| preview_card.id }).delete_all if to_delete.any?
PreviewCardTrend.connection.exec_update('UPDATE preview_card_trends SET rank = t0.calculated_rank FROM (SELECT id, row_number() OVER w AS calculated_rank FROM preview_card_trends WINDOW w AS (PARTITION BY language ORDER BY score DESC)) t0 WHERE preview_card_trends.id = t0.id')
end
end

def filter_for_allowed_items(items)
items.select { |item| item[:item].trendable? }
end

def would_be_trending?(id)
score(id) > score_at_rank(options[:review_threshold] - 1)
end
end
25 changes: 17 additions & 8 deletions app/models/trends/preview_card_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ def initialize(params)
end

def results
scope = PreviewCard.unscoped
scope = initial_scope

params.each do |key, value|
next if %w(page locale).include?(key.to_s)
next if %w(page).include?(key.to_s)

scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
end
Expand All @@ -26,21 +26,30 @@ def results

private

def initial_scope
PreviewCard.select(PreviewCard.arel_table[Arel.star])
.joins(:trend)
.eager_load(:trend)
.reorder(score: :desc)
end

def scope_for(key, value)
case key.to_s
when 'trending'
trending_scope(value)
when 'locale'
PreviewCardTrend.where(language: value)
else
raise "Unknown filter: #{key}"
end
end

def trending_scope(value)
scope = Trends.links.query

scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present?
scope = scope.allowed if value == 'allowed'

scope.to_arel
case value
when 'allowed'
PreviewCardTrend.allowed
else
PreviewCardTrend.all
end
end
end
25 changes: 17 additions & 8 deletions app/models/trends/status_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ def initialize(params)
end

def results
scope = Status.unscoped.kept
scope = initial_scope

params.each do |key, value|
next if %w(page locale).include?(key.to_s)
next if %w(page).include?(key.to_s)

scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
end
Expand All @@ -26,21 +26,30 @@ def results

private

def initial_scope
Status.select(Status.arel_table[Arel.star])
.joins(:trend)
.eager_load(:trend)
.reorder(score: :desc)
end

def scope_for(key, value)
case key.to_s
when 'trending'
trending_scope(value)
when 'locale'
StatusTrend.where(language: value)
else
raise "Unknown filter: #{key}"
end
end

def trending_scope(value)
scope = Trends.statuses.query

scope = scope.in_locale(@params[:locale].to_s) if @params[:locale].present?
scope = scope.allowed if value == 'allowed'

scope.to_arel
case value
when 'allowed'
StatusTrend.allowed
else
StatusTrend.all
end
end
end

0 comments on commit 45ebdb7

Please sign in to comment.