this repo has no description

Initial commit

Eric Davis 76771805

Changed files
+273
cmd
+51
cmd/plcwatch/comparisons.go
···
+
package main
+
+
import (
+
"fmt"
+
"reflect"
+
)
+
+
type CompareResults struct {
+
Title string
+
Description string
+
}
+
+
func compareAlsoKnownAs(old, new []string) *CompareResults {
+
if !reflect.DeepEqual(old, new) {
+
return &CompareResults{
+
"Alias updated",
+
fmt.Sprintf("%+v -> %+v", old, new),
+
}
+
}
+
return nil
+
}
+
+
func compareServices(old, new map[string]PlcService) *CompareResults {
+
if !reflect.DeepEqual(old, new) {
+
return &CompareResults{
+
"Service updated",
+
fmt.Sprintf("%+v -> %+v", old, new),
+
}
+
}
+
return nil
+
}
+
+
func compareRotationKeys(old, new []string) *CompareResults {
+
if !reflect.DeepEqual(old, new) {
+
return &CompareResults{
+
"Rotation key updated",
+
fmt.Sprintf("%+v -> %+v", old, new),
+
}
+
}
+
return nil
+
}
+
+
func compareVerificationMethods(old, new map[string]string) *CompareResults {
+
if !reflect.DeepEqual(old, new) {
+
return &CompareResults{
+
"Verification method updated",
+
fmt.Sprintf("%+v -> %+v", old, new),
+
}
+
}
+
return nil
+
}
+105
cmd/plcwatch/main.go
···
+
package main
+
+
import (
+
"encoding/json"
+
"fmt"
+
"io"
+
"net/http"
+
"time"
+
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
"github.com/labstack/echo/v4"
+
)
+
+
const plcDirectoryHost = "https://plc.directory"
+
+
func rssTimeFormat(raw string) string {
+
t, err := syntax.ParseDatetimeTime(raw)
+
if err != nil {
+
return time.Now().UTC().Format(time.RFC1123Z)
+
}
+
return t.Format(time.RFC1123Z)
+
}
+
+
func main() {
+
e := echo.New()
+
e.GET("/:did", func(c echo.Context) error {
+
did := c.Param("did")
+
+
url := fmt.Sprintf("%s/%s/log/audit", plcDirectoryHost, did)
+
resp, err := http.Get(url)
+
if err != nil {
+
return fmt.Errorf("failed fetching did audit log: %w", err)
+
}
+
defer resp.Body.Close()
+
+
respBytes, err := io.ReadAll(resp.Body)
+
if err != nil {
+
return fmt.Errorf("failed reading PLC bytes: %w", err)
+
}
+
+
var entries []PlcAudit
+
err = json.Unmarshal(respBytes, &entries)
+
if err != nil {
+
return fmt.Errorf("failed unmarshalling bytes: %w", err)
+
}
+
+
rss := NewRss(did)
+
var last *PlcAudit
+
for _, entry := range entries {
+
if last == nil {
+
item := RssItem{
+
Title: "Identity created",
+
Published: rssTimeFormat(entry.CreatedAt),
+
Permalink: RssPermalink{entry.Cid, "false"},
+
}
+
rss.Add(item)
+
} else {
+
results := compareAlsoKnownAs(last.Operation.AlsoKnownAs, entry.Operation.AlsoKnownAs)
+
if results != nil {
+
rss.Add(RssItem{
+
Title: results.Title,
+
Description: results.Description,
+
Published: rssTimeFormat(entry.CreatedAt),
+
Permalink: RssPermalink{entry.Cid + "#alsoKnownAs", "false"},
+
})
+
}
+
+
results = compareServices(last.Operation.Services, entry.Operation.Services)
+
if results != nil {
+
rss.Add(RssItem{
+
Title: results.Title,
+
Description: results.Description,
+
Published: rssTimeFormat(entry.CreatedAt),
+
Permalink: RssPermalink{entry.Cid + "#services", "false"},
+
})
+
}
+
+
results = compareRotationKeys(last.Operation.RotationKeys, entry.Operation.RotationKeys)
+
if results != nil {
+
rss.Add(RssItem{
+
Title: results.Title,
+
Description: results.Description,
+
Published: rssTimeFormat(entry.CreatedAt),
+
Permalink: RssPermalink{entry.Cid + "#rotationKeys", "false"},
+
})
+
}
+
+
results = compareVerificationMethods(last.Operation.VerificationMethods, entry.Operation.VerificationMethods)
+
if results != nil {
+
rss.Add(RssItem{
+
Title: results.Title,
+
Description: results.Description,
+
Published: rssTimeFormat(entry.CreatedAt),
+
Permalink: RssPermalink{entry.Cid + "#verificationMethods", "false"},
+
})
+
}
+
}
+
+
last = &entry
+
}
+
+
return c.XMLPretty(http.StatusOK, rss, " ")
+
})
+
e.Logger.Fatal(e.Start(":1234"))
+
}
+24
cmd/plcwatch/plc.go
···
+
package main
+
+
type PlcAudit struct {
+
Did string `json:"did"`
+
Operation PlcOperation `json:"operation"`
+
Cid string `json:"cid"`
+
Nullified bool `json:"nullified"`
+
CreatedAt string `json:"createdAt"`
+
}
+
+
type PlcOperation struct {
+
Signature string `json:"sig"`
+
Previous *string `json:"prev"`
+
Type string `json:"type"`
+
Services map[string]PlcService `json:"services"`
+
AlsoKnownAs []string `json:"alsoKnownAs"`
+
RotationKeys []string `json:"rotationKeys"`
+
VerificationMethods map[string]string `json:"verificationMethods"`
+
}
+
+
type PlcService struct {
+
Type string `json:"type"`
+
Endpoint string `json:"endpoint"`
+
}
+40
cmd/plcwatch/rss.go
···
+
package main
+
+
import (
+
"fmt"
+
)
+
+
type Rss struct {
+
XMLName string `xml:"rss"`
+
Version string `xml:"version,attr"`
+
Title string `xml:"channel>title"`
+
Items []RssItem `xml:"channel>item"`
+
}
+
+
type RssItem struct {
+
Title string `xml:"title"`
+
Description string `xml:"description,omitempty"`
+
Published string `xml:"pubDate"`
+
Permalink RssPermalink `xml:"guid"`
+
}
+
+
type CDATA struct {
+
Value string `xml:",cdata"`
+
}
+
+
type RssPermalink struct {
+
Value string `xml:",chardata"`
+
Permanent string `xml:"isPermaLink,attr"`
+
}
+
+
func NewRss(title string) *Rss {
+
return &Rss{
+
Version: "2.0",
+
Title: fmt.Sprintf("plcwatch: %s", title),
+
}
+
}
+
+
func (r *Rss) Add(item RssItem) {
+
// r.Items = append(r.Items, item)
+
r.Items = append([]RssItem{item}, r.Items...)
+
}
+20
go.mod
···
+
module github.com/edavis/plcwatch
+
+
go 1.23.6
+
+
require (
+
github.com/bluesky-social/indigo v0.0.0-20250324192039-40f397713b63
+
github.com/labstack/echo/v4 v4.13.3
+
)
+
+
require (
+
github.com/labstack/gommon v0.4.2 // indirect
+
github.com/mattn/go-colorable v0.1.13 // indirect
+
github.com/mattn/go-isatty v0.0.20 // indirect
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
+
github.com/valyala/fasttemplate v1.2.2 // indirect
+
golang.org/x/crypto v0.31.0 // indirect
+
golang.org/x/net v0.33.0 // indirect
+
golang.org/x/sys v0.28.0 // indirect
+
golang.org/x/text v0.21.0 // indirect
+
)
+33
go.sum
···
+
github.com/bluesky-social/indigo v0.0.0-20250324192039-40f397713b63 h1:0ohcaD0jwrEgMrPb87f3kL5n1OaIGuBqXdlE7iO3s6M=
+
github.com/bluesky-social/indigo v0.0.0-20250324192039-40f397713b63/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA=
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
+
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
+
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=