mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
Merge discourse-narrative-bot into core plugins.
This commit is contained in:
@ -0,0 +1,85 @@
|
||||
module DiscourseNarrativeBot
|
||||
module Actions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
def self.discobot_user
|
||||
@discobot ||= User.find(-2)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reply_to(post, raw, opts = {})
|
||||
if post
|
||||
default_opts = {
|
||||
raw: raw,
|
||||
topic_id: post.topic_id,
|
||||
reply_to_post_number: post.post_number
|
||||
}
|
||||
|
||||
new_post = PostCreator.create!(self.class.discobot_user, default_opts.merge(opts))
|
||||
reset_rate_limits(post) if new_post
|
||||
new_post
|
||||
else
|
||||
PostCreator.create!(self.class.discobot_user, { raw: raw }.merge(opts))
|
||||
end
|
||||
end
|
||||
|
||||
def reset_rate_limits(post)
|
||||
user = post.user
|
||||
data = DiscourseNarrativeBot::Store.get(user.id.to_s)
|
||||
|
||||
return unless data
|
||||
|
||||
key = "#{DiscourseNarrativeBot::PLUGIN_NAME}:reset-rate-limit:#{post.topic_id}:#{data['state']}"
|
||||
|
||||
if !(count = $redis.get(key))
|
||||
count = 0
|
||||
|
||||
duration =
|
||||
if user && user.new_user?
|
||||
SiteSetting.rate_limit_new_user_create_post
|
||||
else
|
||||
SiteSetting.rate_limit_create_post
|
||||
end
|
||||
|
||||
$redis.setex(key, duration, count)
|
||||
end
|
||||
|
||||
if count.to_i < 2
|
||||
post.default_rate_limiter.rollback!
|
||||
post.limit_posts_per_day&.rollback!
|
||||
$redis.incr(key)
|
||||
end
|
||||
end
|
||||
|
||||
def fake_delay
|
||||
sleep(rand(2..3)) if Rails.env.production?
|
||||
end
|
||||
|
||||
def bot_mentioned?(post)
|
||||
doc = Nokogiri::HTML.fragment(post.cooked)
|
||||
|
||||
valid = false
|
||||
|
||||
doc.css(".mention").each do |mention|
|
||||
valid = true if mention.text == "@#{self.class.discobot_user.username}"
|
||||
end
|
||||
|
||||
valid
|
||||
end
|
||||
|
||||
def reply_to_bot_post?(post)
|
||||
post&.reply_to_post && post.reply_to_post.user_id == -2
|
||||
end
|
||||
|
||||
def pm_to_bot?(post)
|
||||
topic = post.topic
|
||||
return false if !topic
|
||||
|
||||
topic.pm_with_non_human_user? &&
|
||||
topic.topic_allowed_users.where(user_id: -2).exists?
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,384 @@
|
||||
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") },
|
||||
init: {
|
||||
action: :start_advanced_track
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_edit: {
|
||||
next_state: :tutorial_delete,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.delete.instructions") },
|
||||
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") },
|
||||
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.last
|
||||
slug = category.slug
|
||||
|
||||
if parent_category = category.parent_category
|
||||
slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}"
|
||||
end
|
||||
|
||||
I18n.t("#{I18N_KEY}.category_hashtag.instructions",
|
||||
category: "##{slug}"
|
||||
)
|
||||
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 { I18n.t("#{I18N_KEY}.change_topic_notification_level.instructions") },
|
||||
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") },
|
||||
topic_notification_level_changed: {
|
||||
action: :reply_to_topic_notification_level_changed
|
||||
},
|
||||
reply: {
|
||||
next_state: :tutorial_notification_level,
|
||||
action: :missing_topic_notification_level_change
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_poll: {
|
||||
next_state: :tutorial_details,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions") },
|
||||
reply: {
|
||||
action: :reply_to_poll
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_details: {
|
||||
next_state: :end,
|
||||
reply: {
|
||||
action: :reply_to_details
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def self.can_start?(user)
|
||||
return true if user.staff?
|
||||
user.badges.where(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME).exists?
|
||||
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",
|
||||
discobot_username: self.class.discobot_user.username
|
||||
),
|
||||
topic_id: data[:topic_id],
|
||||
skip_bot: 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",
|
||||
discobot_username: self.class.discobot_user.username
|
||||
),
|
||||
topic_id: data[:topic_id],
|
||||
skip_bot: true
|
||||
})
|
||||
|
||||
set_state_data(:post_id, post.id)
|
||||
PostDestroyer.new(@user, post, skip_bot: true).destroy
|
||||
end
|
||||
|
||||
def start_advanced_track
|
||||
raw = I18n.t("#{I18N_KEY}.start_message", username: @user.username)
|
||||
|
||||
raw = <<~RAW
|
||||
#{raw}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
opts = {
|
||||
title: I18n.t("#{I18N_KEY}.title"),
|
||||
target_usernames: @user.username,
|
||||
archetype: Archetype.private_message
|
||||
}
|
||||
|
||||
if @post &&
|
||||
@post.archetype == Archetype.private_message &&
|
||||
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
|
||||
|
||||
opts = opts.merge(topic_id: @post.topic_id)
|
||||
end
|
||||
|
||||
if @data[:topic_id]
|
||||
opts = opts.merge(topic_id: @data[:topic_id])
|
||||
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 = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.edit.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
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",
|
||||
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 = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.delete.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
PostCreator.create!(self.class.discobot_user,
|
||||
raw: raw,
|
||||
topic_id: @topic_id
|
||||
)
|
||||
end
|
||||
|
||||
def missing_delete
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.delete.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
|
||||
def reply_to_recover
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
|
||||
fake_delay
|
||||
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.recover.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
PostCreator.create!(self.class.discobot_user,
|
||||
raw: raw,
|
||||
topic_id: @post.topic_id
|
||||
)
|
||||
end
|
||||
|
||||
def missing_recover
|
||||
return unless valid_topic?(@post.topic_id) &&
|
||||
post_id = get_state_data(:post_id) && @post.id != post_id
|
||||
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.recover.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
|
||||
def reply_to_category_hashtag
|
||||
topic_id = @post.topic_id
|
||||
return unless valid_topic?(topic_id)
|
||||
|
||||
if Nokogiri::HTML.fragment(@post.cooked).css('.hashtag').size > 0
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.category_hashtag.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
reply_to(@post, raw)
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def missing_topic_notification_level_change
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.change_topic_notification_level.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
|
||||
def reply_to_topic_notification_level_changed
|
||||
return unless valid_topic?(@topic_id)
|
||||
|
||||
fake_delay
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.change_topic_notification_level.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
post = PostCreator.create!(self.class.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::HTML.fragment(@post.cooked).css(".poll").size > 0
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.poll.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
reply_to(@post, raw)
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.poll.not_found")) unless @data[:attempted]
|
||||
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::HTML.fragment(@post.cooked).css("details").size > 0
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.details.reply"))
|
||||
else
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.details.not_found")) unless @data[:attempted]
|
||||
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"))
|
||||
else
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def end_reply
|
||||
fake_delay
|
||||
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.end.message",
|
||||
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
|
@ -0,0 +1,192 @@
|
||||
module DiscourseNarrativeBot
|
||||
class Base
|
||||
include Actions
|
||||
|
||||
TIMEOUT_DURATION = 900 # 15 mins
|
||||
|
||||
class InvalidTransitionError < StandardError; end
|
||||
|
||||
def input(input, user, post: nil, topic_id: nil, skip: false)
|
||||
new_post = nil
|
||||
@post = post
|
||||
@topic_id = topic_id
|
||||
@skip = skip
|
||||
|
||||
synchronize(user) do
|
||||
@user = user
|
||||
@data = get_data(user) || {}
|
||||
@state = (@data[:state] && @data[:state].to_sym) || :begin
|
||||
@input = input
|
||||
opts = {}
|
||||
|
||||
begin
|
||||
opts = transition
|
||||
rescue InvalidTransitionError
|
||||
# For given input, no transition for current state
|
||||
return
|
||||
end
|
||||
|
||||
next_state = opts[:next_state]
|
||||
action = opts[:action]
|
||||
|
||||
if next_instructions = opts[:next_instructions]
|
||||
@next_instructions = next_instructions
|
||||
end
|
||||
|
||||
begin
|
||||
old_data = @data.dup
|
||||
new_post = (@skip && @state != :end) ? skip_tutorial(next_state) : self.send(action)
|
||||
|
||||
if new_post
|
||||
old_state = old_data[:state]
|
||||
state_changed = (old_state.to_s != next_state.to_s)
|
||||
clean_up_state(old_state) if state_changed
|
||||
|
||||
@state = @data[:state] = next_state
|
||||
@data[:last_post_id] = new_post.id
|
||||
set_data(@user, @data)
|
||||
|
||||
init_state(next_state) if state_changed
|
||||
|
||||
if next_state == :end
|
||||
end_reply
|
||||
cancel_timeout_job(user)
|
||||
|
||||
BadgeGranter.grant(
|
||||
Badge.find_by(name: self.class::BADGE_NAME),
|
||||
user
|
||||
)
|
||||
|
||||
set_data(@user,
|
||||
topic_id: new_post.topic_id,
|
||||
state: :end,
|
||||
track: self.class.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
@data = old_data
|
||||
set_data(@user, @data)
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
new_post
|
||||
end
|
||||
|
||||
def reset_bot
|
||||
not_implemented
|
||||
end
|
||||
|
||||
def set_data(user, value)
|
||||
DiscourseNarrativeBot::Store.set(user.id, value)
|
||||
end
|
||||
|
||||
def get_data(user)
|
||||
DiscourseNarrativeBot::Store.get(user.id)
|
||||
end
|
||||
|
||||
def notify_timeout(user)
|
||||
@data = get_data(user) || {}
|
||||
|
||||
if post = Post.find_by(id: @data[:last_post_id])
|
||||
reply_to(post, I18n.t("discourse_narrative_bot.timeout.message",
|
||||
username: user.username,
|
||||
skip_trigger: TrackSelector.skip_trigger,
|
||||
reset_trigger: "#{TrackSelector.reset_trigger} #{self.class.reset_trigger}",
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
def certificate(type = nil)
|
||||
options = {
|
||||
user_id: @user.id,
|
||||
date: Time.zone.now.strftime('%b %d %Y'),
|
||||
host: Discourse.base_url,
|
||||
format: :svg
|
||||
}
|
||||
|
||||
options.merge!(type: type) if type
|
||||
src = DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_url(options)
|
||||
"<img class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{I18n.t("#{self.class::I18N_KEY}.certificate.alt")}'>"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_state_data(key, value)
|
||||
@data[@state] ||= {}
|
||||
@data[@state][key] = value
|
||||
set_data(@user, @data)
|
||||
end
|
||||
|
||||
def get_state_data(key)
|
||||
@data[@state] ||= {}
|
||||
@data[@state][key]
|
||||
end
|
||||
|
||||
def reset_data(user, additional_data = {})
|
||||
old_data = get_data(user)
|
||||
new_data = additional_data
|
||||
set_data(user, new_data)
|
||||
new_data
|
||||
end
|
||||
|
||||
def transition
|
||||
options = self.class::TRANSITION_TABLE.fetch(@state).dup
|
||||
input_options = options.fetch(@input)
|
||||
options.merge!(input_options) unless @skip
|
||||
options
|
||||
rescue KeyError
|
||||
raise InvalidTransitionError.new
|
||||
end
|
||||
|
||||
def skip_tutorial(next_state)
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
|
||||
fake_delay
|
||||
|
||||
if next_state != :end
|
||||
reply = reply_to(@post, instance_eval(&@next_instructions))
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
else
|
||||
@post
|
||||
end
|
||||
end
|
||||
|
||||
def valid_topic?(topic_id)
|
||||
topic_id == @data[:topic_id]
|
||||
end
|
||||
|
||||
def cancel_timeout_job(user)
|
||||
Jobs.cancel_scheduled_job(:narrative_timeout, user_id: user.id, klass: self.class.to_s)
|
||||
end
|
||||
|
||||
def enqueue_timeout_job(user)
|
||||
return if Rails.env.test?
|
||||
|
||||
cancel_timeout_job(user)
|
||||
|
||||
Jobs.enqueue_in(TIMEOUT_DURATION, :narrative_timeout,
|
||||
user_id: user.id,
|
||||
klass: self.class.to_s
|
||||
)
|
||||
end
|
||||
|
||||
def not_implemented
|
||||
raise 'Not implemented.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_up_state(state)
|
||||
clean_up_method = "clean_up_#{state}"
|
||||
self.send(clean_up_method) if self.class.private_method_defined?(clean_up_method)
|
||||
end
|
||||
|
||||
def init_state(state)
|
||||
init_method = "init_#{state}"
|
||||
self.send(init_method) if self.class.private_method_defined?(init_method)
|
||||
end
|
||||
end
|
||||
end
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,32 @@
|
||||
module DiscourseNarrativeBot
|
||||
class Dice
|
||||
MAXIMUM_NUM_OF_DICE = 20
|
||||
MAXIMUM_RANGE_OF_DICE = 120
|
||||
|
||||
def self.roll(num_of_dice, range_of_dice)
|
||||
if num_of_dice == 0 || range_of_dice == 0
|
||||
return I18n.t('discourse_narrative_bot.dice.invalid')
|
||||
end
|
||||
|
||||
output = ''
|
||||
|
||||
if num_of_dice > MAXIMUM_NUM_OF_DICE
|
||||
output << I18n.t('discourse_narrative_bot.dice.not_enough_dice',
|
||||
num_of_dice: MAXIMUM_NUM_OF_DICE
|
||||
)
|
||||
output << "\n\n"
|
||||
num_of_dice = MAXIMUM_NUM_OF_DICE
|
||||
end
|
||||
|
||||
if range_of_dice > MAXIMUM_RANGE_OF_DICE
|
||||
output << I18n.t('discourse_narrative_bot.dice.out_of_range')
|
||||
output << "\n\n"
|
||||
range_of_dice = MAXIMUM_RANGE_OF_DICE
|
||||
end
|
||||
|
||||
output << I18n.t('discourse_narrative_bot.dice.results',
|
||||
results: num_of_dice.times.map { rand(1..range_of_dice) }.join(", ")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,9 @@
|
||||
module DiscourseNarrativeBot
|
||||
class Magic8Ball
|
||||
def self.generate_answer
|
||||
I18n.t("discourse_narrative_bot.magic_8_ball.result", result: I18n.t(
|
||||
"discourse_narrative_bot.magic_8_ball.answers.#{rand(1..20)}"
|
||||
))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,519 @@
|
||||
require 'distributed_mutex'
|
||||
|
||||
module DiscourseNarrativeBot
|
||||
class NewUserNarrative < Base
|
||||
I18N_KEY = "discourse_narrative_bot.new_user_narrative".freeze
|
||||
BADGE_NAME = 'Certified'.freeze
|
||||
|
||||
TRANSITION_TABLE = {
|
||||
begin: {
|
||||
init: {
|
||||
next_state: :tutorial_bookmark,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.bookmark.instructions") },
|
||||
action: :say_hello
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_bookmark: {
|
||||
next_state: :tutorial_onebox,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.onebox.instructions") },
|
||||
|
||||
bookmark: {
|
||||
action: :reply_to_bookmark
|
||||
},
|
||||
|
||||
reply: {
|
||||
next_state: :tutorial_bookmark,
|
||||
action: :missing_bookmark
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_onebox: {
|
||||
next_state: :tutorial_emoji,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.emoji.instructions") },
|
||||
|
||||
reply: {
|
||||
action: :reply_to_onebox
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_emoji: {
|
||||
next_state: :tutorial_mention,
|
||||
next_instructions: Proc.new {
|
||||
I18n.t("#{I18N_KEY}.mention.instructions", discobot_username: self.class.discobot_user.username)
|
||||
},
|
||||
reply: {
|
||||
action: :reply_to_emoji
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_mention: {
|
||||
next_state: :tutorial_formatting,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.formatting.instructions") },
|
||||
|
||||
reply: {
|
||||
action: :reply_to_mention
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_formatting: {
|
||||
next_state: :tutorial_quote,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.quoting.instructions") },
|
||||
|
||||
reply: {
|
||||
action: :reply_to_formatting
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_quote: {
|
||||
next_state: :tutorial_images,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.images.instructions") },
|
||||
|
||||
reply: {
|
||||
action: :reply_to_quote
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_images: {
|
||||
next_state: :tutorial_flag,
|
||||
next_instructions: Proc.new {
|
||||
I18n.t("#{I18N_KEY}.flag.instructions",
|
||||
guidelines_url: url_helpers(:guidelines_url),
|
||||
about_url: url_helpers(:about_index_url))
|
||||
},
|
||||
reply: {
|
||||
action: :reply_to_image
|
||||
},
|
||||
like: {
|
||||
action: :track_like
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_flag: {
|
||||
next_state: :tutorial_search,
|
||||
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.search.instructions") },
|
||||
flag: {
|
||||
action: :reply_to_flag
|
||||
},
|
||||
reply: {
|
||||
next_state: :tutorial_flag,
|
||||
action: :missing_flag
|
||||
}
|
||||
},
|
||||
|
||||
tutorial_search: {
|
||||
next_state: :end,
|
||||
reply: {
|
||||
action: :reply_to_search
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SEARCH_ANSWER = ':herb:'.freeze
|
||||
|
||||
def self.reset_trigger
|
||||
I18n.t('discourse_narrative_bot.new_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 synchronize(user)
|
||||
if Rails.env.test?
|
||||
yield
|
||||
else
|
||||
DistributedMutex.synchronize("new_user_narrative_#{user.id}") { yield }
|
||||
end
|
||||
end
|
||||
|
||||
def init_tutorial_search
|
||||
topic = @post.topic
|
||||
post = topic.first_post
|
||||
|
||||
MessageBus.publish('/new_user_narrative/tutorial_search', {}, user_ids: [@user.id])
|
||||
|
||||
raw = <<~RAW
|
||||
#{post.raw}
|
||||
|
||||
#{I18n.t("#{I18N_KEY}.search.hidden_message")}
|
||||
RAW
|
||||
|
||||
PostRevisor.new(post, topic).revise!(
|
||||
self.class.discobot_user,
|
||||
{ raw: raw },
|
||||
{ skip_validations: true, force_new_version: true }
|
||||
)
|
||||
|
||||
set_state_data(:post_version, post.reload.version || 0)
|
||||
end
|
||||
|
||||
def clean_up_tutorial_search
|
||||
first_post = @post.topic.first_post
|
||||
first_post.revert_to(get_state_data(:post_version) - 1)
|
||||
first_post.save!
|
||||
first_post.publish_change_to_clients!(:revised)
|
||||
end
|
||||
|
||||
def say_hello
|
||||
raw = I18n.t(
|
||||
"#{I18N_KEY}.hello.message",
|
||||
username: @user.username,
|
||||
title: SiteSetting.title
|
||||
)
|
||||
|
||||
raw = <<~RAW
|
||||
#{raw}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
opts = {
|
||||
title: I18n.t("#{I18N_KEY}.hello.title", title: SiteSetting.title),
|
||||
target_usernames: @user.username,
|
||||
archetype: Archetype.private_message
|
||||
}
|
||||
|
||||
if @post &&
|
||||
@post.archetype == Archetype.private_message &&
|
||||
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
|
||||
|
||||
opts = opts.merge(topic_id: @post.topic_id)
|
||||
end
|
||||
|
||||
if @data[:topic_id]
|
||||
opts = opts.merge(topic_id: @data[:topic_id])
|
||||
end
|
||||
|
||||
post = reply_to(@post, raw, opts)
|
||||
@data[:topic_id] = post.topic.id
|
||||
@data[:track] = self.class.to_s
|
||||
post
|
||||
end
|
||||
|
||||
def missing_bookmark
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
return if @post.user_id == self.class.discobot_user.id
|
||||
|
||||
fake_delay
|
||||
enqueue_timeout_job(@user)
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.bookmark.not_found")) unless @data[:attempted]
|
||||
false
|
||||
end
|
||||
|
||||
def reply_to_bookmark
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
return unless @post.user_id == self.class.discobot_user.id
|
||||
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.bookmark.reply", profile_page_url: url_helpers(:user_url, username: @user.username))}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
end
|
||||
|
||||
def reply_to_onebox
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
@post.post_analyzer.cook(@post.raw, {})
|
||||
|
||||
if @post.post_analyzer.found_oneboxes?
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.onebox.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.onebox.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def track_like
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
post_liked = PostAction.find_by(
|
||||
post_action_type_id: PostActionType.types[:like],
|
||||
post_id: @data[:last_post_id],
|
||||
user_id: @user.id
|
||||
)
|
||||
|
||||
if post_liked
|
||||
set_state_data(:liked, true)
|
||||
|
||||
if (post_id = get_state_data(:post_id)) && (post = Post.find_by(id: post_id))
|
||||
fake_delay
|
||||
like_post(post)
|
||||
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.images.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
return reply
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def reply_to_image
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
@post.post_analyzer.cook(@post.raw, {})
|
||||
transition = true
|
||||
attempted_count = get_state_data(:attempted) || 0
|
||||
|
||||
if attempted_count < 2
|
||||
@data[:skip_attempted] = true
|
||||
@data[:attempted] = false
|
||||
else
|
||||
@data[:skip_attempted] = false
|
||||
end
|
||||
|
||||
if @post.post_analyzer.image_count > 0
|
||||
set_state_data(:post_id, @post.id)
|
||||
|
||||
if get_state_data(:liked)
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.images.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
like_post(@post)
|
||||
else
|
||||
raw = I18n.t(
|
||||
"#{I18N_KEY}.images.like_not_found",
|
||||
url: Post.find_by(id: @data[:last_post_id]).url
|
||||
)
|
||||
|
||||
transition = false
|
||||
end
|
||||
else
|
||||
raw = I18n.t(
|
||||
"#{I18N_KEY}.images.not_found",
|
||||
image_url: "#{Discourse.base_url}/images/dog-walk.gif"
|
||||
)
|
||||
|
||||
transition = false
|
||||
end
|
||||
|
||||
fake_delay
|
||||
|
||||
set_state_data(:attempted, attempted_count + 1) if !transition
|
||||
reply = reply_to(@post, raw) unless @data[:attempted] && !transition
|
||||
enqueue_timeout_job(@user)
|
||||
transition ? reply : false
|
||||
end
|
||||
|
||||
def reply_to_formatting
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
if Nokogiri::HTML.fragment(@post.cooked).css("b", "strong", "em", "i", ".bbcode-i", ".bbcode-b").size > 0
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.formatting.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.formatting.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_quote
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
doc = Nokogiri::HTML.fragment(@post.cooked)
|
||||
|
||||
if doc.css(".quote").size > 0
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.quoting.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.quoting.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_emoji
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
doc = Nokogiri::HTML.fragment(@post.cooked)
|
||||
|
||||
if doc.css(".emoji").size > 0
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.emoji.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.emoji.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_mention
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
if bot_mentioned?(@post)
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.mention.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
else
|
||||
fake_delay
|
||||
|
||||
unless @data[:attempted]
|
||||
reply_to(@post, I18n.t(
|
||||
"#{I18N_KEY}.mention.not_found",
|
||||
username: @user.username,
|
||||
discobot_username: self.class.discobot_user.username
|
||||
))
|
||||
end
|
||||
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def missing_flag
|
||||
return unless valid_topic?(@post.topic_id)
|
||||
return if @post.user_id == -2
|
||||
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.flag.not_found")) unless @data[:attempted]
|
||||
false
|
||||
end
|
||||
|
||||
def reply_to_flag
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
return unless @post.user.id == -2
|
||||
|
||||
raw = <<~RAW
|
||||
#{I18n.t("#{I18N_KEY}.flag.reply")}
|
||||
|
||||
#{instance_eval(&@next_instructions)}
|
||||
RAW
|
||||
|
||||
fake_delay
|
||||
|
||||
reply = reply_to(@post, raw)
|
||||
@post.post_actions.where(user_id: @user.id).destroy_all
|
||||
|
||||
enqueue_timeout_job(@user)
|
||||
reply
|
||||
end
|
||||
|
||||
def reply_to_search
|
||||
post_topic_id = @post.topic_id
|
||||
return unless valid_topic?(post_topic_id)
|
||||
|
||||
if @post.raw.match(/#{SEARCH_ANSWER}/)
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.search.reply", search_url: url_helpers(:search_url)))
|
||||
else
|
||||
fake_delay
|
||||
reply_to(@post, I18n.t("#{I18N_KEY}.search.not_found")) unless @data[:attempted]
|
||||
enqueue_timeout_job(@user)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def end_reply
|
||||
fake_delay
|
||||
|
||||
reply_to(
|
||||
@post,
|
||||
I18n.t("#{I18N_KEY}.end.message",
|
||||
username: @user.username,
|
||||
base_url: Discourse.base_url,
|
||||
certificate: certificate,
|
||||
discobot_username: self.class.discobot_user.username,
|
||||
advanced_trigger: AdvancedUserNarrative.reset_trigger
|
||||
),
|
||||
topic_id: @data[:topic_id]
|
||||
)
|
||||
end
|
||||
|
||||
def like_post(post)
|
||||
PostAction.act(self.class.discobot_user, post, PostActionType.types[:like])
|
||||
end
|
||||
|
||||
def welcome_topic
|
||||
Topic.find_by(slug: 'welcome-to-discourse', archetype: Archetype.default) ||
|
||||
Topic.recent(1).first
|
||||
end
|
||||
|
||||
def url_helpers(url, opts = {})
|
||||
Rails.application.routes.url_helpers.send(url, opts.merge(host: Discourse.base_url))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,27 @@
|
||||
require 'excon'
|
||||
|
||||
module DiscourseNarrativeBot
|
||||
class QuoteGenerator
|
||||
API_ENDPOINT = 'http://api.forismatic.com/api/1.0/'.freeze
|
||||
|
||||
def self.generate(user)
|
||||
quote, author =
|
||||
if user.effective_locale != 'en'
|
||||
translation_key = "discourse_narrative_bot.quote.#{rand(1..10)}"
|
||||
|
||||
[
|
||||
I18n.t("#{translation_key}.quote"),
|
||||
I18n.t("#{translation_key}.author")
|
||||
]
|
||||
else
|
||||
connection = Excon.new("#{API_ENDPOINT}?lang=en&format=json&method=getQuote")
|
||||
response = connection.request(expects: [200, 201], method: :Get)
|
||||
|
||||
response_body = JSON.parse(response.body)
|
||||
[response_body["quoteText"].strip, response_body["quoteAuthor"].strip]
|
||||
end
|
||||
|
||||
I18n.t('discourse_narrative_bot.quote.results', quote: quote, author: author)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,266 @@
|
||||
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 = [
|
||||
NewUserNarrative,
|
||||
AdvancedUserNarrative
|
||||
]
|
||||
|
||||
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 && !is_topic_action?
|
||||
is_reply = @input == :reply
|
||||
return if is_reply && reset_track
|
||||
|
||||
topic_id = @post.topic_id
|
||||
|
||||
if (data && data[:topic_id] == topic_id)
|
||||
state = data[:state]
|
||||
klass = (data[:track] || NewUserNarrative.to_s).constantize
|
||||
|
||||
if is_reply && like_user_post
|
||||
Store.set(@user.id, data.merge!(state: nil, topic_id: nil))
|
||||
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
|
||||
$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 && (pm_to_bot?(@post) || public_reply?)
|
||||
like_user_post
|
||||
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)
|
||||
return if klass.respond_to?(:can_start?) && !klass.can_start?(@user)
|
||||
post_raw = @post.raw
|
||||
trigger = "#{self.class.reset_trigger} #{klass.reset_trigger}"
|
||||
|
||||
if post_raw.length < RESET_TRIGGER_EXACT_MATCH_LENGTH && pm_to_bot?(@post)
|
||||
post_raw.match(Regexp.new("\\b\\W\?#{trigger}\\W\?\\b", 'i'))
|
||||
else
|
||||
match_trigger?(trigger)
|
||||
end
|
||||
end
|
||||
|
||||
def bot_commands(hint = true)
|
||||
raw =
|
||||
if 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.class.discobot_user.username,
|
||||
help_trigger: self.class.help_trigger
|
||||
)
|
||||
|
||||
if public_reply?
|
||||
key = "#{PUBLIC_DISPLAY_BOT_HELP_KEY}:#{@post.topic_id}"
|
||||
last_bot_help_post_number = $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 - $redis.ttl(key)) > 6.hours.to_i)
|
||||
|
||||
$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
|
||||
tracks = [NewUserNarrative.reset_trigger]
|
||||
|
||||
if @user.staff? ||
|
||||
@user.badges.where(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME).exists?
|
||||
|
||||
tracks << AdvancedUserNarrative.reset_trigger
|
||||
end
|
||||
|
||||
discobot_username = self.class.discobot_user.username
|
||||
|
||||
message = I18n.t(
|
||||
self.class.i18n_key('random_mention.tracks'),
|
||||
discobot_username: discobot_username,
|
||||
reset_trigger: self.class.reset_trigger,
|
||||
default_track: NewUserNarrative.reset_trigger,
|
||||
tracks: tracks.join(', ')
|
||||
)
|
||||
|
||||
message << "\n\n#{I18n.t(self.class.i18n_key('random_mention.bot_actions'),
|
||||
discobot_username: discobot_username,
|
||||
dice_trigger: self.class.dice_trigger,
|
||||
quote_trigger: self.class.quote_trigger,
|
||||
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 = ($redis.get(key) || $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'),
|
||||
reset_trigger: self.class.reset_trigger
|
||||
))
|
||||
else
|
||||
# Stay out of the user's way
|
||||
end
|
||||
|
||||
$redis.incr(key)
|
||||
end
|
||||
|
||||
def self.i18n_key(key)
|
||||
"discourse_narrative_bot.track_selector.#{key}"
|
||||
end
|
||||
|
||||
def skip_track?
|
||||
if pm_to_bot?(@post)
|
||||
post_raw = @post.raw
|
||||
|
||||
post_raw.match(/^@#{self.class.discobot_user.username} #{self.class.skip_trigger}/i) ||
|
||||
post_raw.strip == self.class.skip_trigger
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def match_trigger?(trigger)
|
||||
discobot_username = self.class.discobot_user.username
|
||||
regexp = Regexp.new("<a class=\"mention\".*>@#{discobot_username}</a> #{trigger}", 'i')
|
||||
match = @post.cooked.match(regexp)
|
||||
|
||||
if pm_to_bot?(@post)
|
||||
match || @post.raw.strip.match(Regexp.new("^#{trigger}$", 'i'))
|
||||
else
|
||||
match
|
||||
end
|
||||
end
|
||||
|
||||
def like_user_post
|
||||
if @post.raw.match(/thank/i)
|
||||
PostAction.act(self.class.discobot_user, @post, PostActionType.types[:like])
|
||||
end
|
||||
end
|
||||
|
||||
def bot_mentioned?
|
||||
@bot_mentioned ||= PostAnalyzer.new(@post.raw, @post.topic_id).raw_mentions.include?(
|
||||
self.class.discobot_user.username
|
||||
)
|
||||
end
|
||||
|
||||
def public_reply?
|
||||
!SiteSetting.discourse_narrative_bot_disable_public_replies &&
|
||||
(bot_mentioned? || reply_to_bot_post?(@post))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,18 @@
|
||||
module DiscourseNarrativeBot
|
||||
class WelcomePostTypeSiteSetting
|
||||
def self.valid_value?(val)
|
||||
values.any? { |v| v[:value] == val.to_s }
|
||||
end
|
||||
|
||||
def self.values
|
||||
@values ||= [
|
||||
{ name: 'discourse_narrative_bot.welcome_post_type.new_user_track', value: 'new_user_track' },
|
||||
{ name: 'discourse_narrative_bot.welcome_post_type.welcome_message', value: 'welcome_message' }
|
||||
]
|
||||
end
|
||||
|
||||
def self.translate_names?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user