mirror of
https://github.com/discourse/discourse.git
synced 2025-03-02 01:09:26 +00:00
The discobot advanced tutorial was failing when the new hashtags were enabled with enable_experimental_hashtag_autocomplete set to true, since the CSS selector is different. This commit fixes the issue and also changes the instructions if this is enabled since we no longer require the hashtag to not be at the start of the line. c.f. https://meta.discourse.org/t/it-is-impossible-to-complete-the-hashtag-section-of-the-discobot-advanced-tutorial/251494
436 lines
12 KiB
Ruby
436 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module DiscourseNarrativeBot
|
|
class AdvancedUserNarrative < Base
|
|
I18N_KEY = "discourse_narrative_bot.advanced_user_narrative".freeze
|
|
BADGE_NAME = "Licensed".freeze
|
|
|
|
TRANSITION_TABLE = {
|
|
begin: {
|
|
next_state: :tutorial_edit,
|
|
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.edit.instructions", i18n_post_args) },
|
|
init: {
|
|
action: :start_advanced_track,
|
|
},
|
|
},
|
|
tutorial_edit: {
|
|
next_state: :tutorial_delete,
|
|
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.delete.instructions", i18n_post_args) },
|
|
edit: {
|
|
action: :reply_to_edit,
|
|
},
|
|
reply: {
|
|
next_state: :tutorial_edit,
|
|
action: :missing_edit,
|
|
},
|
|
},
|
|
tutorial_delete: {
|
|
next_state: :tutorial_recover,
|
|
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.recover.instructions", i18n_post_args) },
|
|
delete: {
|
|
action: :reply_to_delete,
|
|
},
|
|
reply: {
|
|
next_state: :tutorial_delete,
|
|
action: :missing_delete,
|
|
},
|
|
},
|
|
tutorial_recover: {
|
|
next_state: :tutorial_category_hashtag,
|
|
next_instructions:
|
|
Proc.new do
|
|
category = Category.secured(Guardian.new(@user)).last
|
|
slug = category.slug
|
|
|
|
if parent_category = category.parent_category
|
|
slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}"
|
|
end
|
|
|
|
# TODO (martin) When enable_experimental_hashtag_autocomplete is the only option
|
|
# update the instructions and remove instructions_experimental, as well as the
|
|
# not_found translation
|
|
if SiteSetting.enable_experimental_hashtag_autocomplete
|
|
I18n.t(
|
|
"#{I18N_KEY}.category_hashtag.instructions_experimental",
|
|
i18n_post_args(category: "##{slug}"),
|
|
)
|
|
else
|
|
I18n.t(
|
|
"#{I18N_KEY}.category_hashtag.instructions",
|
|
i18n_post_args(category: "##{slug}"),
|
|
)
|
|
end
|
|
end,
|
|
recover: {
|
|
action: :reply_to_recover,
|
|
},
|
|
reply: {
|
|
next_state: :tutorial_recover,
|
|
action: :missing_recover,
|
|
},
|
|
},
|
|
tutorial_category_hashtag: {
|
|
next_state: :tutorial_change_topic_notification_level,
|
|
next_instructions:
|
|
Proc.new do
|
|
I18n.t("#{I18N_KEY}.change_topic_notification_level.instructions", i18n_post_args)
|
|
end,
|
|
reply: {
|
|
action: :reply_to_category_hashtag,
|
|
},
|
|
},
|
|
tutorial_change_topic_notification_level: {
|
|
next_state: :tutorial_poll,
|
|
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.poll.instructions", i18n_post_args) },
|
|
topic_notification_level_changed: {
|
|
action: :reply_to_topic_notification_level_changed,
|
|
},
|
|
reply: {
|
|
next_state: :tutorial_change_topic_notification_level,
|
|
action: :missing_topic_notification_level_change,
|
|
},
|
|
},
|
|
tutorial_poll: {
|
|
prerequisite:
|
|
Proc.new do
|
|
SiteSetting.poll_enabled &&
|
|
@user.has_trust_level?(SiteSetting.poll_minimum_trust_level_to_create)
|
|
end,
|
|
next_state: :tutorial_details,
|
|
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions", i18n_post_args) },
|
|
reply: {
|
|
action: :reply_to_poll,
|
|
},
|
|
},
|
|
tutorial_details: {
|
|
next_state: :end,
|
|
reply: {
|
|
action: :reply_to_details,
|
|
},
|
|
},
|
|
}
|
|
|
|
def self.badge_name
|
|
BADGE_NAME
|
|
end
|
|
|
|
def self.reset_trigger
|
|
I18n.t("discourse_narrative_bot.advanced_user_narrative.reset_trigger")
|
|
end
|
|
|
|
def reset_bot(user, post)
|
|
if pm_to_bot?(post)
|
|
reset_data(user, topic_id: post.topic_id)
|
|
else
|
|
reset_data(user)
|
|
end
|
|
|
|
Jobs.enqueue_in(2.seconds, :narrative_init, user_id: user.id, klass: self.class.to_s)
|
|
end
|
|
|
|
private
|
|
|
|
def init_tutorial_edit
|
|
data = get_data(@user)
|
|
|
|
fake_delay
|
|
|
|
post =
|
|
PostCreator.create!(
|
|
@user,
|
|
raw:
|
|
I18n.t(
|
|
"#{I18N_KEY}.edit.bot_created_post_raw",
|
|
i18n_post_args(discobot_username: self.discobot_username),
|
|
),
|
|
topic_id: data[:topic_id],
|
|
skip_bot: true,
|
|
skip_validations: true,
|
|
)
|
|
|
|
set_state_data(:post_id, post.id)
|
|
post
|
|
end
|
|
|
|
def init_tutorial_recover
|
|
data = get_data(@user)
|
|
|
|
post =
|
|
PostCreator.create!(
|
|
@user,
|
|
raw:
|
|
I18n.t(
|
|
"#{I18N_KEY}.recover.deleted_post_raw",
|
|
i18n_post_args(discobot_username: self.discobot_username),
|
|
),
|
|
topic_id: data[:topic_id],
|
|
skip_bot: true,
|
|
skip_validations: true,
|
|
)
|
|
|
|
set_state_data(:post_id, post.id)
|
|
|
|
opts = { skip_bot: true }
|
|
|
|
if SiteSetting.delete_removed_posts_after < 1
|
|
opts[:delete_removed_posts_after] = 1
|
|
|
|
result = PostActionCreator.notify_moderators(self.discobot_user, post)
|
|
result.reviewable.perform(self.discobot_user, :ignore)
|
|
end
|
|
|
|
PostDestroyer.new(@user, post, opts).destroy
|
|
end
|
|
|
|
def start_advanced_track
|
|
raw = I18n.t("#{I18N_KEY}.start_message", i18n_post_args(username: @user.username))
|
|
|
|
raw = <<~MD
|
|
#{raw}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
opts = {
|
|
title: I18n.t("#{I18N_KEY}.title"),
|
|
target_usernames: @user.username,
|
|
archetype: Archetype.private_message,
|
|
}
|
|
|
|
if @post && @post.topic.private_message? &&
|
|
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
|
|
end
|
|
|
|
if @data[:topic_id]
|
|
opts = opts.merge(topic_id: @data[:topic_id]).except(:title, :target_usernames, :archetype)
|
|
end
|
|
post = reply_to(@post, raw, opts)
|
|
|
|
@data[:topic_id] = post.topic_id
|
|
@data[:track] = self.class.to_s
|
|
post
|
|
end
|
|
|
|
def reply_to_edit
|
|
return unless valid_topic?(@post.topic_id)
|
|
|
|
fake_delay
|
|
|
|
raw = <<~MD
|
|
#{I18n.t("#{I18N_KEY}.edit.reply", i18n_post_args)}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
reply_to(@post, raw)
|
|
end
|
|
|
|
def missing_edit
|
|
post_id = get_state_data(:post_id)
|
|
return unless valid_topic?(@post.topic_id) && post_id != @post.id
|
|
|
|
fake_delay
|
|
|
|
unless @data[:attempted]
|
|
reply_to(
|
|
@post,
|
|
I18n.t("#{I18N_KEY}.edit.not_found", i18n_post_args(url: Post.find_by(id: post_id).url)),
|
|
)
|
|
end
|
|
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
|
|
def reply_to_delete
|
|
return unless valid_topic?(@topic_id)
|
|
|
|
fake_delay
|
|
|
|
raw = <<~MD
|
|
#{I18n.t("#{I18N_KEY}.delete.reply", i18n_post_args)}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
PostCreator.create!(self.discobot_user, raw: raw, topic_id: @topic_id)
|
|
end
|
|
|
|
def missing_delete
|
|
return unless valid_topic?(@post.topic_id)
|
|
fake_delay
|
|
unless @data[:attempted]
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.delete.not_found", i18n_post_args))
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
|
|
def reply_to_recover
|
|
return unless valid_topic?(@post.topic_id)
|
|
|
|
fake_delay
|
|
|
|
raw = <<~MD
|
|
#{I18n.t("#{I18N_KEY}.recover.reply", i18n_post_args(deletion_after: SiteSetting.delete_removed_posts_after))}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
PostCreator.create!(self.discobot_user, raw: raw, topic_id: @post.topic_id)
|
|
end
|
|
|
|
def missing_recover
|
|
unless valid_topic?(@post.topic_id) &&
|
|
post_id = get_state_data(:post_id) && @post.id != post_id
|
|
return
|
|
end
|
|
|
|
fake_delay
|
|
unless @data[:attempted]
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.recover.not_found", i18n_post_args))
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
|
|
def reply_to_category_hashtag
|
|
topic_id = @post.topic_id
|
|
return unless valid_topic?(topic_id)
|
|
|
|
hashtag_css_class =
|
|
SiteSetting.enable_experimental_hashtag_autocomplete ? ".hashtag-cooked" : ".hashtag"
|
|
if Nokogiri::HTML5.fragment(@post.cooked).css(hashtag_css_class).size > 0
|
|
raw = <<~MD
|
|
#{I18n.t("#{I18N_KEY}.category_hashtag.reply", i18n_post_args)}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
fake_delay
|
|
reply_to(@post, raw)
|
|
else
|
|
fake_delay
|
|
unless @data[:attempted]
|
|
if SiteSetting.enable_experimental_hashtag_autocomplete
|
|
reply_to(
|
|
@post,
|
|
I18n.t("#{I18N_KEY}.category_hashtag.not_found_experimental", i18n_post_args),
|
|
)
|
|
else
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found", i18n_post_args))
|
|
end
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
end
|
|
|
|
def missing_topic_notification_level_change
|
|
return unless valid_topic?(@post.topic_id)
|
|
|
|
fake_delay
|
|
unless @data[:attempted]
|
|
reply_to(
|
|
@post,
|
|
I18n.t("#{I18N_KEY}.change_topic_notification_level.not_found", i18n_post_args),
|
|
)
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
|
|
def reply_to_topic_notification_level_changed
|
|
return unless valid_topic?(@topic_id)
|
|
|
|
fake_delay
|
|
raw = <<~MD
|
|
#{I18n.t("#{I18N_KEY}.change_topic_notification_level.reply", i18n_post_args)}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
fake_delay
|
|
|
|
post = PostCreator.create!(self.discobot_user, raw: raw, topic_id: @topic_id)
|
|
|
|
enqueue_timeout_job(@user)
|
|
post
|
|
end
|
|
|
|
def reply_to_poll
|
|
topic_id = @post.topic_id
|
|
return unless valid_topic?(topic_id)
|
|
|
|
if Nokogiri::HTML5.fragment(@post.cooked).css(".poll").size > 0
|
|
raw = <<~MD
|
|
#{I18n.t("#{I18N_KEY}.poll.reply", i18n_post_args)}
|
|
|
|
#{instance_eval(&@next_instructions)}
|
|
MD
|
|
|
|
fake_delay
|
|
reply_to(@post, raw)
|
|
else
|
|
fake_delay
|
|
unless @data[:attempted]
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.poll.not_found", i18n_post_args))
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
end
|
|
|
|
def reply_to_details
|
|
topic_id = @post.topic_id
|
|
return unless valid_topic?(topic_id)
|
|
|
|
fake_delay
|
|
|
|
if Nokogiri::HTML5.fragment(@post.cooked).css("details").size > 0
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.details.reply", i18n_post_args))
|
|
else
|
|
unless @data[:attempted]
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.details.not_found", i18n_post_args))
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
end
|
|
|
|
def reply_to_wiki
|
|
topic_id = @post.topic_id
|
|
return unless valid_topic?(topic_id)
|
|
|
|
fake_delay
|
|
|
|
if @post.wiki
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.reply", i18n_post_args))
|
|
else
|
|
unless @data[:attempted]
|
|
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.not_found", i18n_post_args))
|
|
end
|
|
enqueue_timeout_job(@user)
|
|
false
|
|
end
|
|
end
|
|
|
|
def end_reply
|
|
fake_delay
|
|
|
|
reply_to(
|
|
@post,
|
|
I18n.t("#{I18N_KEY}.end.message", i18n_post_args(certificate: certificate("advanced"))),
|
|
)
|
|
end
|
|
|
|
def synchronize(user)
|
|
if Rails.env.test?
|
|
yield
|
|
else
|
|
DistributedMutex.synchronize("advanced_user_narrative_#{user.id}") { yield }
|
|
end
|
|
end
|
|
end
|
|
end
|