274 lines
5.8 KiB
Go
274 lines
5.8 KiB
Go
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
|
|
}
|