mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-23 19:44:20 +00:00
Merge branch 'osu'
# Conflicts: # pyproject.toml
This commit is contained in:
commit
c41ae6b116
9 changed files with 343 additions and 3 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "royalpack"
|
name = "royalpack"
|
||||||
version = "5.13.1"
|
version = "5.13.2"
|
||||||
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+"
|
||||||
|
|
|
@ -38,6 +38,7 @@ from .steampowered import SteampoweredCommand
|
||||||
from .treasure import TreasureCommand
|
from .treasure import TreasureCommand
|
||||||
from .trivia import TriviaCommand
|
from .trivia import TriviaCommand
|
||||||
from .userinfo import UserinfoCommand
|
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 = [
|
||||||
|
@ -80,6 +81,7 @@ available_commands = [
|
||||||
TreasureCommand,
|
TreasureCommand,
|
||||||
TriviaCommand,
|
TriviaCommand,
|
||||||
UserinfoCommand,
|
UserinfoCommand,
|
||||||
|
OsuCommand,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
125
royalpack/commands/osu.py
Normal file
125
royalpack/commands/osu.py
Normal 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].")
|
|
@ -13,6 +13,7 @@ from .api_cvstats_avg import ApiCvstatsAvgStar
|
||||||
from .api_user_ryg import ApiUserRygStar
|
from .api_user_ryg import ApiUserRygStar
|
||||||
from .api_user_ryg_list import ApiUserRygListStar
|
from .api_user_ryg_list import ApiUserRygListStar
|
||||||
from .api_user_avatar import ApiUserAvatarStar
|
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 = [
|
||||||
|
@ -30,6 +31,7 @@ available_page_stars = [
|
||||||
ApiUserRygStar,
|
ApiUserRygStar,
|
||||||
ApiUserRygListStar,
|
ApiUserRygListStar,
|
||||||
ApiUserAvatarStar,
|
ApiUserAvatarStar,
|
||||||
|
ApiAuthLoginOsuStar,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
99
royalpack/stars/api_auth_login_osu.py
Normal file
99
royalpack/stars/api_auth_login_osu.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
|
@ -18,6 +18,7 @@ from .mmevents import MMEvent
|
||||||
from .mmresponse import MMResponse
|
from .mmresponse import MMResponse
|
||||||
from .cvstats import Cvstats
|
from .cvstats import Cvstats
|
||||||
from .treasure import Treasure
|
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 = [
|
||||||
|
@ -40,6 +41,7 @@ available_tables = [
|
||||||
MMResponse,
|
MMResponse,
|
||||||
Cvstats,
|
Cvstats,
|
||||||
Treasure,
|
Treasure,
|
||||||
|
Osu,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
94
royalpack/tables/osu.py
Normal file
94
royalpack/tables/osu.py
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
from typing import *
|
||||||
|
import aiohttp
|
||||||
|
import datetime
|
||||||
|
from sqlalchemy import *
|
||||||
|
from sqlalchemy.orm import relationship, backref
|
||||||
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
|
|
||||||
|
from ..types import Updatable, oauth_refresh
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyAttributeOutsideInit
|
||||||
|
class Osu(Updatable):
|
||||||
|
__tablename__ = "osu"
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user_id(self):
|
||||||
|
return Column(Integer, ForeignKey("users.uid"))
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def user(self):
|
||||||
|
return relationship("User", backref=backref("osu"))
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def access_token(self):
|
||||||
|
return Column(String, nullable=False)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def refresh_token(self):
|
||||||
|
return Column(String, nullable=False)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def expiration_date(self):
|
||||||
|
return Column(DateTime, nullable=False)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def osu_id(self):
|
||||||
|
return Column(Integer, primary_key=True)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def username(self):
|
||||||
|
return Column(String, nullable=False)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def avatar_url(self):
|
||||||
|
return Column(String, nullable=False)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def standard_pp(self):
|
||||||
|
return Column(Float)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def taiko_pp(self):
|
||||||
|
return Column(Float)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def catch_pp(self):
|
||||||
|
return Column(Float)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def mania_pp(self):
|
||||||
|
return Column(Float)
|
||||||
|
|
||||||
|
async def refresh(self, *, client_id, client_secret, base_url, path):
|
||||||
|
j = await oauth_refresh(url="https://osu.ppy.sh/oauth/token",
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
redirect_uri=f"{base_url}{path}",
|
||||||
|
refresh_code=self.refresh_token)
|
||||||
|
self.access_token = j["access_token"]
|
||||||
|
self.refresh_token = j["refresh_token"]
|
||||||
|
self.expiration_date = datetime.datetime.now() + datetime.timedelta(seconds=j["expires_in"])
|
||||||
|
|
||||||
|
async def refresh_if_expired(self, *, client_id, client_secret, base_url, path):
|
||||||
|
if datetime.datetime.now() >= self.expiration_date:
|
||||||
|
await self.refresh(client_id=client_id, client_secret=client_secret, base_url=base_url, path=path)
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return {
|
||||||
|
"osu_id": self.osu_id,
|
||||||
|
"username": self.username,
|
||||||
|
"avatar_url": self.avatar_url,
|
||||||
|
"standard": {
|
||||||
|
"pp": self.standard_pp,
|
||||||
|
},
|
||||||
|
"taiko": {
|
||||||
|
"pp": self.taiko_pp,
|
||||||
|
},
|
||||||
|
"catch": {
|
||||||
|
"pp": self.catch_pp,
|
||||||
|
},
|
||||||
|
"mania": {
|
||||||
|
"pp": self.mania_pp,
|
||||||
|
},
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ from .brawlhallametal import BrawlhallaMetal
|
||||||
from .brawlhallarank import BrawlhallaRank
|
from .brawlhallarank import BrawlhallaRank
|
||||||
from .pollmood import PollMood
|
from .pollmood import PollMood
|
||||||
from .updatable import Updatable
|
from .updatable import Updatable
|
||||||
|
from .oauth_refresh import oauth_refresh
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -28,4 +29,5 @@ __all__ = [
|
||||||
"BrawlhallaTier",
|
"BrawlhallaTier",
|
||||||
"PollMood",
|
"PollMood",
|
||||||
"Updatable",
|
"Updatable",
|
||||||
|
"oauth_refresh",
|
||||||
]
|
]
|
||||||
|
|
14
royalpack/types/oauth_refresh.py
Normal file
14
royalpack/types/oauth_refresh.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
async def oauth_refresh(*, url, client_id, client_secret, redirect_uri, refresh_code):
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(url, data={
|
||||||
|
"client_id": client_id,
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"code": refresh_code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"redirect_uri": redirect_uri
|
||||||
|
}) as response:
|
||||||
|
j = await response.json()
|
||||||
|
return j
|
Loading…
Reference in a new issue