# Copyright MelisaDev 2022 - Present
# Full MIT License can be found in `LICENSE.txt` at the project root.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import IntEnum
from typing import List, Any, Optional, overload, Dict, TYPE_CHECKING, Union
from .channel import (
ThreadsList,
Thread,
_choose_channel_type,
)
from .emoji import Emoji
from .member import GuildMember
from .role import Role
from ...utils import Snowflake, Timestamp
from ...utils.api_model import APIModelBase
from ...utils.conversion import try_enum
from ...utils.types import APINullable
if TYPE_CHECKING:
from .channel import ChannelType, Channel
[docs]class DefaultMessageNotificationLevel(IntEnum):
"""Message notification level
Attributes
----------
ALL_MESSAGES:
Members will receive notifications for all messages by default
ONLY_MENTIONS:
Members will receive notifications only for messages that @mention them by default
"""
ALL_MESSAGES = 0
ONLY_MENTIONS = 1
def __int__(self):
return self.value
[docs]class ExplicitContentFilterLevel(IntEnum):
"""Explicit Content Filter Level
Attributes
----------
DISABLED:
Media content will not be scanned
MEMBERS_WITHOUT_ROLES:
Media content sent by members without roles will be scanned
ALL_MEMBERS:
Media content sent by all members will be scanned
"""
DISABLED = 0
MEMBERS_WITHOUT_ROLES = 1
ALL_MEMBERS = 2
def __int__(self):
return self.value
[docs]class MFALevel(IntEnum):
"""MFA Level
Attributes
----------
NONE:
Guild has no MFA/2FA requirement for moderation actions
ELEVATED:
Guild has a 2FA requirement for moderation actions
"""
NONE = 0
ELEVATED = 1
def __int__(self):
return self.value
[docs]class VerificationLevel(IntEnum):
"""Verification level on the server
Attributes
----------
NONE:
Unrestricted
LOW:
Must have verified email on account
MEDIUM:
Must be registered on Discord for longer than 5 minutes
HIGH:
Must be a member of the server for longer than 10 minutes
VERY_HIGH:
Must have a verified phone number
"""
NONE = 0
LOW = 1
MEDIUM = 2
HIGH = 3
VERY_HIGH = 4
def __int__(self):
return self.value
[docs]class GuildNSFWLevel(IntEnum):
"""NSFW level on the server
Attributes
----------
DEFAULT:
Default value on server
EXPLICIT:
Explicit value on server
SAFE:
Safe value on server
AGE_RESTRICTED:
Age restricted on server
"""
DEFAULT = 0
EXPLICIT = 1
SAFE = 2
AGE_RESTRICTED = 3
def __int__(self):
return self.value
[docs]class PremiumTier(IntEnum):
"""Boost the server with boosters and nitro
Attributes
----------
NONE:
Guild has not unlocked any Server Boost perks
TIER_1:
Guild has unlocked Server Boost level 1 perks
TIER_2:
Guild has unlocked Server Boost level 2 perks
TIER_3:
Guild has unlocked Server Boost level 3 perks
"""
NONE = 0
TIER_1 = 1
TIER_2 = 2
TIER_3 = 3
def __int__(self):
return self.value
[docs]class SystemChannelFlags(IntEnum):
"""System channel flags
Attributes
----------
SUPPRESS_JOIN_NOTIFICATIONS:
Suppress member join notifications
SUPPRESS_PREMIUM_SUBSCRIPTIONS:
Suppress server boost notifications
SUPPRESS_GUILD_REMINDER_NOTIFICATIONS:
Suppress server setup tips
SUPPRESS_JOIN_NOTIFICATION_REPLIES:
Hide member join sticker reply buttons
"""
SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0
SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1
SUPPRESS_GUILD_REMINDER_NOTIFICATIONS = 1 << 2
SUPPRESS_JOIN_NOTIFICATION_REPLIES = 1 << 3
def __int__(self):
return self.value
[docs]@dataclass(repr=False)
class Guild(APIModelBase):
"""Guilds in Discord represent an isolated collection of users and channels,
and are often referred to as "servers" in the UI.
Attributes
----------
id: :class:`~melisa.utils.types.Snowflake`
Guild id
name: :class:`str`
Guild name (2-100 characters, excluding trailing and leading whitespace)
icon: :class:`str`
Icon hash
icon_hash: APINullable[:class:`str`]
Icon hash, returned when in the template object
splash: Optional[:class:`str`]
Splash hash
discovery_splash: APINullable[:class:`str`]
Discovery splash hash; only present for guilds with the "DISCOVERABLE" feature
owner: :class:`bool`
True if the user is the owner of the guild
owner_id: APINullable[:class:`~melisa.utils.types.Snowflake`]
Id of owner
permissions: APINullable[:class:`str`]
Total permissions for the user in the guild (excludes overwrites)
region: APINullable[:class:`str`]
Voice region id for the guild (deprecated)
afk_channel_id: APINullable[:class:`~melisa.utils.types.Snowflake`]
Id of afk channel
afk_timeout: :class:`int`
Afk timeout in seconds
widget_enabled: :class:`bool`
True if the server widget is enabled
widget_channel_id: :class:`~melisa.utils.types.Snowflake`
The channel id that the widget will generate an invite to, or `null` if set to no invite
verification_level: :class:`int`
Verification level required for the guild
default_message_notifications: APINullable[:class:`int`]
Default message notifications level
explicit_content_filter: :class:`int`
Explicit content filter level
features: APINullable[List[:class:`str`]]
Enabled guild features
roles: Dict[:class:`~melisa.utils.snowflake.Snowflake`, :class:`~melisa.models.guild.role.Role`]
Roles in the guild
emojis: Dict[:class:`~melisa.utils.snowflake.Snowflake`, :class:`~melisa.models.guild.emoji.Emoji`]
Custom guild emojis
mfa_level: :class:`int`
Required MFA level for the guild
application_id: APINullable[:class:`~melisa.utils.types.Snowflake`]
Application id of the guild creator if it is bot-created
system_channel_id: APINullable[:class:`~melisa.utils.types.Snowflake`]
The id of the channel where guild notices
such as welcome messages and boost events are posted
system_channel_flags: :class:`int`
System channel flags
rules_channel_id: APINullable[:class:`~melisa.utils.types.Snowflake`]
The id of the channel where Community guilds can display rules and/or guidelines
large: APINullable[:class:`bool`]
True if this is considered a large guild
unavailable: :class:`bool`
True if this guild is unavailable due to an outage
member_count: APINullable[:class:`int`]
Total number of members in this guild
voice_states:
States of members currently in voice channels; lacks the `guild_id` key
members: APINullable[:class:`~melisa.models.guild.member.GuildMember`]
Users in the guild
channels: APINullable[Dict[:class:`~melisa.models.guild.channel.Channel`]]
Channels in the guild
threads: APINullable[Dict[:class:`~melisa.models.guild.channel.Thread`]]
All active threads in the guild that current user has permission to view
presences: APINullable[:class:`typing.Any`]
Presences of the members in the guild, will only include non-offline members
if the size is greater than `large threshold`
max_presences: APINullable[:class:`int`]
The maximum number of presences for the guild
(`null` is always returned, apart from the largest of guilds)
max_members: APINullable[:class:`int`]
The maximum number of members for the guild
vanity_url_code: APINullable[:class:`str`]
The vanity url code for the guild
description: APINullable[:class:`str`]
The description of a Community guild
banner: Optional[:class:`str`]
Banner hash
premium_tier: APINullable[:class:`str`]
Premium tier (Server Boost level)
premium_subscription_count: APINullable[:class:`int`]
The number of boosts this guild currently has
preferred_locale: APINullable[:class:`str`]
The preferred locale of a Community guild;
used in server discovery and notices from Discord,
and sent in interactions; defaults to "en-US"
public_updates_channel_id: APINullable[:class:`~melisa.utils.types.Snowflake`]
The id of the channel where admins and moderators of
Community guilds receive notices from Discord
max_video_channel_users: APINullable[:class:`int`]
The maximum amount of users in a video channel
approximate_member_count: APINullable[:class:`int`]
Approximate number of members in this guild,
returned from the `GET /guilds/<id>` endpoint when `with_counts` is `true`
approximate_presence_count: APINullable[:class:`int`]
Approximate number of non-offline members in this guild,
returned from the `GET /guilds/<id>`
endpoint when `with_counts` is `true`
nsfw_level: APINullable[:class:`int`]
Guild NSFW level
premium_progress_bar_enabled: APINullable[:class:`bool`]
Whether the guild has the boost progress bar enabled
stage_instances: APINullable[:class:`typing.Any`]
Stage instances in the guild
stickers: APINullable[:class:`typing.Any`]
Custom guild stickers
welcome_screen: APINullable[:class:`typing.Any`]
The welcome screen of a Community guild, shown to new members,
returned in an Invite's guild object
guild_scheduled_events: APINullable[:class:`typing.Any`]
The scheduled events in the guild
"""
id: Snowflake
roles: APINullable[Dict]
members: APINullable[Dict]
threads: APINullable[Dict]
presences: APINullable[Dict]
channels: APINullable[Dict]
name: APINullable[str] = None
icon: APINullable[str] = None
icon_hash: APINullable[str] = None
splash: APINullable[str] = None
discovery_splash: APINullable[str] = None
owner: APINullable[bool] = None
owner_id: APINullable[Snowflake] = None
permissions: APINullable[str] = None
region: APINullable[str] = None
afk_channel_id: APINullable[Snowflake] = None
afk_timeout: APINullable[int] = None
widget_enabled: APINullable[bool] = None
widget_channel_id: APINullable[Snowflake] = None
verification_level: APINullable[int] = None
default_message_notifications: APINullable[int] = None
explicit_content_filter: APINullable[int] = None
features: APINullable[List[str]] = None
emojis: APINullable[Dict[str, Emoji]] = None
mfa_level: APINullable[MFALevel] = None
application_id: APINullable[Snowflake] = None
system_channel_id: APINullable[Snowflake] = None
system_channel_flags: APINullable[SystemChannelFlags] = None
rules_channel_id: APINullable[Snowflake] = None
large: APINullable[bool] = None
unavailable: APINullable[bool] = None
member_count: APINullable[int] = None
voice_states: APINullable[List] = None
# TODO: Make a structure for voice_states, members, channels, threads, presences(?)
max_presences: APINullable[int] = None
max_members: APINullable[int] = None
vanity_url_code: APINullable[str] = None
description: APINullable[str] = None
banner: APINullable[str] = None
premium_tier: APINullable[str] = None
premium_subscription_count: APINullable[int] = None
preferred_locale: APINullable[str] = None
public_updates_channel_id: APINullable[Snowflake] = None
max_video_channel_users: APINullable[int] = None
approximate_member_count: APINullable[int] = None
approximate_presence_count: APINullable[int] = None
nsfw_level: APINullable[int] = None
premium_progress_bar_enabled: APINullable[bool] = None
stage_instances: APINullable[List] = None
stickers: APINullable[List] = None
welcome_screen: APINullable = None
guild_scheduled_events: APINullable[List] = None
# TODO: Make a structure for welcome_screen, stage_instances,
# stickers and guild_scheduled_events
[docs] @classmethod
def from_dict(cls, data: Dict[str, Any]) -> Guild:
"""Generate a guild from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into a guild.
"""
self: Guild = super().__new__(cls)
self.id = Snowflake(int(data.get("id", 0)))
self.member_count = data.get("member_count", 0)
self.name = data.get("name", "")
self.region = data.get("region")
self.verification_level = try_enum(
VerificationLevel, data.get("verification_level")
)
self.vanity_url_code = data.get("vanity_url_code")
self.default_notifications = data.get("default_message_notifications")
self.explicit_content_filter = data.get("explicit_content_filter", 0)
self.afk_timeout = data.get("afk_timeout", 0)
self.icon = data.get("icon")
self.icon_hash = data.get("icon_hash")
self.banner = data.get("banner")
self.unavailable = data.get("unavailable", False)
self.permissions = data.get("permissions")
self.welcome_screen = data.get("welcome_screen")
self.guild_scheduled_events = data.get("guild_scheduled_events")
self.mfa_level = try_enum(MFALevel, data.get("mfa_level", 0))
self.emojis = data.get("emojis", {})
self.stickers = data.get("stickers", {})
self.features = data.get("features", [])
self.splash = data.get("splash")
self.description = data.get("description")
self.max_presences = data.get("max_presences")
self.max_members = data.get("max_members")
self.max_video_channel_users = data.get("max_video_channel_users")
self.premium_tier = try_enum(PremiumTier, data.get("premium_tier", 0))
self.premium_subscription_count = data.get("premium_subscription_count", 0)
self.system_channel_flags = try_enum(
SystemChannelFlags, data.get("system_channel_flags", 0)
)
self.preferred_locale = data.get("preferred_locale")
self.discovery_splash = data.get("discovery_splash")
self.nsfw_level = data.get("nsfw_level", 0)
self.premium_progress_bar_enabled = data.get(
"premium_progress_bar_enabled", False
)
self.approximate_presence_count = data.get("approximate_presence_count")
self.approximate_member_count = data.get("approximate_member_count")
self.widget_enabled = data.get("widget_enabled")
if data.get("widget_channel_id") is not None:
self.widget_channel_id = Snowflake(data["widget_channel_id"])
else:
self.widget_channel_id = None
if data.get("system_channel_id") is not None:
self.system_channel_id = Snowflake(data["system_channel_id"])
else:
self.system_channel_id = None
if data.get("rules_channel_id") is not None:
self.rules_channel_id = Snowflake(data["rules_channel_id"])
else:
self.rules_channel_id = None
if data.get("public_updates_channel_id") is not None:
self.public_updates_channel_id = Snowflake(
data["public_updates_channel_id"]
)
else:
self.public_updates_channel_id = None
if data.get("owner_id") is not None:
self.owner_id = Snowflake(data["owner_id"])
else:
self.owner_id = None
if data.get("afk_channel_id") is not None:
self.afk_channel_id = Snowflake(data["afk_channel_id"])
else:
self.afk_channel_id = None
self.stage_instances = data.get("stage_instances")
self.scheduled_events = data.get("guild_scheduled_events")
self.owner = None if data.get("owner") is None else data["owner"]
self.large = None if self.member_count == 0 else self.member_count >= 250
self.voice_states = data.get("voice_states")
self.presences = data.get("presences")
self.threads = {}
self.channels = {}
self.members = {}
self.roles = {}
self.emojis = {}
for member in data.get("members", []):
member = GuildMember.from_dict(member)
self.members[member.user.id] = member
for thread in data.get("threads", []):
self.threads[thread["id"]] = Thread.from_dict(thread)
for channel in data.get("channels", []):
channel = _choose_channel_type(channel)
self.channels[str(channel.id)] = channel
for role in data.get("roles", []):
role["guild_id"] = self.id
self.roles[Snowflake(int(role["id"]))] = Role.from_dict(role)
for emoji in data.get("emojis", []):
emoji["guild_id"] = self.id
self.emojis[Snowflake(emoji["id"])] = Emoji.from_dict(emoji)
return self
@property
def created_at(self):
""":class:`datetime.datetime`: Returns the guild's creation time in UTC."""
return datetime.fromtimestamp(self.id.timestamp, tz=timezone.utc)
[docs] def icon_url(self, *, size: int = 1024, image_format: str = None) -> str | None:
# ToDo: Add Docstrings
"""Icon Url (from the Discord CDN server)"""
if self.icon is None:
return None
else:
return self._client.rest.cdn.avatar_url(
self.id, self.icon, size=size, image_format=image_format
)
@overload
async def create_channel(
self,
*,
name: str,
type: Optional[ChannelType] = None,
topic: Optional[str] = None,
bitrate: Optional[int] = None,
user_limit: Optional[int] = None,
rate_limit_per_user: Optional[int] = None,
position: Optional[int] = None,
permission_overwrites: Optional[List[Any]] = None,
parent_id: Optional[Snowflake] = None,
nsfw: Optional[bool] = None,
reason: Optional[str] = None,
) -> Channel:
...
[docs] async def create_channel(self, *, reason: Optional[str] = None, **kwargs):
"""|coro|
Create a new channel object for the guild.
Parameters
----------
name: str
channel name (1-100 characters)
type: Optional[:class:`int`]
the type of channel
topic: Optional[:class:`str`]
channel topic (0-1024 characters)
bitrate: Optional[:class:`int`]
the bitrate (in bits) of the voice channel (voice only)
user_limit: Optional[:class:`int`]
the user limit of the voice channel (voice only)
rate_limit_per_user: Optional[:class:`int`]
amount of seconds a user has to wait
before sending another message (0-21600)
bots, as well as users with the permission
``MANAGE_MESSAGES`` or ``MANAGE_CHANNEL``, are unaffected
position: Optional[:class:`int`]
sorting position of the channel
permission_overwrites: Optional[List[Any]]
the channel's permission overwrites
parent_id: Optional[:class:`~melisa.Snowflake`]
id of the parent category for a channel
nsfw: Optional[:class:`bool`]
whether the channel is nsfw
reason: Optional[:class:`str`]
audit log reason |default| :data:`None`
Raises
-------
BadRequestError
If some specified parameters are wrong.
ForbiddenError
You do not have proper permissions to do the actions required.
This method requires `MANAGE_CHANNELS` permission.
Setting `MANAGE_ROLES` permission in channels is only possible for guild administrators.
Returns
-------
:class:`~melisa.models.guild.channel.Channel`
New channel object.
"""
data = await self._http.post(
f"guilds/{self.id}/channels",
data=kwargs,
headers={"X-Audit-Log-Reason": reason},
)
return _choose_channel_type(data)
[docs] async def active_threads(self) -> ThreadsList:
"""|coro|
Returns a Threadslist of active ``Thread`` that the client can access.
This includes both private and public threads.
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
Returns
-------
:class:`~melisa.models.channel.ThreadsList`
The active threads.
"""
return ThreadsList.from_dict(
await self._http.get(f"/guilds/{self.id}/threads/active")
)
[docs] async def unban(
self,
user_id: Union[Snowflake, str, int],
*,
reason: Optional[str] = None,
):
"""|coro|
Remove the ban for a user.
**Required permissions:** ``BAN_MEMBERS``
Parameters
----------
user_id: Union[:class:`int`, :class:`str`, :class:`~.melisa.utils.snowflake.Snowflake`]
Id of user to operate with.
reason: Optional[:class:`str`]
The reason of the action.
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
ForbiddenError
You do not have proper permissions to do the actions required.
BadRequestError
You provided a wrong guild, user or something else
Or if the user is not banned
"""
await self._client.rest.remove_guild_ban(self.id, user_id, reason=reason)
[docs] async def fetch_emojis(self) -> List[Emoji]:
"""|coro|
Getting all emojis from guild
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
ForbiddenError
You do not have proper permissions to do the actions required.
BadRequestError
You provided a wrong guild
"""
return await self._client.rest.list_guild_emojis(self.id)
[docs] async def fetch_emoji(self, emoji_id: Union[Snowflake, str, int]) -> Emoji:
"""|coro|
Get emoji from guild
Parameters
----------
emoji_id: Union[:class:`int`, :class:`str`, :class:`~.melisa.utils.snowflake.Snowflake`]
The ID of the emoji that we will get
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
ForbiddenError
You do not have proper permissions to do the actions required.
BadRequestError
You provided a wrong guild and emoji
"""
return await self._client.rest.get_guild_emoji(self.id, emoji_id)
[docs] async def create_emoji(
self,
emoji_name: str,
emoji_image: Any,
*,
reason: Optional[str] = None,
role_id: List[Union[Snowflake, str, int]] = None,
) -> Emoji:
# FIXME: emoji_image != str, it works another way
"""|coro|
Create a new emoji for the guild.
Requires the `MANAGE_EMOJIS_AND_STICKERS` permission.
Returns the new emoji object on success. Fires a Guild Emojis Update Gateway event.
Parameters
----------
emoji_name: Optional[:class:`str`]
Name of the emoji
emoji_image: Optional[:class:`str`]
The 128x128 emoji image
reason: Optional[:class:`str`]
The reason of the action
role_id: List[Union[:class:`int`, :class:`str`, :class:`~.melisa.utils.snowflake.Snowflake`]]
Roles allowed to use this emoji
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
ForbiddenError
You do not have proper permissions to do the actions required.
BadRequestError
You provided a wrong guild
"""
return await self._client.rest.create_guild_emoji(
self.id, emoji_name, emoji_image, reason=reason, role_id=role_id
)
[docs] async def edit_emoji(
self,
emoji_id: Union[Snowflake, str, int],
emoji_name: Optional[str],
*,
reason: Optional[str] = None,
role_id: List[Union[Snowflake, str, int]] = None,
):
"""|coro|
Modify the given emoji.
Requires the `MANAGE_EMOJIS_AND_STICKERS` permission.
Returns the updated emoji object on success.
Parameters
----------
emoji_id: Union[:class:`int`, :class:`str`, :class:`~.melisa.utils.snowflake.Snowflake`]
The ID of the emoji that we will modify
emoji_name: Optional[:class:`str`]
Name of the emoji
reason: Optional[:class:`str`]
The reason of the action
role_id: Union[:class:`int`, :class:`str`, :class:`~.melisa.utils.snowflake.Snowflake`]
Roles allowed to use this emoji
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
ForbiddenError
You do not have proper permissions to do the actions required.
BadRequestError
You provided a wrong guild and emoji
"""
return await self._client.rest.modify_guild_emoji(
self.id, emoji_id, emoji_name, reason=reason, role_id=role_id
)
[docs] async def delete_emoji(
self, emoji_id: Union[Snowflake, str, int], *, reason: Optional[str] = None
) -> None:
"""|coro|
Delete the given emoji.
Requires the `MANAGE_EMOJIS_AND_STICKERS` permission.
Returns `204 No Content` on success. Fires a Guild Emojis Update Gateway event.
Parameters
----------
emoji_id: Union[:class:`int`, :class:`str`, :class:`~.melisa.utils.snowflake.Snowflake`]
The ID of the emoji that we will delete
reason: Optional[:class:`str`]
The reason of the action
Raises
-------
HTTPException
The request to perform the action failed with other http exception.
ForbiddenError
You do not have proper permissions to do the actions required.
BadRequestError
You provided a wrong guild and emoji
"""
await self._client.rest.delete_guild_emoji(self.id, emoji_id, reason=reason)
[docs]@dataclass(repr=False)
class UnavailableGuild(APIModelBase):
"""A partial guild object.
Represents an Offline Guild, or a Guild whose information has not been provided
through Guild Create events during the Gateway connect.
Attributes
----------
id: :class:`~melisa.utils.types.Snowflake`
Guild id
unavailable: :class:`bool`
True if this guild is unavailable due to an outage
"""
id: APINullable[Snowflake] = None
unavailable: APINullable[bool] = True
[docs] @classmethod
def from_dict(cls, data: Dict[str, Any]) -> UnavailableGuild:
"""Generate a unavailable guild from the given data.
Parameters
----------
data: :class:`dict`
The dictionary to convert into an unavailable guild.
"""
self: UnavailableGuild = super().__new__(cls)
self.id = Snowflake(int(data["id"]))
self.unavailable = data["unavailable"]
return self