├── .gitignore ├── DOKKU_SCALE ├── Dockerfile ├── Procfile ├── README.md ├── docker-compose.yml ├── dtb ├── __init__.py ├── asgi.py ├── celery.py ├── settings.py ├── urls.py └── wsgi.py ├── entrypoint.sh ├── logs └── .keep ├── manage.py ├── media └── .keep ├── requirements.txt ├── run_pooling.py ├── runtime.txt └── tgbot ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── handlers ├── __init__.py ├── admin.py ├── commands.py ├── dispatcher.py ├── files.py ├── handlers.py ├── keyboard_utils.py ├── location.py ├── manage_data.py ├── static_text.py └── utils.py ├── migrations ├── 0001_initial.py ├── 0002_arcgis.py ├── 0002_log.py ├── 0003_useractionlog_text.py └── __init__.py ├── models.py ├── tasks.py ├── templates └── admin │ └── broadcast_message.html ├── urls.py ├── utils.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | staticfiles/* 2 | !staticfiles/.keep 3 | 4 | media/* 5 | !media/.keep 6 | 7 | logs/* 8 | # exception to the rule 9 | !logs/.keep 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | pip-wheel-metadata/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .nox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | *.py,cover 61 | .hypothesis/ 62 | .pytest_cache/ 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | db.sqlite3 72 | db.sqlite3-journal 73 | 74 | # Flask stuff: 75 | instance/ 76 | .webassets-cache 77 | 78 | # Scrapy stuff: 79 | .scrapy 80 | 81 | # Sphinx documentation 82 | docs/_build/ 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | .DS_Store 141 | 142 | #PyCharm 143 | .idea/ 144 | 145 | #Docker 146 | .Dockerfile.swp -------------------------------------------------------------------------------- /DOKKU_SCALE: -------------------------------------------------------------------------------- 1 | web=1 2 | worker=1 3 | beat=1 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | ENV PYTHONUNBUFFERED=1 4 | 5 | RUN mkdir /code 6 | WORKDIR /code 7 | 8 | COPY requirements.txt /code/ 9 | RUN pip install --upgrade pip 10 | RUN pip install -r requirements.txt 11 | 12 | COPY entrypoint.sh /entrypoint.sh 13 | RUN chmod +x /entrypoint.sh 14 | 15 | COPY . /code/ 16 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: python manage.py migrate --noinput 2 | web: gunicorn --bind :$PORT --workers 4 --worker-class uvicorn.workers.UvicornWorker dtb.asgi:application 3 | worker: celery -A dtb worker --loglevel=INFO 4 | beat: celery -A dtb beat --loglevel=INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Info 2 | Django Telegram Bot Skeleton. 3 | 4 | Based on: https://github.com/ohld/django-telegram-bot/ 5 | 6 | # Установка 7 | Проект настроен для деплоя через Dokku. Берем VPS, ставим dokku, создаем приложение там, postgres, redis, git. 8 | Создаем необходимые переменные окружения: 9 | 10 | BUILDPACK_URL: https://github.com/heroku/heroku-buildpack-python.git#v191 11 | 12 | DISABLE_COLLECTSTATIC: 1 13 | 14 | DJANGO_DEBUG: False 15 | 16 | DOKKU_LETSENCRYPT_EMAIL: email@gmail.com 17 | 18 | MEDIA_DOMAIN: https://my_site.com 19 | 20 | WEB_DOMAIN: https://my_site.com 21 | 22 | TELEGRAM_TOKEN: tg_token_from_bot_father 23 | 24 | Говорит TG, что будем webhook-ом работать через запрос: 25 | 26 | https://api.telegram.org/bot[ТУТ_ТОКЕН]/setWebhook?url=[ТУТ_URL_WEBHOOK]/super_secter_webhook/ 27 | 28 | Пушим проект в dokku и готово. Детальнее тут: https://github.com/ohld/django-telegram-bot/wiki/Production-Deployment-using-Dokku 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | db: 5 | image: postgres:12 6 | container_name: postgres_db_poetry 7 | restart: always 8 | volumes: 9 | - postgres_data:/var/lib/postgresql/data/ 10 | - ./sql:/sql/ 11 | env_file: 12 | - ./.env 13 | ports: 14 | - 5434:5432 15 | redis: 16 | image: redis:alpine 17 | container_name: redis_poetry 18 | web: 19 | build: . 20 | container_name: django_poetry 21 | command: bash -c "python manage.py runserver 0.0.0.0:8001" 22 | volumes: 23 | - .:/code 24 | ports: 25 | - 8001:8000 26 | environment: 27 | - DJANGO_DEBUG='True' 28 | env_file: 29 | - ./.env 30 | depends_on: 31 | - db 32 | - redis 33 | entrypoint: /entrypoint.sh 34 | bot: 35 | build: . 36 | container_name: tg_bot_poetry 37 | command: python run_pooling.py 38 | volumes: 39 | - .:/code 40 | env_file: 41 | - ./.env 42 | depends_on: 43 | - web 44 | celery: 45 | build: . 46 | container_name: celery_poetry 47 | command: celery -A dtb worker --loglevel=INFO 48 | volumes: 49 | - .:/code 50 | env_file: 51 | - ./.env 52 | depends_on: 53 | - redis 54 | - web 55 | celery-beat: 56 | build: . 57 | container_name: celery_beat_poetry 58 | command: celery -A dtb beat --loglevel=INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler 59 | volumes: 60 | - .:/code 61 | env_file: 62 | - ./.env 63 | depends_on: 64 | - redis 65 | - celery 66 | - web 67 | 68 | volumes: 69 | postgres_data: -------------------------------------------------------------------------------- /dtb/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | 7 | __all__ = ('celery_app',) -------------------------------------------------------------------------------- /dtb/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for dtb project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dtb.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /dtb/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | # set the default Django settings module for the 'celery' program. 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dtb.settings') 6 | 7 | app = Celery('dtb') 8 | 9 | # Using a string here means the worker doesn't have to serialize 10 | # the configuration object to child processes. 11 | # - namespace='CELERY' means all celery-related configuration keys 12 | # should have a `CELERY_` prefix. 13 | app.config_from_object('django.conf:settings', namespace='CELERY') 14 | 15 | # Load task modules from all registered Django app configs. 16 | app.autodiscover_tasks() 17 | app.conf.enable_utc = False 18 | 19 | -------------------------------------------------------------------------------- /dtb/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dj_database_url 3 | import dotenv 4 | 5 | from pathlib import Path 6 | 7 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 8 | BASE_DIR = Path(__file__).resolve().parent.parent 9 | 10 | 11 | # Load env variables from file 12 | dotenv_file = BASE_DIR / ".env" 13 | if os.path.isfile(dotenv_file): 14 | dotenv.load_dotenv(dotenv_file) 15 | 16 | 17 | # SECURITY WARNING: keep the secret key used in production secret! 18 | SECRET_KEY = os.getenv( 19 | "DJANGO_SECRET_KEY", 20 | 'x%#3&%giwv8f0+%r946en7z&d@9*rc$sl0qoql56xr%bh^w2mj', 21 | ) 22 | 23 | DEBUG = not not os.getenv("DJANGO_DEBUG", False) 24 | 25 | ALLOWED_HOSTS = ["*",] # since Telegram uses a lot of IPs for webhooks 26 | 27 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 28 | 29 | 30 | INSTALLED_APPS = [ 31 | 'django.contrib.admin', 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.sessions', 35 | 'django.contrib.messages', 36 | 'django.contrib.staticfiles', 37 | 38 | # 3rd party apps 39 | 'django_celery_beat', 40 | 41 | # local apps 42 | 'tgbot.apps.TgbotConfig', 43 | 44 | 'django_cleanup', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'whitenoise.middleware.WhiteNoiseMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | 56 | 'corsheaders.middleware.CorsMiddleware', 57 | 58 | 'django.middleware.common.CommonMiddleware', 59 | ] 60 | 61 | CORS_ORIGIN_ALLOW_ALL = True 62 | CORS_ALLOW_CREDENTIALS = True 63 | 64 | ROOT_URLCONF = 'dtb.urls' 65 | 66 | TEMPLATES = [ 67 | { 68 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 69 | 'DIRS': [], 70 | 'APP_DIRS': True, 71 | 'OPTIONS': { 72 | 'context_processors': [ 73 | 'django.template.context_processors.debug', 74 | 'django.template.context_processors.request', 75 | 'django.contrib.auth.context_processors.auth', 76 | 'django.contrib.messages.context_processors.messages', 77 | ], 78 | }, 79 | }, 80 | ] 81 | 82 | WSGI_APPLICATION = 'dtb.wsgi.application' 83 | ASGI_APPLICATION = 'dtb.asgi.application' 84 | 85 | 86 | # Database 87 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 88 | 89 | DATABASES = { 90 | 'default': dj_database_url.config(conn_max_age=600), 91 | } 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | 112 | # Internationalization 113 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 114 | 115 | LANGUAGE_CODE = 'ru-ru' 116 | 117 | TIME_ZONE = 'Europe/Moscow' 118 | 119 | USE_I18N = True 120 | 121 | USE_L10N = True 122 | 123 | USE_TZ = True 124 | 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 128 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 129 | 130 | WEB_DOMAIN = os.getenv("WEB_DOMAIN") 131 | MEDIA_DOMAIN = os.getenv("MEDIA_DOMAIN") 132 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 133 | MEDIA_URL = '/media/' 134 | 135 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 136 | STATIC_URL = '/staticfiles/' 137 | # STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') 138 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 139 | 140 | 141 | # -----> CELERY 142 | REDIS_URL = os.getenv('REDIS_URL', 'redis://redis:6379') 143 | BROKER_URL = REDIS_URL 144 | CELERY_BROKER_URL = REDIS_URL 145 | CELERY_RESULT_BACKEND = REDIS_URL 146 | CELERY_ACCEPT_CONTENT = ['application/json'] 147 | CELERY_TASK_SERIALIZER = 'json' 148 | CELERY_RESULT_SERIALIZER = 'json' 149 | CELERY_TIMEZONE = TIME_ZONE 150 | CELERY_TASK_DEFAULT_QUEUE = 'default' 151 | 152 | 153 | # -----> TELEGRAM 154 | TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") 155 | 156 | 157 | # -----> LOGGING 158 | ENABLE_DECORATOR_LOGGING = os.getenv('ENABLE_DECORATOR_LOGGING', True) 159 | 160 | LOGGING = { 161 | 'version': 1, 162 | 'disable_existing_loggers': False, 163 | 'formatters': { 164 | 'verbose': { 165 | 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 166 | 'style': '{', 167 | }, 168 | 'simple': { 169 | 'format': '{levelname} {message}', 170 | 'style': '{', 171 | }, 172 | }, 173 | 'handlers': { 174 | 'default': { 175 | 'level': 'DEBUG', 176 | 'class': 'logging.handlers.RotatingFileHandler', 177 | 'filename': os.path.join(BASE_DIR, 'logs/main.log'), 178 | 'maxBytes': 1024 * 1024 * 5, # 5 MB 179 | 'backupCount': 5, 180 | 'formatter': 'verbose', 181 | 'encoding': 'utf-8', 182 | }, 183 | 'request_handler': { 184 | 'level': 'DEBUG', 185 | 'class': 'logging.handlers.RotatingFileHandler', 186 | 'filename': os.path.join(BASE_DIR, 'logs/django_request.log'), 187 | 'maxBytes': 1024 * 1024 * 5, # 5 MB 188 | 'backupCount': 5, 189 | 'formatter': 'verbose', 190 | 'encoding': 'utf-8', 191 | }, 192 | }, 193 | 'loggers': { 194 | '': { 195 | 'handlers': ['default'], 196 | 'level': 'DEBUG', 197 | 'propagate': True 198 | }, 199 | 'django.request': { # Stop SQL debug from logging to main logger 200 | 'handlers': ['request_handler'], 201 | 'level': 'DEBUG', 202 | 'propagate': False 203 | }, 204 | }, 205 | } 206 | 207 | 208 | # -----> SENTRY 209 | # import sentry_sdk 210 | # from sentry_sdk.integrations.django import DjangoIntegration 211 | # from sentry_sdk.integrations.celery import CeleryIntegration 212 | # from sentry_sdk.integrations.redis import RedisIntegration 213 | 214 | # sentry_sdk.init( 215 | # dsn="INPUT ...ingest.sentry.io/ LINK", 216 | # integrations=[ 217 | # DjangoIntegration(), 218 | # CeleryIntegration(), 219 | # RedisIntegration(), 220 | # ], 221 | # traces_sample_rate=0.1, 222 | 223 | # # If you wish to associate users to errors (assuming you are using 224 | # # django.contrib.auth) you may enable sending PII data. 225 | # send_default_pii=True 226 | # ) 227 | 228 | -------------------------------------------------------------------------------- /dtb/urls.py: -------------------------------------------------------------------------------- 1 | """dtb URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.conf import settings 18 | from django.conf.urls.static import static 19 | from django.contrib import admin 20 | from django.urls import path, include 21 | 22 | urlpatterns = [ 23 | path('tgadmin/', admin.site.urls), 24 | path('', include('tgbot.urls')), 25 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 26 | -------------------------------------------------------------------------------- /dtb/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for dtb project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dtb.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Apply database migrations 4 | echo "Applying database migrations ..." 5 | python manage.py migrate 6 | 7 | # Create superuser 8 | echo "Creating superuser ..." 9 | python manage.py createsuperuser --noinput 10 | 11 | # Load initial data (fixtures) 12 | echo "Load initial data" 13 | # python manage.py loaddata MyFixture.json 14 | 15 | # Collecting static 16 | echo "Collecting static ..." 17 | python manage.py collectstatic 18 | 19 | # Start server 20 | echo "Starting server ..." 21 | python manage.py runserver 0.0.0.0:8000 22 | -------------------------------------------------------------------------------- /logs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UNREALre/DjangoTelegramBot_Skeleton/06ac2d9e9c3fd0438ae6f502ded0042c6521e3e9/logs/.keep -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dtb.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /media/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UNREALre/DjangoTelegramBot_Skeleton/06ac2d9e9c3fd0438ae6f502ded0042c6521e3e9/media/.keep -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytz 2 | requests 3 | python-dotenv 4 | click==7.1.2 5 | 6 | # Django 7 | django 8 | django-cleanup==5.2.0 9 | django-timezone-field 10 | django-cors-headers 11 | pillow==8.2.0 12 | whitenoise 13 | 14 | # Django 3.0 async requirements 15 | gunicorn==20.1.0 16 | uvicorn 17 | uvloop 18 | httptools 19 | 20 | # Databases 21 | psycopg2-binary 22 | dj-database-url 23 | 24 | # Distributed tasks 25 | celery==5.0.5 26 | redis 27 | django-celery-beat 28 | 29 | # Telegram 30 | python-telegram-bot 31 | python-telegram-bot-pagination==0.0.2 32 | 33 | # monitoring 34 | # sentry-sdk 35 | -------------------------------------------------------------------------------- /run_pooling.py: -------------------------------------------------------------------------------- 1 | import os, django 2 | 3 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dtb.settings') 4 | django.setup() 5 | 6 | from tgbot.handlers.dispatcher import run_pooling 7 | 8 | if __name__ == "__main__": 9 | run_pooling() -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.3 -------------------------------------------------------------------------------- /tgbot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UNREALre/DjangoTelegramBot_Skeleton/06ac2d9e9c3fd0438ae6f502ded0042c6521e3e9/tgbot/__init__.py -------------------------------------------------------------------------------- /tgbot/admin.py: -------------------------------------------------------------------------------- 1 | import random 2 | import telegram 3 | from django.contrib import admin 4 | from django.http import HttpResponseRedirect 5 | from django.shortcuts import render 6 | 7 | from dtb.settings import DEBUG 8 | 9 | from tgbot.models import Location, Arcgis 10 | from tgbot.models import ( 11 | Config, 12 | User, 13 | UserActionLog, 14 | ) 15 | from tgbot.forms import BroadcastForm 16 | from tgbot.handlers import utils 17 | from tgbot.tasks import broadcast_message 18 | 19 | 20 | @admin.register(User) 21 | class UserAdmin(admin.ModelAdmin): 22 | list_display = [ 23 | 'user_id', 'username', 'first_name', 'last_name', 24 | 'language_code', 'deep_link', 25 | 'created_at', 'updated_at', 'is_blocked_bot' 26 | ] 27 | list_filter = ["is_blocked_bot", "is_moderator"] 28 | search_fields = ('username', 'user_id') 29 | 30 | actions = ['broadcast'] 31 | 32 | def invited_users(self, obj): 33 | return obj.invited_users().count() 34 | 35 | def broadcast(self, request, queryset): 36 | """ Select users via check mark in django-admin panel, then select "Broadcast" to send message""" 37 | if 'apply' in request.POST: 38 | broadcast_message_text = request.POST["broadcast_text"] 39 | 40 | # TODO: for all platforms? 41 | if len(queryset) <= 3 or DEBUG: # for test / debug purposes - run in same thread 42 | for u in queryset: 43 | utils.send_message(user_id=u.id, text=broadcast_message_text, parse_mode=telegram.ParseMode.MARKDOWN) 44 | self.message_user(request, "Just broadcasted to %d users" % len(queryset)) 45 | else: 46 | user_ids = list(set(u.user_id for u in queryset)) 47 | random.shuffle(user_ids) 48 | broadcast_message.delay(message=broadcast_message_text, user_ids=user_ids) 49 | self.message_user(request, "Broadcasting of %d messages has been started" % len(queryset)) 50 | 51 | return HttpResponseRedirect(request.get_full_path()) 52 | 53 | form = BroadcastForm(initial={'_selected_action': queryset.values_list('user_id', flat=True)}) 54 | return render( 55 | request, "admin/broadcast_message.html", {'items': queryset,'form': form, 'title':u' '} 56 | ) 57 | 58 | 59 | @admin.register(Location) 60 | class LocationAdmin(admin.ModelAdmin): 61 | list_display = ['id', 'user_id', 'created_at'] 62 | 63 | 64 | @admin.register(Arcgis) 65 | class ArcgisAdmin(admin.ModelAdmin): 66 | list_display = ['location', 'city', 'country_code'] 67 | 68 | 69 | @admin.register(UserActionLog) 70 | class UserActionLogAdmin(admin.ModelAdmin): 71 | list_display = ['user', 'action', 'created_at'] 72 | 73 | 74 | @admin.register(Config) 75 | class ConfigAdmin(admin.ModelAdmin): 76 | pass 77 | -------------------------------------------------------------------------------- /tgbot/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TgbotConfig(AppConfig): 5 | name = 'tgbot' 6 | -------------------------------------------------------------------------------- /tgbot/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class BroadcastForm(forms.Form): 4 | _selected_action = forms.CharField(widget=forms.MultipleHiddenInput) 5 | broadcast_text = forms.CharField(widget=forms.Textarea) 6 | -------------------------------------------------------------------------------- /tgbot/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UNREALre/DjangoTelegramBot_Skeleton/06ac2d9e9c3fd0438ae6f502ded0042c6521e3e9/tgbot/handlers/__init__.py -------------------------------------------------------------------------------- /tgbot/handlers/admin.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import telegram 3 | 4 | from django.utils.timezone import now 5 | 6 | from tgbot.handlers import static_text as st 7 | from tgbot.models import User 8 | 9 | 10 | def admin(update, context): 11 | """ Show help info about all secret admins commands """ 12 | u = User.get_user(update, context) 13 | if not u.is_admin: 14 | return 15 | 16 | return update.message.reply_text(f'{st.secret_level}\n{st.secret_admin_commands}') 17 | 18 | 19 | def stats(update, context): 20 | """ Show help info about all secret admins commands """ 21 | u = User.get_user(update, context) 22 | if not u.is_admin: 23 | return 24 | 25 | text = f""" 26 | *Users*: {User.objects.count()} 27 | *24h active*: {User.objects.filter(updated_at__gte=now() - datetime.timedelta(hours=24)).count()} 28 | """ 29 | 30 | return update.message.reply_text( 31 | text, 32 | parse_mode=telegram.ParseMode.MARKDOWN, 33 | disable_web_page_preview=True, 34 | ) 35 | -------------------------------------------------------------------------------- /tgbot/handlers/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import logging 5 | import re 6 | import telegram 7 | 8 | from django.utils import timezone 9 | from tgbot.handlers import static_text 10 | from tgbot.models import User 11 | from tgbot.utils import extract_user_data_from_update 12 | from tgbot.handlers.keyboard_utils import make_keyboard_for_start_command, keyboard_confirm_decline_broadcasting 13 | from tgbot.handlers.utils import handler_logging 14 | 15 | logger = logging.getLogger('default') 16 | logger.info("Command handlers check!") 17 | 18 | 19 | @handler_logging() 20 | def command_start(update, context): 21 | user, created = User.get_user_and_created(update, context) 22 | 23 | payload = context.args[0] if context.args else user.deep_link # if empty payload, check what was stored in DB 24 | text = 'Hello!' 25 | 26 | user_id = extract_user_data_from_update(update)['user_id'] 27 | context.bot.send_message(chat_id=user_id, text=text, reply_markup=make_keyboard_for_start_command()) 28 | 29 | 30 | def stats(update, context): 31 | """ Show help info about all secret admins commands """ 32 | u = User.get_user(update, context) 33 | if not u.is_admin: 34 | return 35 | 36 | text = f""" 37 | *Users*: {User.objects.count()} 38 | *24h active*: {User.objects.filter(updated_at__gte=timezone.now() - datetime.timedelta(hours=24)).count()} 39 | """ 40 | 41 | return update.message.reply_text( 42 | text, 43 | parse_mode=telegram.ParseMode.MARKDOWN, 44 | disable_web_page_preview=True, 45 | ) 46 | 47 | 48 | def broadcast_command_with_message(update, context): 49 | """ Type /broadcast . Then check your message in Markdown format and broadcast to users.""" 50 | u = User.get_user(update, context) 51 | user_id = extract_user_data_from_update(update)['user_id'] 52 | 53 | if not u.is_admin: 54 | text = static_text.broadcast_no_access 55 | markup = None 56 | 57 | else: 58 | text = f"{update.message.text.replace(f'{static_text.broadcast_command} ', '')}" 59 | markup = keyboard_confirm_decline_broadcasting() 60 | 61 | try: 62 | context.bot.send_message( 63 | text=text, 64 | chat_id=user_id, 65 | parse_mode=telegram.ParseMode.MARKDOWN, 66 | reply_markup=markup 67 | ) 68 | except telegram.error.BadRequest as e: 69 | place_where_mistake_begins = re.findall(r"offset (\d{1,})$", str(e)) 70 | text_error = static_text.error_with_markdown 71 | if len(place_where_mistake_begins): 72 | text_error += f"{static_text.specify_word_with_error}'{text[int(place_where_mistake_begins[0]):].split(' ')[0]}'" 73 | context.bot.send_message( 74 | text=text_error, 75 | chat_id=user_id 76 | ) -------------------------------------------------------------------------------- /tgbot/handlers/dispatcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Telegram event handlers.""" 4 | 5 | import telegram 6 | 7 | from telegram.ext import ( 8 | Updater, Dispatcher, Filters, 9 | CommandHandler, MessageHandler, 10 | InlineQueryHandler, CallbackQueryHandler, 11 | ChosenInlineResultHandler, PollAnswerHandler, 12 | ) 13 | 14 | from celery.decorators import task # event processing in async mode 15 | 16 | from dtb.settings import TELEGRAM_TOKEN 17 | 18 | from tgbot.handlers import admin, commands, files, location 19 | from tgbot.handlers.commands import broadcast_command_with_message 20 | from tgbot.handlers import handlers as hnd 21 | from tgbot.handlers import manage_data as md 22 | from tgbot.handlers.static_text import broadcast_command 23 | 24 | 25 | def setup_dispatcher(dp): 26 | """ 27 | Adding handlers for events from Telegram 28 | """ 29 | 30 | dp.add_handler(CommandHandler("start", commands.command_start)) 31 | 32 | # admin commands 33 | dp.add_handler(CommandHandler("admin", admin.admin)) 34 | dp.add_handler(CommandHandler("stats", admin.stats)) 35 | 36 | dp.add_handler(MessageHandler( 37 | Filters.animation, files.show_file_id, 38 | )) 39 | 40 | # base buttons 41 | dp.add_handler(CallbackQueryHandler(hnd.btn1_hnd, pattern=f'^{md.BTN_1}')) 42 | dp.add_handler(CallbackQueryHandler(hnd.btn2_hnd, pattern=f'^{md.BTN_2}')) 43 | dp.add_handler(CallbackQueryHandler(hnd.btn3_hnd, pattern=f'^{md.BTN_3}')) 44 | 45 | dp.add_handler(CallbackQueryHandler(hnd.back_to_main_menu_handler, pattern=f'^{md.BUTTON_BACK_IN_PLACE}')) 46 | 47 | # location 48 | dp.add_handler(CommandHandler("ask_location", location.ask_for_location)) 49 | dp.add_handler(MessageHandler(Filters.location, location.location_handler)) 50 | 51 | dp.add_handler(CallbackQueryHandler(hnd.secret_level, pattern=f"^{md.SECRET_LEVEL_BUTTON}")) 52 | 53 | dp.add_handler(MessageHandler(Filters.regex(rf'^{broadcast_command} .*'), broadcast_command_with_message)) 54 | dp.add_handler(CallbackQueryHandler(hnd.broadcast_decision_handler, pattern=f"^{md.CONFIRM_DECLINE_BROADCAST}")) 55 | 56 | # EXAMPLES FOR HANDLERS 57 | # dp.add_handler(MessageHandler(Filters.text, )) 58 | # dp.add_handler(MessageHandler( 59 | # Filters.document, , 60 | # )) 61 | # dp.add_handler(CallbackQueryHandler(, pattern="^r\d+_\d+")) 62 | # dp.add_handler(MessageHandler( 63 | # Filters.chat(chat_id=int(TELEGRAM_FILESTORAGE_ID)), 64 | # # & Filters.forwarded & (Filters.photo | Filters.video | Filters.animation), 65 | # , 66 | # )) 67 | 68 | return dp 69 | 70 | 71 | def run_pooling(): 72 | """ Run bot in pooling mode """ 73 | updater = Updater(TELEGRAM_TOKEN, use_context=True) 74 | 75 | dp = updater.dispatcher 76 | dp = setup_dispatcher(dp) 77 | 78 | bot_info = telegram.Bot(TELEGRAM_TOKEN).get_me() 79 | bot_link = f"https://t.me/" + bot_info["username"] 80 | 81 | print(f"Pooling of '{bot_link}' started") 82 | updater.start_polling(timeout=123) 83 | updater.idle() 84 | 85 | 86 | @task(ignore_result=True) 87 | def process_telegram_event(update_json): 88 | update = telegram.Update.de_json(update_json, bot) 89 | dispatcher.process_update(update) 90 | 91 | 92 | # Global variable - best way I found to init Telegram bot 93 | bot = telegram.Bot(TELEGRAM_TOKEN) 94 | dispatcher = setup_dispatcher(Dispatcher(bot, None, workers=0, use_context=True)) 95 | TELEGRAM_BOT_USERNAME = bot.get_me()["username"] -------------------------------------------------------------------------------- /tgbot/handlers/files.py: -------------------------------------------------------------------------------- 1 | """ 2 | 'document': { 3 | 'file_name': 'preds (4).csv', 'mime_type': 'text/csv', 4 | 'file_id': 'BQACAgIAAxkBAAIJ8F-QAVpXcgUgCUtr2OAHN-OC_2bmAAJwBwAC53CASIpMq-3ePqBXGwQ', 5 | 'file_unique_id': 'AgADcAcAAudwgEg', 'file_size': 28775 6 | } 7 | 'photo': [ 8 | {'file_id': 'AgACAgIAAxkBAAIJ-F-QCOHZUv6Kmf_Z3eVSmByix_IwAAOvMRvncIBIYJQP2Js-sAWGaBiVLgADAQADAgADbQADjpMFAAEbBA', 'file_unique_id': 'AQADhmgYlS4AA46TBQAB', 'file_size': 13256, 'width': 148, 'height': 320}, 9 | {'file_id': 'AgACAgIAAxkBAAIJ-F-QCOHZUv6Kmf_Z3eVSmByix_IwAAOvMRvncIBIYJQP2Js-sAWGaBiVLgADAQADAgADeAADkJMFAAEbBA', 'file_unique_id': 'AQADhmgYlS4AA5CTBQAB', 'file_size': 50857, 'width': 369, 'height': 800}, 10 | {'file_id': 'AgACAgIAAxkBAAIJ-F-QCOHZUv6Kmf_Z3eVSmByix_IwAAOvMRvncIBIYJQP2Js-sAWGaBiVLgADAQADAgADeQADj5MFAAEbBA', 'file_unique_id': 'AQADhmgYlS4AA4-TBQAB', 'file_size': 76018, 'width': 591, 'height': 1280} 11 | ] 12 | 'video_note': { 13 | 'duration': 2, 'length': 300, 14 | 'thumb': {'file_id': 'AAMCAgADGQEAAgn_XaLgADAQAHbQADQCYAAhsE', 'file_unique_id': 'AQADWoxsmi4AA0AmAAI', 'file_size': 11684, 'width': 300, 'height': 300}, 15 | 'file_id': 'DQACAgIAAxkBAAIJCASO6_6Hj8qY3PGwQ', 'file_unique_id': 'AgADeQcAAudwgEg', 'file_size': 102840 16 | } 17 | 'voice': { 18 | 'duration': 1, 'mime_type': 'audio/ogg', 19 | 'file_id': 'AwACAgIAAxkBAAIKAAFfkAu_8Ntpv8n9WWHETutijg20nAACegcAAudwgEi8N3Tjeom0IxsE', 20 | 'file_unique_id': 'AgADegcAAudwgEg', 'file_size': 4391 21 | } 22 | 'sticker': { 23 | 'width': 512, 'height': 512, 'emoji': '🤔', 'set_name': 's121356145_282028_by_stickerfacebot', 'is_animated': False, 24 | 'thumb': { 25 | 'file_id': 'AAMCAgADGQEAAgJUX5A5icQq_0qkwXnihR_MJuCKSRAAAmQAA3G_Owev57igO1Oj4itVTZguAAMBAAdtAAObPwACGwQ', 'file_unique_id': 'AQADK1VNmC4AA5s_AAI', 'file_size': 14242, 'width': 320, 'height': 320 26 | }, 27 | 'file_id': 'CAACAgIAAxkBAAICVF-QOYnEKv9KpMF54oUfzCbgikkQAAJkAANxvzsHr-e4oDtTo-IbBA', 'file_unique_id': 'AgADZAADcb87Bw', 'file_size': 25182 28 | } 29 | 'video': { 30 | 'duration': 14, 'width': 480, 'height': 854, 'mime_type': 'video/mp4', 31 | 'thumb': {'file_id': 'AAMCAgADGQEAAgoIX5BAQy-AfwmWLgADAQAHbQADJhAAAhsE', 'file_unique_id': 'AQAD5H8Jli4AAyYQAAI', 'file_size': 9724, 'width': 180, 'height': 320}, 32 | 'file_id': 'BAACAgIIAAKaCAACCcGASLV2hk3MavHGGwQ', 33 | 'file_unique_id': 'AgADmggAAgnBgEg', 'file_size': 1260506}, 'caption': '50603' 34 | } 35 | """ 36 | 37 | import telegram 38 | from tgbot.models import User 39 | 40 | ALL_TG_FILE_TYPES = ["document", "video_note", "voice", "sticker", "audio", "video", "animation", "photo"] 41 | 42 | def _get_file_id(m): 43 | """ extract file_id from message (and file type?) """ 44 | 45 | for doc_type in ALL_TG_FILE_TYPES: 46 | if doc_type in m and doc_type != "photo": 47 | return m[doc_type]["file_id"] 48 | 49 | if "photo" in m: 50 | best_photo = m["photo"][-1] 51 | return best_photo["file_id"] 52 | 53 | 54 | def show_file_id(update, context): 55 | """ Returns file_id of the attached file/media """ 56 | u = User.get_user(update, context) 57 | 58 | if u.is_admin: 59 | update_json = update.to_dict() 60 | file_id = _get_file_id(update_json["message"]) 61 | message_id = update_json["message"]["message_id"] 62 | update.message.reply_text(text=f"`{file_id}`", parse_mode=telegram.ParseMode.MARKDOWN, reply_to_message_id=message_id) 63 | -------------------------------------------------------------------------------- /tgbot/handlers/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import logging 5 | import telegram 6 | 7 | from django.utils import timezone 8 | 9 | from tgbot.handlers import commands 10 | from tgbot.handlers import static_text as st 11 | from tgbot.handlers import manage_data as md 12 | from tgbot.handlers import keyboard_utils as kb 13 | from tgbot.handlers.utils import handler_logging 14 | from tgbot.models import User 15 | from tgbot.tasks import broadcast_message 16 | from tgbot.utils import convert_2_user_time, extract_user_data_from_update, get_chat_id 17 | 18 | logger = logging.getLogger('default') 19 | 20 | 21 | @handler_logging() 22 | def btn1_hnd(update, context): 23 | user_id = extract_user_data_from_update(update)['user_id'] 24 | 25 | markup = kb.make_btn_keyboard() 26 | msg = f'{st.pressed}1' 27 | 28 | context.bot.edit_message_text( 29 | text=msg, 30 | chat_id=user_id, 31 | message_id=update.callback_query.message.message_id, 32 | reply_markup=markup, 33 | parse_mode=telegram.ParseMode.MARKDOWN, 34 | ) 35 | 36 | 37 | @handler_logging() 38 | def btn2_hnd(update, context): 39 | user_id = extract_user_data_from_update(update)['user_id'] 40 | 41 | markup = kb.make_btn_keyboard() 42 | msg = f'{st.pressed}2' 43 | 44 | context.bot.edit_message_text( 45 | text=msg, 46 | chat_id=user_id, 47 | message_id=update.callback_query.message.message_id, 48 | reply_markup=markup, 49 | parse_mode=telegram.ParseMode.MARKDOWN, 50 | ) 51 | 52 | 53 | @handler_logging() 54 | def btn3_hnd(update, context): 55 | user_id = extract_user_data_from_update(update)['user_id'] 56 | 57 | markup = kb.make_btn_keyboard() 58 | msg = f'{st.pressed}3' 59 | 60 | context.bot.edit_message_text( 61 | text=msg, 62 | chat_id=user_id, 63 | message_id=update.callback_query.message.message_id, 64 | reply_markup=markup, 65 | parse_mode=telegram.ParseMode.MARKDOWN, 66 | ) 67 | 68 | 69 | @handler_logging() 70 | def back_to_main_menu_handler(update, context): # callback_data: BUTTON_BACK_IN_PLACE variable from manage_data.py 71 | user, created = User.get_user_and_created(update, context) 72 | 73 | payload = context.args[0] if context.args else user.deep_link # if empty payload, check what was stored in DB 74 | text = st.welcome 75 | 76 | user_id = extract_user_data_from_update(update)['user_id'] 77 | context.bot.edit_message_text( 78 | chat_id=user_id, 79 | text=text, 80 | message_id=update.callback_query.message.message_id, 81 | reply_markup=kb.make_keyboard_for_start_command(), 82 | parse_mode=telegram.ParseMode.MARKDOWN 83 | ) 84 | 85 | 86 | @handler_logging() 87 | def secret_level(update, context): #callback_data: SECRET_LEVEL_BUTTON variable from manage_data.py 88 | """ Pressed 'secret_level_button_text' after /start command""" 89 | user_id = extract_user_data_from_update(update)['user_id'] 90 | text = "Congratulations! You've opened a secret room👁‍🗨. There is some information for you:\n" \ 91 | "*Users*: {user_count}\n" \ 92 | "*24h active*: {active_24}".format( 93 | user_count=User.objects.count(), 94 | active_24=User.objects.filter(updated_at__gte=timezone.now() - datetime.timedelta(hours=24)).count() 95 | ) 96 | 97 | context.bot.edit_message_text( 98 | text=text, 99 | chat_id=user_id, 100 | message_id=update.callback_query.message.message_id, 101 | parse_mode=telegram.ParseMode.MARKDOWN 102 | ) 103 | 104 | 105 | def broadcast_decision_handler(update, context): #callback_data: CONFIRM_DECLINE_BROADCAST variable from manage_data.py 106 | """ Entered /broadcast . 107 | Shows text in Markdown style with two buttons: 108 | Confirm and Decline 109 | """ 110 | broadcast_decision = update.callback_query.data[len(md.CONFIRM_DECLINE_BROADCAST):] 111 | entities_for_celery = update.callback_query.message.to_dict().get('entities') 112 | entities = update.callback_query.message.entities 113 | text = update.callback_query.message.text 114 | if broadcast_decision == md.CONFIRM_BROADCAST: 115 | admin_text = st.msg_sent, 116 | user_ids = list(User.objects.all().values_list('user_id', flat=True)) 117 | broadcast_message.delay(user_ids=user_ids, message=text, entities=entities_for_celery) 118 | else: 119 | admin_text = text 120 | 121 | context.bot.edit_message_text( 122 | text=admin_text, 123 | chat_id=update.callback_query.message.chat_id, 124 | message_id=update.callback_query.message.message_id, 125 | entities=None if broadcast_decision == md.CONFIRM_BROADCAST else entities 126 | ) -------------------------------------------------------------------------------- /tgbot/handlers/keyboard_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup 4 | 5 | from tgbot.handlers import manage_data as md 6 | from tgbot.handlers import static_text as st 7 | 8 | 9 | def make_btn_keyboard(): 10 | buttons = [ 11 | [ 12 | InlineKeyboardButton(st.go_back, callback_data=f'{md.BUTTON_BACK_IN_PLACE}'), 13 | ] 14 | ] 15 | 16 | return InlineKeyboardMarkup(buttons) 17 | 18 | 19 | def make_keyboard_for_start_command(): 20 | buttons = [ 21 | [ 22 | InlineKeyboardButton(st.btn1, callback_data=f'{md.BTN_1}'), 23 | ], 24 | [ 25 | InlineKeyboardButton(st.btn2, callback_data=f'{md.BTN_2}'), 26 | InlineKeyboardButton(st.btn3, callback_data=f'{md.BTN_3}'), 27 | ] 28 | ] 29 | 30 | return InlineKeyboardMarkup(buttons) 31 | 32 | 33 | def keyboard_confirm_decline_broadcasting(): 34 | buttons = [[ 35 | InlineKeyboardButton(st.confirm_broadcast, callback_data=f'{md.CONFIRM_DECLINE_BROADCAST}{md.CONFIRM_BROADCAST}'), 36 | InlineKeyboardButton(st.decline_broadcast, callback_data=f'{md.CONFIRM_DECLINE_BROADCAST}{md.DECLINE_BROADCAST}') 37 | ]] 38 | 39 | return InlineKeyboardMarkup(buttons) 40 | 41 | 42 | -------------------------------------------------------------------------------- /tgbot/handlers/location.py: -------------------------------------------------------------------------------- 1 | import telegram 2 | 3 | from tgbot.handlers.static_text import share_location, thanks_for_location 4 | from tgbot.models import User, Location 5 | 6 | 7 | def ask_for_location(update, context): 8 | """ Entered /ask_location command""" 9 | u = User.get_user(update, context) 10 | 11 | context.bot.send_message( 12 | chat_id=u.user_id, text=share_location, 13 | reply_markup=telegram.ReplyKeyboardMarkup([ 14 | [telegram.KeyboardButton(text="Send 🌏🌎🌍", request_location=True)] 15 | ], resize_keyboard=True), #'False' will make this button appear on half screen (become very large). Likely, 16 | # it will increase click conversion but may decrease UX quality. 17 | ) 18 | 19 | 20 | def location_handler(update, context): 21 | u = User.get_user(update, context) 22 | lat, lon = update.message.location.latitude, update.message.location.longitude 23 | l = Location.objects.create(user=u, latitude=lat, longitude=lon) 24 | 25 | update.message.reply_text( 26 | thanks_for_location, 27 | reply_markup=telegram.ReplyKeyboardRemove(), 28 | ) 29 | -------------------------------------------------------------------------------- /tgbot/handlers/manage_data.py: -------------------------------------------------------------------------------- 1 | BTN_1 = 'BTN_1' 2 | BTN_2 = 'BTN_2' 3 | BTN_3 = 'BTN_3' 4 | 5 | BUTTON_BACK_IN_PLACE = 'IN_PLACE_BACK' 6 | 7 | SECRET_LEVEL_BUTTON = 'SCRT_LVL' 8 | CONFIRM_DECLINE_BROADCAST = 'CNFM_DCLN_BRDCST' 9 | CONFIRM_BROADCAST = 'CONFIRM' 10 | DECLINE_BROADCAST = 'DECLINE' -------------------------------------------------------------------------------- /tgbot/handlers/static_text.py: -------------------------------------------------------------------------------- 1 | welcome = "Добро пожаловать!" 2 | btn1 = "Кнопка №1" 3 | btn2 = "Кнопка №2" 4 | btn3 = "Кнопка №3" 5 | pressed = "Вы нажали кнопку #" 6 | 7 | go_back = "Вернуться" 8 | confirm_broadcast = "Подтвердить" 9 | decline_broadcast = "Отклонить" 10 | secret_level = "Секретный уровень" 11 | msg_sent = "Сообщение отослано" 12 | share_location = "Позвольте мне узнать, откуда вы?" 13 | thanks_for_location = "Спасибо за ваше местоположение!" 14 | broadcast_command = '/broadcast' 15 | broadcast_no_access = "Sorry, you don't have access to this function." 16 | broadcast_header = "This message will be sent to all users.\n\n" 17 | declined_message_broadcasting = "Рассылка сообщений отклонена❌\n\n" 18 | error_with_markdown = "Невозможно обработать ваш текст в формате Markdown." 19 | specify_word_with_error = " У вас ошибка в слове " 20 | secret_admin_commands = "⚠️ Секретные команды администратора\n" \ 21 | "/stats - bot stats" 22 | -------------------------------------------------------------------------------- /tgbot/handlers/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import telegram 3 | 4 | from functools import wraps 5 | from dtb.settings import ENABLE_DECORATOR_LOGGING, TELEGRAM_TOKEN 6 | from django.utils import timezone 7 | from tgbot.models import UserActionLog, User 8 | from telegram import MessageEntity 9 | 10 | logger = logging.getLogger('default') 11 | 12 | 13 | def send_typing_action(func): 14 | """Sends typing action while processing func command.""" 15 | 16 | @wraps(func) 17 | def command_func(update, context, *args, **kwargs): 18 | context.bot.send_chat_action(chat_id=update.effective_message.chat_id, action=telegram.ChatAction.TYPING) 19 | return func(update, context, *args, **kwargs) 20 | 21 | return command_func 22 | 23 | 24 | def handler_logging(action_name=None): 25 | """ Turn on this decorator via ENABLE_DECORATOR_LOGGING variable in dtb.settings """ 26 | def decor(func): 27 | def handler(update, context, *args, **kwargs): 28 | user, _ = User.get_user_and_created(update, context) 29 | action = f"{func.__module__}.{func.__name__}" if not action_name else action_name 30 | try: 31 | text = update.message['text'] if update.message else '' 32 | except AttributeError: 33 | text = '' 34 | UserActionLog.objects.create(user_id=user.user_id, action=action, text=text, created_at=timezone.now()) 35 | return func(update, context, *args, **kwargs) 36 | return handler if ENABLE_DECORATOR_LOGGING else func 37 | return decor 38 | 39 | 40 | def send_message(user_id, text, parse_mode=None, reply_markup=None, reply_to_message_id=None, 41 | disable_web_page_preview=None, entities=None, tg_token=TELEGRAM_TOKEN): 42 | bot = telegram.Bot(tg_token) 43 | try: 44 | if entities: 45 | entities = [ 46 | MessageEntity(type=entity['type'], 47 | offset=entity['offset'], 48 | length=entity['length'] 49 | ) 50 | for entity in entities 51 | ] 52 | 53 | m = bot.send_message( 54 | chat_id=user_id, 55 | text=text, 56 | parse_mode=parse_mode, 57 | reply_markup=reply_markup, 58 | reply_to_message_id=reply_to_message_id, 59 | disable_web_page_preview=disable_web_page_preview, 60 | entities=entities, 61 | ) 62 | except telegram.error.Unauthorized: 63 | print(f"Can't send message to {user_id}. Reason: Bot was stopped.") 64 | User.objects.filter(user_id=user_id).update(is_blocked_bot=True) 65 | success = False 66 | except Exception as e: 67 | print(f"Can't send message to {user_id}. Reason: {e}") 68 | success = False 69 | else: 70 | success = True 71 | User.objects.filter(user_id=user_id).update(is_blocked_bot=False) 72 | return success 73 | -------------------------------------------------------------------------------- /tgbot/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-03-05 11:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='User', 17 | fields=[ 18 | ('user_id', models.IntegerField(primary_key=True, serialize=False)), 19 | ('username', models.CharField(blank=True, max_length=32, null=True)), 20 | ('first_name', models.CharField(max_length=256)), 21 | ('last_name', models.CharField(blank=True, max_length=256, null=True)), 22 | ('language_code', models.CharField(blank=True, help_text="Telegram client's lang", max_length=8, null=True)), 23 | ('deep_link', models.CharField(blank=True, max_length=64, null=True)), 24 | ('is_blocked_bot', models.BooleanField(default=False)), 25 | ('is_banned', models.BooleanField(default=False)), 26 | ('is_admin', models.BooleanField(default=False)), 27 | ('is_moderator', models.BooleanField(default=False)), 28 | ('created_at', models.DateTimeField(auto_now_add=True)), 29 | ('updated_at', models.DateTimeField(auto_now=True)), 30 | ], 31 | ), 32 | migrations.CreateModel( 33 | name='Location', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('latitude', models.FloatField()), 37 | ('longitude', models.FloatField()), 38 | ('created_at', models.DateTimeField(auto_now_add=True)), 39 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tgbot.user')), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /tgbot/migrations/0002_arcgis.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-05 16:28 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tgbot', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Arcgis', 16 | fields=[ 17 | ('location', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='tgbot.location')), 18 | ('match_addr', models.CharField(max_length=200)), 19 | ('long_label', models.CharField(max_length=200)), 20 | ('short_label', models.CharField(max_length=128)), 21 | ('addr_type', models.CharField(max_length=128)), 22 | ('location_type', models.CharField(max_length=64)), 23 | ('place_name', models.CharField(max_length=128)), 24 | ('add_num', models.CharField(max_length=50)), 25 | ('address', models.CharField(max_length=128)), 26 | ('block', models.CharField(max_length=128)), 27 | ('sector', models.CharField(max_length=128)), 28 | ('neighborhood', models.CharField(max_length=128)), 29 | ('district', models.CharField(max_length=128)), 30 | ('city', models.CharField(max_length=64)), 31 | ('metro_area', models.CharField(max_length=64)), 32 | ('subregion', models.CharField(max_length=64)), 33 | ('region', models.CharField(max_length=128)), 34 | ('territory', models.CharField(max_length=128)), 35 | ('postal', models.CharField(max_length=128)), 36 | ('postal_ext', models.CharField(max_length=128)), 37 | ('country_code', models.CharField(max_length=32)), 38 | ('lng', models.DecimalField(decimal_places=18, max_digits=21)), 39 | ('lat', models.DecimalField(decimal_places=18, max_digits=21)), 40 | ('updated_at', models.DateTimeField(auto_now=True)), 41 | ('created_at', models.DateTimeField(auto_now_add=True)), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /tgbot/migrations/0002_log.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-07 19:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('tgbot', '0002_arcgis'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='UserActionLog', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('action', models.CharField(max_length=128)), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tgbot.user')), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /tgbot/migrations/0003_useractionlog_text.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-28 10:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('tgbot', '0002_log'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='useractionlog', 15 | name='text', 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /tgbot/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UNREALre/DjangoTelegramBot_Skeleton/06ac2d9e9c3fd0438ae6f502ded0042c6521e3e9/tgbot/migrations/__init__.py -------------------------------------------------------------------------------- /tgbot/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import requests 4 | 5 | from datetime import timedelta 6 | from typing import Dict, List, Optional 7 | 8 | from django.db import models 9 | from django.utils import timezone 10 | from django.utils.translation import ugettext_lazy as _ 11 | from tgbot import utils 12 | 13 | 14 | class Config(models.Model): 15 | """Модель настроек бота.""" 16 | 17 | param_name = models.CharField(_('Наименование параметра'), max_length=255) 18 | param_val = models.TextField(_('Значение параметра'), null=True, blank=True) 19 | 20 | def __str__(self): 21 | return self.param_name 22 | 23 | class Meta: 24 | ordering = ['param_name'] 25 | verbose_name = 'Параметр бота' 26 | verbose_name_plural = 'Параметры бота' 27 | 28 | @classmethod 29 | def load_config(cls) -> Dict[str, str]: 30 | config_params = cls.objects.all() 31 | result = {} 32 | for config_param in config_params: 33 | result[config_param.param_name] = config_param.param_val 34 | 35 | return result 36 | 37 | 38 | class User(models.Model): 39 | user_id = models.BigIntegerField(primary_key=True) 40 | username = models.CharField(max_length=32, null=True, blank=True) 41 | first_name = models.CharField(max_length=256) 42 | last_name = models.CharField(max_length=256, null=True, blank=True) 43 | language_code = models.CharField(max_length=8, null=True, blank=True, help_text="Telegram client's lang") 44 | deep_link = models.CharField(max_length=64, null=True, blank=True) 45 | 46 | is_blocked_bot = models.BooleanField(default=False) 47 | is_banned = models.BooleanField(default=False) 48 | 49 | is_admin = models.BooleanField(default=False) 50 | is_moderator = models.BooleanField(default=False) 51 | 52 | created_at = models.DateTimeField(auto_now_add=True) 53 | updated_at = models.DateTimeField(auto_now=True) 54 | 55 | def __str__(self): 56 | return f'@{self.username}' if self.username is not None else f'{self.user_id}' 57 | 58 | @classmethod 59 | def get_user_and_created(cls, update, context): 60 | """ python-telegram-bot's Update, Context --> User instance """ 61 | data = utils.extract_user_data_from_update(update) 62 | u, created = cls.objects.update_or_create(user_id=data["user_id"], defaults=data) 63 | 64 | if created: 65 | if context is not None and context.args is not None and len(context.args) > 0: 66 | payload = context.args[0] 67 | if str(payload).strip() != str(data["user_id"]).strip(): # you can't invite yourself 68 | u.deep_link = payload 69 | u.save() 70 | 71 | return u, created 72 | 73 | @classmethod 74 | def get_user(cls, update, context): 75 | u, _ = cls.get_user_and_created(update, context) 76 | return u 77 | 78 | @classmethod 79 | def get_user_by_username_or_user_id(cls, string): 80 | """ Search user in DB, return User or None if not found """ 81 | username = str(string).replace("@", "").strip().lower() 82 | if username.isdigit(): # user_id 83 | return cls.objects.filter(user_id=int(username)).first() 84 | return cls.objects.filter(username__iexact=username).first() 85 | 86 | def invited_users(self): # --> User queryset 87 | return User.objects.filter(deep_link=str(self.user_id), created_at__gt=self.created_at) 88 | 89 | class Meta: 90 | verbose_name = 'Пользователь' 91 | verbose_name_plural = 'Пользователи' 92 | 93 | 94 | class Location(models.Model): 95 | user = models.ForeignKey(User, on_delete=models.CASCADE) 96 | latitude = models.FloatField() 97 | longitude = models.FloatField() 98 | created_at = models.DateTimeField(auto_now_add=True) 99 | 100 | def __str__(self): 101 | return f"user: {self.user}, created at {self.created_at.strftime('(%H:%M, %d %B %Y)')}" 102 | 103 | def save(self, *args, **kwargs): 104 | super(Location, self).save(*args, **kwargs) 105 | # Parse location with arcgis 106 | from .tasks import save_data_from_arcgis 107 | save_data_from_arcgis.delay(latitude=self.latitude, longitude=self.longitude, location_id=self.pk) 108 | 109 | 110 | class Arcgis(models.Model): 111 | location = models.OneToOneField(Location, on_delete=models.CASCADE, primary_key=True) 112 | 113 | match_addr = models.CharField(max_length=200) 114 | long_label = models.CharField(max_length=200) 115 | short_label = models.CharField(max_length=128) 116 | 117 | addr_type = models.CharField(max_length=128) 118 | location_type = models.CharField(max_length=64) 119 | place_name = models.CharField(max_length=128) 120 | 121 | add_num = models.CharField(max_length=50) 122 | address = models.CharField(max_length=128) 123 | block = models.CharField(max_length=128) 124 | sector = models.CharField(max_length=128) 125 | neighborhood = models.CharField(max_length=128) 126 | district = models.CharField(max_length=128) 127 | city = models.CharField(max_length=64) 128 | metro_area = models.CharField(max_length=64) 129 | subregion = models.CharField(max_length=64) 130 | region = models.CharField(max_length=128) 131 | territory = models.CharField(max_length=128) 132 | postal = models.CharField(max_length=128) 133 | postal_ext = models.CharField(max_length=128) 134 | 135 | country_code = models.CharField(max_length=32) 136 | 137 | lng = models.DecimalField(max_digits=21, decimal_places=18) 138 | lat = models.DecimalField(max_digits=21, decimal_places=18) 139 | 140 | updated_at = models.DateTimeField(auto_now=True) 141 | created_at = models.DateTimeField(auto_now_add=True) 142 | 143 | def __str__(self): 144 | return f"{self.location}, city: {self.city}, country_code: {self.country_code}" 145 | 146 | @classmethod 147 | def from_json(cls, j, location_id): 148 | a = j.get("address") 149 | l = j.get("location") 150 | 151 | if "address" not in j or "location" not in j: 152 | return 153 | 154 | arcgis_data = { 155 | "match_addr": a.get("Match_addr"), 156 | "long_label": a.get("LongLabel"), 157 | "short_label": a.get("ShortLabel"), 158 | "addr_type": a.get("Addr_type"), 159 | "location_type": a.get("Type"), 160 | "place_name": a.get("PlaceName"), 161 | "add_num": a.get("AddNum"), 162 | "address": a.get("Address"), 163 | "block": a.get("Block"), 164 | "sector": a.get("Sector"), 165 | "neighborhood": a.get("Neighborhood"), 166 | "district": a.get("District"), 167 | "city": a.get("City"), 168 | "metro_area": a.get("MetroArea"), 169 | "subregion": a.get("Subregion"), 170 | "region": a.get("Region"), 171 | "territory": a.get("Territory"), 172 | "postal": a.get("Postal"), 173 | "postal_ext": a.get("PostalExt"), 174 | "country_code": a.get("CountryCode"), 175 | "lng": l.get("x"), 176 | "lat": l.get("y") 177 | } 178 | 179 | arc, _ = cls.objects.update_or_create(location_id=location_id, defaults=arcgis_data) 180 | return 181 | 182 | @staticmethod 183 | def reverse_geocode(lat, lng): 184 | r = requests.post( 185 | "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode", 186 | params={ 187 | 'f': 'json', 188 | 'location': f'{lng}, {lat}', 189 | 'distance': 50000, 190 | 'outSR': '', 191 | }, 192 | headers={ 193 | 'Content-Type': 'application/json', 194 | } 195 | ) 196 | return r.json() 197 | 198 | 199 | class UserActionLog(models.Model): 200 | user = models.ForeignKey(User, on_delete=models.CASCADE) 201 | action = models.CharField(max_length=128) 202 | text = models.TextField(blank=True, null=True) 203 | created_at = models.DateTimeField(auto_now_add=True) 204 | 205 | def __str__(self): 206 | return f"user: {self.user}, made: {self.action}, created at {self.created_at.strftime('(%H:%M, %d %B %Y)')}" 207 | -------------------------------------------------------------------------------- /tgbot/tasks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Celery tasks. Some of them will be launched periodically from admin panel via django-celery-beat 3 | """ 4 | 5 | import telegram 6 | import time 7 | 8 | from dtb.celery import app 9 | from celery.utils.log import get_task_logger 10 | 11 | from tgbot.handlers.utils import send_message 12 | from tgbot.models import ( 13 | Arcgis, 14 | User 15 | ) 16 | 17 | logger = get_task_logger(__name__) 18 | 19 | 20 | @app.task(ignore_result=True) 21 | def broadcast_message(user_ids, message, entities=None, sleep_between=0.4, parse_mode=None): 22 | """ It's used to broadcast message to big amount of users """ 23 | logger.info(f"Going to send message: '{message}' to {len(user_ids)} users") 24 | 25 | for user_id in user_ids: 26 | try: 27 | send_message(user_id=user_id, text=message, entities=entities, parse_mode=parse_mode) 28 | logger.info(f"Broadcast message was sent to {user_id}") 29 | except Exception as e: 30 | logger.error(f"Failed to send message to {user_id}, reason: {e}" ) 31 | time.sleep(max(sleep_between, 0.1)) 32 | 33 | logger.info("Broadcast finished!") 34 | 35 | 36 | @app.task(ignore_result=True) 37 | def save_data_from_arcgis(latitude, longitude, location_id): 38 | Arcgis.from_json(Arcgis.reverse_geocode(latitude, longitude), location_id=location_id) -------------------------------------------------------------------------------- /tgbot/templates/admin/broadcast_message.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block content %} 4 |
{% csrf_token %} 5 | {{ form }} 6 |

Broadcast Message will be sent to these users:

7 |
    {{ items|unordered_list }}
8 | 9 | 10 |
11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /tgbot/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.urls import path, include 4 | from django.views.decorators.csrf import csrf_exempt 5 | 6 | from . import views 7 | 8 | urlpatterns = [ 9 | # TODO: make webhook more secure 10 | path('', views.index, name="index"), 11 | path('super_secter_webhook/', csrf_exempt(views.TelegramBotWebhookView.as_view())), 12 | ] 13 | -------------------------------------------------------------------------------- /tgbot/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import logging 5 | import uuid 6 | 7 | from django.conf import settings 8 | 9 | logger = logging.getLogger('default') 10 | 11 | 12 | def extract_user_data_from_update(update): 13 | """ python-telegram-bot's Update instance --> User info """ 14 | if update.message is not None: 15 | user = update.message.from_user.to_dict() 16 | elif update.inline_query is not None: 17 | user = update.inline_query.from_user.to_dict() 18 | elif update.chosen_inline_result is not None: 19 | user = update.chosen_inline_result.from_user.to_dict() 20 | elif update.callback_query is not None and update.callback_query.from_user is not None: 21 | user = update.callback_query.from_user.to_dict() 22 | elif update.callback_query is not None and update.callback_query.message is not None: 23 | user = update.callback_query.message.chat.to_dict() 24 | elif update.poll_answer is not None: 25 | user = update.poll_answer.user.to_dict() 26 | else: 27 | raise Exception(f"Can't extract user data from update: {update}") 28 | 29 | return dict( 30 | user_id=user["id"], 31 | is_blocked_bot=False, 32 | **{ 33 | k: user[k] 34 | for k in ["username", "first_name", "last_name", "language_code"] 35 | if k in user and user[k] is not None 36 | }, 37 | ) 38 | 39 | 40 | def get_chat_id(update, context): 41 | """Extract chat_id based on the incoming object.""" 42 | 43 | chat_id = -1 44 | 45 | if update.message is not None: 46 | chat_id = update.message.chat.id 47 | elif update.callback_query is not None: 48 | chat_id = update.callback_query.message.chat.id 49 | elif update.poll is not None: 50 | chat_id = context.bot_data[update.poll.id] 51 | 52 | return chat_id 53 | 54 | 55 | def get_file_path(instance, filename): 56 | """Create random unique filename for files, uploaded via admin.""" 57 | 58 | ext = filename.split('.')[-1] 59 | filename = f'{uuid.uuid4()}.{ext}' 60 | return filename 61 | 62 | 63 | def convert_2_user_time(date: datetime.datetime): 64 | """Получает дату в UTC. Возвращает в Мск.""" 65 | 66 | return date + datetime.timedelta(hours=3) 67 | -------------------------------------------------------------------------------- /tgbot/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from django.views import View 4 | from django.http import JsonResponse 5 | 6 | from dtb.settings import DEBUG 7 | 8 | from tgbot.handlers.dispatcher import process_telegram_event, TELEGRAM_BOT_USERNAME 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | BOT_URL = f"https://t.me/{TELEGRAM_BOT_USERNAME}" 13 | 14 | 15 | def index(request): 16 | return JsonResponse({"error": "sup hacker"}) 17 | 18 | 19 | class TelegramBotWebhookView(View): 20 | # WARNING: if fail - Telegram webhook will be delivered again. 21 | # Can be fixed with async celery task execution 22 | def post(self, request, *args, **kwargs): 23 | if DEBUG: 24 | process_telegram_event(json.loads(request.body)) 25 | else: # use celery in production 26 | process_telegram_event.delay(json.loads(request.body)) 27 | 28 | # TODO: there is a great trick to send data in webhook response 29 | # e.g. remove buttons 30 | return JsonResponse({"ok": "POST request processed"}) 31 | 32 | def get(self, request, *args, **kwargs): # for debug 33 | return JsonResponse({"ok": "Get request processed. But nothing done"}) 34 | --------------------------------------------------------------------------------