Skip to content
forked from udf/uniborg

About

My fork of uniborg, which is a pluggable Telegram bot and userbot based on Telethon. Gives you a Unix shell in Telegram!

Topics

Resources

Stars

10 stars

Watchers

2 watching

Forks

 
 

Latest commit

 

History

1,356 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

betterborg

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+.

Public Instances of Bots

llm-chat

# 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.

llm-stt

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.

reply-chainer

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)'"

uniborg

Pluggable =asyncio= Telegram userbot based on Telethon.

installing

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

design

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')

internals

The core features offered by the custom TelegramClient live under the =uniborg/= directory, with some utilities, enhancements and the core plugin.

Telegram AI-bot APIs

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.

Chat bot mentions

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.

Draft streaming

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.

Shell streaming

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.

Guest mode

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.

JSON buttons

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.

Telegram layers and safety nets

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.

Concurrent chat delivery

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.

Private-chat topics

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.

timetracker time zones

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.

Redis

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.

all mentions

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.

reactions in LLM chat

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 prompting

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 Last-N Context

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.

history export

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.

deleter

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.

Codex models

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.

learning

Check out the already-mentioned plugins directory to learn how to write your own, and consider reading Telethon’s documentation.

About

My fork of uniborg, which is a pluggable Telegram bot and userbot based on Telethon. Gives you a Unix shell in Telegram!

Topics

Resources

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages