# rotur

> Talk to rotur.dev from OSL: sign in, read the feed, post, manage groups and storage, listen in realtime, and sign and encrypt messages.

This is the Markdown version of https://osl-lang.org/packages/rotur. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:rotur"
```

`std:rotur` is a client for the rotur.dev API. Almost everything the website does is a method here: accounts, posts, friends, groups, the economy, key-value storage, signing and encryption, and a realtime socket for presence and rooms.

Calls talk to the network, so most examples on this page compile but don't run in the browser. Run them on your machine. A few helpers that work offline run right here.

## A client

`rotur.new(token)` makes a client. With no token (or `""`) it is anonymous and can call public endpoints:

```osl
import "std:rotur"

auto rotur_client = rotur.new()
auto feed = rotur_client.posts().list({limit: 3}).unwrap()
for post in feed (
  log post
)
```

Every API method returns a `result`. The response is the API's own JSON, so keys keep the names rotur.dev uses. Errors have a kind you can match on: `NotFoundError` (404), `PermissionError` (no token, or refused), `ValueError` (a rejected request), `StateError` (conflicts and rate limits), `TimeoutError` and `NetworkError`.

## Signing in

The first time, `client.login()` runs the device flow. It prints a code, you approve it on rotur.dev, and the token is saved where the rotur command line tool keeps it. After that `rotur.load()` finds it again. A program asks only once:

```osl
import "std:rotur"

*rotur.Client client = rotur.load().unwrap()
if !client.hasToken() (
  client.login().unwrap()
)
log "signed in as " ++ client.username().unwrap()
```

`ROTUR_TOKEN` in the environment also works, and is checked first. To skip the saved file, build the client with `rotur.new(token)`.

## Check who you are

`client.whoami()` asks the API what the token is allowed to do:

```osl
import "std:rotur"

*rotur.Client client = rotur.load().unwrap()
auto me = client.whoami().unwrap()
log me.username me.token_type
```

A call that needs a token without one fails with `PermissionError`, which you can handle:

```osl
import "std:rotur"

auto client = rotur.new()
auto attempt = client.posts().create("hello")
if attempt.isErr() (
  log attempt.unwrapErr().kind
)
```

**Quiz.** `rotur.new()` with no token calls `client.posts().list()`. What happens?

- [x] It works: the public feed needs no token.
- [ ] It fails with `PermissionError`.
- [ ] It throws and stops the program.

Answer: Anonymous clients can call public endpoints. `posts().create` needs a token and would fail with `PermissionError`.

## Posts and people

Each area of the API is a group of methods on the client: `posts()`, `friends()`, `following()`, `groups()`, `profiles()`, `notifications()`, `storage()` and more. They read like the website:

```osl
import "std:rotur"

*rotur.Client client = rotur.load().unwrap()

auto post = client.posts().create("Hello from OSL", {profileOnly: true}).unwrap()
client.posts().like(post.id).unwrap()
client.posts().reply(post.id, "A reply").unwrap()

log client.profiles().userExists("mist").unwrap()
for name in ["a", "b"] (
  client.following().follow(name).unwrap()
)
```

`posts().find(query)` searches, `posts().popular()` is the front page, and `groups()` has methods for creating, inviting, roles, bans and events.

## Storage

Your account has a small key-value store per application id. It is how a program remembers things across machines:

```osl
import "std:rotur"

*rotur.Client client = rotur.load().unwrap()
auto store = client.storage()

store.set("my-app", "theme", "dark").unwrap()
log store.value("my-app", "theme").unwrap()
store.delete("my-app", "theme").unwrap()
```

## Endpoints without a method

`client.request(method, path, options)` calls any endpoint the client has no method for, with `query` and `body` options. The token goes only to the client's own host:

```osl
import "std:rotur"

auto client = rotur.new()
auto limits = client.request("GET", "/limits").unwrap()
log limits
```

## Realtime

`client.socket()` is a websocket connection to rotur. `start()` connects and authenticates, `join` enters rooms, and `on(event, handler)` listens. Handlers run on background threads:

```osl
import "std:rotur"
import "std:thread"

*rotur.Client client = rotur.load().unwrap()
auto socket = client.socket()

socket.on("gmsg", def(any message) ( log message ))
socket.start().unwrap()
socket.join(["lobby"]).unwrap()
socket.broadcast("lobby", {hello: "world"}).unwrap()

thread.sleep(60000)
socket.stop()
```

## Offline helpers

These work without a network. `rotur.avatarURL`, `bannerURL`, `backgroundURL` and `overlayURL` build the image addresses for a user, and `version` busts the cache when a picture changes:

```osl
import "std:rotur"

log rotur.avatarURL("mist", {version: "2"})
```

Output:

```text
https://avatars.rotur.dev/mist?v=2
```

`rotur.canonical(value)` writes a value as canonical JSON: keys sorted, no spaces. Those bytes are what rotur signatures are made over, so two sides get the same bytes from the same data:

```osl
import "std:rotur"

log rotur.canonical({b: 1, a: [1, 2], c: "x"})
```

Output:

```text
{"a":[1,2],"b":1,"c":"x"}
```

`rotur.generateKey()` makes an Ed25519 key pair as JWK objects, and `rotur.signWith(seed, content)` and `rotur.verifyWith(publicKey, content, signature)` sign and check. For a signed-in client, `client.signing()` signs content as you, encrypts to another user, and decrypts what was sent to you.

```osl
import "std:rotur"

auto pair = rotur.generateKey().unwrap()
log pair.algorithm
```

Output:

```text
Ed25519
```

### Exercise: Same bytes

Two programs describe the same data with keys in different orders. Print `true` if `rotur.canonical` gives the same text for both.

Starting code:

```osl
import "std:rotur"

auto first = {name: "Ada", age: 36}
auto second = {age: 36, name: "Ada"}

log first == second
```

It should print:

```text
true
```

Hint 1: Call `rotur.canonical` on each and compare the strings.

A solution:

```osl
import "std:rotur"

auto first = {name: "Ada", age: 36}
auto second = {age: 36, name: "Ada"}

