Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"@types/convert-source-map": "^2.0.3",
"@types/css-tree": "^2.3.11",
"@types/eslint-config-prettier": "^6.11.3",
"@types/estree": "^1.0.9",
"@types/gtag.js": "^0.0.20",
"@types/is-url": "^1.2.32",
"@types/jquery": "^4.0.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/src/bot_type_values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export const INCOMING_WEBHOOK_BOT_TYPE_INT = 2;
export const OUTGOING_WEBHOOK_BOT_TYPE_INT = 3;

// String forms used as HTML form values.
export const INCOMING_WEBHOOK_BOT_TYPE = "2";
export const OUTGOING_WEBHOOK_BOT_TYPE = "3";
export const EMBEDDED_BOT_TYPE = "4";
76 changes: 75 additions & 1 deletion web/src/portico/integrations_dev_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const integrations_api_response_schema = z.object({

type ServerResponse = z.infer<typeof integrations_api_response_schema>;

let last_computed_header_key: string | null = null; // Tracks the current signature header for auto-clearing when switching integrations

const loaded_fixtures = new Map<string, Fixtures>();
const url_base = "/api/v1/external/";

Expand Down Expand Up @@ -231,11 +233,83 @@ function update_url(): void {
params.set("topic", topic_name);
}
}
const webhook_secret = $<HTMLInputElement>("input#webhook_secret").val()!;
const url = `${url_base}${integration_name}?${params.toString()}`;
url_field!.value = url;

sync_signature_headers(integration_name, webhook_secret);
}
}

return;
function sync_signature_headers(integration_name: string, webhook_secret: string): void {
const $custom_headers_field = $<HTMLTextAreaElement>("textarea#custom_http_headers");
const current_headers_raw = $custom_headers_field.val()?.toString().trim() ?? "";

let headers_object: Record<string, string> = {};
if (current_headers_raw !== "") {
try {
headers_object = z
.record(z.string(), z.string())
.parse(JSON.parse(current_headers_raw));
} catch {
headers_object = {};
}
}

if (last_computed_header_key && Object.hasOwn(headers_object, last_computed_header_key)) {
Reflect.deleteProperty(headers_object, last_computed_header_key);
}

if (webhook_secret.trim() === "") {
last_computed_header_key = null;
if (Object.keys(headers_object).length === 0) {
$custom_headers_field.val("{}");
} else {
$custom_headers_field.val(JSON.stringify(headers_object, null, 4));
}
return;
}

const raw_payload = $<HTMLTextAreaElement>("textarea#fixture_body").val() ?? "";
let cleaned_payload: string;

try {
cleaned_payload = JSON.stringify(JSON.parse(raw_payload));
} catch {
cleaned_payload = raw_payload.trim();
}

channel.post({
url: "/devtools/integrations/recalculate_signature",
data: JSON.stringify({
secret: webhook_secret,
payload: cleaned_payload,
integration_name,
}),
success(raw_data: unknown) {
const data = z
.object({
supported: z.optional(z.boolean()),
clear_signature: z.optional(z.boolean()),
header_key: z.string(),
signature: z.string(),
})
.parse(raw_data);

if (!data.supported || data.clear_signature) {
last_computed_header_key = null;
if (Object.keys(headers_object).length === 0) {
$custom_headers_field.val("{}");
} else {
$custom_headers_field.val(JSON.stringify(headers_object, null, 4));
}
} else {
headers_object[data.header_key] = data.signature;
last_computed_header_key = data.header_key;
$custom_headers_field.val(JSON.stringify(headers_object, null, 4));
}
},
});
}

