package feta import "encoding/json" import "fmt" import "io/ioutil" import "net/http" import "strings" import "sync" import "time" import "errors" import "github.com/gin-gonic/gin" import "github.com/rs/zerolog/log" const NodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0" const INSTANCE_HTTP_TIMEOUT = time.Second * 60 const INSTANCE_SPIDER_INTERVAL = time.Second * 60 const INSTANCE_ERROR_INTERVAL = time.Second * 60 * 30 type InstanceImplementation int const ( Unknown InstanceImplementation = iota Mastodon Pleroma ) type InstanceStatus int const ( InstanceStatusNone InstanceStatus = iota InstanceStatusUnknown InstanceStatusAlive InstanceStatusIdentified InstanceStatusFailure ) type Instance struct { sync.RWMutex errorCount uint successCount uint highestId int hostname string identified bool fetching bool impl InstanceImplementation backend *InstanceBackend status InstanceStatus nextFetch time.Time nodeInfoUrl string serverVersion string } func NewInstance(hostname InstanceHostname) *Instance { self := new(Instance) self.hostname = string(hostname) self.status = InstanceStatusUnknown self.nextFetch = time.Now().Add(-1 * time.Second) return self } func (self *Instance) bumpFetch() { self.Lock() defer self.Unlock() self.nextFetch = time.Now().Add(10 * time.Second) } func (self *Instance) setNextFetchAfter(d time.Duration) { self.Lock() defer self.Unlock() self.nextFetch = time.Now().Add(d) } func (self *Instance) Fetch() { err := self.detectNodeTypeIfNecessary() if err != nil { self.setNextFetchAfter(INSTANCE_ERROR_INTERVAL) log.Debug(). Str("hostname", self.hostname). Err(err). Msg("unable to fetch instance metadata") return } //self.setNextFetchAfter(INSTANCE_SPIDER_INTERVAL) log.Info().Msgf("i (%s) should check for toots", self.hostname) } func (self *Instance) dueForFetch() bool { self.Lock() defer self.Unlock() nf := self.nextFetch return nf.Before(time.Now()) } func (self *Instance) nodeIdentified() bool { self.RLock() defer self.RUnlock() if self.impl > Unknown { return true } return false } func (self *Instance) detectNodeTypeIfNecessary() error { if !self.nodeIdentified() { return self.fetchNodeInfo() } else { return nil } } func (self *Instance) registerError() { self.setNextFetchAfter(INSTANCE_ERROR_INTERVAL) self.Lock() defer self.Unlock() self.errorCount++ } func (self *Instance) registerSuccess() { self.setNextFetchAfter(INSTANCE_SPIDER_INTERVAL) self.Lock() defer self.Unlock() self.successCount++ } func (self *Instance) ApiReport() *gin.H { r := gin.H{} return &r } func (i *Instance) Up() bool { i.Lock() defer i.Unlock() return i.successCount > 0 } func (i *Instance) fetchNodeInfoURL() error { url := fmt.Sprintf("https://%s/.well-known/nodeinfo", i.hostname) var c = &http.Client{ Timeout: INSTANCE_HTTP_TIMEOUT, } log.Debug(). Str("url", url). Str("hostname", i.hostname). Msg("fetching nodeinfo reference URL") resp, err := c.Get(url) if err != nil { log.Debug(). Str("hostname", i.hostname). Err(err). Msg("unable to fetch nodeinfo, node is down?") i.registerError() return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Debug(). Str("hostname", i.hostname). Err(err). Msg("unable to read nodeinfo") i.registerError() return err } nir := new(NodeInfoWellKnownResponse) err = json.Unmarshal(body, &nir) if err != nil { log.Debug(). Str("hostname", i.hostname). Err(err). Msg("unable to parse nodeinfo, node is weird") i.registerError() return err } for _, item := range nir.Links { if item.Rel == NodeInfoSchemaVersionTwoName { log.Debug(). Str("hostname", i.hostname). Str("nodeinfourl", item.Href). Msg("success fetching url for nodeinfo") i.Lock() i.nodeInfoUrl = item.Href i.Unlock() i.registerSuccess() return nil } log.Debug(). Str("hostname", i.hostname). Str("item-rel", item.Rel). Str("item-href", item.Href). Msg("found key in nodeinfo") } log.Error(). Str("hostname", i.hostname). Msg("incomplete nodeinfo") i.registerError() return errors.New("incomplete nodeinfo") } func (i *Instance) fetchNodeInfo() error { err := i.fetchNodeInfoURL() if err != nil { return err } var c = &http.Client{ Timeout: INSTANCE_HTTP_TIMEOUT, } //FIXME make sure the nodeinfourl is on the same domain as the instance //hostname i.RLock() url := i.nodeInfoUrl i.RUnlock() resp, err := c.Get(url) if err != nil { log.Debug(). Str("hostname", i.hostname). Err(err). Msgf("unable to fetch nodeinfo data") i.registerError() return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error(). Str("hostname", i.hostname). Err(err). Msgf("unable to read nodeinfo data") i.registerError() return err } ni := new(NodeInfoVersionTwoSchema) err = json.Unmarshal(body, &ni) if err != nil { log.Error(). Str("hostname", i.hostname). Err(err). Msgf("unable to parse nodeinfo") i.registerError() return err } log.Debug(). Str("serverVersion", ni.Software.Version). Str("software", ni.Software.Name). Str("hostname", i.hostname). Str("nodeInfoUrl", i.nodeInfoUrl). Msg("received nodeinfo from instance") i.Lock() defer i.Unlock() i.serverVersion = ni.Software.Version ni.Software.Name = strings.ToLower(ni.Software.Name) if ni.Software.Name == "pleroma" { log.Debug(). Str("hostname", i.hostname). Str("software", ni.Software.Name). Msg("detected server software") i.registerSuccess() i.identified = true i.impl = Pleroma i.status = InstanceStatusIdentified return nil } else if ni.Software.Name == "mastodon" { i.registerSuccess() i.identified = true i.impl = Mastodon i.status = InstanceStatusIdentified return nil } else { log.Error(). Str("hostname", i.hostname). Str("software", ni.Software.Name). Msg("FIXME unknown server implementation") i.registerError() return errors.New("FIXME unknown server implementation") } } /* func (i *Instance) fetchRecentToots() ([]byte, error) { i.Lock() impl := i.impl i.Unlock() if impl == Mastodon { return i.fetchRecentTootsJsonFromMastodon() } else if impl == Pleroma { return i.fetchRecentTootsJsonFromPleroma() } else { panic("unimplemented") } } */ /* func (self *PleromaBackend) fetchRecentToots() ([]byte, error) { //url := //fmt.Sprintf("https://%s/api/statuses/public_and_external_timeline.json?count=100", //i.hostname) return nil, nil } func (self *MastodonBackend) fetchRecentTootsJsonFromMastodon() ([]byte, error) { //url := //fmt.Sprintf("https://%s/api/v1/timelines/public?limit=40&local=true", //i.hostname) return nil, nil } */