SecureChat SDK 4.2
Python code that joins SecureChat as a real, end-to-end encrypted client. It holds its own keys, connects through your relay with TLS pinned to the server, and speaks the same post-quantum protocol as the apps.
There are two ways to use it, and they are separate things:
🤖 Bot — Bot |
👤 User session — Client |
|
|---|---|---|
| What it is | A separate bot account (its name ends in bot) |
Your own account, running as one more of your devices |
| Signs in with | A bot token from the app (Menu → My Bots) | A session file you create once with your username + passphrase |
| People see | Messages from @yourbot |
Messages from you (they show on your phone and PC too) |
| Style | Like aiogram: handlers, filters, keyboards, FSM | Like Telethon: events, send_message, iter_messages |
| Can | Answer anyone who writes to it, show buttons and menus, guard groups as an admin | Everything you can do: read your history, write to anyone, join groups |
| Can't | Read chats it isn't in, or touch anyone's account | Send bot keyboards or buttons (only bots can) |
| Use it for | Menus, shops, games, support desks, group guards, notifications | Auto-replies, archiving and searching your chats, personal automation |
Rule of thumb: building something for other people: make a Bot. Automating your own account: use Client.
Contents
- Install
- Connecting to the server
- Bots
- 1. Get a token
- 2. Your first bot
- 3. Handlers
- 4. Filters
- 5. The message object
- 6. Sending, editing, deleting
- 7. Formatting
- 8. Keyboards and buttons
- 9. Callback queries (button presses)
- 10. Files and stickers
- 11. Conversations (FSM)
- 12. Groups and channels
- 13. Routers, middleware, errors
- 14. The bot's profile and command menu
- 15. Running a bot
- User sessions
- Chat ids
- Errors
- Environment variables
- Examples
- Troubleshooting
Install
pip install -r securechat_sdk/requirements.txt # pynacl, websockets, cryptography
Python 3.10+. Put the securechat_sdk folder next to your script (or on PYTHONPATH), then
from securechat_sdk import Bot or from securechat_sdk import Client, events.
Connecting to the server
There is nothing to configure. Like the apps, the SDK is locked to the SecureChat server: its address is built in
(encoded in securechat_sdk/server_address.py: the file holds no readable address, every lock writes different bytes,
and an edited file is refused), and bots and sessions connect only there.
- They go through the relay, with TLS pinned to the server's certificate. An intercepted or tampered connection stops
with
AuthErrorbefore anything is sent. - There is no
server=setting and no environment variable for it:SC_SERVERis ignored, andserver=with another address raises an error.
🤖 Bots
1. Get a token
In the app: Menu → My Bots → New bot, then open the bot and choose API token → Copy. A token looks like
mybot:AbC…. Anyone who has it is the bot, so treat it like a password.
bot = Bot("mybot:AbC…") # directly (fine for a quick test; don't commit it)
bot = Bot.from_env() # from the BOT_TOKEN environment variable, or a .bot_token file next to the script
Revoke in My Bots rotates the token. The old one stops working at once: a running bot gets TokenRevoked and
stops.
2. Your first bot
from securechat_sdk import Bot, F, InlineKeyboardBuilder
from securechat_sdk.formatting import bold
bot = Bot.from_env()
@bot.command("start", description="Say hi") # also adds /start to the bot's "/" menu
async def start(msg):
kb = InlineKeyboardBuilder()
kb.button(text="👍 Yes", callback_data="yes", style="success")
kb.button(text="Docs", url="https://example.org")
await msg.answer(f"Hi {bold(msg.from_user.full_name)}! Ready?", reply_markup=kb.as_markup())
@bot.callback_query(F.data == "yes")
async def yes(cb):
await cb.answer("Saved") # a small toast in the app
await cb.edit_message_text("You said yes ✅")
@bot.message(F.text)
async def echo(msg):
await msg.reply(msg.text)
bot.run()
export BOT_TOKEN="mybot:AbC…" # Windows: set BOT_TOKEN=mybot:AbC…
python my_bot.py
3. Handlers
A handler is an async function with a decorator. For each incoming event, handlers are tried in the order they were
registered, and the first one whose filters match runs.
| Decorator | Runs for |
|---|---|
@bot.message(*filters) |
every incoming message: text, photo, file, sticker, a shared contact… |
@bot.command("start", "help", description="…") |
/start, /start args, /start@yourbot args; with description=, the command also goes into the "/" menu |
@bot.callback_query(*filters) |
a press on one of the bot's inline buttons |
@bot.chat_member(*filters) |
every group event: join, leave, add, remove, ban, unban, mute, unmute, promote, demote, pin, unpin, unpin_all, title, photo, slow |
@bot.new_chat_members(*filters) |
shortcut: join / add |
@bot.left_chat_member(*filters) |
shortcut: leave / remove / ban |
@bot.on_start |
after every (re)connect (a good place for set_commands) |
@bot.on_error |
async def (event, exc): a handler raised |
Several filters in one decorator must all match (AND). Inside a handler, raise SkipHandler passes the event on to the
next matching handler.
What a handler can ask for. The first argument is the event. Add parameters by name and the SDK fills them in:
async def handler(msg): ...
async def handler(msg, state): ... # FSMContext for this user (section 11)
async def handler(msg, bot): ... # the Bot
async def handler(msg, state, bot): ...
4. Filters
Each line below is a filter; pass it to a decorator, e.g. @bot.message(F.text == "hi"):
from securechat_sdk import F, Command, Text, Regexp
F.text == "hi" # exact text
F.text.startswith("!") # also .endswith(), .contains(), .in_([...])
F.photo # truthy = present (F.document, F.sticker, F.contact, F.file …)
F.chat.type == "private" # attribute paths work: F.from_user.username == "alice"
Command("help") # what @bot.command("help") uses
Text("Yes", "No") # one of these texts
Regexp(r"^order (\d+)$") # the match is in msg.regexp_match
(F.text == "x") & (F.chat.type == "group") # combine with & | ~
lambda m: len(m.text) > 100 # any function (sync or async) works as a filter
@bot.message(F.text.startswith("!"), F.chat.type == "group") # several filters: all must match
async def group_command(msg): ...
F comparisons: ==, !=, <, >, <=, >=, .is_(), .in_(), .startswith(), .endswith(), .contains().
5. The message object
Message (what handlers receive):
| Attribute | Meaning |
|---|---|
message_id / id |
the message's id |
chat |
Chat(id, type): id is the sender's username in a private chat, "g:<id>" in a group; type is "private" or "group"; .is_private, .is_group |
from_user |
User(username, display, is_bot): .full_name, .mention (@name), .id (= username) |
text |
the text (the caption for a file) |
ts |
when it was sent (unix ms) |
reply_to |
the message it replies to (a dict), or None |
file, photo, document |
a File (photo = an image, document = anything else), or None |
caption |
the text of a file message |
sticker |
{"p": pack, "i": item, "e": emoji, "m": mime} for stickers / GIFs |
command, args |
for commands: CommandObject(command, args, raw); msg.args is the text after /start (deep-link payloads) |
regexp_match |
set by Regexp(...) |
contact |
Contact(user_id, first_name): the user shared their profile (request_contact button) |
users_shared |
UsersShared(request_id, users), .user_ids (request_users button) |
chat_shared |
ChatShared(request_id, chat_id, title, username) (request_chat button) |
| Method | Does |
|---|---|
await msg.answer(text, reply_markup=None, parse_mode=…) |
sends to the same chat |
await msg.reply(text, reply_markup=None) |
the same, as a reply to this message |
await msg.answer_photo(photo, caption="", reply_markup=None) |
sends a picture (bytes or a path) |
await msg.edit_text(text, reply_markup=…) |
edits it (only the bot's own messages) |
await msg.edit_reply_markup(reply_markup) |
changes or removes its inline buttons |
await msg.delete() |
deletes it for everyone |
await msg.react("👍") |
a reaction |
await msg.typing() |
shows "typing…" for a few seconds |
6. Sending, editing, deleting
All are methods of Bot; chat_id is a username or "g:<id>":
| Method | Notes |
|---|---|
send_message(chat_id, text, reply_markup=None, reply_to=None, parse_mode=…) |
returns the sent Message |
send_file(chat_id, file, caption="", filename=None, reply_markup=None, reply_to=None) |
file = bytes or a path; encrypted before upload. send_photo / send_document are the same method |
send_sticker(chat_id, pack, item_id=None, emoji=None, reply_to=None) |
by the pack's link; picks item_id, else the first matching emoji, else the first sticker |
send_chat_action(chat_id, "typing") |
"typing…" |
edit_message_text(chat_id, message_id, text, reply_markup=UNSET, parse_mode=UNSET) |
UNSET keeps the current keyboard |
edit_message_reply_markup(chat_id, message_id, reply_markup=None) |
None removes the buttons |
delete_message(chat_id, message_id) |
the bot's own, or anyone's as a group admin |
react(chat_id, message_id, emoji) |
7. Formatting
By default (parse_mode="md") text is rendered as SecureChat markup:
| You write | You get |
|---|---|
**bold** |
bold |
__italic__ |
italic |
~~strike~~ |
|
||spoiler|| |
a hidden spoiler |
`code` and ```pre``` |
monospace |
[text](https://…) |
a link |
> quote |
a quote |
Helpers in securechat_sdk.formatting: bold, italic, strike, spoiler, code, pre(text, lang), quote,
link(label, url), mention(username) and escape(text). Always escape() text that comes from users before you
put it in markup. Bot(token, parse_mode=None) (or parse_mode=None on one call) sends text exactly as written.
8. Keyboards and buttons
Inline buttons (under a message):
from securechat_sdk import InlineKeyboardMarkup, InlineKeyboardButton, InlineKeyboardBuilder, WebAppInfo
kb = InlineKeyboardMarkup().row(
InlineKeyboardButton("Buy", callback_data="buy", style="success", icon="🛒"),
InlineKeyboardButton("Cancel", callback_data="no", style="danger"))
kb.row(InlineKeyboardButton("Open site", url="https://example.org"),
InlineKeyboardButton("Copy code", copy_text="ABC-123"))
kb.row(InlineKeyboardButton("Search", switch_inline_query_current_chat="cats"), # puts "@yourbot cats" in the box
InlineKeyboardButton("Web app", web_app=WebAppInfo("https://example.org")))
# or the builder:
b = InlineKeyboardBuilder()
for n in range(1, 7):
b.button(text=str(n), callback_data=f"n:{n}")
b.adjust(3) # rows of 3 (the last size repeats: adjust(2, 3) = 2, then 3, 3…)
await msg.answer("Pick one", reply_markup=b.as_markup())
Each button takes exactly one action:
| Action | What it does |
|---|---|
callback_data= |
sends the bot a callback (≤ 64 chars) |
url= |
opens a web link, or jumps to a user / group / channel (@username, sc://username, sc://join/CODE) |
copy_text= |
copies text |
web_app= |
opens a page after a confirm |
switch_inline_query_current_chat= |
puts @yourbot … in the input box |
Styles: primary (blue), success (green), danger (red); blue / green / red work too. icon= is one emoji.
Limits: 10 rows × 8 buttons, 64 characters of text.
Reply keyboards (instead of the phone's keyboard):
from securechat_sdk import (ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove, ForceReply,
KeyboardButtonRequestUsers, KeyboardButtonRequestChat)
menu = ReplyKeyboardMarkup([
[KeyboardButton("📋 Menu", style="primary"), KeyboardButton("Share my profile", request_contact=True)],
[KeyboardButton("Pick friends", request_users=KeyboardButtonRequestUsers(1, max_quantity=3)),
KeyboardButton("Pick a group", request_chat=KeyboardButtonRequestChat(2))],
], is_persistent=True, one_time_keyboard=False, input_field_placeholder="Choose…")
await msg.answer("Main menu", reply_markup=menu)
await msg.answer("Bye", reply_markup=ReplyKeyboardRemove()) # take it away
await msg.answer("Your name?", reply_markup=ForceReply(input_field_placeholder="Name")) # the app opens a reply
@bot.message(F.contact)
async def got_contact(msg): ... # msg.contact.user_id / .first_name
@bot.message(F.users_shared)
async def got_users(msg): ... # msg.users_shared.request_id / .user_ids
@bot.message(F.chat_shared)
async def got_chat(msg): ... # msg.chat_shared.request_id / .chat_id ("g:…") / .title
A reply keyboard stays in the user's app (kept in their encrypted vault) until you send ReplyKeyboardRemove(). A
one-time keyboard hides after a press but stays behind the ⌨ button.
9. Callback queries (button presses)
@bot.callback_query(F.data.startswith("n:"))
async def picked(cb):
n = cb.data.split(":")[1]
await cb.answer(f"You picked {n}") # toast; show_alert=True for a dialog
await cb.edit_message_text(f"Picked **{n}**") # edit the message the button was on
CallbackQuery:
- Attributes:
id,data,from_user,chat,message_id,message. - Methods:
answer(text="", show_alert=False, url=""):url=makes the app open a link or jump to@username.edit_message_text(text, reply_markup=…)edit_message_reply_markup(markup)
A press you don't answer() is answered for you (the spinner on the button stops). Presses are end-to-end encrypted to
the bot: the server never learns what users press.
Since SDK 4.2, a press only counts for a message this bot sent buttons on, in the chat it sent them to (others are
dropped before your handler runs). Still check cb.from_user yourself when a button is meant for one person.
10. Files and stickers
@bot.message(F.photo)
async def photo(msg):
data = await msg.photo.download() # bytes (downloaded and decrypted)
await msg.answer(f"{msg.photo.name}: {len(data)} bytes")
await bot.send_file(chat, "report.pdf", caption="Monthly report")
await msg.answer_photo(open("cat.jpg", "rb").read(), caption="🐱")
await bot.send_sticker(chat, "cats_by_alice", emoji="😺")
@bot.message(F.sticker)
async def sticker(msg): await msg.answer(f"nice {msg.sticker['e']}")
File:
- Attributes:
file_id,name,size,mime,is_image. - Method:
await file.download(), which returns the decrypted bytes. - The server only ever stores ciphertext.
11. Conversations (FSM)
Remember where each user is in a multi-step dialog:
from securechat_sdk import SkipHandler, State, StatesGroup
class Order(StatesGroup):
name = State()
address = State()
@bot.command("order")
async def order(msg, state):
await state.set_state(Order.name)
await msg.answer("Your name?")
@bot.message(lambda m: True)
async def steps(msg, state):
step = await state.get_state()
if step == str(Order.name):
await state.update_data(name=msg.text)
await state.set_state(Order.address)
await msg.answer("Address?")
elif step == str(Order.address):
data = await state.get_data()
await state.clear()
await msg.answer(f"Thanks {data['name']}, shipping to {msg.text}")
else:
raise SkipHandler # not in a dialog: let other handlers have it
FSMContext methods:
set_state(state),get_state()update_data(**kw),get_data()clear()/finish()
bot.fsm(username) gets one for any user. The state lives in memory; store anything that must survive a restart
yourself (e.g. in SQLite, as examples/links_bot.py does).
12. Groups and channels
Add the bot to a group like any member. Then:
- Private groups: messages are end-to-end encrypted to the bot too.
- Public groups / channels: it reads and posts through the server history like everyone else.
Either way msg.chat.id is "g:<id>". For most of the moderation methods below, make the bot an admin (group info
→ Members → the bot → Make admin).
| Method | Does |
|---|---|
get_chat(chat) |
ChatInfo: title, kind (group / channel), public, handle, description, owner, administrators, member_count, pinned, slow_mode, members (private groups) |
get_chat_member_count(chat) / get_chat_administrators(chat) |
|
get_chat_member(chat, user) |
ChatMember: status (owner / admin / member / banned / left), .is_admin, .is_muted, muted_until |
ban_chat_member(chat, user) / unban_chat_member |
banned users can't come back, even with a link |
kick_chat_member(chat, user) |
removed, may rejoin |
mute_chat_member(chat, user, seconds=None) / unmute_chat_member |
None = until unmuted |
restrict_chat_member(chat, user, until_date=None, can_send_messages=False) |
Telegram-style alias of mute |
promote_chat_member(chat, user, is_admin=True) |
owner-only on the server side |
add_chat_members(chat, ["bob", "carol"]) |
returns those whose privacy settings only allow invite links |
delete_message(chat, message_id) |
anyone's message |
pin_chat_message / unpin_chat_message(chat, mid) / unpin_all_chat_messages(chat) |
|
set_chat_title / set_chat_description / set_chat_photo(chat, path_or_bytes) |
|
set_slow_mode(chat, seconds) |
0, 10, 30, 60, 300, 900 or 3600 |
export_chat_invite_link(chat, reset=False) |
"sc:join/…" |
delete_service_message(chat, event_id) |
removes "Alice joined" and similar |
leave_chat(chat) |
Group events come as ChatMemberUpdated:
- Attributes:
event("join","leave","ban"…),user(who it happened to),actor(who did it),chat,until,title,event_id,.joined,.left. - Methods:
await ev.answer(text), andawait ev.delete()to remove the service message from the chat.
@bot.new_chat_members()
async def welcome(ev):
await ev.answer(f"Welcome @{ev.user.username}!")
@bot.chat_member()
async def tidy(ev): # keep the group clean of "joined / left" lines
if ev.event in ("join", "add", "leave", "remove"):
await ev.delete()
Group events arrive while the bot is online; they aren't queued. See examples/guard_bot.py for a full group guard
(captcha, link filter, flood control, admin commands).
13. Routers, middleware, errors
Split a big bot into parts:
from securechat_sdk import Router
shop = Router("shop")
@shop.command("buy")
async def buy(msg): ...
bot.include_router(shop) # routers can include routers
Middleware wraps every handler of its router (and the routers below it):
@bot.middleware
async def timing(handler, event, data): # data: {"bot", "event", "state"}; add your own keys
import time
t = time.time()
try:
return await handler(event, data)
finally:
print(f"{type(event).__name__} took {time.time() - t:.3f}s")
@bot.on_error
async def oops(event, exc):
print("handler failed:", exc)
14. The bot's profile and command menu
@bot.on_start
async def setup():
await bot.set_commands({"start": "Open the menu", "help": "How it works"}) # or [("start", "…"), …]
await bot.set_my_name("Pizza Bot")
await bot.set_my_description("Order pizza in two taps")
await bot.set_my_photo("logo.png")
Commands registered with @bot.command(..., description=…) are published automatically; set_commands replaces that
list.
15. Running a bot
bot.run()blocks and reconnects by itself. Ctrl+C or SIGTERM shut it down cleanly, after the encryption state has been saved.- In your own asyncio program:
await bot.run_async()andawait bot.stop(). - Encryption state: bots speak protocol v3 (a post-quantum handshake, then a new key for every message). Their
session keys are kept encrypted in
./securechat_state/<bot>.e2e. Choose the folder withBot(token, state_dir="…")orSC_STATE_DIR.- Keep the folder between restarts, and out of git and shared backups.
- Deleting it is safe: the bot publishes fresh keys and senders' apps re-send whatever it couldn't read.
- As a service: run it under systemd, NSSM or Docker with
BOT_TOKENand a persistentstate_dir. - Privacy: a bot sees the plaintext of the chats it is in. That is what adding a bot means.
👤 User sessions
Client runs as your own account, like Telethon or Pyrogram:
- You sign in once with your username and passphrase; the SDK saves a session file. From then on the script runs without the passphrase.
- The session is one more of your devices. It appears in the app under Settings → Devices, and you can log it out there any time.
- It receives every message end-to-end encrypted for itself.
- What it sends shows on your phone and PC too.
1. Create a session file
python -m securechat_sdk.login me # asks: username, passphrase, a password for the file -> me.session
python -m securechat_sdk.login me --no-password # a file WITHOUT a password (not recommended)
python -m securechat_sdk.login me --recovery # sign in with your recovery key instead of the passphrase
python -m securechat_sdk.login me --name "Home server" # how it's named in Settings → Devices
python -m securechat_sdk.login me --logout # log the session out and delete the files
Or let the script do it: client.start() asks in the terminal the first time.
The session file is password-protected by default. A script then opens it with the same password:
Client("me", session_password="…"), or the SC_SESSION_PASSWORD environment variable. In a terminal it asks. A
script without a terminal (a service) and without the password stops with AuthError instead of guessing.
2. Your first script
from securechat_sdk import Client, events
client = Client("me") # uses me.session
@client.on(events.NewMessage(incoming=True, pattern=r"(?i)^ping$"))
async def pong(event):
await event.reply("pong 🏓")
@client.on(events.NewMessage(outgoing=True, pattern=r"^!chats$"))
async def chats(event): # type "!chats" in any chat, from your phone or PC
dialogs = await client.get_dialogs()
await event.respond("\n".join(f"{d.name} · {d.unread_count} unread" for d in dialogs[:10]))
client.start()
client.run_until_disconnected()
Async style:
import asyncio
from securechat_sdk import Client
async def main():
async with Client("me") as client:
await client.send_message("alice", "Hello from Python 👋")
async for m in client.iter_messages("alice", limit=20):
print(m.date, m.sender_id, m.text)
asyncio.run(main())
TelegramClient is the same class under another name, for people coming from Telethon.
3. Events
Register with @client.on(EventType(...)) or client.add_event_handler(fn, EventType(...));
client.remove_event_handler(fn) removes one. Every event type takes:
chats=: one chat id or a listfrom_users=: one username or a listfunc=: a function that gets the event and returns True to accept it
| Event | Fires when | The event has |
|---|---|---|
events.NewMessage(incoming=, outgoing=, pattern=, …) |
a message arrives (incoming) or you send one, from here or your other devices (outgoing); both by default |
message, text, chat_id, sender_id, out, is_private, is_group, id, pattern_match; reply(), respond(), delete(), mark_read(), download_media(), get_sender(), get_chat() |
events.MessageEdited(...) |
a message was edited (same options) | the same; text is the new text |
events.MessageDeleted() |
messages were deleted | chat_id, deleted_ids, deleted_id, sender_id |
events.ChatAction() |
group events: joins, leaves, adds, bans, title / photo changes | chat_id, action, user_id, by, user_joined, user_added, user_left, user_kicked, new_title, raw |
events.UserUpdate() |
someone is typing, or comes online / goes away | user_id, chat_id, typing, online |
events.Raw() |
every message from the server, as a dict (advanced) | the dict |
pattern= is a regex string, a compiled pattern or a function. Matching uses re.match, from the start of the text.
raise StopPropagation (also events.StopPropagation) in a handler stops the handlers after it.
4. Client methods
Connection
| Method | |
|---|---|
Client(session="securechat", session_password=None, device_name=None) (session_password defaults to SC_SESSION_PASSWORD) |
session: a name (me → me.session) or a path |
start(username=None, passphrase=None, recovery_key=None) |
signs in the first time (asking for what's missing), then connects |
sign_in(username, passphrase=None, recovery_key=None) |
writes the session file only |
connect() / disconnect() |
with an existing session file |
run_until_disconnected() |
blocks while events are handled (run_until_disconnected_async() in async code) |
log_out() |
removes this session from your devices and deletes its files |
Reading
| Method | Returns |
|---|---|
get_me() |
your User |
get_entity("alice" | "@handle" | "g:<id>") |
a User or a Chat |
get_dialogs(limit=None) |
your chats as Dialogs, newest first |
iter_messages(entity, limit=100, search=None, from_user=None, reverse=False) |
an async iterator of Messages, newest first. History comes from your end-to-end encrypted vault; public groups and channels come from the server's history |
get_messages(entity, limit=100, …) |
the same, as a list |
get_participants(chat) |
a group's members as Users |
download_media(message, file=None) |
bytes, or saves into file (a path or a folder) and returns the path |
Writing
| Method | |
|---|---|
send_message(entity, text, reply_to=None, silent=False, parse_mode="md") |
silent=True = no notification sound |
send_file(entity, file, caption="", reply_to=None, file_name=None, voice_note=False) |
bytes or a path |
edit_message(entity, message, text) |
your own messages |
delete_messages(entity, message_ids) |
one id / message, or a list |
send_reaction(entity, message, emoji) |
|
send_read_acknowledge(entity, message=None) |
marks the chat read (the sender sees ✓✓) |
action(entity, "typing") |
"typing…" for a few seconds |
join_chat("@handle" | "sc:join/CODE") |
returns the Chat; raises if the group needs approval (the request is sent) |
leave_chat(chat) |
5. Objects
Message
- Attributes:
id,chat_id,sender_id,text(alsoraw_text,message),date(UTC datetime),out(sent by you),reply_to_msg_id,file,sticker,media,edit_date,silent,fwd_from,is_private,is_group,is_reply. - Methods:
reply(),respond(),edit(),delete(),react(),mark_read(),download_media(),get_sender(),get_chat().
User: id / username, display / first_name, handle (the @username), bot.
Chat (groups and channels): id ("g:<id>"), gid, title, kind (group / channel), public, handle,
participants_count, broadcast (True for channels).
Dialog: entity (a User or Chat), id, name / title, message (the last one), date,
unread_count, is_user, is_group, is_channel.
6. Keeping the session safe
me.session holds your identity key. Anyone with the file can read and send as you.
- It's written readable by you only. Keep it out of git, chats and cloud folders.
- It's password-protected by default (Argon2id), so a copied file is useless without the password. A file made with
--no-passwordlogs a warning every time it's used. - End-to-end session keys live next to it in
me.session.e2e, encrypted with a key derived from your identity. - Lost the machine? Open the app, go to Settings → Devices and log the session out. It stops working at once.
- Secret chats stay on the device that started them, so a session doesn't see them (the same as the apps).
- Keys are pinned (SDK 4.2). The first key seen for each person is kept in the encrypted state file. If the server
later reports a different key, the SDK keeps using the pinned one, logs a warning, and doesn't encrypt to the new key.
Once you've checked the new key yourself (its safety number in the app), call
client._t.accept_new_key(user, pub). - Someone who has used the advanced (v3) encryption with you can't be quietly switched back to v2: such messages are refused and their app is asked to send them again.
download_media(msg, folder)keeps the sender's file name insidefolder(no paths, never overwrites).
Chat ids
| You mean | Write |
|---|---|
| a person or a bot | their username: "alice" ("@alice" and sc://alice work in the Client) |
| a group or channel | "g:<group id>", e.g. msg.chat.id, event.chat_id, ChatShared.chat_id |
| a public group by link (Client) | get_entity("@handle"), join_chat("@handle") |
Errors
All errors derive from SecureChatError:
| Error | Meaning |
|---|---|
AuthError |
the token / session / passphrase was refused, or the server's pin didn't match the address |
TokenRevoked (an AuthError) |
the bot's owner revoked the token. Make a new one and restart |
SecureChatError |
anything else the server refused (no such user / group, not allowed …) |
Environment variables
| Variable | Used by | Meaning |
|---|---|---|
BOT_TOKEN |
Bot.from_env() |
the token (or put it alone in a .bot_token file next to the script) |
SC_STATE_DIR |
Bot |
where the bot keeps its encrypted session keys (default ./securechat_state) |
SC_SESSION_PASSWORD |
Client, login |
the session file's password (for scripts that run unattended) |
Examples
| File | Shows |
|---|---|
examples/echo_bot.py |
the smallest bot: echo, a reply keyboard, photos and documents |
examples/menu_bot.py |
inline keyboards: a +/- counter, URL and copy buttons, removing a keyboard |
examples/links_bot.py |
categories and links in SQLite, a form with FSM, inline keyboards |
examples/guard_bot.py |
a group guard: captcha for newcomers, a link filter, flood control, /ban /mute /pin /slow |
examples/user_client.py |
a user session: !ping, !chats, welcoming people who join your groups |
Troubleshooting
| Problem | Fix |
|---|---|
'str' object has no attribute 'on_start' |
You wrote bot = "mybot:…". It must be bot = Bot("mybot:…") |
AuthError right away |
Wrong or old token (copy it again from My Bots), or the connection to the server was intercepted |
only connects to the SecureChat server it is locked to |
Remove server= from Bot(...) / Client(...): the address is built in |
TokenRevoked |
The token was revoked in My Bots. Copy the new one |
| The bot doesn't see messages in a group | Is it a member? Moderation methods also need it to be an admin |
| Moving the bot to another machine | Copy its state_dir folder along. Without it the bot still works: it publishes new keys and senders' apps re-send what it couldn't read |
me.session is password-protected |
Pass session_password= or set SC_SESSION_PASSWORD (a service has no terminal to ask in) |
no session file me.session |
Run python -m securechat_sdk.login me first, or call client.start() |
| The session stopped working | It was logged out in Settings → Devices. Create a new one |
Metadata
Release files for securechat-sdk 4.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| securechat_sdk-4.2.2.tar.gz | 79.1 kB | Without provenance | Details for securechat_sdk-4.2.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| securechat_sdk-4.2.2-py3-none-any.whl | Python 3 | none | any | Details for securechat_sdk-4.2.2-py3-none-any.whl |
Total release size: 155.0 kB
Release files / securechat_sdk-4.2.2.tar.gz
| Download URL | securechat_sdk-4.2.2.tar.gz |
|---|---|
| Size | 79.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
86b10a3c92d6940a7d98947ffb2e9b2b59fced9dbc5c0d2bf0d30b7abda8990a
|
|
BLAKE2b-256 checksum How to use checksums |
6657e9f2e8f4664ff26b3926f1c09c2de3435baa6122e545ff352a1a4eefbbe0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.7
|
Release files / securechat_sdk-4.2.2-py3-none-any.whl
| Download URL | securechat_sdk-4.2.2-py3-none-any.whl |
|---|---|
| Size | 75.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b99f39ff1748986c311f8823e3007ace680ce75b0a6a932e3e8659171359fc96
|
|
BLAKE2b-256 checksum How to use checksums |
1981889e7fb1b26606f5d0be5ae0d57a7795ba9fe3bbda78362c41dcb729fb3b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.7
|
Security for securechat-sdk 4.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.