this repo has no description

Initial commit

Eric Davis 76771805

Changed files
+273
cmd
+51
cmd/plcwatch/comparisons.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "reflect" 6 + ) 7 + 8 + type CompareResults struct { 9 + Title string 10 + Description string 11 + } 12 + 13 + func compareAlsoKnownAs(old, new []string) *CompareResults { 14 + if !reflect.DeepEqual(old, new) { 15 + return &CompareResults{ 16 + "Alias updated", 17 + fmt.Sprintf("%+v -> %+v", old, new), 18 + } 19 + } 20 + return nil 21 + } 22 + 23 + func compareServices(old, new map[string]PlcService) *CompareResults { 24 + if !reflect.DeepEqual(old, new) { 25 + return &CompareResults{ 26 + "Service updated", 27 + fmt.Sprintf("%+v -> %+v", old, new), 28 + } 29 + } 30 + return nil 31 + } 32 + 33 + func compareRotationKeys(old, new []string) *CompareResults { 34 + if !reflect.DeepEqual(old, new) { 35 + return &CompareResults{ 36 + "Rotation key updated", 37 + fmt.Sprintf("%+v -> %+v", old, new), 38 + } 39 + } 40 + return nil 41 + } 42 + 43 + func compareVerificationMethods(old, new map[string]string) *CompareResults { 44 + if !reflect.DeepEqual(old, new) { 45 + return &CompareResults{ 46 + "Verification method updated", 47 + fmt.Sprintf("%+v -> %+v", old, new), 48 + } 49 + } 50 + return nil 51 + }
+105
cmd/plcwatch/main.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/labstack/echo/v4" 12 + ) 13 + 14 + const plcDirectoryHost = "https://plc.directory" 15 + 16 + func rssTimeFormat(raw string) string { 17 + t, err := syntax.ParseDatetimeTime(raw) 18 + if err != nil { 19 + return time.Now().UTC().Format(time.RFC1123Z) 20 + } 21 + return t.Format(time.RFC1123Z) 22 + } 23 + 24 + func main() { 25 + e := echo.New() 26 + e.GET("/:did", func(c echo.Context) error { 27 + did := c.Param("did") 28 + 29 + url := fmt.Sprintf("%s/%s/log/audit", plcDirectoryHost, did) 30 + resp, err := http.Get(url) 31 + if err != nil { 32 + return fmt.Errorf("failed fetching did audit log: %w", err) 33 + } 34 + defer resp.Body.Close() 35 + 36 + respBytes, err := io.ReadAll(resp.Body) 37 + if err != nil { 38 + return fmt.Errorf("failed reading PLC bytes: %w", err) 39 + } 40 + 41 + var entries []PlcAudit 42 + err = json.Unmarshal(respBytes, &entries) 43 + if err != nil { 44 + return fmt.Errorf("failed unmarshalling bytes: %w", err) 45 + } 46 + 47 + rss := NewRss(did) 48 + var last *PlcAudit 49 + for _, entry := range entries { 50 + if last == nil { 51 + item := RssItem{ 52 + Title: "Identity created", 53 + Published: rssTimeFormat(entry.CreatedAt), 54 + Permalink: RssPermalink{entry.Cid, "false"}, 55 + } 56 + rss.Add(item) 57 + } else { 58 + results := compareAlsoKnownAs(last.Operation.AlsoKnownAs, entry.Operation.AlsoKnownAs) 59 + if results != nil { 60 + rss.Add(RssItem{ 61 + Title: results.Title, 62 + Description: results.Description, 63 + Published: rssTimeFormat(entry.CreatedAt), 64 + Permalink: RssPermalink{entry.Cid + "#alsoKnownAs", "false"}, 65 + }) 66 + } 67 + 68 + results = compareServices(last.Operation.Services, entry.Operation.Services) 69 + if results != nil { 70 + rss.Add(RssItem{ 71 + Title: results.Title, 72 + Description: results.Description, 73 + Published: rssTimeFormat(entry.CreatedAt), 74 + Permalink: RssPermalink{entry.Cid + "#services", "false"}, 75 + }) 76 + } 77 + 78 + results = compareRotationKeys(last.Operation.RotationKeys, entry.Operation.RotationKeys) 79 + if results != nil { 80 + rss.Add(RssItem{ 81 + Title: results.Title, 82 + Description: results.Description, 83 + Published: rssTimeFormat(entry.CreatedAt), 84 + Permalink: RssPermalink{entry.Cid + "#rotationKeys", "false"}, 85 + }) 86 + } 87 + 88 + results = compareVerificationMethods(last.Operation.VerificationMethods, entry.Operation.VerificationMethods) 89 + if results != nil { 90 + rss.Add(RssItem{ 91 + Title: results.Title, 92 + Description: results.Description, 93 + Published: rssTimeFormat(entry.CreatedAt), 94 + Permalink: RssPermalink{entry.Cid + "#verificationMethods", "false"}, 95 + }) 96 + } 97 + } 98 + 99 + last = &entry 100 + } 101 + 102 + return c.XMLPretty(http.StatusOK, rss, " ") 103 + }) 104 + e.Logger.Fatal(e.Start(":1234")) 105 + }
+24
cmd/plcwatch/plc.go
··· 1 + package main 2 + 3 + type PlcAudit struct { 4 + Did string `json:"did"` 5 + Operation PlcOperation `json:"operation"` 6 + Cid string `json:"cid"` 7 + Nullified bool `json:"nullified"` 8 + CreatedAt string `json:"createdAt"` 9 + } 10 + 11 + type PlcOperation struct { 12 + Signature string `json:"sig"` 13 + Previous *string `json:"prev"` 14 + Type string `json:"type"` 15 + Services map[string]PlcService `json:"services"` 16 + AlsoKnownAs []string `json:"alsoKnownAs"` 17 + RotationKeys []string `json:"rotationKeys"` 18 + VerificationMethods map[string]string `json:"verificationMethods"` 19 + } 20 + 21 + type PlcService struct { 22 + Type string `json:"type"` 23 + Endpoint string `json:"endpoint"` 24 + }
+40
cmd/plcwatch/rss.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + ) 6 + 7 + type Rss struct { 8 + XMLName string `xml:"rss"` 9 + Version string `xml:"version,attr"` 10 + Title string `xml:"channel>title"` 11 + Items []RssItem `xml:"channel>item"` 12 + } 13 + 14 + type RssItem struct { 15 + Title string `xml:"title"` 16 + Description string `xml:"description,omitempty"` 17 + Published string `xml:"pubDate"` 18 + Permalink RssPermalink `xml:"guid"` 19 + } 20 + 21 + type CDATA struct { 22 + Value string `xml:",cdata"` 23 + } 24 + 25 + type RssPermalink struct { 26 + Value string `xml:",chardata"` 27 + Permanent string `xml:"isPermaLink,attr"` 28 + } 29 + 30 + func NewRss(title string) *Rss { 31 + return &Rss{ 32 + Version: "2.0", 33 + Title: fmt.Sprintf("plcwatch: %s", title), 34 + } 35 + } 36 + 37 + func (r *Rss) Add(item RssItem) { 38 + // r.Items = append(r.Items, item) 39 + r.Items = append([]RssItem{item}, r.Items...) 40 + }
+20
go.mod
··· 1 + module github.com/edavis/plcwatch 2 + 3 + go 1.23.6 4 + 5 + require ( 6 + github.com/bluesky-social/indigo v0.0.0-20250324192039-40f397713b63 7 + github.com/labstack/echo/v4 v4.13.3 8 + ) 9 + 10 + require ( 11 + github.com/labstack/gommon v0.4.2 // indirect 12 + github.com/mattn/go-colorable v0.1.13 // indirect 13 + github.com/mattn/go-isatty v0.0.20 // indirect 14 + github.com/valyala/bytebufferpool v1.0.0 // indirect 15 + github.com/valyala/fasttemplate v1.2.2 // indirect 16 + golang.org/x/crypto v0.31.0 // indirect 17 + golang.org/x/net v0.33.0 // indirect 18 + golang.org/x/sys v0.28.0 // indirect 19 + golang.org/x/text v0.21.0 // indirect 20 + )
+33
go.sum
··· 1 + github.com/bluesky-social/indigo v0.0.0-20250324192039-40f397713b63 h1:0ohcaD0jwrEgMrPb87f3kL5n1OaIGuBqXdlE7iO3s6M= 2 + github.com/bluesky-social/indigo v0.0.0-20250324192039-40f397713b63/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 3 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 + github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 6 + github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 7 + github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 8 + github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 9 + github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 10 + github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 11 + github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 12 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 13 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 14 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 17 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 18 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 19 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 20 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 21 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 22 + golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 23 + golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 24 + golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 25 + golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 26 + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 + golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 29 + golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 + golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 31 + golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 32 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=