switch to uv and add cors headers

This commit is contained in:
bakatrouble 2025-07-25 01:23:29 +03:00
parent 0ea2ca744a
commit 7f47ac6849
30 changed files with 3326 additions and 2849 deletions

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.9

53
Pipfile
View File

@ -1,53 +0,0 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
pillow = "*"
pyrogram = "*"
python-telegram-bot = "==13.14"
django-yamlfield = "*"
django-timezone-field = "*"
django-redis-cache = "*"
django-jsoneditor = "*"
django-jet = "*"
django-extensions = "*"
django-environ = "*"
django-djconfig = "*"
django-crispy-forms = "*"
django-bootstrap4 = "*"
daphne = "*"
markovify = "*"
python-anticaptcha = "*"
vk-api = "11.9.7"
sentry-sdk = "*"
python-twitter = "*"
pytelegrambotapi = "*"
celery = "*"
celery-once = "*"
feedparser = "*"
json-rpc = "*"
humanize = "*"
jsonfield2 = "*"
tgcrypto = "*"
psycopg2-binary = "*"
hiredis = "*"
twisted = {extras = ["http2", "tls"], version = "*"}
whitenoise = "*"
django-picklefield = "*"
boto3 = "*"
filetype = "*"
qrtools = "*"
vkwave = "*"
imagehash = "*"
tqdm = "*"
tiktokapi = "*"
ffmpeg-python = "*"
crispy-bootstrap4 = "*"
[dev-packages]
[requires]
python_version = "3.9"

2148
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

View File

@ -1,8 +0,0 @@
from django.contrib import admin
from .models import AggregationSource, Chat, Message
admin.site.register(AggregationSource)
admin.site.register(Chat)
admin.site.register(Message)

View File

@ -1,23 +0,0 @@
from django.apps import AppConfig
from django.db.models.signals import post_delete
class AggregatorConfig(AppConfig):
name = 'aggregator'
def ready(self):
self.register_config()
self.register_signals()
def register_config(self):
import djconfig
from .forms import AggregatorAppConfigForm
djconfig.register(AggregatorAppConfigForm)
def register_signals(self):
from .models import AggregationSource, Chat
from .signals import aggregation_source_deleted, chat_deleted
post_delete.connect(aggregation_source_deleted, sender=AggregationSource)
post_delete.connect(chat_deleted, sender=Chat)

View File

@ -1,93 +0,0 @@
import os
from typing import List
from django.core.files.storage import default_storage
from djconfig import config
from pyrogram import Client
from pyrogram.handlers import MessageHandler, DeletedMessagesHandler
from pyrogram.types import Message as PyrogramMessage
from pyrogram.errors import ChannelPrivate
from pyrogram.session import Session
from aggregator.models import AggregationSource, Message, Chat
Session.notice_displayed = True
def get_client(takeout=False):
config._reload_maybe()
if not config.pyrogram_session or not config.pyrogram_app_id or not config.pyrogram_app_hash:
raise RuntimeError('Pyrogram is not configured')
session_path = os.path.relpath(default_storage.path(config.pyrogram_session.replace('.session', '')))
return Client(session_path, config.pyrogram_app_id, config.pyrogram_app_hash, config.pyrogram_app_version or None,
config.pyrogram_device_model or None, config.pyrogram_system_version or None, takeout=takeout)
def save_message(client, message: PyrogramMessage):
if not AggregationSource.objects.filter(chat_id=message.chat.id).exists():
return
Message.from_obj(message, client)
def delete_messages(client, messages: List[PyrogramMessage]):
for message in messages:
Message.objects.filter(chat__chat_id=message.chat.id, message_id=message.message_id).update(deleted=True)
def collect_new_messages(client, chat):
# Collecting new messages
last_message = chat.messages.order_by('-message_id').first()
if last_message:
itr = client.iter_history(chat.chat_id, reverse=True, offset_id=last_message.message_id + 1)
else:
itr = client.iter_history(chat.chat_id, reverse=True, limit=10)
for message in itr:
Message.from_obj(message, client)
def startup_collect(client: Client):
for chat in Chat.objects.all():
try:
client.get_chat(chat.chat_id)
except ChannelPrivate:
print('I was banned in chat id="{}"'.format(chat.chat_id))
continue
# Collecting edited & deleted messages
offset = 0
qs = Message.objects.active_messages().filter(chat__chat_id=chat.chat_id)
while True:
lst = qs[200*offset:200*(offset+1)]
if not lst:
break
messages = client.get_messages(chat.chat_id, [m.message_id for m in lst])
for message in messages.messages:
m_qs = Message.objects.active_messages() \
.filter(chat__chat_id=chat.chat_id, message_id=message.message_id)
if message.empty:
m_qs.update(deleted=True)
elif message.edit_date:
m = m_qs.first() # type: Message
if m and (not m.edit_date or m.edit_date.timestamp() != message.edit_date):
Message.from_obj(message, client)
offset += 1
collect_new_messages(client, chat)
def start_client():
app = get_client()
app.add_handler(MessageHandler(save_message))
app.add_handler(DeletedMessagesHandler(delete_messages))
print('Starting pyrogram client...')
app.start()
print('Loading updates that happened while I was offline...')
startup_collect(app)
print('Idling...')
app.idle()

