Skip to content

Commit c4e142a

Browse files
committed
Add base adapter design
1 parent e47ce30 commit c4e142a

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed

twitchio/ext/overlays/adapters.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
MIT License
3+
4+
Copyright (c) 2017 - Present PythonistaGuild
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import abc
28+
import logging
29+
import secrets
30+
from typing import TYPE_CHECKING, Any
31+
32+
from aiohttp import web
33+
34+
from twitchio.web import AiohttpAdapter, BaseAdapter
35+
36+
from .exceptions import *
37+
38+
39+
if TYPE_CHECKING:
40+
from .core import Overlay
41+
42+
43+
LOGGER: logging.Logger = logging.getLogger(__name__)
44+
45+
46+
__all__ = ("AiohttpOverlayAdapter", "BaseOverlayAdapter")
47+
48+
49+
class BaseOverlayAdapter(BaseAdapter):
50+
@abc.abstractmethod
51+
async def _overlay_callback(self, request: Any) -> ...: ...
52+
53+
54+
class AiohttpOverlayAdapter(BaseOverlayAdapter, AiohttpAdapter):
55+
# TODO: Type args/kwargs
56+
57+
def __init__(self, *args: Any, **kwargs: Any) -> None:
58+
super().__init__(*args, **kwargs)
59+
self._overlays: dict[str, Overlay] = {}
60+
61+
self.router.add_route("GET", "/overlays/{secret}", self.overlay_route)
62+
self.router.add_route("POST", "/overlays/{secret}/callback", self._overlay_callback)
63+
self.router.add_route("GET", "/overlays/{secret}/connect", self.websocket_connect)
64+
65+
async def add_overlay(self, overlay: Overlay) -> Overlay:
66+
try:
67+
await overlay.setup()
68+
except Exception as e:
69+
raise OverlayLoadError(f"An error occurred during setup in {overlay!r}.") from e
70+
71+
if not overlay.secret:
72+
raise OverlayLoadError(f"No secret was provided for {overlay!r}.")
73+
74+
elif overlay.secret in self._overlays:
75+
raise OverlayLoadError("An overlay has already been loaded with the provided secret.")
76+
77+
self._overlays[overlay.secret] = overlay
78+
return overlay
79+
80+
async def overlay_route(self, request: web.Request) -> web.Response:
81+
info = request.match_info
82+
secret = info.get("secret")
83+
84+
if not secret:
85+
return web.Response(text="No overlay path was provided.", status=400)
86+
87+
overlay = self._overlays.get(secret)
88+
if not overlay:
89+
return web.Response(text=f"Overlay '{secret}' not found.", status=404)
90+
91+
headers = {"Content-Type": "text/html"}
92+
return web.Response(body=overlay.generate_html(), headers=headers)
93+
94+
async def _overlay_callback(self, request: web.Request) -> web.Response:
95+
print(await request.text())
96+
return web.Response(body="Hi! CALLBACK")
97+
98+
async def _ws_gen(self, overlay: Overlay, id: str, ws: web.WebSocketResponse) -> None:
99+
while True:
100+
msg = await ws.receive()
101+
print(msg)
102+
103+
if msg.type == web.WSMsgType.CLOSED:
104+
break
105+
106+
try:
107+
overlay.__sockets__.pop(id, None)
108+
except ValueError:
109+
pass
110+
111+
async def close(self, *args: Any, **kwargs: Any) -> None:
112+
for overlay in self._overlays.values():
113+
await overlay.close()
114+
115+
return await super().close(*args, **kwargs)
116+
117+
async def websocket_connect(self, request: web.Request) -> web.WebSocketResponse | web.Response:
118+
info = request.match_info
119+
secret = info.get("secret")
120+
121+
if not secret:
122+
return web.Response(text="No overlay path was provided.", status=400)
123+
124+
overlay = self._overlays.get(secret)
125+
if not overlay:
126+
return web.Response(text=f"Overlay '{secret}' not found.", status=404)
127+
128+
ws = web.WebSocketResponse(heartbeat=10)
129+
await ws.prepare(request)
130+
131+
socket_id = secrets.token_urlsafe(16)
132+
overlay.__sockets__[socket_id] = ws
133+
134+
try:
135+
await self._ws_gen(overlay, socket_id, ws)
136+
except Exception as e:
137+
LOGGER.warning(e)
138+
139+
return ws
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
MIT License
3+
4+
Copyright (c) 2017 - Present PythonistaGuild
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
"""
24+
25+
from twitchio import TwitchioException
26+
27+
28+
__all__ = ("OverlayException", "OverlayLoadError")
29+
30+
31+
class OverlayException(TwitchioException): ...
32+
33+
34+
class OverlayLoadError(OverlayException): ...
35+
36+
37+
class OverlayNotSetupError(OverlayException): ...
38+
39+
40+
class BlueprintError(OverlayException): ...
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
MIT License
3+
4+
Copyright (c) 2017 - Present PythonistaGuild
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.
23+
"""
24+
25+
from starlette.requests import Request
26+
from starlette.responses import Response
27+
28+
from twitchio.web import StarletteAdapter
29+
30+
from .adapters import BaseOverlayAdapter
31+
32+
33+
__all__ = ("StarletteOverlayAdapter",)
34+
35+
36+
class StarletteOverlayAdapter(BaseOverlayAdapter, StarletteAdapter):
37+
async def overlay_callback(self, request: Request) -> Response: ...
38+
39+
async def push(self) -> ...: ...

0 commit comments

Comments
 (0)