import ClipboardJS from "clipboard"; import $ from "jquery"; import render_settings_deactivation_bot_modal from "../templates/confirm_dialog/confirm_deactivate_bot.hbs"; import render_add_new_bot_form from "../templates/settings/add_new_bot_form.hbs"; import render_bot_avatar_row from "../templates/settings/bot_avatar_row.hbs"; import render_bot_settings_tip from "../templates/settings/bot_settings_tip.hbs"; import render_edit_bot_form from "../templates/settings/edit_bot_form.hbs"; import render_settings_edit_embedded_bot_service from "../templates/settings/edit_embedded_bot_service.hbs"; import render_settings_edit_outgoing_webhook_service from "../templates/settings/edit_outgoing_webhook_service.hbs"; import * as avatar from "./avatar"; import * as blueslip from "./blueslip"; import * as bot_data from "./bot_data"; import * as channel from "./channel"; import {csrf_token} from "./csrf"; import * as dialog_widget from "./dialog_widget"; import {DropdownListWidget} from "./dropdown_list_widget"; import {$t, $t_html} from "./i18n"; import {page_params} from "./page_params"; import * as people from "./people"; import * as settings_config from "./settings_config"; import * as settings_users from "./settings_users"; import * as ui_report from "./ui_report"; import * as user_profile from "./user_profile"; const OUTGOING_WEBHOOK_BOT_TYPE = "3"; const EMBEDDED_BOT_TYPE = "4"; const focus_tab = { active_bots_tab() { $("#bots_lists_navbar .active").removeClass("active"); $("#bots_lists_navbar .active-bots-tab").addClass("active"); $("#active_bots_list").show(); $("#inactive_bots_list").hide(); }, inactive_bots_tab() { $("#bots_lists_navbar .active").removeClass("active"); $("#bots_lists_navbar .inactive-bots-tab").addClass("active"); $("#active_bots_list").hide(); $("#inactive_bots_list").show(); }, }; function add_bot_row(info) { const $row = $(render_bot_avatar_row(info)); if (info.is_active) { $("#active_bots_list").append($row); } else { $("#inactive_bots_list").append($row); } } function is_local_part(value) { // Adapted from Django's EmailValidator return /^[\w!#$%&'*+/=?^`{|}~-]+(\.[\w!#$%&'*+/=?^`{|}~-]+)*$/i.test(value); } export function type_id_to_string(type_id) { return page_params.bot_types.find((bot_type) => bot_type.type_id === type_id).name; } export function render_bots() { $("#active_bots_list").empty(); $("#inactive_bots_list").empty(); const all_bots_for_current_user = bot_data.get_all_bots_for_current_user(); let user_owns_an_active_bot = false; for (const elem of all_bots_for_current_user) { add_bot_row({ name: elem.full_name, email: elem.email, user_id: elem.user_id, type: type_id_to_string(elem.bot_type), avatar_url: elem.avatar_url, api_key: elem.api_key, is_active: elem.is_active, zuliprc: "zuliprc", // Most browsers do not allow filename starting with `.` }); user_owns_an_active_bot = user_owns_an_active_bot || elem.is_active; } } export function generate_zuliprc_uri(bot_id) { const bot = bot_data.get(bot_id); const data = generate_zuliprc_content(bot); return encode_zuliprc_as_uri(data); } export function encode_zuliprc_as_uri(zuliprc) { return "data:application/octet-stream;charset=utf-8," + encodeURIComponent(zuliprc); } export function generate_zuliprc_content(bot) { let token; // For outgoing webhooks, include the token in the zuliprc. // It's needed for authenticating to the Botserver. if (bot.bot_type === 3) { token = bot_data.get_services(bot.user_id)[0].token; } return ( "[api]" + "\nemail=" + bot.email + "\nkey=" + bot.api_key + "\nsite=" + page_params.realm_uri + (token === undefined ? "" : "\ntoken=" + token) + // Some tools would not work in files without a trailing new line. "\n" ); } export function generate_botserverrc_content(email, api_key, token) { return ( "[]" + "\nemail=" + email + "\nkey=" + api_key + "\nsite=" + page_params.realm_uri + "\ntoken=" + token + "\n" ); } export const bot_creation_policy_values = { admins_only: { code: 3, description: $t({defaultMessage: "Admins"}), }, everyone: { code: 1, description: $t({defaultMessage: "Admins, moderators and members"}), }, restricted: { code: 2, description: $t({ defaultMessage: "Admins, moderators and members, but only admins can add generic bots", }), }, }; export function can_create_new_bots() { if (page_params.is_admin) { return true; } if (page_params.is_guest) { return false; } return page_params.realm_bot_creation_policy !== bot_creation_policy_values.admins_only.code; } export function update_bot_settings_tip($tip_container, for_org_settings) { if ( !page_params.is_admin && page_params.realm_bot_creation_policy === bot_creation_policy_values.everyone.code ) { $tip_container.hide(); return; } if (page_params.is_admin && !for_org_settings) { $tip_container.hide(); return; } const rendered_tip = render_bot_settings_tip({ realm_bot_creation_policy: page_params.realm_bot_creation_policy, permission_type: bot_creation_policy_values, }); $tip_container.show(); $tip_container.html(rendered_tip); } function update_add_bot_button() { if (can_create_new_bots()) { $("#bot-settings .add-a-new-bot").show(); $("#admin-bot-list .add-a-new-bot").show(); } else { $("#bot-settings .add-a-new-bot").hide(); $("#admin-bot-list .add-a-new-bot").hide(); } } export function update_bot_permissions_ui() { update_bot_settings_tip($("#admin-bot-settings-tip"), true); update_bot_settings_tip($("#personal-bot-settings-tip"), false); update_add_bot_button(); $("#id_realm_bot_creation_policy").val(page_params.realm_bot_creation_policy); } export function add_a_new_bot() { const html_body = render_add_new_bot_form({ bot_types: page_params.bot_types, realm_embedded_bots: page_params.realm_embedded_bots, realm_bot_domain: page_params.realm_bot_domain, }); let create_avatar_widget; function create_a_new_bot() { const bot_type = $("#create_bot_type :selected").val(); const full_name = $("#create_bot_name").val(); const short_name = $("#create_bot_short_name").val() || $("#create_bot_short_name").text(); const payload_url = $("#create_payload_url").val(); const interface_type = $("#create_interface_type").val(); const service_name = $("#select_service_name :selected").val(); const formData = new FormData(); formData.append("csrfmiddlewaretoken", csrf_token); formData.append("bot_type", bot_type); formData.append("full_name", full_name); formData.append("short_name", short_name); // If the selected bot_type is Outgoing webhook if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { formData.append("payload_url", JSON.stringify(payload_url)); formData.append("interface_type", interface_type); } else if (bot_type === EMBEDDED_BOT_TYPE) { formData.append("service_name", service_name); const config_data = {}; $(`#config_inputbox [name*='${CSS.escape(service_name)}'] input`).each(function () { config_data[$(this).attr("name")] = $(this).val(); }); formData.append("config_data", JSON.stringify(config_data)); } for (const [i, file] of Array.prototype.entries.call( $("#bot_avatar_file_input")[0].files, )) { formData.append("file-" + i, file); } channel.post({ url: "/json/bots", data: formData, cache: false, processData: false, contentType: false, success() { create_avatar_widget.clear(); dialog_widget.close_modal(); }, error(xhr) { ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error")); dialog_widget.hide_dialog_spinner(); }, }); } function set_up_form_fields() { $("#payload_url_inputbox").hide(); $("#create_payload_url").val(""); $("#service_name_list").hide(); $("#config_inputbox").hide(); const selected_embedded_bot = "converter"; $("#select_service_name").val(selected_embedded_bot); // TODO: Use 'select a bot'. $("#config_inputbox").children().hide(); $(`[name*='${CSS.escape(selected_embedded_bot)}']`).show(); create_avatar_widget = avatar.build_bot_create_widget(); $("#create_bot_type").on("change", () => { const bot_type = $("#create_bot_type :selected").val(); // For "generic bot" or "incoming webhook" both these fields need not be displayed. $("#service_name_list").hide(); $("#select_service_name").removeClass("required"); $("#config_inputbox").hide(); $("#payload_url_inputbox").hide(); $("#create_payload_url").removeClass("required"); if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { $("#payload_url_inputbox").show(); $("#create_payload_url").addClass("required"); } else if (bot_type === EMBEDDED_BOT_TYPE) { $("#service_name_list").show(); $("#select_service_name").addClass("required"); $("#select_service_name").trigger("change"); $("#config_inputbox").show(); } }); $("#select_service_name").on("change", () => { $("#config_inputbox").children().hide(); const selected_bot = $("#select_service_name :selected").val(); $(`[name*='${CSS.escape(selected_bot)}']`).show(); }); } function validate_input(e) { e.preventDefault(); e.stopPropagation(); const bot_short_name = $("#create_bot_short_name").val(); if (is_local_part(bot_short_name)) { return true; } ui_report.error( $t_html({ defaultMessage: "Please only use characters that are valid in an email address", }), undefined, $("#dialog_error"), ); return false; } dialog_widget.launch({ form_id: "create_bot_form", help_link: "/help/add-a-bot-or-integration", html_body, html_heading: $t_html({defaultMessage: "Add a new bot"}), html_submit_button: $t_html({defaultMessage: "Add"}), loading_spinner: true, on_click: create_a_new_bot, on_shown: () => $("#create_bot_type").trigger("focus"), post_render: set_up_form_fields, validate_input, }); } export function confirm_bot_deactivation(bot_id, handle_confirm, loading_spinner) { const bot = people.get_by_user_id(bot_id); const html_body = render_settings_deactivation_bot_modal(); dialog_widget.launch({ html_heading: $t_html({defaultMessage: "Deactivate {name}?"}, {name: bot.full_name}), help_link: "/help/deactivate-or-reactivate-a-bot", html_body, html_submit_button: $t_html({defaultMessage: "Deactivate"}), on_click: handle_confirm, loading_spinner, }); } export function show_edit_bot_info_modal(user_id, from_user_info_popover) { const bot = people.get_by_user_id(user_id); if (!bot) { return; } const html_body = render_edit_bot_form({ user_id, email: bot.email, full_name: bot.full_name, user_role_values: settings_config.user_role_values, disable_role_dropdown: !page_params.is_admin || (bot.is_owner && !page_params.is_owner), bot_avatar_url: bot.avatar_url, }); let owner_widget; let avatar_widget; const bot_type = bot.bot_type.toString(); const service = bot_data.get_services(bot.user_id)[0]; function submit_bot_details() { const role = Number.parseInt($("#bot-role-select").val().trim(), 10); const $full_name = $("#dialog_widget_modal").find("input[name='full_name']"); const url = "/json/bots/" + encodeURIComponent(bot.user_id); const formData = new FormData(); formData.append("csrfmiddlewaretoken", csrf_token); formData.append("full_name", $full_name.val()); formData.append("role", JSON.stringify(role)); if (owner_widget === undefined) { blueslip.error("get_bot_owner_widget not called"); } const human_user_id = owner_widget.value(); if (human_user_id) { formData.append("bot_owner_id", human_user_id); } if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { const service_payload_url = $("#edit_service_base_url").val(); const service_interface = $("#edit_service_interface :selected").val(); formData.append("service_payload_url", JSON.stringify(service_payload_url)); formData.append("service_interface", service_interface); } else if (bot_type === EMBEDDED_BOT_TYPE && service !== undefined) { const config_data = {}; $("#config_edit_inputbox input").each(function () { config_data[$(this).attr("name")] = $(this).val(); }); formData.append("config_data", JSON.stringify(config_data)); } const $file_input = $("#bot-edit-form").find(".edit_bot_avatar_file_input"); for (const [i, file] of Array.prototype.entries.call($file_input[0].files)) { formData.append("file-" + i, file); } channel.patch({ url, data: formData, processData: false, contentType: false, success() { avatar_widget.clear(); dialog_widget.close_modal(); }, error(xhr) { ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error")); dialog_widget.hide_dialog_spinner(); }, }); } function edit_bot_post_render() { $("#edit_bot_modal .dialog_submit_button").prop("disabled", true); const owner_id = bot_data.get(user_id).owner_id; const user_ids = people.get_active_human_ids(); const users_list = user_ids.map((user_id) => ({ name: people.get_full_name(user_id), value: user_id.toString(), })); const opts = { widget_name: "edit_bot_owner", data: users_list, default_text: $t({defaultMessage: "No owner"}), value: owner_id, on_update(value) { $("#edit_bot_modal .dialog_submit_button").prop("disabled", value === null); }, }; // Note: Rendering this is quite expensive in // organizations with 10Ks of users. owner_widget = new DropdownListWidget(opts); owner_widget.setup(); $("#bot-role-select").val(bot.role); if (!page_params.is_owner) { $("#bot-role-select") .find(`option[value="${CSS.escape(settings_config.user_role_values.owner.code)}"]`) .hide(); } avatar_widget = avatar.build_bot_edit_widget($("#bot-edit-form")); if (bot_type === OUTGOING_WEBHOOK_BOT_TYPE) { $("#service_data").append( render_settings_edit_outgoing_webhook_service({ service, }), ); $("#edit_service_interface").val(service.interface); } if (bot_type === EMBEDDED_BOT_TYPE) { $("#service_data").append( render_settings_edit_embedded_bot_service({ service, }), ); } // Hide the avatar if the user has uploaded an image $("#bot-edit-form").on("input", ".edit_bot_avatar_file_input", () => { $("#current_bot_avatar_image").hide(); }); // Show the avatar if the user has cleared the image $("#bot-edit-form").on("click", ".edit_bot_avatar_clear_button", () => { $("#current_bot_avatar_image").show(); }); $("#bot-edit-form").on("click", ".deactivate_bot_button", (e) => { e.preventDefault(); e.stopPropagation(); const bot_id = $("#bot-edit-form").data("user-id"); function handle_confirm() { const url = "/json/bots/" + encodeURIComponent(bot_id); dialog_widget.submit_api_request(channel.del, url); } const open_deactivate_modal_callback = () => confirm_bot_deactivation(bot_id, handle_confirm, true); dialog_widget.close_modal(open_deactivate_modal_callback); }); $("#bot-edit-form").on("input", "input, select", (e) => { if ($(e.target).hasClass("no-input-change-detection")) { // Don't enable the save button if the target element is a // dropdown_list_widget, since it is handled by the dropdown_list_widget's // `on_update` function. return; } $("#edit_bot_modal .dialog_submit_button").prop("disabled", false); }); } dialog_widget.launch({ html_heading: $t_html({defaultMessage: "Manage bot"}), html_body, id: "edit_bot_modal", on_click: submit_bot_details, post_render: edit_bot_post_render, loading_spinner: from_user_info_popover, }); } export function set_up() { $("#download_botserverrc").on("click", function () { const OUTGOING_WEBHOOK_BOT_TYPE_INT = 3; let content = ""; for (const bot of bot_data.get_all_bots_for_current_user()) { if (bot.is_active && bot.bot_type === OUTGOING_WEBHOOK_BOT_TYPE_INT) { const bot_token = bot_data.get_services(bot.user_id)[0].token; content += generate_botserverrc_content(bot.email, bot.api_key, bot_token); } } $(this).attr( "href", "data:application/octet-stream;charset=utf-8," + encodeURIComponent(content), ); }); // This needs to come before render_bots() in case the user // has no active bots focus_tab.active_bots_tab(); render_bots(); $("#active_bots_list").on("click", "button.deactivate_bot", (e) => { const bot_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); function handle_confirm() { const url = "/json/bots/" + encodeURIComponent(bot_id); const opts = { success_continuation() { const $row = $(e.currentTarget).closest("li"); $row.hide("slow", () => { $row.remove(); }); }, }; dialog_widget.submit_api_request(channel.del, url, {}, opts); } confirm_bot_deactivation(bot_id, handle_confirm, true); }); $("#inactive_bots_list").on("click", "button.reactivate_bot", (e) => { const user_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); e.stopPropagation(); e.preventDefault(); function handle_confirm() { channel.post({ url: "/json/users/" + encodeURIComponent(user_id) + "/reactivate", success() { dialog_widget.close_modal(); }, error(xhr) { ui_report.error($t_html({defaultMessage: "Failed"}), xhr, $("#dialog_error")); dialog_widget.hide_dialog_spinner(); }, }); } settings_users.confirm_reactivation(user_id, handle_confirm, true); }); $("#active_bots_list").on("click", "button.regenerate_bot_api_key", (e) => { const bot_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); channel.post({ url: "/json/bots/" + encodeURIComponent(bot_id) + "/api_key/regenerate", success(data) { const $row = $(e.currentTarget).closest("li"); $row.find(".api_key").find(".value").text(data.api_key); $row.find("api_key_error").hide(); }, error(xhr) { const $row = $(e.currentTarget).closest("li"); $row.find(".api_key_error").text(JSON.parse(xhr.responseText).msg).show(); }, }); }); $("#active_bots_list").on("click", "button.open_edit_bot_form", (e) => { e.preventDefault(); e.stopPropagation(); const $li = $(e.currentTarget).closest("li"); const bot_id = Number.parseInt($li.find(".bot_info").attr("data-user-id"), 10); show_edit_bot_info_modal(bot_id, false); }); $("#active_bots_list").on("click", "a.download_bot_zuliprc", function () { const $bot_info = $(this).closest(".bot-information-box").find(".bot_info"); const bot_id = Number.parseInt($bot_info.attr("data-user-id"), 10); $(this).attr("href", generate_zuliprc_uri(bot_id)); }); $("#active_bots_list").on("click", "button.open_bots_subscribed_streams", (e) => { e.preventDefault(); e.stopPropagation(); const bot_id = Number.parseInt($(e.currentTarget).attr("data-user-id"), 10); const bot = people.get_by_user_id(bot_id); user_profile.show_user_profile(bot, "user-profile-streams-tab"); }); new ClipboardJS("#copy_zuliprc", { text(trigger) { const $bot_info = $(trigger).closest(".bot-information-box").find(".bot_info"); const bot_id = Number.parseInt($bot_info.attr("data-user-id"), 10); const bot = bot_data.get(bot_id); const data = generate_zuliprc_content(bot); return data; }, }); $("#bots_lists_navbar .active-bots-tab").on("click", (e) => { e.preventDefault(); e.stopPropagation(); focus_tab.active_bots_tab(); }); $("#bots_lists_navbar .inactive-bots-tab").on("click", (e) => { e.preventDefault(); e.stopPropagation(); focus_tab.inactive_bots_tab(); }); $("#bot-settings .add-a-new-bot").on("click", (e) => { e.preventDefault(); e.stopPropagation(); add_a_new_bot(); }); }