View File

@ -1,58 +0,0 @@
from django import forms
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.forms import ModelForm
from djconfig.forms import ConfigForm
from pyrogram.errors import ChannelPrivate
from config.utils import parse_mtproto_chat
from .client import get_client
from .models import AggregationSource, Chat
class AggregationSourceForm(ModelForm):
invite_link = forms.CharField(help_text='Invite link (with joinchat) or username')
def clean(self):
cleaned_data = super(AggregationSourceForm, self).clean()
invite_link = cleaned_data.pop('invite_link')
if invite_link:
with get_client() as app:
try:
upd = app.join_chat(invite_link)
except ChannelPrivate:
raise ValidationError('I was banned in this chat')
chat = parse_mtproto_chat(app, upd.chats[0])
cleaned_data['chat_id'] = chat.id
Chat.from_obj(chat, app)
return cleaned_data
class Meta:
model = AggregationSource
fields = 'title', 'invite_link',
class AggregationSourceEditForm(AggregationSourceForm):
invite_link = forms.CharField(help_text='Invite link (with joinchat) or username; '
'leave empty if change is not needed', required=False)
class AggregatorAppConfigForm(ConfigForm):
slug = 'aggregator'
title = 'Aggregator'
pyrogram_app_id = forms.CharField()
pyrogram_app_hash = forms.CharField()
pyrogram_app_version = forms.CharField(required=False)
pyrogram_device_model = forms.CharField(required=False)
pyrogram_system_version = forms.CharField(required=False)
pyrogram_session = forms.FileField()
def save_session(self):
session = self.cleaned_data.get('pyrogram_session')
if session:
session.name = default_storage.save(session.name, session)
def save(self):
self.save_session()
super(AggregatorAppConfigForm, self).save()

View File

@ -1,8 +0,0 @@
from django.core.management import BaseCommand
from aggregator.client import start_client
class Command(BaseCommand):
def handle(self, *args, **options):
start_client()

View File