// API callers: These methods handle communicating with the Python backend API.
Expand Down
40 changes: 31 additions & 9 deletions web/src/settings_bots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as bot_helper from "./bot_helper.ts";
import {
EMBEDDED_BOT_TYPE,
GENERIC_BOT_TYPE,
INCOMING_WEBHOOK_BOT_TYPE,
INCOMING_WEBHOOK_BOT_TYPE_INT,
OUTGOING_WEBHOOK_BOT_TYPE,
OUTGOING_WEBHOOK_BOT_TYPE_INT,
Expand Down Expand Up @@ -298,6 +299,20 @@ export function add_a_new_bot(): void {
formData.append("interface_type", interface_type);
break;
}
case INCOMING_WEBHOOK_BOT_TYPE: {
const config_data: Record<string, string> = {};
$<HTMLInputElement>("#webhook_secret_inputbox input").each(function () {
const key = $(this).attr("name")!;
const raw_val = $(this).val();
if (typeof raw_val === "string" && raw_val.trim() !== "") {
config_data[key] = raw_val.trim();
}
});
if (Object.keys(config_data).length > 0) {
formData.append("config_data", JSON.stringify(config_data));
}
break;
}
case EMBEDDED_BOT_TYPE: {
formData.append("service_name", service_name);
const config_data: Record<string, string> = {};
Expand Down Expand Up @@ -336,7 +351,7 @@ export function add_a_new_bot(): void {
}

function set_up_form_fields(): void {
$("#create_bot_type").val(INCOMING_WEBHOOK_BOT_TYPE_INT);
$("#create_bot_type").val(INCOMING_WEBHOOK_BOT_TYPE).trigger("change");
$("#payload_url_inputbox").hide();
$("#create_payload_url").val("");
$("#service_name_list").hide();
Expand All @@ -362,7 +377,13 @@ export function add_a_new_bot(): void {

$("#payload_url_inputbox").hide();
$("#create_payload_url").removeClass("required");

$("#webhook_secret_inputbox").hide();
switch (bot_type) {
case INCOMING_WEBHOOK_BOT_TYPE: {
$("#webhook_secret_inputbox").show();
break;
}
case OUTGOING_WEBHOOK_BOT_TYPE: {
$("#payload_url_inputbox").show();
$("#create_payload_url").addClass("required");
Expand All @@ -377,7 +398,7 @@ export function add_a_new_bot(): void {
}
}
});

$("#create_bot_type").val(INCOMING_WEBHOOK_BOT_TYPE).trigger("change");
$("#select_service_name").on("change", () => {
$("#config_inputbox").children().hide();
const selected_bot = $<HTMLSelectOneElement>(
Expand Down Expand Up @@ -742,11 +763,12 @@ function set_up_bot_handlers($container: JQuery): void {
add_a_new_bot();
});

$container.find(".download-botserverrc-file").on("click", function () {
$container.find(".download-botserverrc-file").on("click", (e) => {
const currentTarget = e.currentTarget;
void (async () => {
let content = "";
buttons.show_button_loading_indicator($(this));
$(this).prop("disabled", true);
buttons.show_button_loading_indicator($(currentTarget));
$(currentTarget).prop("disabled", true);
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_helper.get_outgoing_webhook_token(bot.user_id);
Expand All @@ -755,15 +777,15 @@ function set_up_bot_handlers($container: JQuery): void {
$("#admin-your-bots-list .bot-list-error"),
);
if (!api_key) {
buttons.hide_button_loading_indicator($(this));
$(this).prop("disabled", false);
buttons.hide_button_loading_indicator($(currentTarget));
$(currentTarget).prop("disabled", false);
return;
}
content += generate_botserverrc_content(bot.email, api_key, bot_token);
}
}
buttons.hide_button_loading_indicator($(this));
$(this).prop("disabled", false);
buttons.hide_button_loading_indicator($(currentTarget));
$(currentTarget).prop("disabled", false);

$container
.find(".hidden-botserverrc-download")
Expand Down
42 changes: 36 additions & 6 deletions web/src/user_profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ClipboardJS from "clipboard";
import {parseISO} from "date-fns";
import {parseOneAddress} from "email-addresses";
import {$} from "jquery";
import _ from "lodash";
import assert from "minimalistic-assert";
Expand All @@ -24,6 +23,7 @@ import * as bot_data from "./bot_data.ts";
import * as bot_helper from "./bot_helper.ts";
import {
EMBEDDED_BOT_TYPE,
INCOMING_WEBHOOK_BOT_TYPE,
INCOMING_WEBHOOK_BOT_TYPE_INT,
OUTGOING_WEBHOOK_BOT_TYPE,
} from "./bot_type_values.ts";
Expand Down Expand Up @@ -851,11 +851,7 @@ export function show_edit_bot_info_modal(user_id: number, $container: JQuery): v

assert(bot.is_bot);
// Extract short_name from email (format: {short_name}-bot@domain)
const parsed_address = parseOneAddress(bot.email);
assert(parsed_address?.type === "mailbox");
const short_name = parsed_address.local.endsWith("-bot")
? parsed_address.local.slice(0, -"-bot".length)
: parsed_address.local;
const short_name = bot.email.split("@", 1)[0]!.slice(0, -4);
const modal_content_html = render_edit_bot_form({
user_id,
is_active,
Expand All @@ -879,7 +875,32 @@ export function show_edit_bot_info_modal(user_id: number, $container: JQuery): v
const bot_type = bot.bot_type.toString();
const services = bot_data.get_services(bot.user_id);
const service = services?.[0];
let is_delete_requested = false;
edit_bot_post_render();

$("#bot-edit-form").on("click", "#clear_webhook_secret_button", (e) => {
e.preventDefault();
is_delete_requested = true;

// Clear the input value and set visual feedback
const $secret_input = $("#edit_webhook_secret");
$secret_input.val("");
$secret_input.attr(
"placeholder",
$t({defaultMessage: "Secret will be deleted when saved."}),
);

// Notify form handler that a change was made so the save button is enabled
$("#user-profile-modal .dialog_submit_button").prop("disabled", false);
});

// If the user types anything manually into the input, reset the clear flag
$("#bot-edit-form").on("input", "#edit_webhook_secret", function () {
if ($(this).val() !== "") {
$(this).data("clear-secret", false);
}
});

original_values = get_current_values($("#bot-edit-form"));
$("#bot-edit-form").on("input", "input, select, button", (e) => {
e.preventDefault();
Expand Down Expand Up @@ -935,6 +956,14 @@ export function show_edit_bot_info_modal(user_id: number, $container: JQuery): v
config_data[$(this).attr("name")!] = $(this).val()!;
});
formData.append("config_data", JSON.stringify(config_data));
} else if (bot_type === INCOMING_WEBHOOK_BOT_TYPE) {
const webhook_secret = $<HTMLInputElement>("#edit_webhook_secret").val()?.trim();
if (is_delete_requested) {
formData.append("config_data", JSON.stringify({webhook_secret: ""}));
is_delete_requested = false;
} else if (webhook_secret) {
formData.append("config_data", JSON.stringify({webhook_secret}));
}
}

const files = util.the(
Expand All @@ -957,6 +986,7 @@ export function show_edit_bot_info_modal(user_id: number, $container: JQuery): v
contentType: false,
success() {
$("#bot-edit-form-error").hide();
$("#edit-webhook-secret").val("");
avatar_widget.clear();
hide_button_spinner($submit_button);
original_values = get_current_values($("#bot-edit-form"));
Expand Down
7 changes: 7 additions & 0 deletions web/templates/settings/add_new_bot_form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
<div><label for="create_interface_type" generated="true" class="text-error"></label></div>
</div>
</div>
<div id="webhook_secret_inputbox">
<div class="input-group">
<label for="create_webhook_secret" class="modal-field-label">{{t "Webhook secret (optional)" }}</label>
<input type="password" name="webhook_secret" id="create_webhook_secret" class="modal_text_input" placeholder="{{t 'Cookie secret' }}" value=""/>
<div><label for="create_webhook_secret" generated="true" class="text-error"></label></div>
</div>
</div>
<div id="config_inputbox">
{{#each realm_embedded_bots}}
{{#each (object_entries config) as |entry|}}
Expand Down
21 changes: 20 additions & 1 deletion web/templates/settings/edit_bot_form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,15 @@
widget_name="edit_bot_owner"
label=(t 'Owner')}}

<div id="service_data">
<div id="service_data"></div>

{{#if is_incoming_webhook_bot}}
<div class="input-group edit_bot_webhook_secret_container">
<label for="edit_webhook_secret" class="modal-field-label">{{t "Webhook secret" }}</label>
<input type="password" name="webhook_secret" id="edit_webhook_secret" class="modal_text_input" placeholder="{{t 'Leave blank to keep current secret' }}" value=""/>
</div>
{{/if}}

<div class="input-group edit-avatar-section">
<label class="modal-field-label">{{t "Avatar" }}</label>
{{!-- Shows the current avatar --}}
Expand Down Expand Up @@ -109,6 +116,18 @@
}}
</div>
{{/if}}

{{#if is_incoming_webhook_bot}}
<div class="input-group">
{{> ../components/action_button
label=(t "Delete secret")
variant="subtle"
intent="danger"
id="clear_webhook_secret_button"
}}
</div>
{{/if}}

<div class="input-group">
{{#if is_active}}
{{> ../components/action_button
Expand Down
Loading