diff --git a/app/controllers/discourse_chatbot/chatbot_controller.rb b/app/controllers/discourse_chatbot/chatbot_controller.rb index e3af491..6789a1e 100644 --- a/app/controllers/discourse_chatbot/chatbot_controller.rb +++ b/app/controllers/discourse_chatbot/chatbot_controller.rb @@ -15,6 +15,9 @@ def start_bot_convo bot_user = ::User.find_by(username: bot_username) channel_type = SiteSetting.chatbot_quick_access_talk_button + evaluation = ::DiscourseChatbot::EventEvaluation.new + over_quota = evaluation.over_quota(current_user.id) + if channel_type == "chat" bot_author = ::User.find_by(username: SiteSetting.chatbot_bot_user) @@ -37,11 +40,11 @@ def start_bot_convo last_chat = ::Chat::Message.find_by(id: chat_channel.latest_not_deleted_message_id) - if (last_chat && last_chat.message != I18n.t("chatbot.quick_access_kick_off.announcement")) || last_chat.nil? + if (last_chat && (over_quota && last_chat.message != I18n.t('chatbot.errors.overquota') || !over_quota && last_chat.message != I18n.t("chatbot.quick_access_kick_off.announcement"))) || last_chat.nil? Chat::CreateMessage.call( chat_channel_id: chat_channel_id, guardian: guardian, - message: I18n.t("chatbot.quick_access_kick_off.announcement"), + message: over_quota ? I18n.t('chatbot.errors.overquota') : I18n.t("chatbot.quick_access_kick_off.announcement"), ) end @@ -50,7 +53,7 @@ def start_bot_convo elsif channel_type == "personal message" default_opts = { post_alert_options: { skip_send_email: true }, - raw: I18n.t("chatbot.quick_access_kick_off.announcement"), + raw: over_quota ? I18n.t('chatbot.errors.overquota') : I18n.t("chatbot.quick_access_kick_off.announcement"), skip_validations: true, title: I18n.t("chatbot.pm_prefix"), archetype: Archetype.private_message, diff --git a/app/jobs/scheduled/chatbot_quota_reset.rb b/app/jobs/scheduled/chatbot_quota_reset.rb index 6c12872..765db90 100644 --- a/app/jobs/scheduled/chatbot_quota_reset.rb +++ b/app/jobs/scheduled/chatbot_quota_reset.rb @@ -14,6 +14,10 @@ def execute(args) current_queries = "0" UserCustomField.create!(user_id: u.id, name: ::DiscourseChatbot::CHATBOT_QUERIES_CUSTOM_FIELD, value: current_queries) end + + if current_record = UserCustomField.find_by(user_id: u.id, name: ::DiscourseChatbot::CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD) + current_record.delete + end end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bc54070..04f9d7a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -40,6 +40,8 @@ en: chatbot_quota_high_trust: "The allowed number of bot responses allowed by prompting user per week - high trust groups" chatbot_quota_medium_trust: "The allowed number of bot responses allowed by prompting user per week - medium trust groups" chatbot_quota_low_trust: "The allowed number of bot responses allowed by prompting user per week - low trust groups" + chatbot_quota_reach_escalation_groups: "The group(s) to which the bot will escalate when the quota is reached" + chatbot_quota_reach_escalation_cool_down_period: "The number of days the bot will wait before escalating again after reaching the quota" chatbot_high_trust_groups: "Groups considered high trust for bot interaction" chatbot_medium_trust_groups: "Groups considered medium trust for bot interaction" chatbot_low_trust_groups: "Groups considered low trust for bot interaction" @@ -73,6 +75,10 @@ en: pm_prefix: "A Discussion with Chatbot" quick_access_kick_off: announcement: "Hello, how can I help you?" + quota_reached: + escalation: + title: "Chatbot Quota Reached: %{username}" + message: "I'm afraid user '%{username}' has reached their weekly quota for Chatbot interactions. I'm escalating this issue to staff for further assistance." prompt: system: basic: @@ -134,6 +140,7 @@ en: title: "Support bot chat escalation" announcement: "This is a follow-up Personal Message which includes Staff. The following chat conversation was had between the user and the bot:\n\n%{content}" answer_summary: "The function ran and this chat has been successfully escalated and raised to the attention of staff. A Personal Message to discuss things further with Staff has been created at the following url: %{url}. I must now share this news & the url with the user. There is no need for me to do anything more than communicate this fact with the web link and wish the user a good day." + no_escalation_groups: "No escalation groups have been set up for this function. Please contact your administrator." wrong_type_error: "This function cannot be used outside of Chat Messages" error: "Escalation of user and bot chat failed" forum_get_user_address: diff --git a/config/settings.yml b/config/settings.yml index 8632e9b..1e07dca 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -179,6 +179,18 @@ plugins: chatbot_quota_low_trust: default: 10 client: false + chatbot_quota_reach_escalation_groups: + client: false + type: group_list + list_type: compact + default: "" + allow_any: false + refresh: true + chatbot_quota_reach_escalation_cool_down_period: + client: false + default: 1 + min: 0 + max: 7 chatbot_max_look_behind: default: 5 max: 160 diff --git a/lib/discourse_chatbot/event_evaluation.rb b/lib/discourse_chatbot/event_evaluation.rb index 852b508..da26d5b 100644 --- a/lib/discourse_chatbot/event_evaluation.rb +++ b/lib/discourse_chatbot/event_evaluation.rb @@ -54,7 +54,51 @@ def over_quota(user_id) UserCustomField.create!(user_id: user_id, name: CHATBOT_QUERIES_CUSTOM_FIELD, value: current_queries) end - current_queries > max_quota + breach = current_queries > max_quota + escalate_as_required(user_id) if breach + breach + end + + def escalate_as_required(user_id) + escalation_date = UserCustomField.find_by(name: ::DiscourseChatbot::CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD, user_id: user_id) + + if SiteSetting.chatbot_quota_reach_escalation_cool_down_period > 0 + if escalation_date.nil? || !SiteSetting.chatbot_quota_reach_escalation_cool_down_period.nil? && + escalation_date.value < SiteSetting.chatbot_quota_reach_escalation_cool_down_period.days.ago + escalate_quota_breach(user_id) + end + end + end + + def escalate_quota_breach(user_id) + user = User.find_by(id: user_id) + system_user = User.find_by(username_lower: "system") + + target_group_names = [] + + Array(SiteSetting.chatbot_quota_reach_escalation_groups).each do |g| + unless g.to_i == 0 + target_group_names << Group.find(g.to_i).name + end + end + + if !target_group_names.empty? + target_group_names = target_group_names.join(",") + + default_opts = { + post_alert_options: { skip_send_email: true }, + raw: I18n.t("chatbot.quota_reached.escalation.message", username: user.username), + skip_validations: true, + title: I18n.t("chatbot.quota_reached.escalation.title", username: user.username), + archetype: Archetype.private_message, + target_group_names: target_group_names + } + + post = PostCreator.create!(system_user, default_opts) + + user.custom_fields[::DiscourseChatbot::CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD] = DateTime.now + user.save_custom_fields + end end private diff --git a/lib/discourse_chatbot/functions/escalate_to_staff_function.rb b/lib/discourse_chatbot/functions/escalate_to_staff_function.rb index e1aa58d..4357ebc 100644 --- a/lib/discourse_chatbot/functions/escalate_to_staff_function.rb +++ b/lib/discourse_chatbot/functions/escalate_to_staff_function.rb @@ -37,32 +37,38 @@ def process(args, opts) target_group_names = [] Array(SiteSetting.chatbot_escalate_to_staff_groups).each do |g| - target_group_names << Group.find(g.to_i).name + unless g.to_i == 0 + target_group_names << Group.find(g.to_i).name + end end - target_group_names = target_group_names.join(",") + if !target_group_names.empty? + target_group_names = target_group_names.join(",") - message_or_post_id = opts[:reply_to_message_or_post_id] + message_or_post_id = opts[:reply_to_message_or_post_id] - message_collection = get_messages(message_or_post_id) - - content = generate_transcript(message_collection, bot_user) + message_collection = get_messages(message_or_post_id) - default_opts = { - post_alert_options: { skip_send_email: true }, - raw: I18n.t("chatbot.prompt.function.escalate_to_staff.announcement", content: content), - skip_validations: true, - title: I18n.t("chatbot.prompt.function.escalate_to_staff.title"), - archetype: Archetype.private_message, - target_usernames: target_usernames, - target_group_names: target_group_names - } + content = generate_transcript(message_collection, bot_user) - post = PostCreator.create!(current_user, default_opts) + default_opts = { + post_alert_options: { skip_send_email: true }, + raw: I18n.t("chatbot.prompt.function.escalate_to_staff.announcement", content: content), + skip_validations: true, + title: I18n.t("chatbot.prompt.function.escalate_to_staff.title"), + archetype: Archetype.private_message, + target_usernames: target_usernames, + target_group_names: target_group_names + } - url = "https://#{Discourse.current_hostname}/t/slug/#{post.topic_id}" + post = PostCreator.create!(current_user, default_opts) - response = I18n.t("chatbot.prompt.function.escalate_to_staff.answer_summary", url: url) + url = "https://#{Discourse.current_hostname}/t/slug/#{post.topic_id}" + + response = I18n.t("chatbot.prompt.function.escalate_to_staff.answer_summary", url: url) + else + response = I18n.t("chatbot.prompt.function.escalate_to_staff.no_escalation_groups") + end rescue I18n.t("chatbot.prompt.function.escalate_to_staff.error", parameter: args[parameters[0][:name]]) end diff --git a/lib/discourse_chatbot/post/post_evaluation.rb b/lib/discourse_chatbot/post/post_evaluation.rb index 3838620..618633f 100644 --- a/lib/discourse_chatbot/post/post_evaluation.rb +++ b/lib/discourse_chatbot/post/post_evaluation.rb @@ -57,7 +57,7 @@ def trigger_response(submission) ::DiscourseChatbot.progress_debug_message("humans found in this convo: #{human_participants_count}") - if bot_user && (user != bot_user) && (mentions_bot_name || explicit_reply_to_bot || (last_post_was_bot && human_participants_count == 1)) + if bot_user && (user.id > 0) && (mentions_bot_name || explicit_reply_to_bot || (last_post_was_bot && human_participants_count == 1)) opts = { type: POST, private: topic.archetype == Archetype.private_message, diff --git a/plugin.rb b/plugin.rb index a5a4cd6..60b2a92 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # name: discourse-chatbot # about: a plugin that allows you to have a conversation with a configurable chatbot in Discourse Chat, Topics and Private Messages -# version: 0.9.7 +# version: 0.9.8 # authors: merefield # url: https://github.com/merefield/discourse-chatbot @@ -22,6 +22,7 @@ module ::DiscourseChatbot POST = "post" MESSAGE = "message" CHATBOT_QUERIES_CUSTOM_FIELD = "chatbot_queries" + CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD = "chatbot_queries_quota_reach_escalation_date" POST_TYPES_REGULAR_ONLY = [1] POST_TYPES_INC_WHISPERS = [1, 4] @@ -111,6 +112,8 @@ def progress_debug_message(message) register_user_custom_field_type(::DiscourseChatbot::CHATBOT_QUERIES_CUSTOM_FIELD, :integer) + register_user_custom_field_type(::DiscourseChatbot::CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD, :date) + add_to_serializer(:current_user, :chatbot_access) do !::DiscourseChatbot::EventEvaluation.new.trust_level(object.id).blank? diff --git a/spec/lib/event_evaluation_spec.rb b/spec/lib/event_evaluation_spec.rb index 730c3d6..abfbb06 100644 --- a/spec/lib/event_evaluation_spec.rb +++ b/spec/lib/event_evaluation_spec.rb @@ -2,6 +2,7 @@ require_relative '../plugin_helper' CHATBOT_QUERIES_CUSTOM_FIELD = "chatbot_queries" +CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD = "chatbot_queries_quota_reach_escalation_date" describe ::DiscourseChatbot::EventEvaluation do let(:normal_user) { Fabricate(:user, trust_level: TrustLevel[1], refresh_auto_groups: true) } @@ -12,6 +13,7 @@ before(:each) do SiteSetting.chatbot_enabled = true + SiteSetting.chatbot_quota_reach_escalation_groups = "3" end it "returns the correct trust level for user in high trust group" do @@ -54,7 +56,7 @@ expect(event.over_quota(high_trust_user.id)).to equal(false) end - it "returns the correct quota decision if user is in high trust group and user is outside of quota" do + it "returns the correct quota decision if user is in high trust group and user is outside of quota and escalates" do UserCustomField.create!(user_id: high_trust_user.id, name: CHATBOT_QUERIES_CUSTOM_FIELD, value: 3) SiteSetting.chatbot_high_trust_groups = "13|14" SiteSetting.chatbot_medium_trust_groups = "11|12" @@ -64,6 +66,22 @@ SiteSetting.chatbot_quota_low_trust = 1 event = ::DiscourseChatbot::EventEvaluation.new + expect { event.over_quota(high_trust_user.id) }.to change { Topic.where(archetype: Archetype.private_message).count }.by(1) + expect(event.over_quota(high_trust_user.id)).to equal(true) + end + + it "returns the correct quota decision if user is in high trust group and user is outside of quota but doesn't escalate" do + UserCustomField.create!(user_id: high_trust_user.id, name: CHATBOT_QUERIES_CUSTOM_FIELD, value: 3) + UserCustomField.create!(user_id: high_trust_user.id, name: CHATBOT_QUERIES_QUOTA_REACH_ESCALATION_DATE_CUSTOM_FIELD, value: 30.minutes.ago) + SiteSetting.chatbot_high_trust_groups = "13|14" + SiteSetting.chatbot_medium_trust_groups = "11|12" + SiteSetting.chatbot_low_trust_groups = "10" + SiteSetting.chatbot_quota_high_trust = 3 + SiteSetting.chatbot_quota_medium_trust = 2 + SiteSetting.chatbot_quota_low_trust = 1 + + event = ::DiscourseChatbot::EventEvaluation.new + expect { event.over_quota(high_trust_user.id) }.not_to change { Topic.where(archetype: Archetype.private_message).count } expect(event.over_quota(high_trust_user.id)).to equal(true) end