# -*- coding: utf-8 -*-
# The MIT License (MIT)
# Copyright (c) 2021 Assanali Mukhanov
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
__all__ = [
"endpoint",
"BoundWebhookEndpoint",
"WebhookEndpoint",
"WebhookManager",
"WebhookType",
]
import enum
import typing as t
import aiohttp
from aiohttp import web
from aiohttp.typedefs import Handler
from topgg.errors import TopGGException
from .data import DataContainerMixin
from .types import BotVoteData, GuildVoteData
T = t.TypeVar("T", bound="WebhookEndpoint")
[docs]class WebhookType(enum.Enum):
"""An enum that represents the type of an endpoint."""
BOT = enum.auto()
"""Marks the endpoint as a bot webhook."""
GUILD = enum.auto()
"""Marks the endpoint as a guild webhook."""
[docs]class WebhookManager(DataContainerMixin):
"""
A class for managing Top.gg webhooks.
"""
__app: web.Application
_webserver: web.TCPSite
_is_closed: bool
__slots__ = ("__app", "_webserver", "_is_running")
def __init__(self) -> None:
super().__init__()
self.__app = web.Application()
self._is_running = False
@t.overload
def endpoint(self, endpoint_: None = None) -> "BoundWebhookEndpoint":
...
@t.overload
def endpoint(self, endpoint_: "WebhookEndpoint") -> "WebhookManager":
...
[docs] def endpoint(self, endpoint_: t.Optional["WebhookEndpoint"] = None) -> t.Any:
"""Helper method that returns a WebhookEndpoint object.
Args:
`endpoint_` (:obj:`typing.Optional` [ :obj:`WebhookEndpoint` ])
The endpoint to add.
Returns:
:obj:`typing.Union` [ :obj:`WebhookManager`, :obj:`BoundWebhookEndpoint` ]:
An instance of :obj:`WebhookManager` if endpoint was provided,
otherwise :obj:`BoundWebhookEndpoint`.
Raises:
:obj:`~.errors.TopGGException`
If the endpoint is lacking attributes.
"""
if endpoint_:
if not hasattr(endpoint_, "_callback"):
raise TopGGException("endpoint missing callback.")
if not hasattr(endpoint_, "_type"):
raise TopGGException("endpoint missing type.")
if not hasattr(endpoint_, "_route"):
raise TopGGException("endpoint missing route.")
self.app.router.add_post(
endpoint_._route,
self._get_handler(
endpoint_._type, endpoint_._auth, endpoint_._callback
),
)
return self
return BoundWebhookEndpoint(manager=self)
[docs] async def start(self, port: int) -> None:
"""Runs the webhook.
Args:
port (int)
The port to run the webhook on.
"""
runner = web.AppRunner(self.__app)
await runner.setup()
self._webserver = web.TCPSite(runner, "0.0.0.0", port)
await self._webserver.start()
self._is_running = True
@property
def is_running(self) -> bool:
"""Returns whether or not the webserver is running."""
return self._is_running
@property
def app(self) -> web.Application:
"""Returns the internal web application that handles webhook requests.
Returns:
:class:`aiohttp.web.Application`:
The internal web application.
"""
return self.__app
[docs] async def close(self) -> None:
"""Stops the webhook."""
await self._webserver.stop()
self._is_running = False
def _get_handler(
self, type_: WebhookType, auth: str, callback: t.Callable[..., t.Any]
) -> Handler:
async def _handler(request: aiohttp.web.Request) -> web.Response:
if request.headers.get("Authorization", "") != auth:
return web.Response(status=401, text="Unauthorized")
data = await request.json()
await self._invoke_callback(
callback,
(BotVoteData if type_ is WebhookType.BOT else GuildVoteData)(**data),
)
return web.Response(status=200, text="OK")
return _handler
CallbackT = t.Callable[..., t.Any]
[docs]class WebhookEndpoint:
"""
A helper class to setup webhook endpoint.
"""
__slots__ = ("_callback", "_auth", "_route", "_type")
def __init__(self) -> None:
self._auth = ""
def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
return self._callback(*args, **kwargs)
[docs] def type(self: T, type_: WebhookType) -> T:
"""Sets the type of this endpoint.
Args:
`type_` (:obj:`WebhookType`)
The type of the endpoint.
Returns:
:obj:`WebhookEndpoint`
"""
self._type = type_
return self
[docs] def route(self: T, route_: str) -> T:
"""
Sets the route of this endpoint.
Args:
`route_` (str)
The route of this endpoint.
Returns:
:obj:`WebhookEndpoint`
"""
self._route = route_
return self
[docs] def auth(self: T, auth_: str) -> T:
"""
Sets the auth of this endpoint.
Args:
`auth_` (str)
The auth of this endpoint.
Returns:
:obj:`WebhookEndpoint`
"""
self._auth = auth_
return self
@t.overload
def callback(self, callback_: None) -> t.Callable[[CallbackT], CallbackT]:
...
@t.overload
def callback(self: T, callback_: CallbackT) -> T:
...
[docs] def callback(self, callback_: t.Any = None) -> t.Any:
"""
Registers a vote callback, called whenever this endpoint receives POST requests.
The callback can be either sync or async.
This method can be used as a decorator or a decorator factory.
:Example:
.. code-block:: python
import topgg
webhook_manager = topgg.WebhookManager()
endpoint = (
topgg.WebhookEndpoint()
.type(topgg.WebhookType.BOT)
.route("/dblwebhook")
.auth("youshallnotpass")
)
# The following are valid.
endpoint.callback(lambda vote_data: print("Receives a vote!", vote_data))
# Used as decorator, the decorated function will become the WebhookEndpoint object.
@endpoint.callback
def endpoint(vote_data: topgg.BotVoteData):
...
# Used as decorator factory, the decorated function will still be the function itself.
@endpoint.callback()
def on_vote(vote_data: topgg.BotVoteData):
...
webhook_manager.endpoint(endpoint)
"""
if callback_ is not None:
self._callback = callback_
return self
return self.callback
[docs]class BoundWebhookEndpoint(WebhookEndpoint):
"""
A WebhookEndpoint with a WebhookManager bound to it.
You can instantiate this object using the :meth:`WebhookManager.endpoint` method.
:Example:
.. code-block:: python
import topgg
webhook_manager = (
topgg.WebhookManager()
.endpoint()
.type(topgg.WebhookType.BOT)
.route("/dblwebhook")
.auth("youshallnotpass")
)
# The following are valid.
endpoint.callback(lambda vote_data: print("Receives a vote!", vote_data))
# Used as decorator, the decorated function will become the BoundWebhookEndpoint object.
@endpoint.callback
def endpoint(vote_data: topgg.BotVoteData):
...
# Used as decorator factory, the decorated function will still be the function itself.
@endpoint.callback()
def on_vote(vote_data: topgg.BotVoteData):
...
endpoint.add_to_manager()
"""
__slots__ = ("manager",)
def __init__(self, manager: WebhookManager):
super().__init__()
self.manager = manager
[docs] def add_to_manager(self) -> WebhookManager:
"""
Adds this endpoint to the webhook manager.
Returns:
:obj:`WebhookManager`
Raises:
:obj:`errors.TopGGException`:
If the object lacks attributes.
"""
self.manager.endpoint(self)
return self.manager
[docs]def endpoint(
route: str, type: WebhookType, auth: str = ""
) -> t.Callable[[t.Callable[..., t.Any]], WebhookEndpoint]:
"""
A decorator factory for instantiating WebhookEndpoint.
Args:
route (str)
The route for the endpoint.
type (WebhookType)
The type of the endpoint.
auth (str)
The auth for the endpoint.
Returns:
:obj:`typing.Callable` [[ :obj:`typing.Callable` [..., :obj:`typing.Any` ]], :obj:`WebhookEndpoint` ]:
The actual decorator.
:Example:
.. code-block:: python
import topgg
@topgg.endpoint("/dblwebhook", WebhookType.BOT, "youshallnotpass")
async def on_vote(
vote_data: topgg.BotVoteData,
# database here is an injected data
database: Database = topgg.data(Database),
):
...
"""
def decorator(func: t.Callable[..., t.Any]) -> WebhookEndpoint:
return WebhookEndpoint().route(route).type(type).auth(auth).callback(func)
return decorator