@ -1,54 +0,0 @@
# Generated by Django 2.1.5 on 2019-01-27 12:20
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AggregationSource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=64)),
('chat_id', models.IntegerField(db_index=True)),
('last_id', models.PositiveIntegerField()),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Chat',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('chat_id', models.IntegerField(db_index=True)),
('title', models.TextField()),
('username', models.CharField(blank=True, max_length=64, null=True)),
('photo', models.ImageField(blank=True, null=True, upload_to='')),
('photo_id', models.CharField(blank=True, max_length=64, null=True)),
],
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message_id', models.PositiveIntegerField(db_index=True)),
('text', models.TextField(blank=True)),
('date', models.DateTimeField()),
('edit_date', models.DateTimeField(blank=True, null=True)),
('deleted', models.BooleanField(default=False)),
('chat', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='aggregator.Chat')),
('replaced_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='aggregator.Message')),
],
options={
'ordering': ('message_id', 'edit_date'),
},
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.1.5 on 2019-01-27 13:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('aggregator', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='aggregationsource',
name='last_id',
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.1.5 on 2019-01-27 21:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('aggregator', '0002_remove_aggregationsource_last_id'),
]
operations = [
migrations.AlterField(
model_name='aggregationsource',
name='chat_id',
field=models.CharField(db_index=True, max_length=64),
),
migrations.AlterField(
model_name='chat',
name='chat_id',
field=models.CharField(db_index=True, max_length=64),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.1.5 on 2019-01-30 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('aggregator', '0003_auto_20190128_0048'),
]
operations = [
migrations.AlterField(
model_name='aggregationsource',
name='chat_id',
field=models.BigIntegerField(db_index=True),
),
migrations.AlterField(
model_name='chat',
name='chat_id',
field=models.BigIntegerField(db_index=True),
),
]

View File

@ -1,99 +0,0 @@
import os
from datetime import datetime
from tempfile import TemporaryDirectory
import pytz
from django.conf import settings
from django.db import models, transaction
from pyrogram.types import Chat as PyrogramChat, Message as PyrogramMessage
class AggregationSource(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=64)
chat_id = models.BigIntegerField(db_index=True)
def __str__(self):
return self.title
class Chat(models.Model):
chat_id = models.BigIntegerField(db_index=True)
title = models.TextField()
username = models.CharField(max_length=64, null=True, blank=True)
photo = models.ImageField(null=True, blank=True)
photo_id = models.CharField(max_length=64, null=True, blank=True)
@classmethod
def from_obj(cls, chat: PyrogramChat, client):
from aggregator.tasks import collect_new_messages
obj, created = Chat.objects.update_or_create(
chat_id=chat.id,
defaults={
'title': chat.title or '{} {}'.format(chat.first_name, chat.last_name).rstrip(),
'username': chat.username,
}
)
if chat.photo is None:
if obj.photo_id is not None:
obj.photo = obj.photo_id = None
obj.save()
else:
photo_file_id = chat.photo.small_file_id
if photo_file_id != obj.photo_id:
with TemporaryDirectory() as d:
path = client.download_media(photo_file_id, os.path.join(d, ''), block=True)
with open(path, 'rb') as f:
obj.photo.save(os.path.basename(path), f, save=True)
obj.photo_id = chat.photo.small_file_id
obj.save()
if created:
transaction.on_commit(lambda: collect_new_messages.delay(obj.pk))
return obj
def __str__(self):
return '{} (chat_id="{}")'.format(self.title, self.chat_id)
class MessageManager(models.Manager):
def active_messages(self):
return self.get_queryset().filter(deleted=False, replaced_by__isnull=True)
class Message(models.Model):
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name='messages')
message_id = models.PositiveIntegerField(db_index=True)
text = models.TextField(blank=True)
date = models.DateTimeField()
edit_date = models.DateTimeField(null=True, blank=True)
deleted = models.BooleanField(default=False)
replaced_by = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
objects = MessageManager()
@classmethod
def from_obj(cls, message: PyrogramMessage, client):
tz = pytz.timezone('UTC')
chat = Chat.from_obj(message.chat, client)
try:
old = Message.objects.active_messages().get(chat=chat, message_id=message.message_id)
except Message.DoesNotExist:
old = None
obj = Message.objects.create(
chat=chat,
message_id=message.message_id,
text=message.text.html if message.text else '',
date=tz.localize(datetime.utcfromtimestamp(message.date)),
edit_date=tz.localize(datetime.utcfromtimestamp(message.edit_date)) if message.edit_date else None,
)
if old is not None:
old.replaced_by = obj
old.save()
return obj
def __str__(self):
return 'id="{}" (chat_id="{}")'.format(self.message_id, self.chat.chat_id)
class Meta:
ordering = 'message_id', 'edit_date',

View File

@ -1,17 +0,0 @@
from pyrogram.errors import RPCError
from .client import get_client
from .models import Chat, AggregationSource
def aggregation_source_deleted(sender, instance: AggregationSource, **kwargs):
if not AggregationSource.objects.filter(chat_id=instance.chat_id):
Chat.objects.filter(chat_id=instance.chat_id).delete()
def chat_deleted(sender, instance: Chat, **kwargs):
with get_client() as client:
try:
client.leave_chat(instance.chat_id)
except RPCError:
pass

View File

@ -1,17 +0,0 @@
from celery_once import QueueOnce
from aggregator.client import get_client
from config.celery import app
from .client import collect_new_messages as _collect_new_messages
@app.task(base=QueueOnce, once={'keys': ['chat_id'], 'graceful': True})
def collect_new_messages(chat_id):
from .models import Chat
chat = Chat.objects.get(pk=chat_id)
client = get_client(takeout=True)
client.start()
_collect_new_messages(client, chat)
client.stop()

