local visibility scope, chat scope+tags, unlisted tags
parent
0a5eba734e
commit
cf3ec71aa5
|
@ -23,7 +23,7 @@ class HomeController < ApplicationController
|
|||
when 'statuses'
|
||||
status = Status.find_by(id: matches[2])
|
||||
|
||||
if status && (status.public_visibility? || status.unlisted_visibility?)
|
||||
if status && status.distributable?
|
||||
redirect_to(ActivityPub::TagManager.instance.url_for(status))
|
||||
return
|
||||
end
|
||||
|
|
|
@ -202,6 +202,10 @@ module StreamEntriesHelper
|
|||
fa_icon 'globe fw'
|
||||
when 'unlisted'
|
||||
fa_icon 'unlock fw'
|
||||
when 'local'
|
||||
fa_icon 'users fw'
|
||||
when 'chat'
|
||||
fa_icon 'paper-plane fw'
|
||||
when 'private'
|
||||
fa_icon 'lock fw'
|
||||
when 'direct'
|
||||
|
|
|
@ -116,6 +116,10 @@ function statusToTextMentions(state, status) {
|
|||
set = set.add(`@${status.getIn(['account', 'acct'])} `);
|
||||
}
|
||||
|
||||
set = set.union(status.get('tags').filter(
|
||||
tag => tag.get('name') && tag.get('name').startsWith("chat.")
|
||||
).map(tag => `#${tag.get('name')} `));
|
||||
|
||||
return set.union(status.get('mentions').filterNot(mention => mention.get('id') === me).map(mention => `@${mention.get('acct')} `)).join('');
|
||||
};
|
||||
|
||||
|
@ -126,6 +130,10 @@ function apiStatusToTextMentions (state, status) {
|
|||
set = set.add(`@${status.account.acct} `);
|
||||
}
|
||||
|
||||
set = set.union(status.tags.filter(
|
||||
tag => tag.name && tag.name.startsWith("chat.")
|
||||
).map(tag => `#${tag.name} `));
|
||||
|
||||
return set.union(status.mentions.filter(
|
||||
mention => mention.id !== me
|
||||
).map(
|
||||
|
|
|
@ -153,7 +153,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
def attach_tags(status)
|
||||
@tags.each do |tag|
|
||||
status.tags << tag
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.distributable?
|
||||
tag.chatters.find_or_create_by(account_id: status.account) if tag.chat?
|
||||
next unless status.distributable? && !tag.chat?
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at)
|
||||
end
|
||||
|
||||
@mentions.each do |mention|
|
||||
|
@ -181,7 +183,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
hashtag = tag['name'].gsub(/\A#/, '').gsub(':', '.').mb_chars.downcase
|
||||
|
||||
return if !@options[:imported] && hashtag.starts_with?('self.', '_self.', 'local.', '_local.')
|
||||
return if !@options[:imported] && (
|
||||
hashtag.in?(%w(self .self local .local chat.local .chat.local)) ||
|
||||
hashtag.starts_with?('self.', '.self', 'local.', '.local', 'chat.local.', '.chat.local.')
|
||||
)
|
||||
|
||||
if tag['name'].starts_with?('chat.', '.chat.')
|
||||
@params[:visibility] = :chat
|
||||
@params[:thread] = nil
|
||||
end
|
||||
|
||||
hashtag = Tag.where(name: hashtag).first_or_create!(name: hashtag)
|
||||
|
||||
|
|
|
@ -62,9 +62,9 @@ class ActivityPub::TagManager
|
|||
case status.visibility
|
||||
when 'public'
|
||||
[COLLECTIONS[:public]]
|
||||
when 'unlisted', 'private'
|
||||
when 'unlisted', 'private', 'local'
|
||||
[account_followers_url(status.account)]
|
||||
when 'direct', 'limited'
|
||||
when 'direct', 'limited', 'chat'
|
||||
if status.account.silenced?
|
||||
# Only notify followers if the account is locally silenced
|
||||
account_ids = status.active_mentions.pluck(:account_id)
|
||||
|
@ -89,11 +89,11 @@ class ActivityPub::TagManager
|
|||
case status.visibility
|
||||
when 'public'
|
||||
cc << account_followers_url(status.account)
|
||||
when 'unlisted'
|
||||
when 'unlisted', 'local'
|
||||
cc << COLLECTIONS[:public]
|
||||
end
|
||||
|
||||
unless status.direct_visibility? || status.limited_visibility?
|
||||
unless status.direct_visibility? || status.limited_visibility? || status.chat_visibility?
|
||||
if status.account.silenced?
|
||||
# Only notify followers if the account is locally silenced
|
||||
account_ids = status.active_mentions.pluck(:account_id)
|
||||
|
|
|
@ -452,8 +452,9 @@ class Bangtags
|
|||
'group' => :private,
|
||||
|
||||
'unlisted' => :unlisted,
|
||||
'local' => :unlisted,
|
||||
'monsterpit' => :unlisted,
|
||||
|
||||
'local' => :local,
|
||||
'monsterpit' => :local,
|
||||
|
||||
'public' => :public,
|
||||
'world' => :public,
|
||||
|
@ -552,9 +553,25 @@ class Bangtags
|
|||
end
|
||||
|
||||
def add_tags(to_status, *tags)
|
||||
records = []
|
||||
valid_name = /^[[:word:]:_\-]*[[:alpha:]:_·\-][[:word:]:_\-]*$/
|
||||
valid_name = /^[[:word:]:._\-]*[[:alpha:]:._·\-][[:word:]:._\-]*$/
|
||||
tags = tags.select {|t| t.present? && valid_name.match?(t)}.uniq
|
||||
ProcessHashtagsService.new.call(to_status, tags)
|
||||
to_status.save
|
||||
end
|
||||
|
||||
def del_tags(from_status, *tags)
|
||||
valid_name = /^[[:word:]:._\-]*[[:alpha:]:._·\-][[:word:]:._\-]*$/
|
||||
tags = tags.select {|t| t.present? && valid_name.match?(t)}.uniq
|
||||
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
|
||||
name.gsub!(/[:.]+/, '.')
|
||||
next if name.blank? || name == '.'
|
||||
if name.ends_with?('.')
|
||||
filtered_tags = from_status.tags.select { |t| t.name == name || t.name.starts_with?(name) }
|
||||
else
|
||||
filtered_tags = from_status.tags.select { |t| t.name == name }
|
||||
end
|
||||
from_status.tags.destroy(filtered_tags)
|
||||
end
|
||||
from_status.save
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,7 +71,8 @@ class Account < ApplicationRecord
|
|||
|
||||
LOCAL_DOMAINS = ENV.fetch('LOCAL_DOMAINS', '').chomp.split(/\.?\s+/).freeze
|
||||
|
||||
enum protocol: [:ostatus, :activitypub]
|
||||
has_many :chat_accounts, dependent: :destroy, inverse_of: :account
|
||||
has_many :chat_tags, through: :chat_accounts, source: :tag
|
||||
|
||||
validates :username, presence: true
|
||||
|
||||
|
@ -545,6 +546,7 @@ class Account < ApplicationRecord
|
|||
|
||||
before_create :generate_keys
|
||||
before_create :set_domain_from_inbox_url
|
||||
before_create :set_chat_support
|
||||
before_validation :prepare_contents, if: :local?
|
||||
before_validation :prepare_username, on: :create
|
||||
before_destroy :clean_feed_manager
|
||||
|
@ -567,6 +569,11 @@ class Account < ApplicationRecord
|
|||
nil
|
||||
end
|
||||
|
||||
def set_chat_support
|
||||
return unless local?
|
||||
self.supports_chat = true
|
||||
end
|
||||
|
||||
def generate_keys
|
||||
return unless local? && !Rails.env.test?
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: chat_accounts
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8) not null
|
||||
# tag_id :bigint(8) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class ChatAccount < ApplicationRecord
|
||||
belongs_to :account, inverse_of: :chat_accounts
|
||||
belongs_to :tag, inverse_of: :chat_accounts
|
||||
|
||||
validates :account_id, uniqueness: { scope: :tag_id }
|
||||
end
|
|
@ -12,7 +12,7 @@ module StatusThreadingConcern
|
|||
end
|
||||
|
||||
def self_replies(limit)
|
||||
account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted]).reorder(id: :asc).limit(limit)
|
||||
account.statuses.where(in_reply_to_id: id, visibility: [:public, :unlisted, :local]).reorder(id: :asc).limit(limit)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -31,12 +31,12 @@ class FeaturedTag < ApplicationRecord
|
|||
end
|
||||
|
||||
def decrement(deleted_status_id)
|
||||
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at)
|
||||
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted local)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at)
|
||||
end
|
||||
|
||||
def reset_data
|
||||
self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count
|
||||
self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at
|
||||
self.statuses_count = account.statuses.where(visibility: %i(public unlisted local)).tagged_with(tag).count
|
||||
self.last_status_at = account.statuses.where(visibility: %i(public unlisted local)).tagged_with(tag).select(:created_at).first&.created_at
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -53,7 +53,7 @@ class Status < ApplicationRecord
|
|||
|
||||
update_index('statuses#status', :proper) if Chewy.enabled?
|
||||
|
||||
enum visibility: [:public, :unlisted, :private, :direct, :limited], _suffix: :visibility
|
||||
enum visibility: [:public, :unlisted, :private, :direct, :limited, :local, :chat], _suffix: :visibility
|
||||
|
||||
belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
|
||||
|
||||
|
@ -103,7 +103,8 @@ class Status < ApplicationRecord
|
|||
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
|
||||
scope :reblogs, -> { where('statuses.reblog_of_id IS NOT NULL') } # all reblogs
|
||||
scope :with_public_visibility, -> { where(visibility: :public) }
|
||||
scope :public_browsable, -> { where(visibility: [:public, :unlisted]) }
|
||||
scope :public_local_visibility, -> { where(visibility: [:public, :local]) }
|
||||
scope :public_browsable, -> { where(visibility: [:public, :unlisted, :local, :chat]) }
|
||||
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
|
||||
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
|
||||
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
|
||||
|
@ -240,7 +241,7 @@ class Status < ApplicationRecord
|
|||
end
|
||||
|
||||
def distributable?
|
||||
public_visibility? || unlisted_visibility?
|
||||
public_visibility? || unlisted_visibility? || local_visibility?
|
||||
end
|
||||
|
||||
def with_media?
|
||||
|
@ -261,6 +262,11 @@ class Status < ApplicationRecord
|
|||
@emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
|
||||
end
|
||||
|
||||
def chat_tags
|
||||
return @chat_tags if defined?(@chat_tags)
|
||||
@chat_tags = tags.only_chat
|
||||
end
|
||||
|
||||
def mark_for_mass_destruction!
|
||||
@marked_for_mass_destruction = true
|
||||
end
|
||||
|
@ -313,7 +319,7 @@ class Status < ApplicationRecord
|
|||
pattern = sanitize_sql_like(term)
|
||||
pattern = "#{pattern}"
|
||||
scope = Status.where("tsv @@ plainto_tsquery('english', ?)", pattern)
|
||||
query = scope.where(visibility: :public)
|
||||
query = scope.public_local_visibility
|
||||
if account.present?
|
||||
query = query
|
||||
.or(scope.where(account: account))
|
||||
|
@ -333,7 +339,7 @@ class Status < ApplicationRecord
|
|||
end
|
||||
|
||||
def as_home_timeline(account)
|
||||
where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private])
|
||||
where(account: [account] + account.following, visibility: [:public, :unlisted, :local, :private])
|
||||
end
|
||||
|
||||
def as_direct_timeline(account, limit = 20, max_id = nil, since_id = nil, cache_ids = false)
|
||||
|
@ -390,7 +396,7 @@ class Status < ApplicationRecord
|
|||
|
||||
def as_tag_timeline(tag, account = nil, local_only = false, priv = false)
|
||||
query = tag_timeline_scope(account, local_only, priv).tagged_with(tag)
|
||||
apply_timeline_filters(query, account, local_only)
|
||||
apply_timeline_filters(query, account, local_only, true)
|
||||
end
|
||||
|
||||
def as_outbox_timeline(account)
|
||||
|
@ -438,7 +444,7 @@ class Status < ApplicationRecord
|
|||
end
|
||||
|
||||
def permitted_for(target_account, account)
|
||||
visibility = [:public, :unlisted]
|
||||
visibility = [:public, :unlisted, :local]
|
||||
|
||||
if account.nil?
|
||||
query = where(visibility: visibility).not_local_only
|
||||
|
@ -464,7 +470,7 @@ class Status < ApplicationRecord
|
|||
|
||||
def timeline_scope(local_only = false)
|
||||
starting_scope = local_only ? Status.network : Status
|
||||
starting_scope = starting_scope.with_public_visibility
|
||||
starting_scope = local_only ? starting_scope.public_local_visibility : starting_scope.with_public_visibility
|
||||
if Setting.show_reblogs_in_public_timelines
|
||||
starting_scope
|
||||
else
|
||||
|
@ -498,19 +504,19 @@ class Status < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def apply_timeline_filters(query, account, local_only)
|
||||
def apply_timeline_filters(query, account = nil, local_only = false, tag_timeline = false)
|
||||
if account.nil?
|
||||
filter_timeline_default(query)
|
||||
else
|
||||
filter_timeline_for_account(query, account, local_only)
|
||||
filter_timeline_for_account(query, account, local_only, tag_timeline)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_timeline_for_account(query, account, local_only)
|
||||
def filter_timeline_for_account(query, account, local_only, tag_timeline)
|
||||
query = query.not_excluded_by_account(account)
|
||||
query = query.not_domain_blocked_by_account(account) unless local_only
|
||||
query = query.in_chosen_languages(account) if account.chosen_languages.present?
|
||||
query = query.reply_not_excluded_by_account(account)
|
||||
query = query.reply_not_excluded_by_account(account) unless tag_timeline
|
||||
query = query.mention_not_excluded_by_account(account)
|
||||
query.merge(account_silencing_filter(account))
|
||||
end
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
# updated_at :datetime not null
|
||||
# local :boolean default(FALSE), not null
|
||||
# private :boolean default(FALSE), not null
|
||||
# unlisted :boolean default(FALSE), not null
|
||||
# chat :boolean default(FALSE), not null
|
||||
#
|
||||
|
||||
class Tag < ApplicationRecord
|
||||
|
@ -17,6 +19,9 @@ class Tag < ApplicationRecord
|
|||
has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
|
||||
|
||||
has_many :featured_tags, dependent: :destroy, inverse_of: :tag
|
||||
has_many :chat_accounts, dependent: :destroy, inverse_of: :tag
|
||||
has_many :chatters, through: :chat_accounts, source: :account
|
||||
|
||||
has_one :account_tag_stat, dependent: :destroy
|
||||
|
||||
HASHTAG_NAME_RE = '[[:word:]:._\-]*[[:alpha:]:._·\-][[:word:]:._\-]*'
|
||||
|
@ -28,10 +33,12 @@ class Tag < ApplicationRecord
|
|||
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
|
||||
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }
|
||||
|
||||
scope :only_local, -> { where(local: true) }
|
||||
scope :only_global, -> { where(local: false) }
|
||||
scope :only_local, -> { where(local: true, unlisted: false) }
|
||||
scope :only_global, -> { where(local: false, unlisted: false) }
|
||||
scope :only_private, -> { where(private: true) }
|
||||
scope :only_public, -> { where(private: false) }
|
||||
scope :only_unlisted, -> { where(unlisted: true) }
|
||||
scope :only_chat, -> { where(chat: true) }
|
||||
scope :only_public, -> { where(unlisted: false) }
|
||||
|
||||
delegate :accounts_count,
|
||||
:accounts_count=,
|
||||
|
@ -73,9 +80,11 @@ class Tag < ApplicationRecord
|
|||
|
||||
class << self
|
||||
def search_for(term, limit = 5, offset = 0)
|
||||
pattern = sanitize_sql_like(term.strip.gsub(':', '.')) + '%'
|
||||
term = term.strip.gsub(':', '.')
|
||||
pattern = sanitize_sql_like(term) + '%'
|
||||
|
||||
Tag.where('lower(name) like lower(?)', pattern)
|
||||
Tag.only_public.where('lower(name) like lower(?)', pattern)
|
||||
.or(Tag.only_unlisted.where(name: term))
|
||||
.order(:name)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
@ -98,7 +107,11 @@ class Tag < ApplicationRecord
|
|||
end
|
||||
|
||||
def set_scope
|
||||
self.private = true if name.in?(['self', '_self']) || name.starts_with?('self.', '_self.')
|
||||
self.local = true if self.private || name.in?(['local', '_local']) || name.starts_with?('local.', '_local.')
|
||||
self.private = true if name.in?(%w(self .self)) || name.starts_with?('self.', '.self.')
|
||||
self.unlisted = true if self.private || name.starts_with?('.')
|
||||
self.chat = true if name.starts_with?('chat.', '.chat')
|
||||
self.local = true if self.private ||
|
||||
name.in?(%w(local .local chat.local .chat.local)) ||
|
||||
name.starts_with?('local.', '.local', 'chat.local.' '.chat.local')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ class StatusRelationshipsPresenter
|
|||
statuses = statuses.compact
|
||||
status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact
|
||||
conversation_ids = statuses.map(&:conversation_id).compact.uniq
|
||||
pinnable_status_ids = statuses.map(&:proper).select { |s| s.account_id == current_account_id && %w(public unlisted private).include?(s.visibility) }.map(&:id)
|
||||
pinnable_status_ids = statuses.map(&:proper).select { |s| s.account_id == current_account_id && %w(public unlisted local private).include?(s.visibility) }.map(&:id)
|
||||
|
||||
@reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {})
|
||||
@favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {})
|
||||
|
|
|
@ -55,11 +55,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def visibility
|
||||
# This visibility is masked behind "private"
|
||||
# to avoid API changes because there are no
|
||||
# UX differences
|
||||
if object.limited_visibility?
|
||||
'private'
|
||||
elsif object.local_visibility? || object.chat_visibility?
|
||||
'unlisted'
|
||||
else
|
||||
object.visibility
|
||||
end
|
||||
|
@ -121,7 +120,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||
current_user? &&
|
||||
current_user.account_id == object.account_id &&
|
||||
!object.reblog? &&
|
||||
%w(public unlisted private).include?(object.visibility)
|
||||
%w(public unlisted local private).include?(object.visibility)
|
||||
end
|
||||
|
||||
def source_requested?
|
||||
|
|
|
@ -6,7 +6,7 @@ class FanOutOnWriteService < BaseService
|
|||
def call(status)
|
||||
raise Mastodon::RaceConditionError if status.visibility.nil?
|
||||
|
||||
deliver_to_self(status) if status.account.local?
|
||||
deliver_to_self(status) if status.account.local? && !status.chat_visibility?
|
||||
|
||||
render_anonymous_payload(status)
|
||||
|
||||
|
@ -16,28 +16,35 @@ class FanOutOnWriteService < BaseService
|
|||
deliver_to_own_conversation(status)
|
||||
elsif status.limited_visibility?
|
||||
deliver_to_mentioned_followers(status)
|
||||
elsif status.chat_visibility?
|
||||
deliver_to_mentioned_followers(status)
|
||||
deliver_to_hashtags(status)
|
||||
elsif status.local_visibility?
|
||||
deliver_to_followers(status)
|
||||
deliver_to_lists(status)
|
||||
deliver_to_local(status) unless filtered?(status)
|
||||
else
|
||||
deliver_to_followers(status)
|
||||
deliver_to_lists(status)
|
||||
|
||||
return if status.reblog? && !Setting.show_reblogs_in_public_timelines
|
||||
return if filtered?(status) || (status.reblog? && filtered?(status.reblog))
|
||||
|
||||
if !status.reblog? && status.distributable?
|
||||
deliver_to_hashtags(status)
|
||||
deliver_to_public(status) if status.curated
|
||||
end
|
||||
|
||||
if status.relayed?
|
||||
status = Status.find(status.reblog_of_id)
|
||||
return if filtered?(status)
|
||||
render_anonymous_payload(status)
|
||||
end
|
||||
|
||||
return unless status.network? && status.public_visibility? && !status.reblog
|
||||
|
||||
deliver_to_local(status)
|
||||
end
|
||||
|
||||
return if status.reblog? && !Setting.show_reblogs_in_public_timelines
|
||||
return if filtered?(status)
|
||||
|
||||
if !status.reblog? && status.distributable?
|
||||
deliver_to_hashtags(status)
|
||||
deliver_to_public(status) if status.curated
|
||||
end
|
||||
|
||||
if status.relayed?
|
||||
status = Status.find(status.reblog_of_id)
|
||||
return if filtered?(status)
|
||||
render_anonymous_payload(status)
|
||||
end
|
||||
|
||||
return unless status.network? && status.public_visibility? && !status.reblog
|
||||
|
||||
deliver_to_local(status)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -4,6 +4,15 @@ class PostStatusService < BaseService
|
|||
include Redisable
|
||||
|
||||
MIN_SCHEDULE_OFFSET = 5.minutes.freeze
|
||||
VISIBILITY_RANK = {
|
||||
'public' => 0,
|
||||
'unlisted' => 1,
|
||||
'local' => 1,
|
||||
'private' => 2,
|
||||
'direct' => 3,
|
||||
'limited' => 3,
|
||||
'chat' => 4
|
||||
}
|
||||
|
||||
# Post a text status update, fetch and notify remote users mentioned
|
||||
# @param [Account] account Account from which to post
|
||||
|
@ -30,9 +39,10 @@ class PostStatusService < BaseService
|
|||
@text = @options[:text] || ''
|
||||
@footer = @options[:footer]
|
||||
@in_reply_to = @options[:thread]
|
||||
@tags = @options[:tags]
|
||||
@tags = @options[:tags] || []
|
||||
@local_only = @options[:local_only]
|
||||
@sensitive = (@account.force_sensitive? ? true : @options[:sensitive])
|
||||
@preloaded_tags = @options[:preloaded_tags] || []
|
||||
|
||||
return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
|
||||
|
||||
|
@ -55,9 +65,54 @@ class PostStatusService < BaseService
|
|||
private
|
||||
|
||||
def set_footer_from_i_am
|
||||
return if @footer.nil? || @options[:no_footer]
|
||||
name = @account.vars['_they:are']
|
||||
return if name.blank?
|
||||
@account.vars["_they:are:#{name}"]
|
||||
@footer = @account.vars["_they:are:#{name}"]
|
||||
end
|
||||
|
||||
def set_initial_visibility
|
||||
@visibility = @options[:visibility] || @account.user_default_visibility
|
||||
end
|
||||
|
||||
def limit_visibility_if_silenced
|
||||
@visibility = :unlisted if @visibility.in?([nil, 'public']) && @account.silenced? || @account.force_unlisted
|
||||
end
|
||||
|
||||
def limit_visibility_to_reply
|
||||
return if @in_reply_to.nil?
|
||||
@visibility = @in_reply_to.visibility if @visibility.nil? ||
|
||||
VISIBILITY_RANK[@visibility] < VISIBILITY_RANK[@in_reply_to.visibility]
|
||||
end
|
||||
|
||||
def set_local_only
|
||||
@local_only = true if @account.user_always_local_only? || @in_reply_to&.local_only
|
||||
end
|
||||
|
||||
def set_chat
|
||||
if @in_reply_to.present?
|
||||
unless @in_reply_to.chat_tags.blank?
|
||||
@preloaded_tags |= @in_reply_to.chat_tags
|
||||
@visibility = :chat
|
||||
@in_reply_to = nil
|
||||
end
|
||||
elsif @tags.present? && @tags.any? { |t| t.start_with?('chat.', '.chat.') }
|
||||
@visibility = :chat
|
||||
@local_only = true if @tags.any? { |t| t.in?(%w(chat.local .chat.local)) || t.start_with?('chat.local.', '.chat.local.') }
|
||||
end
|
||||
end
|
||||
|
||||
# move tags out of body so we can format them later
|
||||
def extract_tags
|
||||
chunks = []
|
||||
@text.split(/^((?:#[\w:._·\-]*\s*)+)/).each do |chunk|
|
||||
if chunk.start_with?('#')
|
||||
@tags |= chunk[1..-1].split(/\s+/)
|
||||
else
|
||||
chunks << chunk
|
||||
end
|
||||
end
|
||||
@text = chunks.join
|
||||
end
|
||||
|
||||
def preprocess_attributes!
|
||||
|
@ -66,18 +121,17 @@ class PostStatusService < BaseService
|
|||
@text = @media.find(&:video?) ? '📹' : '🖼' if @media.size > 0
|
||||
end
|
||||
|
||||
@footer = set_footer_from_i_am if @footer.nil? && !@options[:no_footer]
|
||||
set_footer_from_i_am
|
||||
extract_tags
|
||||
set_chat
|
||||
set_local_only
|
||||
|
||||
@visibility = @options[:visibility] || @account.user_default_visibility
|
||||
@visibility = :unlisted if @visibility.in?([nil, 'public']) && @account.silenced? || @account.force_unlisted
|
||||
|
||||
if @in_reply_to.present? && @in_reply_to.visibility.present?
|
||||
v = %w(public unlisted private direct limited)
|
||||
@visibility = @in_reply_to.visibility if @visibility.nil? || v.index(@visibility) < v.index(@in_reply_to.visibility)
|
||||
unless @visibility == :chat
|
||||
set_initial_visibility
|
||||
limit_visibility_if_silenced
|
||||
limit_visibility_to_reply
|
||||
end
|
||||
|
||||
@local_only = true if @account.user_always_local_only? || @in_reply_to&.local_only
|
||||
|
||||
@sensitive = (@account.user_defaults_to_sensitive? || @options[:spoiler_text].present?) if @sensitive.nil?
|
||||
|
||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||
|
@ -94,7 +148,7 @@ class PostStatusService < BaseService
|
|||
@status = @account.statuses.create!(status_attributes)
|
||||
end
|
||||
|
||||
process_hashtags_service.call(@status, @tags)
|
||||
process_hashtags_service.call(@status, @tags, @preloaded_tags)
|
||||
process_mentions_service.call(@status)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,24 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProcessHashtagsService < BaseService
|
||||
def call(status, tags = [])
|
||||
def call(status, tags = [], preloaded_tags = [])
|
||||
status.tags |= preloaded_tags unless preloaded_tags.blank?
|
||||
|
||||
if status.local?
|
||||
tags = Extractor.extract_hashtags(status.text) | (tags.nil? ? [] : tags)
|
||||
end
|
||||
records = []
|
||||
|
||||
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
|
||||
name = name.gsub(/[:.]+/, '.')
|
||||
next if name.blank?
|
||||
component_indices = name.size.times.select {|i| name[i] == '.'}
|
||||
component_indices << name.size - 1
|
||||
name.gsub!(/[:.]+/, '.')
|
||||
next if name.blank? || name == '.'
|
||||
|
||||
chat = name.starts_with?('chat.', '.chat.')
|
||||
if chat
|
||||
component_indices = [name.size - 1]
|
||||
else
|
||||
component_indices = 1.upto(name.size).select { |i| name[i] == '.' }
|
||||
component_indices << name.size - 1
|
||||
end
|
||||
|
||||
component_indices.take(6).each_with_index do |i, nest|
|
||||
frag = (nest != 5) ? name[0..i] : name
|
||||
tag = Tag.where(name: frag).first_or_create(name: frag)
|
||||
|
||||
tag.chatters.find_or_create_by(id: status.account_id) if chat
|
||||
|
||||
next if status.tags.include?(tag)
|
||||
status.tags << tag
|
||||
next if tag.local || tag.private
|
||||
next if tag.unlisted || component_indices.size > 1
|
||||
|
||||
records << tag
|
||||
TrendingTags.record_use!(tag, status.account, status.created_at) if status.distributable?
|
||||
|
|
|
@ -6,7 +6,7 @@ class StatusPinValidator < ActiveModel::Validator
|
|||
def validate(pin)
|
||||
pin.errors.add(:base, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog?
|
||||
pin.errors.add(:base, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id
|
||||
pin.errors.add(:base, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted private).include?(pin.status.visibility)
|
||||
pin.errors.add(:base, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted local private).include?(pin.status.visibility)
|
||||
pin.errors.add(:base, I18n.t('statuses.pin_errors.limit')) if pin.account.status_pins.count >= MAX_PINNED && pin.account.local?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ class ActivityPub::DistributePollUpdateWorker
|
|||
end
|
||||
end
|
||||
|
||||
@inboxes.concat(@account.followers.inboxes) unless @status.direct_visibility?
|
||||
@inboxes.concat(@account.followers.inboxes) unless @status.direct_visibility? || @status.chat_visibility?
|
||||
@inboxes.uniq!
|
||||
@inboxes.compact!
|
||||
@inboxes
|
||||
|
|
|
@ -23,7 +23,7 @@ class ActivityPub::DistributionWorker
|
|||
private
|
||||
|
||||
def skip_distribution?
|
||||
@status.direct_visibility? || @status.limited_visibility?
|
||||
@status.direct_visibility? || @status.limited_visibility? || @status.chat_visibility?
|
||||
end
|
||||
|
||||
def relayable?
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class AddChatAndLocalIndexesToStatuses < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
def change
|
||||
add_index :statuses, [:account_id, :id, :visibility], where: 'visibility IN (0, 1, 2, 4, 5)', algorithm: :concurrently
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
class AddUnlistedToTags < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
def change
|
||||
safety_assured {
|
||||
add_column :tags, :unlisted, :boolean, default: false, null: false
|
||||
add_index :tags, :unlisted, where: :unlisted, algorithm: :concurrently
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class MakePrivateTagsUnlisted < ActiveRecord::Migration[5.2]
|
||||
def up
|
||||
Tag.where(private: true).in_batches.update_all(unlisted: true)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
class AddChatToTags < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
def change
|
||||
safety_assured {
|
||||
add_column :tags, :chat, :boolean, default: false, null: false
|
||||
add_index :tags, :chat, where: :chat, algorithm: :concurrently
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
class CreateChatAccounts < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :chat_accounts do |t|
|
||||
t.references :account, foreign_key: { on_delete: :cascade }, null: false
|
||||
t.references :tag, foreign_key: { on_delete: :cascade }, null: false
|
||||
t.index [:account_id, :tag_id], unique: true
|
||||
t.index [:tag_id, :account_id]
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
17
db/schema.rb
17
db/schema.rb
|
@ -208,6 +208,17 @@ ActiveRecord::Schema.define(version: 2019_05_21_003909) do
|
|||
t.index ["status_id"], name: "index_bookmarks_on_status_id"
|
||||
end
|
||||
|
||||
create_table "chat_accounts", force: :cascade do |t|
|
||||
t.bigint "account_id", null: false
|
||||
t.bigint "tag_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "tag_id"], name: "index_chat_accounts_on_account_id_and_tag_id", unique: true
|
||||
t.index ["account_id"], name: "index_chat_accounts_on_account_id"
|
||||
t.index ["tag_id", "account_id"], name: "index_chat_accounts_on_tag_id_and_account_id"
|
||||
t.index ["tag_id"], name: "index_chat_accounts_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "conversation_mutes", force: :cascade do |t|
|
||||
t.bigint "conversation_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
|
@ -687,8 +698,12 @@ ActiveRecord::Schema.define(version: 2019_05_21_003909) do
|
|||
t.datetime "updated_at", null: false
|
||||
t.boolean "local", default: false, null: false
|
||||
t.boolean "private", default: false, null: false
|
||||
t.boolean "unlisted", default: false, null: false
|
||||
t.boolean "chat", default: false, null: false
|
||||
t.index "lower((name)::text) text_pattern_ops", name: "hashtag_search_index"
|
||||
t.index ["chat"], name: "index_tags_on_chat", where: "chat"
|
||||
t.index ["name"], name: "index_tags_on_name", unique: true
|
||||
t.index ["unlisted"], name: "index_tags_on_unlisted", where: "unlisted"
|
||||
end
|
||||
|
||||
create_table "tombstones", force: :cascade do |t|
|
||||
|
@ -791,6 +806,8 @@ ActiveRecord::Schema.define(version: 2019_05_21_003909) do
|
|||
add_foreign_key "blocks", "accounts", name: "fk_4269e03e65", on_delete: :cascade
|
||||
add_foreign_key "bookmarks", "accounts", on_delete: :cascade
|
||||
add_foreign_key "bookmarks", "statuses", on_delete: :cascade
|
||||
add_foreign_key "chat_accounts", "accounts", on_delete: :cascade
|
||||
add_foreign_key "chat_accounts", "tags", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
|
||||
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
|
||||
add_foreign_key "custom_filters", "accounts", on_delete: :cascade
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Fabricator(:chat_account) do
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ChatAccount, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
Loading…
Reference in New Issue