Skip to content

Commit

Permalink
Merge pull request EGCETSII#16 from Full-Tortuga/feature/telegramBot
Browse files Browse the repository at this point in the history
Merge branch feature/telegram bot
  • Loading branch information
alvechdel authored Dec 17, 2021
2 parents 7f626a2 + 67b2153 commit 674d450
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 7 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
decide/*/migrations

# Flask stuff:
instance/
Expand Down Expand Up @@ -101,3 +102,6 @@ ENV/
.mypy_cache/

.vagrant

# Visual Studio Code settings
.vscode
3 changes: 3 additions & 0 deletions decide/decide/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@

STATIC_URL = '/static/'

#temporary link to visualizer page for bots (until hosted)
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

Expand Down
11 changes: 10 additions & 1 deletion decide/visualizer/admin.py
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 21 additions & 0 deletions decide/visualizer/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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)),
],
),
]
17 changes: 17 additions & 0 deletions decide/visualizer/migrations/0002_auto_20211210_1800.py
Original file line number Diff line number Diff line change
@@ -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'},
),
]
10 changes: 9 additions & 1 deletion decide/visualizer/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
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)

class Meta:
verbose_name = 'Telegram user'
31 changes: 31 additions & 0 deletions decide/visualizer/static/visualizer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#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;
}

#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;
}
198 changes: 198 additions & 0 deletions decide/visualizer/telegramBot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
from django.conf import settings
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 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(os.environ.get('TELEGRAM_TOKEN'),
use_context=True)

BOT=Bot(token=os.environ.get('TELEGRAM_TOKEN'))

#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')
setup_commands(UPDATER)
updates_setting()
#starts the bot
UPDATER.start_polling()

#configures commands and handlers for the bot
def setup_commands(votitos):

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$)"))

#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
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)

# 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)

#shut down the bot
def stop(update, context):
UPDATER.stop()

#list of available commands
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, 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)

#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)
keyboard_buttons=[]
for v in finished_votings:
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
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)

#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)
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):

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.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:
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)
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
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
# ===================

#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="<b>{}\n\n</b><b><i>Descripción:</i></b> {}\n<b><i>Pregunta:</i></b> {}\n".format(str(voting.name).upper(), voting.desc, str(voting.question))
msg+="<b><i>Opciones:</i></b>\n{}\n".format(opt_msg)
msg+="<b><i>Fecha de incio:</i></b> {}<b><i>Fecha de finalización:</i></b> {}<b><i>Conteo actual:</i></b> {}".format(start_d, end_d, tally)

return msg

#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)
if 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,
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.send_message(chat_id=id, text=msg, parse_mode="HTML")


13 changes: 13 additions & 0 deletions decide/visualizer/templates/visualizer/telegram_admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends 'admin/change_list.html' %}
{% load i18n static %}

{% block extrahead %}
<link type="text/css" rel="stylesheet" href="{% static "/visualizer.css" %}" />
{% endblock %}

{% block content %}
<br>
<a id="start-tlg" href="{% url 'start_telegram' %}">Start Telegram Bot</a>
<div><br></div>
{{ block.super }}
{% endblock %}
8 changes: 8 additions & 0 deletions decide/visualizer/templates/visualizer/visualizer.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<link type="text/css" rel="stylesheet"
href="https://unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.css" />
<link type="text/css" rel="stylesheet" href="{% static "booth/style.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "/visualizer.css" %}" />
{% endblock %}

{% block content %}
Expand Down Expand Up @@ -49,8 +50,15 @@ <h2 class="heading">Resultados:</h2>
{% endif %}
</div>
</div>
<footer class="bg-secondary" id="bots-footer">
<p class="navbar-brand">Echa un vistazo a nuestros bots si quieres estar al tanto de toda la información de las votaciones
<img src="https://img.icons8.com/color/48/000000/telegram-app--v1.png"
onclick="window.location='https://t.me/votitos_bot'">
</p>
</footer>
{% endblock %}


{% block extrabody %}
<!-- Vuejs -->
<script src="https://unpkg.com/vue"></script>
Expand Down
3 changes: 2 additions & 1 deletion decide/visualizer/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.urls import path
from .views import VisualizerView
from .views import VisualizerView, initialize


urlpatterns = [
path('<int:voting_id>/', VisualizerView.as_view()),
path('startTelegram/', initialize, name="start_telegram")
]
Loading

0 comments on commit 674d450

Please sign in to comment.