1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

Add mm command (VERY WIP; PROBABLY HAS 9001 RACE CONDITIONS)

This commit is contained in:
Steffo 2019-09-04 18:34:14 +02:00
parent e7b98880e0
commit e63086393b
11 changed files with 481 additions and 5 deletions

View file

@ -127,6 +127,8 @@ class DiscordBot(GenericBot):
error_message += '\n'.join(e.args) error_message += '\n'.join(e.args)
log.error(f"Error in {command.name}: {error_message}") log.error(f"Error in {command.name}: {error_message}")
await data.reply(f"⛔️ {error_message}") await data.reply(f"⛔️ {error_message}")
if __debug__:
raise
async def on_ready(cli): async def on_ready(cli):
log.debug("Connection successful, client is ready") log.debug("Connection successful, client is ready")

View file

@ -154,6 +154,8 @@ class GenericBot:
self._init_royalnet(royalnet_config=royalnet_config) self._init_royalnet(royalnet_config=royalnet_config)
except Exception as e: except Exception as e:
sentry_sdk.capture_exception(e) sentry_sdk.capture_exception(e)
log.error(f"{e.__class__.__name__} while initializing Royalnet: {' | '.join(e.args)}")
raise
async def run(self): async def run(self):
"""A blocking coroutine that should make the bot start listening to commands and requests.""" """A blocking coroutine that should make the bot start listening to commands and requests."""

View file

@ -67,7 +67,12 @@ class TelegramBot(GenericBot):
disable_web_page_preview=True) disable_web_page_preview=True)
async def get_author(data, error_if_none=False): async def get_author(data, error_if_none=False):
user: telegram.User = data.update.effective_user if data.update.message is not None:
user: telegram.User = data.update.message.from_user
elif data.update.callback_query is not None:
user: telegram.user = data.update.callback_query.from_user
else:
raise UnregisteredError("Author can not be determined")
if user is None: if user is None:
if error_if_none: if error_if_none:
raise UnregisteredError("No author for this message") raise UnregisteredError("No author for this message")
@ -148,6 +153,8 @@ class TelegramBot(GenericBot):
error_message = f"⛔️ [b]{e.__class__.__name__}[/b]\n" error_message = f"⛔️ [b]{e.__class__.__name__}[/b]\n"
error_message += '\n'.join(e.args) error_message += '\n'.join(e.args)
await data.reply(error_message) await data.reply(error_message)
if __debug__:
raise
async def _handle_callback_query(self, update: telegram.Update): async def _handle_callback_query(self, update: telegram.Update):
query: telegram.CallbackQuery = update.callback_query query: telegram.CallbackQuery = update.callback_query

View file