log rotur.canonical(first) == rotur.canonical(second)
```

## API reference

## API reference

Every member of `std:rotur`, generated from the source of OSL.

### Functions

#### `rotur.avatarURL(string username, object? options) string`

avatarURL is the profile picture of username on avatars.rotur.dev.
Options: version (string appended as ?v= so a changed picture isn't
served from cache).

#### `rotur.backgroundURL(string username, object? options) string`

backgroundURL is the profile background video of username. Options as
for avatarURL.

#### `rotur.bannerURL(string username, object? options) string`

bannerURL is the profile banner of username. Options as for avatarURL.

#### `rotur.canonical(any value) string`

canonical serialises value as canonical JSON (sorted object keys, numbers
as JavaScript prints them), the bytes rotur signatures are made over.

#### `rotur.configPath() result<string, error>`

configPath is the rotur CLI config file (~/.config/rotur/config.json) that
load, loadToken, logout and client.saveToken share with the rotur CLI.
Fails with IOError when the home directory is unknown.

#### `rotur.generateKey() result<{algorithm: string, private_key_jwk: object, public_key_jwk: object}, error>`

generateKey makes a new Ed25519 key pair, returned as
{private_key_jwk, public_key_jwk, algorithm}. Fails with RuntimeError when
the system has no randomness.

#### `rotur.load() result<*rotur.Client, error>`

load builds a client from the saved token (see loadToken). Without one the
client is anonymous; check client.hasToken(). Fails like loadToken.

#### `rotur.loadToken() result<string?, error>`

loadToken returns the saved token: the ROTUR_TOKEN environment variable,
else the token in the rotur CLI config file, else null. Fails with IOError
or ParseError when the config file exists but can't be read.

#### `rotur.login(any ...timeoutSecs) *rotur.Client`

**Deprecated.** Deprecated: use client.login(options?) on rotur.new() instead.

login runs the device flow and returns the signed-in client, or null.

#### `rotur.logout() result<unit, error>`

logout removes the saved token and username from the rotur CLI config
file. Fails with IOError or ParseError when the file can't be updated.

#### `rotur.new(any ...token) *rotur.Client`

new builds a client: new(token?). Pass a rotur account token, or nothing
(or "") for an anonymous client that can only call public endpoints.

Every API method returns result<T, error> whose message carries the API's
own error text. The kinds are NotFoundError for a missing resource (HTTP
404), PermissionError when the client has no token or the API refuses the
request (401, 402, 403), ValueError for other rejected requests (4xx),
StateError for conflicts and rate limits (409, 429), TimeoutError,
NetworkError when the API can't be reached (and for 502 and 503), and
RuntimeError for anything else. Response objects keep the API's own keys.
Throws TypeError when token isn't a string.

#### `rotur.overlayURL(string username, object? options) string`

overlayURL is the avatar overlay of username. Options as for avatarURL.

#### `rotur.savedToken() string`

**Deprecated.** Deprecated: use rotur.loadToken() instead, which returns null when there is no token.

savedToken returns the saved token, or "" when there is none.

#### `rotur.signWith(string seed, any content) result<string, error>`

signWith signs content's canonical JSON with an Ed25519 seed given as
base64url (the private JWK's "d") and returns the base64url signature.
Fails with ValueError when seed isn't a 32-byte base64url seed.

#### `rotur.verifyWith(string publicKey, any content, string signature) result<boolean, error>`

verifyWith reports whether signature is a valid Ed25519 signature of
content's canonical JSON for publicKey (base64url, the JWK's "x"). A
well-formed signature that doesn't match is false. Fails with ValueError
when publicKey or signature isn't well-formed base64url of the right size.

### `*rotur.Client`

#### `client.accounts() *rotur.Accounts`

accounts registers accounts and handles password resets.

#### `client.ai() *rotur.Ai`

ai talks to the rotur AI.

#### `client.avatars() *rotur.Avatars`

avatars uploads and removes profile pictures, banners and overlays.

#### `client.baseUrl(string url) *rotur.Client`

**Deprecated.** Deprecated: use client.useBaseURL(url) instead.

baseUrl points the client at another API root and returns the client.

#### `client.cosmetics() *rotur.Cosmetics`

cosmetics browses, buys and equips profile cosmetics.

#### `client.delete(string path, any ...body) result`

**Deprecated.** Deprecated: use client.request("DELETE", path, {body}) instead.

delete calls DELETE path with an optional body; it needs a token.

#### `client.emojis() *rotur.Emojis`

emojis manages custom emojis.

#### `client.files() *rotur.Files`

files reads and writes the account's cloud files.

#### `client.following() *rotur.Following`

following follows users and lists followers.

#### `client.friends() *rotur.Friends`

friends manages friends and friend requests.

#### `client.get(string path, object? options) result`

**Deprecated.** Deprecated: use client.request("GET", path, {query}) instead.

get calls GET path with optional query params.

#### `client.gifts() *rotur.Gifts`

gifts creates and claims credit gifts.

#### `client.groups() *rotur.Groups`

groups manages groups, members, roles, events and products.

#### `client.hasToken() boolean`

hasToken reports whether the client has a token. It doesn't ask the API
whether the token is valid; whoami does that.

#### `client.items() *rotur.Items`

items manages tradeable items.

#### `client.keys() *rotur.Keys`

keys manages purchasable access keys.

#### `client.link() *rotur.Link`

link drives the rotur.dev/link device sign-in flow step by step.

#### `client.loggedIn() boolean`

**Deprecated.** Deprecated: use client.hasToken() instead.

loggedIn reports whether the client has a token.

#### `client.login(object? options) result<unit, error>`

login signs the client in with the rotur.dev/link device flow: it requests
a code, shows it, waits for the user to approve it on rotur.dev and then
uses the new token. Options: timeout (ms, default 180000), save (boolean,
default true: also store the token like saveToken), onCode (function
called with the code and the approval URL; without it the instructions are
printed). Fails with TimeoutError when nobody approves the code in time,
and with the usual API errors.

#### `client.me() *rotur.Me`

me is the signed-in account: profile data, credits, blocks, notes, billing.

#### `client.notifications() *rotur.Notifications`

notifications reads, marks and sends notifications.

#### `client.patch(string path, any ...body) result`

**Deprecated.** Deprecated: use client.request("PATCH", path, {body}) instead.

patch calls PATCH path with an optional body; it needs a token.

#### `client.pets() *rotur.Pets`

pets browses, buys and equips pets.

#### `client.post(string path, any ...body) result`

**Deprecated.** Deprecated: use client.request("POST", path, {body}) instead.

post calls POST path with an optional body.

#### `client.posts() *rotur.Posts`

posts creates, reads and reacts to posts.

#### `client.profiles() *rotur.Profiles`

profiles reads public profiles.

#### `client.push() *rotur.Push`

push manages web push devices and sends push notifications.

#### `client.put(string path, any ...body) result`

**Deprecated.** Deprecated: use client.request("PUT", path, {body}) instead.

put calls PUT path with an optional body; it needs a token.

#### `client.request(string method, string path, object? options) result<any, error>`

request calls any API endpoint the client has no method for. method is an
HTTP method and path is relative to baseURL ("/limits") or a full URL.
Options: query (object of query parameters), body (a string is sent as
text/plain, anything else as JSON). Returns the decoded JSON response, or
the text when it isn't JSON. The token is sent only to the scheme and
host of the client's base URL, never with a full URL to anywhere else.

#### `client.save(any ...username) boolean`

**Deprecated.** Deprecated: use client.saveToken(options?) instead.

save stores the token (and optional username) in the config file and
reports whether that worked.

#### `client.saveToken(object? options) result<unit, error>`

saveToken stores the client's token in the rotur CLI config file so
rotur.load() and the rotur CLI pick it up. Options: username (string,
saved alongside). Fails with StateError when the client has no token, and
IOError or ParseError when the file can't be written.

#### `client.sessions() *rotur.Sessions`

sessions starts and ends usage sessions.

#### `client.setToken(string token) *rotur.Client`

**Deprecated.** Deprecated: use client.useToken(token) instead.

setToken replaces the token and returns the client.

#### `client.signing() *rotur.Signing`

signing signs, verifies, encrypts and decrypts with rotur identity keys.

#### `client.socket() *rotur.Socket`

socket is the client's realtime connection for presence, rooms, live
account keys and activities. Each client has one; call start() on it.

#### `client.stats() *rotur.Stats`

stats reads public platform statistics.

#### `client.status() *rotur.Status`

status reads and sets live status.

#### `client.storage() *rotur.Storage`

storage reads and writes per-app key/value storage.

#### `client.systems() *rotur.Systems`

systems manages registered systems and their badges.

#### `client.token() string`

**Deprecated.** Deprecated: tokens are write-only; use client.hasToken() to check for one.

token returns the client's token.

#### `client.tokens() *rotur.Tokens`

tokens manages sub-tokens with limited permissions.

#### `client.trust() *rotur.Trust`

trust reads trust scores and verifies users.

#### `client.useBaseURL(string url)`

useBaseURL points the client at another API root instead of
https://api.rotur.dev/v2, such as a test server
("http://127.0.0.1:8080/v2"). A trailing slash is dropped.

#### `client.useToken(string token)`

useToken makes the client send token from now on, replacing any previous
one; "" makes it anonymous. The token is write-only: nothing returns it.

#### `client.username() result<string, error>`

username is the account name the token belongs to. Fails with
PermissionError when the client has no token or the API rejects it.

#### `client.validators() *rotur.Validators`

validators generates and checks validator tokens for third-party sign-in.

#### `client.whoami() result<object, error>`

whoami checks the token with the API and returns
{auth, username, token_type, permissions}.

### `*rotur.Me`

#### `me.abilities() result<object, error>`

abilities returns what the client's token is allowed to do. Needs a token.

#### `me.acceptTOS() result<object, error>`

acceptTOS accepts the current terms of service. Needs a token.

#### `me.badgePreferences() result<object, error>`

badgePreferences returns {preferences, badges}: how the account's badges
are shown. Needs a token.

#### `me.badges() result<object, error>`

badges returns {badge_names}, the badges the account has. Needs a token.

#### `me.benefits() result<object, error>`

benefits returns {benefits, subscription} for the account's tier. Needs a token.

#### `me.billing() result<object, error>`

billing returns the account's billing status. Needs a token.

#### `me.billingPortal() result<object, error>`

billingPortal returns a billing portal session (with its URL). Needs a token.

#### `me.block(string username) result<object, error>`

block blocks username. Needs a token.

#### `me.blocked() result<object, error>`

blocked returns {blocked}, the usernames the account blocks. Needs a token.

#### `me.changePassword(string current, string next) result<object, error>`

changePassword changes the account password. Fails with PermissionError
when current is wrong. Needs a token.

#### `me.checkAuth() result`

**Deprecated.** Deprecated: use client.whoami() instead.

checkAuth checks the token.

#### `me.checkout(string lookupKey) result<object, error>`

checkout starts buying the subscription lookupKey and returns the checkout
session (with its URL). Needs a token.

#### `me.claimDaily() result<object, error>`

claimDaily claims the daily credit reward. Fails with StateError or
ValueError (as the API reports it) when it was already claimed. Needs a token.

#### `me.claimTime() result<object, error>`

claimTime returns {wait_time}, how long until the daily reward can be
claimed again, as the API reports it. Needs a token.

#### `me.deleteAccount(string username) result`

**Deprecated.** Deprecated: use client.profiles().delete(username) instead.

deleteAccount deletes the signed-in account.

#### `me.deleteKey(string key) result<object, error>`

deleteKey removes one key from the account data. Needs a token.

#### `me.deleteNote(string username) result<object, error>`

deleteNote removes the note about username. Needs a token.

#### `me.get() result<object, error>`

get returns the signed-in account's data object, including sys.* keys
such as sys.currency. Needs a token.

#### `me.note(string username, string content) result<object, error>`

note saves a private note about username, replacing any earlier one.
Needs a token.

#### `me.notes() result<object, error>`

notes returns {notes}, an object of username to note. Needs a token.

#### `me.outgoing() result`

**Deprecated.** Deprecated: use client.friends().outgoing() instead.

outgoing returns the outgoing friend requests.

#### `me.pay(string to, number amount, object? options) result<object, error>`

pay sends amount credits to the user to. Options: note (string shown to
the receiver). Needs a token.

#### `me.refreshToken(string password) result<object, error>`

refreshToken asks for a new token, returned as {token}, and needs the
account password. The old token stops working; the client keeps using it
until you pass the new one to useToken. Fails with ValueError when password
is empty and PermissionError when it is wrong. Needs a token.

#### `me.requests() result`

**Deprecated.** Deprecated: use client.friends().requests() instead.

requests returns the incoming friend requests.

#### `me.resendVerification() result<object, error>`

resendVerification sends the verification email again. Needs a token.

#### `me.subscription() result<{active: boolean, next_billing: number, tier: string}, error>`

subscription returns {active, tier, next_billing} for the account; an
account without one is {active: false, tier: "Free", next_billing: 0}.
next_billing is unix milliseconds. Needs a token.

#### `me.transactions() result<array, error>`

transactions returns the account's credit transactions, newest first as
the API stores them. Needs a token.

#### `me.transfer(string to, number amount, any ...note) result`

**Deprecated.** Deprecated: use client.me().pay(to, amount, {note}) instead.

transfer sends credits with an optional note.

#### `me.unblock(string username) result<object, error>`

unblock unblocks username. Needs a token.

#### `me.update(string key, any value) result<object, error>`

update sets one key of the account data and returns the API's answer.
Needs a token.

#### `me.updateBadgePreferences(object preferences) result<object, error>`

updateBadgePreferences replaces the badge display preferences and returns
{preferences, badges}. Needs a token.

### `*rotur.Posts`

#### `posts.bookmark(string id) result<object, error>`

bookmark bookmarks the post id. Needs a token.

#### `posts.bookmarks() result<array, error>`

bookmarks returns the bookmarked posts. Needs a token.

#### `posts.castVote(string id, int option) result<object, error>`

castVote votes for option (1 for the first) in the poll on post id,
replacing an earlier vote, and returns {poll}. Throws ValueError when
option is below 1. Needs a token.

#### `posts.create(string content, object? options) result<object, error>`

create publishes a post and returns it (or the scheduled post). Options:
attachment (string URL), attachments (array of URLs), profileOnly (boolean,
keep it off the public feed), os (string, a registered system to tag),
poll (array of 2-6 option strings), scheduledFor (unix ms to publish at).
The older spellings profile_only and scheduled_for, and attachments as one
comma-separated string, still work. Fails with ValueError when the API
rejects the content. Needs a token.

Deprecated option profile_only: pass profileOnly instead.
Deprecated option scheduled_for: pass scheduledFor instead.

#### `posts.delete(string id) result<object, error>`

delete deletes the post id. Needs a token.

#### `posts.edit(string id, string content) result<object, error>`

edit replaces the text of the post id. Needs a token.

#### `posts.feed(any ...args) result`

**Deprecated.** Deprecated: use client.posts().list({limit, offset}) instead.

feed returns the public feed: feed(limit?, offset?).

#### `posts.find(string query, object? options) result<array, error>`

find searches posts for query. Options: limit (1-20, default 20). Public.

#### `posts.following(object? options) result<array, error>`

following returns recent posts from accounts the user follows. Options:
limit (default 100). Needs a token.

#### `posts.followingFeed(any ...limit) result`

**Deprecated.** Deprecated: use client.posts().following({limit}) instead.

followingFeed returns posts from followed accounts: followingFeed(limit?).

#### `posts.get(string id) result<object, error>`

get returns the post id. Fails with NotFoundError when it doesn't exist. Public.

#### `posts.like(string id) result<object, error>`

like likes the post id. Needs a token.

#### `posts.limits() result<object, error>`

limits returns the posting limits for each subscription tier. Public.

#### `posts.list(object? options) result<array, error>`

list returns the public feed, newest first. Options: limit (1-100,
default 100), offset (posts to skip, default 0). Public.

#### `posts.pin(string id) result<object, error>`

pin pins the post id to the account's profile. Needs a token.

#### `posts.popular(object? options) result<array, error>`

popular returns the most liked recent posts. Options: limit (default 50),
period (ms to look back, default 86400000 = 24 hours, counted in whole
hours). Public.

#### `posts.reply(string id, string content) result<object, error>`

reply replies to the post id and returns the reply. Needs a token.

#### `posts.repost(string id) result<object, error>`

repost reposts the post id and returns the new post. Needs a token.

#### `posts.scheduled() result<array, error>`

scheduled returns the account's scheduled posts. Needs a token.

#### `posts.search(string query, any ...limit) result`

**Deprecated.** Deprecated: use client.posts().find(query, {limit}) instead.

search searches posts: search(query, limit?).

#### `posts.top(any ...args) result`

**Deprecated.** Deprecated: use client.posts().popular({limit, period}) instead, with period in ms.

top returns the top posts: top(limit?, hours?).

#### `posts.unbookmark(string id) result<object, error>`

unbookmark removes the bookmark on post id. Needs a token.

#### `posts.unlike(string id) result<object, error>`

unlike removes the like on post id. Needs a token.

#### `posts.unpin(string id) result<object, error>`

unpin unpins the post id. Needs a token.

#### `posts.view(string id) result<object, error>`

view counts a view of the post id and returns {views}. Needs a token.

#### `posts.viewMany(array ids) result<object, error>`

viewMany counts a view of each post id and returns {views}, an object of
id to view count. Needs a token.

### `*rotur.Friends`

#### `friends.accept(string username) result<object, error>`

accept accepts the friend request from username. Needs a token.

#### `friends.cancel(string username) result<object, error>`

cancel withdraws the friend request sent to username. Needs a token.

#### `friends.list() result<object, error>`

list returns {friends}, the account's friends. Needs a token.

#### `friends.outgoing() result<object, error>`

outgoing returns the friend requests the account sent, as
{requests_out}. Needs a token.

#### `friends.reject(string username) result<object, error>`

reject rejects the friend request from username. Needs a token.

#### `friends.remove(string username) result<object, error>`

remove unfriends username. Needs a token.

#### `friends.request(string username) result<object, error>`

request sends a friend request to username. Needs a token.

#### `friends.requests() result<object, error>`

requests returns the incoming friend requests, as {requests}. Needs a token.

### `*rotur.Following`

#### `following.follow(string username) result<object, error>`

follow follows username. Needs a token.

#### `following.followers(string username) result<object, error>`

followers returns who follows username. Public.

#### `following.following(string username) result<object, error>`

following returns who username follows. Public.

#### `following.unfollow(string username) result<object, error>`

unfollow stops following username. Needs a token.

### `*rotur.Notifications`

#### `notifications.clear() result<object, error>`

clear deletes every notification. Needs a token.

#### `notifications.create(string target, string kind, object? options) result<object, error>`

create sends a notification of type kind to the user target. Options:
actor (string), data (object), platform (string), platformData (object;
the older platform_data still works). Needs a token.

Deprecated option platform_data: pass platformData instead.

#### `notifications.delete(string id) result<object, error>`

delete deletes the notification id. Needs a token.

#### `notifications.list(any ...afterDays) result`

**Deprecated.** Deprecated: use client.notifications().recent({period}) instead, with period in ms.

list returns notifications from the last afterDays days (default 30).

#### `notifications.markRead(any ...ids) result`

**Deprecated.** Deprecated: use client.notifications().readMany(ids) or readAll() instead.

markRead marks the given ids read, or every notification without ids.

#### `notifications.read(string id) result<object, error>`

read marks the notification id read. Needs a token.

#### `notifications.readAll() result<object, error>`

readAll marks every notification read. Needs a token.

#### `notifications.readMany(array ids) result<object, error>`

readMany marks the notifications with these ids read. Needs a token.

#### `notifications.recent(object? options) result<array, error>`

recent returns the account's notifications, newest first. Options: period
(ms to look back, default 30 days, counted in whole days). Needs a token.

### `*rotur.Keys`

#### `keys.addUser(string id, string user) result<object, error>`

addUser gives the key id to user. Needs a token.

#### `keys.buy(string id) result<object, error>`

buy buys the key id. Needs a token.

#### `keys.cancel(string id) result<object, error>`

cancel cancels the subscription to the key id. Needs a token.

#### `keys.check(string username, string key) result<object, error>`

check reports whether username holds key, as the API's answer object. Public.

#### `keys.create(string name, object? options) result<object, error>`

create makes a new key called name. Options: description (string), price
(credits), subscription (boolean), frequency (number), period (string such
as "month"). Needs a token.

#### `keys.delete(string id) result<object, error>`

delete deletes the key id. Needs a token.

#### `keys.get(string id) result<object, error>`

get returns the key id. Public.

#### `keys.mine() result<array, error>`

mine returns the keys the account owns. Needs a token.

#### `keys.removeUser(string id, string user) result<object, error>`

removeUser removes user from the key id. Needs a token.

#### `keys.rename(string id, string name) result<object, error>`

rename renames the key id. Needs a token.

#### `keys.revoke(string id, string user) result<object, error>`

revoke takes the key id away from user. Needs a token.

#### `keys.update(string id, string key, any data) result<object, error>`

update sets one field of the key id. Needs a token.

### `*rotur.Items`

#### `items.buy(string name) result<object, error>`

buy buys the item name. Needs a token.

#### `items.create(object item) result<object, error>`

create makes a new item from its fields (name, description, price, ...)
and returns it. Needs a token.

#### `items.delete(string name) result<object, error>`

delete deletes the item name. Needs a token.

#### `items.forSale(object? options) result<array, error>`

forSale returns items listed for sale. Options: limit (default 50). Public.

#### `items.get(string name) result<object, error>`

get returns the item name. Public.

#### `items.list(string username) result<array, error>`

list returns the items username owns. Public.

#### `items.sell(string name) result<object, error>`

sell lists the item name for sale. Needs a token.

#### `items.selling(any ...limit) result`

**Deprecated.** Deprecated: use client.items().forSale({limit}) instead.

selling returns items for sale: selling(limit?).

#### `items.setPrice(string name, number price) result<object, error>`

setPrice sets the price of the item name in credits. Needs a token.

#### `items.stopSelling(string name) result<object, error>`

stopSelling takes the item name off sale. Needs a token.

#### `items.transfer(string name, string username) result<object, error>`

transfer gives the item name to username. Needs a token.

#### `items.update(string name, any data) result<object, error>`

update replaces the data of the item name and returns the item. Needs a token.

### `*rotur.Gifts`

#### `gifts.cancel(string id) result<object, error>`

cancel cancels the account's unclaimed gift id. Needs a token.

#### `gifts.claim(string code) result<object, error>`

claim claims the gift code. Needs a token.

#### `gifts.create(number amount, object? options) result<object, error>`

create makes a claimable gift of amount credits and returns it with its
code. Options: note (string), expiresIn (ms until it expires, counted in
whole hours). Deprecated: expiresInHrs and expires_in_hrs still take
hours. Needs a token.

Deprecated option expiresInHrs: pass expiresIn (milliseconds) instead.
Deprecated option expires_in_hrs: pass expiresIn (milliseconds) instead.

#### `gifts.get(string code) result<object, error>`

get returns {gift} for the gift code. Public.

#### `gifts.mine() result<object, error>`

mine returns {gifts, count}, the gifts the account made. Needs a token.

### `*rotur.Tokens`

#### `tokens.active() result<object, error>`

active returns {tokens, total}, the sub-tokens that still work. Needs a token.

#### `tokens.activity(string id) result<object, error>`

activity returns the recent use of the sub-token id. Needs a token.

#### `tokens.create(string name, array permissions, object? options) result<object, error>`

create makes a sub-token called name with the given permission strings
and returns it, including the token itself. Options: expiresIn (ms,
counted in whole hours), origin (string), description (string), websites
(array of strings). Deprecated: expiresInHrs and expires_in_hrs still
take hours. Needs a token.

Deprecated option expiresInHrs: pass expiresIn (milliseconds) instead.
Deprecated option expires_in_hrs: pass expiresIn (milliseconds) instead.

#### `tokens.delete(string id) result<object, error>`

delete deletes the sub-token id. Needs a token.

#### `tokens.get(string id) result<object, error>`

get returns the sub-token id. Needs a token.

#### `tokens.list() result<object, error>`

list returns {tokens, total}, every sub-token. Needs a token.

#### `tokens.permissions() result<object, error>`

permissions returns the permissions a sub-token can have. Public.

#### `tokens.rename(string id, string name) result<object, error>`

rename renames the sub-token id. Needs a token.

#### `tokens.revoke(string id) result<object, error>`

revoke stops the sub-token id from working. Needs a token.

#### `tokens.update(string id, object changes) result<object, error>`

update changes fields of the sub-token id (the API's field names). Needs a token.

### `*rotur.Groups`

#### `groups.acceptInvite(string tag, string inviteID) result<object, error>`

acceptInvite accepts the invite inviteID to the group tag. Needs a token.

#### `groups.acceptJoinRequest(string tag, string requestID) result<object, error>`

acceptJoinRequest lets the requester of requestID in. Needs a token.

#### `groups.announce(string tag, string title, string body, object? options) result<object, error>`

announce posts an announcement to the group tag and returns it. Options:
pingMembers (boolean, notify every member). Needs a token.

#### `groups.announcements(string tag) result<array, error>`

announcements returns the announcements of the group tag. Public.

#### `groups.askToJoin(string tag, object? options) result<object, error>`

askToJoin asks to join the group tag. Options: message (string for the
group's admins). Needs a token.

#### `groups.assignRole(string tag, string userID, string roleID) result<object, error>`

assignRole gives the member userID the role roleID. Needs a token.

#### `groups.ban(string tag, string userID, any ...reason) result`

**Deprecated.** Deprecated: use client.groups().banMember(tag, userID, {reason}) instead.

ban bans a member: ban(tag, userID, reason?).

#### `groups.banMember(string tag, string userID, object? options) result<object, error>`

banMember bans userID from the group tag. Options: reason (string). Needs a token.

#### `groups.bans(string tag) result<array, error>`

bans returns the bans of the group tag. Needs a token.

#### `groups.cancelProductSubscription(string tag, string productID) result<object, error>`

cancelProductSubscription cancels the subscription to productID. Needs a token.

#### `groups.checkBan(string tag, string userID) result<object, error>`

checkBan returns the ban record of userID, as the API's answer object.
Needs a token.

#### `groups.create(string tag, string name, object? options) result<object, error>`

create makes a group with the short tag and display name and returns it.
Options: description (string), iconURL (string), bannerURL (string),
public (boolean), joinPolicy (string such as "open" or "request"). The
older spellings icon_url, iconUrl, banner_url, bannerUrl and join_policy
still work. Needs a token.

Deprecated option icon_url: pass iconURL instead.
Deprecated option iconUrl: pass iconURL instead.
Deprecated option banner_url: pass bannerURL instead.
Deprecated option bannerUrl: pass bannerURL instead.
Deprecated option join_policy: pass joinPolicy instead.

#### `groups.createAnnouncement(string tag, string title, string body, any ...pingMembers) result`

**Deprecated.** Deprecated: use client.groups().announce(tag, title, body, {pingMembers}) instead.

createAnnouncement posts an announcement: createAnnouncement(tag, title, body, pingMembers?).

#### `groups.createEvent(string tag, object event) result<object, error>`

createEvent creates an event in the group tag from its fields (the API's
field names) and returns it. Needs a token.

#### `groups.createProduct(string tag, object product) result<object, error>`

createProduct adds a product to the group tag. product has name,
description, priceCredits, roleID, subscription, frequency and period (the
API's snake_case names are accepted too). Needs a token.

#### `groups.createRole(string tag, object role) result<object, error>`

createRole creates a role in the group tag from its fields and returns it.
Needs a token.

#### `groups.declineInvite(string tag, string inviteID) result<object, error>`

declineInvite declines the invite inviteID. Needs a token.

#### `groups.declineJoinRequest(string tag, string requestID) result<object, error>`

declineJoinRequest turns down the join request requestID. Needs a token.

#### `groups.delete(string tag) result<object, error>`

delete deletes the group tag. Needs a token.

#### `groups.deleteAnnouncement(string tag, string id) result<object, error>`

deleteAnnouncement deletes the announcement id of the group tag. Needs a token.

#### `groups.deleteEvent(string tag, string eventID) result<object, error>`

deleteEvent deletes the event eventID. Needs a token.

#### `groups.deleteProduct(string tag, string productID) result<object, error>`

deleteProduct deletes the product productID. Needs a token.

#### `groups.deleteRole(string tag, string roleID) result<object, error>`

deleteRole deletes the role roleID. Needs a token.

#### `groups.disrepresent(string tag) result<object, error>`

disrepresent stops showing the group tag on the profile. Needs a token.

#### `groups.events(string tag) result<array, error>`

events returns the events of the group tag. Needs a token.

#### `groups.get(string tag) result<object, error>`

get returns the group tag. Needs a token.

#### `groups.invite(string tag, string username) result<object, error>`

invite invites username to the group tag. Needs a token.

#### `groups.invites(string tag) result<array, error>`

invites returns the pending invites of the group tag. Needs a token.

#### `groups.join(string tag) result<object, error>`

join joins the open group tag. Needs a token.

#### `groups.joinRequests(string tag) result<array, error>`

joinRequests returns the pending join requests of the group tag. Needs a token.

#### `groups.kick(string tag, string userID) result<object, error>`

kick removes the member userID from the group tag. Needs a token.

#### `groups.leave(string tag) result<object, error>`

leave leaves the group tag. Needs a token.

#### `groups.memberInfo(string tag, string userID) result<object, error>`

memberInfo returns the membership of userID in the group tag. Needs a token.

#### `groups.members(string tag, object? options) result<object, error>`

members returns a page of the members of the group tag. Options: page
(1-based, default 1), perPage (number; the older per_page still works),
search (string). Needs a token.

Deprecated option per_page: pass perPage instead.

#### `groups.mine() result<array, error>`

mine returns the groups the account is in. Needs a token.

#### `groups.muteAnnouncements(string tag) result<object, error>`

muteAnnouncements stops announcement notifications from the group tag.
Needs a token.

#### `groups.myInvites() result<array, error>`

myInvites returns the group invites the account received. Needs a token.

#### `groups.myProductSubscriptions() result<array, error>`

myProductSubscriptions returns the account's group product subscriptions.
Needs a token.

#### `groups.popular(object? options) result<array, error>`

popular returns the largest public groups. Options: limit (default 10). Public.

#### `groups.productOwnership(string tag, string productID, string username) result<object, error>`

productOwnership reports whether username owns productID, as the API's
answer object. Public.

#### `groups.products(string tag) result<array, error>`

products returns the products the group tag sells. Needs a token.

#### `groups.purchaseProduct(string tag, string productID) result<object, error>`

purchaseProduct buys the product productID. Needs a token.

#### `groups.removeRole(string tag, string userID, string roleID) result<object, error>`

removeRole takes the role roleID from the member userID. Needs a token.

#### `groups.report(string tag) result<object, error>`

report reports the group tag to moderators. Needs a token.

#### `groups.represent(string tag) result<object, error>`

represent shows the group tag on the account's profile. Needs a token.

#### `groups.requestJoin(string tag, any ...message) result`

**Deprecated.** Deprecated: use client.groups().askToJoin(tag, {message}) instead.

requestJoin asks to join a group: requestJoin(tag, message?).

#### `groups.revokeInvite(string tag, string inviteID) result<object, error>`

revokeInvite withdraws the invite inviteID. Needs a token.

#### `groups.roles(string tag) result<array, error>`

roles returns the roles of the group tag. Needs a token.

#### `groups.search(string query) result<array, error>`

search returns groups matching query. Needs a token.

#### `groups.sendTip(string tag, number amount, any ...note) result`

**Deprecated.** Deprecated: use client.groups().tip(tag, amount, {note}) instead.

sendTip tips a group: sendTip(tag, amount, note?).

#### `groups.tip(string tag, number amount, object? options) result<object, error>`

tip sends amount credits to the group tag. Options: note (string). Needs a token.

#### `groups.tipWithdrawals(string tag, object? options) result<array, error>`

tipWithdrawals returns past tip withdrawals of the group tag. Options:
limit (default 20). Needs a token.

#### `groups.tips(string tag) result<array, error>`

tips returns the tips the group tag received. Needs a token.

#### `groups.top(any ...limit) result`

**Deprecated.** Deprecated: use client.groups().popular({limit}) instead.

top returns the largest groups: top(limit?).

#### `groups.transferOwnership(string tag, string userID) result<object, error>`

transferOwnership makes userID the owner of the group tag. Needs a token.

#### `groups.unban(string tag, string userID) result<object, error>`

unban lifts the ban on userID. Needs a token.

#### `groups.update(string tag, object updates) result<object, error>`

update changes fields of the group tag (the API's field names). Needs a token.

#### `groups.updateEvent(string tag, string eventID, object updates) result<object, error>`

updateEvent changes fields of the event eventID. Needs a token.

#### `groups.updateRole(string tag, string roleID, object updates) result<object, error>`

updateRole changes fields of the role roleID. Needs a token.

#### `groups.userBenefits(string tag, string userID) result<array, error>`

userBenefits returns the benefit names the member userID has. Needs a token.

#### `groups.userPermissions(string tag, string userID) result<array, error>`

userPermissions returns the permission names the member userID has. Needs a token.

#### `groups.userRoles(string tag, string userID) result<array, error>`

userRoles returns the roles the member userID has. Needs a token.

#### `groups.withdrawTip(string tag, number amount) result<object, error>`

withdrawTip withdraws amount tipped credits from the group tag. Needs a token.

#### `groups.withdrawals(string tag, any ...limit) result`

**Deprecated.** Deprecated: use client.groups().tipWithdrawals(tag, {limit}) instead.

withdrawals returns tip withdrawals: withdrawals(tag, limit?).

### `*rotur.Stats`

#### `stats.economy() result<object, error>`

economy returns credit economy statistics. Public.

#### `stats.followers(any ...max) result`

**Deprecated.** Deprecated: use client.stats().topFollowed({limit}) instead.

followers returns the most followed accounts: followers(max?).

#### `stats.mostGained(any ...max) result`

**Deprecated.** Deprecated: use client.stats().topEarners({limit}) instead.

mostGained returns the top earners: mostGained(max?).

#### `stats.postActivity(object? options) result<object, error>`

postActivity returns posting activity as {days, total, buckets}. Options:
period (ms to look back, default 7 days, counted in whole days). Public.

#### `stats.posts(any ...days) result`

**Deprecated.** Deprecated: use client.stats().postActivity({period}) instead, with period in ms.

posts returns posting activity: posts(days?).

#### `stats.systems() result<object, error>`

systems returns an object of system name to account count. Public.

#### `stats.topEarners(object? options) result<array, error>`

topEarners returns the accounts that gained the most credits, as
[{user, earned}]. Options: limit (default 10). Public.

#### `stats.topFollowed(object? options) result<array, error>`

topFollowed returns the most followed accounts, as [{username,
follower_count}]. Options: limit (default 10). Public.

#### `stats.users() result<object, error>`

users returns account statistics. Public.

### `*rotur.Status`

#### `status.get(string username) result<object, error>`

get returns the live status of username. Public.

#### `status.setLive(object status) result<object, error>`

setLive publishes the account's live status from its fields (the API's
field names). Needs a token.

### `*rotur.Validators`

#### `validators.generate(string key) result<object, error>`

generate returns {validator}, a one-time proof of the account for the
service identified by key. Needs a token.

#### `validators.validate(string validator, string key) result<object, error>`

validate checks a validator another client generated for key and returns
the API's answer (whether it is valid and for which user). Public.

### `*rotur.Link`

#### `link.code() result<string, error>`

code requests a fresh device link code for the user to approve. Public.

#### `link.getCode() result`

**Deprecated.** Deprecated: use client.link().code() instead.

getCode requests a link code as {code}.

#### `link.linkCode(string code) result<object, error>`

linkCode approves code as the signed-in account, signing in the device
that shows it. Needs a token.

#### `link.linkedUser(string code) result<object, error>`

linkedUser returns {linked, token?} for code: the token appears once the
user approved it. Public.

#### `link.status(string code) result<object, error>`

status returns {status} for code. Public.

#### `link.url(string code) string`

url is the page where the user approves code.

#### `link.wait(string code, any ...timeoutSecs) string`

**Deprecated.** Deprecated: use client.link().waitForToken(code, {timeout}) instead, with timeout in ms.

wait polls until code is approved and returns the token, or "" on timeout.

#### `link.waitForToken(string code, object? options) result<string, error>`

waitForToken polls until code is approved, makes the client use the new
token and returns it. Options: timeout (ms, default 120000). Fails with
TimeoutError when nobody approves the code in time.

### `*rotur.Cosmetics`

#### `cosmetics.backgroundURL(string id) string`

backgroundURL is the background video of the cosmetic id.

#### `cosmetics.equip(string id) result<object, error>`

equip wears the cosmetic id. Needs a token.

#### `cosmetics.forUser(string username) result<object, error>`

forUser returns the cosmetics username wears. Public.

#### `cosmetics.forUsers(array usernames) result<object, error>`

forUsers returns the cosmetics several users wear. Public.

#### `cosmetics.get(string id) result<object, error>`

get returns the cosmetic id. Public.

#### `cosmetics.gift(string id, string to, any ...note) result`

**Deprecated.** Deprecated: use client.cosmetics().give(id, to, {note}) instead.

gift gifts a cosmetic: gift(id, to, note?).

#### `cosmetics.gifts() result<object, error>`

gifts returns {received, sent}, the account's cosmetic gifts. Needs a token.

#### `cosmetics.give(string id, string to, object? options) result<object, error>`

give gifts the cosmetic id to the user to. Options: note (string). Needs a token.

#### `cosmetics.mine() result<object, error>`

mine returns the cosmetics the account owns. Needs a token.

#### `cosmetics.overlayURL(string id) string`

overlayURL is the animated overlay image of the cosmetic id.

#### `cosmetics.purchase(string id) result<object, error>`

purchase buys the cosmetic id. Needs a token.

#### `cosmetics.shop(object? options) result<object, error>`

shop returns the cosmetics catalog. Options: type (string), featured
(boolean), search (string), sort (string), limit (number), offset (number).
Public.

#### `cosmetics.unequip(string kind) result<object, error>`

unequip takes off the cosmetic of type kind. Needs a token.

### `*rotur.Push`

#### `push.addDevice(object device) result<object, error>`

addDevice registers a web push subscription. device has endpoint,
p256dh, auth, source and fingerprint (all strings). Needs a token.

#### `push.allowSender(string username, string source) result<object, error>`

allowSender lets username push to the account for source. Needs a token.

#### `push.allowedSenders() result<object, error>`

allowedSenders returns, per source, who may push to the account. Needs a token.

#### `push.check(string source, string fingerprint) result<object, error>`

check reports whether a device is registered for source, as the API's
answer object. Needs a token.

#### `push.deleteDevice(string deviceID) result<object, error>`

deleteDevice unregisters the device deviceID. Needs a token.

#### `push.endpoints() result<object, error>`

endpoints returns {endpoints, count}, the registered devices. Needs a token.

#### `push.log() result<object, error>`

log returns {log, count}, recently sent pushes. Needs a token.

#### `push.notifiableUsers(string source) result<object, error>`

notifiableUsers returns {source, users}, who allows pushes from source.
Needs a token.

#### `push.register(string endpoint, string p256dh, string auth, any ...rest) result`

**Deprecated.** Deprecated: use client.push().addDevice({endpoint, p256dh, auth, source, fingerprint}) instead.

register registers a push device: register(endpoint, p256dh, auth, source, fingerprint).

#### `push.removeSender(string username, string source) result<object, error>`

removeSender stops username pushing to the account for source. Needs a token.

#### `push.send(string username, string source, object? options) result<object, error>`

send pushes a notification for source to username. Options: title
(string), body (string), data (object). Needs a token.

#### `push.sendMany(array users, string source, object? options) result<object, error>`

sendMany pushes a notification for source to every username in users.
Options as for send. Needs a token.

#### `push.vapidKeys() result<object, error>`

vapidKeys returns {public_key, subject} for web push subscriptions. Public.

### `*rotur.Files`

#### `files.all() result<array, error>`

all returns every file entry with its contents. Needs a token.

#### `files.byPath(string path) result<object, error>`

byPath returns the file at path. Fails with NotFoundError when it doesn't
exist. Needs a token.

#### `files.byUUID(string uuid) result<object, error>`

byUUID returns the file uuid. Fails with NotFoundError when it doesn't
exist. Needs a token.

#### `files.byUUIDs(array uuids) result<array, error>`

byUUIDs returns the files with these uuids. Needs a token.

#### `files.deleteAll() result<object, error>`

deleteAll deletes every file. Needs a token.

#### `files.getByPath(string path) result`

**Deprecated.** Deprecated: use client.files().byPath(path) instead.

getByPath returns one file.

#### `files.getByUUID(string uuid) result`

**Deprecated.** Deprecated: use client.files().byUUID(uuid) instead.

getByUUID returns one file.

#### `files.index() result<array, error>`

index returns every file entry without contents. Needs a token.

#### `files.pathIndex() result<object, error>`

pathIndex returns an object of path to file uuid. Needs a token.

#### `files.stats(array uuids) result<object, error>`

stats returns {stats} for the files with these uuids. Needs a token.

#### `files.upload(object files) result<object, error>`

upload writes files, an upload request in the API's format. Needs a token.

#### `files.usage() result<object, error>`

usage returns the storage used by files and the quota, in bytes. Needs a token.

### `*rotur.Profiles`

#### `profiles.avatarUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.avatarURL(username, {version}) instead.