View File

@ -1,59 +0,0 @@
{% extends 'cabinet/_internal_base.html' %}
{% load bootstrap4 %}
{% block breadcrumbs %}
<li><a href="{% url 'cabinet:aggregator:index' %}">Aggregation source list</a></li>
<li><span>{{ title }}</span></li>
{% endblock %}
{% block content %}
<form action="" method="post" class="card">
{% csrf_token %}
<header class="card-header">
<h2 class="card-title">{% if source %}Source "{{ source.title }}" configuration{% else %}New source{% endif %}</h2>
</header>
<div class="card-body">
{% bootstrap_form form layout='horizontal' %}
{% if source %}
<div class="form-group row">
<label class="col-md-3 col-form-label" for="id_feed-last_check">Chat ID</label>
<div class="col-md-9">
<input type="text" value="{{ source.chat_id }}" class="form-control" placeholder="Chat ID" title="" id="id_chat_id" disabled>
</div>
</div>
{% endif %}
</div>
<footer class="card-footer text-right">
{% if source %}<a href="#delete-modal" class="modal-basic btn btn-danger">Delete</a>{% endif %}
<button type="submit" class="btn btn-primary">Save</button>
</footer>
</form>
{% if source %}
<div id="delete-modal" class="modal-block modal-full-color modal-block-danger mfp-hide">
<section class="card">
<header class="card-header">
<h2 class="card-title">Delete source "{{ source.title }}"</h2>
</header>
<div class="card-body">
<div class="modal-wrapper">
<div class="modal-icon"><i class="fas fa-times-circle"></i></div>
<div class="modal-text">
<h4>Are you sure?</h4>
<p>This action cannot be undone.</p>
</div>
</div>
</div>
<footer class="card-footer">
<div class="row">
<form action="{% url 'cabinet:aggregator:delete' pk=source.pk %}" method="post" class="col-md-12 text-right">
{% csrf_token %}
<button type="button" class="btn btn-default modal-dismiss">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</footer>
</section>
</div>
{% endif %}
{% endblock %}

View File

@ -1,40 +0,0 @@
{% extends 'cabinet/_internal_base.html' %}
{% block breadcrumbs %}
<li><span>Aggregator source list</span></li>
{% endblock %}
{% block content %}
<section class="card">
<header class="card-header">
<div class="card-button-actions">
<a href="{% url 'cabinet:aggregator:new' %}" class="btn btn-primary btn-sm" type="button">
Add new source
</a>
</div>
<h2 class="card-title">Your sources</h2>
</header>
<div class="card-body">
<table class="table table-hover table-responsive-md mb-0">
<thead>
<tr>
<th>Title</th>
<th>Chat ID</th>
</tr>
</thead>
{% for source in object_list %}
<tr class="clickable-row" data-href="{% url 'cabinet:aggregator:edit' pk=source.pk %}">
<td>{{ source.title }}</td>
<td>{{ source.chat_id }}</td>
</tr>
{% empty %}
<tfoot>
<tr>
<td colspan="2">No sources added</td>
</tr>
</tfoot>
{% endfor %}
</table>
</div>
</section>
{% endblock %}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,13 +0,0 @@
from django.urls import path
from .views import AggregationSourceListView, AggregationSourceCreateView, AggregationSourceUpdateView, \
AggregationSourceDeleteView
app_name = 'aggregator'
urlpatterns = [
path('', AggregationSourceListView.as_view(), name='index'),
path('<int:pk>/', AggregationSourceUpdateView.as_view(), name='edit'),
path('new/', AggregationSourceCreateView.as_view(), name='new'),
path('delete/<int:pk>/', AggregationSourceDeleteView.as_view(), name='delete'),
]

View File

