From 58806f2103f1cc5b5d9bf34fd95effee2ab4f021 Mon Sep 17 00:00:00 2001 From: bakatrouble Date: Tue, 12 Nov 2019 20:13:56 +0300 Subject: [PATCH] implement bots --- .gitignore | 94 +- .idea/codeStyles/Project.xml | 52 - .idea/dataSources.xml | 28 - .idea/encodings.xml | 4 - .idea/inspectionProfiles/Project_Default.xml | 17 - .../markdown-navigator/profiles_settings.xml | 3 - .idea/misc.xml | 7 - .idea/modules.xml | 8 - .idea/telegram_bots.iml | 34 - .idea/vcs.xml | 8 - .idea/watcherTasks.xml | 25 - .idea/webResources.xml | 14 - bots/__init__.py | 0 bots/admin.py | 3 + bots/apps.py | 5 + bots/forms.py | 19 + bots/management/__init__.py | 0 bots/management/commands/__init__.py | 0 bots/management/commands/run_bots.py | 53 + bots/migrations/0001_initial.py | 33 + bots/migrations/0002_echobotmoduleconfig.py | 23 + bots/migrations/0003_telegrambot_active.py | 18 + bots/migrations/0004_auto_20191112_1908.py | 28 + .../0005_channelhelperbotmoduleconfig.py | 23 + bots/migrations/__init__.py | 0 bots/models.py | 57 + bots/modules/__init__.py | 4 + bots/modules/channel_helper.py | 87 + bots/modules/echo.py | 18 + bots/templates/cabinet/bots/bot_form.html | 55 + bots/templates/cabinet/bots/bot_list.html | 48 + bots/tests.py | 3 + bots/urls.py | 12 + bots/utils.py | 39 + bots/views.py | 96 + .../templates/cabinet/_includes/sidebar.html | 6 + cabinet/urls.py | 1 + config/settings.py | 2 +- config/suit_config.py | 5 - config/urls.py | 3 + config/utils.py | 24 + feeds/admin.py | 1 + feeds/forms.py | 10 - feeds/utils.py | 3 +- requirements.txt | 11 +- static/vendor/codemirror/lib/codemirror.css | 341 + static/vendor/codemirror/lib/codemirror.js | 9231 +++++++++++++++++ 47 files changed, 10251 insertions(+), 305 deletions(-) delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/dataSources.xml delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/markdown-navigator/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/telegram_bots.iml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/watcherTasks.xml delete mode 100644 .idea/webResources.xml create mode 100644 bots/__init__.py create mode 100644 bots/admin.py create mode 100644 bots/apps.py create mode 100644 bots/forms.py create mode 100644 bots/management/__init__.py create mode 100644 bots/management/commands/__init__.py create mode 100644 bots/management/commands/run_bots.py create mode 100644 bots/migrations/0001_initial.py create mode 100644 bots/migrations/0002_echobotmoduleconfig.py create mode 100644 bots/migrations/0003_telegrambot_active.py create mode 100644 bots/migrations/0004_auto_20191112_1908.py create mode 100644 bots/migrations/0005_channelhelperbotmoduleconfig.py create mode 100644 bots/migrations/__init__.py create mode 100644 bots/models.py create mode 100644 bots/modules/__init__.py create mode 100644 bots/modules/channel_helper.py create mode 100644 bots/modules/echo.py create mode 100644 bots/templates/cabinet/bots/bot_form.html create mode 100644 bots/templates/cabinet/bots/bot_list.html create mode 100644 bots/tests.py create mode 100644 bots/urls.py create mode 100644 bots/utils.py create mode 100644 bots/views.py delete mode 100644 config/suit_config.py create mode 100644 static/vendor/codemirror/lib/codemirror.css create mode 100644 static/vendor/codemirror/lib/codemirror.js diff --git a/.gitignore b/.gitignore index f944bfb..a306fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,88 +1,8 @@ -# Created by .ignore support plugin (hsz.mobi) -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml -.idea/**/gradle.xml -.idea/**/libraries -cmake-build-*/ -.idea/**/mongoSettings.xml -*.iws -out/ -.idea_modules/ -atlassian-ide-plugin.xml -.idea/replstate.xml -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties -.idea/httpRequests -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST -*.manifest -*.spec -pip-log.txt -pip-delete-this-directory.txt -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ -*.mo -*.pot -*.log -local_settings.py -db.sqlite3 -instance/ -.webassets-cache -.scrapy -docs/_build/ -target/ -.ipynb_checkpoints -.python-version -celerybeat-schedule -*.sage.py .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ -.spyderproject -.spyproject -.ropeproject -/site -.mypy_cache/ +db.sqlite3 +logs/* +*.py[co] +__pycache__ +!.gitkeep +.idea +build diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 49f7b87..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index 0233bb4..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - sqlite.xerial - true - org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/db.sqlite3 - - - - - - file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/sqlite-jdbc-3.20.1.1.jar - - - file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/xerial-sqlite-license.txt - - - file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.16.1/sqlite-jdbc-3.16.1.jar - - - file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.16.1/xerial-sqlite-license.txt - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 15a15b2..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 52a93e4..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator/profiles_settings.xml b/.idea/markdown-navigator/profiles_settings.xml deleted file mode 100644 index db06266..0000000 --- a/.idea/markdown-navigator/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index ddb21c6..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 353a43c..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/telegram_bots.iml b/.idea/telegram_bots.iml deleted file mode 100644 index 4c19a44..0000000 --- a/.idea/telegram_bots.iml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index bfd2f6f..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml deleted file mode 100644 index 0f1f38c..0000000 --- a/.idea/watcherTasks.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/webResources.xml b/.idea/webResources.xml deleted file mode 100644 index 614e30f..0000000 --- a/.idea/webResources.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/bots/__init__.py b/bots/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/admin.py b/bots/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/bots/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/bots/apps.py b/bots/apps.py new file mode 100644 index 0000000..ac94929 --- /dev/null +++ b/bots/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class BotsConfig(AppConfig): + name = 'bots' diff --git a/bots/forms.py b/bots/forms.py new file mode 100644 index 0000000..4b25d99 --- /dev/null +++ b/bots/forms.py @@ -0,0 +1,19 @@ +from django.forms import ModelForm + +from bots.models import TelegramBot + + +class BotForm(ModelForm): + prefix = 'bot' + + def __init__(self, *args, module, **kwargs): + super().__init__(*args, **kwargs) + if not hasattr(module, 'rpc_dispatcher'): + self.fields.pop('rpc_name') + if not hasattr(module, 'periodic_task'): + self.fields.pop('periodic_interval') + self.fields.pop('periodic_last_run') + + class Meta: + model = TelegramBot + exclude = 'owner', 'config_type', 'config_id', diff --git a/bots/management/__init__.py b/bots/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/management/commands/__init__.py b/bots/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/management/commands/run_bots.py b/bots/management/commands/run_bots.py new file mode 100644 index 0000000..8a34315 --- /dev/null +++ b/bots/management/commands/run_bots.py @@ -0,0 +1,53 @@ +import logging +import traceback + +import sentry_sdk +from django.core.cache import cache +from django.core.management import BaseCommand +from telegram import TelegramError +from telegram.error import TimedOut + +from bots.models import TelegramBot + + +class Command(BaseCommand): + def handle(self, *args, **options): + dispatchers = [] + while True: + try: + if not dispatchers or cache.get('bots_reset'): + logging.warning('Reloading dispatchers') + dispatchers = [] + for bot in TelegramBot.objects.filter(active=True): + try: + dispatcher = bot.build_dispatcher() + dispatcher.last_update_id = 0 + dispatchers.append(dispatcher) + except TelegramError: + pass + cache.delete('bots_reset') + + for dispatcher in dispatchers: + try: + updates = dispatcher.bot.get_updates(dispatcher.last_update_id) + except TimedOut: + continue + except TelegramError as e: + sentry_sdk.capture_exception(e) + traceback.print_exc() + updates = [] + + for update in updates: + try: + dispatcher.process_update(update) + except KeyboardInterrupt: + return + except Exception as e: + sentry_sdk.capture_exception(e) + traceback.print_exc() + dispatcher.last_update_id = update.update_id + 1 + except KeyboardInterrupt: + return + except Exception as e: + sentry_sdk.capture_exception(e) + traceback.print_exc() diff --git a/bots/migrations/0001_initial.py b/bots/migrations/0001_initial.py new file mode 100644 index 0000000..a0e0718 --- /dev/null +++ b/bots/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 2.1.5 on 2019-11-12 15:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='TelegramBot', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=32)), + ('bot_token', models.CharField(max_length=256)), + ('config_id', models.PositiveIntegerField()), + ('config_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='telegrambot', + unique_together={('config_type', 'config_id')}, + ), + ] diff --git a/bots/migrations/0002_echobotmoduleconfig.py b/bots/migrations/0002_echobotmoduleconfig.py new file mode 100644 index 0000000..490adae --- /dev/null +++ b/bots/migrations/0002_echobotmoduleconfig.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.5 on 2019-11-12 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EchoBotModuleConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('prefix', models.TextField()), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/bots/migrations/0003_telegrambot_active.py b/bots/migrations/0003_telegrambot_active.py new file mode 100644 index 0000000..35a4c8d --- /dev/null +++ b/bots/migrations/0003_telegrambot_active.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.5 on 2019-11-12 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0002_echobotmoduleconfig'), + ] + + operations = [ + migrations.AddField( + model_name='telegrambot', + name='active', + field=models.BooleanField(default=True), + ), + ] diff --git a/bots/migrations/0004_auto_20191112_1908.py b/bots/migrations/0004_auto_20191112_1908.py new file mode 100644 index 0000000..af4b763 --- /dev/null +++ b/bots/migrations/0004_auto_20191112_1908.py @@ -0,0 +1,28 @@ +# Generated by Django 2.1.5 on 2019-11-12 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0003_telegrambot_active'), + ] + + operations = [ + migrations.AddField( + model_name='telegrambot', + name='periodic_interval', + field=models.DurationField(blank=True, null=True), + ), + migrations.AddField( + model_name='telegrambot', + name='periodic_last_run', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='telegrambot', + name='rpc_name', + field=models.CharField(blank=True, max_length=32, null=True), + ), + ] diff --git a/bots/migrations/0005_channelhelperbotmoduleconfig.py b/bots/migrations/0005_channelhelperbotmoduleconfig.py new file mode 100644 index 0000000..72e8b5a --- /dev/null +++ b/bots/migrations/0005_channelhelperbotmoduleconfig.py @@ -0,0 +1,23 @@ +# Generated by Django 2.1.5 on 2019-11-12 16:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0004_auto_20191112_1908'), + ] + + operations = [ + migrations.CreateModel( + name='ChannelHelperBotModuleConfig', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('chat_id', models.CharField(max_length=32)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/bots/migrations/__init__.py b/bots/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bots/models.py b/bots/models.py new file mode 100644 index 0000000..fc61be5 --- /dev/null +++ b/bots/models.py @@ -0,0 +1,57 @@ +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.models import ContentType +from django.db import models +from telegram import Bot +from telegram.ext import Dispatcher + + +class TelegramBot(models.Model): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + active = models.BooleanField(default=True) + title = models.CharField(max_length=32) + bot_token = models.CharField(max_length=256) + + rpc_name = models.CharField(max_length=32, blank=True, null=True) + periodic_interval = models.DurationField(blank=True, null=True) + periodic_last_run = models.DateTimeField(blank=True, null=True) + + config_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + config_id = models.PositiveIntegerField() + config = GenericForeignKey('config_type', 'config_id') + + def get_bot(self): + return Bot(self.bot_token) + + def build_dispatcher(self): + bot = self.get_bot() + bot.get_me() + dispatcher = Dispatcher(bot, None, workers=0, use_context=True) + self.config.build_dispatcher(dispatcher) + return dispatcher + + def __str__(self): + return f'#{self.pk} {self.title}' + + class Meta: + unique_together = ('config_type', 'config_id') + + +class TelegramBotModuleConfig(models.Model): + _bot = GenericRelation(TelegramBot, content_type_field='config_type', object_id_field='config_id') + + MODULE_NAME = '' + + @property + def bot(self): + return self._bot.get() + + @property + def content_type(self): + return ContentType.objects.get_for_model(self.__class__) + + def build_dispatcher(self, dispatcher: Dispatcher): + raise NotImplementedError() + + class Meta: + abstract = True diff --git a/bots/modules/__init__.py b/bots/modules/__init__.py new file mode 100644 index 0000000..0126892 --- /dev/null +++ b/bots/modules/__init__.py @@ -0,0 +1,4 @@ +from .channel_helper import ChannelHelperBotModuleConfig +from .echo import EchoBotModuleConfig + +BOT_MODULES = [EchoBotModuleConfig, ChannelHelperBotModuleConfig] diff --git a/bots/modules/channel_helper.py b/bots/modules/channel_helper.py new file mode 100644 index 0000000..c1be835 --- /dev/null +++ b/bots/modules/channel_helper.py @@ -0,0 +1,87 @@ +import base64 +import os +import tempfile +from io import BytesIO +from uuid import uuid4 + +import requests +from PIL import Image +from django.db import models +from telegram import Update +from telegram.ext import Dispatcher, CallbackContext, MessageHandler, Filters +from jsonrpc import Dispatcher as RPCDispatcher + +from bots.models import TelegramBotModuleConfig + + +class ChannelHelperBotModuleConfig(TelegramBotModuleConfig): + chat_id = models.CharField(max_length=32) + + MODULE_NAME = 'Channel helper' + + rpc_dispatcher = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.rpc_dispatcher = RPCDispatcher() + self.rpc_dispatcher['post_photo'] = self.rpc_post_photo + + def rpc_post_photo(self, photo, is_base64=False): + try: + if is_base64: + f = BytesIO(base64.b64decode(photo)) + else: + resp = requests.get(photo) + resp.raise_for_status() + f = BytesIO(resp.content) + except: + raise RuntimeError('Could not load image') + im = Image.open(f) # type: Image.Image + width, height = im.size + if width > 2000 or height > 2000: + im.thumbnail((2000, 2000)) + with tempfile.TemporaryDirectory() as d: + fpath = os.path.join(d, '{}.jpg'.format(uuid4())) + im.save(fpath) + self.bot.get_bot().send_photo(self.chat_id, open(fpath, 'rb')) + return True + + def handle_message(self, update: Update, ctx: CallbackContext): + m = update.effective_message + bot = ctx.bot + if hasattr(m, 'audio') and m.audio: + a = m.audio + r = bot.send_audio(self.chat_id, a.file_id, a.duration, a.performer, a.title) + elif hasattr(m, 'document') and m.document: + d = m.document + r = bot.send_document(self.chat_id, d.file_id, d.file_name) + elif hasattr(m, 'photo') and m.photo: + p = m.photo + r = bot.send_photo(self.chat_id, p[-1].file_id) + elif hasattr(m, 'sticker') and m.sticker: + s = m.sticker + r = bot.send_sticker(self.chat_id, s.file_id) + elif hasattr(m, 'video') and m.video: + v = m.video + r = bot.send_video(self.chat_id, v.file_id, v.duration) + elif hasattr(m, 'voice') and m.voice: + v = m.voice + r = bot.send_voice(self.chat_id, v.file_id, v.duration) + elif hasattr(m, 'video_note') and m.video_note: + vn = m.video_note + r = bot.send_video_note(self.chat_id, vn.file_id, vn.duration, vn.length) + elif hasattr(m, 'contact') and m.contact: + c = m.contact + r = bot.send_contact(self.chat_id, c.phone_number, c.first_name, c.last_name) + elif hasattr(m, 'location') and m.location: + l = m.location + r = bot.send_location(self.chat_id, l.latitude, l.longitude) + elif hasattr(m, 'venue') and m.venue: + v = m.venue + r = bot.send_venue(self.chat_id, v.location.latitude, v.location.longitude, v.title, v.address, v.foursquare_id) + elif hasattr(m, 'text') and m.text: + r = bot.send_message(self.chat_id, m.text_html, 'html') + + def build_dispatcher(self, dispatcher: Dispatcher): + dispatcher.add_handler(MessageHandler(Filters.all, self.handle_message)) + return dispatcher diff --git a/bots/modules/echo.py b/bots/modules/echo.py new file mode 100644 index 0000000..48d9c36 --- /dev/null +++ b/bots/modules/echo.py @@ -0,0 +1,18 @@ +from django.db import models +from telegram import Update +from telegram.ext import Dispatcher, CallbackContext, MessageHandler, Filters + +from bots.models import TelegramBotModuleConfig + + +class EchoBotModuleConfig(TelegramBotModuleConfig): + prefix = models.TextField() + + MODULE_NAME = 'Echo' + + def message_handler(self, update: Update, ctx: CallbackContext): + update.effective_message.reply_text(self.prefix + update.effective_message.text) + + def build_dispatcher(self, dispatcher: Dispatcher): + dispatcher.add_handler(MessageHandler(Filters.text, self.message_handler)) + return dispatcher diff --git a/bots/templates/cabinet/bots/bot_form.html b/bots/templates/cabinet/bots/bot_form.html new file mode 100644 index 0000000..dbe3607 --- /dev/null +++ b/bots/templates/cabinet/bots/bot_form.html @@ -0,0 +1,55 @@ +{% extends 'cabinet/_internal_base.html' %} +{% load bootstrap4 %} + +{% block breadcrumbs %} +
  • Bot list
  • +
  • {{ title }}
  • +{% endblock %} + +{% block content %} +
    + {% csrf_token %} +
    +

    {% if feed %}Bot "{{ feed.title }}" configuration{% else %}New bot{% endif %}

    +
    +
    +

    General options

    + {% bootstrap_form bot_form layout='horizontal' %} +
    +

    Module options

    + {% bootstrap_form config_form layout='horizontal' %} +
    +
    + {% if feed %}Delete{% endif %} + +
    +
    + + {% if feed %} + + {% endif %} +{% endblock %} diff --git a/bots/templates/cabinet/bots/bot_list.html b/bots/templates/cabinet/bots/bot_list.html new file mode 100644 index 0000000..d952bc7 --- /dev/null +++ b/bots/templates/cabinet/bots/bot_list.html @@ -0,0 +1,48 @@ +{% extends 'cabinet/_internal_base.html' %} + +{% block breadcrumbs %} +
  • Bot list
  • +{% endblock %} + +{% block content %} +
    +
    +
    + +
    +

    Your bots

    +
    +
    + + + + + + + + {% for bot in object_list %} + + + + + {% empty %} + + + + + + {% endfor %} +
    TitleModule
    {{ bot.title }}{{ bot.config.MODULE_NAME }}
    No bots added
    +
    +
    +{% endblock %} diff --git a/bots/tests.py b/bots/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/bots/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bots/urls.py b/bots/urls.py new file mode 100644 index 0000000..823b031 --- /dev/null +++ b/bots/urls.py @@ -0,0 +1,12 @@ +from django.urls import path + +from bots.views import BotListView, BotConfigEditView, BotConfigDeleteView, BotConfigCreateView + +app_name = 'bots' +urlpatterns = [ + path('', BotListView.as_view(), name='index'), + path('/', BotConfigEditView.as_view(), name='edit'), + path('new//', BotConfigCreateView.as_view(), name='new'), + path('delete//', BotConfigDeleteView.as_view(), name='delete'), +] + diff --git a/bots/utils.py b/bots/utils.py new file mode 100644 index 0000000..5430057 --- /dev/null +++ b/bots/utils.py @@ -0,0 +1,39 @@ +from django.views.generic import TemplateView + +from bots.forms import BotForm +from bots.models import TelegramBot +from cabinet.utils import CabinetViewMixin +from config.utils import get_config_form + + +class BaseBotConfigView(CabinetViewMixin, TemplateView): + template_name = 'cabinet/bots/bot_form.html' + context_object_name = 'feed' + + def get_queryset(self): + return TelegramBot.objects.filter(owner=self.request.user) + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return self.render_to_response(self.get_context_data()) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + bot_form, config_form = self.get_forms() + if bot_form.is_valid() and config_form.is_valid(): + return self.form_valid(bot_form, config_form) + else: + context = self.get_context_data(forms=(bot_form, config_form)) + return self.render_to_response(context) + + def get_forms(self): + bot = self.get_object() + data = self.request.POST if self.request.method == 'POST' else None + return BotForm(data=data, instance=bot, module=self.get_content_type().model_class()), \ + get_config_form(self.get_content_type().model_class())(data=data, instance=bot.config if bot else None) + + def get_context_data(self, forms=None, **kwargs): + ctx = super(BaseBotConfigView, self).get_context_data(**kwargs) + ctx['bot_form'], ctx['config_form'] = self.get_forms() if forms is None else forms + ctx['bot_module'] = self.get_content_type().model_class() + return ctx \ No newline at end of file diff --git a/bots/views.py b/bots/views.py new file mode 100644 index 0000000..fa3206a --- /dev/null +++ b/bots/views.py @@ -0,0 +1,96 @@ +from braces.views import AjaxResponseMixin, JSONResponseMixin +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.contenttypes.models import ContentType +from django.core.cache import cache +from django.core.exceptions import PermissionDenied +from django.http import HttpResponseRedirect, HttpRequest +from django.shortcuts import redirect, get_object_or_404 +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import ListView +from django.views.generic.detail import SingleObjectMixin +from jsonrpc import JSONRPCResponseManager + +from bots.models import TelegramBot +from bots.modules import BOT_MODULES +from bots.utils import BaseBotConfigView +from cabinet.utils import CabinetViewMixin +from config.utils import AllowCORSMixin + + +class BotListView(CabinetViewMixin, ListView): + template_name = 'cabinet/bots/bot_list.html' + title = 'Bot list' + sidebar_section = 'bots' + + def get_queryset(self): + return TelegramBot.objects.filter(owner=self.request.user) + + def get_context_data(self, *args, **kwargs): + ctx = super(BotListView, self).get_context_data(*args, **kwargs) + ctx['bot_modules'] = BOT_MODULES + return ctx + + +class BotConfigEditView(BaseBotConfigView, SingleObjectMixin): + title = 'Configure bot' + sidebar_section = 'bots' + + def form_valid(self, bot_form, config_form): + bot_form.save() + config_form.save() + cache.set('bots_reset', '1') + messages.success(self.request, 'Config was successfully saved') + return HttpResponseRedirect('') + + def get_content_type(self): + return self.get_object().config_type + + +class BotConfigCreateView(BaseBotConfigView): + title = 'Create bot' + sidebar_section = 'bots' + + def get_object(self): + return None + + def form_valid(self, bots_form, config_form): + config_form.save() + bots_form.instance.owner = self.request.user + bots_form.instance.config_type = self.get_content_type() + bots_form.instance.config_id = config_form.instance.pk + bots_form.save() + cache.set('bots_reset', '1') + messages.success(self.request, 'Config was successfully saved') + return redirect('cabinet:bots:edit', pk=bots_form.instance.pk) + + def get_content_type(self): + return get_object_or_404(ContentType, model=self.kwargs['content_type']) + + +class BotConfigDeleteView(LoginRequiredMixin, SingleObjectMixin, View): + def get_queryset(self): + return TelegramBot.objects.filter(owner=self.request.user) + + def post(self, request, *args, **kwargs): + feed = self.get_object() + messages.success(self.request, 'Bot "{}" was successfully deleted'.format(feed.title)) + feed.delete() + return redirect('cabinet:bots:index') + + +class BotRPCView(JSONResponseMixin, AjaxResponseMixin, AllowCORSMixin, View): + @method_decorator(csrf_exempt) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + + def post(self, request: HttpRequest, *args, **kwargs): + bot = get_object_or_404(TelegramBot, rpc_name=self.kwargs.get('endpoint')) + if not hasattr(bot.config, 'rpc_dispatcher'): + raise PermissionDenied() + rpc_response = JSONRPCResponseManager.handle(request.body, bot.config.rpc_dispatcher) + response = self.render_json_response(rpc_response.data) + self.add_access_control_headers(response) + return response diff --git a/cabinet/templates/cabinet/_includes/sidebar.html b/cabinet/templates/cabinet/_includes/sidebar.html index 8218888..5b50760 100644 --- a/cabinet/templates/cabinet/_includes/sidebar.html +++ b/cabinet/templates/cabinet/_includes/sidebar.html @@ -15,6 +15,12 @@ Feeds +