Initial commit

This commit is contained in:
2026-01-19 01:51:08 +02:00
commit 2dbb7e5d7e
18 changed files with 1475 additions and 0 deletions

32
furcadia/character.go Normal file
View 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
View 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
View File

@@ -0,0 +1,9 @@
package furcadia
///////////////////////////////////////////////////////////////////////////////////////////////////
// CONSTANTS //////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
const (
DEFAULT_GAMESERVER_ADDRESS = "lightbringer.furcadia.com:6500"
)

34
furcadia/errors.go Normal file
View 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
View 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
View 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
View 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
View 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")
}