179 lines
3.8 KiB
Go
179 lines
3.8 KiB
Go
/* SPDX-License-Identifier: GPL-2.0
|
|
*
|
|
* Copyright (C) 2021 Jason A. Donenfeld. All Rights Reserved.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"container/list"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"path"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type CgitCommit struct {
|
|
Text string `xml:",chardata"`
|
|
Title string `xml:"title"`
|
|
Updated string `xml:"updated"`
|
|
Author struct {
|
|
Text string `xml:",chardata"`
|
|
Name string `xml:"name"`
|
|
Email string `xml:"email"`
|
|
} `xml:"author"`
|
|
Published string `xml:"published"`
|
|
Link struct {
|
|
Text string `xml:",chardata"`
|
|
Rel string `xml:"rel,attr"`
|
|
Type string `xml:"type,attr"`
|
|
Href string `xml:"href,attr"`
|
|
} `xml:"link"`
|
|
ID string `xml:"id"`
|
|
Content []struct {
|
|
Text string `xml:",chardata"`
|
|
Type string `xml:"type,attr"`
|
|
Div struct {
|
|
Text string `xml:",chardata"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
Pre string `xml:"pre"`
|
|
} `xml:"div"`
|
|
} `xml:"content"`
|
|
}
|
|
|
|
type cgitFeed struct {
|
|
XMLName xml.Name `xml:"feed"`
|
|
Text string `xml:",chardata"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
Title string `xml:"title"`
|
|
Subtitle string `xml:"subtitle"`
|
|
Link struct {
|
|
Text string `xml:",chardata"`
|
|
Rel string `xml:"rel,attr"`
|
|
Type string `xml:"type,attr"`
|
|
Href string `xml:"href,attr"`
|
|
} `xml:"link"`
|
|
Entry []CgitCommit `xml:"entry"`
|
|
}
|
|
|
|
type cgitFeedStatus struct {
|
|
repo string
|
|
title string
|
|
seenEntries map[[32]byte]bool
|
|
orderedEntries list.List
|
|
}
|
|
|
|
type CgitFeedMonitorer struct {
|
|
feeds []*cgitFeedStatus
|
|
updates chan CgitFeedUpdate
|
|
ticker *time.Ticker
|
|
wg sync.WaitGroup
|
|
mu sync.Mutex
|
|
}
|
|
|
|
type CgitFeedUpdate struct {
|
|
RepoTitle string
|
|
Commit *CgitCommit
|
|
}
|
|
|
|
func NewCgitFeedMonitorer(pollInterval time.Duration) *CgitFeedMonitorer {
|
|
fm := &CgitFeedMonitorer{
|
|
updates: make(chan CgitFeedUpdate, 32),
|
|
ticker: time.NewTicker(pollInterval),
|
|
}
|
|
fm.wg.Add(1)
|
|
go func() {
|
|
defer fm.wg.Done()
|
|
for range fm.ticker.C {
|
|
fm.mu.Lock()
|
|
for _, feed := range fm.feeds {
|
|
fm.wg.Add(1)
|
|
go func(feed *cgitFeedStatus) {
|
|
defer fm.wg.Done()
|
|
fm.updateFeed(feed, true)
|
|
}(feed)
|
|
}
|
|
fm.mu.Unlock()
|
|
}
|
|
}()
|
|
return fm
|
|
}
|
|
|
|
func (fm *CgitFeedMonitorer) Stop() {
|
|
fm.ticker.Stop()
|
|
fm.wg.Wait()
|
|
close(fm.updates)
|
|
}
|
|
|
|
func (fm *CgitFeedMonitorer) Updates() <-chan CgitFeedUpdate {
|
|
return fm.updates
|
|
}
|
|
|
|
func (fm *CgitFeedMonitorer) AddFeed(repo string) {
|
|
go func() {
|
|
status := &cgitFeedStatus{
|
|
repo: repo,
|
|
title: path.Base(repo),
|
|
seenEntries: make(map[[32]byte]bool, 1024),
|
|
}
|
|
fm.updateFeed(status, false)
|
|
fm.mu.Lock()
|
|
fm.feeds = append(fm.feeds, status)
|
|
fm.mu.Unlock()
|
|
}()
|
|
}
|
|
|
|
func (fm *CgitFeedMonitorer) updateFeed(fs *cgitFeedStatus, alert bool) {
|
|
feed, err := fs.fetchFeed()
|
|
if err != nil {
|
|
log.Printf("Unable to fetch commits for %q: %v", fs.title, err)
|
|
return
|
|
}
|
|
for i := len(feed.Entry) - 1; i >= 0; i-- {
|
|
commit := &feed.Entry[i]
|
|
var commitID [32]byte
|
|
if hex.DecodedLen(len(commit.ID)) > len(commitID) {
|
|
continue
|
|
}
|
|
n, err := hex.Decode(commitID[:], []byte(commit.ID))
|
|
if err != nil || n < 20 {
|
|
continue
|
|
}
|
|
if _, ok := fs.seenEntries[commitID]; ok {
|
|
continue
|
|
}
|
|
fs.seenEntries[commitID] = true
|
|
fs.orderedEntries.PushBack(commitID)
|
|
for len(fs.seenEntries) > 1024 {
|
|
first := fs.orderedEntries.Front()
|
|
delete(fs.seenEntries, first.Value.([32]byte))
|
|
fs.orderedEntries.Remove(first)
|
|
}
|
|
if alert {
|
|
fm.updates <- CgitFeedUpdate{fs.title, commit}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (fs *cgitFeedStatus) fetchFeed() (*cgitFeed, error) {
|
|
resp, err := http.Get(fs.repo + "/atom/")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bytes, err := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var feed cgitFeed
|
|
err = xml.Unmarshal(bytes, &feed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &feed, nil
|
|
}
|