@ -23,6 +23,7 @@ from .videochannel import VideochannelCommand
from .dnditem import DnditemCommand from .dnditem import DnditemCommand
from .dndspell import DndspellCommand from .dndspell import DndspellCommand
from .trivia import TriviaCommand from .trivia import TriviaCommand
from .mm import MmCommand
__all__ = [ __all__ = [
"CiaoruoziCommand", "CiaoruoziCommand",
@ -44,5 +45,6 @@ __all__ = [
"VideochannelCommand", "VideochannelCommand",
"DnditemCommand", "DnditemCommand",
"DndspellCommand", "DndspellCommand",
"TriviaCommand" "TriviaCommand",
"MmCommand"
] ]

View file

@ -0,0 +1,271 @@
import typing
import datetime
import dateparser
import os
import telegram
import asyncio
from ..command import Command
from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...database.tables import MMEvent, MMDecision, MMResponse
from ...error import *
from ...utils import asyncify, telegram_escape, sleep_until
class MmCommand(Command):
"""Matchmaking command.
Requires the MM_CHANNEL_ID envvar to be set."""
name: str = "mm"
description: str = "Trova giocatori per una partita a qualcosa."
syntax: str = "[ (data) ] (nomegioco)\n[descrizione]"
require_alchemy_tables = {MMEvent, MMDecision, MMResponse}
@staticmethod
def _decision_string(mmevent: MMEvent) -> str:
return f"⚫️ Hai detto che forse parteciperai a [b]{mmevent.title}[/b] alle [b]{mmevent.datetime.strftime('%H:%M')}[/b].\n" \
f"Confermi di volerci essere?"
@staticmethod
def _decision_keyboard(mmevent: MMEvent) -> telegram.InlineKeyboardMarkup:
return telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton("🔵 Ci sarò!", callback_data=f"mm_{mmevent.mmid}_d_YES"),
telegram.InlineKeyboardButton("🔴 Non mi interessa...", callback_data=f"mm_{mmevent.mmid}_d_NO")]
])
@staticmethod
def _response_string(mmevent: MMEvent, count: int = 0) -> str:
if count == 0:
return f"🚩 E' ora di [b]{mmevent.title}[/b]!\n" \
f"Sei pronto?"
else:
return f"🕒 Sei in ritardo di [b]{count * 5}[/b] minuti per [b]{mmevent.title}[/b]...\n" \
f"Sei pronto?"
@staticmethod
def _response_keyboard(mmevent: MMEvent) -> telegram.InlineKeyboardMarkup:
return telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton("✅ Ci sono!", callback_data=f"mm_{mmevent.mmid}_r_YES")],
[telegram.InlineKeyboardButton("🕒 Aspettatemi 5 minuti!", callback_data=f"mm_{mmevent.mmid}_r_LATER")],
[telegram.InlineKeyboardButton("❌ Non ci sono più, mi spiace.", callback_data=f"mm_{mmevent.mmid}_r_NO")]
])
async def _update_message(self, mmevent: MMEvent) -> None:
client: telegram.Bot = self.interface.bot.client
try:
await asyncify(client.edit_message_text,
text=telegram_escape(str(mmevent)),
chat_id=os.environ["MM_CHANNEL_ID"],
message_id=mmevent.message_id,
parse_mode="HTML",
disable_web_page_preview=True,
reply_markup=mmevent.main_keyboard())
except telegram.error.BadRequest:
pass
async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name != "telegram":
raise UnsupportedError("mm is supported only on Telegram")
client: telegram.Bot = self.interface.bot.client
creator = await data.get_author(error_if_none=True)
timestring, title, description = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*\n?\s*(.+)?\s*")
try:
dt: typing.Optional[datetime.datetime] = dateparser.parse(timestring)
except OverflowError:
dt = None
if dt is None:
await data.reply("⚠️ La data che hai specificato non è valida.")
return
if dt <= datetime.datetime.now():
await data.reply("⚠️ La data che hai specificato è nel passato.")
return
mmevent: MMEvent = self.interface.alchemy.MMEvent(creator=creator,
datetime=dt,
title=title,
description=description,
state="WAITING")
self.interface.session.add(mmevent)
await asyncify(self.interface.session.commit)
async def decision_yes(data: CommandData):
royal = await data.get_author()
mmdecision: MMDecision = await asyncify(self.interface.session.query(self.interface.alchemy.MMDecision).filter_by(mmevent=mmevent, royal=royal).one_or_none)
if mmdecision is None:
mmdecision: MMDecision = self.interface.alchemy.MMDecision(royal=royal,
mmevent=mmevent,
decision="YES")
self.interface.session.add(mmdecision)
else:
mmdecision.decision = "YES"
# Can't asyncify this
self.interface.session.commit()
await self._update_message(mmevent)
return "🔵 Hai detto che ci sarai!"
async def decision_maybe(data: CommandData):
royal = await data.get_author()
mmdecision: MMDecision = await asyncify(self.interface.session.query(self.interface.alchemy.MMDecision).filter_by(mmevent=mmevent, royal=royal).one_or_none)
if mmdecision is None:
mmdecision: MMDecision = self.interface.alchemy.MMDecision(royal=royal,
mmevent=mmevent,
decision="MAYBE")
self.interface.session.add(mmdecision)
else:
mmdecision.decision = "MAYBE"
# Can't asyncify this
self.interface.session.commit()
await self._update_message(mmevent)
return "⚫️ Hai detto che forse ci sarai. Rispondi al messaggio di conferma 5 minuti prima dell'inizio!"
async def decision_no(data: CommandData):
royal = await data.get_author()
mmdecision: MMDecision = await asyncify(self.interface.session.query(self.interface.alchemy.MMDecision).filter_by(mmevent=mmevent, royal=royal).one_or_none)
if mmdecision is None:
mmdecision: MMDecision = self.interface.alchemy.MMDecision(royal=royal,
mmevent=mmevent,
decision="NO")
self.interface.session.add(mmdecision)
else:
mmdecision.decision = "NO"
# Can't asyncify this
self.interface.session.commit()
await self._update_message(mmevent)
return "🔴 Hai detto che non ti interessa."
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_d_YES", decision_yes)
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_d_MAYBE", decision_maybe)
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_d_NO", decision_no)
message: telegram.Message = await asyncify(client.send_message,
chat_id=os.environ["MM_CHANNEL_ID"],
text=telegram_escape(str(mmevent)),
parse_mode="HTML",
disable_webpage_preview=True,
reply_markup=mmevent.main_keyboard())
mmevent.message_id = message.message_id
# Can't asyncify this
self.interface.session.commit()
await sleep_until(dt - datetime.timedelta(minutes=10))
mmevent.state = "DECISION"
await asyncify(self.interface.session.commit)
for mmdecision in mmevent.decisions:
mmdecision: MMDecision
if mmdecision.decision == "MAYBE":
await asyncify(client.send_message,
chat_id=mmdecision.royal.telegram[0].tg_id,
text=telegram_escape(self._decision_string(mmevent)),
parse_mode="HTML",
disable_webpage_preview=True,
reply_markup=self._decision_keyboard(mmevent))
await self._update_message(mmevent)
await sleep_until(dt)
async def response_yes(data: CommandData):
royal = await data.get_author()
mmresponse: MMResponse = await asyncify(
self.interface.session.query(self.interface.alchemy.MMResponse).filter_by(mmevent=mmevent, royal=royal).one_or_none)
mmresponse.response = "YES"
# Can't asyncify this
self.interface.session.commit()
await self._update_message(mmevent)
return "✅ Sei pronto!"
async def response_later(data: CommandData):
royal = await data.get_author()
mmresponse: MMResponse = await asyncify(
self.interface.session.query(self.interface.alchemy.MMResponse).filter_by(mmevent=mmevent, royal=royal).one_or_none)
mmresponse.response = "LATER"
# Can't asyncify this
self.interface.session.commit()
await self._update_message(mmevent)
return "🕒 Hai chiesto agli altri di aspettarti 5 minuti."
async def response_no(data: CommandData):
royal = await data.get_author()
mmresponse: MMResponse = await asyncify(
self.interface.session.query(self.interface.alchemy.MMResponse).filter_by(mmevent=mmevent, royal=royal).one_or_none)
mmresponse.response = "NO"
# Can't asyncify this
self.interface.session.commit()
await self._update_message(mmevent)
return "❌ Hai detto che non ci sarai."
async def start_now():
mmevent.state = "STARTED"
for mmresponse in mmevent.responses:
if mmresponse.response is None:
mmresponse.response = "NO"
if mmresponse.response == "LATER":
mmresponse.response = "NO"
await self._update_message(mmevent)
await asyncify(self.interface.session.commit)
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_r_YES")
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_r_LATER")
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_r_NO")
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_start")
async def start_key(data: CommandData):
royal = await data.get_author()
if royal == creator:
await start_now()
mmevent.state = "READY_CHECK"
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_d_YES")
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_d_MAYBE")
self.interface.unregister_keyboard_key(f"mm_{mmevent.mmid}_d_NO")
for mmdecision in mmevent.decisions:
if mmdecision.decision == "MAYBE":
mmdecision.decision = "NO"
elif mmdecision.decision == "YES":
mmresponse: MMResponse = self.interface.alchemy.MMResponse(royal=mmdecision.royal, mmevent=mmevent)
self.interface.session.add(mmresponse)
await asyncify(self.interface.session.commit)
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_r_YES", response_yes)
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_r_LATER", response_later)
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_r_NO", response_no)
self.interface.register_keyboard_key(f"mm_{mmevent.mmid}_start", start_key)
count = 0
while True:
for mmresponse in mmevent.responses:
# Send messages
if mmresponse.response is None:
await asyncify(client.send_message,
chat_id=mmresponse.royal.telegram[0].tg_id,
text=telegram_escape(self._response_string(mmevent, count=count)),
parse_mode="HTML",
disable_webpage_preview=True,
reply_markup=self._response_keyboard(mmevent))
await self._update_message(mmevent)
# Wait
await asyncio.sleep(300)
# Advance cycle
for mmresponse in mmevent.responses:
if mmresponse.response is None:
mmresponse.response = "NO"
if mmresponse.response == "LATER":
mmresponse.response = None
# Check if the event can start
for mmresponse in mmevent.responses:
if mmresponse.response is None:
break
else:
break
count += 1
await start_now()

