commit 197a01440c3652f1f62671a186e694a8c25c4c6d Author: Zsolt Tasnadi Date: Sun Jan 18 21:40:51 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..553034c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +cache.json +updater diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a6208e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Use an official Go runtime as a parent image +FROM golang:1.25.5-alpine + +# Set the working directory in the container +WORKDIR /app + +# Copy the go mod and sum files to download dependencies +COPY go.mod go.sum ./ + +# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed +RUN go mod download + +# Copy the source code from the current directory to the working directory inside the container +COPY . . + +# Command to run the application +CMD ["go", "run", "main.go"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..af11bb6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + updater: + build: . + container_name: updater-app + restart: unless-stopped + volumes: + - ./:/app + env_file: + - .env diff --git a/env-example b/env-example new file mode 100644 index 0000000..5a96df4 --- /dev/null +++ b/env-example @@ -0,0 +1,12 @@ +WIKI_BASE_URL= +WIKI_TOKEN= + +REDMINE_BASE_URL= +REDMINE_KEY= + +GITEA_TOKEN= +GITEA_BASE_URL= +GITEA_REPOS=org/repo1,org/repo2 + +DISCORD_WEBHOOK= +INTERVAL_MINUTES=5 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..421e7b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module updater + +go 1.25.5 + +require github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/lib/cache.go b/lib/cache.go new file mode 100644 index 0000000..4df891e --- /dev/null +++ b/lib/cache.go @@ -0,0 +1,80 @@ +package lib + +import ( + "encoding/json" + "os" +) + +const cacheFile = "cache.json" + +type CacheItem struct { + Fetcher string + Value string +} + +type Cache struct { + Items []CacheItem +} + +func (c *Cache) Update(fetcher string, value string) { + for i := range c.Items { + if c.Items[i].Fetcher == fetcher { + c.Items[i].Value = value + return + } + } + c.Items = append(c.Items, CacheItem{ + Fetcher: fetcher, + Value: value, + }) +} + +func (c *Cache) Get(fetcher string) (string, bool) { + for _, item := range c.Items { + if item.Fetcher == fetcher { + return item.Value, true + } + } + return "", false +} + +func (c *Cache) Has(fetcher string) bool { + _, found := c.Get(fetcher) + return found +} + +func (c *Cache) IsChanged(fetcher string, value string) bool { + cachedValue, found := c.Get(fetcher) + if !found || cachedValue != value { + return true + } + return false +} + +func (c *Cache) TryUpdate(fetcher string, value string) bool { + if c.IsChanged(fetcher, value) { + c.Update(fetcher, value) + return true + } + return false +} + +func (c *Cache) Load() { + f, err := os.Open(cacheFile) + if err != nil { + return + } + defer f.Close() + + _ = json.NewDecoder(f).Decode(&c.Items) +} + +func (c *Cache) Save() { + f, err := os.Create(cacheFile) + if err != nil { + return + } + defer f.Close() + + _ = json.NewEncoder(f).Encode(c.Items) +} diff --git a/lib/config.go b/lib/config.go new file mode 100644 index 0000000..2dbb9d8 --- /dev/null +++ b/lib/config.go @@ -0,0 +1,15 @@ +package lib + +import "time" + +type Config struct { + WikiBaseURL string + WikiToken string + RedmineBaseURL string + RedmineKey string + GiteaToken string + GiteaBaseURL string + GiteaRepos []string + Webhook string + Interval time.Duration +} diff --git a/lib/fetcher.gitea.go b/lib/fetcher.gitea.go new file mode 100644 index 0000000..d434cb6 --- /dev/null +++ b/lib/fetcher.gitea.go @@ -0,0 +1,44 @@ +package lib + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +type GiteaFetcher struct { + URL string + Token string + Cache *Cache +} + +func (f GiteaFetcher) Fetch() string { + req, _ := http.NewRequest("GET", f.URL, nil) + if f.Token != "" { + req.Header.Set("Authorization", "token "+f.Token) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return "" + } + defer res.Body.Close() + + var r []struct { + Sha string `json:"sha"` + Commit struct { + Message string `json:"message"` + } `json:"commit"` + HTMLURL string `json:"html_url"` + } + json.NewDecoder(res.Body).Decode(&r) + if len(r) == 0 { + return "" + } + c := r[0] + + if f.Cache.TryUpdate("gitea_"+url.QueryEscape(f.URL), c.Sha) { + return fmt.Sprintf("Gitea: %s <%s>", c.Commit.Message, c.HTMLURL) + } + return "" +} diff --git a/lib/fetcher.redmine.go b/lib/fetcher.redmine.go new file mode 100644 index 0000000..f429c79 --- /dev/null +++ b/lib/fetcher.redmine.go @@ -0,0 +1,43 @@ +package lib + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type RedmineFetcher struct { + Config Config + Cache *Cache +} + +func (f RedmineFetcher) Fetch() string { + redmineURL := fmt.Sprintf("%s/issues.json", f.Config.RedmineBaseURL) + req, _ := http.NewRequest("GET", redmineURL, nil) + req.Header.Set("X-Redmine-API-Key", f.Config.RedmineKey) + res, err := http.DefaultClient.Do(req) + if err != nil { + return "" + } + defer res.Body.Close() + + var r struct { + Issues []struct { + ID int + UpdatedOn string + Subject string + } + } + json.NewDecoder(res.Body).Decode(&r) + + if len(r.Issues) == 0 { + return "" + } + + i := r.Issues[0] + if f.Cache.TryUpdate("redmine", i.UpdatedOn) { + url := fmt.Sprintf("%s/issues/%d", f.Config.RedmineBaseURL, i.ID) + return fmt.Sprintf("Redmine: #%d %s <%s>", i.ID, i.Subject, url) + } + return "" +} diff --git a/lib/fetcher.wikijs.go b/lib/fetcher.wikijs.go new file mode 100644 index 0000000..a18ebf2 --- /dev/null +++ b/lib/fetcher.wikijs.go @@ -0,0 +1,48 @@ +package lib + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type WikiFetcher struct { + Config Config + Cache *Cache +} + +func (f WikiFetcher) Fetch() string { + q := `{"query":"{ pages { list(orderBy: UPDATED, orderByDirection: DESC, limit: 1){ updatedAt title }}}"}` + wikiURL := fmt.Sprintf("%s/graphql", f.Config.WikiBaseURL) + req, _ := http.NewRequest("POST", wikiURL, bytes.NewBuffer([]byte(q))) + req.Header.Set("Authorization", "Bearer "+f.Config.WikiToken) + req.Header.Set("Content-Type", "application/json") + res, err := http.DefaultClient.Do(req) + if err != nil { + return "" + } + defer res.Body.Close() + + var r struct { + Data struct { + Pages struct { + List []struct { + UpdatedAt string + Title string + Path string + } + } + } + } + json.NewDecoder(res.Body).Decode(&r) + if len(r.Data.Pages.List) == 0 { + return "" + } + u := r.Data.Pages.List[0] + if f.Cache.TryUpdate("wiki", u.UpdatedAt) { + url := fmt.Sprintf("%s/%s", f.Config.WikiBaseURL, u.Path) + return fmt.Sprintf("Wiki: %s <%s>", u.Title, url) + } + return "" +} diff --git a/lib/runner.go b/lib/runner.go new file mode 100644 index 0000000..11c0691 --- /dev/null +++ b/lib/runner.go @@ -0,0 +1,69 @@ +package lib + +import ( + "fmt" + "log" + "os" + "strconv" + "strings" + "time" + + "github.com/joho/godotenv" +) + +type Fetcher interface { + Fetch() string +} + +func Runner() { + if err := godotenv.Load(); err != nil { + log.Println("Warning: .env file not found, using environment variables") + } + + intervalMinutes, err := strconv.Atoi(os.Getenv("INTERVAL_MINUTES")) + if err != nil { + intervalMinutes = 5 + } + + config := Config{ + WikiBaseURL: os.Getenv("WIKI_BASE_URL"), + WikiToken: os.Getenv("WIKI_TOKEN"), + RedmineBaseURL: os.Getenv("REDMINE_BASE_URL"), + RedmineKey: os.Getenv("REDMINE_KEY"), + GiteaToken: os.Getenv("GITEA_TOKEN"), + GiteaBaseURL: os.Getenv("GITEA_BASE_URL"), + GiteaRepos: strings.Split(os.Getenv("GITEA_REPOS"), ","), + Webhook: os.Getenv("DISCORD_WEBHOOK"), + Interval: time.Duration(intervalMinutes) * time.Minute, + } + + cache := Cache{} + cache.Load() + discord := DiscordSender{Config: config} + + var fetchers []Fetcher + for _, repo := range config.GiteaRepos { + if repo == "" { + continue + } + giteaURL := fmt.Sprintf("%s/api/v1/repos/%s/commits", config.GiteaBaseURL, strings.TrimSpace(repo)) + fetchers = append(fetchers, &GiteaFetcher{URL: giteaURL, Token: config.GiteaToken, Cache: &cache}) + } + + fetchers = append(fetchers, &WikiFetcher{Config: config, Cache: &cache}) + fetchers = append(fetchers, &RedmineFetcher{Config: config, Cache: &cache}) + + for { + fmt.Println("Update") + + for _, f := range fetchers { + if msg := f.Fetch(); msg != "" { + discord.Send(msg) + } + } + + cache.Save() + + time.Sleep(config.Interval) + } +} diff --git a/lib/sender.discord.go b/lib/sender.discord.go new file mode 100644 index 0000000..85c1c82 --- /dev/null +++ b/lib/sender.discord.go @@ -0,0 +1,18 @@ +package lib + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +type DiscordSender struct { + Config Config +} + +func (d DiscordSender) Send(msg string) { + fmt.Printf("Send to Discord: %s\n", msg) + b, _ := json.Marshal(map[string]string{"content": msg}) + http.Post(d.Config.Webhook, "application/json", bytes.NewBuffer(b)) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b739603 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "updater/lib" +) + +func main() { + lib.Runner() +}