Skip to content

Session reference

Session (unseenet.client.session) is one class. Its code is written in parts by area, and this page follows them: each part says what belongs in it, then lists its functions. All of them are called on a Session, for example s.subscribe(group), whichever part they are written in.

Generated from the docstrings in the code; functions whose name starts with _ are internal and not shown.

The shared types

Client session: one person's (or one service's) client of the Unseenet network.

Session is the client core that every client is built on - the command line, the TUI, unseenet-gui, the local client API (UN-RFC 0046), bots and gateways - so they all behave alike. It holds the encrypted vault (UN-RFC 0007), talks to nodes over Tor, and reads and posts. Clients use the one class; its code is split by area:

base.py       the vault, identities, settings, events
ripening.py   key ripening (UN-RFC 0025)
nodes.py      nodes, the connection, push, Tor
labels.py     how people and groups are shown: names and labels (UN-RFC 0019)
groups.py     manifests, creating and changing groups, moderation, roles, portals
private.py    the inbox, private posts and chats, key-bound groups and renewal
profiles.py   profiles and pictures (UN-RFC 0022)
people.py     contacts, ignoring, for you and mentions, notices, bots, reporting
reading.py    following, fetching, threads and views, the offline cache, read marks
posting.py    posting, postage, editing and withdrawing
attachments.py  files on posts: preparing and checking them (UN-RFC 0027)
kept.py       pinned posts and the pages of sites (UN-RFC 0033, 0035)
personal.py   an identity's personal feed and site (UN-RFC 0029 section 2.1)
following.py  following feeds and people, the timeline (UN-RFC 0034 sections 6, 7)
state.py      your state in the network, encrypted, merged and written (UN-RFC 0044)
reactions.py  reactions under articles: counting, reacting (UN-RFC 0048)
recovery.py   recovery words: showing, reminding, restoring (UN-RFC 0050 section 4)
copying.py    copying identities to another device (UN-RFC 0050 section 3)
queue.py      a posting queue: many posts paced by postage, ripening, quotas
notify.py     what new has landed in For you, for a client to tell
exporting.py  your data as a file (UN-RFC 0049 section 7)
rules.py      your own rules for your feed and groups (#615)
reports.py    spam reports: issuers you follow, hidden posts, issuing (UN-RFC 0045)
teams.py      team inboxes: readers, invitations, replying on an inbox's behalf (0047)

New code goes in the file whose docstring says it belongs there; a new area gets a file of its own, added to Session below. This file holds what all parts share: imports, constants, Message, GroupInfo, Permission, SessionError and helpers. Names that tests patch on this module (DEFAULT_POW, TorProcess) are read through it (_s.NAME) in the parts.

Session

Session(vault: Vault, status=print)

One person's (or one service's) client: the vault, the network, groups, posting. Split into parts by area (client/session/*.py, #390); clients use this one class as before.

SessionError

Message dataclass

Message(
    number: int,
    message_id: str,
    group: str,
    subject: str = "",
    from_name: str = "",
    author_key: str = "",
    date: datetime | None = None,
    body: str = "",
    references: list[str] = list(),
    encrypted: bool = False,
    readable: bool = True,
    error: str | None = None,
    raw: bytes = b"",
    private: bool = False,
    private_profile: bool = False,
    sender_key: str | None = None,
    reply_inbox: str | None = None,
    to_key: str | None = None,
    content_type: str = "",
    kind: str = KIND_POST,
    reaction: str = "",
    mention_group: str | None = None,
    mention_of: str | None = None,
    notices_for: str | None = None,
    supersedes: list[str] = list(),
    edited: datetime | None = None,
    versions: list[str] = list(),
    sender_ripening: str | None = None,
    page: str | None = None,
    page_removed: bool = False,
    blobs: tuple = (),
    attachments: tuple = (),
    origin_network: str = "",
    origin_date: datetime | None = None,
    origin_id: str = "",
    origin_from: str = "",
    origin_proven: str = "",
    file_keys: dict = dict(),
    node: str = "",
    comment_days: int | None = None,
    tags: tuple = (),
    languages: tuple = (),
    labels: dict | None = None,
    team: dict | None = None,
    team_invite: dict | None = None,
    reported: dict | None = None,
)

old_on_arrival property

old_on_arrival: bool

First published more than 24 hours before it came into the network (UN-RFC 0049 section 5.2): not new, no notification, not in For you.

sort_date property

sort_date: datetime | None

What it is ordered by in feeds, the timeline and threads (UN-RFC 0049 5.3).

to_dict

to_dict() -> dict

The message as plain JSON-safe data, shape DICT_VERSION (#139).

Dates are ISO 8601 strings, the raw article is left out, and the body is also given split into text and signature. How a name or a key is shown is the client's: this holds only what the message says.

GroupInfo dataclass

GroupInfo(
    group_id: str,
    description: str = "",
    type: str | None = None,
    owner_key: str | None = None,
    subscribed: bool = False,
    nodes: list[str] = list(),
    title: str = "",
    view: str = "threads",
    owner_view: str = "threads",
    kinds: tuple[str, ...] = (),
    tags: tuple[str, ...] = (),
    languages: tuple[str, ...] = (),
    local: bool = False,
    low_latency: bool = False,
    owner: bool = False,
    listed: bool | None = None,
    read_from: list[str] = list(),
    team: dict | None = None,
    unread: int | None = None,
    replies: int | None = None,
    mentions: int | None = None,
)

short property

short: str

The ID with a shortened fingerprint; a pseudo group (for-you:, contact:<key>, threads.py) has no fingerprint and keeps its ID.

label property

label: str

The name without the owner fingerprint, e.g. dk.snak.

hierarchy property

hierarchy: str

The first part of the name, as on Usenet: dk for dk.snak.

to_dict

to_dict() -> dict

The group as plain JSON-safe data, shape DICT_VERSION (#139).

Permission dataclass

Permission(
    allowed: bool, reason: str = "", code: str = "ok"
)

Whether you may write somewhere, and if not, the reason to show (#181).

The vault, identities, settings and events

The vault, identities, settings and events: what every other part stands on.

Belongs here: opening a session (__init__, from_key for services), saving the vault and changing its passphrase; the identities in it (identity, new_identity, act_as, display_name); the user's settings that are not about one area (the signature, Markdown or plain text); and events (on, emit, say) that tell front ends what happened (client.events).

Not here: anything that talks to a node (nodes.py), a group (groups.py) or a person (people.py, private.py).

BaseMixin

BaseMixin(vault: Vault, status=print)

data property

data: dict

The vault's contents (a dict): identities, nodes, groups, settings. Change it and call save(); clients should prefer the functions that do that for them.

from_key classmethod

from_key(
    identity: Identity,
    state_path,
    *,
    name: str = "service",
    display: str | None = None,
    status=print,
) -> "Session"

A session for a service with only a key: a gateway, a bot, a registrar (client/headless.py). Everything but the key is kept in state_path.

save

save() -> None

Write the vault to disk (encrypted, UN-RFC 0007).

change_passphrase

change_passphrase(old: str, new: str) -> None

A new passphrase for the vault file (slow: runs the key derivation twice).

Only this file changes; copies of it elsewhere still open with the old one. Raises VaultError if old is wrong.

has_identity

has_identity() -> bool

True when the vault has at least one identity.

on

on(callback)

Call callback(event) for every event (client.events); returns a function that stops it. Callbacks run on the event loop and must not block.

emit

emit(event: Event) -> None

Tell every listener (on) about an event (client.events). A listener that raises is logged and skipped, so a front end's bug cannot stop the session.

say

say(text: str, idle: bool = False) -> None

A status line: to the old status callback and as a Status event (idle when it only says a node is reached).

default_identity_name

default_identity_name() -> str

The name of the identity in use: the one act_as chose for this run, else the vault's default, else the first. SessionError if there is none yet.

act_as

act_as(name: str | None) -> None

Use the identity name for this run, without changing the vault's default (a command-line client's -i).

identity

identity(name: str | None = None) -> crypto.Identity

An identity's key pair (crypto.Identity), by name; by default the one in use (default_identity_name).

my_fingerprints

my_fingerprints() -> tuple[str, ...]

The fingerprints of all your identities (owner:me in a group search, #13).

display_name

display_name(name: str | None = None) -> str

The display name (From) of an identity, by default the one in use.

signature_template

signature_template(name: str | None = None) -> str

An identity's signature template: the default until the user sets one, and empty when they chose none.

set_signature

set_signature(
    template: str, name: str | None = None
) -> None

Set an identity's signature template (at most MAX_SIG_LINES lines; {name} and {key} are filled in). An empty template means none.

signature

signature(name: str | None = None) -> str

The signature with {name}, {key} and {fp} filled in ('' if none).

new_identity

new_identity(
    name: str, display: str | None = None
) -> crypto.Identity

Make a new identity, with display as its name in From (default: its name) and the default signature. The first one becomes the vault's default.

text_type

text_type() -> str

The Content-Type your posts get: Markdown, unless you turned it off (UN-RFC 0031).

preview

preview(
    text: str,
    kind: str = "post",
    content_type: str | None = None,
) -> dict

What a text will look like once posted (#632): {"tree", "signature"} - the same safe tree (UN-RFC 0031) messages carry, from the same core, so every client previews exactly what readers will see. content_type: yours by default.

set_markdown

set_markdown(on: bool) -> None

Whether you write in Markdown (UN-RFC 0031) or plain text.

Key ripening

Key ripening (UN-RFC 0025): a key works for a while, once, before it posts.

Belongs here: the chain in the vault and its published point (ripe@<fp>), computing segments (ripen_segment, in a thread or a browser worker through _prove_segment), taking over a chain published elsewhere, ensure_ripe before posting, the Key-Ripening header, what a node asks for (UNSEENET-MINRIPE), and the level private senders must have (section 3.5).

Not here: running ripening in the background (client.runtime.ClientRuntime), or postage itself (posting.py).

RipeningMixin

ripening

ripening(name: str | None = None) -> rp.Chain

The chain this identity has computed so far, in the vault (section 3.2).

ripening_kept_paused

ripening_kept_paused(name: str | None = None) -> bool

True when the user paused ripening this key on this device, until they resume it (#337): kept with the key's chain in this vault, not with your state, so a warm phone pausing does not pause the desktop.

set_ripening_kept_paused

set_ripening_kept_paused(
    paused: bool, name: str | None = None
) -> None

Remember (or forget) that ripening this key is paused on this device, and save; the client runtime calls it from pause_ripening and resume_ripening.

ripening_target

ripening_target(name: str | None = None) -> int | None

The level this key is ripened to on this device when the user chose one (3 or 4, #740), else None - the program's own default then (3 for clients). Kept with the key's chain like the pause (#337): the work is this machine's, and two devices should not both ripen one key.

set_ripening_target

set_ripening_target(
    level: int, name: str | None = None
) -> None

Ripen this key to level (3 or 4) on this device (UN-RFC 0025 section 7): 4 costs weeks of one core while the program runs, and then pays 2 bits less postage than 3 (section 4). The client runtime follows it at once.

ripening_speed

ripening_speed(name: str | None = None) -> float | None

Iterations a second this device proved the key's last segments at, or None.

ripening_seconds_left

ripening_seconds_left(
    level: int, name: str | None = None
) -> float | None

About how long ripening to level takes from where the key is, at this device's speed (its last segments): None when unknown, 0 when there.

ripe_group

ripe_group(name: str | None = None) -> str

The Group ID where an identity publishes its ripening chain: ripe@<fingerprint> (UN-RFC 0025).

published_level

published_level(name: str | None = None) -> int | None

The level nodes know for this identity: the newest point it published.

ripening_header

ripening_header(name: str | None = None) -> str | None

Key-Ripening for our articles: the newest published point (section 3.4).

node_min_ripe async

node_min_ripe(node: str | None = None) -> str | None

The node's UNSEENET-MINRIPE: a level, "-", or None for a node that knows nothing of ripening (before UN-RFC 0025). Such a node puts Key-Ripening in the canonical form, so it must never get the header: the signature would not match. Unreachable now: None, so nothing is computed for it.

node_wants_ripening async

node_wants_ripening(node: str | None = None) -> bool

Does this node ask for ripening (UNSEENET-MINRIPE other than "-")?

private_min_ripe

private_min_ripe() -> int | None

Hide private posts from senders below this level; None shows them all.

set_private_min_ripe

set_private_min_ripe(level: int | None) -> None

The lowest ripening level a sender of private posts must have for you to see them (UN-RFC 0025 section 3.5); None takes everybody.

sender_level async

sender_level(msg: 'Message') -> int | None

The level a private post's sender has, checked against the sender's own ripe@ chain (UN-RFC 0025 section 3.5); None if unripe or not shown.

check_private_senders async

check_private_senders() -> None

Learn the senders' levels for the private posts the setting may hide.

sender_shown

sender_shown(msg: 'Message') -> bool

With the setting on, a private post from someone who is not a contact is shown only when its sender is known to be ripe enough. A local choice: the node knows nothing of it (UN-RFC 0025 sections 3.5, 6).

ripen_segment async

ripen_segment(
    name: str | None = None, run_file=None
) -> bool

Compute this identity's next segment (section 3.2) in a worker thread and keep it in the vault. False at the top, or when run_file was removed. True also when the chain grew meanwhile from another caller (posting and the background may ask at once): that one's segment is kept, not ours.

adopt_published_ripening async

adopt_published_ripening(
    name: str | None = None, node: str | None = None
) -> bool

Take over this identity's chain from its own ripe@ group when that is further than the one kept here: the same key on another machine, a vault copy, a service reinstalled, a CI key ripened once elsewhere (UN-RFC 0025 section 3.3). The chain is checked first. True when it was taken over.

ensure_ripe async

ensure_ripe(
    name: str | None = None, node: str | None = None
) -> None

Before posting: ripen to level 0 if needed, and publish ripe@ when its chain has reached a level nodes don't know yet (sections 3.3, 7). node: publish there (the node about to be posted to); otherwise to all our nodes.

publish_ripening async

publish_ripening(
    name: str | None = None, node: str | None = None
) -> None

Post this identity's ripe@ manifest with its whole chain (section 3.3).

Nodes and the connection

Nodes and the connection to them: where the network is reached.

Belongs here: the nodes the user has (add_node, node_for - the node a group is read on), the known-node list and releases from the project's channels (UN-RFC 0010, 0020), the one connection per node over Tor with retries and backoff (_connect, _call), push (watch, UN-RFC 0008), Tor and bridges (set_tor, set_bridges), a node's information (UN-RFC 0011), and close.

Not here: what is read from a node (reading.py, groups.py) or posted to it (posting.py).

NodesMixin

nodes property

nodes: list[str]

The vault's node addresses, the default first.

node property

node: str | None

The default node: new groups and your inbox live here.

add_node

add_node(address: str, default: bool = False) -> None

Add a node address (xyz.onion or host:port) to the vault. default puts it first, so new groups and your inbox go there; an address already known only moves. A node added on purpose is tried at once, whatever its backoff.

remove_node

remove_node(address: str) -> list[str]

Stop using a node. Returns the followed groups that were read from it.

They are no longer bound to any node: the next read looks for them on your other nodes, default first, and keeps the one that carries each (fetch_group) - never moved blindly to a node that does not carry them (#603). A group nobody else carries shows as not reachable. rehome_groups does the looking at once, and says which groups no node of yours carries.

node_for

node_for(group_id: str | None) -> str | None

The node a group is read from and posted to.

pin_group

pin_group(group_id: str, address: str) -> None

Read a group from address from now on.

carried_by async

carried_by(address: str) -> set[str] | None

The Group IDs address carries, or None when it does not answer.

down_for

down_for(address: str | None) -> float

Seconds address has failed without a connection opening in between (0: up).

rehome_groups async

rehome_groups(group_ids=None) -> dict

Find a node of yours for groups without one (after remove_node), asking your nodes what they carry: the default first. {"moved": {gid: node}, "unavailable": [gid]} - unavailable: no node of yours carries it.

fall_back_to_known_node async

fall_back_to_known_node() -> str | None

None of your nodes answers (#603): take a random node from the newest signed known-node list (UN-RFC 0010 section 5) that does, make it your default, and read the groups there that it carries; your inbox goes along. Only listed nodes - never one learned from an article or a page. Returns its address, or None when no listed node answers either.

promote_default async

promote_default(give_up: float | None = None) -> str | None

The default node has failed for give_up seconds while another of your nodes answers (#605): make that one the default, so new groups and your inbox go there. The groups keep their own nodes. Returns the new default, or None.

node_name

node_name(address: str) -> tuple[str | None, str]

(name, source) of a node: source known (the signed node list), self (the node's own name, not checked) or none.

node_label

node_label(address: str | None) -> str

How a node is shown in one line: its listed name; its own name marked (unlisted); else the start of its onion address.

node_entry

node_entry(address: str) -> dict

One node for a front end: name, where it is from, label, the full address.

node_entries

node_entries() -> list[dict]

Your nodes, the default first, each with how many followed groups it serves.

known_nodes

known_nodes() -> list

The newest node list this client has: read from the network, else built in.

refresh_known_nodes async

refresh_known_nodes() -> bool

Read the newest signed node list from the project's channel. True if it changed.

The group ID carries the project key's fingerprint, its manifest is signed by that key, and only the owner and the manifest's posters may post: the node enforces that, and we check it again here.

refresh_releases async

refresh_releases() -> dict | None

Read the newest signed releases from unseenet.releases (#264) and return update_offer(). The posts only carry a release's checksums and their signature; the signature is checked against the keys the channel trusts (unseenet.update), whoever posted it.

update_offer

update_offer() -> dict | None

A signed release newer than this program, on its channel (a development build follows development builds): {"version", "channel"}, or None.

retry_in

retry_in(address: str) -> float

Seconds until address is tried again after it failed or refused (0: now).

reset_backoff

reset_backoff(address: str | None = None) -> None

Try again at once: after the user chose a node or entrance on purpose.

watch async

watch(
    on_new, pattern: str = "*", node: str | None = None
) -> bool

Listen for new articles (UN-RFC 0008) on a second connection.

Calls on_new(group_id, message_id) for each one. Returns False at once if the node cannot push; otherwise runs until the connection fails (raises). Watching "*" tells the node nothing about which groups you follow.

bridge_setting

bridge_setting() -> dict

The Tor bridge setting as stored: {"mode": "off"}, or the mode and the bridges the user gave (see set_bridges).

tor_in_use

tor_in_use() -> Socks | None

The running Tor this session uses, or None when it starts its own.

tor_setting

tor_setting() -> str

"auto", "own" or host:port - which Tor to use (the vault setting).

set_tor async

set_tor(value: str) -> None

Use a running Tor (host:port), our own (own), or decide (auto).

auto uses Tails' Tor on Tails and starts our own everywhere else.

set_bridges async

set_bridges(
    mode: str, custom: list[str] | None = None
) -> None

Change how Tor connects; Tor is restarted at the next connection.

close async

close() -> None

Close every node connection and stop the Tor this session started. Call it when the client ends; the vault itself needs no closing.

node_info async

node_info(node: str | None = None) -> dict

UN-RFC 0011 node information, or {} if the node does not offer it - with carries, how many groups the node itself carries (one LIST ACTIVE), so a client can show both that and how many of yours you read there (unseenet-gui#153).

Labels

Labels (UN-RFC 0019): how people and groups are shown, by name or key.

Belongs here: reading the registrar's lists (refresh_naming) and turning keys and Group IDs into what a front end shows - a verified name, a contact's name, a short form with the fingerprint (label_of, identity_label, group_label), so every client names people alike.

Not here: claiming or giving names (unseenet.names), contacts (people.py).

LabelsMixin

refresh_naming async

refresh_naming() -> bool

Read the root list, hierarchy lists and names (UN-RFC 0018, 0019). True if changed.

group_label

group_label(
    group_id: str, also_known: set[str] = frozenset()
) -> str

The name to show for a group: without the fingerprint where UN-RFC 0018 allows. A personal feed or site is named after its owner - "Andreas~42e5's feed" (#423).

identity_label

identity_label(key_text: str, from_name: str = '') -> str

Petname, else @domain, else @name, else Name~abcd (UN-RFC 0019 section 8).

seen_name

seen_name(key_text: str | None) -> str

The name someone gave themselves in a post we have, newest first, for naming a bare key (a chat partner, a moderator): "" if none is seen.

label_of

label_of(key_text: str) -> str

identity_label for a bare key, with the name seen in its posts.

Groups

Groups: what they are, and the owner's and moderators' side of them.

Belongs here: a group's manifest (fetching and checking it, manifest_data, UN-RFC 0004), creating and changing groups (create_group, update_group with the fields of 0004, 0014, 0027-0035), may you write here (write_permission, #181), moderated groups and submissions (0004 section 5, 0009 section 3), roles, bans and members, portals to other networks (0023), removing a group (0013), and finding groups (filter_groups, find_group_ref).

Not here: following and reading groups (reading.py), posting to them (posting.py).

GroupsMixin

filter_groups

filter_groups(
    groups: list["GroupInfo"],
    query: str,
    known: dict | None = None,
) -> list["GroupInfo"]

The groups that match a search (#13, client.groupfilter), locally. known: manifest data fetched for groups you do not follow.

group_manifests async

group_manifests(
    groups: list["GroupInfo"], known: dict
) -> int

Fetch the manifests of listed groups whose type is not known yet, from the node that carries each, in a few requests (UN-RFC 0041), into known (Group ID -> manifest data) and each GroupInfo's type. Asking for all of them tells a node nothing about what you search for. Returns how many were asked.

group_entry

group_entry(group_id: str) -> dict

The vault's entry for a group (created empty if missing): type, ownership, keys of closed groups, the node it is read on, and what the client remembers.

list_groups async

list_groups(
    pattern: str = "*",
    node: str | None = None,
    budget: float | None = None,
) -> list[GroupInfo]

Groups on one node, or on all your nodes (merged; unreachable ones skipped).

budget: seconds to wait for each node's first answer (#704). A node slower than that does not hold up the others: what the rest list comes back at once, the slow one is in slow - its answer still awaited in the background (a task the caller can wait on, late_answers) - and its groups come with the next listing. Without a budget every node is waited for, as before.

late_answers

late_answers() -> list[asyncio.Task]

The listings still awaited from slow nodes (#704), to act when they come.

is_slow

is_slow(node: str | None) -> bool

Whether a node was asked and has not answered yet (#704).

find_group_ref async

find_group_ref(ref: 'GroupRef') -> list[GroupInfo]

The groups a #reference (#172) can mean: from your nodes, plus groups you already know of. Groups you follow come first, then the one the name stands for on its own (endorsed or the only one, UN-RFC 0018), then the rest.

create_group async

create_group(
    name: str,
    gtype: str,
    title: str = "",
    description: str = "",
    min_pow: int = _s.DEFAULT_POW,
    local: bool = False,
    retention_days: int | None = None,
    charter: str = "",
    view: str | None = None,
    low_latency: bool = False,
    kinds: list[str] | None = None,
    node: str | None = None,
    comments: str | None = None,
    comment_days: int | None = None,
    charter_type: str | None = None,
    moderators=(),
    posters=(),
    extra: dict | None = None,
) -> str

charter: the group's charter, what it is for and asks of those who write (UN-RFC 0014), with charter_type for Markdown; moderators/posters: keys as text; extra: attachments, max_pins, max_pages, home (see groups.EXTRA_FIELDS).

write_permission

write_permission(
    group_id: str,
    reply_to: "Message | None" = None,
    kind: str | None = KIND_POST,
    now: datetime | None = None,
) -> "Permission"

Whether you may write here, and if not, why - to tell people before they write (#181). Checks, from the cached manifest: the kinds a group takes (UN-RFC 0028), the ban list (0004 section 5.1) and maintainers' blocks (0018), type and posters, closed-group membership, and comments in channels with their comment_days (0030). reply_to: the post you would answer; kind None: any kind. An unknown group is allowed: the node has the last word, and this only spares surprises.

find_message

find_message(
    group_id: str, message_id: str
) -> "Message | None"

A message of a group this client has read, by Message-ID.

may_post

may_post(group_id: str) -> bool

Whether a new post of yours would be accepted here (any kind).

may_reply

may_reply(
    group_id: str, reply_to: "Message | None" = None
) -> bool

Whether a reply of yours would be accepted, comments in channels included.

moderates

moderates(group_id: str) -> bool

True when you own or moderate the group, by the manifest or because the vault says you own it - the client's view, for showing moderation tools.

submission_targets

submission_targets(
    group_id: str,
) -> list[tuple[str, bytes | None]]

(inbox, owner key or None) to send a submission to (UN-RFC 0009, 3).

was_submitted

was_submitted(group_id: str, message_id: str) -> bool

True when you sent this post to the group's moderators and it waits for them (UN-RFC 0009 section 3).

submissions

submissions() -> list[tuple['Message', Article]]

Submissions waiting in your inbox for groups you moderate, oldest first.

approve_submission async

approve_submission(msg: 'Message', art: Article) -> None

Approve and post a submitted article; its Message-ID stays the same.

reject_submission async

reject_submission(msg: 'Message', reason: str = '') -> None

Moderator: turn a submission down (UN-RFC 0009 section 3). With a reason, the author gets it as a private post, if they can be answered.

is_data_group

is_data_group(group_id: str) -> bool

A group for programs, not people (UN-RFC 0008 section 3.1.1).

manifest_data

manifest_data(group_id: str) -> dict

The newest manifest seen for a group ({} if none yet), with its charter under the current names also when it was signed with the earlier ones (UN-RFC 0014 2.2).

group_portals

group_portals(group_id: str) -> list[tuple[str, str]]

(network, source) of the group's portals: where its posts are also published.

portal_warning

portal_warning(group_id: str) -> str | None

The warning to show before posting, or None if there are no portals or the user has accepted this set of them. A new or changed portal asks again.

accept_portals

accept_portals(group_id: str) -> None

Accept the group's current portals (UN-RFC 0023 section 3.5): the client stops warning that posts here also reach other networks, until the portals change.

group_kinds

group_kinds(group_id: str) -> tuple[str, ...]

What a group takes: chat lines, posts with a subject, or both (UN-RFC 0028, 4.1).

update_group async

update_group(group_id: str, **changes) -> dict

Publish a new manifest with a higher serial (owner only).

Allowed changes: title, description, charter, charter_type (UN-RFC 0014 2.1), view, kinds, low_latency, min_pow, retention_days. Type, owner and locality can never change (UN-RFC 0004, 4).

banned

banned(group_id: str) -> list[tuple[str, str]]

(key, reason) of the keys the group's current manifest bans.

is_banned

is_banned(group_id: str, key_text: str | None) -> bool

True when the group's current manifest bans this key (UN-RFC 0004 section 5.1).

ban async

ban(group_id: str, key_text: str, reason: str = '') -> None

Owner: ban a key from posting in the group (a new manifest).

unban async

unban(group_id: str, key_text: str) -> None

Owner: lift a ban (a new manifest without that key).

charter_unseen

charter_unseen(group_id: str) -> bool

True if the group has a charter the reader has not seen in this version, so the client shows it before they read or write there (UN-RFC 0014 section 3).

mark_charter_seen

mark_charter_seen(group_id: str) -> None

Note that the user has seen the group's charter in its current version, so the client stops showing it until the owner changes it.

remove_group async

remove_group(group_id: str, reason: str = '') -> None

UN-RFC 0013: remove a group you own from the whole network. Final.

forget_group

forget_group(group_id: str) -> None

Unsubscribe and delete everything stored locally about a group.

invite async

invite(group_id: str, member_key: str) -> None

Owner of a closed group: give member_key the group's current key (a key update granting access, UN-RFC 0004 section 6) and note them as a member.

members

members(group_id: str) -> list[str]

Owner: the keys you invited to a closed group (envelopes do not say whom they are for, so this record is the list). Members invited from another device, or before this was recorded, are missing.

remove_member async

remove_member(group_id: str, member_key: str) -> int

Owner: start a new epoch without the member (UN-RFC 0004, 6.1-6.2). The others get the new key; the removed member keeps what they read. Returns the new epoch.

set_role async

set_role(
    group_id: str, role: str, key_text: str, on: bool = True
) -> None

Owner: add or remove a moderator or a poster (UN-RFC 0004, 3.1 and 5).

fetch_manifests async

fetch_manifests(
    group_ids: list[str],
    node: str | None = None,
    fallback: bool = True,
) -> dict

The current manifests of many groups, Group ID -> Manifest or None.

With XUN-MANIFESTS (UN-RFC 0041) in requests of up to 256 groups; else one group at a time. Cached Message-IDs are not named (section 3.3), so a node cannot recognise this client by what it holds. fallback False: on a node without the command, ask nothing (single lookups read whole groups, fine for profile groups but not for ordinary ones) and return None for each.

fetch_manifest async

fetch_manifest(group_id: str, node: str | None = None)

The current valid, unexpired manifest of one group; None if there is none.

moderates_or_owns

moderates_or_owns(group_id: str) -> bool

True when the group's current manifest names you as owner or moderator: who may withdraw others' posts (UN-RFC 0004 section 7).

Private posts and key-bound groups

Private posts and the key-bound groups that are yours (UN-RFC 0009, 0029).

Belongs here: the personal inbox (my_inbox, ensure_inbox), sending and reading private posts and chats (send_private, send_chat, conversation, private_messages), the sent copies, a new identity's first lifetime (0029 section 4.1, #270), and renewing the key-bound groups of an identity together (0029 section 4).

Not here: contacts and who you ignore (people.py), profiles (profiles.py).

PrivateMixin

my_inbox property

my_inbox: str

The Group ID of your personal inbox (UN-RFC 0009): inbox@<fingerprint>.

ensure_inbox async

ensure_inbox() -> str

Make sure this identity's personal inbox exists on the node and is subscribed, and renew its key-bound groups if due (every client opens with this).

in_use

in_use(
    name: str | None = None, now: datetime | None = None
) -> bool

Has this identity been used, so that its key-bound groups get the full lifetime? Until then a visitor's inbox lives FIRST_LIFETIME.

key_bound_lifetime

key_bound_lifetime(name: str | None = None) -> timedelta

How long a key-bound group of this identity lives when started or renewed: short until the identity is in use, the full lifetime after (UN-RFC 0029 section 4).

note_opened

note_opened(today: date | None = None) -> None

Count a day this identity was opened; on IN_USE_DAYS different days it is in use.

mark_in_use

mark_in_use(name: str | None = None) -> None

The identity is used (posted, wrote, added a contact, made a group, ...): its key-bound groups are renewed with the full lifetime at the next renewal, which comes at once since a first lifetime is short.

inbox_owner async

inbox_owner(group_id: str) -> bytes

The owner key of an inbox, from its manifest on the node.

key_bound_groups

key_bound_groups(name: str | None = None) -> list[str]

The key-bound groups this identity owns and has a manifest for.

key_bound_expiry

key_bound_expiry(
    name: str | None = None,
) -> datetime | None

When this identity's key-bound groups run out, if not renewed (the earliest).

renew_key_bound async

renew_key_bound(
    now: datetime | None = None, rng: Random | None = None
) -> list[str]

Renew key-bound groups that are due, without asking (UN-RFC 0029 section 4).

Each identity is renewed at a random moment, chosen on its own, within 30 days of becoming due, and all of its key-bound groups together at that moment. If expiry is near (or the manifest has no expires), it renews at once. Returns the groups renewed.

send_private async

send_private(
    group_id: str,
    subject: str,
    body: str,
    *,
    owner_key: bytes | None = None,
    references: list[str] | None = None,
    anonymous: bool = False,
    content_type: str | None = None,
    kind: str = KIND_POST,
    remember: bool = True,
    extra: list[tuple[str, str]] | None = None,
    files=None,
    as_team: str | None = None,
) -> str

Send a message to an inbox. Only its owner can read it.

kind is "post" (a subject, read on its own, answered with a quote) or "chat" (a line in a running conversation, no subject) - UN-RFC 0028. files are prepared files, sent as encrypted blobs whose keys travel inside the sealed box (UN-RFC 0027 sections 3.3, 4.3), if the inbox takes them. To a team inbox it is a team box its readers open (UN-RFC 0047 section 4), from its current manifest. as_team: write on a team inbox's behalf, with your team key for it, and answers go there (section 8).

reply_target

reply_target(msg: 'Message') -> str | None

Where a reply to a private message goes.

sent_messages

sent_messages(to: str | None = None) -> list['Message']

Your copies of the private messages you sent (to one key, or all).

They are sealed to their recipients, so the node's copies are unreadable to you; these come from the encrypted sent sidecar of the vault.

conversation

conversation(
    key_text: str, kind: str | None = None
) -> list["Message"]

Private messages between you and a key, both directions, oldest first.

kind keeps only chats or only posts (UN-RFC 0028); None keeps both.

private_messages

private_messages(
    kind: str | None = None,
) -> list["Message"]

Your inbox and what you sent privately, oldest first; only one kind if given.

unread_from

unread_from(key_text: str, kind: str | None = None) -> int

How many private posts from this key you have not read; kind chat or post counts only those.

unread_private

unread_private(kind: str | None = None) -> int

Unread private messages of one kind (or all), from anyone.

send_chat async

send_chat(
    group_id: str, text: str, files=None, languages=None
) -> str

Send a chat line (UN-RFC 0028): to a group, or to one person when group_id is contact:<key>. No subject and no signature, unlike a post. files are prepared files, as for post() (UN-RFC 0028 section 6.1, #496). Returns the Message-ID. Portals and mention notices are the client's to ask about first, as for post(). languages: what it is written in; in a group, left out, the suggestion for the group (UN-RFC 0052 5a) - a chat line is not asked about.

private_posts

private_posts() -> list['Message']

What the "Private posts" view shows (UN-RFC 0028 section 6.2), oldest first.

unread_private_posts

unread_private_posts() -> int

How many private posts (not chats or notices) you have not read.

chat_partners

chat_partners() -> list[str]

Everyone you have a chat with: your contacts, and anyone else you have chatted with (so no chat ever goes missing, UN-RFC 0028 section 6.2).

Contacts first, by name; then the others, the most recent chat first.

node_has async

node_has(capability: str, node: str | None = None) -> bool

Whether a node lists a capability (UN-RFC 0005); False when unreachable.

takes_sealedbox2 async

takes_sealedbox2(inbox: str) -> bool

Whether a private post to this inbox may be sealedbox2: its owner says so in its manifest, and our node takes the form (UN-RFC 0009 section 2.3).

offer_sealedbox2 async

offer_sealedbox2() -> bool

Say in our inbox's manifest that we take sealedbox2 - once every node of ours does (UN-RFC 0009 section 2.2). True if the manifest was changed.

inbox_files

inbox_files() -> bool

Whether your inbox takes pictures and PDFs in private posts and chats (#497,

637); on unless turned off with set_inbox_files.

set_inbox_files async

set_inbox_files(on: bool) -> None

Let your inbox take pictures and PDFs, or not; the manifest is changed right away.

offer_inbox_files async

offer_inbox_files() -> bool

Make our inbox's manifest say what inbox_files says: pictures and PDFs, up to 4 MiB each, encrypted (UN-RFC 0027 sections 3.3, 5.1), or none - an inbox that took pictures only takes PDFs too from now on (#637). True if the manifest was changed. The blobs are only carried by nodes that carry encrypted blobs, which senders are told when theirs does not.

fetch_sent_copies async

fetch_sent_copies() -> int

Your private posts sent from another device, read from the recipients' inboxes through their second envelope (UN-RFC 0044 section 6.2), into your sent list. Returns how many were added; those that cannot be had now are tried again later.

Profiles

Profiles and pictures (UN-RFC 0022).

Belongs here: reading profiles (profile_of, read in batches so a node cannot tell whose you open, prefetch_profiles), publishing your own and your picture (publish_profile, publish_picture), hiding someone's profile for yourself, and the pictures of many keys at once, read together and decoded in a separate process before they are shown (pictures, UN-RFC 0022 section 8, UN-RFC 0027 section 9.2), with a switch to show identicons only (show_pictures).

Not here: private profiles shared in a group or a private post (#182, coming), labels (labels.py).

ProfilesMixin

profile_of async

profile_of(
    key_text: str, refresh: bool = False
) -> "prof.Profile | None"

The public profile of a key (UN-RFC 0022 section 5.2), cached for a day. None when there is none: that is not an error (5.2).

prefetch_profiles async

prefetch_profiles(keys) -> int

Read the profiles of many keys at once into the cache - all the authors of a group, rather than one when the user opens it (UN-RFC 0022 section 9.2, UN-RFC 0041 section 3.2). Hidden profiles and our own are skipped. Returns the number of keys asked for.

my_profile

my_profile() -> dict | None

This identity's published profile object, or None.

has_published_profile

has_published_profile() -> bool

True when this client has the manifest of your profile group, i.e. your profile has been published (UN-RFC 0022).

publish_profile async

publish_profile(profile: dict | None) -> dict

Publish (or with None, empty) this identity's public profile (5.1, 5.3).

publish_picture async

publish_picture(picture: dict | None) -> dict

Publish (or with None, remove) this identity's picture (section 8.2). picture must already be re-encoded without metadata (8.1).

publish_picture_file async

publish_picture_file(path, alt: str = '') -> dict

Publish a picture from a file as this identity's profile picture: the middle square, at most 128 pixels and 8 KiB, saved again without metadata (client.media.avatar, section 8.1). Returns the published manifest data.

make_picture staticmethod

make_picture(octets: bytes, alt: str = '') -> dict

The picture object that would be published from a picture's octets: the middle square, at most 128 pixels and 8 KiB, saved again without metadata (section 8.1). A client shows it before publishing it (8.5).

my_picture

my_picture() -> dict | None

This identity's published picture object, or None.

show_pictures

show_pictures() -> bool

False when the user chose identicons only (section 8.5).

set_show_pictures

set_show_pictures(on: bool) -> None

Show people's pictures (True) or identicons only (False); kept in the vault.

pictures async

pictures(keys) -> dict

The pictures of many keys, read together so a node cannot tell whose you look at (sections 8.2, 13): key -> {"type", "px", "octets", "alt"} or None. Each is decoded and saved again in a separate process and kept only if it is the square still picture it says (8.1); none for a key you hid or ignore, or when pictures are off - then the identicon is shown (8.5). Kept in memory a day.

picture_of async

picture_of(key_text: str) -> dict | None

The picture of a key, if its node carries avatar groups (section 8.2).

share_profile async

share_profile(to: str, profile: dict | None = None) -> str

Share a profile with one person (their inbox) or a closed group, where only they can read it (section 6): by default your public one, or another made for them - with a picture inline if you like (8.3). Returns the Message-ID.

withdraw_shared_profile async

withdraw_shared_profile(to: str) -> str

Take back the profile you shared there (an empty one, section 6).

private_profile

private_profile(key_text: str, context: str) -> dict | None

The profile this key shared in this context ("me", or a closed group's ID), or None. Never one from another context (section 6).

shared_contexts

shared_contexts(key_text: str) -> list[str]

The contexts in which this key has shared a profile you can see.

hide_profile

hide_profile(key_text: str, hidden: bool = True) -> None

Section 9.2: the user may hide the profile of a key.

profile_hidden

profile_hidden(key_text: str) -> bool

True when you hid this key's profile for yourself (see hide_profile).

refused_picture

refused_picture(exc: SessionError) -> SessionError

A node's refusal of a picture on its refusal lists (#715), in words for the person; any other error as it is.

People

People: contacts, who you ignore, what is for you, and reporting.

Belongs here: contacts and the address book (UN-RFC 0028 section 6.2), ignoring keys (the reader's own killfile, never sent), replies and mentions for you (#109, #170) and mention notices (UN-RFC 0032), hierarchy blocks (0018 section 8.2), bots (0030 section 6), and reporting a post to those who can act on it (#74).

Not here: private posts themselves (private.py), profiles (profiles.py).

PeopleMixin

add_contact

add_contact(key_text: str, name: str) -> None

Give a key a name of your own (a "petname"). Others cannot fake it.

remove_contact

remove_contact(key_text: str) -> None

Remove a key from your contacts.

close_chat

close_chat(key_text: str) -> None

Take a chat with a person off the list of chats, on every device (it is in your state). The messages stay; it opens again by itself with the next chat line from or to them, so no new message is ever hidden (UN-RFC 0028, 6.2).

open_chat

open_chat(key_text: str) -> None

Put a closed chat back on the list.

chat_closed

chat_closed(key_text: str) -> bool

Whether a chat is closed: closed, and no chat line from or to them since.

contact_name

contact_name(key_text: str | None) -> str | None

The name the user gave this key in their contacts, or None.

ignore

ignore(key_text: str, label: str = '') -> None

Ignore a key: its posts are hidden from you everywhere. Only this vault knows; nothing is sent (#72). label is how the list shows it.

unignore

unignore(key_text: str) -> None

Stop ignoring a key.

is_ignored

is_ignored(key_text: str | None) -> bool

True when you ignore this key (see ignore).

ignored

ignored() -> list[tuple[str, str]]

(key, label) of the people you ignore, sorted by label.

my_message_ids

my_message_ids() -> set[str]

The Message-IDs of posts by any identity in this vault, for finding replies to you (#109).

for_me

for_me(
    msg: "Message",
    mine: set[str] | None = None,
    patterns: list | None = None,
) -> str | None

'reply' if the post answers one of yours, 'mention' if it names you, else None. A checked mention notice (UN-RFC 0032) is a 'mention' too.

for_you

for_you(
    unread_only: bool = True, rules: bool = True
) -> list["Message"]

Replies to you and mentions of you in the groups you follow: newest first, or as your [foryou] rules have it (UN-RFC 0051 5.5) - hidden ones left out. rules=False: Show all, without them.

scored_for_you

scored_for_you(
    unread_only: bool = False, rules: bool = True
) -> list[tuple["Message", "sc.Result"]]

For you with each entry's result: "quiet" in its actions means listed but not counted and no notification (UN-RFC 0051 5.5). Only [foryou] rules act; without them (or with rules=False), newest first, nothing left out.

for_you_quiet

for_you_quiet() -> set[str]

The Message-IDs your [foryou] rules quiet or hide: not counted, not told.

for_you_count

for_you_count(group_id: str) -> int

How many posts in the group are for you - replies to you and mentions - and not read yet (the total of for_you_counts).

for_you_counts

for_you_counts(
    group_id: str | None = None,
    quiet: set[str] | None = None,
) -> "ForYouCount"

Unread replies to you and unread mentions of you, counted apart (#170): in one group, or in all the groups you follow when group_id is None. quiet: for_you_quiet() when the caller already has it - it looks at every group, so counting group by group asks for it once (#728).

notice_settings

notice_settings() -> dict

Whether you send notices to people you mention, and accept them (both on).

set_notice_settings

set_notice_settings(
    send: bool | None = None, receive: bool | None = None
) -> None

Whether you send mention notices, and whether you take them (UN-RFC 0032). None leaves a setting as it is.

mentioned

mentioned(subject: str, body: str) -> list[tuple[str, str]]

(name as written, key) of the people a text names by a certified name that this client can resolve, in order, without you, each key once (section 4.1).

may_notify

may_notify(group_id: str) -> bool

Notices are for posts in open, moderated and channel groups (section 4.1).

send_mention_notices async

send_mention_notices(
    group_id: str,
    msgid: str,
    subject: str,
    kind: str,
    keys: list[str],
    delay: float | None = None,
) -> int

Tell up to 5 people that a post of yours names them (section 4). Waits a random time first unless delay is given. Returns how many were sent.

A post sent to the moderators is not in the group yet: the notices are built now and go to the moderators with it, who post them when they approve it (section 4.3) - so it does not matter when you are online again.

notice_bundle

notice_bundle(art: Article) -> list[Article]

The notices the author sent with a submission: inbox articles to personal inboxes, from a bundle the author signed. At most 5.

is_notice staticmethod

is_notice(m: 'Message') -> bool

True when a private post is a mention notice (UN-RFC 0032): it names the post and group that mention you.

notice_ok

notice_ok(m: 'Message') -> bool

A notice to show: checked, wanted, and not already marked in a followed group (section 5, rules 1-2).

notices

notices() -> list['Message']

Checked mention notices in your inbox, for For you.

notice_post

notice_post(m: 'Message') -> dict

What the check found about the mentioned post: group, id, subject, from.

check_notices async

check_notices() -> int

Check new mention notices against their posts (section 5, rule 3). Returns how many were found good.

visible

visible(msgs: list['Message']) -> list['Message']

Leave out messages from people you ignore (your own are always shown), and show only the newest version of an edited post (UN-RFC 0042). With private_min_ripe set, private posts from unripe strangers are left out too. Posts hidden by spam reports stay, marked with Message.reported, so that clients show them as hidden and why (UN-RFC 0045 section 7.3).

hierarchy_blocks

hierarchy_blocks(
    group_id: str,
) -> dict[str, tuple[str, str]]

{key: (hierarchy, reason)} that maintainers block for this group.

hidden_by_blocks

hidden_by_blocks(
    group_id: str,
) -> list[tuple[str, str, int]]

(hierarchy, reason, posts) hidden in a group by maintainers' blocks.

set_show_blocked

set_show_blocked(on: bool) -> None

Whether posts by keys a hierarchy's maintainers block are shown anyway (UN-RFC 0018 section 8.2).

is_bot

is_bot(key_text: str | None) -> bool

Whether the key's profile says it posts automatically (bot: true), from the cached profiles only (no network; the client runtime keeps them fresh). A statement by the key's holder: a missing flag proves nothing (0030, 6).

client_setting

client_setting(client: str, key: str, default=None)

A setting of one kind of client - client_setting("tui", "theme") - kept in the vault and, with your state, in the network (UN-RFC 0044 section 4.2), so it follows you from device to device. Clients choose their own keys.

set_client_setting

set_client_setting(client: str, key: str, value) -> None

Change one of a client's settings (None removes it), and save.

hide_bots

hide_bots() -> bool

True when the user chose to hide posts by bots (UN-RFC 0022 section 4).

set_hide_bots

set_hide_bots(on: bool) -> None

Leave out posts by keys whose profile says bot (UN-RFC 0030 section 6).

authors

authors(limit: int = 500) -> list[str]

The keys that wrote what you can read in your groups, the newest first: whose profiles to read in one batch (UN-RFC 0041 section 3.2).

contacts

contacts(query: str = '') -> list[tuple[str, str]]

(key, name) sorted by name; with query, only those it finds (#523): any part of the name you gave them, of their key or of a name they hold (sonia@lorensen), case-insensitive - so a long address book is quick to search, and a pasted key finds its contact.

contact_words

contact_words(key: str, name: str = '') -> list[str]

What a search for a contact looks at, lower-cased: the name you gave them, their key (with and without its ed25519:) and the names they hold.

held_names

held_names(key: str) -> list[str]

The names a key holds (sonia@lorensen), as shown; [] if none known.

report_targets async

report_targets(
    msg: "Message",
) -> list[tuple[str, str, str]]

(who, inbox, the inbox's owner key) of those who can act on a post: the group's owner, its moderators, the maintainers of its hierarchy (at their contact, UN-RFC 0018 4.1), the operator of the node it was read from, and the spam report issuers that apply (at their contact, UN-RFC 0045 section 3). Two that share an inbox - a team inbox - are one target.

report async

report(
    msg: "Message",
    targets: list[tuple[str, str, str]],
    note: str = "",
    anonymous: bool = False,
    files=None,
    reason: str = "",
) -> int

Send a report of a post to the chosen people. Returns how many got it.

note is Markdown (UN-RFC 0031); files are prepared files - a screenshot, a document for the police - sent encrypted to each, as on a private post (UN-RFC 0027 section 3.3); reason one of REPORT_REASONS. The post's group, Message-ID and author are lines of their own, so the receiver's client can find the post and, for an issuer, publish it (UN-RFC 0045 section 7.4).

report_fields staticmethod

report_fields(msg: 'Message') -> dict

The lines of a report you received: Group, Message-ID, Author key and Reason where given; {} when it is not a report.

reported_post classmethod

reported_post(msg: 'Message') -> tuple[str, str] | None

(group, Message-ID) named in a report you received.

Reading

Reading: what you follow, and how a group is fetched, kept and shown.

Belongs here: subscribing (also the starter groups), the groups you follow with their counts (subscribed_groups), fetching a group from its node (fetch_group), decrypting closed groups, threads and views (threads, view_of), the encrypted offline cache (UN-RFC 0007), and read marks.

Not here: a group's manifest (groups.py), private posts (private.py).

ReadingMixin

subscribe

subscribe(
    group_id: str, on: bool = True, node: str | None = None
) -> None

Follow a group (or stop, on=False). It is read on node if given, else on the default node. Emits GroupsChanged when the list changed.

peek_group async

peek_group(
    group_id: str,
    node: str | None = None,
    max_heads: int = 300,
) -> tuple[dict, int]

(manifest, number of posts) of a group you may not follow, for browsing.

Reads only headers, and the manifest articles themselves; the manifest must be signed by the owner named in the group ID. Nothing is cached or subscribed.

subscribe_starter_groups

subscribe_starter_groups() -> bool

Follow the project's starter groups (a new client has something to read): each version of the list once per vault, so groups added later reach those who started earlier too. A group left later is never added again - only the groups of versions this vault has not taken yet are. True if any were added.

subscribed_groups

subscribed_groups(
    available: dict[str, GroupInfo] | None = None,
    *,
    counts: bool = True,
) -> list[GroupInfo]

The groups you follow, in your order, as every client lists them.

available is what your nodes list (list_groups()), when you have it: a group missing from it gets listed=False. Key-bound groups such as your inbox are never listed, so they always count as listed (UN-RFC 0040 section 3). counts fills in unread posts, replies and mentions; leave it off when the client counts in its own time.

group_info

group_info(
    group_id: str,
    available: dict[str, GroupInfo] | None = None,
    *,
    counts: bool = True,
    quiet: set[str] | None = None,
) -> GroupInfo

One group as every client shows it, followed or not (a group being browsed, say): type, title, view, what it takes, and with counts the unread posts, replies and mentions. available as for subscribed_groups(). Nothing is stored for a group you don't follow.

threads

threads(group_id: str) -> list[tuple['Message', int]]

A group's posts as every client shows them: (message, depth) rows.

Threads newest first for a post view; oldest first, flat, for a chat view and a contact's chat; newest first, flat, for "For you"; entries newest first with their comments for a feed (feed_rows). People you ignore are left out and an edited post shows its newest version.

feed_rows

feed_rows(group_id: str) -> list[tuple['Message', int]]

A group as a feed (UN-RFC 0034 section 4), whatever view you chose for it: every post or note that answers nothing is an entry, newest first by Date, each followed by its comments in order, at most two levels deep - the same rows in every client. The timeline is entries only.

view_of

view_of(group_id: str | None) -> str

"threads", "chat", "feed" or "data": your own choice for the group, else its owner's (UN-RFC 0008 section 3.1), else threads. Private posts and "For you" are always threads, the timeline a feed, a contact's chat always starts as chat.

set_view

set_view(
    group_id: str, view: str, *, save: bool = True
) -> None

Remember how you want to see a group; None goes back to the owner's.

skip_to_now async

skip_to_now(group_id: str) -> int

Begin following a group at its newest article, without fetching what is there (#669: a bot that judges only what comes): its manifest is read so its settings are known; the next fetch_group reads only what comes after. Returns the article number it starts after.

fetch_group async

fetch_group(
    group_id: str, skip=None, keep: bool = True
) -> list[Message]

Fetch new articles in a group; returns all cached messages, oldest first.

For programs that follow many groups (#669): skip, a function of an article's overview headers (Subject, From, Date, Message-ID, References, :bytes, number) that returns True for articles not to fetch at all - a manifest or control article is always fetched, since the group's state depends on it. keep=False keeps nothing in the cache or the offline cache, only how far it has read, and returns just the new messages.

node_of_number

node_of_number(group_id: str, number: int) -> str | None

The node an article of a local group came from, by its number here.

read_from

read_from(group_id: str) -> list[str]

The nodes a group is read from: the one it is read from, and for a local group your other nodes that carry it too (#134).

load_offline

load_offline() -> int

Load cached articles so groups can be read without a connection.

Article numbers are local to a node, so the cache is only used for the node it was made with. Returns the number of articles loaded.

save_offline

save_offline() -> None

Write the offline cache - the newest articles of each group, encrypted with the vault's key - so the client can show them before it is connected.

is_read

is_read(msg: Message) -> bool

True when you have read this post, or an earlier version of it - an edit of a read post stays read (UN-RFC 0042).

mark_read

mark_read(msg: Message, save: bool = True) -> None

Mark a post read. Each group keeps the newest marks only (MAX_READ_MARKS); save=False when marking many at once and saving after.

mark_group_read

mark_group_read(group_id: str) -> int

Mark a whole group read in one go - a newsreader's "catch up" (#625): every post this client has of it, saved once; your other devices learn it with your state (UN-RFC 0044 section 4.3). Not a "read up to" date of its own: Dates are rounded to 10 minutes, so a post coming a minute later could carry the same one and be taken as read unseen. Returns how many posts were unread.

read_marks

read_marks() -> dict

How far each group read with fetch_group(..., keep=False) got: {group ID: {"node", "high"}} - article numbers belong to one node. Kept in the vault (or the service's state file) by fetch_group itself, so a restart goes on from there; a program may also keep them where it likes.

set_read_marks

set_read_marks(marks: dict) -> None

Go on from marks (as read_marks gives them): each group read from its number on, where its node is still the one it is read from.

unread_count

unread_count(group_id: str) -> int

How many posts in the group you have not read, among those shown (not ignored or blocked), from what this client has fetched.

Posting

Posting: writing to the network, and taking it back.

Belongs here: posting a post or chat line (post), postage (UN-RFC 0006, with the ripening level, 0025 section 5), posting articles built elsewhere (post_article, for bots, gateways and the local API), editing (edit, UN-RFC 0042) and withdrawing (withdraw, cancel, UN-RFC 0004 section 7).

Not here: private posts (private.py), group settings (groups.py).

PostingMixin

required_pow async

required_pow(
    group_id: str | None,
    group_min: int | None = None,
    node: str | None = None,
) -> int

Postage to attach: max(node minimum, group minimum) - UN-RFC 0008 section 4.

may_cancel

may_cancel(msg: Message) -> bool

Your own post, or any post in a group you own or moderate (UN-RFC 0004, 7.1).

withdraw async

withdraw(
    group_id: str, message_id: str, reason: str = ""
) -> str

Withdraw a post by its Message-ID (UN-RFC 0004 section 7): any post in a group you own or moderate, else your own. The node has the last word.

withdraw_all async

withdraw_all(
    group_id: str,
    keep=(),
    reason: str = "",
    keep_authors=(),
) -> list[str]

Owner or moderator: withdraw every post in the group but those in keep, and those by the keys in keep_authors (for example a demo's weekly reset, which keeps its own people's posts). Returns the Message-IDs withdrawn.

cancel async

cancel(msg: Message, reason: str = '') -> str

Withdraw a post from the network (UN-RFC 0004, section 7). Best effort: people who read it already, and nodes that ignore cancels, may keep it.

post_article async

post_article(
    art: Article,
    node: str | None = None,
    sign_with: Identity | None = None,
    *,
    blobs=(),
    extra_bits: int = 0,
) -> str

Post an article built elsewhere (a gateway's imports, a registrar's answers, a bot's posts), as everything else is posted: ripening first where the node asks for it, Key-Ripening set, our chain sent to a node that lacks it. Returns the Message-ID; raises SessionError if refused.

With sign_with, the article is signed here with the postage this node and group need for that key's level (UN-RFC 0025 section 5), and once more if the node asks for more. Without it, it must be signed already.

blobs are (blob ID, octets) for the files the article's Blobs and Attachment-<n> headers name; they are sent first on the same connection (BLOBPOST, UN-RFC 0027 section 6). extra_bits is the postage the files add (section 4.5, core.blobs.extra_bits_for), paid on top.

postage_for async

postage_for(art, node: str | None = None) -> int

The postage for an article (or a group ID) on this node: a manifest or a removal pays only the node's minimum; anything else the group's too, which is learned from the network if we don't have it, rather than guessed high (a guess of 20 bits plus a surcharge is a minute of work in Python).

edit async

edit(
    msg: Message,
    subject: str,
    body: str,
    content_type: str | None = None,
    files=None,
    tags=None,
    languages=None,
    comment_days: int | None = None,
) -> str

Post a new version of your own post or chat line (UN-RFC 0042 section 4.3): same group, kind and place in its thread, naming every earlier version.

files: prepared files for the new version; None keeps the files of the earlier version (fetched and sent again with it, #496), [] none. tags (warnings included) and languages (UN-RFC 0052): the new version's; None keeps the earlier version's (#733) - its tags apart from the #words in its text, so a #word taken out of the text goes with it, while #words in the new text are added as for any post. [] sends none.

tags_to_keep

tags_to_keep(msg: Message) -> list[str]

The tags a new version of msg starts with (UN-RFC 0042, 0052; #733): its own tags and warnings apart from the #words written in its subject and text, which come back from the new text if they are still there. What an edit box fills in.

may_edit

may_edit(msg: Message) -> bool

True when you may publish a new version of this post (UN-RFC 0042): your own, public, readable, and not past the number of versions an edit may name.

remember_languages

remember_languages(group_id: str, languages) -> None

What you chose to write in, in a group - offered there next time; [] forgets it. Kept in your settings (UN-RFC 0044), never published.

choose_chat_language

choose_chat_language(group_id: str, languages) -> None

What you chose to write your chat lines and comments in, in a group - it wins over everything the client would suggest for them there; [] takes the choice back. Kept in your settings (UN-RFC 0044), never published (UN-RFC 0052 5a).

suggest_languages

suggest_languages(
    group_id: str,
    text: str | None = None,
    reply_to=None,
    kind: str = "post",
) -> tuple[list[str], str]

What to suggest you write in, and why (UN-RFC 0052 section 5a, #686) - only a suggestion; you always decide:

  • a post: the text, when it tells for sure ("text"); else the group's languages ("group"); else what you chose there last ("you"); else a one-language profile ("profile"); else ([], "") - better none than wrong;
  • a comment (reply_to, a Message): the language of the post it answers ("answered") - a comment is too short to tell - else as a post;
  • your choice for chat lines and comments in the group (choose_chat_language) wins for them ("chosen");
  • a chat line (kind="chat"): the conversation's language - its newest line's ("conversation") - unless the line is long enough and sure of another ("text"); a short "ok" keeps it.

post async

post(
    group_id: str,
    subject: str,
    body: str,
    references: list[str] | None = None,
    kind: str = KIND_POST,
    content_type: str | None = None,
    supersedes: list[str] | None = None,
    extra: list[tuple[str, str]] | None = None,
    from_name: str | None = None,
    files=None,
    origin: dict | None = None,
    tags=None,
    languages=None,
    comment_days: int | None = None,
) -> str

Post to a group: a post (with a subject) or a chat line (UN-RFC 0028).

comment_days: a new thread's own time for comments, from 0 (none) up to the group's comment_days (UN-RFC 0030 section 3.1); written only when it differs from the group's. Not for replies.

extra are more headers, for a program's own (X-, Gateway-*, UN-RFC 0046 section 4.9); from_name replaces your display name. origin: an import of your own from another network (UN-RFC 0049 section 4) - {"network", "date", "id", "from"}, the date when it was first published there; its Date is still the time of posting. files are prepared files (prepare_files): checked against the group first, then sent as blobs with the post, which pays their extra postage (UN-RFC 0027 sections 4.5, 6). tags: the author's own, at most 8 (UN-RFC 0052) - normalised here; in a closed group they go inside the encryption. languages: what it is written in (UN-RFC 0052 5a), as the author chose - remembered for the group, so suggest_languages offers it next time; [] says none, None leaves the header out and remembers nothing.

Files on posts

Files on posts (UN-RFC 0027): preparing them, and checking them against a group.

Belongs here: reading files and preparing them for a post (prepare_files: the type from the content, pictures cleaned of their metadata by client.media), and checking them before anything is computed or sent (check_files: does the group take files, of this type and size, may they go here at all). post(files=...) in posting.py sends them, as blobs on the same connection as the post (BLOBPOST). And the other way: fetching the file a post carries from the node, checked against its blob ID and size before it is anything (fetch_blob), a picture of it made safe to show (picture), and whether a group's node carries files at all (node_files).

Not here: the rules every node checks (core.blobs), cleaning a picture (client.media), decoding one from the network (client.decode).

Part of Session (see init.py).

FileUnavailable

FileUnavailable(text: str, why: str)

A file a post carries that cannot be had now. why is none (the node carries no files), later (it is still fetching it), gone (it has not got it) or wrong (it sent another file than the post names).

AttachmentsMixin

prepare_files

prepare_files(paths) -> list

Read files and prepare them for a post: a list of media.Prepared, each with its media type found from its content, a picture saved again without its metadata (GPS, time, camera), and a note for what was not cleaned. SessionError if a file cannot be read or is not a picture it claims to be.

check_files

check_files(group_id: str, files) -> None

Raise SessionError with a sentence for the user unless these prepared files may go on a post here: the group takes files (its manifest's attachments, UN-RFC 0027 section 5.1), of these types and sizes, at most four and 8 MiB together. In closed groups and inboxes they are encrypted, 40 octets larger (section 3.3). A submission to a moderated group sends them to the moderators encrypted (section 5.3).

attachment

attachment(
    group_id: str, blob_id: str
) -> tuple[dict, "Message"]

The attachment with this blob ID on a post of this group that this client has read, and that post. SessionError when no such post carries it.

node_files async

node_files(group_id: str) -> bool

True when the node this group is read from carries files (it announces UNSEENET-BLOB, UN-RFC 0027 section 6.1).

node_files_known

node_files_known(group_id: str) -> bool | None

What node_files says, from the open connection to the group's node, without asking it anything; None while there is none.

fetch_blob async

fetch_blob(group_id: str, blob_id: str) -> bytes

The file a post in this group carries, from the group's node - or, when that node has not got it, from your other nodes: checked against its blob ID and size before anything else is done with it (section 9.2), and kept in memory for a while. In closed groups and inboxes the blob is decrypted with the file's key from the post (UN-RFC 0027 section 3.3) and checked again. SessionError with a sentence for the user when it cannot be had.

picture async

picture(
    group_id: str, blob_id: str, *, asked: bool = False
)

A picture a post in this group carries, made safe to show: fetched and checked (fetch_blob), then decoded and saved again in a separate, limited process (client.decode). Not from an author you ignore unless asked (section 9.2). Returns a decode.Picture; SessionError otherwise.

Pins and pages

Kept articles in a client (UN-RFC 0017 section 2.1): pinned posts and the pages of sites.

Belongs here: which posts are pinned in a group (pins, UN-RFC 0033) and pinning or unpinning one (pin); the pages of a site - each path's current and previous version and the pages in force (site, UN-RFC 0035) - and writing or removing a page (put_page, remove_page). They are worked out from what the client has read with the same rules every node uses (core.pins, core.pages), so a client shows what the nodes keep.

Not here: the markers and page articles as they are read (reading.py), personal sites (personal.py).

Part of Session (see init.py).

KeptMixin

pins

pins(group_id: str) -> list[dict]

The pinned posts of a group, newest pin first: {"id", "note", "by", "pinned"} with the pinned post's Message-ID. Only pins by the owner and moderators of the current manifest count, at most its max_pins, and not of withdrawn posts (UN-RFC 0033 section 4.2).

pin async

pin(
    group_id: str,
    message_id: str,
    note: str = "",
    *,
    unpin: bool = False,
) -> str

Owner or moderator: pin a post at the top of the group, kept beyond its window, with an optional note (or unpin it, unpin=True). The marker names the one it replaces (UN-RFC 0033 section 3.1). Returns its Message-ID.

comment_root

comment_root(group_id: str, message_id: str) -> str

The Message-ID comments name for a thread root shown as message_id (an edit names its first version last, UN-RFC 0042). SessionError for a reply.

comments_state

comments_state(
    group_id: str,
    message_id: str,
    now: datetime | None = None,
) -> dict

Whether a thread takes comments now (UN-RFC 0030): {"open", "reason", "until", "note", "by", "own_days", "group_days"} - reason "deadline" or "closed" when not open, until when comments close (None: no deadline), note and by of a close, own_days the entry's own Comment-Days.

may_close_comments

may_close_comments(group_id: str, message_id: str) -> bool

Whether you may close or open comments on this entry: its author, the owner or a moderator, in a group that has comments to close.

close_comments async

close_comments(
    group_id: str,
    message_id: str,
    note: str = "",
    *,
    reopen: bool = False,
) -> str

Close comments on an entry (or open them again, reopen=True) with an optional note for readers (UN-RFC 0030 section 3.2): its author, the owner or a moderator. The marker names the one it replaces in its chain - the author's own, or the owner's and moderators'. Returns its Message-ID.

site

site(group_id: str) -> dict

A site's pages as every client shows them: {"home", "pages"}, pages the paths in force (UN-RFC 0035 section 5.2, the home page first), each {"path", "title", "current", "previous"} with Messages. Only versions by the site's writers now count, so a removed writer's edits fall back.

put_page async

put_page(
    group_id: str,
    path: str,
    text: str,
    title: str = "",
    *,
    content_type: str | None = None,
    files=None,
) -> str

Write a page of a site you write in: its first version, or a new one naming the versions it replaces. title defaults to the path; files are prepared files (prepare_files) its images point at by blob ID (UN-RFC 0031 section 3.5). Read the site first (fetch_group) so the current version is named. Returns its Message-ID.

publish_site async

publish_site(
    group_id: str,
    folder,
    *,
    dry_run: bool = False,
    prune: bool = False,
    progress=None,
) -> dict

Publish a folder of Markdown (and HTML) as the pages of a site you write in (#538, client.siteimport): unchanged pages are left as they are, changed ones get a new version naming the old (UN-RFC 0042), new ones are put; with prune pages no longer in the folder are removed. dry_run does all but send. Answers {"plan", "changes": [{"path", "title", "action", "source", "files"}], "notes", "problems", "sent"}; nothing is sent when there are problems. progress(done, total, path) is called as pages go.

remove_page async

remove_page(group_id: str, path: str) -> str

Remove a page from a site (Page-Removed): its versions stop counting, and the removal is kept while inside the window. Returns its Message-ID.

Personal feeds and sites

Personal groups (UN-RFC 0029 section 2.1): an identity's own feed and site.

Belongs here: the Group IDs of an identity's personal feed (feed@<fp>, UN-RFC 0034 section 5) and site (site@<fp>, UN-RFC 0035 section 7), starting one when the user asks for it, and the warning a client must show first. Once started they are renewed with the identity's other key-bound groups (private.py) and ended by removing them (groups.py, remove_group).

Not here: posting in a feed (posting.py), the pages of a site (kept.py), following someone's feed (coming with following).

Part of Session (see init.py).

PersonalMixin

personal_group

personal_group(kind: str, name: str | None = None) -> str

The Group ID of an identity's personal feed or site (kind feed or site), started or not: feed@<fingerprint>, site@<fingerprint>.

personal_groups_of async

personal_groups_of(key: str, keep: bool = False) -> dict

Someone's personal feed and site, if they have started them: ``{"feed":

, "site": ...}``, from their manifests, read in one request (UN-RFC 0041) - so a profile can offer to look at them without following. ``keep``: their manifests are kept, so the groups can be opened at once; either way nothing is followed.

followed_people_groups

followed_people_groups(group_ids=None) -> list[dict]

What you follow because of a person (#706): other people's personal groups (feed@<fp>, site@<fp>) among those you follow, by the person they belong to - [{"fingerprint", "key", "label", "groups": {kind: Group ID}}], sorted by the person's label - so clients show a person's feed and site together, by name, rather than as hierarchies called feed and site. Your own are left out (clients show them at the top).

has_personal_group

has_personal_group(kind: str) -> bool

Whether this identity has started its personal feed or site in this vault: followed and owned here - what clients show as "Your feed" or "Your site".

personal_group_without_files

personal_group_without_files(kind: str) -> bool

Your personal feed or site is started but takes no files - made before they took pictures by default (#496); clients offer to turn pictures on, once.

allow_pictures async

allow_pictures(group_id: str) -> None

Let a group you own carry pictures (blobs.PICTURES).

personal_group_warning

personal_group_warning(kind: str) -> str

What the user must be told before a personal feed or site is started.

start_personal_group async

start_personal_group(
    kind: str,
    *,
    comments: str = "open",
    title: str = "",
    attachments: dict | None = None,
) -> str

Start this identity's personal feed or site - only when the user asked for it, after showing personal_group_warning. A feed takes comments unless comments="none" (UN-RFC 0034 section 5.2); a site is a channel shown as pages. attachments ({"max_size": ..., "types": [...]}, UN-RFC 0027 section 5.1) is what files it carries; a new one takes pictures unless told otherwise (#496), {} for none. If it exists already, it is followed again, and given attachments if they are given and differ. Returns its Group ID.

Following and the timeline

Following feeds and people, and the timeline (UN-RFC 0034 sections 6, 7).

Belongs here: following a feed (subscribing to it), following a person - their personal groups and the feeds their profile lists that are really theirs (UN-RFC 0022 section 4.5), resolved from a key, a fingerprint, an address or a name once and then kept by key (follow_person) - keeping that up to date as profiles change (refresh_following), unfollowing, and the timeline: the entries of every feed you follow, newest first, in the order they were written and nothing else.

The list of whom you follow is kept in the vault only (data["following"]) and is never sent anywhere; following someone does not tell them (section 6.1).

Not here: subscribing as such (reading.py), profiles (profiles.py), personal groups of your own (personal.py).

Part of Session (see init.py).

FollowingMixin

follow_person async

follow_person(target: str) -> dict

Follow a person - by key, fingerprint, unseenet: address or name - which follows their own feeds now and as they change (UN-RFC 0034 section 6.1). A name is resolved to a key once, and the key is followed from then on. Returns {"fingerprint", "key", "name", "feeds", "new"}; nobody is told.

refresh_following async

refresh_following() -> list[str]

Read the profiles and personal groups of everyone you follow again: follow feeds that are new, and stop following those that are no longer theirs (unless followed on their own or for someone else). Returns the new ones.

unfollow_person async

unfollow_person(target: str) -> list[str]

Stop following a person: the feeds followed only because of them are unsubscribed (UN-RFC 0034 section 6.1). Returns those.

is_following

is_following(key: str) -> bool

Whether you follow this person, given by key (for a Follow/Following button).

following

following() -> list[dict]

The people you follow: {"fingerprint", "key", "name", "label", "feeds"}, feeds those followed because of them.

follow_feed

follow_feed(group_id: str, on: bool = True) -> None

Follow a feed on its own (or stop, on=False): subscribing, kept when a person who lists it is unfollowed - and not followed again for them once you stopped (section 6).

feeds_followed

feeds_followed() -> list[str]

The groups you follow that are feeds (their manifest's view is feed), as far as this client knows their manifests - fetch_unknown_manifests asks for the ones it does not know.

timeline_possible

timeline_possible(group_id: str) -> bool

Whether a group can be in the timeline at all: a group of people (open, moderated, channel, closed) whose owner shows it as a feed or as threads, as far as this client knows its manifest. Chat, data, inboxes and states never are.

timeline_default

timeline_default(group_id: str) -> bool

In the timeline unless you chose otherwise: a feed yes, threads no.

in_timeline

in_timeline(group_id: str) -> bool

Whether a group you follow comes into your timeline: your own choice for it if you made one, else its default (timeline_default). The view you read the group in does not change this.

set_in_timeline

set_in_timeline(group_id: str, on: bool | None) -> bool

Put a group you follow into the timeline or take it out (None: back to its default). Only a choice that differs from the default is kept, in the state (UN-RFC 0044 timeline). Returns whether it is in the timeline now.

timeline_groups

timeline_groups() -> list[str]

The groups whose entries make up your timeline.

fetch_unknown_manifests async

fetch_unknown_manifests() -> int

Fetch the manifest of every group you follow that this client has none of, so a feed is known to be one before the timeline is built. Returns how many.

tagged

tagged(tag: str, limit: int = 200) -> list[Message]

Every post with a tag in the groups and feeds you follow, newest first, from what has been fetched (UN-RFC 0052 section 7): the author's own tags, its group's, and topics from labellers you follow for topics (UN-RFC 0053). Never private posts. Shown as the reader would see them (visible: reports, warnings marked).

timeline

timeline(limit: int = 100) -> list[Message]

The timeline (UN-RFC 0034 section 7): the entries - posts that start a thread - of every group you have in it (timeline_groups: feeds unless you took them out, threads you put in), newest first by Date, from what has been fetched. Comments and replies stay under their entry. Nothing reorders it, and nothing from a group outside it is in it.