Add movies trivial Bot for Telegram

This commit is contained in:
Manuel Vergara 2024-07-30 00:43:20 +02:00
parent be39d5b1d3
commit ecd77967a0
40 changed files with 1108 additions and 13 deletions

View File

@ -0,0 +1,25 @@
# Utiliza una imagen base de Python en Alpine
FROM python:3.12.4-alpine
WORKDIR /app
# Instala bash, herramientas de compilación y librerías necesarias
RUN apk update && \
apk upgrade && \
python -m venv /env && \
/env/bin/pip install --upgrade pip
# Añade el archivo de requisitos a la imagen
COPY requirements.txt /app/requirements.txt
RUN /env/bin/pip install --no-cache-dir -r /app/requirements.txt
# Añade el resto del código de la aplicación
COPY . /app
# Configura el entorno virtual
ENV VIRTUAL_ENV=/env
ENV PATH=/env/bin:$PATH
# Comando por defecto para ejecutar el bot
CMD ["python", "main.py"]

View File

@ -0,0 +1,67 @@
# QuizBot
__Basado en el repositorio de [CineMonster](https://github.com/RogueFairyStudios/CineMonster)__
Bot de Telegram con un juego basado en preguntas sobre películas. Resumen de las funcionalidades:
## **Comandos Disponibles:**
1. **`/start`**:
- **Descripción**: Inicia una nueva sesión de juego para el chat.
- **Acciones**: Crea una nueva instancia de `Session` y la almacena en `SESSIONS`. Si se encuentra una clase `Quiz` en el módulo `quiz`, la inicializa para la sesión.
2. **`/roll`**:
- **Descripción**: Lanza una pregunta de trivia sobre películas.
- **Acciones**: Llama a `show` en el objeto `Quiz` de la sesión activa. Envía una imagen de una película al chat y establece el estado del juego en "running".
3. **`/leaderboard`**:
- **Descripción**: Muestra la tabla de clasificación de los jugadores.
- **Acciones**: Envía la tabla de clasificación actual a través del `messenger`. La tabla muestra los jugadores y sus puntos.
4. **`/repeat`**:
- **Descripción**: Repite la última pregunta de trivia sobre películas.
- **Acciones**: Envía de nuevo la imagen de la película al chat junto con la pregunta sobre el título de la película.
5. **`/cut`**:
- **Descripción**: Permite que un jugador abandone el juego.
- **Acciones**: Elimina al jugador de la sesión actual y notifica al chat que el jugador ha abandonado el juego.
6. **`/stop`**:
- **Descripción**: Finaliza la sesión de juego actual.
- **Acciones**: Elimina la sesión del chat actual de `SESSIONS` y notifica al chat que el juego ha terminado.
7. **`/check_resps`**:
- **Descripción**: Verifica las respuestas enviadas por los jugadores.
- **Acciones**: Compara la respuesta del usuario con la respuesta correcta de la película y actualiza el puntaje si la respuesta es correcta.
## **Funcionalidades Adicionales:**
- **Manejo de Temporizadores**:
- Utiliza `apscheduler` para ejecutar `update_all_timers` cada minuto, lo que actualiza los temporizadores de todas las sesiones y verifica la expiración del tiempo de juego.
- **Mensajería**:
- Usa un objeto `messenger` para enviar mensajes y fotos a los usuarios en el chat, manejando la comunicación con Telegram.
- **Gestión de Jugadores**:
- Permite agregar y quitar jugadores de la sesión. Actualiza el puntaje de los jugadores en función de sus respuestas correctas.
- **Control de Estado del Juego**:
- Los estados del juego (`running`, `stopped`, `timed_out`) controlan el flujo del juego, incluyendo la verificación de respuestas y el manejo de tiempos de espera.
- **Manejo de Errores**:
- Maneja errores durante el proceso de actualización y respuesta, notificando a los usuarios en caso de problemas con la pregunta de trivia o el estado de la sesión.
## **Estructura del Código:**
1. **`Session`**:
- Maneja la lógica del juego, incluidos los jugadores, el estado de la sesión, y los temporizadores.
2. **`Quiz`**:
- Se encarga de la lógica relacionada con las preguntas sobre películas, incluida la selección de una película al azar y la verificación de las respuestas.
3. **`Server`**:
- Configura el bot de Telegram, maneja los comandos y los eventos, y gestiona las sesiones de juego.

View File

@ -0,0 +1 @@
theme: jekyll-theme-slate

View File

@ -0,0 +1,40 @@
from miners import Miner
from random import *
class Collection:
movie_list = ''
def __init__(self, miner, type):
self.miner = miner
self.type = type
def top_250(self):
self.movie_list = self.miner.get_top(250)
def general(self):
pass
def get_rand_movie(self):
movie = None
while movie is None:
if self.type is None:
number = str(randrange(1, 99999))
if len(number) < 7:
number = '0' * (7 - len(number)) + number
movie_id = 'tt' + number
else:
self.top_250()
number = randrange(0, len(self.movie_list) - 1)
movie_id = self.movie_list[number]['tconst']
images, movie = self.miner.get_movie_by_id(movie_id)
print(movie['base']['title'])
if images is not None:
if images['totalImageCount'] < 1:
movie = None
return movie, images

View File

@ -0,0 +1 @@
from collection.Collection import Collection

View File

@ -0,0 +1 @@
from conf.config import Config, ProductionConfig, DevelopmentConfig, TestingConfig

View File

@ -0,0 +1,31 @@
import os
from dotenv import load_dotenv
load_dotenv('.env')
class Config(object):
DEBUG = False
TESTING = False
DATABASE_URI = 'sqlite://:memory:'
DATABASE_URL = os.getenv('DATABASE_URL')
TELEGRAM_BOT_API = os.getenv('TELEGRAM_BOT_API')
LOG_FILE = 'logs/movie2_bot.log'
QUIZ_LANG = 'es'
class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'
SESSION_EXPIRATION_TIME = 30
class DevelopmentConfig(Config):
DEBUG = True
SESSION_EXPIRATION_TIME = 10
class TestingConfig(Config):
TESTING = True

View File

@ -0,0 +1,20 @@
# version: '3.7'
services:
movie2_bot:
env_file:
- .env
image: movie2_bot_python:latest
container_name: movie2_bot
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Madrid
volumes:
- ./movie2_bot_data:/app/db
- ./logs:/app/logs
restart: unless-stopped
volumes:
movie2_bot_data:
logs:

View File

@ -0,0 +1 @@
from interfaces.telegram.messenger import Messenger as TelegramMessenger

View File

@ -0,0 +1,45 @@
from telegram.constants import ParseMode
import telegram.ext
class Messenger:
formats = {
'regular': "*%s*",
'caption': "*%s*",
'title': "=+= *%s* =+=",
'highlight': "--+ %s +--",
'bold': "*%s* %s"
}
def __init__(self, bot, logger):
self.logger = logger
self.logger.debug("Started...")
self.bot = bot
def send_msg(self, chat_id, msg, type_msg='regular'):
if type_msg not in self.formats:
self.logger.error(f"Invalid message type: {type_msg}")
return
formatted_msg = self.format(type_msg, msg)
try:
self.bot.send_message(
chat_id=chat_id,
text=formatted_msg,
parse_mode=ParseMode.MARKDOWN_V2
)
except Exception as e:
self.logger.error(f"Error sending message: {e}")
def format(self, type_msg, msg):
return self.formats[type_msg] % msg
def send_photo(self, chat_id, photo, caption):
try:
self.bot.send_photo(
chat_id=chat_id,
photo=photo,
caption=caption,
parse_mode=ParseMode.MARKDOWN_V2
)
except Exception as e:
self.logger.error(f"Error sending photo: {e}")

View File

@ -0,0 +1,11 @@
from server import Server
def main():
Server()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,20 @@
from abc import ABCMeta, abstractmethod
class Miner(object):
__metaclass__ = ABCMeta
handle = None
@abstractmethod
def top_list(self, number):
pass
@abstractmethod
def get_movie_id(self, index):
pass
@abstractmethod
def get_movie_by_id(self, movie_id):
pass

View File

@ -0,0 +1,2 @@
from miners.Miner import Miner
from miners.imdb.ImdbMiner import IMDB

View File

@ -0,0 +1,20 @@
from miners.Miner import Miner
from imdbpie import Imdb
class IMDB(Miner):
def __init__(self):
self.handle = Imdb()
super(IMDB, self).__init__()
def top_list(self, number):
pop_movies = self.handle.top_250()
return pop_movies
def get_movie_id(self, index):
return "tt" + index
def get_movie_by_id(self, movie_id):
return self.handle.get_title_images(movie_id), self.handle.get_title(movie_id)

View File

@ -0,0 +1 @@
from miners.imdb.ImdbMiner import IMDB

View File

@ -0,0 +1,44 @@
import os
import sys
import datetime
from sqlalchemy import Column, ForeignKey, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy import create_engine
from conf import Config
Base = declarative_base()
class Player(Base):
__tablename__ = 'player'
id = Column(Integer, primary_key=True)
telegram_uid = Column(String(250), nullable=True)
email = Column(String(250), nullable=True)
phone = Column(String(30), nullable=True)
class Group(Base):
__tablename__ = 'group'
id = Column(Integer, primary_key=True)
name = Column(String(250), primary_key=True)
player_id = Column(Integer, ForeignKey('player.id'))
points = Column(Integer, nullable=True)
player = relationship(Player)
class Session(Base):
__tablename__ = 'session'
id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey('group.id'))
group = relationship(Group)
started = Column(DateTime, default=datetime.datetime.utcnow)
ended = Column(DateTime)
engine = create_engine(Config.DATABASE_URL)
Base.metadata.create_all(engine)

