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.
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.
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.
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 ¶
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.
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).
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.
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).
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.
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).
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.
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).
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'slanguages("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":
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.
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.