mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
feat: Add mention support for webhook alerts. Related to #1118
Adds a Mention field to webhook configurations that allows users to tag
individuals or groups when alerts are sent. This works with:
- Discord: @everyone, <@USER_ID>, <@&ROLE_ID>
- Microsoft Teams: @General, user email
- Mattermost: @channel, @all, @username
The mention is included in the webhook payload via the {{.Mention}} template
variable. Built-in templates for Discord, Slack, and Teams now conditionally
include mentions when configured.
Backend changes:
- Add Mention field to WebhookConfig struct
- Add Mention field to WebhookPayloadData for template access
- Pass mention through sendGroupedWebhook
Frontend changes:
- Add mention field to Webhook interface
- Add Mention input to webhook configuration form
- Show service-specific help text for mention formats
This commit is contained in:
@@ -55,6 +55,7 @@ export interface Webhook {
|
||||
enabled: boolean;
|
||||
service?: string; // Added to support Discord, Slack, etc.
|
||||
customFields?: Record<string, string>;
|
||||
mention?: string; // Platform-specific mention (e.g., @everyone, @channel, <@USER_ID>)
|
||||
}
|
||||
|
||||
export interface AppriseConfig {
|
||||
|
||||
@@ -121,6 +121,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
enabled: true,
|
||||
payloadTemplate: '',
|
||||
customFields: {},
|
||||
mention: '',
|
||||
});
|
||||
const [templates, setTemplates] = createSignal<WebhookTemplate[]>([]);
|
||||
const [showServiceDropdown, setShowServiceDropdown] = createSignal(false);
|
||||
@@ -190,6 +191,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
service: data.service,
|
||||
template: data.payloadTemplate,
|
||||
customFields,
|
||||
mention: data.mention,
|
||||
});
|
||||
setEditingId(null);
|
||||
setAdding(false);
|
||||
@@ -206,6 +208,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
service: data.service,
|
||||
template: data.payloadTemplate,
|
||||
customFields,
|
||||
mention: data.mention,
|
||||
};
|
||||
props.onAdd(newWebhook);
|
||||
// Reset form and close the adding panel
|
||||
@@ -218,9 +221,10 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
enabled: true,
|
||||
payloadTemplate: '',
|
||||
customFields: {},
|
||||
mention: '',
|
||||
});
|
||||
setHeaderInputs([]);
|
||||
setCustomFieldInputs([]);
|
||||
setCustomFieldInputs([]);
|
||||
setAdding(false);
|
||||
}
|
||||
};
|
||||
@@ -237,6 +241,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
enabled: true,
|
||||
payloadTemplate: '',
|
||||
customFields: {},
|
||||
mention: '',
|
||||
});
|
||||
setHeaderInputs([]);
|
||||
setCustomFieldInputs([]);
|
||||
@@ -251,6 +256,7 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
service: webhook.service || 'generic',
|
||||
payloadTemplate: webhook.template || '',
|
||||
customFields: webhook.customFields || {},
|
||||
mention: webhook.mention || '',
|
||||
});
|
||||
// Set up header inputs for editing
|
||||
const headers = webhook.headers || {};
|
||||
@@ -366,11 +372,10 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
<span class="font-medium text-gray-800 dark:text-gray-200">{webhook.name}</span>
|
||||
<button
|
||||
onClick={() => props.onUpdate({ ...webhook, enabled: !webhook.enabled })}
|
||||
class={`rounded border px-3 py-1 text-xs font-medium transition-colors ${
|
||||
webhook.enabled
|
||||
? 'border-green-500 text-green-700 hover:bg-green-50 dark:border-green-600 dark:text-green-400 dark:hover:bg-green-900/20'
|
||||
: 'border-gray-300 text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
class={`rounded border px-3 py-1 text-xs font-medium transition-colors ${webhook.enabled
|
||||
? 'border-green-500 text-green-700 hover:bg-green-50 dark:border-green-600 dark:text-green-400 dark:hover:bg-green-900/20'
|
||||
: 'border-gray-300 text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{webhook.enabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
@@ -455,11 +460,10 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectService(service)}
|
||||
class={`px-2 py-1.5 text-left border transition-colors text-xs ${
|
||||
formData().service === service
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/30'
|
||||
}`}
|
||||
class={`px-2 py-1.5 text-left border transition-colors text-xs ${formData().service === service
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700/30'
|
||||
}`}
|
||||
>
|
||||
<div class="font-medium text-xs text-gray-800 dark:text-gray-200">
|
||||
{serviceName(service)}
|
||||
@@ -556,6 +560,37 @@ export function WebhookConfig(props: WebhookConfigProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mention Field - show for supported services */}
|
||||
<Show when={['discord', 'slack', 'teams', 'teams-adaptive', 'mattermost'].includes(formData().service)}>
|
||||
<div class={formField}>
|
||||
<label class={labelClass('flex items-center gap-2')}>
|
||||
Mention
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Optional — tag users or groups
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().mention || ''}
|
||||
onInput={(e) => setFormData({ ...formData(), mention: e.currentTarget.value })}
|
||||
placeholder={
|
||||
formData().service === 'discord' ? '@everyone or <@USER_ID> or <@&ROLE_ID>' :
|
||||
formData().service === 'slack' ? '@channel, @here, or <@USER_ID>' :
|
||||
formData().service === 'teams' || formData().service === 'teams-adaptive' ? '@General or user email' :
|
||||
formData().service === 'mattermost' ? '@channel, @all, or @username' :
|
||||
'@everyone'
|
||||
}
|
||||
class={controlClass('px-2 py-1.5')}
|
||||
/>
|
||||
<p class={formHelpText + ' mt-1'}>
|
||||
{formData().service === 'discord' && 'Discord: Use @everyone, @here, <@USER_ID>, or <@&ROLE_ID>'}
|
||||
{formData().service === 'slack' && 'Slack: Use @channel, @here, <@USER_ID>, or <!subteam^ID>'}
|
||||
{(formData().service === 'teams' || formData().service === 'teams-adaptive') && 'Teams: Use channel names like @General'}
|
||||
{formData().service === 'mattermost' && 'Mattermost: Use @channel, @all, or @username'}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Custom Payload Template - only show for generic service */}
|
||||
<Show when={formData().service === 'generic'}>
|
||||
<div class={formField}>
|
||||
|
||||
@@ -318,6 +318,7 @@ type WebhookConfig struct {
|
||||
Service string `json:"service"` // discord, slack, teams, etc.
|
||||
Template string `json:"template"` // Custom payload template
|
||||
CustomFields map[string]string `json:"customFields,omitempty"`
|
||||
Mention string `json:"mention,omitempty"` // Platform-specific mention (e.g., @everyone, @channel, <@USER_ID>)
|
||||
}
|
||||
|
||||
// AppriseMode identifies how Pulse should deliver notifications through Apprise.
|
||||
@@ -1539,6 +1540,7 @@ func (n *NotificationManager) sendGroupedWebhook(webhook WebhookConfig, alertLis
|
||||
prepared := n.prepareWebhookData(primaryAlert, customFields)
|
||||
prepared.AlertCount = len(alertList)
|
||||
prepared.Alerts = alertList
|
||||
prepared.Mention = webhook.Mention
|
||||
templateData = prepared
|
||||
dataPrepared = true
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestSendGroupedAppriseInvokesExecutor(t *testing.T) {
|
||||
nm.SetGroupingWindow(0)
|
||||
nm.SetEmailConfig(EmailConfig{Enabled: false})
|
||||
|
||||
done := make(chan struct{})
|
||||
done := make(chan struct{}, 1)
|
||||
var capturedArgs []string
|
||||
|
||||
nm.appriseExec = func(ctx context.Context, path string, args []string) ([]byte, error) {
|
||||
@@ -171,7 +171,10 @@ func TestSendGroupedAppriseInvokesExecutor(t *testing.T) {
|
||||
t.Fatalf("expected CLI path 'apprise', got %q", path)
|
||||
}
|
||||
capturedArgs = append([]string(nil), args...)
|
||||
close(done)
|
||||
select {
|
||||
case done <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return []byte("success"), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ type WebhookPayloadData struct {
|
||||
AlertCount int
|
||||
Alerts []*alerts.Alert // For grouped alerts
|
||||
ChatID string // For Telegram webhooks
|
||||
Mention string // Platform-specific mention text
|
||||
}
|
||||
|
||||
// SendEnhancedWebhook sends a webhook with template support
|
||||
|
||||
@@ -22,6 +22,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"username": "Pulse Monitoring",
|
||||
{{if .Mention}}"content": "{{.Mention}}",{{end}}
|
||||
"embeds": [{
|
||||
"title": "Pulse Alert: {{.Level | title}}",
|
||||
"description": "{{.Message}}",
|
||||
@@ -40,7 +41,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
||||
}
|
||||
}]
|
||||
}`,
|
||||
Instructions: "1. In Discord, go to Server Settings > Integrations > Webhooks\n2. Create a new webhook and copy the URL\n3. Paste the URL here (format: https://discord.com/api/webhooks/...)",
|
||||
Instructions: "1. In Discord, go to Server Settings > Integrations > Webhooks\n2. Create a new webhook and copy the URL\n3. Paste the URL here (format: https://discord.com/api/webhooks/...)\n4. Optional: Add a mention in the Mention field (e.g., @everyone, <@USER_ID>, <@&ROLE_ID>)",
|
||||
},
|
||||
{
|
||||
Service: "telegram",
|
||||
@@ -63,8 +64,15 @@ func GetWebhookTemplates() []WebhookTemplate {
|
||||
Method: "POST",
|
||||
Headers: map[string]string{"Content-Type": "application/json"},
|
||||
PayloadTemplate: `{
|
||||
"text": "Pulse Alert: {{.Level | title}} - {{.ResourceName}}",
|
||||
"text": "{{if .Mention}}{{.Mention}} {{end}}Pulse Alert: {{.Level | title}} - {{.ResourceName}}",
|
||||
"blocks": [
|
||||
{{if .Mention}}{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "{{.Mention}}"
|
||||
}
|
||||
},{{end}}
|
||||
{
|
||||
"type": "header",
|
||||
"text": {
|
||||
@@ -102,7 +110,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
||||
}
|
||||
]
|
||||
}`,
|
||||
Instructions: "1. In Slack, go to Apps > Incoming Webhooks\n2. Add to Slack and choose a channel\n3. Copy the webhook URL and paste it here (format: https://hooks.slack.com/services/...)",
|
||||
Instructions: "1. In Slack, go to Apps > Incoming Webhooks\n2. Add to Slack and choose a channel\n3. Copy the webhook URL and paste it here (format: https://hooks.slack.com/services/...)\n4. Optional: Add a mention in the Mention field (e.g., @channel, @here, <@USER_ID>, <!subteam^ID>)",
|
||||
},
|
||||
{
|
||||
Service: "teams",
|
||||
@@ -115,6 +123,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": "{{if eq .Level "critical"}}FF0000{{else if eq .Level "warning"}}FFA500{{else}}00FF00{{end}}",
|
||||
"summary": "Pulse Alert: {{.Level | title}} - {{.ResourceName}}",
|
||||
{{if .Mention}}"text": "{{.Mention}}",{{end}}
|
||||
"sections": [{
|
||||
"activityTitle": "Pulse Alert: {{.Level | title}}",
|
||||
"activitySubtitle": "{{.Message}}",
|
||||
@@ -138,7 +147,7 @@ func GetWebhookTemplates() []WebhookTemplate {
|
||||
}]
|
||||
}]
|
||||
}`,
|
||||
Instructions: "1. In Teams channel, click ... > Connectors\n2. Configure Incoming Webhook\n3. Copy the URL and paste it here\n\nNote: MessageCard format is supported until December 2025. For new implementations, consider using Adaptive Cards.",
|
||||
Instructions: "1. In Teams channel, click ... > Connectors\n2. Configure Incoming Webhook\n3. Copy the URL and paste it here\n4. Optional: Add a mention in the Mention field (e.g., @General)\n\nNote: MessageCard format is supported until December 2025. For new implementations, consider using Adaptive Cards.",
|
||||
},
|
||||
{
|
||||
Service: "pagerduty",
|
||||
|
||||
Reference in New Issue
Block a user