Skip to content

Data

Everything socialchimp passes around is frozen: once made, it never changes. A refresh produces a new Connection rather than editing the old one, and anything holding a secret hides it from repr(). See the tutorial for how these fit together.

Post

Post dataclass

Post(
    text: str = "",
    media: tuple[Media, ...] = (),
    reply_to: str | None = None,
    publish_at: datetime | None = None,
    options: RawData = dict(),
)

Something to publish.

The fields here work on most networks. Anything that belongs to one network only goes in options.

Attributes:

Name Type Description
text str

The words. Some networks call this a caption or a body.

media tuple[Media, ...]

Pictures or videos to attach.

reply_to str | None

Identifier of the post being replied to.

publish_at datetime | None

When to publish, for networks that can schedule. Check Feature.SCHEDULE first - most cannot, and socialchimp will say so rather than quietly posting straight away.

options RawData

Settings for one network only, such as Pinterest's board_id or Mastodon's visibility. Each platform's page lists what it accepts.

PostResult and PostState

PostResult dataclass

PostResult(
    id: str,
    url: str | None = None,
    state: PostState = DONE,
    raw: RawData = dict(),
    cid: str | None = None,
)

What came back after publishing.

Attributes:

Name Type Description
id str

The network's identifier for the new post.

url str | None

Link to the post, where the network gives us one.

state PostState

Whether the network has finished with it.

raw RawData

The network's untouched reply, for anything we did not model.

cid str | None

Bluesky's content hash for the new post. None everywhere else. Added in 0.8.0, after raw, so code from before 0.8.0 that builds a PostResult by position - id, url, state, raw - still puts its fourth argument in raw, not here.

is_done property

is_done: bool

Whether the post is live. False while a network is still working.

PostState

Bases: Enum

How far along a post is.

Most networks finish while we wait. YouTube and TikTok keep working after they accept the upload, so a post can come back as PROCESSING and finish later. You hear about it through an update (see socialchimp.events).

DONE class-attribute instance-attribute

DONE = auto()

The post is live now.

SCHEDULED class-attribute instance-attribute

SCHEDULED = auto()

The network accepted it and will publish it later.

PROCESSING class-attribute instance-attribute

PROCESSING = auto()

The network is still working on it, usually a video being encoded.

WAITING_FOR_PERSON class-attribute instance-attribute

WAITING_FOR_PERSON = auto()

The network has finished, and now somebody has to tap a button.

TikTok can put a video in a person's drafts rather than posting it, so they can add their own caption and publish it themselves. Nothing is wrong and nothing more will happen on its own, so do not sit and wait for this one to change.

FAILED class-attribute instance-attribute

FAILED = auto()

The network gave up on it.

PostStats

How a published post is doing, in one shape for every network. What account.read_stats(...) hands back. Every number may be None, which means the network does not count that - never that it counted zero.

PostStats dataclass

PostStats(
    id: str,
    likes: int | None = None,
    comments: int | None = None,
    shares: int | None = None,
    raw: RawData = dict(),
)

How a published post is doing.

Every number may be None, which means "this network does not count that" - never "zero". A post nobody has liked and a network that keeps no likes are two different answers, and only one of them is a number.

Networks all use their own words for these: Mastodon counts favourites and boosts, X counts likes and reposts. They arrive here under one set of names, so an app does not learn a vocabulary per network.

Only the numbers a network really publishes are here. Reach, impressions and clicks are deliberately missing: most networks do not give them out at all, and a field that could never be filled in reads like one that is always zero.

Attributes:

Name Type Description
id str

The network's identifier for the post these numbers are about - the same one PostResult.id carried.

likes int | None

How many people liked, favourited or reacted to it.

comments int | None

How many replies it has.

shares int | None

How many times it was passed on - boosted, reposted, reblogged, whichever word that network uses.

raw RawData

The network's untouched reply, for any number we did not model.

Media

Media dataclass

Media(
    kind: MediaKind,
    content: bytes | None = None,
    path: Path | None = None,
    url: str | None = None,
    filename: str | None = None,
    alt_text: str | None = None,
)

A picture or video to attach to a post.

Build one with from_file, from_bytes or from_url rather than calling Media(...) directly - those work out the kind for you.

Attributes:

Name Type Description
kind MediaKind

Picture or video.

