Skip to main content

django-tgcms

A reusable Django app for building and sending Telegram posts, fully embedded in Django admin — no project-side code needed. Compose posts from blocks (heading, formatted text, photo, video) with a WYSIWYG editor, send them to configured channels via an admin action, or take Post.render() — a Bot API-ready payload — and send however you like.

No dependencies beyond Django. Sending uses stdlib urllib only.


Installation

pip install django-tgcms
# or
uv add django-tgcms

settings.py:

INSTALLED_APPS = [
    ...
    "tgcms",
]

# Optional — enables the "Send to Telegram: <name>" admin actions
TGCMS = {
    "BOT_TOKEN": env("YOUR_BOT_TOKEN"),   # map whatever name your project uses
    "CHANNELS": {
        "My channel": "-1001234567890",   # name → chat_id (or "@username")
    },
}

Run migrations:

python manage.py migrate

Content — Django admin

Posts are edited in the standard Django admin at /admin/tgcms/post/. The change page shows a collapsible "Bot API payload" preview of render().

Block types (drag-and-drop reordering inside each post):

  • heading — plain text title, sent bold
  • text — formatted text: bold, italic, underline, strikethrough, spoiler, code, pre, blockquote, links
  • photo / video — media asset + caption

MediaAsset (/admin/tgcms/mediaasset/) is a shared media registry. One asset can be referenced by any number of blocks across any number of posts. After the first Telegram send the telegram_file_id is cached on the asset — subsequent sends reuse it without re-uploading.


Sending from admin

With BOT_TOKEN and CHANNELS configured, the Post changelist gets a "Send to Telegram: " action per channel:

  • select posts → choose the action → each block is sent as a separate message
  • successful posts are marked published; failures are reported per post
  • photo/video blocks cache telegram_file_id on their MediaAsset after the first upload

The same logic is importable for workers/cron:

from tgcms.publish import send_post, PublishError

send_post(post, chat_id="-1001234567890", token=BOT_TOKEN)

Bot integration

from tgcms.models import Post

post = Post.objects.prefetch_related("blocks__media").get(pk=post_id)
payload = post.render()
# {
#   "blocks": [
#     {"type": "heading", "text": "Title"},
#     {"type": "text", "text": "Hello!", "entities": [{"type": "bold", "offset": 0, "length": 5}]},
#     {"type": "photo", "media_asset_id": 3, "file": "AgACAgI...", "caption": "..."},
#   ]
# }

aiogram broadcast pattern:

from asgiref.sync import sync_to_async

async def send_post(bot, chat_id: int, post_id: int):
    post = await sync_to_async(
        Post.objects.prefetch_related("blocks__media").get
    )(pk=post_id)

    for block in post.blocks.all():
        data = block.render()

        if data["type"] == "heading":
            await bot.send_message(chat_id, f"<b>{data['text']}</b>", parse_mode="HTML")

        elif data["type"] == "text":
            await bot.send_message(chat_id, data["text"])

        elif data["type"] == "photo":
            msg = await bot.send_photo(
                chat_id,
                photo=data["file"],        # telegram_file_id, S3 URL, or local path
                caption=data.get("caption"),
            )
            # Cache file_id after first upload — all future renders return it
            if block.media and not block.media.telegram_file_id:
                await sync_to_async(block.media.cache_file_id)(msg.photo[-1].file_id)

        elif data["type"] == "video":
            msg = await bot.send_video(chat_id, video=data["file"], caption=data.get("caption"))
            if block.media and not block.media.telegram_file_id:
                await sync_to_async(block.media.cache_file_id)(msg.video.file_id)

Once cache_file_id() is called, block.media.source returns the cached telegram_file_id for every subsequent post that references the same asset.


Testing — management command

# Token is read from settings.TGCMS["BOT_TOKEN"] automatically
python manage.py send_post <post_id> <chat_id>

# Or pass it explicitly
python manage.py send_post 1 @mychannel --token 123456:ABC...

# Or via env var
TELEGRAM_BOT_TOKEN=123456:ABC... python manage.py send_post 1 123456789

Token lookup order: --tokensettings.TGCMS["BOT_TOKEN"]TELEGRAM_BOT_TOKEN env var.


Models

MediaAsset
  file              FileField — upload from disk
  file_url          URLField  — S3 / CDN link
  telegram_file_id  Cached after first send (read-only in admin)
  .source           Property: returns the best available file reference
  .cache_file_id()  Persists telegram_file_id; call once after the first send

Post
  title, status     draft / published
  .render()         Returns {"blocks": [...]}
  .mark_published() Sets status and published_at

Block               FK → Post, FK → MediaAsset (nullable)
  type              heading / text / photo / video
  order             Managed by drag-and-drop in admin
  text, entities    heading and text blocks
  media             FK → MediaAsset, photo and video blocks
  caption, caption_entities
  .render()         Returns one block in Bot API format

UTF-16 offsets

MessageEntity.offset and length are counted in UTF-16 code units, not Python characters. Non-BMP characters (e.g. 😀 U+1F600) occupy 2 units, not 1. All offset arithmetic in tgcms.formatting goes through utf16_len().


License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_tgcms-0.2.0.tar.gz (20.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_tgcms-0.2.0-py3-none-any.whl (25.8 kB view details)

Uploaded Python 3

File details

Details for the file django_tgcms-0.2.0.tar.gz.

File metadata

  • Download URL: django_tgcms-0.2.0.tar.gz
  • Upload date:
  • Size: 20.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for django_tgcms-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c30f8aae495062495250e541d27dbb9cc74a187a9d01a8fe138b99a6c71fdfec
MD5 ef1f8e0eccb49587da0cbcba20f3a59e
BLAKE2b-256 135a3f34c3d7f6a2d9a8a6c7583c1ee0b2d0d02f350f78ed18d17fcce86a1577

See more details on using hashes here.

File details

Details for the file django_tgcms-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: django_tgcms-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 25.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for django_tgcms-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90f2958b31105d91ba6d65ad779c51aaefba21ab1760a5dceb1b805720e5d8ff
MD5 fdb6d639e5a4a57b5b9aaf0a18836dbe
BLAKE2b-256 cf2b6007c3146e89d1ed63564531bf50275cfa4bd3c9d6c2e8124ca7ad3fbbdd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page