implement bots

master
bakatrouble 5 years ago
parent bf76e3c3ed
commit 58806f2103

94
.gitignore vendored

@ -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

@ -1,52 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<DBN-PSQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false" />
</DBN-PSQL>
<DBN-SQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false">
<option name="STATEMENT_SPACING" value="one_line" />
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
</formatting-settings>
</DBN-SQL>
<DBN-PSQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false" />
</DBN-PSQL>
<DBN-SQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false">
<option name="STATEMENT_SPACING" value="one_line" />
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
</formatting-settings>
</DBN-SQL>
</code_scheme>
</component>

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db" uuid="c627db26-52a1-49af-829b-0c777acb061b">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db.sqlite3</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/sqlite-jdbc-3.20.1.1.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/xerial-sqlite-license.txt</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.16.1/sqlite-jdbc-3.16.1.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.16.1/xerial-sqlite-license.txt</url>
</library>
</libraries>
</data-source>
</component>
</project>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

@ -1,17 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="django-compressor-develop" />
<item index="1" class="java.lang.String" itemvalue="django-recaptcha-develop" />
<item index="2" class="java.lang.String" itemvalue="django-robokassa" />
<item index="3" class="java.lang.String" itemvalue="python-dateutil" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

@ -1,3 +0,0 @@
<component name="MarkdownNavigator.ProfileManager">
<settings default="" pdf-export="" plain-text-search-scope="Project Files" />
</component>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (telegram_bots)" project-jdk-type="Python SDK" />
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/telegram_bots.iml" filepath="$PROJECT_DIR$/.idea/telegram_bots.iml" />
</modules>
</component>
</project>

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="config/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.7 (telegram_bots)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
<option value="$MODULE_DIR$/feeds/templates" />
<option value="$MODULE_DIR$/cabinet/templates" />
<option value="$MODULE_DIR$/aggregator/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/build/modular-admin-html" vcs="Git" />
<mapping directory="$PROJECT_DIR$/build/octopus" vcs="Git" />
</component>
</project>

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="$FileName$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="styl" />
<option name="immediateSync" value="true" />
<option name="name" value="Stylus" />
<option name="output" value="$FileNameWithoutExtension$.css" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="stylus" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$FileDir$" />
<envs />
</TaskOptions>
</component>
</project>

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/static" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class BotsConfig(AppConfig):
name = 'bots'

@ -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',

@ -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()

@ -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')},
),
]

@ -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,
},
),
]

@ -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),
),
]

@ -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),
),
]

@ -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,
},
),
]

@ -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 = '<Not set>'
@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

@ -0,0 +1,4 @@
from .channel_helper import ChannelHelperBotModuleConfig
from .echo import EchoBotModuleConfig
BOT_MODULES = [EchoBotModuleConfig, ChannelHelperBotModuleConfig]

@ -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

@ -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

@ -0,0 +1,55 @@
{% extends 'cabinet/_internal_base.html' %}
{% load bootstrap4 %}
{% block breadcrumbs %}
<li><a href="{% url 'cabinet:bots:index' %}">Bot 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 feed %}Bot "{{ feed.title }}" configuration{% else %}New bot{% endif %}</h2>
</header>
<div class="card-body">
<h4>General options</h4>
{% bootstrap_form bot_form layout='horizontal' %}
<hr>
<h4>Module options</h4>
{% bootstrap_form config_form layout='horizontal' %}
</div>
<footer class="card-footer text-right">
{% if feed %}<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 feed %}
<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 bot "{{ feed.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:bots:delete' pk=feed.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 %}

@ -0,0 +1,48 @@
{% extends 'cabinet/_internal_base.html' %}
{% block breadcrumbs %}
<li><span>Bot list</span></li>
{% endblock %}
{% block content %}
<section class="card">
<header class="card-header">
<div class="card-button-actions">
<div class="dropdown">
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-toggle="dropdown">
Add new bot
</button>
<div class="dropdown-menu dropdown-menu-right">
{% for module in bot_modules %}
<a class="dropdown-item"
href="{% url 'cabinet:bots:new' content_type=module.content_type.model %}">{{ module.MODULE_NAME }}</a>
{% endfor %}
</div>
</div>
</div>
<h2 class="card-title">Your bots</h2>
</header>
<div class="card-body">
<table class="table table-hover table-responsive-md mb-0">
<thead>
<tr>
<th>Title</th>
<th>Module</th>
</tr>
</thead>
{% for bot in object_list %}
<tr class="clickable-row" data-href="{% url 'cabinet:bots:edit' pk=bot.pk %}">
<td>{{ bot.title }}</td>
<td>{{ bot.config.MODULE_NAME }}</td>
</tr>
{% empty %}
<tfoot>
<tr>
<td colspan="4">No bots added</td>
</tr>
</tfoot>
{% endfor %}
</table>
</div>
</section>
{% endblock %}

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

@ -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('<int:pk>/', BotConfigEditView.as_view(), name='edit'),
path('new/<content_type>/', BotConfigCreateView.as_view(), name='new'),
path('delete/<int:pk>/', BotConfigDeleteView.as_view(), name='delete'),
]

@ -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

@ -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

