mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-30 15:04:18 +00:00
456 lines
16 KiB
Python
456 lines
16 KiB
Python
|
import contextlib
|
||
|
import random
|
||
|
from typing import *
|
||
|
import logging
|
||
|
import datetime
|
||
|
import enum
|
||
|
import asyncio as aio
|
||
|
|
||
|
from telegram import InlineKeyboardMarkup as InKM
|
||
|
from telegram import InlineKeyboardButton as InKB
|
||
|
from telegram import Message as PTBMessage
|
||
|
from telegram import TelegramError
|
||
|
|
||
|
import royalnet.commands as rc
|
||
|
import royalnet.utils as ru
|
||
|
import royalnet.serf.telegram as rst
|
||
|
|
||
|
from ..types import MMChoice, MMInterfaceDataTelegram
|
||
|
from ..tables import MMEvent, MMResponse, FiorygiTransaction
|
||
|
|
||
|
|
||
|
class Interrupts(enum.Enum):
|
||
|
TIME_RAN_OUT = enum.auto()
|
||
|
MANUAL_START = enum.auto()
|
||
|
MANUAL_DELETE = enum.auto()
|
||
|
|
||
|
|
||
|
log = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
mmchoice_sorting = {
|
||
|
MMChoice.YES: -4,
|
||
|
MMChoice.LATE_SHORT: -3,
|
||
|
MMChoice.LATE_MEDIUM: -2,
|
||
|
MMChoice.LATE_LONG: -1,
|
||
|
MMChoice.MAYBE: 0,
|
||
|
MMChoice.NO: 1
|
||
|
}
|
||
|
|
||
|
|
||
|
class MMTask:
|
||
|
def __init__(self, mmid: int, *, command: rc.Command):
|
||
|
log.debug(f"Creating task for: {mmid}")
|
||
|
|
||
|
self.loop: aio.AbstractEventLoop = command.loop
|
||
|
self.task: Optional[aio.Task] = None
|
||
|
self.queue: aio.Queue = aio.Queue(loop=self.loop)
|
||
|
self.command: rc.Command = command
|
||
|
self.mmid: int = mmid
|
||
|
|
||
|
self._session: Optional = None
|
||
|
self._EventT: Optional[Type[MMEvent]] = None
|
||
|
self._ResponseT: Optional[Type[MMResponse]] = None
|
||
|
self._mmevent: Optional[MMEvent] = None
|
||
|
|
||
|
@property
|
||
|
def is_running(self):
|
||
|
return self.task is not None
|
||
|
|
||
|
def sync(self):
|
||
|
self._session.refresh(self._mmevent)
|
||
|
|
||
|
def get_response_line(self, response: MMResponse):
|
||
|
self.sync()
|
||
|
|
||
|
# noinspection PyListCreation
|
||
|
line = []
|
||
|
|
||
|
# Emoji
|
||
|
line.append(f"{response.choice.value}")
|
||
|
|
||
|
# Mention the user if he said yes, otherwise just display his name
|
||
|
if response.choice == MMChoice.NO:
|
||
|
line.append(f"{response.user.telegram[0].name()}")
|
||
|
else:
|
||
|
line.append(f"{response.user.telegram[0].mention()}")
|
||
|
|
||
|
# Late time
|
||
|
if response.choice == MMChoice.LATE_SHORT:
|
||
|
td = self._mmevent.datetime + datetime.timedelta(minutes=10)
|
||
|
line.append(f"[{td.strftime('%H:%M')}]")
|
||
|
|
||
|
elif response.choice == MMChoice.LATE_MEDIUM:
|
||
|
td = self._mmevent.datetime + datetime.timedelta(minutes=30)
|
||
|
line.append(f"[{td.strftime('%H:%M')}]")
|
||
|
|
||
|
elif response.choice == MMChoice.LATE_LONG:
|
||
|
td = self._mmevent.datetime + datetime.timedelta(minutes=60)
|
||
|
line.append(f"[{td.strftime('%H:%M')}+]")
|
||
|
|
||
|
# Creator
|
||
|
if response.user == self._mmevent.creator:
|
||
|
line.append("👑")
|
||
|
|
||
|
# Result
|
||
|
return " ".join(line)
|
||
|
|
||
|
@property
|
||
|
def channel_text(self) -> str:
|
||
|
self.sync()
|
||
|
|
||
|
# noinspection PyListCreation
|
||
|
text = []
|
||
|
|
||
|
# First line
|
||
|
if self._mmevent.datetime is None:
|
||
|
text.append(f"🌐 [Prossimamente] [b]{self._mmevent.title}[/b]")
|
||
|
else:
|
||
|
text.append(f"🚩 [{self._mmevent.datetime.strftime('%Y-%m-%d %H:%M')}] [b]{self._mmevent.title}[/b]")
|
||
|
|
||
|
# Description
|
||
|
if self._mmevent.description:
|
||
|
text.append(f"{self._mmevent.description}")
|
||
|
|
||
|
# Spacer
|
||
|
text.append("")
|
||
|
|
||
|
# Responses
|
||
|
responses = sorted(self._mmevent.responses, key=lambda r: mmchoice_sorting[r.choice])
|
||
|
for response in responses:
|
||
|
text.append(self.get_response_line(response))
|
||
|
|
||
|
# Result
|
||
|
return "\n".join(text)
|
||
|
|
||
|
@property
|
||
|
def start_text(self) -> str:
|
||
|
self.sync()
|
||
|
|
||
|
# noinspection PyListCreation
|
||
|
text = []
|
||
|
|
||
|
# First line
|
||
|
if self._mmevent.datetime is None:
|
||
|
text.append(f"🌐 Le iscrizioni all'evento [b]{self._mmevent.title}[/b] sono terminate!")
|
||
|
else:
|
||
|
text.append(f"🚩 L'evento [b]{self._mmevent.title}[/b] è iniziato!")
|
||
|
|
||
|
# Description
|
||
|
if self._mmevent.description:
|
||
|
text.append(f"{self._mmevent.description}")
|
||
|
|
||
|
# Spacer
|
||
|
text.append("")
|
||
|
|
||
|
# Responses
|
||
|
responses = sorted(self._mmevent.responses, key=lambda r: mmchoice_sorting[r.choice])
|
||
|
for response in responses:
|
||
|
text.append(self.get_response_line(response))
|
||
|
|
||
|
# Result
|
||
|
return "\n".join(text)
|
||
|
|
||
|
@property
|
||
|
def delete_text(self) -> str:
|
||
|
return f"🗑 L'evento [b]{self._mmevent.title}[/b] è stato eliminato."
|
||
|
|
||
|
def get_answer_callback(self, choice: MMChoice):
|
||
|
async def callback(data: rc.CommandData):
|
||
|
# Find the user who clicked on the button
|
||
|
user = await data.get_author(error_if_none=True)
|
||
|
|
||
|
# Get the related MMEvent
|
||
|
mmevent: MMEvent = await ru.asyncify(data.session.query(self._EventT).get, self.mmid)
|
||
|
|
||
|
# Check if the user had already responded
|
||
|
mmresponse: MMResponse = await ru.asyncify(
|
||
|
data.session.query(self._ResponseT).filter_by(user=user, mmevent=mmevent).one_or_none
|
||
|
)
|
||
|
|
||
|
if mmresponse is None:
|
||
|
# If they didn't respond, create a new MMResponse
|
||
|
mmresponse = self._ResponseT(user=user, mmevent=mmevent, choice=choice)
|
||
|
data.session.add(mmresponse)
|
||
|
|
||
|
# Drop fiorygi
|
||
|
if random.randrange(100) < self.command.config["Matchmaking"]["fiorygi_award_chance"]:
|
||
|
await FiorygiTransaction.spawn_fiorygi(data, user, 1, "aver risposto a un matchmaking")
|
||
|
else:
|
||
|
# Change their response
|
||
|
mmresponse.choice = choice
|
||
|
await data.session_commit()
|
||
|
|
||
|
await self.telegram_channel_message_update()
|
||
|
|
||
|
await data.reply(f"{choice.value} Hai risposto al matchmaking!")
|
||
|
return callback
|
||
|
|
||
|
def get_delete_callback(self):
|
||
|
async def callback(data: rc.CommandData):
|
||
|
# Find the user who clicked on the button
|
||
|
user = await data.get_author(error_if_none=True)
|
||
|
|
||
|
# Get the related MMEvent
|
||
|
mmevent: MMEvent = await ru.asyncify(data.session.query(self._EventT).get, self.mmid)
|
||
|
|
||
|
# Ensure the user has the required roles to start the matchmaking
|
||
|
if user != mmevent.creator and "admin" not in user.roles:
|
||
|
raise rc.UserError("Non hai i permessi per eliminare questo matchmaking!")
|
||
|
|
||
|
# Interrupt the matchmaking with the MANUAL_DELETE reason
|
||
|
await self.queue.put(Interrupts.MANUAL_DELETE)
|
||
|
|
||
|
await data.reply(f"🗑 Evento eliminato!")
|
||
|
return callback
|
||
|
|
||
|
def get_start_callback(self):
|
||
|
async def callback(data: rc.CommandData):
|
||
|
# Find the user who clicked on the button
|
||
|
user = await data.get_author(error_if_none=True)
|
||
|
|
||
|
# Get the related MMEvent
|
||
|
mmevent: MMEvent = await ru.asyncify(data.session.query(self._EventT).get, self.mmid)
|
||
|
|
||
|
# Ensure the user has the required roles to start the matchmaking
|
||
|
if user != mmevent.creator and "admin" not in user.roles:
|
||
|
raise rc.UserError("Non hai i permessi per eliminare questo matchmaking!")
|
||
|
|
||
|
# Interrupt the matchmaking with the MANUAL_DELETE reason
|
||
|
await self.queue.put(Interrupts.MANUAL_START)
|
||
|
|
||
|
await data.reply(f"🚩 Evento avviato!")
|
||
|
return callback
|
||
|
|
||
|
@property
|
||
|
def royalnet_keyboard(self):
|
||
|
# noinspection PyListCreation
|
||
|
rows = []
|
||
|
|
||
|
rows.append([
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"{MMChoice.YES.value}",
|
||
|
text="Ci sarò!",
|
||
|
callback=self.get_answer_callback(MMChoice.YES)
|
||
|
),
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"{MMChoice.MAYBE.value}",
|
||
|
text="Forse...",
|
||
|
callback=self.get_answer_callback(MMChoice.MAYBE)
|
||
|
),
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"{MMChoice.NO.value}",
|
||
|
text="Non mi interessa.",
|
||
|
callback=self.get_answer_callback(MMChoice.NO)
|
||
|
),
|
||
|
])
|
||
|
|
||
|
if self._mmevent.datetime is not None:
|
||
|
rows.append([
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"{MMChoice.LATE_SHORT.value}",
|
||
|
text="10 min",
|
||
|
callback=self.get_answer_callback(MMChoice.LATE_SHORT)
|
||
|
),
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"{MMChoice.LATE_MEDIUM.value}",
|
||
|
text="30 min",
|
||
|
callback=self.get_answer_callback(MMChoice.LATE_MEDIUM)
|
||
|
),
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"{MMChoice.LATE_LONG.value}",
|
||
|
text="60 min",
|
||
|
callback=self.get_answer_callback(MMChoice.LATE_LONG)
|
||
|
)
|
||
|
])
|
||
|
|
||
|
rows.append([
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"🗑",
|
||
|
text="Elimina",
|
||
|
callback=self.get_delete_callback()
|
||
|
),
|
||
|
rc.KeyboardKey(
|
||
|
interface=self.command.interface,
|
||
|
short=f"🚩",
|
||
|
text="Inizia",
|
||
|
callback=self.get_start_callback()
|
||
|
),
|
||
|
])
|
||
|
|
||
|
return rows
|
||
|
|
||
|
@property
|
||
|
def telegram_keyboard(self):
|
||
|
# noinspection PyListCreation
|
||
|
rows = []
|
||
|
key_id = 0
|
||
|
|
||
|
for r_row in self.royalnet_keyboard:
|
||
|
row = []
|
||
|
for r_key in r_row:
|
||
|
# Generate a unique callback string
|
||
|
callback_str = f"mm{self.mmid}_{key_id}"
|
||
|
|
||
|
# Create a InlineKeyboardButton with that callback string
|
||
|
row.append(InKB(f"{r_key.short} {r_key.text}", callback_data=callback_str))
|
||
|
|
||
|
# Increase the key_id
|
||
|
key_id += 1
|
||
|
rows.append(row)
|
||
|
|
||
|
# Return the resulting InlineKeyboardMarkup
|
||
|
return InKM(rows)
|
||
|
|
||
|
def register_telegram_keyboard(self, inkm: InKM):
|
||
|
# noinspection PyListCreation
|
||
|
royalnet_keyboard = self.royalnet_keyboard
|
||
|
for x, row in enumerate(inkm.inline_keyboard):
|
||
|
for y, key in enumerate(row):
|
||
|
key: InKB
|
||
|
self.command.interface.serf.register_keyboard_key(key.callback_data, key=royalnet_keyboard[x][y])
|
||
|
|
||
|
def unregister_telegram_keyboard(self, inkm: InKM):
|
||
|
for row in inkm.inline_keyboard:
|
||
|
for key in row:
|
||
|
key: InKB
|
||
|
self.command.interface.serf.unregister_keyboard_key(key.callback_data)
|
||
|
|
||
|
async def wait_until_due(self):
|
||
|
"""When the event is due, interrupt the MMTask with the TIME_RAN_OUT reason."""
|
||
|
if self._mmevent.datetime is None:
|
||
|
return
|
||
|
await ru.sleep_until(self._mmevent.datetime)
|
||
|
await self.queue.put(Interrupts.TIME_RAN_OUT)
|
||
|
|
||
|
@property
|
||
|
def telegram_channel_id(self):
|
||
|
return self.command.config["Matchmaking"]["mm_telegram_channel_id"]
|
||
|
|
||
|
@property
|
||
|
def telegram_group_id(self):
|
||
|
return self.command.config["Matchmaking"]["mm_telegram_group_id"]
|
||
|
|
||
|
@contextlib.asynccontextmanager
|
||
|
async def telegram_channel_message(self):
|
||
|
|
||
|
# Generate the InlineKeyboardMarkup
|
||
|
inkm = self.telegram_keyboard
|
||
|
|
||
|
# Bind the Royalnet buttons to the Telegram keyboard
|
||
|
log.debug(f"Registering keyboard for: {self.mmid}")
|
||
|
self.register_telegram_keyboard(inkm)
|
||
|
|
||
|
# If the event has no associated interface data...
|
||
|
if self._mmevent.interface_data is None:
|
||
|
# Send the channel message
|
||
|
log.debug(f"Sending message for: {self.mmid}")
|
||
|
message: PTBMessage = await self.command.interface.serf.api_call(
|
||
|
self.command.interface.serf.client.send_message,
|
||
|
chat_id=self.telegram_channel_id,
|
||
|
text=rst.escape(self.channel_text),
|
||
|
parse_mode="HTML",
|
||
|
disable_webpage_preview=True,
|
||
|
reply_markup=inkm
|
||
|
)
|
||
|
|
||
|
# Register the interface data on the database
|
||
|
self._mmevent.interface_data = MMInterfaceDataTelegram(
|
||
|
chat_id=self.telegram_channel_id,
|
||
|
message_id=message.message_id
|
||
|
)
|
||
|
self._session.commit()
|
||
|
|
||
|
# Wait until the event starts
|
||
|
yield
|
||
|
|
||
|
# Delete the channel message
|
||
|
log.debug(f"Deleting message for: {self.mmid}")
|
||
|
await self.command.interface.serf.api_call(
|
||
|
self.command.interface.serf.client.delete_message,
|
||
|
chat_id=self._mmevent.interface_data.chat_id,
|
||
|
message_id=self._mmevent.interface_data.message_id
|
||
|
)
|
||
|
|
||
|
# Unregister the Telegram keyboard bindings
|
||
|
log.debug(f"Unregistering keyboard for: {self.mmid}")
|
||
|
self.unregister_telegram_keyboard(inkm)
|
||
|
|
||
|
async def telegram_channel_message_update(self):
|
||
|
log.debug(f"Updating message for: {self.mmid}")
|
||
|
try:
|
||
|
await ru.asyncify(
|
||
|
self.command.interface.serf.client.edit_message_text,
|
||
|
chat_id=self._mmevent.interface_data.chat_id,
|
||
|
text=rst.escape(self.channel_text),
|
||
|
message_id=self._mmevent.interface_data.message_id,
|
||
|
parse_mode="HTML",
|
||
|
disable_web_page_preview=True,
|
||
|
reply_markup=self.telegram_keyboard
|
||
|
)
|
||
|
except TelegramError as e:
|
||
|
log.warning(f"TelegramError during update: {e}")
|
||
|
|
||
|
async def telegram_group_message_start(self):
|
||
|
await self.command.interface.serf.api_call(
|
||
|
self.command.interface.serf.client.send_message,
|
||
|
chat_id=self.telegram_group_id,
|
||
|
text=rst.escape(self.start_text),
|
||
|
parse_mode="HTML",
|
||
|
disable_webpage_preview=True
|
||
|
)
|
||
|
|
||
|
async def telegram_group_message_delete(self):
|
||
|
await self.command.interface.serf.api_call(
|
||
|
self.command.interface.serf.client.send_message,
|
||
|
chat_id=self.telegram_group_id,
|
||
|
text=rst.escape(self.delete_text),
|
||
|
parse_mode="HTML",
|
||
|
disable_webpage_preview=True
|
||
|
)
|
||
|
|
||
|
def start(self):
|
||
|
log.debug(f"Starting task for: {self.mmid}")
|
||
|
self.task = self.loop.create_task(self.run())
|
||
|
|
||
|
@ru.sentry_async_wrap()
|
||
|
async def run(self):
|
||
|
log.debug(f"Running task for: {self.mmid}")
|
||
|
|
||
|
# Create a new session for the MMTask
|
||
|
self._session = self.command.alchemy.Session()
|
||
|
self._EventT = self.command.alchemy.get(MMEvent)
|
||
|
self._ResponseT = self.command.alchemy.get(MMResponse)
|
||
|
self._mmevent: MMEvent = self._session.query(self._EventT).get(self.mmid)
|
||
|
|
||
|
if self._mmevent is None:
|
||
|
raise rc.InvalidInputError(f"No event exists with the mmid {self.mmid}.")
|
||
|
|
||
|
if self._mmevent.interface != "telegram":
|
||
|
raise rc.UnsupportedError("Currently only the Telegram interface is supported.")
|
||
|
|
||
|
async with self.telegram_channel_message():
|
||
|
self.loop.create_task(self.wait_until_due())
|
||
|
|
||
|
# Sleep until something interrupts the task
|
||
|
interrupt = await self.queue.get()
|
||
|
|
||
|
# Mark the event as interrupted
|
||
|
self._mmevent.interrupted = True
|
||
|
self._session.commit()
|
||
|
|
||
|
# Send a group notification if the MMEvent wasn't deleted
|
||
|
if interrupt != Interrupts.MANUAL_DELETE:
|
||
|
await self.telegram_group_message_start()
|
||
|
else:
|
||
|
await self.telegram_group_message_delete()
|
||
|
|
||
|
# Close the database session
|
||
|
await ru.asyncify(self._session.close)
|