discourse/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/track_selector.rb
Roman Rizzi 87b5d16c10
UX: Let users know they disabled discobot and they can't interact with it. (#12534)
Discobot will reply to users they need to enable onboarding tips to interact with them when issued a command.
2021-03-29 14:21:30 -03:00

267 lines
8.0 KiB
Ruby

# frozen_string_literal: true
module DiscourseNarrativeBot
class TrackSelector
include Actions
GENERIC_REPLIES_COUNT_PREFIX = 'discourse-narrative-bot:track-selector-count:'.freeze
PUBLIC_DISPLAY_BOT_HELP_KEY = 'discourse-narrative-bot:track-selector:display-bot-help'.freeze
TRACKS = [
AdvancedUserNarrative,
NewUserNarrative
]
TOPIC_ACTIONS = [
:delete,
:topic_notification_level_changed
].each(&:freeze)
RESET_TRIGGER_EXACT_MATCH_LENGTH = 200
def initialize(input, user, post_id:, topic_id: nil)
@input = input
@user = user
@post_id = post_id
@topic_id = topic_id
@post = Post.find_by(id: post_id)
end
def select
data = Store.get(@user.id)
if @post && @post.post_type == Post.types[:regular] && !is_topic_action?
is_reply = @input == :reply
@is_pm_to_bot = pm_to_bot?(@post)
return if is_reply && reset_track
if data && (data[:topic_id] == @post.topic_id) && @is_pm_to_bot
state = data[:state]
klass = (data[:track] || NewUserNarrative.to_s).constantize
if is_reply && like_user_post
terminate_track(data)
elsif state&.to_sym == :end && is_reply
bot_commands(bot_mentioned?) || generic_replies(klass.reset_trigger)
elsif is_reply
previous_status = data[:attempted]
current_status = klass.new.input(@input, @user, post: @post, skip: skip_track?)
data = Store.get(@user.id)
data[:attempted] = !current_status
if previous_status && data[:attempted] == previous_status && !data[:skip_attempted]
generic_replies(klass.reset_trigger, state)
else
Discourse.redis.del(generic_replies_key(@user))
end
Store.set(@user.id, data)
else
klass.new.input(@input, @user, post: @post, skip: skip_track?)
end
elsif is_reply && (@is_pm_to_bot || public_reply?)
like_user_post if @is_pm_to_bot
bot_commands
end
elsif data && data.dig(:state)&.to_sym != :end && is_topic_action?
klass = (data[:track] || NewUserNarrative.to_s).constantize
klass.new.input(@input, @user, post: @post, topic_id: @topic_id)
end
end
def self.reset_trigger
I18n.t(i18n_key("reset_trigger"))
end
def self.skip_trigger
I18n.t(i18n_key("skip_trigger"))
end
def self.help_trigger
I18n.t(i18n_key("help_trigger"))
end
def self.quote_trigger
I18n.t("discourse_narrative_bot.quote.trigger")
end
def self.dice_trigger
I18n.t("discourse_narrative_bot.dice.trigger")
end
def self.magic_8_ball_trigger
I18n.t("discourse_narrative_bot.magic_8_ball.trigger")
end
private
def is_topic_action?
@is_topic_action ||= TOPIC_ACTIONS.include?(@input)
end
def reset_track
reset = false
TRACKS.each do |klass|
if selected_track(klass)
klass.new.reset_bot(@user, @post)
reset = true
break
end
end
reset
end
def selected_track(klass)
trigger = "#{self.class.reset_trigger} #{klass.reset_trigger}"
if @post.raw.length < RESET_TRIGGER_EXACT_MATCH_LENGTH && @is_pm_to_bot
@post.raw.match(Regexp.new("\\b\\W\?#{trigger}\\W\?\\b", 'i'))
else
match_trigger?(trigger)
end
end
def bot_commands(hint = true)
raw =
if @user.manually_disabled_discobot?
I18n.t(self.class.i18n_key('random_mention.discobot_disabled'))
elsif match_data = match_trigger?("#{self.class.dice_trigger} (\\d+)d(\\d+)")
DiscourseNarrativeBot::Dice.roll(match_data[1].to_i, match_data[2].to_i)
elsif match_trigger?(self.class.quote_trigger)
DiscourseNarrativeBot::QuoteGenerator.generate(@user)
elsif match_trigger?(self.class.magic_8_ball_trigger)
DiscourseNarrativeBot::Magic8Ball.generate_answer
elsif match_trigger?(self.class.help_trigger)
help_message
elsif hint
message = I18n.t(self.class.i18n_key('random_mention.reply'),
discobot_username: self.discobot_username,
help_trigger: self.class.help_trigger
)
if public_reply?
key = "#{PUBLIC_DISPLAY_BOT_HELP_KEY}:#{@post.topic_id}"
last_bot_help_post_number = Discourse.redis.get(key)
if !last_bot_help_post_number ||
(last_bot_help_post_number &&
@post.post_number - 10 > last_bot_help_post_number.to_i &&
(1.day.to_i - Discourse.redis.ttl(key)) > 6.hours.to_i)
Discourse.redis.setex(key, 1.day.to_i, @post.post_number)
message
end
else
message
end
end
if raw
fake_delay
reply_to(@post, raw, skip_validations: true)
end
end
def help_message
message = I18n.t(
self.class.i18n_key('random_mention.tracks'),
discobot_username: self.discobot_username,
reset_trigger: self.class.reset_trigger,
tracks: [NewUserNarrative.reset_trigger, AdvancedUserNarrative.reset_trigger].join(', ')
)
message << "\n\n#{I18n.t(self.class.i18n_key('random_mention.bot_actions'),
discobot_username: self.discobot_username,
dice_trigger: self.class.dice_trigger,
quote_trigger: self.class.quote_trigger,
quote_sample: DiscourseNarrativeBot::QuoteGenerator.generate(@user),
magic_8_ball_trigger: self.class.magic_8_ball_trigger
)}"
end
def generic_replies_key(user)
"#{GENERIC_REPLIES_COUNT_PREFIX}#{user.id}"
end
def generic_replies(track_reset_trigger, state = nil)
reset_trigger = "#{self.class.reset_trigger} #{track_reset_trigger}"
key = generic_replies_key(@user)
count = (Discourse.redis.get(key) || Discourse.redis.setex(key, 900, 0)).to_i
case count
when 0
raw = I18n.t(self.class.i18n_key('do_not_understand.first_response'))
if state && state.to_sym != :end
raw = "#{raw}\n\n#{I18n.t(self.class.i18n_key('do_not_understand.track_response'), reset_trigger: reset_trigger, skip_trigger: self.class.skip_trigger)}"
end
reply_to(@post, raw)
when 1
reply_to(@post, I18n.t(self.class.i18n_key('do_not_understand.second_response'),
base_path: Discourse.base_path,
reset_trigger: self.class.reset_trigger
))
else
# Stay out of the user's way
end
Discourse.redis.incr(key)
end
def self.i18n_key(key)
"discourse_narrative_bot.track_selector.#{key}"
end
def skip_track?
if @is_pm_to_bot
@post.raw.match(/((^@#{self.discobot_username} #{self.class.skip_trigger})|(^#{self.class.skip_trigger}$))/i)
else
false
end
end
@@cooked_triggers = {}
def cook(trigger)
@@cooked_triggers[trigger] ||= PrettyText.cook("@#{self.discobot_username} #{trigger}")
end
def match_trigger?(trigger)
# we remove the leading <p> to allow for trigger to be at the end of a paragraph
cooked_trigger = cook(trigger)[3..-1]
regexp = Regexp.new(cooked_trigger, 'i')
match = @post.cooked.match(regexp)
if @is_pm_to_bot
match || @post.raw.strip.match(Regexp.new("^#{trigger}$", 'i'))
else
match
end
end
def like_user_post
if @post.raw.match(/thank/i)
PostActionCreator.like(self.discobot_user, @post)
end
end
def bot_mentioned?
@bot_mentioned ||= PostAnalyzer.new(@post.raw, @post.topic_id).raw_mentions.include?(self.discobot_username)
end
def public_reply?
!SiteSetting.discourse_narrative_bot_disable_public_replies &&
(reply_to_bot_post?(@post) || bot_mentioned?)
end
def terminate_track(data)
Store.set(@user.id, data.merge!(track: nil, state: nil, topic_id: nil))
cancel_timeout_job(@user)
end
end
end