@ -15,6 +15,12 @@
<span>Feeds</span>
</a>
</li>
<li class="{% if sidebar_section == 'bots' %}nav-active{% endif %}">
<a href="{% url 'cabinet:bots:index' %}">
<i class="fas fa-robot" aria-hidden="true"></i>
<span>Telegram bots</span>
</a>
</li>
<li class="{% if sidebar_section == 'aggregator' %}nav-active{% endif %}">
<a href="{% url 'cabinet:aggregator:index' %}">
<i class="fas fa-copy" aria-hidden="true"></i>

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

@ -23,7 +23,6 @@ INSTALLED_APPS = [
'djconfig',
'django_celery_results',
'django_celery_beat',
'config.suit_config.SuitConfig',
'django.contrib.admin',
'django.contrib.auth',
@ -35,6 +34,7 @@ INSTALLED_APPS = [
'cabinet.apps.CabinetConfig',
'feeds.apps.FeedsConfig',
'aggregator.apps.AggregatorConfig',
'bots.apps.BotsConfig',
]
MIDDLEWARE = [

@ -1,5 +0,0 @@
from suit.apps import DjangoSuitConfig
class SuitConfig(DjangoSuitConfig):
layout = 'vertical'

@ -2,8 +2,11 @@ from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
from bots.views import BotRPCView
urlpatterns = [
path('', RedirectView.as_view(pattern_name='cabinet:index')),
path('config/', admin.site.urls),
path('cabinet/', include('cabinet.urls', namespace='cabinet')),
path('bots_rpc/<str:endpoint>/', BotRPCView.as_view(), name='bots_rpc'),
]

@ -1,5 +1,6 @@
from urllib.parse import urlparse
from django.forms import ModelForm
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from pyrogram import Chat as PyrogramChat
from pyrogram.api.types.chat import Chat as MTProtoChat
@ -54,3 +55,26 @@ class TurbolinksMiddleware:
response['Turbolinks-Location'] = loc
return response
def get_config_form(mdl):
class ConfigForm(ModelForm):
prefix = 'config'
class Meta:
model = mdl
exclude = ()
return ConfigForm
class AllowCORSMixin(object):
def add_access_control_headers(self, response):
response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Methods"] = "GET, 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

@ -4,6 +4,7 @@ from .models import Feed
from feeds.modules import EchoFeedModuleConfig, DankMemesFeedModuleConfig, ShittyWatercolourFeedModuleConfig,\
VKFeedModuleConfig, VKMusicFeedModuleConfig, WPComicFeedModuleConfig
@admin.register(Feed)
class FeedAdmin(admin.ModelAdmin):
list_display = '__str__', 'lock',

@ -15,16 +15,6 @@ class FeedForm(ModelForm):
exclude = 'owner', 'lock', 'config_type', 'config_id', 'last_check', 'last_id',
def get_config_form(mdl):
class ConfigForm(ModelForm):
prefix = 'config'
class Meta:
model = mdl
exclude = ()
return ConfigForm
class FeedsAppConfigForm(ConfigForm):
slug = 'feeds'
title = 'Feeds'

@ -9,7 +9,8 @@ from python_anticaptcha import AnticaptchaClient, ImageToTextTask
from yaml.parser import ParserError
from cabinet.utils import CabinetViewMixin
from feeds.forms import FeedForm, get_config_form, FeedsAppConfigForm
from feeds.forms import FeedForm, FeedsAppConfigForm
from config.utils import get_config_form
from feeds.models import Feed

@ -1,37 +1,45 @@
amqp==2.3.2
asn1crypto==0.24.0
async-generator==1.10
beautifulsoup4==4.7.1
billiard==3.5.0.5
bs4==0.0.1
celery==4.2.1
celery-once==2.0.0
certifi==2018.11.29
cffi==1.12.3
chardet==3.0.4
cryptography==2.7
Django==2.1.5
django-bootstrap4==0.0.7
django-braces==1.13.0
django-celery-beat==1.4.0
django-celery-results==1.0.4
django-crispy-forms==1.7.2
django-djconfig==0.9.0
django-environ==0.4.5
django-extensions==2.1.4
django-jet==1.0.8
django-redis==4.10.0
-e git+https://github.com/darklow/django-suit@9941211bc7d63d94478a3c6479c51926dc75db09#egg=django_suit
django-timezone-field==3.0
django-yamlfield==1.0.3
enum34==1.1.6
future==0.17.1
idna==2.8
json-rpc==1.12.1
kombu==4.2.2.post1
oauthlib==3.0.1
Pillow==5.4.1
psycopg2-binary==2.7.6.1
pyaes==1.6.1
pycparser==2.19
Pyrogram==0.11.0
PySocks==1.6.8
pyTelegramBotAPI==3.6.6
python-anticaptcha==0.3.1
python-crontab==2.3.6
python-dateutil==2.7.5
python-telegram-bot==12.2.0
python-twitter==3.5
pytz==2018.9
PyYAML==3.13
@ -42,6 +50,7 @@ sentry-sdk==0.6.9
six==1.12.0
soupsieve==1.7.3
TgCrypto==1.1.1
tornado==6.0.3
urllib3==1.24.1
uWSGI==2.0.17.1
vine==1.2.0

@ -0,0 +1,341 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save