Релиз

This commit is contained in:
2026-02-26 17:59:13 +03:00
commit d0c39c5930
12 changed files with 725 additions and 0 deletions

View File

View File

@ -0,0 +1,145 @@
from typing import Literal, Never
from oxidehttp.client import OxideHTTP
from oxidehttp.schema import CachedResponse, Response
from pydantic import BaseModel
from . import schema as s
class DonationAlertsAPIClient(OxideHTTP):
def __init__(
self,
redis_url: str | None = None,
proxy_url: str | None = None,
) -> None:
self.base_uri = 'https://www.donationalerts.com/api/v1'
super().__init__(
base_url=self.base_uri,
redis_url=redis_url,
ratelimit_key='donation_alerts' if redis_url else None,
ratelimit_limit=60 if redis_url else None,
proxy_url=proxy_url,
)
def _auth(self, access_token: str) -> dict[str, str]:
return {'Authorization': f'Bearer {access_token}'}
async def _process[T: BaseModel](
self, req: Response | CachedResponse, schema: type[T]
) -> T:
if req.status_code >= 500:
raise s.InternalError(req.status_code, 'Internal Server Error')
if req.status_code >= 400 and req.status_code < 500:
data = await req.json()
message = data.get('message', 'DonationAlerts API Error')
raise s.ClientError(req.status_code, message)
data = await req.json()
return schema.model_validate(data)
async def user_oauth(
self, access_token: str, *, cache_ttl: int | None = None
) -> s.UserOauth:
req = await self.get(
'/user/oauth', None, self._auth(access_token), cache_ttl
)
return await self._process(req, s.UserOauth)
async def alerts_donations(
self, access_token: str, *, page: int = 1, cache_ttl: int | None = None
) -> s.AlertsDonations:
req = await self.get(
'/alerts/donations',
{'page': page},
self._auth(access_token),
cache_ttl,
)
return await self._process(req, s.AlertsDonations)
async def custom_alert(
self,
access_token: str,
external_id: str,
header: str,
message: str,
image_url: str,
sound_url: str,
is_shown: Literal[0, 1] = 0,
) -> Never:
raise NotImplementedError
async def merchandise(
self,
access_token: str,
merchant_identifier: str,
merchandise_identifier: str,
title: dict[str, str],
currency: str,
price_user: int,
price_service: str,
url: str,
img_url: str,
end_at_ts: int,
is_active: Literal[0, 1] = 0,
is_percentage: Literal[0, 1] = 0,
) -> Never:
raise NotImplementedError
async def update_merchandise(
self,
access_token: str,
merchant_identifier: str,
merchandise_identifier: str,
title: dict[str, str],
currency: str,
price_user: int,
price_service: str,
url: str,
img_url: str,
end_at_ts: int,
is_active: Literal[0, 1] = 0,
is_percentage: Literal[0, 1] = 0,
) -> Never:
raise NotImplementedError
async def get_user_data_from_promocode(
self,
access_token: str,
promocode: str,
*,
cache_ttl: int | None = None,
) -> Never:
raise NotImplementedError
async def send_sale_alerts(
self,
access_token: str,
user_id: int,
external_id: str,
merchant_identifier: str,
merchandise_identifier: str,
amount: float,
currency: str,
username: str,
message: str,
bought_amount: int = 1,
) -> Never:
raise NotImplementedError
async def centrifuge_subscribe(
self, access_token: str, client: str, subscriptions: list[str]
) -> s.CentrifugeSubscribe:
req = await self.post(
'/centrifuge/subscribe',
None,
self._auth(access_token),
{'client': client, 'channels': subscriptions},
)
return await self._process(req, s.CentrifugeSubscribe)

View File

