Files
python-client-sdk/revolt/http.py
T
2023-05-20 03:04:52 +01:00

429 lines
16 KiB
Python
Executable File

from __future__ import annotations
from typing import (TYPE_CHECKING, Any, Coroutine, Literal, Optional, TypeVar,
Union, overload)
import aiohttp
import ulid
from .errors import Forbidden, HTTPError, ServerError
from .file import File
try:
import ujson as _json
except ImportError:
import json as _json
if TYPE_CHECKING:
import aiohttp
from .enums import SortType
from .file import File
from .types import ApiInfo
from .types import Autumn as AutumnPayload
from .types import Channel, DMChannel
from .types import Emoji as EmojiPayload
from .types import EmojiParent, GetServerMembers, GroupDMChannel
from .types import Interactions as InteractionsPayload
from .types import Invite
from .types import Masquerade as MasqueradePayload
from .types import Member
from .types import Member as MemberPayload
from .types import Message as MessagePayload
from .types import (MessageReplyPayload, MessageWithUserData,
PartialInvite, Role)
from .types import SendableEmbed as SendableEmbedPayload
from .types import Server, ServerBans, TextChannel
from .types import User as UserPayload
from .types import UserProfile, VoiceChannel
__all__ = ("HttpClient",)
T = TypeVar("T")
Request = Coroutine[Any, Any, T]
class HttpClient:
__slots__ = ("session", "token", "api_url", "api_info", "auth_header")
def __init__(self, session: aiohttp.ClientSession, token: str, api_url: str, api_info: ApiInfo, bot: bool = True):
self.session: aiohttp.ClientSession = session
self.token: str = token
self.api_url: str = api_url
self.api_info: ApiInfo = api_info
self.auth_header: str = "x-bot-token" if bot else "x-session-token"
async def request(self, method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"], route: str, *, json: Optional[dict[str, Any]] = None, nonce: bool = True, params: Optional[dict[str, Any]] = None) -> Any:
url = f"{self.api_url}{route}"
kwargs = {}
headers = {
"User-Agent": "Revolt.py (https://github.com/revoltchat/revolt.py)",
self.auth_header: self.token
}
if json:
headers["Content-Type"] = "application/json"
if nonce:
json["nonce"] = ulid.new().str # type: ignore
kwargs["data"] = _json.dumps(json)
kwargs["headers"] = headers
if params:
kwargs["params"] = params
async with self.session.request(method, url, **kwargs) as resp:
text = await resp.text()
if text:
try:
response = _json.loads(await resp.text())
except ValueError:
raise HTTPError(f"Invalid json response:\n{text}") from None
else:
response = text
resp_code = resp.status
if 200 <= resp_code <= 300:
return response
elif resp_code == 401:
raise Forbidden("401: Missing Permissions")
else:
raise HTTPError(resp_code)
async def upload_file(self, file: File, tag: Literal["attachments", "avatars", "backgrounds", "icons", "banners", "emojis"]) -> AutumnPayload:
url = f"{self.api_info['features']['autumn']['url']}/{tag}"
headers = {
"User-Agent": "Revolt.py (https://github.com/revoltchat/revolt.py)"
}
form = aiohttp.FormData()
form.add_field("file", file.f.read(), filename=file.filename)
async with self.session.post(url, data=form, headers=headers) as resp:
response: AutumnPayload = _json.loads(await resp.text())
resp_code = resp.status
if resp_code == 400:
raise HTTPError(response)
elif 500 <= resp_code <= 600:
raise ServerError
else:
return response
async def send_message(self, channel: str, content: Optional[str], embeds: Optional[list[SendableEmbedPayload]], attachments: Optional[list[File]], replies: Optional[list[MessageReplyPayload]], masquerade: Optional[MasqueradePayload], interactions: Optional[InteractionsPayload]) -> MessagePayload:
json: dict[str, Any] = {}
if content:
json["content"] = content
if embeds:
json["embeds"] = embeds
if attachments:
attachment_ids: list[str] = []
for attachment in attachments:
data = await self.upload_file(attachment, "attachments")
attachment_ids.append(data["id"])
json["attachments"] = attachment_ids
if replies:
json["replies"] = replies
if masquerade:
json["masquerade"] = masquerade
if interactions:
json["interactions"] = interactions
return await self.request("POST", f"/channels/{channel}/messages", json=json)
def edit_message(self, channel: str, message: str, content: Optional[str], embeds: Optional[list[SendableEmbedPayload]] = None) -> Request[None]:
json = {}
if content is not None:
json["content"] = content
if embeds is not None:
json["embeds"] = embeds
return self.request("PATCH", f"/channels/{channel}/messages/{message}", json=json)
def delete_message(self, channel: str, message: str) -> Request[None]:
return self.request("DELETE", f"/channels/{channel}/messages/{message}")
def fetch_message(self, channel: str, message: str) -> Request[MessagePayload]:
return self.request("GET", f"/channels/{channel}/messages/{message}")
@overload
def fetch_messages(
self,
channel: str,
sort: SortType,
*,
limit: Optional[int] = ...,
before: Optional[str] = ...,
after: Optional[str] = ...,
nearby: Optional[str] = ...,
include_users: Literal[False] = ...
) -> Request[list[MessagePayload]]:
...
@overload
def fetch_messages(
self,
channel: str,
sort: SortType,
*,
limit: Optional[int] = ...,
before: Optional[str] = ...,
after: Optional[str] = ...,
nearby: Optional[str] = ...,
include_users: Literal[True] = ...
) -> Request[MessageWithUserData]:
...
def fetch_messages(
self,
channel: str,
sort: SortType,
*,
limit: Optional[int] = None,
before: Optional[str] = None,
after: Optional[str] = None,
nearby: Optional[str] = None,
include_users: bool = False
) -> Request[Union[list[MessagePayload], MessageWithUserData]]:
json: dict[str, Any] = {"sort": sort.value, "include_users": str(include_users)}
if limit:
json["limit"] = limit
if before:
json["before"] = before
if after:
json["after"] = after
if nearby:
json["nearby"] = nearby
return self.request("GET", f"/channels/{channel}/messages", params=json)
@overload
def search_messages(
self,
channel: str,
query: str,
*,
limit: Optional[int] = ...,
before: Optional[str] = ...,
after: Optional[str] = ...,
sort: Optional[SortType] = ...,
include_users: Literal[False] = ...
) -> Request[list[MessagePayload]]:
...
@overload
def search_messages(
self,
channel: str,
query: str,
*,
limit: Optional[int] = ...,
before: Optional[str] = ...,
after: Optional[str] = ...,
sort: Optional[SortType] = ...,
include_users: Literal[True] = ...
) -> Request[MessageWithUserData]:
...
def search_messages(
self,
channel: str,
query: str,
*,
limit: Optional[int] = None,
before: Optional[str] = None,
after: Optional[str] = None,
sort: Optional[SortType] = None,
include_users: bool = False
) -> Request[Union[list[MessagePayload], MessageWithUserData]]:
json: dict[str, Any] = {"query": query, "include_users": include_users}
if limit:
json["limit"] = limit
if before:
json["before"] = before
if after:
json["after"] = after
if sort:
json["sort"] = sort.value
return self.request("POST", f"/channels/{channel}/search", json=json)
async def request_file(self, url: str) -> bytes:
async with self.session.get(url) as resp:
return await resp.content.read()
def fetch_user(self, user_id: str) -> Request[UserPayload]:
return self.request("GET", f"/users/{user_id}")
def fetch_profile(self, user_id: str) -> Request[UserProfile]:
return self.request("GET", f"/users/{user_id}/profile")
def fetch_default_avatar(self, user_id: str) -> Request[bytes]:
return self.request_file(f"{self.api_url}/users/{user_id}/default_avatar")
def fetch_dm_channels(self) -> Request[list[Union[DMChannel, GroupDMChannel]]]:
return self.request("GET", "/users/dms")
def open_dm(self, user_id: str) -> Request[DMChannel]:
return self.request("GET", f"/users/{user_id}/dm")
def fetch_channel(self, channel_id: str) -> Request[Channel]:
return self.request("GET", f"/channels/{channel_id}")
def close_channel(self, channel_id: str) -> Request[None]:
return self.request("DELETE", f"/channels/{channel_id}")
def fetch_server(self, server_id: str) -> Request[Server]:
return self.request("GET", f"/servers/{server_id}")
def delete_leave_server(self, server_id: str) -> Request[None]:
return self.request("DELETE", f"/servers/{server_id}")
@overload
def create_channel(self, server_id: str, channel_type: Literal["Text"], name: str, description: Optional[str]) -> Request[TextChannel]:
...
@overload
def create_channel(self, server_id: str, channel_type: Literal["Voice"], name: str, description: Optional[str]) -> Request[VoiceChannel]:
...
def create_channel(self, server_id: str, channel_type: Literal["Text", "Voice"], name: str, description: Optional[str]) -> Request[Union[TextChannel, VoiceChannel]]:
payload = {
"type": channel_type,
"name": name
}
if description:
payload["description"] = description
return self.request("POST", f"/servers/{server_id}/channels", json=payload)
def fetch_server_invites(self, server_id: str) -> Request[list[PartialInvite]]:
return self.request("GET", f"/servers/{server_id}/invites")
def fetch_member(self, server_id: str, member_id: str) -> Request[Member]:
return self.request("GET", f"/servers/{server_id}/members/{member_id}")
def kick_member(self, server_id: str, member_id: str) -> Request[None]:
return self.request("DELETE", f"/servers/{server_id}/members/{member_id}")
def fetch_members(self, server_id: str) -> Request[GetServerMembers]:
return self.request("GET", f"/servers/{server_id}/members")
def ban_member(self, server_id: str, member_id: str, reason: Optional[str]) -> Request[GetServerMembers]:
payload = {"reason": reason} if reason else None
return self.request("PUT", f"/servers/{server_id}/bans/{member_id}", json=payload, nonce=False)
def unban_member(self, server_id: str, member_id: str) -> Request[None]:
return self.request("DELETE", f"/servers/{server_id}/bans/{member_id}")
def fetch_bans(self, server_id: str) -> Request[ServerBans]:
return self.request("GET", f"/servers/{server_id}/bans")
def create_role(self, server_id: str, name: str) -> Request[Role]:
return self.request("POST", f"/servers/{server_id}/roles", json={"name": name}, nonce=False)
def delete_role(self, server_id: str, role_id: str) -> Request[None]:
return self.request("DELETE", f"/servers/{server_id}/roles/{role_id}")
def fetch_invite(self, code: str) -> Request[Invite]:
return self.request("GET", f"/invites/{code}")
def delete_invite(self, code: str) -> Request[None]:
return self.request("DELETE", f"/invites/{code}")
def edit_channel(self, channel_id: str, remove: list[str] | None, values: dict[str, Any]) -> Request[None]:
if remove:
values["remove"] = remove
return self.request("PATCH", f"/channels/{channel_id}", json=values)
def edit_role(self, server_id: str, role_id: str, remove: list[str] | None, values: dict[str, Any]) -> Request[None]:
if remove:
values["remove"] = remove
return self.request("PATCH", f"/servers/{server_id}/roles/{role_id}", json=values)
async def edit_self(self, remove: list[str] | None, values: dict[str, Any]) -> Request[None]:
if remove:
values["remove"] = remove
if avatar := values.get("avatar"):
asset = await self.upload_file(avatar, "avatars")
values["avatar"] = asset["id"]
if profile := values.get("profile"):
if background := profile.background():
asset = await self.upload_file(background, "backgrounds")
profile["background"] = asset["id"]
return await self.request("PATCH", "/users/@me", json=values)
def set_guild_channel_default_permissions(self, channel_id: str, allow: int, deny: int) -> Request[None]:
return self.request("PUT", f"/channels/{channel_id}/permissions/default", json={"permissions": {"allow": allow, "deny": deny}})
def set_guild_channel_role_permissions(self, channel_id: str, role_id: str, allow: int, deny: int) -> Request[None]:
return self.request("PUT", f"/channels/{channel_id}/permissions/{role_id}", json={"permissions": {"allow": allow, "deny": deny}})
def set_group_channel_default_permissions(self, channel_id: str, value: int) -> Request[None]:
return self.request("PUT", f"/channels/{channel_id}/permissions/default", json={"permissions": value})
def set_server_role_permissions(self, server_id: str, role_id: str, allow: int, deny: int) -> Request[None]:
return self.request("PUT", f"/servers/{server_id}/permissions/{role_id}", json={"permissions": {"allow": allow, "deny": deny}})
def set_server_default_permissions(self, server_id: str, value: int) -> Request[None]:
return self.request("PUT", f"/servers/{server_id}/permissions/default", json={"permissions": value})
def add_reaction(self, channel_id: str, message_id: str, emoji: str) -> Request[None]:
return self.request("PUT", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}")
def remove_reaction(self, channel_id: str, message_id: str, emoji: str, user_id: Optional[str], remove_all: bool) -> Request[None]:
return self.request("PUT", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji}")
def remove_all_reactions(self, channel_id: str, message_id: str) -> Request[None]:
return self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}/reactions")
def delete_emoji(self, emoji_id: str) -> Request[None]:
return self.request("DELETE", f"/custom/emoji/{emoji_id}")
def fetch_emoji(self, emoji_id: str) -> Request[EmojiPayload]:
return self.request("GET", f"/custom/emoji/{emoji_id}")
async def create_emoji(self, name: str, file: File, nsfw: bool, parent: EmojiParent) -> EmojiPayload:
asset = await self.upload_file(file, "emojis")
return await self.request("PUT", f"/custom/emoji/{asset['id']}", json={"name": name, "parent": parent, "nsfw": nsfw})
def edit_member(self, server_id: str, member_id: str, remove: list[str] | None, values: dict[str, Any]) -> Request[MemberPayload]:
return self.request("PATCH", f"/servers/{server_id}/members/{member_id}", json={"remove": remove, **values})
def delete_messages(self, channel_id: str, messages: list[str]) -> Request[None]:
return self.request("DELETE", f"/channels/{channel_id}/messages/bulk", json={"ids": messages})