fix + feat
This commit is contained in:
parent
b990cd91f0
commit
a607143392
70
build.ipynb
70
build.ipynb
@ -269,6 +269,55 @@
|
|||||||
],
|
],
|
||||||
"display_name": ".venv"
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "fef1918f",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"### tg build"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "4489de4b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!docker tag mongo:latest registry.spiritofweb.ru/mongo:latest"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "81571f0b",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!docker build -t $REGISTRY_HOST/api-tg:latest tg/."
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "3244466f",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"!docker push $REGISTRY_HOST/api-tg:latest"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"id": "37eabaef",
|
"id": "37eabaef",
|
||||||
@ -368,6 +417,24 @@
|
|||||||
"### containers"
|
"### containers"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f4b322e5",
|
||||||
|
"metadata": {
|
||||||
|
"vscode": {
|
||||||
|
"languageId": "shellscript"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"%%bash\n",
|
||||||
|
"docker pull redis:latest\n",
|
||||||
|
"docker tag redis:latest $REGISTRY_HOST/redis:latest\n",
|
||||||
|
"docker push $REGISTRY_HOST/redis:latest"
|
||||||
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
@ -383,7 +450,9 @@
|
|||||||
"docker pull busybox:1.36\n",
|
"docker pull busybox:1.36\n",
|
||||||
"docker pull rabbitmq:3.11-management\n",
|
"docker pull rabbitmq:3.11-management\n",
|
||||||
"docker pull alpine:3.14\n",
|
"docker pull alpine:3.14\n",
|
||||||
|
"docker pull redis:latest\n",
|
||||||
"docker pull mongo:latest\n",
|
"docker pull mongo:latest\n",
|
||||||
|
"docker tag redis:latest $REGISTRY_HOST/redis:latest\n",
|
||||||
"docker tag mongo:latest $REGISTRY_HOST/mongo:latest\n",
|
"docker tag mongo:latest $REGISTRY_HOST/mongo:latest\n",
|
||||||
"docker tag busybox:1.36 $REGISTRY_HOST/busybox:1.36\n",
|
"docker tag busybox:1.36 $REGISTRY_HOST/busybox:1.36\n",
|
||||||
"docker tag rabbitmq:3.11-management $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
"docker tag rabbitmq:3.11-management $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
||||||
@ -403,6 +472,7 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"%%bash\n",
|
"%%bash\n",
|
||||||
|
"docker push $REGISTRY_HOST/redis:latest\n",
|
||||||
"docker push $REGISTRY_HOST/mongo:latest\n",
|
"docker push $REGISTRY_HOST/mongo:latest\n",
|
||||||
"docker push $REGISTRY_HOST/busybox:1.36\n",
|
"docker push $REGISTRY_HOST/busybox:1.36\n",
|
||||||
"docker push $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
"docker push $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
||||||
|
|||||||
@ -17,7 +17,7 @@ spec:
|
|||||||
- name: regcred
|
- name: regcred
|
||||||
containers:
|
containers:
|
||||||
- name: kultmost-api-container
|
- name: kultmost-api-container
|
||||||
image: registry.spiritofweb.ru/avtopost-api:latest
|
image: $REGISTRY_HOST/avtopost-api:latest
|
||||||
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080","--workers","8"]
|
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080","--workers","8"]
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
@ -56,7 +56,7 @@ spec:
|
|||||||
- name: regcred
|
- name: regcred
|
||||||
containers:
|
containers:
|
||||||
- name: email
|
- name: email
|
||||||
image: registry.spiritofweb.ru/avtopost-api:latest
|
image: $REGISTRY_HOST/avtopost-api:latest
|
||||||
command: ["celery", "-A", "email_task.celery_app_email", "worker", "--loglevel=info"]
|
command: ["celery", "-A", "email_task.celery_app_email", "worker", "--loglevel=info"]
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
@ -81,7 +81,7 @@ spec:
|
|||||||
- name: regcred
|
- name: regcred
|
||||||
containers:
|
containers:
|
||||||
- name: vk
|
- name: vk
|
||||||
image: registry.spiritofweb.ru/avtopost-api:latest
|
image: $REGISTRY_HOST/avtopost-api:latest
|
||||||
command: ["celery", "-A", "vk.celery_app_vk", "worker", "--loglevel=info"]
|
command: ["celery", "-A", "vk.celery_app_vk", "worker", "--loglevel=info"]
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
@ -229,7 +229,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: redis
|
- name: redis
|
||||||
image: redis:latest
|
image: $REGISTRY_HOST/redis:latest
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 6379
|
- containerPort: 6379
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
@ -339,7 +339,7 @@ spec:
|
|||||||
- name: regcred
|
- name: regcred
|
||||||
containers:
|
containers:
|
||||||
- name: mongodb
|
- name: mongodb
|
||||||
image: registry.spiritofweb.ru/mongo:latest
|
image: $REGISTRY_HOST/mongo:latest
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 27017
|
- containerPort: 27017
|
||||||
env:
|
env:
|
||||||
@ -447,7 +447,7 @@ stringData:
|
|||||||
# restartPolicy: OnFailure
|
# restartPolicy: OnFailure
|
||||||
# containers:
|
# containers:
|
||||||
# - name: cron-day
|
# - name: cron-day
|
||||||
# image: registry.spiritofweb.ru/alpine/curl
|
# image: $REGISTRY_HOST/alpine/curl
|
||||||
# command: ["sh","-c","curl -sS http://kultmost-api.kultmost/cron/day -H \"Authorization: Bearer ${ATOKEN}\""]
|
# command: ["sh","-c","curl -sS http://kultmost-api.kultmost/cron/day -H \"Authorization: Bearer ${ATOKEN}\""]
|
||||||
# env:
|
# env:
|
||||||
# - name: ATOKEN
|
# - name: ATOKEN
|
||||||
@ -472,7 +472,7 @@ stringData:
|
|||||||
# restartPolicy: OnFailure
|
# restartPolicy: OnFailure
|
||||||
# containers:
|
# containers:
|
||||||
# - name: cron-five
|
# - name: cron-five
|
||||||
# image: registry.spiritofweb.ru/alpine/curl
|
# image: $REGISTRY_HOST/alpine/curl
|
||||||
# command: ["sh","-c","curl -sS http://kultmost-api.kultmost/cron/5 -H \"Authorization: Bearer ${ATOKEN}\""]
|
# command: ["sh","-c","curl -sS http://kultmost-api.kultmost/cron/5 -H \"Authorization: Bearer ${ATOKEN}\""]
|
||||||
# env:
|
# env:
|
||||||
# - name: ATOKEN
|
# - name: ATOKEN
|
||||||
|
|||||||
@ -54,7 +54,7 @@ spec:
|
|||||||
fsGroup: 999
|
fsGroup: 999
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: fix-permissions
|
- name: fix-permissions
|
||||||
image: registry.spiritofweb.ru/busybox:1.36
|
image: localhost:32000/busybox:1.36
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
@ -69,7 +69,7 @@ spec:
|
|||||||
mountPath: /var/lib/rabbitmq
|
mountPath: /var/lib/rabbitmq
|
||||||
containers:
|
containers:
|
||||||
- name: rabbitmq
|
- name: rabbitmq
|
||||||
image: registry.spiritofweb.ru/rabbitmq:3.11-management
|
image: localhost:32000/rabbitmq:3.11-management
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 5671
|
- containerPort: 5671
|
||||||
name: amqps
|
name: amqps
|
||||||
@ -169,7 +169,8 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: rabbitmq-management
|
name: rabbitmq-management
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
cert-manager.io/cluster-issuer: google-public-ca
|
||||||
|
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
|
||||||
spec:
|
spec:
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
|
|||||||
@ -4748,10 +4748,10 @@
|
|||||||
" obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]\n",
|
" obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]\n",
|
||||||
" posts = await posts_collection.find({\"_id\": {\"$in\": obj_ids}}).to_list() if obj_ids else []\n",
|
" posts = await posts_collection.find({\"_id\": {\"$in\": obj_ids}}).to_list() if obj_ids else []\n",
|
||||||
" d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()\n",
|
" d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()\n",
|
||||||
" for doc in posts:\n",
|
" for post in posts:\n",
|
||||||
" for post in d_posts:\n",
|
" for doc in d_posts:\n",
|
||||||
" if str(doc.get('_id')) == post.get('post_id'):\n",
|
" if str(post.get('_id')) == doc.get('post_id'):\n",
|
||||||
" doc.update({'status':post.get('status')})\n",
|
" post.update({'status':doc.get('status'), 'd_id':doc.get('_id')})\n",
|
||||||
" return templates.TemplateResponse(\n",
|
" return templates.TemplateResponse(\n",
|
||||||
" request=request, name=\"profile/partner/exhibition.html\", context={\"now\": datetime.now(), \"user\": user, \"exhibition\": exhibition, \"posts\": posts}\n",
|
" request=request, name=\"profile/partner/exhibition.html\", context={\"now\": datetime.now(), \"user\": user, \"exhibition\": exhibition, \"posts\": posts}\n",
|
||||||
" )\n",
|
" )\n",
|
||||||
|
|||||||
8
main.py
8
main.py
@ -3617,10 +3617,10 @@ async def profile_partner_exhibitions(
|
|||||||
obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]
|
obj_ids = [ObjectId(x) for x in exhibition.get('posts', [])]
|
||||||
posts = await posts_collection.find({"_id": {"$in": obj_ids}}).to_list() if obj_ids else []
|
posts = await posts_collection.find({"_id": {"$in": obj_ids}}).to_list() if obj_ids else []
|
||||||
d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()
|
d_posts = await delivery_collection.find({'post_id':{'$in':[str(i.get('_id')) for i in posts]}}).to_list()
|
||||||
for doc in posts:
|
for post in posts:
|
||||||
for post in d_posts:
|
for doc in d_posts:
|
||||||
if str(doc.get('_id')) == post.get('post_id'):
|
if str(post.get('_id')) == doc.get('post_id'):
|
||||||
doc.update({'status':post.get('status')})
|
post.update({'status':doc.get('status'), 'd_id':doc.get('_id')})
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request, name="profile/partner/exhibition.html", context={"now": datetime.now(), "user": user, "exhibition": exhibition, "posts": posts}
|
request=request, name="profile/partner/exhibition.html", context={"now": datetime.now(), "user": user, "exhibition": exhibition, "posts": posts}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,8 +20,13 @@
|
|||||||
<a class="b_289i1" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
<a class="b_289i1" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||||
<span class="c_d2l0b b_al8e7">Каталог картин</span>
|
<span class="c_d2l0b b_al8e7">Каталог картин</span>
|
||||||
</a>
|
</a>
|
||||||
{% set ids = posts | map(attribute='user._id') | list %}
|
{% set ids = posts
|
||||||
|
| selectattr('status', 'equalto', 'В каталоге')
|
||||||
|
| map(attribute='user._id')
|
||||||
|
| list
|
||||||
|
%}
|
||||||
{% set uniq_count = ids | unique | list | length %}
|
{% set uniq_count = ids | unique | list | length %}
|
||||||
|
|
||||||
{% if uniq_count > 0 %}
|
{% if uniq_count > 0 %}
|
||||||
<button class="b_289i1" onclick="sendInvitations('{{exhibition._id}}')" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
<button class="b_289i1" onclick="sendInvitations('{{exhibition._id}}')" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||||
<span class="c_d2l0b b_al8e7">Отправить приглашени{% if uniq_count > 1 %}я{% else %}е{% endif %}</span>
|
<span class="c_d2l0b b_al8e7">Отправить приглашени{% if uniq_count > 1 %}я{% else %}е{% endif %}</span>
|
||||||
@ -86,9 +91,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if post.status=='В каталоге' %}
|
||||||
<button onclick="selectPost(this, '{{post._id}}')" class="c_ctfod b_289i1">
|
<button onclick="selectPost(this, '{{post._id}}')" class="c_ctfod b_289i1">
|
||||||
<span class="c_d2l0b b_al8e7">Убрать с выставки</span>
|
<span class="c_d2l0b b_al8e7">Убрать с выставки</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.status == 'Ожидает получения' %}
|
||||||
|
<button class="b_289i1" onclick="btnAccept(this, '{{post.d_id}}')">
|
||||||
|
<span class="b_al8e7">Получить</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -97,6 +109,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script>
|
<script>
|
||||||
|
async function btnAccept(btn, itemId){
|
||||||
|
const figcaption = btn.parentElement;
|
||||||
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
|
const statusSpan = statusDiv.querySelector('span')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/delivery/accept', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
item: itemId,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let errorMessage = 'Ошибка сохранения';
|
||||||
|
try {
|
||||||
|
const errorData = await res.json();
|
||||||
|
errorMessage = errorData.detail || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
const errorText = await res.text();
|
||||||
|
errorMessage = errorText || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast({ message: errorMessage, duration: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusDiv.className = 'b_thlt1';
|
||||||
|
statusSpan.className = 'b_5dr47';
|
||||||
|
statusSpan.textContent = 'Получено';
|
||||||
|
btn.style.display = 'none'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
async function sendInvitations(exhibition_id) {
|
async function sendInvitations(exhibition_id) {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.set('exhibition_id', exhibition_id);
|
data.set('exhibition_id', exhibition_id);
|
||||||
|
|||||||
57
tg.ipynb
57
tg.ipynb
@ -48,7 +48,8 @@
|
|||||||
" load_env('env')\n",
|
" load_env('env')\n",
|
||||||
"except:\n",
|
"except:\n",
|
||||||
" load_env('env_example')\n"
|
" load_env('env_example')\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -81,7 +82,8 @@
|
|||||||
"import asyncio\n",
|
"import asyncio\n",
|
||||||
"import json\n",
|
"import json\n",
|
||||||
"from datetime import datetime, time, timedelta, timezone\n"
|
"from datetime import datetime, time, timedelta, timezone\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -93,7 +95,8 @@
|
|||||||
"Image.MAX_IMAGE_PIXELS = 50_000_000\n",
|
"Image.MAX_IMAGE_PIXELS = 50_000_000\n",
|
||||||
"TMP_DIR = Path(\"/tmp\")\n",
|
"TMP_DIR = Path(\"/tmp\")\n",
|
||||||
"TMP_DIR.mkdir(parents=True, exist_ok=True)"
|
"TMP_DIR.mkdir(parents=True, exist_ok=True)"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -103,7 +106,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"celery_app_tg = Celery('tg', broker=f'amqps://admin:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/tg', task_track_started=True )\n"
|
"celery_app_tg = Celery('tg', broker=f'amqps://admin:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/tg', task_track_started=True )\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -120,7 +124,8 @@
|
|||||||
" if chunk:\n",
|
" if chunk:\n",
|
||||||
" chunks.append(chunk)\n",
|
" chunks.append(chunk)\n",
|
||||||
" return b\"\".join(chunks)\n"
|
" return b\"\".join(chunks)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -141,7 +146,8 @@
|
|||||||
" img = await fetch_bytes(client, f\"https://api.telegram.org/file/bot{BOT_TOKEN}/{file_path}\")\n",
|
" img = await fetch_bytes(client, f\"https://api.telegram.org/file/bot{BOT_TOKEN}/{file_path}\")\n",
|
||||||
" new_tg = (channel_name, BytesIO(img))\n",
|
" new_tg = (channel_name, BytesIO(img))\n",
|
||||||
" print(channel_name)\n"
|
" print(channel_name)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -155,10 +161,6 @@
|
|||||||
" return TMP_DIR / f\"{h}.img\"\n",
|
" return TMP_DIR / f\"{h}.img\"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"REPLACE_FROM = \"avtopost.kultmost.ru\"\n",
|
|
||||||
"REPLACE_TO = \"kultmost.spiritofweb.ru\"\n",
|
|
||||||
"\n",
|
|
||||||
"\n",
|
|
||||||
"async def _fetch_or_load(client: httpx.AsyncClient, url: str) -> bytes:\n",
|
"async def _fetch_or_load(client: httpx.AsyncClient, url: str) -> bytes:\n",
|
||||||
" cache_file = _cache_path(url)\n",
|
" cache_file = _cache_path(url)\n",
|
||||||
" if cache_file.is_file():\n",
|
" if cache_file.is_file():\n",
|
||||||
@ -177,8 +179,6 @@
|
|||||||
" return data\n",
|
" return data\n",
|
||||||
" except Exception as e:\n",
|
" except Exception as e:\n",
|
||||||
" last_exc = e\n",
|
" last_exc = e\n",
|
||||||
" if attempt == 0:\n",
|
|
||||||
" url = url.replace(REPLACE_FROM, REPLACE_TO)\n",
|
|
||||||
"\n",
|
"\n",
|
||||||
" raise last_exc # type: ignore\n",
|
" raise last_exc # type: ignore\n",
|
||||||
"\n",
|
"\n",
|
||||||
@ -217,7 +217,8 @@
|
|||||||
" images.append(img)\n",
|
" images.append(img)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" return images"
|
" return images"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -242,7 +243,8 @@
|
|||||||
" \"media\": f\"attach://file{i}\",\n",
|
" \"media\": f\"attach://file{i}\",\n",
|
||||||
" })\n",
|
" })\n",
|
||||||
" return media"
|
" return media"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -320,7 +322,8 @@
|
|||||||
" await asyncio.sleep(0.8 * attempt)\n",
|
" await asyncio.sleep(0.8 * attempt)\n",
|
||||||
"\n",
|
"\n",
|
||||||
" raise last_exc\n"
|
" raise last_exc\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -341,7 +344,8 @@
|
|||||||
" # The API returns JSON like {\"ok\":true,\"result\":true}\n",
|
" # The API returns JSON like {\"ok\":true,\"result\":true}\n",
|
||||||
" data = response.json()\n",
|
" data = response.json()\n",
|
||||||
" return data.get(\"ok\") and data.get(\"result\") is True"
|
" return data.get(\"ok\") and data.get(\"result\") is True"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -370,7 +374,8 @@
|
|||||||
" # httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={\"Authorization\": f\"bearer {ATOKEN}\"})\n",
|
" # httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={\"Authorization\": f\"bearer {ATOKEN}\"})\n",
|
||||||
" except Exception as exc:\n",
|
" except Exception as exc:\n",
|
||||||
" raise self.retry(exc=exc, countdown=30)\n"
|
" raise self.retry(exc=exc, countdown=30)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -399,7 +404,8 @@
|
|||||||
" httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={\"Authorization\": f\"bearer {ATOKEN}\"})\n",
|
" httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={\"Authorization\": f\"bearer {ATOKEN}\"})\n",
|
||||||
" except Exception as exc:\n",
|
" except Exception as exc:\n",
|
||||||
" raise self.retry(exc=exc, countdown=30)\n"
|
" raise self.retry(exc=exc, countdown=30)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -420,7 +426,8 @@
|
|||||||
" asyncio.run(new_channel_reg(chat_id))\n",
|
" asyncio.run(new_channel_reg(chat_id))\n",
|
||||||
" except Exception as exc:\n",
|
" except Exception as exc:\n",
|
||||||
" raise self.retry(exc=exc, countdown=30)\n"
|
" raise self.retry(exc=exc, countdown=30)\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -459,7 +466,8 @@
|
|||||||
"\n",
|
"\n",
|
||||||
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
|
" with open(f'{filename_without_extension}.py', 'w') as f:\n",
|
||||||
" f.write('\\n\\n'.join(app))\n"
|
" f.write('\\n\\n'.join(app))\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -469,7 +477,8 @@
|
|||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"!celery -A tg.celery_app_tg worker --loglevel=info"
|
"!celery -A tg.celery_app_tg worker --loglevel=info"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
@ -519,7 +528,8 @@
|
|||||||
" else:\n",
|
" else:\n",
|
||||||
" print(\"Failed to delete message.\")\n",
|
" print(\"Failed to delete message.\")\n",
|
||||||
"\n"
|
"\n"
|
||||||
]
|
],
|
||||||
|
"display_name": ".venv"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
@ -527,7 +537,8 @@
|
|||||||
"id": "416f454c",
|
"id": "416f454c",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": []
|
"source": [],
|
||||||
|
"display_name": ".venv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
6
tg.py
6
tg.py
@ -56,10 +56,6 @@ def _cache_path(url: str) -> Path:
|
|||||||
return TMP_DIR / f"{h}.img"
|
return TMP_DIR / f"{h}.img"
|
||||||
|
|
||||||
|
|
||||||
REPLACE_FROM = "avtopost.kultmost.ru"
|
|
||||||
REPLACE_TO = "kultmost.spiritofweb.ru"
|
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_or_load(client: httpx.AsyncClient, url: str) -> bytes:
|
async def _fetch_or_load(client: httpx.AsyncClient, url: str) -> bytes:
|
||||||
cache_file = _cache_path(url)
|
cache_file = _cache_path(url)
|
||||||
if cache_file.is_file():
|
if cache_file.is_file():
|
||||||
@ -78,8 +74,6 @@ async def _fetch_or_load(client: httpx.AsyncClient, url: str) -> bytes:
|
|||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_exc = e
|
last_exc = e
|
||||||
if attempt == 0:
|
|
||||||
url = url.replace(REPLACE_FROM, REPLACE_TO)
|
|
||||||
|
|
||||||
raise last_exc # type: ignore
|
raise last_exc # type: ignore
|
||||||
|
|
||||||
|
|||||||
16
tg/Dockerfile
Normal file
16
tg/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Use the official Python image from the Docker Hub
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the requirements file to the container
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY images.py images.py
|
||||||
|
COPY config.py config.py
|
||||||
|
COPY certs.pem certs.pem
|
||||||
|
COPY tg.py tg.py
|
||||||
|
|
||||||
3892
tg/certs.pem
Normal file
3892
tg/certs.pem
Normal file
File diff suppressed because it is too large
Load Diff
45
tg/config.py
Normal file
45
tg/config.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.load_verify_locations(cafile="certs.pem")
|
||||||
|
|
||||||
|
|
||||||
|
PROD = os.getenv("PROD", "True").lower() != "false"
|
||||||
|
SOCIALS = {'vk', 'tg', 'max', 'fb', 'inst'}
|
||||||
|
ATOKEN = os.getenv("ATOKEN")
|
||||||
|
MONGO_PASSWORD = os.getenv("MONGO_INITDB_ROOT_PASSWORD")
|
||||||
|
MONGODB_HOST = os.getenv("MONGODB_SERVICE_HOST")
|
||||||
|
MONGO_PORT = os.getenv("MONGODB_SERVICE_PORT", 27017)
|
||||||
|
REDIS_HOST = os.getenv("REDIS_SERVICE_HOST")
|
||||||
|
REDIS_PORT = os.getenv("REDIS_SERVICE_PORT", 6379)
|
||||||
|
FROM_NAME = "Avtopost Kultmost"
|
||||||
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
|
FULL_DOMAIN = os.getenv("FULL_PATH_DOMAIN")
|
||||||
|
FULL_PATH_DOMAIN = FULL_DOMAIN
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
FROM_EMAIL = os.getenv("FROM_EMAIL")
|
||||||
|
EMAIL_PASS = os.getenv("EMAIL_PASS")
|
||||||
|
EMAIL_SMTP_SERVER = os.getenv("EMAIL_SMTP_SERVER")
|
||||||
|
EMAIL_SMTP_PORT = os.getenv("EMAIL_SMTP_PORT")
|
||||||
|
RABBITMQ_PASS = os.getenv("RABBITMQ_PASS")
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES = 700
|
||||||
|
REFRESH_TOKEN_EXPIRE_DAYS = 170
|
||||||
|
TOKEN_TTL_DAYS = 7
|
||||||
|
ACCESS_COOKIE = "access_token"
|
||||||
|
REFRESH_COOKIE = "refresh_token"
|
||||||
|
RABBITMQ_USER = os.getenv("RABBITMQ_USER", 'admin')
|
||||||
|
RABBITMQ_HOST = os.getenv("RABBITMQ_HOST")
|
||||||
|
RABBITMQ_PORT = os.getenv("RABBITMQ_PORT", 5671)
|
||||||
|
EMAIL_SUBJECT = 'Культмост'
|
||||||
|
PROTECTED_FIELDS_USER = {"_id", "img", 'original_img', "hashed_password", "password", "roles"}
|
||||||
|
VK_CLIENT_ID = os.getenv("VK_CLIENT_ID")
|
||||||
|
VK_REDIRECT_URI = os.getenv("VK_REDIRECT_URI")
|
||||||
|
VK_API_URI = 'https://api.vk.ru/method/'
|
||||||
|
FULL_DOMAIN_LOCAL = 'http://kultmost-api.kultmost' if PROD else 'http://127.0.0.1:8005'
|
||||||
|
KULTMOST_SSH_SERVICE_HOST = os.getenv("KULTMOST_SSH_SERVICE_HOST")
|
||||||
|
ENC_KEY_B64 = os.getenv("ENC_KEY_B64")
|
||||||
|
CLIENT_ID = 51852444
|
||||||
|
MSK_OFFSET = timedelta(hours=3)
|
||||||
66
tg/images.py
Normal file
66
tg/images.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter
|
||||||
|
|
||||||
|
|
||||||
|
Image.MAX_IMAGE_PIXELS = 50_000_000
|
||||||
|
|
||||||
|
|
||||||
|
def insert_logo(
|
||||||
|
background,
|
||||||
|
logo,
|
||||||
|
scale_factor=0.095,
|
||||||
|
padding=8,
|
||||||
|
radius=15,
|
||||||
|
quality=95
|
||||||
|
):
|
||||||
|
background = background.copy().convert("RGBA")
|
||||||
|
|
||||||
|
# Логотип
|
||||||
|
logo = logo.copy().convert("RGBA")
|
||||||
|
|
||||||
|
# Масштабирование
|
||||||
|
logo_w, logo_h = logo.size
|
||||||
|
back_w, back_h = background.size
|
||||||
|
new_w = max(1, int(back_w * scale_factor))
|
||||||
|
ratio = logo_w / logo_h if logo_h > 0 else 1
|
||||||
|
new_h = max(1, int(new_w / ratio))
|
||||||
|
logo = logo.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# === Создаём полностью прозрачный фон ===
|
||||||
|
transparent = Image.new("RGBA", (new_w, new_h), (0, 0, 0, 0))
|
||||||
|
|
||||||
|
# Компонуем логотип на полностью прозрачный фон (убирает все артефакты)
|
||||||
|
logo = Image.alpha_composite(transparent, logo)
|
||||||
|
|
||||||
|
# === Маска закругления ===
|
||||||
|
mask = Image.new("L", (new_w, new_h), 0)
|
||||||
|
draw = ImageDraw.Draw(mask)
|
||||||
|
draw.rounded_rectangle([(0, 0), (new_w-1, new_h-1)], radius=radius, fill=255)
|
||||||
|
mask = mask.filter(ImageFilter.SMOOTH)
|
||||||
|
|
||||||
|
# Применяем маску через composite (самый чистый способ)
|
||||||
|
logo = Image.composite(logo, transparent, mask)
|
||||||
|
|
||||||
|
# Позиция (нижний левый угол)
|
||||||
|
pos_x = padding
|
||||||
|
pos_y = back_h - new_h - padding
|
||||||
|
if pos_y < 0:
|
||||||
|
pos_y = 0
|
||||||
|
|
||||||
|
# Вставка
|
||||||
|
background.paste(logo, (pos_x, pos_y), logo)
|
||||||
|
|
||||||
|
# Сохранение
|
||||||
|
out = BytesIO()
|
||||||
|
background.convert("RGB").save(out, format="JPEG", quality=quality, optimize=True)
|
||||||
|
out.seek(0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def convert_bio_to_webp(bio: BytesIO, quality: int = 95) -> BytesIO:
|
||||||
|
bio.seek(0)
|
||||||
|
im = Image.open(bio).convert("RGB")
|
||||||
|
out = BytesIO()
|
||||||
|
im.save(out, format="WEBP", quality=quality, method=6)
|
||||||
|
out.seek(0)
|
||||||
|
return out
|
||||||
18
tg/requirements.txt
Normal file
18
tg/requirements.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
anyio==4.12.1
|
||||||
|
certifi==2026.2.25
|
||||||
|
cryptography==46.0.5
|
||||||
|
dnspython==2.8.0
|
||||||
|
email-validator==2.3.0
|
||||||
|
fastar==0.8.0
|
||||||
|
h11==0.16.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httptools==0.7.1
|
||||||
|
httpx[http2,socks]==0.28.1
|
||||||
|
passlib==1.7.4
|
||||||
|
pyzmq==27.1.0
|
||||||
|
rsa==4.9.1
|
||||||
|
socksio==1.0.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
celery[redis]==5.6.3
|
||||||
|
pillow==12.1.1
|
||||||
|
pytz
|
||||||
272
tg/tg.py
Normal file
272
tg/tg.py
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
# TG
|
||||||
|
|
||||||
|
## Class
|
||||||
|
|
||||||
|
from config import *
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
import httpx
|
||||||
|
import os
|
||||||
|
from celery import Celery
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
from images import *
|
||||||
|
import json
|
||||||
|
from typing import List, Optional
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from datetime import datetime, time, timedelta, timezone
|
||||||
|
|
||||||
|
|
||||||
|
Image.MAX_IMAGE_PIXELS = 50_000_000
|
||||||
|
TMP_DIR = Path("/tmp")
|
||||||
|
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
celery_app_tg = Celery('tg', broker=f'amqps://admin:{RABBITMQ_PASS}@{RABBITMQ_HOST}:{RABBITMQ_PORT}/tg', task_track_started=True )
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_bytes(client: httpx.AsyncClient, url: str) -> bytes:
|
||||||
|
async with client.stream("GET", url, timeout=60000) as resp:
|
||||||
|
resp.raise_for_status()
|
||||||
|
chunks = []
|
||||||
|
async for chunk in resp.aiter_bytes():
|
||||||
|
if chunk:
|
||||||
|
chunks.append(chunk)
|
||||||
|
return b"".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
async def new_channel_reg(chat_id):
|
||||||
|
async with httpx.AsyncClient(http2=True, http1=False, timeout=30.0, verify=ctx) as client:
|
||||||
|
res = await client.post(f"https://api.telegram.org/bot{BOT_TOKEN}/getChat", data={'chat_id':chat_id})
|
||||||
|
channel_data = res.json()['result']
|
||||||
|
channel_name = channel_data['title']
|
||||||
|
file_id = channel_data['photo']['big_file_id']
|
||||||
|
if file_id:
|
||||||
|
res = await client.post(f"https://api.telegram.org/bot{BOT_TOKEN}/getFile", data={'file_id': file_id})
|
||||||
|
file_path = res.json()['result']['file_path']
|
||||||
|
img = await fetch_bytes(client, f"https://api.telegram.org/file/bot{BOT_TOKEN}/{file_path}")
|
||||||
|
new_tg = (channel_name, BytesIO(img))
|
||||||
|
print(channel_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_path(url: str) -> Path:
|
||||||
|
h = hashlib.sha256(url.encode()).hexdigest()
|
||||||
|
return TMP_DIR / f"{h}.img"
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_or_load(client: httpx.AsyncClient, url: str) -> bytes:
|
||||||
|
cache_file = _cache_path(url)
|
||||||
|
if cache_file.is_file():
|
||||||
|
return cache_file.read_bytes()
|
||||||
|
|
||||||
|
last_exc: Exception | None = None
|
||||||
|
for attempt in range(2):
|
||||||
|
try:
|
||||||
|
resp = await client.get(url, timeout=30)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.content
|
||||||
|
|
||||||
|
temp_name = f"{cache_file}.tmp"
|
||||||
|
Path(temp_name).write_bytes(data)
|
||||||
|
os.replace(temp_name, cache_file)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
last_exc = e
|
||||||
|
|
||||||
|
raise last_exc # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
async def get_images(urls: list[str], sign_avatar: bool = True, quality: int = 95):
|
||||||
|
async with httpx.AsyncClient(http2=True, http1=False, timeout=30.0, verify=True) as client:
|
||||||
|
bytes_list = await asyncio.gather(
|
||||||
|
*(_fetch_or_load(client, u) for u in urls)
|
||||||
|
)
|
||||||
|
|
||||||
|
if sign_avatar:
|
||||||
|
avatar_bytes = bytes_list[0]
|
||||||
|
photo_bytes = bytes_list[1:]
|
||||||
|
logo = Image.open(BytesIO(avatar_bytes)).convert("RGB")
|
||||||
|
logo.thumbnail((200, 200), Image.Resampling.LANCZOS)
|
||||||
|
else:
|
||||||
|
photo_bytes = bytes_list
|
||||||
|
|
||||||
|
images: list = []
|
||||||
|
MAX_SIZE = (3000, 3000) # Telegram-friendly + memory safe
|
||||||
|
|
||||||
|
for b in photo_bytes:
|
||||||
|
img = Image.open(BytesIO(b)).convert("RGB")
|
||||||
|
|
||||||
|
# <<< CRITICAL: Resize early >>>
|
||||||
|
img.thumbnail(MAX_SIZE, Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
if sign_avatar:
|
||||||
|
img = insert_logo(img, logo, quality=quality) # now receives already resized image
|
||||||
|
else:
|
||||||
|
bio = BytesIO()
|
||||||
|
img.save(bio, format="JPEG", quality=quality, optimize=True)
|
||||||
|
bio.seek(0)
|
||||||
|
img = bio
|
||||||
|
|
||||||
|
images.append(img)
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
def build_media(images_len: int, text: str) -> list:
|
||||||
|
media = []
|
||||||
|
for i in range(images_len):
|
||||||
|
if i == 0:
|
||||||
|
media.append({
|
||||||
|
"type": "photo",
|
||||||
|
"media": f"attach://file{i}",
|
||||||
|
"caption": text,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
media.append({
|
||||||
|
"type": "photo",
|
||||||
|
"media": f"attach://file{i}",
|
||||||
|
})
|
||||||
|
return media
|
||||||
|
|
||||||
|
def is_msk_night_window() -> bool:
|
||||||
|
now_msk_dt = datetime.now(timezone.utc) + MSK_OFFSET
|
||||||
|
now_msk = now_msk_dt.time()
|
||||||
|
|
||||||
|
start = time(22, 0)
|
||||||
|
end = time(8, 0)
|
||||||
|
|
||||||
|
return (now_msk >= start) or (now_msk < end)
|
||||||
|
|
||||||
|
async def send_tg(urls: list, text: str, chat_id: str, sign_avatar: bool = True, max_retries: int = 4):
|
||||||
|
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMediaGroup"
|
||||||
|
|
||||||
|
plans = [
|
||||||
|
{"limit": None, "webp": False, "webp_quality": 90},
|
||||||
|
{"limit": 85, "webp": False, "webp_quality": 85},
|
||||||
|
{"limit": None, "webp": True, "webp_quality": 90},
|
||||||
|
{"limit": None, "webp": True, "webp_quality": 80},
|
||||||
|
][:max_retries]
|
||||||
|
|
||||||
|
last_exc: Optional[Exception] = None
|
||||||
|
|
||||||
|
night = is_msk_night_window()
|
||||||
|
|
||||||
|
for attempt, plan in enumerate(plans, start=1):
|
||||||
|
try:
|
||||||
|
if plan["limit"] is None:
|
||||||
|
images: List[BytesIO] = await get_images(urls, sign_avatar)
|
||||||
|
else:
|
||||||
|
images = await get_images(urls, sign_avatar, plan["limit"])
|
||||||
|
|
||||||
|
if plan.get("webp", False):
|
||||||
|
images_webp: List[BytesIO] = []
|
||||||
|
for bio in images:
|
||||||
|
images_webp.append(await convert_bio_to_webp(bio, quality=plan["webp_quality"]))
|
||||||
|
images = images_webp
|
||||||
|
|
||||||
|
media = build_media(len(images), text)
|
||||||
|
|
||||||
|
multipart = [
|
||||||
|
("chat_id", (None, str(chat_id))),
|
||||||
|
("media", (None, json.dumps(media))),
|
||||||
|
]
|
||||||
|
|
||||||
|
if night:
|
||||||
|
multipart.append(("disable_notification", (None, "true")))
|
||||||
|
|
||||||
|
for i, bio in enumerate(images):
|
||||||
|
bio.seek(0)
|
||||||
|
ext = "webp" if plan.get("webp", False) else "jpg"
|
||||||
|
filename = f"image{i}.{ext}"
|
||||||
|
content = bio.read()
|
||||||
|
content_type = f"image/{ext}"
|
||||||
|
multipart.append((f"file{i}", (filename, content, content_type)))
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(http2=True, timeout=60.0) as client:
|
||||||
|
resp = await client.post(url, files=multipart)
|
||||||
|
|
||||||
|
if resp.status_code >= 300:
|
||||||
|
raise RuntimeError(f"Telegram HTTP {resp.status_code}: {resp.text}")
|
||||||
|
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
last_exc = e
|
||||||
|
if attempt == len(plans):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.8 * attempt)
|
||||||
|
|
||||||
|
raise last_exc
|
||||||
|
|
||||||
|
|
||||||
|
def delete_message(chat_id: int, message_id: int) -> bool:
|
||||||
|
API_URL = f"https://api.telegram.org/bot{BOT_TOKEN}/deleteMessage"
|
||||||
|
payload = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
}
|
||||||
|
# Telegram expects a POST request with JSON body
|
||||||
|
with httpx.Client() as client:
|
||||||
|
response = client.post(API_URL, json=payload, timeout=10)
|
||||||
|
# The API returns JSON like {"ok":true,"result":true}
|
||||||
|
data = response.json()
|
||||||
|
return data.get("ok") and data.get("result") is True
|
||||||
|
|
||||||
|
@celery_app_tg.task(
|
||||||
|
bind=True,
|
||||||
|
rate_limit='60/m',
|
||||||
|
name='tg.delete_message',
|
||||||
|
retry_backoff=True,
|
||||||
|
retry_backoff_max=600,
|
||||||
|
retry_jitter=True,
|
||||||
|
max_retries=20,
|
||||||
|
soft_time_limit=600,
|
||||||
|
time_limit=700,
|
||||||
|
)
|
||||||
|
def delete_from_tg(self, message_id: str, social: dict):
|
||||||
|
try:
|
||||||
|
chat_id = social.get('screen_name')
|
||||||
|
logging.info(f"TG удаление публикации {chat_id} {message_id}")
|
||||||
|
result = asyncio.run(delete_message(chat_id, message_id))
|
||||||
|
# result['doc'] = social
|
||||||
|
# httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={"Authorization": f"bearer {ATOKEN}"})
|
||||||
|
except Exception as exc:
|
||||||
|
raise self.retry(exc=exc, countdown=30)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app_tg.task(
|
||||||
|
bind=True,
|
||||||
|
rate_limit='60/m',
|
||||||
|
name='tg.send_tg',
|
||||||
|
retry_backoff=True,
|
||||||
|
retry_backoff_max=600,
|
||||||
|
retry_jitter=True,
|
||||||
|
max_retries=20,
|
||||||
|
soft_time_limit=600,
|
||||||
|
time_limit=700,
|
||||||
|
)
|
||||||
|
def notify(self, item: dict, text: str, urls: list, social: dict, sign_avatar: bool = True):
|
||||||
|
try:
|
||||||
|
chat_id = social.get('screen_name')
|
||||||
|
logging.info(f"TG начало публикации {item}")
|
||||||
|
result = asyncio.run(send_tg(urls, text, chat_id, sign_avatar))
|
||||||
|
result['doc'] = social
|
||||||
|
httpx.post(f'{FULL_PATH_DOMAIN}/tg', json={'item': item, 'result': result}, headers={"Authorization": f"bearer {ATOKEN}"})
|
||||||
|
except Exception as exc:
|
||||||
|
raise self.retry(exc=exc, countdown=30)
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app_tg.task(
|
||||||
|
rate_limit='60/m',
|
||||||
|
name='tg.new_channel_reg',
|
||||||
|
retry_kwargs={'max_retries': None, 'countdown': 30},
|
||||||
|
retry_backoff=True,
|
||||||
|
)
|
||||||
|
def new_channel(self, chat_id: str):
|
||||||
|
try:
|
||||||
|
logging.info(f"TG новый канал {chat_id}")
|
||||||
|
asyncio.run(new_channel_reg(chat_id))
|
||||||
|
except Exception as exc:
|
||||||
|
raise self.retry(exc=exc, countdown=30)
|
||||||
Loading…
Reference in New Issue
Block a user