mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 19:05:18 +08:00
5f64fd0a21
Introduce new patterns for direct sql that are safe and fast. MiniSql is not prone to memory bloat that can happen with direct PG usage. It also has an extremely fast materializer and very a convenient API - DB.exec(sql, *params) => runs sql returns row count - DB.query(sql, *params) => runs sql returns usable objects (not a hash) - DB.query_hash(sql, *params) => runs sql returns an array of hashes - DB.query_single(sql, *params) => runs sql and returns a flat one dimensional array - DB.build(sql) => returns a sql builder See more at: https://github.com/discourse/mini_sql
167 lines
5.8 KiB
Ruby
167 lines
5.8 KiB
Ruby
module Jobs
|
|
class UpdateUsername < Jobs::Base
|
|
|
|
sidekiq_options queue: 'low'
|
|
|
|
def execute(args)
|
|
@user_id = args[:user_id]
|
|
@old_username = args[:old_username]
|
|
@new_username = args[:new_username]
|
|
@avatar_img = PrettyText.avatar_img(args[:avatar_template], "tiny")
|
|
|
|
@raw_mention_regex = /(?:(?<![\w`_])|(?<=_))@#{@old_username}(?:(?![\w\-\.])|(?=[\-\.](?:\s|$)))/i
|
|
@raw_quote_regex = /(\[quote\s*=\s*["'']?)#{@old_username}(\,?[^\]]*\])/i
|
|
|
|
cooked_username = PrettyText::Helpers.format_username(@old_username)
|
|
@cooked_mention_username_regex = /^@#{cooked_username}$/i
|
|
@cooked_mention_user_path_regex = /^\/u(?:sers)?\/#{cooked_username}$/i
|
|
@cooked_quote_username_regex = /(?<=\s)#{cooked_username}(?=:)/i
|
|
|
|
update_posts
|
|
update_revisions
|
|
update_notifications
|
|
update_post_custom_fields
|
|
end
|
|
|
|
def update_posts
|
|
Post.with_deleted.where(post_conditions("posts.id"), post_condition_args).find_each do |post|
|
|
begin
|
|
post.raw = update_raw(post.raw)
|
|
post.cooked = update_cooked(post.cooked)
|
|
|
|
# update without running validations and hooks
|
|
post.update_columns(raw: post.raw, cooked: post.cooked)
|
|
|
|
SearchIndexer.index(post, force: true) if post.topic
|
|
rescue => e
|
|
Discourse.warn_exception(e, message: "Failed to update post with id #{post.id}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_revisions
|
|
PostRevision.where(post_conditions("post_revisions.post_id"), post_condition_args).find_each do |revision|
|
|
begin
|
|
if revision.modifications.key?("raw") || revision.modifications.key?("cooked")
|
|
revision.modifications["raw"]&.map! { |raw| update_raw(raw) }
|
|
revision.modifications["cooked"]&.map! { |cooked| update_cooked(cooked) }
|
|
revision.save!
|
|
end
|
|
rescue => e
|
|
Discourse.warn_exception(e, message: "Failed to update post revision with id #{revision.id}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_notifications
|
|
params = {
|
|
user_id: @user_id,
|
|
old_username: @old_username,
|
|
new_username: @new_username
|
|
}
|
|
|
|
DB.exec(<<~SQL, params)
|
|
UPDATE notifications
|
|
SET data = (data :: JSONB ||
|
|
jsonb_strip_nulls(
|
|
jsonb_build_object(
|
|
'original_username', CASE data :: JSONB ->> 'original_username'
|
|
WHEN :old_username
|
|
THEN :new_username
|
|
ELSE NULL END,
|
|
'display_username', CASE data :: JSONB ->> 'display_username'
|
|
WHEN :old_username
|
|
THEN :new_username
|
|
ELSE NULL END,
|
|
'username', CASE data :: JSONB ->> 'username'
|
|
WHEN :old_username
|
|
THEN :new_username
|
|
ELSE NULL END,
|
|
'username2', CASE data :: JSONB ->> 'username2'
|
|
WHEN :old_username
|
|
THEN :new_username
|
|
ELSE NULL END
|
|
)
|
|
)) :: JSON
|
|
WHERE data ILIKE '%' || :old_username || '%'
|
|
SQL
|
|
end
|
|
|
|
def update_post_custom_fields
|
|
DB.exec(<<~SQL, old_username: @old_username, new_username: @new_username)
|
|
UPDATE post_custom_fields
|
|
SET value = :new_username
|
|
WHERE name = 'action_code_who' AND value = :old_username
|
|
SQL
|
|
end
|
|
|
|
protected
|
|
|
|
def post_conditions(post_id_column)
|
|
<<~SQL
|
|
EXISTS(
|
|
SELECT 1
|
|
FROM user_actions AS a
|
|
WHERE a.target_post_id = #{post_id_column} AND
|
|
a.action_type = :mentioned AND
|
|
a.user_id = :user_id
|
|
) OR EXISTS(
|
|
SELECT 1
|
|
FROM quoted_posts AS q
|
|
JOIN posts AS p ON (q.quoted_post_id = p.id)
|
|
WHERE q.post_id = #{post_id_column} AND
|
|
p.user_id = :user_id
|
|
)
|
|
SQL
|
|
end
|
|
|
|
def post_condition_args
|
|
{ mentioned: UserAction::MENTION, user_id: @user_id }
|
|
end
|
|
|
|
def update_raw(raw)
|
|
raw.gsub(@raw_mention_regex, "@#{@new_username}")
|
|
.gsub(@raw_quote_regex, "\\1#{@new_username}\\2")
|
|
end
|
|
|
|
# Uses Nokogiri instead of rebake, because it works for posts and revisions
|
|
# and there is no reason to invalidate oneboxes, run the post analyzer etc.
|
|
# when only the username changes.
|
|
def update_cooked(cooked)
|
|
doc = Nokogiri::HTML.fragment(cooked)
|
|
|
|
doc.css("a.mention").each do |a|
|
|
a.content = a.content.gsub(@cooked_mention_username_regex, "@#{@new_username}")
|
|
a["href"] = a["href"].gsub(@cooked_mention_user_path_regex, "/u/#{@new_username}") if a["href"]
|
|
end
|
|
|
|
doc.css("aside.quote").each do |aside|
|
|
next unless div = aside.at_css("div.title")
|
|
|
|
username_replaced = false
|
|
|
|
div.children.each do |child|
|
|
if child.text?
|
|
content = child.content
|
|
username_replaced = content.gsub!(@cooked_quote_username_regex, @new_username).present?
|
|
child.content = content if username_replaced
|
|
end
|
|
end
|
|
|
|
if username_replaced || quotes_correct_user?(aside)
|
|
div.at_css("img.avatar")&.replace(@avatar_img)
|
|
end
|
|
end
|
|
|
|
doc.to_html
|
|
end
|
|
|
|
def quotes_correct_user?(aside)
|
|
Post.where(
|
|
topic_id: aside["data-topic"],
|
|
post_number: aside["data-post"]
|
|
).pluck(:user_id).first == @user_id
|
|
end
|
|
end
|
|
end
|