package main import ( "encoding/json" "fmt" "github.com/rs/zerolog/log" "net/http" "strings" "time" ) const NodeInfoSchemaVersionTwoName = "http://nodeinfo.diaspora.software/ns/schema/2.0" const NODE_TIMEOUT = time.Second * 30 type ServerImplementation int const ( ServerUnknown ServerImplementation = iota ServerMastodon ServerPleroma ) type Instance struct { errorCount uint highestId int hostName string impl ServerImplementation lastFailure *time.Time lastSuccess *time.Time nodeInfoUrl string serverVersion string identified bool up bool shouldSkip bool } func NewInstance(hostname string) *Instance { i := new(Instance) i.hostName = hostname foreverago := time.Now().Add((-1 * 86400 * 365 * 100) * time.Second) i.lastSuccess = &foreverago i.lastFailure = &foreverago i.identified = false i.up = false go func() { i.detectNodeType() }() return i } func (i *Instance) detectNodeType() { if i.impl > ServerUnknown { return } 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.errorCount = i.errorCount + 1 t := time.Now() i.lastFailure = &t } func (i *Instance) registerSuccess() { 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.nodeInfoUrl = item.Href i.registerSuccess() return } } log.Error(). Str("hostname", i.hostName). Msg("incomplete nodeinfo") i.registerError() return } } func (i *Instance) fetchNodeInfo() { i.fetchNodeInfoURL() if i.nodeInfoUrl == "" { log.Error(). Str("hostname", i.hostName). Msg("unable to fetch nodeinfo as nodeinfo URL cannot be determined") return } var c = &http.Client{ Timeout: NODE_TIMEOUT, } //FIXME make sure the nodeinfourl is on the same domain as the instance //hostname resp, err := c.Get(i.nodeInfoUrl) 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.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) { if i.impl == ServerMastodon { return i.fetchRecentTootsJsonFromMastodon() } else if i.impl == ServerPleroma { return i.fetchRecentTootsJsonFromPleroma() } else { panic("nope") } } 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 } func fetchLatestToots(lastId int) { log.Debug().Msg("This message appears only when log level set to Debug") log.Info().Msg("This message appears when log level set to Debug or Info") }