Initial commit
This commit is contained in:
32
furcadia/character.go
Normal file
32
furcadia/character.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package furcadia
|
||||
|
||||
type Character struct {
|
||||
// e-mail address of the Furcadia account (e.g., artex@furcadia.com)
|
||||
AccountEmail string
|
||||
|
||||
// password for the Furcadia account
|
||||
AccountPassword string
|
||||
|
||||
// display name of the character (with regular spaces) (e.g., "Ice Dragon")
|
||||
DisplayName string
|
||||
|
||||
// shortname of the character (derived from DisplayName) (e.g., "icedragon")
|
||||
ShortName string
|
||||
|
||||
// Furcadia color string
|
||||
Color []byte
|
||||
|
||||
// description text
|
||||
Description string
|
||||
}
|
||||
|
||||
func NewCharacter(account, password, displayName string, color []byte, desc string) (*Character, error) {
|
||||
return &Character{
|
||||
AccountEmail: account,
|
||||
AccountPassword: password,
|
||||
DisplayName: displayName,
|
||||
ShortName: GetShortName(displayName),
|
||||
Color: append([]byte(nil), color...),
|
||||
Description: desc,
|
||||
}, nil
|
||||
}
|
||||
75
furcadia/client.go
Normal file
75
furcadia/client.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package furcadia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// TYPE: FurcadiaClient ///////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type FurcadiaClient struct {
|
||||
// Furcadia character to authenticate as
|
||||
Character *Character
|
||||
|
||||
// Network connection object
|
||||
_Conn net.Conn
|
||||
}
|
||||
|
||||
func NewFurcadiaClient(c Character) *FurcadiaClient {
|
||||
return &FurcadiaClient{
|
||||
Character: &c,
|
||||
_Conn: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// IsConnected returns true if this furcadia client instance had Connect()
|
||||
// called on it at least once.
|
||||
func (fc *FurcadiaClient) IsConnected() bool {
|
||||
return fc._Conn != nil
|
||||
}
|
||||
|
||||
// Connect establishes a connection to the Furcadia game server
|
||||
// (for the first time).
|
||||
//
|
||||
// Note: this method CANNOT be called twice on the same client instance!
|
||||
func (fc *FurcadiaClient) Connect(address string) error {
|
||||
if fc.IsConnected() {
|
||||
return ErrAlreadyConnected
|
||||
}
|
||||
|
||||
c, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
log.Printf("[%v] Cannot connect to %v: %v", fc.String(), address, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[%v] Connected to %v...", fc.String(), address)
|
||||
fc._Conn = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes an existing connection to the Furcadia game server.
|
||||
// Calling this method allows future calls to Connect() again. If this instance
|
||||
// was not connected at the time, nothing will happen and nil is returned.
|
||||
//
|
||||
// If closing the underlying network connection produces an error, it will be
|
||||
// returned back to the caller. In either case, the connection will be closed
|
||||
// and no longer owned by this instance.
|
||||
func (fc *FurcadiaClient) Disconnect() error {
|
||||
if !fc.IsConnected() {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := fc._Conn.Close()
|
||||
fc._Conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns a string representation of a FurcadiaClient instance for
|
||||
// printing/logging purposes
|
||||
func (fc *FurcadiaClient) String() string {
|
||||
return fmt.Sprintf("FurcadiaClient<%s>", fc.Character.DisplayName)
|
||||
}
|
||||
9
furcadia/consts.go
Normal file
9
furcadia/consts.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package furcadia
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const (
|
||||
DEFAULT_GAMESERVER_ADDRESS = "lightbringer.furcadia.com:6500"
|
||||
)
|
||||
34
furcadia/errors.go
Normal file
34
furcadia/errors.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package furcadia
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ERRORS /////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var (
|
||||
// ErrAlreadyConnected indicates that a FurcadiaClient instance has already
|
||||
// been connected via a Connect() call, and cannot have it done twice.
|
||||
ErrAlreadyConnected = errors.New("already connected")
|
||||
|
||||
// ErrExists is returned when we are trying to add an entry, but an
|
||||
// equivalent entry is already present and would have otherwise been
|
||||
// overwritten.
|
||||
ErrExists = errors.New("entry exists")
|
||||
|
||||
// ErrInvalidDreamURL is returned when a provided dream url is invalid.
|
||||
ErrInvalidDreamURL = errors.New("invalid dream url")
|
||||
|
||||
// ErrNotFound is returned when we were asked to change an entry, but there
|
||||
// is no entry on record.
|
||||
//
|
||||
// This is not necessarily an error if, say, we are deleting a record which
|
||||
// was already deleted. In such cases, this errors may be ignored.
|
||||
ErrNotFound = errors.New("entry not found")
|
||||
|
||||
// ErrNotImplemented indicates that a requested feature has not yet been
|
||||
// implemented and cannot be used.
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
)
|
||||
292
furcadia/pounce.go
Normal file
292
furcadia/pounce.go
Normal file
@@ -0,0 +1,292 @@
|
||||
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
|
||||
}
|
||||
298
furcadia/pounce_test.go
Normal file
298
furcadia/pounce_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package furcadia
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddFurre(t *testing.T) {
|
||||
testNames := []string{
|
||||
"Artex",
|
||||
"Albert Quirky",
|
||||
"A|B|C",
|
||||
"#SA",
|
||||
}
|
||||
|
||||
pounce := NewPounceList()
|
||||
|
||||
// Success Scenarios //////////////////////////////////////////////////////////////////////////
|
||||
|
||||
for _, name := range testNames {
|
||||
// check AddFurre() success
|
||||
furre, err := pounce.AddFurre(name)
|
||||
if err != nil {
|
||||
t.Errorf("AddFurre(%v) failed: %v", name, err)
|
||||
}
|
||||
|
||||
// check display name
|
||||
if furre.DisplayName != name {
|
||||
t.Errorf("AddFurre(%v).DisplayName == %v (mismatch)", name, furre.DisplayName)
|
||||
}
|
||||
|
||||
// check shortname
|
||||
expShort := GetShortName(name)
|
||||
furreShort := furre.GetShortName()
|
||||
if furreShort != expShort {
|
||||
t.Errorf("AddFurre(%v).GetShortName() -> %v (expected: %v)", name, furreShort, expShort)
|
||||
}
|
||||
}
|
||||
|
||||
if len(pounce.Furres) != len(testNames) {
|
||||
t.Errorf("Length inconsistency: len(pounce.Furres)=%v, len(testNames)=%v", len(pounce.Furres), len(testNames))
|
||||
}
|
||||
|
||||
// Bad Scenarios //////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// already on record
|
||||
furre, err := pounce.AddFurre("||ARTEX||")
|
||||
if err == nil {
|
||||
t.Errorf("AddFurre(||ARTEX||) unexpectedly succeeded: %v", furre)
|
||||
}
|
||||
|
||||
// already on record
|
||||
furre, err = pounce.AddFurre("albertquirky")
|
||||
if err == nil {
|
||||
t.Errorf("AddFurre(albertquirky) unexpectedly succeeded: %v", furre)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFurre(t *testing.T) {
|
||||
testNames := []string{
|
||||
"Artex",
|
||||
"Albert Quirky",
|
||||
"A|B|C",
|
||||
"#SA",
|
||||
}
|
||||
|
||||
// create a pounce list and populate it with names
|
||||
pounce := NewPounceList()
|
||||
for _, name := range testNames {
|
||||
pounce.AddFurre(name)
|
||||
}
|
||||
|
||||
// delete using a different display name - should work
|
||||
err := pounce.DeleteFurre("ALBERT|QUIRKY")
|
||||
if err != nil {
|
||||
t.Errorf("DeleteFurre(ALBERT|QUIRKY) failed: %v", err)
|
||||
}
|
||||
if len(pounce.Furres) != len(testNames)-1 {
|
||||
t.Errorf("DeleteFurre(ALBERT|QUIRKY) caused invalid furre count: %v (testNames: %v)", len(pounce.Furres), len(testNames))
|
||||
}
|
||||
|
||||
// delete for the second time - expect ErrNotFound
|
||||
err = pounce.DeleteFurre("Albert Quirky")
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("DeleteFurre(Albert Quirky) for a missing record produced: %v (expected ErrNotFound)", err)
|
||||
}
|
||||
|
||||
// delete everyone
|
||||
for _, name := range testNames {
|
||||
pounce.DeleteFurre(name)
|
||||
}
|
||||
if len(pounce.Furres) != 0 {
|
||||
t.Errorf("Mass-delete did not empty furre list: %v", pounce.Furres)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDream(t *testing.T) {
|
||||
pounce := NewPounceList()
|
||||
dream, err := pounce.AddDream("Ice Dragon", "Some dream name")
|
||||
if err != nil {
|
||||
t.Errorf("AddDream() failed: %v", err)
|
||||
}
|
||||
|
||||
// test basic output
|
||||
if dream.Title != "Ice Dragon: Some dream name" {
|
||||
t.Errorf("AddDream(): unexpected title '%v'", dream.Title)
|
||||
}
|
||||
if dream.ShortURL != "icedragon:somedreamname" {
|
||||
t.Errorf("AddDream(): unexpected short url '%v'", dream.ShortURL)
|
||||
}
|
||||
if dream.LastState != StateUnknown {
|
||||
t.Errorf("AddDream(): unexpected initial state '%v'", dream.LastState)
|
||||
}
|
||||
if dream.LastState.IsOnline() {
|
||||
t.Errorf("AddDream(): dream is with unknown state is initially online")
|
||||
}
|
||||
if dream.GetDreamURL() != "furc://icedragon:somedreamname/" {
|
||||
t.Errorf("AddDream(): unexpected URL from GetDreamURL(): '%v'", dream.GetDreamURL())
|
||||
}
|
||||
|
||||
// try to add this dream again; make sure it fails
|
||||
d, err := pounce.AddDream("icedragon", "some_dream_name")
|
||||
if !errors.Is(err, ErrExists) {
|
||||
t.Errorf("AddDream(): duplicate add did not produce ErrExists, but %T(%v)", err, err)
|
||||
}
|
||||
if d != dream {
|
||||
t.Errorf("AddDream(): expected existing dream, but got %T(%v)", d, d)
|
||||
}
|
||||
|
||||
// try to add an analogous dream via dreamurl; make sure it also fails
|
||||
d, err = pounce.AddDreamURL(dream.GetDreamURL())
|
||||
if !errors.Is(err, ErrExists) {
|
||||
t.Errorf("AddDreamURL(): analogous duplicate add did not produce ErrExists, but %T(%v)", err, err)
|
||||
}
|
||||
if d != dream {
|
||||
t.Errorf("AddDreamURL(): expected existing dream, but got %T(%v)", d, d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDreamURL(t *testing.T) {
|
||||
const longURL = "furc://a_r_t_e_x:some_dream_name"
|
||||
const normalURL = "furc://artex:somedreamname/"
|
||||
const shortURL = "artex:somedreamname"
|
||||
|
||||
pounce := NewPounceList()
|
||||
dream, err := pounce.AddDreamURL(longURL)
|
||||
if err != nil {
|
||||
t.Errorf("AddDreamURL() failed: %v", err)
|
||||
}
|
||||
|
||||
// test basic output
|
||||
if dream.Title != longURL {
|
||||
t.Errorf("AddDreamURL(): unexpected title '%v' (expected '%v')", dream.Title, longURL)
|
||||
}
|
||||
if dream.ShortURL != shortURL {
|
||||
t.Errorf("AddDreamURL(): unexpected dream short url '%v' (expected '%v')", dream.ShortURL, shortURL)
|
||||
}
|
||||
if dream.LastState != StateUnknown {
|
||||
t.Errorf("AddDream(): unexpected initial state '%v'", dream.LastState)
|
||||
}
|
||||
if dream.LastState.IsOnline() {
|
||||
t.Errorf("AddDream(): dream is with unknown state is initially online")
|
||||
}
|
||||
if dream.GetDreamURL() != normalURL {
|
||||
t.Errorf("AddDream(): unexpected URL from GetDreamURL(): '%v' (expected: '%v')", dream.GetDreamURL(), normalURL)
|
||||
}
|
||||
|
||||
// try to add this dream again; make sure it fails
|
||||
testDup := func(url string) {
|
||||
d, err := pounce.AddDreamURL(url)
|
||||
if !errors.Is(err, ErrExists) {
|
||||
t.Errorf("AddDreamURL(): duplicate add for '%v' did not produce ErrExists, but %T(%v)", url, err, err)
|
||||
}
|
||||
|
||||
// in case of ErrExists, d is the existing dream object
|
||||
if d != dream {
|
||||
t.Errorf("AddDreamURL(): expected existing dream for '%v', but got %+v", url, d)
|
||||
}
|
||||
}
|
||||
testDup(longURL)
|
||||
testDup(normalURL)
|
||||
|
||||
// try to add analogous dream via AddDream(); make sure it also fails
|
||||
d, err := pounce.AddDream("Artex", "Some Dream Name")
|
||||
if !errors.Is(err, ErrExists) {
|
||||
t.Errorf("AddDream(): duplicate analogous add did not produce ErrExists, but %T(%v)", err, err)
|
||||
}
|
||||
if d != dream {
|
||||
// in case of ErrExists, d is the existing dream object
|
||||
t.Errorf("AddDream(): expected existing dream, but got %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDream(t *testing.T) {
|
||||
const uploader = "Ice Dragon"
|
||||
const dreamName = "Some dream name"
|
||||
|
||||
pounce := NewPounceList()
|
||||
|
||||
// try deleting a non-existant dream
|
||||
err := pounce.DeleteDream(uploader, dreamName)
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("DeleteDream(): expected ErrNotFound for non-existent dream, but got %T(%v)", err, err)
|
||||
}
|
||||
|
||||
// add the dream
|
||||
_, err = pounce.AddDream(uploader, dreamName)
|
||||
if err != nil {
|
||||
t.Errorf("AddDream() failed: %v", err)
|
||||
}
|
||||
|
||||
// delete should succeed now
|
||||
err = pounce.DeleteDream(uploader, dreamName)
|
||||
if err != nil {
|
||||
t.Errorf("DeleteDream() unexpectedly failed with %T(%v)", err, err)
|
||||
}
|
||||
|
||||
// dream is no longer available again
|
||||
err = pounce.DeleteDream(uploader, dreamName)
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("DeleteDream(): expected ErrNotFound for deleted dream, but got %T(%v)", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteDreamURL(t *testing.T) {
|
||||
const dreamURL = "furc://icedragon:somedreamname"
|
||||
|
||||
pounce := NewPounceList()
|
||||
|
||||
// try deleting a non-existant dream
|
||||
err := pounce.DeleteDreamURL(dreamURL)
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("DeleteDreamURL(): expected ErrNotFound for non-existent dream, but got %T(%v)", err, err)
|
||||
}
|
||||
|
||||
// add the dream
|
||||
_, err = pounce.AddDreamURL(dreamURL)
|
||||
if err != nil {
|
||||
t.Errorf("AddDreamURL() failed: %v", err)
|
||||
}
|
||||
|
||||
// delete should succeed now
|
||||
err = pounce.DeleteDreamURL(dreamURL)
|
||||
if err != nil {
|
||||
t.Errorf("DeleteDreamURL() unexpectedly failed with %T(%v)", err, err)
|
||||
}
|
||||
|
||||
// dream is no longer available again
|
||||
err = pounce.DeleteDreamURL(dreamURL)
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("DeleteDreamURL(): expected ErrNotFound for deleted dream, but got %T(%v)", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetQueryString(t *testing.T) {
|
||||
pounce := NewPounceList()
|
||||
pounce.AddFurre("Artex")
|
||||
pounce.AddFurre("Albert Quirky")
|
||||
pounce.AddFurre("|Test|Alt|666#SA")
|
||||
pounce.AddDream("Allegria", "")
|
||||
pounce.AddDream("FurN", "")
|
||||
pounce.AddDream("Bramblebrook", "Welcome")
|
||||
pounce.AddDreamURL("furc://artex")
|
||||
pounce.AddDreamURL("furc://artex:somename/")
|
||||
|
||||
query := pounce.GetQueryString() + "&"
|
||||
expectedSubs := []string{
|
||||
"u[]=artex&",
|
||||
"u[]=albertquirky&",
|
||||
"u[]=testalt666sa&",
|
||||
"d[]=allegria&",
|
||||
"d[]=furn&",
|
||||
"d[]=bramblebrook:welcome&",
|
||||
"d[]=artex&",
|
||||
"d[]=artex:somename&",
|
||||
}
|
||||
unexpectedSubs := []string{
|
||||
"&&",
|
||||
}
|
||||
|
||||
nItems := len(strings.Split(query, "&"))
|
||||
if nItems != len(expectedSubs)+1 {
|
||||
t.Errorf("Unexpected number of items (%v) in query: %v", nItems, query)
|
||||
}
|
||||
|
||||
for _, sub := range expectedSubs {
|
||||
if !strings.Contains(query, sub) {
|
||||
t.Errorf("Missing substring '%v' in query: %v", sub, query)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sub := range unexpectedSubs {
|
||||
if strings.Contains(query, sub) {
|
||||
t.Errorf("Unexpected substring '%v' in query: %v", sub, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
77
furcadia/util.go
Normal file
77
furcadia/util.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package furcadia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetShortName returns a "shortname" derived from a given Furcadia display name
|
||||
func GetShortName(displayName string) string {
|
||||
sb := &strings.Builder{}
|
||||
for _, r := range displayName {
|
||||
if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||||
sb.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// GetShortDreamURL converts a regular "furc://uploader:name/" dream url into
|
||||
// a "short" form that can be used as a hash key (e.g., "uploader:name").
|
||||
//
|
||||
// ErrInvalidDreamURL is returned if the given url is not a valid dream URL.
|
||||
func GetShortDreamURL(url string) (string, error) {
|
||||
// cut prefix
|
||||
u, found := strings.CutPrefix(strings.ToLower(url), "furc://")
|
||||
if !found {
|
||||
return "", ErrInvalidDreamURL
|
||||
}
|
||||
|
||||
// cut suffix
|
||||
u = strings.SplitN(u, "/", 2)[0]
|
||||
|
||||
// split uploader name and dream name
|
||||
var uploaderName string
|
||||
var dreamName string
|
||||
|
||||
elements := strings.Split(u, ":")
|
||||
switch len(elements) {
|
||||
case 0:
|
||||
panic(fmt.Sprintf("len(%v) == 0", elements))
|
||||
case 1:
|
||||
uploaderName = elements[0]
|
||||
dreamName = ""
|
||||
case 2:
|
||||
uploaderName = elements[0]
|
||||
dreamName = elements[1]
|
||||
default:
|
||||
return "", ErrInvalidDreamURL
|
||||
}
|
||||
|
||||
short := GetShortDreamURLFromName(uploaderName, dreamName)
|
||||
if short == "" || short[0] == ':' {
|
||||
// furc://
|
||||
// furc:///
|
||||
// furc://:/
|
||||
// furc://:something/
|
||||
return "", ErrInvalidDreamURL
|
||||
}
|
||||
|
||||
return short, nil
|
||||
}
|
||||
|
||||
func GetShortDreamURLFromName(uploaderName, dreamName string) (short string) {
|
||||
shortName := GetShortName(uploaderName)
|
||||
shortDream := GetShortName(dreamName)
|
||||
if shortDream == "" {
|
||||
short = shortName
|
||||
} else {
|
||||
short = shortName + ":" + shortDream
|
||||
}
|
||||
return
|
||||
}
|
||||
67
furcadia/util_test.go
Normal file
67
furcadia/util_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package furcadia
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetShortName(t *testing.T) {
|
||||
check := func(displayName, expectedShortname string) {
|
||||
short := GetShortName(displayName)
|
||||
if short != expectedShortname {
|
||||
t.Errorf("GetShortName('%v') -> '%v' (expected: '%v')", displayName, short, expectedShortname)
|
||||
}
|
||||
}
|
||||
|
||||
check("ArTeX", "artex")
|
||||
check("Ice|Dragon", "icedragon")
|
||||
check("123_arte_X", "123artex")
|
||||
check("|~!@#$%^&*()_+<>[]{}:;'\",./?artex ", "artex")
|
||||
}
|
||||
|
||||
func TestGetShortDreamURL(t *testing.T) {
|
||||
check := func(dreamURL, expectedShortURL string) {
|
||||
short, err := GetShortDreamURL(dreamURL)
|
||||
if err != nil {
|
||||
t.Errorf("GetShortDreamURL('%v') -> unexpected error %T (%v)", dreamURL, err, err)
|
||||
}
|
||||
if short != expectedShortURL {
|
||||
t.Errorf("GetShortDreamURL('%v') -> '%v' (expected: '%v')", dreamURL, short, expectedShortURL)
|
||||
}
|
||||
}
|
||||
|
||||
check("furc://ice_dragon:some-dream-name/128", "icedragon:somedreamname")
|
||||
check("furc://artex", "artex")
|
||||
check("furc://artex:", "artex")
|
||||
check("furc://artex/128", "artex")
|
||||
check("furc://name:", "name")
|
||||
|
||||
checkErr := func(dreamURL string, expectedErr error) {
|
||||
_, err := GetShortDreamURL(dreamURL)
|
||||
if !errors.Is(err, expectedErr) {
|
||||
t.Errorf("GetShortDreamURL('%v') -> unexpected error %T (%v)", dreamURL, err, err)
|
||||
}
|
||||
}
|
||||
|
||||
checkErr("furc://", ErrInvalidDreamURL)
|
||||
checkErr("furc:///", ErrInvalidDreamURL)
|
||||
checkErr("furc://:", ErrInvalidDreamURL)
|
||||
checkErr("furc://:/", ErrInvalidDreamURL)
|
||||
checkErr("furc://:nouploader", ErrInvalidDreamURL)
|
||||
checkErr("furc://:nouploader/", ErrInvalidDreamURL)
|
||||
checkErr("furc://artex::", ErrInvalidDreamURL)
|
||||
}
|
||||
|
||||
func TestGetShortDreamURLFromName(t *testing.T) {
|
||||
check := func(uploader, name, expected string) {
|
||||
short := GetShortDreamURLFromName(uploader, name)
|
||||
if short != expected {
|
||||
t.Errorf("GetShortDreamURLFromName('%v', '%v') -> '%v' (expected: '%v')", uploader, name, short, expected)
|
||||
}
|
||||
}
|
||||
|
||||
check("Ice Dragon", "", "icedragon")
|
||||
check("Ice Dragon", "Some name here", "icedragon:somenamehere")
|
||||
check("icedragon", "some_name_here", "icedragon:somenamehere")
|
||||
check("", "Illegal but possible", ":illegalbutpossible")
|
||||
}
|
||||
Reference in New Issue
Block a user