implement bots

This commit is contained in:
bakatrouble 2019-11-12 20:13:56 +03:00
parent bf76e3c3ed
commit 58806f2103
47 changed files with 10251 additions and 305 deletions

94
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
bots/__init__.py Normal file
View File

3
bots/admin.py Normal file
View File

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

5
bots/apps.py Normal file
View File

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

19
bots/forms.py Normal file
View File

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

View File

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

57
bots/models.py Normal file
View File

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

4
bots/modules/__init__.py Normal file
View File

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

View File

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

18
bots/modules/echo.py Normal file
View File

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

View File

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

View File

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

3
bots/tests.py Normal file
View File

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

12
bots/urls.py Normal file
View File

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

39
bots/utils.py Normal file
View File

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

96
bots/views.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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