├── .gitignore ├── README.md ├── bot ├── __init__.py ├── admin.py ├── bot.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── runbot.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20191118_0825.py │ ├── 0003_auto_20191118_0839.py │ └── __init__.py ├── models.py └── views.py ├── manage.py ├── requirements.txt ├── screen.png └── survey ├── __init__.py ├── questions.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea/ 3 | db.sqlite3 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SurveyBot 2 | ## Telegram bot for Surveys built with Django and Telebot 3 | 4 |

5 | 6 |

7 | 8 | ### First get the repo on your machine: 9 | ```bash 10 | $ git clone https://github.com/azimjohn/survey-bot.git 11 | ``` 12 | 13 | 14 | ### Then install requirements: 15 | ```bash 16 | $ pip install -r requirements.txt 17 | ``` 18 | 19 | ### Make the migrations and apply them: 20 | ```bash 21 | $ python manage.py makemigrations 22 | $ python manage.py migrate 23 | ``` 24 | 25 | ### Export the bot token, replace `REPLACE_ME` with your bot's token 26 | ```bash 27 | $ export BOT_TOKEN=REPLACE_ME 28 | ``` 29 | 30 | ### Finally, run the server 🎉 31 | ```bash 32 | $ python manage.py runbot 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azimjohn/survey-bot/fa5c275ac3d415bdaeccba361d850f4b9e66ba7a/bot/__init__.py -------------------------------------------------------------------------------- /bot/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Respondent, Response 3 | 4 | admin.site.site_header = 'SurveyBot Admin' 5 | 6 | 7 | class ResponseInline(admin.StackedInline): 8 | model = Response 9 | max_num = 0 10 | can_delete = False 11 | fields = ("html",) 12 | readonly_fields = ("html",) 13 | 14 | 15 | @admin.register(Respondent) 16 | class RespondentAdmin(admin.ModelAdmin): 17 | search_fields = ("first_name", "last_name", "username") 18 | fields = ("first_name", "last_name", "username", "completed_responses_count") 19 | list_display = ("first_name", "last_name", "username", "completed_responses_count") 20 | readonly_fields = ("first_name", "last_name", "username", "completed_responses_count") 21 | list_select_related = ("responses",) 22 | inlines = (ResponseInline,) 23 | -------------------------------------------------------------------------------- /bot/bot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import telebot 3 | from django.conf import settings 4 | 5 | from survey.questions import QUESTIONS 6 | from .models import Respondent 7 | 8 | bot = telebot.TeleBot(settings.TOKEN, num_threads=5) 9 | number_of_questions = len(QUESTIONS) 10 | start_command = "/start" 11 | restart_command = "Отправить еще один отзыв" 12 | 13 | 14 | def markup_choices(choices): 15 | if not choices: 16 | return telebot.types.ReplyKeyboardRemove(selective=False) 17 | 18 | markup = telebot.types.ReplyKeyboardMarkup(True, False) 19 | for choice in choices: 20 | markup.add(telebot.types.KeyboardButton(choice)) 21 | 22 | return markup 23 | 24 | 25 | @bot.message_handler() 26 | def handler(message): 27 | registrant, _ = Respondent.objects.get_or_create( 28 | user_id=message.from_user.id, 29 | defaults={ 30 | "first_name": message.from_user.first_name or "", 31 | "last_name": message.from_user.last_name or "", 32 | "username": message.from_user.username or "", 33 | } 34 | ) 35 | 36 | response = registrant.responses.filter(completed=False).last() 37 | if not response or message.text in [start_command, restart_command]: 38 | response = registrant.responses.create() 39 | 40 | if number_of_questions >= response.step >= 1: 41 | prev_question = QUESTIONS[response.step - 1]["text"] 42 | response.details[prev_question] = message.text 43 | response.save(update_fields=["details"]) 44 | 45 | if response.step >= number_of_questions: 46 | response.completed = True 47 | response.save(update_fields=["completed"]) 48 | bot.send_message( 49 | registrant.user_id, 50 | "Готово. Спасибо за участие.", 51 | reply_markup=markup_choices([restart_command]) 52 | ) 53 | return 54 | 55 | question = QUESTIONS[response.step] 56 | text = question["text"] 57 | choices = question.get("choices") or [] 58 | 59 | bot.send_message(registrant.user_id, text, reply_markup=markup_choices(choices)) 60 | 61 | response.step += 1 62 | response.save(update_fields=["step"]) 63 | -------------------------------------------------------------------------------- /bot/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azimjohn/survey-bot/fa5c275ac3d415bdaeccba361d850f4b9e66ba7a/bot/management/__init__.py -------------------------------------------------------------------------------- /bot/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azimjohn/survey-bot/fa5c275ac3d415bdaeccba361d850f4b9e66ba7a/bot/management/commands/__init__.py -------------------------------------------------------------------------------- /bot/management/commands/runbot.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from django.core.management import BaseCommand 4 | 5 | from bot.bot import bot 6 | 7 | 8 | class Command(BaseCommand): 9 | def handle(self, *args, **options): 10 | print("Started Development Bot Client") 11 | try: 12 | bot.polling() 13 | except Exception as e: 14 | print(e) 15 | time.sleep(5) 16 | self.handle(*args, **options) 17 | -------------------------------------------------------------------------------- /bot/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.9 on 2019-11-13 10:59 2 | 3 | from django.db import migrations, models 4 | import jsonfield.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Respondent', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('user_id', models.IntegerField(null=True)), 20 | ('first_name', models.CharField(blank=True, max_length=128)), 21 | ('last_name', models.CharField(blank=True, max_length=128)), 22 | ('username', models.CharField(blank=True, max_length=128)), 23 | ('step', models.SmallIntegerField(default=0)), 24 | ('details', jsonfield.fields.JSONField(default=dict, max_length=8192)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /bot/migrations/0002_auto_20191118_0825.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-11-18 08:25 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import jsonfield.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('bot', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='respondent', 17 | name='details', 18 | ), 19 | migrations.RemoveField( 20 | model_name='respondent', 21 | name='step', 22 | ), 23 | migrations.CreateModel( 24 | name='Response', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('step', models.SmallIntegerField(default=0)), 28 | ('details', jsonfield.fields.JSONField(default=dict, max_length=8192)), 29 | ('completed', models.BooleanField(default=False)), 30 | ('respondent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bot.Respondent')), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /bot/migrations/0003_auto_20191118_0839.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-11-18 08:39 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 | ('bot', '0002_auto_20191118_0825'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='response', 16 | name='respondent', 17 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='bot.Respondent'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /bot/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azimjohn/survey-bot/fa5c275ac3d415bdaeccba361d850f4b9e66ba7a/bot/migrations/__init__.py -------------------------------------------------------------------------------- /bot/models.py: -------------------------------------------------------------------------------- 1 | import jsonfield 2 | from django.db import models 3 | from django.utils.html import format_html 4 | 5 | 6 | class Respondent(models.Model): 7 | user_id = models.IntegerField(null=True) 8 | first_name = models.CharField(max_length=128, blank=True) 9 | last_name = models.CharField(max_length=128, blank=True) 10 | username = models.CharField(max_length=128, blank=True) 11 | 12 | @property 13 | def completed_responses_count(self): 14 | return self.responses.filter(completed=True).count() 15 | 16 | 17 | class Response(models.Model): 18 | step = models.SmallIntegerField(default=0) 19 | details = jsonfield.JSONField(max_length=8192, default=dict) 20 | respondent = models.ForeignKey(Respondent, related_name="responses", on_delete=models.CASCADE) 21 | 22 | completed = models.BooleanField(default=False) 23 | 24 | @property 25 | def html(self): 26 | table = "" 27 | 28 | for question in self.details: 29 | answer = self.details[question] 30 | table += f""" 31 | 32 | 33 | 34 | 35 | """ 36 | 37 | table += "
{question}{answer}
" 38 | return format_html(table) 39 | -------------------------------------------------------------------------------- /bot/views.py: -------------------------------------------------------------------------------- 1 | import telebot 2 | from django.http import HttpResponse 3 | from django.views.decorators.csrf import csrf_exempt 4 | from .bot import bot 5 | 6 | 7 | @csrf_exempt 8 | def get_updates(request): 9 | json_string = request.body.decode('utf-8') 10 | update = telebot.types.Update.de_json(json_string) 11 | bot.process_new_updates([update]) 12 | 13 | return HttpResponse('') 14 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'survey.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.2.24 2 | pyTelegramBotAPI==3.6.6 3 | django-jsonfield==1.3.1 4 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azimjohn/survey-bot/fa5c275ac3d415bdaeccba361d850f4b9e66ba7a/screen.png -------------------------------------------------------------------------------- /survey/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azimjohn/survey-bot/fa5c275ac3d415bdaeccba361d850f4b9e66ba7a/survey/__init__.py -------------------------------------------------------------------------------- /survey/questions.py: -------------------------------------------------------------------------------- 1 | QUESTIONS = [ 2 | { 3 | "text": "What's your name?", 4 | }, 5 | { 6 | "text": "How old are you?", 7 | "choices": ["-18", "18-25", "25-30", "30+"] 8 | }, 9 | { 10 | "text": "How do you rate our app?", 11 | "choices": [1, 2, 3, 4, 5], 12 | }, 13 | { 14 | "text": "How can we make it better?" 15 | }, 16 | { 17 | "text": "Where did you hear about us?", 18 | "choices": [ 19 | "Telegram", 20 | "Facebook", 21 | "Instagram", 22 | "Other", 23 | ] 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /survey/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = '#)tr$f!pycw8u+-u3@wba$sp*qi_qt*f0@if8!z*+ny%^7^bo-' 6 | 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS = ['*'] 10 | 11 | INSTALLED_APPS = [ 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 'bot', 19 | ] 20 | 21 | MIDDLEWARE = [ 22 | 'django.middleware.security.SecurityMiddleware', 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.middleware.csrf.CsrfViewMiddleware', 26 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 27 | 'django.contrib.messages.middleware.MessageMiddleware', 28 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 29 | ] 30 | 31 | ROOT_URLCONF = 'survey.urls' 32 | 33 | TEMPLATES = [ 34 | { 35 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 36 | 'DIRS': [], 37 | 'APP_DIRS': True, 38 | 'OPTIONS': { 39 | 'context_processors': [ 40 | 'django.template.context_processors.debug', 41 | 'django.template.context_processors.request', 42 | 'django.contrib.auth.context_processors.auth', 43 | 'django.contrib.messages.context_processors.messages', 44 | ], 45 | }, 46 | }, 47 | ] 48 | 49 | WSGI_APPLICATION = 'survey.wsgi.application' 50 | 51 | DATABASES = { 52 | 'default': { 53 | 'ENGINE': 'django.db.backends.sqlite3', 54 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 55 | } 56 | } 57 | STATIC_URL = '/static/' 58 | STATIC_ROOT = os.path.join(BASE_DIR, 'static/') 59 | 60 | TOKEN = os.environ.get("BOT_TOKEN") 61 | -------------------------------------------------------------------------------- /survey/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from bot.views import get_updates 4 | 5 | urlpatterns = [ 6 | path('', admin.site.urls), 7 | path('bot/get_updates/', get_updates), 8 | ] 9 | -------------------------------------------------------------------------------- /survey/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.wsgi import get_wsgi_application 3 | 4 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'survey.settings') 5 | application = get_wsgi_application() 6 | --------------------------------------------------------------------------------