@ -1,63 +0,0 @@
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.views import View
from django.views.generic import ListView, UpdateView, CreateView
from django.views.generic.detail import SingleObjectMixin
from aggregator.forms import AggregationSourceForm, AggregationSourceEditForm
from aggregator.models import AggregationSource
from cabinet.utils import CabinetViewMixin
class AggregationSourceListView(CabinetViewMixin, ListView):
template_name = 'cabinet/aggregator/source_list.html'
title = 'Aggregation source list'
sidebar_section = 'aggregator'
def get_queryset(self):
return AggregationSource.objects.filter(owner=self.request.user)
class AggregationSourceCreateView(CabinetViewMixin, CreateView):
template_name = 'cabinet/aggregator/source_form.html'
title = 'Create aggregation source'
sidebar_section = 'aggregator'
form_class = AggregationSourceForm
def form_valid(self, form):
form.instance.owner = self.request.user
form.instance.chat_id = form.cleaned_data['chat_id']
form.save()
return redirect('cabinet:aggregator:edit', pk=form.instance.pk)
class AggregationSourceUpdateView(CabinetViewMixin, UpdateView):
template_name = 'cabinet/aggregator/source_form.html'
title = 'Configure aggregation source'
sidebar_section = 'aggregator'
form_class = AggregationSourceEditForm
context_object_name = 'source'
def get_queryset(self):
return AggregationSource.objects.filter(owner=self.request.user)
def form_valid(self, form):
if 'chat_id' in form.cleaned_data:
form.instance.chat_id = form.cleaned_data['chat_id']
form.save()
return redirect('cabinet:aggregator:edit', pk=form.instance.pk)
class AggregationSourceDeleteView(LoginRequiredMixin, SingleObjectMixin, View):
def get_queryset(self):
return AggregationSource.objects.filter(owner=self.request.user)
def post(self, request, *args, **kwargs):
source = self.get_object()
messages.success(self.request, 'Source "{}" was successfully deleted'.format(source.title))
source.delete()
return redirect('cabinet:aggregator:index')

View File

@ -16,7 +16,6 @@ from bots.models import TelegramBot
from bots.modules import BOT_MODULES from bots.modules import BOT_MODULES
from bots.utils import BaseBotConfigView, JSONResponseMixin, AjaxResponseMixin from bots.utils import BaseBotConfigView, JSONResponseMixin, AjaxResponseMixin
from cabinet.utils import CabinetViewMixin from cabinet.utils import CabinetViewMixin
from config.utils import AllowCORSMixin
class BotListView(CabinetViewMixin, ListView): class BotListView(CabinetViewMixin, ListView):
@ -84,7 +83,7 @@ class BotConfigDeleteView(LoginRequiredMixin, SingleObjectMixin, View):
return redirect('cabinet:bots:index') return redirect('cabinet:bots:index')
class BotRPCView(JSONResponseMixin, AjaxResponseMixin, AllowCORSMixin, View): class BotRPCView(JSONResponseMixin, AjaxResponseMixin, View):
@method_decorator(csrf_exempt) @method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -95,5 +94,4 @@ class BotRPCView(JSONResponseMixin, AjaxResponseMixin, AllowCORSMixin, View):
raise PermissionDenied() raise PermissionDenied()
rpc_response = JSONRPCResponseManager.handle(request.body, bot.config.rpc_dispatcher) rpc_response = JSONRPCResponseManager.handle(request.body, bot.config.rpc_dispatcher)
response = self.render_json_response(rpc_response.data) response = self.render_json_response(rpc_response.data)
self.add_access_control_headers(response)
return response return response

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('login/', LoginView.as_view(), name='login'), path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'), path('logout/', LogoutView.as_view(), name='logout'),
path('feeds/', include('feeds.urls', namespace='feeds')), path('feeds/', include('feeds.urls', namespace='feeds')),
path('aggregator/', include('aggregator.urls', namespace='aggregator')),
path('bots/', include('bots.urls', namespace='bots')), path('bots/', include('bots.urls', namespace='bots')),
path('admin/config/<slug>/', AdminConfigView.as_view(), name='admin_config'), path('admin/config/<slug>/', AdminConfigView.as_view(), name='admin_config'),
] ]

View File