View file

@ -61,6 +61,9 @@ class ReminderCommand(Command):
if date is None: if date is None:
await data.reply("⚠️ La data che hai inserito non è valida.") await data.reply("⚠️ La data che hai inserito non è valida.")
return return
if date <= datetime.datetime.now():
await data.reply("⚠️ La data che hai specificato è nel passato.")
return
await data.reply(f"✅ Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]") await data.reply(f"✅ Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]")
if self.interface.name == "telegram": if self.interface.name == "telegram":
interface_data = pickle.dumps(data.update.effective_chat.id) interface_data = pickle.dumps(data.update.effective_chat.id)

View file

@ -13,6 +13,10 @@ from .medalawards import MedalAward
from .bios import Bio from .bios import Bio
from .reminders import Reminder from .reminders import Reminder
from .triviascores import TriviaScore from .triviascores import TriviaScore
from .mmdecisions import MMDecision
from .mmevents import MMEvent
from .mmresponse import MMResponse
__all__ = ["Royal", "Telegram", "Diario", "Alias", "ActiveKvGroup", "Keyvalue", "Keygroup", "Discord", "WikiPage", __all__ = ["Royal", "Telegram", "Diario", "Alias", "ActiveKvGroup", "Keyvalue", "Keygroup", "Discord", "WikiPage",
"WikiRevision", "Medal", "MedalAward", "Bio", "Reminder", "TriviaScore"] "WikiRevision", "Medal", "MedalAward", "Bio", "Reminder", "TriviaScore", "MMDecision", "MMEvent",
"MMResponse"]

