initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
cache.json
|
||||
updater
|
||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -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"]
|
||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
updater:
|
||||
build: .
|
||||
container_name: updater-app
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./:/app
|
||||
env_file:
|
||||
- .env
|
||||
12
env-example
Normal file
12
env-example
Normal file
@@ -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
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module updater
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -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=
|
||||
80
lib/cache.go
Normal file
80
lib/cache.go
Normal file
@@ -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)
|
||||
}
|
||||
15
lib/config.go
Normal file
15
lib/config.go
Normal file
@@ -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
|
||||
}
|
||||
44
lib/fetcher.gitea.go
Normal file
44
lib/fetcher.gitea.go
Normal file
@@ -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 ""
|
||||
}
|
||||
43
lib/fetcher.redmine.go
Normal file
43
lib/fetcher.redmine.go
Normal file
@@ -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 ""
|
||||
}
|
||||
48
lib/fetcher.wikijs.go
Normal file
48
lib/fetcher.wikijs.go
Normal file
@@ -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 ""
|
||||
}
|
||||
69
lib/runner.go
Normal file
69
lib/runner.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
18
lib/sender.discord.go
Normal file
18
lib/sender.discord.go
Normal file
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user