correspond.model#
The data model every channel is described in.
Accounts, conversation references, channel identities, authenticity, messages,
attachments, events, capabilities, audiences, drafts and send results: frozen dataclasses
that round-trip through JSON-ready dicts (to_dict / from_dict), because every
surface (the CLI, MCP, a ledger on disk) moves them as JSON.
There is no person here. A ChannelIdentity is who the platform says sent a
message and Authenticity is how sure the channel is. Linking an identity to a
person is a job for a people registry, with its own evidence. An Audience lists
readers the same way: as channel identities, never as people.
>>> ref = ConversationRef.parse("github:octocat/hello-world#1")
>>> ref.channel, ref.id, str(ref)
('github', 'octocat/hello-world#1', 'github:octocat/hello-world#1')
>>> ConversationRef.from_dict(ref.to_dict()) == ref
True
>>> Grade("bound") in {Grade.PLATFORM, Grade.BOUND, Grade.CRYPTO}
True
Module Attributes
The operations, in the order capabilities and surfaces list them. |
|
Whose name a write goes out under. |
|
What an |
|
The priorities a draft may ask for; channels without priorities refuse anything but |
|
searchable, archived by third parties, copies delivered to readers (email notifications), an edit history anyone who reads can see. |
|
How a readership can grow after a send. |
|
Short phrases for reader classes, used by |
|
they differ between two computations of the same audience. |
Functions
|
ISO 8601 in UTC with a |
|
The inverse of |
Classes
|
The credentialed endpoint correspond acts through: whose name a write goes out under. |
|
A file that came with a message: referenced by |
|
Who can read a conversation, now and plausibly later, as far as its channel can tell. |
|
A grade and its evidence, computed on correspond's side of the hop and never read from a payload. |
|
What a channel can do, graded, with its limits. |
|
Who a channel says sent something: its native id and what the platform attests about it. |
|
Where a conversation lives: a channel and that channel's own id, encoded |
|
What to write: the text, and the few things channels share (a title, the message answered, a priority, copies). |
|
Something that happened on a channel, as a listener reports it: dedupe on |
|
How sure the channel is about who sent a message: a vocabulary, not a ranking. |
|
How far back |
|
One message in a conversation, normalised, with the channel's own fields kept in |
|
The widest class of reader a conversation can reach. |
|
What a write did, or would do ( |
|
How well a channel does something: |
- correspond.model.ACTS_AS = ('bot', 'user', 'app', 'service')#
Whose name a write goes out under.
- correspond.model.AUDIENCE_UNHASHED = ('as_of', 'evidence')#
they differ between two computations of the same audience.
- Type:
The fields
Audience.hashleaves out
- class correspond.model.Account(*, channel, id, acts_as='user')[source]#
Bases:
objectThe credentialed endpoint correspond acts through: whose name a write goes out under.
- class correspond.model.Attachment(*, ref, media_type='application/octet-stream', name=None, size=None, sha256=None, loader=None)[source]#
Bases:
objectA file that came with a message: referenced by
ref, fetched only oncontent(), never inlined.- content()[source]#
The bytes, fetched now through the channel that produced the attachment.
- Return type:
- class correspond.model.Audience(*, ref, scope, readers=(), complete=False, classes=(), external=None, retractable=False, durability=(), widening=(), as_of=_now(), evidence=(), defaulted=False)[source]#
Bases:
objectWho can read a conversation, now and plausibly later, as far as its channel can tell.
scopeis the widest class of reader it can reach.readersare the channel identities known to read it, a lower bound unlesscomplete;classesname, in words, the readers that cannot be listed.externalsays whether readers outside the operator’s own accounts exist (None: unknown).durabilityis what a send leaves behind (DURABILITY),wideninghow the readership can grow (WIDENING).evidencerecords the calls made and why anything was assumed;defaultedis true when an unknown resolved topublic, which it always does.Set-valued fields (
readers,classes,durability,widening) are stored deduplicated and sorted, so the same audience compares and hashes alike whatever order a platform listed it in. A durability or widening flag this version does not know is kept, never dropped. A public audience is never complete, retractable or free of external readers, and a defaulted one is always public.>>> a = Audience(ref="github:example/app#12", scope="public", classes=["watchers and participants receive the body by email"], ... durability=["indexed", "archived_by_others", "copies_pushed", "edit_history_visible"], as_of="2026-09-15T12:00:00Z") >>> a.in_words() 'world-readable; emailed to watchers and participants; archived by others; edits keep a visible history; not retractable' >>> Audience.from_dict(a.to_dict()) == a True
- classmethod from_dict(data)[source]#
The inverse of
to_dict(); unknown keys are ignored.- Return type:
- property hash: str#
The SHA-256, in hex, of the canonical JSON of
to_dict()withoutAUDIENCE_UNHASHED.datais exactly whatto_dict()returns, derivedreaders[].addressincluded. Canonical JSON isjson.dumps(data, sort_keys=True, separators=(",", ":"))(Python’s default ASCII escaping), encoded as UTF-8. Approvals bind to this value, so the construction is a contract. Ato_dictoutput can be hashed as it stands; any other dict should go throughAudience.from_dict(d).hash, which normalises.
- class correspond.model.Authenticity(*, grade, evidence=dict())[source]#
Bases:
objectA grade and its evidence, computed on correspond’s side of the hop and never read from a payload.
>>> Authenticity(grade="bound", evidence={"method": "hmac-sha256"}).to_dict() {'grade': 'bound', 'evidence': {'method': 'hmac-sha256'}}
- correspond.model.CLASS_WORDS = {'watchers and participants receive the body by email': 'emailed to watchers and participants'}#
Short phrases for reader classes, used by
Audience.in_words(); a class not listed here is shown as written.
- class correspond.model.Capabilities(*, channel, read=Support.NONE, listen=Support.NONE, send=Support.NONE, edit=Support.NONE, react=Support.NONE, upload=Support.NONE, verify=Support.NONE, audience=Support.NONE, label=Support.NONE, unlabel=Support.NONE, close=Support.NONE, reopen=Support.NONE, initiate=Support.NONE, reply=Support.NONE, priority=Support.NONE, cc=Support.NONE, notify=Support.NONE, history_depth=HistoryDepth.NONE, listen_modes=(), grades=(), max_text_length=None, max_title_length=None, edit_max_age_s=None, reactions=(), reactions_per_message=None, max_upload_bytes=None, max_attachments=0, formats=('plain',), draft_fields=(), upload_fields=(), native_fields=None, rate_limits=(), notes=())[source]#
Bases:
objectWhat a channel can do, graded, with its limits.
One
Supportper operation inOPERATIONS(audience: can the channel say who reads a conversation;label/unlabel: can labels be added to or removed from a conversation, apart from its opening send;close/reopen: can a conversation be settled, and unsettled), plus five features of writing:initiate(can a write start a conversation; Telegram bots cannot),reply(can a draft answer a specific message),priority,cc(can a draft copy further recipients,Draft.ccandDraft.bcc), andnotify(can the writer guarantee that the people inDraft.notifyare notified: a mention it adds, an address it copies).history_depthsays how far backreadsees;gradesare the authenticity grades the channel can attest;native_fieldsare the keys its messages may carry innative(what a routing condition can test), orNonewhen the channel does not declare them;draft_fieldsare the keys aDraftmay carry in itsnativefor this channel (GitHub:labels,assignees,milestonewhen opening an issue), and any other key is refused rather than dropped;upload_fieldsare the optionsuploadtakes in itsnative.max_attachmentsis how many files a send’sDraft.attachmentsmay carry (0: a send takes none, thoughuploadmay still send a file on its own).- supports(operation)[source]#
The support level for one of
OPERATIONS.- Return type:
- class correspond.model.ChannelIdentity(*, channel, native_id, handle=None, display_name=None, is_bot=False, is_self=False, authority=None)[source]#
Bases:
objectWho a channel says sent something: its native id and what the platform attests about it. Not a person.
- property address: str#
the form a people registry resolves.
>>> ChannelIdentity(channel="github", native_id="583231", handle="octocat").address 'github:octocat'
- Type:
<channel>:<handle>, or the native id when there is no handle
- class correspond.model.ConversationRef(*, channel, id='', kind='', parent=None)[source]#
Bases:
objectWhere a conversation lives: a channel and that channel’s own id, encoded
<channel>:<id>.Two references are equal when channel and id are.
kind(issue,discussion,chat,address, …) andparentdescribe the conversation and are filled in by the channel’s adapter, so a freshly parsed reference equals its normalised form.>>> ConversationRef.parse("telegram:-4001/7") == ConversationRef(channel="telegram", id="-4001/7", kind="topic") True >>> ConversationRef.parse("nonsense") Traceback (most recent call last): ... correspond.errors.InvalidRef: 'nonsense' is not a conversation reference: the form is <channel>:<id>, e.g. github:octocat/hello-world#1
- classmethod from_dict(data)[source]#
The inverse of
to_dict(); a bare encoded string also works.- Return type:
- correspond.model.DURABILITY = ('indexed', 'archived_by_others', 'copies_pushed', 'edit_history_visible')#
searchable, archived by third parties, copies delivered to readers (email notifications), an edit history anyone who reads can see.
- Type:
What a send leaves behind
- class correspond.model.Draft(*, text, title=None, reply_to=None, priority=None, cc=(), bcc=(), native=dict(), notify=(), attachments=())[source]#
Bases:
objectWhat to write: the text, and the few things channels share (a title, the message answered, a priority, copies).
ccandbccare further recipients, on channels whose capabilities gradecc(email). They count in the conversation’s audience.>>> Draft(text="hi", cc=["bob@example.org", " "]).cc ('bob@example.org',)
nativecarries the fields only one channel has, in that channel’s vocabulary – the write-side mirror ofMessage.native. Which keys a channel takes isCapabilities.draft_fields;sendrefuses any other key rather than dropping it.>>> Draft(text="hi", native={"labels": ["question"]}).native {'labels': ['question']}
notifynames who must be notified: handles on a mention platform (octocator@octocat), addresses on email. The writer guarantees it – a mention added when the text lacks one, an address put in Cc when it is not in To or Cc – and a channel that cannot raisesNotSupported("notify")(seecorrespond.notify).>>> Draft(text="hi", notify=["octocat", " ", "Octocat", "hubot"]).notify ('octocat', 'hubot')
attachmentsare files that go with the text, each anAttachmentwhosecontent()gives its bytes, on channels whose capabilities gradeupload. Thebefore_sendcheck sees them (name, media type, size, SHA-256,ref), never their bytes.
- correspond.model.EVENT_KINDS = ('message.created', 'message.updated', 'message.deleted', 'reaction.added', 'identity.changed')#
What an
Eventreports.
- class correspond.model.Event(*, kind, channel, delivery_id, cursor=None, message=None, payload=dict())[source]#
Bases:
objectSomething that happened on a channel, as a listener reports it: dedupe on
delivery_id, resume fromcursor.
- class correspond.model.Grade(*values)[source]#
Bases:
StrEnumHow sure the channel is about who sent a message: a vocabulary, not a ranking.
forged: verification was attempted and failed.claimed: nothing verifiable (a typed name, an unauthenticated From header, anonymous input).platform: the platform authenticated the account (GitHub, Telegram).domain: email authenticated by your own receiving server’s Authentication-Results.bound: a host application signed an assertion about its logged-in user.crypto: a signed payload (a GitHub webhook’sX-Hub-Signature-256).
A policy names the grades it accepts for a permission. Comparing grades with
<raises, becausedomainis neither stronger nor weaker thanplatform.>>> Grade.CLAIMED < Grade.PLATFORM Traceback (most recent call last): ... TypeError: authenticity grades are not ranked; test membership in the grades a policy accepts
- class correspond.model.HistoryDepth(*values)[source]#
Bases:
StrEnumHow far back
readsees: all history, a 24-hour buffer, only what correspond has seen since it was linked or started listening, or nothing.
- class correspond.model.Message(*, id, conversation, author, authenticity, sent_at, text, body=None, body_format='plain', attachments=(), reply_to=None, thread_root=None, edited_at=None, url=None, native=dict(), raw=None)[source]#
Bases:
objectOne message in a conversation, normalised, with the channel’s own fields kept in
native.
- correspond.model.OPERATIONS = ('read', 'listen', 'send', 'edit', 'react', 'upload', 'verify', 'audience', 'label', 'unlabel', 'close', 'reopen')#
The operations, in the order capabilities and surfaces list them.
- correspond.model.PRIORITIES = ('low', 'normal', 'high', 'urgent')#
The priorities a draft may ask for; channels without priorities refuse anything but
None.
- class correspond.model.Scope(*values)[source]#
Bases:
StrEnumThe widest class of reader a conversation can reach.
operator: only the operator’s own devices (a notification banner).named: exactly the explicit recipients (an email, a user’s private repository).group: a bounded membership (a Telegram group).org: an organisation or workspace (a private organisation repository).public: anyone. What an unknown audience resolves to.
- class correspond.model.SendResult(*, ok, channel, conversation, operation='send', dry_run=False, message_id=None, url=None, account=None, plan=dict(), error=None, error_kind=None, retryable=False, retry_after=None)[source]#
Bases:
objectWhat a write did, or would do (
dry_run), or why it failed and whether a retry could help.- classmethod failure(error, *, channel, conversation, operation='send', dry_run=False, plan=None)[source]#
A result carrying a
ChannelError’s classification.- Return type:
- class correspond.model.Support(*values)[source]#
Bases:
StrEnumHow well a channel does something:
full,partial(with limits innotes), ornone.
- correspond.model.WIDENING = ('visibility_flip', 'joiners_read_history', 'forwarding', 'forks', 'list_expansion')#
How a readership can grow after a send.
- correspond.model.format_time(value)[source]#
ISO 8601 in UTC with a
Zsuffix; a naive datetime is taken to be UTC.>>> format_time(datetime(2026, 9, 11, 12, 30, tzinfo=timezone.utc)) '2026-09-11T12:30:00Z'
- correspond.model.parse_time(value)[source]#
The inverse of
format_time(); also accepts offsets and datetimes. Naive means UTC.>>> parse_time("2026-09-11T12:30:00Z") == datetime(2026, 9, 11, 12, 30, tzinfo=timezone.utc) True