initial verstion

This commit is contained in:
2026-03-26 19:06:08 +04:00
commit a20e4719af
8 changed files with 238 additions and 0 deletions

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# Use a specific version for stability
FROM python:3.11-slim
# Install system dependencies for image processing
RUN apt-get update && apt-get install -y \
libheif-dev \
libde265-0 \
libjpeg62-turbo \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install python dependencies
# Using --no-cache-dir to keep the image small
RUN pip install --no-cache-dir \
aiogram \
pillow-heif \
Pillow \
prometheus_client \
aiohttp
# Copy your bot code
COPY bot.py .
# Standardize ports
EXPOSE 8000 8080
CMD ["python", "bot.py"]

115
bot.py Normal file
View File

@@ -0,0 +1,115 @@
import asyncio
import io
import logging
import os
from typing import Any, Dict, List, Union
import pillow_heif
from aiogram import BaseMiddleware, Bot, Dispatcher, F, types
from aiogram.types import BufferedInputFile, Message
from aiogram.utils.media_group import MediaGroupBuilder
from aiohttp import web
from PIL import Image
from prometheus_client import Counter, start_http_server
# --- CONFIG & METRICS ---
API_TOKEN = os.getenv("BOT_TOKEN")
METRICS_PORT = 8000
HEALTH_PORT = 8080
CONVERSION_COUNT = Counter("heic_conversions_total", "Total HEIC to JPEG conversions")
logging.basicConfig(level=logging.INFO)
pillow_heif.register_heif_opener()
if not API_TOKEN:
raise ValueError("No BOT_TOKEN found in environment variables")
# --- MIDDLEWARE FOR ALBUMS ---
class AlbumMiddleware(BaseMiddleware):
"""Groups multiple messages in a media group into a single list."""
def __init__(self, latency: float = 0.6):
self.latency = latency
self.album_data: Dict[str, List[Message]] = {}
async def __call__(self, handler, event: Message, data: Dict[str, Any]):
if not event.media_group_id:
return await handler(event, data)
if event.media_group_id not in self.album_data:
self.album_data[event.media_group_id] = [event]
await asyncio.sleep(self.latency)
data["album"] = self.album_data.pop(event.media_group_id)
return await handler(event, data)
self.album_data[event.media_group_id].append(event)
return
# --- HANDLERS ---
async def convert_image(bot: Bot, msg: Message) -> BufferedInputFile:
"""Helper to download and convert a single HEIC to JPEG."""
file_id = msg.document.file_id if msg.document else msg.photo[-1].file_id
file_name = msg.document.file_name if msg.document else f"{file_id}.heic"
file = await bot.get_file(file_id)
heic_buffer = await bot.download_file(file.file_path)
with Image.open(heic_buffer) as img:
jpeg_buffer = io.BytesIO()
img.convert("RGB").save(jpeg_buffer, format="JPEG", quality=95)
jpeg_buffer.seek(0)
new_name = file_name.rsplit(".", 1)[0] + ".jpg"
CONVERSION_COUNT.inc()
return BufferedInputFile(jpeg_buffer.read(), filename=new_name)
@dp.message(F.media_group_id)
async def handle_album(message: Message, album: List[Message], bot: Bot):
"""Handles multiple HEIC images sent as an album."""
media_group = MediaGroupBuilder(caption="Converted Images")
for msg in album:
if (
msg.document and msg.document.file_name.lower().endswith(".heic")
) or msg.photo:
conv_file = await convert_image(bot, msg)
media_group.add_document(media=conv_file)
await message.answer_media_group(media=media_group.build())
@dp.message(F.document.file_name.lower().endswith(".heic"))
async def handle_single_doc(message: Message, bot: Bot):
"""Handles a single HEIC file."""
jpeg_file = await convert_image(bot, message)
await message.reply_document(document=jpeg_file)
# --- K8S HEALTH CHECK ---
async def health_check(request):
return web.Response(text="OK", status=200)
# --- MAIN ---
async def main():
bot = Bot(token=API_TOKEN)
dp = Dispatcher()
dp.message.middleware(AlbumMiddleware())
# Start Prometheus
start_http_server(METRICS_PORT)
# Start Health Check Server
server = web.Application()
server.router.add_get("/healthz", health_check)
runner = web.AppRunner(server)
await runner.setup()
site = web.TCPSite(runner, "0.0.0.0", HEALTH_PORT)
await asyncio.gather(site.start(), dp.start_polling(bot))
if __name__ == "__main__":
asyncio.run(main())

14
compose.yaml Normal file
View File

@@ -0,0 +1,14 @@
services:
heic-bot:
image:
build: .
container_name: heic-converter
environment:
- BOT_TOKEN=${BOT_TOKEN}
ports:
- "8000:8000" # Prometheus Metrics
- "8080:8080" # Health Checks
volumes:
# Optional: mount a local folder if you want to log to file
- ./logs:/app/logs
restart: unless-stopped

51
k8s/base/bot.yaml Normal file
View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: heic-converter-bot
spec:
replicas: 1
selector:
matchLabels:
app: heic-bot
template:
metadata:
labels:
app: heic-bot
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
spec:
containers:
- name: bot
image: git.danilkolesnikov.ru/danilko09/heic-bot:latest
env:
- name: BOT_TOKEN
valueFrom:
configMapKeyRef:
name: heic-bot-config
key: BOT_TOKEN
ports:
- containerPort: 8000
name: metrics
- containerPort: 8080
name: health
# Critical for image processing pods
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10

View File

@@ -0,0 +1,2 @@
resources:
- deployment.yaml

View File

@@ -0,0 +1,3 @@
namespace: iphone_to_jpeg_bot_prod
resources:
- ../../base

View File

@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: heic-bot-config
data:
BOT_TOKEN: "YOUR_TELEGRAM_BOT_TOKEN_HERE"

19
skaffold.yaml Normal file
View File

@@ -0,0 +1,19 @@
apiVersion: skaffold/v4beta13
kind: Config
metadata:
name: iphone-to-jpeg-bot
build:
artifacts:
- image: git.danilkolesnikov.ru/danilko09/heic-bot
docker:
dockerfile: Dockerfile
manifests:
kustomize:
paths:
- k8s/base
profiles:
- name: prod
manifests:
kustomize:
paths:
- k8s/overlays/prod