Initial import with modified Dockerfile for env-based config generation
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
package slack
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/slack-go/slack"
|
||||
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/config"
|
||||
"github.com/vincentc-afk/gitea-notification-hub/internal/event"
|
||||
)
|
||||
|
||||
// Notifier implements notifier.Notifier and identity.SlackLookup using Slack API
|
||||
type Notifier struct {
|
||||
client *slack.Client
|
||||
defaultChannel string
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// New creates a new Slack notifier
|
||||
func New(cfg *config.SlackConfig, logger zerolog.Logger) *Notifier {
|
||||
client := slack.New(cfg.BotToken)
|
||||
|
||||
return &Notifier{
|
||||
client: client,
|
||||
defaultChannel: cfg.DefaultChannel,
|
||||
logger: logger.With().Str("component", "slack-notifier").Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// LookupSlackIDByEmail finds a Slack user ID by email address
|
||||
// This implements identity.SlackLookup interface
|
||||
func (n *Notifier) LookupSlackIDByEmail(ctx context.Context, email string) (slackID, slackName string, err error) {
|
||||
user, err := n.client.GetUserByEmailContext(ctx, email)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("looking up Slack user by email %s: %w", email, err)
|
||||
}
|
||||
|
||||
return user.ID, user.RealName, nil
|
||||
}
|
||||
|
||||
// SendDirect sends a direct message to a Slack user
|
||||
func (n *Notifier) SendDirect(ctx context.Context, userID string, msg *event.Notification) error {
|
||||
// Open a DM channel with the user
|
||||
channel, _, _, err := n.client.OpenConversationContext(ctx, &slack.OpenConversationParameters{
|
||||
Users: []string{userID},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening DM channel: %w", err)
|
||||
}
|
||||
|
||||
// Build message blocks
|
||||
blocks := n.buildMessageBlocks(msg)
|
||||
|
||||
// Send message
|
||||
_, _, err = n.client.PostMessageContext(ctx,
|
||||
channel.ID,
|
||||
slack.MsgOptionBlocks(blocks...),
|
||||
slack.MsgOptionText(msg.Message, false), // Fallback text
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending DM: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendChannel sends a message to a Slack channel
|
||||
func (n *Notifier) SendChannel(ctx context.Context, channel string, msg *event.Notification) error {
|
||||
if channel == "" {
|
||||
channel = n.defaultChannel
|
||||
}
|
||||
|
||||
// Build message blocks
|
||||
blocks := n.buildMessageBlocks(msg)
|
||||
|
||||
// Send message
|
||||
_, _, err := n.client.PostMessageContext(ctx,
|
||||
channel,
|
||||
slack.MsgOptionBlocks(blocks...),
|
||||
slack.MsgOptionText(msg.Message, false), // Fallback text
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending channel message: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildMessageBlocks creates Slack Block Kit message blocks
|
||||
func (n *Notifier) buildMessageBlocks(msg *event.Notification) []slack.Block {
|
||||
e := msg.Event
|
||||
|
||||
// Header with emoji based on event type
|
||||
emoji := n.getEventEmoji(e.Type)
|
||||
headerText := fmt.Sprintf("%s %s", emoji, n.getEventTitle(e, msg.Reason))
|
||||
|
||||
headerBlock := slack.NewSectionBlock(
|
||||
slack.NewTextBlockObject(slack.MarkdownType, headerText, false, false),
|
||||
nil, nil,
|
||||
)
|
||||
|
||||
// Repository and PR/Issue info
|
||||
contextElements := []slack.MixedElement{
|
||||
slack.NewTextBlockObject(slack.MarkdownType,
|
||||
fmt.Sprintf("*<%s|%s>*", e.RepoURL, e.RepoFullName), false, false),
|
||||
}
|
||||
|
||||
if e.Number > 0 {
|
||||
contextElements = append(contextElements,
|
||||
slack.NewTextBlockObject(slack.MarkdownType,
|
||||
fmt.Sprintf("<%s|#%d %s>", e.URL, e.Number, truncate(e.Title, 50)), false, false))
|
||||
}
|
||||
|
||||
contextBlock := slack.NewContextBlock("", contextElements...)
|
||||
|
||||
blocks := []slack.Block{headerBlock, contextBlock}
|
||||
|
||||
// Add comment body if present
|
||||
if e.CommentBody != "" {
|
||||
commentText := truncate(e.CommentBody, 300)
|
||||
// Replace @mentions with Slack-style if we have them
|
||||
commentBlock := slack.NewSectionBlock(
|
||||
slack.NewTextBlockObject(slack.MarkdownType,
|
||||
fmt.Sprintf(">>> %s", commentText), false, false),
|
||||
nil, nil,
|
||||
)
|
||||
blocks = append(blocks, commentBlock)
|
||||
}
|
||||
|
||||
// Add review state if present
|
||||
if e.ReviewState != "" {
|
||||
reviewEmoji := n.getReviewStateEmoji(e.ReviewState)
|
||||
reviewBlock := slack.NewContextBlock("",
|
||||
slack.NewTextBlockObject(slack.MarkdownType,
|
||||
fmt.Sprintf("%s Review: *%s*", reviewEmoji, e.ReviewState), false, false))
|
||||
blocks = append(blocks, reviewBlock)
|
||||
}
|
||||
|
||||
// Add commits list if present (for synchronize events)
|
||||
if len(e.Commits) > 0 {
|
||||
var commitLines []string
|
||||
maxCommits := 5 // Show max 5 commits
|
||||
for i, c := range e.Commits {
|
||||
if i >= maxCommits {
|
||||
commitLines = append(commitLines, fmt.Sprintf("_... and %d more commits_", len(e.Commits)-maxCommits))
|
||||
break
|
||||
}
|
||||
commitLines = append(commitLines, fmt.Sprintf("• `%s` %s", c.SHA, truncate(c.Message, 60)))
|
||||
}
|
||||
commitsText := strings.Join(commitLines, "\n")
|
||||
commitsBlock := slack.NewSectionBlock(
|
||||
slack.NewTextBlockObject(slack.MarkdownType, commitsText, false, false),
|
||||
nil, nil,
|
||||
)
|
||||
blocks = append(blocks, commitsBlock)
|
||||
}
|
||||
|
||||
// Add action button
|
||||
buttonBlock := slack.NewActionBlock("actions",
|
||||
slack.NewButtonBlockElement("view", e.URL,
|
||||
slack.NewTextBlockObject(slack.PlainTextType, "View in Gitea", false, false)).
|
||||
WithURL(e.URL),
|
||||
)
|
||||
blocks = append(blocks, buttonBlock)
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
// getEventEmoji returns an emoji for the event type
|
||||
func (n *Notifier) getEventEmoji(eventType event.Type) string {
|
||||
switch eventType {
|
||||
case event.TypePROpened:
|
||||
return ":git-pull-request:"
|
||||
case event.TypePRClosed:
|
||||
return ":git-pull-request-closed:"
|
||||
case event.TypePRMerged:
|
||||
return ":git-merge:"
|
||||
case event.TypePRAssigned:
|
||||
return ":bust_in_silhouette:"
|
||||
case event.TypePRReviewRequested:
|
||||
return ":eyes:"
|
||||
case event.TypePRReviewed:
|
||||
return ":memo:"
|
||||
case event.TypePRCommented:
|
||||
return ":speech_balloon:"
|
||||
case event.TypePRSynchronized:
|
||||
return ":arrows_counterclockwise:"
|
||||
case event.TypeIssueOpened:
|
||||
return ":issue-opened:"
|
||||
case event.TypeIssueClosed:
|
||||
return ":issue-closed:"
|
||||
case event.TypeIssueCommented:
|
||||
return ":speech_balloon:"
|
||||
default:
|
||||
return ":bell:"
|
||||
}
|
||||
}
|
||||
|
||||
// getEventTitle returns a human-readable title for the event
|
||||
func (n *Notifier) getEventTitle(e *event.Event, reason event.NotificationReason) string {
|
||||
actorName := e.Actor.GiteaUsername
|
||||
if e.Actor.FullName != "" {
|
||||
actorName = e.Actor.FullName
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case event.TypePROpened:
|
||||
if reason == event.ReasonAssignee {
|
||||
return fmt.Sprintf("*%s* assigned you to a pull request", actorName)
|
||||
}
|
||||
if reason == event.ReasonReviewer {
|
||||
return fmt.Sprintf("*%s* requested your review on a pull request", actorName)
|
||||
}
|
||||
return fmt.Sprintf("*%s* opened a pull request", actorName)
|
||||
|
||||
case event.TypePRClosed:
|
||||
return fmt.Sprintf("*%s* closed your pull request", actorName)
|
||||
|
||||
case event.TypePRMerged:
|
||||
return fmt.Sprintf("*%s* merged your pull request", actorName)
|
||||
|
||||
case event.TypePRAssigned:
|
||||
return fmt.Sprintf("*%s* assigned you to a pull request", actorName)
|
||||
|
||||
case event.TypePRReviewRequested:
|
||||
return fmt.Sprintf("*%s* requested your review", actorName)
|
||||
|
||||
case event.TypePRReviewed:
|
||||
return fmt.Sprintf("*%s* reviewed your pull request", actorName)
|
||||
|
||||
case event.TypePRCommented:
|
||||
if reason == event.ReasonMention {
|
||||
return fmt.Sprintf("*%s* mentioned you in a PR comment", actorName)
|
||||
}
|
||||
return fmt.Sprintf("*%s* commented on your pull request", actorName)
|
||||
|
||||
case event.TypePRSynchronized:
|
||||
commitCount := len(e.Commits)
|
||||
if commitCount == 1 {
|
||||
return fmt.Sprintf("*%s* pushed 1 new commit to PR #%d", actorName, e.Number)
|
||||
}
|
||||
return fmt.Sprintf("*%s* pushed %d new commits to PR #%d", actorName, commitCount, e.Number)
|
||||
|
||||
case event.TypeIssueOpened:
|
||||
if reason == event.ReasonAssignee {
|
||||
return fmt.Sprintf("*%s* assigned you to an issue", actorName)
|
||||
}
|
||||
return fmt.Sprintf("*%s* opened an issue", actorName)
|
||||
|
||||
case event.TypeIssueClosed:
|
||||
return fmt.Sprintf("*%s* closed your issue", actorName)
|
||||
|
||||
case event.TypeIssueCommented:
|
||||
if reason == event.ReasonMention {
|
||||
return fmt.Sprintf("*%s* mentioned you in an issue comment", actorName)
|
||||
}
|
||||
return fmt.Sprintf("*%s* commented on your issue", actorName)
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("New notification from *%s*", actorName)
|
||||
}
|
||||
}
|
||||
|
||||
// getReviewStateEmoji returns an emoji for the review state
|
||||
func (n *Notifier) getReviewStateEmoji(state string) string {
|
||||
switch strings.ToUpper(state) {
|
||||
case "APPROVED":
|
||||
return ":white_check_mark:"
|
||||
case "CHANGES_REQUESTED":
|
||||
return ":x:"
|
||||
case "COMMENT":
|
||||
return ":speech_balloon:"
|
||||
default:
|
||||
return ":memo:"
|
||||
}
|
||||
}
|
||||
|
||||
// truncate truncates a string to the specified length
|
||||
func truncate(s string, maxLen int) string {
|
||||
// Remove newlines for preview
|
||||
s = strings.ReplaceAll(s, "\n", " ")
|
||||
s = strings.ReplaceAll(s, "\r", "")
|
||||
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user