-
Notifications
You must be signed in to change notification settings - Fork 246
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c263f48
commit ac18aea
Showing
2 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
extend type Query { | ||
destinationTypes: [DestinationTypeInfo!]! | ||
destinationFieldValidate(input: DestinationFieldValidateInput!): Boolean! | ||
} | ||
|
||
input DestinationFieldValidateInput { | ||
destType: DestinationType! # the type of destination to validate | ||
fieldID: ID! # the ID of the input field to validate | ||
value: String! # the value to validate | ||
} | ||
|
||
# DestinationType represents a type of destination that can be used for | ||
# notifications. | ||
scalar DestinationType | ||
|
||
# Destination represents a destination that can be used for notifications. | ||
type Destination { | ||
type: DestinationType! | ||
values: [FieldValuePair!]! | ||
typeInfo: DestinationTypeInfo! | ||
} | ||
|
||
type FieldValuePair { | ||
fieldID: ID! # The ID of the input field that this value is for. | ||
|
||
value: String! # The value of the input field. | ||
} | ||
|
||
input DestinationInput { | ||
type: DestinationType! | ||
values: [FieldValueInput!]! | ||
} | ||
|
||
input FieldValueInput { | ||
fieldID: ID! # The ID of the input field that this value is for. | ||
value: String! | ||
} | ||
|
||
type DestinationTypeInfo { | ||
type: DestinationType! | ||
|
||
name: String! | ||
|
||
iconURL: String! # URL to an icon to display for the destination type | ||
iconAltText: String! # alt text for the icon | ||
|
||
disabledMessage: String! | ||
|
||
enabled: Boolean! # if false, the destination type is disabled and cannot be used | ||
|
||
requiredFields: [DestinationFieldConfig!]! | ||
|
||
userDisclaimer: String! # disclaimer text to display when a user is selecting this destination type for a contact method | ||
|
||
isContactMethod: Boolean! # this destination type can be used as a user contact method | ||
isEPTarget: Boolean! # this destination type can be used as an escalation policy step action | ||
isSchedOnCallNotify: Boolean! # this destination type can be used for schedule on-call notifications | ||
} | ||
|
||
type DestinationFieldConfig { | ||
fieldID: ID! # unique ID for the input field | ||
|
||
labelSingular: String! # user-friendly label | ||
labelPlural: String! # user-friendly plural label | ||
|
||
hint: String! # user-friendly helper text for input fields (i.e., "Enter a phone number") | ||
hintURL: String! # URL to link to for more information about the destination type | ||
placeholderText: String! # placeholder text to display in input fields (e.g., "Phone Number") | ||
|
||
prefix: String! # the prefix to use when displaying the destination (e.g., "+" for phone numbers) | ||
inputType: String! # the type of input field (type attribute) to use (e.g., "text" or "tel") | ||
|
||
isSearchSelectable: Boolean! # if true, the destination can be selected via search | ||
supportsValidation: Boolean! # if true, the destination type supports validation | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
package graphqlapp | ||
|
||
import ( | ||
"context" | ||
"slices" | ||
|
||
"github.com/nyaruka/phonenumbers" | ||
"github.com/target/goalert/config" | ||
"github.com/target/goalert/graphql2" | ||
"github.com/target/goalert/validation" | ||
"github.com/target/goalert/validation/validate" | ||
) | ||
|
||
// builtin-types | ||
const ( | ||
destTwilioSMS = "builtin-twilio-sms" | ||
destTwilioVoice = "builtin-twilio-voice" | ||
destSMTP = "builtin-smtp-email" | ||
destWebhook = "builtin-webhook" | ||
destSlackDM = "builtin-slack-dm" | ||
destSlackChan = "builtin-slack-channel" | ||
destSlackUG = "builtin-slack-usergroup" | ||
destUser = "builtin-user" | ||
destRotation = "builtin-rotation" | ||
destSchedule = "builtin-schedule" | ||
|
||
fieldPhoneNumber = "phone-number" | ||
fieldEmailAddress = "email-address" | ||
fieldWebhookURL = "webhook-url" | ||
fieldSlackUserID = "slack-user-id" | ||
fieldSlackChanID = "slack-channel-id" | ||
fieldSlackUGID = "slack-usergroup-id" | ||
fieldUserID = "user-id" | ||
fieldRotationID = "rotation-id" | ||
fieldScheduleID = "schedule-id" | ||
) | ||
|
||
func (q *Query) DestinationFieldValidate(ctx context.Context, input graphql2.DestinationFieldValidateInput) (bool, error) { | ||
switch input.DestType { | ||
case destTwilioSMS, destTwilioVoice: | ||
if input.FieldID != fieldPhoneNumber { | ||
return false, validation.NewGenericError("unsupported field") | ||
} | ||
n, err := phonenumbers.Parse(input.Value, "") | ||
if err != nil { | ||
return false, nil | ||
} | ||
return phonenumbers.IsValidNumber(n), nil | ||
case destSMTP: | ||
if input.FieldID != fieldEmailAddress { | ||
return false, validation.NewGenericError("unsupported field") | ||
} | ||
|
||
return validate.Email("Email", input.Value) == nil, nil | ||
case destWebhook: | ||
if input.FieldID != fieldWebhookURL { | ||
return false, validation.NewGenericError("unsupported field") | ||
} | ||
|
||
err := validate.AbsoluteURL("URL", input.Value) | ||
return err == nil, nil | ||
} | ||
|
||
return false, validation.NewGenericError("unsupported data type") | ||
} | ||
|
||
func (q *Query) DestinationTypes(ctx context.Context) ([]graphql2.DestinationTypeInfo, error) { | ||
cfg := config.FromContext(ctx) | ||
types := []graphql2.DestinationTypeInfo{ | ||
{ | ||
Type: destTwilioSMS, | ||
Name: "Text Message (SMS)", | ||
Enabled: cfg.Twilio.Enable, | ||
DisabledMessage: "Twilio must be configured by an administrator", | ||
UserDisclaimer: cfg.General.NotificationDisclaimer, | ||
IsContactMethod: true, | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldPhoneNumber, | ||
LabelSingular: "Phone Number", | ||
LabelPlural: "Phone Numbers", | ||
Hint: "Include country code e.g. +1 (USA), +91 (India), +44 (UK)", | ||
PlaceholderText: "11235550123", | ||
Prefix: "+", | ||
InputType: "tel", | ||
SupportsValidation: true, | ||
}}, | ||
}, | ||
{ | ||
Type: destTwilioVoice, | ||
Name: "Voice Call", | ||
Enabled: cfg.Twilio.Enable, | ||
DisabledMessage: "Twilio must be configured by an administrator", | ||
UserDisclaimer: cfg.General.NotificationDisclaimer, | ||
IsContactMethod: true, | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldPhoneNumber, | ||
LabelSingular: "Phone Number", | ||
LabelPlural: "Phone Numbers", | ||
Hint: "Include country code e.g. +1 (USA), +91 (India), +44 (UK)", | ||
PlaceholderText: "11235550123", | ||
Prefix: "+", | ||
InputType: "tel", | ||
SupportsValidation: true, | ||
}}, | ||
}, | ||
{ | ||
Type: destSMTP, | ||
Name: "Email", | ||
Enabled: cfg.SMTP.Enable, | ||
IsContactMethod: true, | ||
DisabledMessage: "SMTP must be configured by an administrator", | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldEmailAddress, | ||
LabelSingular: "Email Address", | ||
LabelPlural: "Email Addresses", | ||
PlaceholderText: "[email protected]", | ||
InputType: "email", | ||
SupportsValidation: true, | ||
}}, | ||
}, | ||
{ | ||
Type: destWebhook, | ||
Name: "Webhook", | ||
Enabled: cfg.Webhook.Enable, | ||
IsContactMethod: true, | ||
IsEPTarget: true, | ||
IsSchedOnCallNotify: true, | ||
DisabledMessage: "Webhooks must be enabled by an administrator", | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldWebhookURL, | ||
LabelSingular: "Webhook URL", | ||
LabelPlural: "Webhook URLs", | ||
PlaceholderText: "https://example.com", | ||
InputType: "url", | ||
Hint: "Webhook Documentation", | ||
HintURL: "/docs#webhooks", | ||
SupportsValidation: true, | ||
}}, | ||
}, | ||
{ | ||
Type: destSlackDM, | ||
Name: "Slack Message (DM)", | ||
Enabled: cfg.Slack.Enable, | ||
IsContactMethod: true, | ||
DisabledMessage: "Slack must be enabled by an administrator", | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldSlackUserID, | ||
LabelSingular: "Slack User", | ||
LabelPlural: "Slack Users", | ||
PlaceholderText: "member ID", | ||
InputType: "text", | ||
// IsSearchSelectable: true, // TODO: implement search select functionality for users | ||
Hint: `Go to your Slack profile, click the three dots, and select "Copy member ID".`, | ||
}}, | ||
}, | ||
{ | ||
Type: destSlackChan, | ||
Name: "Slack Channel", | ||
Enabled: cfg.Slack.Enable, | ||
IsEPTarget: true, | ||
IsSchedOnCallNotify: true, | ||
DisabledMessage: "Slack must be enabled by an administrator", | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldSlackChanID, | ||
LabelSingular: "Slack Channel", | ||
LabelPlural: "Slack Channels", | ||
InputType: "text", | ||
IsSearchSelectable: true, | ||
}}, | ||
}, | ||
{ | ||
Type: destSlackUG, | ||
Name: "Update Slack User Group", | ||
Enabled: cfg.Slack.Enable, | ||
IsSchedOnCallNotify: true, | ||
DisabledMessage: "Slack must be enabled by an administrator", | ||
RequiredFields: []graphql2.DestinationFieldConfig{{ | ||
FieldID: fieldSlackUGID, | ||
LabelSingular: "User Group", | ||
LabelPlural: "User Groups", | ||
InputType: "text", | ||
IsSearchSelectable: true, | ||
Hint: "The selected group's membership will be replaced/set to the schedule's on-call user(s).", | ||
}, { | ||
FieldID: fieldSlackChanID, | ||
LabelSingular: "Slack Channel (for errors)", | ||
LabelPlural: "Slack Channels (for errors)", | ||
InputType: "text", | ||
IsSearchSelectable: true, | ||
Hint: "If the user group update fails, an error will be posted to this channel.", | ||
}}, | ||
}, | ||
} | ||
|
||
slices.SortStableFunc(types, func(a, b graphql2.DestinationTypeInfo) int { | ||
if a.Enabled && !b.Enabled { | ||
return -1 | ||
} | ||
if !a.Enabled && b.Enabled { | ||
return 1 | ||
} | ||
|
||
// keep order for types that are both enabled or both disabled | ||
return 0 | ||
}) | ||
|
||
return types, nil | ||
} |