content bytes | None

The bytes, when they were handed to us directly.

path Path | None

Where the file lives on disk, read only when needed.

url str | None

Where the file lives online. Some networks fetch it themselves; for the rest socialchimp downloads it first.

filename str | None

Name to send along with the upload.

alt_text str | None

Description for people using a screen reader. Worth setting.

content_type property

content_type: str

The MIME type to send with the upload.

size property

size: int | None

How many bytes this is, when we can tell.

None for a file that is only a web address, because finding out would mean downloading it - which is usually the thing a web address was used to avoid.

Returns:

Type Description
int | None

The size in bytes, or None if it is not knowable yet.

from_file classmethod

from_file(
    path: str | Path,
    *,
    kind: MediaKind | None = None,
    alt_text: str | None = None,
) -> Media

Attach a file from disk. It is read when the upload happens.

Parameters:

Name Type Description Default
path str | Path

Where the file is.

required
kind MediaKind | None

Picture or video. Worked out from the name if left out.

None
alt_text str | None

Description for screen readers.

None

Returns:

Type Description
Media

The media, ready to attach to a post.

Source code in src/socialchimp/models.py
@classmethod
def from_file(
    cls,
    path: str | Path,
    *,
    kind: MediaKind | None = None,
    alt_text: str | None = None,
) -> Media:
    """Attach a file from disk. It is read when the upload happens.

    Args:
        path: Where the file is.
        kind: Picture or video. Worked out from the name if left out.
        alt_text: Description for screen readers.

    Returns:
        The media, ready to attach to a post.
    """
    location = Path(path)
    return cls(
        kind=cls._guess_kind(location.name, kind),
        path=location,
        filename=location.name,
        alt_text=alt_text,
    )

from_bytes classmethod

from_bytes(
    content: bytes,
    *,
    filename: str,
    kind: MediaKind | None = None,
    alt_text: str | None = None,
) -> Media

Attach data you already hold in memory.

Parameters:

Name Type Description Default
content bytes

The file's bytes.

required
filename str

Name to send with the upload. Also used to work out the kind.

required
kind MediaKind | None

Picture or video. Worked out from the name if left out.

None
alt_text str | None

Description for screen readers.

None

Returns:

Type Description
Media

The media, ready to attach to a post.

Source code in src/socialchimp/models.py
@classmethod
def from_bytes(
    cls,
    content: bytes,
    *,
    filename: str,
    kind: MediaKind | None = None,
    alt_text: str | None = None,
) -> Media:
    """Attach data you already hold in memory.

    Args:
        content: The file's bytes.
        filename: Name to send with the upload. Also used to work out
            the kind.
        kind: Picture or video. Worked out from the name if left out.
        alt_text: Description for screen readers.

    Returns:
        The media, ready to attach to a post.
    """
    return cls(
        kind=cls._guess_kind(filename, kind),
        content=content,
        filename=filename,
        alt_text=alt_text,
    )

from_url classmethod

from_url(
    url: str,
    *,
    kind: MediaKind | None = None,
    alt_text: str | None = None,
) -> Media

Point at a file already online.

Parameters:

Name Type Description Default
url str

Where the file is. It must be reachable by the network.

required
kind MediaKind | None

Picture or video. Worked out from the address if left out.

None
alt_text str | None

Description for screen readers.

None

Returns:

Type Description
Media

The media, ready to attach to a post.

Source code in src/socialchimp/models.py
@classmethod
def from_url(
    cls,
    url: str,
    *,
    kind: MediaKind | None = None,
    alt_text: str | None = None,
) -> Media:
    """Point at a file already online.

    Args:
        url: Where the file is. It must be reachable by the network.
        kind: Picture or video. Worked out from the address if left out.
        alt_text: Description for screen readers.

    Returns:
        The media, ready to attach to a post.
    """
    return cls(
        kind=cls._guess_kind(url, kind),
        url=url,
        filename=Path(url).name or None,
        alt_text=alt_text,
    )

piece

piece(start: int, length: int) -> bytes

Read part of the file.

Networks that take large video want it in pieces - YouTube, TikTok and Facebook all do. Reading a piece at a time keeps a four gigabyte video from becoming four gigabytes of memory, so use this rather than slicing what read() gives you.

Parameters:

Name Type Description Default
start int

How many bytes in to begin.

