initial verstion
This commit is contained in:
28
Dockerfile
Normal file
28
Dockerfile
Normal 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
115
bot.py
Normal 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
14
compose.yaml
Normal 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
51
k8s/base/bot.yaml
Normal 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
|
||||
2
k8s/base/kustomization.yaml
Normal file
2
k8s/base/kustomization.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- deployment.yaml
|
||||
3
k8s/overlays/prod/kustomization.yaml
Normal file
3
k8s/overlays/prod/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace: iphone_to_jpeg_bot_prod
|
||||
resources:
|
||||
- ../../base
|
||||
6
k8s/templates/bot_config.yaml
Normal file
6
k8s/templates/bot_config.yaml
Normal 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
19
skaffold.yaml
Normal 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
|
||||
Reference in New Issue
Block a user