978 lines
41 KiB
Plaintext
978 lines
41 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "4563b7d7",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# ignore\n",
|
||
"import os\n",
|
||
"\n",
|
||
"def load_env(file_path):\n",
|
||
" \"\"\"Загружает переменные окружения из файла .env с поддержкой булевых значений.\"\"\"\n",
|
||
" with open(file_path) as f:\n",
|
||
" for line in f:\n",
|
||
" line = line.strip()\n",
|
||
" if not line or line.startswith('#'):\n",
|
||
" continue\n",
|
||
" \n",
|
||
" try:\n",
|
||
" key, value = line.split('=', 1)\n",
|
||
" key = key.strip()\n",
|
||
" value = value.strip('\"\\' ')\n",
|
||
" # Преобразование строковых значений в булевые\n",
|
||
" if value.lower() in ['true', '1', 'yes']:\n",
|
||
" value = True\n",
|
||
" elif value.lower() in ['false', '0', 'no']:\n",
|
||
" value = False\n",
|
||
" \n",
|
||
" # Проверка, что ключ не пустой\n",
|
||
" if key:\n",
|
||
" os.environ[key] = str(value) # Храним как строку\n",
|
||
" except ValueError:\n",
|
||
" print(f\"Ошибка: неверный формат в строке '{line}'\")\n",
|
||
"\n",
|
||
"try:\n",
|
||
" load_env('env')\n",
|
||
"except:\n",
|
||
" load_env('env_example')"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b66cc5eb",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from config import *\n",
|
||
"from db import *\n",
|
||
"from typing import Dict, Any, List, Optional, Callable, Union\n",
|
||
"import email_task as email_task\n",
|
||
"import vk as vk\n",
|
||
"import tg as tg\n",
|
||
"from utils import *\n",
|
||
"from models import NotifyItem\n",
|
||
"from auth import token_create\n",
|
||
"import asyncio\n"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "019d30b5",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def notify_in_profile(item: NotifyItem):\n",
|
||
" return await notify_collection.update_one(\n",
|
||
" {\n",
|
||
" \"user_id\": item.user_id,\n",
|
||
" \"type\": item.type_item,\n",
|
||
" \"title\": item.title,\n",
|
||
" \"link\": item.link,\n",
|
||
" \"text\": item.text,\n",
|
||
" },\n",
|
||
" {\n",
|
||
" \"$set\": {\n",
|
||
" \"user_id\": item.user_id,\n",
|
||
" \"type\": item.type_item,\n",
|
||
" \"title\": item.title,\n",
|
||
" \"text\": item.text,\n",
|
||
" \"link\": item.link,\n",
|
||
" \"read\": True,\n",
|
||
" \"prod\": PROD,\n",
|
||
" }\n",
|
||
" },\n",
|
||
" upsert=True,\n",
|
||
" )\n"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "f26f18e9",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def email_new_post(new_post: Dict):\n",
|
||
" async for user in users_collection.find({'roles':'partner'}):\n",
|
||
" login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/paintings/{new_post['_id']}')}'\n",
|
||
" if PROD:\n",
|
||
" email_task.notify.delay(user['email'], {\n",
|
||
" 'text':'На сервисе опубликована новая картина',\n",
|
||
" 'list':[[\n",
|
||
" {'label':'Название','text':new_post['title'], 'img':f\"{FULL_DOMAIN}/b/posts/{new_post['medias_original'][0]}\"},\n",
|
||
" {'label':'Звание художника','text':new_post['grade']},\n",
|
||
" {'label':'Город','text':new_post['city']},\n",
|
||
" {'label':'Размер','text':new_post['size']},\n",
|
||
" {'label':'Материал','text':new_post['material']},\n",
|
||
" {'label':'Жанр','text':new_post['genre']},\n",
|
||
" {'label':'Цена','text':new_post['price']},\n",
|
||
" ]],\n",
|
||
" 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n",
|
||
" 'title': f\"от художника {new_post['author']}\",\n",
|
||
" 'login_link': login_link,\n",
|
||
" },\n",
|
||
" 'Новая картина', 'main.html')\n"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "cbff5107",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def update_social_post(post):\n",
|
||
" pass"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "e09b70e2",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def publish_social_post(post):\n",
|
||
" query = {\n",
|
||
" 'active': True,\n",
|
||
" \"$or\": [{'service': True}, {'user_id': post['user']['_id']}]\n",
|
||
" }\n",
|
||
" post_ref = {'_id': post['_id'], 'db': 'posts', 'col': 'posts'}\n",
|
||
" medias = post.get(\"medias_original\", [])\n",
|
||
"\n",
|
||
" platforms = [\n",
|
||
" ('tg', tg, lambda s: [\n",
|
||
" f\"{FULL_PATH_DOMAIN}/a/{post['user']['_id']}\" if should_sign_avatar(post, s) else None,\n",
|
||
" *[f\"{FULL_PATH_DOMAIN}/b/posts/{m}\" for m in medias]\n",
|
||
" ]),\n",
|
||
" ('vk', vk, lambda s: [\n",
|
||
" post['user']['_id'] if should_sign_avatar(post, s) else None,\n",
|
||
" *medias\n",
|
||
" ])\n",
|
||
" ]\n",
|
||
"\n",
|
||
" for platform_type, client, get_attachments in platforms:\n",
|
||
" if post.get(platform_type) != []:\n",
|
||
" continue\n",
|
||
" socials = [doc async for doc in social_collection.find({**query, 'type': platform_type})]\n",
|
||
" socials = {item[\"id\"]: item for item in socials}.values()\n",
|
||
" for social in socials:\n",
|
||
" text = parse_posts_text(post, platform_type, social.get('service', False))\n",
|
||
" sign_avatar = should_sign_avatar(post, social)\n",
|
||
" attachments = [a for a in get_attachments(social) if a is not None]\n",
|
||
" \n",
|
||
" client.notify.delay(post_ref, text, attachments, bson_clear(social), sign_avatar)"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "846846ca",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def user_social_competition(competition):\n",
|
||
" async for user in users_collection.find({'roles':'admin'}):\n",
|
||
" # email_task.notify.delay\n",
|
||
" if PROD:\n",
|
||
" pass\n",
|
||
" else:\n",
|
||
" print(competition)"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "829dfa3d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class SocialContentHandler:\n",
|
||
" \"\"\"Base class for content type specific handlers\"\"\"\n",
|
||
" \n",
|
||
" def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n",
|
||
" \"\"\"Return reference for the content item\"\"\"\n",
|
||
" raise NotImplementedError\n",
|
||
" \n",
|
||
" def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n",
|
||
" \"\"\"Return list of media file_ids\"\"\"\n",
|
||
" raise NotImplementedError\n",
|
||
" \n",
|
||
" def get_text(self, item: Dict[str, Any]) -> str:\n",
|
||
" \"\"\"Parse text for the content\"\"\"\n",
|
||
" raise NotImplementedError\n",
|
||
"\n",
|
||
"\n",
|
||
"class CompetitionHandler(SocialContentHandler):\n",
|
||
" def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n",
|
||
" return {'_id': item['_id'], 'db': 'competitions', 'col': 'competitions'}\n",
|
||
" \n",
|
||
" def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n",
|
||
" return [item.get(\"poster_file_id\")]\n",
|
||
" \n",
|
||
" def get_text(self, item: Dict[str, Any]) -> str:\n",
|
||
" return parse_text_competition(item)\n",
|
||
"\n",
|
||
"\n",
|
||
"class PostHandler(SocialContentHandler):\n",
|
||
" def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n",
|
||
" return {'_id': item['_id'], 'db': 'posts', 'col': 'posts'}\n",
|
||
" \n",
|
||
" def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n",
|
||
" # Adjust according to your Post schema\n",
|
||
" return item.get(\"media_file_ids\", []) or [item.get(\"main_image\")]\n",
|
||
" \n",
|
||
" def get_text(self, item: Dict[str, Any]) -> str:\n",
|
||
" return item.get(\"text\", \"\")\n",
|
||
"\n",
|
||
"\n",
|
||
"class NewsHandler(SocialContentHandler):\n",
|
||
" def get_post_ref(self, item: Dict[str, Any]) -> Dict[str, Any]:\n",
|
||
" return {'_id': item['_id'], 'db': 'news', 'col': 'news'}\n",
|
||
" \n",
|
||
" def get_medias(self, item: Dict[str, Any]) -> List[Optional[str]]:\n",
|
||
" return item.get(\"media_ids\", [])\n",
|
||
" \n",
|
||
" def get_text(self, item: Dict[str, Any]) -> str:\n",
|
||
" return item.get(\"title\", \"\") + \"\\n\\n\" + item.get(\"content\", \"\")\n",
|
||
"\n",
|
||
"\n",
|
||
"# Registry for handlers\n",
|
||
"CONTENT_HANDLERS: Dict[str, SocialContentHandler] = {\n",
|
||
" \"competition\": CompetitionHandler(),\n",
|
||
" \"post\": PostHandler(),\n",
|
||
" \"news\": NewsHandler(),\n",
|
||
" # Add more: \"user\": UserProfileHandler(), etc.\n",
|
||
"}\n",
|
||
"\n",
|
||
"\n",
|
||
"class SocialPlatform:\n",
|
||
" \"\"\"Platform configuration\"\"\"\n",
|
||
" def __init__(\n",
|
||
" self,\n",
|
||
" type_name: str,\n",
|
||
" client: Any,\n",
|
||
" get_attachments: Callable[[Dict], List[str]],\n",
|
||
" sign_avatar_func: Optional[Callable] = None\n",
|
||
" ):\n",
|
||
" self.type = type_name\n",
|
||
" self.client = client\n",
|
||
" self.get_attachments = get_attachments\n",
|
||
" self.sign_avatar_func = sign_avatar_func or should_sign_avatar\n",
|
||
"\n",
|
||
"\n",
|
||
"# Platform registry - easy to extend\n",
|
||
"PLATFORMS: Dict[str, SocialPlatform] = {\n",
|
||
" \"tg\": SocialPlatform(\n",
|
||
" type_name=\"tg\",\n",
|
||
" client=tg,\n",
|
||
" get_attachments=lambda social, medias: [\n",
|
||
" f\"{FULL_PATH_DOMAIN}/a/{social.get('user_id')}\" if should_sign_avatar(social) else None,\n",
|
||
" *[f\"{FULL_PATH_DOMAIN}/b/competition/{m}\" for m in medias if m] # generalize path\n",
|
||
" ]\n",
|
||
" ),\n",
|
||
" \"vk\": SocialPlatform(\n",
|
||
" type_name=\"vk\",\n",
|
||
" client=vk,\n",
|
||
" get_attachments=lambda social, medias: [\n",
|
||
" social.get('user_id') if should_sign_avatar(social) else None,\n",
|
||
" *medias\n",
|
||
" ]\n",
|
||
" ),\n",
|
||
" # Add more platforms easily:\n",
|
||
" # \"ok\": SocialPlatform(...),\n",
|
||
" # \"telegram_channel\": ...\n",
|
||
"}\n",
|
||
"\n",
|
||
"\n",
|
||
"async def get_socials_for_content(\n",
|
||
" content_type: str,\n",
|
||
" item: Dict[str, Any],\n",
|
||
" service_social: bool = False,\n",
|
||
" user_social: bool = False,\n",
|
||
" users_social: bool = False\n",
|
||
") -> List[Dict]:\n",
|
||
" \"\"\"Get relevant social accounts based on rules\"\"\"\n",
|
||
" query: Dict = {'active': True, \"$or\": []}\n",
|
||
" \n",
|
||
" if service_social:\n",
|
||
" query[\"$or\"].append({'service': True})\n",
|
||
" if user_social:\n",
|
||
" query[\"$or\"].append({'user_id': item.get('user', {}).get('_id')})\n",
|
||
" if users_social:\n",
|
||
" query[\"$or\"].extend([\n",
|
||
" {\"competition\": True},\n",
|
||
" {\"competition\": {\"$exists\": False}}\n",
|
||
" ])\n",
|
||
" \n",
|
||
" # Filter by platform types that exist in registry\n",
|
||
" platform_types = list(PLATFORMS.keys())\n",
|
||
" cursor = social_collection.find({**query, 'type': {'$in': platform_types}})\n",
|
||
" socials = [doc async for doc in cursor]\n",
|
||
" \n",
|
||
" # Deduplicate by id\n",
|
||
" return list({item[\"id\"]: item for item in socials}.values())\n",
|
||
"\n",
|
||
"\n",
|
||
"async def send_to_social(\n",
|
||
" content_type: str,\n",
|
||
" item: Dict[str, Any],\n",
|
||
" social: Dict[str, Any],\n",
|
||
" platform: SocialPlatform\n",
|
||
") -> bool:\n",
|
||
" \"\"\"Send one item to one social account with spam protection\"\"\"\n",
|
||
" handler = CONTENT_HANDLERS.get(content_type)\n",
|
||
" if not handler:\n",
|
||
" print(f\"Unknown content type: {content_type}\")\n",
|
||
" return False\n",
|
||
"\n",
|
||
" redis_key = f\"nospamsends:{content_type}:{item['_id']}:{social['_id']}\"\n",
|
||
" \n",
|
||
" already_sent = await redis_client.set(\n",
|
||
" redis_key,\n",
|
||
" \"1\",\n",
|
||
" ex=8 * 60 * 60, # 8 hours\n",
|
||
" nx=True\n",
|
||
" )\n",
|
||
" \n",
|
||
" if not already_sent:\n",
|
||
" return False # already sent recently\n",
|
||
"\n",
|
||
" post_ref = handler.get_post_ref(item)\n",
|
||
" medias = handler.get_medias(item)\n",
|
||
" text = handler.get_text(item)\n",
|
||
" \n",
|
||
" sign_avatar = should_sign_avatar(item, social) if hasattr(handler, 'should_sign') else should_sign_avatar(item, social)\n",
|
||
" \n",
|
||
" attachments = [a for a in platform.get_attachments(social, medias) if a is not None]\n",
|
||
" \n",
|
||
" try:\n",
|
||
" platform.client.notify.delay(\n",
|
||
" post_ref,\n",
|
||
" text,\n",
|
||
" attachments,\n",
|
||
" bson_clear(social),\n",
|
||
" sign_avatar\n",
|
||
" )\n",
|
||
" print(f\"Queued {content_type} {item['_id']} to {platform.type} social {social['_id']}\")\n",
|
||
" return True\n",
|
||
" except Exception as e:\n",
|
||
" print(f\"Failed to queue to {platform.type}: {e}\")\n",
|
||
" return False\n",
|
||
"\n",
|
||
"\n",
|
||
"async def service_social_publish(\n",
|
||
" content_type: str,\n",
|
||
" item: Dict[str, Any],\n",
|
||
" service_social: bool = False,\n",
|
||
" user_social: bool = False,\n",
|
||
" users_social: bool = False\n",
|
||
"):\n",
|
||
" \"\"\"\n",
|
||
" Generic social publishing service.\n",
|
||
" \n",
|
||
" Usage:\n",
|
||
" await service_social_publish(\"competition\", competition_dict)\n",
|
||
" await service_social_publish(\"post\", post_dict)\n",
|
||
" \"\"\"\n",
|
||
" if content_type not in CONTENT_HANDLERS:\n",
|
||
" raise ValueError(f\"Unsupported content type: {content_type}\")\n",
|
||
" \n",
|
||
" socials = await get_socials_for_content(\n",
|
||
" content_type, item, service_social, user_social, users_social\n",
|
||
" )\n",
|
||
" \n",
|
||
" tasks = []\n",
|
||
" for social in socials:\n",
|
||
" platform_key = social.get(\"type\")\n",
|
||
" platform = PLATFORMS.get(platform_key)\n",
|
||
" if not platform:\n",
|
||
" continue\n",
|
||
" \n",
|
||
" tasks.append(send_to_social(content_type, item, social, platform))\n",
|
||
" \n",
|
||
" if tasks:\n",
|
||
" await asyncio.gather(*tasks, return_exceptions=True)\n",
|
||
"\n",
|
||
"\n",
|
||
"# # Convenience wrappers\n",
|
||
"# async def service_social_competition(competition: Dict, **kwargs):\n",
|
||
"# return await service_social_publish(\"competition\", competition, **kwargs)\n",
|
||
"\n",
|
||
"\n",
|
||
"# async def service_social_post(post: Dict, **kwargs):\n",
|
||
"# return await service_social_publish(\"post\", post, **kwargs)\n",
|
||
"\n",
|
||
"\n",
|
||
"# async def service_social_news(news: Dict, **kwargs):\n",
|
||
"# return await service_social_publish(\"news\", news, **kwargs)"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "6b68a608",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def service_social_exhibition(exhibition):\n",
|
||
" query = { 'active': True, \"$or\": []}\n",
|
||
" query[\"$or\"].append({'service': True})\n",
|
||
" query[\"$or\"].append({'user_id': exhibition['user']['_id']})\n",
|
||
"\n",
|
||
" post_ref = {'_id': str(exhibition['_id']), 'db': 'exhibition', 'col': 'exhibition'}\n",
|
||
" medias = [exhibition.get(\"poster_orig_id\")]\n",
|
||
"\n",
|
||
" platforms = [\n",
|
||
" ('tg', tg, lambda s: [\n",
|
||
" # f\"{FULL_PATH_DOMAIN}/a/{exhibition['user']['_id']}\" if should_sign_avatar(exhibition, s) else None,\n",
|
||
" *[f\"{FULL_PATH_DOMAIN}/b/exhibition/{m}\" for m in medias]\n",
|
||
" ]),\n",
|
||
" ('vk', vk, lambda s: [\n",
|
||
" # exhibition['user']['_id'] if should_sign_avatar(exhibition, s) else None,\n",
|
||
" *medias\n",
|
||
" ])\n",
|
||
" ]\n",
|
||
"\n",
|
||
" for platform_type, client, get_attachments in platforms:\n",
|
||
" if exhibition.get(platform_type) != []:\n",
|
||
" continue\n",
|
||
" socials = [doc async for doc in social_collection.find({**query, 'type': platform_type})]\n",
|
||
" socials = {item[\"id\"]: item for item in socials}.values()\n",
|
||
" for social in socials:\n",
|
||
" text = parse_text_exhibition(exhibition)\n",
|
||
" sign_avatar = should_sign_avatar(exhibition, social)\n",
|
||
" attachments = [a for a in get_attachments(social) if a is not None]\n",
|
||
" redis_key = f\"nospamsends:{platform_type}:{exhibition['_id']}:{social['_id']}\"\n",
|
||
" already_sent = await redis_client.set(\n",
|
||
" redis_key, \n",
|
||
" 1, \n",
|
||
" ex=8*60*60,\n",
|
||
" nx=True\n",
|
||
" )\n",
|
||
" print(f\"nospamsends:{platform_type}:{exhibition['_id']}:{social['_id']}\")\n",
|
||
" if already_sent:\n",
|
||
" print(social)\n",
|
||
" client.notify.delay(post_ref, text, attachments, bson_clear(social), False)"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "e19b21ec",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def service_social_competition(competition, serviceSocial, userSocial, usersSocial):\n",
|
||
" query = { 'active': True, \"$or\": []}\n",
|
||
" if serviceSocial:\n",
|
||
" query[\"$or\"].append({'service': True})\n",
|
||
"\n",
|
||
" if userSocial:\n",
|
||
" query[\"$or\"].append({'user_id': competition['user']['_id']})\n",
|
||
"\n",
|
||
" if usersSocial:\n",
|
||
" query[\"$or\"].append({\"competition\": True})\n",
|
||
" query[\"$or\"].append({\"competition\": {\"$exists\": False}})\n",
|
||
" post_ref = {'_id': str(competition['_id']), 'db': 'competitions', 'col': 'competitions'}\n",
|
||
" medias = [competition.get(\"poster_file_id\")]\n",
|
||
"\n",
|
||
" platforms = [\n",
|
||
" ('tg', tg, lambda s: [\n",
|
||
" # f\"{FULL_PATH_DOMAIN}/a/{competition['user']['_id']}\" if should_sign_avatar(competition, s) else None,\n",
|
||
" *[f\"{FULL_PATH_DOMAIN}/b/competition/{m}\" for m in medias]\n",
|
||
" ]),\n",
|
||
" ('vk', vk, lambda s: [\n",
|
||
" # competition['user']['_id'] if should_sign_avatar(competition, s) else None,\n",
|
||
" *medias\n",
|
||
" ])\n",
|
||
" ]\n",
|
||
"\n",
|
||
" for platform_type, client, get_attachments in platforms:\n",
|
||
" \n",
|
||
" socials = [doc async for doc in social_collection.find({**query, 'type': platform_type})]\n",
|
||
" socials = {item[\"id\"]: item for item in socials}.values()\n",
|
||
" for social in socials:\n",
|
||
" text = parse_text_competition(competition)\n",
|
||
" sign_avatar = should_sign_avatar(competition, social)\n",
|
||
" attachments = [a for a in get_attachments(social) if a is not None]\n",
|
||
" redis_key = f\"nospamsends:{platform_type}:{competition['_id']}:{social['_id']}\"\n",
|
||
" already_sent = await redis_client.set(\n",
|
||
" redis_key, \n",
|
||
" 1, \n",
|
||
" ex=8*60*60,\n",
|
||
" nx=True\n",
|
||
" )\n",
|
||
" print(f\"nospamsends:{platform_type}:{competition['_id']}:{social['_id']}\")\n",
|
||
" if already_sent:\n",
|
||
" print(social)\n",
|
||
" client.notify.delay(post_ref, text, attachments, bson_clear(social), False)"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "031c0d4a",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def email_new_user(new_user):\n",
|
||
" async for user in users_collection.find({'roles':'admin'}):\n",
|
||
" login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/artists/{new_user['_id']}')}'\n",
|
||
" role = {'user':'Художник','galerist':'Галерея',\"reporter\":'Репортер'}[new_user['roles'][0]]\n",
|
||
" if PROD:\n",
|
||
" email_task.notify.delay(user['email'], {\n",
|
||
" 'list':[[\n",
|
||
" {'label':'ФИО','text': new_user['female_name_lastname'], 'img':f\"{FULL_DOMAIN}/a/{new_user['_id']}\"},\n",
|
||
" {'label':'Звание','text': new_user['grade']} if new_user['roles'][0] != 'galerist' else {},\n",
|
||
" {'label':'Город проживания','text': new_user['living_place']}\n",
|
||
" ]],\n",
|
||
" 'text': f'',\n",
|
||
" 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n",
|
||
" 'title': f\"{role} {new_user['username']}\",\n",
|
||
" 'login_link': login_link\n",
|
||
" },\n",
|
||
" 'Новый пользователь', 'main.html')"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "1d03d7e1",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def invite_users_to_competitions(doc):\n",
|
||
" new_notify = []\n",
|
||
"\n",
|
||
" query = {\n",
|
||
" \"roles\": {\"$in\": [\"user\"]},\n",
|
||
" \"$or\": [\n",
|
||
" {\"email_subscribed\": True},\n",
|
||
" {\"email_subscribed\": {\"$exists\": False}}\n",
|
||
" ]\n",
|
||
" }\n",
|
||
"\n",
|
||
" async for user in users_collection.find(query):\n",
|
||
" if not user.get(\"email\"):\n",
|
||
" continue\n",
|
||
"\n",
|
||
" redis_key = f\"nospam:{user['email']}:{doc['_id']}\"\n",
|
||
"\n",
|
||
" already_sent = await redis_client.set(\n",
|
||
" redis_key, \n",
|
||
" 1, \n",
|
||
" ex=24*60*60,\n",
|
||
" nx=True\n",
|
||
" )\n",
|
||
"\n",
|
||
" if already_sent:\n",
|
||
" new_notify.append(\n",
|
||
" notify_in_profile(NotifyItem(\n",
|
||
" user_id=str(user['_id']),\n",
|
||
" type_item='competition',\n",
|
||
" title='Приглашение на конкурс',\n",
|
||
" link=f'/profile/competition/{doc.get(\"id\") or doc.get(\"_id\")}',\n",
|
||
" text=f'«{doc['title']}» Организатор {doc['user']['username']}'\n",
|
||
" ))\n",
|
||
" )\n",
|
||
" if PROD:\n",
|
||
" email_task.notify.delay(user['email'], {'competition': bson_clear(doc), 'login_link': f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/competition/{doc.get(\"id\") or doc.get(\"_id\")}')}'}, 'Конкурс', 'competition.html')\n",
|
||
" await asyncio.gather(*new_notify)"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "3e22dab5",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def accept_competition_req(competition, user_id):\n",
|
||
" user = await users_collection.find_one({'_id': ObjectId(user_id)},{'email':1})\n",
|
||
" login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=f'/profile/invitation')}'\n",
|
||
" if PROD:\n",
|
||
" email_task.notify.delay(user['email'], {\n",
|
||
" 'text':f'на конкурсе {competition['title']}',\n",
|
||
" 'list':[[ ]],\n",
|
||
" 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n",
|
||
" 'title': f\"Вы прошли отбор\",\n",
|
||
" 'login_link': login_link,\n",
|
||
" },\n",
|
||
" 'Приглашение', 'main.html')\n",
|
||
" await notify_in_profile(NotifyItem(user_id=user_id, type_item='invation', title='Приглашение', link='/profile/invitation', text=f'Вы прошли отбор на конкурсе {competition['title']}'))\n"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "b25adab4",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def get_sender(post_id):\n",
|
||
" delivery_cursor = delivery_collection.find(\n",
|
||
" {\n",
|
||
" 'post_id': post_id,\"prod\": PROD,\n",
|
||
" 'status': { '$nin': [\"Ожидает отправки\", \"Ожидает принятия условий\", \"Третье\"] }\n",
|
||
" },\n",
|
||
" {'receiver': 1}\n",
|
||
" ).sort('_id', 1) # 1 = ascending, -1 = descending (newest first)\n",
|
||
"\n",
|
||
" delivery = await delivery_cursor.limit(1).to_list(length=1)\n",
|
||
" delivery = delivery[0] if delivery else {}\n",
|
||
" return delivery.get('receiver')"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "3c8e1473",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from typing import List\n",
|
||
"from bson import ObjectId\n",
|
||
"\n",
|
||
"async def accept_invitations(invitations_id: str, posts_ids: List[str]):\n",
|
||
" invitation = await invitations_collection.find_one_and_update(\n",
|
||
" {\"_id\": ObjectId(invitations_id)},\n",
|
||
" {\"$set\": {\"accept\": True}},\n",
|
||
" return_document=True,\n",
|
||
" )\n",
|
||
"\n",
|
||
" invitation_posts = invitation.get('posts', []) or []\n",
|
||
" invitation_post_ids = [str(doc['_id']) for doc in invitation_posts]\n",
|
||
"\n",
|
||
" selected_set = set(str(x) for x in posts_ids)\n",
|
||
" selected_ids = [pid for pid in invitation_post_ids if pid in selected_set]\n",
|
||
" other_ids = [pid for pid in invitation_post_ids if pid not in selected_set]\n",
|
||
"\n",
|
||
" if selected_ids:\n",
|
||
" await delivery_collection.update_many(\n",
|
||
" {'post_id': {'$in': selected_ids}},\n",
|
||
" {'$set': {'status': 'Ожидает отправки'}}\n",
|
||
" )\n",
|
||
"\n",
|
||
" if other_ids:\n",
|
||
" await delivery_collection.update_many(\n",
|
||
" {'post_id': {'$in': other_ids}},\n",
|
||
" {'$set': {'status': 'Приглашение отклонено'}} # как вы написали\n",
|
||
" )\n",
|
||
"\n",
|
||
" await notify_in_profile(NotifyItem(\n",
|
||
" user_id=invitation.get('exhibition',{}).get(\"user\",{}).get('_id'),\n",
|
||
" type_item='c',\n",
|
||
" title=f'Выставка \"{invitation.get('exhibition',{}).get(\"title\")}\"',\n",
|
||
" link=f'/profile/delivery',\n",
|
||
" text=f'Художник принял условия'\n",
|
||
" ))"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "dc239a3e",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def exhibition_send_invitations(exhibition_id):\n",
|
||
" exhibition = await exhibition_collection.find_one({'_id': ObjectId(exhibition_id)})\n",
|
||
" exhibition_owner = exhibition.get('user')\n",
|
||
" obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]\n",
|
||
" posts = await posts_collection.find({\"_id\": {\"$in\": obj_ids}, 'status': 'В каталоге'}).to_list() if obj_ids else []\n",
|
||
" new_posts = {}\n",
|
||
" all_posts = {}\n",
|
||
" for post in posts:\n",
|
||
" post_id = str(post.get('_id'))\n",
|
||
" user = display_user(post.get('user'))\n",
|
||
" doc = await delivery_collection.update_one(\n",
|
||
" {\n",
|
||
" \"exhibition_id\": str(exhibition.get('_id')),\"post_id\": post_id, \"status\": \"Ожидает принятия условий\", \"type\": \"exhibition\",\n",
|
||
" },{\n",
|
||
" \"$set\":{\n",
|
||
" \"sender\": user,\n",
|
||
" \"user\": user,\n",
|
||
" \"receiver\": display_user(exhibition_owner),\n",
|
||
" \"exhibition_id\": str(exhibition.get('_id')),\n",
|
||
" \"address\": exhibition.get('address'),\n",
|
||
" \"post_id\": post_id, \n",
|
||
" \"prod\": PROD,\n",
|
||
" \"img\": post.get('medias',[0])[0],\n",
|
||
" \"title\": post.get('title'),\n",
|
||
" \"status\": \"Ожидает принятия условий\",\n",
|
||
" \"type\": \"exhibition\",\n",
|
||
" \"updated_at\": datetime.utcnow()\n",
|
||
" }\n",
|
||
" }, upsert=True)\n",
|
||
" if doc.upserted_id:\n",
|
||
" if (user_id:=user.get('_id')) in new_posts:\n",
|
||
" new_posts[user_id].append(post)\n",
|
||
" else:\n",
|
||
" new_posts.update({user_id: [post]})\n",
|
||
" if (user_id:=user.get('_id')) in all_posts:\n",
|
||
" all_posts[user_id].append(post)\n",
|
||
" else:\n",
|
||
" all_posts.update({user_id: [post]})\n",
|
||
" print(all_posts)\n",
|
||
" if all_posts:\n",
|
||
" for user_id, posts in all_posts.items():\n",
|
||
" doc = await invitations_collection.update_one(\n",
|
||
" {'exhibition_id': str(exhibition.get('_id')),'user_id': user_id,'type': 'Выставка'},\n",
|
||
" \n",
|
||
" {\"$set\":{'exhibition': bson_clear(exhibition), \n",
|
||
" 'exhibition_id': str(exhibition.get('_id')),\n",
|
||
" 'accept': False, \n",
|
||
" 'user':display_user(exhibition_owner), \n",
|
||
" 'user_id': user_id, \n",
|
||
" 'posts': posts,\n",
|
||
" 'prod': PROD, \n",
|
||
" 'type': 'Выставка', \n",
|
||
" 'item': exhibition.get('title')}},\n",
|
||
" upsert=True\n",
|
||
" )\n",
|
||
" if new_posts:\n",
|
||
" for user_id, posts in new_posts.items():\n",
|
||
" await notify_in_profile(NotifyItem(\n",
|
||
" user_id=user_id,\n",
|
||
" type_item='invitation',\n",
|
||
" title='Приглашение',\n",
|
||
" link=f'/profile/invitation',\n",
|
||
" text=f'Приглашаем на выставку \"{exhibition.get(\"title\")}\"'\n",
|
||
" ))\n",
|
||
" \n",
|
||
" user = posts[0].get('user')\n",
|
||
" token = await token_create(\n",
|
||
" str(user['_id']), \n",
|
||
" url=f'/profile/invitation#main'\n",
|
||
" )\n",
|
||
" login_link = f'{FULL_DOMAIN}/login/{token}'\n",
|
||
" email_data = {\n",
|
||
" 'text': 'Приглашаем на выставку со следующими картинами: ',\n",
|
||
" 'list': [\n",
|
||
" [\n",
|
||
" {'label': 'Название', 'text': item.get('title', ''), 'img':f\"{FULL_DOMAIN}/b/posts/{item['medias_original'][0]}\"},\n",
|
||
" ]\n",
|
||
" for item in posts\n",
|
||
" ],\n",
|
||
" 'primary': {\n",
|
||
" 'href': login_link,\n",
|
||
" 'text': 'Войти и посмотреть'\n",
|
||
" },\n",
|
||
" 'title': exhibition.get('title', 'Выставка'),\n",
|
||
" 'login_link': login_link,\n",
|
||
" }\n",
|
||
" if PROD:\n",
|
||
" email_task.notify.delay(\n",
|
||
" user['email'],\n",
|
||
" email_data,\n",
|
||
" f'Приглашение на выставку {exhibition.get('title')}',\n",
|
||
" 'main.html'\n",
|
||
" )\n",
|
||
" \n"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "5db0ae46",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def update_competition_req(competition, user, posts):\n",
|
||
" competition_id = competition.get('_id')\n",
|
||
"\n",
|
||
" if not user or not posts or not competition_id:\n",
|
||
" return\n",
|
||
"\n",
|
||
" posts_sorted = sorted(posts, key=lambda p: p.get('status') != 'Проходит')\n",
|
||
"\n",
|
||
" # Генерация login link\n",
|
||
" token = await token_create(\n",
|
||
" str(user['_id']), \n",
|
||
" url=f'/profile/invitation#main'\n",
|
||
" )\n",
|
||
" login_link = f'{FULL_DOMAIN}/login/{token}'\n",
|
||
"\n",
|
||
" if True:\n",
|
||
" competition_owner = competition.get('user', {})\n",
|
||
"\n",
|
||
" # === Создание доставок ===\n",
|
||
" \n",
|
||
" post_ids_to_mark = [] # для последующего обновления delivery: True\n",
|
||
" post_ids_to_remove_delivery = []\n",
|
||
" for post in posts:\n",
|
||
" post_id = str(post.get('_id'))\n",
|
||
" if post.get('status') == 'Проходит':\n",
|
||
" sender = await get_sender(post_id) or display_user(user)\n",
|
||
" delivery_collection.update_one({\n",
|
||
" \"competition_id\": competition_id,\"post_id\": post_id, \"status\": \"Ожидает отправки\",\n",
|
||
" \"sender._id\": sender.get('_id'), \"receiver._id\": competition_owner.get('_id')\n",
|
||
" },{\n",
|
||
" \"$set\":{\n",
|
||
" \"sender\": sender,\n",
|
||
" \"user\": display_user(user),\n",
|
||
" \"receiver\": display_user(competition_owner),\n",
|
||
" \"competition_id\": competition_id,\n",
|
||
" \"address\": competition.get('address'),\n",
|
||
" \"post_id\": post_id,\n",
|
||
" \"prod\": PROD,\n",
|
||
" \"img\": post.get('medias',[0])[0],\n",
|
||
" \"title\": post.get('title'),\n",
|
||
" \"status\": \"Ожидает отправки\",\n",
|
||
" \"type\": \"competition_reward\",\n",
|
||
" \"updated_at\": datetime.utcnow()\n",
|
||
" }\n",
|
||
" }, upsert=True)\n",
|
||
" post_ids_to_mark.append(post_id)\n",
|
||
" elif post.get('status') != 'Проходит':\n",
|
||
" post_ids_to_remove_delivery.append(post_id)\n",
|
||
"\n",
|
||
"\n",
|
||
" if post_ids_to_remove_delivery:\n",
|
||
" await delivery_collection.delete_many({\n",
|
||
" \"competition_id\": competition_id,\n",
|
||
" \"status\": \"Ожидает отправки\",\n",
|
||
" \"post_id\": {\"$in\": post_ids_to_remove_delivery}\n",
|
||
" })\n",
|
||
"\n",
|
||
" await notify_in_profile(NotifyItem(\n",
|
||
" user_id=str(user['_id']),\n",
|
||
" type_item='invitation',\n",
|
||
" title='Приглашение',\n",
|
||
" link=f'/profile/invitation',\n",
|
||
" text=f'Вы прошли отбор на конкурсе \"{competition.get(\"title\")}\"'\n",
|
||
" ))\n",
|
||
" all_allowed = all(\n",
|
||
" post.get('status') in ('Проходит', 'На рассмотрении')\n",
|
||
" for post in posts\n",
|
||
" )\n",
|
||
" has_approved = any(p.get('status') == 'Проходит' for p in posts)\n",
|
||
"\n",
|
||
" # Email-уведомление\n",
|
||
" if user.get('email') and all_allowed and has_approved:\n",
|
||
" await invitations_collection.update_one({\n",
|
||
" \"user_id\": str(user['_id']),\n",
|
||
" \"item\": competition.get(\"title\"),\n",
|
||
" \"title\": \"Вы прошли отбор\",\n",
|
||
" \"type_\": competition.get(\"type_\"),\n",
|
||
" \"user\": display_user(competition_owner),\n",
|
||
" \"prod\": PROD,\n",
|
||
" \"type\": \"Конкурс\"\n",
|
||
" },{'$set':{\n",
|
||
" \"user_id\": str(user['_id']),\n",
|
||
" \"item\": competition.get(\"title\"),\n",
|
||
" \"title\": \"Вы прошли отбор\",\n",
|
||
" \"competition\": competition,\n",
|
||
" \"type_\": competition.get(\"type_\"),\n",
|
||
" \"user\": display_user(competition_owner),\n",
|
||
" \"prod\": PROD,\n",
|
||
" \"type\": \"Конкурс\"\n",
|
||
" }},upsert=True)\n",
|
||
" if PROD and user.get('email') and all_allowed and has_approved:\n",
|
||
" email_data = {\n",
|
||
" 'text': 'Статус заявки обновлен',\n",
|
||
" 'list': [\n",
|
||
" [\n",
|
||
" {'label': 'Название', 'text': item.get('title', ''), 'img':f\"{FULL_DOMAIN}/b/posts/{item['medias_original'][0]}\"},\n",
|
||
" {'label': 'Статус', 'text': item.get('status', '')}\n",
|
||
" ]\n",
|
||
" for item in posts_sorted\n",
|
||
" ],\n",
|
||
" 'primary': {\n",
|
||
" 'href': login_link,\n",
|
||
" 'text': 'Войти и посмотреть'\n",
|
||
" },\n",
|
||
" 'title': competition.get('title', 'Конкурс'),\n",
|
||
" 'login_link': login_link,\n",
|
||
" }\n",
|
||
"\n",
|
||
" email_task.notify.delay(\n",
|
||
" user['email'],\n",
|
||
" email_data,\n",
|
||
" 'Обновление статуса заявки на конкурс',\n",
|
||
" 'main.html'\n",
|
||
" )"
|
||
],
|
||
"display_name": ".venv"
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "8ca4a900",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"async def new_competiton_req(request):\n",
|
||
" competition = await competitions_col.find_one({'_id':ObjectId(request['competition_id'])})\n",
|
||
" user = competition['user']\n",
|
||
" if competition.get('type_') == 'artist':\n",
|
||
" link = f'/profile/partner/competition/{request['competition_id']}/{request['user']['_id']}'\n",
|
||
" else:\n",
|
||
" link = f'/profile/partner/competition/{request['competition_id']}'\n",
|
||
" login_link = f'{FULL_DOMAIN}/login/{await token_create(str(user['_id']), url=link)}'\n",
|
||
" print(user)\n",
|
||
" await notify_in_profile(NotifyItem(\n",
|
||
" user_id=user['_id'],\n",
|
||
" type_item='competition_req',\n",
|
||
" title='Новая заявка на конкурс',\n",
|
||
" link=link,\n",
|
||
" text=f'«{competition['title']}» От {request['user']['username']}'\n",
|
||
" ))\n",
|
||
" if PROD:\n",
|
||
" email_task.notify.delay(user['email'], {\n",
|
||
" 'list':[[{'label':'Название','text':i['title'], 'img':f\"{FULL_DOMAIN}/b/posts/{i['medias'][0]}\"},{'label':'Размер','text':i['size']},{'label':'Описание','text':i['description']}] for i in request['posts']],\n",
|
||
" 'text': f'Картины на конкурс «{competition['title']}»:',\n",
|
||
" 'primary': {'href':login_link,'text':'Войти и посмотреть'},\n",
|
||
" 'title': f\"От {request['user']['username']}\",\n",
|
||
" 'login_link': login_link\n",
|
||
" },\n",
|
||
" 'Новая заявка на конкурс', 'main.html')"
|
||
],
|
||
"display_name": ".venv"
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": ".venv",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.13.5"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|