Writing a platform¶
You only need this page if you are adding support for a network socialchimp does not have yet - see Adding a platform for the story. Everyone else can skip straight to Networks.
The protocol¶
Every platform provides these seven methods. A platform never subclasses anything; matching the shape is enough.
Platform
¶
Bases: Protocol
What every platform file provides.
A platform is built with no arguments - MastodonPlatform() - because
SocialChimp builds it for you from the name you asked for. Everything
it needs for a particular account arrives as an argument: credentials on
the LoginRequest, the account on the Connection. Keep nothing per
account on the instance, so one platform can serve every account your
app holds.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
How this network is named in code, for example |
features |
Feature
|
What this network can do. Anything not listed here is refused with a clear message instead of being attempted. |
api_base
¶
api_base(connection: Connection) -> str
Return where this network's API lives for this account.
Most networks have one address for everyone. Mastodon has a different one per server, which is why the connection is passed in rather than this being a plain attribute.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account we are about to act as. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The address to send requests to, with no trailing slash, for |
str
|
example |
Source code in src/socialchimp/platform.py
auth_headers
¶
auth_headers(connection: Connection) -> Mapping[str, str]
Return the headers that prove we may act as this account.
Usually one Authorization header. Called on every request, so
keep it cheap and do not go to the network here - the token has
already been renewed by the time this runs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account we are acting as. |
required |
Returns:
| Type | Description |
|---|---|
Mapping[str, str]
|
Headers to add to the request. |
Source code in src/socialchimp/platform.py
limits
async
¶
limits(connection: Connection) -> Limits
Look up the numbers this network is enforcing right now.
Some of these genuinely change: a Mastodon server's post length is set by whoever runs it, and Instagram counts down how many posts are left today. Results are worth caching for a short while.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. |
required |
Returns:
| Type | Description |
|---|---|
Limits
|
The current limits. |
Source code in src/socialchimp/platform.py
start_login
async
¶
start_login(request: LoginRequest) -> LoginStep
Begin signing someone in.
Most networks answer with SendToNetwork: redirect the person there
and wait for them to come back. Networks that use an app password or
a bot token answer with AskForDetails instead, because there is
nowhere to send anyone - your app shows a form and passes the answers
to finish_login.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
LoginRequest
|
Where to send them back to, and what to ask for. |
required |
Returns:
| Type | Description |
|---|---|
LoginStep
|
What to do next. |
Source code in src/socialchimp/platform.py
finish_login
async
¶
finish_login(
request: LoginRequest,
callback: Mapping[str, str],
remember: RawData | None = None,
) -> LoginStep
Carry on after the person comes back from the network.
Usually this finishes the job and returns Finished. Networks that
need to know which page or channel to use return ChooseAccount
first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
LoginRequest
|
The same request used to start the login. |
required |
callback
|
Mapping[str, str]
|
The query values the network sent back. |
required |
remember
|
RawData | None
|
Whatever |
None
|
Returns:
| Type | Description |
|---|---|
LoginStep
|
Either the finished connection or a question to ask. |
Source code in src/socialchimp/platform.py
refresh
async
¶
refresh(
connection: Connection,
app: AppCredentials | None = None,
) -> Token
Get a fresh token for an account.
Called for you before a token runs out. A platform whose tokens do not expire can return the existing one unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account whose token is running out. |
required |
app
|
AppCredentials | None
|
Your app's credentials for this network. Most networks want them to renew a token - Google, Meta and X all do. Networks that do not can ignore this. |
None
|
Returns:
| Type | Description |
|---|---|
Token
|
The new token. Save it - if the network rotates refresh tokens, |
Token
|
the old one has already stopped working. |
Source code in src/socialchimp/platform.py
publish
async
¶
publish(connection: Connection, post: Post) -> PostResult
Publish a post.
Networks that publish in several steps, such as Instagram, do all of them here and return once the post is live or the network has taken over.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to publish as. |
required |
post
|
Post
|
What to publish. |
required |
Returns:
| Type | Description |
|---|---|
PostResult
|
What the network said about the new post. |
Source code in src/socialchimp/platform.py
What a platform can opt into¶
Anything a network cannot do is left off rather than stubbed - a platform
with no create_app simply has no create_app method, and socialchimp asks
before calling it.
CanCreateApp
¶
Bases: Protocol
Extra for networks that let us register an app automatically.
Mastodon is the only one today. Everywhere else you register your app by hand in a developer portal, and several networks review it before it works at all.
create_app
async
¶
create_app(
*,
name: str,
redirect_uri: str,
host: str | None = None,
scopes: tuple[str, ...] = (),
) -> AppCredentials
Register an app with the network and return its credentials.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The app name people will see when approving it. |
required |
redirect_uri
|
str
|
Where the network sends people back to. |
required |
host
|
str | None
|
Which server to register on, for networks with many. |
None
|
scopes
|
tuple[str, ...]
|
Permissions the app will ask for. |
()
|
Returns:
| Type | Description |
|---|---|
AppCredentials
|
Credentials to save and reuse. Registering again for the same |
AppCredentials
|
server wastes a record on that server, so save these. |
Source code in src/socialchimp/platform.py
CanResumeLogin
¶
Bases: Protocol
Extra for networks that pause to ask which account to use.
Facebook asks which page, YouTube which channel. Those platforms answer
finish_login with ChooseAccount, and finish the job here once the
person has picked one.
resume_login
async
¶
resume_login(
request: LoginRequest,
*,
resume_token: str,
account_id: str,
remember: RawData | None = None,
) -> LoginStep
Carry on with the login, now that an account has been picked.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
LoginRequest
|
The same request the login was started with. |
required |
resume_token
|
str
|
The value from |
required |
account_id
|
str
|
Which of the offered accounts the person picked. |
required |
remember
|
RawData | None
|
Whatever |
None
|
Returns:
| Type | Description |
|---|---|
LoginStep
|
Usually the finished connection. A network that asks twice can |
LoginStep
|
answer with another question instead. |
Source code in src/socialchimp/platform.py
CanDeletePosts
¶
Bases: Protocol
Extra for networks that let us remove a post we published.
delete_post
async
¶
delete_post(connection: Connection, post_id: str) -> None
Remove a post.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account that published it. |
required |
post_id
|
str
|
The network's identifier for the post. |
required |
CanReadStats
¶
Bases: Protocol
Extra for networks that say how a published post is doing.
Mastodon counts replies, favourites and boosts, and hands all three back on the post itself. Plenty of networks keep nothing an app can read, and a few keep numbers behind a permission most apps never ask for - so this is an extra rather than something every platform has.
A platform with this also lists Feature.READ_STATS, because that flag
is what socialchimp reads before calling. Account.read_stats is what
your app calls; it checks the flag first and refuses plainly where a
network keeps no numbers.
read_stats
async
¶
read_stats(
connection: Connection, post_id: str
) -> PostStats
Ask the network how a post is doing.
Called with both arguments by position, so the order matters and the names do not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account the post belongs to, with a token that works right now. |
required |
post_id
|
str
|
The network's identifier for the post, which is what
|
required |
Returns:
| Type | Description |
|---|---|
PostStats
|
The numbers that network keeps. Anything it does not count comes |
PostStats
|
back as |
Source code in src/socialchimp/platform.py
CanReadUpdates
¶
Bases: Protocol
Extra for networks we can ask "what has happened since?".
Used for networks that cannot tell us themselves. LinkedIn, Pinterest,
Reddit and Tumblr all work this way: socialchimp calls this on a timer,
works out what is new, and hands your app the same Update objects a
pushing network would have produced. Your handlers never learn which
kind of network they are dealing with.
A network that pushes updates should say so with Feature.PUSH_UPDATES
and provide CanCheckSignature instead. Providing both is fine, and
lets an app fall back to checking on a timer if it cannot receive
incoming requests.
fetch_updates
async
¶
fetch_updates(
connection: Connection, since: datetime | None
) -> Sequence[Update]
Return what has happened on this account since a moment in time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. |
required |
since
|
datetime | None
|
Only return things newer than this. |
required |
Returns:
| Type | Description |
|---|---|
Sequence[Update]
|
The updates, oldest first. |
Source code in src/socialchimp/platform.py
CanCheckSignature
¶
Bases: Protocol
Extra for networks that send us requests when something happens.
Every network signs these differently: Meta uses HMAC-SHA256 in a header, Telegram echoes a shared secret, Discord signs with Ed25519. A platform file knows which, and this is where it says so.
The check must work on the raw bytes of the request, exactly as they arrived. Any framework that parses the JSON and builds it again first will change the bytes and break the signature, so never accept a parsed body here.
check_signature
¶
Check an incoming request really came from the network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The request body, untouched. |
required |
headers
|
Mapping[str, str]
|
The request headers. |
required |
secret
|
str
|
The shared secret for this network, from your settings. |
required |
Raises:
| Type | Description |
|---|---|
SignatureError
|
If the request cannot be trusted. Answer 401 and do nothing else with it. |
Source code in src/socialchimp/platform.py
read_update
¶
read_update(
body: bytes, headers: Mapping[str, str]
) -> Update
Turn a checked request into an update your app understands.
Only call this after check_signature has passed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The request body, untouched. |
required |
headers
|
Mapping[str, str]
|
The request headers. |
required |
Returns:
| Type | Description |
|---|---|
Update
|
What happened, in socialchimp's own words. |
Source code in src/socialchimp/platform.py
CanCheckState
¶
Bases: Protocol
Extra for networks that keep working after they accept a post.
YouTube encodes a video for minutes, sometimes hours. TikTok can put one
in somebody's drafts instead of publishing it. Both answer publish
before they have finished, so a PostResult that comes back
PROCESSING is not the end of the story - this is how an app finds out
the rest of it.
Account.check_state is what your app calls. It looks for this, renews
the token, and hands the connection down.
check_state
async
¶
check_state(
connection: Connection, post_id: str
) -> PostResult
Ask the network how far it has got with a post.
Called with both arguments by position, so the order matters and the names do not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account the post belongs to, with a token that works right now. |
required |
post_id
|
str
|
The network's identifier for the post, which is what
|
required |
Returns:
| Type | Description |
|---|---|
PostResult
|
Where the post has got to now. The same shape |
PostResult
|
so an app can treat the two the same way. |
Source code in src/socialchimp/platform.py
CanAnswerSetupCheck
¶
Bases: Protocol
Extra for networks that ask a question before they will push anything.
Facebook, Instagram and Threads all do this. Point Meta at a URL of yours and it does a GET to it first, carrying a token you chose and a challenge to echo back. Get it wrong and Meta says the URL could not be verified, without saying why.
This happens before anybody has connected an account, so there is no
connection to hang it on. SocialChimp.answer_setup_check is what your
app calls.
answer_setup_check
¶
Answer the one-off check and hand back what to reply with.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
Mapping[str, str]
|
The query values from that GET, such as Django's
|
required |
verify_token
|
str
|
The token you typed into the network's own form. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The challenge. Send it back as the whole body, with a 200 and a |
str
|
content type of |
Raises:
| Type | Description |
|---|---|
SignatureError
|
If this is not a setup check, or the token is wrong. Answer 403 and send nothing back. |
Source code in src/socialchimp/platform.py
CanReadPushedUpdates
¶
Bases: Protocol
Extra for reading everything one pushed request carries.
Not the same as CanReadUpdates, which asks a network what has happened
since. This is for a request the network sent us, and it hands back the
whole of what that request held.
Which matters because Meta batches. One message from Facebook can carry
changes for several pages, and several changes for each of them, and it
does that when it is busy - exactly when you least want to drop the
rest. read_update on CanCheckSignature gives you the first one only.
A network that never batches still has this, handing back a list of one, so that an app written against one network works against all of them.
SocialChimp.read_updates is what your app calls, after
SocialChimp.check_signature has passed.
read_updates
¶
read_updates(body: bytes) -> list[Update]
Turn a checked request into every update it carries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
bytes
|
The request body, exactly as it arrived. Check its signature first. |
required |
Returns:
| Type | Description |
|---|---|
list[Update]
|
What happened, in the order the network listed it. Empty when |
list[Update]
|
the message carried nothing we can act on, which is not an |
list[Update]
|
error - networks send shapes we have no interest in. |
Source code in src/socialchimp/platform.py
The social inbox¶
Added in 0.8.0. See the use case for how an
app calls these through Account, and docs/social-inbox-contract.md in the
repository for the design behind them.
CanReadPost
¶
Bases: Protocol
Extra for reading one post back in full.
Not only what publishing it returned - everything socialchimp models
about a post. Account.read_post is what your app calls.
read_post
async
¶
read_post(
connection: Connection, post_id: str
) -> PostDetails
Read one post, with everything socialchimp models about it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to read it as. |
required |
post_id
|
str
|
The network's identifier for the post or comment. |
required |
Returns:
| Type | Description |
|---|---|
PostDetails
|
The post, in full. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
If the network refuses, or the post is gone. |
Source code in src/socialchimp/platform.py
CanReadThread
¶
Bases: Protocol
Extra for reading a post together with its replies.
Account.read_thread is what your app calls.
read_thread
async
¶
read_thread(
connection: Connection,
post_id: str,
*,
depth: int | None = None,
limit: int | None = None,
) -> Thread
Read a post and the replies underneath it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to read it as. |
required |
post_id
|
str
|
The network's identifier for the post to read. |
required |
depth
|
int | None
|
How many reply levels to fetch. |
None
|
limit
|
int | None
|
A cap on how many replies come back. |
None
|
Returns:
| Type | Description |
|---|---|
Thread
|
The post and its replies. |
Thread
|
|
Thread
|
replies off before the end. |
Source code in src/socialchimp/platform.py
CanReply
¶
Bases: Protocol
Extra for replying to any post or comment, at any depth.
publish(Post(reply_to=...)) keeps working without this. This is the
recommended way to reply once a network provides it, because it can do
things publish cannot know to - keeping a Mastodon reply's visibility
no wider than its parent's, or mentioning the people already in the
thread.
Account.reply is what your app calls.
reply
async
¶
reply(
connection: Connection,
post_id: str,
text: str,
*,
media: tuple[Media, ...] = (),
options: RawData | None = None,
) -> PostResult
Reply to a post or comment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to reply as. |
required |
post_id
|
str
|
The post or comment being replied to, at any depth. |
required |
text
|
str
|
The reply's words. |
required |
media
|
tuple[Media, ...]
|
Pictures or videos to attach to the reply. |
()
|
options
|
RawData | None
|
Settings for one network only. |
None
|
Returns:
| Type | Description |
|---|---|
PostResult
|
What the network said about the new reply. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
If the network refuses, or the target is gone. |
Source code in src/socialchimp/platform.py
CanLike
¶
Bases: Protocol
Extra for liking and unliking a post or a comment.
Both calls are idempotent: liking something twice, or unliking something not liked, succeeds and does nothing.
Account.like and Account.unlike are what your app calls.
like
async
¶
like(connection: Connection, post_id: str) -> LikeResult
Like a post or a comment.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account doing the liking. |
required |
post_id
|
str
|
The post or comment to like. |
required |
Returns:
| Type | Description |
|---|---|
LikeResult
|
What the network said about the like. Liking something already |
LikeResult
|
liked returns the existing like rather than making a new one. |
Source code in src/socialchimp/platform.py
unlike
async
¶
unlike(
connection: Connection,
post_id: str,
*,
like_id: str | None = None,
) -> None
Take back a like.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account taking the like back. |
required |
post_id
|
str
|
The post or comment to unlike. |
required |
like_id
|
str | None
|
The like's own identifier, from |
None
|
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
If the network refuses. |
Source code in src/socialchimp/platform.py
CanReadLikes
¶
Bases: Protocol
Extra for listing who liked a post.
Some networks that can like something cannot list who did - a Facebook
Page can like a comment but only ever sees the count - so this is a
separate extra from CanLike rather than part of it.
Account.read_likes is what your app calls.
read_likes
async
¶
read_likes(
connection: Connection,
post_id: str,
*,
after: str | None = None,
limit: int | None = None,
) -> Page[Like]
List who liked a post.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask as. |
required |
post_id
|
str
|
The post or comment to list likes for. |
required |
after
|
str | None
|
A |
None
|
limit
|
int | None
|
A cap on how many come back. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Like]
|
One page of likes. |
Source code in src/socialchimp/platform.py
CanReadUpdatesAfter
¶
Bases: Protocol
Extra for polling with a marker that can be resumed after a restart.
Unlike CanReadUpdates.fetch_updates, which takes a moment in time, this
takes an opaque marker your app stores and passes back - see
UpdateBatch. That is what makes it resumable: a moment in time can miss
or repeat updates around the edges, where a marker cannot.
Account.fetch_updates_after and Account.mark_seen are what your app
calls.
fetch_updates_after
async
¶
fetch_updates_after(
connection: Connection,
marker: str | None,
*,
limit: int | None = None,
) -> UpdateBatch
Read what is new since a marker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask about. |
required |
marker
|
str | None
|
The marker from the last call's |
required |
limit
|
int | None
|
A cap on how many updates come back in this page. |
None
|
Returns:
| Type | Description |
|---|---|
UpdateBatch
|
The new updates, and a marker to store for next time. |
Source code in src/socialchimp/platform.py
mark_seen
async
¶
mark_seen(connection: Connection, marker: str) -> None
Tell the network a marker has been seen.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to mark it for. |
required |
marker
|
str
|
The marker that has been handled. |
required |
CanMessage
¶
Bases: Protocol
Extra for reading and sending direct messages.
Account.read_conversations, Account.read_messages,
Account.send_message and Account.mark_read are what your app calls.
read_conversations
async
¶
read_conversations(
connection: Connection,
*,
after: str | None = None,
limit: int | None = None,
) -> Page[Conversation]
List this account's conversations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask as. |
required |
after
|
str | None
|
A |
None
|
limit
|
int | None
|
A cap on how many come back. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Conversation]
|
One page of conversations. |
Source code in src/socialchimp/platform.py
read_messages
async
¶
read_messages(
connection: Connection,
conversation_id: str,
*,
after: str | None = None,
limit: int | None = None,
) -> Page[Message]
Read the messages in one conversation, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to ask as. |
required |
conversation_id
|
str
|
Which conversation to read. |
required |
after
|
str | None
|
A |
None
|
limit
|
int | None
|
A cap on how many come back. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Message]
|
One page of messages, newest first. |
Source code in src/socialchimp/platform.py
send_message
async
¶
send_message(
connection: Connection,
conversation_id: str,
text: str,
*,
options: RawData | None = None,
) -> Message
Send a message into an existing conversation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to send as. |
required |
conversation_id
|
str
|
Which conversation to send into. |
required |
text
|
str
|
The message's words. |
required |
options
|
RawData | None
|
Settings for one network only, such as a Meta message tag. |
None
|
Returns:
| Type | Description |
|---|---|
Message
|
The message that was sent. |
Raises:
| Type | Description |
|---|---|
ReplyWindowClosedError
|
If a 24-hour reply window has closed. |
Source code in src/socialchimp/platform.py
mark_read
async
¶
mark_read(
connection: Connection, conversation_id: str
) -> None
Mark a conversation as read.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to mark it for. |
required |
conversation_id
|
str
|
Which conversation to mark. |
required |
CanStartConversations
¶
Bases: Protocol
Extra for starting a new conversation, rather than only answering one.
Meta cannot do this: the customer has to write first. A platform with
CanMessage but not this one can still be replied to - it just cannot
open the first message.
Account.start_conversation is what your app calls.
start_conversation
async
¶
start_conversation(
connection: Connection,
person_ids: Sequence[str],
text: str,
) -> Message
Start a conversation with one or more people.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection
|
Connection
|
The account to send as. |
required |
person_ids
|
Sequence[str]
|
Who to start it with. |
required |
text
|
str
|
The first message's words. |
required |
Returns:
| Type | Description |
|---|---|
Message
|
The message that was sent. |
Source code in src/socialchimp/platform.py
Signing someone in¶
start_login and finish_login return one of these four. socialchimp's
type for "one of these four" is LoginStep = SendToNetwork | AskForDetails |
ChooseAccount | Finished.
LoginRequest
dataclass
¶
LoginRequest(
redirect_uri: str,
scopes: tuple[str, ...] = (),
host: str | None = None,
state: str | None = None,
app: AppCredentials | None = None,
)
What we need in order to start signing someone in.
Attributes:
| Name | Type | Description |
|---|---|---|
redirect_uri |
str
|
Where the network sends the person back to. It must match what the network's developer portal has on file. |
scopes |
tuple[str, ...]
|
Permissions to ask for. Each platform's page lists sensible defaults; leaving this empty uses them. |
host |
str | None
|
Which server, for networks that have more than one. Required for Mastodon, ignored elsewhere. |
state |
str | None
|
A value handed back to you at the end, so you can tell which of your users came back. One is made for you if you leave it out. |
app |
AppCredentials | None
|
Your app's credentials for this network. |
SendToNetwork
dataclass
¶
Step one: send the person to the network to approve your app.
Attributes:
| Name | Type | Description |
|---|---|---|
url |
str
|
Where to send them. Redirect their browser here. |
state |
str
|
The value that will come back, for matching up the reply. |
remember |
RawData
|
Something the platform needs again when the person comes
back, such as the secret half of a PKCE pair. Keep it with the
rest of that person's session and hand it back to It has to travel through your app because the two halves of a sign-in can happen in different processes - the person may be sent away by one web worker and come back to another. Holding it in memory would work on your laptop and fail in production. |
LoginField
dataclass
¶
One thing to ask a person for.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
What to call this value when handing it back. Put it in the
|
label |
str
|
What to show next to the box, in words a person understands. |
secret |
bool
|
True for anything that should not be shown as it is typed, or written to a log. |
help_text |
str | None
|
A sentence under the box, usually saying where on the network to find the value. |
AskForDetails
dataclass
¶
AskForDetails(
fields: tuple[LoginField, ...],
help_url: str | None = None,
)
Step one, for networks that have no sign-in page to send people to.
Bluesky uses an app password, and Discord and Telegram use a bot token
that someone pastes in. There is nowhere to redirect to, so instead the
platform says what to ask for, your app shows a form, and the answers go
back through finish_login as the callback mapping.
Show the fields in the order given, and never log anything marked
secret.
Nothing leaves your app on this route, so LoginRequest.state is not
used - there is no trip through a browser to match up afterwards. If your
sign-in code expects every step to carry state back, this is the one that
will not.
Attributes:
| Name | Type | Description |
|---|---|---|
fields |
tuple[LoginField, ...]
|
What to ask for. |
help_url |
str | None
|
A page explaining where to get these, worth linking to beside the form. |
AccountChoice
dataclass
¶
One of several accounts a person could connect.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
The network's identifier, passed back to carry on. |
name |
str
|
Something a person would recognise, shown in your UI. |
kind |
str | None
|
What sort of thing it is, such as |
ChooseAccount
dataclass
¶
ChooseAccount(
options: tuple[AccountChoice, ...], resume_token: str
)
A pause: the person has approved, but we need to know which account.
Facebook asks which page, YouTube which channel. Show options, then
carry on with the one they picked.
Attributes:
| Name | Type | Description |
|---|---|---|
options |
tuple[AccountChoice, ...]
|
What they can choose from. |
resume_token |
str
|
Hand this back to carry on. Treat it as meaningless text; only the platform file understands it. Treat it as a secret. On some networks it has to carry the
tokens themselves, because the sign-in code can only be swapped
once and that happens before the person picks. Keep it with
their session, the way you keep |
Finished
dataclass
¶
Finished(connection: Connection)
The last step: the account is connected.
Attributes:
| Name | Type | Description |
|---|---|---|
connection |
Connection
|
Save this. It is everything needed to act as the account. |
Finding installed platforms¶
How SocialChimp turns a name like "facebook" into a platform instance.
See socialchimp.registry for the full story of how packages register
themselves.
register_platform
¶
register_platform(
name: str, platform_class: type[Platform]
) -> None
Tell socialchimp about a platform, without installing a package.
A platform registered this way is used in place of an installed one with the same name, which is how a test swaps in a fake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
How the platform will be asked for, for example |
required |
platform_class
|
type[Platform]
|
The class to use. It is not created here; that happens when the platform is used. |
required |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If the class does not provide what a platform must. |
Source code in src/socialchimp/registry.py
unregister_platform
¶
Forget a platform that was registered in code.
Quiet if there was nothing registered under that name. Installed
packages are left alone - this only undoes register_platform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name it was registered under. |
required |
Source code in src/socialchimp/registry.py
available_platforms
¶
List every platform socialchimp can use right now.
Returns:
| Type | Description |
|---|---|
list[str]
|
The names, in alphabetical order, from both places we look. A name |
list[str]
|
appearing here does not promise its package imports cleanly - a |
list[str]
|
broken package says so when you ask for it. |
Source code in src/socialchimp/registry.py
get_platform_class
¶
get_platform_class(name: str) -> type[Platform]
Find the class for one platform.
The class is imported the first time it is asked for and remembered after that.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Which network, for example |
required |
Returns:
| Type | Description |
|---|---|
type[Platform]
|
The platform class. Create it to use it. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If nothing is installed under that name, if its package could not be imported, or if what came back is not a platform. |
Source code in src/socialchimp/registry.py
clear_platform_cache
¶
Look for installed platforms again next time one is asked for.
socialchimp reads the installed packages once and remembers what it found. Call this after installing a package while the program is running, and between tests. Platforms registered in code are kept.
Source code in src/socialchimp/registry.py
Making requests¶
Every platform file sends its requests through HttpClient: retrying after
a hiccup, waiting as long as a network asks, and turning an unhappy reply
into a socialchimp error, written once instead of nine times.
HttpClient
¶
HttpClient(
base_url: str = "",
*,
platform: str = _UNNAMED,
headers: Mapping[str, str] | None = None,
timeout: float | Timeout = _DEFAULT_TIMEOUT,
transport: AsyncBaseTransport | None = None,
retries: Retries | None = None,
errors: Callable[[Response], SocialChimpError]
| None = None,
)
The shared way to send requests to one network.
Wraps an httpx.AsyncClient and adds the parts every platform needs:
trying again after a hiccup, waiting as long as the network asks,
remembering how much of the allowance is left, and raising a socialchimp
error instead of handing back a reply nobody checked.
A failed request is sent again with exactly the arguments you gave, so pass bytes rather than an open file - a file read once cannot be read again.
Example
async with HttpClient( "https://mastodon.social", platform="mastodon", ) as http: me = await http.json("GET", "/api/v1/accounts/verify_credentials")
Set up a client for one network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str
|
What every path is joined onto, such as
|
''
|
platform
|
str
|
Which network this talks to, used in error messages. |
_UNNAMED
|
headers
|
Mapping[str, str] | None
|
Sent with every request. A token usually goes here. |
None
|
timeout
|
float | Timeout
|
Seconds to wait for a reply before giving up on it. |
_DEFAULT_TIMEOUT
|
transport
|
AsyncBaseTransport | None
|
Where requests actually go. Leave it out for ordinary network calls; pass your own to send them through something else, which is also how tests answer without a network. |
None
|
retries
|
Retries | None
|
How many times to try again, and how long to wait in between. Left out, four tries with growing waits. |
None
|
errors
|
Callable[[Response], SocialChimpError] | None
|
Your own function turning an unhappy reply into an
error, for a network with quirks worth naming. Left out,
|
None
|
Source code in src/socialchimp/http.py
rate_limit
property
¶
rate_limit: RateLimit | None
What the network last said about how much allowance is left.
None until a reply mentions it. A later reply that says nothing
leaves the last figures alone, because "no headers" means "no news",
not "nothing left".
request
async
¶
Send a request, trying again if it is worth it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
|
required |
path
|
str
|
Joined onto the base url. |
required |
**kwargs
|
object
|
Anything |
{}
|
Returns:
| Type | Description |
|---|---|
Response
|
The reply, which is always one the network was happy with. |
Raises:
| Type | Description |
|---|---|
SocialChimpError
|
If the network refused, or could not be
reached at all. Which error depends on what it said; see
|
Source code in src/socialchimp/http.py
get
async
¶
Send a GET request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Joined onto the base url. |
required |
**kwargs
|
object
|
Anything |
{}
|
Returns:
| Type | Description |
|---|---|
Response
|
The reply. |
Source code in src/socialchimp/http.py
post
async
¶
Send a POST request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Joined onto the base url. |
required |
**kwargs
|
object
|
Anything |
{}
|
Returns:
| Type | Description |
|---|---|
Response
|
The reply. |
Source code in src/socialchimp/http.py
put
async
¶
Send a PUT request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Joined onto the base url. |
required |
**kwargs
|
object
|
Anything |
{}
|
Returns:
| Type | Description |
|---|---|
Response
|
The reply. |
Source code in src/socialchimp/http.py
delete
async
¶
Send a DELETE request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Joined onto the base url. |
required |
**kwargs
|
object
|
Anything |
{}
|
Returns:
| Type | Description |
|---|---|
Response
|
The reply. |
Source code in src/socialchimp/http.py
json
async
¶
Send a request and read the reply as a JSON object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
|
required |
path
|
str
|
Joined onto the base url. |
required |
**kwargs
|
object
|
Anything |
{}
|
Returns:
| Type | Description |
|---|---|
RawData
|
The reply, parsed. |
Raises:
| Type | Description |
|---|---|
PlatformError
|
If the reply was not JSON, or was JSON but not an
object. The reply itself is kept on |
SocialChimpError
|
If the network refused the request. |
Source code in src/socialchimp/http.py
Retries
dataclass
¶
Retries(
attempts: int = 4,
first_wait: float = 0.5,
biggest_wait: float = 30.0,
spread: float = 0.5,
)
How many times to try again, and how long to wait in between.
Waits double after each failure, so a network that is struggling is not hammered. Part of each wait is random, so that every client which failed at the same moment does not come back at the same moment.
Attributes:
| Name | Type | Description |
|---|---|---|
attempts |
int
|
How many tries in total, counting the first one. |
first_wait |
float
|
Seconds to wait after the first failure. |
biggest_wait |
float
|
The longest we will ever wait between two tries. |
spread |
float
|
How much of a wait is random, from 0 (never) to 1 (up to double the wait). |
wait_after
¶
Work out how long to wait before the next try.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failures
|
int
|
How many tries have failed so far. |
required |
asked_for
|
float | None
|
Seconds the network itself asked us to wait, when it said. This is the least we will wait; if the wait we worked out is longer, we take the longer one. |
None
|
Returns:
| Type | Description |
|---|---|
float
|
Seconds to wait. |
Source code in src/socialchimp/http.py
RateLimit
dataclass
¶
RateLimit(
limit: int | None = None,
remaining: int | None = None,
resets_at: datetime | None = None,
)
How much of a network's allowance is left, as it last told us.
Every field may be None, which means "the network did not say" - never
"zero". Read this before sending a burst of requests and you can slow
down before being told to.
Attributes:
| Name | Type | Description |
|---|---|---|
limit |
int | None
|
How many requests are allowed in the current stretch of time. |
remaining |
int | None
|
How many of those are left. |
resets_at |
datetime | None
|
When the count starts again. |
is_used_up
property
¶
Whether there are no requests left before the count starts again.
rate_limit_from_headers
¶
rate_limit_from_headers(
headers: Headers, *, now: datetime | None = None
) -> RateLimit | None
Read a reply's rate-limit headers.
Both spellings are read: x-ratelimit-limit, which nearly everybody
uses, and X's x-rate-limit-limit. So is Pinterest's habit of listing
every window in one header - "100, 100;w=1, 1000;w=60" - where the
bare number in front is the one that applies right now.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
headers
|
Headers
|
The reply's headers. |
required |
now
|
datetime | None
|
What to treat as the current moment, for networks that count down in seconds. Only useful in tests. |
None
|
Returns:
| Type | Description |
|---|---|
RateLimit | None
|
What the network said, or |
Source code in src/socialchimp/http.py
retry_after_seconds
¶
Read how long a network has asked us to wait, in seconds.
Networks write Retry-After two ways: a number of seconds ("30") or a
date ("Wed, 21 Oct 2026 07:28:00 GMT"). Both come back here as seconds
from now. A date that has already gone by, or a negative number, comes
back as zero rather than as a wait that runs backwards.
Where a reply carries no Retry-After at all, a rate-limit reset header
is read instead: Mastodon's X-RateLimit-Reset, written as an ISO-8601
timestamp, or Bluesky's RateLimit-Reset, written as a unix time in
seconds. Both come back the same way - seconds from now, never negative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
Response
|
The reply to read. |
required |
now
|
datetime | None
|
What to treat as the current moment. Only useful in tests. |
None
|
Returns:
| Type | Description |
|---|---|
float | None
|
Seconds to wait, or |
float | None
|
something we cannot read. |
Source code in src/socialchimp/http.py
error_from_response
¶
error_from_response(
response: Response, *, platform: str = _UNNAMED
) -> SocialChimpError
Turn an unhappy reply into the socialchimp error that describes it.
This is the shared mapping every network starts from. A platform that wants to name its own quirks writes its own function, handles the replies it recognises, and calls this one for the rest:
def bluesky_errors(response: httpx.Response) -> SocialChimpError:
body = read_body(response)
if body.get("error") == "TextTooLong":
return InvalidPostError("This post is too long for bluesky.")
return error_from_response(response, platform="bluesky")
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
Response
|
The reply to turn into an error. |
required |
platform
|
str
|
Which network sent it, used in the message. |
_UNNAMED
|
Returns:
| Type | Description |
|---|---|
SocialChimpError
|
The error to raise. Always an error, never |
SocialChimpError
|
cannot forget a case. |
Source code in src/socialchimp/http.py
read_body
¶
Return a reply's body as a dictionary, whatever shape it arrived in.
Most networks answer with a JSON object and that is handed straight back.
Anything else - a JSON list, a plain string, an HTML error page - is put
under a body key, so callers never have to guess what they were given.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
response
|
Response
|
The reply to read. |
required |
Returns:
| Type | Description |
|---|---|
RawData
|
The body as a dictionary. |