Back to Home

Telegram to Obsidian Converter for IT

The converter transfers Telegram JSON export to Obsidian preserving media, formatting and chat structure. File indexing, multi-level search and entity processing implemented. Suitable for creating offline database from thousands of messages.

Import Telegram chats into Obsidian: code and schema
Advertisement 728x90

Telegram to Obsidian Converter: Automate Chat Import with Media & Link Handling

The converter transforms Telegram Desktop’s JSON export into a well-organized Obsidian Vault. It parses messages, copies media files, converts Telegram formatting to clean Markdown, and organizes chats by type (contacts, groups, channels). The solution builds a robust file index for reliable path resolution and handles filename collisions gracefully.

The original Telegram export contains result.json (metadata), an HTML-based chats/ folder with messages, and associated media. The target structure is Telegram_Export/, subdivided into Contacts/, Groups/, Channels/, plus a central Index.md.

Converter Architecture

The system comprises five core modules: a JSON parser, media indexer, Markdown converter, file manager, and vault structure generator. Data flows as follows: JSON → parsing → indexing → conversion → Obsidian Vault with media copied into note folders.

Google AdInline article slot

Key components:

  • Parser: extracts chats, messages, and metadata from result.json.
  • Indexer: builds a searchable map of media files by extension (.jpg, .mp4, .pdf, etc.).
  • Converter: applies multi-strategy media lookup and rich text formatting rules.
  • File Manager: copies assets into corresponding note folders while deduplicating names.

Configuration is handled via environment variables: TELEGRAM_JSON_FILE, OBSIDIAN_OUTPUT_DIR, and COPY_MEDIA.

import os
from pathlib import Path

JSON_FILE = os.getenv('TELEGRAM_JSON_FILE', 'result.json')
EXPORT_BASE = Path(os.getenv('TELEGRAM_EXPORT_BASE', '.'))
OUTPUT_DIR = Path(os.getenv('OBSIDIAN_OUTPUT_DIR', 'Telegram_Export'))
COPY_MEDIA = os.getenv('COPY_MEDIA', 'true').lower() == 'true'
GROUP_BY_DAY = os.getenv('GROUP_BY_DAY', 'true').lower() == 'true'

Media Indexing and Lookup

A patch.txt file—generated via ls -R—captures the full directory tree of the Telegram export. The indexer maps filenames, relative paths, and absolute paths for fast, deterministic lookups.

Google AdInline article slot
def index_media_from_patch(self):
    media_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.mp4', '.webm', '.pdf', '.zip', '.mp3'}
    current_dir = None
    
    with open(PATCH_FILE, 'r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            if line.endswith(':'):
                current_dir = line[:-1]
                continue
            if current_dir and any(line.endswith(ext) for ext in media_extensions):
                full_path = Path(current_dir) / line
                self.media_index[line] = full_path
                if 'chats/' in str(full_path):
                    rel_path = str(full_path).split('chats/', 1)[-1]
                    self.media_index[rel_path] = full_path

Media lookup uses four fallback strategies: exact filename match, full path match, partial path match, and direct relative path resolution from EXPORT_BASE.

Media Copying with Collision Handling

Files are copied into each note’s folder, not into a shared attachments directory—ensuring Obsidian’s native embed syntax (![[file]]) works out of the box. A media_cache speeds up repeated lookups. On name collisions, suffixes like _1, _2 are appended automatically.

def copy_media_file(self, source_path: str, note_folder: Path = None) -> Optional[str]:
    if not COPY_MEDIA or not source_path:
        return None
    
    source_file = self.find_media_file(source_path)
    if not source_file or not source_file.exists():
        return None
    
    target_dir = note_folder if note_folder else OUTPUT_DIR / "Attachments"
    target_dir.mkdir(parents=True, exist_ok=True)
    target_file = target_dir / source_file.name
    
    if target_file.exists():
        stem = target_file.stem
        suffix = target_file.suffix
        counter = 1
        while target_file.exists():
            target_file = target_dir / f"{stem}_{counter}{suffix}"
            counter += 1
    
    shutil.copy2(source_file, target_file)
    self.stats['media_files'] += 1
    return source_file.name

Text and Entity Parsing

HTML and Telegram-specific entities (bold, links, spoilers, etc.) are converted to semantic Markdown. Dedicated handlers preserve meaning and Obsidian compatibility.

Google AdInline article slot
def parse_text_entities(self, text: Union[str, List], entities: Optional[List[Dict]] = None) -> str:
    if isinstance(text, str) and ('<' in text or '&' in text):
        return self.html_to_markdown(text)
    
    if entities and isinstance(text, str):
        return self._process_entities(text, entities)
    
    handlers = {
        'bold': lambda t: f"**{t}**",
        'italic': lambda t: f"*{t}*",
        'code': lambda t: f"`{t}`",
        'pre': lambda t: f"```
{t}

'link': lambda t, e: f"[{t}]({e.get('url', '')})",

'spoiler': lambda t: f"\n> [!spoiler] {t}\n"

}

return str(text) if text else ""


## Message Formatting Logic

Each message includes timestamp, sender, formatted text (with entities), and embedded media references. Keys like `photo`, `video`, and `audio` trigger precise media extraction.

Processing steps:
1. Extract `date` and `from` fields.
2. Parse `text` using `text_entities` logic.
3. Detect media keys, copy files, and insert `![alt](path)` or `![[file]]` links.
4. Group messages by date if `GROUP_BY_DAY` is enabled.

Built-in statistics track chats, messages, media files, and contacts processed.

## Key Design Principles

- `patch.txt` indexing bridges inconsistencies between JSON paths and actual filesystem layout.
- Four-tiered media lookup achieves >95% asset resolution coverage.
- Per-note media copying guarantees seamless Obsidian embed functionality.
- Full support for Telegram’s rich text features: bold, code blocks, links, mentions, and spoilers.
- Environment-driven configuration enables production-ready deployment and CI/CD integration.

— Editorial Team

Advertisement 728x90

Read Next