switch to uv and add cors headers
This commit is contained in:
parent
0ea2ca744a
commit
7f47ac6849
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.9
|
53
Pipfile
53
Pipfile
@ -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
2148
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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'),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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',
|
|
@ -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
|
|
@ -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()
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -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'),
|
|
||||||
]
|
|
||||||
|
|
@ -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')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
@ -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()],
|
||||||
|
@ -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
49
pyproject.toml
Normal 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",
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user