package main import "encoding/json" import "fmt" import "io/ioutil" import "net/http" import "strings" import "sync" import "time" 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 InstanceStatusFailure ) type Instance struct { sync.Mutex errorCount uint successCount uint highestId int hostName string identified bool impl InstanceImplementation status InstanceStatus nextCheck *time.Time nodeInfoUrl string serverVersion string } func NewInstance(hostname string) *Instance { i := new(Instance) i.hostName = hostname i.status = InstanceStatusUnknown t := time.Now().Add(-1 * time.Second) i.nextCheck = &t // FIXME make checks detect the node type instead of in the constructor return i } func (i *Instance) setNextCheck(d time.Duration) { i.Lock() defer i.Unlock() then := time.Now().Add(d) i.nextCheck = &then } func (i *Instance) dueForCheck() bool { i.Lock() defer i.Unlock() return i.nextCheck.Before(time.Now()) } func (i *Instance) detectNodeType() { i.Lock() if i.impl > Unknown { i.Unlock() return } i.Unlock() i.fetchNodeInfo() } func (i *Instance) registerError() { i.setNextCheck(INSTANCE_ERROR_INTERVAL) i.Lock() defer i.Unlock() i.errorCount++ } func (i *Instance) registerSuccess() { i.setNextCheck(INSTANCE_SPIDER_INTERVAL) i.Lock() defer i.Unlock() i.successCount++ } func (i *Instance) fetchNodeInfoURL() { 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 } 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 } nir := new(NodeInfoWellKnownResponse) err = json.Unmarshal(body, &nir) if err != nil { log.Error(). Str("hostname", i.hostName). Err(err). Msg("unable to parse nodeinfo, node is weird") i.registerError() return } for _, item := range nir.Links { if item.Rel == NodeInfoSchemaVersionTwoName { log.Info(). 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 } } log.Error(). Str("hostname", i.hostName). Msg("incomplete nodeinfo") i.registerError() return } func (i *Instance) fetchNodeInfo() { i.fetchNodeInfoURL() i.Lock() failure := false if i.nodeInfoUrl == "" { log.Error(). Str("hostname", i.hostName). Msg("unable to fetch nodeinfo as nodeinfo URL cannot be determined") failure = true } i.Unlock() if failure == true { return } var c = &http.Client{ Timeout: INSTANCE_HTTP_TIMEOUT, } //FIXME make sure the nodeinfourl is on the same domain as the instance //hostname i.Lock() url := i.nodeInfoUrl i.Unlock() resp, err := c.Get(url) if err != nil { log.Error(). Str("hostname", i.hostName). Err(err). Msgf("unable to fetch nodeinfo data") i.registerError() return } 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 } 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 } log.Info(). 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.Info(). Str("hostname", i.hostName). Str("software", ni.Software.Name). Msg("detected server software") i.registerSuccess() i.identified = true i.impl = Pleroma i.status = InstanceStatusAlive } else if ni.Software.Name == "mastodon" { log.Info(). Str("hostname", i.hostName). Str("software", ni.Software.Name). Msg("detected server software") i.registerSuccess() i.identified = true i.impl = Mastodon i.status = InstanceStatusAlive } else { log.Error(). Str("hostname", i.hostName). Str("software", ni.Software.Name). Msg("unknown implementation on server") i.registerError() } return } 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 (i *Instance) fetchRecentTootsJsonFromPleroma() ([]byte, error) { //url := fmt.Sprintf("https://%s/api/statuses/public_and_external_timeline.json?count=100", i.hostName) return nil, nil } func (i *Instance) fetchRecentTootsJsonFromMastodon() ([]byte, error) { //url := fmt.Sprintf("https://%s/api/v1/timelines/public?limit=40&local=true", i.hostName) return nil, nil }