@ -0,0 +1,78 @@
from oxidehttp.client import OxideHTTP
from oxidehttp.schema import CachedResponse, Response
from pydantic import BaseModel
from . import schema as s
from . import scopes as sp
class DonationAlertsAuthClient(OxideHTTP):
def __init__(
self,
client_id: str,
client_secret: str,
redirect_uri: str,
redis_url: str | None = None,
proxy_url: str | None = None,
) -> None:
self.base_uri = 'https://www.donationalerts.com/oauth'
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
super().__init__(
base_url=self.base_uri,
redis_url=redis_url,
ratelimit_key='donation_alerts' if redis_url else None,
ratelimit_limit=60 if redis_url else None,
proxy_url=proxy_url,
)
def _auth(self, access_token: str) -> dict[str, str]:
return {'Authorization': f'Bearer {access_token}'}
async def _process[T: BaseModel](
self, req: Response | CachedResponse, schema: type[T]
) -> T:
if req.status_code >= 500:
raise s.InternalError(req.status_code, 'Internal Server Error')
if req.status_code >= 400 and req.status_code < 500:
data = await req.json()
message = data.get('message', 'DonationAlerts API Error')
raise s.ClientError(req.status_code, message)
data = await req.json()
return schema.model_validate(data)
async def user_access_token(self, code: str) -> s.UserAccessToken:
req = await self.post(
'/token',
json={
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri,
'grant_type': 'authorization_code',
'code': code,
},
)
return await self._process(req, s.UserAccessToken)
async def refresh_access_token(
self, refresh_code: str, scopes: list[sp.Any]
) -> s.UserAccessToken:
req = await self.post(
'/token',
json={
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri,
'grant_type': 'refresh_token',
'refresh_token': refresh_code,
'scopes': ' '.join(scopes),
},
)
return await self._process(req, s.UserAccessToken)

View File

View File

@ -0,0 +1,115 @@
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl
class Error(Exception):
status_code: int
error: str
def __init__(self, status_code: int, error: str) -> None:
self.status_code = status_code
self.error = error
super().__init__(f'{status_code}: {error}')
class ClientError(Error):
pass
class InternalError(Error):
pass
class BaseSchema(BaseModel):
model_config = ConfigDict(extra='forbid')
class UserOauthData(BaseSchema):
id: int
code: str
name: str
avatar: HttpUrl
email: EmailStr
socket_connection_token: str
class UserOauth(BaseSchema):
data: UserOauthData
class AlertsDonationsDataPayinSystem(BaseSchema):
title: str
class AlertsDonationsDataRecipient(BaseSchema):
user_id: int
code: str
name: str
avatar: HttpUrl
class AlertsDonationsData(BaseSchema):
id: int
name: str
username: str | None
message: str | None
message_type: Literal['text', 'audio']
payin_system: AlertsDonationsDataPayinSystem | None
amount: float
currency: str
is_shown: bool
amount_in_user_currency: float
recipient_name: str
recipient: AlertsDonationsDataRecipient
created_at: datetime
created_at_ts: int
shown_at: datetime | None
shown_at_ts: int | None
class AlertsDonationsLinks(BaseSchema):
first: HttpUrl
last: HttpUrl
prev: HttpUrl | None
next: HttpUrl | None
class MetaLink(BaseSchema):
url: HttpUrl | None
label: str
active: bool
class AlertsDonationsMeta(BaseSchema):
current_page: int
from_page: int | None = Field(alias='from')
last_page: int
links: list[MetaLink]
path: HttpUrl
per_page: int
to: int | None
total: int
class AlertsDonations(BaseSchema):
data: list[AlertsDonationsData]
links: AlertsDonationsLinks
meta: AlertsDonationsMeta
class CentrifugeSubscribeChannel(BaseSchema):
channel: str
token: str
class CentrifugeSubscribe(BaseSchema):
channels: list[CentrifugeSubscribeChannel]
class UserAccessToken(BaseSchema):
token_type: Literal['Bearer']
access_token: str
refresh_token: str
expires_in: int

View File

@ -0,0 +1,18 @@
from typing import Literal
OAUTH_USER_SHOW = 'oauth-user-show'
OAUTH_DONATION_SUBSCRIBE = 'oauth-donation-subscribe'
OAUTH_DONATION_INDEX = 'oauth-donation-index'
OAUTH_CUSTOM_ALERT_STORE = 'oauth-custom_alert-store'
OAUTH_GOAL_SUBSCRIBE = 'oauth-goal-subscribe'
OAUTH_POLL_SUBSCRIBE = 'oauth-poll-subscribe'
type Any = Literal[
'oauth-user-show',
'oauth-donation-subscribe',
'oauth-donation-index',
'oauth-custom_alert-store',
'oauth-goal-subscribe',
'oauth-poll-subscribe',
]