View File

@ -0,0 +1 @@
from models.Model import Player, Group, Session, engine

View File

@ -0,0 +1,16 @@
from abc import ABC, abstractmethod
class Movie(ABC):
id = 0
name = ''
type = ''
@abstractmethod
def __init__(self, name):
self.name = name
self.type = None
def get_name(self):
return self.name

View File

@ -0,0 +1,8 @@
from movie.Movie import Movie
class Pop(Movie):
def __init__(self):
super(Pop, self).__init__(name='Pop Movie')
self.type = "pop"

View File

@ -0,0 +1,2 @@
from movie.Movie import Movie
from movie.Pop import Pop

View File

@ -0,0 +1,17 @@
class Player:
id = ''
points = 0
name = ''
def __init__(self, uid):
self.id = uid
def get_points(self):
return self.points
def set_name(self, name):
self.name = name
def add_points(self, points):
self.points += points

View File

@ -0,0 +1 @@
from player.Player import Player

View File

@ -0,0 +1,98 @@
from player.Player import Player
from random import choice
from collection.Collection import Collection
from miners.imdb.ImdbMiner import IMDB
class Quiz:
movies_type = ''
movie = None
images = None
def __init__(self, session):
self.miner = IMDB()
self.session = session
def set_level(self, level):
# Implementar el ajuste del nivel si es necesario
pass
def rand_movie(self, rand_type=None):
collection = Collection(self.miner, rand_type)
self.movie, self.images = collection.get_rand_movie()
def get_movie_photo(self):
if not self.images or 'images' not in self.images:
raise ValueError("No images available")
try:
return choice(self.images['images'])['url']
except (IndexError, KeyError) as e:
raise ValueError("Error selecting image URL") from e
def get_question(self, rand_type=None):
try:
self.rand_movie(rand_type)
return self.get_movie_photo()
except ValueError:
return _("not_possible_find_movie")
def show(self, update, rand_type):
chat_id = update.message.chat_id
try:
movie_img = self.get_question(rand_type)
self.session.messenger.send_msg(chat_id, "movie_bot", "title")
self.session.messenger.send_photo(
chat_id, movie_img, caption=_("question_which_movie")
)
self.session.updater_counter()
self.session.status = "running"
except ValueError as e:
self.session.messenger.send_msg(
chat_id,
msg=_("error_fetching_question"),
type_msg="bold"
)
self.session.status = "stopped"
def check_resps(self, update):
chat_id = update.message.chat_id
if not self.movie or 'base' not in self.movie or 'title' not in self.movie['base']:
self.session.messenger.send_msg(
chat_id,
msg=_("error_movie_data"),
type_msg="bold"
)
return
if str.lower(self.movie['base']['title']) == str.lower(update.message.text):
player = Player(update.message.from_user.id)
player.name = f"{update.message.from_user.first_name} {update.message.from_user.last_name}"
try:
self.session.player_add(player)
except ValueError:
pass
self.session.players[update.message.from_user.id].add_points(1)
self.session.messenger.send_msg(
chat_id,
msg=(player.name, _("correct_answer")),
type_msg="bold"
)
self.movie = None
self.session.status = "stopped"
def check_expiration(self):
try:
self.session.update_timer()
except ValueError:
pass
if self.session.status == "timed_out":
self.session.messenger.send_msg(
chat_id=self.session.chat_id,
msg=_("times_up", self.movie['base']
['title'] if self.movie else ""),
type_msg="bold"
)
self.session.status = "stopped"
self.movie = None

