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
4 changes: 4 additions & 0 deletions templates/zerver/development/integrations_dev_panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
<label class="optional"><b>Topic</b></label>
<input id="topic_name" type="text" />
</div>
<div>
<label class="optional"><b>Webhook Secret</b></label>
<input id="webhook_secret" type="text" />
</div>
</div>

<br />
Expand Down
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";
86 changes: 83 additions & 3 deletions web/src/portico/integrations_dev_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type HTMLSelectOneElement = HTMLSelectElement & {type: "select-one"};
type ClearHandlers = {
stream_name: string;
topic_name: string;
webhook_secret: string;
URL: string;
results_notice: string;
bot_name: () => void;
Expand All @@ -47,6 +48,8 @@ const integrations_api_response_schema = z.object({
result: z.string(),
});

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

type ServerResponse = z.infer<typeof integrations_api_response_schema>;

const loaded_fixtures = new Map<string, Fixtures>();
Expand All @@ -56,6 +59,7 @@ const url_base = "/api/v1/external/";
const clear_handlers: ClearHandlers = {
stream_name: "#stream_name",
topic_name: "#topic_name",
webhook_secret: "#webhook_secret",
URL: "#URL",
results_notice: "#results_notice",
bot_name() {
Expand Down Expand Up @@ -180,6 +184,8 @@ function load_fixture_body(fixture_name: string): void {
null,
4,
);
const webhook_secret = $<HTMLInputElement>("input#webhook_secret").val()!;
sync_signature_headers(integration_name, webhook_secret);

return;
}
Expand Down Expand Up @@ -210,8 +216,8 @@ function load_fixture_options(integration_name: string): void {

function update_url(): void {
/* Construct the URL that the webhook should be targeting, using
the bot's API key and the integration name. The stream and topic
are both optional, and for the sake of completeness, it should be
the bot's API key, the integration name, and webhook secret. The stream, topic,
and webhook secret are all optional, and for the sake of completeness, it should be
noted that the topic is irrelevant without specifying the
stream. */
const url_field = $<HTMLInputElement>("input#URL")[0];
Expand All @@ -231,11 +237,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 && last_computed_header_key in headers_object) {
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 = raw_payload;

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 Expand Up @@ -440,4 +518,6 @@ $(() => {
$("#stream_name").on("change", update_url);

$("#topic_name").on("change", update_url);

$("#webhook_secret").on("change", update_url);
});
25 changes: 23 additions & 2 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
35 changes: 35 additions & 0 deletions web/src/user_profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,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 @@ -874,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 @@ -930,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 @@ -952,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
3 changes: 2 additions & 1 deletion web/styles/portico/integrations_dev_panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
}

#stream_name,
#topic_name {
#topic_name,
#webhook_secret {
width: 206px;
}

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
4 changes: 2 additions & 2 deletions zerver/actions/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,15 +827,15 @@ def do_update_outgoing_webhook_service(
def do_update_bot_config_data(bot_profile: UserProfile, config_data: dict[str, str]) -> None:
for key, value in config_data.items():
set_bot_config(bot_profile, key, value)
updated_config_data = get_bot_config(bot_profile)
service_dicts = get_service_dicts_for_bot(bot_profile.id)
send_event_on_commit(
bot_profile.realm,
dict(
type="realm_bot",
op="update",
bot=dict(
user_id=bot_profile.id,
services=[dict(config_data=updated_config_data)],
services=service_dicts,
),
),
bot_owner_user_ids(bot_profile),
Expand Down
Loading
Loading