From b1abc72c4b8fa8a295a2a709b29582d9e030d4cb9b8dfd97c39995d1b6cffb9e Mon Sep 17 00:00:00 2001 From: Vladislav Date: Tue, 30 Jun 2026 19:13:54 -0400 Subject: [PATCH] =?UTF-8?q?Bot=20commit:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20vk.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vk.py | 331 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 vk.py diff --git a/vk.py b/vk.py new file mode 100644 index 0000000..34a0c5f --- /dev/null +++ b/vk.py @@ -0,0 +1,331 @@ +# VK + +## Class + +import httpx +import asyncio +from db import * +from config import * +from auth import * +from token_manager import TokenManager +import logging +from PIL import Image +from io import BytesIO +from celery import Celery +from bson import ObjectId +from fastapi import status +from fastapi import APIRouter,Depends +from fastapi.responses import RedirectResponse +from images import insert_logo + +### Конфигурация + +celery_app_vk = Celery('vk ', broker=f'amqps://admin:{RABBITMQ_PASS}@{RABBITMQ_HOST}/vk', task_track_started=True ) + + +token_manager = TokenManager() + + +router = APIRouter(prefix="/vk", tags=["VK OAuth"]) +@router.get("/auth") +async def auth_redirect( + user=Depends(get_current_user) +): + url = await token_manager.get_auth_url(user_id=user['id']) + return RedirectResponse(url=url, status_code=status.HTTP_303_SEE_OTHER) + + +@router.get("/token") +async def oauth_callback(code: str, state: str, device_id: str = None): + token_data = await token_manager.get_token(code=code, state=state, device_id=device_id) + return RedirectResponse(url='/profile/social', status_code=status.HTTP_303_SEE_OTHER) + +async def get_images(db_name: str, file_ids: list, sign_avatar: bool = True): + images = {} + photo_bytes_list= [] + + if sign_avatar: + user = await users_collection.find_one({'_id': ObjectId(file_ids[0])},{'img':1}) + stream = await gridfs_map['users'].open_download_stream(ObjectId(user['img'])) + avatar_bytes = await stream.read() + logo = Image.open(BytesIO(avatar_bytes)).convert("RGBA") + file_ids = file_ids[1:] + + for _id in file_ids: + stream = await gridfs_map[db_name].open_download_stream(ObjectId(_id)) + photo_bytes_list.append((stream.name, BytesIO(await stream.read()), _id)) + + for name, background, _id in photo_bytes_list: + if sign_avatar: + background = insert_logo(Image.open(background).convert("RGBA"), logo) + images[_id] = ("photo", (name, background, "image/jpeg")) + + return images + +@router.post("/send") +async def send_vk(data:dict, request: Request): + auth = request.headers.get("Authorization") + if auth and auth.lower().startswith("bearer "): + parts = auth.split(" ", 1) + if len(parts) == 2: + if parts[1].strip() != ATOKEN: + return None + item = data['item'] + text = data['text'] + file_ids = data['file_ids'] + doc = data['doc'] + + sign_avatar = data['sign_avatar'] + db_name = item.get('db') + # if db_name == 'competitions': + # social = await social_collection.find_one({'service': True, 'type':'vk'}) + # else: + social = doc + logging.info(doc) + logging.info(social) + images = await get_images(db_name, file_ids, sign_avatar) + async with httpx.AsyncClient(http2=True, verify=ctx) as client: + attachments = [] + for _id, img in images.items(): + # doc_photo = await mongo_client['vk']['photos'].find_one({'id':f"{_id}:{social.get('_id')}"}) + # if doc_photo: + # attachments.append(doc_photo['attachment']) + # continue + payload = {"v": "5.199"} + if 'group' in social: + payload['group_id'] = social['id'] + resp = await client.post(f"{VK_API_URI}photos.getWallUploadServer", headers={"Authorization": f"Bearer {await token_manager.get_access_token(social['user_id'])}"}, data=payload, timeout=30.0) + resp.raise_for_status() + upload_url = resp.json()['response']['upload_url'] + resp = await client.post(upload_url, files=[img], timeout=60.0) + resp.raise_for_status() + payload.update(resp.json()) + payload.update({'caption':text}) + resp = await client.post(f"{VK_API_URI}photos.saveWallPhoto", headers={"Authorization": f"Bearer {await token_manager.get_access_token(social['user_id'])}"}, data=payload, timeout=30.0) + resp.raise_for_status() + attachment = ','.join([f'photo{i['owner_id']}_{i['id']}' for i in resp.json()['response']]) + if not attachment: + return {'ok': False} + # await mongo_client['vk']['photos'].insert_one({'attachment':attachment,'id':f"{_id}:{social.get('_id')}"}) + attachments.append(attachment) + logging.info(attachments) + + resp = await client.post(f"{VK_API_URI}wall.post", data={ + "access_token": await token_manager.get_access_token(doc['user_id']), + "owner_id": f'-{doc['id']}' if 'group' in doc else doc['id'], + 'message': text, + 'attachments': ','.join(attachments), + 'primary_attachments_mode': 'grid', + "v": "5.199", + }) + res = resp.json() + res.update({'doc': doc}) + if 'error' in res: + return {'ok': False} + update_spec = {"$push": {"vk": res}} + await mongo_client[db_name][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec) + return {"ok": True} + + +@router.post("/edit") +async def edit_vk(data:dict, request: Request): + auth = request.headers.get("Authorization") + if auth and auth.lower().startswith("bearer "): + parts = auth.split(" ", 1) + if len(parts) == 2: + if parts[1].strip() != ATOKEN: + return None + item = data['item'] + text = data['text'] + file_ids = data['file_ids'] + doc = data['doc'] + post_id = data['post_id'] + sign_avatar = data['sign_avatar'] +# async def edit_vk(item: dict, text: str, file_ids: list, doc: dict, sign_avatar: bool = True): + db_name = item.get('db') + if db_name == 'competitions': + social = await social_collection.find_one({'service': True, 'type':'vk'}) + else: + social = doc + logging.info(doc) + logging.info(social) + images = await get_images(db_name, file_ids, sign_avatar) + async with httpx.AsyncClient(http2=True, verify=ctx) as client: + attachments = [] + for _id, img in images.items(): + vk_photos_key = f"{item.get('_id')}:{_id}:{social.get('_id')}" + doc_photo = await mongo_client['vk']['photos'].find_one({'id':vk_photos_key}) + if doc_photo: + attachments.append(doc_photo['attachment']) + continue + payload = {"v": "5.199"} + if 'group' in social: + payload['group_id'] = social['id'] + resp = await client.post(f"{VK_API_URI}photos.getWallUploadServer", headers={"Authorization": f"Bearer {await token_manager.get_access_token(social['user_id'])}"}, data=payload, timeout=30.0) + resp.raise_for_status() + upload_url = resp.json()['response']['upload_url'] + resp = await client.post(upload_url, files=[img], timeout=60.0) + resp.raise_for_status() + payload.update(resp.json()) + payload.update({'caption':text}) + resp = await client.post(f"{VK_API_URI}photos.saveWallPhoto", headers={"Authorization": f"Bearer {await token_manager.get_access_token(social['user_id'])}"}, data=payload, timeout=30.0) + resp.raise_for_status() + attachment = ','.join([f'photo{i['owner_id']}_{i['id']}' for i in resp.json()['response']]) + if not attachment: + return {'ok': False} + await mongo_client['vk']['photos'].insert_one({'attachment':attachment,'id':vk_photos_key}) + attachments.append(attachment) + logging.info(attachments) + + resp = await client.post(f"{VK_API_URI}wall.edit", data={ + "access_token": await token_manager.get_access_token(doc['user_id']), + "owner_id": f'-{doc['id']}' if 'group' in doc else doc['id'], + "post_id": post_id, + 'message': text, + 'attachments': attachments, + 'primary_attachments_mode': 'grid', + "v": "5.199", + }) + res = resp.json() + res.update({'doc': doc}) + if 'error' in res: + return {'ok': False} + update_spec = {"$push": {"vk": res}} + await mongo_client[db_name][item.get('col')].update_one({'_id': ObjectId(item.get('_id'))}, update_spec) + return {"ok": True} + + +@router.post("/delete") +async def wall_delete(data: dict, request: Request): + # ---- auth ---- + auth = request.headers.get("Authorization") + if auth and auth.lower().startswith("bearer "): + token = auth.split(" ", 1)[1].strip() + if token != ATOKEN: + return {"ok": False, "error": "unauthorized"} + + item = data["item"] + post_id = data["post_id"] + doc = data["doc"] + + db_name = item.get("db") + if db_name == "competitions": + social = await social_collection.find_one({"service": True, "type": "vk"}) + else: + social = doc + + logging.info(doc) + logging.info(social) + + owner_id = f"-{doc['id']}" if "group" in doc else doc["id"] + + # ---- VK request ---- + async with httpx.AsyncClient(http2=True, verify=ctx) as client: + resp = await client.post(f"{VK_API_URI}wall.delete", data={ + "access_token": await token_manager.get_access_token(doc["user_id"]), + "owner_id": owner_id, + "post_id": post_id, + "v": "5.199", + }) + + res = resp.json() + logging.info(f"DELETE VK response: {res}") + res["doc"] = doc + + if "error" in res: + return {"ok": False, "error": res["error"]} + + # ---- Mongo remove vk post record ---- + col = item.get("col") + mongo_col = mongo_client[db_name][col] + + mongo_col.update_one( + {"_id": ObjectId(item.get("_id"))}, + {"$pull": {"vk": {"response": {"post_id": post_id}}}} + ) + + return {"ok": True} + + +@celery_app_vk.task( + bind=True, + rate_limit='60/m', + name='vk.edit_vk', + retry_backoff=True, + retry_backoff_max=600, + retry_jitter=True, + max_retries=20, + soft_time_limit=60, + time_limit=100, +) +def edit(self, post_id: int, item: dict, text: str, file_ids: list, social: dict, sign_avatar: bool = True): + try: + logging.info(f"VK начало edit {item} {file_ids}") + res = httpx.post( + 'http://kultmost-api.kultmost/api/v1/social/vk/edit', + headers={"Authorization": f"bearer {ATOKEN}"}, + json={'post_id': post_id, 'item': item, 'text': text, 'file_ids': file_ids, 'doc': social, 'sign_avatar': sign_avatar}, + timeout=100 + ).json() + if res['ok']: + return res + raise RuntimeError(f"VK send failed: {res}") + + except Exception as exc: + raise self.retry(exc=exc) + + +@celery_app_vk.task( + bind=True, + rate_limit='60/m', + name='vk.wall_delete', + retry_backoff=True, + retry_backoff_max=600, + retry_jitter=True, + max_retries=20, + soft_time_limit=60, + time_limit=100, +) +def delete_wall(self, item: dict, post_id: int, doc: dict): + try: + logging.info(f"VK удаления публикации {item} {post_id}") + res = httpx.post( + 'http://kultmost-api.kultmost/api/v1/social/vk/delete', + headers={"Authorization": f"bearer {ATOKEN}"}, + json={'item': item, 'post_id': post_id, "doc": doc}, + timeout=100 + ).json() + if res.get('ok'): + return res + raise RuntimeError(f"VK send failed: {res}") + + except Exception as exc: + raise self.retry(exc=exc) + + +@celery_app_vk.task( + bind=True, + rate_limit='60/m', + name='vk.send_vk', + retry_backoff=True, + retry_backoff_max=600, + retry_jitter=True, + max_retries=20, + soft_time_limit=60, + time_limit=100, +) +def notify(self, item: dict, text: str, file_ids: list, social: dict, sign_avatar: bool = True): + try: + logging.info(f"VK начало публикации {item} {file_ids}") + res = httpx.post( + 'http://kultmost-api.kultmost/api/v1/social/vk/send', + headers={"Authorization": f"bearer {ATOKEN}"}, + json={'item': item, 'text': text, 'file_ids': file_ids, 'doc': social, 'sign_avatar': sign_avatar}, + timeout=100 + ).json() + if res.get('ok'): + return res + raise RuntimeError(f"VK send failed: {res}") + + except Exception as exc: + raise self.retry(exc=exc)