multiple discord senders

This commit is contained in:
2026-01-20 20:58:18 +01:00
parent 76933a04d8
commit 53e6eecb3f
9 changed files with 100 additions and 69 deletions

View File

@@ -1,16 +1,19 @@
WIKI_BASE_URL= WIKI_BASE_URL=
WIKI_TOKEN= WIKI_TOKEN=
WIKI_CONTENT_LIMIT=10 WIKI_CONTENT_LIMIT=10
WIKI_DISCORD_WEBHOOK=
REDMINE_BASE_URL= REDMINE_BASE_URL=
REDMINE_KEY= REDMINE_KEY=
REDMINE_CONTENT_LIMIT=10 REDMINE_CONTENT_LIMIT=10
REDMINE_DISCORD_WEBHOOK=
GITEA_TOKEN= GITEA_TOKEN=
GITEA_BASE_URL= GITEA_BASE_URL=
GITEA_REPOS=org/repo1,org/repo2 GITEA_REPOS=org/repo1,org/repo2
GITEA_CONTENT_LIMIT=10 GITEA_CONTENT_LIMIT=10
GITEA_DISCORD_WEBHOOK=
DISCORD_WEBHOOK= DISCORD_WEBHOOK=

View File

@@ -13,13 +13,16 @@ type Config struct {
WikiBaseURL string WikiBaseURL string
WikiToken string WikiToken string
WikiContentLimit int WikiContentLimit int
WikiDiscordWebhook string
RedmineBaseURL string RedmineBaseURL string
RedmineKey string RedmineKey string
RedmineContentLimit int RedmineContentLimit int
RedmineDiscordWebhook string
GiteaToken string GiteaToken string
GiteaBaseURL string GiteaBaseURL string
GiteaRepos []string GiteaRepos []string
GiteaContentLimit int GiteaContentLimit int
GiteaDiscordWebhook string
DiscordWebhook string DiscordWebhook string
DiscordFake bool DiscordFake bool
Interval time.Duration Interval time.Duration
@@ -40,6 +43,6 @@ func envToInteger(key string, defaultValue int) int {
func loadEnv() { func loadEnv() {
if err := godotenv.Load(); err != nil { if err := godotenv.Load(); err != nil {
log.Println("Warning: .env file not found, using environment variables") log.Println("[BOOT] .env file not found")
} }
} }

View File

@@ -24,8 +24,8 @@ func (f *GiteaFetcher) Name() string {
return "Gitea" return "Gitea"
} }
func (f *GiteaFetcher) Fetch() []Entry { func (f *GiteaFetcher) Fetch() []Message {
var entries []Entry var messages []Message
for _, repo := range f.Repos { for _, repo := range f.Repos {
repo = strings.TrimSpace(repo) repo = strings.TrimSpace(repo)
@@ -54,18 +54,19 @@ func (f *GiteaFetcher) Fetch() []Entry {
for _, content := range response { for _, content := range response {
cacheKey := "gitea_commit_" + url.QueryEscape(f.BaseURL+repo+"_"+content.Sha) cacheKey := "gitea_commit_" + url.QueryEscape(f.BaseURL+repo+"_"+content.Sha)
entry := f.TryCreateEntry( message := f.TryCreateMessage(
"gitea",
cacheKey, cacheKey,
content.Sha, content.Sha,
fmt.Sprintf("📝 [%s] - (%s) %s", f.Name(), repo, content.Commit.Message), fmt.Sprintf("📝 [%s] - (%s) %s", f.Name(), repo, content.Commit.Message),
content.HTMLURL, content.HTMLURL,
) )
if entry != nil { if message != nil {
entries = append(entries, *entry) messages = append(messages, *message)
} }
} }
} }
return entries return messages
} }

View File

@@ -7,16 +7,10 @@ import (
"net/http" "net/http"
) )
// Entry represents a single fetched item.
type Entry struct {
Title string
URL string
}
// Fetcher is the interface for a fetcher. // Fetcher is the interface for a fetcher.
type Fetcher interface { type Fetcher interface {
Name() string Name() string
Fetch() []Entry Fetch() []Message
} }
// BaseFetcher contains common fields for all fetchers. // BaseFetcher contains common fields for all fetchers.
@@ -27,10 +21,14 @@ type BaseFetcher struct {
ContentLimit int ContentLimit int
} }
// TryCreateEntry checks the cache and creates an Entry if the value has changed. // TryCreateMessage checks the cache and creates a Message if the value has changed.
func (b *BaseFetcher) TryCreateEntry(cacheKey, cacheValue, title, url string) *Entry { func (b *BaseFetcher) TryCreateMessage(channel, cacheKey, cacheValue, title, url string) *Message {
if b.Cache.TryUpdate(cacheKey, cacheValue) { if b.Cache.TryUpdate(cacheKey, cacheValue) {
return &Entry{Title: title, URL: url} return &Message{
Channel: channel,
Title: title,
URL: url,
}
} }
return nil return nil
} }