avatarUrl is a profile picture URL: avatarUrl(username, cache?).

#### `profiles.backgroundUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.backgroundURL(username, {version}) instead.

backgroundUrl is a background URL: backgroundUrl(username, cache?).

#### `profiles.bannerUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.bannerURL(username, {version}) instead.

bannerUrl is a banner URL: bannerUrl(username, cache?).

#### `profiles.delete(string username) result<object, error>`

delete deletes the signed-in account, which must be username. Needs a token.

#### `profiles.exists(string username) result`

**Deprecated.** Deprecated: use client.profiles().userExists(username) instead, which returns a boolean.

exists returns {exists} for username.

#### `profiles.get(string username, any ...includePosts) result`

**Deprecated.** Deprecated: use client.profiles().lookup(username, {includePosts}) instead.

get returns a profile: get(username, includePosts?).

#### `profiles.lookup(string username, object? options) result<object, error>`

lookup returns the public profile of username. Options: includePosts
(boolean, default true). Fails with NotFoundError when there is no such
user. Public.

#### `profiles.overlayUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.overlayURL(username, {version}) instead.

overlayUrl is an overlay URL: overlayUrl(username, cache?).

#### `profiles.supporters() result<array, error>`

supporters returns the accounts with a paid subscription, as
[{username, subscription}]. Public.

