diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e557799 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Ignore pycache + +__pycache__ + +# Ignore dist + +/dist + +# Ignore pyproject + +pyproject.toml \ No newline at end of file diff --git a/donationalerts_api.py b/donationalerts_api.py deleted file mode 100644 index b6311a1..0000000 --- a/donationalerts_api.py +++ /dev/null @@ -1,224 +0,0 @@ -import requests -import json -from flask import request -from websocket import create_connection -import socketio - - -class Scopes: - """Удобный класс, для простого указания прав в приложении""" - - USER_SHOW = "oauth-user-show" - - DONATION_SUBSCRIBE = "oauth-donation-subscribe" - DONATION_INDEX = "oauth-donation-index" - - CUSTOM_ALERT_STORE = "oauth-custom_alert-store" - - GOAL_SUBSCRIBE = "oauth-goal-subscribe" - POLL_SUBSCRIBE = "oauth-poll-subscribe" - - ALL_SCOPES = [USER_SHOW, DONATION_INDEX, DONATION_SUBSCRIBE, CUSTOM_ALERT_STORE, - GOAL_SUBSCRIBE, POLL_SUBSCRIBE] - - -class Channels: - """Права для подписки на каналы Centrifugo""" - - NEW_DONATION_ALERTS = "$alerts:donation_" - - DONATION_GOALS_UPDATES = "$goals:goal_" - - POLLS_UPDATES = "$polls:poll_" - - ALL_CHANNELS = [NEW_DONATION_ALERTS, DONATION_GOALS_UPDATES, POLLS_UPDATES] - - -class DonationAlertsApi: - """Основной класс для работы с DA API""" - - def __init__(self, client_id, client_secret, redirect_uri, scopes): - symbols = [",", ", ", " ", "%20"] - - if isinstance(scopes, list): - obj_scopes = [] - for scope in scopes: - obj_scopes.append(scope) - - scopes = " ".join(obj_scopes) - - for symbol in symbols: - if symbol in scopes: - self.scope = scopes.replace(symbol, "%20").strip() # Replaces some symbols on '%20' for stable work - else: - self.scope = scopes - - self.client_id = client_id - self.client_secret = client_secret - self.redirect_uri = redirect_uri - self.login_url = f"https://www.donationalerts.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={self.scope}" - self.token_url = f"https://www.donationalerts.com/oauth/token" - - # API LINKS - self.user_api = "https://www.donationalerts.com/api/v1/user/oauth" - self.donations_api = "https://www.donationalerts.com/api/v1/alerts/donations" - self.custom_alerts_api = "https://www.donationalerts.com/api/v1/custom_alert" - - def login(self): - return self.login_url - - def get_code(self): - return request.args.get("code") # Получаем аргмент "code" из адресной строки - - def get_access_token(self, code, full=False): - """ Параметр full=False - - Если True, то выводит весь json объект, а не только access_token - - """ - - payload = { - "client_id": self.client_id, - "client_secret": self.client_secret, - "grant_type": "authorization_code", - "code": code, - "redirect_uri": self.redirect_uri, - "scope": self.scope - } - - access_token = requests.post(url=self.token_url, data=payload).json() - self.refresh_token = access_token.get("refresh_token") - - return access_token if full else access_token.get("access_token") - - def get_donations(self, access_token): - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/x-www-form-urlencoded" - } - donate_object = requests.get(url=self.donations_api, headers=headers).json() - - return donate_object - - def get_user(self, access_token): - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/x-www-form-urlencoded" - } - user_object = requests.get(url=self.user_api, headers=headers).json() - - return user_object["data"] - - def send_custom_alert(self, access_token, external_id, headline, message, image_url=None, sound_url=None, is_shown=0): - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/x-www-form-urlencoded" - } - data = { - "external_id": external_id, - "header": headline, - "message": message, - "is_shown": is_shown, - "image_url": image_url, - "sound_url": sound_url - } - custom_alert_object = requests.post(url=self.custom_alerts_api, data=data, headers=headers).json() - - return custom_alert_object - - def get_refresh_token(self): - headers = {"Content-Type": "application/x-www-form-urlencoded"} - data = { - "grant_type": "refresh_token", - "client_id": self.client_id, - "client_secret": self.client_secret, - "refresh_token": self.refresh_token, - "redirect_uri": self.redirect_uri, - "scope": self.scope - } - - response = requests.post(url=self.token_url, data=data, headers=headers).json() - return response - - -class Centrifugo: - """Получение событий в реальном времени (Oauth2)""" - - def __init__(self, socket_connection_token, access_token, user_id): - self.socket_connection_token = socket_connection_token - self.access_token = access_token - self.user_id = user_id - - self.uri = "wss://centrifugo.donationalerts.com/connection/websocket" - - def connect(self): - self.ws = create_connection(self.uri) # Подключаемся к серверу - self.ws.send(json.dumps( - { - "params": { - "token": self.socket_connection_token - }, - "id": self.user_id - } - )) - self.ws_response = json.loads(self.ws.recv()) - - return self.ws_response - - def subscribe(self, channels): - chnls = [f"{channels}{self.user_id}"] - - if isinstance(channels, list): - chnls = [] - for channel in channels: - chnls.append(f"{channel}{self.user_id}") - - headers = { - "Authorization": f"Bearer {self.access_token}", - "Content-Type": "application/json" - } - data = { - "channels": chnls, - "client": self.ws_response["result"]["client"] - } - - response = requests.post(url="https://www.donationalerts.com/api/v1/centrifuge/subscribe", data=json.dumps(data), headers=headers).json() - for ch in response["channels"]: - self.ws.send(json.dumps( - { - "params": { - "channel": ch["channel"], - "token": ch["token"] - }, - "method": 1, - "id": self.user_id - } - )) - - answer = {"response": self.ws.recv(), "sec_response": self.ws.recv()} - return answer # Возвращаем первые два ответа от сервера - - def listen(self): - return json.loads(self.ws.recv())["result"]["data"]["data"] # Возвращаем 3-й ответ от сервера - -sio = socketio.Client() - - -class Alert: - """Получение донатов в реальном времени без Oauth2""" - - def __init__(self, token): - self.token = token # TOKEN можно скопировать здесь - https://www.donationalerts.com/dashboard/general - - def event(self): - def wrapper(function): - @sio.on("connect") - def on_connect(): - sio.emit("add-user", {"token": self.token, "type": "alert_widget"}) - - @sio.on("donation") - def on_message(data): - function(json.loads(data)) # Отправляем полученные данные в функцию - - sio.connect("wss://socket.donationalerts.ru:443", transports="websocket") - return wrapper \ No newline at end of file diff --git a/donationalerts_api/__init__.py b/donationalerts_api/__init__.py new file mode 100644 index 0000000..f5d66e0 --- /dev/null +++ b/donationalerts_api/__init__.py @@ -0,0 +1,2 @@ +from .utils import * +from .donationalerts_api import * \ No newline at end of file diff --git a/donationalerts_api/asyncio_api.py b/donationalerts_api/asyncio_api.py new file mode 100644 index 0000000..419004c --- /dev/null +++ b/donationalerts_api/asyncio_api.py @@ -0,0 +1,300 @@ +import json + +import asyncio +import socketio +import aiohttp +import websockets + +from .utils import Event, User, Data, Donations, DonationsData, CentrifugoResponse + +DEFAULT_URL = "https://www.donationalerts.com/oauth/" +DEFAULT_API_LINK = "https://www.donationalerts.com/api/v1/" + + +class DonationAlertsApi: + + def __init__(self, client_id, client_secret, redirect_uri, scopes): + symbols = [",", ", ", " ", "%20"] + + if isinstance(scopes, list): + obj_scopes = [] + for scope in scopes: + obj_scopes.append(scope) + + scopes = " ".join(obj_scopes) + + for symbol in symbols: + if symbol in scopes: + self.scope = scopes.replace(symbol, "%20").strip() # Replaces some symbols on '%20' for stable work + else: + self.scope = scopes + + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def login(self): + return f"{DEFAULT_URL}authorize?client_id={self.client_id}&redirect_uri={self.redirect_uri}&response_type=code&scope={self.scope}" + + async def get_access_token(self, code, *, full_json=False): + payload = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": self.redirect_uri, + "scope": self.scope + } + + async with aiohttp.ClientSession() as session: + async with session.post(f"{DEFAULT_URL}token", data=payload) as response: + obj = await response.json() + + return Data( + obj["access_token"], + obj["expires_in"], + obj["refresh_token"], + obj["token_type"], + obj + ) if full_json else obj["access_token"] + + async def donations_list(self, access_token, *, current_page: int=1, from_page: int=1, last_page: int=13, per_page: int=30, to: int=30, total: int=385): + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/x-www-form-urlencoded" + } + + """Pagination in development""" + meta = { + "current_page": current_page, + "from": from_page, + "last_page": last_page, + "path": f"{DEFAULT_API_LINK}alerts/donations", + "per_page": per_page, + "to": to, + "total": total + } + links = { + "first": f"{DEFAULT_API_LINK}alerts/donations?page={from_page}", + "last": f"{DEFAULT_API_LINK}alerts/donations?page={last_page}", + "last": f"{DEFAULT_API_LINK}alerts/donations?page={last_page}", + "next": f"{DEFAULT_API_LINK}alerts/donations?page={current_page + 1}", + "prev": None + } + + async with aiohttp.ClientSession() as session: + async with session.get(f"{DEFAULT_API_LINK}alerts/donations", headers=headers) as response: + objs = await response.json() + donations = Donations(objects=objs["data"]) + + for obj in objs["data"]: + donation_object = DonationsData( + obj["amount"], + obj["amount_in_user_currency"], + obj["created_at"], + obj["currency"], + obj["id"], + obj["is_shown"], + obj["message"], + obj["message_type"], + obj["name"], + obj["payin_system"], + obj["recipient_name"], + obj["shown_at"], + obj["username"] + ) + donations.donation.append(donation_object) + + return donations + + async def user(self, access_token): + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/x-www-form-urlencoded" + } + + async with aiohttp.ClientSession() as session: + async with session.get(f"{DEFAULT_API_LINK}user/oauth", headers=headers) as response: + obj = await response.json() + + return User( + obj["data"]["avatar"], + obj["data"]["code"], + obj["data"]["email"], + obj["data"]["id"], + obj["data"]["language"], + obj["data"]["name"], + obj["data"]["socket_connection_token"], + obj["data"] + ) + + async def send_custom_alert(self, access_token, external_id, headline, message, *, image_url=None, sound_url=None, is_shown=0): + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/x-www-form-urlencoded" + } + payload = { + "external_id": external_id, + "headline": headline, + "message": message, + "is_shown": is_shown, + "image_url": image_url, + "sound_url": sound_url + } + + async with aiohttp.ClientSession() as session: + async with session.post(f"{DEFAULT_API_LINK}custom_alert", data=payload, headers=headers) as response: + return await response.json() + + async def get_refresh_token(self, access_token, refresh_token): + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + payload = { + "grant_type": "refresh_token", + "client_id": self.client_id, + "client_secret": self.client_secret, + "refresh_token": refresh_token, + "redirect_uri": self.redirect_uri, + "scope": self.scope + } + + async with aiohttp.ClientSession() as session: + async with session.post(f"{DEFAULT_URL}token", data=payload, headers=headers) as response: + obj = await response.json() + + return Data( + obj["access_token"], + obj["expires_in"], + obj["refresh_token"], + obj["token_type"], + obj + ) + + +class Centrifugo: + + def __init__(self, socket_connection_token, access_token, user_id): + self.socket_connection_token = socket_connection_token + self.access_token = access_token + self.user_id = user_id + + async def subscribe(self, channels): + chnls = [f"{channels}{self.user_id}"] + if isinstance(channels, list): + chnls = [] + for channel in channels: + chnls.append(f"{channel}{self.user_id}") + + async with websockets.connect("wss://centrifugo.donationalerts.com/connection/websocket") as websocket: + await websocket.send(json.dumps( + { + "params": { + "token": self.socket_connection_token + }, + "id": self.user_id + } + )) + + websocket_response = json.loads(await websocket.recv()) + + headers = { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json" + } + data = { + "channels": chnls, + "client": websocket_response["result"]["client"] + } + + async with aiohttp.ClientSession(headers=headers) as session: + async with session.post(f"{DEFAULT_API_LINK}centrifuge/subscribe", data=json.dumps(data)) as response: + response = await response.json() + + for ch in response["channels"]: + await websocket.send(json.dumps( + { + "params": { + "channel": ch["channel"], + "token": ch["token"] + }, + "method": 1, + "id": self.user_id + } + )) + + await websocket.recv() + await websocket.recv() + + obj = json.loads(await websocket.recv())["result"]["data"]["data"] + return CentrifugoResponse( + obj["amount"], + obj["amount_in_user_currency"], + obj["created_at"], + obj["currency"], + obj["id"], + obj["is_shown"], + obj["message"], + obj["message_type"], + obj["name"], + obj["payin_system"], + obj["recipient_name"], + obj["shown_at"], + obj["username"], + obj["reason"], + obj + ) + + +sio = socketio.AsyncClient() + + +class Alert: + + def __init__(self, token): + self.token = token + + def event(self): + def decorate(function): + async def wrapper(): + + @sio.on("connect") + async def on_connect(): + await sio.emit("add-user", {"token": self.token, "type": "alert_widget"}) + + @sio.on("donation") + async def on_message(data): + data = json.loads(data) + + await function( + Event( + data["id"], + data["alert_type"], + data["is_shown"], + json.loads(data["additional_data"]), + data["billing_system"], + data["billing_system_type"], + data["username"], + data["amount"], + data["amount_formatted"], + data["amount_main"], + data["currency"], + data["message"], + data["header"], + data["date_created"], + data["emotes"], + data["ap_id"], + data["_is_test_alert"], + data["message_type"], + data["preset_id"], + data + ) + ) + + await sio.connect("wss://socket.donationalerts.ru:443", transports="websocket") + + loop = asyncio.get_event_loop() + loop.run_until_complete(wrapper()) + return loop.run_forever() + + return decorate \ No newline at end of file diff --git a/donationalerts_api/donationalerts_api.py b/donationalerts_api/donationalerts_api.py new file mode 100644 index 0000000..8f22361 --- /dev/null +++ b/donationalerts_api/donationalerts_api.py @@ -0,0 +1,286 @@ +import json +import requests + +from websocket import create_connection +import socketio + +from .utils import Event, User, Data, Donations, DonationsData, CentrifugoResponse + +DEFAULT_URL = "https://www.donationalerts.com/oauth/" +DEFAULT_API_LINK = "https://www.donationalerts.com/api/v1/" + + +class DonationAlertsApi: + """ + This class describes work with Donation Alerts API + """ + + def __init__(self, client_id, client_secret, redirect_uri, scopes): + symbols = [",", ", ", " ", "%20"] + + if isinstance(scopes, list): + obj_scopes = [] + for scope in scopes: + obj_scopes.append(scope) + + scopes = " ".join(obj_scopes) + + for symbol in symbols: + if symbol in scopes: + self.scope = scopes.replace(symbol, "%20").strip() # Replaces some symbols on '%20' for stable work + else: + self.scope = scopes + + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def login(self): + return f"{DEFAULT_URL}authorize?client_id={self.client_id}&redirect_uri={self.redirect_uri}&response_type=code&scope={self.scope}" + + def get_access_token(self, code, *, full_json=False): + payload = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": self.redirect_uri, + "scope": self.scope + } + + access_token = requests.post(f"{DEFAULT_URL}token", data=payload).json() + self.refresh_token = access_token.get("refresh_token") + + return Data( + obj["access_token"], + obj["expires_in"], + obj["refresh_token"], + obj["token_type"], + obj + ) if full_json else obj["access_token"] + + def donations_list(self, access_token, *, current_page: int=1, from_page: int=1, last_page: int=13, per_page: int=30, to: int=30, total: int=385): + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/x-www-form-urlencoded" + } + + """Pagination in development""" + meta = { + "current_page": current_page, + "from": from_page, + "last_page": last_page, + "path": f"{DEFAULT_API_LINK}alerts/donations", + "per_page": per_page, + "to": to, + "total": total + } + links = { + "first": f"{DEFAULT_API_LINK}alerts/donations?page={from_page}", + "last": f"{DEFAULT_API_LINK}alerts/donations?page={last_page}", + "last": f"{DEFAULT_API_LINK}alerts/donations?page={last_page}", + "next": f"{DEFAULT_API_LINK}alerts/donations?page={current_page + 1}", + "prev": None + } + + objs = requests.get(f"{DEFAULT_API_LINK}alerts/donations", headers=headers).json() + donations = Donations(objects=objs["data"]) + + for obj in objs["data"]: + donation_object = DonationsData( + obj["amount"], + obj["amount_in_user_currency"], + obj["created_at"], + obj["currency"], + obj["id"], + obj["is_shown"], + obj["message"], + obj["message_type"], + obj["name"], + obj["payin_system"], + obj["recipient_name"], + obj["shown_at"], + obj["username"] + ) + donations.donation.append(donation_object) + + return donations + + def user(self, access_token): + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/x-www-form-urlencoded" + } + obj = requests.get(f"{DEFAULT_API_LINK}user/oauth", headers=headers).json() + + return User( + obj["data"]["avatar"], + obj["data"]["code"], + obj["data"]["email"], + obj["data"]["id"], + obj["data"]["language"], + obj["data"]["name"], + obj["data"]["socket_connection_token"], + obj["data"] + ) + + def send_custom_alert(self, access_token, external_id, headline, message, *, image_url=None, sound_url=None, is_shown=0): + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/x-www-form-urlencoded" + } + payload = { + "external_id": external_id, + "headline": headline, + "message": message, + "is_shown": is_shown, + "image_url": image_url, + "sound_url": sound_url + } + + obj = requests.post(f"{DEFAULT_API_LINK}custom_alert", data=payload, headers=headers).json() + return obj + + def get_refresh_token(self, access_token, refresh_token): + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + payload = { + "grant_type": "refresh_token", + "client_id": self.client_id, + "client_secret": self.client_secret, + "refresh_token": refresh_token, + "redirect_uri": self.redirect_uri, + "scope": self.scope + } + + obj = requests.post(f"{DEFAULT_URL}token", data=payload, headers=headers).json() + return Data( + obj["access_token"], + obj["expires_in"], + obj["refresh_token"], + obj["token_type"], + obj + ) + + +class Centrifugo: + + def __init__(self, socket_connection_token, access_token, user_id): + self.socket_connection_token = socket_connection_token + self.access_token = access_token + self.user_id = user_id + + self.uri = "wss://centrifugo.donationalerts.com/connection/websocket" + + def subscribe(self, channels): + chnls = [f"{channels}{self.user_id}"] + if isinstance(channels, list): + chnls = [] + for channel in channels: + chnls.append(f"{channel}{self.user_id}") + + ws = create_connection(self.uri) + ws.send(json.dumps( + { + "params": { + "token": self.socket_connection_token + }, + "id": self.user_id + } + )) + + ws_response = json.loads(ws.recv()) + + headers = { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json" + } + data = { + "channels": chnls, + "client": ws_response["result"]["client"] + } + + response = requests.post(f"{DEFAULT_API_LINK}centrifuge/subscribe", data=json.dumps(data), headers=headers).json() + for ch in response["channels"]: + self.ws.send(json.dumps( + { + "params": { + "channel": ch["channel"], + "token": ch["token"] + }, + "method": 1, + "id": self.user_id + } + )) + + ws.recv() + ws.recv() + + obj = json.loads(ws.recv())["result"]["data"]["data"] + return CentrifugoResponse( + obj["amount"], + obj["amount_in_user_currency"], + obj["created_at"], + obj["currency"], + obj["id"], + obj["is_shown"], + obj["message"], + obj["message_type"], + obj["name"], + obj["payin_system"], + obj["recipient_name"], + obj["shown_at"], + obj["username"], + obj["reason"], + obj + ) + + +sio = socketio.Client() + + +class Alert: + + def __init__(self, token): + self.token = token + + def event(self): + def wrapper(function): + + @sio.on("connect") + def on_connect(): + sio.emit("add-user", {"token": self.token, "type": "alert_widget"}) + + @sio.on("donation") + def on_message(data): + data = json.loads(data) + + function( + Event( + data["id"], + data["alert_type"], + data["is_shown"], + json.loads(data["additional_data"]), + data["billing_system"], + data["billing_system_type"], + data["username"], + data["amount"], + data["amount_formatted"], + data["amount_main"], + data["currency"], + data["message"], + data["header"], + data["date_created"], + data["emotes"], + data["ap_id"], + data["_is_test_alert"], + data["message_type"], + data["preset_id"], + data + ) + ) + + sio.connect("wss://socket.donationalerts.ru:443", transports="websocket") + + return wrapper \ No newline at end of file diff --git a/donationalerts_api/modules.py b/donationalerts_api/modules.py new file mode 100644 index 0000000..a7850cf --- /dev/null +++ b/donationalerts_api/modules.py @@ -0,0 +1,23 @@ +class Scopes: + USER_SHOW = "oauth-user-show" + + DONATION_SUBSCRIBE = "oauth-donation-subscribe" + DONATION_INDEX = "oauth-donation-index" + + CUSTOM_ALERT_STORE = "oauth-custom_alert-store" + + GOAL_SUBSCRIBE = "oauth-goal-subscribe" + POLL_SUBSCRIBE = "oauth-poll-subscribe" + + ALL_SCOPES = [USER_SHOW, DONATION_INDEX, DONATION_SUBSCRIBE, CUSTOM_ALERT_STORE, + GOAL_SUBSCRIBE, POLL_SUBSCRIBE] + + +class Channels: + NEW_DONATION_ALERTS = "$alerts:donation_" + + DONATION_GOALS_UPDATES = "$goals:goal_" + + POLLS_UPDATES = "$polls:poll_" + + ALL_CHANNELS = [NEW_DONATION_ALERTS, DONATION_GOALS_UPDATES, POLLS_UPDATES] \ No newline at end of file diff --git a/donationalerts_api/utils.py b/donationalerts_api/utils.py new file mode 100644 index 0000000..4b9963f --- /dev/null +++ b/donationalerts_api/utils.py @@ -0,0 +1,94 @@ +from dataclasses import dataclass + + +@dataclass +class Event: + + id: int + alert_type: str + is_shown: str + additional_data: dict + billing_system: str + billing_system_type: str + username: str + amount: str + amount_formatted: str + amount_main: int + currency: str + message: str + header: str + date_created: str + emotes: str + ap_id: str + _is_test_alert: bool + message_type: str + preset_id: int + objects: dict + + +@dataclass +class Donations: + + donation = [] + objects: dict = None + + +@dataclass +class DonationsData: + + amount: int + amount_in_user_currency: float + created_at: str + currency: str + id: int + is_shown: int + message: str + message_type: str + name: str + payin_system: str + recipient_name: str + shown_at: str + username: str + + +@dataclass +class User: + + avatar: str + code: str + email: str + id: int + language: str + name: str + socket_connection_token: str + objects: dict + + +@dataclass +class Data: + + access_token: str + expires_in: int + refresh_token: str + token_type: str + objects: dict + + +@dataclass +class CentrifugoResponse: + + amount: int + amount_in_user_currency: float + created_at: str + currency: str + id: int + is_shown: int + message: str + message_type: str + name: str + payin_system: str + recipient_name: str + shown_at: str + username: str + reason: str + objects: dict \ No newline at end of file