The crust of this fork is the advanced_get plugin, which gives you a unix shell in Telegram, with the ability to exchange files. Its user guide is at nightmachinery.github.io/betterborg/telegram_remote_shell; how that site is published is in docs/pages.md.
Needs Python 3.10+.
# Install dash, zsh, tmux, grealpath (GNU realpath, on Linux you could probably use `alias grealpath=realpath`)
# Run in Zsh:
##
borgdir=~/code/betterborg/
# set this the directory containing betterborg
##
function ec {
#: `ec` should almost always be used instead of `echo`.
print -r -- "$@"
}
function ecn {
print -rn -- "$@"
}
function ecerr {
ec "$@" >&2
}
function gquote {
ec "${(qq@)@}"
}
alias gq=gquote
tmuxnew () {
tmux kill-session -t "$1" &> /dev/null
tmux new -d -s "$@"
}
function realpath2 {
test -e "$1" && grealpath -e -- "$1" || {
(( ${+commands[$1]} )) && grealpath -e -- "${commands[$1]}"
}
##
# -e, --canonicalize-existing: all components of the path must exist
##
}
##
tmuxnew betterborg_llm_chat "dash -c 'cd $(gq $borgdir) && borg_session=session_llm_chat borg_plugin_path=llm_chat_plugins borg_brish_count=1 $(gq "$(realpath2 python3)") $(gq $borgdir/stdborg.py)'"Gemini models run on Google AI Studio or, per user, on Vertex AI Express mode:
/geminiProvider switches, /setVertexKey sets the key. Vertex serves fewer
models, has no Files API or explicit caching, and live mode stays on AI Studio; see
docs/gemini_vertex_chat.md.
GPT Sol and GPT Astra appear once in the model menu. Each user’s
/openaiProvider runs them on their OpenAI key (/setOpenAIKey, the default),
on Codex, on OpenRouter, or automatically on one and then the other when the
first runs out of credit or quota. See
docs/openai_provider.md.
Users can share an API key with each other without revealing it (/shareKey),
and choose in /keys the order in which their own and shared keys are tried.
A request moves to the next key when one is rejected, out of credit or rate
limited. See docs/key_sharing.md.
borgdir=~/code/betterborg/
# set this the directory containing betterborg
##
# IMPORTANT: needs the same functions as `llm_chat` defined, copy those function definitions here before running!
##
tmuxnew betterborg_stt "dash -c 'cd $(gq $borgdir) && borg_session=session_stt borg_plugin_path=stt_plugins borg_brish_count=1 $(gq "$(realpath2 python3)") $(gq $borgdir/stdborg.py)'"Each user chooses Google AI Studio or Vertex AI Express mode with /provider
in a private chat. The panel manages both API keys and switches only after a
missing key is checked and saved. /setGeminiKey and /setVertexKey update
keys without switching providers. Each provider remembers its own /model
choice: Auto falls back within that provider, and an explicit model runs
alone. /settings discards hallucinated visual descriptions for audio-only
media by default; turn it off to keep them. Both providers also offer Gemini
3.5 Transcribe (speech only, plain
text; videos need ffmpeg on the host). Files it cannot hear, such as images,
are read by the newest Flash Lite, under a warning. Provider setup, models and retry behavior are in
docs/stt_models.md.
borgdir=~/code/betterborg/
# set this the directory containing betterborg
##
# IMPORTANT: needs the same functions as `llm_chat` defined, copy those function definitions here before running!
##
tmuxnew betterborg_reply_chainer "dash -c 'cd $(gq $borgdir) && borg_session=session_reply_chainer borg_plugin_path=reply_chainer_plugins borg_brish_count=1 $(gq "$(realpath2 python3)") $(gq $borgdir/stdborg.py)'"Pluggable =asyncio= Telegram userbot based on Telethon.
Simply clone the repository and run the main file:
sudo apt-get install -y coreutils fzf zsh python3 mkdir -p ~/code cd ~/code git clone 'https://github.com/NightMachinery/betterborg.git' cd betterborg pip3 install -r requirements.txt # Optional: override Telethon API credentials. export borg_api_id='...' export borg_api_hash='...' # =TELEGRAM_API_ID= / =TELEGRAM_API_HASH= also work. python3 stdborg.py
The modular design of the project enhances your Telegram experience through plugins which you can enable or disable on demand.
Each plugin gets the borg, logger and storage magical variables to ease their use. Thus creating a plugin as easy as adding a new file under the plugin directory to do the job:
# stdplugins/myplugin.py
from telethon import events
@borg.on(events.NewMessage(pattern='hi'))
async def handler(event):
await event.reply('hey')The core features offered by the custom TelegramClient live under the =uniborg/= directory, with some utilities, enhancements and the core plugin.
Telegram’s newer bot features (draft streaming, rich Markdown messages, guest mentions in chats the bot is not in, private-chat topics) and how they map onto MTProto and Telethon versions are summarized in docs/telegram_ai_apis.md, along with the workarounds considered and why this repo upgrades Telethon instead.
Mention any active bot username anywhere in a group message to ask the chat
bot. Aliases also work in model shortcuts (@alias .f question) and command
suffixes (/status@alias). See chat bot addressing.
In private chats, the chat bot shows its answer as a live Telegram draft that
grows about once a second and then becomes the real message, instead of
editing a placeholder (which Telegram rate-limits, so long answers slow down).
On Telethon 1.45 the draft has a Stop button. /stream picks drafts or edits
separately for private chats and for groups; groups default to edits, since
Telegram refuses drafts there. See
docs/draft_streaming.md.
The shell’s commands can stream their output while they run and be stopped:
util.brishz_capture (.a, .af, through Brish.popen) and
util.simple_run_capture (.aa) take a job from uniborg/shell_stream.py,
and uniborg/term_render.py shows progress bars as a terminal would. Other
plugins run on a brish pool of their own. In chats, a command that runs longer
than 2 s shows its output live, in a draft (private chats with a bot, with a
Stop button on Telethon 1.45) or an edited message (with a Stop button on a
bot), which becomes its output when it ends, and .k stops a running
command; guest answers show their output live too, and @bot .k stops
them. On a bot, each admin picks drafts or edits, how a preview ends and
whether the renderer runs in a /settings panel. A shutdown or .restart
stops the running commands first and sends their output before it
disconnects. borg_shell_streaming=0 turns live output and stopping off.
See docs/shell_streaming.md.
In a shell bot’s own chat, dot commands also accept a leading mention, such as
@bot .a CMD or @alias .k. Active additional usernames work there too.
The bots answer where they are mentioned, even in chats they are not in.
The primary username and all active additional usernames (including AI names)
work interchangeably; restart the bots after changing their usernames.
@bot .a CMD runs a shell command for an admin, shows its output live in the
answer, and @bot .k stops it, with any files sent to the caller’s DM (see the
shell guide); @vlm_chat_bot question answers with the caller’s own model and
API key, can generate images into its answer, and a reply to the answer, to
the question, or to what your own question replied to continues the exchange,
media included (an album sent as a reply is one question, but a reply to one
album item sees only that item); @llm_stt_bot in a
reply to a voice note posts its transcript. Reply .stop or /stop to its
progress to cancel; a bare /stop in the STT bot’s private chat stops all your
transcriptions, including guest jobs (see STT controls).
The flow, the safety rules (never
send by the guest chat id, one answer sent once, admin checks on the caller
alone), the chat bot’s guest config section and the steps to enable it are
in docs/guest_mode.md.
The standard .jjson plugin echoes custom inline button data as literal
text, with its own callback tokens so Stop and settings buttons keep working.
See docs/json_buttons.md.
uniborg/telethon_safety.py stops one object Telethon cannot parse from
dropping a whole message container or disconnecting the bot during
getDifference, and alerts the log chat when that happens. Disable it with
borg_tg_safety_nets=0. How layers work here, the upgrade path to Telethon
1.45 and the rollback steps are in
docs/telethon_upgrade.md.
The chat bot paces Telegram sends and edits across a private chat’s topics.
During a Telegram flood cooldown, model output keeps accumulating while
optional streaming edits are skipped. Placeholders and final answers wait
for Telegram to accept them; /stop also cancels requests waiting to send.
The queue lives in memory, so pending work does not survive a bot restart.
See docs/telegram_queue.md.
With threaded mode on in BotFather, a bot’s DM holds topics, and a message
typed in “All” opens a new one. Uniborg adds the topic to every reply to a
message inside a private topic, so answers land next to their question, and
llm_chat’s pending prompts only take an answer from the topic that asked.
Inside a topic, llm_chat defaults to Topic Thread: its own earlier messages,
up to the topic limit (200 by default, /setThreadLastN). /contextModeHere
switches it to Reply Chain (explicit replies only) or Until Separator
(after that topic’s last ---), or sets the default for all topics of the
chat. Resolution is topic setting, chat default for topics, then Topic Thread.
The separator does nothing in Topic Thread; Until Separator uses the same
topic limit. Outside-topic context modes stay separate.
Inside a topic, /setModelHere, /setThinkHere and /setSystemPromptHere
set that topic’s own model, effort and prompt, which come before the chat’s;
their menus have an “Apply to” row that switches them to the whole chat.
/getModelHere shows the effective model and reasoning effort with each
setting’s source, plus the saved topic, whole-chat and personal model defaults.
Separate saved-settings blocks include every per-model effort and omit unset
values and empty layers.
Model and 🧠 Effort choices have separate checkmarks in the model picker. See
docs/private_topics.md.
In a topic that Telegram named (one opened by typing in “All”), the bot puts
the model’s emoji and a circle that fills with the reasoning effort before the
initial name as soon as it starts answering, and sets the model’s topic icon.
The /setTitleModel panel offers New Chat (default) or the question text
as that initial name. After the answer it renames the topic to that badge and
a short title, as in ⚡◕ Monads explained, and sets a topic icon chosen by
the title model. The bot deletes each automatic rename’s service notice for
everyone immediately after the rename. Flood-blocked renames and notice
cleanup wait and retry in the background. A ready final title replaces a
queued initial badge rename, and delayed renames preserve manual name changes.
Only completed title work is marked permanently. See
docs/topic_titles.md.
The timetracker stores each activity’s local times together with its IANA
zone, plus UTC copies that keep ordering right across daylight saving and
travel. .tz reports the current zone and .tz ZONE changes it; the setting
lives in the database, so it follows you rather than the server. .. lists
recent activities with their zones. Each message counts from when it was
sent, so messages sent while the bot was down still land where they belong. A
database recorded without zones can be migrated with
python3 uniborg/timetracker_tz.py migrate. See
docs/timetracker.md.
Habit heatmaps also work on Python 3.13+: the async shell helper snapshots
frame locals to a dictionary before Brish interpolates its command. Restart
the bots after updating that shared helper; reloading a plugin alone leaves
the old helper imported.
The bots keep history and caches in Redis under borg:* keys, as their own
ACL user borg, which may touch only those keys and no dangerous commands.
When the admin secret is available (REDISCLI_AUTH or ~/.redis-auth), the
first bot start creates the user and its secret file ~/.borg/redis-auth,
and the bots recreate the user whenever a Redis restart has forgotten it, so
a fresh server needs no manual steps. See docs/redis.md.
stdplugins/all.py adds admin-only .all=/=@all, .allf=/=@allf, and
.allIDs=/=@allIDs commands for mentioning or listing chat participants. These
commands skip Telegram bots and the Borg/userbot account itself before batching.
See docs/all.md.
llm_chat adds Telegram reaction metadata to model history when available. Reaction
history is stored by history_util; bot sessions require bot admin status in the
group and Redis for restart persistence. Set
REACTION_HISTORY_CACHE_VERBOSITY_MODE=print_each_update to debug reaction
delivery. See docs/llm_chat_reactions.md.
llm_chat appends a short instruction telling models to use injected metadata as
context, not as reply wording. See docs/llm_chat_prompting.md.
llm_chat defaults Last N Messages context to 100 messages. The effective
limit is resolved as chat override, then personal default, then the global
default. Use /contextMode, /groupContextMode, or /contextModeHere outside topics for
inline quick picks (50, 100, 200, 400, 800), or use /setLastN and
/setLastNHere for arbitrary valid values. See
docs/llm_chat_last_n_context.md.
A long answer is also sent as a .md file. That twin file is left out of the
window modes (Last N, Until Separator, a topic’s thread), where its text
already carries the answer, and kept in Reply Chain. See
docs/twin_files.md.
stdplugins/history_export.py adds an admin-only .export [number-of-messages=all]
command that silently writes a Telegram Desktop-style, text-only result.json
for the current chat.
By default exports are saved below ~/tmp/.borg/chat_exports/. Set
BORG_HISTORY_EXPORT_DIR to override the root directory. Reactions include
available reactor identities. Media files are not downloaded; placeholders and
available media metadata are written instead. Progress is printed in the server
terminal, and SIGINT during an active export writes a partial export instead of
interrupting the process. Collection is newest-first for useful partial exports,
while the saved JSON remains oldest-first. See
docs/history_export.md.
stdplugins/deleter.py adds the admin-only .del N, .del s N,
.delalltext N, .delallself, and .delallselfreactions commands. The self-only
form deletes the userbot account’s own messages and selectively clears that
account’s reactions from scanned messages. The bulk text command deletes
text-only posts and conservative text-file attachments from the scanned window
except hashtag-only marker posts. Message-deleting commands export deleted text
to ~/tmp/tlg-deleter/ with a durable WIP JSONL plus final result.json. The
bulk self-delete command clears all of the account’s reactions before deleting
all of its messages; the reaction-only bulk command is available separately. See
docs/deleter.md.
llm_chat can answer GPT Sol and GPT Astra on Codex
(openai-codex/gpt-sol-latest, openai-codex/gpt-astra-latest, which follow
Codex’s catalog) for users whose /openaiProvider picks Codex, using the
published llm-openai-via-codex package and local Codex OAuth credentials. Access is configured in
~/.borg/llm_chat_config.json5 (or LLM_CHAT_CONFIG_PATH) using numeric
Telegram user IDs and the optional MAGIC_ADMINS sentinel. See
docs/codex_models.md and
docs/codex_caching.md.
Connection failures report a specific network diagnosis and exception types;
see Codex network errors for troubleshooting.
The codex_users roster stores names and separate Codex/image access flags.
Bot admins can use .codex-users to toggle those grants and manage personal
default models and per-model reasoning effort. Its compact overview omits
unknown Telegram profile/contact fields and shows configured personal API-key
provider names without exposing key values.
Known private contact appears as Started. Pages contain at most eight users
and split earlier when needed to stay within Telegram’s message limit.
The detail panel separates configured labels from Telegram identity and lists
personal API-key providers with their last-set UTC times. Historical keys show
an unknown date. For older contacts missing from local metadata, opening the
panel can recover positive Started evidence from the bot’s private-dialog list
without sending anything; an absent or unavailable dialog remains unknown.
Use Add user, then Choose user in a private chat to select someone in
Telegram and confirm their identity. Numeric ID and @username entry also work.
Phone-number lookup requires a Telegram user account and is not supported by
the bot. New users start with Codex and images off; their saved model is preserved.
In groups, reply to the add-user prompt. Roster controls require the caller to
be a bot admin, independently of trusted-chat model grants.
Disabled users remain listed; configured trusted-chat access
still applies. Use .codex-users <user-id> <model-id> to set a default
immediately. If Codex is unavailable, ordinary messages fall back to Gemini
Flash with a notice, preserving the saved default.
When the shared account’s allowance runs out, the quota panel offers a one-tap
answer from the Luna Reserve, reached through the gpt-reserve routing slug
rather than gpt-5.6-luna, and also selectable with .cr / .چر. If the
Reserve is spent too, /codexStatus reports the account’s live meters and can
park a user’s saved default on another provider until the window resets. See
docs/codex_luna_reserve.md and
docs/codex_quota_fallback.md.
File titles and summaries, and the names of new private topics, come from
each user’s title model (/setTitleModel): by default the Luna Reserve with
Codex access, else Gemini Flash Lite; see
docs/title_model.md.
GPT quick prefixes have matching Persian aliases: .چ* for GPT Sol and
.اس* for GPT Astra, both on the user’s provider; see
the complete mapping.
Use .i on the current message to enable Codex image generation, for example
.i .cl draw a fox. Both access policies must allow the sender. Previews and
final images are sent separately, alongside any text response; the switch does
not persist. See Codex image generation.
Check out the already-mentioned plugins directory to learn how to write your own, and consider reading Telethon’s documentation.