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 }