implement bots
This commit is contained in:
parent
bf76e3c3ed
commit
58806f2103
94
.gitignore
vendored
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="<map/>" />
|
||||
<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
bots/__init__.py
Normal file
0
bots/__init__.py
Normal file
3
bots/admin.py
Normal file
3
bots/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
bots/apps.py
Normal file
5
bots/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BotsConfig(AppConfig):
|
||||
name = 'bots'
|
19
bots/forms.py
Normal file
19
bots/forms.py
Normal 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',
|
0
bots/management/__init__.py
Normal file
0
bots/management/__init__.py
Normal file
0
bots/management/commands/__init__.py
Normal file
0
bots/management/commands/__init__.py
Normal file
53
bots/management/commands/run_bots.py
Normal file
53
bots/management/commands/run_bots.py
Normal 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()
|
33
bots/migrations/0001_initial.py
Normal file
33
bots/migrations/0001_initial.py
Normal 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')},
|
||||
),
|
||||
]
|
23
bots/migrations/0002_echobotmoduleconfig.py
Normal file
23
bots/migrations/0002_echobotmoduleconfig.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
18
bots/migrations/0003_telegrambot_active.py
Normal file
18
bots/migrations/0003_telegrambot_active.py
Normal 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),
|
||||
),
|
||||
]
|
28
bots/migrations/0004_auto_20191112_1908.py
Normal file
28
bots/migrations/0004_auto_20191112_1908.py
Normal 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),
|
||||
),
|
||||
]
|
23
bots/migrations/0005_channelhelperbotmoduleconfig.py
Normal file
23
bots/migrations/0005_channelhelperbotmoduleconfig.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
0
bots/migrations/__init__.py
Normal file
0
bots/migrations/__init__.py
Normal file
57
bots/models.py
Normal file
57
bots/models.py
Normal 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
4
bots/modules/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .channel_helper import ChannelHelperBotModuleConfig
|
||||
from .echo import EchoBotModuleConfig
|
||||
|
||||
BOT_MODULES = [EchoBotModuleConfig, ChannelHelperBotModuleConfig]
|
87
bots/modules/channel_helper.py
Normal file
87
bots/modules/channel_helper.py
Normal 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
18
bots/modules/echo.py
Normal 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
|
55
bots/templates/cabinet/bots/bot_form.html
Normal file
55
bots/templates/cabinet/bots/bot_form.html
Normal 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 %}
|
48
bots/templates/cabinet/bots/bot_list.html
Normal file
48
bots/templates/cabinet/bots/bot_list.html
Normal 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
3
bots/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
12
bots/urls.py
Normal file
12
bots/urls.py
Normal 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
39
bots/utils.py
Normal 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
96
bots/views.py
Normal 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
|
@ -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
|
||||
|
341
static/vendor/codemirror/lib/codemirror.css
vendored
Normal file
341
static/vendor/codemirror/lib/codemirror.css
vendored
Normal 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;
|
||||
}
|
||||
|
||||