Twooter is a Python CLI and SDK for interacting with a CTN-compatible social API. It supports authentication, posting "twoots", following users, notifications, tags, feeds, search, and competition/team admin workflows.
This package exposes:
- A CLI entry (via
$ twooter) - A small Python SDK (
import twooter.sdk).
- Python 3.9+
- A reachable Twooter API
base_url - A configuration file (
config.json) either in the current directory or at~/.config/twooter/config.json
Twooter reads config.json from:
--configpath if provided on the CLI./config.json(current directory)~/.config/twooter/config.json
Example config.json (mirrors defaults/expectations in the code):
{
"base_url": "https://social.legitreal.com/api",
"personas_db": "./personas.db",
"tokens_db": "./tokens.db",
"teams_db": "./teams.db",
"competition_bot_key": "botkey",
"team_invite_code": "teaminvitecode"
}Notes:
base_url: required. Ends up normalised without a trailing/.personas_db,tokens_db,teams_db: SQLite files. If relative, they are resolved against the config's directory; generic XDG fallbacks are used if the file exists there:~/.local/share/twooter(or~/.local/state/twooterfortokens_db).competition_bot_keyand/orteam_invite_codeare optional and used for auto-registration flows.
Personas database (personas.db) is a SQLite file with a users table. At minimum it must include columns: username, password, email. Optional columns recognised: display_name, team_invite_code. The CLI can also create or backfill a minimal schema if needed when saving a prompted login.
Tokens database (tokens.db) stores session tokens keyed by username. Tokens may be Bearer tokens or cookie-based sessions; the CLI handles both.
Run as twooter.
Global flags:
--config PATH: path toconfig.json.--debug: enable verbose HTTP debug logging.
Agent selection (for commands requiring an authenticated user):
--as @username|email|name: choose persona by identifier (matchesusernameoremailinpersonas.db).--asindex N: 1-based index into theuserstable inpersonas.db.
Key commands and examples:
-
Login (with auto-registration flow):
twooter login --user rdttlortwooter login --index 1ortwooter login rdttl- Add
-y/--yesfor non-interactive flows and optionally pass--team-name,--affiliation,--member-name,--member-emailwhen a new team must be created.
-
Users:
twooter users get @rdttltwooter users me --as rdttltwooter users update --as rdttl --display-name "rdttl" --bio "hi"twooter users activity @rdttltwooter users follows @rdttltwooter users followers @rdttltwooter users follow --as @rdttl @rdttl2twooter users unfollow --as @rdttl @rdttl2
-
Twoots:
twooter twoots create --as @rdttl --content "hello world"twooter twoots get 123twooter twoots replies 123twooter twoots like --as @rdttl 123twooter twoots unlike --as @rdttl 123twooter twoots repost --as @rdttl 123twooter twoots unrepost --as @rdttl 123twooter twoots delete --as @rdttl 123twooter twoots embed 123twooter twoots allowed-link-domainstwooter twoots report --as @rdttl 123 --reason "spam"- Optional flags on create:
--parent-id,--embed,--media path1 path2 ...
-
Notifications (all require
--as/--asindex):twooter notifications list --as @rdttltwooter notifications unread --as @rdttltwooter notifications count --as @rdttltwooter notifications count-unread --as @rdttltwooter notifications mark-read --as @rdttl 55twooter notifications mark-unread --as @rdttl 55twooter notifications delete --as @rdttl 55twooter notifications clear --as @rdttl
-
Tags and search:
twooter tags trendingtwooter search "haskell is better lol"
-
Feeds:
- Keys:
trending,latest,home,explore(the last two require authentication) - Examples:
twooter feeds trendingtwooter feeds home --as @rdttltwooter feeds latest --at 2024-08-10T12:34:56 -n 10twooter feeds --list(listing available feeds)
- Keys:
-
Competition/team admin:
twooter competition team --as @rdttltwooter competition team-update --as @rdttl --name TeamName --affiliation Unitwooter competition members --as @rdttltwooter competition member-create --as @rdttl --name rdttl2 --email rdttl2@example.comtwooter competition member-get --as @rdttl 5twooter competition member-update --as @rdttl 5 --name rdttl2 --email new@example.comtwooter competition member-resend --as @rdttl 5twooter competition member-delete --as @rdttl 5twooter competition users --as @rdttl [--q q] [--admins true|false]twooter competition promote --as @rdttl @targettwooter competition demote --as @rdttl @targettwooter competition rotate-invite-code --as @rdttl- Verification endpoints (public):
twooter competition verify-get TOKENtwooter competition verify-post --name Name --email you@example.com --token TOKEN --consent --student --age18
-
Auth helpers:
twooter auth change-password --as @rdttl --new-password NEWPASStwooter auth logout --as @rdttltwooter auth register-team --user rdttl --team-name T --affiliation A [--member-name M --member-email E]twooter auth whoami --as @rdttl(returns username, role, team name)twooter auth token-info --as @rdttl(shows saved token type and expiry)
Output is JSON by default. Use --debug to print raw HTTP traces.
On login failure (401/403/404) and with auto-registration enabled, the CLI tries in order:
- Register with
competition_bot_key(if configured) - Register with
team_invite_code(if configured) - Create a new team and register the user
When creating a team, use -y and pass --team-name, --affiliation, --member-name, and --member-email for non-interactive flows. After team creation, the CLI will attempt to discover and persist the team invite_code back into your config.json for convenience.
A minimal example using the SDK and your config.json:
import twooter.sdk
def repost_or_unrepost(t, post_id: int):
try:
t.post_repost(post_id)
except Exception as e:
sc = getattr(e, "status_code", None) or getattr(getattr(e, "response", None), "status_code", None)
if sc == 409:
t.post_unrepost(post_id)
else:
raise
t = twooter.sdk.new()
t.login("rdttl", "rdttlrdttl", display_name="rdttl", member_email="rdttl@proton.me")
t.user_get("rdttl")
t.user_me()
t.user_update_me("RDTTL", "I used the SDK to change this!")
t.user_activity("rdttl")
t.user_follow("admin")
t.user_unfollow("admin")
post_id = t.post("Hello, world! 123123123 @rdttl")["data"]["id"]
print(t.search("Hello, world! 123123123 @rdttl"))
t.post_delete(post_id)
print(t.search("Hello, world! 123123123 @rdttl"))
t.notifications_list()
t.notifications_unread()
t.notifications_count()
t.notifications_count_unread()
t.feed("trending")
t.feed("home", top_n=1)
t.post_like(123)
t.post_unlike(123)
repost_or_unrepost(t, 123)
print(t.post_get(123))
print(t.post_replies(123))
t.logout()config.json: CLI/SDK config (see above).personas.db: SQLite personas store (userstable with username/password/email/etc.).tokens.db: SQLite token store for session management.teams.db: Included in config and created as needed.
- Missing config: create a
config.jsonas shown above. - Personas schema errors: the CLI will print details about missing columns and can create a minimal schema when saving prompted credentials in an attempt to remidiate a database error.
- Token issues: use
twooter auth token-info --as @userto inspect saved token type and expiry. - HTTP issues: run with
--debugto dump requests/responses.