#### `profiles.userExists(string username) result<boolean, error>`

userExists reports whether an account called username exists. Public.

### `*rotur.Storage`

#### `storage.clear(string id) result<object, error>`

clear removes every key of the app id and returns {id, cleared}. Needs a token.

#### `storage.clearAll() result<object, error>`

clearAll removes every app's stored data. Needs a token.

#### `storage.delete(string id, string key) result<object, error>`

delete removes key from the app id and returns {id, data}. Needs a token.

#### `storage.get(string id) result<object, error>`

get returns {id, data}, every key stored under the app id. Needs a token.

#### `storage.getKey(string id, string key) result`

**Deprecated.** Deprecated: use client.storage().value(id, key) instead.

getKey returns one stored value.

#### `storage.list() result<object, error>`

list returns {ids, usage, max}: every app id with stored data, and the
usage and quota in bytes. Needs a token.

#### `storage.set(string id, string key, any value) result<object, error>`

set stores value under key for the app id and returns {id, data}. Needs a token.

#### `storage.usage() result<object, error>`

usage returns {usage, max} in bytes. Needs a token.

#### `storage.value(string id, string key) result<any, error>`

value returns one key stored under the app id, or null when it isn't set.
Needs a token.

### `*rotur.Signing`

#### `signing.compact(object envelope) result<string, error>`

