package adapter import ( "fmt" . "forge.lightcrystal.systems/lightcrystal/underbbs/models" "github.com/yitsushi/go-misskey" mkm "github.com/yitsushi/go-misskey/models" n "github.com/yitsushi/go-misskey/services/notes" tl "github.com/yitsushi/go-misskey/services/notes/timeline" _ "strings" "time" ) type MisskeyAdapter struct { data chan SocketData nickname string server string apiKey string mk *misskey.Client // unlike the mastodon client, we have to manage combining resources // from different API calls instead of streaming them in a single channel cache map[string]time.Time stop chan bool } func (self *MisskeyAdapter) Init(settings Settings, data chan SocketData) error { fmt.Println("initializing misskey adapter") self.nickname = settings.Nickname self.server = *settings.Server self.apiKey = *settings.ApiKey self.data = data fmt.Println("getting ready to initialize internal client") client, err := misskey.NewClientWithOptions( misskey.WithAPIToken(self.apiKey), misskey.WithBaseURL("https", self.server, ""), ) if err != nil { fmt.Println(err.Error()) return err } fmt.Println("misskey client initialized") self.mk = client self.cache = make(map[string]time.Time) return nil } func (self *MisskeyAdapter) Subscribe(filter string) []error { // misskey streaming API is undocumented.... // we could try to reverse engineer it by directly connecting to the websocket??? // alternatively, we can poll timelines, mentions, etc with a cancellation channel, // keep a cache of IDs in memory, and send new objects on the data channel // TODO: decode the filter so we can send data to the mk services // same as in masto, we will want to close and reopen the stop channel if self.stop != nil { close(self.stop) } self.stop = make(chan bool) // in the background, continuously read data from these API endpoints // if they are newer than the cache, convert them to UnderBBS objects // and send them on the data channel go self.poll() return nil } func (self *MisskeyAdapter) poll() { var latest *time.Time notesService := self.mk.Notes() timelineService := notesService.Timeline() for { select { case _, ok := <-self.stop: if !ok { return } default: // TODO: we have to actually decode and pass our filter criteria // probe for new notes probenote, err := timelineService.Get(tl.GetRequest{ Limit: 1, }) if err == nil && len(probenote) > 0 && self.isNew(probenote[0]) { if latest == nil { latest = &probenote[0].CreatedAt // this is the first fetch of notes, we can just grab them notes, err := timelineService.Get(tl.GetRequest{ Limit: 100, }) // if latest is nil also get mentions history mentions, merr := notesService.Mentions(n.MentionsRequest{ Limit: 100, }) if err != nil { fmt.Println(err.Error()) } if merr != nil { fmt.Println(merr.Error()) } // check the cache for everything we just collected // if anything is newer or as of yet not in the cache, add it // and convert it to a SocketData implementation before sending on data channel for _, n := range notes { msg := self.cacheAndConvert(n) if msg != nil { self.data <- msg } } for _, n := range mentions { msg := self.cacheAndConvert(n) if msg != nil { self.data <- msg } } } else { for { // get notes since latest, until the probe notes, err := timelineService.Get(tl.GetRequest{ SinceDate: uint64(latest.Unix()), UntilDate: uint64(probenote[0].CreatedAt.Unix()), Limit: 100, }) if err != nil { fmt.Println(err.Error()) } for _, n := range notes { msg := self.cacheAndConvert(n) if msg == nil { latest = &probenote[0].CreatedAt break } self.data <- msg } if *latest == probenote[0].CreatedAt { break } } } } time.Sleep(5 * time.Second) } } } func (self *MisskeyAdapter) isNew(n mkm.Note) bool { timestamp, exists := self.cache[n.ID] return !exists || timestamp.Before(n.CreatedAt) } func (self *MisskeyAdapter) cacheAndConvert(n mkm.Note) *Message { timestamp, exists := self.cache[n.ID] if !exists || timestamp.Before(n.CreatedAt) { self.cache[n.ID] = n.CreatedAt msg := Message{ Uri: n.URI, Author: Author{ Id: n.User.ID, Name: n.User.Name, // ProfileUri: *n.User.URL, ProfilePic: n.User.AvatarURL, }, Protocol: "misskey", Adapter: self.nickname, Created: n.CreatedAt, Content: n.Text, } return &msg } return nil } func (self *MisskeyAdapter) Fetch(query string) error { return nil } func (self *MisskeyAdapter) Do(action string) error { return nil } func (self *MisskeyAdapter) DefaultSubscriptionFilter() string { return "" }