mirror of
https://github.com/Luzifer/twitch-bot.git
synced 2024-12-30 00:21:16 +00:00
[messagehook] Add actor for Discord / Slack hook posts
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
2f0572c256
commit
d92824a892
8 changed files with 666 additions and 0 deletions
|
@ -127,6 +127,71 @@ Delete message which caused the rule to be executed
|
||||||
# Does not have configuration attributes
|
# Does not have configuration attributes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Discord Message-Webhook
|
||||||
|
|
||||||
|
Sends a message to a Discord Web-hook
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: discordhook
|
||||||
|
attributes:
|
||||||
|
# URL to send the POST request to
|
||||||
|
# Optional: false
|
||||||
|
# Type: string
|
||||||
|
hook_url: ""
|
||||||
|
# Overwrites the username set in the webhook configuration
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
username: ""
|
||||||
|
# Overwrites the avatar set in the webhook configuration
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
avatar_url: ""
|
||||||
|
# Message content to send to the web-hook (this must be set if embed is disabled)
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
content: ""
|
||||||
|
# Whether to include the embed in the post
|
||||||
|
# Optional: true
|
||||||
|
# Type: bool
|
||||||
|
add_embed: false
|
||||||
|
# Title of the embed
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_title: ""
|
||||||
|
# Description of the embed
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_description: ""
|
||||||
|
# URL the title should link to
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_url: ""
|
||||||
|
# URL of the big image displayed in the embed
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_image: ""
|
||||||
|
# URL of the small image displayed in the embed
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_thumbnail: ""
|
||||||
|
# Name of the post author (if empty all other author-fields are ignored)
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_author_name: ""
|
||||||
|
# URL the author name should link to
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_author_url: ""
|
||||||
|
# URL of the author avatar
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_author_icon_url: ""
|
||||||
|
# Fields to display in the embed (must yield valid JSON: `[{"name": "", "value": "", "inline": false}]`)
|
||||||
|
# Optional: true
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
embed_fields: ""
|
||||||
|
```
|
||||||
|
|
||||||
## Enforce Link-Protection
|
## Enforce Link-Protection
|
||||||
|
|
||||||
Uses link- and clip-scanner to detect links / clips and applies link protection as defined
|
Uses link- and clip-scanner to detect links / clips and applies link protection as defined
|
||||||
|
@ -473,6 +538,23 @@ Perform a Twitch-native shoutout
|
||||||
user: ""
|
user: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Slack Message-Webhook
|
||||||
|
|
||||||
|
Sends a message to a Slack(-compatible) Web-hook
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: slackhook
|
||||||
|
attributes:
|
||||||
|
# URL to send the POST request to
|
||||||
|
# Optional: false
|
||||||
|
# Type: string
|
||||||
|
hook_url: ""
|
||||||
|
# Text to send to the web-hook
|
||||||
|
# Optional: false
|
||||||
|
# Type: string (Supports Templating)
|
||||||
|
text: ""
|
||||||
|
```
|
||||||
|
|
||||||
## Stop Execution
|
## Stop Execution
|
||||||
|
|
||||||
Stop Rule Execution on Condition
|
Stop Rule Execution on Condition
|
||||||
|
|
|
@ -73,6 +73,37 @@ title: "Rule Examples"
|
||||||
match_message: '^!death'
|
match_message: '^!death'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Notify Discord when stream is live
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- actions:
|
||||||
|
- type: discordhook
|
||||||
|
attributes:
|
||||||
|
add_embed: true
|
||||||
|
avatar_url: '{{ profileImage .channel }}'
|
||||||
|
content: |
|
||||||
|
<@&123456789012345678> {{ displayName (fixUsername .channel) (fixUsername .channel) }}
|
||||||
|
is now live on https://www.twitch.tv/{{ fixUsername .channel }} - join us!
|
||||||
|
embed_author_icon_url: '{{ profileImage .channel }}'
|
||||||
|
embed_author_name: '{{ displayName (fixUsername .channel) (fixUsername .channel) }}'
|
||||||
|
embed_fields: |
|
||||||
|
{{
|
||||||
|
toJson (
|
||||||
|
list
|
||||||
|
(dict
|
||||||
|
"name" "Game"
|
||||||
|
"value" (recentGame .channel))
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
embed_image: https://static-cdn.jtvnw.net/previews-ttv/live_user_{{ fixUsername .channel }}-1280x720.jpg
|
||||||
|
embed_thumbnail: '{{ profileImage .channel }}'
|
||||||
|
embed_title: '{{ recentTitle .channel }}'
|
||||||
|
embed_url: https://twitch.tv/{{ fixUsername .channel }}
|
||||||
|
hook_url: https://discord.com/api/webhooks/[...]/[...]
|
||||||
|
username: 'Stream-Live: {{ displayName (fixUsername .channel) (fixUsername .channel) }}'
|
||||||
|
match_event: stream_online
|
||||||
|
```
|
||||||
|
|
||||||
## Post follow date for an user
|
## Post follow date for an user
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
69
internal/actors/messagehook/actor.go
Normal file
69
internal/actors/messagehook/actor.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package messagehook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
postTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
formatMessage plugins.MsgFormatter
|
||||||
|
|
||||||
|
ptrBoolFalse = func(v bool) *bool { return &v }(false)
|
||||||
|
ptrStringEmpty = func(s string) *string { return &s }("")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(args plugins.RegistrationArguments) error {
|
||||||
|
formatMessage = args.FormatMessage
|
||||||
|
|
||||||
|
discordActor{}.register(args)
|
||||||
|
slackCompatibleActor{}.register(args)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPayload(hookURL string, payload any, expRespCode int) (preventCooldown bool, err error) {
|
||||||
|
body := new(bytes.Buffer)
|
||||||
|
if err = json.NewEncoder(body).Encode(payload); err != nil {
|
||||||
|
return false, errors.Wrap(err, "marshalling payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithField("payload", body.String()).Trace("sending webhook payload")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), postTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, hookURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "creating request")
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "executing request")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != expRespCode {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
body = []byte(errors.Wrap(err, "reading body").Error())
|
||||||
|
}
|
||||||
|
return false, errors.Errorf("unexpected response code %d (Body: %s)", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
302
internal/actors/messagehook/discord.go
Normal file
302
internal/actors/messagehook/discord.go
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
package messagehook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-irc/irc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
discordActor struct {
|
||||||
|
plugins.ActorKit
|
||||||
|
}
|
||||||
|
|
||||||
|
discordPayload struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
AvatarURL string `json:"avatar_url,omitempty"`
|
||||||
|
Embeds []discordPayloadEmbed `json:"embeds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
discordPayloadEmbed struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Color int64 `json:"color,omitempty"`
|
||||||
|
Image *discordPayloadEmbedImage `json:"image,omitempty"`
|
||||||
|
Thumbnail *discordPayloadEmbedImage `json:"thumbnail,omitempty"`
|
||||||
|
Author *discordPayloadEmbedAuthor `json:"author,omitempty"`
|
||||||
|
Fields []discordPayloadEmbedField `json:"fields,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
discordPayloadEmbedAuthor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
IconURL string `json:"icon_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
discordPayloadEmbedField struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Inline bool `json:"inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
discordPayloadEmbedImage struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d discordActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||||
|
var payload discordPayload
|
||||||
|
|
||||||
|
if payload.Content, err = formatMessage(attrs.MustString("content", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return false, errors.Wrap(err, "parsing content")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Username, err = formatMessage(attrs.MustString("username", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return false, errors.Wrap(err, "parsing username")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.AvatarURL, err = formatMessage(attrs.MustString("avatar_url", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return false, errors.Wrap(err, "parsing avatar_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = d.addEmbed(&payload, m, r, eventData, attrs); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendPayload(attrs.MustString("hook_url", ptrStringEmpty), payload, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (discordActor) IsAsync() bool { return false }
|
||||||
|
|
||||||
|
func (discordActor) Name() string { return "discordhook" }
|
||||||
|
|
||||||
|
func (d discordActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||||
|
if err = d.ValidateRequireNonEmpty(attrs, "hook_url"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = d.ValidateRequireValidTemplate(tplValidator, attrs, "content"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = d.ValidateRequireValidTemplateIfSet(tplValidator, attrs, "avatar_url", "username"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !attrs.MustBool("add_embed", ptrBoolFalse) {
|
||||||
|
// We're not validating the rest if embeds are disabled but in
|
||||||
|
// this case the content is mandatory
|
||||||
|
return d.ValidateRequireNonEmpty(attrs, "content")
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.ValidateRequireValidTemplateIfSet(
|
||||||
|
tplValidator, attrs,
|
||||||
|
"embed_title",
|
||||||
|
"embed_description",
|
||||||
|
"embed_url",
|
||||||
|
"embed_image",
|
||||||
|
"embed_thumbnail",
|
||||||
|
"embed_author_name",
|
||||||
|
"embed_author_url",
|
||||||
|
"embed_author_icon_url",
|
||||||
|
"embed_fields",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo // It's complex but just a bunch of converters
|
||||||
|
func (discordActor) addEmbed(payload *discordPayload, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (err error) {
|
||||||
|
if !attrs.MustBool("add_embed", ptrBoolFalse) {
|
||||||
|
// No embed? No problem!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
embed discordPayloadEmbed
|
||||||
|
sv string
|
||||||
|
)
|
||||||
|
|
||||||
|
if embed.Title, err = formatMessage(attrs.MustString("embed_title", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_title")
|
||||||
|
}
|
||||||
|
|
||||||
|
if embed.Description, err = formatMessage(attrs.MustString("embed_description", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_description")
|
||||||
|
}
|
||||||
|
|
||||||
|
if embed.URL, err = formatMessage(attrs.MustString("embed_url", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv, err = formatMessage(attrs.MustString("embed_image", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_image")
|
||||||
|
} else if sv != "" {
|
||||||
|
embed.Image = &discordPayloadEmbedImage{URL: sv}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv, err = formatMessage(attrs.MustString("embed_thumbnail", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_thumbnail")
|
||||||
|
} else if sv != "" {
|
||||||
|
embed.Thumbnail = &discordPayloadEmbedImage{URL: sv}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv, err = formatMessage(attrs.MustString("embed_author_name", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_author_name")
|
||||||
|
} else if sv != "" {
|
||||||
|
embed.Author = &discordPayloadEmbedAuthor{Name: sv}
|
||||||
|
|
||||||
|
if embed.Author.URL, err = formatMessage(attrs.MustString("embed_author_url", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_author_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if embed.Author.IconURL, err = formatMessage(attrs.MustString("embed_author_icon_url", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_author_icon_url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv, err = formatMessage(attrs.MustString("embed_fields", ptrStringEmpty), m, r, eventData); err != nil {
|
||||||
|
return errors.Wrap(err, "parsing embed_fields")
|
||||||
|
} else if sv != "" {
|
||||||
|
var flds []discordPayloadEmbedField
|
||||||
|
if err = json.Unmarshal([]byte(sv), &flds); err != nil {
|
||||||
|
return errors.Wrap(err, "unmarshalling embed_fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.Fields = flds
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.Embeds = append(payload.Embeds, embed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:funlen // This is just a bunch of field descriptions
|
||||||
|
func (discordActor) register(args plugins.RegistrationArguments) {
|
||||||
|
args.RegisterActor("discordhook", func() plugins.Actor { return &discordActor{} })
|
||||||
|
|
||||||
|
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||||
|
Description: "Sends a message to a Discord Web-hook",
|
||||||
|
Name: "Discord Message-Webhook",
|
||||||
|
Type: "discordhook",
|
||||||
|
|
||||||
|
Fields: []plugins.ActionDocumentationField{
|
||||||
|
{
|
||||||
|
Description: "URL to send the POST request to",
|
||||||
|
Key: "hook_url",
|
||||||
|
Name: "Hook URL",
|
||||||
|
Optional: false,
|
||||||
|
SupportTemplate: false,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Overwrites the username set in the webhook configuration",
|
||||||
|
Key: "username",
|
||||||
|
Name: "Username",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Overwrites the avatar set in the webhook configuration",
|
||||||
|
Key: "avatar_url",
|
||||||
|
Name: "Avatar URL",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Message content to send to the web-hook (this must be set if embed is disabled)",
|
||||||
|
Key: "content",
|
||||||
|
Name: "Message",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Default: "false",
|
||||||
|
Description: "Whether to include the embed in the post",
|
||||||
|
Key: "add_embed",
|
||||||
|
Name: "Add Embed",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: false,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeBool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Title of the embed",
|
||||||
|
Key: "embed_title",
|
||||||
|
Name: "Embed Title",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Description of the embed",
|
||||||
|
Key: "embed_description",
|
||||||
|
Name: "Embed Description",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "URL the title should link to",
|
||||||
|
Key: "embed_url",
|
||||||
|
Name: "Embed URL",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "URL of the big image displayed in the embed",
|
||||||
|
Key: "embed_image",
|
||||||
|
Name: "Embed Image URL",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "URL of the small image displayed in the embed",
|
||||||
|
Key: "embed_thumbnail",
|
||||||
|
Name: "Embed Thumbnail URL",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Name of the post author (if empty all other author-fields are ignored)",
|
||||||
|
Key: "embed_author_name",
|
||||||
|
Name: "Embed Author Name",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "URL the author name should link to",
|
||||||
|
Key: "embed_author_url",
|
||||||
|
Name: "Embed Author URL",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "URL of the author avatar",
|
||||||
|
Key: "embed_author_icon_url",
|
||||||
|
Name: "Embed Author Avatar URL",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Fields to display in the embed (must yield valid JSON: `[{\"name\": \"\", \"value\": \"\", \"inline\": false}]`)",
|
||||||
|
Key: "embed_fields",
|
||||||
|
Name: "Embed Fields",
|
||||||
|
Optional: true,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
82
internal/actors/messagehook/slack.go
Normal file
82
internal/actors/messagehook/slack.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package messagehook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-irc/irc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
type slackCompatibleActor struct {
|
||||||
|
plugins.ActorKit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s slackCompatibleActor) Execute(_ *irc.Client, m *irc.Message, r *plugins.Rule, eventData *plugins.FieldCollection, attrs *plugins.FieldCollection) (preventCooldown bool, err error) {
|
||||||
|
text, err := formatMessage(attrs.MustString("text", nil), m, r, eventData)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "parsing text")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendPayload(
|
||||||
|
s.fixHookURL(attrs.MustString("hook_url", ptrStringEmpty)),
|
||||||
|
map[string]string{
|
||||||
|
"text": text,
|
||||||
|
},
|
||||||
|
http.StatusOK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slackCompatibleActor) IsAsync() bool { return false }
|
||||||
|
|
||||||
|
func (slackCompatibleActor) Name() string { return "slackhook" }
|
||||||
|
|
||||||
|
func (s slackCompatibleActor) Validate(tplValidator plugins.TemplateValidatorFunc, attrs *plugins.FieldCollection) (err error) {
|
||||||
|
if err = s.ValidateRequireNonEmpty(attrs, "hook_url", "text"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.ValidateRequireValidTemplate(tplValidator, attrs, "text")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slackCompatibleActor) fixHookURL(hookURL string) string {
|
||||||
|
if strings.HasPrefix(hookURL, "https://discord.com/api/webhooks/") && !strings.HasSuffix(hookURL, "/slack") {
|
||||||
|
hookURL = strings.Join([]string{
|
||||||
|
strings.TrimRight(hookURL, "/"),
|
||||||
|
"slack",
|
||||||
|
}, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (slackCompatibleActor) register(args plugins.RegistrationArguments) {
|
||||||
|
args.RegisterActor("slackhook", func() plugins.Actor { return &slackCompatibleActor{} })
|
||||||
|
|
||||||
|
args.RegisterActorDocumentation(plugins.ActionDocumentation{
|
||||||
|
Description: "Sends a message to a Slack(-compatible) Web-hook",
|
||||||
|
Name: "Slack Message-Webhook",
|
||||||
|
Type: "slackhook",
|
||||||
|
|
||||||
|
Fields: []plugins.ActionDocumentationField{
|
||||||
|
{
|
||||||
|
Description: "URL to send the POST request to",
|
||||||
|
Key: "hook_url",
|
||||||
|
Name: "Hook URL",
|
||||||
|
Optional: false,
|
||||||
|
SupportTemplate: false,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "Text to send to the web-hook",
|
||||||
|
Key: "text",
|
||||||
|
Name: "Message",
|
||||||
|
Optional: false,
|
||||||
|
SupportTemplate: true,
|
||||||
|
Type: plugins.ActionDocumentationFieldTypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
71
plugins/actorkit.go
Normal file
71
plugins/actorkit.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ActorKit contains some common validation functions to be used
|
||||||
|
// when implementing actors
|
||||||
|
ActorKit struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateRequireNonEmpty checks whether the fields are gettable
|
||||||
|
// (not returning ErrValueNotSet) and does not contain zero value
|
||||||
|
// recognized by reflect (to just check whether the field is set
|
||||||
|
// but allow zero values use HasAll on the FieldCollection)
|
||||||
|
func (ActorKit) ValidateRequireNonEmpty(attrs *FieldCollection, fields ...string) error {
|
||||||
|
for _, field := range fields {
|
||||||
|
v, err := attrs.Any(field)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "getting field %s", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.ValueOf(v).IsZero() {
|
||||||
|
return errors.Errorf("field %s has zero-value", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRequireValidTemplate checks whether fields are gettable
|
||||||
|
// as strings and do have a template which validates (this does not
|
||||||
|
// check for empty strings as an empty template is indeed valid)
|
||||||
|
func (ActorKit) ValidateRequireValidTemplate(tplValidator TemplateValidatorFunc, attrs *FieldCollection, fields ...string) error {
|
||||||
|
for _, field := range fields {
|
||||||
|
v, err := attrs.String(field)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "getting string field %s", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tplValidator(v); err != nil {
|
||||||
|
return errors.Wrapf(err, "validaging template field %s", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRequireValidTemplateIfSet checks whether the field is
|
||||||
|
// either not set or a valid template (this does not
|
||||||
|
// check for empty strings as an empty template is indeed valid)
|
||||||
|
func (ActorKit) ValidateRequireValidTemplateIfSet(tplValidator TemplateValidatorFunc, attrs *FieldCollection, fields ...string) error {
|
||||||
|
for _, field := range fields {
|
||||||
|
v, err := attrs.String(field)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrValueNotSet) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return errors.Wrapf(err, "getting string field %s", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tplValidator(v); err != nil {
|
||||||
|
return errors.Wrapf(err, "validaging template field %s", field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
27
plugins/actorkit_test.go
Normal file
27
plugins/actorkit_test.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateRequireNonEmpty(t *testing.T) {
|
||||||
|
attrs := FieldCollectionFromData(map[string]any{
|
||||||
|
"str": "",
|
||||||
|
"str_v": "valid",
|
||||||
|
"int": 0,
|
||||||
|
"int_v": 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, field := range []string{"int", "str"} {
|
||||||
|
errUnset := ActorKit{}.ValidateRequireNonEmpty(attrs, strings.Join([]string{field, "unset"}, "_"))
|
||||||
|
errInval := ActorKit{}.ValidateRequireNonEmpty(attrs, field)
|
||||||
|
errValid := ActorKit{}.ValidateRequireNonEmpty(attrs, strings.Join([]string{field, "v"}, "_"))
|
||||||
|
|
||||||
|
assert.Error(t, errUnset)
|
||||||
|
assert.Error(t, errInval)
|
||||||
|
assert.NoError(t, errValid)
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkdetector"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/linkprotect"
|
||||||
logActor "github.com/Luzifer/twitch-bot/v3/internal/actors/log"
|
logActor "github.com/Luzifer/twitch-bot/v3/internal/actors/log"
|
||||||
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/messagehook"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/modchannel"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/nuke"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/nuke"
|
||||||
"github.com/Luzifer/twitch-bot/v3/internal/actors/punish"
|
"github.com/Luzifer/twitch-bot/v3/internal/actors/punish"
|
||||||
|
@ -69,6 +70,7 @@ var (
|
||||||
linkdetector.Register,
|
linkdetector.Register,
|
||||||
linkprotect.Register,
|
linkprotect.Register,
|
||||||
logActor.Register,
|
logActor.Register,
|
||||||
|
messagehook.Register,
|
||||||
modchannel.Register,
|
modchannel.Register,
|
||||||
nuke.Register,
|
nuke.Register,
|
||||||
punish.Register,
|
punish.Register,
|
||||||
|
|
Loading…
Reference in a new issue