from datetime import datetime, timezone
from enum import IntEnum, Enum, EnumMeta
import copy
from dataclasses import _is_dataclass_instance, fields, dataclass
from typing import (
Dict,
Union,
Generic,
Tuple,
TypeVar,
get_origin,
get_args,
Optional,
List,
)
from sys import modules
from itertools import chain
from typing_extensions import get_type_hints
KT = TypeVar("KT")
VT = TypeVar("VT")
T = TypeVar("T")
class Singleton(type):
# Thanks to this stackoverflow answer (method 3):
# https://stackoverflow.com/q/6760685/12668716
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class TypeCache(metaclass=Singleton):
# Thanks to Pincer Devs. This class is from the Pincer Library.
cache = {}
def __init__(self):
lcp = modules.copy()
for module in lcp:
if not module.startswith("melisa"):
continue
TypeCache.cache.update(lcp[module].__dict__)
def _asdict_ignore_none(obj: Generic[T]) -> Union[Tuple, Dict, T]:
"""
Returns a dict from a dataclass that ignores
all values that are None
Modification of _asdict_inner from dataclasses
Parameters
----------
obj: Generic[T]
The object to convert
Returns
-------
A dict without None values
"""
print(obj)
if _is_dataclass_instance(obj):
result = []
for f in fields(obj):
value = _asdict_ignore_none(getattr(obj, f.name))
if isinstance(value, Enum):
result.append((f.name, value.value))
elif not f.name.startswith("_"):
result.append((f.name, value))
return dict(result)
elif isinstance(obj, tuple) and hasattr(obj, "_fields"):
return type(obj)(*[_asdict_ignore_none(v) for v in obj])
elif isinstance(obj, (list, tuple)):
return type(obj)(_asdict_ignore_none(v) for v in obj)
elif isinstance(obj, datetime):
return str(round(obj.timestamp() * 1000))
elif isinstance(obj, dict):
return type(obj)(
(_asdict_ignore_none(k), _asdict_ignore_none(v)) for k, v in obj.items()
)
else:
return copy.deepcopy(obj)
class APIObjectBase:
"""
Represents an object which has been fetched from the BotiCord API.
"""
def __attr_convert(self, attr_value: Dict, attr_type: T) -> T:
factory = attr_type
# Always use `__factory__` over __init__
if getattr(attr_type, "__factory__", None):
factory = attr_type.__factory__
if attr_value is None:
return None
if attr_type is not None and isinstance(attr_value, attr_type):
return attr_value
if isinstance(attr_value, dict):
return factory(attr_value)
return factory(attr_value)
def __post_init__(self):
TypeCache()
attributes = chain.from_iterable(
get_type_hints(cls, globalns=TypeCache.cache).items()
for cls in chain(self.__class__.__bases__, (self,))
)
for attr, attr_type in attributes:
# Ignore private attributes.
if attr.startswith("_"):
continue
types = self.__get_types(attr, attr_type)
types = tuple(filter(lambda tpe: tpe is not None, types))
if not types:
raise ValueError(
f"Attribute `{attr}` in `{type(self).__name__}` only "
"consisted of missing/optional type!"
)
specific_tp = types[0]
attr_gotten = getattr(self, attr)
if tp := get_origin(specific_tp):
specific_tp = tp
if isinstance(specific_tp, EnumMeta) and not attr_gotten:
attr_value = None
elif tp == list and attr_gotten and (classes := get_args(types[0])):
attr_value = [
self.__attr_convert(attr_item, classes[0])
for attr_item in attr_gotten
]
elif tp == dict and attr_gotten and (classes := get_args(types[0])):
attr_value = {
key: self.__attr_convert(value, classes[1])
for key, value in attr_gotten.items()
}
else:
attr_value = self.__attr_convert(attr_gotten, specific_tp)
setattr(self, attr, attr_value)
def __get_types(self, attr: str, arg_type: type) -> Tuple[type]:
origin = get_origin(arg_type)
if origin is Union:
# Ahh yes, typing module has no type annotations for this...
# noinspection PyTypeChecker
args: Tuple[type] = get_args(arg_type)
if 2 <= len(args) < 4:
return args
raise ValueError(
f"Attribute `{attr}` in `{type(self).__name__}` has too many "
f"or not enough arguments! (got {len(args)} expected 2-3)"
)
return (arg_type,)
@classmethod
def __factory__(cls: Generic[T], *args, **kwargs) -> T:
return cls.from_dict(*args, **kwargs)
def __repr__(self):
attrs = ", ".join(
f"{k}={v!r}"
for k, v in self.__dict__.items()
if v and not k.startswith("_")
)
return f"{type(self).__name__}({attrs})"
def __str__(self):
if _name := getattr(self, "__name__", None):
return f"{_name} {self.__class__.__name__.lower()}"
return super().__str__()
def to_dict(self) -> Dict:
"""
Transform the current object to a dictionary representation. Parameters that
start with an underscore are not serialized.
"""
return _asdict_ignore_none(self)
[docs]class BotLibrary(IntEnum):
"""The library that the bot is based on"""
DISCORD4J = 1
"""Discord4j"""
DISCORDCR = 2
"""Discordcr"""
DISCORDGO = 3
"""DiscordGO"""
DISCORDDOO = 4
"""Discordoo"""
DSHARPPLUS = 5
"""DSharpPlus"""
DISCORDJS = 6
"""Discord.js"""
DISCORDNET = 7
"""Discord.Net"""
DISCORDPY = 8
"""discord.py"""
ERIS = 9
"""eris"""
JAVACORD = 10
"""JavaCord"""
JDA = 11
"""JDA"""
OTHER = 12
"""Other"""
NONE = 0
"""Bot's library doesn't specified"""
[docs]class ResourceStatus(IntEnum):
"""Status of the project on monitoring"""
HIDDEN = 0
"""is hidden"""
PUBLIC = 1
"""is public"""
BANNED = 2
"""is banned"""
PENDING = 3
"""is pending"""
[docs]class ServerTag(IntEnum):
"""Tags of the server"""
SPEAKING = 130
"""Speaking"""
FUN = 131
"""Fun"""
GAMES = 132
"""Games"""
CINEMA = 133
"""Cinema"""
ANIME = 134
"""Anime"""
ART = 135
"""Art"""
CODING = 136
"""Coding"""
MUSIC = 137
"""Music"""
ADULT = 138
"""18+"""
ROLEPLAY = 139
"""Role-Play"""
HUMOUR = 140
"""Humour"""
GENSHIN = 160
"""Genshin"""
MINECRAFT = 161
"""Minecraft"""
GTA = 162
"""GTA"""
CS = 163
"""CS"""
DOTA = 164
"""Dota"""
AMONG_US = 165
"""Among Us"""
FORTNITE = 166
"""Fortnite"""
BRAWL_STARS = 167
"""Brawl Stars"""
[docs]class BotTag(IntEnum):
"""Tags of the bot"""
MODERATION = 0
"""Moderation"""
BOT = 1
"""Bot"""
UTILITIES = 2
"""Utilities"""
ENTERTAINMENT = 3
"""Entertainment"""
MUSIC = 4
"""Music"""
ECONOMY = 5
"""Economy"""
LOGS = 6
"""Logs"""
LEVELS = 7
"""Levels"""
NSFW = 8
"""NSFW (18+)"""
SETTINGS = 9
"""Settings"""
ROLE_PLAY = 10
"""Role-Play"""
MEMES = 11
"""Memes"""
GAMES = 12
"""Games"""
AI = 13
"""AI"""
[docs]@dataclass(repr=False)
class UserLinks(APIObjectBase):
"""Links of the userk"""
vk: Optional[str]
"""vk.com"""
telegram: Optional[str]
"""t.me"""
donate: Optional[str]
"""Donate"""
git: Optional[str]
"""Link to git of the user"""
custom: Optional[str]
"""Custom link"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a UserLinks from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a UserLinks.
"""
self: UserLinks = super().__new__(cls)
data = data or {}
self.vk = data.get("vk")
self.telegram = data.get("telegram")
self.donate = data.get("donate")
self.git = data.get("git")
self.custom = data.get("custom")
return self
[docs]@dataclass(repr=False)
class UserBadge(APIObjectBase):
"""Information about user's profile badge"""
id: int
"""Badge's ID"""
name: str
"""Badge's name"""
asset_url: str
"""Badge's icon URL"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a UserBadge from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a UserBadge.
"""
self: UserBadge = super().__new__(cls)
self.id = data["id"]
self.name = data["name"]
self.asset_url = data["assetURL"]
return self
[docs]@dataclass(repr=False)
class ResourceUp(APIObjectBase):
"""Information about bump (bot/server)"""
id: str
"""Bump's id"""
expires: datetime
"""Expiration date. (ATTENTION! When using `to_dict()`, the data may not correspond to the actual data due to the peculiarities of the `datetime` module)"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a ResourceUp from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a ResourceUp.
"""
self: ResourceUp = super().__new__(cls)
self.id = data["id"]
self.expires = datetime.fromtimestamp(
int(int(data["expires"]) / 1000), tz=timezone.utc
)
return self
[docs]@dataclass(repr=False)
class ResourceRating(APIObjectBase):
"""Rating of bot/server"""
count: int
"""Number of ratings"""
rating: int
"""Rating (from 1 to 5)"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a ResourceRating from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a ResourceRating.
"""
self: ResourceRating = super().__new__(cls)
self.count = data["count"]
self.rating = data["rating"]
return self
[docs]@dataclass(repr=False)
class PartialUser(APIObjectBase):
"""Partial user from BotiCord."""
username: str
"""Username"""
discriminator: str
"""Discriminator"""
avatar: Optional[str]
"""Avatar of the user"""
id: str
"""Id of the user"""
socials: UserLinks
"""Links of the user"""
description: Optional[str]
"""Description of the user"""
short_description: Optional[str]
"""Short description of the user"""
status: Optional[str]
"""Status of the user"""
short_domain: Optional[str]
"""Short domain"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a PartialUser from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a PartialUser.
"""
self: cls = super().__new__(cls)
self.username = data["username"]
self.discriminator = data.get("discriminator")
self.avatar = data.get("avatar")
self.id = data["id"]
self.socials = UserLinks.from_dict(data.get("socials", {}))
self.description = data.get("description")
self.short_description = data.get("shortDescription")
self.status = data.get("status")
self.short_domain = data.get("shortDomain")
return self
[docs]@dataclass(repr=False)
class ResourceServer(APIObjectBase):
"""Information about server from BotiCord.
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str
"""Server's ID"""
name: str
"""Server's name"""
short_description: str
"""Server's short description"""
description: str
"""Server's description"""
avatar: Optional[str]
"""Server's avatar"""
short_link: Optional[str]
"""Server's short link"""
invite_link: str
"""Server's invite link"""
premium_active: bool
"""Server's premium state"""
premium_auto_fetch: Optional[bool]
"""Server's premium auto fetch state"""
premium_banner_url: Optional[str]
"""Server's premium banner URL"""
premium_splash_url: Optional[str]
"""Server's premium splash URL"""
standart_banner_id: int
"""Server's standart banner ID"""
owner: str
"""Server's owner ID"""
status: ResourceStatus
"""Server's status"""
ratings: List[ResourceRating]
"""Server's ratings"""
created_date: datetime
"""Server's creation time"""
members: Optional[int]
"""Server's members count"""
website: Optional[str]
"""Server's website"""
tags: List[ServerTag]
"""Server's tags"""
moderators: List[PartialUser]
"""Server's moderators"""
up_count: int
"""Server's up count"""
ups: Optional[ResourceUp]
"""Server's ups"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a ResourceServer from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a ResourceServer."""
self = super().__new__(cls)
self.id = data.get("id")
self.name = data.get("name")
self.short_description = data.get("shortDescription")
self.description = data.get("description")
self.avatar = data.get("avatar")
self.short_link = data.get("shortLink")
self.invite_link = data.get("inviteLink")
self.owner = data.get("owner")
self.website = data.get("website")
self.up_count = data.get("upCount")
self.standart_banner_id = data.get("standartBannerID")
self.premium_active = data["premium"].get("active")
self.premium_splash_url = data["premium"].get("splashURL")
self.premium_auto_fetch = data["premium"].get("autoFetch")
self.premium_banner_url = data["premium"].get("bannerURL")
self.status = ResourceStatus(data.get("status"))
self.ratings = [
ResourceRating.from_dict(rating) for rating in data.get("ratings", [])
]
self.created_date = datetime.strptime(
data.get("createdDate"), "%Y-%m-%dT%H:%M:%S.%f%z"
)
self.tags = [ServerTag(tag) for tag in data.get("tags", [])]
self.ups = [ResourceUp.from_dict(up) for up in data.get("ups", [])]
self.moderators = [
PartialUser.from_dict(mod) for mod in data.get("moderators", [])
]
self.members = data.get("memberCount")
return self
[docs]@dataclass(repr=False)
class ResourceBot(APIObjectBase):
"""Bot published on BotiCord
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str
"""ID of the bot"""
name: str
"""Name of the bot"""
short_description: str
"""Short description of the bot"""
description: str
"""Description of the bot"""
avatar: Optional[str]
"""Avatar of the bot"""
short_link: Optional[str]
"""Short link to the bot's page"""
standart_banner_id: int
"""Server's standart banner ID"""
invite_link: str
"""Invite link"""
premium_active: bool
"""Is premium status active? (True/False)"""
premium_splash_url: Optional[str]
"""Link to the splash"""
premium_auto_fetch: Optional[bool]
"""Is auto-fetch enabled? (True/False)"""
premium_banner_url: Optional[str]
"""Premium banner URL"""
owner: str
"""Owner of the bot"""
status: ResourceStatus
"""Status of the bot"""
ratings: List[ResourceRating]
"""Bot's ratings"""
prefix: str
"""Prefix of the bot"""
discriminator: str
"""Bot's discriminator"""
created_date: datetime
"""Date when the bot was published"""
support_server_invite_link: Optional[str]
"""Link to the support server"""
library: Optional[BotLibrary]
"""The library that the bot is based on"""
guilds: Optional[int]
"""Number of guilds"""
shards: Optional[int]
"""Number of shards"""
members: Optional[int]
"""Number of members"""
website: Optional[str]
"""Link to bot's website"""
tags: List[BotTag]
"""List of bot tags"""
up_count: int
"""Number of ups"""
ups: List[ResourceUp]
"""List of bot's ups"""
developers: List[PartialUser]
"""List of bot's developers"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a ResourceBot from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a ResourceBot.
"""
self: ResourceBot = super().__new__(cls)
self.id = data.get("id")
self.name = data.get("name")
self.short_description = data.get("shortDescription")
self.description = data.get("description")
self.avatar = data.get("avatar")
self.short_link = data.get("shortLink")
self.invite_link = data.get("inviteLink")
self.owner = data.get("owner")
self.prefix = data.get("prefix")
self.discriminator = data.get("discriminator")
self.support_server_invite_link = data.get("support_server_invite")
self.website = data.get("website")
self.up_count = data.get("upCount")
self.standart_banner_id = data.get("standartBannerID")
self.premium_active = data["premium"].get("active")
self.premium_splash_url = data["premium"].get("splashURL")
self.premium_auto_fetch = data["premium"].get("autoFetch")
self.premium_banner_url = data["premium"].get("bannerURL")
self.status = ResourceStatus(data.get("status"))
self.ratings = [
ResourceRating.from_dict(rating) for rating in data.get("ratings", [])
]
self.created_date = datetime.strptime(
data["createdDate"], "%Y-%m-%dT%H:%M:%S.%f%z"
)
self.library = (
BotLibrary(data["library"]) if data.get("library") is not None else None
)
self.tags = [BotTag(tag) for tag in data.get("tags", [])]
self.ups = [ResourceUp.from_dict(up) for up in data.get("ups", [])]
self.developers = [
PartialUser.from_dict(dev) for dev in data.get("developers", [])
]
self.guilds = data.get("guilds")
self.shards = data.get("shards")
self.members = data.get("members")
return self
[docs]@dataclass(repr=False)
class UserProfile(PartialUser):
"""Information about user's profile from BotiCord.'"""
badges: List[UserBadge]
"""User's badges list."""
bots: List[ResourceBot]
"""User's bots list"""
servers: List[ResourceServer]
"""User's servers list"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a UserProfile from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a UserProfile."""
self = super().from_dict(data)
self.badges = [UserBadge.from_dict(badge) for badge in data.get("badges", [])]
self.bots = [ResourceBot.from_dict(bot) for bot in data.get("bots", [])]
self.servers = [
ResourceServer.from_dict(server) for server in data.get("servers", [])
]
return self
[docs]@dataclass(repr=False)
class MeiliIndexedBot(APIObjectBase):
"""Bot found on BotiCord
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str
"""ID of the bot"""
name: str
"""Name of the bot"""
short_description: str
"""Short description of the bot"""
description: str
"""Description of the bot"""
avatar: Optional[str]
"""Avatar of the bot"""
invite: str
"""Invite link"""
premium_active: bool
"""Is premium status active? (True/False)"""
premium_banner: Optional[str]
"""Premium banner URL"""
banner: int
"""Standart banner"""
rating: int
"""Bot's rating"""
discriminator: str
"""Bot's discriminator"""
library: Optional[BotLibrary]
"""The library that the bot is based on"""
guilds: Optional[int]
"""Number of guilds"""
shards: Optional[int]
"""Number of shards"""
members: Optional[int]
"""Number of members"""
tags: List[BotTag]
"""List of bot tags"""
ups: int
"""List of bot's ups"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a MeiliIndexedBot from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a MeiliIndexedBot.
"""
self: MeiliIndexedBot = super().__new__(cls)
self.id = data.get("id")
self.name = data.get("name")
self.short_description = data.get("shortDescription")
self.description = data.get("description")
self.avatar = data.get("avatar")
self.invite = data.get("invite")
self.discriminator = data.get("discriminator")
self.ups = data.get("ups")
self.rating = data.get("rating")
self.banner = data.get("banner")
self.premium_active = data.get("premiumActive")
self.premium_banner = data.get("premiumBanner")
self.library = (
BotLibrary(data["library"]) if data.get("library") is not None else None
)
self.tags = [BotTag(tag) for tag in data.get("tags", [])]
self.guilds = data.get("guilds")
self.shards = data.get("shards")
self.members = data.get("members")
return self
[docs]@dataclass(repr=False)
class MeiliIndexedServer(APIObjectBase):
"""Server found on BotiCord
.. warning::
The result of the reverse conversion (`.to_dict()`) may not match the actual data.
"""
id: str
"""ID of the server"""
name: str
"""Name of the server"""
short_description: str
"""Short description of the server"""
description: str
"""Description of the server"""
avatar: Optional[str]
"""Avatar of the server"""
invite: str
"""Invite link"""
premium_active: bool
"""Is premium status active? (True/False)"""
premium_banner: Optional[str]
"""Premium banner URL"""
banner: int
"""Standart banner"""
discord_banner: Optional[str]
"""Discord banner URL"""
rating: int
"""Server's rating"""
members: Optional[int]
"""Number of members"""
tags: List[ServerTag]
"""List of server tags"""
ups: int
"""List of server's ups"""
[docs] @classmethod
def from_dict(cls, data: dict):
"""Generate a MeiliIndexedServer from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a MeiliIndexedServer.
"""
self: MeiliIndexedServer = super().__new__(cls)
self.id = data.get("id")
self.name = data.get("name")
self.short_description = data.get("shortDescription")
self.description = data.get("description")
self.avatar = data.get("avatar")
self.invite = data.get("invite")
self.ups = data.get("ups")
self.rating = data.get("rating")
self.banner = data.get("banner")
self.premium_active = data.get("premiumActive")
self.premium_banner = data.get("premiumBanner")
self.discord_banner = data.get("discordBanner")
self.tags = [ServerTag(tag) for tag in data.get("tags", [])]
self.members = data.get("members")
return self