View File

@ -0,0 +1 @@
from quiz.Quiz import Quiz

View File

@ -0,0 +1,6 @@
Babel==2.15.0
boto3==1.34.145
imdbpie~=4.0
python-dotenv==1.0.1
python-telegram-bot[job-queue]==21.4
six==1.16.0

View File

@ -0,0 +1 @@
python-3.12.4

View File

@ -0,0 +1,225 @@
import logging
from argparse import ArgumentParser
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import Application, CommandHandler, MessageHandler, filters, CallbackContext
import conf
import interfaces
import player
import quiz
import session
from translations.required import *
class Server:
logger = logging.getLogger(__name__)
SESSIONS = dict()
def __init__(self):
self.config_instance = self.config_init()
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG,
handlers=[
logging.FileHandler(self.config_instance.LOG_FILE),
logging.StreamHandler()
]
)
self.logger = logging.getLogger(__name__)
self.application = Application.builder().token(
self.config_instance.TELEGRAM_BOT_API).build()
# Add handlers
self.application.add_handler(MessageHandler(
filters.TEXT & ~filters.COMMAND, self.command_check_resps)) # Add handler for non-command messages
self.application.add_handler(
CommandHandler('start', self.command_start))
self.application.add_handler(CommandHandler('roll', self.command_roll))
self.application.add_handler(CommandHandler(
"leaderboard", self.command_leaderboard))
self.application.add_handler(
CommandHandler("repeat", self.command_repeat))
self.application.add_handler(CommandHandler("cut", self.command_cut))
self.application.add_handler(CommandHandler("stop", self.command_stop))
self.application.add_error_handler(self.error)
# Set up job queue
self.application.job_queue.run_repeating(
self.update_all_timers, interval=60)
self.logger.info("Iniciando...")
# Start the Bot
self.application.run_polling()
def config_init(self):
self.logger.info("Config init...")
arg_parser = ArgumentParser(description="Movie2 Bot")
arg_parser.add_argument(
"-e", "--env", metavar="env", type=str, default="prod",
help="Environment to run the bot: dev, test or prod"
)
arg_parser.add_argument(
"-v", "--verbose", metavar="verbose", type=bool, default=False,
help="Print information about running bot"
)
args = arg_parser.parse_args()
if args.env == "prod":
return conf.ProductionConfig()
elif args.env == "dev":
return conf.DevelopmentConfig()
else:
return conf.TestingConfig()
async def error(self, update: Update, context: CallbackContext):
self.logger.warning(
f'Update "{update}" caused error "{context.error}"')
async def update_all_timers(self, context: CallbackContext):
self.logger.info("Updating all timers...")
for sess in self.SESSIONS.values():
sess.update_timers()
if hasattr(sess, 'quiz'):
sess.quiz.check_expiration()
else:
self.logger.error("Quiz class not found in session")
async def command_start(self, update: Update, context: CallbackContext):
self.logger.info("Command start...")
chat_id = update.message.chat_id
await update.message.reply_text('¡Hola! El comando /start ha sido recibido.')
if chat_id not in self.SESSIONS:
self.messenger = interfaces.TelegramMessenger(
context.bot, self.logger
)
self.SESSIONS[chat_id] = session.Session(
chat_id, self.config_instance, self.logger
)
self.SESSIONS[chat_id].set_messenger(self.messenger)
# Crear una instancia de la clase Quiz
try:
self.SESSIONS[chat_id].quiz = quiz.Quiz(self.SESSIONS[chat_id])
except AttributeError as e:
self.logger.error(f"Error creating Quiz instance: {e}")
await self.messenger.send_msg(
chat_id, _("error_creating_quiz_instance")
)
logging.error(f"Error creating Quiz instance: {e}")
async def command_roll(self, update: Update, context: CallbackContext):
self.logger.info("Command roll...")
chat_id = update.message.chat_id
args = context.args
rand_type = args[0] if args else None
await self.SESSIONS[chat_id].messenger.send_msg(
chat_id, _("searching_movies"))
if hasattr(self.SESSIONS[chat_id], 'quiz'):
await self.SESSIONS[chat_id].quiz.show(update, rand_type)
else:
self.logger.error("Quiz instance not found in session")
async def command_leaderboard(self, update: Update, context: CallbackContext):
self.logger.info("Command leaderboard...")
chat_id = update.message.chat_id
sess = self.SESSIONS.get(chat_id)
if sess:
try:
await sess.messenger.send_msg(
chat_id, _("leader_board_title"), 'highlights'
)
ldb = sess.get_leaderboard()
await sess.messenger.send_msg(chat_id, ldb)
except ValueError as e:
await sess.messenger.send_msg(
chat_id, f'{update.message.from_user.first_name} {e.args[0]}'
)
else:
await update.message.reply_text(_("session_not_found"))
async def command_action(self, update: Update, context: CallbackContext):
self.logger.info("Command action...")
chat_id = update.message.chat_id
try:
player = player.Player(update.message.from_user.id)
player.name = f'{update.message.from_user.first_name} {update.message.from_user.last_name}'
self.SESSIONS[chat_id].player_add(player)
await self.SESSIONS[chat_id].messenger.send(
update, f'{player.name} {_("joined_the_game")}'
)
except ValueError as e:
await self.SESSIONS[chat_id].messenger.send(
update, f'{update.message.from_user.first_name} {e.args[0]}'
)
async def command_repeat(self, update: Update, context: CallbackContext):
self.logger.info("Command repeat...")
chat_id = update.message.chat_id
if chat_id in self.SESSIONS:
movie_img = self.SESSIONS[chat_id].quiz.get_question()
await self.SESSIONS[chat_id].messenger.send_msg(
chat_id, _("repeting")
)
await self.SESSIONS[chat_id].messenger.send_photo(
chat_id=chat_id, photo=movie_img,
caption=_("what_is_the_movie_series_name")
)
await self.SESSIONS[chat_id].messenger.send_msg(
chat_id, "==========================="
)
else:
await update.message.reply_text(_("session_not_found"))
async def command_cut(self, update: Update, context: CallbackContext):
self.logger.info("Command cut...")
chat_id = update.message.chat_id
if chat_id in self.SESSIONS:
try:
player = player.Player(update.message.from_user.id)
self.SESSIONS[chat_id].player_quit(player)
await self.SESSIONS[chat_id].messenger.send(
update, f'{player.name} {_("quit_the_game")}'
)
except ValueError as e:
await self.SESSIONS[chat_id].messenger.send(
update, f'{update.message.from_user.first_name} {e.args[0]}'
)
else:
await update.message.reply_text(_("session_not_found"))
async def command_stop(self, update: Update, context: CallbackContext):
self.logger.info("Command stop...")
chat_id = update.message.chat_id
if chat_id in self.SESSIONS:
self.SESSIONS[chat_id].stop()
await self.SESSIONS[chat_id].messenger.send(
update, _("game_stopped")
)
else:
await update.message.reply_text(_("session_not_found"))
# Implement the command_check_resps method
async def command_check_resps(self, update: Update, context: CallbackContext):
self.logger.info("Checking responses...")
chat_id = update.message.chat_id
if chat_id in self.SESSIONS:
user_response = update.message.text
sess = self.SESSIONS[chat_id]
# Add logic to handle user responses here
await sess.messenger.send_msg(chat_id, f"Received: {user_response}")
else:
await update.message.reply_text(_("session_not_found"))
if __name__ == "__main__":
Server()