View file

@ -0,0 +1,36 @@
from sqlalchemy import Column, \
Integer, \
String, \
ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declared_attr
from .royals import Royal
from .mmevents import MMEvent
class MMDecision:
__tablename__ = "mmdecisions"
@declared_attr
def royal_id(self):
return Column(Integer, ForeignKey("royals.uid"), primary_key=True)
@declared_attr
def royal(self):
return relationship("Royal", backref="mmdecisions_taken")
@declared_attr
def mmevent_id(self):
return Column(Integer, ForeignKey("mmevents.mmid"), primary_key=True)
@declared_attr
def mmevent(self):
return relationship("MMEvent", backref="decisions")
@declared_attr
def decision(self):
# Valid decisions are YES, MAYBE or NO
return Column(String, nullable=False)
def __repr__(self):
return f"<MMDecision of {self.royal}: {self.decision}>"

View file

@ -0,0 +1,113 @@
import telegram
import typing
from sqlalchemy import Column, \
Integer, \
DateTime, \
String, \
Text, \
ForeignKey, \
BigInteger
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declared_attr
from .royals import Royal
if typing.TYPE_CHECKING:
from .mmdecisions import MMDecision
from .mmresponse import MMResponse
class MMEvent:
__tablename__ = "mmevents"
@declared_attr
def creator_id(self):
return Column(Integer, ForeignKey("royals.uid"), nullable=False)
@declared_attr
def creator(self):
return relationship("Royal", backref="mmevents_created")
@declared_attr
def mmid(self):
return Column(Integer, primary_key=True)
@declared_attr
def datetime(self):
return Column(DateTime, nullable=False)
@declared_attr
def title(self):
return Column(String, nullable=False)
@declared_attr
def description(self):
return Column(Text, nullable=False, default="")
@declared_attr
def state(self):
# Valid states are WAITING, DECISION, READY_CHECK, STARTED
return Column(String, nullable=False, default="WAITING")
@declared_attr
def message_id(self):
return Column(BigInteger)
def __repr__(self):
return f"<MMEvent {self.mmid}: {self.title}>"
def __str__(self):
text = f"🌐 [b]{self.title}[/b] - [b]{self.datetime.strftime('%Y-%m-%d %H:%M')}[/b]\n"
if self.description:
text += f"{self.description}\n"
text += "\n"
if self.state == "WAITING" or self.state == "DECISION":
for mmdecision in self.decisions:
mmdecision: "MMDecision"
if mmdecision.decision == "YES":
text += "🔵 "
elif mmdecision.decision == "MAYBE":
text += "⚫️ "
elif mmdecision.decision == "NO":
text += "🔴 "
else:
raise ValueError(f"decision is of an unknown value ({mmdecision.decision})")
text += f"{mmdecision.royal}\n"
elif self.state == "READY_CHECK":
for mmresponse in self.responses:
mmresponse: "MMResponse"
if mmresponse.response is None:
text += ""
elif mmresponse.response == "YES":
text += ""
elif mmresponse.response == "LATER":
text += "🕒 "
elif mmresponse.response == "NO":
text += ""
else:
raise ValueError(f"response is of an unknown value ({mmresponse.response})")
text += f"{mmresponse.royal}\n"
elif self.state == "STARTED":
for mmresponse in self.responses:
if mmresponse.response == "YES":
text += f"{mmresponse.royal}\n"
return text
def main_keyboard(self) -> typing.Optional[telegram.InlineKeyboardMarkup]:
if self.state == "WAITING":
return telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton("🔵 Ci sarò!", callback_data=f"mm_{self.mmid}_d_YES")],
[telegram.InlineKeyboardButton("⚫️ Forse...", callback_data=f"mm_{self.mmid}_d_MAYBE")],
[telegram.InlineKeyboardButton("🔴 Non mi interessa.", callback_data=f"mm_{self.mmid}_d_NO")]
])
elif self.state == "DECISION":
return telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton("🔵 Ci sarò!", callback_data=f"mm_{self.mmid}_d_YES"),
telegram.InlineKeyboardButton("🔴 Non mi interessa...", callback_data=f"mm_{self.mmid}_d_NO")]
])
elif self.state == "READY_CHECK":
return telegram.InlineKeyboardMarkup([
[telegram.InlineKeyboardButton("🚩 Avvia la partita", callback_data=f"mm_{self.mmid}_start")]
])
elif self.state == "STARTED":
return None
else:
raise ValueError(f"state is of an unknown value ({self.state})")