@ -24,6 +24,7 @@ INSTALLED_APPS = [
'crispy_bootstrap4', 'crispy_bootstrap4',
'djconfig', 'djconfig',
'jsoneditor', 'jsoneditor',
'corsheaders',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@ -34,7 +35,6 @@ INSTALLED_APPS = [
'cabinet.apps.CabinetConfig', 'cabinet.apps.CabinetConfig',
'feeds.apps.FeedsConfig', 'feeds.apps.FeedsConfig',
'aggregator.apps.AggregatorConfig',
'bots.apps.BotsConfig', 'bots.apps.BotsConfig',
] ]
@ -43,6 +43,7 @@ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
@ -131,6 +132,8 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4'
JSON_EDITOR_JS = 'https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/7.0.4/jsoneditor.min.js' JSON_EDITOR_JS = 'https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/7.0.4/jsoneditor.min.js'
JSON_EDITOR_CSS = 'https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/7.0.4/jsoneditor.min.css' JSON_EDITOR_CSS = 'https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/7.0.4/jsoneditor.min.css'
CORS_ALLOW_ALL_ORIGINS = True
sentry_sdk.init( sentry_sdk.init(
dsn=env.str('SENTRY_DSN', None), dsn=env.str('SENTRY_DSN', None),
integrations=[DjangoIntegration()], integrations=[DjangoIntegration()],

View File

@ -2,18 +2,6 @@ from urllib.parse import urlparse
from django.forms import ModelForm from django.forms import ModelForm
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from pyrogram.types import Chat as PyrogramChat
from pyrogram.raw.types.chat import Chat as MTProtoChat
from pyrogram.raw.types.user import User as MTProtoUser
from pyrogram.raw.types.channel import Channel as MTProtoChannel
def parse_mtproto_chat(client, chat):
if isinstance(chat, MTProtoChat):
return PyrogramChat._parse_chat_chat(client, chat)
elif isinstance(chat, MTProtoUser):
return PyrogramChat._parse_user_chat(client, chat)
return PyrogramChat._parse_channel_chat(client, chat)
def same_origin(current_uri, redirect_uri): def same_origin(current_uri, redirect_uri):
@ -66,16 +54,3 @@ def get_config_form(mdl):
exclude = mdl.EXCLUDE_FIELDS if hasattr(mdl, 'EXCLUDE_FIELDS') else () exclude = mdl.EXCLUDE_FIELDS if hasattr(mdl, 'EXCLUDE_FIELDS') else ()
widgets = mdl.CUSTOM_WIDGETS if hasattr(mdl, 'CUSTOM_WIDGETS') else {} widgets = mdl.CUSTOM_WIDGETS if hasattr(mdl, 'CUSTOM_WIDGETS') else {}
return ConfigForm return ConfigForm
class AllowCORSMixin(object):
def add_access_control_headers(self, response):
response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
response["Access-Control-Max-Age"] = "1000"
response["Access-Control-Allow-Headers"] = "X-Requested-With, Content-Type"
def options(self, request, *args, **kwargs):
response = HttpResponse()
self.add_access_control_headers(response)
return response

49
pyproject.toml Normal file
View File

@ -0,0 +1,49 @@
[project]
name = "telegram-bots"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"boto3>=1.39.13",
"celery>=5.4.0",
"celery-once>=3.0.1",
"crispy-bootstrap4>=2024.1",
"daphne>=4.2.1",
"django==4.2.16",
"django-bootstrap4>=24.1",
"django-cors-headers>=4.7.0",
"django-crispy-forms>=2.3",
"django-djconfig>=0.11.0",
"django-environ>=0.11.3",
"django-extensions>=4.1",
"django-jsoneditor>=0.2.4",
"django-picklefield>=3.3",
"django-redis-cache>=3.0.1",
"django-timezone-field>=7.1",
"django-yamlfield>=1.2.2",
"feedparser>=6.0.11",
"ffmpeg-python>=0.2.0",
"filetype>=1.2.0",
"hiredis>=3.2.1",
"humanize>=4.12.3",
"imagehash>=4.3.2",
"json-rpc>=1.15.0",
"jsonfield2>=4.0.0.post0",
"markovify>=0.9.4",
"pillow>=11.3.0",
"psycopg2-binary>=2.9.10",
"python-anticaptcha>=1.0.0",
"python-telegram-bot==13.14",
"python-twitter>=3.5",
"qrtools>=0.0.2",
"sentry-sdk>=2.33.2",
"tgcrypto>=1.2.5",
"tiktokapi>=7.1.0",
"tqdm>=4.67.1",
"twisted[http2,tls]>=23.8.0",
"vk-api>=11.10.0",
"vkwave>=0.2.17",
"werkzeug>=3.1.3",
"whitenoise>=6.9.0",
]

3271
uv.lock Normal file

File diff suppressed because it is too large Load Diff