required
length int

How many bytes to read. Fewer come back at the end of the file, which is how you know you have reached it.

required

Returns:

Type Description
bytes

The bytes read.

Raises:

Type Description
InvalidPostError

If this media is only a web address.

Source code in src/socialchimp/models.py
def piece(self, start: int, length: int) -> bytes:
    """Read part of the file.

    Networks that take large video want it in pieces - YouTube, TikTok
    and Facebook all do. Reading a piece at a time keeps a four gigabyte
    video from becoming four gigabytes of memory, so use this rather
    than slicing what `read()` gives you.

    Args:
        start: How many bytes in to begin.
        length: How many bytes to read. Fewer come back at the end of
            the file, which is how you know you have reached it.

    Returns:
        The bytes read.

    Raises:
        InvalidPostError: If this media is only a web address.
    """
    if self.content is not None:
        return self.content[start : start + length]
    if self.path is not None:
        with self.path.open("rb") as opened:
            opened.seek(start)
            return opened.read(length)

    message = (
        f"This media is a url ({self.url!r}), so there are no bytes to "
        f"read yet. Download it first, or let the platform fetch it."
    )
    raise InvalidPostError(message)

read

read() -> bytes

Return the file's bytes.

Returns:

Type Description
bytes

The content, read from disk if it is not already in memory.

Raises:

Type Description
InvalidPostError

If this media is only a URL. Download it first, or use a network that fetches URLs itself.

Source code in src/socialchimp/models.py
def read(self) -> bytes:
    """Return the file's bytes.

    Returns:
        The content, read from disk if it is not already in memory.

    Raises:
        InvalidPostError: If this media is only a URL. Download it
            first, or use a network that fetches URLs itself.
    """
    if self.content is not None:
        return self.content
    if self.path is not None:
        return self.path.read_bytes()

    message = (
        f"This media is a url ({self.url!r}), so there are no bytes to "
        f"read yet. Download it first, or let the platform fetch it."
    )
    raise InvalidPostError(message)

MediaKind

Bases: Enum

What sort of file is being attached.

Connection

Connection dataclass

Connection(
    id: str,
    platform: str,
    host: str | None,
    account_id: str,
    account_name: str,
    token: Token,
    scopes: tuple[str, ...] = (),
    extra: RawData = dict(),
    avatar_url: str | None = None,
)

One social account someone has connected to your app.

This is the thing your app saves. socialchimp hands it to you; where and how you store it is entirely up to you.

Attributes:

Name Type Description
id str

Your identifier for this connection. You choose it.

platform str

Which network, for example "bluesky".

host str | None

Which server, for networks that have more than one.

account_id str

The identifier the network itself uses.

account_name str

Something a person would recognise, shown in your UI.

token Token

Current permission to act as this account.

scopes tuple[str, ...]

What this token is allowed to do.

extra RawData

Anything else one network needs, such as a Facebook page id or a YouTube channel id.

avatar_url str | None

The account's picture, when the network gave one when it was connected. Some networks put a short-lived address here, which is why CanReadProfile.read_profile exists - to ask the network for a fresh one when this one has gone stale.

with_token

with_token(token: Token) -> Connection

Return a copy of this connection carrying a new token.

Used after a refresh. The original is left alone.

Parameters:

Name Type Description Default
token Token

The replacement token.

required

Returns:

Type Description
Connection

A new Connection, same in every other way.

Source code in src/socialchimp/models.py
def with_token(self, token: Token) -> Connection:
    """Return a copy of this connection carrying a new token.

    Used after a refresh. The original is left alone.

    Args:
        token: The replacement token.

    Returns:
        A new `Connection`, same in every other way.
    """
    return Connection(
        id=self.id,
        platform=self.platform,
        host=self.host,
        account_id=self.account_id,
        account_name=self.account_name,
        token=token,
        scopes=self.scopes,
        extra=self.extra,
        avatar_url=self.avatar_url,
    )

Token

Token dataclass

Token(
    access_token: str,
    refresh_token: str | None = None,
    expires_at: datetime | None = None,
    refresh_token_expires_at: datetime | None = None,
)

Permission to act as someone on a social network.

Attributes:

Name Type Description
access_token str

The token used on every request.

refresh_token str | None

Used to get a new access token. None where the network does not offer one.