compact turns an envelope into one "rotur_enc_v1." string, the form the
rotur CLI prints. Fails with ValueError when envelope can't be encoded.

#### `signing.currentKey(string username) result<object, error>`

currentKey returns the current public key record of username. Public.

#### `signing.decrypt(any envelope) result<any, error>`

decrypt opens an envelope addressed to the signed-in account and returns
its content. envelope is the object encrypt returns or the string compact
returns. Fails with ParseError for a malformed or tampered envelope,
UnsupportedError for an unknown version or algorithm, and ValueError when
the envelope is for another key. Throws TypeError when envelope is
neither an object nor a string. Needs a token.

#### `signing.encrypt(object recipient, any content) result<{algorithm: string, ciphertext: string, ephemeral_public_key: string, iv: string, recipient_id: string, recipient_key_id: string, salt: string, version: number}, error>`

encrypt encrypts content (any JSON value) for the key recipient
{user_id, key_id, username?} and returns the envelope {version, algorithm,
recipient_id, recipient_key_id, ephemeral_public_key, salt, iv,
ciphertext}. Fails with NotFoundError for an unknown key and ValueError
when username doesn't own it.

#### `signing.encryptFor(string username, any content) result<{algorithm: string, ciphertext: string, ephemeral_public_key: string, iv: string, recipient_id: string, recipient_key_id: string, salt: string, version: number}, error>`

