feta/instance.go
2019-11-03 03:09:04 -08:00

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
}