expires_at datetime | None

When the access token stops working. None means it does not expire on its own (Mastodon, Discord and Telegram work this way).

refresh_token_expires_at datetime | None

When the refresh token itself stops working. None on the networks that never expire theirs, which is most of them. Pinterest's lasts sixty days, and renewing an access token does not extend it - so an account nobody has posted from since the summer needs signing in again, and without this an app cannot know until the day it breaks.

is_expired property

is_expired: bool

Whether this token has already run out.

refresh_token_is_expired property

refresh_token_is_expired: bool

Whether the refresh token has already run out.

True here means the person has to sign in again. There is nothing left to renew with.

expires_within

expires_within(seconds: float) -> bool

Say whether this token runs out inside the next seconds.

Used to refresh early, before a request fails.

Parameters:

Name Type Description Default
seconds float

How far ahead to look.

required

Returns:

Type Description
bool

True if the token expires within that window. Always False for a

bool

token that does not expire.

Source code in src/socialchimp/models.py
def expires_within(self, seconds: float) -> bool:
    """Say whether this token runs out inside the next `seconds`.

    Used to refresh early, before a request fails.

    Args:
        seconds: How far ahead to look.

    Returns:
        True if the token expires within that window. Always False for a
        token that does not expire.
    """
    return self._runs_out_within(self.expires_at, seconds)

refresh_token_expires_within

refresh_token_expires_within(seconds: float) -> bool

Say whether the refresh token runs out inside the next seconds.

Nothing socialchimp does can renew a refresh token, so this is not a warning to act on in code - it is a warning to show a person, far enough ahead that they can connect their account again before anything stops working. A week is a reasonable window.

Parameters:

Name Type Description Default
seconds float

How far ahead to look.

required

Returns:

Type Description
bool

True if the refresh token expires within that window. Always

bool

False where the network never told us, which is most of them.

Source code in src/socialchimp/models.py
def refresh_token_expires_within(self, seconds: float) -> bool:
    """Say whether the refresh token runs out inside the next `seconds`.

    Nothing socialchimp does can renew a refresh token, so this is not a
    warning to act on in code - it is a warning to show a person, far
    enough ahead that they can connect their account again before
    anything stops working. A week is a reasonable window.

    Args:
        seconds: How far ahead to look.

    Returns:
        True if the refresh token expires within that window. Always
        False where the network never told us, which is most of them.
    """
    return self._runs_out_within(self.refresh_token_expires_at, seconds)

App credentials

What create_app and a manually-registered app store about themselves.

AppCredentials dataclass

AppCredentials(
    platform: str,
    host: str | None,
    client_id: str,
    client_secret: str,
)

Your app's own identity on one social network.

On most networks you create this by hand in a developer portal. On Mastodon socialchimp can create it for you, and it has to be created again for every server, because each Mastodon server is separate. That is why host is part of the key.

Attributes:

Name Type Description
platform str

Which network, for example "mastodon".

host str | None

Which server, for networks that have more than one. None everywhere else.

client_id str

Public half, given to you by the network.

client_secret str

Private half. Never logged, never printed.

key property

key: tuple[str, str | None]

How these credentials are looked up in storage.

The social inbox

What reading a post, its thread, its likes and its conversations hands back - added in 0.8.0. See the social inbox use case for working examples.

Page dataclass

Page(items: tuple[T, ...], next: str | None = None)

Bases: Generic[T]

One page of results from a list call.

Every list call - reading likes, reading replies, reading conversations, reading messages - takes after: str | None = None, limit: int | None = None and hands one of these back.

Attributes:

Name Type Description
items tuple[T, ...]

What this page holds.

next str | None

Pass this back as after= to read the page after this one. None means there is no more. Treat it as opaque - store it as a string and never parse it. Mastodon fills it from the Link header it sends back; Bluesky and Meta fill it from the network's own cursor.

Person dataclass

Person(
    id: str,
    handle: str | None,
    display_name: str | None,
    avatar_url: str | None,
    url: str | None,
    raw: RawData = dict(),
)

Someone on a social network.

The author of a post, the person behind a like, the other side of a conversation - all of them are a Person.

Attributes:

Name Type Description
id str

The network's identifier for them - a Mastodon account id, a Bluesky DID, or a Meta PSID, IGSID or user id.

handle str | None

