diff --git a/adapter/adapter.go b/adapter/adapter.go index 1309fbe..807a232 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -1,13 +1,7 @@ package adapter import ( - "context" - "encoding/json" - "errors" - "fmt" . "forge.lightcrystal.systems/lightcrystal/underbbs/models" - nostr "github.com/nbd-wtf/go-nostr" - "strings" ) type Adapter interface { @@ -20,108 +14,3 @@ type Adapter interface { UpdateMetadata(interface{}) error DefaultSubscriptionFilter() string } - -type NostrAdapter struct { - data chan SocketData - nickname string - privkey string - relays []*nostr.Relay -} - -func (self *NostrAdapter) Init(settings Settings, data chan SocketData) error { - self.nickname = settings.Nickname - self.privkey = *settings.PrivKey - self.data = data - - ctx := context.Background() - - for _, r := range settings.Relays { - pr, _ := nostr.RelayConnect(ctx, strings.Trim(r, " ")) - if pr == nil { - return errors.New("Relay connection could not be completed") - } - self.relays = append(self.relays, pr) - } - return nil -} - -func (self *NostrAdapter) Subscribe(filter string) []error { - var filters nostr.Filters - err := json.Unmarshal([]byte(filter), &filters) - if err != nil { - return []error{err} - } - - errs := make([]error, 0) - - fmt.Print("unmarshalled filter from json; iterating through relays to subscribe..") - - for _, r := range self.relays { - fmt.Print(".") - sub, err := r.Subscribe(context.Background(), filters) - if err != nil { - errs = append(errs, err) - } else { - go func() { - for ev := range sub.Events { - fmt.Print("!") - // try sequentially to encode into an underbbs object - // and send it to the appropriate channel - m, err := nostrEventToMsg(ev) - if err == nil { - self.data <- m - } - - } - }() - } - fmt.Println() - } - - if len(errs) > 0 { - fmt.Println("subscription operation completed with errors") - return errs - } - fmt.Println("subscription operation completed without errors") - return nil -} -func (self *NostrAdapter) SendMessage(msg Message) error { - return nil -} -func (self *NostrAdapter) Follow(author Author) error { - return nil -} -func (self *NostrAdapter) Unfollow(author Author) error { - return nil -} -func (self *NostrAdapter) GetFollowers() error { - return nil -} -func (self *NostrAdapter) UpdateMetadata(data interface{}) error { - return nil -} - -func (self *NostrAdapter) DefaultSubscriptionFilter() string { - return "[{\"kinds\":[1]}]" -} - -func nostrEventToMsg(evt *nostr.Event) (Message, error) { - m := Message{ - Protocol: "nostr", - } - if evt == nil { - return m, errors.New("no event") - } - switch evt.Kind { - case nostr.KindTextNote: - m.Uri = evt.ID - m.Author = Author{ - Id: evt.PubKey, - } - m.Created = evt.CreatedAt.Time() - m.Content = evt.Content - return m, nil - default: - return m, errors.New(fmt.Sprintf("unsupported event kind: %d", evt.Kind)) - } -} diff --git a/adapter/mastodon.go b/adapter/mastodon.go new file mode 100644 index 0000000..962b85f --- /dev/null +++ b/adapter/mastodon.go @@ -0,0 +1,109 @@ +package adapter + +import ( + . "forge.lightcrystal.systems/lightcrystal/underbbs/models" + madon "github.com/McKael/madon" +) + +type MastoAdapter struct { + data chan SocketData + nickname string + server string + apiKey string + + masto *madon.Client + + events chan madon.StreamEvent + stop chan bool + done chan bool +} + +var scopes = []string{"read", "write", "follow"} + +func (self *MastoAdapter) Init(settings Settings, data chan SocketData) error { + self.nickname = settings.Nickname + self.server = *settings.Server + self.apiKey = *settings.ApiKey + self.data = data + + masto, err := madon.NewApp("underbbs", "https://lightcrystal.systems", scopes, madon.NoRedirect, self.server) + if err != nil { + return err + } + self.masto = masto + err = self.masto.SetUserToken(self.apiKey, "", "", []string{}) + return err +} + +func (self *MastoAdapter) Subscribe(filter string) []error { + // TODO: decode separate timelines and hashtags + // for now, the filter is just the timeline + + // if any existing events channel, close it and create a new one + if self.events != nil { + close(self.events) + } + self.events = make(chan madon.StreamEvent) + // if any existing stop channel, close it and create a new one + if self.stop != nil { + close(self.stop) + } + self.stop = make(chan bool) + // make a new done channel + self.done = make(chan bool) + + // call StreamListener + self.masto.StreamListener(filter, "", self.events, self.stop, self.done) + go func() { + for e := range self.events { + switch e.Event { + case "error": + case "update": + var msg *Message + switch v := e.Data.(type) { + case int64: + s, _ := self.masto.GetStatus(v) + if s != nil { + msg = mastoUpdateToMessage(*s) + } + case madon.Status: + msg = mastoUpdateToMessage(v) + } + if msg != nil { + self.data <- msg + } + case "notification": + case "delete": + } + } + }() + // in the background, read and translate events from the stream + // and check for doneCh closing + // the stopCh will be closed by a subsequent call to subscribe + errs := make([]error, 0) + return errs +} +func (self *MastoAdapter) SendMessage(msg Message) error { + return nil +} +func (self *MastoAdapter) Follow(author Author) error { + return nil +} +func (self *MastoAdapter) Unfollow(author Author) error { + return nil +} +func (self *MastoAdapter) GetFollowers() error { + return nil +} +func (self *MastoAdapter) UpdateMetadata(data interface{}) error { + return nil +} + +func (self *MastoAdapter) DefaultSubscriptionFilter() string { + return "home" +} + +func mastoUpdateToMessage(status madon.Status) *Message { + // decode that fucker + return nil +} diff --git a/adapter/nostr.go b/adapter/nostr.go new file mode 100644 index 0000000..ed87ddd --- /dev/null +++ b/adapter/nostr.go @@ -0,0 +1,116 @@ +package adapter + +import ( + "context" + "encoding/json" + "errors" + "fmt" + . "forge.lightcrystal.systems/lightcrystal/underbbs/models" + nostr "github.com/nbd-wtf/go-nostr" + "strings" +) + +type NostrAdapter struct { + data chan SocketData + nickname string + privkey string + relays []*nostr.Relay +} + +func (self *NostrAdapter) Init(settings Settings, data chan SocketData) error { + self.nickname = settings.Nickname + self.privkey = *settings.PrivKey + self.data = data + + ctx := context.Background() + + for _, r := range settings.Relays { + pr, _ := nostr.RelayConnect(ctx, strings.Trim(r, " ")) + if pr == nil { + return errors.New("Relay connection could not be completed") + } + self.relays = append(self.relays, pr) + } + return nil +} + +func (self *NostrAdapter) Subscribe(filter string) []error { + var filters nostr.Filters + err := json.Unmarshal([]byte(filter), &filters) + if err != nil { + return []error{err} + } + + errs := make([]error, 0) + + fmt.Print("unmarshalled filter from json; iterating through relays to subscribe..") + + for _, r := range self.relays { + fmt.Print(".") + sub, err := r.Subscribe(context.Background(), filters) + if err != nil { + errs = append(errs, err) + } else { + go func() { + for ev := range sub.Events { + fmt.Print("!") + // try sequentially to encode into an underbbs object + // and send it to the appropriate channel + m, err := nostrEventToMsg(ev) + if err == nil { + self.data <- m + } + + } + }() + } + fmt.Println() + } + + if len(errs) > 0 { + fmt.Println("subscription operation completed with errors") + return errs + } + fmt.Println("subscription operation completed without errors") + return nil +} +func (self *NostrAdapter) SendMessage(msg Message) error { + return nil +} +func (self *NostrAdapter) Follow(author Author) error { + return nil +} +func (self *NostrAdapter) Unfollow(author Author) error { + return nil +} +func (self *NostrAdapter) GetFollowers() error { + return nil +} +func (self *NostrAdapter) UpdateMetadata(data interface{}) error { + return nil +} + +func (self *NostrAdapter) DefaultSubscriptionFilter() string { + return "[{\"kinds\":[1]}]" +} + +func nostrEventToMsg(evt *nostr.Event) (Message, error) { + m := Message{ + Protocol: "nostr", + } + if evt == nil { + return m, errors.New("no event") + } + switch evt.Kind { + case nostr.KindTextNote: + m.Uri = evt.ID + m.Author = Author{ + Id: evt.PubKey, + } + m.Created = evt.CreatedAt.Time() + m.Content = evt.Content + return m, nil + default: + return m, errors.New(fmt.Sprintf("unsupported event kind: %d", evt.Kind)) + } +} diff --git a/go.mod b/go.mod index e278905..0294768 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module forge.lightcrystal.systems/lightcrystal/underbbs go 1.22.0 require ( + github.com/McKael/madon v2.3.0+incompatible github.com/nbd-wtf/go-nostr v0.31.2 golang.org/x/time v0.5.0 hacklab.nilfm.cc/quartzgun v0.3.2 @@ -17,12 +18,17 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.2.0 // indirect + github.com/gorilla/websocket v1.5.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/puzpuzpuz/xsync/v3 v3.0.2 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 9ec49f8..9ba91e9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/McKael/madon v2.3.0+incompatible h1:xMUA+Fy4saDV+8tN3MMnwJUoYWC//5Fy8LeOqJsRNIM= +github.com/McKael/madon v2.3.0+incompatible/go.mod h1:+issnvJjN1rpjAHZwXRB/x30uHh/NoQR7QaojJK/lSI= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= @@ -14,14 +16,26 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= +github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/nbd-wtf/go-nostr v0.31.2 h1:PkHCAsSzG0Ce8tfF7LKyvZOjYtCdC+hPh5KfO/Rl1b4= github.com/nbd-wtf/go-nostr v0.31.2/go.mod h1:vHKtHyLXDXzYBN0fi/9Y/Q5AD0p+hk8TQVKlldAi0gI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew= github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -30,13 +44,17 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= hacklab.nilfm.cc/quartzgun v0.3.2 h1:PmRFZ/IgsXVWyNn1iOsQ/ZeMnOQIQy0PzFakhXBdZoU= hacklab.nilfm.cc/quartzgun v0.3.2/go.mod h1:P6qK4HB0CD/xfyRq8wdEGevAPFDDmv0KCaESSvv93LU= nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=