encryptFor encrypts content for the current key of username; see encrypt.

#### `signing.identity() result<{algorithm: string, key_id: string, public_key_jwk: object, user_id: string, username: string}, error>`

identity returns the public half of the signed-in account's identity key,
{user_id, username, key_id, algorithm, public_key_jwk}. The private key
never leaves the client. Needs a token.

#### `signing.key() result`

**Deprecated.** Deprecated: use client.signing().identity() instead, which never exposes the private key.

key returns the signed-in account's identity key record, private key included.

#### `signing.lookupKey(string userID, string keyID, object? options) result<object, error>`

lookupKey returns the public key record keyID of the user userID, from a
shared cache when possible. Options: refresh (boolean, skip the cache).
Fails with NotFoundError when the key doesn't exist.

#### `signing.publicKey(string userID, string keyID, any ...refresh) result`

**Deprecated.** Deprecated: use client.signing().lookupKey(userID, keyID, {refresh}) instead.

publicKey returns a user's public key record: publicKey(userID, keyID, refresh?).

#### `signing.rotate() result<object, error>`

rotate replaces the account's identity key; signatures made with the old
key stay verifiable. Needs a token.

#### `signing.sign(any content) result<{author_id: string, key_id: string, signature: string}, error>`

sign signs content's canonical JSON with the account's identity key and
returns the proof {author_id, key_id, signature}. When the signed content
must contain the author id, pass a function: it receives the id and
returns the content to sign. Needs a token.