Something like "user@host" or "name.bsky.social". None where the network has no such thing, which is how Meta's messaging works.

display_name str | None

The name they chose to show, when the network gives one.

avatar_url str | None

Their picture, when the network gives one.

url str | None

Their profile page, when the network has one.

raw RawData

The network's untouched reply, for anything we did not model.

PostDetails dataclass

PostDetails(
    id: str,
    cid: str | None,
    url: str | None,
    author: Person | None,
    text: str,
    html: str | None,
    links: tuple[TextLink, ...],
    attachments: tuple[Attachment, ...],
    created_at: datetime | None,
    visibility: Visibility | None,
    parent_id: str | None,
    root_id: str | None,
    reply_count: int | None,
    like_count: int | None,
    repost_count: int | None,
    quote_count: int | None,
    liked_by_me: bool | None,
    my_like_id: str | None,
    is_mine: bool,
    unavailable: Unavailable | None,
    raw: RawData = dict(),
)

A post, read back in full - not just what publishing it returned.

Attributes:

Name Type Description
id str

The network's identifier for it - a Mastodon status id, a Bluesky at:// uri, or a Meta object id.

cid str | None

Bluesky's content hash. None everywhere else.

url str | None

The permalink on the network's own website, when there is one.

author Person | None

Who wrote it. None only when unavailable is set.

text str

The words, as plain text. Mastodon's HTML is converted to plain text here. Empty when unavailable is set.

html str | None

The network's own HTML, where it has one - Mastodon does. Untrusted: sanitise it yourself before showing it to anyone.

links tuple[TextLink, ...]

The mentions, links and tags inside text.

attachments tuple[Attachment, ...]

The pictures, videos and other files on this post.

created_at datetime | None

When it was posted, according to the network.

visibility Visibility | None

Who it was shared with. None when the network has no such idea at all - Bluesky and Meta do not.

parent_id str | None

The post this one replies to, when it replies to one.

root_id str | None

The top of the thread this post sits in. The same as id for a top-level post.

reply_count int | None

How many replies it has. None means the network does not say - never "zero".

like_count int | None

How many people liked it. None means the network does not say.

repost_count int | None

How many times it was reposted. None means the network does not say.

quote_count int | None

How many times it was quoted. None means the network does not say.

liked_by_me bool | None

Whether the connected account has liked it. None means we do not know.

my_like_id str | None

Bluesky's like-record uri for the connected account's own like, when there is one. Pass it to unlike to save a lookup.

is_mine bool

Whether the connected account wrote this post.

unavailable Unavailable | None

Set when this is a placeholder standing in for a post a thread could not actually fetch, and says why.

raw RawData

The network's untouched reply, for anything we did not model.

Visibility

Bases: Enum

Who a post was shared with.

None on PostDetails.visibility means the network has no such idea at all - Bluesky and Meta do not model this the way Mastodon does.

PUBLIC class-attribute instance-attribute

PUBLIC = 'public'

Shown to anyone, including people who do not follow the author.

UNLISTED class-attribute instance-attribute

UNLISTED = 'unlisted'

Public, but left out of public timelines and search. Mastodon only.

FOLLOWERS class-attribute instance-attribute

FOLLOWERS = 'followers'

Shown only to people who follow the author. Mastodon calls this "private" on the wire; socialchimp uses the clearer word.

DIRECT class-attribute instance-attribute

DIRECT = 'direct'

Shown only to the people mentioned in it. Mastodon calls this "direct".

TextLink(
    start: int,
    end: int,
    kind: LinkKind,
    target: str,
    url: str | None,
)

A mention, a link or a tag, sitting inside PostDetails.text.

Attributes:

Name Type Description
start int

Where this link starts, as a Python string index into PostDetails.text - character offsets, not bytes.

end int

Where it ends, the same way.

kind LinkKind

What sort of link this is.

target str

What it points at: a URL for LINK, the mentioned person's id for MENTION, or the tag's name with no leading # for TAG.

url str | None

A clickable address for this link, where one is known.

LinkKind

Bases: Enum

What a TextLink inside a post's text points at.

MENTION class-attribute instance-attribute

MENTION = 'mention'

Names another person.

LINK = 'link'

Points at a web address.

TAG class-attribute instance-attribute

TAG = 'tag'

A hashtag.

