package main import "encoding/json" import "fmt" 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 NODE_TIMEOUT = time.Second * 10 const ONE_HOUR = time.Second * 60 * 60 const ONE_DAY = time.Second * 60 * 60 * 24 type ServerImplementation int const ( ServerUnknown ServerImplementation = iota ServerMastodon ServerPleroma ) type Instance struct { sync.Mutex errorCount uint highestId int hostName string up bool identified bool impl ServerImplementation lastError *time.Time lastSuccess *time.Time nextCheck *time.Time nodeInfoUrl string serverVersion string } func NewInstance(hostname string) *Instance { foreverago := time.Now().Add((-1 * 86400 * 365 * 100) * time.Second) i := new(Instance) i.hostName = hostname i.nextCheck = &foreverago i.up = false go func() { i.detectNodeType() }() 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 > ServerUnknown { i.Unlock() return } i.Unlock() i.fetchNodeInfo() } type NodeInfoWellKnownResponse struct { Links []struct { Rel string `json:"rel"` Href string `json:"href"` } `json:"links"` } type NodeInfoVersionTwoSchema struct { Version string `json:"version"` Software struct { Name string `json:"name"` Version string `json:"version"` } `json:"software"` Protocols []string `json:"protocols"` Usage struct { Users struct { Total int `json:"total"` ActiveMonth int `json:"activeMonth"` ActiveHalfyear int `json:"activeHalfyear"` } `json:"users"` LocalPosts int `json:"localPosts"` } `json:"usage"` OpenRegistrations bool `json:"openRegistrations"` } func (i *Instance) registerError() { i.Lock() defer i.Unlock() i.errorCount = i.errorCount + 1 t := time.Now() i.lastError = &t } func (i *Instance) registerSuccess() { i.Lock() defer i.Unlock() t := time.Now() i.lastSuccess = &t } func (i *Instance) fetchNodeInfoURL() { url := fmt.Sprintf("https://%s/.well-known/nodeinfo", i.hostName) var c = &http.Client{ Timeout: NODE_TIMEOUT, } log.Debug(). Str("url", url). Str("hostname", i.hostName). Msg("fetching nodeinfo reference URL") resp, err := c.Get(url) if err != nil { log.Error(). Str("hostname", i.hostName). Msg("unable to fetch nodeinfo, node is down?") i.registerError() } else { i.up = true // node is alive and responding to us nir := new(NodeInfoWellKnownResponse) err = json.NewDecoder(resp.Body).Decode(&nir) if err != nil { log.Error(). Str("hostname", i.hostName). Msg("unable to parse nodeinfo") 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: NODE_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). Msgf("unable to fetch nodeinfo data: %s", err) i.registerError() } else { ni := new(NodeInfoVersionTwoSchema) err = json.NewDecoder(resp.Body).Decode(&ni) if err != nil { log.Error(). Str("hostname", i.hostName). Msgf("unable to parse nodeinfo: %s", err) 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 = ServerPleroma } 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 = ServerMastodon } 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 == ServerMastodon { return i.fetchRecentTootsJsonFromMastodon() } else if impl == ServerPleroma { 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 }