#### `signing.verify(object proof, any content) result<boolean, error>`

verify reports whether proof {author_id, key_id, signature, username?} is
a valid signature of content by that author. A malformed proof, an unknown
key, a username that doesn't own the key or a wrong signature is false.
Fails only when the author's key can't be fetched (NetworkError and the
like).

### `*rotur.Avatars`

#### `avatars.backgroundUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.backgroundURL(username, {version}) instead.

backgroundUrl is a background URL: backgroundUrl(username, cache?).

#### `avatars.bannerUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.bannerURL(username, {version}) instead.

bannerUrl is a banner URL: bannerUrl(username, cache?).

#### `avatars.overlayUrl(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.overlayURL(username, {version}) instead.

overlayUrl is an overlay URL: overlayUrl(username, cache?).

#### `avatars.removeBackground() result<object, error>`

removeBackground removes the profile background. Needs a token.

#### `avatars.removeBanner() result<object, error>`

removeBanner removes the profile banner. Needs a token.

#### `avatars.removeOverlay() result<object, error>`

removeOverlay removes the avatar overlay. Needs a token.

#### `avatars.uploadAvatar(string image) result<object, error>`

uploadAvatar sets the profile picture from image, a data URL or base64
string. Uploads go to avatars.rotur.dev, or to the host of the client's
base URL when useBaseURL changed it. Needs a token.

#### `avatars.uploadBanner(string image) result<object, error>`

uploadBanner sets the profile banner from image, a data URL or base64
string. Needs a token.

#### `avatars.url(string username, any ...cache) string`

**Deprecated.** Deprecated: use rotur.avatarURL(username, {version}) instead.

url is a profile picture URL: url(username, cache?).

### `*rotur.Emojis`

#### `emojis.add(string id, string name) result<object, error>`

add saves someone else's emoji id to the account under name. Needs a token.

#### `emojis.delete(string id) result<object, error>`

delete deletes the account's own emoji id. Needs a token.

#### `emojis.mine() result<object, error>`

mine returns the account's uploaded and saved emojis. Needs a token.

#### `emojis.unsave(string id) result<object, error>`

unsave removes a saved emoji id from the account. Needs a token.

#### `emojis.upload(string name, string image) result<object, error>`

upload adds a custom emoji called name from image, a data URL or base64
string. Needs a token.

#### `emojis.url(string id) string`

url is the image of the emoji id.

### `*rotur.Trust`

#### `trust.score(string username) result<object, error>`

score returns the trust score of username. Needs a token.

#### `trust.verify(object? options) result<object, error>`

verify checks a user against trust requirements and returns the API's
verdict. Options: username, userID, accountToken (strings), minScore
(number), requiredStanding (string), maxRisk (number). The API's
snake_case names (user_id, account_token, min_score, required_standing,
max_risk) and userId still work. Needs a token.

Deprecated option user_id: pass userID instead.
Deprecated option userId: pass userID instead.
Deprecated option account_token: pass accountToken instead.
Deprecated option min_score: pass minScore instead.
Deprecated option required_standing: pass requiredStanding instead.
Deprecated option max_risk: pass maxRisk instead.

### `*rotur.Accounts`

#### `accounts.bannedCheck(array usernames) result<object, error>`

bannedCheck returns {banned}, which of these usernames are banned. Public.

#### `accounts.deletedCheck(array ids) result<object, error>`

deletedCheck reports which of these user ids belong to deleted accounts. Public.

#### `accounts.register(object account) result<object, error>`

register creates an account from its fields (username, password, email,
... as the API names them) and returns it. Public.

#### `accounts.requestPasswordReset(string email) result<object, error>`

requestPasswordReset emails a password reset link to email. Public.

#### `accounts.resetPassword(string token, string newPassword) result<object, error>`

resetPassword sets a new password with the token from the reset email. Public.

#### `accounts.verifyEmail(string token) result<object, error>`

verifyEmail confirms an email address with the token from the email. Public.

### `*rotur.Sessions`

#### `sessions.current() result<object, error>`

current returns the current usage session. Needs a token.

#### `sessions.end() result<unit, error>`

end ends the current usage session. Needs a token.