Attachment dataclass

Attachment(
    kind: str,
    url: str | None,
    preview_url: str | None,
    alt_text: str | None,
    width: int | None,
    height: int | None,
    raw: RawData = dict(),
)

A picture, video or other file attached to a post.

Attributes:

Name Type Description
kind str

What sort of file this is - "image", "video", "gifv", "audio", "link" or "unknown".

url str | None

Where to fetch the file, when the network gives one.

preview_url str | None

A smaller version to show before the full file loads, when the network gives one.

alt_text str | None

A description for people using a screen reader, when the author wrote one.

width int | None

The file's width in pixels, when known.

height int | None

The file's height in pixels, when known.

raw RawData

The network's untouched reply, for anything we did not model.

Unavailable

Bases: Enum

Why a post that should be here could not be shown.

Set on a PostDetails standing in for a post a thread could not actually fetch - a placeholder rather than the real thing.

DELETED class-attribute instance-attribute

DELETED = 'deleted'

The post was removed, or never existed.

BLOCKED class-attribute instance-attribute

BLOCKED = 'blocked'

The author blocked us, or we blocked them.

HIDDEN class-attribute instance-attribute

HIDDEN = 'hidden'

Hidden by moderation - a hidden comment on Meta, or a Bluesky label or threadgate.

Thread dataclass

Thread(
    post: PostDetails,
    replies: tuple[PostDetails, ...],
    complete: bool,
    raw: RawData = dict(),
)

A post together with its replies.

Attributes:

Name Type Description
post PostDetails

The post that was asked for.

replies tuple[PostDetails, ...]

Every reply read back, flat and oldest first. Build the tree yourself by matching each one's parent_id.

complete bool

False if depth, limit or one of the network's own caps cut the replies off before the end.

raw RawData

The network's untouched reply, for anything we did not model.

Like dataclass

Like(
    person: Person,
    liked_at: datetime | None,
    raw: RawData = dict(),
)

One person's like on a post.

Attributes:

Name Type Description
person Person

Who liked it.

liked_at datetime | None

When they liked it. Bluesky has this; Mastodon never does, so it is always None there.

raw RawData

The network's untouched reply, for anything we did not model.

LikeResult dataclass

LikeResult(
    post_id: str, like_id: str | None, raw: RawData = dict()
)

What came back after liking a post.

Attributes:

Name Type Description
post_id str

The post that was liked.

like_id str | None

Bluesky's like-record uri, worth keeping so unlike can skip a lookup. None on Mastodon and Meta - there is nothing to keep.

raw RawData

The network's untouched reply, for anything we did not model.

Conversation dataclass

Conversation(
    id: str,
    people: tuple[Person, ...],
    last_message: Message | None,
    unread_count: int | None,
    updated_at: datetime | None,
    can_reply_until: datetime | None,
    full_history: bool,
    raw: RawData = dict(),
)

A direct message conversation with one or more people.

Attributes:

Name Type Description
id str

The network's identifier for this conversation.

people tuple[Person, ...]

Everyone in it except the connected account.

last_message Message | None

The most recent message, when there is one to show.

unread_count int | None

How many messages are unread. Mastodon only says yes or no, so it reports 1 or 0 rather than a real count.

updated_at datetime | None

When this conversation last changed.

can_reply_until datetime | None

Meta's 24-hour window to reply closes at this moment. None means there is no deadline.

full_history bool

False on Mastodon, which has no "every message" call - read_messages there only reaches as far as the last status's own thread.

raw RawData

The network's untouched reply, for anything we did not model.

Message dataclass

Message(
    id: str,
    conversation_id: str,
    sender: Person,
    text: str,
    sent_at: datetime,
    is_mine: bool,
    deleted: bool,
    attachments: tuple[Attachment, ...],
    raw: RawData = dict(),
)

One message inside a Conversation.

Attributes:

Name Type Description
id str

The network's identifier for this message.

conversation_id str

Which conversation it belongs to.

sender Person

Who sent it.

text str

The words. Empty when deleted is set.

sent_at datetime

When it was sent.

is_mine bool

Whether the connected account sent it.

deleted bool

Whether it has been deleted since.

attachments tuple[Attachment, ...]

Pictures, videos or other files sent with it.

raw RawData

The network's untouched reply, for anything we did not model.