From 5bd679d0248f13dffea8e4349f1b4a02b32c9be7 Mon Sep 17 00:00:00 2001 From: alvechdel Date: Fri, 3 Dec 2021 16:50:24 +0100 Subject: [PATCH 1/5] feature-telegramBot-001 Added telegramBot '@VotitosBot' with commands for retrieving info from Decide's website and allow opt-in and opt-out to auto-notification --- .gitignore | 3 + decide/decide/settings.py | 6 + decide/visualizer/migrations/0001_initial.py | 21 +++ decide/visualizer/models.py | 7 +- decide/visualizer/telegramBot.py | 166 +++++++++++++++++++ decide/visualizer/views.py | 10 +- decide/visualizer/website_scrapping.py | 25 +++ decide/voting/admin.py | 4 +- requirements.txt | 3 +- 9 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 decide/visualizer/migrations/0001_initial.py create mode 100644 decide/visualizer/telegramBot.py create mode 100644 decide/visualizer/website_scrapping.py diff --git a/.gitignore b/.gitignore index b823e313b4..99d20becfe 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,6 @@ ENV/ .mypy_cache/ .vagrant + +# Visual Studio Code settings +.vscode \ No newline at end of file diff --git a/decide/decide/settings.py b/decide/decide/settings.py index 1d22b67324..4ab2d402db 100644 --- a/decide/decide/settings.py +++ b/decide/decide/settings.py @@ -22,6 +22,9 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '^##ydkswfu0+=ofw0l#$kv^8n)0$i(qd&d&ol#p9!b$8*5%j1+' +# Token for telegram bot +TELEGRAM_TOKEN = '2111051748:AAH1R736I0_HsZEW6_22Tf0r-OqihtF5x88' + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -158,6 +161,9 @@ STATIC_URL = '/static/' +#temporary link to visualizer page for bots (until hosted) +VISUALIZER_VIEW="http://127.0.0.1:8000/visualizer/" + # number of bits for the key, all auths should use the same number of bits KEYBITS = 256 diff --git a/decide/visualizer/migrations/0001_initial.py b/decide/visualizer/migrations/0001_initial.py new file mode 100644 index 0000000000..394118c9a1 --- /dev/null +++ b/decide/visualizer/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 2.0 on 2021-12-03 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='TelegramBot', + fields=[ + ('user_id', models.BigIntegerField(primary_key=True, serialize=False)), + ('auto_msg', models.BooleanField(default=False)), + ], + ), + ] diff --git a/decide/visualizer/models.py b/decide/visualizer/models.py index 71a8362390..c32c94a31d 100644 --- a/decide/visualizer/models.py +++ b/decide/visualizer/models.py @@ -1,3 +1,8 @@ from django.db import models -# Create your models here. +class TelegramBot(models.Model): + user_id=models.BigIntegerField(primary_key=True) + auto_msg=models.BooleanField(default=False) + + def __str__(self): + return '{}'.format(self.auto_msg) diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py new file mode 100644 index 0000000000..7a4f125dbc --- /dev/null +++ b/decide/visualizer/telegramBot.py @@ -0,0 +1,166 @@ +from django.conf import settings +from django.db.models import query +from voting import models +from store import models as stmodels +from telegram import Update, CallbackQuery, InputMediaPhoto, Bot +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters , CallbackContext, CallbackQueryHandler +from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton +from telegram.inline.inlinekeyboardmarkup import InlineKeyboardMarkup +import datetime,logging +from .website_scrapping import get_graphs +from .models import TelegramBot + +#configures and activate '@VotitosBot' to receive any messages from users +def init_bot(): + + #logging + logging.basicConfig(level=logging.ERROR, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + #auth token needed + updater = Updater(settings.TELEGRAM_TOKEN, + use_context=True) + + setup_commands(updater) + + #starts the bot + updater.start_polling() + +#configures commands and handlers for the bot +def setup_commands(votitos): + + votitos.dispatcher.add_handler(CommandHandler('start', start)) + votitos.dispatcher.add_handler(CommandHandler('results', show_results)) + votitos.dispatcher.add_handler(CommandHandler('details', show_details)) + votitos.dispatcher.add_handler(CommandHandler('auto', change_auto_status)) + votitos.dispatcher.add_handler(CommandHandler('help', help)) + votitos.dispatcher.add_handler(MessageHandler(Filters.command, unknown_command)) + votitos.dispatcher.add_handler(CallbackQueryHandler(results_query_handler, pattern="^[1-9][0-9]*$")) + votitos.dispatcher.add_handler(CallbackQueryHandler(details_query_handler, pattern="^d[1-9][0-9]*$")) + votitos.dispatcher.add_handler(CallbackQueryHandler(auto_query_handler, pattern="(^True$|^False$)")) + +#gives the user a warming welcome +def start(update, context): + name=update.message.from_user.first_name + id=update.message.chat.id + context.bot.send_message(chat_id=id, text="Hola {}, a las buenas tardes. ¿En qué puedo ayudarte?".format(name)) + TelegramBot.objects.get_or_create(user_id=id) + help(update) + +#list of commands available +def help(update): + + update.message.reply_text("""Esta es mi lista de comandos: + /start - Inicia la interacción conmigo + /results - Muestra los resultados de las votaciones cerradas + /details - Proporciona detalles de todas las votaciones + /auto - Permite activar o desactivar las notificaciones automáticas para nuevas votaciones + """) + +#replies to invalid command inputs +def unknown_command(update): + update.message.reply_text("Lo siento, no sé qué es '%s'. Revisa que has escrito bien el comando o bien revisa mi lista de comandos, puedes hacerlo con\n/help" % update.message.text) + +#allow to select an closed voting and show its results +def show_results(update, context): + update.message.reply_text("Aquí tienes la lista de votaciones finalizadas.") + finished_votings=models.Voting.objects.exclude(start_date__isnull=True).exclude(end_date__isnull=True) + keyboard_buttons=[] + for v in finished_votings: + keyboard_buttons.append([InlineKeyboardButton(text=str(v.name), callback_data=str(v.id))]) + keyboard=InlineKeyboardMarkup(keyboard_buttons) + context.bot.send_message(chat_id=update.message.chat.id, text= "Elige por favor:", reply_markup=keyboard) + +#handler for '/results' command +def results_query_handler(update, context): + + query=update.callback_query + query.answer("¡A la orden!") + results_graph(query.data, update.callback_query.message.chat_id, context) + +#allow to select an active or closed voting and show its details +def show_details(update, context): + update.message.reply_text("Selecciona la votación de la que desea ver sus detalles") + votings=models.Voting.objects.exclude(start_date__isnull=True) + keyboard_buttons=[[InlineKeyboardButton(text=str(v.name), callback_data="d"+str(v.id)) for v in votings]] + keyboard=InlineKeyboardMarkup(keyboard_buttons) + context.bot.send_message(chat_id=update.message.chat.id, text= "Seleccione una por favor:", reply_markup=keyboard) + +#handler for '/details' command +def details_query_handler(update, context): + + query=update.callback_query + query.answer("¡A la orden!") + vot_id=query.data[1] + voting=models.Voting.objects.exclude(start_date__isnull=True).get(id=vot_id) + msg=aux_message_builder(voting) + context.bot.send_message(chat_id=query.message.chat_id,text=msg, parse_mode="HTML") + +#opt-in and opt-out for auto notifications +def change_auto_status(update, context): + id=update.message.chat.id + status_user=TelegramBot.objects.get(user_id=id) + if status_user is True: + msg="activadas" + choose_msg="¿Desea desactivarlas?" + else: + msg="desativadas" + choose_msg="¿Desea activarlas?" + keyboard_buttons=[[InlineKeyboardButton(text="Sí", callback_data="True")], [InlineKeyboardButton(text="No", callback_data="False")]] #REVISAR CALLBACK DATA + keyboard=InlineKeyboardMarkup(keyboard_buttons) + update.message.reply_text("Actualmente las notificaciones automáticas se encuentran {}.".format(msg)) + context.bot.send_message(chat_id=id, text=choose_msg, reply_markup=keyboard) + +#handler for '/auto' command +def auto_query_handler(update, context): + query=update.callback_query + query.answer("¡Listo! He actualizado tus preferencia") + id=update.callback_query.message.chat_id + new_status=query.data + TelegramBot.objects.filter(user_id=id).update(auto_msg=new_status) + +# =================== +# AUXILIARY METHODS +# =================== + +#auxiliary message to print details from votings +def aux_message_builder(voting): + + options=list(voting.question.options.values_list('option', flat=True)) + tally=stmodels.Vote.objects.filter(voting_id=voting.id).values('voter_id').distinct().count() #gets unique votes for a voting + start_d=voting.start_date.strftime('%d-%m-%Y %H:%M:%S')+"\n" + end_d="Por decidir\n" + + if voting.end_date is not None: + end_d=voting.end_date.strftime('%d-%m-%Y %H:%M:%S')+"\n" + elif tally is None: + tally="Desconocido por el momento" + + opt_msg="" + for i,o in enumerate(options,1): + opt_msg+=" " + str(i)+". " + o+"\n" + + msg="{}\n\nDescripción: {}\nPregunta: {}\n".format(str(voting.name).upper(), voting.desc, str(voting.question)) + msg+="Opciones:\n{}\n".format(opt_msg) + msg+="Fecha de incio: {}Fecha de finalización: {}Conteo actual: {}".format(start_d, end_d, tally) + + return msg + +#extract graph's images from website selected voting and send them to the user +def results_graph(id, chat_identifier, context): + url=settings.VISUALIZER_VIEW+ str(id) + images=get_graphs(url) + if images: + media_group=[InputMediaPhoto(media=i, caption="PUM en la boquita bb") for i in images] + context.bot.sendMediaGroup(chat_id=chat_identifier, media=media_group) + else: + context.bot.send_message(chat_id=chat_identifier, + text="Upss! Parece que aún no hay ningún gráfico asociado a esta votación.\nInténtalo de nuevo en otro momento.") + +#sends notifications when a new voting is created +def auto_notifications(voting): + users_id_enabled=list(TelegramBot.objects.values_list('user_id', flat=True).exclude(auto_msg=False)) + msg=aux_message_builder(voting) + for id in users_id_enabled: + Bot(token=settings.TELEGRAM_TOKEN).send_message(chat_id=id, text=msg, parse_mode="HTML") + + diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 8fea64ecb2..d2a1635b90 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -4,15 +4,15 @@ from django.http import Http404 from base import mods - +from .telegramBot import init_bot class VisualizerView(TemplateView): template_name = 'visualizer/visualizer.html' - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) vid = kwargs.get('voting_id', 0) - + try: r = mods.get('voting', params={'id': vid}) context['voting'] = json.dumps(r[0]) @@ -20,3 +20,7 @@ def get_context_data(self, **kwargs): raise Http404 return context + +#call to initialize bot +init_bot() + \ No newline at end of file diff --git a/decide/visualizer/website_scrapping.py b/decide/visualizer/website_scrapping.py new file mode 100644 index 0000000000..a0f109395c --- /dev/null +++ b/decide/visualizer/website_scrapping.py @@ -0,0 +1,25 @@ +from bs4 import BeautifulSoup as bs +import lxml +import urllib.request as request +from urllib.parse import urlparse + +#returns images of a voting (FOR NOW) +def get_graphs(link): + file=request.urlopen(link) + s=bs(file, "lxml") + images=s.find_all("img") + urls=[] + for img in images: + img_url=img.attrs.get("src") + if not img_url: + continue + if validate_url(img_url): + urls.append(img_url) + if len(urls) == 0: + urls=False + return urls + +#checks whether url is valid or not (Might be removed in a near future) +def validate_url(link): + parse=urlparse(link) + return bool(parse.netloc) and (parse.scheme) \ No newline at end of file diff --git a/decide/voting/admin.py b/decide/voting/admin.py index dff206a94f..cf17357512 100644 --- a/decide/voting/admin.py +++ b/decide/voting/admin.py @@ -4,7 +4,7 @@ from .models import QuestionOption from .models import Question from .models import Voting - +from visualizer.telegramBot import auto_notifications from .filters import StartedFilter @@ -13,6 +13,8 @@ def start(modeladmin, request, queryset): v.create_pubkey() v.start_date = timezone.now() v.save() + #for users who have auto notifications enabled + auto_notifications(v) def stop(ModelAdmin, request, queryset): diff --git a/requirements.txt b/requirements.txt index d5860a1eb4..024fc01fee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,9 @@ djangorestframework==3.7.7 django-cors-headers==2.1.0 requests==2.18.4 django-filter==1.1.0 -psycopg2==2.7.4 +psycopg2-binary==2.8.4 django-rest-swagger==2.2.0 coverage==4.5.2 django-nose==1.4.6 jsonnet==0.12.1 +python-telegram-bot==13.8.2 \ No newline at end of file From ba35013e37c6cd2bf9b5322f63217c75207fa91c Mon Sep 17 00:00:00 2001 From: alvechdel Date: Sun, 5 Dec 2021 14:34:22 +0100 Subject: [PATCH 2/5] feature-telegramBot-002 Minor refactorization and added several commands to restart and stop the bot, to consume Heroku's resources only when necessary --- decide/visualizer/telegramBot.py | 66 +++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py index 7a4f125dbc..4fe31899e6 100644 --- a/decide/visualizer/telegramBot.py +++ b/decide/visualizer/telegramBot.py @@ -2,13 +2,21 @@ from django.db.models import query from voting import models from store import models as stmodels -from telegram import Update, CallbackQuery, InputMediaPhoto, Bot -from telegram.ext import Updater, CommandHandler, MessageHandler, Filters , CallbackContext, CallbackQueryHandler +from telegram import InputMediaPhoto, Bot +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackQueryHandler from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton from telegram.inline.inlinekeyboardmarkup import InlineKeyboardMarkup -import datetime,logging +import datetime,logging, os, sys from .website_scrapping import get_graphs from .models import TelegramBot +from threading import Thread + + +#auth and front-end for '@VotitosBot' +UPDATER = Updater(settings.TELEGRAM_TOKEN, + use_context=True) + +BOT=Bot(token=settings.TELEGRAM_TOKEN) #configures and activate '@VotitosBot' to receive any messages from users def init_bot(): @@ -16,28 +24,27 @@ def init_bot(): #logging logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - #auth token needed - updater = Updater(settings.TELEGRAM_TOKEN, - use_context=True) - - setup_commands(updater) - + setup_commands(UPDATER) + #starts the bot - updater.start_polling() + UPDATER.start_polling() #configures commands and handlers for the bot def setup_commands(votitos): - votitos.dispatcher.add_handler(CommandHandler('start', start)) - votitos.dispatcher.add_handler(CommandHandler('results', show_results)) - votitos.dispatcher.add_handler(CommandHandler('details', show_details)) - votitos.dispatcher.add_handler(CommandHandler('auto', change_auto_status)) - votitos.dispatcher.add_handler(CommandHandler('help', help)) - votitos.dispatcher.add_handler(MessageHandler(Filters.command, unknown_command)) - votitos.dispatcher.add_handler(CallbackQueryHandler(results_query_handler, pattern="^[1-9][0-9]*$")) - votitos.dispatcher.add_handler(CallbackQueryHandler(details_query_handler, pattern="^d[1-9][0-9]*$")) - votitos.dispatcher.add_handler(CallbackQueryHandler(auto_query_handler, pattern="(^True$|^False$)")) - + dp=votitos.dispatcher + dp.add_handler(CommandHandler('start', start)) + dp.add_handler(CommandHandler('relaunch', relaunch, Filters.user(user_id=1931864468))) + dp.add_handler(CommandHandler('stop', stop, Filters.user(user_id=1931864468))) + dp.add_handler(CommandHandler('results', show_results)) + dp.add_handler(CommandHandler('details', show_details)) + dp.add_handler(CommandHandler('auto', change_auto_status)) + dp.add_handler(CommandHandler('help', help)) + dp.add_handler(MessageHandler(Filters.command, unknown_command)) + dp.add_handler(CallbackQueryHandler(results_query_handler, pattern="^[1-9][0-9]*$")) + dp.add_handler(CallbackQueryHandler(details_query_handler, pattern="^d[1-9][0-9]*$")) + dp.add_handler(CallbackQueryHandler(auto_query_handler, pattern="(^True$|^False$)")) + #gives the user a warming welcome def start(update, context): name=update.message.from_user.first_name @@ -46,6 +53,19 @@ def start(update, context): TelegramBot.objects.get_or_create(user_id=id) help(update) +# relaunch the bot and also the whole project (limited to admin) +def relaunch(update, context): + Thread(target=stop_restart).start() + +# aux for relaunch +def stop_restart(): + UPDATER.stop() + os.execl(sys.executable, sys.executable, *sys.argv) + +#shutdowns the bot +def stop(update, context): + UPDATER.stop() + #list of commands available def help(update): @@ -57,7 +77,7 @@ def help(update): """) #replies to invalid command inputs -def unknown_command(update): +def unknown_command(update, context): update.message.reply_text("Lo siento, no sé qué es '%s'. Revisa que has escrito bien el comando o bien revisa mi lista de comandos, puedes hacerlo con\n/help" % update.message.text) #allow to select an closed voting and show its results @@ -83,7 +103,7 @@ def show_details(update, context): votings=models.Voting.objects.exclude(start_date__isnull=True) keyboard_buttons=[[InlineKeyboardButton(text=str(v.name), callback_data="d"+str(v.id)) for v in votings]] keyboard=InlineKeyboardMarkup(keyboard_buttons) - context.bot.send_message(chat_id=update.message.chat.id, text= "Seleccione una por favor:", reply_markup=keyboard) + context.bot.send_message(chat_id=update.message.chat.id, text="Seleccione una por favor:", reply_markup=keyboard) #handler for '/details' command def details_query_handler(update, context): @@ -161,6 +181,6 @@ def auto_notifications(voting): users_id_enabled=list(TelegramBot.objects.values_list('user_id', flat=True).exclude(auto_msg=False)) msg=aux_message_builder(voting) for id in users_id_enabled: - Bot(token=settings.TELEGRAM_TOKEN).send_message(chat_id=id, text=msg, parse_mode="HTML") + BOT.send_message(chat_id=id, text=msg, parse_mode="HTML") From 66f6dac7337e934f28a5c6ed28319bada51c3421 Mon Sep 17 00:00:00 2001 From: alvechdel Date: Sun, 5 Dec 2021 21:05:52 +0100 Subject: [PATCH 3/5] feature-telegramBot-003 Fixed command '/auto', refactored others commands as well as comments and imports. Also, added footer with links to bots in visualizer view --- decide/visualizer/static/visualizer.css | 12 +++++ decide/visualizer/telegramBot.py | 52 +++++++++++-------- .../templates/visualizer/visualizer.html | 8 +++ decide/visualizer/website_scrapping.py | 2 +- 4 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 decide/visualizer/static/visualizer.css diff --git a/decide/visualizer/static/visualizer.css b/decide/visualizer/static/visualizer.css new file mode 100644 index 0000000000..490ce710fb --- /dev/null +++ b/decide/visualizer/static/visualizer.css @@ -0,0 +1,12 @@ +#bots-footer { + position: fixed; + width: 100%; + bottom : 0px; + height : 60px; + text-align: center; + vertical-align: middle; + padding-top: 0.5 rem; + padding-bottom: 0.5 rem; + background-color: rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity)); + color: #fff; +} \ No newline at end of file diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py index 4fe31899e6..41388c341d 100644 --- a/decide/visualizer/telegramBot.py +++ b/decide/visualizer/telegramBot.py @@ -1,12 +1,11 @@ from django.conf import settings -from django.db.models import query from voting import models from store import models as stmodels from telegram import InputMediaPhoto, Bot from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackQueryHandler from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton from telegram.inline.inlinekeyboardmarkup import InlineKeyboardMarkup -import datetime,logging, os, sys +import logging, os, sys from .website_scrapping import get_graphs from .models import TelegramBot from threading import Thread @@ -25,7 +24,7 @@ def init_bot(): logging.basicConfig(level=logging.ERROR, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') setup_commands(UPDATER) - + updates_setting() #starts the bot UPDATER.start_polling() @@ -44,8 +43,15 @@ def setup_commands(votitos): dp.add_handler(CallbackQueryHandler(results_query_handler, pattern="^[1-9][0-9]*$")) dp.add_handler(CallbackQueryHandler(details_query_handler, pattern="^d[1-9][0-9]*$")) dp.add_handler(CallbackQueryHandler(auto_query_handler, pattern="(^True$|^False$)")) - -#gives the user a warming welcome + +#set bot configuration not to reply to old messages +def updates_setting(): + updates=BOT.get_updates() + if updates: + last_update=updates[-1].update_id + BOT.get_updates(offset=last_update+1) + +#gives the users a warming welcome def start(update, context): name=update.message.from_user.first_name id=update.message.chat.id @@ -62,11 +68,11 @@ def stop_restart(): UPDATER.stop() os.execl(sys.executable, sys.executable, *sys.argv) -#shutdowns the bot +#shut down the bot def stop(update, context): UPDATER.stop() -#list of commands available +#list of available commands def help(update): update.message.reply_text("""Esta es mi lista de comandos: @@ -80,7 +86,7 @@ def help(update): def unknown_command(update, context): update.message.reply_text("Lo siento, no sé qué es '%s'. Revisa que has escrito bien el comando o bien revisa mi lista de comandos, puedes hacerlo con\n/help" % update.message.text) -#allow to select an closed voting and show its results +#allows you to select a closed voting and show its results def show_results(update, context): update.message.reply_text("Aquí tienes la lista de votaciones finalizadas.") finished_votings=models.Voting.objects.exclude(start_date__isnull=True).exclude(end_date__isnull=True) @@ -97,7 +103,7 @@ def results_query_handler(update, context): query.answer("¡A la orden!") results_graph(query.data, update.callback_query.message.chat_id, context) -#allow to select an active or closed voting and show its details +#allows you to select an active or closed voting and show its details def show_details(update, context): update.message.reply_text("Selecciona la votación de la que desea ver sus detalles") votings=models.Voting.objects.exclude(start_date__isnull=True) @@ -119,25 +125,27 @@ def details_query_handler(update, context): def change_auto_status(update, context): id=update.message.chat.id status_user=TelegramBot.objects.get(user_id=id) - if status_user is True: - msg="activadas" - choose_msg="¿Desea desactivarlas?" + if status_user.auto_msg: + choose_msg="Actualmente las notificaciones automáticas se encuentran activadas.\n¿Desea desactivarlas?" + keyboard_buttons=[[InlineKeyboardButton(text="Sí", callback_data="False")], + [InlineKeyboardButton(text="No", callback_data="True")]] else: - msg="desativadas" - choose_msg="¿Desea activarlas?" - keyboard_buttons=[[InlineKeyboardButton(text="Sí", callback_data="True")], [InlineKeyboardButton(text="No", callback_data="False")]] #REVISAR CALLBACK DATA + choose_msg="Actualmente las notificaciones automáticas se encuentran desactivadas.\n¿Desea activarlas?" + keyboard_buttons=[[InlineKeyboardButton(text="Sí", callback_data="True")], + [InlineKeyboardButton(text="No", callback_data="False")]] keyboard=InlineKeyboardMarkup(keyboard_buttons) - update.message.reply_text("Actualmente las notificaciones automáticas se encuentran {}.".format(msg)) context.bot.send_message(chat_id=id, text=choose_msg, reply_markup=keyboard) #handler for '/auto' command def auto_query_handler(update, context): query=update.callback_query - query.answer("¡Listo! He actualizado tus preferencia") - id=update.callback_query.message.chat_id - new_status=query.data - TelegramBot.objects.filter(user_id=id).update(auto_msg=new_status) - + u_id=update.callback_query.message.chat_id + msg_id=update.callback_query.message.message_id + for id in range(msg_id-2, msg_id+1): + context.bot.delete_message(chat_id=u_id, message_id=id) + TelegramBot.objects.filter(user_id=u_id).update(auto_msg=query.data) + query.answer("¡Listo! He actualizado tus preferencias") + # =================== # AUXILIARY METHODS # =================== @@ -165,7 +173,7 @@ def aux_message_builder(voting): return msg -#extract graph's images from website selected voting and send them to the user +#extracts graph's images from website selected voting and sends them to the user def results_graph(id, chat_identifier, context): url=settings.VISUALIZER_VIEW+ str(id) images=get_graphs(url) diff --git a/decide/visualizer/templates/visualizer/visualizer.html b/decide/visualizer/templates/visualizer/visualizer.html index 0faed6bac3..90dc1319bd 100644 --- a/decide/visualizer/templates/visualizer/visualizer.html +++ b/decide/visualizer/templates/visualizer/visualizer.html @@ -7,6 +7,7 @@ + {% endblock %} {% block content %} @@ -44,8 +45,15 @@

