mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
5.0a93
This commit is contained in:
parent
1f6992cfb8
commit
2c0c9d300b
71 changed files with 10 additions and 4266 deletions
|
@ -1,44 +1,9 @@
|
||||||
aiohttp==3.5.4
|
dateparser
|
||||||
async-timeout==3.0.1
|
youtube_dl
|
||||||
attrs==19.3.0
|
ffmpeg_python
|
||||||
bcrypt==3.1.7
|
urllib3
|
||||||
certifi==2019.9.11
|
sqlalchemy
|
||||||
cffi==1.13.01
|
starlette
|
||||||
chardet==3.0.4
|
keyring
|
||||||
click==7.0
|
click
|
||||||
cryptography==2.8
|
royalherald
|
||||||
dateparser==0.7.2
|
|
||||||
dice==2.4.2
|
|
||||||
dnspython==1.15.0
|
|
||||||
dnspython3==1.15.0
|
|
||||||
entrypoints==0.3
|
|
||||||
ffmpeg-python==0.2.0
|
|
||||||
future==0.18.1
|
|
||||||
idna==2.8
|
|
||||||
keyring==19.2.0
|
|
||||||
markdown2==2.3.8
|
|
||||||
markupsafe==1.1.1
|
|
||||||
mcstatus==2.2.1
|
|
||||||
multidict==4.5.2
|
|
||||||
psycopg2-binary==2.8.3
|
|
||||||
pycparser==2.19
|
|
||||||
python-dateutil==2.8.0
|
|
||||||
python-telegram-bot==12.2.0
|
|
||||||
pytz==2019.3
|
|
||||||
pywin32-ctypes==0.2.0
|
|
||||||
regex==2019.8.19
|
|
||||||
royalherald==5.1b2
|
|
||||||
sentry-sdk==0.13.0
|
|
||||||
six==1.12.0
|
|
||||||
sortedcontainers==2.1.0
|
|
||||||
sqlalchemy==1.3.10
|
|
||||||
tornado==6.0.3
|
|
||||||
tzlocal==2.0.0
|
|
||||||
urllib3==1.25.6
|
|
||||||
websockets==8.1
|
|
||||||
yarl==1.3.0
|
|
||||||
youtube-dl==2019.10.16
|
|
||||||
riotwatcher==2.7.1
|
|
||||||
# discord.py is missing as we currently use the git version and we ignore the websockets<7.0 requirement
|
|
||||||
uvicorn==0.10.3
|
|
||||||
starlette==0.12.13
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import typing
|
import typing
|
||||||
import asyncio
|
import asyncio
|
||||||
import royalherald as rh
|
|
||||||
from .commanderrors import UnsupportedError
|
from .commanderrors import UnsupportedError
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from .command import Command
|
from .command import Command
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
from . import common
|
from . import common
|
||||||
from . import royal
|
|
||||||
from . import rpg
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"common",
|
"common",
|
||||||
"royal",
|
|
||||||
"rpg",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# This is a template Pack __init__. You can use this without changing anything in other packages too!
|
|
||||||
|
|
||||||
from . import commands, tables, stars
|
|
||||||
from .commands import available_commands
|
|
||||||
from .tables import available_tables
|
|
||||||
from .stars import available_page_stars, available_exception_stars
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"commands",
|
|
||||||
"tables",
|
|
||||||
"stars",
|
|
||||||
"available_commands",
|
|
||||||
"available_tables",
|
|
||||||
"available_page_stars",
|
|
||||||
"available_exception_stars",
|
|
||||||
]
|
|
|
@ -1,58 +0,0 @@
|
||||||
# Imports go here!
|
|
||||||
from .ciaoruozi import CiaoruoziCommand
|
|
||||||
from .color import ColorCommand
|
|
||||||
from .cv import CvCommand
|
|
||||||
from .diario import DiarioCommand
|
|
||||||
from .rage import RageCommand
|
|
||||||
from .reminder import ReminderCommand
|
|
||||||
from .ship import ShipCommand
|
|
||||||
from .smecds import SmecdsCommand
|
|
||||||
from .videochannel import VideochannelCommand
|
|
||||||
from .trivia import TriviaCommand
|
|
||||||
from .matchmaking import MatchmakingCommand
|
|
||||||
from .pause import PauseCommand
|
|
||||||
from .play import PlayCommand
|
|
||||||
from .playmode import PlaymodeCommand
|
|
||||||
from .queue import QueueCommand
|
|
||||||
from .skip import SkipCommand
|
|
||||||
from .summon import SummonCommand
|
|
||||||
from .youtube import YoutubeCommand
|
|
||||||
from .soundcloud import SoundcloudCommand
|
|
||||||
from .zawarudo import ZawarudoCommand
|
|
||||||
from .emojify import EmojifyCommand
|
|
||||||
from .leagueoflegends import LeagueoflegendsCommand
|
|
||||||
from .diarioquote import DiarioquoteCommand
|
|
||||||
from .mp3 import Mp3Command
|
|
||||||
from .peertube import PeertubeCommand
|
|
||||||
|
|
||||||
# Enter the commands of your Pack here!
|
|
||||||
available_commands = [
|
|
||||||
CiaoruoziCommand,
|
|
||||||
ColorCommand,
|
|
||||||
CvCommand,
|
|
||||||
DiarioCommand,
|
|
||||||
RageCommand,
|
|
||||||
ReminderCommand,
|
|
||||||
ShipCommand,
|
|
||||||
SmecdsCommand,
|
|
||||||
VideochannelCommand,
|
|
||||||
TriviaCommand,
|
|
||||||
MatchmakingCommand,
|
|
||||||
PauseCommand,
|
|
||||||
PlayCommand,
|
|
||||||
PlaymodeCommand,
|
|
||||||
QueueCommand,
|
|
||||||
SkipCommand,
|
|
||||||
SummonCommand,
|
|
||||||
YoutubeCommand,
|
|
||||||
SoundcloudCommand,
|
|
||||||
ZawarudoCommand,
|
|
||||||
EmojifyCommand,
|
|
||||||
LeagueoflegendsCommand,
|
|
||||||
DiarioquoteCommand,
|
|
||||||
Mp3Command,
|
|
||||||
PeertubeCommand,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
|
||||||
__all__ = [command.__name__ for command in available_commands]
|
|
|
@ -1,22 +0,0 @@
|
||||||
import typing
|
|
||||||
import telegram
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class CiaoruoziCommand(Command):
|
|
||||||
name: str = "ciaoruozi"
|
|
||||||
|
|
||||||
description: str = "Saluta Ruozi, un leggendario essere che una volta era in User Games."
|
|
||||||
|
|
||||||
syntax: str = ""
|
|
||||||
|
|
||||||
tables: typing.Set = set()
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
update: telegram.Update = data.update
|
|
||||||
user: telegram.User = update.effective_user
|
|
||||||
if user.id == 112437036:
|
|
||||||
await data.reply("👋 Ciao me!")
|
|
||||||
return
|
|
||||||
await data.reply("👋 Ciao Ruozi!")
|
|
|
@ -1,12 +0,0 @@
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class ColorCommand(Command):
|
|
||||||
name: str = "color"
|
|
||||||
|
|
||||||
description: str = "Invia un colore in chat...?"
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
await data.reply("""
|
|
||||||
[i]I am sorry, unknown error occured during working with your request, Admin were notified[/i]
|
|
||||||
""")
|
|
|
@ -1,130 +0,0 @@
|
||||||
import discord
|
|
||||||
import typing
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import andformat
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class CvCommand(Command):
|
|
||||||
name: str = "cv"
|
|
||||||
|
|
||||||
description: str = "Elenca le persone attualmente connesse alla chat vocale."
|
|
||||||
|
|
||||||
syntax: str = "[guildname] [all]"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_cv_handler(bot: DiscordBot, guild_name: typing.Optional[str], everyone: bool):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("No guilds with the specified name found.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Multiple guilds with the specified name found.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Edit the message, sorted by channel
|
|
||||||
discord_members = list(guild.members)
|
|
||||||
channels = {0: None}
|
|
||||||
members_in_channels = {0: []}
|
|
||||||
message = ""
|
|
||||||
# Find all the channels
|
|
||||||
for member in discord_members:
|
|
||||||
if member.voice is not None:
|
|
||||||
channel = members_in_channels.get(member.voice.channel.id)
|
|
||||||
if channel is None:
|
|
||||||
members_in_channels[member.voice.channel.id] = list()
|
|
||||||
channel = members_in_channels[member.voice.channel.id]
|
|
||||||
channels[member.voice.channel.id] = member.voice.channel
|
|
||||||
channel.append(member)
|
|
||||||
else:
|
|
||||||
members_in_channels[0].append(member)
|
|
||||||
# Edit the message, sorted by channel
|
|
||||||
for channel in sorted(channels, key=lambda c: -c):
|
|
||||||
members_in_channels[channel].sort(key=lambda x: x.nick if x.nick is not None else x.name)
|
|
||||||
if channel == 0 and len(members_in_channels[0]) > 0:
|
|
||||||
message += "[b]Non in chat vocale:[/b]\n"
|
|
||||||
else:
|
|
||||||
message += f"[b]In #{channels[channel].name}:[/b]\n"
|
|
||||||
for member in members_in_channels[channel]:
|
|
||||||
member: typing.Union[discord.User, discord.Member]
|
|
||||||
# Ignore not-connected non-notable members
|
|
||||||
if not everyone and channel == 0 and len(member.roles) < 2:
|
|
||||||
continue
|
|
||||||
# Ignore offline members
|
|
||||||
if member.status == discord.Status.offline and member.voice is None:
|
|
||||||
continue
|
|
||||||
# Online status emoji
|
|
||||||
if member.bot:
|
|
||||||
message += "🤖 "
|
|
||||||
elif member.status == discord.Status.online:
|
|
||||||
message += "🔵 "
|
|
||||||
elif member.status == discord.Status.idle:
|
|
||||||
message += "⚫ "
|
|
||||||
elif member.status == discord.Status.dnd:
|
|
||||||
message += "🔴 "
|
|
||||||
elif member.status == discord.Status.offline:
|
|
||||||
message += "⚪ "
|
|
||||||
# Voice
|
|
||||||
if channel != 0:
|
|
||||||
# Voice status
|
|
||||||
if member.voice.afk:
|
|
||||||
message += "💤 "
|
|
||||||
elif member.voice.self_deaf or member.voice.deaf:
|
|
||||||
message += "🔇 "
|
|
||||||
elif member.voice.self_mute or member.voice.mute:
|
|
||||||
message += "🔈 "
|
|
||||||
elif member.voice.self_video or member.voice.self_stream:
|
|
||||||
message += "🖥 "
|
|
||||||
else:
|
|
||||||
message += "🔊 "
|
|
||||||
# Nickname
|
|
||||||
# if member.nick is not None:
|
|
||||||
# message += f"[i]{member.nick}[/i]"
|
|
||||||
# else:
|
|
||||||
message += member.name
|
|
||||||
# Game or stream
|
|
||||||
if member.activity is not None:
|
|
||||||
if member.activity.type == discord.ActivityType.playing:
|
|
||||||
message += f" | 🎮 {member.activity.name}"
|
|
||||||
# Rich presence
|
|
||||||
try:
|
|
||||||
if member.activity.state is not None:
|
|
||||||
message += f" ({member.activity.state}" \
|
|
||||||
f" | {member.activity.details})"
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
elif member.activity.type == discord.ActivityType.streaming:
|
|
||||||
message += f" | 📡 {member.activity.url}"
|
|
||||||
elif member.activity.type == discord.ActivityType.listening:
|
|
||||||
if isinstance(member.activity, discord.Spotify):
|
|
||||||
if member.activity.title == member.activity.album:
|
|
||||||
message += f" | 🎧 {member.activity.title} ({andformat(member.activity.artists, final=' e ')})"
|
|
||||||
else:
|
|
||||||
message += f" | 🎧 {member.activity.title} ({member.activity.album} | {andformat(member.activity.artists, final=' e ')})"
|
|
||||||
else:
|
|
||||||
message += f" | 🎧 {member.activity.name}"
|
|
||||||
elif member.activity.type == discord.ActivityType.watching:
|
|
||||||
message += f" | 📺 {member.activity.name}"
|
|
||||||
else:
|
|
||||||
message += f" | ❓ {member.activity.state}"
|
|
||||||
message += "\n"
|
|
||||||
message += "\n"
|
|
||||||
return {"response": message}
|
|
||||||
|
|
||||||
_event_name = "_legacy_cv"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_cv_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
guild_name, everyone = args.match(r"(?:\[(.+)])?\s*(\S+)?\s*")
|
|
||||||
response = await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name,
|
|
||||||
"everyone": everyone
|
|
||||||
})
|
|
||||||
await data.reply(response["response"])
|
|
|
@ -1,206 +0,0 @@
|
||||||
import typing
|
|
||||||
import re
|
|
||||||
import datetime
|
|
||||||
import telegram
|
|
||||||
import aiohttp
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from ..tables import User, Diario, Alias
|
|
||||||
|
|
||||||
|
|
||||||
async def to_imgur(imgur_api_key, photosizes: typing.List[telegram.PhotoSize], caption="") -> str:
|
|
||||||
# Select the largest photo
|
|
||||||
largest_photo = sorted(photosizes, key=lambda p: p.width * p.height)[-1]
|
|
||||||
# Get the photo url
|
|
||||||
photo_file: telegram.File = await asyncify(largest_photo.get_file)
|
|
||||||
# Forward the url to imgur, as an upload
|
|
||||||
async with aiohttp.request("post", "https://api.imgur.com/3/upload", data={
|
|
||||||
"image": photo_file.file_path,
|
|
||||||
"type": "URL",
|
|
||||||
"title": "Diario image",
|
|
||||||
"description": caption
|
|
||||||
}, headers={
|
|
||||||
"Authorization": f"Client-ID {imgur_api_key}"
|
|
||||||
}) as request:
|
|
||||||
response = await request.json()
|
|
||||||
if not response["success"]:
|
|
||||||
raise CommandError("Imgur returned an error in the image upload.")
|
|
||||||
return response["data"]["link"]
|
|
||||||
|
|
||||||
|
|
||||||
class DiarioCommand(Command):
|
|
||||||
name: str = "diario"
|
|
||||||
|
|
||||||
description: str = "Aggiungi una citazione al Diario."
|
|
||||||
|
|
||||||
syntax = "[!] \"{testo}\" --[autore], [contesto]"
|
|
||||||
|
|
||||||
tables = {User, Diario, Alias}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
update: telegram.Update = data.update
|
|
||||||
message: telegram.Message = update.message
|
|
||||||
reply: telegram.Message = message.reply_to_message
|
|
||||||
creator = await data.get_author()
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
quoted: typing.Optional[str]
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
text: typing.Optional[str]
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
context: typing.Optional[str]
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
timestamp: datetime.datetime
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
media_url: typing.Optional[str]
|
|
||||||
# noinspection PyUnusedLocal
|
|
||||||
spoiler: bool
|
|
||||||
if creator is None:
|
|
||||||
await data.reply("⚠️ Devi essere registrato a Royalnet per usare questo comando!")
|
|
||||||
return
|
|
||||||
if reply is not None:
|
|
||||||
# Get the message text
|
|
||||||
text = reply.text
|
|
||||||
# Check if there's an image associated with the reply
|
|
||||||
photosizes: typing.Optional[typing.List[telegram.PhotoSize]] = reply.photo
|
|
||||||
if photosizes:
|
|
||||||
# Text is a caption
|
|
||||||
text = reply.caption
|
|
||||||
media_url = await to_imgur(self.interface.bot.get_secret("imgur"),
|
|
||||||
photosizes, text if text is not None else "")
|
|
||||||
else:
|
|
||||||
media_url = None
|
|
||||||
# Ensure there is a text or an image
|
|
||||||
if not (text or media_url):
|
|
||||||
raise InvalidInputError("Missing text.")
|
|
||||||
# Find the Royalnet account associated with the sender
|
|
||||||
quoted_tg = await asyncify(data.session.query(self.interface.alchemy.Telegram)
|
|
||||||
.filter_by(tg_id=reply.from_user.id)
|
|
||||||
.one_or_none)
|
|
||||||
quoted_account = quoted_tg.royal if quoted_tg is not None else None
|
|
||||||
# Find the quoted name to assign
|
|
||||||
quoted_user: telegram.User = reply.from_user
|
|
||||||
quoted = quoted_user.full_name
|
|
||||||
# Get the timestamp
|
|
||||||
timestamp = reply.date
|
|
||||||
# Set the other properties
|
|
||||||
spoiler = False
|
|
||||||
context = None
|
|
||||||
else:
|
|
||||||
# Get the current timestamp
|
|
||||||
timestamp = datetime.datetime.now()
|
|
||||||
# Get the message text
|
|
||||||
raw_text = " ".join(args)
|
|
||||||
# Check if there's an image associated with the reply
|
|
||||||
photosizes: typing.Optional[typing.List[telegram.PhotoSize]] = message.photo
|
|
||||||
if photosizes:
|
|
||||||
media_url = await to_imgur(self.interface.bot.get_secret("imgur"),
|
|
||||||
photosizes, raw_text if raw_text is not None else "")
|
|
||||||
else:
|
|
||||||
media_url = None
|
|
||||||
# Parse the text, if it exists
|
|
||||||
if raw_text:
|
|
||||||
# Pass the sentence through the diario regex
|
|
||||||
match = re.match(
|
|
||||||
r'(!)? *["«‘“‛‟❛❝〝"`]([^"]+)["»’”❜❞〞"`] *(?:(?:-{1,2}|—) *([^,]+))?(?:, *([^ ].*))?',
|
|
||||||
raw_text)
|
|
||||||
# Find the corresponding matches
|
|
||||||
if match is not None:
|
|
||||||
spoiler = bool(match.group(1))
|
|
||||||
text = match.group(2)
|
|
||||||
quoted = match.group(3)
|
|
||||||
context = match.group(4)
|
|
||||||
# Otherwise, consider everything part of the text
|
|
||||||
else:
|
|
||||||
spoiler = False
|
|
||||||
text = raw_text
|
|
||||||
quoted = None
|
|
||||||
context = None
|
|
||||||
# Ensure there's a quoted
|
|
||||||
if not quoted:
|
|
||||||
quoted = None
|
|
||||||
if not context:
|
|
||||||
context = None
|
|
||||||
# Find if there's a Royalnet account associated with the quoted name
|
|
||||||
if quoted is not None:
|
|
||||||
quoted_alias = await asyncify(
|
|
||||||
data.session.query(self.interface.alchemy.Alias)
|
|
||||||
.filter_by(alias=quoted.lower()).one_or_none)
|
|
||||||
else:
|
|
||||||
quoted_alias = None
|
|
||||||
quoted_account = quoted_alias.royal if quoted_alias is not None else None
|
|
||||||
else:
|
|
||||||
text = None
|
|
||||||
quoted = None
|
|
||||||
quoted_account = None
|
|
||||||
spoiler = False
|
|
||||||
context = None
|
|
||||||
# Ensure there is a text or an image
|
|
||||||
if not (text or media_url):
|
|
||||||
raise InvalidInputError("Missing text.")
|
|
||||||
# Create the diario quote
|
|
||||||
diario = self.interface.alchemy.Diario(creator=creator,
|
|
||||||
quoted_account=quoted_account,
|
|
||||||
quoted=quoted,
|
|
||||||
text=text,
|
|
||||||
context=context,
|
|
||||||
timestamp=timestamp,
|
|
||||||
media_url=media_url,
|
|
||||||
spoiler=spoiler)
|
|
||||||
data.session.add(diario)
|
|
||||||
await asyncify(data.session.commit)
|
|
||||||
await data.reply(f"✅ {str(diario)}")
|
|
||||||
else:
|
|
||||||
# Find the creator of the quotes
|
|
||||||
creator = await data.get_author(error_if_none=True)
|
|
||||||
# Recreate the full sentence
|
|
||||||
raw_text = " ".join(args)
|
|
||||||
# Pass the sentence through the diario regex
|
|
||||||
match = re.match(r'(!)? *["«‘“‛‟❛❝〝"`]([^"]+)["»’”❜❞〞"`] *(?:(?:-{1,2}|—) *([^,]+))?(?:, *([^ ].*))?',
|
|
||||||
raw_text)
|
|
||||||
# Find the corresponding matches
|
|
||||||
if match is not None:
|
|
||||||
spoiler = bool(match.group(1))
|
|
||||||
text = match.group(2)
|
|
||||||
quoted = match.group(3)
|
|
||||||
context = match.group(4)
|
|
||||||
# Otherwise, consider everything part of the text
|
|
||||||
else:
|
|
||||||
spoiler = False
|
|
||||||
text = raw_text
|
|
||||||
quoted = None
|
|
||||||
context = None
|
|
||||||
timestamp = datetime.datetime.now()
|
|
||||||
# Ensure there is some text
|
|
||||||
if not text:
|
|
||||||
raise InvalidInputError("Missing text.")
|
|
||||||
# Or a quoted
|
|
||||||
if not quoted:
|
|
||||||
quoted = None
|
|
||||||
if not context:
|
|
||||||
context = None
|
|
||||||
# Find if there's a Royalnet account associated with the quoted name
|
|
||||||
if quoted is not None:
|
|
||||||
quoted_alias = await asyncify(
|
|
||||||
data.session.query(self.interface.alchemy.Alias)
|
|
||||||
.filter_by(alias=quoted.lower())
|
|
||||||
.one_or_none)
|
|
||||||
else:
|
|
||||||
quoted_alias = None
|
|
||||||
quoted_account = quoted_alias.royal if quoted_alias is not None else None
|
|
||||||
if quoted_alias is not None and quoted_account is None:
|
|
||||||
await data.reply("⚠️ Il nome dell'autore è ambiguo, quindi la riga non è stata aggiunta.\n"
|
|
||||||
"Per piacere, ripeti il comando con un nome più specifico!")
|
|
||||||
return
|
|
||||||
# Create the diario quote
|
|
||||||
diario = self.interface.alchemy.Diario(creator=creator,
|
|
||||||
quoted_account=quoted_account,
|
|
||||||
quoted=quoted,
|
|
||||||
text=text,
|
|
||||||
context=context,
|
|
||||||
timestamp=timestamp,
|
|
||||||
media_url=None,
|
|
||||||
spoiler=spoiler)
|
|
||||||
data.session.add(diario)
|
|
||||||
await asyncify(data.session.commit)
|
|
||||||
await data.reply(f"✅ {str(diario)}")
|
|
|
@ -1,25 +0,0 @@
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import *
|
|
||||||
from ..tables import Diario
|
|
||||||
|
|
||||||
|
|
||||||
class DiarioquoteCommand(Command):
|
|
||||||
name: str = "diarioquote"
|
|
||||||
|
|
||||||
description: str = "Cita una riga del diario."
|
|
||||||
|
|
||||||
aliases = ["dq", "quote", "dquote"]
|
|
||||||
|
|
||||||
syntax = "{id}"
|
|
||||||
|
|
||||||
tables = {Diario}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
try:
|
|
||||||
entry_id = int(args[0].lstrip("#"))
|
|
||||||
except ValueError:
|
|
||||||
raise CommandError("L'id che hai specificato non è valido.")
|
|
||||||
entry: Diario = await asyncify(data.session.query(self.alchemy.Diario).get, entry_id)
|
|
||||||
if entry is None:
|
|
||||||
raise CommandError("Nessuna riga con quell'id trovata.")
|
|
||||||
await data.reply(str(entry))
|
|
|
@ -1,73 +0,0 @@
|
||||||
import random
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class EmojifyCommand(Command):
|
|
||||||
name: str = "emojify"
|
|
||||||
|
|
||||||
description: str = "Converti un messaggio in emoji."
|
|
||||||
|
|
||||||
syntax = "{messaggio}"
|
|
||||||
|
|
||||||
_emojis = {
|
|
||||||
"abcd": ["🔡", "🔠"],
|
|
||||||
"back": ["🔙"],
|
|
||||||
"cool": ["🆒"],
|
|
||||||
"free": ["🆓"],
|
|
||||||
"abc": ["🔤"],
|
|
||||||
"atm": ["🏧"],
|
|
||||||
"new": ["🆕"],
|
|
||||||
"sos": ["🆘"],
|
|
||||||
"top": ["🔝"],
|
|
||||||
"zzz": ["💤"],
|
|
||||||
"end": ["🔚"],
|
|
||||||
"ab": ["🆎"],
|
|
||||||
"cl": ["🆑"],
|
|
||||||
"id": ["🆔"],
|
|
||||||
"ng": ["🆖"],
|
|
||||||
"no": ["♑️"],
|
|
||||||
"ok": ["🆗"],
|
|
||||||
"on": ["🔛"],
|
|
||||||
"sy": ["💱"],
|
|
||||||
"tm": ["™️"],
|
|
||||||
"wc": ["🚾"],
|
|
||||||
"up": ["🆙"],
|
|
||||||
"a": ["🅰️"],
|
|
||||||
"b": ["🅱️"],
|
|
||||||
"c": ["☪️", "©", "🥐"],
|
|
||||||
"d": ["🇩"],
|
|
||||||
"e": ["📧", "💶"],
|
|
||||||
"f": ["🎏"],
|
|
||||||
"g": ["🇬"],
|
|
||||||
"h": ["🏨", "🏩", "🏋♀", "🏋♂"],
|
|
||||||
"i": ["ℹ️", "♊️", "🕕"],
|
|
||||||
"j": ["⤴️"],
|
|
||||||
"k": ["🎋", "🦅", "💃"],
|
|
||||||
"l": ["🛴", "🕒"],
|
|
||||||
"m": ["♏️", "Ⓜ️", "〽️"],
|
|
||||||
"n": ["📈"],
|
|
||||||
"o": ["⭕️", "🅾️", "📯", "🌝", "🌚", "🌕", "🥯", "🙆♀", "🙆♂"],
|
|
||||||
"p": ["🅿️"],
|
|
||||||
"q": ["🔍", "🍀"],
|
|
||||||
"r": ["®"],
|
|
||||||
"s": ["💰", "💵", "💸", "💲"],
|
|
||||||
"t": ["✝️", "⬆️", "☦️"],
|
|
||||||
"u": ["⛎", "⚓️", "🍉", "🌙", "🐋"],
|
|
||||||
"v": ["✅", "🔽", "☑️", "✔️"],
|
|
||||||
"w": ["🤷♀", "🤷♂", "🤾♀", "🤾♂", "🤽♀", "🤽♂"],
|
|
||||||
"x": ["🙅♀", "🙅♂", "❌", "❎"],
|
|
||||||
"y": ["💴"],
|
|
||||||
"z": ["⚡️"]
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _emojify(cls, string: str):
|
|
||||||
new_string = string.lower()
|
|
||||||
for key in cls._emojis:
|
|
||||||
selected_emoji = random.sample(cls._emojis[key], 1)[0]
|
|
||||||
new_string = new_string.replace(key, selected_emoji)
|
|
||||||
return new_string
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
string = args.joined(require_at_least=1)
|
|
||||||
await data.reply(self._emojify(string))
|
|
|
@ -1,224 +0,0 @@
|
||||||
import typing
|
|
||||||
import riotwatcher
|
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
import sentry_sdk
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import *
|
|
||||||
from ..tables import LeagueOfLegends
|
|
||||||
from ..utils import LeagueLeague
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LeagueoflegendsCommand(Command):
|
|
||||||
name: str = "leagueoflegends"
|
|
||||||
|
|
||||||
aliases = ["lol", "league"]
|
|
||||||
|
|
||||||
description: str = "Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche."
|
|
||||||
|
|
||||||
syntax = "[nomeevocatore]"
|
|
||||||
|
|
||||||
tables = {LeagueOfLegends}
|
|
||||||
|
|
||||||
_region = "euw1"
|
|
||||||
|
|
||||||
_telegram_group_id = -1001153723135
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
self._riotwatcher = riotwatcher.RiotWatcher(api_key=self.interface.bot.get_secret("leagueoflegends"))
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
self.loop.create_task(self._updater(900))
|
|
||||||
|
|
||||||
async def _send(self, message):
|
|
||||||
client = self.interface.bot.client
|
|
||||||
await self.interface.bot.safe_api_call(client.send_message,
|
|
||||||
chat_id=self._telegram_group_id,
|
|
||||||
text=telegram_escape(message),
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_webpage_preview=True)
|
|
||||||
|
|
||||||
async def _notify(self,
|
|
||||||
obj: LeagueOfLegends,
|
|
||||||
attribute_name: str,
|
|
||||||
old_value: typing.Any,
|
|
||||||
new_value: typing.Any):
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
if isinstance(old_value, LeagueLeague):
|
|
||||||
# This is a rank change!
|
|
||||||
# Don't send messages for every rank change, send messages just if the TIER or RANK changes!
|
|
||||||
if old_value.tier == new_value.tier and old_value.rank == new_value.rank:
|
|
||||||
return
|
|
||||||
# Find the queue
|
|
||||||
queue_names = {
|
|
||||||
"rank_soloq": "Solo/Duo",
|
|
||||||
"rank_flexq": "Flex",
|
|
||||||
"rank_twtrq": "3v3",
|
|
||||||
"rank_tftq": "TFT"
|
|
||||||
}
|
|
||||||
# Prepare the message
|
|
||||||
if new_value > old_value:
|
|
||||||
message = f"📈 [b]{obj.user}[/b] è salito a {new_value} su League of Legends " \
|
|
||||||
f"({queue_names[attribute_name]})! Congratulazioni!"
|
|
||||||
else:
|
|
||||||
message = f"📉 [b]{obj.user}[/b] è sceso a {new_value} su League of Legends " \
|
|
||||||
f"({queue_names[attribute_name]})."
|
|
||||||
# Send the message
|
|
||||||
await self._send(message)
|
|
||||||
# Level up!
|
|
||||||
elif attribute_name == "summoner_level":
|
|
||||||
if new_value == 30 or (new_value >= 50 and (new_value % 25 == 0)):
|
|
||||||
await self._send(f"🆙 [b]{obj.user}[/b] è salito al livello [b]{new_value}[/b] su League of Legends!")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _change(obj: LeagueOfLegends,
|
|
||||||
attribute_name: str,
|
|
||||||
new_value: typing.Any,
|
|
||||||
callback: typing.Callable[
|
|
||||||
[LeagueOfLegends, str, typing.Any, typing.Any], typing.Awaitable[None]]):
|
|
||||||
old_value = obj.__getattribute__(attribute_name)
|
|
||||||
if old_value != new_value:
|
|
||||||
await callback(obj, attribute_name, old_value, new_value)
|
|
||||||
obj.__setattr__(attribute_name, new_value)
|
|
||||||
|
|
||||||
async def _update(self, lol: LeagueOfLegends):
|
|
||||||
log.info(f"Updating: {lol}")
|
|
||||||
log.debug(f"Getting summoner data: {lol}")
|
|
||||||
summoner = await asyncify(self._riotwatcher.summoner.by_id, region=self._region,
|
|
||||||
encrypted_summoner_id=lol.summoner_id)
|
|
||||||
await self._change(lol, "profile_icon_id", summoner["profileIconId"], self._notify)
|
|
||||||
await self._change(lol, "summoner_name", summoner["name"], self._notify)
|
|
||||||
await self._change(lol, "puuid", summoner["puuid"], self._notify)
|
|
||||||
await self._change(lol, "summoner_level", summoner["summonerLevel"], self._notify)
|
|
||||||
await self._change(lol, "summoner_id", summoner["id"], self._notify)
|
|
||||||
await self._change(lol, "account_id", summoner["accountId"], self._notify)
|
|
||||||
log.debug(f"Getting leagues data: {lol}")
|
|
||||||
leagues = await asyncify(self._riotwatcher.league.by_summoner, region=self._region,
|
|
||||||
encrypted_summoner_id=lol.summoner_id)
|
|
||||||
soloq = LeagueLeague()
|
|
||||||
flexq = LeagueLeague()
|
|
||||||
twtrq = LeagueLeague()
|
|
||||||
tftq = LeagueLeague()
|
|
||||||
for league in leagues:
|
|
||||||
if league["queueType"] == "RANKED_SOLO_5x5":
|
|
||||||
soloq = LeagueLeague.from_dict(league)
|
|
||||||
if league["queueType"] == "RANKED_FLEX_SR":
|
|
||||||
flexq = LeagueLeague.from_dict(league)
|
|
||||||
if league["queueType"] == "RANKED_FLEX_TT":
|
|
||||||
twtrq = LeagueLeague.from_dict(league)
|
|
||||||
if league["queueType"] == "RANKED_TFT":
|
|
||||||
tftq = LeagueLeague.from_dict(league)
|
|
||||||
await self._change(lol, "rank_soloq", soloq, self._notify)
|
|
||||||
await self._change(lol, "rank_flexq", flexq, self._notify)
|
|
||||||
await self._change(lol, "rank_twtrq", twtrq, self._notify)
|
|
||||||
await self._change(lol, "rank_tftq", tftq, self._notify)
|
|
||||||
log.debug(f"Getting mastery data: {lol}")
|
|
||||||
mastery = await asyncify(self._riotwatcher.champion_mastery.scores_by_summoner, region=self._region,
|
|
||||||
encrypted_summoner_id=lol.summoner_id)
|
|
||||||
await self._change(lol, "mastery_score", mastery, self._notify)
|
|
||||||
|
|
||||||
async def _updater(self, period: int):
|
|
||||||
log.info(f"Started updater with {period}s period")
|
|
||||||
while True:
|
|
||||||
log.info(f"Updating...")
|
|
||||||
session = self.alchemy.Session()
|
|
||||||
log.info("")
|
|
||||||
lols = session.query(self.alchemy.LeagueOfLegends).all()
|
|
||||||
for lol in lols:
|
|
||||||
try:
|
|
||||||
await self._update(lol)
|
|
||||||
except Exception as e:
|
|
||||||
sentry_sdk.capture_exception(e)
|
|
||||||
log.error(f"Error while updating {lol.user.username}: {e}")
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await asyncify(session.commit)
|
|
||||||
session.close()
|
|
||||||
log.info(f"Sleeping for {period}s")
|
|
||||||
await asyncio.sleep(period)
|
|
||||||
|
|
||||||
def _display(self, lol: LeagueOfLegends):
|
|
||||||
string = f"ℹ️ [b]{lol.summoner_name}[/b]\n" \
|
|
||||||
f"Lv. {lol.summoner_level}\n" \
|
|
||||||
f"Mastery score: {lol.mastery_score}\n" \
|
|
||||||
f"\n"
|
|
||||||
if lol.rank_soloq:
|
|
||||||
string += f"Solo: {lol.rank_soloq}\n"
|
|
||||||
if lol.rank_flexq:
|
|
||||||
string += f"Flex: {lol.rank_flexq}\n"
|
|
||||||
if lol.rank_twtrq:
|
|
||||||
string += f"3v3: {lol.rank_twtrq}\n"
|
|
||||||
if lol.rank_tftq:
|
|
||||||
string += f"TFT: {lol.rank_tftq}\n"
|
|
||||||
return string
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
|
|
||||||
name = args.joined()
|
|
||||||
|
|
||||||
if name:
|
|
||||||
# Connect a new League of Legends account to Royalnet
|
|
||||||
log.debug(f"Searching for: {name}")
|
|
||||||
summoner = self._riotwatcher.summoner.by_name(region=self._region, summoner_name=name)
|
|
||||||
# Ensure the account isn't already connected to something else
|
|
||||||
leagueoflegends = await asyncify(
|
|
||||||
data.session.query(self.alchemy.LeagueOfLegends).filter_by(summoner_id=summoner["id"]).one_or_none)
|
|
||||||
if leagueoflegends:
|
|
||||||
raise CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.")
|
|
||||||
# Get rank information
|
|
||||||
log.debug(f"Getting leagues data: {name}")
|
|
||||||
leagues = self._riotwatcher.league.by_summoner(region=self._region, encrypted_summoner_id=summoner["id"])
|
|
||||||
soloq = LeagueLeague()
|
|
||||||
flexq = LeagueLeague()
|
|
||||||
twtrq = LeagueLeague()
|
|
||||||
tftq = LeagueLeague()
|
|
||||||
for league in leagues:
|
|
||||||
if league["queueType"] == "RANKED_SOLO_5x5":
|
|
||||||
soloq = LeagueLeague.from_dict(league)
|
|
||||||
if league["queueType"] == "RANKED_FLEX_SR":
|
|
||||||
flexq = LeagueLeague.from_dict(league)
|
|
||||||
if league["queueType"] == "RANKED_FLEX_TT":
|
|
||||||
twtrq = LeagueLeague.from_dict(league)
|
|
||||||
if league["queueType"] == "RANKED_TFT":
|
|
||||||
tftq = LeagueLeague.from_dict(league)
|
|
||||||
# Get mastery score
|
|
||||||
log.debug(f"Getting mastery data: {name}")
|
|
||||||
mastery = self._riotwatcher.champion_mastery.scores_by_summoner(region=self._region,
|
|
||||||
encrypted_summoner_id=summoner["id"])
|
|
||||||
# Create database row
|
|
||||||
leagueoflegends = self.alchemy.LeagueOfLegends(
|
|
||||||
region=self._region,
|
|
||||||
user=author,
|
|
||||||
profile_icon_id=summoner["profileIconId"],
|
|
||||||
summoner_name=summoner["name"],
|
|
||||||
puuid=summoner["puuid"],
|
|
||||||
summoner_level=summoner["summonerLevel"],
|
|
||||||
summoner_id=summoner["id"],
|
|
||||||
account_id=summoner["accountId"],
|
|
||||||
rank_soloq=soloq,
|
|
||||||
rank_flexq=flexq,
|
|
||||||
rank_twtrq=twtrq,
|
|
||||||
rank_tftq=tftq,
|
|
||||||
mastery_score=mastery
|
|
||||||
)
|
|
||||||
log.debug(f"Saving to the DB: {name}")
|
|
||||||
data.session.add(leagueoflegends)
|
|
||||||
await data.session_commit()
|
|
||||||
await data.reply(f"↔️ Account {leagueoflegends} connesso a {author}!")
|
|
||||||
else:
|
|
||||||
# Update and display the League of Legends stats for the current account
|
|
||||||
if len(author.leagueoflegends) == 0:
|
|
||||||
raise CommandError("Nessun account di League of Legends trovato.")
|
|
||||||
message = ""
|
|
||||||
for account in author.leagueoflegends:
|
|
||||||
try:
|
|
||||||
await self._update(account)
|
|
||||||
message += self._display(account)
|
|
||||||
except riotwatcher.ApiError as e:
|
|
||||||
message += f"⚠️ [b]{account.summoner_name}[/b]\n" \
|
|
||||||
f"{e}"
|
|
||||||
message += "\n"
|
|
||||||
await data.session_commit()
|
|
||||||
await data.reply(message)
|
|
|
@ -1,244 +0,0 @@
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
import dateparser
|
|
||||||
import typing
|
|
||||||
from telegram import Bot as PTBBot
|
|
||||||
from telegram import Message as PTBMessage
|
|
||||||
from telegram.error import BadRequest, Unauthorized
|
|
||||||
from telegram import InlineKeyboardMarkup as InKeMa
|
|
||||||
from telegram import InlineKeyboardButton as InKeBu
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.bots import TelegramBot
|
|
||||||
from royalnet.utils import telegram_escape, asyncify, sleep_until
|
|
||||||
from ..tables import MMEvent, MMResponse, User
|
|
||||||
from ..utils import MMChoice, MMInterfaceDataTelegram
|
|
||||||
|
|
||||||
|
|
||||||
class MatchmakingCommand(Command):
|
|
||||||
name: str = "matchmaking"
|
|
||||||
|
|
||||||
description: str = "Cerca persone per una partita a qualcosa!"
|
|
||||||
|
|
||||||
syntax: str = "[ {ora} ] {nome}\n[descrizione]"
|
|
||||||
|
|
||||||
aliases = ["mm", "lfg"]
|
|
||||||
|
|
||||||
tables = {MMEvent, MMResponse}
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
# Find all relevant MMEvents and run them
|
|
||||||
session = self.alchemy.Session()
|
|
||||||
mmevents = (
|
|
||||||
session
|
|
||||||
.query(self.alchemy.MMEvent)
|
|
||||||
.filter(self.alchemy.MMEvent.interface == self.interface.name,
|
|
||||||
self.alchemy.MMEvent.datetime > datetime.datetime.now())
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for mmevent in mmevents:
|
|
||||||
self.interface.loop.create_task(self._run_mmevent(mmevent.mmid))
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
# Create a new MMEvent and run it
|
|
||||||
if self.interface.name != "telegram":
|
|
||||||
raise UnsupportedError(f"{self.interface.prefix}matchmaking funziona solo su Telegram. Per ora.")
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
timestring, title, description = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL)
|
|
||||||
except InvalidInputError:
|
|
||||||
timestring, title, description = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL)
|
|
||||||
try:
|
|
||||||
dt: typing.Optional[datetime.datetime] = dateparser.parse(timestring, settings={
|
|
||||||
"PREFER_DATES_FROM": "future"
|
|
||||||
})
|
|
||||||
except OverflowError:
|
|
||||||
dt = None
|
|
||||||
if dt is None:
|
|
||||||
raise CommandError("La data che hai specificato non è valida.")
|
|
||||||
if dt <= datetime.datetime.now():
|
|
||||||
raise CommandError("La data che hai specificato è nel passato.")
|
|
||||||
if dt >= datetime.datetime.now() + datetime.timedelta(days=90):
|
|
||||||
raise CommandError("La data che hai specificato è a più di 90 giorni di distanza da oggi.")
|
|
||||||
mmevent: MMEvent = self.interface.alchemy.MMEvent(creator=author,
|
|
||||||
datetime=dt,
|
|
||||||
title=title,
|
|
||||||
description=description,
|
|
||||||
interface=self.interface.name)
|
|
||||||
data.session.add(mmevent)
|
|
||||||
await data.session_commit()
|
|
||||||
self.interface.loop.create_task(self._run_mmevent(mmevent.mmid))
|
|
||||||
await data.reply(f"✅ Evento [b]{mmevent.title}[/b] creato!")
|
|
||||||
|
|
||||||
_mm_chat_id = -1001287169422
|
|
||||||
|
|
||||||
_mm_error_chat_id = -1001153723135
|
|
||||||
|
|
||||||
def _gen_mm_message(self, mmevent: MMEvent) -> str:
|
|
||||||
text = f"🌐 [{mmevent.datetime.strftime('%Y-%m-%d %H:%M')}] [b]{mmevent.title}[/b]\n"
|
|
||||||
if mmevent.description:
|
|
||||||
text += f"{mmevent.description}\n"
|
|
||||||
text += "\n"
|
|
||||||
for response in mmevent.responses:
|
|
||||||
response: MMResponse
|
|
||||||
text += f"{response.choice.value} {response.user}\n"
|
|
||||||
return text
|
|
||||||
|
|
||||||
def _gen_telegram_keyboard(self, mmevent: MMEvent):
|
|
||||||
return InKeMa([
|
|
||||||
[InKeBu(f"{MMChoice.YES.value} Ci sarò!", callback_data=f"mm{mmevent.mmid}_YES")],
|
|
||||||
[InKeBu(f"{MMChoice.MAYBE.value} (Forse.)", callback_data=f"mm{mmevent.mmid}_MAYBE")],
|
|
||||||
[InKeBu(f"{MMChoice.LATE_SHORT.value} Arrivo dopo 5-10 min.",
|
|
||||||
callback_data=f"mm{mmevent.mmid}_LATE_SHORT")],
|
|
||||||
[InKeBu(f"{MMChoice.LATE_MEDIUM.value} Arrivo dopo 15-35 min.",
|
|
||||||
callback_data=f"mm{mmevent.mmid}_LATE_MEDIUM")],
|
|
||||||
[InKeBu(f"{MMChoice.LATE_LONG.value} Arrivo dopo 40+ min.", callback_data=f"mm{mmevent.mmid}_LATE_LONG")],
|
|
||||||
[InKeBu(f"{MMChoice.NO_TIME.value} Non posso a quell'ora...", callback_data=f"mm{mmevent.mmid}_NO_TIME")],
|
|
||||||
[InKeBu(f"{MMChoice.NO_INTEREST.value} Non mi interessa.", callback_data=f"mm{mmevent.mmid}_NO_INTEREST")],
|
|
||||||
[InKeBu(f"{MMChoice.NO_TECH.value} Ho un problema!", callback_data=f"mm{mmevent.mmid}_NO_TECH")],
|
|
||||||
])
|
|
||||||
|
|
||||||
async def _update_telegram_mm_message(self, client: PTBBot, mmevent: MMEvent):
|
|
||||||
try:
|
|
||||||
await self.interface.bot.safe_api_call(client.edit_message_text,
|
|
||||||
chat_id=self._mm_chat_id,
|
|
||||||
text=telegram_escape(self._gen_mm_message(mmevent)),
|
|
||||||
message_id=mmevent.interface_data.message_id,
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_web_page_preview=True,
|
|
||||||
reply_markup=self._gen_telegram_keyboard(mmevent))
|
|
||||||
except BadRequest:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _gen_mm_telegram_callback(self, client: PTBBot, mmid: int, choice: MMChoice):
|
|
||||||
async def callback(data: CommandData):
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
# Find the MMEvent with the current session
|
|
||||||
mmevent: MMEvent = await asyncify(data.session.query(self.alchemy.MMEvent).get, mmid)
|
|
||||||
mmresponse: MMResponse = await asyncify(
|
|
||||||
data.session.query(self.alchemy.MMResponse).filter_by(user=author, mmevent=mmevent).one_or_none)
|
|
||||||
if mmresponse is None:
|
|
||||||
mmresponse = self.alchemy.MMResponse(user=author, mmevent=mmevent, choice=choice)
|
|
||||||
data.session.add(mmresponse)
|
|
||||||
else:
|
|
||||||
mmresponse.choice = choice
|
|
||||||
await data.session_commit()
|
|
||||||
await self._update_telegram_mm_message(client, mmevent)
|
|
||||||
return f"✅ Messaggio ricevuto!"
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def _gen_event_start_message(self, mmevent: MMEvent):
|
|
||||||
text = f"🚩 L'evento [b]{mmevent.title}[/b] è iniziato!\n\n"
|
|
||||||
for response in mmevent.responses:
|
|
||||||
response: MMResponse
|
|
||||||
text += f"{response.choice.value} {response.user}\n"
|
|
||||||
return text
|
|
||||||
|
|
||||||
def _gen_unauth_message(self, user: User):
|
|
||||||
return f"⚠️ Non sono autorizzato a mandare messaggi a [b]{user.username}[/b]!\n" \
|
|
||||||
f"{user.telegram.mention()}, apri una chat privata con me e mandami un messaggio!"
|
|
||||||
|
|
||||||
async def _run_mmevent(self, mmid: int):
|
|
||||||
"""Run a MMEvent."""
|
|
||||||
# Open a new Alchemy Session
|
|
||||||
session = self.alchemy.Session()
|
|
||||||
# Find the MMEvent with the current session
|
|
||||||
mmevent: MMEvent = await asyncify(session.query(self.alchemy.MMEvent).get, mmid)
|
|
||||||
if mmevent is None:
|
|
||||||
raise ValueError("Invalid mmid.")
|
|
||||||
# Ensure the MMEvent hasn't already started
|
|
||||||
if mmevent.datetime <= datetime.datetime.now():
|
|
||||||
raise ValueError("MMEvent has already started.")
|
|
||||||
# Ensure the MMEvent interface matches the current one
|
|
||||||
if mmevent.interface != self.interface.name:
|
|
||||||
raise ValueError("Invalid interface.")
|
|
||||||
# If the matchmaking message hasn't been sent yet, do so now
|
|
||||||
if mmevent.interface_data is None:
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
bot: TelegramBot = self.interface.bot
|
|
||||||
client: PTBBot = bot.client
|
|
||||||
# Build the Telegram keyboard
|
|
||||||
# Send the keyboard
|
|
||||||
message: PTBMessage = await self.interface.bot.safe_api_call(client.send_message,
|
|
||||||
chat_id=self._mm_chat_id,
|
|
||||||
text=telegram_escape(
|
|
||||||
self._gen_mm_message(mmevent)),
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_webpage_preview=True,
|
|
||||||
reply_markup=self._gen_telegram_keyboard(
|
|
||||||
mmevent))
|
|
||||||
# Store message data in the interface data object
|
|
||||||
mmevent.interface_data = MMInterfaceDataTelegram(chat_id=self._mm_chat_id,
|
|
||||||
message_id=message.message_id)
|
|
||||||
await asyncify(session.commit)
|
|
||||||
else:
|
|
||||||
raise UnsupportedError()
|
|
||||||
# Register handlers for the keyboard events
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
bot: TelegramBot = self.interface.bot
|
|
||||||
client: PTBBot = bot.client
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_YES",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid, MMChoice.YES))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_MAYBE",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid, MMChoice.MAYBE))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_LATE_SHORT",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid,
|
|
||||||
MMChoice.LATE_SHORT))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_LATE_MEDIUM",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid,
|
|
||||||
MMChoice.LATE_MEDIUM))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_LATE_LONG",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid,
|
|
||||||
MMChoice.LATE_LONG))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_NO_TIME",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid,
|
|
||||||
MMChoice.NO_TIME))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_NO_INTEREST",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid,
|
|
||||||
MMChoice.NO_INTEREST))
|
|
||||||
self.interface.register_keyboard_key(f"mm{mmevent.mmid}_NO_TECH",
|
|
||||||
callback=self._gen_mm_telegram_callback(client, mmid,
|
|
||||||
MMChoice.NO_TECH))
|
|
||||||
else:
|
|
||||||
raise UnsupportedError()
|
|
||||||
# Sleep until the time of the event
|
|
||||||
await sleep_until(mmevent.datetime)
|
|
||||||
# Notify the positive answers of the event start
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
bot: TelegramBot = self.interface.bot
|
|
||||||
client: PTBBot = bot.client
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_YES")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_MAYBE")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_LATE_SHORT")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_LATE_MEDIUM")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_LATE_LONG")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_NO_TIME")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_NO_INTEREST")
|
|
||||||
self.interface.unregister_keyboard_key(f"mm{mmevent.mmid}_NO_TECH")
|
|
||||||
for response in mmevent.responses:
|
|
||||||
if response.choice == MMChoice.NO_INTEREST or response.choice == MMChoice.NO_TIME:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await self.interface.bot.safe_api_call(client.send_message,
|
|
||||||
chat_id=response.user.telegram[0].tg_id,
|
|
||||||
text=telegram_escape(self._gen_event_start_message(mmevent)),
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_webpage_preview=True)
|
|
||||||
except Unauthorized:
|
|
||||||
await self.interface.bot.safe_api_call(client.send_message,
|
|
||||||
chat_id=self._mm_error_chat_id,
|
|
||||||
text=telegram_escape(
|
|
||||||
self._gen_unauth_message(response.user)),
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_webpage_preview=True)
|
|
||||||
else:
|
|
||||||
raise UnsupportedError()
|
|
||||||
# Delete the event message
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
await self.interface.bot.safe_api_call(client.delete_message,
|
|
||||||
chat_id=mmevent.interface_data.chat_id,
|
|
||||||
message_id=mmevent.interface_data.message_id)
|
|
||||||
# The end!
|
|
||||||
await asyncify(session.close)
|
|
|
@ -1,40 +0,0 @@
|
||||||
import typing
|
|
||||||
import urllib.parse
|
|
||||||
import asyncio
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from royalnet.audio import YtdlMp3
|
|
||||||
|
|
||||||
|
|
||||||
class Mp3Command(Command):
|
|
||||||
name: str = "mp3"
|
|
||||||
|
|
||||||
aliases = ["dlmusic"]
|
|
||||||
|
|
||||||
description: str = "Scarica un video con youtube-dl e invialo in chat."
|
|
||||||
|
|
||||||
syntax = "{ytdlstring}"
|
|
||||||
|
|
||||||
ytdl_args = {
|
|
||||||
"format": "bestaudio",
|
|
||||||
"outtmpl": f"./downloads/%(title)s.%(ext)s"
|
|
||||||
}
|
|
||||||
|
|
||||||
seconds_before_deletion = 15 * 60
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
url = args.joined()
|
|
||||||
if url.startswith("http://") or url.startswith("https://"):
|
|
||||||
vfiles: typing.List[YtdlMp3] = await asyncify(YtdlMp3.create_and_ready_from_url,
|
|
||||||
url,
|
|
||||||
**self.ytdl_args)
|
|
||||||
else:
|
|
||||||
vfiles = await asyncify(YtdlMp3.create_and_ready_from_url, f"ytsearch:{url}", **self.ytdl_args)
|
|
||||||
for vfile in vfiles:
|
|
||||||
await data.reply(f"⬇️ Il file richiesto può essere scaricato a:\n"
|
|
||||||
f"https://scaleway.steffo.eu/{urllib.parse.quote(vfile.mp3_filename.replace('./downloads/', './musicbot_cache/'))}\n"
|
|
||||||
f"Verrà eliminato tra {self.seconds_before_deletion} secondi.")
|
|
||||||
await asyncio.sleep(self.seconds_before_deletion)
|
|
||||||
for vfile in vfiles:
|
|
||||||
vfile.delete()
|
|
||||||
await data.reply(f"⏹ Il file {vfile.info.title} è scaduto ed è stato eliminato.")
|
|
|
@ -1,53 +0,0 @@
|
||||||
import typing
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class PauseCommand(Command):
|
|
||||||
name: str = "pause"
|
|
||||||
|
|
||||||
description: str = "Mette in pausa o riprende la riproduzione della canzone attuale."
|
|
||||||
|
|
||||||
syntax = "[ [guild] ]"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_pause_handler(bot: DiscordBot, guild_name: typing.Optional[str]):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("No guilds with the specified name found.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Multiple guilds with the specified name found.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Set the currently playing source as ended
|
|
||||||
voice_client: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild)
|
|
||||||
if not (voice_client.is_playing() or voice_client.is_paused()):
|
|
||||||
raise CommandError("There is nothing to pause.")
|
|
||||||
# Toggle pause
|
|
||||||
resume = voice_client._player.is_paused()
|
|
||||||
if resume:
|
|
||||||
voice_client._player.resume()
|
|
||||||
else:
|
|
||||||
voice_client._player.pause()
|
|
||||||
return {"resumed": resume}
|
|
||||||
|
|
||||||
_event_name = "_legacy_pause"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_pause_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, = args.match(r"(?:\[(.+)])?")
|
|
||||||
response = await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name
|
|
||||||
})
|
|
||||||
if response["resumed"]:
|
|
||||||
await data.reply(f"▶️ Riproduzione ripresa.")
|
|
||||||
else:
|
|
||||||
await data.reply(f"⏸ Riproduzione messa in pausa.")
|
|
|
@ -1,81 +0,0 @@
|
||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import dateparser
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import telegram_escape
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PeertubeCommand(Command):
|
|
||||||
name: str = "peertube"
|
|
||||||
|
|
||||||
description: str = "Guarda quando è uscito l'ultimo video su RoyalTube."
|
|
||||||
|
|
||||||
_url = r"https://pt.steffo.eu/feeds/videos.json?sort=-publishedAt&filter=local"
|
|
||||||
|
|
||||||
_ready = asyncio.Event()
|
|
||||||
|
|
||||||
_timeout = 300
|
|
||||||
|
|
||||||
_latest_date: datetime.datetime = None
|
|
||||||
|
|
||||||
_telegram_group_id = -1001153723135
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
self.loop.create_task(self._ready_up())
|
|
||||||
self.loop.create_task(self._update())
|
|
||||||
|
|
||||||
async def _get_json(self):
|
|
||||||
log.debug("Getting jsonfeed")
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(self._url) as response:
|
|
||||||
log.debug("Parsing jsonfeed")
|
|
||||||
j = await response.json()
|
|
||||||
log.debug("Jsonfeed parsed successfully")
|
|
||||||
return j
|
|
||||||
|
|
||||||
async def _send(self, message):
|
|
||||||
client = self.interface.bot.client
|
|
||||||
await self.interface.bot.safe_api_call(client.send_message,
|
|
||||||
chat_id=self._telegram_group_id,
|
|
||||||
text=telegram_escape(message),
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_webpage_preview=True)
|
|
||||||
|
|
||||||
async def _ready_up(self):
|
|
||||||
j = await self._get_json()
|
|
||||||
if j["version"] != "https://jsonfeed.org/version/1":
|
|
||||||
raise ConfigurationError("_url is not a jsonfeed")
|
|
||||||
videos = j["items"]
|
|
||||||
for video in reversed(videos):
|
|
||||||
date_modified = dateparser.parse(video["date_modified"])
|
|
||||||
if self._latest_date is None or date_modified > self._latest_date:
|
|
||||||
log.debug(f"Found newer video: {date_modified}")
|
|
||||||
self._latest_date = date_modified
|
|
||||||
self._ready.set()
|
|
||||||
|
|
||||||
async def _update(self):
|
|
||||||
await self._ready.wait()
|
|
||||||
while True:
|
|
||||||
j = await self._get_json()
|
|
||||||
videos = j["items"]
|
|
||||||
for video in reversed(videos):
|
|
||||||
date_modified = dateparser.parse(video["date_modified"])
|
|
||||||
if date_modified > self._latest_date:
|
|
||||||
log.debug(f"Found newer video: {date_modified}")
|
|
||||||
self._latest_date = date_modified
|
|
||||||
await self._send(f"🆕 Nuovo video su RoyalTube!\n"
|
|
||||||
f"[b]{video['title']}[/b]\n"
|
|
||||||
f"{video['url']}")
|
|
||||||
await asyncio.sleep(self._timeout)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self.interface.name != "telegram":
|
|
||||||
raise UnsupportedError()
|
|
||||||
await data.reply(f"ℹ️ Ultimo video caricato il: [b]{self._latest_date.isoformat()}[/b]")
|
|
|
@ -1,79 +0,0 @@
|
||||||
import typing
|
|
||||||
import pickle
|
|
||||||
import datetime
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from royalnet.audio import YtdlDiscord
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class PlayCommand(Command):
|
|
||||||
name: str = "play"
|
|
||||||
|
|
||||||
aliases = ["p"]
|
|
||||||
|
|
||||||
description: str = "Aggiunge un url alla coda della chat vocale."
|
|
||||||
|
|
||||||
syntax = "[ [guild] ] {url}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_play_handler(bot: "DiscordBot", guild_name: typing.Optional[str], url: str):
|
|
||||||
"""Handle a play Royalnet request. That is, add audio to a PlayMode."""
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("Server non trovato.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Il nome del server è ambiguo.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Ensure the guild has a PlayMode before adding the file to it
|
|
||||||
if not bot.music_data.get(guild):
|
|
||||||
raise CommandError("Il bot non è in nessun canale vocale.")
|
|
||||||
# Create url
|
|
||||||
ytdl_args = {
|
|
||||||
"format": "bestaudio/best",
|
|
||||||
"outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s"
|
|
||||||
}
|
|
||||||
# Start downloading
|
|
||||||
dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, url, **ytdl_args)
|
|
||||||
await bot.add_to_music_data(dfiles, guild)
|
|
||||||
# Create response dictionary
|
|
||||||
response = {
|
|
||||||
"videos": [{
|
|
||||||
"title": dfile.info.title,
|
|
||||||
"discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
|
|
||||||
} for dfile in dfiles]
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
|
|
||||||
_event_name = "_legacy_play"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_play_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, url = args.match(r"(?:\[(.+)])?\s*<?(.+)>?")
|
|
||||||
if not (url.startswith("http://") or url.startswith("https://")):
|
|
||||||
raise CommandError(f"Il comando [c]{self.interface.prefix}play[/c] funziona solo per riprodurre file da"
|
|
||||||
f" un URL.\n"
|
|
||||||
f"Se vuoi cercare un video, usa [c]{self.interface.prefix}youtube[/c] o"
|
|
||||||
f" [c]{self.interface.prefix}soundcloud[/c]!")
|
|
||||||
response: dict = await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name,
|
|
||||||
"url": url
|
|
||||||
})
|
|
||||||
if len(response["videos"]) == 0:
|
|
||||||
raise CommandError(f"Nessun file trovato.")
|
|
||||||
for video in response["videos"]:
|
|
||||||
if self.interface.name == "discord":
|
|
||||||
# This is one of the unsafest things ever
|
|
||||||
embed = pickle.loads(eval(video["discord_embed_pickle"]))
|
|
||||||
await data.message.channel.send(content="▶️ Aggiunto alla coda:", embed=embed)
|
|
||||||
else:
|
|
||||||
await data.reply(f"▶️ Aggiunto alla coda: [i]{video['title']}[/i]")
|
|
|
@ -1,57 +0,0 @@
|
||||||
import typing
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.audio.playmodes import Playlist, Pool, Layers
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class PlaymodeCommand(Command):
|
|
||||||
name: str = "playmode"
|
|
||||||
|
|
||||||
aliases = ["pm", "mode"]
|
|
||||||
|
|
||||||
description: str = "Cambia modalità di riproduzione per la chat vocale."
|
|
||||||
|
|
||||||
syntax = "[ [guild] ] {mode}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_playmode_handler(bot: "DiscordBot", guild_name: typing.Optional[str], mode_name: str):
|
|
||||||
"""Handle a playmode Royalnet request. That is, change current PlayMode."""
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("No guilds with the specified name found.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Multiple guilds with the specified name found.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Delete the previous PlayMode, if it exists
|
|
||||||
if bot.music_data[guild] is not None:
|
|
||||||
bot.music_data[guild].playmode.delete()
|
|
||||||
# Create the new PlayMode
|
|
||||||
if mode_name == "playlist":
|
|
||||||
bot.music_data[guild].playmode = Playlist()
|
|
||||||
elif mode_name == "pool":
|
|
||||||
bot.music_data[guild].playmode = Pool()
|
|
||||||
elif mode_name == "layers":
|
|
||||||
bot.music_data[guild].playmode = Layers()
|
|
||||||
else:
|
|
||||||
raise CommandError("Unknown PlayMode specified.")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
_event_name = "_legacy_playmode"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_playmode_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, mode_name = args.match(r"(?:\[(.+)])?\s*(\S+)\s*")
|
|
||||||
await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name,
|
|
||||||
"mode_name": mode_name
|
|
||||||
})
|
|
||||||
await data.reply(f"🔃 Impostata la modalità di riproduzione a: [c]{mode_name}[/c].")
|
|
|
@ -1,93 +0,0 @@
|
||||||
import typing
|
|
||||||
import pickle
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import numberemojiformat
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class QueueCommand(Command):
|
|
||||||
name: str = "queue"
|
|
||||||
|
|
||||||
aliases = ["q"]
|
|
||||||
|
|
||||||
description: str = "Visualizza la coda di riproduzione attuale."
|
|
||||||
|
|
||||||
syntax = "[ [guild] ]"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_queue_handler(bot: "DiscordBot", guild_name: typing.Optional[str]):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("No guilds with the specified name found.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Multiple guilds with the specified name found.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Check if the guild has a PlayMode
|
|
||||||
playmode = bot.music_data.get(guild)
|
|
||||||
if not playmode:
|
|
||||||
return {
|
|
||||||
"type": None
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
queue = playmode.queue_preview()
|
|
||||||
except NotImplementedError:
|
|
||||||
return {
|
|
||||||
"type": playmode.__class__.__name__
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"type": playmode.__class__.__name__,
|
|
||||||
"queue":
|
|
||||||
{
|
|
||||||
"strings": [str(dfile.info) for dfile in queue],
|
|
||||||
"pickled_embeds": str(pickle.dumps([dfile.info.to_discord_embed() for dfile in queue]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_event_name = "_legacy_queue"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_queue_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, = args.match(r"(?:\[(.+)])?")
|
|
||||||
response = await self.interface.call_herald_action("discord", self._event_name, {"guild_name": guild_name})
|
|
||||||
if response["type"] is None:
|
|
||||||
await data.reply("ℹ️ Non c'è nessuna coda di riproduzione attiva al momento.")
|
|
||||||
return
|
|
||||||
elif "queue" not in response:
|
|
||||||
await data.reply(f"ℹ️ La coda di riproduzione attuale ([c]{response['type']}[/c]) non permette l'anteprima.")
|
|
||||||
return
|
|
||||||
if response["type"] == "Playlist":
|
|
||||||
if len(response["queue"]["strings"]) == 0:
|
|
||||||
message = f"ℹ️ Questa [c]Playlist[/c] è vuota."
|
|
||||||
else:
|
|
||||||
message = f"ℹ️ Questa [c]Playlist[/c] contiene {len(response['queue']['strings'])} elementi, e i prossimi saranno:\n"
|
|
||||||
elif response["type"] == "Pool":
|
|
||||||
if len(response["queue"]["strings"]) == 0:
|
|
||||||
message = f"ℹ️ Questo [c]Pool[/c] è vuoto."
|
|
||||||
else:
|
|
||||||
message = f"ℹ️ Questo [c]Pool[/c] contiene {len(response['queue']['strings'])} elementi, tra cui:\n"
|
|
||||||
elif response["type"] == "Layers":
|
|
||||||
if len(response["queue"]["strings"]) == 0:
|
|
||||||
message = f"ℹ️ Nessun elemento è attualmente in riproduzione, pertanto non ci sono [c]Layers[/c]:"
|
|
||||||
else:
|
|
||||||
message = f"ℹ️ I [c]Layers[/c] dell'elemento attualmente in riproduzione sono {len(response['queue']['strings'])}, tra cui:\n"
|
|
||||||
else:
|
|
||||||
if len(response["queue"]["strings"]) == 0:
|
|
||||||
message = f"ℹ️ Il PlayMode attuale, [c]{response['type']}[/c], è vuoto.\n"
|
|
||||||
else:
|
|
||||||
message = f"ℹ️ Il PlayMode attuale, [c]{response['type']}[/c], contiene {len(response['queue']['strings'])} elementi:\n"
|
|
||||||
if self.interface.name == "discord":
|
|
||||||
await data.reply(message)
|
|
||||||
for embed in pickle.loads(eval(response["queue"]["pickled_embeds"]))[:5]:
|
|
||||||
await data.message.channel.send(embed=embed)
|
|
||||||
else:
|
|
||||||
message += numberemojiformat(response["queue"]["strings"][:10])
|
|
||||||
await data.reply(message)
|
|
|
@ -1,20 +0,0 @@
|
||||||
import typing
|
|
||||||
import random
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class RageCommand(Command):
|
|
||||||
name: str = "rage"
|
|
||||||
|
|
||||||
aliases = ["balurage", "madden"]
|
|
||||||
|
|
||||||
description: str = "Arrabbiati per qualcosa, come una software house californiana."
|
|
||||||
|
|
||||||
MAD = ["MADDEN MADDEN MADDEN MADDEN",
|
|
||||||
"EA bad, praise Geraldo!",
|
|
||||||
"Stai sfogando la tua ira sul bot!",
|
|
||||||
"Basta, io cambio gilda!",
|
|
||||||
"Fondiamo la RRYG!"]
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
await data.reply(f"😠 {random.sample(self.MAD, 1)[0]}")
|
|
|
@ -1,86 +0,0 @@
|
||||||
import typing
|
|
||||||
import dateparser
|
|
||||||
import datetime
|
|
||||||
import pickle
|
|
||||||
import telegram
|
|
||||||
import discord
|
|
||||||
from sqlalchemy import and_
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import sleep_until, asyncify, telegram_escape, discord_escape
|
|
||||||
from ..tables import Reminder
|
|
||||||
|
|
||||||
|
|
||||||
class ReminderCommand(Command):
|
|
||||||
name: str = "reminder"
|
|
||||||
|
|
||||||
aliases = ["calendar"]
|
|
||||||
|
|
||||||
description: str = "Ti ricorda di fare qualcosa dopo un po' di tempo."
|
|
||||||
|
|
||||||
syntax: str = "[ {data} ] {messaggio}"
|
|
||||||
|
|
||||||
tables = {Reminder}
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
session = interface.alchemy.Session()
|
|
||||||
reminders = (
|
|
||||||
session.query(interface.alchemy.Reminder)
|
|
||||||
.filter(and_(
|
|
||||||
interface.alchemy.Reminder.datetime >= datetime.datetime.now(),
|
|
||||||
interface.alchemy.Reminder.interface_name == interface.name))
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
for reminder in reminders:
|
|
||||||
interface.loop.create_task(self._remind(reminder))
|
|
||||||
|
|
||||||
async def _remind(self, reminder):
|
|
||||||
await sleep_until(reminder.datetime)
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
chat_id: int = pickle.loads(reminder.raw_interface_data)
|
|
||||||
bot: telegram.Bot = self.interface.bot.client
|
|
||||||
await asyncify(bot.send_message,
|
|
||||||
chat_id=chat_id,
|
|
||||||
text=telegram_escape(f"❗️ {reminder.message}"),
|
|
||||||
parse_mode="HTML",
|
|
||||||
disable_web_page_preview=True)
|
|
||||||
elif self.interface.name == "discord":
|
|
||||||
channel_id: int = pickle.loads(reminder.raw_interface_data)
|
|
||||||
bot: discord.Client = self.interface.bot.client
|
|
||||||
channel = bot.get_channel(channel_id)
|
|
||||||
await channel.send(discord_escape(f"❗️ {reminder.message}"))
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
try:
|
|
||||||
date_str, reminder_text = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*")
|
|
||||||
except InvalidInputError:
|
|
||||||
date_str, reminder_text = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*")
|
|
||||||
|
|
||||||
try:
|
|
||||||
date: typing.Optional[datetime.datetime] = dateparser.parse(date_str, settings={
|
|
||||||
"PREFER_DATES_FROM": "future"
|
|
||||||
})
|
|
||||||
except OverflowError:
|
|
||||||
date = None
|
|
||||||
if date is None:
|
|
||||||
await data.reply("⚠️ La data che hai inserito non è valida.")
|
|
||||||
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]")
|
|
||||||
if self.interface.name == "telegram":
|
|
||||||
interface_data = pickle.dumps(data.update.effective_chat.id)
|
|
||||||
elif self.interface.name == "discord":
|
|
||||||
interface_data = pickle.dumps(data.message.channel.id)
|
|
||||||
else:
|
|
||||||
raise UnsupportedError("This command does not support the current interface.")
|
|
||||||
creator = await data.get_author()
|
|
||||||
reminder = self.interface.alchemy.Reminder(creator=creator,
|
|
||||||
interface_name=self.interface.name,
|
|
||||||
interface_data=interface_data,
|
|
||||||
datetime=date,
|
|
||||||
message=reminder_text)
|
|
||||||
self.interface.loop.create_task(self._remind(reminder))
|
|
||||||
data.session.add(reminder)
|
|
||||||
await asyncify(data.session.commit)
|
|
|
@ -1,40 +0,0 @@
|
||||||
import typing
|
|
||||||
import re
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import safeformat
|
|
||||||
|
|
||||||
|
|
||||||
class ShipCommand(Command):
|
|
||||||
name: str = "ship"
|
|
||||||
|
|
||||||
aliases = ["⛵️"]
|
|
||||||
|
|
||||||
description: str = "Crea una ship tra due nomi."
|
|
||||||
|
|
||||||
syntax = "{nomeuno} {nomedue}"
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
name_one = args[0]
|
|
||||||
name_two = args[1]
|
|
||||||
if name_two == "+":
|
|
||||||
name_two = args[2]
|
|
||||||
name_one = name_one.lower()
|
|
||||||
name_two = name_two.lower()
|
|
||||||
# Get all letters until the first vowel, included
|
|
||||||
match_one = re.search(r"^[A-Za-z][^aeiouAEIOU]*[aeiouAEIOU]?", name_one)
|
|
||||||
if match_one is None:
|
|
||||||
part_one = name_one[:int(len(name_one) / 2)]
|
|
||||||
else:
|
|
||||||
part_one = match_one.group(0)
|
|
||||||
# Get all letters from the second to last vowel, excluded
|
|
||||||
match_two = re.search(r"[^aeiouAEIOU]*[aeiouAEIOU]?[A-Za-z]$", name_two)
|
|
||||||
if match_two is None:
|
|
||||||
part_two = name_two[int(len(name_two) / 2):]
|
|
||||||
else:
|
|
||||||
part_two = match_two.group(0)
|
|
||||||
# Combine the two name parts
|
|
||||||
mixed = part_one + part_two
|
|
||||||
await data.reply(safeformat("💕 {one} + {two} = [b]{result}[/b]",
|
|
||||||
one=name_one.capitalize(),
|
|
||||||
two=name_two.capitalize(),
|
|
||||||
result=mixed.capitalize()))
|
|
|
@ -1,48 +0,0 @@
|
||||||
import typing
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class SkipCommand(Command):
|
|
||||||
name: str = "skip"
|
|
||||||
|
|
||||||
aliases = ["s", "next", "n"]
|
|
||||||
|
|
||||||
description: str = "Salta la canzone attualmente in riproduzione in chat vocale."
|
|
||||||
|
|
||||||
syntax: str = "[ [guild] ]"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_skip_handler(bot: "DiscordBot", guild_name: typing.Optional[str]):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("No guilds with the specified name found.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Multiple guilds with the specified name found.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Set the currently playing source as ended
|
|
||||||
voice_client: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild)
|
|
||||||
if voice_client and not (voice_client.is_playing() or voice_client.is_paused()):
|
|
||||||
raise CommandError("Nothing to skip")
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
voice_client._player.stop()
|
|
||||||
return {}
|
|
||||||
|
|
||||||
_event_name = "_legacy_skip"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_skip_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, = args.match(r"(?:\[(.+)])?")
|
|
||||||
await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name
|
|
||||||
})
|
|
||||||
await data.reply(f"⏩ Richiesto lo skip della canzone attuale.")
|
|
|
@ -1,70 +0,0 @@
|
||||||
import typing
|
|
||||||
import random
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import safeformat
|
|
||||||
|
|
||||||
|
|
||||||
class SmecdsCommand(Command):
|
|
||||||
name: str = "smecds"
|
|
||||||
|
|
||||||
aliases = ["secondomeecolpadellostagista"]
|
|
||||||
|
|
||||||
description: str = "Secondo me, è colpa dello stagista..."
|
|
||||||
|
|
||||||
syntax = ""
|
|
||||||
|
|
||||||
DS_LIST = ["della secca", "del seccatore", "del secchiello", "del secchio", "del secchione", "del secondino",
|
|
||||||
"del sedano", "del sedativo", "della sedia", "del sedicente", "del sedile", "della sega", "del segale",
|
|
||||||
"della segatura", "della seggiola", "del seggiolino", "della seggiovia", "della segheria",
|
|
||||||
"del seghetto", "del segnalibro", "del segnaposto", "del segno", "del segretario", "della segreteria",
|
|
||||||
"del seguace", "del segugio", "della selce", "della sella", "della selz", "della selva",
|
|
||||||
"della selvaggina", "del semaforo", "del seme", "del semifreddo", "del seminario", "della seminarista",
|
|
||||||
"della semola", "del semolino", "del semplicione", "della senape", "del senatore", "del seno",
|
|
||||||
"del sensore", "della sentenza", "della sentinella", "del sentore", "della seppia", "del sequestratore",
|
|
||||||
"della serenata", "del sergente", "del sermone", "della serpe", "del serpente", "della serpentina",
|
|
||||||
"della serra", "del serraglio", "del serramanico", "della serranda", "della serratura", "del servitore",
|
|
||||||
"della servitù", "del servizievole", "del servo", "del set", "della seta", "della setola", "del sigaro",
|
|
||||||
"del sidecar", "del siderurgico", "del sidro", "della siepe", "del sifone", "della sigaretta",
|
|
||||||
"del sigillo", "della signora", "della signorina", "del silenziatore", "della silhouette", "del silicio",
|
|
||||||
"del silicone", "del siluro", "della sinagoga", "della sindacalista", "del sindacato", "del sindaco",
|
|
||||||
"della sindrome", "della sinfonia", "del sipario", "del sire", "della sirena", "della siringa",
|
|
||||||
"del sismografo", "del sobborgo", "del sobillatore", "del sobrio", "del soccorritore", "del socio",
|
|
||||||
"del sociologo", "della soda", "del sofà", "della soffitta", "del software", "dello sogghignare",
|
|
||||||
"del soggiorno", "della sogliola", "del sognatore", "della soia", "del solaio", "del solco",
|
|
||||||
"del soldato", "del soldo", "del sole", "della soletta", "della solista", "del solitario",
|
|
||||||
"del sollazzare", "del sollazzo", "del sollecito", "del solleone", "del solletico", "del sollevare",
|
|
||||||
"del sollievo", "del solstizio", "del solubile", "del solvente", "della soluzione", "del somaro",
|
|
||||||
"del sombrero", "del sommergibile", "del sommo", "della sommossa", "del sommozzatore", "del sonar",
|
|
||||||
"della sonda", "del sondaggio", "del sondare", "del sonnacchioso", "del sonnambulo", "del sonnellino",
|
|
||||||
"del sonnifero", "del sonno", "della sonnolenza", "del sontuoso", "del soppalco", "del soprabito",
|
|
||||||
"del sopracciglio", "del sopraffare", "del sopraffino", "del sopraluogo", "del sopramobile",
|
|
||||||
"del soprannome", "del soprano", "del soprappensiero", "del soprassalto", "del soprassedere",
|
|
||||||
"del sopravvento", "del sopravvivere", "del soqquadro", "del sorbetto", "del sordido", "della sordina",
|
|
||||||
"del sordo", "della sorella", "della sorgente", "del sornione", "del sorpasso", "della sorpresa",
|
|
||||||
"del sorreggere", "del sorridere", "della sorsata", "del sorteggio", "del sortilegio",
|
|
||||||
"del sorvegliante", "del sorvolare", "del sosia", "del sospettoso", "del sospirare", "della sosta",
|
|
||||||
"della sostanza", "del sostegno", "del sostenitore", "del sostituto", "del sottaceto", "della sottana",
|
|
||||||
"del sotterfugio", "del sotterraneo", "del sottile", "del sottilizzare", "del sottintendere",
|
|
||||||
"del sottobanco", "del sottobosco", "del sottomarino", "del sottopassaggio", "del sottoposto",
|
|
||||||
"del sottoscala", "della sottoscrizione", "del sottostare", "del sottosuolo", "del sottotetto",
|
|
||||||
"del sottotitolo", "del sottovalutare", "del sottovaso", "della sottoveste", "del sottovuoto",
|
|
||||||
"del sottufficiale", "della soubrette", "del souvenir", "del soverchiare", "del sovrano",
|
|
||||||
"del sovrapprezzo", "della sovvenzione", "del sovversivo", "del sozzo", "dello suadente", "del sub",
|
|
||||||
"del subalterno", "del subbuglio", "del subdolo", "del sublime", "del suburbano", "del successore",
|
|
||||||
"del succo", "della succube", "del succulento", "della succursale", "del sudario", "della sudditanza",
|
|
||||||
"del suddito", "del sudicio", "del suffisso", "del suffragio", "del suffumigio", "del suggeritore",
|
|
||||||
"del sughero", "del sugo", "del suino", "della suite", "del sulfureo", "del sultano", "di Steffo",
|
|
||||||
"di Spaggia", "di Sabrina", "del sas", "del ses", "del sis", "del sos", "del sus", "della supremazia",
|
|
||||||
"del Santissimo", "della scatola", "del supercalifragilistichespiralidoso", "del sale", "del salame",
|
|
||||||
"di (Town of) Salem", "di Stronghold", "di SOMA", "dei Saints", "di S.T.A.L.K.E.R.", "di Sanctum",
|
|
||||||
"dei Sims", "di Sid", "delle Skullgirls", "di Sonic", "di Spiral (Knights)", "di Spore", "di Starbound",
|
|
||||||
"di SimCity", "di Sensei", "di Ssssssssssssss... Boom! E' esploso il dizionario", "della scala",
|
|
||||||
"di Sakura", "di Suzie", "di Shinji", "del senpai", "del support", "di Superman", "di Sekiro",
|
|
||||||
"dello Slime God", "del salassato", "della salsa", "di Senjougahara", "di Sugar", "della Stampa",
|
|
||||||
"della Stampante"]
|
|
||||||
|
|
||||||
SMECDS = "🤔 Secondo me, è colpa {ds}."
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
ds = random.sample(self.DS_LIST, 1)[0]
|
|
||||||
await data.reply(safeformat(self.SMECDS, ds=ds))
|
|
|
@ -1,77 +0,0 @@
|
||||||
import typing
|
|
||||||
import pickle
|
|
||||||
import datetime
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from royalnet.audio import YtdlDiscord
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class SoundcloudCommand(Command):
|
|
||||||
name: str = "soundcloud"
|
|
||||||
|
|
||||||
aliases = ["sc"]
|
|
||||||
|
|
||||||
description: str = "Cerca una canzone su Soundcloud e la aggiunge alla coda della chat vocale."
|
|
||||||
|
|
||||||
syntax = "[ [guild] ] {url}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_soundcloud_handler(bot: "DiscordBot", guild_name: typing.Optional[str], search: str):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("Server non trovato.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Il nome del server è ambiguo.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Ensure the guild has a PlayMode before adding the file to it
|
|
||||||
if not bot.music_data.get(guild):
|
|
||||||
raise CommandError("Il bot non è in nessun canale vocale.")
|
|
||||||
# Create url
|
|
||||||
ytdl_args = {
|
|
||||||
"format": "bestaudio/best",
|
|
||||||
"outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s"
|
|
||||||
}
|
|
||||||
# Start downloading
|
|
||||||
dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, f'scsearch:{search}',
|
|
||||||
**ytdl_args)
|
|
||||||
await bot.add_to_music_data(dfiles, guild)
|
|
||||||
# Create response dictionary
|
|
||||||
return {
|
|
||||||
"videos": [{
|
|
||||||
"title": dfile.info.title,
|
|
||||||
"discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
|
|
||||||
} for dfile in dfiles]
|
|
||||||
}
|
|
||||||
|
|
||||||
_event_name = "_legacy_soundcloud"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_soundcloud_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, search = args.match(r"(?:\[(.+)])?\s*<?(.+)>?")
|
|
||||||
if search.startswith("http://") or search.startswith("https://"):
|
|
||||||
raise CommandError(f"Il comando [c]{self.interface.prefix}soundcloud[/c] funziona solo per cercare audio su"
|
|
||||||
f" Soundcloud con un dato nome.\n"
|
|
||||||
f"Se vuoi riprodurre una canzone da un URL, usa [c]{self.interface.prefix}play[/c]!")
|
|
||||||
response = await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name,
|
|
||||||
"search": search
|
|
||||||
})
|
|
||||||
if len(response["videos"]) == 0:
|
|
||||||
raise CommandError(f"Il video non può essere scaricato a causa di un blocco imposto da Soundcloud.")
|
|
||||||
for video in response["videos"]:
|
|
||||||
if self.interface.name == "discord":
|
|
||||||
# This is one of the unsafest things ever
|
|
||||||
embed = pickle.loads(eval(video["discord_embed_pickle"]))
|
|
||||||
await data.message.channel.send(content="▶️ Aggiunto alla coda:", embed=embed)
|
|
||||||
else:
|
|
||||||
await data.reply(f"▶️ Aggiunto alla coda: [i]{video['title']}[/i]")
|
|
|
@ -1,77 +0,0 @@
|
||||||
import typing
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class SummonCommand(Command):
|
|
||||||
name: str = "summon"
|
|
||||||
|
|
||||||
aliases = ["cv"]
|
|
||||||
|
|
||||||
description: str = "Evoca il bot in un canale vocale."
|
|
||||||
|
|
||||||
syntax: str = "[nomecanale]"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_summon_handler(bot: "DiscordBot", channel_name: str):
|
|
||||||
"""Handle a summon Royalnet request.
|
|
||||||
That is, join a voice channel, or move to a different one if that is not possible."""
|
|
||||||
channels = bot.client.find_channel_by_name(channel_name)
|
|
||||||
if len(channels) < 1:
|
|
||||||
raise CommandError(f"Nessun canale vocale con il nome [c]{channel_name}[/c] trovato.")
|
|
||||||
channel = channels[0]
|
|
||||||
if not isinstance(channel, discord.VoiceChannel):
|
|
||||||
raise CommandError(f"Il canale [c]{channel}[/c] non è un canale vocale.")
|
|
||||||
bot.loop.create_task(bot.client.vc_connect_or_move(channel))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
_event_name = "_legacy_summon"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_summon_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self.interface.name == "discord":
|
|
||||||
bot = self.interface.bot.client
|
|
||||||
message: discord.Message = data.message
|
|
||||||
channel_name: str = args.optional(0)
|
|
||||||
if channel_name:
|
|
||||||
guild: typing.Optional[discord.Guild] = message.guild
|
|
||||||
if guild is not None:
|
|
||||||
channels: typing.List[discord.abc.GuildChannel] = guild.channels
|
|
||||||
else:
|
|
||||||
channels = bot.get_all_channels()
|
|
||||||
matching_channels: typing.List[discord.VoiceChannel] = []
|
|
||||||
for channel in channels:
|
|
||||||
if isinstance(channel, discord.VoiceChannel):
|
|
||||||
if channel.name == channel_name:
|
|
||||||
matching_channels.append(channel)
|
|
||||||
if len(matching_channels) == 0:
|
|
||||||
await data.reply("⚠️ Non esiste alcun canale vocale con il nome specificato.")
|
|
||||||
return
|
|
||||||
elif len(matching_channels) > 1:
|
|
||||||
await data.reply("⚠️ Esiste più di un canale vocale con il nome specificato.")
|
|
||||||
return
|
|
||||||
channel = matching_channels[0]
|
|
||||||
else:
|
|
||||||
author: discord.Member = message.author
|
|
||||||
try:
|
|
||||||
voice: typing.Optional[discord.VoiceState] = author.voice
|
|
||||||
except AttributeError:
|
|
||||||
await data.reply("⚠️ Non puoi evocare il bot da una chat privata!")
|
|
||||||
return
|
|
||||||
if voice is None:
|
|
||||||
await data.reply("⚠️ Non sei connesso a nessun canale vocale!")
|
|
||||||
return
|
|
||||||
channel = voice.channel
|
|
||||||
await bot.vc_connect_or_move(channel)
|
|
||||||
await data.reply(f"✅ Mi sono connesso in [c]#{channel.name}[/c].")
|
|
||||||
else:
|
|
||||||
channel_name: str = args[0].lstrip("#")
|
|
||||||
response = await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"channel_name": channel_name
|
|
||||||
})
|
|
||||||
await data.reply(f"✅ Mi sono connesso in [c]#{channel_name}[/c].")
|
|
|
@ -1,139 +0,0 @@
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
import random
|
|
||||||
import uuid
|
|
||||||
import html
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from ..tables import TriviaScore
|
|
||||||
|
|
||||||
|
|
||||||
class TriviaCommand(Command):
|
|
||||||
name: str = "trivia"
|
|
||||||
|
|
||||||
aliases = ["t"]
|
|
||||||
|
|
||||||
description: str = "Manda una domanda dell'OpenTDB in chat."
|
|
||||||
|
|
||||||
tables = {TriviaScore}
|
|
||||||
|
|
||||||
syntax = "[credits|scores]"
|
|
||||||
|
|
||||||
_letter_emojis = ["🇦", "🇧", "🇨", "🇩"]
|
|
||||||
|
|
||||||
_medal_emojis = ["🥇", "🥈", "🥉", "🔹"]
|
|
||||||
|
|
||||||
_correct_emoji = "✅"
|
|
||||||
|
|
||||||
_wrong_emoji = "❌"
|
|
||||||
|
|
||||||
_answer_time = 17
|
|
||||||
|
|
||||||
_question_lock: bool = False
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
self._answerers: typing.Dict[uuid.UUID, typing.Dict[str, bool]] = {}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
arg = args.optional(0)
|
|
||||||
if arg == "credits":
|
|
||||||
await data.reply(f"ℹ️ [c]{self.interface.prefix}{self.name}[/c] di [i]Steffo[/i]\n"
|
|
||||||
f"\n"
|
|
||||||
f"Tutte le domande vengono dall'[b]Open Trivia Database[/b] di [i]Pixeltail Games[/i],"
|
|
||||||
f" creatori di Tower Unite, e sono rilasciate sotto la licenza [b]CC BY-SA 4.0[/b].")
|
|
||||||
return
|
|
||||||
elif arg == "scores":
|
|
||||||
trivia_scores = await asyncify(data.session.query(self.alchemy.TriviaScore).all)
|
|
||||||
strings = ["🏆 [b]Trivia Leaderboards[/b]\n"]
|
|
||||||
for index, ts in enumerate(sorted(trivia_scores, key=lambda ts: -ts.correct_rate)):
|
|
||||||
if index > 3:
|
|
||||||
index = 3
|
|
||||||
strings.append(f"{self._medal_emojis[index]} {ts.royal.username}"
|
|
||||||
f" ({ts.correct_answers}/{ts.total_answers})")
|
|
||||||
await data.reply("\n".join(strings))
|
|
||||||
return
|
|
||||||
if self._question_lock:
|
|
||||||
raise CommandError("C'è già un'altra domanda attiva!")
|
|
||||||
self._question_lock = True
|
|
||||||
# Fetch the question
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get("https://opentdb.com/api.php?amount=1") as response:
|
|
||||||
j = await response.json()
|
|
||||||
# Parse the question
|
|
||||||
if j["response_code"] != 0:
|
|
||||||
raise CommandError(f"OpenTDB returned an error response_code ({j['response_code']}).")
|
|
||||||
question = j["results"][0]
|
|
||||||
text = f'❓ [b]{question["category"]} - {question["difficulty"].capitalize()}[/b]\n' \
|
|
||||||
f'{html.unescape(question["question"])}'
|
|
||||||
# Prepare answers
|
|
||||||
correct_answer: str = question["correct_answer"]
|
|
||||||
wrong_answers: typing.List[str] = question["incorrect_answers"]
|
|
||||||
answers: typing.List[str] = [correct_answer, *wrong_answers]
|
|
||||||
if question["type"] == "multiple":
|
|
||||||
random.shuffle(answers)
|
|
||||||
elif question["type"] == "boolean":
|
|
||||||
answers.sort(key=lambda a: a)
|
|
||||||
answers.reverse()
|
|
||||||
else:
|
|
||||||
raise NotImplementedError("Unknown question type")
|
|
||||||
# Find the correct index
|
|
||||||
for index, answer in enumerate(answers):
|
|
||||||
if answer == correct_answer:
|
|
||||||
correct_index = index
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError("correct_index not found")
|
|
||||||
# Add emojis
|
|
||||||
for index, answer in enumerate(answers):
|
|
||||||
answers[index] = f"{self._letter_emojis[index]} {html.unescape(answers[index])}"
|
|
||||||
# Create the question id
|
|
||||||
question_id = uuid.uuid4()
|
|
||||||
self._answerers[question_id] = {}
|
|
||||||
|
|
||||||
# Create the correct and wrong functions
|
|
||||||
async def correct(data: CommandData):
|
|
||||||
answerer_ = await data.get_author(error_if_none=True)
|
|
||||||
try:
|
|
||||||
self._answerers[question_id][answerer_.uid] = True
|
|
||||||
except KeyError:
|
|
||||||
raise KeyboardExpiredError("Tempo scaduto!")
|
|
||||||
return "🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!"
|
|
||||||
|
|
||||||
async def wrong(data: CommandData):
|
|
||||||
answerer_ = await data.get_author(error_if_none=True)
|
|
||||||
try:
|
|
||||||
self._answerers[question_id][answerer_.uid] = False
|
|
||||||
except KeyError:
|
|
||||||
raise KeyboardExpiredError("Tempo scaduto!")
|
|
||||||
return "🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!"
|
|
||||||
|
|
||||||
# Add question
|
|
||||||
keyboard = {}
|
|
||||||
for index, answer in enumerate(answers):
|
|
||||||
if index == correct_index:
|
|
||||||
keyboard[answer] = correct
|
|
||||||
else:
|
|
||||||
keyboard[answer] = wrong
|
|
||||||
await data.keyboard(text, keyboard)
|
|
||||||
await asyncio.sleep(self._answer_time)
|
|
||||||
results = f"❗️ Tempo scaduto!\n" \
|
|
||||||
f"La risposta corretta era [b]{answers[correct_index]}[/b]!\n\n"
|
|
||||||
for answerer_id in self._answerers[question_id]:
|
|
||||||
answerer = data.session.query(self.alchemy.User).get(answerer_id)
|
|
||||||
if answerer.trivia_score is None:
|
|
||||||
ts = self.interface.alchemy.TriviaScore(royal=answerer)
|
|
||||||
data.session.add(ts)
|
|
||||||
await asyncify(data.session.commit)
|
|
||||||
if self._answerers[question_id][answerer_id]:
|
|
||||||
results += self._correct_emoji
|
|
||||||
answerer.trivia_score.correct_answers += 1
|
|
||||||
else:
|
|
||||||
results += self._wrong_emoji
|
|
||||||
answerer.trivia_score.wrong_answers += 1
|
|
||||||
results += f" {answerer} ({answerer.trivia_score.correct_answers}/{answerer.trivia_score.total_answers})\n"
|
|
||||||
await data.reply(results)
|
|
||||||
del self._answerers[question_id]
|
|
||||||
await asyncify(data.session.commit)
|
|
||||||
self._question_lock = False
|
|
|
@ -1,50 +0,0 @@
|
||||||
import typing
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class VideochannelCommand(Command):
|
|
||||||
name: str = "videochannel"
|
|
||||||
|
|
||||||
aliases = ["golive", "live", "video"]
|
|
||||||
|
|
||||||
description: str = "Converti il canale vocale in un canale video."
|
|
||||||
|
|
||||||
syntax = "[channelname]"
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self.interface.name == "discord":
|
|
||||||
bot: discord.Client = self.interface.bot
|
|
||||||
message: discord.Message = data.message
|
|
||||||
channel_name: str = args.optional(0)
|
|
||||||
if channel_name:
|
|
||||||
guild: typing.Optional[discord.Guild] = message.guild
|
|
||||||
if guild is not None:
|
|
||||||
channels: typing.List[discord.abc.GuildChannel] = guild.channels
|
|
||||||
else:
|
|
||||||
channels = bot.get_all_channels()
|
|
||||||
matching_channels: typing.List[discord.VoiceChannel] = []
|
|
||||||
for channel in channels:
|
|
||||||
if isinstance(channel, discord.VoiceChannel):
|
|
||||||
if channel.name == channel_name:
|
|
||||||
matching_channels.append(channel)
|
|
||||||
if len(matching_channels) == 0:
|
|
||||||
raise CommandError("Non esiste alcun canale vocale con il nome specificato.")
|
|
||||||
elif len(matching_channels) > 1:
|
|
||||||
raise CommandError("Esiste più di un canale vocale con il nome specificato.")
|
|
||||||
channel = matching_channels[0]
|
|
||||||
else:
|
|
||||||
author: discord.Member = message.author
|
|
||||||
voice: typing.Optional[discord.VoiceState] = author.voice
|
|
||||||
if voice is None:
|
|
||||||
raise CommandError("Non sei connesso a nessun canale vocale.")
|
|
||||||
channel = voice.channel
|
|
||||||
if author.is_on_mobile():
|
|
||||||
await data.reply(f"📹 Per entrare in modalità video, clicca qui:\n"
|
|
||||||
f"<https://discordapp.com/channels/{channel.guild.id}/{channel.id}>\n"
|
|
||||||
f"[b]Attenzione: la modalità video non funziona su Android e iOS![/b]")
|
|
||||||
return
|
|
||||||
await data.reply(f"📹 Per entrare in modalità video, clicca qui:\n"
|
|
||||||
f"<https://discordapp.com/channels/{channel.guild.id}/{channel.id}>")
|
|
||||||
else:
|
|
||||||
raise UnsupportedError()
|
|
|
@ -1,76 +0,0 @@
|
||||||
import typing
|
|
||||||
import pickle
|
|
||||||
import datetime
|
|
||||||
import discord
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from royalnet.audio import YtdlDiscord
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class YoutubeCommand(Command):
|
|
||||||
name: str = "youtube"
|
|
||||||
|
|
||||||
aliases = ["yt"]
|
|
||||||
|
|
||||||
description: str = "Cerca un video su YouTube e lo aggiunge alla coda della chat vocale."
|
|
||||||
|
|
||||||
syntax = "[ [guild] ] {url}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _legacy_youtube_handler(cls, bot: "DiscordBot", guild_name: typing.Optional[str], search: str):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("Server non trovato.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Il nome del server è ambiguo.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Ensure the guild has a PlayMode before adding the file to it
|
|
||||||
if not bot.music_data.get(guild):
|
|
||||||
raise CommandError("Il bot non è in nessun canale vocale.")
|
|
||||||
# Create url
|
|
||||||
ytdl_args = {
|
|
||||||
"format": "bestaudio/best",
|
|
||||||
"outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s"
|
|
||||||
}
|
|
||||||
# Start downloading
|
|
||||||
dfiles: typing. List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, f'ytsearch:{search}', **ytdl_args)
|
|
||||||
await bot.add_to_music_data(dfiles, guild)
|
|
||||||
# Create response dictionary
|
|
||||||
return {
|
|
||||||
"videos": [{
|
|
||||||
"title": dfile.info.title,
|
|
||||||
"discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
|
|
||||||
} for dfile in dfiles]
|
|
||||||
}
|
|
||||||
|
|
||||||
_event_name = "_legacy_youtube"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_youtube_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, search = args.match(r"(?:\[(.+)])?\s*<?(.+)>?")
|
|
||||||
if search.startswith("http://") or search.startswith("https://"):
|
|
||||||
raise CommandError(f"Il comando [c]{self.interface.prefix}youtube[/c] funziona solo per cercare video su"
|
|
||||||
f" YouTube con un dato nome.\n"
|
|
||||||
f"Se vuoi riprodurre una canzone da un URL, usa [c]{self.interface.prefix}play[/c]!")
|
|
||||||
response = await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name,
|
|
||||||
"search": search
|
|
||||||
})
|
|
||||||
if len(response["videos"]) == 0:
|
|
||||||
raise CommandError(f"Il video non può essere scaricato a causa di un blocco imposto da YouTube.")
|
|
||||||
for video in response["videos"]:
|
|
||||||
if self.interface.name == "discord":
|
|
||||||
# This is one of the unsafest things ever
|
|
||||||
embed = pickle.loads(eval(video["discord_embed_pickle"]))
|
|
||||||
await data.message.channel.send(content="▶️ Aggiunto alla coda:", embed=embed)
|
|
||||||
else:
|
|
||||||
await data.reply(f"▶️ Aggiunto alla coda: [i]{video['title']}[/i]")
|
|
|
@ -1,91 +0,0 @@
|
||||||
import typing
|
|
||||||
import discord
|
|
||||||
import asyncio
|
|
||||||
import datetime
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from royalnet.audio import YtdlDiscord
|
|
||||||
from royalnet.audio.playmodes import Playlist
|
|
||||||
from royalnet.bots import DiscordBot
|
|
||||||
|
|
||||||
|
|
||||||
class ZawarudoCommand(Command):
|
|
||||||
name: str = "zawarudo"
|
|
||||||
|
|
||||||
aliases = ["theworld", "world"]
|
|
||||||
|
|
||||||
description: str = "Ferma il tempo!"
|
|
||||||
|
|
||||||
syntax = "[ [guild] ] [1-9]"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _legacy_zawarudo_handler(bot: "DiscordBot", guild_name: typing.Optional[str], time: int):
|
|
||||||
# Find the matching guild
|
|
||||||
if guild_name:
|
|
||||||
guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(guild_name)
|
|
||||||
else:
|
|
||||||
guilds = bot.client.guilds
|
|
||||||
if len(guilds) == 0:
|
|
||||||
raise CommandError("Server non trovato.")
|
|
||||||
if len(guilds) > 1:
|
|
||||||
raise CommandError("Il nome del server è ambiguo.")
|
|
||||||
guild = list(bot.client.guilds)[0]
|
|
||||||
# Ensure the guild has a PlayMode before adding the file to it
|
|
||||||
if not bot.music_data.get(guild):
|
|
||||||
raise CommandError("Il bot non è in nessun canale vocale.")
|
|
||||||
# Create url
|
|
||||||
ytdl_args = {
|
|
||||||
"format": "bestaudio",
|
|
||||||
"outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s"
|
|
||||||
}
|
|
||||||
# Start downloading
|
|
||||||
zw_start: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url,
|
|
||||||
"https://scaleway.steffo.eu/jojo/zawarudo_intro.mp3",
|
|
||||||
**ytdl_args)
|
|
||||||
zw_end: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url,
|
|
||||||
"https://scaleway.steffo.eu/jojo/zawarudo_outro.mp3",
|
|
||||||
**ytdl_args)
|
|
||||||
old_playlist = bot.music_data[guild]
|
|
||||||
bot.music_data[guild].playmode = Playlist()
|
|
||||||
# Get voice client
|
|
||||||
vc: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild)
|
|
||||||
channel: discord.VoiceChannel = vc.channel
|
|
||||||
affected: typing.List[typing.Union[discord.User, discord.Member]] = channel.members
|
|
||||||
await bot.add_to_music_data(zw_start, guild)
|
|
||||||
for member in affected:
|
|
||||||
if member.bot:
|
|
||||||
continue
|
|
||||||
await member.edit(mute=True)
|
|
||||||
await asyncio.sleep(time)
|
|
||||||
await bot.add_to_music_data(zw_end, guild)
|
|
||||||
for member in affected:
|
|
||||||
member: typing.Union[discord.User, discord.Member]
|
|
||||||
if member.bot:
|
|
||||||
continue
|
|
||||||
await member.edit(mute=False)
|
|
||||||
bot.music_data[guild] = old_playlist
|
|
||||||
await bot.advance_music_data(guild)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
_event_name = "_legacy_zawarudo"
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
if interface.name == "discord":
|
|
||||||
interface.register_herald_action(self._event_name, self._legacy_zawarudo_handler)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
guild_name, time = args.match(r"(?:\[(.+)])?\s*(.+)?")
|
|
||||||
if time is None:
|
|
||||||
time = 5
|
|
||||||
else:
|
|
||||||
time = int(time)
|
|
||||||
if time < 1:
|
|
||||||
raise InvalidInputError("The World can't stop time for less than a second.")
|
|
||||||
if time > 10:
|
|
||||||
raise InvalidInputError("The World can stop time only for 10 seconds.")
|
|
||||||
await data.reply(f"🕒 ZA WARUDO! TOKI WO TOMARE!")
|
|
||||||
await self.interface.call_herald_action("discord", self._event_name, {
|
|
||||||
"guild_name": guild_name,
|
|
||||||
"time": time
|
|
||||||
})
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Imports go here!
|
|
||||||
from .api_user_list import ApiUserListStar
|
|
||||||
from .api_user_get import ApiUserGetStar
|
|
||||||
from .api_diario_list import ApiDiarioListStar
|
|
||||||
from .api_diario_get import ApiDiarioGetStar
|
|
||||||
|
|
||||||
# Enter the PageStars of your Pack here!
|
|
||||||
available_page_stars = [
|
|
||||||
ApiUserListStar,
|
|
||||||
ApiUserGetStar,
|
|
||||||
ApiDiarioListStar,
|
|
||||||
ApiDiarioGetStar,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Enter the ExceptionStars of your Pack here!
|
|
||||||
available_exception_stars = [
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
|
||||||
__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]]
|
|
|
@ -1,22 +0,0 @@
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import *
|
|
||||||
from royalnet.web import *
|
|
||||||
from royalnet.utils import *
|
|
||||||
from ..tables import Diario
|
|
||||||
|
|
||||||
|
|
||||||
class ApiDiarioGetStar(PageStar):
|
|
||||||
path = "/api/diario/get/{diario_id}"
|
|
||||||
tables = {Diario}
|
|
||||||
|
|
||||||
async def page(self, request: Request) -> JSONResponse:
|
|
||||||
diario_id_str = request.path_params.get("diario_id", "")
|
|
||||||
try:
|
|
||||||
diario_id = int(diario_id_str)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return error(400, "Invalid diario_id")
|
|
||||||
async with self.alchemy.session_acm() as session:
|
|
||||||
entry: Diario = await asyncify(session.query(self.alchemy.User).get, diario_id)
|
|
||||||
if entry is None:
|
|
||||||
return error(404, "No such user")
|
|
||||||
return JSONResponse(entry.json())
|
|
|
@ -1,25 +0,0 @@
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import *
|
|
||||||
from royalnet.web import *
|
|
||||||
from royalnet.utils import *
|
|
||||||
from ..tables import Diario
|
|
||||||
|
|
||||||
|
|
||||||
class ApiDiarioListStar(PageStar):
|
|
||||||
path = "/api/diario/list"
|
|
||||||
tables = {Diario}
|
|
||||||
|
|
||||||
async def page(self, request: Request) -> JSONResponse:
|
|
||||||
page_str = request.query_params.get("page", "0")
|
|
||||||
try:
|
|
||||||
page = int(page_str)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return error(400, "Invalid offset")
|
|
||||||
async with self.alchemy.session_acm() as session:
|
|
||||||
if page < 0:
|
|
||||||
page = -page-1
|
|
||||||
entries: typing.List[Diario] = await asyncify(session.query(self.alchemy.Diario).order_by(self.alchemy.Diario.diario_id.desc()).limit(500).offset(page * 500).all)
|
|
||||||
else:
|
|
||||||
entries: typing.List[Diario] = await asyncify(session.query(self.alchemy.Diario).order_by(self.alchemy.Diario.diario_id).limit(500).offset(page * 500).all)
|
|
||||||
response = [entry.json() for entry in entries]
|
|
||||||
return JSONResponse(response)
|
|
|
@ -1,22 +0,0 @@
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import *
|
|
||||||
from royalnet.web import *
|
|
||||||
from royalnet.utils import *
|
|
||||||
from royalnet.packs.common.tables import User
|
|
||||||
|
|
||||||
|
|
||||||
class ApiUserGetStar(PageStar):
|
|
||||||
path = "/api/user/get/{uid_str}"
|
|
||||||
tables = {User}
|
|
||||||
|
|
||||||
async def page(self, request: Request) -> JSONResponse:
|
|
||||||
uid_str = request.path_params.get("uid_str", "")
|
|
||||||
try:
|
|
||||||
uid = int(uid_str)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return error(400, "Invalid uid")
|
|
||||||
async with self.alchemy.session_acm() as session:
|
|
||||||
user: User = await asyncify(session.query(self.alchemy.User).get, uid)
|
|
||||||
if user is None:
|
|
||||||
return error(404, "No such user")
|
|
||||||
return JSONResponse(user.json())
|
|
|
@ -1,15 +0,0 @@
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import *
|
|
||||||
from royalnet.web import *
|
|
||||||
from royalnet.utils import *
|
|
||||||
from royalnet.packs.common.tables import User
|
|
||||||
|
|
||||||
|
|
||||||
class ApiUserListStar(PageStar):
|
|
||||||
path = "/api/user/list"
|
|
||||||
tables = {User}
|
|
||||||
|
|
||||||
async def page(self, request: Request) -> JSONResponse:
|
|
||||||
async with self.alchemy.session_acm() as session:
|
|
||||||
users: typing.List[User] = await asyncify(session.query(self.alchemy.User).all)
|
|
||||||
return JSONResponse([user.json() for user in users])
|
|
|
@ -1,35 +0,0 @@
|
||||||
# Imports go here!
|
|
||||||
from royalnet.packs.common.tables import User
|
|
||||||
from royalnet.packs.common.tables import Telegram
|
|
||||||
from royalnet.packs.common.tables import Discord
|
|
||||||
|
|
||||||
from .diario import Diario
|
|
||||||
from .aliases import Alias
|
|
||||||
from .wikipages import WikiPage
|
|
||||||
from .wikirevisions import WikiRevision
|
|
||||||
from .bios import Bio
|
|
||||||
from .reminders import Reminder
|
|
||||||
from .triviascores import TriviaScore
|
|
||||||
from .mmevents import MMEvent
|
|
||||||
from .mmresponse import MMResponse
|
|
||||||
from .leagueoflegends import LeagueOfLegends
|
|
||||||
|
|
||||||
# Enter the tables of your Pack here!
|
|
||||||
available_tables = [
|
|
||||||
User,
|
|
||||||
Telegram,
|
|
||||||
Discord,
|
|
||||||
Diario,
|
|
||||||
Alias,
|
|
||||||
WikiPage,
|
|
||||||
WikiRevision,
|
|
||||||
Bio,
|
|
||||||
Reminder,
|
|
||||||
TriviaScore,
|
|
||||||
MMEvent,
|
|
||||||
MMResponse,
|
|
||||||
LeagueOfLegends
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
|
||||||
__all__ = [table.__name__ for table in available_tables]
|
|
|
@ -1,28 +0,0 @@
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Integer, \
|
|
||||||
String, \
|
|
||||||
ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class Alias:
|
|
||||||
__tablename__ = "aliases"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def alias(self):
|
|
||||||
return Column(String, primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal(self):
|
|
||||||
return relationship("User", backref="aliases")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Alias {str(self)}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.alias}->{self.royal_id}"
|
|
|
@ -1,28 +0,0 @@
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Integer, \
|
|
||||||
Text, \
|
|
||||||
ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship, backref
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class Bio:
|
|
||||||
__tablename__ = "bios"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal(self):
|
|
||||||
return relationship("User", backref=backref("bio", uselist=False))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def contents(self):
|
|
||||||
return Column(Text, nullable=False, default="")
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Bio of {self.royal}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.contents
|
|
|
@ -1,105 +0,0 @@
|
||||||
import re
|
|
||||||
import datetime
|
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Integer, \
|
|
||||||
Text, \
|
|
||||||
Boolean, \
|
|
||||||
DateTime, \
|
|
||||||
ForeignKey, \
|
|
||||||
String
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class Diario:
|
|
||||||
__tablename__ = "diario"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def diario_id(self):
|
|
||||||
return Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def quoted_account_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def quoted(self):
|
|
||||||
return Column(String)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def text(self):
|
|
||||||
return Column(Text)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def context(self):
|
|
||||||
return Column(Text)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def timestamp(self) -> datetime.datetime:
|
|
||||||
return Column(DateTime, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def media_url(self):
|
|
||||||
return Column(String)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def spoiler(self):
|
|
||||||
return Column(Boolean, default=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator(self):
|
|
||||||
return relationship("User", foreign_keys=self.creator_id, backref="diario_created")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def quoted_account(self):
|
|
||||||
return relationship("User", foreign_keys=self.quoted_account_id, backref="diario_quoted")
|
|
||||||
|
|
||||||
def json(self) -> dict:
|
|
||||||
return {
|
|
||||||
"diario_id": self.diario_id,
|
|
||||||
"creator": self.creator.json() if self.creator else None,
|
|
||||||
"quoted_account": self.quoted_account.json() if self.quoted_account else None,
|
|
||||||
"quoted": self.quoted,
|
|
||||||
"text": self.text,
|
|
||||||
"context": self.context,
|
|
||||||
"timestamp": self.timestamp.isoformat(),
|
|
||||||
"media_url": self.media_url,
|
|
||||||
"spoiler": self.spoiler
|
|
||||||
}
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Diario diario_id={self.diario_id}" \
|
|
||||||
f" creator_id={self.creator_id}" \
|
|
||||||
f" quoted_account_id={self.quoted_account_id}" \
|
|
||||||
f" quoted={self.quoted}" \
|
|
||||||
f" text={self.text}" \
|
|
||||||
f" context={self.context}" \
|
|
||||||
f" timestamp={self.timestamp}" \
|
|
||||||
f" media_url={self.media_url}" \
|
|
||||||
f" spoiler={self.spoiler}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
text = f"Riga #{self.diario_id}"
|
|
||||||
text += f" (salvata da {str(self.creator)}"
|
|
||||||
text += f" il {self.timestamp.strftime('%Y-%m-%d %H:%M')}):\n"
|
|
||||||
if self.media_url is not None:
|
|
||||||
text += f"{self.media_url}\n"
|
|
||||||
if self.text is not None:
|
|
||||||
if self.spoiler:
|
|
||||||
hidden = re.sub(r"\w", "█", self.text)
|
|
||||||
text += f"\"{hidden}\"\n"
|
|
||||||
else:
|
|
||||||
text += f"[b]\"{self.text}\"[/b]\n"
|
|
||||||
if self.quoted_account is not None:
|
|
||||||
text += f" —{str(self.quoted_account)}"
|
|
||||||
elif self.quoted is not None:
|
|
||||||
text += f" —{self.quoted}"
|
|
||||||
else:
|
|
||||||
text += f" —Anonimo"
|
|
||||||
if self.context:
|
|
||||||
text += f", [i]{self.context}[/i]"
|
|
||||||
return text
|
|
|
@ -1,256 +0,0 @@
|
||||||
from sqlalchemy import *
|
|
||||||
from sqlalchemy.orm import relationship, composite
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
from ..utils import LeagueRank, LeagueTier, LeagueLeague
|
|
||||||
|
|
||||||
|
|
||||||
class LeagueOfLegends:
|
|
||||||
__tablename__ = "leagueoflegends"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def region(self):
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user(self):
|
|
||||||
return relationship("User", backref="leagueoflegends")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def profile_icon_id(self):
|
|
||||||
# 3777
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def summoner_name(self):
|
|
||||||
# SteffoRYG
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def puuid(self):
|
|
||||||
# iNW0i7w_cC2kxgNB13UhyGPeyxZChmRqKylZ--bzbZAhFM6EXAImUqeRWmGtK6iKiYbz3bkCV8fMQQ
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def summoner_level(self):
|
|
||||||
# 68
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def summoner_id(self):
|
|
||||||
# aEsHyfXA2q8bK-g7GlT4kFK_0uLL3w-jBPyfMAy8kOXTJXo
|
|
||||||
return Column(String, nullable=False, primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def account_id(self):
|
|
||||||
# -2Ex-VpkkNBN4ceQev8oJsamxY5iGb2liRUqkES5TU_7vtI
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_tier(self):
|
|
||||||
return Column(Enum(LeagueTier))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_rank(self):
|
|
||||||
return Column(Enum(LeagueRank))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_points(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_wins(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_losses(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_inactive(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_hot_streak(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_fresh_blood(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq_veteran(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_soloq(self):
|
|
||||||
return composite(LeagueLeague,
|
|
||||||
self.rank_soloq_tier,
|
|
||||||
self.rank_soloq_rank,
|
|
||||||
self.rank_soloq_points,
|
|
||||||
self.rank_soloq_wins,
|
|
||||||
self.rank_soloq_losses,
|
|
||||||
self.rank_soloq_inactive,
|
|
||||||
self.rank_soloq_hot_streak,
|
|
||||||
self.rank_soloq_fresh_blood,
|
|
||||||
self.rank_soloq_veteran)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_tier(self):
|
|
||||||
return Column(Enum(LeagueTier))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_rank(self):
|
|
||||||
return Column(Enum(LeagueRank))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_points(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_wins(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_losses(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_inactive(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_hot_streak(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_fresh_blood(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq_veteran(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_flexq(self):
|
|
||||||
return composite(LeagueLeague,
|
|
||||||
self.rank_flexq_tier,
|
|
||||||
self.rank_flexq_rank,
|
|
||||||
self.rank_flexq_points,
|
|
||||||
self.rank_flexq_wins,
|
|
||||||
self.rank_flexq_losses,
|
|
||||||
self.rank_flexq_inactive,
|
|
||||||
self.rank_flexq_hot_streak,
|
|
||||||
self.rank_flexq_fresh_blood,
|
|
||||||
self.rank_flexq_veteran)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_tier(self):
|
|
||||||
return Column(Enum(LeagueTier))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_rank(self):
|
|
||||||
return Column(Enum(LeagueRank))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_points(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_wins(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_losses(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_inactive(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_hot_streak(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_fresh_blood(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq_veteran(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_twtrq(self):
|
|
||||||
return composite(LeagueLeague,
|
|
||||||
self.rank_twtrq_tier,
|
|
||||||
self.rank_twtrq_rank,
|
|
||||||
self.rank_twtrq_points,
|
|
||||||
self.rank_twtrq_wins,
|
|
||||||
self.rank_twtrq_losses,
|
|
||||||
self.rank_twtrq_inactive,
|
|
||||||
self.rank_twtrq_hot_streak,
|
|
||||||
self.rank_twtrq_fresh_blood,
|
|
||||||
self.rank_twtrq_veteran)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_tier(self):
|
|
||||||
return Column(Enum(LeagueTier))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_rank(self):
|
|
||||||
return Column(Enum(LeagueRank))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_points(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_wins(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_losses(self):
|
|
||||||
return Column(Integer)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_inactive(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_hot_streak(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_fresh_blood(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq_veteran(self):
|
|
||||||
return Column(Boolean)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def rank_tftq(self):
|
|
||||||
return composite(LeagueLeague,
|
|
||||||
self.rank_tftq_tier,
|
|
||||||
self.rank_tftq_rank,
|
|
||||||
self.rank_tftq_points,
|
|
||||||
self.rank_tftq_wins,
|
|
||||||
self.rank_tftq_losses,
|
|
||||||
self.rank_tftq_inactive,
|
|
||||||
self.rank_tftq_hot_streak,
|
|
||||||
self.rank_tftq_fresh_blood,
|
|
||||||
self.rank_tftq_veteran)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def mastery_score(self):
|
|
||||||
return Column(Integer, nullable=False, default=0)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__qualname__} {str(self)}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"[c]{self.__tablename__}:{self.summoner_name}[/c]"
|
|
|
@ -1,52 +0,0 @@
|
||||||
import pickle
|
|
||||||
from sqlalchemy import *
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class MMEvent:
|
|
||||||
__tablename__ = "mmevents"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"), nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator(self):
|
|
||||||
return relationship("User", 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 interface(self):
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def raw_interface_data(self):
|
|
||||||
# The default is a pickled None
|
|
||||||
return Column(Binary, nullable=False, default=b'\x80\x03N.')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def interface_data(self):
|
|
||||||
return pickle.loads(self.raw_interface_data)
|
|
||||||
|
|
||||||
@interface_data.setter
|
|
||||||
def interface_data(self, value):
|
|
||||||
self.raw_interface_data = pickle.dumps(value)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<MMEvent {self.mmid}: {self.title}>"
|
|
|
@ -1,31 +0,0 @@
|
||||||
from sqlalchemy import *
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
from ..utils import MMChoice
|
|
||||||
|
|
||||||
|
|
||||||
class MMResponse:
|
|
||||||
__tablename__ = "mmresponse"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user(self):
|
|
||||||
return relationship("User", 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 choice(self):
|
|
||||||
return Column(Enum(MMChoice), nullable=False)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<MMResponse of {self.user}: {self.choice}>"
|
|
|
@ -1,46 +0,0 @@
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Integer, \
|
|
||||||
String, \
|
|
||||||
LargeBinary, \
|
|
||||||
DateTime, \
|
|
||||||
ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class Reminder:
|
|
||||||
__tablename__ = "reminder"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def reminder_id(self):
|
|
||||||
return Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator(self):
|
|
||||||
return relationship("User", backref="reminders_created")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def interface_name(self):
|
|
||||||
return Column(String)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def interface_data(self):
|
|
||||||
return Column(LargeBinary)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def datetime(self):
|
|
||||||
return Column(DateTime)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def message(self):
|
|
||||||
return Column(String)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Reminder for {self.datetime.isoformat()} about {self.message}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message
|
|
|
@ -1,40 +0,0 @@
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Integer, \
|
|
||||||
ForeignKey
|
|
||||||
from sqlalchemy.orm import relationship, backref
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class TriviaScore:
|
|
||||||
__tablename__ = "triviascores"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def royal(self):
|
|
||||||
return relationship("User", backref=backref("trivia_score", uselist=False))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def correct_answers(self):
|
|
||||||
return Column(Integer, nullable=False, default=0)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def wrong_answers(self):
|
|
||||||
return Column(Integer, nullable=False, default=0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_answers(self):
|
|
||||||
return self.correct_answers + self.wrong_answers
|
|
||||||
|
|
||||||
@property
|
|
||||||
def offset(self):
|
|
||||||
return self.correct_answers - self.wrong_answers
|
|
||||||
|
|
||||||
@property
|
|
||||||
def correct_rate(self):
|
|
||||||
return self.correct_answers / self.total_answers
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<TriviaScore of {self.royal}: ({self.correct_answers}|{self.wrong_answers})>"
|
|
|
@ -1,38 +0,0 @@
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Text, \
|
|
||||||
String
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
from royalnet.utils import to_urluuid
|
|
||||||
|
|
||||||
|
|
||||||
class WikiPage:
|
|
||||||
"""Wiki page properties.
|
|
||||||
|
|
||||||
Warning:
|
|
||||||
Requires PostgreSQL!"""
|
|
||||||
__tablename__ = "wikipages"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def page_id(self):
|
|
||||||
return Column(UUID(as_uuid=True), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def title(self):
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def contents(self):
|
|
||||||
return Column(Text)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def format(self):
|
|
||||||
return Column(String, nullable=False, default="markdown")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def css(self):
|
|
||||||
return Column(String)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def page_short_id(self):
|
|
||||||
return to_urluuid(self.page_id)
|
|
|
@ -1,48 +0,0 @@
|
||||||
from sqlalchemy import Column, \
|
|
||||||
Integer, \
|
|
||||||
Text, \
|
|
||||||
DateTime, \
|
|
||||||
ForeignKey
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
|
||||||
|
|
||||||
|
|
||||||
class WikiRevision:
|
|
||||||
"""A wiki page revision.
|
|
||||||
|
|
||||||
Warning:
|
|
||||||
Requires PostgreSQL!"""
|
|
||||||
__tablename__ = "wikirevisions"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def revision_id(self):
|
|
||||||
return Column(UUID(as_uuid=True), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def page_id(self):
|
|
||||||
return Column(UUID(as_uuid=True), ForeignKey("wikipages.page_id"), nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def page(self):
|
|
||||||
return relationship("WikiPage", foreign_keys=self.page_id, backref="revisions")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def author_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"), nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def author(self):
|
|
||||||
return relationship("User", foreign_keys=self.author_id, backref="wiki_contributions")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def timestamp(self):
|
|
||||||
return Column(DateTime, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def reason(self):
|
|
||||||
return Column(Text)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def diff(self):
|
|
||||||
return Column(Text)
|
|
|
@ -1,7 +0,0 @@
|
||||||
from .mmchoice import MMChoice
|
|
||||||
from .mminterfacedata import MMInterfaceData, MMInterfaceDataTelegram
|
|
||||||
from .leaguetier import LeagueTier
|
|
||||||
from .leaguerank import LeagueRank
|
|
||||||
from .leagueleague import LeagueLeague
|
|
||||||
|
|
||||||
__all__ = ["MMChoice", "MMInterfaceData", "MMInterfaceDataTelegram", "LeagueTier", "LeagueRank", "LeagueLeague"]
|
|
|
@ -1,134 +0,0 @@
|
||||||
from .leaguetier import LeagueTier
|
|
||||||
from .leaguerank import LeagueRank
|
|
||||||
|
|
||||||
|
|
||||||
class LeagueLeague:
|
|
||||||
def __init__(self,
|
|
||||||
tier: LeagueTier = None,
|
|
||||||
rank: LeagueRank = None,
|
|
||||||
points: int = None,
|
|
||||||
wins: int = None,
|
|
||||||
losses: int = None,
|
|
||||||
inactive: bool = None,
|
|
||||||
hot_streak: bool = None,
|
|
||||||
fresh_blood: bool = None,
|
|
||||||
veteran: bool = None):
|
|
||||||
self.tier: LeagueTier = tier # IRON
|
|
||||||
self.rank: LeagueRank = rank # I
|
|
||||||
self.points: int = points # 40 LP
|
|
||||||
self.wins: int = wins
|
|
||||||
self.losses: int = losses
|
|
||||||
self.inactive: bool = inactive
|
|
||||||
self.hot_streak: bool = hot_streak
|
|
||||||
self.fresh_blood: bool = fresh_blood
|
|
||||||
self.veteran: bool = veteran
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
emojis = ""
|
|
||||||
if self.veteran:
|
|
||||||
emojis += "🏆"
|
|
||||||
if self.hot_streak:
|
|
||||||
emojis += "🔥"
|
|
||||||
if self.fresh_blood:
|
|
||||||
emojis += "⭐️"
|
|
||||||
return f"[b]{self.tier} {self.rank}[/b] ({self.points} LP){' ' if emojis else ''}{emojis}"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<{self.__class__.__qualname__} {self}>"
|
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
|
||||||
if other is None:
|
|
||||||
return False
|
|
||||||
if not isinstance(other, LeagueLeague):
|
|
||||||
raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}")
|
|
||||||
equal = True
|
|
||||||
if other.veteran:
|
|
||||||
equal &= self.veteran == other.veteran
|
|
||||||
if other.fresh_blood:
|
|
||||||
equal &= self.fresh_blood == other.fresh_blood
|
|
||||||
if other.hot_streak:
|
|
||||||
equal &= self.hot_streak == other.hot_streak
|
|
||||||
if other.inactive:
|
|
||||||
equal &= self.inactive == other.inactive
|
|
||||||
if other.losses:
|
|
||||||
equal &= self.losses == other.losses
|
|
||||||
if other.wins:
|
|
||||||
equal &= self.wins == other.wins
|
|
||||||
if other.points:
|
|
||||||
equal &= self.points == other.points
|
|
||||||
if other.rank:
|
|
||||||
equal &= self.rank == other.rank
|
|
||||||
if other.tier:
|
|
||||||
equal &= self.tier == other.tier
|
|
||||||
return equal
|
|
||||||
|
|
||||||
def __ne__(self, other) -> bool:
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __gt__(self, other) -> bool:
|
|
||||||
if other is None:
|
|
||||||
return True
|
|
||||||
if not isinstance(other, LeagueLeague):
|
|
||||||
raise TypeError(f"Can't compare {self.__class__.__qualname__} with {other.__class__.__qualname__}")
|
|
||||||
if not (bool(self) and bool(other)):
|
|
||||||
raise ValueError("Can't compare partial LeagueLeagues.")
|
|
||||||
if self.tier != other.tier:
|
|
||||||
# Silver is better than Bronze
|
|
||||||
return self.tier > other.tier
|
|
||||||
elif self.rank != other.rank:
|
|
||||||
# Silver I is better than Silver IV
|
|
||||||
return self.rank > other.rank
|
|
||||||
elif self.points != other.points:
|
|
||||||
# Silver I (100 LP) is better than Silver I (0 LP)
|
|
||||||
return self.points > other.points
|
|
||||||
elif self.winrate != other.winrate:
|
|
||||||
# Silver I (100 LP with 60% winrate) is better than Silver I (100 LP with 40% winrate)
|
|
||||||
return self.winrate > other.winrate
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
result = True
|
|
||||||
result &= self.veteran is not None
|
|
||||||
result &= self.fresh_blood is not None
|
|
||||||
result &= self.hot_streak is not None
|
|
||||||
result &= self.inactive is not None
|
|
||||||
result &= self.losses is not None
|
|
||||||
result &= self.wins is not None
|
|
||||||
result &= self.points is not None
|
|
||||||
result &= self.rank is not None
|
|
||||||
result &= self.tier is not None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __composite_values__(self):
|
|
||||||
return self.tier, \
|
|
||||||
self.rank, \
|
|
||||||
self.points, \
|
|
||||||
self.wins, \
|
|
||||||
self.losses, \
|
|
||||||
self.inactive, \
|
|
||||||
self.hot_streak, \
|
|
||||||
self.fresh_blood, \
|
|
||||||
self.veteran
|
|
||||||
|
|
||||||
@property
|
|
||||||
def played(self):
|
|
||||||
return self.wins + self.losses
|
|
||||||
|
|
||||||
@property
|
|
||||||
def winrate(self):
|
|
||||||
return self.wins / self.played
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, d: dict):
|
|
||||||
return cls(
|
|
||||||
tier=LeagueTier.from_string(d["tier"]),
|
|
||||||
rank=LeagueRank.from_string(d["rank"]),
|
|
||||||
points=d["leaguePoints"],
|
|
||||||
wins=d["wins"],
|
|
||||||
losses=d["losses"],
|
|
||||||
inactive=d["inactive"],
|
|
||||||
hot_streak=d["hotStreak"],
|
|
||||||
fresh_blood=d["freshBlood"],
|
|
||||||
veteran=d["veteran"],
|
|
||||||
)
|
|
|
@ -1,21 +0,0 @@
|
||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
class LeagueRank(enum.Enum):
|
|
||||||
I = 1
|
|
||||||
II = 2
|
|
||||||
III = 3
|
|
||||||
IV = 4
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__class__.__qualname__}.{self.name}"
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.value < other.value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_string(cls, string: str):
|
|
||||||
return cls.__members__.get(string)
|
|
|
@ -1,26 +0,0 @@
|
||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
class LeagueTier(enum.Enum):
|
|
||||||
IRON = 0
|
|
||||||
BRONZE = 1
|
|
||||||
SILVER = 2
|
|
||||||
GOLD = 3
|
|
||||||
PLATINUM = 4
|
|
||||||
DIAMOND = 5
|
|
||||||
MASTER = 6
|
|
||||||
GRANDMASTER = 7
|
|
||||||
CHALLENGER = 8
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name.capitalize()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"{self.__class__.__qualname__}.{self.name}"
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.value > other.value
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_string(cls, string: str):
|
|
||||||
return cls.__members__.get(string)
|
|
|
@ -1,12 +0,0 @@
|
||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
class MMChoice(enum.Enum):
|
|
||||||
YES = "🔵"
|
|
||||||
MAYBE = "❔"
|
|
||||||
LATE_SHORT = "🕐"
|
|
||||||
LATE_MEDIUM = "🕒"
|
|
||||||
LATE_LONG = "🕗"
|
|
||||||
NO_TIME = "🔴"
|
|
||||||
NO_INTEREST = "❌"
|
|
||||||
NO_TECH = "❗️"
|
|
|
@ -1,10 +0,0 @@
|
||||||
class MMInterfaceData:
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MMInterfaceDataTelegram(MMInterfaceData):
|
|
||||||
def __init__(self, chat_id: int, message_id: int):
|
|
||||||
super().__init__()
|
|
||||||
self.chat_id = chat_id
|
|
||||||
self.message_id = message_id
|
|
|
@ -1,16 +0,0 @@
|
||||||
# This is a template Pack __init__. You can use this without changing anything in other packages too!
|
|
||||||
|
|
||||||
from . import commands, tables, stars
|
|
||||||
from .commands import available_commands
|
|
||||||
from .tables import available_tables
|
|
||||||
from .stars import available_page_stars, available_exception_stars
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"commands",
|
|
||||||
"tables",
|
|
||||||
"stars",
|
|
||||||
"available_commands",
|
|
||||||
"available_tables",
|
|
||||||
"available_page_stars",
|
|
||||||
"available_exception_stars",
|
|
||||||
]
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Imports go here!
|
|
||||||
from .roll import RollCommand
|
|
||||||
from .dice import DiceCommand
|
|
||||||
from .dndactive import DndactiveCommand
|
|
||||||
from .dndinfo import DndinfoCommand
|
|
||||||
from .dndnew import DndnewCommand
|
|
||||||
from .dndedit import DndeditCommand
|
|
||||||
from .dndroll import DndrollCommand
|
|
||||||
from .dnditem import DnditemCommand
|
|
||||||
from .dndspell import DndspellCommand
|
|
||||||
|
|
||||||
# Enter the commands of your Pack here!
|
|
||||||
available_commands = [
|
|
||||||
RollCommand,
|
|
||||||
DiceCommand,
|
|
||||||
DndactiveCommand,
|
|
||||||
DndinfoCommand,
|
|
||||||
DndnewCommand,
|
|
||||||
DndeditCommand,
|
|
||||||
DndrollCommand,
|
|
||||||
DnditemCommand,
|
|
||||||
DndspellCommand,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
|
||||||
__all__ = [command.__name__ for command in available_commands]
|
|
|
@ -1,40 +0,0 @@
|
||||||
import dice
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class DiceCommand(Command):
|
|
||||||
name: str = "dice"
|
|
||||||
|
|
||||||
description: str = "Roll a dice, using 'dice'."
|
|
||||||
|
|
||||||
syntax = "{dice}"
|
|
||||||
|
|
||||||
aliases = ["d"]
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
dice_str = args.joined(require_at_least=1)
|
|
||||||
try:
|
|
||||||
roll = dice.roll(dice_str)
|
|
||||||
except dice.DiceFatalException as e:
|
|
||||||
raise CommandError(e.msg)
|
|
||||||
except dice.DiceException as e:
|
|
||||||
raise CommandError(e.msg)
|
|
||||||
except dice.DiceBaseException as e:
|
|
||||||
raise CommandError(str(e))
|
|
||||||
try:
|
|
||||||
result = list(roll)
|
|
||||||
except TypeError:
|
|
||||||
result = [roll]
|
|
||||||
message = f"🎲 {dice_str}"
|
|
||||||
total = 0
|
|
||||||
if len(result) > 1:
|
|
||||||
message += f" = "
|
|
||||||
for index, die in enumerate(result):
|
|
||||||
message += f"{die}"
|
|
||||||
total += int(die)
|
|
||||||
if (index + 1) < len(result):
|
|
||||||
message += "+"
|
|
||||||
else:
|
|
||||||
total += int(result[0])
|
|
||||||
message += f" = [b]{total}[/b]"
|
|
||||||
await data.reply(message)
|
|
|
@ -1,56 +0,0 @@
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from ..tables import DndCharacter, DndActiveCharacter
|
|
||||||
|
|
||||||
|
|
||||||
class DndactiveCommand(Command):
|
|
||||||
name: str = "dndactive"
|
|
||||||
|
|
||||||
description: str = "Set a DnD character as active."
|
|
||||||
|
|
||||||
aliases = ["da", "dnda", "active", "dactive"]
|
|
||||||
|
|
||||||
syntax = "{name|id}"
|
|
||||||
|
|
||||||
tables = {DndCharacter, DndActiveCharacter}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
identifier = args.optional(0)
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
if identifier is None:
|
|
||||||
# Display the active character
|
|
||||||
if author.dnd_active_character is None:
|
|
||||||
await data.reply("ℹ️ You have no active characters.")
|
|
||||||
else:
|
|
||||||
await data.reply(f"ℹ️ You currently active character is [b]{author.dnd_active_character}[/b].")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
identifier = int(identifier)
|
|
||||||
except ValueError:
|
|
||||||
# Find the character by name
|
|
||||||
chars = await asyncify(data.session.query(self.alchemy.DndCharacter).filter_by(name=identifier).all)
|
|
||||||
if len(chars) >= 2:
|
|
||||||
char_string = "\n".join([f"[c]{char.character_id}[/c] (LV {char.level}) by {char.creator})" for char in chars])
|
|
||||||
raise CommandError(f"Multiple characters share the name {identifier}, "
|
|
||||||
f"please activate them using their id:\n{char_string}")
|
|
||||||
elif len(chars) == 1:
|
|
||||||
char = chars[0]
|
|
||||||
else:
|
|
||||||
char = None
|
|
||||||
else:
|
|
||||||
# Find the character by id
|
|
||||||
char = await asyncify(data.session.query(self.alchemy.DndCharacter)
|
|
||||||
.filter_by(character_id=identifier)
|
|
||||||
.one_or_none)
|
|
||||||
if char is None:
|
|
||||||
raise CommandError("No character found.")
|
|
||||||
# Check if the player already has an active character
|
|
||||||
if author.dnd_active_character is None:
|
|
||||||
# Create a new active character
|
|
||||||
achar = self.alchemy.DndActiveCharacter(character=char, user=author)
|
|
||||||
data.session.add(achar)
|
|
||||||
else:
|
|
||||||
# Change the active character
|
|
||||||
author.dnd_active_character.character = char
|
|
||||||
await data.session_commit()
|
|
||||||
await data.reply(f"✅ Active character set to [b]{char}[/b]!")
|
|
|
@ -1,35 +0,0 @@
|
||||||
import re
|
|
||||||
from royalnet.commands import *
|
|
||||||
from .dndnew import DndnewCommand
|
|
||||||
from ..tables import DndCharacter, DndActiveCharacter
|
|
||||||
|
|
||||||
|
|
||||||
class DndeditCommand(DndnewCommand):
|
|
||||||
name: str = "dndedit"
|
|
||||||
|
|
||||||
description: str = "Edit the active DnD character."
|
|
||||||
|
|
||||||
aliases = ["de", "dnde", "edit", "dedit"]
|
|
||||||
|
|
||||||
tables = {DndCharacter, DndActiveCharacter}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
character_sheet = args.joined()
|
|
||||||
|
|
||||||
if character_sheet == "":
|
|
||||||
await data.reply(self._syntax())
|
|
||||||
return
|
|
||||||
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
if author.dnd_active_character is None:
|
|
||||||
raise CommandError("You don't have an active character.")
|
|
||||||
|
|
||||||
char: DndCharacter = author.dnd_active_character.character
|
|
||||||
|
|
||||||
arguments = self._parse(character_sheet)
|
|
||||||
for key in arguments:
|
|
||||||
char.__setattr__(key, arguments[key])
|
|
||||||
|
|
||||||
await data.session_commit()
|
|
||||||
|
|
||||||
await data.reply(f"✅ Edit successful!")
|
|
|
@ -1,19 +0,0 @@
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import asyncify
|
|
||||||
from ..tables import DndCharacter, DndActiveCharacter
|
|
||||||
|
|
||||||
|
|
||||||
class DndinfoCommand(Command):
|
|
||||||
name: str = "dndinfo"
|
|
||||||
|
|
||||||
description: str = "Display the character sheet of the active DnD character."
|
|
||||||
|
|
||||||
aliases = ["di", "dndi", "info", "dinfo"]
|
|
||||||
|
|
||||||
tables = {DndCharacter, DndActiveCharacter}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
if author.dnd_active_character is None:
|
|
||||||
raise CommandError("You don't have an active character.")
|
|
||||||
await data.reply(author.dnd_active_character.character.character_sheet())
|
|
|
@ -1,56 +0,0 @@
|
||||||
import aiohttp
|
|
||||||
import sortedcontainers
|
|
||||||
from royalnet.commands import *
|
|
||||||
from ..utils import parse_5etools_entry
|
|
||||||
|
|
||||||
|
|
||||||
class DnditemCommand(Command):
|
|
||||||
name: str = "dnditem"
|
|
||||||
|
|
||||||
aliases = ["item"]
|
|
||||||
|
|
||||||
description: str = "Ottieni informazioni su un oggetto di D&D5e."
|
|
||||||
|
|
||||||
syntax = "{nomeoggetto}"
|
|
||||||
|
|
||||||
_dnddata: sortedcontainers.SortedKeyList = None
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
interface.loop.create_task(self._fetch_dnddata())
|
|
||||||
|
|
||||||
async def _fetch_dnddata(self):
|
|
||||||
self._dnddata = self._dnddata = sortedcontainers.SortedKeyList([], key=lambda i: i["name"].lower())
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get("https://5e.tools/data/items.json") as response:
|
|
||||||
j = await response.json()
|
|
||||||
for item in j["item"]:
|
|
||||||
self._dnddata.add(item)
|
|
||||||
async with session.get("https://5e.tools/data/fluff-items.json") as response:
|
|
||||||
j = await response.json()
|
|
||||||
for item in j["item"]:
|
|
||||||
self._dnddata.add(item)
|
|
||||||
async with session.get("https://5e.tools/data/items-base.json") as response:
|
|
||||||
j = await response.json()
|
|
||||||
for item in j["baseitem"]:
|
|
||||||
self._dnddata.add(item)
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self._dnddata is None:
|
|
||||||
await data.reply("⚠️ Il database degli oggetti di D&D non è ancora stato scaricato.")
|
|
||||||
return
|
|
||||||
search = args.joined().lower()
|
|
||||||
result = self._dnddata[self._dnddata.bisect_key_left(search)]
|
|
||||||
string = f'📦 [b]{result["name"]}[/b]\n'
|
|
||||||
if "source" in result:
|
|
||||||
string += f'[i]{result["source"]}, page {result["page"]}[/i]\n'
|
|
||||||
string += f'\n' \
|
|
||||||
f'Type: [b]{result.get("type", "None")}[/b]\n' \
|
|
||||||
f'Value: [b]{result.get("value", "-")}[/b]\n' \
|
|
||||||
f'Weight: [b]{result.get("weight", "0")} lb[/b]\n' \
|
|
||||||
f'Rarity: [b]{result["rarity"] if result.get("rarity", "None") != "None" else "Mundane"}[/b]\n' \
|
|
||||||
f'\n'
|
|
||||||
for entry in result.get("entries", []):
|
|
||||||
string += parse_5etools_entry(entry)
|
|
||||||
string += "\n\n"
|
|
||||||
await data.reply(string)
|
|
|
@ -1,71 +0,0 @@
|
||||||
import re
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
from royalnet.commands import *
|
|
||||||
from ..tables import DndCharacter
|
|
||||||
from ..utils import DndProficiencyType
|
|
||||||
|
|
||||||
|
|
||||||
class DndnewCommand(Command):
|
|
||||||
name: str = "dndnew"
|
|
||||||
|
|
||||||
description: str = "Create a new DnD character."
|
|
||||||
|
|
||||||
aliases = ["dn", "dndn", "new", "dnew"]
|
|
||||||
|
|
||||||
syntax = "{name}\n{character_sheet}"
|
|
||||||
|
|
||||||
tables = {DndCharacter}
|
|
||||||
|
|
||||||
def _search_value(self, name: str, string: str):
|
|
||||||
return re.search(r"\s*" + name + r"\s*([0-9]+)\s*", string, re.IGNORECASE)
|
|
||||||
|
|
||||||
def _parse(self, character_sheet: str) -> dict:
|
|
||||||
columns = list(self.alchemy.DndCharacter.__table__.columns)
|
|
||||||
column_names = [column.name for column in columns if (not column.primary_key and
|
|
||||||
not column.foreign_keys and
|
|
||||||
column.name != "name")]
|
|
||||||
arguments = {}
|
|
||||||
for column_name in column_names:
|
|
||||||
match = self._search_value(column_name, character_sheet)
|
|
||||||
if match:
|
|
||||||
if column_name.endswith("_proficiency"):
|
|
||||||
arguments[column_name] = DndProficiencyType(float(match.group(1)))
|
|
||||||
else:
|
|
||||||
arguments[column_name] = match.group(1)
|
|
||||||
return arguments
|
|
||||||
|
|
||||||
def _syntax(self) -> str:
|
|
||||||
columns = list(self.alchemy.DndCharacter.__table__.columns)
|
|
||||||
column_names = [column.name for column in columns if (not column.primary_key and
|
|
||||||
not column.foreign_keys and
|
|
||||||
column.name != "name")]
|
|
||||||
message = "ℹ️ How to create a new character:\n[p]/dndnew YOUR_CHARACTER_NAME\n"
|
|
||||||
for column_name in column_names:
|
|
||||||
message += f"{column_name} _\n"
|
|
||||||
message += "[/p]"
|
|
||||||
return message
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
character_sheet = args.joined()
|
|
||||||
|
|
||||||
if character_sheet == "":
|
|
||||||
await data.reply(self._syntax())
|
|
||||||
return
|
|
||||||
|
|
||||||
creator = await data.get_author()
|
|
||||||
|
|
||||||
name, rest = character_sheet.split("\n", 1)
|
|
||||||
|
|
||||||
character = self.alchemy.DndCharacter(name=name, creator=creator, **self._parse(rest))
|
|
||||||
data.session.add(character)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await data.session_commit()
|
|
||||||
except Exception as err:
|
|
||||||
# THIS IS INTENDED
|
|
||||||
if err.__class__.__name__ == "IntegrityError":
|
|
||||||
param_name = re.search(r'in column "(\S+)"', err.args[0]).group(1)
|
|
||||||
raise CommandError(f"Mandatory parameter '{param_name}' is missing.")
|
|
||||||
raise
|
|
||||||
|
|
||||||
await data.reply(f"✅ Character [b]{character.name}[/b] created!")
|
|
|
@ -1,146 +0,0 @@
|
||||||
import re
|
|
||||||
import random
|
|
||||||
from royalnet.commands import *
|
|
||||||
from ..tables import DndCharacter, DndActiveCharacter
|
|
||||||
from royalnet.utils import plusformat
|
|
||||||
|
|
||||||
|
|
||||||
class DndrollCommand(Command):
|
|
||||||
name: str = "dndroll"
|
|
||||||
|
|
||||||
description: str = "Roll dice as the active DnD character."
|
|
||||||
|
|
||||||
aliases = ["dr", "dndr", "roll", "droll"]
|
|
||||||
|
|
||||||
tables = {DndCharacter, DndActiveCharacter}
|
|
||||||
|
|
||||||
_skill_names = {
|
|
||||||
"str": "strength",
|
|
||||||
"for": "strength",
|
|
||||||
"dex": "dexterity",
|
|
||||||
"des": "dexterity",
|
|
||||||
"con": "constitution",
|
|
||||||
"cos": "constitution",
|
|
||||||
"inte": "intelligence",
|
|
||||||
"wis": "wisdom",
|
|
||||||
"sag": "wisdom",
|
|
||||||
"cha": "charisma",
|
|
||||||
"car": "charisma",
|
|
||||||
|
|
||||||
"ststr": "strength_save",
|
|
||||||
"stfor": "strength_save",
|
|
||||||
"stdex": "dexterity_save",
|
|
||||||
"stdes": "dexterity_save",
|
|
||||||
"stcon": "constitution_save",
|
|
||||||
"stcos": "constitution_save",
|
|
||||||
"stint": "intelligence_save",
|
|
||||||
"stwis": "wisdom_save",
|
|
||||||
"stsag": "wisdom_save",
|
|
||||||
"stcha": "charisma_save",
|
|
||||||
"stcar": "charisma_save",
|
|
||||||
|
|
||||||
"tsstr": "strength_save",
|
|
||||||
"tsfor": "strength_save",
|
|
||||||
"tsdex": "dexterity_save",
|
|
||||||
"tsdes": "dexterity_save",
|
|
||||||
"tscon": "constitution_save",
|
|
||||||
"tscos": "constitution_save",
|
|
||||||
"tsint": "intelligence_save",
|
|
||||||
"tswis": "wisdom_save",
|
|
||||||
"tssag": "wisdom_save",
|
|
||||||
"tscha": "charisma_save",
|
|
||||||
"tscar": "charisma_save",
|
|
||||||
|
|
||||||
"acr": "acrobatics",
|
|
||||||
"add": "animal_handling",
|
|
||||||
"ani": "animal_handling",
|
|
||||||
"arc": "arcana",
|
|
||||||
"ath": "athletics",
|
|
||||||
"dec": "deception",
|
|
||||||
"ing": "deception",
|
|
||||||
"his": "history",
|
|
||||||
"sto": "history",
|
|
||||||
"ins": "insight",
|
|
||||||
"intu": "insight",
|
|
||||||
"inti": "intimidation",
|
|
||||||
"inv": "investigation",
|
|
||||||
"med": "medicine",
|
|
||||||
"nat": "nature",
|
|
||||||
"perc": "perception",
|
|
||||||
"perf": "performance",
|
|
||||||
"pers": "persuasion",
|
|
||||||
"rel": "religion",
|
|
||||||
"sle": "sleight_of_hand",
|
|
||||||
"soh": "sleight_of_hand",
|
|
||||||
"rap": "sleight_of_hand",
|
|
||||||
"ste": "stealth",
|
|
||||||
"nas": "stealth",
|
|
||||||
"sur": "survival",
|
|
||||||
"sop": "sopravvivenza",
|
|
||||||
}
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
author = await data.get_author(error_if_none=True)
|
|
||||||
if author.dnd_active_character is None:
|
|
||||||
raise CommandError("You don't have an active character.")
|
|
||||||
char: DndCharacter = author.dnd_active_character.character
|
|
||||||
|
|
||||||
first = args[0]
|
|
||||||
second = args.optional(1)
|
|
||||||
third = args.optional(2)
|
|
||||||
|
|
||||||
advantage = False
|
|
||||||
disadvantage = False
|
|
||||||
extra_modifier = 0
|
|
||||||
|
|
||||||
if third:
|
|
||||||
try:
|
|
||||||
extra_modifier = int(third)
|
|
||||||
except ValueError:
|
|
||||||
raise InvalidInputError("Invalid modifier value (third parameter).")
|
|
||||||
if second.startswith("a") or second.startswith("v"):
|
|
||||||
advantage = True
|
|
||||||
elif second.startswith("d") or second.startswith("d"):
|
|
||||||
disadvantage = True
|
|
||||||
else:
|
|
||||||
raise InvalidInputError("Invalid advantage string (second parameter).")
|
|
||||||
|
|
||||||
elif second:
|
|
||||||
try:
|
|
||||||
extra_modifier = int(second)
|
|
||||||
except ValueError:
|
|
||||||
if second.startswith("a") or second.startswith("v"):
|
|
||||||
advantage = True
|
|
||||||
elif second.startswith("d") or second.startswith("d"):
|
|
||||||
disadvantage = True
|
|
||||||
else:
|
|
||||||
raise InvalidInputError("Invalid modifier value or advantage string (second parameter).")
|
|
||||||
|
|
||||||
skill_short_name = first.lower()
|
|
||||||
for root in self._skill_names:
|
|
||||||
if skill_short_name.startswith(root):
|
|
||||||
skill_name = self._skill_names[root]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise CommandError("Invalid skill name (first parameter).")
|
|
||||||
|
|
||||||
skill_modifier = char.__getattribute__(skill_name)
|
|
||||||
modifier = skill_modifier + extra_modifier
|
|
||||||
modifier_str = plusformat(modifier, empty_if_zero=True)
|
|
||||||
|
|
||||||
if advantage:
|
|
||||||
roll_a = random.randrange(1, 21)
|
|
||||||
roll_b = random.randrange(1, 21)
|
|
||||||
roll = max([roll_a, roll_b])
|
|
||||||
total = roll + modifier
|
|
||||||
await data.reply(f"🎲 2d20h1{modifier_str} = ({roll_a}|{roll_b}){modifier_str} = [b]{total}[/b]")
|
|
||||||
elif disadvantage:
|
|
||||||
roll_a = random.randrange(1, 21)
|
|
||||||
roll_b = random.randrange(1, 21)
|
|
||||||
roll = min([roll_a, roll_b])
|
|
||||||
total = roll + modifier
|
|
||||||
await data.reply(f"🎲 2d20l1{modifier_str} = ({roll_a}|{roll_b}){modifier_str} = [b]{total}[/b]")
|
|
||||||
else:
|
|
||||||
roll = random.randrange(1, 21)
|
|
||||||
total = roll + modifier
|
|
||||||
await data.reply(f"🎲 1d20{modifier_str} = {roll}{modifier_str} = [b]{total}[/b]")
|
|
|
@ -1,114 +0,0 @@
|
||||||
import aiohttp
|
|
||||||
import sortedcontainers
|
|
||||||
from royalnet.commands import *
|
|
||||||
from royalnet.utils import ordinalformat, andformat
|
|
||||||
from ..utils import parse_5etools_entry
|
|
||||||
|
|
||||||
|
|
||||||
class DndspellCommand(Command):
|
|
||||||
name: str = "dndspell"
|
|
||||||
|
|
||||||
aliases = ["spell"]
|
|
||||||
|
|
||||||
description: str = "Ottieni informazioni su una magia di D&D5e."
|
|
||||||
|
|
||||||
syntax = "{nomemagia}"
|
|
||||||
|
|
||||||
_dnddata: sortedcontainers.SortedKeyList = None
|
|
||||||
|
|
||||||
def __init__(self, interface: CommandInterface):
|
|
||||||
super().__init__(interface)
|
|
||||||
interface.loop.create_task(self._fetch_dnddata())
|
|
||||||
|
|
||||||
async def _fetch_dnddata(self):
|
|
||||||
self._dnddata = self._dnddata = sortedcontainers.SortedKeyList([], key=lambda i: i["name"].lower())
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
for url in [
|
|
||||||
"https://5e.tools/data/spells/spells-ai.json",
|
|
||||||
"https://5e.tools/data/spells/spells-ggr.json",
|
|
||||||
"https://5e.tools/data/spells/spells-llk.json",
|
|
||||||
"https://5e.tools/data/spells/spells-phb.json",
|
|
||||||
"https://5e.tools/data/spells/spells-scag.json",
|
|
||||||
"https://5e.tools/data/spells/spells-stream.json",
|
|
||||||
"https://5e.tools/data/spells/spells-ua-ar.json",
|
|
||||||
"https://5e.tools/data/spells/spells-ua-mm.json",
|
|
||||||
"https://5e.tools/data/spells/spells-ua-ss.json",
|
|
||||||
"https://5e.tools/data/spells/spells-ua-tobm.json",
|
|
||||||
"https://5e.tools/data/spells/spells-xge.json"
|
|
||||||
]:
|
|
||||||
async with session.get(url) as response:
|
|
||||||
j = await response.json()
|
|
||||||
for spell in j["spell"]:
|
|
||||||
self._dnddata.add(spell)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_spell(spell: dict) -> str:
|
|
||||||
string = f'✨ [b]{spell["name"]}[/b]\n'
|
|
||||||
if "source" in spell:
|
|
||||||
string += f'[i]{spell["source"]}, page {spell["page"]}[/i]\n'
|
|
||||||
string += "\n"
|
|
||||||
if spell["level"] == 0:
|
|
||||||
string += f'[b]Cantrip[/b] {spell["school"]}\n'
|
|
||||||
else:
|
|
||||||
string += f'[b]{ordinalformat(spell["level"])}[/b] level {spell["school"]}\n'
|
|
||||||
if "time" in spell:
|
|
||||||
for time in spell["time"]:
|
|
||||||
string += f'Cast time: ⌛️ [b]{time["number"]} {time["unit"]}[/b]\n'
|
|
||||||
if "range" in spell:
|
|
||||||
if spell["range"]["distance"]["type"] == "touch":
|
|
||||||
string += "Range: 👉 [b]Touch[/b]\n"
|
|
||||||
elif spell["range"]["distance"]["type"] == "self":
|
|
||||||
string += "Range: 👤 [b]Self[/b]\n"
|
|
||||||
else:
|
|
||||||
string += f'Range: 🏹 [b]{spell["range"]["distance"]["amount"]} {spell["range"]["distance"]["type"]}[/b] ({spell["range"]["type"]})\n'
|
|
||||||
if "components" in spell:
|
|
||||||
string += f'Components: '
|
|
||||||
if spell["components"].get("v", False):
|
|
||||||
string += "👄 [b]Verbal[/b] | "
|
|
||||||
if spell["components"].get("s", False):
|
|
||||||
string += "🤙 [b]Somatic[/b] | "
|
|
||||||
if spell["components"].get("r", False):
|
|
||||||
# TODO: wtf is this
|
|
||||||
string += "❓ [b]R...?[/b] | "
|
|
||||||
if spell["components"].get("m", False):
|
|
||||||
if "text" in spell["components"]["m"]:
|
|
||||||
string += f'💎 [b]Material[/b] ([i]{spell["components"]["m"]["text"]}[/i]) | '
|
|
||||||
else:
|
|
||||||
string += f'💎 [b]Material[/b] ([i]{spell["components"]["m"]}[/i]) | '
|
|
||||||
string = string.rstrip(" ").rstrip("|")
|
|
||||||
string += "\n"
|
|
||||||
string += "\n"
|
|
||||||
if "duration" in spell:
|
|
||||||
for duration in spell["duration"]:
|
|
||||||
if duration["type"] == "timed":
|
|
||||||
string += f'Duration: 🕒 [b]{duration["duration"]["amount"]} {duration["duration"]["type"]}[/b]'
|
|
||||||
elif duration["type"] == "instant":
|
|
||||||
string += 'Duration: ☁️ [b]Instantaneous[/b]'
|
|
||||||
elif duration["type"] == "special":
|
|
||||||
string += 'Duration: ⭐️ [b]Special[/b]'
|
|
||||||
elif duration["type"] == "permanent":
|
|
||||||
string += f"Duration: ♾ [b]Permanent[/b] (ends on {andformat(duration['ends'], final=' or ')})"
|
|
||||||
else:
|
|
||||||
string += f'Duration: ⚠️[b]UNKNOWN[/b]'
|
|
||||||
if duration.get("concentration", False):
|
|
||||||
string += " (requires concentration)"
|
|
||||||
string += "\n"
|
|
||||||
if "meta" in spell:
|
|
||||||
if spell["meta"].get("ritual", False):
|
|
||||||
string += "🔮 Can be casted as ritual\n"
|
|
||||||
string += "\n"
|
|
||||||
for entry in spell.get("entries", []):
|
|
||||||
string += parse_5etools_entry(entry)
|
|
||||||
string += "\n\n"
|
|
||||||
for entry in spell.get("entriesHigherLevel", []):
|
|
||||||
string += parse_5etools_entry(entry)
|
|
||||||
string += "\n\n"
|
|
||||||
return string
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
if self._dnddata is None:
|
|
||||||
await data.reply("⚠️ Il database degli oggetti di D&D non è ancora stato scaricato.")
|
|
||||||
return
|
|
||||||
search = args.joined().lower()
|
|
||||||
result = self._dnddata[self._dnddata.bisect_key_left(search)]
|
|
||||||
await data.reply(self._parse_spell(result))
|
|
|
@ -1,28 +0,0 @@
|
||||||
import typing
|
|
||||||
import random
|
|
||||||
from royalnet.commands import *
|
|
||||||
|
|
||||||
|
|
||||||
class RollCommand(Command):
|
|
||||||
name: str = "roll"
|
|
||||||
|
|
||||||
description: str = "Roll a dice, from N to M (defaults to 1-100)."
|
|
||||||
|
|
||||||
syntax = "[min] [max]"
|
|
||||||
|
|
||||||
aliases = ["r", "random"]
|
|
||||||
|
|
||||||
async def run(self, args: CommandArgs, data: CommandData) -> None:
|
|
||||||
first: typing.Optional[str] = args.optional(0)
|
|
||||||
second: typing.Optional[str] = args.optional(1)
|
|
||||||
if second:
|
|
||||||
minimum = int(first)
|
|
||||||
maximum = int(second)
|
|
||||||
elif first:
|
|
||||||
minimum = 1
|
|
||||||
maximum = int(first)
|
|
||||||
else:
|
|
||||||
minimum = 1
|
|
||||||
maximum = 100
|
|
||||||
result = random.randrange(minimum, maximum+1)
|
|
||||||
await data.reply(f"🎲 Dice roll [{minimum}-{maximum}]: [b]{result}[/b]")
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Imports go here!
|
|
||||||
|
|
||||||
|
|
||||||
# Enter the PageStars of your Pack here!
|
|
||||||
available_page_stars = [
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
# Enter the ExceptionStars of your Pack here!
|
|
||||||
available_exception_stars = [
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
|
||||||
__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]]
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Imports go here!
|
|
||||||
from .dndactivecharacters import DndActiveCharacter
|
|
||||||
from .dndcharacters import DndCharacter
|
|
||||||
|
|
||||||
# Enter the tables of your Pack here!
|
|
||||||
available_tables = [
|
|
||||||
DndActiveCharacter,
|
|
||||||
DndCharacter,
|
|
||||||
]
|
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
|
||||||
__all__ = [table.__name__ for table in available_tables]
|
|
|
@ -1,26 +0,0 @@
|
||||||
from sqlalchemy import *
|
|
||||||
from sqlalchemy.orm import *
|
|
||||||
from sqlalchemy.ext.declarative import *
|
|
||||||
|
|
||||||
|
|
||||||
class DndActiveCharacter:
|
|
||||||
__tablename__ = "dndactivecharacters"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def character_id(self):
|
|
||||||
return Column(Integer, ForeignKey("dndcharacters.character_id"), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"), primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def character(self):
|
|
||||||
return relationship("DndCharacter", foreign_keys=self.character_id, backref="activated_by")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def user(self):
|
|
||||||
return relationship("User", foreign_keys=self.user_id, backref=backref("dnd_active_character", uselist=False))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__qualname__} for {self.user_id}: {self.character_id}>"
|
|
|
@ -1,301 +0,0 @@
|
||||||
from sqlalchemy import *
|
|
||||||
from sqlalchemy.orm import *
|
|
||||||
from sqlalchemy.ext.declarative import *
|
|
||||||
from ..utils import DndProficiencyType
|
|
||||||
|
|
||||||
|
|
||||||
class DndCharacter:
|
|
||||||
__tablename__ = "dndcharacters"
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def character_id(self):
|
|
||||||
return Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator_id(self):
|
|
||||||
return Column(Integer, ForeignKey("users.uid"))
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def creator(self):
|
|
||||||
return relationship("User", foreign_keys=self.creator_id, backref="dndcharacters_created")
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def name(self):
|
|
||||||
return Column(String, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def strength_score(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def strength(self):
|
|
||||||
return (self.strength_score - 10) // 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def dexterity_score(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dexterity(self):
|
|
||||||
return (self.dexterity_score - 10) // 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def constitution_score(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def constitution(self):
|
|
||||||
return (self.constitution_score - 10) // 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def intelligence_score(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def intelligence(self):
|
|
||||||
return (self.intelligence_score - 10) // 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def wisdom_score(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def wisdom(self):
|
|
||||||
return (self.wisdom_score - 10) // 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def charisma_score(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def charisma(self):
|
|
||||||
return (self.charisma_score - 10) // 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def level(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def proficiency_bonus(self):
|
|
||||||
return ((self.level - 1) // 4) + 2
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def current_hp(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def max_hp(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def armor_class(self):
|
|
||||||
return Column(Integer, nullable=False)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def strength_save_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def dexterity_save_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def constitution_save_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def intelligence_save_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def wisdom_save_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def charisma_save_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def acrobatics_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def animal_handling_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def arcana_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def athletics_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def deception_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def history_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def insight_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def intimidation_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def investigation_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def medicine_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def nature_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def perception_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def performance_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def persuasion_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def religion_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def sleight_of_hand_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def stealth_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@declared_attr
|
|
||||||
def survival_proficiency(self):
|
|
||||||
return Column(Enum(DndProficiencyType), nullable=False, default=DndProficiencyType.NONE)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def strength_save(self):
|
|
||||||
return self.strength + self.proficiency_bonus * self.strength_save_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dexterity_save(self):
|
|
||||||
return self.dexterity + self.proficiency_bonus * self.dexterity_save_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def constitution_save(self):
|
|
||||||
return self.constitution + self.proficiency_bonus * self.constitution_save_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def intelligence_save(self):
|
|
||||||
return self.intelligence + self.proficiency_bonus * self.intelligence_save_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def wisdom_save(self):
|
|
||||||
return self.wisdom + self.proficiency_bonus * self.wisdom_save_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def charisma_save(self):
|
|
||||||
return self.charisma + self.proficiency_bonus * self.charisma_save_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def acrobatics(self):
|
|
||||||
return self.dexterity + self.proficiency_bonus * self.acrobatics_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def animal_handling(self):
|
|
||||||
return self.wisdom + self.proficiency_bonus * self.animal_handling_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def arcana(self):
|
|
||||||
return self.intelligence + self.proficiency_bonus * self.arcana_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def athletics(self):
|
|
||||||
return self.strength + self.proficiency_bonus * self.athletics_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def deception(self):
|
|
||||||
return self.charisma + self.proficiency_bonus * self.deception_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def history(self):
|
|
||||||
return self.intelligence + self.proficiency_bonus * self.history_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def insight(self):
|
|
||||||
return self.wisdom + self.proficiency_bonus * self.insight_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def intimidation(self):
|
|
||||||
return self.charisma + self.proficiency_bonus * self.intimidation_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def investigation(self):
|
|
||||||
return self.intelligence + self.proficiency_bonus * self.investigation_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def medicine(self):
|
|
||||||
return self.wisdom + self.proficiency_bonus * self.medicine_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nature(self):
|
|
||||||
return self.intelligence + self.proficiency_bonus * self.nature_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def perception(self):
|
|
||||||
return self.wisdom + self.proficiency_bonus * self.perception_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def performance(self):
|
|
||||||
return self.charisma + self.proficiency_bonus * self.performance_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def persuasion(self):
|
|
||||||
return self.charisma + self.proficiency_bonus * self.persuasion_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def religion(self):
|
|
||||||
return self.intelligence + self.proficiency_bonus * self.religion_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sleight_of_hand(self):
|
|
||||||
return self.dexterity + self.proficiency_bonus * self.sleight_of_hand_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stealth(self):
|
|
||||||
return self.dexterity + self.proficiency_bonus * self.stealth_proficiency.value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def survival(self):
|
|
||||||
return self.wisdom + self.proficiency_bonus * self.survival_proficiency.value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__qualname__} {self.name}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name}"
|
|
||||||
|
|
||||||
def character_sheet(self) -> str:
|
|
||||||
columns = list(self.__class__.__table__.columns)
|
|
||||||
column_names = [column.name for column in columns if (not column.primary_key and
|
|
||||||
not column.foreign_keys and
|
|
||||||
column.name != "name")]
|
|
||||||
message = f"[b]{self.name}[/b]\n"
|
|
||||||
for column_name in column_names:
|
|
||||||
value = self.__getattribute__(column_name)
|
|
||||||
message += f"{column_name} {value}\n"
|
|
||||||
return message
|
|
|
@ -1,4 +0,0 @@
|
||||||
from .dndproficiencytype import DndProficiencyType
|
|
||||||
from .parse5etoolsentry import parse_5etools_entry
|
|
||||||
|
|
||||||
__all__ = ["DndProficiencyType", "parse_5etools_entry"]
|
|
|
@ -1,11 +0,0 @@
|
||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
class DndProficiencyType(enum.Enum):
|
|
||||||
NONE = 0
|
|
||||||
HALF_PROFICIENCY = 0.5
|
|
||||||
FULL_PROFICIENCY = 1
|
|
||||||
EXPERTISE = 2
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.value)
|
|
|
@ -1,31 +0,0 @@
|
||||||
def parse_5etools_entry(entry) -> str:
|
|
||||||
if isinstance(entry, str):
|
|
||||||
return entry
|
|
||||||
elif isinstance(entry, dict):
|
|
||||||
string = ""
|
|
||||||
if entry["type"] == "entries":
|
|
||||||
string += f'[b]{entry.get("name", "")}[/b]\n'
|
|
||||||
for subentry in entry["entries"]:
|
|
||||||
string += parse_5etools_entry(subentry)
|
|
||||||
string += "\n\n"
|
|
||||||
elif entry["type"] == "table":
|
|
||||||
string += "[i][table hidden][/i]"
|
|
||||||
# for label in entry["colLabels"]:
|
|
||||||
# string += f"| {label} "
|
|
||||||
# string += "|"
|
|
||||||
# for row in entry["rows"]:
|
|
||||||
# for column in row:
|
|
||||||
# string += f"| {self._parse_entry(column)} "
|
|
||||||
# string += "|\n"
|
|
||||||
elif entry["type"] == "cell":
|
|
||||||
return parse_5etools_entry(entry["entry"])
|
|
||||||
elif entry["type"] == "list":
|
|
||||||
string = ""
|
|
||||||
for item in entry["items"]:
|
|
||||||
string += f"- {parse_5etools_entry(item)}\n"
|
|
||||||
string.rstrip("\n")
|
|
||||||
else:
|
|
||||||
string += "[i]⚠️ [unknown type][/i]"
|
|
||||||
else:
|
|
||||||
return "[/i]⚠️ [unknown data][/i]"
|
|
||||||
return string
|
|
|
@ -1,4 +1,4 @@
|
||||||
semantic = "5.0a91"
|
semantic = "5.0a93"
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print(semantic)
|
print(semantic)
|
||||||
|
|
Loading…
Reference in a new issue