1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-27 13:34:28 +00:00

Merge branch 'master' of github.com:Steffo99/royalpack into master

This commit is contained in:
Steffo 2020-07-30 23:16:28 +02:00
commit 08582bfc82
132 changed files with 5594 additions and 1603 deletions

3
.gitignore vendored
View file

@ -1,7 +1,7 @@
# Royalnet ignores # Royalnet ignores
config*.toml config*.toml
downloads/ downloads/
markov/
# Python ignores # Python ignores
**/__pycache__/ **/__pycache__/
@ -11,4 +11,3 @@ dist/
# PyCharm ignores # PyCharm ignores
.idea/ .idea/

242
README.md
View file

@ -1,204 +1,84 @@
<!--This documentation was autogenerated with `python -m royalnet.generate -f markdown`.-->
# `royalpack` # `royalpack`
## Commands ## Configuration
### `ciaoruozi` ```toml
[Packs."royalpack"]
Saluta Ruozi, un leggendario essere che una volta era in User Games. # The main Telegram group
Telegram.main_group_id = -1001153723135
### `color` # The main Discord channel
Discord.main_channel_id = 566023556618518538
Invia un colore in chat...? # A Imgur API token (https://apidocs.imgur.com/?version=latest)
Imgur.token = "1234567890abcde"
### `cv` # A Steam Web API key (https://steamcommunity.com/dev/apikey)
Steam.web_api_key = "123567890ABCDEF123567890ABCDEF12"
Elenca le persone attualmente connesse alla chat vocale. # The Peertube instance you want to use for new video notifications
Peertube.instance_url = "https://pt.steffo.eu"
### `diario` # The delay in seconds between two new video checks
Peertube.feed_update_timeout = 300
Aggiungi una citazione al Diario. # The Funkwhale instance you want to use for the fw commands
Funkwhale.instance_url = "https://fw.steffo.eu"
### `rage` # The id of the role that users should have to be displayed by default in cv
Cv.displayed_role_id = 424549048561958912
Arrabbiati per qualcosa, come una software house californiana. # The max duration of a song downloaded with the play commands
Play.max_song_duration = 7230
> Aliases: `balurage` `madden` # The Telegram channel where matchmaking messages should be sent in
Matchmaking.mm_chat_id = -1001204402796
### `reminder` [Packs."royalpack"."steampowered"]
token = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
Ti ricorda di fare qualcosa dopo un po' di tempo. [Packs."royalpack"."steampowered".updater]
enabled = false
period = 86400
delay = 1
target = -1001153723135
> Aliases: `calendar` [Packs."royalpack"."dota".updater]
enabled = true
period = 86400
delay = 1
target = -1001153723135
### `ship` [Packs."royalpack"."brawlhalla"]
token = "1234567890ABCDEFGHJKLMNOPQRST"
Crea una ship tra due nomi. [Packs."royalpack"."brawlhalla".updater]
enabled = true
period = 86400
delay = 1
target = -1001153723135
### `smecds` [Packs."royalpack"."leagueoflegends"]
token = "RGAPI-AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"
region = "euw1"
Secondo me, è colpa dello stagista... [Packs."royalpack"."leagueoflegends".updater]
enabled = true
period = 86400
delay = 1
target = -1001153723135
> Aliases: `secondomeecolpadellostagista` [Packs."royalpack"."osu"]
client_id = 123456789
client_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
### `videochannel` [Packs."royalpack"."osu".login]
enabled = false
Converti il canale vocale in un canale video.
> Aliases: `golive` `live` `video`
### `pause`
Metti in pausa o riprendi la riproduzione di un file.
> Aliases: `resume`
### `play`
Aggiunge un url alla coda della chat vocale.
> Aliases: `p`
### `queue`
Visualizza la coda di riproduzione attuale..
> Aliases: `q`
### `skip`
Salta il file attualmente in riproduzione.
> Aliases: `s`
### `summon`
Evoca il bot in un canale vocale.
> Aliases: `cv`
### `youtube`
Cerca un video su YouTube e lo aggiunge alla coda della chat vocale.
> Aliases: `yt`
### `soundcloud`
Cerca un video su SoundCloud e lo aggiunge alla coda della chat vocale.
> Aliases: `sc`
### `emojify`
Converti un messaggio in emoji.
### `leagueoflegends`
Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche.
> Aliases: `lol` `league`
### `diarioquote`
Cita una riga del diario.
> Aliases: `dq` `quote` `dquote`
### `peertube`
Guarda quando è uscito l'ultimo video su RoyalTube.
### `googlevideo`
Cerca un video su Google Video e lo aggiunge alla coda della chat vocale.
> Aliases: `gv`
### `yahoovideo`
Cerca un video su Yahoo Video e lo aggiunge alla coda della chat vocale.
> Aliases: `yv`
### `userinfo`
Visualizza informazioni su un utente.
> Aliases: `uinfo` `ui` `useri`
### `spell`
Genera casualmente una spell!
### `ahnonlosoio`
Ah, non lo so io!
### `eat`
Mangia qualcosa!
### `pmots`
Confondi Proto!
## Events
### `discord_cv`
### `discord_summon`
### `discord_play`
### `discord_skip`
### `discord_queue`
### `discord_pause`
## Page Stars
### `/api/user/list`
### `/api/user/get/{uid_str}`
### `/api/diario/list`
### `/api/diario/get/{diario_id}`
## Exception Stars
## Tables
### `diario`
### `aliases`
### `wikipages`
Wiki page properties.
Warning:
Requires PostgreSQL!
### `wikirevisions`
A wiki page revision.
Warning:
Requires PostgreSQL!
### `bios`
### `reminder`
### `triviascores`
### `mmevents`
### `mmresponse`
### `leagueoflegends`
[Packs."royalpack"."osu".updater]
enabled = true
period = 86400
delay = 5
target = -1001153723135
```

764
poetry.lock generated

File diff suppressed because it is too large Load diff

2
publish.bat Normal file
View file

@ -0,0 +1,2 @@
git commit -am "publish: %1"
git push && poetry build && poetry publish && hub release create "%1" -m "Royalnet %1"

View file

@ -2,7 +2,7 @@
[tool.poetry] [tool.poetry]
name = "royalpack" name = "royalpack"
version = "5.1.9" version = "5.13.4"
description = "A Royalnet command pack for the Royal Games community" description = "A Royalnet command pack for the Royal Games community"
authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"] authors = ["Stefano Pigozzi <ste.pigozzi@gmail.com>"]
license = "AGPL-3.0+" license = "AGPL-3.0+"
@ -20,11 +20,14 @@
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
riotwatcher = "^2.7.1" riotwatcher = "^3.0.0"
royalspells = "^3.2" royalspells = "^3.2"
steam = "*"
sqlalchemy = "^1.3.18"
bcrypt = "^3.1.7"
[tool.poetry.dependencies.royalnet] [tool.poetry.dependencies.royalnet]
version = "^5.1.6" version = "~5.10.4"
# Maybe... there is a way to make these selectable? # Maybe... there is a way to make these selectable?
extras = [ extras = [
"telegram", "telegram",
@ -34,7 +37,7 @@
"constellation", "constellation",
"sentry", "sentry",
"herald", "herald",
"coloredlogs" "coloredlogs",
] ]
# Development dependencies # Development dependencies

View file

@ -1,21 +0,0 @@
# This is a template Pack __init__. You can use this without changing anything in other packages too!
from . import commands, tables, stars, events
from .commands import available_commands
from .tables import available_tables
from .stars import available_page_stars, available_exception_stars
from .events import available_events
from .version import semantic as __version__
__all__ = [
"commands",
"tables",
"stars",
"events",
"available_commands",
"available_tables",
"available_page_stars",
"available_exception_stars",
"available_events",
]

View file

@ -1,67 +1,87 @@
# Imports go here! # Imports go here!
from .ahnonlosoio import AhnonlosoioCommand
from .answer import AnswerCommand
from .brawlhalla import BrawlhallaCommand
from .cat import CatCommand
from .ciaoruozi import CiaoruoziCommand from .ciaoruozi import CiaoruoziCommand
from .color import ColorCommand from .color import ColorCommand
from .cv import CvCommand from .cv import CvCommand
from .cvstats import CvstatsCommand
from .diario import DiarioCommand 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 .pause import PauseCommand
from .play import PlayCommand
from .queue import QueueCommand
from .skip import SkipCommand
from .summon import SummonCommand
from .youtube import YoutubeCommand
from .soundcloud import SoundcloudCommand
from .emojify import EmojifyCommand
from .leagueoflegends import LeagueoflegendsCommand
from .diarioquote import DiarioquoteCommand from .diarioquote import DiarioquoteCommand
from .peertubeupdates import PeertubeUpdatesCommand from .diarioshuffle import DiarioshuffleCommand
from .googlevideo import GooglevideoCommand from .dota import DotaCommand
from .yahoovideo import YahoovideoCommand
from .userinfo import UserinfoCommand
from .spell import SpellCommand
from .ahnonlosoio import AhnonlosoioCommand
from .eat import EatCommand from .eat import EatCommand
from .pmots import PmotsCommand from .emojify import EmojifyCommand
from .peertube import PeertubeCommand
from .eval import EvalCommand from .eval import EvalCommand
from .exec import ExecCommand from .exec import ExecCommand
from .fortune import FortuneCommand
from .givefiorygi import GivefiorygiCommand
from .givetreasure import GivetreasureCommand
from .help import HelpCommand
from .leagueoflegends import LeagueoflegendsCommand
from .magickfiorygi import MagickfiorygiCommand
from .magicktreasure import MagicktreasureCommand
from .matchmaking import MatchmakingCommand
from .peertubeupdates import PeertubeUpdatesCommand
from .ping import PingCommand
from .pmots import PmotsCommand
from .dog import DogCommand
from .rage import RageCommand
from .reminder import ReminderCommand
from .royalpackversion import RoyalpackCommand
from .ship import ShipCommand
from .smecds import SmecdsCommand
from .spell import SpellCommand
from .steammatch import SteammatchCommand
from .steampowered import SteampoweredCommand
from .treasure import TreasureCommand
from .trivia import TriviaCommand
from .userinfo import UserinfoCommand
from .osu import OsuCommand
# Enter the commands of your Pack here! # Enter the commands of your Pack here!
available_commands = [ available_commands = [
AhnonlosoioCommand,
AnswerCommand,
BrawlhallaCommand,
CatCommand,
CiaoruoziCommand, CiaoruoziCommand,
ColorCommand, ColorCommand,
CvCommand, CvCommand,
CvstatsCommand,
DiarioCommand, DiarioCommand,
RageCommand,
ReminderCommand,
ShipCommand,
SmecdsCommand,
VideochannelCommand,
PauseCommand,
PlayCommand,
QueueCommand,
SkipCommand,
SummonCommand,
YoutubeCommand,
SoundcloudCommand,
EmojifyCommand,
LeagueoflegendsCommand,
DiarioquoteCommand, DiarioquoteCommand,
PeertubeUpdatesCommand, DiarioshuffleCommand,
GooglevideoCommand, DotaCommand,
YahoovideoCommand,
UserinfoCommand,
SpellCommand,
AhnonlosoioCommand,
EatCommand, EatCommand,
PmotsCommand, EmojifyCommand,
PeertubeCommand,
EvalCommand, EvalCommand,
ExecCommand, ExecCommand,
FortuneCommand,
GivefiorygiCommand,
GivetreasureCommand,
HelpCommand,
LeagueoflegendsCommand,
MagickfiorygiCommand,
MagicktreasureCommand,
MatchmakingCommand,
PeertubeUpdatesCommand,
PingCommand,
PmotsCommand,
DogCommand,
RageCommand,
ReminderCommand,
RoyalpackCommand,
ShipCommand,
SmecdsCommand,
SpellCommand,
SteammatchCommand,
SteampoweredCommand,
TreasureCommand,
TriviaCommand,
UserinfoCommand,
OsuCommand,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__

View file

@ -0,0 +1,202 @@
from typing import *
import royalnet.commands as rc
import royalnet.utils as ru
import royalnet.serf.telegram as rst
import royalnet.backpack.tables as rbt
import abc
import logging
import asyncio as aio
from ...types import Updatable
log = logging.getLogger(__name__)
class LinkerCommand(rc.Command, metaclass=abc.ABCMeta):
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface)
self.updater_task = None
if self.enabled():
self.updater_task = self.loop.create_task(self.run_updater())
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
author = await data.get_author(error_if_none=True)
if len(args) == 0:
message = []
for obj in await self.get_updatables_of_user(session=data.session, user=author):
async def change(attribute: str, value: Any):
"""A shortcut for self.__change."""
await self._change(session=data.session,
obj=obj,
attribute=attribute,
new=value)
await self.update(session=data.session, obj=obj, change=change)
message.append(self.describe(obj))
if len(message) == 0:
raise rc.UserError("Nessun account connesso.")
await data.session_commit()
await data.reply("\n".join(message))
else:
created = await self.create(session=data.session, user=author, args=args, data=data)
await data.session_commit()
if created is not None:
message = ["🔗 Account collegato!", "", self.describe(created)]
await data.reply("\n".join(message))
def describe(self, obj: Updatable) -> str:
"""The text that should be appended to the report message for a given Updatable."""
return str(obj)
@abc.abstractmethod
async def get_updatables_of_user(self, session, user: rbt.User) -> List[Updatable]:
"""Get the updatables of a specific user."""
...
@abc.abstractmethod
async def get_updatables(self, session) -> List[Updatable]:
"""Return a list of all objects that should be updated at this updater cycle."""
...
@abc.abstractmethod
async def create(self,
session,
user: rbt.User,
args: rc.CommandArgs,
data: Optional[rc.CommandData] = None) -> Optional[Updatable]:
"""Create a new updatable object for a user.
This function is responsible for adding the object to the session."""
...
@abc.abstractmethod
async def update(self, session, obj, change: Callable[[str, Any], Awaitable[None]]):
"""Update a single updatable object. Use the change method to change values on the object!"""
...
@abc.abstractmethod
async def on_increase(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None:
"""Called when the attribute has increased from the old value."""
...
@abc.abstractmethod
async def on_unchanged(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None:
"""Called when the attribute stayed the same as the old value."""
...
@abc.abstractmethod
async def on_decrease(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None:
"""Called when the attribute has decreased from the old value."""
...
@abc.abstractmethod
async def on_first(self, session, obj: Updatable, attribute: str, old: None, new: Any) -> None:
"""Called when the attribute changed from None."""
...
@abc.abstractmethod
async def on_reset(self, session, obj: Updatable, attribute: str, old: Any, new: None) -> None:
"""Called when the attribute changed to None."""
...
async def _change(self,
session,
obj,
attribute: str,
new) -> None:
"""Set the value of an attribute of an object to a value, and call the corresponding method."""
old = obj.__getattribute__(attribute)
if new == old:
await self.on_unchanged(session=session,
obj=obj,
attribute=attribute,
old=old,
new=new)
else:
if old is None:
await self.on_first(session=session,
obj=obj,
attribute=attribute,
old=old,
new=new)
elif new is None:
await self.on_reset(session=session,
obj=obj,
attribute=attribute,
old=old,
new=new)
elif new > old:
await self.on_increase(session=session,
obj=obj,
attribute=attribute,
old=old,
new=new)
else:
await self.on_decrease(session=session,
obj=obj,
attribute=attribute,
old=old,
new=new)
obj.__setattr__(attribute, new)
def enabled(self) -> bool:
"""Whether the updater is enabled or not."""
return self.config[self.name]["updater"]["enabled"] and self.interface.name == "telegram"
def period(self) -> int:
"""The time between two updater cycles."""
return self.config[self.name]["updater"]["period"]
def delay(self) -> int:
"""The time between two object updates."""
return self.config[self.name]["updater"]["delay"]
def target(self) -> int:
"""The id of the Telegram chat where notifications should be sent."""
return self.config[self.name]["updater"]["target"]
async def run_updater(self):
log.info(f"Starting updater: {self.name}")
while True:
log.debug(f"Updater cycle: {self.name}")
session = self.alchemy.Session()
objects = await self.get_updatables(session)
for obj in objects:
log.debug(f"Updating: {obj} ({self.name})")
async def change(attribute: str, value: Any):
"""A shortcut for self.__change."""
await self._change(session=session,
obj=obj,
attribute=attribute,
new=value)
try:
await self.update(session=session,
obj=obj,
change=change)
except Exception as e:
ru.sentry_exc(e)
delay = self.delay()
log.debug(f"Waiting for: {delay} seconds (delay)")
await aio.sleep(delay)
log.debug(f"Committing updates: {self.name}")
await ru.asyncify(session.commit)
session.close()
period = self.period()
log.debug(f"Waiting for: {period} seconds (period)")
await aio.sleep(period)
async def notify(self, message):
await self.serf.api_call(self.serf.client.send_message,
chat_id=self.target(),
text=rst.escape(message),
parse_mode="HTML",
disable_webpage_preview=True)

View file

@ -0,0 +1,82 @@
from typing import *
import royalnet
import royalnet.commands as rc
import random
import datetime
class AnswerCommand(rc.Command):
name: str = "answer"
description: str = "Fai una domanda al bot, che possa essere risposta con un sì o un no: lui ti risponderà!"
syntax: str = ""
_answers = [
#Cerchiamo di tenere bilanciate le tre colonne, o almeno le prime due.
#Se avete un'idea ma metterebbe troppe opzioni in un'unica categoria, mettetela sotto commento.
#risposte "sì"
"Sì.",
"Decisamente sì!",
"Uhm, secondo me sì.",
"Sì! Sì! SÌ!",
"Yup.",
"👍",
"Direi proprio di sì.",
"Assolutamente sì.",
"Ma certo!",
"✔️",
"👌",
"Esatto!",
"Senz'altro!",
"Ovviamente.",
"Questa domanda ha risposta affermativa.",
"Hell yeah.",
#risposte "no"
"No.",
"Decisamente no!",
"Uhm, secondo me sì.",
"No, no, e ancora NO!",
"Nope.",
"👎",
"Direi proprio di no.",
"Assolutamente no.",
"Certo che no!",
"✖️",
"🙅",
"Neanche per idea!",
"Neanche per sogno!",
"Niente affatto!",
"Questa domanda ha risposta negativa.",
"Hell no.",
#risposte "boh"
"Boh.",
"E io che ne so?!",
"Non so proprio rispondere",
"Non lo so",
"Mi rifiuto di rispondere alla domanda!",
"Non parlerò senza il mio avvocato!",
"Dunno.",
"Perché lo chiedi a me?",
"🤷 Ah, non lo so io! ¯\_(ツ)_/¯",
"🤷",
"¯\_(ツ)_/¯",
"No idea.",
"Dunno.",
"Boooooh!",
"Non ne ho la più pallida idea.",
]
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
h = hash(datetime.datetime.now())
r = random.Random(x=h)
message = r.sample(self._answers, 1)[0]
await data.reply(message)

View file

@ -0,0 +1,169 @@
from typing import *
import asyncio
import logging
import aiohttp
from royalnet.backpack import tables as rbt
import royalnet.commands as rc
import royalnet.utils as ru
from sqlalchemy import or_, and_
from .abstract.linker import LinkerCommand
from ..tables import Steam, Brawlhalla, BrawlhallaDuo
from ..types import BrawlhallaRank, BrawlhallaMetal, BrawlhallaTier, Updatable
log = logging.getLogger(__name__)
class BrawlhallaCommand(LinkerCommand):
name: str = "brawlhalla"
aliases = ["bh", "bruhalla", "bruhlalla"]
description: str = "Visualizza le tue statistiche di Brawlhalla."
syntax: str = ""
def token(self):
return self.config['brawlhalla']['token']
async def get_updatables_of_user(self, session, user: rbt.User) -> List[Brawlhalla]:
return user.steam
async def get_updatables(self, session) -> List[Brawlhalla]:
return await ru.asyncify(session.query(self.alchemy.get(Steam)).all)
async def create(self,
session,
user: rbt.User,
args: rc.CommandArgs,
data: Optional[rc.CommandData] = None) -> Optional[Brawlhalla]:
raise rc.InvalidInputError("Brawlhalla accounts are automatically linked from Steam.")
async def update(self, session, obj, change: Callable[[str, Any], Awaitable[None]]):
BrawlhallaT = self.alchemy.get(Brawlhalla)
DuoT = self.alchemy.get(BrawlhallaDuo)
log.info(f"Updating: {obj}")
async with aiohttp.ClientSession() as hcs:
bh: Brawlhalla = obj.brawlhalla
if bh is None:
log.debug(f"Checking if player has an account...")
async with hcs.get(f"https://api.brawlhalla.com/search?steamid={obj.steamid.as_64}&api_key={self.token()}") as response:
if response.status != 200:
raise rc.ExternalError(f"Brawlhalla API /search returned {response.status}!")
j = await response.json()
if j == {} or j == []:
log.debug("No account found.")
return
bh = BrawlhallaT(
steam=obj,
brawlhalla_id=j["brawlhalla_id"],
name=j["name"]
)
session.add(bh)
session.flush()
async with hcs.get(f"https://api.brawlhalla.com/player/{bh.brawlhalla_id}/ranked?api_key={self.token()}") as response:
if response.status != 200:
raise rc.ExternalError(f"Brawlhalla API /ranked returned {response.status}!")
j = await response.json()
if j == {} or j == []:
log.debug("No ranked info found.")
else:
await self._change(session=session, obj=bh, attribute="rating_1v1", new=j["rating"])
metal_name, tier_name = j["tier"].split(" ", 1)
metal = BrawlhallaMetal[metal_name.upper()]
tier = BrawlhallaTier(int(tier_name))
rank = BrawlhallaRank(metal=metal, tier=tier)
await self._change(session=session, obj=bh, attribute="rank_1v1", new=rank)
for jduo in j.get("2v2", []):
bhduo: Optional[BrawlhallaDuo] = await ru.asyncify(
session.query(DuoT)
.filter(
or_(
and_(
DuoT.id_one == jduo["brawlhalla_id_one"],
DuoT.id_two == jduo["brawlhalla_id_two"]
),
and_(
DuoT.id_one == jduo["brawlhalla_id_two"],
DuoT.id_two == jduo["brawlhalla_id_one"]
)
)
)
.one_or_none
)
if bhduo is None:
if bh.brawlhalla_id == jduo["brawlhalla_id_one"]:
otherbh: Optional[Brawlhalla] = await ru.asyncify(
session.query(BrawlhallaT).get, jduo["brawlhalla_id_two"]
)
else:
otherbh: Optional[Brawlhalla] = await ru.asyncify(
session.query(BrawlhallaT).get, jduo["brawlhalla_id_one"]
)
if otherbh is None:
continue
bhduo = DuoT(
one=bh,
two=otherbh,
)
session.add(bhduo)
await self._change(session=session, obj=bhduo, attribute="rating_2v2", new=jduo["rating"])
metal_name, tier_name = jduo["tier"].split(" ", 1)
metal = BrawlhallaMetal[metal_name.upper()]
tier = BrawlhallaTier(int(tier_name))
rank = BrawlhallaRank(metal=metal, tier=tier)
await self._change(session=session, obj=bhduo, attribute="rank_2v2", new=rank)
async def on_increase(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None:
if attribute == "rank_1v1":
await self.notify(f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla! Congratulazioni!")
elif attribute == "rank_2v2":
await self.notify(f"📈 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono saliti a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla! Congratulazioni!")
async def on_unchanged(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None:
pass
async def on_decrease(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: Any) -> None:
if attribute == "rank_1v1":
await self.notify(f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla.")
elif attribute == "rank_2v2":
await self.notify(f"📉 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] sono scesi a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla.")
async def on_first(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: None, new: Any) -> None:
if attribute == "rank_1v1":
await self.notify(f"🌟 [b]{obj.steam.user}[/b] si è classificato a [b]{new}[/b] ({obj.rating_1v1} MMR) in 1v1 su Brawlhalla!")
elif attribute == "rank_2v2":
await self.notify(f"🌟 [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] si sono classificati a [b]{new}[/b] ({obj.rating_2v2} MMR) in 2v2 su Brawlhalla!")
async def on_reset(self, session, obj: Union[Brawlhalla, BrawlhallaDuo], attribute: str, old: Any, new: None) -> None:
if attribute == "rank_1v1":
await self.notify(f"⬜️ [b]{obj.steam.user}[/b] non ha più un rank su Brawlhalla.")
elif attribute == "rank_2v2":
await self.notify(f"⬜️ [b]{obj.one.steam.user}[/b] e [b]{obj.two.steam.user}[/b] non hanno più un rank su Brawlhalla.")
def describe(self, obj: Steam) -> str:
bh = obj.brawlhalla
string = [f" [b]{bh.name}[/b]", ""]
if bh.rank_1v1:
string.append("👤 [b]1v1[/b]")
string.append(f"[b]{bh.rank_1v1}[/b] ({bh.rating_1v1} MMR)")
string.append("")
if len(bh.duos) != 0:
string.append(f"👥 [b]2v2[/b]")
for duo in sorted(bh.duos, key=lambda d: -d.rating_2v2):
other = duo.other(bh)
string.append(f"Con [b]{other.steam.user}[/b]: [b]{duo.rank_2v2}[/b] ({duo.rating_2v2} MMR)")
if len(bh.duos) != 0:
string.append("")
return "\n".join(string)

28
royalpack/commands/cat.py Normal file
View file

@ -0,0 +1,28 @@
from typing import *
import royalnet.commands as rc
import aiohttp
import io
class CatCommand(rc.Command):
name: str = "cat"
description: str = "Invia un gatto casuale in chat."
syntax: str = ""
aliases = ["catto", "kat", "kitty", "kitten", "gatto", "miao", "garf", "basta"]
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
async with aiohttp.ClientSession() as session:
async with session.get("https://api.thecatapi.com/v1/images/search") as response:
if response.status >= 400:
raise rc.ExternalError(f"Request returned {response.status}")
result = await response.json()
assert len(result) == 1
cat = result[0]
assert "url" in cat
url = cat["url"]
async with session.get(url) as response:
img = await response.content.read()
await data.reply_image(image=io.BytesIO(img))

View file

@ -1,3 +1,4 @@
from typing import *
import telegram import telegram
from royalnet.commands import * from royalnet.commands import *
@ -5,12 +6,11 @@ from royalnet.commands import *
class CiaoruoziCommand(Command): class CiaoruoziCommand(Command):
name: str = "ciaoruozi" name: str = "ciaoruozi"
description: str = "Saluta Ruozi, un leggendario essere che una volta era in User Games." description: str = "Saluta Ruozi, un leggendario essere che è tornato in Royal Games."
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name == "telegram": if self.interface.name == "telegram":
update: telegram.Update = data.update user: telegram.User = data.message.from_user
user: telegram.User = update.effective_user
# Se sei Ruozi, salutati da solo! # Se sei Ruozi, salutati da solo!
if user.id == 112437036: if user.id == 112437036:
await data.reply("👋 Ciao me!") await data.reply("👋 Ciao me!")

View file

@ -1,3 +1,4 @@
from typing import *
from royalnet.commands import * from royalnet.commands import *

View file

@ -77,7 +77,15 @@ class CvCommand(Command):
activity += f" | 📺 {mact['name']}" activity += f" | 📺 {mact['name']}"
# Custom Status # Custom Status
elif mact["type"] == 4: elif mact["type"] == 4:
activity += f" | ❓ {mact['state']}" if "emoji" in mact:
emoji = f"{mact['emoji']['name']}"
else:
emoji = f""
if "state" in mact:
state = f" {mact['state']}"
else:
state = ""
activity += f" | {emoji}{state}"
else: else:
raise ExternalError(f"Unknown Discord activity type: {mact['type']}") raise ExternalError(f"Unknown Discord activity type: {mact['type']}")

View file

@ -0,0 +1,136 @@
from typing import *
import logging
import asyncio
import datetime
import royalnet.commands as rc
import royalnet.utils as ru
from ..tables import Cvstats
log = logging.getLogger(__name__)
class CvstatsCommand(rc.Command):
name: str = "cvstats"
description: str = ""
syntax: str = ""
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface)
if self.interface.name == "discord":
self.loop.create_task(self._updater(1800))
def _is_ryg_member(self, member: dict):
for role in member["roles"]:
if role["id"] == self.interface.config["Cv"]["displayed_role_id"]:
return True
return False
async def _update(self, db_session):
log.info(f"Gathering Cvstats...")
while True:
try:
response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_cv")
except rc.ConfigurationError:
await asyncio.sleep(10)
continue
else:
break
users_total = 0
members_total = 0
users_online = 0
members_online = 0
users_connected = 0
members_connected = 0
users_playing = 0
members_playing = 0
# noinspection PyUnboundLocalVariable
for m in response["guild"]["members"]:
users_total += 1
if self._is_ryg_member(m):
members_total += 1
if m["status"]["main"] != "offline" and m["status"]["main"] != "idle":
users_online += 1
if self._is_ryg_member(m):
members_online += 1
if m["voice"] is not None and not m["voice"]["afk"]:
users_connected += 1
if self._is_ryg_member(m):
members_connected += 1
for mact in m["activities"]:
if mact.get("type") == 0:
users_playing += 1
if self._is_ryg_member(m):
members_playing += 1
assert users_online >= members_online
assert users_online >= users_connected
assert users_online >= users_playing
assert members_online >= members_connected
assert members_online >= members_playing
log.debug(f"Total users: {users_total}")
log.debug(f"Total members: {members_total}")
log.debug(f"Online users: {users_online}")
log.debug(f"Online members: {members_online}")
log.debug(f"Connected users: {users_connected}")
log.debug(f"Connected members: {members_connected}")
log.debug(f"Playing users: {users_playing}")
log.debug(f"Playing members: {members_playing}")
CvstatsT = self.alchemy.get(Cvstats)
cvstats = CvstatsT(
timestamp=datetime.datetime.now(),
users_total=users_total,
members_total=members_total,
users_online=users_online,
members_online=members_online,
users_connected=users_connected,
members_connected=members_connected,
users_playing=users_playing,
members_playing=members_playing
)
log.debug("Saving to database...")
db_session.add(cvstats)
await ru.asyncify(db_session.commit)
log.debug("Done!")
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()
await self._update(session)
session.close()
log.info(f"Sleeping for {period}s")
await asyncio.sleep(period)
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
CvstatsT = self.alchemy.get(Cvstats)
cvstats = data.session.query(CvstatsT).order_by(CvstatsT.timestamp.desc()).first()
message = [
f" [b]Statistiche[/b]",
f"Ultimo aggiornamento: [b]{cvstats.timestamp.strftime('%Y-%m-%d %H:%M')}[/b]",
f"Utenti totali: [b]{cvstats.users_total}[/b]",
f"Membri totali: [b]{cvstats.members_total}[/b]",
f"Utenti online: [b]{cvstats.users_online}[/b]",
f"Membri online: [b]{cvstats.members_online}[/b]",
f"Utenti connessi: [b]{cvstats.users_connected}[/b]",
f"Membri connessi: [b]{cvstats.members_connected}[/b]",
f"Utenti in gioco: [b]{cvstats.users_playing}[/b]",
f"Membri in gioco: [b]{cvstats.members_playing}[/b]"
]
await data.reply("\n".join(message))

View file

@ -1,11 +1,12 @@
from typing import *
import re import re
import datetime import datetime
import telegram import telegram
import aiohttp import aiohttp
from typing import * import royalnet.commands as rc
from royalnet.commands import * import royalnet.utils as ru
from royalnet.utils import asyncify import royalnet.backpack.tables as rbt
from royalnet.backpack.tables import *
from ..tables import * from ..tables import *
@ -13,7 +14,7 @@ async def to_imgur(imgur_api_key, photosizes: List[telegram.PhotoSize], caption=
# Select the largest photo # Select the largest photo
largest_photo = sorted(photosizes, key=lambda p: p.width * p.height)[-1] largest_photo = sorted(photosizes, key=lambda p: p.width * p.height)[-1]
# Get the photo url # Get the photo url
photo_file: telegram.File = await asyncify(largest_photo.get_file) photo_file: telegram.File = await ru.asyncify(largest_photo.get_file)
# Forward the url to imgur, as an upload # Forward the url to imgur, as an upload
async with aiohttp.request("post", "https://api.imgur.com/3/upload", data={ async with aiohttp.request("post", "https://api.imgur.com/3/upload", data={
"image": photo_file.file_path, "image": photo_file.file_path,
@ -25,21 +26,20 @@ async def to_imgur(imgur_api_key, photosizes: List[telegram.PhotoSize], caption=
}) as request: }) as request:
response = await request.json() response = await request.json()
if not response["success"]: if not response["success"]:
raise CommandError("Imgur returned an error in the image upload.") raise rc.CommandError("Imgur returned an error in the image upload.")
return response["data"]["link"] return response["data"]["link"]
class DiarioCommand(Command): class DiarioCommand(rc.Command):
name: str = "diario" name: str = "diario"
description: str = "Aggiungi una citazione al Diario." description: str = "Aggiungi una citazione al Diario."
syntax = "[!] \"{testo}\" --[autore], [contesto]" syntax = "[!] \"{testo}\" --[autore], [contesto]"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
if self.interface.name == "telegram": if self.interface.name == "telegram":
update: telegram.Update = data.update message: telegram.Message = data.message
message: telegram.Message = update.message
reply: telegram.Message = message.reply_to_message reply: telegram.Message = message.reply_to_message
creator = await data.get_author() creator = await data.get_author()
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
@ -71,12 +71,12 @@ class DiarioCommand(Command):
media_url = None media_url = None
# Ensure there is a text or an image # Ensure there is a text or an image
if not (text or media_url): if not (text or media_url):
raise InvalidInputError("Il messaggio a cui hai risposto non contiene testo o immagini.") raise rc.InvalidInputError("Il messaggio a cui hai risposto non contiene testo o immagini.")
# Find the Royalnet account associated with the sender # Find the Royalnet account associated with the sender
quoted_tg = await asyncify(data.session.query(self.alchemy.get(Telegram)) quoted_tg = await ru.asyncify(data.session.query(self.alchemy.get(rbt.Telegram))
.filter_by(tg_id=reply.from_user.id) .filter_by(tg_id=reply.from_user.id)
.one_or_none) .one_or_none)
quoted_account = quoted_tg.royal if quoted_tg is not None else None quoted_account = quoted_tg.user if quoted_tg is not None else None
# Find the quoted name to assign # Find the quoted name to assign
quoted_user: telegram.User = reply.from_user quoted_user: telegram.User = reply.from_user
quoted = quoted_user.full_name quoted = quoted_user.full_name
@ -122,13 +122,13 @@ class DiarioCommand(Command):
context = None context = None
# Find if there's a Royalnet account associated with the quoted name # Find if there's a Royalnet account associated with the quoted name
if quoted is not None: if quoted is not None:
quoted_alias = await asyncify( quoted_alias = await ru.asyncify(
data.session.query(self.alchemy.get(Alias)) data.session.query(self.alchemy.get(rbt.Alias))
.filter_by(alias=quoted.lower()).one_or_none .filter_by(alias=quoted.lower()).one_or_none
) )
else: else:
quoted_alias = None quoted_alias = None
quoted_account = quoted_alias.royal if quoted_alias is not None else None quoted_account = quoted_alias.user if quoted_alias is not None else None
else: else:
text = None text = None
quoted = None quoted = None
@ -137,7 +137,7 @@ class DiarioCommand(Command):
context = None context = None
# Ensure there is a text or an image # Ensure there is a text or an image
if not (text or media_url): if not (text or media_url):
raise InvalidInputError("Manca il testo o l'immagine da inserire nel diario.") raise rc.InvalidInputError("Manca il testo o l'immagine da inserire nel diario.")
# Create the diario quote # Create the diario quote
diario = self.alchemy.get(Diario)(creator=creator, diario = self.alchemy.get(Diario)(creator=creator,
quoted_account=quoted_account, quoted_account=quoted_account,
@ -148,7 +148,7 @@ class DiarioCommand(Command):
media_url=media_url, media_url=media_url,
spoiler=spoiler) spoiler=spoiler)
data.session.add(diario) data.session.add(diario)
await asyncify(data.session.commit) await ru.asyncify(data.session.commit)
await data.reply(f"{str(diario)}") await data.reply(f"{str(diario)}")
else: else:
# Find the creator of the quotes # Find the creator of the quotes
@ -173,7 +173,7 @@ class DiarioCommand(Command):
timestamp = datetime.datetime.now() timestamp = datetime.datetime.now()
# Ensure there is some text # Ensure there is some text
if not text: if not text:
raise InvalidInputError("Manca il testo o l'immagine da inserire nel diario.") raise rc.InvalidInputError("Manca il testo o l'immagine da inserire nel diario.")
# Or a quoted # Or a quoted
if not quoted: if not quoted:
quoted = None quoted = None
@ -181,17 +181,17 @@ class DiarioCommand(Command):
context = None context = None
# Find if there's a Royalnet account associated with the quoted name # Find if there's a Royalnet account associated with the quoted name
if quoted is not None: if quoted is not None:
quoted_alias = await asyncify( quoted_alias = await ru.asyncify(
data.session.query(self.alchemy.get(Alias)) data.session.query(self.alchemy.get(rbt.Alias))
.filter_by(alias=quoted.lower()) .filter_by(alias=quoted.lower())
.one_or_none .one_or_none
) )
else: else:
quoted_alias = None quoted_alias = None
quoted_account = quoted_alias.royal if quoted_alias is not None else None quoted_account = quoted_alias.user if quoted_alias is not None else None
if quoted_alias is not None and quoted_account is None: if quoted_alias is not None and quoted_account is None:
raise UserError("Il nome dell'autore è ambiguo, quindi la riga non è stata aggiunta.\n" raise rc.UserError("Il nome dell'autore è ambiguo, quindi la riga non è stata aggiunta.\n"
"Per piacere, ripeti il comando con un nome più specifico!") "Per piacere, ripeti il comando con un nome più specifico!")
# Create the diario quote # Create the diario quote
diario = self.alchemy.Diario(creator=creator, diario = self.alchemy.Diario(creator=creator,
quoted_account=quoted_account, quoted_account=quoted_account,
@ -202,5 +202,5 @@ class DiarioCommand(Command):
media_url=None, media_url=None,
spoiler=spoiler) spoiler=spoiler)
data.session.add(diario) data.session.add(diario)
await asyncify(data.session.commit) await ru.asyncify(data.session.commit)
await data.reply(f"{str(diario)}") await data.reply(f"{str(diario)}")

View file

@ -1,9 +1,11 @@
from royalnet.commands import * from typing import *
from royalnet.utils import * import royalnet.commands as rc
import royalnet.utils as ru
from ..tables import Diario from ..tables import Diario
class DiarioquoteCommand(Command): class DiarioquoteCommand(rc.Command):
name: str = "diarioquote" name: str = "diarioquote"
description: str = "Cita una riga del diario." description: str = "Cita una riga del diario."
@ -12,12 +14,12 @@ class DiarioquoteCommand(Command):
syntax = "{id}" syntax = "{id}"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
try: try:
entry_id = int(args[0].lstrip("#")) entry_id = int(args[0].lstrip("#"))
except ValueError: except ValueError:
raise CommandError("L'id che hai specificato non è valido.") raise rc.CommandError("L'id che hai specificato non è valido.")
entry: Diario = await asyncify(data.session.query(self.alchemy.get(Diario)).get, entry_id) entry: Diario = await ru.asyncify(data.session.query(self.alchemy.get(Diario)).get, entry_id)
if entry is None: if entry is None:
raise CommandError("Nessuna riga con quell'id trovata.") raise rc.CommandError("Nessuna riga con quell'id trovata.")
await data.reply(f" {entry}") await data.reply(f" {entry}")

View file

@ -0,0 +1,29 @@
from typing import *
import royalnet.commands as rc
import royalnet.utils as ru
from sqlalchemy import func
from ..tables import Diario
class DiarioshuffleCommand(rc.Command):
name: str = "diarioshuffle"
description: str = "Cita una riga casuale del diario."
aliases = ["dis", "dishuffle", "dish"]
syntax = ""
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
DiarioT = self.alchemy.get(Diario)
entry: List[Diario] = await ru.asyncify(
data.session
.query(DiarioT)
.order_by(func.random())
.limit(1)
.one_or_none
)
if entry is None:
raise rc.CommandError("Nessuna riga del diario trovata.")
await data.reply(f" {entry}")

181
royalpack/commands/dog.py Normal file
View file

@ -0,0 +1,181 @@
from typing import *
import royalnet.commands as rc
import aiohttp
import io
class DogCommand(rc.Command):
name: str = "dog"
description: str = "Invia un cane della razza specificata in chat."
syntax: str = "[razza|list]"
_breeds = [
"affenpinscher",
"african",
"airedale",
"akita",
"appenzeller",
"australian-shepherd",
"basenji",
"beagle",
"bluetick",
"borzoi",
"bouvier",
"boxer",
"brabancon",
"briard",
"buhund-norwegian",
"bulldog-boston",
"bulldog-english",
"bulldog-french",
"bullterrier-staffordshire",
"cairn",
"cattledog-australian",
"chihuahua",
"chow",
"clumber",
"cockapoo",
"collie-border",
"coonhound",
"corgi-cardigan",
"cotondetulear",
"dachshund",
"dalmatian",
"dane-great",
"deerhound-scottish",
"dhole",
"dingo",
"doberman",
"elkhound-norwegian",
"entlebucher",
"eskimo",
"finnish-lapphund",
"frise-bichon",
"germanshepherd",
"greyhound-italian",
"groenendael",
"havanese",
"hound-afghan",
"hound-basset",
"hound-blood",
"hound-english",
"hound-ibizan",
"hound-plott",
"hound-walker",
"husky",
"keeshond",
"kelpie",
"komondor",
"kuvasz",
"labrador",
"leonberg",
"lhasa",
"malamute",
"malinois",
"maltese",
"mastiff-bull",
"mastiff-english",
"mastiff-tibetan",
"mexicanhairless",
"mix",
"mountain-bernese",
"mountain-swiss",
"newfoundland",
"otterhound",
"ovcharka-caucasian",
"papillon",
"pekinese",
"pembroke",
"pinscher-miniature",
"pitbull",
"pointer-german",
"pointer-germanlonghair",
"pomeranian",
"poodle-miniature",
"poodle-standard",
"poodle-toy",
"pug",
"puggle",
"pyrenees",
"redbone",
"retriever-chesapeake",
"retriever-curly",
"retriever-flatcoated",
"retriever-golden",
"ridgeback-rhodesian",
"rottweiler",
"saluki",
"samoyed",
"schipperke",
"schnauzer-giant",
"schnauzer-miniature",
"setter-english",
"setter-gordon",
"setter-irish",
"sheepdog-english",
"sheepdog-shetland",
"shiba",
"shihtzu",
"spaniel-blenheim",
"spaniel-brittany",
"spaniel-cocker",
"spaniel-irish",
"spaniel-japanese",
"spaniel-sussex",
"spaniel-welsh",
"springer-english",
"stbernard",
"terrier-american",
"terrier-australian",
"terrier-bedlington",
"terrier-border",
"terrier-dandie",
"terrier-fox",
"terrier-irish",
"terrier-kerryblue",
"terrier-lakeland",
"terrier-norfolk",
"terrier-norwich",
"terrier-patterdale",
"terrier-russell",
"terrier-scottish",
"terrier-sealyham",
"terrier-silky",
"terrier-tibetan",
"terrier-toy",
"terrier-westhighland",
"terrier-wheaten",
"terrier-yorkshire",
"vizsla",
"waterdog-spanish",
"weimaraner",
"whippet",
"wolfhound-irish",
]
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
breed = args.joined()
if breed:
if breed == "list":
await data.reply("\n".join([" Razze disponibili:", [f"[c]{breed}[/c]" for breed in self._breeds]]))
if breed in self._breeds:
url = f"https://dog.ceo/api/breed/{breed}/images/random"
else:
raise rc.InvalidInputError("Questa razza non è disponibile.\n")
else:
url = f"https://dog.ceo/api/breeds/image/random"
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status >= 400:
raise rc.ExternalError(f"Request returned {response.status}")
result = await response.json()
assert "status" in result
assert result["status"] == "success"
assert "message" in result
url = result["message"]
async with session.get(url) as response:
img = await response.content.read()
await data.reply_image(image=io.BytesIO(img))

106
royalpack/commands/dota.py Normal file
View file

@ -0,0 +1,106 @@
from typing import *
import logging
import aiohttp
import royalnet.commands as rc
import royalnet.utils as ru
from royalnet.backpack import tables as rbt
from .abstract.linker import LinkerCommand
from ..tables import Steam, Dota
from ..types import DotaRank
log = logging.getLogger(__name__)
class DotaCommand(LinkerCommand):
name: str = "dota"
aliases = ["dota2", "doto", "doto2", "dotka", "dotka2"]
description: str = "Visualizza le tue statistiche di Dota."
syntax: str = ""
def describe(self, obj: Steam) -> str:
string = f" [b]{obj.persona_name}[/b]\n"
if obj.dota.rank:
string += f"{obj.dota.rank}\n"
string += f"\n" \
f"Wins: [b]{obj.dota.wins}[/b]\n" \
f"Losses: [b]{obj.dota.losses}[/b]\n" \
f"\n"
return string
async def get_updatables_of_user(self, session, user: rbt.User) -> List[Dota]:
return user.steam
async def get_updatables(self, session) -> List[Dota]:
return await ru.asyncify(session.query(self.alchemy.get(Steam)).all)
async def create(self,
session,
user: rbt.User,
args: rc.CommandArgs,
data: Optional[rc.CommandData] = None) -> Optional[Dota]:
raise rc.InvalidInputError("Dota accounts are automatically linked from Steam.")
async def update(self, session, obj: Steam, change: Callable[[str, Any], Awaitable[None]]):
log.debug(f"Getting player data from OpenDota...")
async with aiohttp.ClientSession() as hcs:
# Get profile data
async with hcs.get(f"https://api.opendota.com/api/players/{obj.steamid.as_32}/") as response:
if response.status != 200:
raise rc.ExternalError(f"OpenDota / returned {response.status}!")
p = await response.json()
# No such user
if "profile" not in p:
log.debug(f"Not found: {obj}")
return
# Get win/loss data
async with hcs.get(f"https://api.opendota.com/api/players/{obj.steamid.as_32}/wl") as response:
if response.status != 200:
raise rc.ExternalError(f"OpenDota /wl returned {response.status}!")
wl = await response.json()
# No such user
if wl["win"] == 0 and wl["lose"] == 0:
log.debug(f"Not found: {obj}")
return
# Find the Dota record, if it exists
dota: Dota = obj.dota
if dota is None:
# Autocreate the Dota record
dota = self.alchemy.get(Dota)(steam=obj)
session.add(dota)
session.flush()
# Make a custom change function
async def change(attribute: str, new: Any):
await self._change(session=session, obj=dota, attribute=attribute, new=new)
await change("wins", wl["win"])
await change("losses", wl["lose"])
if p["rank_tier"]:
await change("rank", DotaRank(rank_tier=p["rank_tier"]))
else:
await change("rank", None)
async def on_increase(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None:
if attribute == "rank":
await self.notify(f"📈 [b]{obj.steam.user}[/b] è salito a [b]{new}[/b] su Dota 2! Congratulazioni!")
async def on_unchanged(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None:
pass
async def on_decrease(self, session, obj: Dota, attribute: str, old: Any, new: Any) -> None:
if attribute == "rank":
await self.notify(f"📉 [b]{obj.steam.user}[/b] è sceso a [b]{new}[/b] su Dota 2.")
async def on_first(self, session, obj: Dota, attribute: str, old: None, new: Any) -> None:
if attribute == "wins":
await self.notify(f"↔️ Account {obj} connesso a {obj.steam.user}!")
elif attribute == "rank":
await self.notify(f"🌟 [b]{obj.steam.user}[/b] si è classificato [b]{new}[/b] su Dota 2!")
async def on_reset(self, session, obj: Dota, attribute: str, old: Any, new: None) -> None:
if attribute == "rank":
await self.notify(f"⬜️ [b]{obj.steam.user}[/b] non ha più un rank su Dota 2.")

View file

@ -28,8 +28,7 @@ class EatCommand(Command):
"evilbalu": "🚹 Hai mangiato {food}.\n[i]Sa di snado.[/i]", "evilbalu": "🚹 Hai mangiato {food}.\n[i]Sa di snado.[/i]",
"balubis": "🚹 Hai mangiato {food}.\n[i]Sa di acqua calda.[/i]", "balubis": "🚹 Hai mangiato {food}.\n[i]Sa di acqua calda.[/i]",
"goodbalu": "🚹 Hai mangiato {food}.\n[i]Sa di acqua calda.[/i]", "goodbalu": "🚹 Hai mangiato {food}.\n[i]Sa di acqua calda.[/i]",
"chiara": "🚺 Hai mangiato {food}.\n[i]Sa un po' di biscotto, ma per lo più sa di curcuma, pepe e spezie" "chiara": "🚺 Hai mangiato {food}.\n[i]Sa un po' di biscotto, ma per lo più sa di curcuma e pepe.[/i]",
" varie.[/i]",
"fabio": "🚹 Hai mangiato {food}.\n[i]Sa di gelatina tuttigusti+1.[/i]", "fabio": "🚹 Hai mangiato {food}.\n[i]Sa di gelatina tuttigusti+1.[/i]",
"proto": "🚹 Hai mangiato {food}.\n[i]Sa di gelatina tuttigusti+1.[/i]", "proto": "🚹 Hai mangiato {food}.\n[i]Sa di gelatina tuttigusti+1.[/i]",
"marco": "🚹 Hai mangiato {food}.\n[i]Sa di carlino <.<[/i]", "marco": "🚹 Hai mangiato {food}.\n[i]Sa di carlino <.<[/i]",
@ -38,15 +37,28 @@ class EatCommand(Command):
"maxsensei": "🚹 Hai mangiato {food}.\n[i]Sa di merda.[/i]", "maxsensei": "🚹 Hai mangiato {food}.\n[i]Sa di merda.[/i]",
"steffo": "🚹 Hai mangiato {food}.\n[i]Sa di gelato e di Coca-Cola.[/i]", "steffo": "🚹 Hai mangiato {food}.\n[i]Sa di gelato e di Coca-Cola.[/i]",
# Sezione in cui mangi i professori dei membri Royal Games
"arrigo": "🖍 Hai mangiato {food}!\n[i]Ti scrive F: V→W sulla parete dello stomaco con i gessetti colorati.[/i]",
"bonisoli": "🖍 Hai mangiato {food}!\n[i]Ti scrive F: V→W sulla parete dello stomaco con i gessetti colorati.[/i]",
"montangero": "📝 Hai mangiato la {food}!\n[i]La digerisci in O(n!).[/i]",
"marongiu": "🔧 Hai mangiato {food}!\n[i]Il tuo apparato digerente viene trasformato in una pipeline.[/i]",
"mandreoli": "⚠️ Hai mangiato la {food}!\n[c]Error: Segmentation fault (core dumped)[/c]",
"la rocca": "📊 Hai mangiato {food}!\n[i]Si distribuisce nel tuo intestino come una Normale.[/i]",
"villani": "🐜 Hai mangiato {food}!\n[i]Crea una rete neurale sfruttando i tuoi neuroni e le tue cellule.[/i]",
"novellani": "❓Volevi mangiare {food}...\n[i]...ma invece trovi solo Dell'Amico.[/i]",
# Sezione delle supercazzole # Sezione delle supercazzole
"antani": "❔ Hai mangiato {food}. \n[i]Con tarapia tapioco o scherziamo? No, mi permetta. Noi siamo in 4.\n" "antani": "❔ Hai mangiato {food}. \n[i]Con tarapia tapioco o scherziamo? No, mi permetta. Noi siamo in 4.\n"
"Come se fosse antani anche per lei soltanto in due, oppure in quattro anche scribàcchi confaldina?\n" "Come se fosse antani anche per lei soltanto in due, oppure in quattro anche scribàcchi confaldina?\n"
"Come antifurto, per esempio.[/i]", "Come antifurto, per esempio.[/i]",
"indice": "☝️ Hai mangiato l'{food}. \n[i]Ecco, lo alzi. Lo vede, lo vede che stuzzica?[/i]", "indice": "☝️ Hai mangiato l'{food}. \n[i]Ecco, lo alzi. Lo vede, lo vede che stuzzica?[/i]",
# sezione con piante e anmali # Sezione con piante e animali
"cactus": "🌵 Hai mangiato un {food}.\n[i]Gli hai tolto le spine prima, vero?[/i]", "cactus": "🌵 Hai mangiato un {food}.\n[i]Gli hai tolto le spine prima, vero?[/i]",
"tango": "🌳 Hai mangiato un {food}, e un albero insieme ad esso.\n[i]Senti le tue ferite curarsi...[/i]", "tango": "🌳 Hai mangiato un {food}, e un albero insieme ad esso.\n[i]Senti le tue ferite curarsi...[/i]",
"foglia": "🍁 Hai mangiato la {food}.\n[i]A te non la si fa![/i]",
"pug": "🐶 Hai provato a mangiare un {food}...\n[i]...Ma Mallllco si è precipitato in soccorso e lo ha salvato![/i]",
"carlino": "🐶 Hai provato a mangiare un {food}...\n[i]...Ma Mallllco si è precipitato in soccorso e lo ha salvato![/i]",
"gatto": "🐱 Vieni fermato prima di poter compiere questo gesto orribile.\n" "gatto": "🐱 Vieni fermato prima di poter compiere questo gesto orribile.\n"
"[i]Il {food} verrà pettato da tutti per farlo riavere dal trauma.[/i]", "[i]Il {food} verrà pettato da tutti per farlo riavere dal trauma.[/i]",
"3 porcellini": "🐷 Hai mangiato i {food}.\n[i]La casa di mattoni non è bastata a fermarti![/i]", "3 porcellini": "🐷 Hai mangiato i {food}.\n[i]La casa di mattoni non è bastata a fermarti![/i]",
@ -73,13 +85,19 @@ class EatCommand(Command):
"little salami": "🥓 Mmmh, tasty!\n[i]Cats can have {food} too![/i]", "little salami": "🥓 Mmmh, tasty!\n[i]Cats can have {food} too![/i]",
"a little salami": "🥓 Mmmh, tasty!\n[i]Cats can have {food} too![/i]", "a little salami": "🥓 Mmmh, tasty!\n[i]Cats can have {food} too![/i]",
"pollo": '🍗 Il {food} che hai appena mangiato proveniva dallo spazio.\n[i]Coccodè?[/i]', "pollo": '🍗 Il {food} che hai appena mangiato proveniva dallo spazio.\n[i]Coccodè?[/i]',
"pranzo di marco": '🍗 Hai mangiato il {food}.\n[i]Ti senti lactose-free, ma un po\' povero in calcio.[/i]',
"pranzo di mallllco": '🍗 Hai mangiato il {food}.\n[i]Ti senti lactose-free, ma un po\' povero in calcio.[/i]',
"gnocchetti": "🥘 Ullà, sono duri 'sti {food}!\n[i]Fai fatica a digerirli.[/i]", "gnocchetti": "🥘 Ullà, sono duri 'sti {food}!\n[i]Fai fatica a digerirli.[/i]",
"spam": "🥫 Hai mangiato {food}. La famosa carne in gelatina, ovviamente!\n[i]A questo proposito, di " "spam": "🥫 Hai mangiato {food}. La famosa carne in gelatina, ovviamente!\n[i]A questo proposito, di "
"sicuro sarai interessato all'acquisto di 1087 scatole di Simmenthal in offerta speciale![/i]", "sicuro sarai interessato all'acquisto di 1087 scatole di Simmenthal in offerta speciale![/i]",
"riso": "🍚 Hai mangiato del {food}. Non ci resta che il Pianto! \n[i]Ba dum tsss![/i]", "riso": "🍚 Hai mangiato del {food}. Non ci resta che il Pianto! \n[i]Ba dum tsss![/i]",
"gelato": "🍨 Mangiando del {food}, hai invocato Steffo.\n[i]Cedigli ora il tuo gelato.[/i]", "gelato": "🍨 Mangiando del {food}, hai invocato Steffo.\n[i]Cedigli ora il tuo gelato.[/i]",
"gelato di steffo": "🍨 Hai provato a rubare il {food}...\n[i]...Ma sei arrivato tardi: l'ha già mangiato.[/i]",
"biscotto": "🍪 Hai mangiato un {food} di contrabbando.\n[i]L'Inquisizione non lo saprà mai![/i]", "biscotto": "🍪 Hai mangiato un {food} di contrabbando.\n[i]L'Inquisizione non lo saprà mai![/i]",
"biscotti": "🍪 Hai mangiato tanti {food} di contrabbando.\n[i]Attento! L'Inquisizione è sulle tue tracce![/i]", "biscotti": "🍪 Hai mangiato tanti {food} di contrabbando.\n[i]Attento! L'Inquisizione è sulle tue tracce![/i]",
"crocchette di pollo": "🍗 Hai mangiato {food}!\n[i]Dio porco maledetto, infame, CAPRA, porca Madonna, Dio cane, "
"HAI PERSO. UN POMERIGGIO PER C- ooh se è questo dio cane, altro che sfondamento dei cieli "
"*roba non capibile*, sfondi tutti dio can li distruggi, non ci rimane più niente.[/i]",
# Sezione delle bevande # Sezione delle bevande
"acqua": "💧 Hai bevuto un po' d'{food}.\n[i]Ti depura e ti fa fare tanta plin plin![/i}", "acqua": "💧 Hai bevuto un po' d'{food}.\n[i]Ti depura e ti fa fare tanta plin plin![/i}",
@ -95,6 +113,7 @@ class EatCommand(Command):
"kaffé": "☕️ Ma BUONGIORNISSIMOOO !!!!\n[i]Non si può iniziare la giornata senza un buon {food} !![/i]", "kaffé": "☕️ Ma BUONGIORNISSIMOOO !!!!\n[i]Non si può iniziare la giornata senza un buon {food} !![/i]",
"kaffe": "☕️ Ma BUONGIORNISSIMOOO !!!!\n[i]Non si può iniziare la giornata senza un buon {food} !![/i]", "kaffe": "☕️ Ma BUONGIORNISSIMOOO !!!!\n[i]Non si può iniziare la giornata senza un buon {food} !![/i]",
"birra": "🍺 Hai mangiato {food}.\n[i]Adesso sei un povero barbone alcolizzato.[/i]", "birra": "🍺 Hai mangiato {food}.\n[i]Adesso sei un povero barbone alcolizzato.[/i]",
"martini": "🍸 Hai ordinato un {food}. Agitato, non mescolato.\n[i]Adesso hai licenza di uccidere![/i]",
"redbull": "🍾 Hai mangiato {food}.\n[i]Adesso puoi volare![/i]", "redbull": "🍾 Hai mangiato {food}.\n[i]Adesso puoi volare![/i]",
"red bull": "🍾 Hai mangiato {food}.\n[i]Adesso puoi volare![/i]", "red bull": "🍾 Hai mangiato {food}.\n[i]Adesso puoi volare![/i]",
@ -110,7 +129,19 @@ class EatCommand(Command):
"redhat": "🐧 Hai mangiato {food}.\n[i]La tua anima appartiene a IBM, ora.[/i]", "redhat": "🐧 Hai mangiato {food}.\n[i]La tua anima appartiene a IBM, ora.[/i]",
"linux from scratch": "🐧 Hai mangiato {food}.\n[i]Sei diventato un puzzle.[/i]", "linux from scratch": "🐧 Hai mangiato {food}.\n[i]Sei diventato un puzzle.[/i]",
# Citazioni da film (nello specifico dai Blues Brothers)
"pane bianco tostato liscio, quattro polli fritti e una coca": "🕶 Tu e tuo fratello avete ordinato {food}."
" Il cuoco vi ha riconosciuto e vuole tornare a suonare nella vostra band.\n[i]Sua moglie gliene canta"
" quattro (letteralmente), ma non riesce a fargli cambiare idea. Siete in missione per conto di Dio![/i]",
"pane bianco tostato liscio": "🕶 Tu e tuo fratello avete ordinato {food}, quattro polli fritti e una coca."
" Il cuoco vi ha riconosciuto e vuole tornare a suonare nella vostra band.\n[i]Sua moglie gliene canta"
" quattro (letteralmente), ma non riesce a fargli cambiare idea. Siete in missione per conto di Dio![/i]",
"quattro polli fritti e una coca": "🕶 Tu e tuo fratello avete ordinato pane bianco tostato liscio, {food}."
" Il cuoco vi ha riconosciuto e vuole tornare a suonare nella vostra band.\n[i]Sua moglie gliene canta"
" quattro (letteralmente), ma non riesce a fargli cambiare idea. Siete in missione per conto di Dio![/i]",
# Altro # Altro
"vendetta": "😈 Ti sei gustato la tua {food}.\n[i]Deliziosa, se servita fredda![/i]",
"demone": "👿 Hai mangiato un {food}. Non l'ha presa bene...\n[i]Hai terribili bruciori di stomaco.[/i]", "demone": "👿 Hai mangiato un {food}. Non l'ha presa bene...\n[i]Hai terribili bruciori di stomaco.[/i]",
"diavolo": "👿 Hai mangiato un {food}. Non l'ha presa bene...\n[i]Hai terribili bruciori di stomaco.[/i]", "diavolo": "👿 Hai mangiato un {food}. Non l'ha presa bene...\n[i]Hai terribili bruciori di stomaco.[/i]",
"cacca": "💩 Che schifo! Hai mangiato {food}!\n[i]Allontati per favore, PLEH![/i]", "cacca": "💩 Che schifo! Hai mangiato {food}!\n[i]Allontati per favore, PLEH![/i]",
@ -121,12 +152,19 @@ class EatCommand(Command):
"bot": "🤖 Come osi provare a mangiarmi?!\n[i]Il {food} è arrabbiato con te.[/i]", "bot": "🤖 Come osi provare a mangiarmi?!\n[i]Il {food} è arrabbiato con te.[/i]",
"royal bot": "🤖 Come osi provare a mangiarmi?!\n[i]Il {food} è arrabbiato con te.[/i]", "royal bot": "🤖 Come osi provare a mangiarmi?!\n[i]Il {food} è arrabbiato con te.[/i]",
"re": "👑 Hai mangiato il {food} avversario! \n[i]Scacco matto![/i]", "re": "👑 Hai mangiato il {food} avversario! \n[i]Scacco matto![/i]",
"furry": "🐕 Hai mangiato {food}.\n[i]OwO[/i]",
"qualcosa che non mi piace": "🥦 Hai assaggiato il cibo, ma non ti piace proprio./n[i]Dai, mangialo, che ti"
" fa bene! In africa i bambini muoiono di fame, e tu... ![/i]",
"qualcosa che non ti piace": "🥦 Hai assaggiato il cibo, ma non ti piace proprio./n[i]Dai, mangialo, che ti"
" fa bene! In africa i bambini muoiono di fame, e tu... ![/i]",
"polvere": "☁️ Hai mangiato la {food}.\n[i]Ti hanno proprio battuto![/i]", "polvere": "☁️ Hai mangiato la {food}.\n[i]Ti hanno proprio battuto![/i]",
"giaroun": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]", "giaroun": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]",
"giarone": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]", "giarone": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]",
"sasso": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]", "sasso": "🥌 Il {food} che hai mangiato era duro come un {food}.\n[i]Stai soffrendo di indigestione![/i]",
"bomba": "💣 Hai mangiato una {food}. Speriamo fosse solo calorica!\n[i]3... 2... 1...[/i]", "bomba": "💣 Hai mangiato una {food}. Speriamo fosse solo calorica!\n[i]3... 2... 1...[/i]",
"ass": "🕳 Hai mangiato {food}./n[i]Bleah! Lo sai cosa fa quel coso per sopravvivere?[/i]", "ass": "🕳 Hai mangiato {food}.\n[i]Bleah! Lo sai cosa fa quel coso per sopravvivere?[/i]",
"onion": "🗞 You ate the {food}. Ci sei proprio cascato!\n [i]Hai mai creduto a una notizia di Lercio,"
" invece?[/i]",
"uranio": "☢️ L'{food} che hai mangiato era radioattivo.\n[i]Stai brillando di verde![/i]", "uranio": "☢️ L'{food} che hai mangiato era radioattivo.\n[i]Stai brillando di verde![/i]",
"tide pod": "☣️ I {food} che hai mangiato erano buonissimi.\n[i]Stai sbiancando![/i]", "tide pod": "☣️ I {food} che hai mangiato erano buonissimi.\n[i]Stai sbiancando![/i]",
"tide pods": "☣️ I {food} che hai mangiato erano buonissimi.\n[i]Stai sbiancando![/i]", "tide pods": "☣️ I {food} che hai mangiato erano buonissimi.\n[i]Stai sbiancando![/i]",
@ -138,6 +176,19 @@ class EatCommand(Command):
"nulla": "⬜️ Non hai mangiato {food}.\n[i]Hai ancora più fame.[/i]", "nulla": "⬜️ Non hai mangiato {food}.\n[i]Hai ancora più fame.[/i]",
"torta": "⬜️ Non hai mangiato niente.\n[i]La {food} è una menzogna![/i]", "torta": "⬜️ Non hai mangiato niente.\n[i]La {food} è una menzogna![/i]",
"cake": "⬜️ Non hai mangiato niente.\n[i]The {food} is a lie![/i]", "cake": "⬜️ Non hai mangiato niente.\n[i]The {food} is a lie![/i]",
"markov": "🗨 Stai cercando di mangiare... un matematico russo di nome {food}?\n[i]Lo trovi un po' indigesto.[/i]",
"mia sul fiume": "💧 Hai mangiato il miglior piatto al mondo, la {food}, esclusivo ai membri Royal Games.\n"
"[i]Nessuno, tranne il bot, sa di cosa è fatta esattamente, ma una cosa è certa: è "
"buonissima![/i]",
"angelo": "👼 Oh mio dio! E' un {food}!\n[i]Ora hai un digramma ad onda blu.[/i]",
"unicode": "🍗 Hai mangiato {food}!\n๓ค 𝔫𝔬𝔫 [i]è[/i] 𝓼𝓾𝓬𝓬𝓮𝓼𝓼𝓸 𝕟𝕦𝕝𝕝𝕒.",
"eco": "🏔 Hai mangiato l'{food} eco eco!\n[i]Ma non è successo nulla ulla ulla.[/i]",
"disinfettante": "🧴Hai mangiato {food}!\n[i]Secondo Trump, ora sei molto più sano.[/i]",
"terraria": "🌳 Hai provato a mangiare {food}, ma non ne sei stato all'Altezza (Coniglio).\n[i]Prova a mangiare qualcos'altro...[/i]",
"cooked fish": "🐟 Hai mangiato {food}.\n[i]Ora sei Well Fed per 20 minuti.[/i]",
"gestione": "🌐 Hai mangiato {food}, su cui si basa Condivisione.\n[i]Fa ridere di sotto, ma fa anche riflettere di sopra.[/i]",
"condivisione": "🌐 Hai mangiato {food}, basato su Gestione.\n[i]Fa ridere di sopra, ma fa anche riflettere di sotto.[/i]",
} }
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: CommandArgs, data: CommandData) -> None:

View file

@ -1,8 +1,9 @@
from typing import *
import random import random
from royalnet.commands import * import royalnet.commands as rc
class EmojifyCommand(Command): class EmojifyCommand(rc.Command):
name: str = "emojify" name: str = "emojify"
description: str = "Converti un messaggio in emoji." description: str = "Converti un messaggio in emoji."
@ -94,6 +95,6 @@ class EmojifyCommand(Command):
new_string = new_string.replace(key, selected_emoji) new_string = new_string.replace(key, selected_emoji)
return new_string return new_string
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
string = args.joined(require_at_least=1) string = args.joined(require_at_least=1)
await data.reply(self._emojify(string)) await data.reply(self._emojify(string))

View file

@ -1,9 +1,9 @@
import royalnet from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.backpack.tables import * import royalnet.backpack.tables as rbt
class EvalCommand(Command): class EvalCommand(rc.Command):
# oh god if there is a security vulnerability # oh god if there is a security vulnerability
name: str = "eval" name: str = "eval"
@ -11,13 +11,13 @@ class EvalCommand(Command):
syntax: str = "{espressione}" syntax: str = "{espressione}"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
user: User = await data.get_author(error_if_none=True) user: rbt.User = await data.get_author(error_if_none=True)
if user.role != "Admin": if "admin" not in user.roles:
raise CommandError("Non sei autorizzato a eseguire codice arbitrario!\n" raise rc.CommandError("Non sei autorizzato a eseguire codice arbitrario!\n"
"(Sarebbe un po' pericoloso se te lo lasciassi eseguire, non trovi?)") "(Sarebbe un po' pericoloso se te lo lasciassi eseguire, non trovi?)")
try: try:
result = eval(args.joined(require_at_least=1)) result = eval(args.joined(require_at_least=1))
except Exception as e: except Exception as e:
raise CommandError(f"Eval fallito: {e}") raise rc.CommandError(f"Eval fallito: {e}")
await data.reply(repr(result)) await data.reply(repr(result))

View file

@ -1,9 +1,9 @@
import royalnet from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.backpack.tables import * import royalnet.backpack.tables as rbt
class ExecCommand(Command): class ExecCommand(rc.Command):
# oh god if there is a security vulnerability # oh god if there is a security vulnerability
name: str = "exec" name: str = "exec"
@ -11,13 +11,13 @@ class ExecCommand(Command):
syntax: str = "{script}" syntax: str = "{script}"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
user: User = await data.get_author(error_if_none=True) user: rbt.User = await data.get_author(error_if_none=True)
if user.role != "Admin": if "admin" not in user.roles:
raise CommandError("Non sei autorizzato a eseguire codice arbitrario!\n" raise rc.CommandError("Non sei autorizzato a eseguire codice arbitrario!\n"
"(Sarebbe un po' pericoloso se te lo lasciassi eseguire, non trovi?)") "(Sarebbe un po' pericoloso se te lo lasciassi eseguire, non trovi?)")
try: try:
exec(args.joined(require_at_least=1)) exec(args.joined(require_at_least=1))
except Exception as e: except Exception as e:
raise CommandError(f"Esecuzione fallita: {e}") raise rc.CommandError(f"Esecuzione fallita: {e}")
await data.reply(f"✅ Fatto!") await data.reply(f"✅ Fatto!")

View file

@ -0,0 +1,53 @@
from typing import *
import royalnet
import royalnet.commands as rc
import random
import datetime
class FortuneCommand(rc.Command):
name: str = "fortune"
description: str = "Quanto sarai fortunato oggi?"
syntax: str = ""
_fortunes = [
"😄 Oggi sarà una fantastica giornata!",
"😌 Oggi sarà una giornata molto chill e rilassante.",
"💰 Oggi sui tuoi alberi cresceranno più Stelline!",
"🍎 Oggi un unicorno ti lascerà la sua Blessed Apple!",
"📈 Oggi il tuo team in ranked sarà più amichevole e competente del solito!",
"🏝 Oggi potrai raggiungere l'Isola Miraggio!",
"🐱 Oggi vedrai più gatti del solito su Internet!",
"🐶 Oggi vedrai più cani del solito su Internet!",
"🐦 Oggi vedrai più uccelli del solito su Internet!",
"🔥 Oggi vedrai più flame del solito su Internet!",
"🤬 Oggi vedrai più discorsi politici del solito su Internet!",
"🐌 Oggi incontrerai una chiocciola sperduta!",
"🎁 Oggi i dispenser di regali in centro funzioneranno senza problemi!",
"🥕 Oggi il tuo raccolto avrà qualità Iridium Star!",
"🔴 Oggi troverai più oggetti di rarità rossa del solito!",
"✨ Oggi farai molti più multicast!",
"♦️ Oggi troverai una Leggendaria Dorata!",
"⭐️ Oggi la stella della RYG ti sembrerà un pochino più dritta!",
"⭐️ Oggi la stella della RYG ti sembrerà anche più storta del solito!",
"💎 Oggi i tuoi avversari non riusciranno a deflettere i tuoi Emerald Splash!",
"⁉️ Oggi le tue supercazzole prematureranno un po' più a sinistra!",
"🌅 Oggi sarà il giorno dopo ieri e il giorno prima di domani!",
"🤖 Oggi il Royal Bot ti dirà qualcosa di molto utile!",
"💤 Oggi rischierai di addormentarti più volte!",
"🥪 Oggi ti verrà fame fuori orario!",
"😓 Oggi dirai molte stupidaggini!",
]
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
author = await data.get_author()
today = datetime.date.today()
h = author.uid * hash(today)
r = random.Random(x=h)
message = r.sample(self._fortunes, 1)[0]
await data.reply(message)

View file

@ -0,0 +1,42 @@
from typing import *
import royalnet.commands as rc
import royalnet.backpack.tables as rbt
from ..tables import FiorygiTransaction
class GivefiorygiCommand(rc.Command):
name: str = "givefiorygi"
description: str = "Cedi fiorygi a un altro utente."
syntax: str = "{destinatario} {quantità} {motivo}"
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
author = await data.get_author(error_if_none=True)
user_arg = args[0]
qty_arg = args[1]
if user_arg is None:
raise rc.InvalidInputError("Non hai specificato un destinatario!")
user = await rbt.User.find(self.alchemy, data.session, user_arg)
if user is None:
raise rc.InvalidInputError("L'utente specificato non esiste!")
if user.uid == author.uid:
raise rc.InvalidInputError("Non puoi inviare fiorygi a te stesso!")
if qty_arg is None:
raise rc.InvalidInputError("Non hai specificato una quantità!")
try:
qty = int(qty_arg)
except ValueError:
raise rc.InvalidInputError("La quantità specificata non è un numero!")
if qty <= 0:
raise rc.InvalidInputError("La quantità specificata deve essere almeno 1!")
if author.fiorygi.fiorygi < qty:
raise rc.InvalidInputError("Non hai abbastanza fiorygi per effettuare la transazione!")
await FiorygiTransaction.spawn_fiorygi(data, author, -qty, f"aver ceduto fiorygi a {user}")
await FiorygiTransaction.spawn_fiorygi(data, user, qty, f"aver ricevuto fiorygi da {author}")

View file

@ -0,0 +1,35 @@
from typing import *
import royalnet
import royalnet.commands as rc
import royalnet.utils as ru
from ..tables import Treasure, FiorygiTransaction
from .magicktreasure import MagicktreasureCommand
class GivetreasureCommand(MagicktreasureCommand):
name: str = "givetreasure"
description: str = "Crea un nuovo Treasure di Fiorygi (usando il tuo credito)"
syntax: str = "{codice} {valore}"
async def _permission_check(self, author, code, value, data):
if author.fiorygi.fiorygi < value:
raise rc.UserError("Non hai abbastanza fiorygi per creare questo Treasure.")
async def _create_treasure(self, author, code, value, data):
TreasureT = self.alchemy.get(Treasure)
treasure = await ru.asyncify(data.session.query(TreasureT).get, code)
if treasure is not None:
raise rc.UserError("Esiste già un Treasure con quel codice.")
treasure = TreasureT(
code=code,
value=value,
redeemed_by=None
)
await FiorygiTransaction.spawn_fiorygi(data, author, -value, "aver creato un tesoro")
return treasure

View file

@ -1,16 +0,0 @@
from .play import PlayCommand
class GooglevideoCommand(PlayCommand):
name: str = "googlevideo"
aliases = ["gv"]
description: str = "Cerca un video su Google Video e lo aggiunge alla coda della chat vocale."
syntax = "{ricerca}"
async def get_url(self, args):
return f"gvsearch:{args.joined()}"
# Too bad gvsearch: always finds nothing.

View file

@ -0,0 +1,37 @@
from typing import *
import royalnet
import royalnet.commands as rc
class HelpCommand(rc.Command):
name: str = "help"
description: str = "Visualizza informazioni su un comando."
syntax: str = "{comando}"
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
if len(args) == 0:
message = [
" Comandi disponibili:"
]
for command in sorted(list(set(self.serf.commands.values())), key=lambda c: c.name):
message.append(f"- [c]{self.interface.prefix}{command.name}[/c]")
await data.reply("\n".join(message))
else:
name: str = args[0].lstrip(self.interface.prefix)
try:
command: rc.Command = self.serf.commands[f"{self.interface.prefix}{name}"]
except KeyError:
raise rc.InvalidInputError("Il comando richiesto non esiste.")
message = [
f" [c]{self.interface.prefix}{command.name} {command.syntax}[/c]",
"",
f"{command.description}"
]
await data.reply("\n".join(message))

View file

@ -1,97 +1,84 @@
import typing from typing import *
import riotwatcher import riotwatcher
import logging import logging
import asyncio import asyncio
import sentry_sdk import sentry_sdk
from royalnet.commands import * import royalnet.commands as rc
from royalnet.utils import * import royalnet.utils as ru
from royalnet.serf.telegram import * import royalnet.serf.telegram as rst
from ..tables import LeagueOfLegends from royalnet.backpack import tables as rbt
from ..utils import LeagueLeague
from .abstract.linker import LinkerCommand
from ..tables import LeagueOfLegends, FiorygiTransaction
from ..types import LeagueLeague, Updatable
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class LeagueoflegendsCommand(Command): class LeagueoflegendsCommand(LinkerCommand):
name: str = "leagueoflegends" name: str = "leagueoflegends"
aliases = ["lol", "league"] aliases = ["lol", "league"]
description: str = "Connetti un account di League of Legends a un account Royalnet, e visualizzane le statistiche." description: str = "Connetti un account di League of Legends a un account Royalnet, o visualizzane le statistiche."
syntax = "[nomeevocatore]" syntax = "[nomeevocatore]"
def __init__(self, interface: CommandInterface): queue_names = {
"rank_soloq": "Solo/Duo",
"rank_flexq": "Flex",
}
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface) super().__init__(interface)
self._riotwatcher = riotwatcher.RiotWatcher(api_key=self.config["Lol"]["token"]) self._lolwatcher: Optional[riotwatcher.RiotWatcher] = None
if self.interface.name == "telegram": self._tftwatcher: Optional[riotwatcher.RiotWatcher] = None
self.loop.create_task(self._updater(900)) if self.enabled():
self._lolwatcher = riotwatcher.LolWatcher(api_key=self.token())
self._tftwatcher = riotwatcher.TftWatcher(api_key=self.token())
async def _send(self, message): def token(self):
client = self.serf.client return self.config["leagueoflegends"]["token"]
await self.serf.api_call(client.send_message,
chat_id=self.config["Telegram"]["main_group_id"],
text=escape(message),
parse_mode="HTML",
disable_webpage_preview=True)
async def _notify(self, def region(self):
obj: LeagueOfLegends, return self.config["leagueoflegends"]["region"]
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 def describe(self, obj: LeagueOfLegends) -> str:
async def _change(obj: LeagueOfLegends, string = f" [b]{obj.summoner_name}[/b]\n" \
attribute_name: str, f"Lv. {obj.summoner_level}\n" \
new_value: typing.Any, f"Mastery score: {obj.mastery_score}\n" \
callback: typing.Callable[ f"\n"
[LeagueOfLegends, str, typing.Any, typing.Any], typing.Awaitable[None]]): if obj.rank_soloq:
old_value = obj.__getattribute__(attribute_name) string += f"Solo: {obj.rank_soloq}\n"
if old_value != new_value: if obj.rank_flexq:
await callback(obj, attribute_name, old_value, new_value) string += f"Flex: {obj.rank_flexq}\n"
obj.__setattr__(attribute_name, new_value) return string
async def _update(self, lol: LeagueOfLegends): async def get_updatables_of_user(self, session, user: rbt.User) -> List[LeagueOfLegends]:
log.info(f"Updating: {lol}") return await ru.asyncify(session.query(self.alchemy.get(LeagueOfLegends)).filter_by(user=user).all)
log.debug(f"Getting summoner data: {lol}")
summoner = await asyncify(self._riotwatcher.summoner.by_id, region=self.config["Lol"]["region"], async def get_updatables(self, session) -> List[LeagueOfLegends]:
encrypted_summoner_id=lol.summoner_id) return await ru.asyncify(session.query(self.alchemy.get(LeagueOfLegends)).all)
await self._change(lol, "profile_icon_id", summoner["profileIconId"], self._notify)
await self._change(lol, "summoner_name", summoner["name"], self._notify) async def create(self,
await self._change(lol, "puuid", summoner["puuid"], self._notify) session,
await self._change(lol, "summoner_level", summoner["summonerLevel"], self._notify) user: rbt.User,
await self._change(lol, "summoner_id", summoner["id"], self._notify) args: rc.CommandArgs,
await self._change(lol, "account_id", summoner["accountId"], self._notify) data: Optional[rc.CommandData] = None) -> Optional[LeagueOfLegends]:
log.debug(f"Getting leagues data: {lol}") name = args.joined()
leagues = await asyncify(self._riotwatcher.league.by_summoner, region=self.config["Lol"]["region"],
encrypted_summoner_id=lol.summoner_id) # Connect a new League of Legends account to Royalnet
log.debug(f"Searching for: {name}")
summoner = self._lolwatcher.summoner.by_name(region=self.region(), summoner_name=name)
# Ensure the account isn't already connected to something else
leagueoflegends = await ru.asyncify(
session.query(self.alchemy.get(LeagueOfLegends)).filter_by(summoner_id=summoner["id"]).one_or_none)
if leagueoflegends:
raise rc.CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.")
# Get rank information
log.debug(f"Getting leagues data: {name}")
leagues = self._lolwatcher.league.by_summoner(region=self.region(),
encrypted_summoner_id=summoner["id"])
soloq = LeagueLeague() soloq = LeagueLeague()
flexq = LeagueLeague() flexq = LeagueLeague()
twtrq = LeagueLeague() twtrq = LeagueLeague()
@ -105,118 +92,80 @@ class LeagueoflegendsCommand(Command):
twtrq = LeagueLeague.from_dict(league) twtrq = LeagueLeague.from_dict(league)
if league["queueType"] == "RANKED_TFT": if league["queueType"] == "RANKED_TFT":
tftq = LeagueLeague.from_dict(league) tftq = LeagueLeague.from_dict(league)
await self._change(lol, "rank_soloq", soloq, self._notify) # Get mastery score
await self._change(lol, "rank_flexq", flexq, self._notify) log.debug(f"Getting mastery data: {name}")
await self._change(lol, "rank_twtrq", twtrq, self._notify) mastery = self._lolwatcher.champion_mastery.scores_by_summoner(region=self.region(),
await self._change(lol, "rank_tftq", tftq, self._notify) encrypted_summoner_id=summoner["id"])
log.debug(f"Getting mastery data: {lol}") # Create database row
mastery = await asyncify(self._riotwatcher.champion_mastery.scores_by_summoner, leagueoflegends = self.alchemy.get(LeagueOfLegends)(
region=self.config["Lol"]["region"], region=self.region(),
encrypted_summoner_id=lol.summoner_id) user=user,
await self._change(lol, "mastery_score", mastery, self._notify) 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
)
async def _updater(self, period: int): await FiorygiTransaction.spawn_fiorygi(
log.info(f"Started updater with {period}s period") data=data,
while True: user=user,
log.info(f"Updating...") qty=1,
session = self.alchemy.Session() reason="aver collegato a Royalnet il proprio account di League of Legends"
log.info("") )
lols = session.query(self.alchemy.get(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)
@staticmethod session.add(leagueoflegends)
def _display(lol: LeagueOfLegends) -> str: return 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: async def update(self, session, obj: LeagueOfLegends, change: Callable[[str, Any], Awaitable[None]]):
author = await data.get_author(error_if_none=True) log.debug(f"Getting summoner data: {obj}")
summoner = await ru.asyncify(self._lolwatcher.summoner.by_id, region=self.region(),
encrypted_summoner_id=obj.summoner_id)
await change("profile_icon_id", summoner["profileIconId"])
await change("summoner_name", summoner["name"])
await change("puuid", summoner["puuid"])
await change("summoner_level", summoner["summonerLevel"])
await change("summoner_id", summoner["id"])
await change("account_id", summoner["accountId"])
log.debug(f"Getting leagues data: {obj}")
leagues = await ru.asyncify(self._lolwatcher.league.by_summoner, region=self.region(),
encrypted_summoner_id=obj.summoner_id)
soloq = LeagueLeague()
flexq = 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)
await change("rank_soloq", soloq)
await change("rank_flexq", flexq)
log.debug(f"Getting mastery data: {obj}")
mastery = await ru.asyncify(self._lolwatcher.champion_mastery.scores_by_summoner,
region=self.region(),
encrypted_summoner_id=obj.summoner_id)
await change("mastery_score", mastery)
name = args.joined() async def on_increase(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None:
if attribute in self.queue_names.keys():
await self.notify(f"📈 [b]{obj.user}[/b] è salito a {new} su League of Legends ({self.queue_names[attribute]})! Congratulazioni!")
if name: async def on_unchanged(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None:
# Connect a new League of Legends account to Royalnet pass
log.debug(f"Searching for: {name}")
summoner = self._riotwatcher.summoner.by_name(region=self.config["Lol"]["region"], summoner_name=name) async def on_decrease(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: Any) -> None:
# Ensure the account isn't already connected to something else if attribute in self.queue_names.keys():
leagueoflegends = await asyncify( await self.notify(f"📉 [b]{obj.user}[/b] è sceso a {new} su League of Legends ({self.queue_names[attribute]}).")
data.session.query(self.alchemy.get(LeagueOfLegends)).filter_by(summoner_id=summoner["id"]).one_or_none)
if leagueoflegends: async def on_first(self, session, obj: LeagueOfLegends, attribute: str, old: None, new: Any) -> None:
raise CommandError(f"L'account {leagueoflegends} è già registrato su Royalnet.") if attribute in self.queue_names.keys():
# Get rank information await self.notify(f"🌟 [b]{obj.user}[/b] si è classificato {new} su League of Legends ({self.queue_names[attribute]}!")
log.debug(f"Getting leagues data: {name}")
leagues = self._riotwatcher.league.by_summoner(region=self.config["Lol"]["region"], async def on_reset(self, session, obj: LeagueOfLegends, attribute: str, old: Any, new: None) -> None:
encrypted_summoner_id=summoner["id"]) if attribute in self.queue_names.keys():
soloq = LeagueLeague() await self.notify(f"⬜️ [b]{obj.user}[/b] non ha più un rank su League of Legends ({self.queue_names[attribute]}).")
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.config["Lol"]["region"],
encrypted_summoner_id=summoner["id"])
# Create database row
leagueoflegends = self.alchemy.get(LeagueOfLegends)(
region=self.config["Lol"]["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 UserError("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)

View file

@ -0,0 +1,40 @@
from typing import *
import royalnet.commands as rc
import royalnet.backpack.tables as rbt
from ..tables import FiorygiTransaction
class MagickfiorygiCommand(rc.Command):
name: str = "magickfiorygi"
description: str = "Crea fiorygi dal nulla."
syntax: str = "{destinatario} {quantità} {motivo}"
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
author = await data.get_author(error_if_none=True)
if "banker" not in author.roles:
raise rc.UserError("Non hai permessi sufficienti per eseguire questo comando.")
user_arg = args[0]
qty_arg = args[1]
reason_arg = " ".join(args[2:])
if user_arg is None:
raise rc.InvalidInputError("Non hai specificato un destinatario!")
user = await rbt.User.find(self.alchemy, data.session, user_arg)
if user is None:
raise rc.InvalidInputError("L'utente specificato non esiste!")
if qty_arg is None:
raise rc.InvalidInputError("Non hai specificato una quantità!")
try:
qty = int(qty_arg)
except ValueError:
raise rc.InvalidInputError("La quantità specificata non è un numero!")
if reason_arg == "":
raise rc.InvalidInputError("Non hai specificato un motivo!")
await FiorygiTransaction.spawn_fiorygi(data, user, qty, reason_arg)

View file

@ -0,0 +1,53 @@
from typing import *
import royalnet
import royalnet.commands as rc
import royalnet.utils as ru
from ..tables import Treasure
class MagicktreasureCommand(rc.Command):
name: str = "magicktreasure"
description: str = "Crea un nuovo Treasure di Fiorygi (senza spendere i tuoi)."
syntax: str = "{codice} {valore}"
async def _permission_check(self, author, code, value, data):
if "banker" not in author.roles:
raise rc.UserError("Non hai permessi sufficienti per eseguire questo comando.")
return author
async def _create_treasure(self, author, code, value, data):
TreasureT = self.alchemy.get(Treasure)
treasure = await ru.asyncify(data.session.query(TreasureT).get, code)
if treasure is not None:
raise rc.UserError("Esiste già un Treasure con quel codice.")
treasure = TreasureT(
code=code,
value=value,
redeemed_by=None
)
return treasure
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
await data.delete_invoking()
author = await data.get_author(error_if_none=True)
code = args[0].lower()
try:
value = int(args[1])
except ValueError:
raise rc.InvalidInputError("Il valore deve essere maggiore o uguale a 0.")
if value < 0:
raise rc.InvalidInputError("Il valore deve essere maggiore o uguale a 0.")
await self._permission_check(author, code, value, data)
treasure = await self._create_treasure(author, code, value, data)
data.session.add(treasure)
await data.session_commit()
await data.reply("✅ Treasure creato!")

View file

@ -0,0 +1,89 @@
from typing import *
import datetime
import re
import dateparser
import typing
import royalnet.commands as rc
from ..tables import MMEvent
from ..utils import MMTask
class MatchmakingCommand(rc.Command):
name: str = "matchmaking"
description: str = "Cerca persone per una partita a qualcosa!"
syntax: str = ""
aliases = ["mm", "lfg"]
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface)
# Find all active MMEvents and run the tasks for them
session = self.alchemy.Session()
# Create a new MMEvent and run it
if self.interface.name == "telegram":
MMEventT = self.alchemy.get(MMEvent)
active_mmevents = (
session
.query(MMEventT)
.filter(
MMEventT.interface == self.interface.name,
MMEventT.interrupted == False
)
.all()
)
for mmevent in active_mmevents:
task = MMTask(mmevent.mmid, command=self)
task.start()
@staticmethod
def _parse_args(args) -> Tuple[Optional[datetime.datetime], str, str]:
"""Parse command arguments, either using the standard syntax or the Proto syntax."""
try:
timestring, title, description = args.match(r"(?:\[\s*([^]]+)\s*]\s*)?([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL)
except rc.InvalidInputError:
timestring, title, description = args.match(r"(?:\s*(.+?)\s*\n\s*)?([^\n]+)\s*\n?\s*(.+)?\s*", re.DOTALL)
if timestring is not None:
try:
dt: typing.Optional[datetime.datetime] = dateparser.parse(timestring, settings={
"PREFER_DATES_FROM": "future"
})
except OverflowError:
dt = None
if dt is None:
raise rc.InvalidInputError("La data che hai specificato non è valida.")
if dt <= datetime.datetime.now():
raise rc.InvalidInputError("La data che hai specificato è nel passato.")
if dt - datetime.datetime.now() >= datetime.timedelta(days=366):
raise rc.InvalidInputError("Hai specificato una data tra più di un anno!\n"
"Se volevi scrivere un'orario, ricordati che le ore sono separate da "
"due punti (:) e non da punto semplice!")
else:
dt = None
return dt, title, description
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
"""Handle a matchmaking command call."""
author = await data.get_author(error_if_none=True)
# Parse the arguments, either with the standard syntax or with the Proto syntax
dt, title, description = self._parse_args(args)
# Add the MMEvent to the database
mmevent: MMEvent = self.alchemy.get(MMEvent)(creator=author,
datetime=dt,
title=title,
description=description,
interface=self.interface.name)
data.session.add(mmevent)
await data.session_commit()
# Create and run a task for the newly created MMEvent
task = MMTask(mmevent.mmid, command=self)
task.start()
await data.reply(f"🚩 Matchmaking creato!")

125
royalpack/commands/osu.py Normal file
View file

@ -0,0 +1,125 @@
from typing import *
import itsdangerous
import aiohttp
from royalnet.backpack import tables as rbt
import royalnet.commands as rc
import royalnet.utils as ru
from .abstract.linker import LinkerCommand
from ..types import Updatable
from ..tables import Osu
from ..stars.api_auth_login_osu import ApiAuthLoginOsuStar
class OsuCommand(LinkerCommand):
name = "osu"
description = "Connetti e sincronizza il tuo account di osu!"
@property
def client_id(self):
return self.config[self.name]['client_id']
@property
def client_secret(self):
return self.config[self.name]['client_secret']
@property
def base_url(self):
return self.config['base_url']
@property
def secret_key(self):
return self.config['secret_key']
async def get_updatables_of_user(self, session, user: rbt.User) -> List[Osu]:
return user.osu
async def get_updatables(self, session) -> List[Osu]:
return await ru.asyncify(session.query(self.alchemy.get(Osu)).all)
async def create(self,
session,
user: rbt.User,
args: rc.CommandArgs,
data: Optional[rc.CommandData] = None) -> Optional[Osu]:
serializer = itsdangerous.URLSafeSerializer(self.secret_key, salt="osu")
# TODO: Ensure the chat the link is being sent in is secure!!!
await data.reply("🔑 [b]Login necessario[/b]\n"
f"[url=https://osu.ppy.sh/oauth/authorize"
f"?client_id={self.client_id}"
f"&redirect_uri={self.base_url}{ApiAuthLoginOsuStar.path}"
f"&response_type=code"
f"&state={serializer.dumps(user.uid)}]"
f"Connetti account di osu! a {user.username}"
f"[/url]")
return None
async def update(self, session, obj: Osu, change: Callable[[str, Any], Awaitable[None]]):
await obj.refresh_if_expired(client_id=self.client_id,
client_secret=self.client_secret,
base_url=self.base_url,
path=ApiAuthLoginOsuStar.path)
async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {obj.access_token}"}) as session:
async with session.get("https://osu.ppy.sh/api/v2/me/osu") as response:
m = await response.json()
obj.avatar_url = m["avatar_url"]
obj.username = m["username"]
if "statistics" in m:
await change("standard_pp", m["statistics"].get("pp"))
async with session.get("https://osu.ppy.sh/api/v2/me/taiko") as response:
m = await response.json()
if "statistics" in m:
await change("taiko_pp", m["statistics"].get("pp"))
async with session.get("https://osu.ppy.sh/api/v2/me/fruits") as response:
m = await response.json()
if "statistics" in m:
await change("catch_pp", m["statistics"].get("pp"))
async with session.get("https://osu.ppy.sh/api/v2/me/mania") as response:
m = await response.json()
if "statistics" in m:
await change("mania_pp", m["statistics"].get("pp"))
async def on_increase(self, session, obj: Osu, attribute: str, old: Any, new: Any) -> None:
if attribute == "standard_pp":
await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu![/i]! Congratulazioni!")
elif attribute == "taiko_pp":
await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu!taiko[/i]! Congratulazioni!")
elif attribute == "catch_pp":
await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu!catch[/i]! Congratulazioni!")
elif attribute == "mania_pp":
await self.notify(f"📈 [b]{obj.user}[/b] è salito a [b]{new:.0f}pp[/b] su [i]osu!mania[/i]! Congratulazioni!")
async def on_unchanged(self, session, obj: Osu, attribute: str, old: Any, new: Any) -> None:
pass
async def on_decrease(self, session, obj: Osu, attribute: str, old: Any, new: Any) -> None:
if attribute == "standard_pp":
await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu![/i].")
elif attribute == "taiko_pp":
await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu!taiko[/i].")
elif attribute == "catch_pp":
await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu!catch[/i].")
elif attribute == "mania_pp":
await self.notify(f"📉 [b]{obj.user}[/b] è sceso a [b]{new:.0f}pp[/b] su [i]osu!mania[/i].")
async def on_first(self, session, obj: Osu, attribute: str, old: None, new: Any) -> None:
if attribute == "standard_pp":
await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu![/i]!")
elif attribute == "taiko_pp":
await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu!taiko[/i]!")
elif attribute == "catch_pp":
await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu!catch[/i]!")
elif attribute == "mania_pp":
await self.notify(f"⭐️ [b]{obj.user}[/b] ha guadagnato i suoi primi [b]{new:.0f}pp[/b] su [i]osu!mania[/i]!")
async def on_reset(self, session, obj: Osu, attribute: str, old: Any, new: None) -> None:
if attribute == "standard_pp":
await self.notify(f"⬜️ [b]{obj.user}[/b] non è più classificato su [i]osu![/i].")
elif attribute == "taiko_pp":
await self.notify(f" ⬜️[b]{obj.user}[/b] non è più classificato su [i]osu!taiko[/i].")
elif attribute == "catch_pp":
await self.notify(f"⬜️ [b]{obj.user}[/b] non è più classificato su [i]osu!catch[/i].")
elif attribute == "mania_pp":
await self.notify(f"⬜️ [b]{obj.user}[/b] non è più classificato su [i]osu!mania[/i].")

View file

@ -1,27 +0,0 @@
import discord
from typing import *
from royalnet.commands import *
class PauseCommand(Command):
name: str = "pause"
aliases = ["resume"]
description: str = "Metti in pausa o riprendi la riproduzione di un file."
async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name == "discord":
message: discord.Message = data.message
guild: discord.Guild = message.guild
guild_id: Optional[int] = guild.id
else:
guild_id = None
response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_pause",
guild_id=guild_id)
if response["action"] == "paused":
await data.reply("⏸ Riproduzione messa in pausa.")
elif response["action"] == "resumed":
await data.reply("▶️ Riproduzione ripresa!")

View file

@ -1,24 +0,0 @@
from .play import PlayCommand
from royalnet.commands import *
import aiohttp
import urllib.parse
class PeertubeCommand(PlayCommand):
name: str = "peertube"
aliases = ["pt", "royaltube", "rt"]
description: str = "Cerca un video su RoyalTube e lo aggiunge alla coda della chat vocale."
syntax = "{ricerca}"
async def get_url(self, args):
search = urllib.parse.quote(args.joined(require_at_least=1))
async with aiohttp.ClientSession() as session:
async with session.get(self.config["Peertube"]["instance_url"] +
f"/api/v1/search/videos?search={search}") as response:
j = await response.json()
if j["total"] < 1:
raise InvalidInputError("Nessun video trovato.")
return f'{self.config["Peertube"]["instance_url"]}/videos/watch/{j["data"][0]["uuid"]}'

View file

@ -1,16 +1,17 @@
from typing import *
import aiohttp import aiohttp
import asyncio import asyncio
import datetime import datetime
import logging import logging
import dateparser import dateparser
from royalnet.commands import * import royalnet.commands as rc
from royalnet.serf.telegram.escape import escape import royalnet.serf.telegram as rst
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class PeertubeUpdatesCommand(Command): class PeertubeUpdatesCommand(rc.Command):
name: str = "peertubeupdates" name: str = "peertubeupdates"
description: str = "Guarda quando è uscito l'ultimo video su PeerTube." description: str = "Guarda quando è uscito l'ultimo video su PeerTube."
@ -21,7 +22,7 @@ class PeertubeUpdatesCommand(Command):
_latest_date: datetime.datetime = None _latest_date: datetime.datetime = None
def __init__(self, interface: CommandInterface): def __init__(self, interface: rc.CommandInterface):
super().__init__(interface) super().__init__(interface)
if self.interface.name == "telegram": if self.interface.name == "telegram":
self.loop.create_task(self._ready_up()) self.loop.create_task(self._ready_up())
@ -33,6 +34,8 @@ class PeertubeUpdatesCommand(Command):
async with session.get(self.config["Peertube"]["instance_url"] + async with session.get(self.config["Peertube"]["instance_url"] +
"/feeds/videos.json?sort=-publishedAt&filter=local") as response: "/feeds/videos.json?sort=-publishedAt&filter=local") as response:
log.debug("Parsing jsonfeed") log.debug("Parsing jsonfeed")
if response.status != 200:
raise rc.ExternalError("Peertube is unavailable")
j = await response.json() j = await response.json()
log.debug("Jsonfeed parsed successfully") log.debug("Jsonfeed parsed successfully")
return j return j
@ -41,14 +44,14 @@ class PeertubeUpdatesCommand(Command):
client = self.interface.bot.client client = self.interface.bot.client
await self.interface.bot.safe_api_call(client.send_message, await self.interface.bot.safe_api_call(client.send_message,
chat_id=self.config["Telegram"]["main_group_id"], chat_id=self.config["Telegram"]["main_group_id"],
text=escape(message), text=rst.escape(message),
parse_mode="HTML", parse_mode="HTML",
disable_webpage_preview=True) disable_webpage_preview=True)
async def _ready_up(self): async def _ready_up(self):
j = await self._get_json() j = await self._get_json()
if j["version"] != "https://jsonfeed.org/version/1": if j["version"] != "https://jsonfeed.org/version/1":
raise ConfigurationError("url is not a jsonfeed") raise rc.ConfigurationError("url is not a jsonfeed")
videos = j["items"] videos = j["items"]
for video in reversed(videos): for video in reversed(videos):
date_modified = dateparser.parse(video["date_modified"]) date_modified = dateparser.parse(video["date_modified"])
@ -72,7 +75,7 @@ class PeertubeUpdatesCommand(Command):
f"{video['url']}") f"{video['url']}")
await asyncio.sleep(self.config["Peertube"]["feed_update_timeout"]) await asyncio.sleep(self.config["Peertube"]["feed_update_timeout"])
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
if self.interface.name != "telegram": if self.interface.name != "telegram":
raise UnsupportedError() raise rc.UnsupportedError()
await data.reply(f" Ultimo video caricato il: [b]{self._latest_date.isoformat()}[/b]") await data.reply(f" Ultimo video caricato il: [b]{self._latest_date.isoformat()}[/b]")

View file

@ -0,0 +1,41 @@
import datetime
import asyncio
from typing import *
import royalnet
import royalnet.commands as rc
class PingCommand(rc.Command):
name: str = "ping"
description: str = "Display the status of the Herald network."
syntax: str = ""
_targets = ["telegram", "discord", "matrix", "constellation"]
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
await data.reply("📶 Ping...")
tasks = {}
start = datetime.datetime.now()
for target in self._targets:
tasks[target] = self.loop.create_task(self.interface.call_herald_event(target, "pong"))
await asyncio.sleep(10)
lines = ["📶 [b]Pong![/b]", ""]
for name, task in tasks.items():
try:
d = task.result()
except (asyncio.CancelledError, asyncio.InvalidStateError):
lines.append(f"🔴 [c]{name}[/c]")
else:
end = datetime.datetime.fromtimestamp(d["timestamp"])
delta = end - start
lines.append(f"🔵 [c]{name}[/c] ({delta.microseconds // 1000} ms)")
await data.reply("\n".join(lines))

View file

@ -1,62 +0,0 @@
import pickle
import base64
import discord
from typing import *
from royalnet.commands import *
from royalnet.utils import *
class PlayCommand(Command):
name: str = "play"
aliases = ["p"]
description: str = "Aggiunge un url alla coda della chat vocale."
syntax = "{url}"
async def get_url(self, args: CommandArgs):
return args.joined()
async def run(self, args: CommandArgs, data: CommandData) -> None:
# 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, come misura temporanea puoi usare "
# f"[c]ytsearch:nomevideo[/c] o [c]scsearch:nomevideo[/c] come url.")
if self.interface.name == "discord":
message: discord.Message = data.message
guild: discord.Guild = message.guild
if guild is None:
guild_id = None
else:
guild_id: Optional[int] = guild.id
else:
guild_id = None
response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_play",
url=await self.get_url(args),
guild_id=guild_id)
too_long: List[Dict[str, Any]] = response["too_long"]
if len(too_long) > 0:
await data.reply(f"{len(too_long)} file non {'è' if len(too_long) == 1 else 'sono'}"
f" stat{'o' if len(too_long) == 1 else 'i'} scaricat{'o' if len(too_long) == 1 else 'i'}"
f" perchè durava{'' if len(too_long) == 1 else 'no'}"
f" più di [c]{self.config['Play']['max_song_duration']}[/c] secondi.")
added: List[Dict[str, Any]] = response["added"]
if len(added) > 0:
reply = f"▶️ Aggiunt{'o' if len(added) == 1 else 'i'} {len(added)} file alla coda:\n"
if self.interface.name == "discord":
await data.reply(reply)
for item in added:
embed = pickle.loads(base64.b64decode(bytes(item["stringified_base64_pickled_discord_embed"],
encoding="ascii")))
# noinspection PyUnboundLocalVariable
await message.channel.send(embed=embed)
else:
reply += numberemojiformat([a["title"] for a in added])
await data.reply(reply)
if len(added) + len(too_long) == 0:
raise ExternalError("Nessun video trovato.")

View file

@ -1,11 +1,11 @@
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
class PmotsCommand(Command): class PmotsCommand(rc.Command):
name: str = "pmots" name: str = "pmots"
description: str = "Confondi Proto!" description: str = "Confondi Proto!"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
await data.reply("👣 pmots pmots") await data.reply("👣 pmots pmots")

View file

@ -1,62 +0,0 @@
import pickle
import base64
import discord
from typing import *
from royalnet.commands import *
from royalnet.utils import *
class QueueCommand(Command):
name: str = "queue"
aliases = ["q"]
description: str = "Visualizza la coda di riproduzione attuale.."
async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name == "discord":
message: discord.Message = data.message
guild: discord.Guild = message.guild
guild_id: Optional[int] = guild.id
else:
guild_id = None
response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_queue",
guild_id=guild_id)
queue_type = response["type"]
if queue_type == "RoyalQueue":
next_up = response["next_up"]
now_playing = response["now_playing"]
await data.reply(f" La coda contiene {len(next_up)} file.\n\n")
if now_playing is not None:
reply = f"Attualmente, sta venendo riprodotto:\n"
if self.interface.name == "discord":
await data.reply(reply)
embed = pickle.loads(base64.b64decode(bytes(now_playing["stringified_base64_pickled_discord_embed"],
encoding="ascii")))
# noinspection PyUnboundLocalVariable
await message.channel.send(embed=embed)
else:
reply += f"▶️ {now_playing['title']}\n\n"
await data.reply(reply)
else:
await data.reply("⏹ Attualmente, non sta venendo riprodotto nulla.")
reply = ""
if len(next_up) >= 1:
reply += "I prossimi file in coda sono:\n"
if self.interface.name == "discord":
await data.reply(reply)
for item in next_up[:5]:
embed = pickle.loads(base64.b64decode(bytes(item["stringified_base64_pickled_discord_embed"],
encoding="ascii")))
# noinspection PyUnboundLocalVariable
await message.channel.send(embed=embed)
else:
reply += numberemojiformat([a["title"] for a in next_up[:5]])
await data.reply(reply)
else:
await data.reply(" Non ci sono altri file in coda.")
else:
raise CommandError(f"Non so come visualizzare il contenuto di un [c]{queue_type}[/c].")

View file

@ -1,20 +1,22 @@
import typing from typing import *
import random import random
from royalnet.commands import * import royalnet.commands as rc
class RageCommand(Command): class RageCommand(rc.Command):
name: str = "rage" name: str = "rage"
aliases = ["balurage", "madden"] aliases = ["balurage", "madden"]
description: str = "Arrabbiati per qualcosa, come una software house californiana." description: str = "Arrabbiati per qualcosa, come una software house californiana."
_MAD = ["MADDEN MADDEN MADDEN MADDEN", _MAD = [
"EA bad, praise Geraldo!", "MADDEN MADDEN MADDEN MADDEN",
"Stai sfogando la tua ira sul bot!", "EA bad, praise Geraldo!",
"Basta, io cambio gilda!", "Stai sfogando la tua ira sul bot!",
"Fondiamo la RRYG!"] "Basta, io cambio gilda!",
"Fondiamo la RRYG!"
]
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
await data.reply(f"😠 {random.sample(self._MAD, 1)[0]}") await data.reply(f"😠 {random.sample(self._MAD, 1)[0]}")

View file

@ -1,18 +1,19 @@
import typing from typing import *
import dateparser import dateparser
import datetime import datetime
import pickle import pickle
import telegram import telegram
import discord import discord
from sqlalchemy import and_ from sqlalchemy import and_
from royalnet.commands import * import royalnet.commands as rc
from royalnet.utils import * import royalnet.utils as ru
from royalnet.serf.telegram import escape as telegram_escape from royalnet.serf.telegram import escape as telegram_escape
from royalnet.serf.discord import escape as discord_escape from royalnet.serf.discord import escape as discord_escape
from ..tables import Reminder from ..tables import Reminder
class ReminderCommand(Command): class ReminderCommand(rc.Command):
name: str = "reminder" name: str = "reminder"
aliases = ["calendar"] aliases = ["calendar"]
@ -21,7 +22,7 @@ class ReminderCommand(Command):
syntax: str = "[ {data} ] {messaggio}" syntax: str = "[ {data} ] {messaggio}"
def __init__(self, interface: CommandInterface): def __init__(self, interface: rc.CommandInterface):
super().__init__(interface) super().__init__(interface)
session = interface.alchemy.Session() session = interface.alchemy.Session()
reminders = ( reminders = (
@ -35,9 +36,9 @@ class ReminderCommand(Command):
interface.loop.create_task(self._remind(reminder)) interface.loop.create_task(self._remind(reminder))
async def _remind(self, reminder): async def _remind(self, reminder):
await sleep_until(reminder.datetime) await ru.sleep_until(reminder.datetime)
if self.interface.name == "telegram": if self.interface.name == "telegram":
chat_id: int = pickle.loads(reminder.raw_interface_data) chat_id: int = pickle.loads(reminder.interface_data)
client: telegram.Bot = self.serf.client client: telegram.Bot = self.serf.client
await self.serf.api_call(client.send_message, await self.serf.api_call(client.send_message,
chat_id=chat_id, chat_id=chat_id,
@ -45,19 +46,19 @@ class ReminderCommand(Command):
parse_mode="HTML", parse_mode="HTML",
disable_web_page_preview=True) disable_web_page_preview=True)
elif self.interface.name == "discord": elif self.interface.name == "discord":
channel_id: int = pickle.loads(reminder.raw_interface_data) channel_id: int = pickle.loads(reminder.interface_data)
client: discord.Client = self.serf.client client: discord.Client = self.serf.client
channel = client.get_channel(channel_id) channel = client.get_channel(channel_id)
await channel.send(discord_escape(f"❗️ {reminder.message}")) await channel.send(discord_escape(f"❗️ {reminder.message}"))
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
try: try:
date_str, reminder_text = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*") date_str, reminder_text = args.match(r"\[\s*([^]]+)\s*]\s*([^\n]+)\s*")
except InvalidInputError: except rc.InvalidInputError:
date_str, reminder_text = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*") date_str, reminder_text = args.match(r"\s*(.+?)\s*\n\s*([^\n]+)\s*")
try: try:
date: typing.Optional[datetime.datetime] = dateparser.parse(date_str, settings={ date: Optional[datetime.datetime] = dateparser.parse(date_str, settings={
"PREFER_DATES_FROM": "future" "PREFER_DATES_FROM": "future"
}) })
except OverflowError: except OverflowError:
@ -70,11 +71,11 @@ class ReminderCommand(Command):
return return
await data.reply(f"✅ Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]") await data.reply(f"✅ Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]")
if self.interface.name == "telegram": if self.interface.name == "telegram":
interface_data = pickle.dumps(data.update.effective_chat.id) interface_data = pickle.dumps(data.message.chat.id)
elif self.interface.name == "discord": elif self.interface.name == "discord":
interface_data = pickle.dumps(data.message.channel.id) interface_data = pickle.dumps(data.message.channel.id)
else: else:
raise UnsupportedError("This command does not support the current interface.") raise rc.UnsupportedError("This command does not support the current interface.")
creator = await data.get_author() creator = await data.get_author()
reminder = self.interface.alchemy.get(Reminder)(creator=creator, reminder = self.interface.alchemy.get(Reminder)(creator=creator,
interface_name=self.interface.name, interface_name=self.interface.name,
@ -83,4 +84,4 @@ class ReminderCommand(Command):
message=reminder_text) message=reminder_text)
self.interface.loop.create_task(self._remind(reminder)) self.interface.loop.create_task(self._remind(reminder))
data.session.add(reminder) data.session.add(reminder)
await asyncify(data.session.commit) await ru.asyncify(data.session.commit)

View file

@ -0,0 +1,25 @@
from typing import *
import pkg_resources
import royalnet.commands as rc
class RoyalpackCommand(rc.Command):
name: str = "royalpackversion"
description: str = "Visualizza la versione attuale di Royalpack."
syntax: str = ""
@property
def royalpack_version(self):
return pkg_resources.get_distribution("royalpack").version
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
if __debug__:
message = f" Royalpack [url=https://github.com/Steffo99/royalpack/]Unreleased[/url]\n"
else:
message = f" Royalpack [url=https://github.com/Steffo99/royalpack/releases/tag/{self.royalpack_version}]{self.royalpack_version}[/url]\n"
if "69" in semantic:
message += "(Nice.)"
await data.reply(message)

View file

@ -1,15 +1,16 @@
from typing import *
import re import re
from royalnet.commands import * import royalnet.commands as rc
class ShipCommand(Command): class ShipCommand(rc.Command):
name: str = "ship" name: str = "ship"
description: str = "Crea una ship tra due nomi." description: str = "Crea una ship tra due nomi."
syntax = "{nomeuno} {nomedue}" syntax = "{nomeuno} {nomedue}"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
name_one = args[0] name_one = args[0]
name_two = args[1] name_two = args[1]
if name_two == "+": if name_two == "+":

View file

@ -1,21 +0,0 @@
import discord
from typing import *
from royalnet.commands import *
class SkipCommand(Command):
name: str = "skip"
aliases = ["s"]
description: str = "Salta il file attualmente in riproduzione."
async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name == "discord":
message: discord.Message = data.message
guild: discord.Guild = message.guild
guild_id: Optional[int] = guild.id
else:
guild_id = None
response: Dict[str, Any] = await self.interface.call_herald_event("discord", "discord_skip", guild_id=guild_id)
await data.reply("⏩ File attuale saltato!")

View file

@ -1,9 +1,9 @@
import typing from typing import *
import random import random
from royalnet.commands import * import royalnet.commands as rc
class SmecdsCommand(Command): class SmecdsCommand(rc.Command):
name: str = "smecds" name: str = "smecds"
aliases = ["secondomeecolpadellostagista"] aliases = ["secondomeecolpadellostagista"]
@ -61,6 +61,6 @@ class SmecdsCommand(Command):
"dello Slime God", "del salassato", "della salsa", "di Senjougahara", "di Sugar", "della Stampa", "dello Slime God", "del salassato", "della salsa", "di Senjougahara", "di Sugar", "della Stampa",
"della Stampante"] "della Stampante"]
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
ds = random.sample(self._DS_LIST, 1)[0] ds = random.sample(self._DS_LIST, 1)[0]
await data.reply(f"🤔 Secondo me, è colpa {ds}.") await data.reply(f"🤔 Secondo me, è colpa {ds}.")

View file

@ -1,14 +0,0 @@
from .play import PlayCommand
class SoundcloudCommand(PlayCommand):
name: str = "soundcloud"
aliases = ["sc"]
description: str = "Cerca un video su SoundCloud e lo aggiunge alla coda della chat vocale."
syntax = "{ricerca}"
async def get_url(self, args):
return f"scsearch:{args.joined()}"

View file

@ -1,19 +1,17 @@
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.utils import * import royalnet.utils as ru
from royalnet.backpack.tables import User
from sqlalchemy import func
import royalspells as rs import royalspells as rs
class SpellCommand(Command): class SpellCommand(rc.Command):
name: str = "spell" name: str = "spell"
description: str = "Genera casualmente una spell!" description: str = "Genera casualmente una spell!"
syntax = "{nome_spell}" syntax = "{nome_spell}"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
spell_name = args.joined(require_at_least=1) spell_name = args.joined(require_at_least=1)
spell = rs.Spell(spell_name) spell = rs.Spell(spell_name)
@ -23,7 +21,7 @@ class SpellCommand(Command):
dmg: rs.DamageComponent = spell.damage_component dmg: rs.DamageComponent = spell.damage_component
constant_str: str = f"{dmg.constant:+d}" if dmg.constant != 0 else "" constant_str: str = f"{dmg.constant:+d}" if dmg.constant != 0 else ""
rows.append(f"Danni: [b]{dmg.dice_number}d{dmg.dice_type}{constant_str}[/b]" rows.append(f"Danni: [b]{dmg.dice_number}d{dmg.dice_type}{constant_str}[/b]"
f" {andformat(dmg.damage_types, final=' e ')}") f" {ru.andformat(dmg.damage_types, final=' e ')}")
rows.append(f"Precisione: [b]{dmg.miss_chance}%[/b]") rows.append(f"Precisione: [b]{dmg.miss_chance}%[/b]")
if dmg.repeat > 1: if dmg.repeat > 1:
rows.append(f"Multiattacco: [b]×{dmg.repeat}[/b]") rows.append(f"Multiattacco: [b]×{dmg.repeat}[/b]")
@ -39,7 +37,7 @@ class SpellCommand(Command):
stats: rs.StatsComponent = spell.stats_component stats: rs.StatsComponent = spell.stats_component
rows.append("Il caster riceve: ") rows.append("Il caster riceve: ")
for stat_name in stats.stat_changes: for stat_name in stats.stat_changes:
rows.append(f"[b]{stats.stat_changes[stat_name]}{stat_name}[/b]") rows.append(f"[b]{stat_name}{stats.stat_changes[stat_name]}[/b]")
rows.append("") rows.append("")
if spell.status_effect_component: if spell.status_effect_component:

View file

@ -0,0 +1,102 @@
from typing import *
import steam.webapi
import requests.exceptions
import royalnet.commands as rc
import royalnet.utils as ru
import royalnet.backpack.tables as rbt
from ..tables import Steam
class SteamGame:
def __init__(self,
appid=None,
name=None,
playtime_forever=None,
img_icon_url=None,
img_logo_url=None,
has_community_visible_stats=None,
playtime_windows_forever=None,
playtime_mac_forever=None,
playtime_linux_forever=None,
playtime_2weeks=None):
self.appid = appid
self.name = name
self.playtime_forever = playtime_forever
self.img_icon_url = img_icon_url
self.img_logo_url = img_logo_url
self.has_community_visible_stats = has_community_visible_stats
self.playtime_windows_forever = playtime_windows_forever
self.playtime_mac_forever = playtime_mac_forever
self.playtime_linux_forever = playtime_linux_forever
self.playtime_2weeks = playtime_2weeks
def __hash__(self):
return self.appid
def __eq__(self, other):
if isinstance(other, SteamGame):
return self.appid == other.appid
return False
def __str__(self):
return self.name
def __repr__(self):
return f"<{self.__class__.__qualname__} {self.appid} ({self.name})>"
class SteammatchCommand(rc.Command):
name: str = "steammatch"
description: str = "Vedi quali giochi hai in comune con uno o più membri!"
syntax: str = "{royalnet_username}+"
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface)
self._api = steam.webapi.WebAPI(self.config["steampowered"]["token"])
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
users = []
author = await data.get_author(error_if_none=True)
users.append(author)
for arg in args:
user = await rbt.User.find(self.alchemy, data.session, arg)
users.append(user)
if len(users) < 2:
raise rc.InvalidInputError("Devi specificare almeno un altro utente!")
shared_games: Optional[set] = None
for user in users:
user_games = set()
if len(user.steam) == 0:
raise rc.UserError(f"{user} non ha un account Steam registrato!")
for steam_account in user.steam:
steam_account: Steam
try:
response = await ru.asyncify(self._api.IPlayerService.GetOwnedGames,
steamid=steam_account._steamid,
include_appinfo=True,
include_played_free_games=True,
include_free_sub=True,
appids_filter=0)
except requests.exceptions.HTTPError:
raise rc.ExternalError(f"L'account Steam di {user} è privato!")
games = response["response"]["games"]
for game in games:
user_games.add(SteamGame(**game))
if shared_games is None:
shared_games = user_games
else:
shared_games = shared_games.intersection(user_games)
message_rows = [f"🎮 Giochi in comune tra {ru.andformat([str(user) for user in users], final=' e ')}:"]
for game in sorted(list(shared_games), key=lambda g: g.name):
message_rows.append(f"- {game}")
message = "\n".join(message_rows)
await data.reply(message)

View file

@ -0,0 +1,125 @@
from typing import *
import steam.steamid
import steam.webapi
import datetime
import royalnet.commands as rc
import royalnet.utils as ru
import logging
from royalnet.backpack import tables as rbt
from .abstract.linker import LinkerCommand
from ..tables import Steam, FiorygiTransaction
from ..types import Updatable
log = logging.getLogger(__name__)
class SteampoweredCommand(LinkerCommand):
name: str = "steampowered"
description: str = "Connetti e visualizza informazioni sul tuo account di Steam!"
syntax: str = "{url_profilo}"
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface)
self._api = steam.webapi.WebAPI(self.token())
def token(self):
return self.config["steampowered"]["token"]
async def get_updatables_of_user(self, session, user: rbt.User) -> List[Steam]:
return user.steam
async def get_updatables(self, session) -> List[Steam]:
return await ru.asyncify(session.query(self.alchemy.get(Steam)).all)
async def create(self,
session,
user: rbt.User,
args: rc.CommandArgs,
data: Optional[rc.CommandData] = None) -> Optional[Steam]:
url = args.joined()
steamid64 = await self._call(steam.steamid.steam64_from_url, url)
if steamid64 is None:
raise rc.InvalidInputError("Quel link non è associato ad alcun account Steam.")
response = await self._call(self._api.ISteamUser.GetPlayerSummaries_v2, steamids=steamid64)
r = response["response"]["players"][0]
steam_account = self.alchemy.get(Steam)(
user=user,
_steamid=int(steamid64),
persona_name=r["personaname"],
profile_url=r["profileurl"],
avatar=r["avatarfull"],
primary_clan_id=r["primaryclanid"],
account_creation_date=datetime.datetime.fromtimestamp(r["timecreated"])
)
await FiorygiTransaction.spawn_fiorygi(
data=data,
user=user,
qty=1,
reason="aver collegato a Royalnet il proprio account di League of Legends"
)
session.add(steam_account)
return steam_account
async def update(self, session, obj: Steam, change: Callable[[str, Any], Awaitable[None]]):
response = await self._call(self._api.ISteamUser.GetPlayerSummaries_v2, steamids=obj.steamid.as_64)
r = response["response"]["players"][0]
obj.persona_name = r["personaname"]
obj.profile_url = r["profileurl"]
obj.avatar = r["avatar"]
obj.primary_clan_id = r["primaryclanid"]
obj.account_creation_date = datetime.datetime.fromtimestamp(r["timecreated"])
response = await self._call(self._api.IPlayerService.GetSteamLevel_v1, steamid=obj.steamid.as_64)
obj.account_level = response["response"]["player_level"]
response = await self._call(self._api.IPlayerService.GetOwnedGames_v1,
steamid=obj.steamid.as_64,
include_appinfo=False,
include_played_free_games=True,
include_free_sub=False,
appids_filter=None)
obj.owned_games_count = response["response"]["game_count"]
if response["response"]["game_count"] >= 0:
obj.most_played_game_2weeks = sorted(response["response"]["games"], key=lambda g: -g.get("playtime_2weeks", 0))[0]["appid"]
obj.most_played_game_forever = sorted(response["response"]["games"], key=lambda g: -g.get("playtime_forever", 0))[0]["appid"]
async def on_increase(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None:
pass
async def on_unchanged(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None:
pass
async def on_decrease(self, session, obj: Updatable, attribute: str, old: Any, new: Any) -> None:
pass
async def on_first(self, session, obj: Updatable, attribute: str, old: None, new: Any) -> None:
pass
async def on_reset(self, session, obj: Updatable, attribute: str, old: Any, new: None) -> None:
pass
def describe(self, obj: Steam):
return f" [url={obj.profile_url}]{obj.persona_name}[/url]\n" \
f"[b]Level {obj.account_level}[/b]\n" \
f"\n" \
f"Owned games: [b]{obj.owned_games_count}[/b]\n" \
f"Most played 2 weeks: [url=https://store.steampowered.com/app/{obj.most_played_game_2weeks}]{obj.most_played_game_2weeks}[/url]\n" \
f"Most played forever: [url=https://store.steampowered.com/app/{obj.most_played_game_forever}]{obj.most_played_game_forever}[/url]\n" \
f"\n" \
f"SteamID32: [c]{obj.steamid.as_32}[/c]\n" \
f"SteamID64: [c]{obj.steamid.as_64}[/c]\n" \
f"SteamID2: [c]{obj.steamid.as_steam2}[/c]\n" \
f"SteamID3: [c]{obj.steamid.as_steam3}[/c]\n" \
f"\n" \
f"Created on: [b]{obj.account_creation_date}[/b]\n"
async def _call(self, method, *args, **kwargs):
log.debug(f"Calling {method}")
try:
return await ru.asyncify(method, *args, **kwargs)
except Exception as e:
raise rc.ExternalError("\n".join(e.args).replace(self.token(), "HIDDEN"))

View file

@ -1,28 +0,0 @@
import discord
from royalnet.commands import *
class SummonCommand(Command):
name: str = "summon"
aliases = ["cv"]
description: str = "Evoca il bot in un canale vocale."
syntax: str = "[nomecanale]"
async def run(self, args: CommandArgs, data: CommandData) -> None:
channel_name = args.joined()
if self.interface.name == "discord":
message: discord.Message = data.message
guild_id = message.guild.id
user_id = message.author.id
else:
guild_id = None
user_id = None
response = await self.interface.call_herald_event("discord", "discord_summon",
channel_name=channel_name, guild_id=guild_id, user_id=user_id)
if self.interface.name == "discord":
await data.reply(f"✅ Mi sono connesso in <#{response['channel']['id']}>!")
else:
await data.reply(f"✅ Mi sono connesso in [b]#{response['channel']['name']}[/b]!")

View file

@ -0,0 +1,33 @@
from typing import *
import royalnet
import royalnet.commands as rc
import royalnet.utils as ru
from ..tables import Treasure, FiorygiTransaction
class TreasureCommand(rc.Command):
name: str = "treasure"
description: str = "Riscatta un Treasure che hai trovato da qualche parte."
syntax: str = "{code}"
async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
author = await data.get_author(error_if_none=True)
code = args[0].lower()
TreasureT = self.alchemy.get(Treasure)
treasure = await ru.asyncify(data.session.query(TreasureT).get, code)
if treasure is None:
raise rc.UserError("Non esiste nessun Treasure con quel codice.")
if treasure.redeemed_by is not None:
raise rc.UserError(f"Quel tesoro è già stato riscattato da {treasure.redeemed_by}.")
treasure.redeemed_by = author
await data.session_commit()
await FiorygiTransaction.spawn_fiorygi(data,
author,
treasure.value,
f'aver trovato il tesoro "{treasure.code}"')
await data.reply("🤑 Tesoro riscattato!")

View file

@ -0,0 +1,147 @@
from typing import *
import asyncio
import aiohttp
import random
import uuid
import html
import royalnet.commands as rc
import royalnet.utils as ru
import royalnet.backpack.tables as rbt
from ..tables import TriviaScore
class TriviaCommand(rc.Command):
name: str = "trivia"
aliases = ["t"]
description: str = "Manda una domanda dell'OpenTDB in chat."
syntax = "[credits|scores]"
_letter_emojis = ["🇦", "🇧", "🇨", "🇩"]
_medal_emojis = ["🥇", "🥈", "🥉", "🔹"]
_correct_emoji = ""
_wrong_emoji = ""
_answer_time = 20
# _question_lock: bool = False
def __init__(self, interface: rc.CommandInterface):
super().__init__(interface)
self._answerers: Dict[uuid.UUID, Dict[str, bool]] = {}
async def run(self, args: rc.CommandArgs, data: rc.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 ru.asyncify(data.session.query(self.alchemy.get(TriviaScore)).all)
strings = ["🏆 [b]Trivia Leaderboards[/b]\n"]
for index, ts in enumerate(sorted(trivia_scores, key=lambda ts: -ts.score)):
if index > 3:
index = 3
strings.append(f"{self._medal_emojis[index]} {ts.user.username}: [b]{ts.score:.0f}p[/b]"
f" ({ts.correct_answers}/{ts.total_answers})")
await data.reply("\n".join(strings))
return
# if self._question_lock:
# raise rc.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 rc.CommandError(f"OpenTDB returned an error response_code ({j['response_code']}).")
question = j["results"][0]
text = f'❓ [b]{question["category"]}[/b]\n' \
f'{html.unescape(question["question"])}'
# Prepare answers
correct_answer: str = question["correct_answer"]
wrong_answers: List[str] = question["incorrect_answers"]
answers: 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: rc.CommandData):
answerer_ = await data.get_author(error_if_none=True)
try:
self._answerers[question_id][answerer_.uid] = True
except KeyError:
raise rc.UserError("Tempo scaduto!")
await data.reply("🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!")
async def wrong(data: rc.CommandData):
answerer_ = await data.get_author(error_if_none=True)
try:
self._answerers[question_id][answerer_.uid] = False
except KeyError:
raise rc.UserError("Tempo scaduto!")
await data.reply("🆗 Hai risposto alla domanda. Ora aspetta un attimo per i risultati!")
# Add question
keyboard: List[rc.KeyboardKey] = []
for index, answer in enumerate(answers):
if index == correct_index:
keyboard.append(rc.KeyboardKey(interface=self.interface,
short=self._letter_emojis[index],
text=answers[index],
callback=correct))
else:
keyboard.append(rc.KeyboardKey(interface=self.interface,
short=self._letter_emojis[index],
text=answers[index],
callback=wrong))
async with data.keyboard(text=text, keys=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.get(rbt.users.User)).get(answerer_id)
if answerer.trivia_score is None:
ts = self.interface.alchemy.get(TriviaScore)(user=answerer)
data.session.add(ts)
await ru.asyncify(data.session.commit)
previous_score = answerer.trivia_score.score
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
current_score = answerer.trivia_score.score
score_difference = current_score - previous_score
results += f" {answerer}: [b]{current_score:.0f}p[/b] ({score_difference:+.0f}p)\n"
await data.reply(results)
del self._answerers[question_id]
await ru.asyncify(data.session.commit)
# self._question_lock = False

View file

@ -1,11 +1,9 @@
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.utils import * import royalnet.backpack.tables as rbt
from royalnet.backpack.tables import User
from sqlalchemy import func
class UserinfoCommand(Command): class UserinfoCommand(rc.Command):
name: str = "userinfo" name: str = "userinfo"
aliases = ["uinfo", "ui", "useri"] aliases = ["uinfo", "ui", "useri"]
@ -14,31 +12,26 @@ class UserinfoCommand(Command):
syntax = "[username]" syntax = "[username]"
async def run(self, args: CommandArgs, data: CommandData) -> None: async def run(self, args: rc.CommandArgs, data: rc.CommandData) -> None:
username = args.optional(0) username = args.optional(0)
if username is None: if username is None:
user: User = await data.get_author(error_if_none=True) user: rbt.User = await data.get_author(error_if_none=True)
else: else:
found: Optional[User] = await asyncify( found: Optional[rbt.User] = await rbt.User.find(self.alchemy, data.session, username)
data.session
.query(self.alchemy.get(User))
.filter(func.lower(self.alchemy.get(User).username) == func.lower(username))
.one_or_none
)
if not found: if not found:
raise InvalidInputError("Utente non trovato.") raise rc.InvalidInputError("Utente non trovato.")
else: else:
user = found user = found
r = [ r = [
f" [b]{user.username}[/b] (ID: {user.uid})", f" [url=https://ryg.steffo.eu/#/user/{user.uid}]{user.username}[/url]",
f"{user.role}", f"{', '.join(user.roles)}",
"",
] ]
if user.fiorygi: if user.email:
r.append(f"{user.fiorygi}") r.append(f"{user.email}")
r.append("")
r.append("")
# Bios are a bit too long # Bios are a bit too long
# if user.bio: # if user.bio:
@ -50,18 +43,32 @@ class UserinfoCommand(Command):
for account in user.discord: for account in user.discord:
r.append(f"{account}") r.append(f"{account}")
for account in user.steam:
r.append(f"{account}")
if account.dota is not None:
r.append(f"{account.dota}")
if account.brawlhalla is not None:
r.append(f"{account.brawlhalla}")
for account in user.leagueoflegends: for account in user.leagueoflegends:
r.append(f"{account}") r.append(f"{account}")
r.append("") r.append("")
r.append(f"Ha creato [b]{len(user.diario_created)}[/b] righe di diario, e vi compare in" r.append(f"Ha creato [b]{len(user.diario_created)}[/b] righe di "
f"[url=https://ryg.steffo.eu/#/diario]Diario[/url], e vi compare in"
f" [b]{len(user.diario_quoted)}[/b] righe.") f" [b]{len(user.diario_quoted)}[/b] righe.")
r.append("") r.append("")
if user.trivia_score: if user.trivia_score:
r.append(f"Trivia: [b]{user.trivia_score.correct_answers}[/b] risposte corrette / " r.append(f"Ha [b]{user.trivia_score.score:.0f}[/b] punti Trivia, avendo risposto correttamente a"
f"{user.trivia_score.total_answers} totali") f" [b]{user.trivia_score.correct_answers}[/b] domande su"
f" [b]{user.trivia_score.total_answers}[/b].")
r.append("")
if user.fiorygi:
r.append(f"Ha [b]{user.fiorygi}[/b].")
r.append("")
await data.reply("\n".join(r)) await data.reply("\n".join(r))

View file

@ -1,49 +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 = "[nomecanale]"
async def run(self, args: CommandArgs, data: CommandData) -> None:
if self.interface.name != "discord":
raise UnsupportedError(f"{self} non è supportato su {self.interface.name}.")
bot: discord.Client = self.serf.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:
raise InvalidInputError("Non esiste alcun canale vocale con il nome specificato.")
elif len(matching_channels) > 1:
raise UserError("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 InvalidInputError("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}>")

View file

@ -1,16 +0,0 @@
from .play import PlayCommand
class YahoovideoCommand(PlayCommand):
name: str = "yahoovideo"
aliases = ["yv"]
description: str = "Cerca un video su Yahoo Video e lo aggiunge alla coda della chat vocale."
syntax = "{ricerca}"
async def get_url(self, args):
return f"yvsearch:{args.joined()}"
# Too bad yvsearch: always finds nothing.

View file

@ -1,14 +0,0 @@
from .play import PlayCommand
class YoutubeCommand(PlayCommand):
name: str = "youtube"
aliases = ["yt"]
description: str = "Cerca un video su YouTube e lo aggiunge alla coda della chat vocale."
syntax = "{ricerca}"
async def get_url(self, args):
return f"ytsearch:{args.joined()}"

View file

@ -5,6 +5,10 @@ from .discord_play import DiscordPlayEvent
from .discord_skip import DiscordSkipEvent from .discord_skip import DiscordSkipEvent
from .discord_queue import DiscordQueueEvent from .discord_queue import DiscordQueueEvent
from .discord_pause import DiscordPauseEvent from .discord_pause import DiscordPauseEvent
from .discord_playable import DiscordPlaymodeEvent
from .discord_lazy_play import DiscordLazyPlayEvent
from .telegram_message import TelegramMessageEvent
from .pong import PongEvent
# Enter the commands of your Pack here! # Enter the commands of your Pack here!
available_events = [ available_events = [
@ -14,6 +18,10 @@ available_events = [
DiscordSkipEvent, DiscordSkipEvent,
DiscordQueueEvent, DiscordQueueEvent,
DiscordPauseEvent, DiscordPauseEvent,
DiscordPlaymodeEvent,
DiscordLazyPlayEvent,
TelegramMessageEvent,
PongEvent,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__

View file

@ -0,0 +1,100 @@
import datetime
from typing import *
import discord
import royalnet.commands as rc
import royalnet.serf.discord as rsd
import royalnet.bard.discord as rbd
from ..utils import RoyalQueue, RoyalPool
class DiscordLazyPlayEvent(rc.Event):
name = "discord_lazy_play"
async def run(self,
urls: List[str],
guild_id: Optional[int] = None,
user: Optional[str] = None,
force_color: Optional[int] = None,
**kwargs) -> dict:
if not isinstance(self.serf, rsd.DiscordSerf):
raise rc.UnsupportedError()
serf: rsd.DiscordSerf = self.serf
client: discord.Client = self.serf.client
guild: discord.Guild = client.get_guild(guild_id) if guild_id is not None else None
candidate_players: List[rsd.VoicePlayer] = serf.find_voice_players(guild)
if len(candidate_players) == 0:
raise rc.UserError("Il bot non è in nessun canale vocale.\n"
"Evocalo prima con [c]summon[/c]!")
elif len(candidate_players) == 1:
voice_player = candidate_players[0]
else:
raise rc.CommandError("Non so in che Server riprodurre questo file...\n"
"Invia il comando su Discord, per favore!")
added: List[rbd.YtdlDiscord] = []
too_long: List[rbd.YtdlDiscord] = []
for url in urls:
ytds = await rbd.YtdlDiscord.from_url(url)
if isinstance(voice_player.playing, RoyalQueue):
for index, ytd in enumerate(ytds):
if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]):
too_long.append(ytd)
continue
added.append(ytd)
voice_player.playing.contents.append(ytd)
if not voice_player.voice_client.is_playing():
await voice_player.start()
elif isinstance(voice_player.playing, RoyalPool):
for index, ytd in enumerate(ytds):
if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]):
too_long.append(ytd)
continue
added.append(ytd)
voice_player.playing.full_pool.append(ytd)
voice_player.playing.remaining_pool.append(ytd)
if not voice_player.voice_client.is_playing():
await voice_player.start()
else:
raise rc.CommandError(f"Non so come aggiungere musica a [c]{voice_player.playing.__class__.__qualname__}[/c]!")
main_channel: discord.TextChannel = client.get_channel(self.config["Discord"]["main_channel_id"])
if len(added) > 0:
if user:
await main_channel.send(rsd.escape(f"▶️ {user} ha aggiunto {len(added)} file _(lazy)_ alla coda:"))
else:
await main_channel.send(rsd.escape(f"▶️ Aggiunt{'o' if len(added) == 1 else 'i'} {len(added)} file "
f"[i](lazy)[/i] alla coda:"))
for ytd in added[:5]:
embed: discord.Embed = ytd.embed()
if force_color:
embed._colour = discord.Colour(force_color)
await main_channel.send(embed=embed)
if len(added) > 5:
await main_channel.send(f"e altri {len(added) - 5}!")
if len(too_long) > 0:
if user:
await main_channel.send(rsd.escape(
f"{len(too_long)} file non {'è' if len(too_long) == 1 else 'sono'}"
f" stat{'o' if len(too_long) == 1 else 'i'} scaricat{'o' if len(too_long) == 1 else 'i'}"
f" perchè durava{'' if len(too_long) == 1 else 'no'}"
f" più di [c]{self.config['Play']['max_song_duration']}[/c] secondi."
))
if len(added) + len(too_long) == 0:
raise rc.InvalidInputError("Non è stato aggiunto nessun file alla coda.")
return {
"added": [{
"title": ytd.info.title,
} for ytd in added],
"too_long": [{
"title": ytd.info.title,
} for ytd in too_long]
}

View file

@ -1,32 +1,37 @@
import discord import discord
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.serf.discord import * from royalnet.serf.discord import *
class DiscordPauseEvent(Event): class DiscordPauseEvent(rc.Event):
name = "discord_pause" name = "discord_pause"
async def run(self, async def run(self,
guild_id: Optional[int] = None, guild_id: Optional[int] = None,
**kwargs) -> dict: **kwargs) -> dict:
if not isinstance(self.serf, DiscordSerf): if not isinstance(self.serf, DiscordSerf):
raise UnsupportedError() raise rc.UnsupportedError()
client: discord.Client = self.serf.client client: discord.Client = self.serf.client
if len(self.serf.voice_players) == 1: if len(self.serf.voice_players) == 1:
voice_player: VoicePlayer = self.serf.voice_players[0] voice_player: VoicePlayer = self.serf.voice_players[0]
else: else:
if guild_id is None: if guild_id is None:
# TODO: trovare un modo per riprodurre canzoni su più server da Telegram # TODO: trovare un modo per riprodurre canzoni su più server da Telegram
raise InvalidInputError("Non so in che Server riprodurre questo file...\n" raise rc.InvalidInputError("Non so in che Server riprodurre questo file...\n"
"Invia il comando su Discord, per favore!") "Invia il comando su Discord, per favore!")
guild: discord.Guild = client.get_guild(guild_id) guild: discord.Guild = client.get_guild(guild_id)
if guild is None: if guild is None:
raise InvalidInputError("Impossibile trovare il Server specificato.") raise rc.InvalidInputError("Impossibile trovare il Server specificato.")
voice_player: VoicePlayer = self.serf.find_voice_player(guild) candidate_players = self.serf.find_voice_players(guild)
if voice_player is None: if len(candidate_players) == 0:
raise UserError("Il bot non è in nessun canale vocale.\n" raise rc.UserError("Il bot non è in nessun canale vocale.\n"
"Evocalo prima con [c]summon[/c]!") "Evocalo prima con [c]summon[/c]!")
elif len(candidate_players) == 1:
voice_player = candidate_players[0]
else:
raise rc.CommandError("Non so su che Server saltare canzone...\n"
"Invia il comando su Discord, per favore!")
if voice_player.voice_client.is_paused(): if voice_player.voice_client.is_paused():
voice_player.voice_client.resume() voice_player.voice_client.resume()

View file

@ -1,62 +1,102 @@
import discord
import pickle
import base64
import datetime import datetime
from typing import * from typing import *
from royalnet.commands import *
from royalnet.serf.discord import * import discord
from royalnet.bard import * import royalnet.commands as rc
from ..utils import RoyalQueue import royalnet.serf.discord as rsd
import royalnet.bard.discord as rbd
from ..utils import RoyalQueue, RoyalPool
class DiscordPlayEvent(Event): class DiscordPlayEvent(rc.Event):
name = "discord_play" name = "discord_play"
async def run(self, async def run(self,
url: str, urls: List[str],
guild_id: Optional[int] = None, guild_id: Optional[int] = None,
user: Optional[str] = None,
force_color: Optional[int] = None,
**kwargs) -> dict: **kwargs) -> dict:
if not isinstance(self.serf, DiscordSerf): if not isinstance(self.serf, rsd.DiscordSerf):
raise UnsupportedError() raise rc.UnsupportedError()
serf: rsd.DiscordSerf = self.serf
client: discord.Client = self.serf.client client: discord.Client = self.serf.client
if len(self.serf.voice_players) == 1: guild: discord.Guild = client.get_guild(guild_id) if guild_id is not None else None
voice_player: VoicePlayer = self.serf.voice_players[0] candidate_players: List[rsd.VoicePlayer] = serf.find_voice_players(guild)
if len(candidate_players) == 0:
raise rc.UserError("Il bot non è in nessun canale vocale.\n"
"Evocalo prima con [c]summon[/c]!")
elif len(candidate_players) == 1:
voice_player = candidate_players[0]
else: else:
if guild_id is None: raise rc.CommandError("Non so in che Server riprodurre questo file...\n"
# TODO: trovare un modo per riprodurre canzoni su più server da Telegram "Invia il comando su Discord, per favore!")
raise InvalidInputError("Non so in che Server riprodurre questo file...\n"
"Invia il comando su Discord, per favore!") added: List[rbd.YtdlDiscord] = []
guild: discord.Guild = client.get_guild(guild_id) too_long: List[rbd.YtdlDiscord] = []
if guild is None:
raise InvalidInputError("Impossibile trovare il Server specificato.") for url in urls:
voice_player: VoicePlayer = self.serf.find_voice_player(guild) ytds = await rbd.YtdlDiscord.from_url(url)
if voice_player is None: if isinstance(voice_player.playing, RoyalQueue):
raise UserError("Il bot non è in nessun canale vocale.\n" for index, ytd in enumerate(ytds):
"Evocalo prima con [c]summon[/c]!") if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]):
ytds = await YtdlDiscord.from_url(url) too_long.append(ytd)
added: List[YtdlDiscord] = [] continue
too_long: List[YtdlDiscord] = [] await ytd.convert_to_pcm()
if isinstance(voice_player.playing, RoyalQueue): added.append(ytd)
for index, ytd in enumerate(ytds): voice_player.playing.contents.append(ytd)
if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]): if not voice_player.voice_client.is_playing():
too_long.append(ytd) await voice_player.start()
continue elif isinstance(voice_player.playing, RoyalPool):
await ytd.convert_to_pcm() for index, ytd in enumerate(ytds):
added.append(ytd) if ytd.info.duration >= datetime.timedelta(seconds=self.config["Play"]["max_song_duration"]):
voice_player.playing.contents.append(ytd) too_long.append(ytd)
if not voice_player.voice_client.is_playing(): continue
await voice_player.start() await ytd.convert_to_pcm()
else: added.append(ytd)
raise CommandError(f"Non so come aggiungere musica a [c]{voice_player.playing.__class__.__qualname__}[/c]!") voice_player.playing.full_pool.append(ytd)
voice_player.playing.remaining_pool.append(ytd)
if not voice_player.voice_client.is_playing():
await voice_player.start()
else:
raise rc.CommandError(f"Non so come aggiungere musica a [c]{voice_player.playing.__class__.__qualname__}[/c]!")
main_channel: discord.TextChannel = client.get_channel(self.config["Discord"]["main_channel_id"])
if len(added) > 0:
if user:
await main_channel.send(rsd.escape(f"▶️ {user} ha aggiunto {len(added)} file alla coda:"))
else:
await main_channel.send(rsd.escape(f"▶️ Aggiunt{'o' if len(added) == 1 else 'i'} {len(added)} file alla"
f" coda:"))
for ytd in added[:5]:
embed: discord.Embed = ytd.embed()
if force_color:
embed._colour = discord.Colour(force_color)
await main_channel.send(embed=embed)
if len(added) > 5:
await main_channel.send(f"e altri {len(added) - 5}!")
if len(too_long) > 0:
if user:
await main_channel.send(rsd.escape(
f"{len(too_long)} file non {'è' if len(too_long) == 1 else 'sono'}"
f" stat{'o' if len(too_long) == 1 else 'i'} scaricat{'o' if len(too_long) == 1 else 'i'}"
f" perchè durava{'' if len(too_long) == 1 else 'no'}"
f" più di [c]{self.config['Play']['max_song_duration']}[/c] secondi."
))
if len(added) + len(too_long) == 0:
raise rc.InvalidInputError("Non è stato aggiunto nessun file alla coda.")
return { return {
"added": [{ "added": [{
"title": ytd.info.title, "title": ytd.info.title,
"stringified_base64_pickled_discord_embed": str(base64.b64encode(pickle.dumps(ytd.embed())),
encoding="ascii")
} for ytd in added], } for ytd in added],
"too_long": [{ "too_long": [{
"title": ytd.info.title, "title": ytd.info.title,
"stringified_base64_pickled_discord_embed": str(base64.b64encode(pickle.dumps(ytd.embed())),
encoding="ascii")
} for ytd in too_long] } for ytd in too_long]
} }

View file

@ -0,0 +1,48 @@
import datetime
from typing import *
import discord
import royalnet.commands as rc
import royalnet.serf.discord as rsd
import royalnet.bard.discord as rbd
from ..utils import RoyalQueue, RoyalPool
class DiscordPlaymodeEvent(rc.Event):
name = "discord_playmode"
async def run(self,
playable_string: str,
guild_id: Optional[int] = None,
user: Optional[str] = None,
**kwargs) -> dict:
if not isinstance(self.serf, rsd.DiscordSerf):
raise rc.UnsupportedError()
serf: rsd.DiscordSerf = self.serf
client: discord.Client = self.serf.client
guild: discord.Guild = client.get_guild(guild_id) if guild_id is not None else None
candidate_players: List[rsd.VoicePlayer] = serf.find_voice_players(guild)
if len(candidate_players) == 0:
raise rc.UserError("Il bot non è in nessun canale vocale.\n"
"Evocalo prima con [c]summon[/c]!")
elif len(candidate_players) == 1:
voice_player = candidate_players[0]
else:
raise rc.CommandError("Non so a che Server cambiare Playable...\n"
"Invia il comando su Discord, per favore!")
if playable_string.upper() == "QUEUE":
playable = await RoyalQueue.create()
elif playable_string.upper() == "POOL":
playable = await RoyalPool.create()
else:
raise rc.InvalidInputError(f"Unknown playable '{playable_string.upper()}'")
await voice_player.change_playing(playable)
return {
"name": f"{playable.__class__.__qualname__}"
}

View file

@ -2,34 +2,39 @@ import discord
import pickle import pickle
import base64 import base64
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.serf.discord import * from royalnet.serf.discord import *
from ..utils import RoyalQueue from ..utils import RoyalQueue
class DiscordQueueEvent(Event): class DiscordQueueEvent(rc.Event):
name = "discord_queue" name = "discord_queue"
async def run(self, async def run(self,
guild_id: Optional[int] = None, guild_id: Optional[int] = None,
**kwargs) -> dict: **kwargs) -> dict:
if not isinstance(self.serf, DiscordSerf): if not isinstance(self.serf, DiscordSerf):
raise UnsupportedError() raise rc.UnsupportedError()
client: discord.Client = self.serf.client client: discord.Client = self.serf.client
if len(self.serf.voice_players) == 1: if len(self.serf.voice_players) == 1:
voice_player: VoicePlayer = self.serf.voice_players[0] voice_player: VoicePlayer = self.serf.voice_players[0]
else: else:
if guild_id is None: if guild_id is None:
# TODO: trovare un modo per riprodurre canzoni su più server da Telegram # TODO: trovare un modo per riprodurre canzoni su più server da Telegram
raise InvalidInputError("Non so in che Server riprodurre questo file...\n" raise rc.InvalidInputError("Non so in che Server riprodurre questo file...\n"
"Invia il comando su Discord, per favore!") "Invia il comando su Discord, per favore!")
guild: discord.Guild = client.get_guild(guild_id) guild: discord.Guild = client.get_guild(guild_id)
if guild is None: if guild is None:
raise InvalidInputError("Impossibile trovare il Server specificato.") raise rc.InvalidInputError("Impossibile trovare il Server specificato.")
voice_player: VoicePlayer = self.serf.find_voice_player(guild) candidate_players = self.serf.find_voice_players(guild)
if voice_player is None: if len(candidate_players) == 0:
raise UserError("Il bot non è in nessun canale vocale.\n" raise rc.UserError("Il bot non è in nessun canale vocale.\n"
"Evocalo prima con [c]summon[/c]!") "Evocalo prima con [c]summon[/c]!")
elif len(candidate_players) == 1:
voice_player = candidate_players[0]
else:
raise rc.CommandError("Non so di che Server visualizzare la coda...\n"
"Invia il comando su Discord, per favore!")
if isinstance(voice_player.playing, RoyalQueue): if isinstance(voice_player.playing, RoyalQueue):
now_playing = voice_player.playing.now_playing now_playing = voice_player.playing.now_playing
return { return {
@ -46,5 +51,5 @@ class DiscordQueueEvent(Event):
} for ytd in voice_player.playing.contents] } for ytd in voice_player.playing.contents]
} }
else: else:
raise CommandError(f"Non so come visualizzare il contenuto di un " raise rc.CommandError(f"Non so come visualizzare il contenuto di un "
f"[c]{voice_player.playing.__class__.__qualname__}[/c].") f"[c]{voice_player.playing.__class__.__qualname__}[/c].")

View file

@ -1,32 +1,37 @@
import discord import discord
from typing import * from typing import *
from royalnet.commands import * import royalnet.commands as rc
from royalnet.serf.discord import * from royalnet.serf.discord import *
class DiscordSkipEvent(Event): class DiscordSkipEvent(rc.Event):
name = "discord_skip" name = "discord_skip"
async def run(self, async def run(self,
guild_id: Optional[int] = None, guild_id: Optional[int] = None,
**kwargs) -> dict: **kwargs) -> dict:
if not isinstance(self.serf, DiscordSerf): if not isinstance(self.serf, DiscordSerf):
raise UnsupportedError() raise rc.UnsupportedError()
client: discord.Client = self.serf.client client: discord.Client = self.serf.client
if len(self.serf.voice_players) == 1: if len(self.serf.voice_players) == 1:
voice_player: VoicePlayer = self.serf.voice_players[0] voice_player: VoicePlayer = self.serf.voice_players[0]
else: else:
if guild_id is None: if guild_id is None:
# TODO: trovare un modo per riprodurre canzoni su più server da Telegram # TODO: trovare un modo per riprodurre canzoni su più server da Telegram
raise InvalidInputError("Non so in che Server riprodurre questo file...\n" raise rc.InvalidInputError("Non so in che Server riprodurre questo file...\n"
"Invia il comando su Discord, per favore!") "Invia il comando su Discord, per favore!")
guild: discord.Guild = client.get_guild(guild_id) guild: discord.Guild = client.get_guild(guild_id)
if guild is None: if guild is None:
raise InvalidInputError("Impossibile trovare il Server specificato.") raise rc.InvalidInputError("Impossibile trovare il Server specificato.")
voice_player: VoicePlayer = self.serf.find_voice_player(guild) candidate_players = self.serf.find_voice_players(guild)
if voice_player is None: if len(candidate_players) == 0:
raise UserError("Il bot non è in nessun canale vocale.\n" raise rc.UserError("Il bot non è in nessun canale vocale.\n"
"Evocalo prima con [c]summon[/c]!") "Evocalo prima con [c]summon[/c]!")
elif len(candidate_players) == 1:
voice_player = candidate_players[0]
else:
raise rc.CommandError("Non so su che Server saltare canzone...\n"
"Invia il comando su Discord, per favore!")
# Stop the playback of the current song # Stop the playback of the current song
voice_player.voice_client.stop() voice_player.voice_client.stop()
# Done! # Done!

View file

@ -60,6 +60,12 @@ class DiscordSummonEvent(Event):
# Connect to the channel # Connect to the channel
try: try:
await vp.connect(channel) await vp.connect(channel)
except OpusNotLoadedError:
raise ConfigurationError("libopus non è disponibile sul sistema in cui sta venendo eseguito questo bot,"
" pertanto non è possibile con")
except DiscordTimeoutError:
raise ExternalError("Timeout durante la connessione al canale."
" Forse il bot non ha i permessi per entrarci?")
except GuildAlreadyConnectedError: except GuildAlreadyConnectedError:
raise UserError("Il bot è già connesso in un canale vocale nel Server!\n" raise UserError("Il bot è già connesso in un canale vocale nel Server!\n"
"Spostalo manualmente, o disconnettilo e riinvoca [c]summon[/c]!") "Spostalo manualmente, o disconnettilo e riinvoca [c]summon[/c]!")

11
royalpack/events/pong.py Normal file
View file

@ -0,0 +1,11 @@
from typing import *
import royalnet
import royalnet.commands as rc
import datetime
class PongEvent(rc.Event):
name = "pong"
async def run(self, **kwargs) -> dict:
return {"timestamp": datetime.datetime.now().timestamp()}

View file

@ -0,0 +1,28 @@
import logging
import telegram
from typing import *
from royalnet.serf.telegram.telegramserf import TelegramSerf, escape
from royalnet.commands import *
log = logging.getLogger(__name__)
class TelegramMessageEvent(Event):
name = "telegram_message"
async def run(self, chat_id, text, **kwargs) -> dict:
if not self.interface.name == "telegram":
raise UnsupportedError()
# noinspection PyTypeChecker
serf: TelegramSerf = self.interface.serf
log.debug("Forwarding message from Herald to Telegram.")
await serf.api_call(serf.client.send_message,
chat_id=chat_id,
text=escape(text),
parse_mode="HTML",
disable_web_page_preview=True)
return {}

View file

@ -0,0 +1,70 @@
POST http://localhost:44445/api/login/royalnet/v1
Content-Type: application/json
{
"username": "Steffo",
"password": "ciao"
}
###
POST http://localhost:44445/api/token/create/v1
Content-Type: application/json
{
"token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE",
"duration": 31536000
}
###
GET http://localhost:44445/api/bio/get/v1
Content-Type: application/json
{
"token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE",
"id": 1
}
###
POST http://localhost:44445/api/bio/set/v1
Content-Type: application/json
{
"token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE",
"contents": "Ciao!"
}
###
GET http://localhost:44445/api/wiki/list/v1
###
POST http://localhost:44445/api/wiki/edit/v1
Content-Type: application/json
{
"token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE",
"title": "Prova!",
"contents": "Questa è una pagina wiki di prova.",
"format": "text",
"theme": "default"
}
###
POST http://localhost:44445/api/wiki/edit/v1
Content-Type: application/json
{
"id": "80d54849-fab1-4458-9d89-2429773118ef",
"token": "NFFU4qg6-WxfAWMN-IW6dexEjNcLzNQNZJko2_pbTsE",
"title": "Prova2",
"contents": "Questa è una pagina wiki di prova2.",
"format": "text",
"theme": "default"
}
###

View file

@ -1,27 +1,38 @@
# Imports go here! # Imports go here!
from .api_user_list import ApiUserListStar from .api_bio import ApiBioSetStar
from .api_user_get import ApiUserGetStar from .api_diario import ApiDiarioGetStar
from .api_diario_list import ApiDiarioListStar from .api_diario_list import ApiDiarioPagesStar
from .api_diario_get import ApiDiarioGetStar
from .api_discord_cv import ApiDiscordCvStar from .api_discord_cv import ApiDiscordCvStar
from .api_wiki_get import ApiWikiGetStar from .api_discord_play import ApiDiscordPlayStar
from .api_wiki_list import ApiUserListStar from .api_fiorygi import ApiFiorygiStar
from .api_diario_random import ApiDiarioRandomStar
from .api_poll import ApiPollStar
from .api_poll_list import ApiPollsListStar
from .api_cvstats_latest import ApiCvstatsLatestStar
from .api_cvstats_avg import ApiCvstatsAvgStar
from .api_user_ryg import ApiUserRygStar
from .api_user_ryg_list import ApiUserRygListStar
from .api_user_avatar import ApiUserAvatarStar
from .api_auth_login_osu import ApiAuthLoginOsuStar
# Enter the PageStars of your Pack here! # Enter the PageStars of your Pack here!
available_page_stars = [ available_page_stars = [
ApiUserListStar, ApiBioSetStar,
ApiUserGetStar,
ApiDiarioListStar,
ApiDiarioGetStar, ApiDiarioGetStar,
ApiDiarioPagesStar,
ApiDiscordCvStar, ApiDiscordCvStar,
ApiWikiGetStar, ApiDiscordPlayStar,
ApiUserListStar, ApiFiorygiStar,
] ApiDiarioRandomStar,
ApiPollStar,
# Enter the ExceptionStars of your Pack here! ApiPollsListStar,
available_exception_stars = [ ApiCvstatsLatestStar,
ApiCvstatsAvgStar,
ApiUserRygStar,
ApiUserRygListStar,
ApiUserAvatarStar,
ApiAuthLoginOsuStar,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__
__all__ = [star.__name__ for star in [*available_page_stars, *available_exception_stars]] __all__ = [star.__name__ for star in available_page_stars]

View file

@ -0,0 +1,102 @@
import royalnet.utils as ru
import royalnet.backpack.tables as rbt
import royalnet.constellation.api as rca
import royalnet.constellation.api.apierrors as rcae
import itsdangerous
import aiohttp
import aiohttp.client_exceptions
import datetime
from ..types import oauth_refresh
from ..tables import Osu, FiorygiTransaction
class ApiAuthLoginOsuStar(rca.ApiStar):
path = "/api/auth/login/osu/v1"
parameters = {
"get": {
"code": "The code returned by the osu! API.",
"state": "(Optional) The state payload generated by the osu! command to link a new account. "
"If missing, just login."
}
}
auth = {
"get": False,
}
tags = ["auth"]
@property
def client_id(self):
return self.config['osu']['client_id']
@property
def client_secret(self):
return self.config['osu']['client_secret']
@property
def base_url(self):
return self.config['base_url']
@property
def secret_key(self):
return self.config['secret_key']
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Login to Royalnet with your osu! account."""
OsuT = self.alchemy.get(Osu)
TokenT = self.alchemy.get(rbt.Token)
code = data.str("code")
state = data.str("state", optional=True)
if state is not None:
serializer = itsdangerous.URLSafeSerializer(self.config["secret_key"], salt="osu")
uid = serializer.loads(state)
user = await rbt.User.find(self.alchemy, data.session, uid)
else:
user = None
try:
t = await oauth_refresh(url="https://osu.ppy.sh/oauth/token",
client_id=self.client_id,
client_secret=self.client_secret,
redirect_uri=f"{self.base_url}{self.path}",
refresh_code=code)
except aiohttp.client_exceptions.ClientResponseError:
raise rca.ForbiddenError("osu! API returned an error in the OAuth token exchange")
async with aiohttp.ClientSession(headers={"Authorization": f"Bearer {t['access_token']}"}) as session:
async with session.get("https://osu.ppy.sh/api/v2/me/") as response:
m = await response.json()
if user is not None:
osu = OsuT(
user=user,
access_token=t["access_token"],
refresh_token=t["refresh_token"],
expiration_date=datetime.datetime.now() + datetime.timedelta(seconds=t["expires_in"]),
osu_id=m["id"],
username=m["username"]
)
data.session.add(osu)
else:
osu = await ru.asyncify(
data.session.query(OsuT).filter_by(osu_id=m["id"]).all
)
if osu is None:
raise rcae.ForbiddenError("Unknown osu! account")
user = osu.user
if self.config["osu"]["login"]["enabled"]:
token: rbt.Token = TokenT.generate(alchemy=self.alchemy, user=user, expiration_delta=datetime.timedelta(days=7))
data.session.add(token)
await data.session_commit()
return token.json()
else:
raise rcae.ForbiddenError("Account linked successfully; cannot use this account to generate a Royalnet"
" login token, as osu! login is currently disabled on this Royalnet instance.")

View file

@ -0,0 +1,45 @@
import royalnet.utils as ru
import royalnet.backpack.tables as rbt
import royalnet.constellation.api as rca
from ..tables import Bio
class ApiBioSetStar(rca.ApiStar):
path = "/api/bio/v2"
parameters = {
"get": {
"uid": "The id of the user to get the bio of."
},
"put": {
"contents": "The contents of the bio."
}
}
auth = {
"get": False,
"put": True,
}
tags = ["bio"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get the bio of a specific user."""
user = await rbt.User.find(self.alchemy, data.session, data.int("uid"))
return user.bio.json() if user.bio else None
@rca.magic
async def put(self, data: rca.ApiData) -> ru.JSON:
"""Set the bio of current user."""
contents = data["contents"]
BioT = self.alchemy.get(Bio)
user = await data.user()
bio = user.bio
if bio is None:
bio = BioT(user=user, contents=contents)
data.session.add(bio)
else:
bio.contents = contents
await data.session_commit()
return bio.json()

View file

@ -0,0 +1,164 @@
import royalnet.constellation.api as rca
import royalnet.utils as ru
class ApiCvstatsAvgStar(rca.ApiStar):
path = "/api/cvstats/avg/v1"
tags = ["cvstats"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get some averages on the cvstats."""
results = data.session.execute("""
SELECT *
FROM (
SELECT date_part('hour', c.h) ph,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM (
SELECT date_trunc('hour', c.timestamp) h,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM cvstats c
GROUP BY h
) c
GROUP BY ph
) all_time
LEFT JOIN
(
SELECT date_part('hour', c.h) ph,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM (
SELECT date_trunc('hour', c.timestamp) h,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM cvstats c
WHERE c.timestamp > current_timestamp - interval '7 day'
GROUP BY h
) c
GROUP BY ph
) last_week ON last_week.ph = all_time.ph
LEFT JOIN
(
SELECT date_part('hour', c.h) ph,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM (
SELECT date_trunc('hour', c.timestamp) h,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM cvstats c
WHERE c.timestamp > current_timestamp - interval '30 day'
GROUP BY h
) c
GROUP BY ph
) last_month ON last_month.ph = all_time.ph
LEFT JOIN
(
SELECT date_part('hour', c.h) ph,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM (
SELECT date_trunc('hour', c.timestamp) h,
AVG(c.members_connected) members_connected,
AVG(c.users_connected) users_connected,
AVG(c.members_online) members_online,
AVG(c.users_online) users_online,
AVG(c.members_playing) members_playing,
AVG(c.users_playing) users_playing,
AVG(c.members_total) members_total,
AVG(c.users_total) users_total
FROM cvstats c
WHERE c.timestamp > current_timestamp - interval '1 day'
GROUP BY h
) c
GROUP BY ph
) last_day ON last_day.ph = all_time.ph;
""")
return [{
"h": r[0],
"all_time": {
"members_connected": float(r[1]) if r[1] is not None else None,
"users_connected": float(r[2]) if r[2] is not None else None,
"members_online": float(r[3]) if r[3] is not None else None,
"users_online": float(r[4]) if r[4] is not None else None,
"members_playing": float(r[5]) if r[5] is not None else None,
"users_playing": float(r[6]) if r[6] is not None else None,
"members_total": float(r[7]) if r[7] is not None else None,
"users_total": float(r[8]) if r[8] is not None else None,
},
"last_week": {
"members_connected": float(r[10]) if r[10] is not None else None,
"users_connected": float(r[11]) if r[11] is not None else None,
"members_online": float(r[12]) if r[12] is not None else None,
"users_online": float(r[13]) if r[13] is not None else None,
"members_playing": float(r[14]) if r[14] is not None else None,
"users_playing": float(r[15]) if r[15] is not None else None,
"members_total": float(r[16]) if r[16] is not None else None,
"users_total": float(r[17]) if r[17] is not None else None,
},
"last_month": {
"members_connected": float(r[19]) if r[19] is not None else None,
"users_connected": float(r[20]) if r[20] is not None else None,
"members_online": float(r[21]) if r[21] is not None else None,
"users_online": float(r[22]) if r[22] is not None else None,
"members_playing": float(r[23]) if r[23] is not None else None,
"users_playing": float(r[24]) if r[24] is not None else None,
"members_total": float(r[25]) if r[25] is not None else None,
"users_total": float(r[26]) if r[26] is not None else None,
},
"last_day": {
"members_connected": float(r[28]) if r[28] is not None else None,
"users_connected": float(r[29]) if r[29] is not None else None,
"members_online": float(r[30]) if r[30] is not None else None,
"users_online": float(r[31]) if r[31] is not None else None,
"members_playing": float(r[32]) if r[32] is not None else None,
"users_playing": float(r[33]) if r[33] is not None else None,
"members_total": float(r[34]) if r[34] is not None else None,
},
} for r in sorted(results.fetchall(), key=lambda s: s[0])]

View file

@ -0,0 +1,18 @@
import royalnet.utils as ru
import royalnet.constellation.api as rca
from ..tables import Cvstats
class ApiCvstatsLatestStar(rca.ApiStar):
path = "/api/cvstats/latest/v1"
tags = ["cvstats"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get the latest 500 cvstats recorded."""
CvstatsT = self.alchemy.get(Cvstats)
cvstats = data.session.query(CvstatsT).order_by(CvstatsT.timestamp.desc()).limit(500).all()
return list(map(lambda c: c.json(), cvstats))

View file

@ -0,0 +1,24 @@
import royalnet.constellation.api as rca
import royalnet.utils as ru
from ..tables import *
class ApiDiarioGetStar(rca.ApiStar):
path = "/api/diario/v2"
parameters = {
"get": {
"id": "The id of the diario entry to get."
}
}
tags = ["diario"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get a specific diario entry."""
diario_id = data.int("id")
entry: Diario = await ru.asyncify(data.session.query(self.alchemy.get(Diario)).get, diario_id)
if entry is None:
raise rca.NotFoundError("No such diario entry.")
return entry.json()

View file

@ -1,21 +0,0 @@
from starlette.requests import Request
from starlette.responses import *
from royalnet.constellation import *
from royalnet.utils import *
from ..tables import *
class ApiDiarioGetStar(PageStar):
path = "/api/diario/get/{diario_id}"
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 shoot(400, "Invalid diario_id")
async with self.alchemy.session_acm() as session:
entry: Diario = await asyncify(session.query(self.alchemy.get(Diario)).get, diario_id)
if entry is None:
return shoot(404, "No such user")
return JSONResponse(entry.json())

View file

@ -1,34 +1,45 @@
from starlette.requests import Request from typing import *
from starlette.responses import * import royalnet.constellation.api as rca
from royalnet.constellation import * import royalnet.utils as ru
from royalnet.utils import *
from ..tables import * from ..tables import *
class ApiDiarioListStar(PageStar): class ApiDiarioPagesStar(rca.ApiStar):
path = "/api/diario/list" path = "/api/diario/pages/v1"
async def page(self, request: Request) -> JSONResponse: parameters = {
page_str = request.query_params.get("page", "0") "get": {
"page": "The diario page you want to get. Can be negative to get the entries in reverse order."
}
}
tags = ["diario"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get a diario page made of up to 500 diario entries."""
page_str = data["page"]
try: try:
page = int(page_str) page = int(page_str)
except (ValueError, TypeError): except ValueError:
return shoot(400, "Invalid offset") raise rca.InvalidParameterError("'page' is not a valid int.")
async with self.alchemy.session_acm() as session: if page < 0:
if page < 0: page = -page-1
page = -page-1 entries: List[Diario] = await ru.asyncify(
entries: typing.List[Diario] = await asyncify( data.session
session.query(self.alchemy.get(Diario)) .query(self.alchemy.get(Diario))
.order_by(self.alchemy.get(Diario).diario_id.desc()).limit(500) .order_by(self.alchemy.get(Diario).diario_id.desc()).limit(500)
.offset(page * 500) .offset(page * 500)
.all .all
) )
else: else:
entries: typing.List[Diario] = await asyncify( entries: List[Diario] = await ru.asyncify(
session.query(self.alchemy.get(Diario)) data.session
.order_by(self.alchemy.get(Diario).diario_id) .query(self.alchemy.get(Diario))
.limit(500) .order_by(self.alchemy.get(Diario).diario_id)
.offset(page * 500) .limit(500)
.all) .offset(page * 500)
response = [entry.json() for entry in entries] .all
return JSONResponse(response) )
response = [entry.json() for entry in entries]
return response

View file

@ -0,0 +1,36 @@
from typing import *
import royalnet.constellation.api as rca
import royalnet.utils as ru
from ..tables import *
from sqlalchemy import func
class ApiDiarioRandomStar(rca.ApiStar):
path = "/api/diario/random/v1"
parameters = {
"get": {
"amount": "The number of diario entries to get."
}
}
tags = ["diario"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get random diario entries."""
DiarioT = self.alchemy.get(Diario)
try:
amount = int(data["amount"])
except ValueError:
raise rca.InvalidParameterError("'amount' is not a valid int.")
entries: List[Diario] = await ru.asyncify(
data.session
.query(DiarioT)
.order_by(func.random())
.limit(amount)
.all
)
if len(entries) < amount:
raise rca.NotFoundError("Not enough diario entries.")
return list(map(lambda e: e.json(), entries))

View file

@ -1,12 +1,16 @@
from starlette.requests import Request import royalnet.utils as ru
from starlette.responses import * import royalnet.constellation.api as rca
from royalnet.constellation import *
from royalnet.utils import *
class ApiDiscordCvStar(PageStar): class ApiDiscordCvStar(rca.ApiStar):
path = "/api/discord/cv" path = "/api/discord/cv/v1"
async def page(self, request: Request) -> JSONResponse: tags = ["discord"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get the members status of a single Discord guild.
Equivalent to calling /cv in a chat."""
response = await self.interface.call_herald_event("discord", "discord_cv") response = await self.interface.call_herald_event("discord", "discord_cv")
return JSONResponse(response) return response

View file

@ -0,0 +1,40 @@
from typing import *
import royalnet.constellation.api as rca
import logging
log = logging.getLogger(__name__)
class ApiDiscordPlayStar(rca.ApiStar):
path = "/api/discord/play/v2"
parameters = {
"post": {
"url": "The url of the audio file to add.",
"user": "The name to display in the File Added message.",
"guild_id": "The id of the guild owning the RoyalQueue to add the audio file to.",
}
}
tags = ["discord"]
@rca.magic
async def post(self, data: rca.ApiData) -> dict:
"""Add a audio file to the RoyalQueue of a Discord Guild."""
url = data["url"]
user = data.get("user")
guild_id_str = data.get("guild_id")
if guild_id_str:
try:
guild_id: Optional[int] = int(guild_id_str)
except (ValueError, TypeError):
raise rca.InvalidParameterError("'guild_id' is not a valid int.")
else:
guild_id = None
log.info(f"Received request to play {url} on guild_id {guild_id} via web")
response = await self.interface.call_herald_event("discord", "discord_play",
urls=[url],
guild_id=guild_id,
user=user)
return response

View file

@ -0,0 +1,39 @@
from typing import *
import royalnet.utils as ru
import royalnet.backpack.tables as rbt
import royalnet.constellation.api as rca
from ..tables import Fiorygi
class ApiFiorygiStar(rca.ApiStar):
path = "/api/fiorygi/v2"
parameters = {
"get": {
"uid": "The user to get the fiorygi of."
}
}
tags = ["fiorygi"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get fiorygi information about a specific user."""
user = await rbt.User.find(self.alchemy, data.session, data.int("uid"))
if user.fiorygi is None:
return {
"fiorygi": 0,
"transactions": [],
"warning": "No associated fiorygi table"
}
fiorygi: Fiorygi = user.fiorygi
transactions: ru.JSON = sorted(fiorygi.transactions, key=lambda t: -t.id)
return {
"fiorygi": fiorygi.fiorygi,
"transactions": list(map(lambda t: {
"id": t.id,
"change": t.change,
"reason": t.reason,
"timestamp": t.timestamp.isoformat() if t.timestamp else None
}, transactions))
}

View file

@ -0,0 +1,63 @@
from typing import *
import datetime
import uuid
import royalnet.utils as ru
import royalnet.constellation.api as rca
from ..tables import Poll
class ApiPollStar(rca.ApiStar):
path = "/api/poll/v2"
parameters = {
"get": {
"uuid": "The UUID of the poll to get.",
},
"post": {
"question": "The question to ask in the poll.",
"description": "A longer Markdown-formatted description.",
"expires": "A ISO timestamp of the expiration date for the poll.",
}
}
auth = {
"get": False,
"post": True
}
tags = ["poll"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get a specific poll."""
PollT = self.alchemy.get(Poll)
try:
pid = uuid.UUID(data["uuid"])
except (ValueError, AttributeError, TypeError):
raise rca.InvalidParameterError("'uuid' is not a valid UUID.")
poll: Poll = await ru.asyncify(data.session.query(PollT).get, pid)
if poll is None:
raise rca.NotFoundError("No such page.")
return poll.json()
@rca.magic
async def post(self, data: rca.ApiData) -> ru.JSON:
"""Create a new poll."""
PollT = self.alchemy.get(Poll)
poll = PollT(
id=uuid.uuid4(),
creator=await data.user(),
created=datetime.datetime.now(),
expires=datetime.datetime.fromisoformat(data["expires"]) if "expires" in data else None,
question=data["question"],
description=data.get("description"),
)
data.session.add(poll)
await data.session_commit()
return poll.json()

View file

@ -0,0 +1,25 @@
from typing import *
import royalnet.constellation.api as rca
import royalnet.utils as ru
from ..tables import Poll
class ApiPollsListStar(rca.ApiStar):
path = "/api/poll/list/v2"
tags = ["poll"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get a list of all polls."""
PollT = self.alchemy.get(Poll)
polls: List[Poll] = await ru.asyncify(data.session.query(PollT).all)
return list(map(lambda p: {
"id": p.id,
"question": p.question,
"creator": p.creator.json(),
"expires": p.expires.isoformat(),
"created": p.created.isoformat(),
}, polls))

View file

@ -0,0 +1,38 @@
import re
import royalnet.utils as ru
import royalnet.constellation.api as rca
url_validation = re.compile(r'^(?:http|ftp)s?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
class ApiUserAvatarStar(rca.ApiStar):
path = "/api/user/avatar/v2"
parameters = {
"put": {
"avatar_url": "The url that the user wants to set as avatar."
}
}
auth = {
"put": True,
}
tags = ["user"]
@rca.magic
async def put(self, data: rca.ApiData) -> ru.JSON:
"""Set the avatar of current user."""
avatar_url = data["avatar_url"]
user = await data.user()
if not re.match(url_validation, avatar_url):
raise rca.InvalidParameterError("avatar_url is not a valid url.")
user.avatar_url = avatar_url
await data.session_commit()
return user.json()

View file

@ -1,21 +0,0 @@
from starlette.requests import Request
from starlette.responses import *
from royalnet.constellation import *
from royalnet.utils import *
from royalnet.backpack.tables import *
class ApiUserGetStar(PageStar):
path = "/api/user/get/{uid_str}"
async def page(self, request: Request) -> JSONResponse:
uid_str = request.path_params.get("uid_str", "")
try:
uid = int(uid_str)
except (ValueError, TypeError):
return shoot(400, "Invalid uid")
async with self.alchemy.session_acm() as session:
user: User = await asyncify(session.query(self.alchemy.get(User)).get, uid)
if user is None:
return shoot(404, "No such user")
return JSONResponse(user.json())

View file

@ -1,14 +0,0 @@
from starlette.requests import Request
from starlette.responses import *
from royalnet.constellation import *
from royalnet.utils import *
from royalnet.backpack.tables import *
class ApiUserListStar(PageStar):
path = "/api/user/list"
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.get(User)).all)
return JSONResponse([user.json() for user in users])

View file

@ -0,0 +1,45 @@
import royalnet.backpack.tables as rbt
import royalnet.constellation.api as rca
class ApiUserRygStar(rca.ApiStar):
path = "/api/user/ryg/v2"
parameters = {
"get": {
"uid": "(Choose one) The id of the user to get information about.",
"alias": "(Choose one) The alias of the user to get information about.",
}
}
tags = ["user"]
async def get_user(self, data: rca.ApiData):
uid = data.int("uid", optional=True)
alias = data.str("alias", optional=True)
if uid:
user = await rbt.User.find(self.alchemy, data.session, uid)
elif alias:
user = await rbt.User.find(self.alchemy, data.session, alias)
else:
raise rca.MissingParameterError("Neither uid or alias were specified.")
if user is None:
raise rca.NotFoundError("No such user.")
return user
@rca.magic
async def get(self, data: rca.ApiData) -> dict:
"""Get Royalpack information about a user."""
user = await self.get_user(data)
result = {
**user.json(),
"bio": user.bio.json() if user.bio is not None else None,
"fiorygi": user.fiorygi.fiorygi if user.fiorygi is not None else None,
"steam": [steam.json() for steam in user.steam],
"leagueoflegends": [leagueoflegends.json() for leagueoflegends in user.leagueoflegends],
"trivia": user.trivia_score.json() if user.trivia_score is not None else None
}
return result

View file

@ -0,0 +1,23 @@
from starlette.responses import *
import royalnet.utils as ru
import royalnet.backpack.tables as rbt
import royalnet.constellation.api as rca
class ApiUserRygListStar(rca.ApiStar):
path = "/api/user/ryg/list/v1"
tags = ["user"]
@rca.magic
async def get(self, data: rca.ApiData) -> ru.JSON:
"""Get Royalpack information about all user."""
users: typing.List[rbt.User] = await ru.asyncify(data.session.query(self.alchemy.get(rbt.User)).all)
return [{
**user.json(),
"bio": user.bio.json() if user.bio is not None else None,
"fiorygi": user.fiorygi.fiorygi if user.fiorygi is not None else None,
"steam": [steam.json() for steam in user.steam],
"leagueoflegends": [leagueoflegends.json() for leagueoflegends in user.leagueoflegends],
"trivia": user.trivia_score.json() if user.trivia_score is not None else None
} for user in users]

View file

@ -1,22 +0,0 @@
from starlette.requests import Request
from starlette.responses import *
from royalnet.constellation import *
from royalnet.utils import *
from ..tables import *
import uuid
class ApiWikiGetStar(PageStar):
path = "/api/wiki/get/{wiki_page_uuid}"
async def page(self, request: Request) -> JSONResponse:
wiki_page_uuid_str = request.path_params.get("wiki_page_uuid", "")
try:
wiki_page_uuid = uuid.UUID(wiki_page_uuid_str)
except (ValueError, AttributeError, TypeError):
return shoot(400, "Invalid wiki_page_uuid")
async with self.alchemy.session_acm() as session:
wikipage: WikiPage = await asyncify(session.query(self.alchemy.get(WikiPage)).get, wiki_page_uuid)
if wikipage is None:
return shoot(404, "No such page")
return JSONResponse(wikipage.json_full())

View file

@ -1,15 +0,0 @@
from starlette.requests import Request
from starlette.responses import *
from royalnet.constellation import *
from royalnet.utils import *
from royalnet.backpack.tables import *
from ..tables import *
class ApiUserListStar(PageStar):
path = "/api/wiki/list"
async def page(self, request: Request) -> JSONResponse:
async with self.alchemy.session_acm() as session:
pages: typing.List[WikiPage] = await asyncify(session.query(self.alchemy.get(WikiPage)).all)
return JSONResponse([page.json_list() for page in pages])

View file

@ -1,29 +1,47 @@
# Imports go here! # Imports go here!
from .diario import Diario from .diario import Diario
from .aliases import Alias
from .wikipages import WikiPage from .wikipages import WikiPage
from .wikirevisions import WikiRevision
from .bios import Bio from .bios import Bio
from .reminders import Reminder from .reminders import Reminder
from .triviascores import TriviaScore from .triviascores import TriviaScore
from .mmevents import MMEvent
from .mmresponse import MMResponse
from .leagueoflegends import LeagueOfLegends from .leagueoflegends import LeagueOfLegends
from .fiorygi import Fiorygi from .fiorygi import Fiorygi
from .steam import Steam
from .dota import Dota
from .fiorygitransactions import FiorygiTransaction
from .brawlhalla import Brawlhalla
from .polls import Poll
from .pollcomments import PollComment
from .pollvotes import PollVote
from .brawlhalladuos import BrawlhallaDuo
from .mmevents import MMEvent
from .mmresponse import MMResponse
from .cvstats import Cvstats
from .treasure import Treasure
from .osu import Osu
# Enter the tables of your Pack here! # Enter the tables of your Pack here!
available_tables = [ available_tables = [
Diario, Diario,
Alias,
WikiPage, WikiPage,
WikiRevision,
Bio, Bio,
Reminder, Reminder,
TriviaScore, TriviaScore,
MMEvent,
MMResponse,
LeagueOfLegends, LeagueOfLegends,
Fiorygi, Fiorygi,
Steam,
Dota,
FiorygiTransaction,
Brawlhalla,
Poll,
PollComment,
PollVote,
BrawlhallaDuo,
MMEvent,
MMResponse,
Cvstats,
Treasure,
Osu,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__

View file

@ -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}"

View file

@ -10,19 +10,24 @@ class Bio:
__tablename__ = "bios" __tablename__ = "bios"
@declared_attr @declared_attr
def royal_id(self): def user_id(self):
return Column(Integer, ForeignKey("users.uid"), primary_key=True) return Column(Integer, ForeignKey("users.uid"), primary_key=True)
@declared_attr @declared_attr
def royal(self): def user(self):
return relationship("User", backref=backref("bio", uselist=False)) return relationship("User", backref=backref("bio", uselist=False))
@declared_attr @declared_attr
def contents(self): def contents(self):
return Column(Text, nullable=False, default="") return Column(Text, nullable=False, default="")
def json(self) -> dict:
return {
"contents": self.contents
}
def __repr__(self): def __repr__(self):
return f"<Bio of {self.royal}>" return f"<Bio of {self.user}>"
def __str__(self): def __str__(self):
return self.contents return self.contents

View file

@ -0,0 +1,110 @@
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declared_attr
import steam.steamid
from ..types import BrawlhallaRank, BrawlhallaTier, BrawlhallaMetal, Updatable
# noinspection PyAttributeOutsideInit
class Brawlhalla(Updatable):
__tablename__ = "brawlhalla"
@declared_attr
def brawlhalla_id(self):
return Column(Integer, primary_key=True)
@declared_attr
def _steamid(self):
return Column(BigInteger, ForeignKey("steam._steamid"), unique=True)
@declared_attr
def steam(self):
return relationship("Steam", backref=backref("brawlhalla", uselist=False))
@property
def steamid(self):
return steam.steamid.SteamID(self._steamid)
@declared_attr
def name(self):
return Column(String, nullable=False)
@declared_attr
def rating_1v1(self):
return Column(Integer)
@declared_attr
def tier_1v1(self):
return Column(Enum(BrawlhallaTier))
@declared_attr
def metal_1v1(self):
return Column(Enum(BrawlhallaMetal))
@property
def rank_1v1(self):
if self.metal_1v1 is None:
return None
return BrawlhallaRank(metal=self.metal_1v1, tier=self.tier_1v1)
@rank_1v1.setter
def rank_1v1(self, value):
if not isinstance(value, BrawlhallaRank):
raise TypeError("rank_1v1 can only be set to BrawlhallaRank values.")
self.metal_1v1 = value.metal
self.tier_1v1 = value.tier
@property
def duos(self):
return [*self._duos_one, *self._duos_two]
@property
def rating_2v2(self):
duos = sorted(self.duos, key=lambda d: -d.rating_2v2)
if len(duos) == 0:
return None
return duos[0].rating_2v2
@property
def tier_2v2(self):
duos = sorted(self.duos, key=lambda d: -d.rating_2v2)
if len(duos) == 0:
return None
return duos[0].tier_2v2
@property
def metal_2v2(self):
duos = sorted(self.duos, key=lambda d: -d.rating_2v2)
if len(duos) == 0:
return None
return duos[0].metal_2v2
@property
def rank_2v2(self):
duos = sorted(self.duos, key=lambda d: -d.rating_2v2)
if len(duos) == 0:
return None
return duos[0].rank_2v2
def json(self):
one_rank = self.rank_1v1
two_rank = self.rank_2v2
return {
"name": self.name,
"1v1": {
"rating": self.rating_1v1,
"metal": one_rank.metal.name,
"tier": one_rank.tier.name
} if one_rank is not None else None,
"2v2": {
"rating": self.rating_2v2,
"metal": two_rank.metal.name,
"tier": two_rank.tier.name
} if two_rank is not None else None
}
def __repr__(self):
return f"<Brawlhalla account {self._steamid}>"
def __str__(self):
return f"[c]brawlhalla:{self.brawlhalla_id}[/c]"

View file

@ -0,0 +1,58 @@
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declared_attr
from ..types import BrawlhallaRank, BrawlhallaTier, BrawlhallaMetal
class BrawlhallaDuo:
__tablename__ = "brawlhalladuos"
@declared_attr
def id_one(self):
return Column(Integer, ForeignKey("brawlhalla.brawlhalla_id"), primary_key=True)
@declared_attr
def id_two(self):
return Column(Integer, ForeignKey("brawlhalla.brawlhalla_id"), primary_key=True)
@declared_attr
def one(self):
return relationship("Brawlhalla", foreign_keys=self.id_one, backref=backref("_duos_one"))
@declared_attr
def two(self):
return relationship("Brawlhalla", foreign_keys=self.id_two, backref=backref("_duos_two"))
@declared_attr
def rating_2v2(self):
return Column(Integer)
@declared_attr
def tier_2v2(self):
return Column(Enum(BrawlhallaTier))
@declared_attr
def metal_2v2(self):
return Column(Enum(BrawlhallaMetal))
@property
def rank_2v2(self):
return BrawlhallaRank(metal=self.metal_2v2, tier=self.tier_2v2)
@rank_2v2.setter
def rank_2v2(self, value):
if not isinstance(value, BrawlhallaRank):
raise TypeError("rank_1v1 can only be set to BrawlhallaRank values.")
self.metal_2v2 = value.metal
self.tier_2v2 = value.tier
def other(self, bh):
if bh == self.one:
return self.two
elif bh == self.two:
return self.one
else:
raise ValueError("Argument is unrelated to this duo.")
def __repr__(self):
return f"<BrawlhallaDuo {self.id_one} & {self.id_two}>"

View file

@ -0,0 +1,60 @@
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declared_attr
class Cvstats:
__tablename__ = "cvstats"
@declared_attr
def id(self):
return Column(Integer, primary_key=True)
@declared_attr
def timestamp(self):
return Column(DateTime)
@declared_attr
def members_connected(self):
return Column(Integer)
@declared_attr
def users_connected(self):
return Column(Integer)
@declared_attr
def members_online(self):
return Column(Integer)
@declared_attr
def users_online(self):
return Column(Integer)
@declared_attr
def members_playing(self):
return Column(Integer)
@declared_attr
def users_playing(self):
return Column(Integer)
@declared_attr
def members_total(self):
return Column(Integer)
@declared_attr
def users_total(self):
return Column(Integer)
def json(self):
return {
"timestamp": self.timestamp.isoformat(),
"users_total": self.users_total,
"members_total": self.members_total,
"users_online": self.users_online,
"members_online": self.members_online,
"users_connected": self.users_connected,
"members_connected": self.members_connected,
"users_playing": self.users_playing,
"members_playing": self.members_playing,
}

View file

@ -88,7 +88,7 @@ class Diario:
text += f" da {str(self.creator)}" text += f" da {str(self.creator)}"
text += f" il {self.timestamp.strftime('%Y-%m-%d %H:%M')}):\n" text += f" il {self.timestamp.strftime('%Y-%m-%d %H:%M')}):\n"
if self.media_url is not None: if self.media_url is not None:
text += f"{self.media_url}\n" text += f"[url={self.media_url}]Media[/url]\n"
if self.text is not None: if self.text is not None:
if self.spoiler: if self.spoiler:
hidden = re.sub(r"\w", "", self.text) hidden = re.sub(r"\w", "", self.text)

92
royalpack/tables/dota.py Normal file
View file

@ -0,0 +1,92 @@
from typing import *
from sqlalchemy import *
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declared_attr
from ..types import DotaMedal, DotaStars, DotaRank, Updatable
import steam.steamid
class Dota(Updatable):
__tablename__ = "dota"
@declared_attr
def _steamid(self):
return Column(BigInteger, ForeignKey("steam._steamid"), primary_key=True)
@declared_attr
def steam(self):
return relationship("Steam", backref=backref("dota", uselist=False))
@property
def steamid(self):
return steam.steamid.SteamID(self._steamid)
@declared_attr
def _rank_tier(self):
return Column(Integer)
@property
def medal(self) -> Optional[DotaMedal]:
if self._rank_tier is None:
return None
return DotaMedal(self._rank_tier // 10)
@medal.setter
def medal(self, value: DotaMedal):
if not isinstance(value, DotaMedal):
raise AttributeError("medal can only be set to DotaMedal objects.")
self._rank_tier = value.value * 10 + self.stars.value
@property
def stars(self) -> Optional[DotaStars]:
if self._rank_tier is None:
return None
return DotaStars(self._rank_tier % 10)
@stars.setter
def stars(self, value: DotaStars):
if not isinstance(value, DotaStars):
raise AttributeError("stars can only be set to DotaStars objects.")
self._rank_tier = self.medal.value * 10 + value.value
@property
def rank(self) -> Optional[DotaRank]:
if self._rank_tier is None:
return None
return DotaRank(self.medal, self.stars)
@rank.setter
def rank(self, value: Optional[DotaRank]):
if value is None:
self._rank_tier = None
return
if not isinstance(value, DotaRank):
raise AttributeError("rank can only be set to DotaRank objects (or None).")
self._rank_tier = value.rank_tier
@declared_attr
def wins(self):
return Column(Integer)
@declared_attr
def losses(self):
return Column(Integer)
def json(self):
rank = self.rank
return {
"rank": {
"raw": self._rank_tier,
"medal": rank.medal.name,
"rank": rank.stars.name
} if self._rank_tier is not None else None,
"wins": self.wins,
"losses": self.losses
}
def __repr__(self):
return f"<Dota account {self._steamid}>"
def __str__(self):
return f"[c]dota:{self._steamid}[/c]"

View file

@ -19,7 +19,7 @@ class Fiorygi:
return Column(Integer, nullable=False, default=0) return Column(Integer, nullable=False, default=0)
def __repr__(self): def __repr__(self):
return f"<Fiorygi di {self.royal}: {self.fiorygi}>" return f"<{self.__class__.__name__} di {self.user}: {self.fiorygi}>"
def __str__(self): def __str__(self):
return f"{self.fiorygi} fioryg" + ("i" if self.fiorygi != 1 else "") return f"{self.fiorygi} fioryg" + ("i" if self.fiorygi != 1 else "")

View file

@ -0,0 +1,82 @@
from typing import *
import datetime
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declared_attr
from .fiorygi import Fiorygi
if TYPE_CHECKING:
from royalnet.commands import CommandData
class FiorygiTransaction:
__tablename__ = "fiorygitransactions"
@declared_attr
def id(self):
return Column(Integer, primary_key=True)
@declared_attr
def change(self):
return Column(Integer, nullable=False)
@declared_attr
def user_id(self):
return Column(Integer, ForeignKey("fiorygi.user_id"), nullable=False)
@declared_attr
def wallet(self):
return relationship("Fiorygi", backref=backref("transactions"))
@property
def user(self):
return self.wallet.user
@declared_attr
def reason(self):
return Column(String, nullable=False, default="")
@declared_attr
def timestamp(self):
return Column(DateTime)
def __repr__(self):
return f"<{self.__class__.__name__}: {self.change:+} to {self.user.username} for {self.reason}>"
@classmethod
async def spawn_fiorygi(cls, data: "CommandData", user, qty: int, reason: str):
if user.fiorygi is None:
data.session.add(data._interface.alchemy.get(Fiorygi)(
user_id=user.uid,
fiorygi=0
))
await data.session_commit()
transaction = data._interface.alchemy.get(FiorygiTransaction)(
user_id=user.uid,
change=qty,
reason=reason,
timestamp=datetime.datetime.now()
)
data.session.add(transaction)
user.fiorygi.fiorygi += qty
await data.session_commit()
if len(user.telegram) > 0:
user_str = user.telegram[0].mention()
else:
user_str = user.username
if qty > 0:
msg = f"💰 [b]{user_str}[/b] ha ottenuto [b]{qty}[/b] fioryg{'i' if qty != 1 else ''} per [i]{reason}[/i]!"
elif qty == 0:
msg = f"❓ [b]{user_str}[/b] ha mantenuto i suoi fiorygi attuali per [i]{reason}[/i].\nWait, cosa?"
else:
msg = f"💸 [b]{user_str}[/b] ha perso [b]{-qty}[/b] fioryg{'i' if qty != -1 else ''} per [i]{reason}[/i]."
await data._interface.call_herald_event("telegram", "telegram_message",
chat_id=data._interface.config["Telegram"]["main_group_id"],
text=msg)

Some files were not shown because too many files have changed in this diff Show more