2019-05-03 08:17:27 +10:00
# frozen_string_literal: true
2013-02-05 14:16:51 -05:00
class Topic < ActiveRecord :: Base
2017-05-02 15:13:33 +05:30
class UserExists < StandardError
end
2023-12-15 23:46:04 +08:00
2020-07-31 09:52:19 -06:00
class NotAllowed < StandardError
end
2013-02-05 14:16:51 -05:00
include RateLimiter :: OnCreateRecord
2014-04-28 10:31:51 +02:00
include HasCustomFields
2013-05-07 14:39:01 +10:00
include Trashable
2017-08-15 17:46:57 +02:00
include Searchable
2015-02-25 20:53:21 +01:00
include LimitedEdit
2013-08-30 09:12:44 +00:00
extend Forwardable
2022-02-08 20:55:32 -07:00
EXTERNAL_ID_MAX_LENGTH = 50
2022-01-11 10:57:21 +08:00
self . ignored_columns = [
"avg_time" , # TODO(2021-01-04): remove
"image_url" , # TODO(2021-06-01): remove
]
2013-08-30 09:12:44 +00:00
def_delegator :featured_users , :user_ids , :featured_user_ids
def_delegator :featured_users , :choose , :feature_topic_users
def_delegator :notifier , :watch! , :notify_watch!
2015-12-14 23:17:09 +01:00
def_delegator :notifier , :track! , :notify_tracking!
2013-08-30 09:12:44 +00:00
def_delegator :notifier , :regular! , :notify_regular!
2015-12-14 23:17:09 +01:00
def_delegator :notifier , :mute! , :notify_muted!
2013-08-30 09:12:44 +00:00
def_delegator :notifier , :toggle_mute , :toggle_mute
2013-02-05 14:16:51 -05:00
2023-05-31 19:32:06 +05:30
attr_accessor :allowed_user_ids , :allowed_group_ids , :tags_changed , :includes_destination_category
2014-05-12 09:32:49 +02:00
2017-10-17 22:37:13 +02:00
def self . max_fancy_title_length
400
end
2020-05-05 09:07:50 +01:00
def self . share_thumbnail_size
[ 1024 , 1024 ]
end
def self . thumbnail_sizes
2020-05-23 00:56:13 -04:00
[ self . share_thumbnail_size ] + DiscoursePluginRegistry . topic_thumbnail_sizes
2020-05-05 09:07:50 +01:00
end
2020-05-23 00:56:13 -04:00
def thumbnail_job_redis_key ( sizes )
"generate_topic_thumbnail_enqueue_ #{ id } _ #{ sizes . inspect } "
2020-05-05 09:07:50 +01:00
end
def filtered_topic_thumbnails ( extra_sizes : [] )
return nil unless original = image_upload
2020-05-18 12:04:29 +01:00
return nil unless original . read_attribute ( :width ) && original . read_attribute ( :height )
2020-05-05 09:07:50 +01:00
thumbnail_sizes = Topic . thumbnail_sizes + extra_sizes
topic_thumbnails . filter do | record |
thumbnail_sizes . include? ( [ record . max_width , record . max_height ] )
2023-01-09 12:20:10 +00:00
end
2020-05-05 09:07:50 +01:00
end
def thumbnail_info ( enqueue_if_missing : false , extra_sizes : [] )
return nil unless original = image_upload
2023-12-18 09:22:50 -07:00
return nil if original . filesize >= SiteSetting . max_image_size_kb . to_i . kilobytes
2020-05-18 12:04:29 +01:00
return nil unless original . read_attribute ( :width ) && original . read_attribute ( :height )
2020-05-05 09:07:50 +01:00
infos = []
infos << { # Always add original
max_width : nil ,
max_height : nil ,
width : original . width ,
height : original . height ,
url : original . url ,
}
records = filtered_topic_thumbnails ( extra_sizes : extra_sizes )
records . each do | record |
next unless record . optimized_image # Only serialize successful thumbnails
infos << {
max_width : record . max_width ,
max_height : record . max_height ,
width : record . optimized_image &. width ,
height : record . optimized_image &. height ,
url : record . optimized_image &. url ,
}
end
thumbnail_sizes = Topic . thumbnail_sizes + extra_sizes
if SiteSetting . create_thumbnails && enqueue_if_missing &&
records . length < thumbnail_sizes . length &&
2022-06-17 11:07:32 +01:00
Discourse . redis . set ( thumbnail_job_redis_key ( extra_sizes ), 1 , nx : true , ex : 1 . minute )
2020-05-05 09:07:50 +01:00
Jobs . enqueue ( :generate_topic_thumbnails , { topic_id : id , extra_sizes : extra_sizes })
end
2020-12-30 18:13:13 +00:00
infos . each { | i | i [ :url ] = UrlHelper . cook_url ( i [ :url ] , secure : original . secure? , local : true ) }
2020-05-15 13:35:20 +01:00
2020-05-05 09:07:50 +01:00
infos . sort_by! { | i | - i [ :width ] * i [ :height ] }
end
def generate_thumbnails! ( extra_sizes : [] )
return nil unless SiteSetting . create_thumbnails
return nil unless original = image_upload
2023-02-16 10:40:11 +01:00
return nil if original . filesize >= SiteSetting . max_image_size_kb . kilobytes
2020-05-05 09:07:50 +01:00
return nil unless original . width && original . height
2020-07-06 15:30:57 -06:00
extra_sizes = [] unless extra_sizes . kind_of? ( Array )
2020-05-05 09:07:50 +01:00
( Topic . thumbnail_sizes + extra_sizes ) . each do | dim |
TopicThumbnail . find_or_create_for! ( original , max_width : dim [ 0 ] , max_height : dim [ 1 ] )
end
end
2020-07-06 10:59:21 +01:00
def image_url ( enqueue_if_missing : false )
2020-05-05 09:07:50 +01:00
thumbnail =
topic_thumbnails . detect do | record |
record . max_width == Topic . share_thumbnail_size [ 0 ] &&
record . max_height == Topic . share_thumbnail_size [ 1 ]
end
2020-05-15 13:35:20 +01:00
2020-07-06 10:59:21 +01:00
if thumbnail . nil? && image_upload && SiteSetting . create_thumbnails &&
2020-07-16 14:30:23 -06:00
image_upload . filesize < SiteSetting . max_image_size_kb . kilobytes &&
2020-08-13 16:54:28 -06:00
image_upload . read_attribute ( :width ) && image_upload . read_attribute ( :height ) &&
2020-07-06 10:59:21 +01:00
enqueue_if_missing &&
Discourse . redis . set ( thumbnail_job_redis_key ( [] ), 1 , nx : true , ex : 1 . minute )
Jobs . enqueue ( :generate_topic_thumbnails , { topic_id : id })
end
2020-05-15 13:35:20 +01:00
raw_url = thumbnail &. optimized_image &. url || image_upload &. url
2020-12-30 18:13:13 +00:00
UrlHelper . cook_url ( raw_url , secure : image_upload &. secure? , local : true ) if raw_url
2020-05-05 09:07:50 +01:00
end
2013-08-26 10:41:56 +00:00
def featured_users
@featured_users ||= TopicFeaturedUsers . new ( self )
2013-03-12 12:33:42 -04:00
end
2013-02-05 14:16:51 -05:00
2013-07-09 15:20:18 -04:00
def trash! ( trashed_by = nil )
2022-12-06 05:56:16 +08:00
trigger_event = false
2018-02-08 16:36:39 -05:00
if deleted_at . nil?
2021-02-16 17:45:12 +02:00
update_category_topic_count_by ( - 1 ) if visible?
2018-02-08 16:36:39 -05:00
CategoryTagStat . topic_deleted ( self ) if self . tags . present?
2022-12-06 05:56:16 +08:00
trigger_event = true
2018-02-08 16:36:39 -05:00
end
2022-12-06 05:56:16 +08:00
2013-07-09 15:20:18 -04:00
super ( trashed_by )
2022-12-06 05:56:16 +08:00
DiscourseEvent . trigger ( :topic_trashed , self ) if trigger_event
2017-04-24 23:59:04 +05:30
self . topic_embed . trash! if has_topic_embed?
2013-05-07 14:39:01 +10:00
end
2019-01-03 12:03:01 -05:00
def recover! ( recovered_by = nil )
2022-12-06 05:56:16 +08:00
trigger_event = false
2018-02-08 16:36:39 -05:00
unless deleted_at . nil?
2021-02-16 17:45:12 +02:00
update_category_topic_count_by ( 1 ) if visible?
2018-02-08 16:36:39 -05:00
CategoryTagStat . topic_recovered ( self ) if self . tags . present?
2022-12-06 05:56:16 +08:00
trigger_event = true
2018-02-08 16:36:39 -05:00
end
2019-01-03 12:03:01 -05:00
# Note parens are required because superclass doesn't take `recovered_by`
super ()
2022-12-06 05:56:16 +08:00
DiscourseEvent . trigger ( :topic_recovered , self ) if trigger_event
2017-04-24 23:59:04 +05:30
unless ( topic_embed = TopicEmbed . with_deleted . find_by_topic_id ( id )) . nil?
topic_embed . recover!
end
2013-05-07 14:39:01 +10:00
end
2013-02-06 12:13:41 +11:00
2013-02-05 14:16:51 -05:00
rate_limit :default_rate_limiter
rate_limit :limit_topics_per_day
rate_limit :limit_private_messages_per_day
2014-08-11 16:55:26 -04:00
validates :title ,
if : Proc . new { | t | t . new_record? || t . title_changed? },
2014-08-01 17:28:00 -04:00
presence : true ,
2013-06-04 17:58:25 -04:00
topic_title_length : true ,
2017-01-09 16:48:10 +08:00
censored_words : true ,
2019-10-02 10:38:34 +10:00
watched_words : true ,
2013-05-22 21:52:12 -07:00
quality_title : {
unless : :private_message? ,
},
2018-02-17 10:40:30 +05:30
max_emojis : true ,
2013-05-22 21:52:12 -07:00
unique_among : {
unless :
Proc . new { | t | ( SiteSetting . allow_duplicate_topic_titles? || t . private_message? ) },
message : :has_already_been_used ,
allow_blank : true ,
case_sensitive : false ,
2020-06-18 17:19:47 +02:00
collection :
Proc . new { | t |
if SiteSetting . allow_duplicate_topic_titles_category?
Topic . listable_topics . where ( "category_id = ?" , t . category_id )
2023-01-09 12:20:10 +00:00
else
2020-06-18 17:19:47 +02:00
Topic . listable_topics
2023-01-09 12:20:10 +00:00
end
2020-06-18 17:19:47 +02:00
},
}
2013-02-05 14:16:51 -05:00
2014-04-24 09:19:59 +10:00
validates :category_id ,
presence : true ,
exclusion : {
in : Proc . new { [ SiteSetting . uncategorized_category_id ] },
},
if :
Proc . new { | t |
( t . new_record? || t . category_id_changed? ) &&
! SiteSetting . allow_uncategorized_topics && ( t . archetype . nil? || t . regular? )
}
2013-10-24 10:05:51 +11:00
2017-12-11 16:27:33 +08:00
validates :featured_link , allow_nil : true , url : true
2016-12-05 13:31:43 +01:00
validate if : :featured_link do
2021-08-05 13:38:39 +04:00
if featured_link_changed? && ! Guardian . new ( user ) . can_edit_featured_link? ( category_id )
errors . add ( :featured_link )
end
2016-12-05 13:31:43 +01:00
end
2013-10-08 14:40:31 -04:00
2022-02-08 20:55:32 -07:00
validates :external_id ,
allow_nil : true ,
uniqueness : {
case_sensitive : false ,
},
length : {
maximum : EXTERNAL_ID_MAX_LENGTH ,
},
format : {
with : /\A[\w-]+\z/ ,
}
2013-05-31 15:22:34 -04:00
before_validation do
2013-05-22 21:52:12 -07:00
self . title = TextCleaner . clean_title ( TextSentinel . title_sentinel ( title ) . text ) if errors [
:title
]. empty?
2017-11-22 14:53:35 -05:00
self . featured_link = self . featured_link . strip . presence if self . featured_link
2013-05-22 21:52:12 -07:00
end
2013-02-05 14:16:51 -05:00
belongs_to :category
2016-07-06 15:56:40 -04:00
has_many :category_users , through : :category
2013-02-05 14:16:51 -05:00
has_many :posts
2022-01-06 08:56:05 +10:00
2022-05-24 13:52:42 +10:00
# NOTE: To get all Post _and_ Topic bookmarks for a topic by user,
# use the Bookmark.for_user_in_topic scope.
has_many :bookmarks , as : :bookmarkable
2022-01-06 08:56:05 +10:00
2014-07-28 22:50:49 +02:00
has_many :ordered_posts , -> { order ( post_number : :asc ) }, class_name : "Post"
2013-02-05 14:16:51 -05:00
has_many :topic_allowed_users
2013-05-02 15:15:17 +10:00
has_many :topic_allowed_groups
2020-07-10 12:05:55 +03:00
has_many :incoming_email
2013-05-02 15:15:17 +10:00
2015-12-23 11:09:17 +11:00
has_many :group_archived_messages , dependent : :destroy
has_many :user_archived_messages , dependent : :destroy
2013-05-02 15:15:17 +10:00
has_many :allowed_groups , through : :topic_allowed_groups , source : :group
2017-08-31 12:06:56 +08:00
has_many :allowed_group_users , through : :allowed_groups , source : :users
2013-02-05 14:16:51 -05:00
has_many :allowed_users , through : :topic_allowed_users , source : :user
2013-03-28 13:02:59 -04:00
2018-02-08 16:36:39 -05:00
has_many :topic_tags
has_many :tags , through : :topic_tags , dependent : :destroy # dependent destroy applies to the topic_tags records
2016-07-06 15:56:40 -04:00
has_many :tag_users , through : :tags
2016-05-04 14:02:47 -04:00
2013-12-24 00:50:36 +01:00
has_one :top_topic
2018-03-13 15:59:12 -04:00
has_one :shared_draft , dependent : :destroy
2020-04-08 12:52:36 -04:00
has_one :published_page
2018-03-13 15:59:12 -04:00
2013-02-05 14:16:51 -05:00
belongs_to :user
belongs_to :last_poster , class_name : "User" , foreign_key : :last_post_user_id
belongs_to :featured_user1 , class_name : "User" , foreign_key : :featured_user1_id
belongs_to :featured_user2 , class_name : "User" , foreign_key : :featured_user2_id
belongs_to :featured_user3 , class_name : "User" , foreign_key : :featured_user3_id
belongs_to :featured_user4 , class_name : "User" , foreign_key : :featured_user4_id
has_many :topic_users
2021-02-04 11:27:34 +11:00
has_many :dismissed_topic_users
2013-02-05 14:16:51 -05:00
has_many :topic_links
has_many :topic_invites
has_many :invites , through : :topic_invites , source : :invite
2017-05-11 18:23:18 -04:00
has_many :topic_timers , dependent : :destroy
2019-01-03 12:03:01 -05:00
has_many :reviewables
2019-12-09 11:15:47 -08:00
has_many :user_profiles
2013-02-05 14:16:51 -05:00
2017-04-15 12:11:02 +08:00
has_one :user_warning
2017-08-31 12:06:56 +08:00
has_one :first_post , -> { where post_number : 1 }, class_name : "Post"
2020-07-09 10:08:04 +08:00
has_one :topic_search_data
2017-04-24 23:59:04 +05:30
has_one :topic_embed , dependent : :destroy
2020-11-02 12:18:48 +05:30
has_one :linked_topic , dependent : :destroy
2017-04-24 23:59:04 +05:30
2020-05-05 09:07:50 +01:00
belongs_to :image_upload , class_name : "Upload"
has_many :topic_thumbnails , through : :image_upload
2013-02-05 14:16:51 -05:00
# When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data
2019-11-14 11:16:13 +11:00
attr_accessor :category_user_data
2021-02-04 11:27:34 +11:00
attr_accessor :dismissed
2015-06-22 18:09:08 +10:00
2013-02-05 14:16:51 -05:00
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
2014-05-12 09:32:49 +02:00
attr_accessor :participants
2023-05-31 19:32:06 +05:30
attr_accessor :participant_groups
2013-04-02 16:52:51 -04:00
attr_accessor :topic_list
2013-10-17 17:44:56 +11:00
attr_accessor :include_last_poster
2014-07-03 14:43:24 -04:00
attr_accessor :import_mode # set to true to optimize creation and save for imports
2013-02-05 14:16:51 -05:00
# The regular order
2014-05-07 19:04:39 +02:00
scope :topic_list_order , -> { order ( "topics.bumped_at desc" ) }
2013-02-05 14:16:51 -05:00
# Return private message topics
2014-05-07 19:04:39 +02:00
scope :private_messages , -> { where ( archetype : Archetype . private_message ) }
2013-02-05 14:16:51 -05:00
2021-10-26 10:16:38 +03:00
PRIVATE_MESSAGES_SQL_USER = <<~ SQL
2020-03-23 11:02:24 +00:00
SELECT topic_id
FROM topic_allowed_users
WHERE user_id = :user_id
2021-10-26 10:16:38 +03:00
SQL
PRIVATE_MESSAGES_SQL_GROUP = <<~ SQL
2020-03-23 11:02:24 +00:00
SELECT tg . topic_id
FROM topic_allowed_groups tg
JOIN group_users gu ON gu . user_id = :user_id AND gu . group_id = tg . group_id
SQL
2021-10-26 10:16:38 +03:00
scope :private_messages_for_user ,
2023-11-29 16:38:07 +11:00
-> ( user ) do
2021-10-26 10:16:38 +03:00
private_messages . where (
"topics.id IN ( #{ PRIVATE_MESSAGES_SQL_USER } )
OR topics.id IN ( #{ PRIVATE_MESSAGES_SQL_GROUP } )" ,
user_id : user . id ,
)
2023-11-29 16:38:07 +11:00
end
2020-03-23 11:02:24 +00:00
2016-05-30 19:48:46 +02:00
scope :listable_topics , -> { where ( "topics.archetype <> ?" , Archetype . private_message ) }
2013-02-05 14:16:51 -05:00
2014-02-28 15:07:55 +11:00
scope :by_newest , -> { order ( "topics.created_at desc, topics.id desc" ) }
2013-02-27 19:36:12 -08:00
2013-07-03 22:04:22 +02:00
scope :visible , -> { where ( visible : true ) }
2013-05-30 13:23:40 +02:00
2015-01-29 21:10:26 +05:30
scope :created_since , lambda { | time_ago | where ( "topics.created_at > ?" , time_ago ) }
2013-05-30 13:23:40 +02:00
2019-11-19 07:24:18 -07:00
scope :exclude_scheduled_bump_topics , -> { where . not ( id : TopicTimer . scheduled_bump_topics ) }
2014-05-07 19:04:39 +02:00
scope :secured ,
lambda { | guardian = nil |
2013-06-08 23:52:06 +10:00
ids = guardian . secure_category_ids if guardian
2023-01-09 12:20:10 +00:00
2013-06-12 13:43:59 -04:00
# Query conditions
2014-05-07 19:04:39 +02:00
condition =
if ids . present?
2016-05-02 15:26:23 +02:00
[ "NOT read_restricted OR id IN (:cats)" , cats : ids ]
2014-05-07 19:04:39 +02:00
else
2016-05-02 15:26:23 +02:00
[ "NOT read_restricted" ]
2014-05-07 19:04:39 +02:00
end
2023-01-09 12:20:10 +00:00
2016-05-02 15:26:23 +02:00
where (
"topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{ condition [ 0 ] } )" ,
condition [ 1 ] ,
)
2013-06-12 13:43:59 -04:00
}
2023-01-09 12:20:10 +00:00
2018-08-10 08:50:05 +08:00
scope :in_category_and_subcategories ,
lambda { | category_id |
2020-04-09 15:42:24 +03:00
if category_id
where ( "topics.category_id IN (?)" , Category . subcategory_ids ( category_id . to_i ))
2023-01-09 12:20:10 +00:00
end
2018-08-10 08:50:05 +08:00
}
2018-03-27 14:00:08 +05:30
scope :with_subtype , -> ( subtype ) { where ( "topics.subtype = ?" , subtype ) }
2014-10-10 18:21:44 +02:00
attr_accessor :ignore_category_auto_close
2014-10-27 22:06:43 +01:00
attr_accessor :skip_callbacks
2022-02-09 10:37:38 +02:00
attr_accessor :advance_draft
2014-10-10 18:21:44 +02:00
2014-10-27 22:06:43 +01:00
before_create { initialize_default_values }
2013-02-05 14:16:51 -05:00
after_create do
2013-12-13 17:04:45 +11:00
unless skip_callbacks
changed_to_category ( category )
2022-02-09 10:37:38 +02:00
advance_draft_sequence if advance_draft
2013-02-05 14:16:51 -05:00
end
end
2013-05-07 14:25:41 -04:00
before_save do
2014-10-27 22:06:43 +01:00
ensure_topic_has_a_category unless skip_callbacks
2018-09-18 08:54:44 +10:00
write_attribute ( :fancy_title , Topic . fancy_title ( title )) if title_changed?
2017-05-31 17:40:21 +09:00
if category_id_changed? || new_record?
inherit_auto_close_from_category
2021-06-27 21:46:11 +02:00
inherit_slow_mode_from_category
2017-05-31 17:40:21 +09:00
end
2013-05-07 14:25:41 -04:00
end
after_save do
2020-04-30 16:48:34 +10:00
banner = "banner"
2014-11-14 15:39:17 +11:00
2017-08-31 12:06:56 +08:00
if archetype_before_last_save == banner || archetype == banner
2014-11-14 15:39:17 +11:00
ApplicationController . banner_json_cache . clear
end
2016-07-08 12:58:18 +10:00
2020-07-17 11:12:31 +08:00
if tags_changed || saved_change_to_attribute? ( :category_id ) ||
saved_change_to_attribute? ( :title )
2018-02-20 14:41:00 +11:00
SearchIndexer . queue_post_reindex ( self . id )
if tags_changed
TagUser . auto_watch ( topic_id : id )
TagUser . auto_track ( topic_id : id )
self . tags_changed = false
end
2016-07-08 12:58:18 +10:00
end
2016-12-22 13:13:14 +11:00
SearchIndexer . index ( self )
2013-05-07 14:25:41 -04:00
end
2018-02-08 16:36:39 -05:00
after_update do
if saved_changes [ :category_id ] && self . tags . present?
CategoryTagStat . topic_moved ( self , * saved_changes [ :category_id ] )
2019-12-09 11:15:47 -08:00
elsif saved_changes [ :category_id ] && self . category &. read_restricted?
UserProfile . remove_featured_topic_from_all_profiles ( self )
2018-02-08 16:36:39 -05:00
end
end
2014-10-27 22:06:43 +01:00
def initialize_default_values
self . bumped_at ||= Time . now
self . last_post_user_id ||= user_id
end
2014-03-07 18:59:47 +11:00
2014-10-27 22:06:43 +01:00
def advance_draft_sequence
2018-03-05 15:38:05 +08:00
if self . private_message?
2014-10-27 22:06:43 +01:00
DraftSequence . next! ( user , Draft :: NEW_PRIVATE_MESSAGE )
else
DraftSequence . next! ( user , Draft :: NEW_TOPIC )
end
end
def ensure_topic_has_a_category
2018-03-05 16:18:23 +08:00
if category_id . nil? && ( archetype . nil? || self . regular? )
2018-06-05 17:29:17 +10:00
self . category_id = category &. id || SiteSetting . uncategorized_category_id
2014-10-27 22:06:43 +01:00
end
2013-12-12 03:41:34 +01:00
end
2021-11-18 13:42:03 +05:30
def self . visible_post_types ( viewed_by = nil , include_moderator_actions : true )
2015-09-10 16:01:23 -04:00
types = Post . types
2021-11-18 13:42:03 +05:30
result = [ types [ :regular ]]
result += [ types [ :moderator_action ] , types [ :small_action ]] if include_moderator_actions
2022-06-30 10:18:12 +10:00
result << types [ :whisper ] if viewed_by &. whisperer?
2015-09-10 16:01:23 -04:00
result
end
2013-11-13 12:26:32 -05:00
def self . top_viewed ( max = 10 )
2014-02-28 15:07:55 +11:00
Topic . listable_topics . visible . secured . order ( "views desc" ) . limit ( max )
2013-11-13 12:26:32 -05:00
end
def self . recent ( max = 10 )
2014-02-28 15:07:55 +11:00
Topic . listable_topics . visible . secured . order ( "created_at desc" ) . limit ( max )
2013-11-13 12:26:32 -05:00
end
2013-10-28 11:42:07 +05:30
def self . count_exceeds_minimum?
count > SiteSetting . minimum_topics_similar
end
2013-06-03 16:12:24 -04:00
def best_post
2016-08-19 13:19:08 -04:00
posts
. where ( post_type : Post . types [ :regular ] , user_deleted : false )
. order ( "score desc nulls last" )
. limit ( 1 )
. first
2013-06-03 16:12:24 -04:00
end
2022-10-27 06:13:21 +08:00
def self . has_flag_scope
ReviewableFlaggedPost . pending_and_default_visible
end
2015-02-16 13:03:04 +01:00
def has_flags?
2022-10-27 06:13:21 +08:00
self . class . has_flag_scope . exists? ( topic_id : self . id )
2015-02-16 13:03:04 +01:00
end
2016-04-11 18:07:28 +05:30
def is_official_warning?
subtype == TopicSubtype . moderator_warning
end
2021-05-20 23:37:17 -04:00
# all users (in groups or directly targeted) that are going to get the pm
2013-05-02 15:15:17 +10:00
def all_allowed_users
2016-04-11 18:07:28 +05:30
moderators_sql = " UNION #{ User . moderators . to_sql } " if private_message? &&
( has_flags? || is_official_warning? )
2015-10-29 19:39:30 +02:00
User . from (
"( #{ allowed_users . to_sql } UNION #{ allowed_group_users . to_sql }#{ moderators_sql } ) as users" ,
)
2013-05-02 15:15:17 +10:00
end
2013-02-05 14:16:51 -05:00
# Additional rate limits on topics: per day and private messages per day
def limit_topics_per_day
2020-11-04 16:23:49 -08:00
return unless regular?
2016-06-20 16:38:15 -04:00
if user && user . new_user_posting_on_first_day?
2015-09-24 12:04:41 -04:00
limit_first_day_topics_per_day
else
apply_per_day_rate_limit_for ( "topics" , :max_topics_per_day )
end
2013-02-05 14:16:51 -05:00
end
def limit_private_messages_per_day
return unless private_message?
2018-01-31 11:46:25 +05:30
apply_per_day_rate_limit_for ( "pms" , :max_personal_messages_per_day )
2013-02-05 14:16:51 -05:00
end
2015-09-24 13:37:53 +10:00
def self . fancy_title ( title )
2018-09-18 08:54:44 +10:00
return unless escaped = ERB :: Util . html_escape ( title )
2019-07-18 11:55:49 +02:00
fancy_title = Emoji . unicode_unescape ( HtmlPrettify . render ( escaped ))
2018-09-18 08:54:44 +10:00
fancy_title . length > Topic . max_fancy_title_length ? escaped : fancy_title
2015-09-24 13:37:53 +10:00
end
2013-02-25 19:42:20 +03:00
def fancy_title
2015-09-24 13:37:53 +10:00
return ERB :: Util . html_escape ( title ) unless SiteSetting . title_fancy_entities?
2014-04-18 10:18:38 +05:30
2015-09-24 13:37:53 +10:00
unless fancy_title = read_attribute ( :fancy_title )
fancy_title = Topic . fancy_title ( title )
write_attribute ( :fancy_title , fancy_title )
2017-10-19 15:41:03 +08:00
if ! new_record? && ! Discourse . readonly_mode?
2015-09-24 13:37:53 +10:00
# make sure data is set in table, this also allows us to change algorithm
# by simply nulling this column
2018-06-19 16:13:14 +10:00
DB . exec (
"UPDATE topics SET fancy_title = :fancy_title where id = :id" ,
id : self . id ,
fancy_title : fancy_title ,
)
2015-09-24 13:37:53 +10:00
end
end
2013-02-19 16:08:23 -05:00
2015-09-24 13:37:53 +10:00
fancy_title
2013-04-22 13:48:05 +10:00
end
2013-06-03 16:12:24 -04:00
# Returns hot topics since a date for display in email digest.
2014-04-17 16:42:40 -04:00
def self . for_digest ( user , since , opts = nil )
opts = opts || {}
2021-07-23 13:52:35 -04:00
period = ListController . best_period_for ( since )
2014-04-17 15:14:54 -04:00
2013-11-06 15:05:06 -05:00
topics =
Topic
. visible
. secured ( Guardian . new ( user ))
2014-04-17 15:21:55 -04:00
. joins (
"LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{ user . id . to_i } " ,
)
2016-08-15 16:16:04 -04:00
. joins (
"LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{ user . id . to_i } " ,
)
2015-01-29 21:10:26 +05:30
. joins ( "LEFT OUTER JOIN users ON users.id = topics.user_id" )
2013-11-06 15:05:06 -05:00
. where ( closed : false , archived : false )
2014-04-17 15:21:55 -04:00
. where (
"COALESCE(topic_users.notification_level, 1) <> ?" ,
TopicUser . notification_levels [ :muted ] ,
)
2013-11-06 15:05:06 -05:00
. created_since ( since )
2017-08-14 12:47:33 -04:00
. where ( "topics.created_at < ?" , ( SiteSetting . editing_grace_period || 0 ) . seconds . ago )
2013-11-06 15:05:06 -05:00
. listable_topics
2014-04-17 15:43:24 -04:00
. includes ( :category )
2014-04-17 16:42:40 -04:00
2016-12-19 14:53:53 -05:00
unless opts [ :include_tl0 ] || user . user_option . try ( :include_tl0_in_digests )
2016-03-17 17:35:23 -04:00
topics = topics . where ( "COALESCE(users.trust_level, 0) > 0" )
end
2014-04-17 16:42:40 -04:00
if !! opts [ :top_order ]
2021-07-23 13:52:35 -04:00
topics =
topics . joins ( "LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id" ) . order ( <<~ SQL )
COALESCE ( topic_users . notification_level , 1 ) DESC ,
COALESCE ( category_users . notification_level , 1 ) DESC ,
COALESCE ( top_topics . #{TopTopic.score_column_for_period(period)}, 0) DESC,
topics . bumped_at DESC
SQL
2014-04-17 16:42:40 -04:00
end
topics = topics . limit ( opts [ :limit ] ) if opts [ :limit ]
2013-11-06 15:05:06 -05:00
2014-04-17 15:14:54 -04:00
# Remove category topics
2023-09-25 19:38:54 +03:00
topics = topics . where . not ( id : Category . select ( :topic_id ) . where . not ( topic_id : nil ))
2014-04-17 15:14:54 -04:00
2020-08-04 13:35:48 -04:00
# Remove muted and shared draft categories
remove_category_ids =
CategoryUser . where (
user_id : user . id ,
notification_level : CategoryUser . notification_levels [ :muted ] ,
) . pluck ( :category_id )
2016-03-26 00:42:00 +05:30
if SiteSetting . digest_suppress_categories . present?
2021-11-03 12:47:09 +05:30
topics =
topics . where (
"topics.category_id NOT IN (?)" ,
SiteSetting . digest_suppress_categories . split ( "|" ) . map ( & :to_i ),
)
2020-08-04 13:35:48 -04:00
end
2023-08-18 14:28:20 -05:00
if SiteSetting . digest_suppress_tags . present?
2023-09-18 10:45:43 +03:00
tag_ids = Tag . where_name ( SiteSetting . digest_suppress_tags . split ( "|" )) . pluck ( :id )
if tag_ids . present?
topics =
topics . joins ( "LEFT JOIN topic_tags tg ON topics.id = tg.topic_id" ) . where (
"tg.tag_id NOT IN (?) OR tg.tag_id IS NULL" ,
tag_ids ,
)
end
2023-08-18 14:28:20 -05:00
end
2020-08-04 13:46:26 -04:00
remove_category_ids << SiteSetting . shared_drafts_category if SiteSetting . shared_drafts_enabled?
2020-08-04 13:35:48 -04:00
if remove_category_ids . present?
remove_category_ids . uniq!
2021-04-07 00:01:15 +03:00
topics =
topics . where (
"topic_users.notification_level != ? OR topics.category_id NOT IN (?)" ,
TopicUser . notification_levels [ :muted ] ,
remove_category_ids ,
)
2013-11-06 15:05:06 -05:00
end
2016-08-18 17:16:52 -04:00
# Remove muted tags
2016-08-08 15:14:18 -04:00
muted_tag_ids = TagUser . lookup ( user , :muted ) . pluck ( :tag_id )
unless muted_tag_ids . empty?
2017-07-04 16:12:10 -04:00
# If multiple tags per topic, include topics with tags that aren't muted,
# and don't forget untagged topics.
topics =
topics . where (
"EXISTS ( SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id AND tag_id NOT IN (?) )
OR NOT EXISTS (SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id)" ,
muted_tag_ids ,
)
2016-08-08 15:14:18 -04:00
end
2013-11-06 15:05:06 -05:00
topics
2013-02-05 14:16:51 -05:00
end
2013-02-07 16:45:24 +01:00
2013-04-22 13:48:05 +10:00
def reload ( options = nil )
@post_numbers = nil
2017-05-17 09:37:11 +08:00
@public_topic_timer = nil
2021-05-21 18:13:14 +04:00
@slow_mode_topic_timer = nil
2018-05-24 16:41:51 +08:00
@is_category_topic = nil
2013-04-22 13:48:05 +10:00
super ( options )
end
2013-02-05 14:16:51 -05:00
def post_numbers
@post_numbers ||= posts . order ( :post_number ) . pluck ( :post_number )
end
2013-12-06 16:39:35 -05:00
def age_in_minutes
(( Time . zone . now - created_at ) / 1 . minute ) . round
2013-05-27 17:58:57 -07:00
end
2020-04-22 11:52:50 +03:00
def self . listable_count_per_day (
start_date ,
end_date ,
category_id = nil ,
2023-09-05 11:17:18 +05:30
include_subcategories = false ,
group_ids = nil
2018-05-11 13:30:21 +10:00
)
result =
listable_topics . where (
"topics.created_at >= ? AND topics.created_at <= ?" ,
start_date ,
end_date ,
2023-01-09 12:20:10 +00:00
)
2018-05-11 13:30:21 +10:00
result = result . group ( "date(topics.created_at)" ) . order ( "date(topics.created_at)" )
2020-04-22 11:52:50 +03:00
result =
result . where (
category_id : include_subcategories ? Category . subcategory_ids ( category_id ) : category_id ,
) if category_id
2023-09-05 11:17:18 +05:30
if group_ids
result =
result
. joins ( "INNER JOIN users ON users.id = topics.user_id" )
. joins ( "INNER JOIN group_users ON group_users.user_id = users.id" )
. where ( "group_users.group_id IN (?)" , group_ids )
end
2018-04-26 14:49:41 +02:00
result . count
2013-03-07 11:07:59 -05:00
end
2013-02-07 16:45:24 +01:00
def private_message?
2021-01-13 08:49:29 +10:00
self . archetype == Archetype . private_message
2013-02-05 14:16:51 -05:00
end
2018-03-05 16:18:23 +08:00
def regular?
self . archetype == Archetype . default
end
2021-01-13 08:49:29 +10:00
def open?
! self . closed?
end
2017-09-16 01:03:29 +02:00
MAX_SIMILAR_BODY_LENGTH ||= 200
2013-06-12 13:43:59 -04:00
def self . similar_to ( title , raw , user = nil )
2017-09-16 01:03:29 +02:00
return [] if title . blank?
raw = raw . presence || ""
2021-10-04 11:40:22 +08:00
search_data = Search . prepare_data ( title . strip )
2013-03-14 14:45:29 -04:00
2021-10-04 11:40:22 +08:00
return [] if search_data . blank?
tsquery = Search . set_tsquery_weight_filter ( search_data , "A" )
2020-07-28 11:53:25 +08:00
2020-07-28 13:23:53 +08:00
if raw . present?
2020-07-28 15:20:18 +08:00
cooked =
SearchIndexer :: HtmlScrubber . scrub ( PrettyText . cook ( raw [ 0 ... MAX_SIMILAR_BODY_LENGTH ]. strip ))
2020-09-22 07:53:12 +10:00
prepared_data = cooked . present? && Search . prepare_data ( cooked )
if prepared_data . present?
2020-08-21 10:51:37 +08:00
raw_tsquery = Search . set_tsquery_weight_filter ( prepared_data , "B" )
2020-07-28 13:23:53 +08:00
2020-08-21 10:51:37 +08:00
tsquery = " #{ tsquery } & #{ raw_tsquery } "
end
2020-07-28 13:23:53 +08:00
end
2020-07-28 11:53:25 +08:00
2020-07-28 13:23:53 +08:00
tsquery = Search . to_tsquery ( term : tsquery , joiner : "|" )
2014-04-14 15:20:41 -04:00
2022-12-07 11:33:01 +08:00
guardian = Guardian . new ( user )
excluded_category_ids_sql =
Category
. secured ( guardian )
. where ( search_priority : Searchable :: PRIORITIES [ :ignore ] )
. select ( :id )
. to_sql
excluded_category_ids_sql = <<~ SQL if user
#{excluded_category_ids_sql}
UNION
2022-12-12 06:31:46 +08:00
#{CategoryUser.muted_category_ids_query(user, include_direct: true).select("categories.id").to_sql}
2022-12-07 11:33:01 +08:00
SQL
2017-09-16 01:03:29 +02:00
candidates =
Topic
. visible
2014-08-08 12:12:53 +10:00
. listable_topics
2022-12-07 11:33:01 +08:00
. secured ( guardian )
2017-09-16 01:03:29 +02:00
. joins ( "JOIN topic_search_data s ON topics.id = s.topic_id" )
. joins ( "LEFT JOIN categories c ON topics.id = c.topic_id" )
2020-07-28 11:53:25 +08:00
. where ( "search_data @@ #{ tsquery } " )
2017-09-16 01:03:29 +02:00
. where ( "c.topic_id IS NULL" )
2022-12-07 11:33:01 +08:00
. where ( "topics.category_id NOT IN ( #{ excluded_category_ids_sql } )" )
2020-07-28 11:53:25 +08:00
. order ( "ts_rank(search_data, #{ tsquery } ) DESC" )
2014-08-08 12:12:53 +10:00
. limit ( SiteSetting . max_similar_results * 3 )
candidate_ids = candidates . pluck ( :id )
2017-09-16 01:03:29 +02:00
return [] if candidate_ids . blank?
2014-08-08 12:12:53 +10:00
2017-09-16 01:03:29 +02:00
similars =
Topic
2014-08-08 12:12:53 +10:00
. joins ( "JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1" )
. where ( "topics.id IN (?)" , candidate_ids )
2017-09-16 01:03:29 +02:00
. order ( "similarity DESC" )
. limit ( SiteSetting . max_similar_results )
2014-08-08 12:12:53 +10:00
2017-09-16 01:03:29 +02:00
if raw . present?
similars . select (
2020-12-11 10:56:26 +11:00
DB . sql_fragment (
"topics.*, similarity(topics.title, :title) + similarity(p.raw, :raw) AS similarity, p.cooked AS blurb" ,
title : title ,
2017-09-16 01:03:29 +02:00
raw : raw ,
2023-01-09 12:20:10 +00:00
),
2017-09-16 01:03:29 +02:00
) . where (
"similarity(topics.title, :title) + similarity(p.raw, :raw) > 0.2" ,
title : title ,
2023-01-09 12:20:10 +00:00
raw : raw ,
2017-09-16 01:03:29 +02:00
)
else
similars . select (
2020-12-11 10:56:26 +11:00
DB . sql_fragment (
"topics.*, similarity(topics.title, :title) AS similarity, p.cooked AS blurb" ,
title : title ,
2023-01-09 12:20:10 +00:00
),
2017-09-16 01:03:29 +02:00
) . where ( "similarity(topics.title, :title) > 0.2" , title : title )
end
2013-03-14 14:45:29 -04:00
end
2013-03-06 15:17:07 -05:00
2015-07-29 16:34:21 +02:00
def update_status ( status , enabled , user , opts = {})
2017-03-22 11:12:02 +08:00
TopicStatusUpdater . new ( self , user ) . update! ( status , enabled , opts )
2018-02-27 11:07:37 +08:00
DiscourseEvent . trigger ( :topic_status_updated , self , status , enabled )
2020-02-27 11:09:37 +05:30
2020-07-21 14:29:02 -04:00
if status == "closed"
StaffActionLogger . new ( user ) . log_topic_closed ( self , closed : enabled )
elsif status == "archived"
StaffActionLogger . new ( user ) . log_topic_archived ( self , archived : enabled )
end
2020-02-27 11:09:37 +05:30
if enabled && private_message? && status . to_s [ "closed" ]
group_ids = user . groups . pluck ( :id )
if group_ids . present?
allowed_group_ids =
self . allowed_groups . where ( "topic_allowed_groups.group_id IN (?)" , group_ids ) . pluck ( :id )
allowed_group_ids . each { | id | GroupArchivedMessage . archive! ( id , self ) }
end
end
2013-02-05 14:16:51 -05:00
end
# Atomically creates the next post number
2019-03-08 10:49:34 +02:00
def self . next_post_number ( topic_id , opts = {})
2018-06-19 16:13:14 +10:00
highest =
DB
. query_single (
"SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?" ,
topic_id ,
2023-01-09 12:20:10 +00:00
)
2018-06-19 16:13:14 +10:00
. first
. to_i
2013-02-05 14:16:51 -05:00
2019-03-08 10:49:34 +02:00
if opts [ :whisper ]
2018-06-19 16:13:14 +10:00
result = DB . query_single ( <<~ SQL , highest , topic_id )
UPDATE topics
SET highest_staff_post_number = ? + 1
WHERE id = ?
RETURNING highest_staff_post_number
SQL
2016-12-02 17:03:31 +11:00
2018-06-19 16:13:14 +10:00
result . first . to_i
2016-12-02 17:03:31 +11:00
else
2019-03-08 10:49:34 +02:00
reply_sql = opts [ :reply ] ? ", reply_count = reply_count + 1" : ""
posts_sql = opts [ :post ] ? ", posts_count = posts_count + 1" : ""
2016-12-02 17:03:31 +11:00
2018-06-19 16:13:14 +10:00
result = DB . query_single ( <<~ SQL , highest : highest , topic_id : topic_id )
UPDATE topics
SET highest_staff_post_number = :highest + 1 ,
2019-03-08 10:49:34 +02:00
highest_post_number = :highest + 1
#{reply_sql}
#{posts_sql}
2018-06-19 16:13:14 +10:00
WHERE id = :topic_id
RETURNING highest_post_number
SQL
2016-12-02 17:03:31 +11:00
2018-06-19 16:13:14 +10:00
result . first . to_i
2016-12-02 17:03:31 +11:00
end
end
def self . reset_all_highest!
2018-06-19 16:13:14 +10:00
DB . exec <<~ SQL
WITH
X as (
SELECT topic_id ,
COALESCE ( MAX ( post_number ), 0 ) highest_post_number
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
),
Y as (
SELECT topic_id ,
coalesce ( MAX ( post_number ), 0 ) highest_post_number ,
count ( * ) posts_count ,
max ( created_at ) last_posted_at
FROM posts
WHERE deleted_at IS NULL AND post_type <> 4
GROUP BY topic_id
)
UPDATE topics
SET
highest_staff_post_number = X . highest_post_number ,
highest_post_number = Y . highest_post_number ,
last_posted_at = Y . last_posted_at ,
posts_count = Y . posts_count
FROM X , Y
WHERE
2019-03-08 10:49:34 +02:00
topics . archetype <> 'private_message' AND
X . topic_id = topics . id AND
Y . topic_id = topics . id AND (
topics . highest_staff_post_number <> X . highest_post_number OR
topics . highest_post_number <> Y . highest_post_number OR
topics . last_posted_at <> Y . last_posted_at OR
topics . posts_count <> Y . posts_count
)
SQL
DB . exec <<~ SQL
WITH
X as (
SELECT topic_id ,
COALESCE ( MAX ( post_number ), 0 ) highest_post_number
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
),
Y as (
SELECT topic_id ,
coalesce ( MAX ( post_number ), 0 ) highest_post_number ,
count ( * ) posts_count ,
max ( created_at ) last_posted_at
FROM posts
WHERE deleted_at IS NULL AND post_type <> 3 AND post_type <> 4
GROUP BY topic_id
)
UPDATE topics
SET
highest_staff_post_number = X . highest_post_number ,
highest_post_number = Y . highest_post_number ,
last_posted_at = Y . last_posted_at ,
posts_count = Y . posts_count
FROM X , Y
WHERE
topics . archetype = 'private_message' AND
2018-06-19 16:13:14 +10:00
X . topic_id = topics . id AND
Y . topic_id = topics . id AND (
topics . highest_staff_post_number <> X . highest_post_number OR
topics . highest_post_number <> Y . highest_post_number OR
topics . last_posted_at <> Y . last_posted_at OR
topics . posts_count <> Y . posts_count
)
SQL
2013-02-05 14:16:51 -05:00
end
2022-06-27 18:21:05 -03:00
# If a post is deleted we have to update our highest post counters and last post information
2013-02-05 14:16:51 -05:00
def self . reset_highest ( topic_id )
2023-02-13 12:39:45 +08:00
archetype = Topic . where ( id : topic_id ) . pick ( :archetype )
2019-03-08 10:49:34 +02:00
# ignore small_action replies for private messages
post_type =
archetype == Archetype . private_message ? " AND post_type <> #{ Post . types [ :small_action ] } " : ""
2018-06-19 16:13:14 +10:00
result = DB . query_single ( <<~ SQL , topic_id : topic_id )
UPDATE topics
SET
2019-03-08 10:49:34 +02:00
highest_staff_post_number = (
2018-06-19 16:13:14 +10:00
SELECT COALESCE ( MAX ( post_number ), 0 ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL
),
2019-03-08 10:49:34 +02:00
highest_post_number = (
2018-06-19 16:13:14 +10:00
SELECT COALESCE ( MAX ( post_number ), 0 ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
2019-03-08 10:49:34 +02:00
#{post_type}
2018-06-19 16:13:14 +10:00
),
posts_count = (
SELECT count ( * ) FROM posts
WHERE deleted_at IS NULL AND
topic_id = :topic_id AND
post_type <> 4
2019-03-08 10:49:34 +02:00
#{post_type}
2018-06-19 16:13:14 +10:00
),
last_posted_at = (
SELECT MAX ( created_at ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
2019-03-08 10:49:34 +02:00
#{post_type}
2022-06-27 18:21:05 -03:00
),
last_post_user_id = COALESCE ((
SELECT user_id FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type <> 4
#{post_type}
ORDER BY created_at desc
LIMIT 1
), last_post_user_id )
2018-06-19 16:13:14 +10:00
WHERE id = :topic_id
RETURNING highest_post_number
SQL
highest_post_number = result . first . to_i
2013-02-05 14:16:51 -05:00
# Update the forum topic user records
2018-06-19 16:13:14 +10:00
DB . exec ( <<~ SQL , highest : highest_post_number , topic_id : topic_id )
UPDATE topic_users
SET last_read_post_number = CASE
WHEN last_read_post_number > :highest THEN :highest
ELSE last_read_post_number
2021-07-05 14:17:31 +08:00
END
2018-06-19 16:13:14 +10:00
WHERE topic_id = :topic_id
SQL
2013-02-05 14:16:51 -05:00
end
2019-05-10 11:37:37 +10:00
cattr_accessor :update_featured_topics
2014-10-27 22:06:43 +01:00
def changed_to_category ( new_category )
2018-06-01 09:44:14 +08:00
return true if new_category . blank? || Category . exists? ( topic_id : id )
2023-01-20 09:50:24 +08:00
2014-10-27 22:06:43 +01:00
if new_category . id == SiteSetting . uncategorized_category_id &&
! SiteSetting . allow_uncategorized_topics
return false
2023-01-09 12:20:10 +00:00
end
2013-02-05 14:16:51 -05:00
Topic . transaction do
old_category = category
2014-10-27 22:06:43 +01:00
if self . category_id != new_category . id
2023-07-11 15:24:13 -06:00
self . update ( category_id : new_category . id )
2018-05-07 21:29:06 +08:00
if old_category
Category . where ( id : old_category . id ) . update_all ( "topic_count = topic_count - 1" )
2023-01-20 09:50:24 +08:00
count =
if old_category . read_restricted && ! new_category . read_restricted
1
elsif ! old_category . read_restricted && new_category . read_restricted
- 1
end
Tag . update_counters ( self . tags , { public_topic_count : count }) if count
2018-05-07 21:29:06 +08:00
end
2016-07-08 12:58:18 +10:00
# when a topic changes category we may have to start watching it
# if we happen to have read state for it
CategoryUser . auto_watch ( category_id : new_category . id , topic_id : self . id )
CategoryUser . auto_track ( category_id : new_category . id , topic_id : self . id )
2018-05-07 21:29:06 +08:00
2021-10-18 09:04:01 -04:00
if ! SiteSetting . disable_category_edit_notifications && ( post = self . ordered_posts . first )
2018-05-24 17:27:43 +02:00
notified_user_ids = [ post . user_id , post . last_editor_id ]. uniq
2018-07-24 09:41:55 +01:00
DB . after_commit do
2018-07-18 22:04:43 +01:00
Jobs . enqueue (
:notify_category_change ,
post_id : post . id ,
notified_user_ids : notified_user_ids ,
)
end
2018-05-07 21:29:06 +08:00
end
2021-01-29 09:03:44 +10:00
# when a topic changes category we may need to make uploads
# linked to posts secure/not secure depending on whether the
# category is private. this is only done if the category
# has actually changed to avoid noise.
DB . after_commit { Jobs . enqueue ( :update_topic_upload_security , topic_id : self . id ) }
2013-02-05 14:16:51 -05:00
end
2014-10-27 22:06:43 +01:00
Category . where ( id : new_category . id ) . update_all ( "topic_count = topic_count + 1" )
2019-05-10 11:37:37 +10:00
if Topic . update_featured_topics != false
CategoryFeaturedTopic . feature_topics_for ( old_category ) unless @import_mode
unless @import_mode || old_category . try ( :id ) == new_category . id
CategoryFeaturedTopic . feature_topics_for ( new_category )
2023-01-09 12:20:10 +00:00
end
2019-05-10 11:37:37 +10:00
end
2013-02-07 16:45:24 +01:00
end
2014-10-27 22:06:43 +01:00
2013-10-08 14:40:31 -04:00
true
2013-02-05 14:16:51 -05:00
end
2018-07-18 10:17:33 +10:00
def add_small_action ( user , action_code , who = nil , opts = {})
2016-01-19 00:57:55 +01:00
custom_fields = {}
custom_fields [ "action_code_who" ] = who if who . present?
2018-07-18 10:17:33 +10:00
opts =
opts . merge (
post_type : Post . types [ :small_action ] ,
action_code : action_code ,
custom_fields : custom_fields ,
)
add_moderator_post ( user , nil , opts )
2016-01-19 00:57:55 +01:00
end
2015-07-24 16:39:03 -04:00
def add_moderator_post ( user , text , opts = nil )
opts ||= {}
2013-02-05 14:16:51 -05:00
new_post = nil
2015-10-15 08:56:10 +08:00
creator =
PostCreator . new (
user ,
raw : text ,
post_type : opts [ :post_type ] || Post . types [ :moderator_action ] ,
action_code : opts [ :action_code ] ,
no_bump : opts [ :bump ]. blank? ,
topic_id : self . id ,
2020-12-03 10:43:19 +11:00
silent : opts [ :silent ] ,
2016-01-11 12:42:06 +01:00
skip_validations : true ,
2020-07-10 12:05:55 +03:00
custom_fields : opts [ :custom_fields ] ,
import_mode : opts [ :import_mode ] ,
)
2013-02-05 14:16:51 -05:00
2016-06-20 15:47:29 +08:00
if ( new_post = creator . create ) && new_post . present?
increment! ( :moderator_posts_count ) if new_post . persisted?
2013-02-05 14:16:51 -05:00
# If we are moving posts, we want to insert the moderator post where the previous posts were
# in the stream, not at the end.
2019-04-29 17:32:25 +10:00
if opts [ :post_number ]. present?
new_post . update! ( post_number : opts [ :post_number ] , sort_order : opts [ :post_number ] )
2023-01-09 12:20:10 +00:00
end
2013-02-05 14:16:51 -05:00
# Grab any links that are present
TopicLink . extract_from ( new_post )
2014-07-15 17:47:24 +10:00
QuotedPost . extract_from ( new_post )
2013-02-05 14:16:51 -05:00
end
2013-02-07 16:45:24 +01:00
2013-02-05 14:16:51 -05:00
new_post
end
2014-07-16 15:39:39 -04:00
def change_category_to_id ( category_id )
2014-09-11 17:39:20 +10:00
return false if private_message?
2014-10-27 22:06:43 +01:00
new_category_id = category_id . to_i
# if the category name is blank, reset the attribute
new_category_id = SiteSetting . uncategorized_category_id if new_category_id == 0
2013-02-05 14:16:51 -05:00
2014-10-27 22:06:43 +01:00
return true if self . category_id == new_category_id
cat = Category . find_by ( id : new_category_id )
2013-10-24 10:05:51 +11:00
return false unless cat
2014-10-27 22:06:43 +01:00
2019-01-03 12:03:01 -05:00
reviewables . update_all ( category_id : new_category_id )
2013-02-05 14:16:51 -05:00
changed_to_category ( cat )
end
2016-06-20 16:29:11 +10:00
def remove_allowed_group ( removed_by , name )
if group = Group . find_by ( name : name )
group_user = topic_allowed_groups . find_by ( group_id : group . id )
if group_user
group_user . destroy
2021-03-02 15:46:50 +01:00
allowed_groups . reload
2016-06-20 16:29:11 +10:00
add_small_action ( removed_by , "removed_group" , group . name )
return true
end
end
false
end
2016-01-11 12:42:06 +01:00
def remove_allowed_user ( removed_by , username )
2017-10-10 16:26:56 +08:00
user = username . is_a? ( User ) ? username : User . find_by ( username : username )
if user
2014-05-06 14:41:59 +01:00
topic_user = topic_allowed_users . find_by ( user_id : user . id )
2017-10-10 16:26:56 +08:00
2013-10-02 19:11:48 +02:00
if topic_user
2017-10-10 16:26:56 +08:00
if user . id == removed_by &. id
add_small_action ( removed_by , "user_left" , user . username )
else
add_small_action ( removed_by , "removed_user" , user . username )
end
2023-03-02 13:47:54 +08:00
topic_user . destroy
2014-09-25 17:44:48 +02:00
return true
2013-10-02 19:11:48 +02:00
end
2013-06-18 17:17:01 +10:00
end
2014-09-25 17:44:48 +02:00
false
2013-06-18 17:17:01 +10:00
end
2018-08-23 07:36:49 +03:00
def reached_recipients_limit?
return false unless private_message?
topic_allowed_users . count + topic_allowed_groups . count >=
SiteSetting . max_allowed_message_recipients
end
2016-06-20 16:29:11 +10:00
def invite_group ( user , group )
2021-10-22 08:57:51 +10:00
TopicAllowedGroup . create! ( topic_id : self . id , group_id : group . id )
self . allowed_groups . reload
2016-06-20 16:29:11 +10:00
2021-10-22 08:57:51 +10:00
last_post =
self . posts . order ( "post_number desc" ) . where ( "not hidden AND posts.deleted_at IS NULL" ) . first
2016-06-20 16:29:11 +10:00
if last_post
2018-05-24 17:27:43 +02:00
Jobs . enqueue ( :post_alert , post_id : last_post . id )
2016-06-20 16:29:11 +10:00
add_small_action ( user , "invited_group" , group . name )
2021-04-14 19:30:51 +03:00
Jobs . enqueue ( :group_pm_alert , user_id : user . id , group_id : group . id , post_id : last_post . id )
2016-06-20 16:29:11 +10:00
end
2021-10-22 08:57:51 +10:00
# If the group invited includes the OP of the topic as one of is members,
# we cannot strip the topic_allowed_user record since it will be more
# complicated to recover the topic_allowed_user record for the OP if the
# group is removed.
allowed_user_where_clause = <<~ SQL
users . id IN (
SELECT topic_allowed_users . user_id
FROM topic_allowed_users
INNER JOIN group_users ON group_users . user_id = topic_allowed_users . user_id
INNER JOIN topic_allowed_groups ON topic_allowed_groups . group_id = group_users . group_id
WHERE topic_allowed_groups . group_id = :group_id AND
topic_allowed_users . topic_id = :topic_id AND
topic_allowed_users . user_id != :op_user_id
)
SQL
User
. where (
[
allowed_user_where_clause ,
{ group_id : group . id , topic_id : self . id , op_user_id : self . user_id },
] ,
2023-01-09 12:20:10 +00:00
)
2021-10-22 08:57:51 +10:00
. find_each { | allowed_user | remove_allowed_user ( Discourse . system_user , allowed_user ) }
2016-06-20 16:29:11 +10:00
true
end
2016-06-07 22:54:45 +05:30
def invite ( invited_by , username_or_email , group_ids = nil , custom_message = nil )
2018-03-08 01:34:17 +05:30
guardian = Guardian . new ( invited_by )
2017-07-12 15:31:10 +05:30
2022-10-10 19:21:51 +03:00
if target_user = User . find_by_username_or_email ( username_or_email )
2018-12-05 23:43:07 +08:00
if topic_allowed_users . exists? ( user_id : target_user . id )
raise UserExists . new ( I18n . t ( "topic_invite.user_exists" ))
end
2018-03-01 12:41:36 +08:00
2022-07-14 15:23:09 +10:00
comm_screener = UserCommScreener . new ( acting_user : invited_by , target_user_ids : target_user . id )
if comm_screener . ignoring_or_muting_actor? ( target_user . id )
2022-07-13 13:58:01 +10:00
raise NotAllowed . new ( I18n . t ( "not_accepting_pms" , username : target_user . username ))
end
2020-07-31 09:52:19 -06:00
2020-11-06 16:58:10 +02:00
if TopicUser . where (
topic : self ,
user : target_user ,
notification_level : TopicUser . notification_levels [ :muted ] ,
) . exists?
raise NotAllowed . new ( I18n . t ( "topic_invite.muted_topic" ))
end
2022-07-14 15:23:09 +10:00
if comm_screener . disallowing_pms_from_actor? ( target_user . id )
2020-11-06 16:58:10 +02:00
raise NotAllowed . new ( I18n . t ( "topic_invite.receiver_does_not_allow_pm" ))
end
2022-07-14 15:23:09 +10:00
if UserCommScreener . new (
acting_user : target_user ,
target_user_ids : invited_by . id ,
) . disallowing_pms_from_actor? ( invited_by . id )
2020-11-06 16:58:10 +02:00
raise NotAllowed . new ( I18n . t ( "topic_invite.sender_does_not_allow_pm" ))
end
2018-12-05 23:43:07 +08:00
if private_message?
!! invite_to_private_message ( invited_by , target_user , guardian )
2017-07-12 15:31:10 +05:30
else
2018-12-05 23:43:07 +08:00
!! invite_to_topic ( invited_by , target_user , group_ids , guardian )
2017-07-12 15:31:10 +05:30
end
2023-01-20 12:52:49 -06:00
elsif username_or_email =~ /\A.+@.+\z/ && guardian . can_invite_via_email? ( self )
2021-03-03 11:45:29 +02:00
!! Invite . generate (
invited_by ,
email : username_or_email ,
topic : self ,
group_ids : group_ids ,
2021-03-16 17:08:54 +02:00
custom_message : custom_message ,
invite_to_topic : true ,
2018-02-26 10:42:06 +08:00
)
2013-06-19 10:31:19 +10:00
end
2013-02-05 14:16:51 -05:00
end
2013-10-03 17:06:14 -04:00
def email_already_exists_for? ( invite )
invite . email_already_exists && private_message?
end
def grant_permission_to_user ( lower_email )
2017-04-26 19:47:36 +01:00
user = User . find_by_email ( lower_email )
2019-03-29 17:03:33 +01:00
unless topic_allowed_users . exists? ( user_id : user . id )
topic_allowed_users . create! ( user_id : user . id )
2023-01-09 12:20:10 +00:00
end
2013-10-03 17:06:14 -04:00
end
2013-05-25 17:37:23 -07:00
def max_post_number
2014-08-20 12:28:34 -04:00
posts . with_deleted . maximum ( :post_number ) . to_i
2013-05-25 17:37:23 -07:00
end
2013-05-08 13:33:58 -04:00
def move_posts ( moved_by , post_ids , opts )
2018-12-31 17:17:22 +05:30
post_mover =
PostMover . new (
self ,
moved_by ,
post_ids ,
move_to_pm : opts [ :archetype ]. present? && opts [ :archetype ] == "private_message" ,
)
2013-05-08 13:33:58 -04:00
2013-05-25 17:40:33 -07:00
if opts [ :destination_topic_id ]
2023-05-25 15:38:34 -03:00
topic =
post_mover . to_topic (
opts [ :destination_topic_id ] ,
** opts . slice ( :participants , :chronological_order ),
)
2018-07-10 09:48:57 +08:00
DiscourseEvent . trigger ( :topic_merged , post_mover . original_topic , post_mover . destination_topic )
topic
2013-05-25 17:40:33 -07:00
elsif opts [ :title ]
2018-07-06 18:21:32 +02:00
post_mover . to_new_topic ( opts [ :title ] , opts [ :category_id ] , opts [ :tags ] )
2013-02-05 14:16:51 -05:00
end
end
2013-03-12 12:33:42 -04:00
# Updates the denormalized statistics of a topic including featured posters. They shouldn't
# go out of sync unless you do something drastic live move posts from one topic to another.
# this recalculates everything.
def update_statistics
feature_topic_users
update_action_counts
Topic . reset_highest ( id )
end
def update_action_counts
2020-07-13 09:30:00 +03:00
update_column (
:like_count ,
Post . where . not ( post_type : Post . types [ :whisper ] ) . where ( topic_id : id ) . sum ( :like_count ),
)
2013-03-12 12:33:42 -04:00
end
2017-02-17 17:54:43 -05:00
def posters_summary ( options = {}) # avatar lookup in options
2013-05-22 23:21:19 -07:00
@posters_summary ||= TopicPostersSummary . new ( self , options ) . summary
2013-02-05 14:16:51 -05:00
end
2014-05-12 09:32:49 +02:00
def participants_summary ( options = {})
@participants_summary ||= TopicParticipantsSummary . new ( self , options ) . summary
end
2023-05-31 19:32:06 +05:30
def participant_groups_summary ( options = {})
@participant_groups_summary ||= TopicParticipantGroupsSummary . new ( self , options ) . summary
end
2021-06-24 11:35:36 +02:00
def make_banner! ( user , bannered_until = nil )
if bannered_until
bannered_until =
begin
Time . parse ( bannered_until )
rescue ArgumentError
raise Discourse :: InvalidParameters . new ( :bannered_until )
end
end
2014-06-16 19:21:21 +02:00
# only one banner at the same time
previous_banner = Topic . where ( archetype : Archetype . banner ) . first
previous_banner . remove_banner! ( user ) if previous_banner . present?
2017-02-03 15:07:38 -05:00
UserProfile . where ( "dismissed_banner_key IS NOT NULL" ) . update_all ( dismissed_banner_key : nil )
2014-06-16 19:21:21 +02:00
self . archetype = Archetype . banner
2021-06-24 11:35:36 +02:00
self . bannered_until = bannered_until
2017-03-16 17:31:27 -04:00
self . add_small_action ( user , "banner.enabled" )
2014-06-16 19:21:21 +02:00
self . save
2014-06-18 20:04:10 +02:00
2015-05-04 12:21:00 +10:00
MessageBus . publish ( "/site/banner" , banner )
2021-06-24 11:35:36 +02:00
Jobs . cancel_scheduled_job ( :remove_banner , topic_id : self . id )
Jobs . enqueue_at ( bannered_until , :remove_banner , topic_id : self . id ) if bannered_until
2014-06-16 19:21:21 +02:00
end
def remove_banner! ( user )
self . archetype = Archetype . default
2021-06-24 11:35:36 +02:00
self . bannered_until = nil
2017-03-16 17:31:27 -04:00
self . add_small_action ( user , "banner.disabled" )
2014-06-16 19:21:21 +02:00
self . save
2014-06-18 20:04:10 +02:00
2015-05-04 12:21:00 +10:00
MessageBus . publish ( "/site/banner" , nil )
2021-06-24 11:35:36 +02:00
Jobs . cancel_scheduled_job ( :remove_banner , topic_id : self . id )
2014-06-18 20:04:10 +02:00
end
def banner
2017-02-24 12:56:13 +01:00
post = self . ordered_posts . first
2014-06-18 20:04:10 +02:00
{ html : post . cooked , key : self . id , url : self . url }
2014-06-16 19:21:21 +02:00
end
2022-05-25 19:05:06 -03:00
cattr_accessor :slug_computed_callbacks
self . slug_computed_callbacks = []
def slug_for_topic ( title )
return "" unless title . present?
slug = Slug . for ( title )
# this is a hook for plugins that need to modify the generated slug
self . class . slug_computed_callbacks . each { | callback | slug = callback . call ( self , slug , title ) }
slug
end
2013-06-07 14:17:12 -04:00
# Even if the slug column in the database is null, topic.slug will return something:
2013-02-05 14:16:51 -05:00
def slug
2013-04-24 12:46:43 +10:00
unless slug = read_attribute ( :slug )
return "" unless title . present?
2022-05-25 19:05:06 -03:00
slug = slug_for_topic ( title )
2013-04-24 12:46:43 +10:00
if new_record?
write_attribute ( :slug , slug )
else
update_column ( :slug , slug )
end
end
slug
end
2019-10-16 17:08:43 -03:00
def self . find_by_slug ( slug )
if SiteSetting . slug_generation_method != "encoded"
Topic . find_by ( slug : slug . downcase )
else
encoded_slug = CGI . escape ( slug )
Topic . find_by ( slug : encoded_slug )
end
end
2013-04-24 12:46:43 +10:00
def title = ( t )
2022-05-25 19:05:06 -03:00
slug = slug_for_topic ( t . to_s )
2013-04-24 12:46:43 +10:00
write_attribute ( :slug , slug )
2015-09-24 13:37:53 +10:00
write_attribute ( :fancy_title , nil )
2013-04-24 12:46:43 +10:00
write_attribute ( :title , t )
2013-02-05 14:16:51 -05:00
end
2013-05-23 23:06:38 -07:00
# NOTE: These are probably better off somewhere else.
# Having a model know about URLs seems a bit strange.
2013-02-05 14:16:51 -05:00
def last_post_url
2020-10-09 12:51:24 +01:00
" #{ Discourse . base_path } /t/ #{ slug } / #{ id } / #{ posts_count } "
2013-02-05 14:16:51 -05:00
end
2013-05-09 17:37:34 +10:00
def self . url ( id , slug , post_number = nil )
2019-05-13 14:51:45 +02:00
url = + " #{ Discourse . base_url } /t/ #{ slug } / #{ id } "
2013-05-09 17:37:34 +10:00
url << "/ #{ post_number } " if post_number . to_i > 1
url
end
2013-05-25 17:38:15 -07:00
def url ( post_number = nil )
self . class . url id , slug , post_number
end
2015-09-28 16:43:38 +10:00
def self . relative_url ( id , slug , post_number = nil )
2020-10-09 12:51:24 +01:00
url = + " #{ Discourse . base_path } /t/"
2017-04-24 15:26:06 -04:00
url << " #{ slug } /" if slug . present?
url << id . to_s
2013-05-09 17:37:34 +10:00
url << "/ #{ post_number } " if post_number . to_i > 1
2013-02-05 14:16:51 -05:00
url
end
2017-04-24 15:26:06 -04:00
def slugless_url ( post_number = nil )
Topic . relative_url ( id , nil , post_number )
end
2015-09-28 16:43:38 +10:00
def relative_url ( post_number = nil )
Topic . relative_url ( id , slug , post_number )
end
2013-03-06 15:17:07 -05:00
def clear_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : Time . now )
end
2014-04-10 10:56:56 +10:00
def re_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : nil )
end
2021-06-24 11:35:36 +02:00
def update_pinned ( status , global = false , pinned_until = nil )
if pinned_until
pinned_until =
begin
Time . parse ( pinned_until )
rescue ArgumentError
raise Discourse :: InvalidParameters . new ( :pinned_until )
end
2018-03-28 16:20:08 +08:00
end
2015-07-29 16:34:21 +02:00
update_columns (
2018-03-28 16:20:08 +08:00
pinned_at : status ? Time . zone . now : nil ,
2015-07-29 16:34:21 +02:00
pinned_globally : global ,
pinned_until : pinned_until ,
)
Jobs . cancel_scheduled_job ( :unpin_topic , topic_id : self . id )
Jobs . enqueue_at ( pinned_until , :unpin_topic , topic_id : self . id ) if pinned_until
2013-02-05 14:16:51 -05:00
end
def draft_key
2013-02-28 21:54:12 +03:00
" #{ Draft :: EXISTING_TOPIC }#{ id } "
2013-02-05 14:16:51 -05:00
end
2013-05-23 23:06:38 -07:00
def notifier
@topic_notifier ||= TopicNotifier . new ( self )
end
def muted? ( user )
notifier . muted? ( user . id ) if user && user . id
end
2015-07-29 16:34:21 +02:00
def self . ensure_consistency!
# unpin topics that might have been missed
2021-06-24 11:35:36 +02:00
Topic . where ( "pinned_until < ?" , Time . now ) . update_all (
pinned_at : nil ,
pinned_globally : false ,
pinned_until : nil ,
)
Topic
. where ( "bannered_until < ?" , Time . now )
. find_each { | topic | topic . remove_banner! ( Discourse . system_user ) }
2015-07-29 16:34:21 +02:00
end
2021-06-27 21:46:11 +02:00
def inherit_slow_mode_from_category
if self . category &. default_slow_mode_seconds
self . slow_mode_seconds = self . category &. default_slow_mode_seconds
end
end
2021-02-17 07:51:39 +10:00
def inherit_auto_close_from_category ( timer_type : :close )
auto_close_hours = self . category &. auto_close_hours
if self . open? && ! @ignore_category_auto_close && auto_close_hours . present? &&
public_topic_timer &. execute_at . blank?
based_on_last_post = self . category . auto_close_based_on_last_post
duration_minutes = based_on_last_post ? auto_close_hours * 60 : nil
# the timer time can be a timestamp or an integer based
# on the number of hours
auto_close_time = auto_close_hours
if ! based_on_last_post
# set auto close to the original time it should have been
# when the topic was first created.
start_time = self . created_at || Time . zone . now
auto_close_time = start_time + auto_close_hours . hours
# if we have already passed the original close time then
# we should not recreate the auto-close timer for the topic
return if auto_close_time < Time . zone . now
# timestamp must be a string for set_or_create_timer
auto_close_time = auto_close_time . to_s
end
self . set_or_create_timer (
TopicTimer . types [ timer_type ] ,
auto_close_time ,
by_user : Discourse . system_user ,
based_on_last_post : based_on_last_post ,
duration_minutes : duration_minutes ,
)
end
end
2017-05-16 14:49:42 -04:00
def public_topic_timer
2022-07-27 16:21:11 +08:00
@public_topic_timer ||= topic_timers . find_by ( public_type : true )
2013-11-11 10:52:44 +11:00
end
2021-05-21 18:13:14 +04:00
def slow_mode_topic_timer
2022-07-27 16:21:11 +08:00
@slow_mode_topic_timer ||= topic_timers . find_by ( status_type : TopicTimer . types [ :clear_slow_mode ] )
2021-05-21 18:13:14 +04:00
end
2017-08-22 15:22:48 +09:00
def delete_topic_timer ( status_type , by_user : Discourse . system_user )
options = { status_type : status_type }
options . merge! ( user : by_user ) unless TopicTimer . public_types [ status_type ]
self . topic_timers . find_by ( options ) &. trash! ( by_user )
2021-02-17 07:51:39 +10:00
@public_topic_timer = nil
2017-08-22 15:52:16 +09:00
nil
2017-08-22 15:22:48 +09:00
end
2017-03-22 11:12:02 +08:00
# Valid arguments for the time:
# * An integer, which is the number of hours from now to update the topic's status.
# * A timestamp, like "2013-11-25 13:00", when the topic's status should update.
2013-11-26 19:06:20 -05:00
# * A timestamp with timezone in JSON format. (e.g., "2013-11-26T21:00:00.000Z")
2017-03-22 11:12:02 +08:00
# * `nil` to delete the topic's status update.
2015-05-27 12:22:34 -04:00
# Options:
2017-03-22 11:12:02 +08:00
# * by_user: User who is setting the topic's status update.
2017-04-03 17:28:41 +08:00
# * based_on_last_post: True if time should be based on timestamp of the last post.
# * category_id: Category that the update will apply to.
2021-02-05 10:12:56 +10:00
# * duration_minutes: The duration of the timer in minutes, which is used if the timer is based
# on the last post or if the timer type is delete_replies.
# * silent: Affects whether the close topic timer status change will be silent or not.
2021-06-29 09:27:12 +10:00
def set_or_create_timer (
status_type ,
time ,
by_user : nil ,
based_on_last_post : false ,
category_id : SiteSetting . uncategorized_category_id ,
duration_minutes : nil ,
silent : nil
)
if time . blank? && duration_minutes . blank?
return delete_topic_timer ( status_type , by_user : by_user )
2023-01-09 12:20:10 +00:00
end
2021-02-05 10:12:56 +10:00
duration_minutes = duration_minutes ? duration_minutes . to_i : 0
2017-10-04 16:31:40 +08:00
public_topic_timer = !! TopicTimer . public_types [ status_type ]
topic_timer_options = { topic : self , public_type : public_topic_timer }
topic_timer_options . merge! ( user : by_user ) unless public_topic_timer
2020-12-03 10:43:19 +11:00
topic_timer_options . merge! ( silent : silent ) if silent
2017-05-17 09:37:11 +08:00
topic_timer = TopicTimer . find_or_initialize_by ( topic_timer_options )
2017-06-21 15:31:15 +09:00
topic_timer . status_type = status_type
2017-03-22 11:12:02 +08:00
time_now = Time . zone . now
2017-05-11 18:23:18 -04:00
topic_timer . based_on_last_post = ! based_on_last_post . blank?
2017-03-22 11:12:02 +08:00
2017-05-11 18:23:18 -04:00
if status_type == TopicTimer . types [ :publish_to_category ]
topic_timer . category = Category . find_by ( id : category_id )
2017-04-03 17:28:41 +08:00
end
2017-05-11 18:23:18 -04:00
if topic_timer . based_on_last_post
2021-06-29 09:27:12 +10:00
if duration_minutes > 0
2017-03-31 17:57:46 +05:30
last_post_created_at =
self . ordered_posts . last . present? ? self . ordered_posts . last . created_at : time_now
2021-02-05 10:12:56 +10:00
topic_timer . duration_minutes = duration_minutes
topic_timer . execute_at = last_post_created_at + duration_minutes . minutes
2017-05-11 18:23:18 -04:00
topic_timer . created_at = last_post_created_at
2014-10-10 18:21:44 +02:00
end
2020-03-19 21:06:31 +05:30
elsif topic_timer . status_type == TopicTimer . types [ :delete_replies ]
2021-06-29 09:27:12 +10:00
if duration_minutes > 0
2020-03-19 21:06:31 +05:30
first_reply_created_at =
( self . ordered_posts . where ( "post_number > 1" ) . minimum ( :created_at ) || time_now )
2021-02-05 10:12:56 +10:00
topic_timer . duration_minutes = duration_minutes
topic_timer . execute_at = first_reply_created_at + duration_minutes . minutes
2020-03-19 21:06:31 +05:30
topic_timer . created_at = first_reply_created_at
end
2013-11-26 19:06:20 -05:00
else
2015-05-27 12:22:34 -04:00
utc = Time . find_zone ( "UTC" )
2018-03-26 11:32:52 +08:00
is_float =
(
2023-01-09 12:20:10 +00:00
begin
2018-03-26 11:32:52 +08:00
Float ( time )
rescue StandardError
nil
2023-01-09 12:20:10 +00:00
end
2018-03-26 11:32:52 +08:00
)
2017-03-22 11:12:02 +08:00
2018-03-26 11:32:52 +08:00
if is_float
num_hours = time . to_f
topic_timer . execute_at = num_hours . hours . from_now if num_hours > 0
else
timestamp = utc . parse ( time )
2020-12-08 11:13:45 -06:00
raise Discourse :: InvalidParameters unless timestamp && timestamp > utc . now
2015-05-27 12:22:34 -04:00
# a timestamp in client's time zone, like "2015-5-27 12:00"
2017-05-11 18:23:18 -04:00
topic_timer . execute_at = timestamp
2014-10-10 18:21:44 +02:00
end
2013-11-26 19:06:20 -05:00
end
2017-05-11 18:23:18 -04:00
if topic_timer . execute_at
2017-03-22 11:12:02 +08:00
if by_user &. staff? || by_user &. trust_level == TrustLevel [ 4 ]
2017-05-11 18:23:18 -04:00
topic_timer . user = by_user
2014-10-10 18:21:44 +02:00
else
2017-05-11 18:23:18 -04:00
topic_timer . user ||=
(
if self . user . staff? || self . user . trust_level == TrustLevel [ 4 ]
self . user
2023-01-09 12:20:10 +00:00
else
2017-05-11 18:23:18 -04:00
Discourse . system_user
2023-01-09 12:20:10 +00:00
end
2017-05-11 18:23:18 -04:00
)
2014-10-10 18:21:44 +02:00
end
2017-03-22 11:12:02 +08:00
if self . persisted?
2021-01-19 13:30:58 +10:00
# See TopicTimer.after_save for additional context; the topic
# status may be changed by saving.
2017-05-11 18:23:18 -04:00
topic_timer . save!
2013-06-06 17:04:10 -04:00
else
2017-05-11 18:23:18 -04:00
self . topic_timers << topic_timer
2015-12-08 18:13:23 +05:30
end
2017-05-11 18:23:18 -04:00
topic_timer
2013-06-06 17:04:10 -04:00
end
2013-05-07 14:25:41 -04:00
end
2013-07-14 11:24:16 +10:00
def read_restricted_category?
category && category . read_restricted
2013-05-19 23:04:53 -07:00
end
2013-07-08 15:23:20 -04:00
2021-04-14 15:54:09 +10:00
def category_allows_unlimited_owner_edits_on_first_post?
category && category . allow_unlimited_owner_edits_on_first_post?
end
2013-12-12 03:41:34 +01:00
def acting_user
@acting_user || user
end
def acting_user = ( u )
@acting_user = u
end
2014-03-24 12:19:08 +11:00
def secure_group_ids
@secure_group_ids ||=
( self . category . secure_group_ids if self . category && self . category . read_restricted? )
end
2014-04-01 15:29:15 -04:00
def has_topic_embed?
TopicEmbed . where ( topic_id : id ) . exists?
end
def expandable_first_post?
2015-08-18 17:15:46 -04:00
SiteSetting . embed_truncate? && has_topic_embed?
2014-04-01 15:29:15 -04:00
end
2015-12-30 13:26:21 +11:00
def message_archived? ( user )
return false unless user && user . id
2018-04-05 17:17:31 +10:00
# tricky query but this checks to see if message is archived for ALL groups you belong to
# OR if you have it archived as a user explicitly
sql = <<~ SQL
2018-06-19 16:13:14 +10:00
SELECT 1
WHERE
(
SELECT count ( * ) FROM topic_allowed_groups tg
JOIN group_archived_messages gm
ON gm . topic_id = tg . topic_id AND
gm . group_id = tg . group_id
WHERE tg . group_id IN ( SELECT g . group_id FROM group_users g WHERE g . user_id = :user_id )
AND tg . topic_id = :topic_id
) =
(
SELECT case when count ( * ) = 0 then - 1 else count ( * ) end FROM topic_allowed_groups tg
WHERE tg . group_id IN ( SELECT g . group_id FROM group_users g WHERE g . user_id = :user_id )
AND tg . topic_id = :topic_id
)
2018-04-05 17:17:31 +10:00
2018-06-19 16:13:14 +10:00
UNION ALL
2018-04-05 17:17:31 +10:00
2018-06-19 16:13:14 +10:00
SELECT 1 FROM topic_allowed_users tu
JOIN user_archived_messages um ON um . user_id = tu . user_id AND um . topic_id = tu . topic_id
WHERE tu . user_id = :user_id AND tu . topic_id = :topic_id
SQL
2015-12-30 13:26:21 +11:00
2018-06-19 16:13:14 +10:00
DB . exec ( sql , user_id : user . id , topic_id : id ) > 0
2015-12-30 13:26:21 +11:00
end
2015-06-22 19:46:51 +02:00
TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL
SELECT AVG ( t . hours ) :: float AS "hours" , t . created_at AS "date"
FROM (
SELECT t . id , t . created_at :: date AS created_at , EXTRACT ( EPOCH FROM MIN ( p . created_at ) - t . created_at ) :: float / 3600 . 0 AS "hours"
FROM topics t
LEFT JOIN posts p ON p . topic_id = t . id
/*where*/
GROUP BY t . id
) t
GROUP BY t . created_at
ORDER BY t . created_at
SQL
TIME_TO_FIRST_RESPONSE_TOTAL_SQL ||= <<-SQL
SELECT AVG ( t . hours ) :: float AS "hours"
FROM (
SELECT t . id , EXTRACT ( EPOCH FROM MIN ( p . created_at ) - t . created_at ) :: float / 3600 . 0 AS "hours"
FROM topics t
LEFT JOIN posts p ON p . topic_id = t . id
/*where*/
GROUP BY t . id
) t
SQL
2015-06-24 15:19:39 +02:00
def self . time_to_first_response ( sql , opts = nil )
opts ||= {}
2018-06-20 17:48:02 +10:00
builder = DB . build ( sql )
2015-06-24 15:19:39 +02:00
builder . where ( "t.created_at >= :start_date" , start_date : opts [ :start_date ] ) if opts [ :start_date ]
2015-06-25 18:45:11 -04:00
builder . where ( "t.created_at < :end_date" , end_date : opts [ :end_date ] ) if opts [ :end_date ]
2020-04-22 11:52:50 +03:00
if opts [ :category_id ]
if opts [ :include_subcategories ]
builder . where ( "t.category_id IN (?)" , Category . subcategory_ids ( opts [ :category_id ] ))
else
builder . where ( "t.category_id = ?" , opts [ :category_id ] )
end
end
2015-06-22 19:46:51 +02:00
builder . where ( "t.archetype <> ' #{ Archetype . private_message } '" )
builder . where ( "t.deleted_at IS NULL" )
builder . where ( "p.deleted_at IS NULL" )
builder . where ( "p.post_number > 1" )
2015-06-25 18:45:11 -04:00
builder . where ( "p.user_id != t.user_id" )
2015-09-14 13:36:41 -04:00
builder . where ( "p.user_id in (:user_ids)" , user_ids : opts [ :user_ids ] ) if opts [ :user_ids ]
2017-02-02 17:27:41 -05:00
builder . where ( "p.post_type = :post_type" , post_type : Post . types [ :regular ] )
2015-06-22 19:46:51 +02:00
builder . where ( "EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0" )
2018-06-20 17:48:02 +10:00
builder . query_hash
2015-06-22 19:46:51 +02:00
end
2015-09-14 13:36:41 -04:00
def self . time_to_first_response_per_day ( start_date , end_date , opts = {})
time_to_first_response (
TIME_TO_FIRST_RESPONSE_SQL ,
opts . merge ( start_date : start_date , end_date : end_date ),
)
2015-06-22 19:46:51 +02:00
end
2015-06-24 15:19:39 +02:00
def self . time_to_first_response_total ( opts = nil )
total = time_to_first_response ( TIME_TO_FIRST_RESPONSE_TOTAL_SQL , opts )
total . first [ "hours" ]. to_f . round ( 2 )
2015-06-22 19:46:51 +02:00
end
2015-06-25 18:45:11 -04:00
WITH_NO_RESPONSE_SQL ||= <<-SQL
SELECT COUNT ( * ) as count , tt . created_at AS "date"
FROM (
SELECT t . id , t . created_at :: date AS created_at , MIN ( p . post_number ) first_reply
FROM topics t
2017-02-02 17:27:41 -05:00
LEFT JOIN posts p ON p . topic_id = t . id AND p . user_id != t . user_id AND p . deleted_at IS NULL AND p . post_type = #{Post.types[:regular]}
2015-06-25 18:45:11 -04:00
/*where*/
GROUP BY t . id
) tt
2017-02-02 17:27:41 -05:00
WHERE tt . first_reply IS NULL OR tt . first_reply < 2
2015-06-25 18:45:11 -04:00
GROUP BY tt . created_at
ORDER BY tt . created_at
SQL
2020-04-22 11:52:50 +03:00
def self . with_no_response_per_day (
start_date ,
end_date ,
category_id = nil ,
include_subcategories = nil
)
2018-06-20 17:48:02 +10:00
builder = DB . build ( WITH_NO_RESPONSE_SQL )
2015-06-25 18:45:11 -04:00
builder . where ( "t.created_at >= :start_date" , start_date : start_date ) if start_date
builder . where ( "t.created_at < :end_date" , end_date : end_date ) if end_date
2020-04-22 11:52:50 +03:00
if category_id
if include_subcategories
builder . where ( "t.category_id IN (?)" , Category . subcategory_ids ( category_id ))
else
builder . where ( "t.category_id = ?" , category_id )
end
end
2015-06-25 18:45:11 -04:00
builder . where ( "t.archetype <> ' #{ Archetype . private_message } '" )
builder . where ( "t.deleted_at IS NULL" )
2018-06-20 17:48:02 +10:00
builder . query_hash
2015-06-22 19:46:51 +02:00
end
2015-06-25 18:45:11 -04:00
WITH_NO_RESPONSE_TOTAL_SQL ||= <<-SQL
SELECT COUNT ( * ) as count
FROM (
SELECT t . id , MIN ( p . post_number ) first_reply
FROM topics t
2017-02-02 17:27:41 -05:00
LEFT JOIN posts p ON p . topic_id = t . id AND p . user_id != t . user_id AND p . deleted_at IS NULL AND p . post_type = #{Post.types[:regular]}
2015-06-25 18:45:11 -04:00
/*where*/
GROUP BY t . id
) tt
2017-02-02 17:27:41 -05:00
WHERE tt . first_reply IS NULL OR tt . first_reply < 2
2015-06-25 18:45:11 -04:00
SQL
def self . with_no_response_total ( opts = {})
2018-06-20 17:48:02 +10:00
builder = DB . build ( WITH_NO_RESPONSE_TOTAL_SQL )
2020-04-22 11:52:50 +03:00
if opts [ :category_id ]
if opts [ :include_subcategories ]
builder . where ( "t.category_id IN (?)" , Category . subcategory_ids ( opts [ :category_id ] ))
else
builder . where ( "t.category_id = ?" , opts [ :category_id ] )
end
end
2015-06-25 18:45:11 -04:00
builder . where ( "t.archetype <> ' #{ Archetype . private_message } '" )
builder . where ( "t.deleted_at IS NULL" )
2018-06-20 17:48:02 +10:00
builder . query_single . first . to_i
2015-06-22 19:46:51 +02:00
end
2019-07-19 11:52:50 -04:00
def convert_to_public_topic ( user , category_id : nil )
public_topic = TopicConverter . new ( self , user ) . convert_to_public_topic ( category_id )
2023-01-20 09:50:24 +08:00
Tag . update_counters ( public_topic . tags , { public_topic_count : 1 }) if ! category . read_restricted
2016-05-01 17:18:43 +05:30
add_small_action ( user , "public_topic" ) if public_topic
public_topic
end
def convert_to_private_message ( user )
2023-01-20 09:50:24 +08:00
read_restricted = category . read_restricted
2016-05-01 17:18:43 +05:30
private_topic = TopicConverter . new ( self , user ) . convert_to_private_message
2023-01-20 09:50:24 +08:00
Tag . update_counters ( private_topic . tags , { public_topic_count : - 1 }) if ! read_restricted
2016-05-01 17:18:43 +05:30
add_small_action ( user , "private_topic" ) if private_topic
private_topic
end
2020-05-23 00:56:13 -04:00
def update_excerpt ( excerpt )
update_column ( :excerpt , excerpt )
ApplicationController . banner_json_cache . clear if archetype == "banner"
end
2017-04-27 11:53:53 +08:00
def pm_with_non_human_user?
2017-09-12 14:05:25 +08:00
sql = <<~ SQL
SELECT 1 FROM topics
LEFT JOIN topic_allowed_groups ON topics . id = topic_allowed_groups . topic_id
WHERE topic_allowed_groups . topic_id IS NULL
AND topics . archetype = :private_message
AND topics . id = :topic_id
AND (
SELECT COUNT ( * ) FROM topic_allowed_users
WHERE topic_allowed_users . topic_id = :topic_id
AND topic_allowed_users . user_id > 0
) = 1
SQL
2018-06-19 16:13:14 +10:00
result = DB . exec ( sql , private_message : Archetype . private_message , topic_id : self . id )
result != 0
2017-04-27 11:53:53 +08:00
end
2017-11-29 21:52:41 +08:00
def featured_link_root_domain
2019-12-12 12:49:21 +10:00
MiniSuffix . domain ( UrlHelper . encode_and_parse ( self . featured_link ) . hostname )
2017-11-29 21:52:41 +08:00
end
2018-03-27 14:00:08 +05:30
def self . private_message_topics_count_per_day ( start_date , end_date , topic_subtype )
2018-06-05 17:29:17 +10:00
private_messages
. with_subtype ( topic_subtype )
. where ( "topics.created_at >= ? AND topics.created_at <= ?" , start_date , end_date )
. group ( "date(topics.created_at)" )
. order ( "date(topics.created_at)" )
. count
2018-03-27 14:00:08 +05:30
end
2018-05-24 16:41:51 +08:00
def is_category_topic?
@is_category_topic ||= Category . exists? ( topic_id : self . id . to_i )
end
2024-02-15 00:42:42 -05:00
def reset_bumped_at ( post_id = nil )
2018-08-10 02:51:03 +02:00
post =
2024-02-15 00:42:42 -05:00
(
if post_id
Post . find_by ( id : post_id )
else
ordered_posts . where (
user_deleted : false ,
hidden : false ,
post_type : Post . types [ :regular ] ,
) . last || first_post
end
)
return if ! post
2018-08-10 02:51:03 +02:00
2019-11-26 23:12:47 +05:30
self . bumped_at = post . created_at
self . save ( validate : false )
2018-08-10 02:51:03 +02:00
end
2019-01-03 12:03:01 -05:00
def auto_close_threshold_reached?
return if user &. staff?
scores =
ReviewableScore
. pending
. joins ( :reviewable )
2020-04-08 10:44:31 -03:00
. where ( "reviewable_scores.score >= ?" , Reviewable . min_score_for_priority )
. where ( "reviewables.topic_id = ?" , self . id )
. pluck (
"COUNT(DISTINCT reviewable_scores.user_id), COALESCE(SUM(reviewable_scores.score), 0.0)" ,
)
2019-01-03 12:03:01 -05:00
. first
2019-05-24 14:13:03 -04:00
scores [ 0 ] >= SiteSetting . num_flaggers_to_close_topic &&
scores [ 1 ] >= Reviewable . score_to_auto_close_topic
2019-01-03 12:03:01 -05:00
end
2019-04-16 15:16:23 +08:00
def update_category_topic_count_by ( num )
if category_id . present?
Category
2021-02-16 17:45:12 +02:00
. where ( "id = ?" , category_id )
. where ( "topic_id != ? OR topic_id IS NULL" , self . id )
. update_all ( "topic_count = topic_count + #{ num . to_i } " )
2019-04-16 15:16:23 +08:00
end
end
2019-07-04 10:12:39 +02:00
def access_topic_via_group
Group
. joins ( :category_groups )
. where ( "category_groups.category_id = ?" , self . category_id )
. where ( "groups.public_admission OR groups.allow_membership_requests" )
. order ( :allow_membership_requests )
. first
end
2023-01-27 11:27:15 +10:00
def incoming_email_addresses ( group : nil , received_before : Time . zone . now )
email_addresses = Set . new
self
. incoming_email
. where ( "created_at <= ?" , received_before )
. each do | incoming_email |
to_addresses = incoming_email . to_addresses_split
cc_addresses = incoming_email . cc_addresses_split
combined_addresses = [ to_addresses , cc_addresses ]. flatten
# We only care about the emails addressed to the group or CC'd to the
# group if the group is present. If combined addresses is empty we do
# not need to do this check, and instead can proceed on to adding the
# from address.
#
# Will not include test1@gmail.com if the only IncomingEmail
# is:
#
# from: test1@gmail.com
# to: test+support@discoursemail.com
#
# Because we don't care about the from addresses and also the to address
# is not the email_username, which will be something like test1@gmail.com.
if group . present? && combined_addresses . any?
next if combined_addresses . none? { | address | address =~ group . email_username_regex }
end
email_addresses . add ( incoming_email . from_address )
email_addresses . merge ( combined_addresses )
end
email_addresses . subtract ( [ nil , "" ] )
email_addresses . delete ( group . email_username ) if group . present?
email_addresses . to_a
end
2022-07-13 13:58:01 +10:00
def create_invite_notification! ( target_user , notification_type , invited_by , post_number : 1 )
2022-07-14 15:23:09 +10:00
if UserCommScreener . new (
acting_user : invited_by ,
target_user_ids : target_user . id ,
) . ignoring_or_muting_actor? ( target_user . id )
2022-07-13 13:58:01 +10:00
raise NotAllowed . new ( I18n . t ( "not_accepting_pms" , username : target_user . username ))
end
2021-10-28 21:03:00 +05:30
2021-04-23 19:18:23 +03:00
target_user . notifications . create! (
notification_type : notification_type ,
topic_id : self . id ,
2022-02-21 22:14:28 +02:00
post_number : post_number ,
2021-04-23 19:18:23 +03:00
data : {
topic_title : self . title ,
2022-07-13 13:58:01 +10:00
display_username : invited_by . username ,
2021-05-26 05:55:07 +03:00
original_user_id : user . id ,
original_username : user . username ,
2021-04-23 19:18:23 +03:00
} . to_json ,
)
end
def rate_limit_topic_invitation ( invited_by )
RateLimiter . new (
invited_by ,
"topic-invitations-per-day" ,
SiteSetting . max_topic_invitations_per_day ,
1 . day . to_i ,
) . performed!
2022-10-10 19:21:51 +03:00
RateLimiter . new (
invited_by ,
"topic-invitations-per-minute" ,
SiteSetting . max_topic_invitations_per_minute ,
1 . day . to_i ,
) . performed!
2021-04-23 19:18:23 +03:00
end
2021-10-26 18:31:15 +03:00
def cannot_permanently_delete_reason ( user )
2022-08-10 12:11:50 +03:00
all_posts_count =
Post
. with_deleted
. where ( topic_id : self . id )
. where (
post_type : [ Post . types [ :regular ] , Post . types [ :moderator_action ] , Post . types [ :whisper ]] ,
)
. count
if posts_count > 0 || all_posts_count > 1
2021-10-26 18:31:15 +03:00
I18n . t ( "post.cannot_permanently_delete.many_posts" )
elsif self . deleted_by_id == user &. id && self . deleted_at >= Post :: PERMANENT_DELETE_TIMER . ago
time_left =
RateLimiter . time_left (
Post :: PERMANENT_DELETE_TIMER . to_i - Time . zone . now . to_i + self . deleted_at . to_i ,
)
I18n . t ( "post.cannot_permanently_delete.wait_or_different_admin" , time_left : time_left )
end
end
2021-11-16 10:21:49 +10:00
def first_smtp_enabled_group
self . allowed_groups . where ( smtp_enabled : true ) . first
end
2022-06-27 18:21:05 -03:00
def secure_audience_publish_messages
target_audience = {}
if private_message?
target_audience [ :user_ids ] = User . human_users . where ( "admin OR moderator" ) . pluck ( :id )
target_audience [ :user_ids ] |= allowed_users . pluck ( :id )
target_audience [ :user_ids ] |= allowed_group_users . pluck ( :id )
else
target_audience [ :group_ids ] = secure_group_ids
end
target_audience
end
def self . publish_stats_to_clients! ( topic_id , type , opts = {})
topic = Topic . find_by ( id : topic_id )
return unless topic . present?
case type
when :liked , :unliked
stats = { like_count : topic . like_count }
when :created , :destroyed , :deleted , :recovered
stats = {
posts_count : topic . posts_count ,
last_posted_at : topic . last_posted_at . as_json ,
last_poster : BasicUserSerializer . new ( topic . last_poster , root : false ) . as_json ,
}
else
stats = nil
end
if stats
secure_audience = topic . secure_audience_publish_messages
if secure_audience [ :user_ids ] != [] && secure_audience [ :group_ids ] != []
message = stats . merge ({ id : topic_id , updated_at : Time . now , type : :stats })
MessageBus . publish ( "/topic/ #{ topic_id } " , message , opts . merge ( secure_audience ))
end
end
end
2023-03-07 17:58:14 +01:00
def group_pm?
private_message? && all_allowed_users . count > 2
end
2023-07-28 12:53:50 +01:00
def visible_tags ( guardian )
tags . reject { | tag | guardian . hidden_tag_names . include? ( tag [ :name ] ) }
end
2023-10-10 11:23:56 +08:00
def self . editable_custom_fields ( guardian )
fields = []
fields . push ( * DiscoursePluginRegistry . public_editable_topic_custom_fields )
fields . push ( * DiscoursePluginRegistry . staff_editable_topic_custom_fields ) if guardian . is_staff?
fields
end
2013-07-08 15:23:20 -04:00
private
2018-12-05 23:43:07 +08:00
def invite_to_private_message ( invited_by , target_user , guardian )
if ! guardian . can_send_private_message? ( target_user )
raise UserExists . new ( I18n . t ( "activerecord.errors.models.topic.attributes.base.cant_send_pm" ))
end
2022-10-10 19:21:51 +03:00
rate_limit_topic_invitation ( invited_by )
2018-12-05 23:43:07 +08:00
Topic . transaction do
2019-03-29 17:03:33 +01:00
unless topic_allowed_users . exists? ( user_id : target_user . id )
topic_allowed_users . create! ( user_id : target_user . id )
2023-01-09 12:20:10 +00:00
end
2020-02-27 14:45:20 +02:00
user_in_allowed_group = ( user . group_ids & topic_allowed_groups . map ( & :group_id )) . present?
add_small_action ( invited_by , "invited_user" , target_user . username ) if ! user_in_allowed_group
2018-12-05 23:43:07 +08:00
create_invite_notification! (
target_user ,
Notification . types [ :invited_to_private_message ] ,
2022-07-13 13:58:01 +10:00
invited_by ,
2018-12-05 23:43:07 +08:00
)
end
end
def invite_to_topic ( invited_by , target_user , group_ids , guardian )
Topic . transaction do
rate_limit_topic_invitation ( invited_by )
2021-04-23 19:18:23 +03:00
if group_ids . present?
2018-12-05 23:43:07 +08:00
(
self . category . groups . where ( id : group_ids ) . where ( automatic : false ) -
target_user . groups . where ( automatic : false )
) . each do | group |
if guardian . can_edit_group? ( group )
group . add ( target_user )
GroupActionLogger . new ( invited_by , group ) . log_add_user_to_group ( target_user )
end
end
end
if Guardian . new ( target_user ) . can_see_topic? ( self )
create_invite_notification! ( target_user , Notification . types [ :invited_to_topic ] , invited_by )
end
end
end
2013-10-16 14:58:18 +05:30
def limit_first_day_topics_per_day
apply_per_day_rate_limit_for ( "first-day-topics" , :max_topics_in_first_day )
end
def apply_per_day_rate_limit_for ( key , method_name )
2019-05-07 11:00:09 +10:00
RateLimiter . new ( user , " #{ key } -per-day" , SiteSetting . get ( method_name ), 1 . day . to_i )
2013-10-16 14:58:18 +05:30
end
2013-02-05 14:16:51 -05:00
end
2013-05-24 12:48:32 +10:00
# == Schema Information
#
# Table name: topics
#
2017-11-23 15:55:44 -05:00
# id :integer not null, primary key
2019-01-11 14:29:56 -05:00
# title :string not null
2017-11-23 15:55:44 -05:00
# last_posted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# views :integer default(0), not null
# posts_count :integer default(0), not null
# user_id :integer
# last_post_user_id :integer not null
# reply_count :integer default(0), not null
# featured_user1_id :integer
# featured_user2_id :integer
# featured_user3_id :integer
# deleted_at :datetime
# highest_post_number :integer default(0), not null
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# category_id :integer
# visible :boolean default(TRUE), not null
# moderator_posts_count :integer default(0), not null
# closed :boolean default(FALSE), not null
# archived :boolean default(FALSE), not null
# bumped_at :datetime not null
# has_summary :boolean default(FALSE), not null
2019-01-11 14:29:56 -05:00
# archetype :string default("regular"), not null
2017-11-23 15:55:44 -05:00
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# pinned_at :datetime
# score :float
2019-01-03 12:03:01 -05:00
# percent_rank :float default(1.0), not null
2019-01-11 14:29:56 -05:00
# subtype :string
# slug :string
2017-11-23 15:55:44 -05:00
# deleted_by_id :integer
# participant_count :integer default(1)
# word_count :integer
2021-06-02 18:16:03 +04:00
# excerpt :string
2017-11-23 15:55:44 -05:00
# pinned_globally :boolean default(FALSE), not null
# pinned_until :datetime
2021-12-21 21:24:23 +03:00
# fancy_title :string
2017-11-23 15:55:44 -05:00
# highest_staff_post_number :integer default(0), not null
# featured_link :string
2019-01-03 12:03:01 -05:00
# reviewable_score :float default(0.0), not null
2020-05-05 09:07:50 +01:00
# image_upload_id :bigint
2020-10-27 23:42:33 +05:30
# slow_mode_seconds :integer default(0), not null
2021-07-05 23:14:15 +01:00
# bannered_until :datetime
2022-02-08 20:55:32 -07:00
# external_id :string
2013-05-24 12:48:32 +10:00
#
# Indexes
#
2015-09-18 10:41:10 +10:00
# idx_topics_front_page (deleted_at,visible,archetype,category_id,id)
2018-07-16 14:18:07 +08:00
# idx_topics_user_id_deleted_at (user_id) WHERE (deleted_at IS NULL)
# idxtopicslug (slug) WHERE ((deleted_at IS NULL) AND (slug IS NOT NULL))
2021-07-05 23:14:15 +01:00
# index_topics_on_bannered_until (bannered_until) WHERE (bannered_until IS NOT NULL)
2021-09-28 10:05:00 +08:00
# index_topics_on_bumped_at_public (bumped_at) WHERE ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text))
2018-07-16 14:18:07 +08:00
# index_topics_on_created_at_and_visible (created_at,visible) WHERE ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text))
2022-02-08 20:55:32 -07:00
# index_topics_on_external_id (external_id) UNIQUE WHERE (external_id IS NOT NULL)
2015-09-18 10:41:10 +10:00
# index_topics_on_id_and_deleted_at (id,deleted_at)
2019-11-01 11:21:57 +11:00
# index_topics_on_id_filtered_banner (id) UNIQUE WHERE (((archetype)::text = 'banner'::text) AND (deleted_at IS NULL))
2020-05-05 09:07:50 +01:00
# index_topics_on_image_upload_id (image_upload_id)
2017-10-06 11:13:01 +08:00
# index_topics_on_lower_title (lower((title)::text))
2018-07-16 14:18:07 +08:00
# index_topics_on_pinned_at (pinned_at) WHERE (pinned_at IS NOT NULL)
# index_topics_on_pinned_globally (pinned_globally) WHERE pinned_globally
2021-07-05 23:14:15 +01:00
# index_topics_on_pinned_until (pinned_until) WHERE (pinned_until IS NOT NULL)
2020-10-27 23:42:33 +05:30
# index_topics_on_timestamps_private (bumped_at,created_at,updated_at) WHERE ((deleted_at IS NULL) AND ((archetype)::text = 'private_message'::text))
2019-04-05 17:13:12 +08:00
# index_topics_on_updated_at_public (updated_at,visible,highest_staff_post_number,highest_post_number,category_id,created_at,id) WHERE (((archetype)::text <> 'private_message'::text) AND (deleted_at IS NULL))
2013-05-24 12:48:32 +10:00
#