View File

@ -0,0 +1 @@
from server.Server import Server

View File

@ -0,0 +1,55 @@
import datetime
class Session:
def __init__(self, chat_id, config, logger):
self.logger = logger
self.started = datetime.datetime.utcnow()
self.chat_id = chat_id
self.config = config
self.expiration = self.config.SESSION_EXPIRATION_TIME
self.players = {}
self.status = ''
self.messenger = None
self.counter = datetime.datetime.utcnow()
def player_add(self, player):
if player.id not in self.players:
self.players[player.id] = player
else:
self.update_log()
raise ValueError('Ya está en la partida...')
def player_quit(self, player):
if player.id in self.players:
del self.players[player.id]
else:
raise ValueError('no_está_en_la_partida')
def end(self):
self.ended = datetime.datetime.utcnow()
def get_leaderboard(self):
ldb = ''
for player in self.players.values():
ldb += f'{player.name} : {player.get_points()}\n'
return ldb
def set_messenger(self, messenger):
self.messenger = messenger
def update_timers(self):
if self.status == 'running':
elapsed = self.update_log()
if elapsed.seconds > self.expiration:
self.status = 'timed_out'
def update_counter(self):
self.counter = datetime.datetime.utcnow()
self.logger.debug(f'{self.chat_id} : updater_counter: {self.counter}')
def update_log(self):
elapsed = datetime.datetime.utcnow() - self.counter
self.logger.debug(f'{self.chat_id} : updater_timer: {elapsed}')
return elapsed

