Files
fuse-pouncefs-go/furcadia/pounce.go
2026-01-19 01:51:08 +02:00

293 lines
9.2 KiB
Go

package furcadia
import (
"fmt"
"strings"
)
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
const (
POUNCE_API_BASE_URL = "http://on.furcadia.com/q/"
)
///////////////////////////////////////////////////////////////////////////////////////////////////
// TYPE: OnlineState //////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
type OnlineState int
const (
StateUnknown = iota
StateOffline
StateOnline
)
var OnlineStateName = map[OnlineState]string{
StateUnknown: "unknown",
StateOffline: "offline",
StateOnline: "online",
}
func (state OnlineState) String() string {
return OnlineStateName[state]
}
func (state OnlineState) IsOnline() bool {
return state == StateOnline
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// TYPE: PounceFurre //////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
type PounceFurre struct {
DisplayName string
LastState OnlineState
}
func (pf *PounceFurre) GetShortName() string {
return GetShortName(pf.DisplayName)
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// TYPE: PounceDream //////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
type PounceDream struct {
Title string // title of the dream, or its full dream url
ShortURL string // dream url without the "furc://" prefix or the "/" suffix - just "uploader:name"
LastState OnlineState // last known state of this dream
}
// GetDreamURL returns a full Furcadia dream URL for this dream.
func (pd *PounceDream) GetDreamURL() string {
return "furc://" + pd.ShortURL + "/"
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// TYPE: PounceList ///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
type PounceList struct {
Furres map[string]*PounceFurre // shortname -> *PounceFurre
Dreams map[string]*PounceDream // shortname -> *PounceDream
}
func NewPounceList() *PounceList {
return &PounceList{
Furres: make(map[string]*PounceFurre),
Dreams: make(map[string]*PounceDream),
}
}
// GetQueryString returns an HTTP query element of a URL for an online check.
// The string contains all furrenly watched furres and dreams whose status our
// Pounce client wants to check.
func (p *PounceList) GetQueryString() string {
sb := strings.Builder{}
for _, furre := range p.Furres {
sb.WriteString(fmt.Sprintf("u[]=%s&", furre.GetShortName()))
}
for _, dream := range p.Dreams {
sb.WriteString(fmt.Sprintf("d[]=%s&", dream.ShortURL))
}
return strings.TrimSuffix(sb.String(), "&")
}
// AddFurre adds a furre to the Pounce list, and returns the record of that
// furre back to the caller.
//
// If a record for the given name already exists, then the existing record is
// returned along with ErrExists.
func (p *PounceList) AddFurre(name string) (*PounceFurre, error) {
shortname := GetShortName(name)
oldFurre, ok := p.Furres[shortname]
if ok {
return oldFurre, ErrExists
}
newFurre := p._newFurre(name)
if newFurre.GetShortName() != shortname {
panic(fmt.Sprintf("shortname inconsistency: newFurre.GetShortName()=%v; GetShortName(%v)=%v", newFurre.GetShortName(), name, shortname))
}
p.Furres[shortname] = newFurre
return newFurre, nil
}
// DeleteFurre deletes a furre from the Pounce list. If no furre by that name
// exists, the method returns ErrNotFound.
func (p *PounceList) DeleteFurre(name string) error {
shortname := GetShortName(name)
_, ok := p.Furres[shortname]
if !ok {
return ErrNotFound
}
delete(p.Furres, shortname)
return nil
}
// AddDream adds a dream to the Pounce list, and returns the record of that
// dream back to the caller.
//
// This method accepts the uploader character name, and the "dream name" string
// from which a dream url is constructed. The "dreamName" argument MAY be an
// empty string.
//
// If a dream record for the given dream url already exists, then the existing
// record is returned instead, along with ErrExists.
func (p *PounceList) AddDream(uploaderName, dreamName string) (*PounceDream, error) {
shortURL := GetShortDreamURLFromName(uploaderName, dreamName)
var title string
if dreamName == "" {
title = uploaderName
} else {
title = uploaderName + ": " + dreamName
}
return p.__addDream(shortURL, title)
}
// AddDreamURL adds a dream to the Pounce list, and returns the record of that
// dream back to the caller. This method accepts a Furcadia dream URL.
//
// If the URL is invalid, ErrInvalidDreamURL will be returned without a
// PounceDream object.
//
// If a dream record for the given dream URL already exists, then the existing
// record is returned instead, along with ErrExists.
func (p *PounceList) AddDreamURL(url string) (*PounceDream, error) {
shortURL, err := GetShortDreamURL(url)
if err != nil {
return nil, err
}
return p.__addDream(shortURL, url)
}
// DeleteDream deletes a dream from the Pounce list.
// If no dream by this criteria exists, the method returns ErrNotFound.
func (p *PounceList) DeleteDream(uploaderName, dreamName string) error {
shortURL := GetShortDreamURLFromName(uploaderName, dreamName)
return p.__delDream(shortURL)
}
// DeleteDreamURL deletes a dream from the Pouince list.
// If no dream by this dream URL exists, the method returns ErrNotFound.
// If provided dream URL is invalid, the method returns ErrInvalidDreamURL.
func (p *PounceList) DeleteDreamURL(url string) error {
shortURL, err := GetShortDreamURL(url)
if err != nil {
return err
}
return p.__delDream(shortURL)
}
///////////////////////////////////////////////////////////////////////////////////////////////////
func (p *PounceList) __addDream(shortURL, title string) (*PounceDream, error) {
oldDream, ok := p.Dreams[shortURL]
if ok {
return oldDream, ErrExists
}
dream := p._newDream(shortURL, title)
p.Dreams[shortURL] = dream
return dream, nil
}
func (p *PounceList) __delDream(shortURL string) error {
_, ok := p.Dreams[shortURL]
if !ok {
return ErrNotFound
}
delete(p.Dreams, shortURL)
return nil
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// _newFurre creates a new PounceFurre object with the given display name.
//
// displayName is the full name of the furre; it may contain pipes or spaces
// in it.
func (p *PounceList) _newFurre(displayName string) *PounceFurre {
return &PounceFurre{
DisplayName: displayName,
LastState: StateUnknown,
}
}
// _newDream creates a new PounceDream object with the given parameters.
//
// The "title" argument specifies a "name" for this dream. If title is an empty
// string, then the title will be this dream's full dream url.
func (p *PounceList) _newDream(shortDreamURL, title string) *PounceDream {
pd := &PounceDream{
Title: title,
ShortURL: shortDreamURL,
LastState: StateUnknown,
}
// if no title provided, use the full dream URL as the title
if pd.Title == "" {
pd.Title = pd.GetDreamURL()
}
return pd
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// TYPE: PounceResponse ///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
const MIN_POLL_INTERVAL_MSEC = 10000
type PounceResponse struct {
NumOnlineFurres int // O422
NumOnlineDreams int // D751
PollIntervalMsec int // T30000
OnlineFurres []string // @10 Albert|Quirky
OnlineDreams []string // #1furc://allegria:/ #1furc://allegria/
}
func ParsePounceResponse(r []byte) (*PounceResponse, error) {
//// Example Input:
// https://on.furcadia.com/q/?u[]=artex&u[]=albertquirky&d[]=allegria&d[]=ansteorrakingdom
//
//// Example Output:
// T30000 @10 Albert|Quirky O422 #1furc://allegria:/ #1furc://allegria/ #1furc://ansteorrakingdom:medievaltradecity/ #1furc://ansteorrakingdom/ #1furc://ansteorrakingdom:medievaltradecity/ #1furc://ansteorrakingdom/ D751
//
// TODO: Split into lines
// TODO: Process each line
return nil, ErrNotImplemented
}
// ToBytes creates a response string much like a pounce server would produce.
// This method is mostly useful for testing, or if you wish to implement your
// own pounce service that's compatible with the official Pounce client.
func (r *PounceResponse) ToBytes() (output []byte) {
output = fmt.Appendf(output, "T%d\n", r.PollIntervalMsec)
for _, furre := range r.OnlineFurres {
output = fmt.Appendf(output, "@10 %s\n", furre)
}
output = fmt.Appendf(output, "O%d\n", r.NumOnlineFurres)
for _, dreamUrl := range r.OnlineDreams {
output = fmt.Appendf(output, "#1%s\n", dreamUrl)
}
output = fmt.Appendf(output, "D%d\n", r.NumOnlineDreams)
return
}