View File

@@ -21,8 +21,8 @@ func (f *RedmineFetcher) Name() string {
return "Redmine" return "Redmine"
} }
func (f *RedmineFetcher) Fetch() []Entry { func (f *RedmineFetcher) Fetch() []Message {
var entries []Entry var messages []Message
path := fmt.Sprintf("/issues.json?limit=%d&sort=updated_on:desc", f.ContentLimit) path := fmt.Sprintf("/issues.json?limit=%d&sort=updated_on:desc", f.ContentLimit)
req := FetcherRequest{ req := FetcherRequest{
BaseURL: f.BaseURL, BaseURL: f.BaseURL,
@@ -43,16 +43,17 @@ func (f *RedmineFetcher) Fetch() []Entry {
} }
for _, content := range response.Issues { for _, content := range response.Issues {
entry := f.TryCreateEntry( message := f.TryCreateMessage(
"redmine",
fmt.Sprintf("redmine_%d", content.ID), fmt.Sprintf("redmine_%d", content.ID),
content.UpdatedOn, content.UpdatedOn,
fmt.Sprintf("🎫 [%s] - #%d %s", f.Name(), content.ID, content.Subject), fmt.Sprintf("🎫 [%s] - #%d %s", f.Name(), content.ID, content.Subject),
fmt.Sprintf("%s/issues/%d", f.BaseURL, content.ID), fmt.Sprintf("%s/issues/%d", f.BaseURL, content.ID),
) )
if entry != nil { if message != nil {
entries = append(entries, *entry) messages = append(messages, *message)
} }
} }
return entries return messages
} }

View File

@@ -25,8 +25,8 @@ func (f *WikiFetcher) Name() string {
return "WikiJS" return "WikiJS"
} }
func (f *WikiFetcher) Fetch() []Entry { func (f *WikiFetcher) Fetch() []Message {
var entries []Entry var messages []Message
query := fmt.Sprintf(`{"query":"{ pages { list(orderBy: UPDATED, orderByDirection: DESC, limit: %d){ path, updatedAt, title }}}"}`, f.ContentLimit) query := fmt.Sprintf(`{"query":"{ pages { list(orderBy: UPDATED, orderByDirection: DESC, limit: %d){ path, updatedAt, title }}}"}`, f.ContentLimit)
req := FetcherRequest{ req := FetcherRequest{
@@ -50,17 +50,18 @@ func (f *WikiFetcher) Fetch() []Entry {
} }
for _, content := range response.Data.Pages.List { for _, content := range response.Data.Pages.List {
entry := f.TryCreateEntry( message := f.TryCreateMessage(
"wiki",
"wiki_"+content.Path, "wiki_"+content.Path,
content.UpdatedAt, content.UpdatedAt,
fmt.Sprintf("📖 [%s] - %s", f.Name(), content.Title), fmt.Sprintf("📖 [%s] - %s", f.Name(), content.Title),
fmt.Sprintf("%s/%s", f.BaseURL, content.Path), fmt.Sprintf("%s/%s", f.BaseURL, content.Path),
) )
if entry != nil { if message != nil {
entries = append(entries, *entry) messages = append(messages, *message)
} }
} }
return entries return messages
} }

13
lib/message.go Normal file
View File

@@ -0,0 +1,13 @@
package lib
import "fmt"
type Message struct {
Channel string
Title string
URL string
}
func (m Message) ToDiscord() string {
return fmt.Sprintf("[%s](%s)", m.Title, m.URL)
}

View File

@@ -1,7 +1,6 @@
package lib package lib
import ( import (
"fmt"
"log" "log"
"os" "os"
"strings" "strings"
@@ -62,34 +61,50 @@ func getCache() Cache {
return cache return cache
} }
func getDiscordSender(config Config) DiscordSender { func getMessages(fetchers []Fetcher) []Message {
return DiscordSender{ messages := []Message{}
Webhook: config.DiscordWebhook,
Fake: config.DiscordFake,
}
}
func getMessages(fetchers []Fetcher) []string {
messages := []string{}
for _, fetcher := range fetchers { for _, fetcher := range fetchers {
entries := fetcher.Fetch() fetchedMessages := fetcher.Fetch()
for _, entry := range entries { for _, message := range fetchedMessages {
messages = append(messages, fmt.Sprintf("[%s](%s)", entry.Title, entry.URL)) messages = append(messages, message)
} }
} }
return messages return messages
} }
func sendMessageToDiscord(sender DiscordSender, message Message) {
log.Printf("[SCHEDULED] [SEND] (%s) %s\n", message.Channel, message.ToDiscord())
if sender.Fake {
return
}
sender.Send(message.ToDiscord())
}
func Runner() { func Runner() {
loadEnv() loadEnv()
config := getConfig() config := getConfig()
cache := getCache() cache := getCache()
discord_sender := getDiscordSender(config)
fetchers := getFetchers(&config, &cache) fetchers := getFetchers(&config, &cache)
default_discord_sender := NewDiscordSender(config.DiscordWebhook, config.DiscordFake)
wiki_discord_sender := NewDiscordSender(config.WikiDiscordWebhook, config.DiscordFake)
redmine_discord_sender := NewDiscordSender(config.RedmineDiscordWebhook, config.DiscordFake)
gita_discord_sender := NewDiscordSender(config.GiteaDiscordWebhook, config.DiscordFake)
for { for {
log.Println("Run updater...") log.Println("[SCHEDULED] Run updater...")
messages := getMessages(fetchers) messages := getMessages(fetchers)
discord_sender.SendBatch(messages) for _, message := range messages {
switch message.Channel {
case "wiki":
sendMessageToDiscord(wiki_discord_sender, message)
case "redmine":
sendMessageToDiscord(redmine_discord_sender, message)
case "gitea":
sendMessageToDiscord(gita_discord_sender, message)
default:
sendMessageToDiscord(default_discord_sender, message)
}
}
cache.Save() cache.Save()
time.Sleep(config.Interval) time.Sleep(config.Interval)
} }

View File

@@ -3,10 +3,16 @@ package lib
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
) )
func NewDiscordSender(webhook string, fake bool) DiscordSender {
return DiscordSender{
Webhook: webhook,
Fake: fake,
}
}
type DiscordSender struct { type DiscordSender struct {
Webhook string Webhook string
Fake bool Fake bool
@@ -19,13 +25,3 @@ func (d DiscordSender) Send(msg string) {
} }
http.Post(d.Webhook, "application/json", bytes.NewBuffer(b)) http.Post(d.Webhook, "application/json", bytes.NewBuffer(b))
} }
func (d DiscordSender) SendBatch(msgs []string) {
for _, msg := range msgs {
log.Println("Sending to Discord:", msg)
if d.Fake {
continue
}
d.Send(msg)
}
}