Resultados:

+
+ +
{% endblock %} + {% block extrabody %} diff --git a/decide/visualizer/website_scrapping.py b/decide/visualizer/website_scrapping.py index a0f109395c..496db12702 100644 --- a/decide/visualizer/website_scrapping.py +++ b/decide/visualizer/website_scrapping.py @@ -22,4 +22,4 @@ def get_graphs(link): #checks whether url is valid or not (Might be removed in a near future) def validate_url(link): parse=urlparse(link) - return bool(parse.netloc) and (parse.scheme) \ No newline at end of file + return bool(parse.netloc) and bool(parse.scheme) \ No newline at end of file From 17e0047e6ec9bb7d6606f4dbc18c5c3f113376d2 Mon Sep 17 00:00:00 2001 From: alvechdel Date: Mon, 13 Dec 2021 11:53:03 +0100 Subject: [PATCH 4/5] feature-telegramBot-004 Added admin site for Telegram Bot as well as a button to launch it and changed keyboard layout of several commands. --- decide/visualizer/admin.py | 11 ++++++++++- .../migrations/0002_auto_20211210_1800.py | 17 +++++++++++++++++ decide/visualizer/models.py | 3 +++ decide/visualizer/static/visualizer.css | 19 +++++++++++++++++++ decide/visualizer/telegramBot.py | 12 ++++++++---- .../templates/visualizer/telegram_admin.html | 13 +++++++++++++ decide/visualizer/urls.py | 3 ++- decide/visualizer/views.py | 15 +++++++++------ 8 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 decide/visualizer/migrations/0002_auto_20211210_1800.py create mode 100644 decide/visualizer/templates/visualizer/telegram_admin.html diff --git a/decide/visualizer/admin.py b/decide/visualizer/admin.py index 8c38f3f3da..ea7783f5eb 100644 --- a/decide/visualizer/admin.py +++ b/decide/visualizer/admin.py @@ -1,3 +1,12 @@ from django.contrib import admin +from django.http.response import HttpResponseRedirect +from .models import TelegramBot +from .urls import urlpatterns -# Register your models here. +class TelegramBotAdmin(admin.ModelAdmin): + list_display=('user_id', 'auto_msg') + list_filter=('auto_msg',) + ordering=('user_id', 'auto_msg') + change_list_template = "visualizer/telegram_admin.html" + +admin.site.register(TelegramBot, TelegramBotAdmin) \ No newline at end of file diff --git a/decide/visualizer/migrations/0002_auto_20211210_1800.py b/decide/visualizer/migrations/0002_auto_20211210_1800.py new file mode 100644 index 0000000000..b772f6bc7a --- /dev/null +++ b/decide/visualizer/migrations/0002_auto_20211210_1800.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0 on 2021-12-10 18:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('visualizer', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='telegrambot', + options={'verbose_name': 'Telegram user'}, + ), + ] diff --git a/decide/visualizer/models.py b/decide/visualizer/models.py index c32c94a31d..1c622f6e5a 100644 --- a/decide/visualizer/models.py +++ b/decide/visualizer/models.py @@ -6,3 +6,6 @@ class TelegramBot(models.Model): def __str__(self): return '{}'.format(self.auto_msg) + + class Meta: + verbose_name = 'Telegram user' \ No newline at end of file diff --git a/decide/visualizer/static/visualizer.css b/decide/visualizer/static/visualizer.css index 490ce710fb..135d61f328 100644 --- a/decide/visualizer/static/visualizer.css +++ b/decide/visualizer/static/visualizer.css @@ -9,4 +9,23 @@ padding-bottom: 0.5 rem; background-color: rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity)); color: #fff; +} + +#start-tlg { + border: #79aec8; + border-radius: 12px; + font-size: 14px; + color: #fff; + text-transform: uppercase; + background-color: #79aec8; + letter-spacing: 0.5px; + padding: 8px; + -webkit-border-radius: 12px; + -moz-border-radius: 12px; + -ms-border-radius: 12px; + -o-border-radius: 12px; +} + +#start-tlg:hover{ + background-color: #417690; } \ No newline at end of file diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py index 41388c341d..d31cd6ccce 100644 --- a/decide/visualizer/telegramBot.py +++ b/decide/visualizer/telegramBot.py @@ -92,8 +92,8 @@ def show_results(update, context): finished_votings=models.Voting.objects.exclude(start_date__isnull=True).exclude(end_date__isnull=True) keyboard_buttons=[] for v in finished_votings: - keyboard_buttons.append([InlineKeyboardButton(text=str(v.name), callback_data=str(v.id))]) - keyboard=InlineKeyboardMarkup(keyboard_buttons) + keyboard_buttons.append(InlineKeyboardButton(text=str(v.name), callback_data=str(v.id))) + keyboard=InlineKeyboardMarkup(build_keyboard_menu(keyboard_buttons,2)) context.bot.send_message(chat_id=update.message.chat.id, text= "Elige por favor:", reply_markup=keyboard) #handler for '/results' command @@ -107,10 +107,14 @@ def results_query_handler(update, context): def show_details(update, context): update.message.reply_text("Selecciona la votación de la que desea ver sus detalles") votings=models.Voting.objects.exclude(start_date__isnull=True) - keyboard_buttons=[[InlineKeyboardButton(text=str(v.name), callback_data="d"+str(v.id)) for v in votings]] - keyboard=InlineKeyboardMarkup(keyboard_buttons) + keyboard_buttons=[InlineKeyboardButton(text=str(v.name), callback_data="d"+str(v.id)) for v in votings] + keyboard=InlineKeyboardMarkup(build_keyboard_menu(keyboard_buttons,2)) context.bot.send_message(chat_id=update.message.chat.id, text="Seleccione una por favor:", reply_markup=keyboard) +#constructs menu for inline buttons +def build_keyboard_menu(buttons, n_cols): + return [buttons[b:(b + n_cols)] for b in range(0, len(buttons), n_cols)] + #handler for '/details' command def details_query_handler(update, context): diff --git a/decide/visualizer/templates/visualizer/telegram_admin.html b/decide/visualizer/templates/visualizer/telegram_admin.html new file mode 100644 index 0000000000..661aa1b85e --- /dev/null +++ b/decide/visualizer/templates/visualizer/telegram_admin.html @@ -0,0 +1,13 @@ +{% extends 'admin/change_list.html' %} +{% load i18n static %} + +{% block extrahead %} + +{% endblock %} + +{% block content %} +
+ Start Telegram Bot +

