spoonbot 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of spoonbot might be problematic. Click here for more details.
- spoonbot/__init__.py +25 -0
- spoonbot/config/ConstantsConfig.py +21 -0
- spoonbot/constants.py +12 -0
- spoonbot/controller/SpoonBotController.py +56 -0
- spoonbot/controller/SpoonBotWebController.py +52 -0
- spoonbot/controller/SpoonOAuthController.py +76 -0
- spoonbot/db/DBType.py +6 -0
- spoonbot/db/DatabaseClient.py +75 -0
- spoonbot/db/SpoonUserRepository.py +278 -0
- spoonbot/db/config/MySQLConfig.py +11 -0
- spoonbot/db/dto/TokenDataDto.py +23 -0
- spoonbot/db/dto/UpdateResult.py +7 -0
- spoonbot/db/mysql/MySQLClient.py +147 -0
- spoonbot/db/sqlite3/Sqlite3Client.py +111 -0
- spoonbot/db/types.py +1 -0
- spoonbot/dto/AuthorizeUrl.py +7 -0
- spoonbot/dto/ChatBody.py +12 -0
- spoonbot/dto/DonationBody.py +12 -0
- spoonbot/dto/DonationData.py +23 -0
- spoonbot/dto/FanData.py +13 -0
- spoonbot/dto/FollowBody.py +10 -0
- spoonbot/dto/LikeBody.py +21 -0
- spoonbot/dto/ListenerData.py +23 -0
- spoonbot/dto/LiveRoom.py +16 -0
- spoonbot/dto/PresenceBody.py +19 -0
- spoonbot/dto/RateLimit.py +11 -0
- spoonbot/dto/SpoonAuthorization.py +35 -0
- spoonbot/dto/User.py +6 -0
- spoonbot/dto/event/ChatEvent.py +9 -0
- spoonbot/dto/event/ConnectEvent.py +8 -0
- spoonbot/dto/event/DonationEvent.py +9 -0
- spoonbot/dto/event/EndEvent.py +7 -0
- spoonbot/dto/event/FollowEvent.py +9 -0
- spoonbot/dto/event/JoinEvent.py +9 -0
- spoonbot/dto/event/LikeEvent.py +9 -0
- spoonbot/enums/Country.py +6 -0
- spoonbot/enums/SpoonPermission.py +16 -0
- spoonbot/enums/TokenStatus.py +7 -0
- spoonbot/exception/BackoffRetryException.py +2 -0
- spoonbot/exception/BurstRequestException.py +11 -0
- spoonbot/exception/ChatRoomCrowdedException.py +5 -0
- spoonbot/exception/DailyRequestLimitException.py +12 -0
- spoonbot/exception/MessageSendUnavailableException.py +5 -0
- spoonbot/exception/NonBackoffRetryException.py +2 -0
- spoonbot/exception/NonRetryException.py +2 -0
- spoonbot/exception/TokenRefreshException.py +2 -0
- spoonbot/httpclient/APIClient.py +113 -0
- spoonbot/httpclient/SpoonApiClient.py +148 -0
- spoonbot/httpclient/dto/APIResponse.py +10 -0
- spoonbot/httpclient/dto/ChatSendRequestBody.py +24 -0
- spoonbot/httpclient/dto/DonationResponseDto.py +20 -0
- spoonbot/httpclient/dto/FanResponseDto.py +13 -0
- spoonbot/httpclient/dto/ListenersResponseDto.py +16 -0
- spoonbot/httpclient/dto/LiveRoomDto.py +16 -0
- spoonbot/httpclient/dto/TokenResponse.py +9 -0
- spoonbot/httpclient/dto/UserDataDto.py +5 -0
- spoonbot/manager/SpoonEventStreamManager.py +295 -0
- spoonbot/py.typed +0 -0
- spoonbot/spoon_bot.py +473 -0
- spoonbot/types/callback_types.py +17 -0
- spoonbot-0.1.0.dist-info/METADATA +209 -0
- spoonbot-0.1.0.dist-info/RECORD +63 -0
- spoonbot-0.1.0.dist-info/WHEEL +4 -0
spoonbot/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from spoonbot.db.config.MySQLConfig import MySQLConfig
|
|
2
|
+
from spoonbot.dto.event.ChatEvent import ChatEvent
|
|
3
|
+
from spoonbot.dto.event.ConnectEvent import ConnectEvent
|
|
4
|
+
from spoonbot.dto.event.DonationEvent import GiftEvent
|
|
5
|
+
from spoonbot.dto.event.EndEvent import EndEvent
|
|
6
|
+
from spoonbot.dto.event.FollowEvent import FollowEvent
|
|
7
|
+
from spoonbot.dto.event.JoinEvent import JoinEvent
|
|
8
|
+
from spoonbot.dto.event.LikeEvent import LikeEvent
|
|
9
|
+
from spoonbot.enums.Country import Country
|
|
10
|
+
from spoonbot.enums.SpoonPermission import SpoonPermission
|
|
11
|
+
from spoonbot.spoon_bot import SpoonBot
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"MySQLConfig",
|
|
15
|
+
"ChatEvent",
|
|
16
|
+
"ConnectEvent",
|
|
17
|
+
"GiftEvent",
|
|
18
|
+
"EndEvent",
|
|
19
|
+
"FollowEvent",
|
|
20
|
+
"JoinEvent",
|
|
21
|
+
"LikeEvent",
|
|
22
|
+
"Country",
|
|
23
|
+
"SpoonPermission",
|
|
24
|
+
"SpoonBot",
|
|
25
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from spoonbot.constants import (
|
|
4
|
+
API_BASE_URL_JP,
|
|
5
|
+
API_BASE_URL_KR,
|
|
6
|
+
OAUTH_AUTHORIZE_BASE_URL_JP,
|
|
7
|
+
OAUTH_AUTHORIZE_BASE_URL_KR,
|
|
8
|
+
)
|
|
9
|
+
from spoonbot.enums.Country import Country
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConstantsConfig:
|
|
13
|
+
def __init__(self, oauth_authorize_base_url: str, api_base_url: str) -> None:
|
|
14
|
+
self.oauth_authorize_base_url = oauth_authorize_base_url
|
|
15
|
+
self.api_base_url = api_base_url
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def create(cls, country: Country) -> Self:
|
|
19
|
+
if country == Country.KR:
|
|
20
|
+
return cls(OAUTH_AUTHORIZE_BASE_URL_KR, API_BASE_URL_KR)
|
|
21
|
+
return cls(OAUTH_AUTHORIZE_BASE_URL_JP, API_BASE_URL_JP)
|
spoonbot/constants.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
OAUTH_AUTHORIZE_BASE_URL_KR = "https://developers.spooncast.net/kr/oauth/authorize"
|
|
2
|
+
OAUTH_AUTHORIZE_BASE_URL_JP = "https://developers.spooncast.net/jp/oauth/authorize"
|
|
3
|
+
|
|
4
|
+
API_BASE_URL_KR = "https://kr-openapi.spooncast.net/v1/"
|
|
5
|
+
API_BASE_URL_JP = "https://jp-openapi.spooncast.net/v1/"
|
|
6
|
+
|
|
7
|
+
SPACE = " "
|
|
8
|
+
# https://developers.spooncast.net/docs/en/api-reference/tokens
|
|
9
|
+
DEFAULT_REFRESH_TOKEN_EXPIRES_IN = 60 * 60 * 24 * 30 # 30 days
|
|
10
|
+
DEFAULT_SESSION_EXPIRES_IN = (
|
|
11
|
+
DEFAULT_REFRESH_TOKEN_EXPIRES_IN - 60 * 15
|
|
12
|
+
) # 29 days 23 hour 45 minutes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
|
2
|
+
from aiohttp.web_request import Request
|
|
3
|
+
from aiohttp.web_response import Response, json_response
|
|
4
|
+
from aiohttp.web_routedef import RouteTableDef
|
|
5
|
+
|
|
6
|
+
from spoonbot.spoon_bot import SpoonBot
|
|
7
|
+
|
|
8
|
+
routes = RouteTableDef()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpoonBotController:
|
|
12
|
+
spoon_bot: SpoonBot
|
|
13
|
+
|
|
14
|
+
def __init__(self, spoon_bot: SpoonBot) -> None:
|
|
15
|
+
self.spoon_bot = spoon_bot
|
|
16
|
+
|
|
17
|
+
routes.get("/api/v1/csrf-token")(self.csrf_token_function())
|
|
18
|
+
routes.get("/api/v1/bot")(self.bot_information_function())
|
|
19
|
+
routes.post("/api/v1/bot/invite")(self.bot_invite_request_handler())
|
|
20
|
+
|
|
21
|
+
def csrf_token_function(self):
|
|
22
|
+
async def csrf_token_handler(request: Request) -> Response:
|
|
23
|
+
session_id = request.cookies.get("session_id")
|
|
24
|
+
if session_id is None:
|
|
25
|
+
raise HTTPUnauthorized()
|
|
26
|
+
csrf_token = await self.spoon_bot.regenerate_csrf_token(session_id)
|
|
27
|
+
return json_response({"csrf_token": csrf_token})
|
|
28
|
+
|
|
29
|
+
return csrf_token_handler
|
|
30
|
+
|
|
31
|
+
def bot_information_function(self):
|
|
32
|
+
async def bot_information_handler(_: Request) -> Response:
|
|
33
|
+
page = self.spoon_bot.get_bot_operation_page()
|
|
34
|
+
return json_response(
|
|
35
|
+
{"bot_name": self.spoon_bot.get_bot_name(), "page": page}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return bot_information_handler
|
|
39
|
+
|
|
40
|
+
def bot_invite_request_handler(self):
|
|
41
|
+
async def bot_invite_request(request: Request) -> Response:
|
|
42
|
+
session_id = request.cookies.get("session_id")
|
|
43
|
+
csrf_token = request.headers.get("X-CSRF-TOKEN")
|
|
44
|
+
if session_id is None:
|
|
45
|
+
raise HTTPUnauthorized()
|
|
46
|
+
if csrf_token is None:
|
|
47
|
+
raise HTTPForbidden(reason="Bad Request. Need X-CSRF-TOKEN")
|
|
48
|
+
result = await self.spoon_bot.start_stream_by_session(
|
|
49
|
+
session_id, csrf_token
|
|
50
|
+
)
|
|
51
|
+
return Response(status=204 if result else 409)
|
|
52
|
+
|
|
53
|
+
return bot_invite_request
|
|
54
|
+
|
|
55
|
+
def get_route(self) -> RouteTableDef:
|
|
56
|
+
return routes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from aiohttp.web_exceptions import HTTPNotFound
|
|
5
|
+
from aiohttp.web_fileresponse import FileResponse
|
|
6
|
+
from aiohttp.web_request import Request
|
|
7
|
+
from aiohttp.web_response import StreamResponse
|
|
8
|
+
from aiohttp.web_routedef import RouteTableDef
|
|
9
|
+
|
|
10
|
+
routes = RouteTableDef()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_base_dir() -> Path:
|
|
14
|
+
# PyInstaller などで実行ファイル化されている場合
|
|
15
|
+
meipass = getattr(sys, "_MEIPASS", None)
|
|
16
|
+
if meipass is not None:
|
|
17
|
+
return Path(meipass)
|
|
18
|
+
|
|
19
|
+
if getattr(sys, "frozen", False):
|
|
20
|
+
return Path(sys.executable).resolve().parent
|
|
21
|
+
|
|
22
|
+
# src/controller/xxx.py
|
|
23
|
+
return Path(__file__).resolve().parents[3]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PAGES = {
|
|
27
|
+
"connect": "connect.html",
|
|
28
|
+
"success": "success.html",
|
|
29
|
+
"fail": "fail.html",
|
|
30
|
+
"invite": "invite.html",
|
|
31
|
+
}
|
|
32
|
+
ASSETS_DIR = _get_base_dir() / "assets"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SpoonBotWebController:
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
for page in PAGES:
|
|
38
|
+
routes.get(f"/spoonbot/{page}")(self.serve_page)
|
|
39
|
+
routes.static("/assets/", ASSETS_DIR)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
async def serve_page(request: Request) -> StreamResponse:
|
|
43
|
+
filename = PAGES[request.path[len("/spoonbot/") :]]
|
|
44
|
+
filepath = ASSETS_DIR / filename
|
|
45
|
+
|
|
46
|
+
if not filepath.is_file():
|
|
47
|
+
raise HTTPNotFound()
|
|
48
|
+
|
|
49
|
+
return FileResponse(filepath)
|
|
50
|
+
|
|
51
|
+
def get_route(self) -> RouteTableDef:
|
|
52
|
+
return routes
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from aiohttp.web_exceptions import HTTPFound
|
|
4
|
+
from aiohttp.web_request import Request
|
|
5
|
+
from aiohttp.web_routedef import RouteTableDef
|
|
6
|
+
|
|
7
|
+
from spoonbot.constants import DEFAULT_SESSION_EXPIRES_IN
|
|
8
|
+
from spoonbot.spoon_bot import SpoonBot
|
|
9
|
+
|
|
10
|
+
routes = RouteTableDef()
|
|
11
|
+
SIGNED_STATE_KEY = "signed_state"
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SpoonOAuthController:
|
|
16
|
+
spoon_bot: SpoonBot
|
|
17
|
+
|
|
18
|
+
def __init__(self, spoon_bot: SpoonBot) -> None:
|
|
19
|
+
self.spoon_bot = spoon_bot
|
|
20
|
+
|
|
21
|
+
routes.get("/oauth")(self.redirect_authorization_url_handler())
|
|
22
|
+
routes.get("/oauth/callback")(self.get_token_via_oauth_handler())
|
|
23
|
+
|
|
24
|
+
def redirect_authorization_url_handler(self):
|
|
25
|
+
async def redirect_authorization_url(_: Request) -> HTTPFound:
|
|
26
|
+
authorize_url = self.spoon_bot.user_authorize_url()
|
|
27
|
+
response = HTTPFound(location=authorize_url.url)
|
|
28
|
+
|
|
29
|
+
response.set_cookie(
|
|
30
|
+
name=SIGNED_STATE_KEY,
|
|
31
|
+
value=authorize_url.state,
|
|
32
|
+
max_age=60 * 3, # 3分有効
|
|
33
|
+
httponly=True,
|
|
34
|
+
samesite="Lax",
|
|
35
|
+
secure=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return response
|
|
39
|
+
|
|
40
|
+
return redirect_authorization_url
|
|
41
|
+
|
|
42
|
+
def get_token_via_oauth_handler(self):
|
|
43
|
+
async def get_token_via_oauth(request: Request):
|
|
44
|
+
code = request.query.get("code")
|
|
45
|
+
signed_state = request.query.get("state")
|
|
46
|
+
cookie_state = request.cookies.get(SIGNED_STATE_KEY)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
auth = await self.spoon_bot.get_authorization(
|
|
50
|
+
code, cookie_state, signed_state
|
|
51
|
+
)
|
|
52
|
+
await self.spoon_bot.save_authorization(auth)
|
|
53
|
+
session_id = await self.spoon_bot.save_session(auth)
|
|
54
|
+
|
|
55
|
+
response = HTTPFound(location="/spoonbot/success")
|
|
56
|
+
response.del_cookie(SIGNED_STATE_KEY)
|
|
57
|
+
response.set_cookie(
|
|
58
|
+
name="session_id",
|
|
59
|
+
value=session_id,
|
|
60
|
+
max_age=DEFAULT_SESSION_EXPIRES_IN,
|
|
61
|
+
httponly=True,
|
|
62
|
+
samesite="Lax",
|
|
63
|
+
secure=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return response
|
|
67
|
+
except Exception as exception:
|
|
68
|
+
logger.exception(exception)
|
|
69
|
+
response = HTTPFound(location="/spoonbot/fail")
|
|
70
|
+
response.del_cookie(SIGNED_STATE_KEY)
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
return get_token_via_oauth
|
|
74
|
+
|
|
75
|
+
def get_route(self) -> RouteTableDef:
|
|
76
|
+
return routes
|
spoonbot/db/DBType.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from contextlib import AbstractAsyncContextManager
|
|
3
|
+
from typing import Any, LiteralString, TypeVar
|
|
4
|
+
|
|
5
|
+
from spoonbot.db.config.MySQLConfig import MySQLConfig
|
|
6
|
+
from spoonbot.db.DBType import DBType
|
|
7
|
+
from spoonbot.db.types import DBValueType
|
|
8
|
+
|
|
9
|
+
ResponseGenericType = TypeVar("ResponseGenericType")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DatabaseClient(ABC):
|
|
13
|
+
db_type: DBType
|
|
14
|
+
|
|
15
|
+
def __init__(self, db_type: DBType) -> None:
|
|
16
|
+
self.db_type = db_type
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
async def create(cls, config: MySQLConfig | None = None) -> "DatabaseClient":
|
|
20
|
+
# lazy import
|
|
21
|
+
from spoonbot.db.mysql.MySQLClient import MySQLClient
|
|
22
|
+
from spoonbot.db.sqlite3.Sqlite3Client import Sqlite3Client
|
|
23
|
+
|
|
24
|
+
if config is None:
|
|
25
|
+
client = Sqlite3Client(DBType.SQLITE3)
|
|
26
|
+
await client.initialize()
|
|
27
|
+
return client
|
|
28
|
+
client = MySQLClient(DBType.MYSQL)
|
|
29
|
+
await client.initialize(config)
|
|
30
|
+
return client
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def initialize(self, *args, **kwargs):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def close(self) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def execute_one(
|
|
42
|
+
self,
|
|
43
|
+
query: str,
|
|
44
|
+
args: dict[str, DBValueType] | None = None,
|
|
45
|
+
response_type: type[ResponseGenericType] | None = None,
|
|
46
|
+
) -> ResponseGenericType | None:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def execute_many(
|
|
51
|
+
self,
|
|
52
|
+
query: str,
|
|
53
|
+
args: dict[str, DBValueType] | None = None,
|
|
54
|
+
response_type: type[ResponseGenericType] | None = None,
|
|
55
|
+
) -> list[ResponseGenericType] | None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def transaction(self) -> AbstractAsyncContextManager[Any]:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def execute_cud(
|
|
64
|
+
self,
|
|
65
|
+
query: str,
|
|
66
|
+
args: dict[str, DBValueType] | None = None,
|
|
67
|
+
) -> int:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def placeholder(self, name: LiteralString) -> str:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def get_db_type(self) -> DBType:
|
|
75
|
+
return self.db_type
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from spoonbot.db.config.MySQLConfig import MySQLConfig
|
|
4
|
+
from spoonbot.db.DatabaseClient import DatabaseClient
|
|
5
|
+
from spoonbot.db.DBType import DBType
|
|
6
|
+
from spoonbot.db.dto.TokenDataDto import SessionDataDto, TokenDataDto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpoonUserRepository:
|
|
10
|
+
database_client: DatabaseClient
|
|
11
|
+
|
|
12
|
+
async def initialize(self, config: MySQLConfig | None = None):
|
|
13
|
+
self.database_client = await DatabaseClient.create(config)
|
|
14
|
+
await self._create_table()
|
|
15
|
+
|
|
16
|
+
async def close(self):
|
|
17
|
+
await self.database_client.close()
|
|
18
|
+
|
|
19
|
+
async def _create_table(self):
|
|
20
|
+
if self.database_client.get_db_type() == DBType.MYSQL:
|
|
21
|
+
await self._create_table_for_mysql()
|
|
22
|
+
else:
|
|
23
|
+
await self._create_table_for_sqlite()
|
|
24
|
+
|
|
25
|
+
async def _create_table_for_mysql(self):
|
|
26
|
+
await self.database_client.execute_cud(
|
|
27
|
+
query="""-- sql
|
|
28
|
+
CREATE TABLE IF NOT EXISTS authorization_spoon_users (
|
|
29
|
+
id VARCHAR(256) PRIMARY KEY,
|
|
30
|
+
access_token VARCHAR(256) NOT NULL,
|
|
31
|
+
refresh_token VARCHAR(256) NOT NULL,
|
|
32
|
+
expire_at BIGINT,
|
|
33
|
+
refresh_expire_at BIGINT,
|
|
34
|
+
scope VARCHAR(512),
|
|
35
|
+
token_type VARCHAR(32),
|
|
36
|
+
token_status VARCHAR(32) DEFAULT "DEFAULT",
|
|
37
|
+
|
|
38
|
+
created_at BIGINT NOT NULL,
|
|
39
|
+
updated_at BIGINT NOT NULL,
|
|
40
|
+
|
|
41
|
+
INDEX idx_expire_at (expire_at),
|
|
42
|
+
INDEX idx_refresh_expire_at (refresh_expire_at)
|
|
43
|
+
)
|
|
44
|
+
"""
|
|
45
|
+
)
|
|
46
|
+
await self.database_client.execute_cud(
|
|
47
|
+
query="""-- sql
|
|
48
|
+
CREATE TABLE IF NOT EXISTS authorization_spoon_user_sessions (
|
|
49
|
+
id BINARY(16) PRIMARY KEY,
|
|
50
|
+
authorization_spoon_user_id VARCHAR(256) NOT NULL,
|
|
51
|
+
encoded_session_id BINARY(32) NOT NULL,
|
|
52
|
+
csrf_token VARCHAR(64),
|
|
53
|
+
|
|
54
|
+
expires_at BIGINT NOT NULL,
|
|
55
|
+
created_at BIGINT NOT NULL,
|
|
56
|
+
updated_at BIGINT NOT NULL,
|
|
57
|
+
FOREIGN KEY (authorization_spoon_user_id)
|
|
58
|
+
REFERENCES authorization_spoon_users(id),
|
|
59
|
+
INDEX idx_encoded_session_id (encoded_session_id),
|
|
60
|
+
INDEX idx_authorization_spoon_user_id (authorization_spoon_user_id)
|
|
61
|
+
)
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def _create_table_for_sqlite(self):
|
|
66
|
+
await self.database_client.execute_cud(
|
|
67
|
+
query="""-- sql
|
|
68
|
+
CREATE TABLE IF NOT EXISTS authorization_spoon_users (
|
|
69
|
+
id VARCHAR(256) PRIMARY KEY,
|
|
70
|
+
access_token VARCHAR(256) NOT NULL,
|
|
71
|
+
refresh_token VARCHAR(256) NOT NULL,
|
|
72
|
+
expire_at BIGINT,
|
|
73
|
+
refresh_expire_at BIGINT,
|
|
74
|
+
scope VARCHAR(512),
|
|
75
|
+
token_type VARCHAR(32),
|
|
76
|
+
token_status VARCHAR(32) DEFAULT "DEFAULT",
|
|
77
|
+
|
|
78
|
+
created_at BIGINT NOT NULL,
|
|
79
|
+
updated_at BIGINT NOT NULL
|
|
80
|
+
)
|
|
81
|
+
"""
|
|
82
|
+
)
|
|
83
|
+
await self.database_client.execute_cud(
|
|
84
|
+
"""-- sql
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_expire_at ON authorization_spoon_users(expire_at)
|
|
86
|
+
""" # noqa: E501
|
|
87
|
+
)
|
|
88
|
+
await self.database_client.execute_cud(
|
|
89
|
+
"""-- sql
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_refresh_expire_at ON authorization_spoon_users(refresh_expire_at)
|
|
91
|
+
""" # noqa: E501
|
|
92
|
+
)
|
|
93
|
+
await self.database_client.execute_cud(
|
|
94
|
+
query="""-- sql
|
|
95
|
+
CREATE TABLE IF NOT EXISTS authorization_spoon_user_sessions (
|
|
96
|
+
id BINARY(16) PRIMARY KEY,
|
|
97
|
+
authorization_spoon_user_id VARCHAR(256) NOT NULL,
|
|
98
|
+
encoded_session_id BINARY(32) NOT NULL,
|
|
99
|
+
csrf_token VARCHAR(64),
|
|
100
|
+
|
|
101
|
+
expires_at BIGINT NOT NULL,
|
|
102
|
+
created_at BIGINT NOT NULL,
|
|
103
|
+
updated_at BIGINT NOT NULL,
|
|
104
|
+
FOREIGN KEY (authorization_spoon_user_id) REFERENCES authorization_spoon_users(id)
|
|
105
|
+
)
|
|
106
|
+
""" # noqa: E501
|
|
107
|
+
)
|
|
108
|
+
await self.database_client.execute_cud(
|
|
109
|
+
"""-- sql
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_encoded_session_id ON authorization_spoon_user_sessions(encoded_session_id);
|
|
111
|
+
""" # noqa: E501
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
await self.database_client.execute_cud(
|
|
115
|
+
"""-- sql
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_authorization_spoon_user_id ON authorization_spoon_user_sessions(authorization_spoon_user_id);
|
|
117
|
+
""" # noqa: E501
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def insert_token(self, token: TokenDataDto) -> bool:
|
|
121
|
+
epoch_seconds = int(time.time())
|
|
122
|
+
result = await self.database_client.execute_cud(
|
|
123
|
+
query=f"""-- sql
|
|
124
|
+
INSERT INTO authorization_spoon_users(
|
|
125
|
+
id,
|
|
126
|
+
access_token,
|
|
127
|
+
refresh_token,
|
|
128
|
+
expire_at,
|
|
129
|
+
refresh_expire_at,
|
|
130
|
+
scope,
|
|
131
|
+
token_type,
|
|
132
|
+
created_at,
|
|
133
|
+
updated_at
|
|
134
|
+
) VALUES(
|
|
135
|
+
{self.database_client.placeholder("id")},
|
|
136
|
+
{self.database_client.placeholder("access_token")},
|
|
137
|
+
{self.database_client.placeholder("refresh_token")},
|
|
138
|
+
{self.database_client.placeholder("expire_at")},
|
|
139
|
+
{self.database_client.placeholder("refresh_expire_at")},
|
|
140
|
+
{self.database_client.placeholder("scope")},
|
|
141
|
+
{self.database_client.placeholder("token_type")},
|
|
142
|
+
{self.database_client.placeholder("created_at")},
|
|
143
|
+
{self.database_client.placeholder("updated_at")}
|
|
144
|
+
)
|
|
145
|
+
""",
|
|
146
|
+
args=token.model_dump()
|
|
147
|
+
| {"created_at": epoch_seconds, "updated_at": epoch_seconds},
|
|
148
|
+
)
|
|
149
|
+
return result == 1
|
|
150
|
+
|
|
151
|
+
async def update_token(self, token: TokenDataDto):
|
|
152
|
+
epoch_seconds = int(time.time())
|
|
153
|
+
result = await self.database_client.execute_cud(
|
|
154
|
+
query=f"""-- sql
|
|
155
|
+
UPDATE authorization_spoon_users
|
|
156
|
+
SET access_token = {self.database_client.placeholder("access_token")},
|
|
157
|
+
refresh_token = {self.database_client.placeholder("refresh_token")},
|
|
158
|
+
expire_at = {self.database_client.placeholder("expire_at")},
|
|
159
|
+
refresh_expire_at = {self.database_client.placeholder("expire_at")},
|
|
160
|
+
scope = {self.database_client.placeholder("scope")},
|
|
161
|
+
token_type = {self.database_client.placeholder("token_type")},
|
|
162
|
+
updated_at = {self.database_client.placeholder("updated_at")}
|
|
163
|
+
WHERE id = {self.database_client.placeholder("id")}
|
|
164
|
+
LIMIT 1
|
|
165
|
+
""", # noqa: E501
|
|
166
|
+
args=token.model_dump() | {"updated_at": epoch_seconds},
|
|
167
|
+
)
|
|
168
|
+
return result == 1
|
|
169
|
+
|
|
170
|
+
async def get_token_data(self, id: str) -> TokenDataDto | None:
|
|
171
|
+
return await self.database_client.execute_one(
|
|
172
|
+
query=f"""-- sql
|
|
173
|
+
SELECT
|
|
174
|
+
id,
|
|
175
|
+
access_token,
|
|
176
|
+
refresh_token,
|
|
177
|
+
expire_at,
|
|
178
|
+
refresh_expire_at,
|
|
179
|
+
scope,
|
|
180
|
+
token_type,
|
|
181
|
+
token_status
|
|
182
|
+
FROM authorization_spoon_users
|
|
183
|
+
WHERE id = {self.database_client.placeholder("id")}
|
|
184
|
+
LIMIT 1
|
|
185
|
+
""",
|
|
186
|
+
args={"id": id},
|
|
187
|
+
response_type=TokenDataDto,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def get_token_data_by_session_id(
|
|
191
|
+
self, encoded_session_id: bytes
|
|
192
|
+
) -> TokenDataDto | None:
|
|
193
|
+
return await self.database_client.execute_one(
|
|
194
|
+
query=f"""-- sql
|
|
195
|
+
SELECT
|
|
196
|
+
user.id as id,
|
|
197
|
+
access_token,
|
|
198
|
+
refresh_token,
|
|
199
|
+
expire_at,
|
|
200
|
+
refresh_expire_at,
|
|
201
|
+
scope,
|
|
202
|
+
token_type,
|
|
203
|
+
token_status,
|
|
204
|
+
csrf_token
|
|
205
|
+
FROM authorization_spoon_users as user
|
|
206
|
+
INNER JOIN authorization_spoon_user_sessions as session_tb
|
|
207
|
+
ON user.id = session_tb.authorization_spoon_user_id
|
|
208
|
+
WHERE session_tb.encoded_session_id = {self.database_client.placeholder("encoded_session_id")}
|
|
209
|
+
LIMIT 1
|
|
210
|
+
""", # noqa: E501
|
|
211
|
+
args={"encoded_session_id": encoded_session_id},
|
|
212
|
+
response_type=TokenDataDto,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
async def insert_session(self, session_data: SessionDataDto) -> bool:
|
|
216
|
+
epoch_seconds = int(time.time())
|
|
217
|
+
result = await self.database_client.execute_cud(
|
|
218
|
+
query=f"""-- sql
|
|
219
|
+
INSERT INTO authorization_spoon_user_sessions(
|
|
220
|
+
id,
|
|
221
|
+
authorization_spoon_user_id,
|
|
222
|
+
encoded_session_id,
|
|
223
|
+
csrf_token,
|
|
224
|
+
expires_at,
|
|
225
|
+
created_at,
|
|
226
|
+
updated_at
|
|
227
|
+
) VALUES(
|
|
228
|
+
{self.database_client.placeholder("id")},
|
|
229
|
+
{self.database_client.placeholder("authorization_spoon_user_id")},
|
|
230
|
+
{self.database_client.placeholder("encoded_session_id")},
|
|
231
|
+
{self.database_client.placeholder("csrf_token")},
|
|
232
|
+
{self.database_client.placeholder("expires_at")},
|
|
233
|
+
{self.database_client.placeholder("created_at")},
|
|
234
|
+
{self.database_client.placeholder("updated_at")}
|
|
235
|
+
)
|
|
236
|
+
""",
|
|
237
|
+
args=session_data.model_dump()
|
|
238
|
+
| {"created_at": epoch_seconds, "updated_at": epoch_seconds},
|
|
239
|
+
)
|
|
240
|
+
return result == 1
|
|
241
|
+
|
|
242
|
+
async def get_session_data(
|
|
243
|
+
self, encoded_session_id: bytes
|
|
244
|
+
) -> SessionDataDto | None:
|
|
245
|
+
return await self.database_client.execute_one(
|
|
246
|
+
query=f"""-- sql
|
|
247
|
+
SELECT
|
|
248
|
+
id,
|
|
249
|
+
authorization_spoon_user_id,
|
|
250
|
+
encoded_session_id,
|
|
251
|
+
csrf_token,
|
|
252
|
+
expires_at
|
|
253
|
+
FROM authorization_spoon_user_sessions
|
|
254
|
+
WHERE encoded_session_id = {self.database_client.placeholder("encoded_session_id")}
|
|
255
|
+
""", # noqa: E501
|
|
256
|
+
args={"encoded_session_id": encoded_session_id},
|
|
257
|
+
response_type=SessionDataDto,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
async def update_csrf_token(
|
|
261
|
+
self, encoded_session_id: bytes, csrf_token: bytes
|
|
262
|
+
) -> bool:
|
|
263
|
+
epoch_seconds = int(time.time())
|
|
264
|
+
result = await self.database_client.execute_cud(
|
|
265
|
+
query=f"""-- sql
|
|
266
|
+
UPDATE authorization_spoon_user_sessions
|
|
267
|
+
SET csrf_token = {self.database_client.placeholder("csrf_token")},
|
|
268
|
+
updated_at = {self.database_client.placeholder("updated_at")}
|
|
269
|
+
WHERE encoded_session_id = {self.database_client.placeholder("encoded_session_id")}
|
|
270
|
+
LIMIt 1
|
|
271
|
+
""", # noqa: E501
|
|
272
|
+
args={
|
|
273
|
+
"encoded_session_id": encoded_session_id,
|
|
274
|
+
"csrf_token": csrf_token,
|
|
275
|
+
"updated_at": epoch_seconds,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
return result == 1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TokenDataDto(BaseModel):
|
|
5
|
+
id: str
|
|
6
|
+
access_token: str
|
|
7
|
+
token_type: str
|
|
8
|
+
expire_at: int
|
|
9
|
+
refresh_token: str
|
|
10
|
+
refresh_expire_at: int
|
|
11
|
+
scope: str
|
|
12
|
+
token_status: str
|
|
13
|
+
|
|
14
|
+
encoded_session_id: bytes | None = None
|
|
15
|
+
csrf_token: bytes | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SessionDataDto(BaseModel):
|
|
19
|
+
id: bytes
|
|
20
|
+
authorization_spoon_user_id: str
|
|
21
|
+
encoded_session_id: bytes
|
|
22
|
+
csrf_token: bytes | None = None
|
|
23
|
+
expires_at: int
|