the Worthless Writeup Library @ szy.lol

Inoue API

launched on 2022-10

Inoue API is the serverside component to Inoue, an API to download replays off TETR.IO.


TETR.IO is a competitive block stacking game, and is as such related to Tower Bloxx and Minecraft Build Battles, and maaybe distantly to Tetris. When you compete in stacking tetraminos either for time (40L), score (Blitz), or glory (TL), the games are saved as replays. They can then be reviewed.

Sadly, while the official API returns a lot of information about your account and recent games, it does not provide a way to download a replay of a game. This can only be done in-game, and the data is received through the protected game API. Because TETR.IO players enjoy poking the replays for new and exciting ways to slice and dice the data within them into horoscopes, as well as just archiving them (which is what my related Inoue project does), I run a proxy between unauthenticated Beanserver Blasters* and the highly protected licensed TETR.IO game API.

The proxy is a Go program (used to be PHP pile) that logs in as a bot account and downloads requested games, packages them for consumption, and sends them back. For obvious reasons, I can’t share the source code, but it’s quite simple, receiving an HTTP request, making a matching request to the game, turning the response into a replay and sending it back to the client. Except for the part where I decided to write my own JSON parser.

I wrote my own JSON parser because I don’t trust Go’s native one, kind of. It has a lot of smartness in form of turning JSON directly into Go structs, which is undeniably neat when working with your own data, but I don’t quite trust myself to use all this machinery correctly and not fuck up the data, somehow. Therefore, jcrap exists. It only parses one object down, returning a map of key to value as string (you can substring values in JSON and they are still valid). It could be made zero-copy, returning slices of the original JSON string, but it was easier to reason about consuming the input a character at a time by copying. I will probably make this change if this setup turns out to be too inefficient.

Listing of jcrap
package main

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"strings"
)

var keyErr = errors.New("json: expected key name")
var objErr = errors.New("json: not an object")

func readStr(r *bufio.Reader) (s string, err error) {
	// assumes " already read!!
	for {
		seg, err := r.ReadString('"')
		if err != nil {
			return seg, fmt.Errorf("%w while reading key name", err)
		}
		if strings.HasSuffix(s, "\\\"") {
			s = s + strings.TrimSuffix(seg, "\\\"")
		} else {
			return s + strings.TrimSuffix(seg, "\""), nil
		}
	}
}

func skipWhitespace(r *bufio.Reader) error {
	for {
		char, err := r.ReadByte()
		if err != nil {
			return fmt.Errorf("%w while reading whitespace", err)
		}
		switch char {
		case ' ':
		case '\r':
		case '\n':
		case '\t':
			break
		default:
			return r.UnreadByte()
		}
	}
}

func passString(r *bufio.Reader, w *bytes.Buffer) error {
	// assumes " already read!!
	for {
		seg, err := r.ReadBytes('"')
		if err != nil {
			return fmt.Errorf("%w while reading string", err)
		}
		w.Write(seg)
		if !bytes.HasSuffix(seg, []byte("\\\"")) {
			return nil
		} // else have another go
	}
}

func passObject(r *bufio.Reader, w *bytes.Buffer) error {
	level := 0
	for {
		char, err := r.ReadByte()
		if err != nil {
			return fmt.Errorf("%w while reading object", err)
		}
		shouldWrite, shouldExit := true, false
		switch char {
		case '{':
			level++
		case '}':
			level--
			if level == 0 {
				shouldExit = true
			}
			if level < 0 {
				// inner (passed) object ended and we ran into outer's brace
				r.UnreadByte()
				shouldWrite = false
				shouldExit = true
			}
		case '[':
			level++
		case ']':
			level--
			if level == 0 {
				shouldExit = true
			}
			// i think we should never run into the same level < 0 case as for objects above,
			// because only objects are supported at outermost level.
			// in case this assumption is wrong, here's where to fix it :D
		case ',':
			if level == 0 {
				r.UnreadByte()
				shouldWrite = false
				shouldExit = true
			}
		case '"':
			w.WriteByte(char)
			shouldWrite = false
			err = passString(r, w)
			if err != nil {
				return err
			}
		default:
			/* none */
		}
		if shouldWrite {
			w.WriteByte(char)
		}
		if shouldExit {
			return nil
		}
	}
}

func jsonParse(rdr io.Reader) (is map[string][]byte, err error) {
	// can accept malformed json but its fiine, it only gets Trusted Inputs
	r := bufio.NewReader(rdr)
	is = make(map[string][]byte)
	b, err := r.ReadByte()
	if b != '{' {
		return is, objErr
	}
	for {
		err = skipWhitespace(r)
		if err != nil {
			return
		}
		b, err = r.ReadByte()
		if err != nil {
			return
		}
		if b == '}' {
			return is, nil
		}
		if b == ',' {
			continue
		}
		if b != '"' {
			return is, keyErr
		}
		name, err := readStr(r)
		if err != nil {
			return is, err
		}
		_, err = r.ReadBytes(':')
		if err != nil {
			return is, err
		}
		err = skipWhitespace(r)
		if err != nil {
			return is, err
		}
		vbuf := bytes.Buffer{}
		err = passObject(r, &vbuf)
		if err != nil {
			return is, err
		}
		is[name] = vbuf.Bytes()
	}
}

// there is a joke about pass-through to be made

i should get back into tetris someday

* Beanserver Blasters™ is a term coined by osk for applications using (and often abusing) tetrio APIs, named after the Google Sheets useragent which contains beanserver; for reasons unknown to me. Yes, there are Google Sheets for TETR.IO game analysis.