#### `sessions.start() result<object, error>`

start starts a usage session. Needs a token.

### `*rotur.Ai`

#### `ai.ask(string content, object? options) result<string, error>`

ask sends one prompt to the rotur AI and returns its reply text. Options:
user (string, who is asking), model (string). Needs a token.

#### `ai.chat(array history, object? options) result<any, error>`

chat continues a conversation. history is an array of {role, content}.
Returns the reply text, or with returnHistory the whole conversation as an
array of {role, content}. Options: user (string), model (string),
returnHistory (boolean). Needs a token.

Deprecated option return_history: pass returnHistory instead.

### `*rotur.Pets`

#### `pets.catalog() result<object, error>`

catalog returns the pets for sale, as {pets}. Public.

#### `pets.equip(string id) result<any, error>`

equip shows the pet id on the profile. Needs a token.

#### `pets.mine() result<any, error>`

mine returns the account's pets as the API reports them. Needs a token.

#### `pets.profile(string username) result<any, error>`

profile returns the pets username shows, as the API reports them. Public.

#### `pets.purchase(string id) result<any, error>`

purchase buys the pet id. Needs a token.

#### `pets.unequip() result<any, error>`

unequip stops showing a pet. Needs a token.

### `*rotur.Systems`

#### `systems.badges(string system) result<object, error>`

badges returns {badges}, the badges system defines. Public.

#### `systems.createBadge(string system, object definition) result<object, error>`

createBadge defines a badge for system from definition (the API's field
names) and returns it. Needs a token.

#### `systems.deleteBadge(string system, string badge) result<object, error>`

deleteBadge deletes badge. Needs a token.

#### `systems.list() result<object, error>`

list returns the registered systems, as an object of name to system. Public.

#### `systems.revokeBadge(string system, string badge, string username) result<object, error>`

revokeBadge takes badge away from username. Needs a token.

#### `systems.setBadgeProgress(string system, string badge, string username, any ...progress) result`

**Deprecated.** Deprecated: use client.systems().updateBadgeProgress(system, badge, {username: progress}) instead.

setBadgeProgress sets one user's badge progress: setBadgeProgress(system, badge, username, progress).

#### `systems.update(string system, string key, any value) result<object, error>`

update sets one field of the system you own. Needs a token.

#### `systems.updateBadge(string system, string badge, object definition) result<object, error>`

updateBadge replaces the definition of badge. Needs a token.

#### `systems.updateBadgeProgress(string system, string badge, object progress) result<object, error>`

updateBadgeProgress sets users' progress on badge of system. progress is
an object of username to a number of 0 or more; tiers unlock at the
thresholds the badge defines. Returns an object of username to the API's
{grant, badge}. Stops at the first failure. Throws TypeError for a
progress value that isn't a number. Needs a token.

#### `systems.users(string system) result<array, error>`

users returns the usernames registered on system. Needs a token.

### `*rotur.Socket`

#### `socket.addActivity(object activity) result<unit, error>`

addActivity shows an activity on the profile. activity needs an id and
may have title, application, status, image and media (the API's fields).
Fails with StateError when the socket isn't connected.

#### `socket.broadcast(string room, any value, object? options) result<unit, error>`

broadcast sends value to everyone in room, delivered as a "gmsg" message.
Options: listener (string echoed back to match replies). Fails with
StateError when the socket isn't connected.

#### `socket.connect() boolean`

**Deprecated.** Deprecated: use socket.start(options?) instead, which fails with a typed error.

connect opens the socket and reports whether it became ready within 10
seconds; without a token it prints why and returns false.

#### `socket.connected() boolean`

**Deprecated.** Deprecated: use socket.isConnected() instead.

connected reports whether the socket is connected and ready.

#### `socket.disconnect()`

**Deprecated.** Deprecated: use socket.stop() instead.

disconnect closes the socket.

#### `socket.getKey(string key) any`

**Deprecated.** Deprecated: use socket.key(key) instead.

getKey returns one live account key.

#### `socket.isConnected() boolean`

isConnected reports whether the socket is connected and authenticated.

#### `socket.join(any ...rooms) result<unit, error>`

join joins one or more rooms by name (strings, or arrays of strings).
The server answers with a join_ok message per room. Fails with StateError
when the socket isn't connected.

#### `socket.key(string key) any`

key returns the live value of one account key, kept current by
key_update messages, or null when it isn't set.

#### `socket.keys() object`

keys returns every live account key as an object.

#### `socket.leave(any ...rooms) result<unit, error>`

leave leaves one or more rooms; see join.

#### `socket.listRooms() result<unit, error>`

listRooms asks the server for the joined rooms; the answer arrives as a
"rooms" message. Fails with StateError when the socket isn't connected.

#### `socket.off(string event, any handler)`

off removes a handler added with on(event, handler).

#### `socket.on(string event, any handler)`

on adds handler for event: a server command such as "ready", "join_ok",
"gmsg", "pmsg" or "key_update", "close" when the connection drops, or "*"
for every message. The handler receives the message object. Handlers run
on background threads; a throw is printed and the socket keeps running.

#### `socket.removeActivity(string id) result<unit, error>`

removeActivity removes the activity id. Fails with StateError when the
socket isn't connected.

#### `socket.roomState(string room) result<unit, error>`

roomState asks for the members of room; the answer arrives as a
"room_state" message. Fails with StateError when the socket isn't connected.

#### `socket.rooms() array`

rooms lists the rooms the socket has joined.

#### `socket.send(object packet) result<unit, error>`

send sends a raw message object to the server. Fails with StateError when
the socket isn't connected.

#### `socket.sendGroupMessage(string room, any val, any ...listener) boolean`

**Deprecated.** Deprecated: use socket.broadcast(room, value, {listener}) instead.

sendGroupMessage broadcasts to a room and reports whether it was sent.

#### `socket.sendPrivate(string room, string to, any value, object? options) result<unit, error>`

sendPrivate sends value to one member of room (username or user id),
delivered as a "pmsg" message. Options as for broadcast.

#### `socket.sendPrivateMessage(string room, string to, any val, any ...listener) boolean`

**Deprecated.** Deprecated: use socket.sendPrivate(room, to, value, {listener}) instead.

sendPrivateMessage messages one room member and reports whether it was sent.

#### `socket.setListening(string application, object media, object? options) result<unit, error>`

setListening shows music from application on the profile. media has
title, artist, album, start and end (unix ms). Options: title (string,
default "Listening to <application>"). Fails with StateError when the
socket isn't connected.

#### `socket.setMusic(string application, object media, any ...title) boolean`

**Deprecated.** Deprecated: use socket.setListening(application, media, {title}) instead.

setMusic shows music on the profile and reports whether it was sent.

#### `socket.setPlaying(string application, object? options) result<unit, error>`

setPlaying shows "playing application" on the profile. Options: title
(string, default application), status (string), url (string), image
(string). Fails with StateError when the socket isn't connected.

#### `socket.setStatus(string status, any ...presence) boolean`

**Deprecated.** Deprecated: use socket.updateStatus(status, {presence}) instead.

setStatus sets the status text and reports whether it was sent.

#### `socket.setURL(string url)`

setURL changes the websocket address used by the next start().

#### `socket.socketUrl(string url) *rotur.Socket`

**Deprecated.** Deprecated: use socket.setURL(url) instead.

socketUrl sets the websocket address and returns the socket.

#### `socket.start(object? options) result<unit, error>`

start opens the socket, authenticates with the client's token and waits
until the server says it is ready. Joined rooms are rejoined after a
reconnect. Already connected is a no-op. Options: timeout (ms, default
10000). Fails with PermissionError when the client has no token or the
server refuses it, NetworkError when the server can't be reached and
TimeoutError when it doesn't answer in time.

#### `socket.stop()`

stop closes the socket and forgets joined rooms and keys. start() can
open it again. Stopping twice is a no-op.

#### `socket.updateStatus(string status, object? options) result<unit, error>`

updateStatus sets the account's status text. Options: presence (string
such as "online", "idle" or "dnd"). Fails with StateError when the socket
isn't connected.

#### `socket.url() string`

url is the websocket address the socket connects to.

#### `socket.userID() string?`

userID is the signed-in account's id, or null before the socket is ready.

#### `socket.username() string?`

username is the signed-in account's name, or null before the socket is ready.