+ {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/decide/visualizer/urls.py b/decide/visualizer/urls.py index 4baef5f2b9..1b58b8b407 100644 --- a/decide/visualizer/urls.py +++ b/decide/visualizer/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from .views import VisualizerView +from .views import VisualizerView, initialize urlpatterns = [ path('/', VisualizerView.as_view()), + path('startTelegram/', initialize, name="start_telegram") ] diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index d2a1635b90..a259166de8 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -1,13 +1,13 @@ import json +from django.http.response import HttpResponse, HttpResponseRedirect from django.views.generic import TemplateView from django.conf import settings from django.http import Http404 - -from base import mods from .telegramBot import init_bot +from base import mods class VisualizerView(TemplateView): - template_name = 'visualizer/visualizer.html' + template_name = 'visualizer/visualizer' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -21,6 +21,9 @@ def get_context_data(self, **kwargs): return context -#call to initialize bot -init_bot() - \ No newline at end of file +def initialize(request): + #call to initalize telegram bot + init_bot() + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + + From 67b215386bc8923aef136487bdcfb545f633628f Mon Sep 17 00:00:00 2001 From: alvechdel Date: Fri, 17 Dec 2021 23:00:18 +0100 Subject: [PATCH 5/5] feature-telegramBot-005 Change in bot settings for Heorku deploy and add new requirements. --- .gitignore | 1 + decide/decide/settings.py | 5 +---- decide/visualizer/telegramBot.py | 6 +++--- decide/visualizer/website_scrapping.py | 12 ++---------- requirements.txt | 4 +++- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 99d20becfe..caad338b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml # Django stuff: *.log local_settings.py +decide/*/migrations # Flask stuff: instance/ diff --git a/decide/decide/settings.py b/decide/decide/settings.py index 7129f8c65a..005681e78d 100644 --- a/decide/decide/settings.py +++ b/decide/decide/settings.py @@ -22,9 +22,6 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '^##ydkswfu0+=ofw0l#$kv^8n)0$i(qd&d&ol#p9!b$8*5%j1+' -# Token for telegram bot -TELEGRAM_TOKEN = '2111051748:AAH1R736I0_HsZEW6_22Tf0r-OqihtF5x88' - # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -171,7 +168,7 @@ STATIC_URL = '/static/' #temporary link to visualizer page for bots (until hosted) -VISUALIZER_VIEW="http://127.0.0.1:8000/visualizer/" +VISUALIZER_VIEW="https://decide-full-tortuga-2.herokuapp.com/visualizer/" # number of bits for the key, all auths should use the same number of bits KEYBITS = 256 diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py index d31cd6ccce..f9e2fef73f 100644 --- a/decide/visualizer/telegramBot.py +++ b/decide/visualizer/telegramBot.py @@ -12,10 +12,10 @@ #auth and front-end for '@VotitosBot' -UPDATER = Updater(settings.TELEGRAM_TOKEN, +UPDATER = Updater(os.environ.get('TELEGRAM_TOKEN'), use_context=True) -BOT=Bot(token=settings.TELEGRAM_TOKEN) +BOT=Bot(token=os.environ.get('TELEGRAM_TOKEN')) #configures and activate '@VotitosBot' to receive any messages from users def init_bot(): @@ -182,7 +182,7 @@ def results_graph(id, chat_identifier, context): url=settings.VISUALIZER_VIEW+ str(id) images=get_graphs(url) if images: - media_group=[InputMediaPhoto(media=i, caption="PUM en la boquita bb") for i in images] + media_group=[InputMediaPhoto(media=i) for i in images] context.bot.sendMediaGroup(chat_id=chat_identifier, media=media_group) else: context.bot.send_message(chat_id=chat_identifier, diff --git a/decide/visualizer/website_scrapping.py b/decide/visualizer/website_scrapping.py index 496db12702..4fb5f62c7c 100644 --- a/decide/visualizer/website_scrapping.py +++ b/decide/visualizer/website_scrapping.py @@ -1,9 +1,8 @@ from bs4 import BeautifulSoup as bs -import lxml import urllib.request as request from urllib.parse import urlparse -#returns images of a voting (FOR NOW) +#returns images of a voting def get_graphs(link): file=request.urlopen(link) s=bs(file, "lxml") @@ -13,13 +12,6 @@ def get_graphs(link): img_url=img.attrs.get("src") if not img_url: continue - if validate_url(img_url): - urls.append(img_url) if len(urls) == 0: urls=False - return urls - -#checks whether url is valid or not (Might be removed in a near future) -def validate_url(link): - parse=urlparse(link) - return bool(parse.netloc) and bool(parse.scheme) \ No newline at end of file + return urls \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e6a3e782a5..074faa31fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,6 @@ djongo==1.3.6 pymongo==3.12.1 six==1.16.0 sqlparse==0.2.4 -python-telegram-bot==13.8.2 +python-telegram-bot==13.9.0 +bs4 +