View file

@ -0,0 +1,36 @@
from sqlalchemy import Column, \
Integer, \
String, \
ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declared_attr
from .royals import Royal
from .mmevents import MMEvent
class MMResponse:
__tablename__ = "mmresponse"
@declared_attr
def royal_id(self):
return Column(Integer, ForeignKey("royals.uid"), primary_key=True)
@declared_attr
def royal(self):
return relationship("Royal", backref="mmresponses_given")
@declared_attr
def mmevent_id(self):
return Column(Integer, ForeignKey("mmevents.mmid"), primary_key=True)
@declared_attr
def mmevent(self):
return relationship("MMEvent", backref="responses")
@declared_attr
def response(self):
# Valid decisions are YES, LATER or NO
return Column(String)
def __repr__(self):
return f"<MMResponse of {self.royal}: {self.response}>"

View file

@ -24,7 +24,6 @@ sentry_dsn = os.environ.get("SENTRY_DSN")
# noinspection PyUnreachableCode # noinspection PyUnreachableCode
if __debug__: if __debug__:
commands = [ commands = [
] ]
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
else: else:
@ -48,7 +47,8 @@ else:
VideochannelCommand, VideochannelCommand,
DnditemCommand, DnditemCommand,
DndspellCommand, DndspellCommand,
TriviaCommand TriviaCommand,
MmCommand
] ]
log.setLevel(logging.INFO) log.setLevel(logging.INFO)