View File

@ -0,0 +1 @@
from session.Session import Session

View File

@ -0,0 +1,2 @@
[run]
omit = */.env/*

View File

@ -0,0 +1 @@
[python: **.py]

View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Script for Generate Translation files
# @author: manuelver
# requires: pybabel
TRANSLATION_DIR=./
LANGS=("pt_BR" "en" "es" "ca") # todo: smarter detection
GENERATED_DIR=./generated
options=("create" "update" "compile" "clean" "quit")
select opt in "${options[@]}"; do
case $opt in
"create")
echo "Creating files"
pybabel extract -F babel.cfg -o cinemonster.pot ../
for lang in ${LANGS[@]}; do
pybabel init -i cinemonster.pot -d "${GENERATED_DIR}" -l ${lang}
done
;;
"update")
echo "updating files"
pybabel update -i cinemonster.pot --previous -d "${GENERATED_DIR}"
;;
"compile")
echo "compiling files"
pybabel compile -d "${GENERATED_DIR}"
;;
"clean")
echo "cleaning files"
rm -rf "${GENERATED_DIR}"
rm cinemonster.pot
;;
"quit")
break
;;
*) echo invalid option ;;
esac
done

View File

@ -0,0 +1,68 @@
# Catalan translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-07-18 02:00+0200\n"
"PO-Revision-Date: 2024-07-18 02:00+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ca\n"
"Language-Team: ca <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n"
#: ../quiz/Quiz.py:35
msgid "not_possible_find_movie"
msgstr ""
#: ../quiz/Quiz.py:42
msgid "question_which_movie"
msgstr ""
#: ../quiz/Quiz.py:61
msgid "correct_answer"
msgstr ""
#: ../quiz/Quiz.py:76
msgid "times_up"
msgstr ""
#: ../server/Server.py:89
msgid "searching_movies"
msgstr ""
#: ../server/Server.py:97
msgid "leader_board_title"
msgstr ""
#: ../server/Server.py:114
msgid " joined_the_game"
msgstr ""
#: ../server/Server.py:124
msgid "repeting"
msgstr ""
#: ../server/Server.py:128
msgid "what_is_the_movie_series_name"
msgstr ""
#: ../server/Server.py:142
msgid " left_the_game"
msgstr ""
#: ../server/Server.py:153
msgid "ending_the_game"
msgstr ""
#: ../server/Server.py:157
msgid "game_was_not_finished"
msgstr ""

View File

@ -0,0 +1,68 @@
# English translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-07-18 02:00+0200\n"
"PO-Revision-Date: 2024-07-18 02:00+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n"
#: ../quiz/Quiz.py:35
msgid "not_possible_find_movie"
msgstr ""
#: ../quiz/Quiz.py:42
msgid "question_which_movie"
msgstr ""
#: ../quiz/Quiz.py:61
msgid "correct_answer"
msgstr ""
#: ../quiz/Quiz.py:76
msgid "times_up"
msgstr ""
#: ../server/Server.py:89
msgid "searching_movies"
msgstr ""
#: ../server/Server.py:97
msgid "leader_board_title"
msgstr ""
#: ../server/Server.py:114
msgid " joined_the_game"
msgstr ""
#: ../server/Server.py:124
msgid "repeting"
msgstr ""
#: ../server/Server.py:128
msgid "what_is_the_movie_series_name"
msgstr ""
#: ../server/Server.py:142
msgid " left_the_game"
msgstr ""
#: ../server/Server.py:153
msgid "ending_the_game"
msgstr ""
#: ../server/Server.py:157
msgid "game_was_not_finished"
msgstr ""

View File

@ -0,0 +1,68 @@
# Spanish translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-07-18 02:00+0200\n"
"PO-Revision-Date: 2024-07-18 02:00+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es\n"
"Language-Team: es <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n"
#: ../quiz/Quiz.py:35
msgid "not_possible_find_movie"
msgstr ""
#: ../quiz/Quiz.py:42
msgid "question_which_movie"
msgstr ""
#: ../quiz/Quiz.py:61
msgid "correct_answer"
msgstr ""
#: ../quiz/Quiz.py:76
msgid "times_up"
msgstr ""
#: ../server/Server.py:89
msgid "searching_movies"
msgstr ""
#: ../server/Server.py:97
msgid "leader_board_title"
msgstr ""
#: ../server/Server.py:114
msgid " joined_the_game"
msgstr ""
#: ../server/Server.py:124
msgid "repeting"
msgstr ""
#: ../server/Server.py:128
msgid "what_is_the_movie_series_name"
msgstr ""
#: ../server/Server.py:142
msgid " left_the_game"
msgstr ""
#: ../server/Server.py:153
msgid "ending_the_game"
msgstr ""
#: ../server/Server.py:157
msgid "game_was_not_finished"
msgstr ""

View File

@ -0,0 +1,68 @@
# Portuguese (Brazil) translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-07-18 02:00+0200\n"
"PO-Revision-Date: 2024-07-18 02:00+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: pt_BR\n"
"Language-Team: pt_BR <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.12.1\n"
#: ../quiz/Quiz.py:35
msgid "not_possible_find_movie"
msgstr ""
#: ../quiz/Quiz.py:42
msgid "question_which_movie"
msgstr ""
#: ../quiz/Quiz.py:61
msgid "correct_answer"
msgstr ""
#: ../quiz/Quiz.py:76
msgid "times_up"
msgstr ""
#: ../server/Server.py:89
msgid "searching_movies"
msgstr ""
#: ../server/Server.py:97
msgid "leader_board_title"
msgstr ""
#: ../server/Server.py:114
msgid " joined_the_game"
msgstr ""
#: ../server/Server.py:124
msgid "repeting"
msgstr ""
#: ../server/Server.py:128
msgid "what_is_the_movie_series_name"
msgstr ""
#: ../server/Server.py:142
msgid " left_the_game"
msgstr ""
#: ../server/Server.py:153
msgid "ending_the_game"
msgstr ""
#: ../server/Server.py:157
msgid "game_was_not_finished"
msgstr ""

View File

@ -0,0 +1,20 @@
import gettext
""" localization support """
en = gettext.translation(
'messages', 'translations/generated', languages=['en']
)
pt_br = gettext.translation(
'messages', 'translations/generated', languages=['pt_BR']
)
es = gettext.translation(
'messages', 'translations/generated', languages=['es']
)
ca = gettext.translation(
'messages', 'translations/generated', languages=['ca']
)
en.install()

View File

@ -6,16 +6,16 @@
</div> </div>
| Nombre | Descripción | Nivel | | Nombre | Descripción | Nivel |
| ----------------------------------------- | ------------------------------------------------- | ---------- | | ----------------------------------------------------- | ------------------------------------------------- | ---------- |
| [Bot id del chat](./01_id_bot/) | Bot que devuelve el id del bot | básico | | [Bot id del chat](./01_id_bot/) | Bot que devuelve el id del bot | básico |
| [Bot mensajes](./02_pruebas_bot/) | Bot que devuelve mensajes básicos. Loggin | básico | | [Bot mensajes](./02_pruebas_bot/) | Bot que devuelve mensajes básicos. Loggin | básico |
| [Bot de traducción](./03_translator_bot/) | Bot que traduce mensajes a varios idiomas. Loggin | intermedio | | [Bot de traducción](./03_translator_bot/) | Bot que traduce mensajes a varios idiomas. Loggin | intermedio |
| [Bot de clima](./04_clima_bot/) | Bot que devuelve el clima de una ciudad | intermedio | | [Bot de clima](./04_clima_bot/) | Bot que devuelve el clima de una ciudad | intermedio |
| [Bot de noticias](./05_rss_bot/) | Bot que devuelve noticias de última hora | intermedio | | [Bot de noticias](./05_rss_bot/) | Bot que devuelve noticias de última hora | intermedio |
| [Bot de películas I](./06_movie_bot/) | Bot que devuelve información de películas | intermedio | | [Bot de películas](./06_movie_bot/) | Bot que devuelve información de películas | intermedio |
| **Bot de películas II** (próximamente) | Bot que devuelve información de series | avanzado | | [Bot trivial de películas](./07_movie2_bot/README.md) | Bot que devuelve información de series | avanzado |
| **Bot de libros** (próximamente) | Bot que devuelve información de libros | avanzado | | **Bot de libros** (próximamente) | Bot que devuelve información de libros | avanzado |
| **Bot de recetas** (próximamente) | Bot que devuelve recetas de cocina | avanzado | | **Bot de recetas** (próximamente) | Bot que devuelve recetas de cocina | avanzado |
| **Bot de deportes** (próximamente) | Bot que devuelve información de deportes | avanzado | | **Bot de deportes** (próximamente) | Bot que devuelve información de deportes | avanzado |
| **Bot de mareas** (próximamente) | Bot que devuelve información de mareas | avanzado | | **Bot de mareas** (próximamente) | Bot que devuelve información de mareas | avanzado |