breadtube-bot/breadtube_bot/objects.py
2025-10-04 16:51:08 +09:00

512 lines
19 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import Enum, IntFlag
class FileMime(Enum):
AUDIO_OGG = 'audio/ogg'
IMAGE_JPEG = 'image/jpeg'
IMAGE_PNG = 'image/png'
IMAGE_SVG = 'image/svg'
JSON = 'application/json'
PDF = 'application/pdf'
TEXT_CSV = 'text/csv'
TEXT_HTML = 'text/html'
TEXT_MARKDOWN = 'text/markdown'
TEXT_PLAIN = 'text/plain'
UNKNOWN = 'application/unknown'
VIDEO_MP4 = 'video/mp4'
VIDEO_MPEG = 'video/mpeg'
VIDEO_WEBM = 'video/webm'
ZIP = 'application/zip'
@classmethod
def _missing_(cls, value): # noqa: ARG003
return FileMime.UNKNOWN
class ChannelType(Enum):
GUILD_TEXT = 0
DM = 1
GUILD_VOICE = 2
GROUP_DM = 3
GUILD_CATEGORY = 4
GUILD_ANNOUNCEMENT = 5
ANNOUNCEMENT_THREAD = 10
PUBLIC_THREAD = 11
PRIVATE_THREAD = 12
GUILD_STAGE_VOICE = 13
GUILD_DIRECTORY = 14
GUILD_FORUM = 15
GUILD_MEDIA = 16
class ChannelFlags(IntFlag):
# this thread is pinned to the top of its parent GUILD_FORUM or GUILD_MEDIA channel
PINNED = 1 << 1
# whether a tag is required to be specified when creating a thread in a GUILD_FORUM or a GUILD_MEDIA channel.
# Tags are specified in the applied_tags field.
REQUIRE_TAG = 1 << 4
# when set hides the embedded media download options. Available only for media channels
HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15
class OverwriteType(Enum):
ROLE = 0
MEMBER = 1
class Permissions(IntFlag):
NONE = 0
# Allows creation of instant invites
CREATE_INSTANT_INVITE = 1
# Allows kicking members
KICK_MEMBERS = 1 << 1
# Allows banning members
BAN_MEMBERS = 1 << 2
# Allows all permissions and bypasses channel permission overwrites
ADMINISTRATOR = 1 << 3
# Allows management and editing of channels
MANAGE_CHANNELS = 1 << 4
# Allows management and editing of the guild
MANAGE_GUILD = 1 << 5
# Allows for adding new reactions to messages.
# This permission does not apply to reacting with an existing reaction on a message.
ADD_REACTIONS = 1 << 6
# Allows for viewing of audit logs
VIEW_AUDIT_LOG = 1 << 7
# Allows for using priority speaker in a voice channel
PRIORITY_SPEAKER = 1 << 8
# Allows the user to go live
STREAM = 1 << 9
# Allows guild members to view a channel,
# which includes reading messages in text channels and joining voice channels
VIEW_CHANNEL = 1 << 10
# Allows for sending messages in a channel and creating threads in a forum
# (does not allow sending messages in threads)
SEND_MESSAGES = 1 << 11
# Allows for sending of /tts messages
SEND_TTS_MESSAGES = 1 << 12
# Allows for deletion of other users messages
MANAGE_MESSAGES = 1 << 13
# Links sent by users with this permission will be auto-embedded
EMBED_LINKS = 1 << 14
# Allows for uploading images and files
ATTACH_FILES = 1 << 15
# Allows for reading of message history
READ_MESSAGE_HISTORY = 1 << 16
# Allows for using the @everyone tag to notify all users in a channel,
# and the @here tag to notify all online users in a channel
MENTION_EVERYONE = 1 << 17
# Allows the usage of custom emojis from other servers
USE_EXTERNAL_EMOJIS = 1 << 18
# Allows for viewing guild insights
VIEW_GUILD_INSIGHTS = 1 << 19
# Allows for joining of a voice channel
CONNECT = 1 << 20
# Allows for speaking in a voice channel
SPEAK = 1 << 21
# Allows for muting members in a voice channel
MUTE_MEMBERS = 1 << 22
# Allows for deafening of members in a voice channel
DEAFEN_MEMBERS = 1 << 23
# Allows for moving of members between voice channels
MOVE_MEMBERS = 1 << 24
# Allows for using voice-activity-detection in a voice channel
USE_VAD = 1 << 25
# Allows for modification of own nickname
CHANGE_NICKNAME = 1 << 26
# Allows for modification of other users nicknames
MANAGE_NICKNAMES = 1 << 27
# Allows management and editing of roles
MANAGE_ROLES = 1 << 28
# Allows management and editing of webhooks
MANAGE_WEBHOOKS = 1 << 29
# Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users
MANAGE_GUILD_EXPRESSIONS = 1 << 30
# Allows members to use application commands, including slash commands and context menu commands.
USE_APPLICATION_COMMANDS = 1 << 31
# Allows for requesting to speak in stage channels. (This permission is under active development
# and may be changed or removed.)
REQUEST_TO_SPEAK = 1 << 32
# Allows for editing and deleting scheduled events created by all users
MANAGE_EVENTS = 1 << 33
# Allows for deleting and archiving threads, and viewing all private threads
MANAGE_THREADS = 1 << 34
# Allows for creating public and announcement threads
CREATE_PUBLIC_THREADS = 1 << 35
# Allows for creating private threads
CREATE_PRIVATE_THREADS = 1 << 36
# Allows the usage of custom stickers from other servers
USE_EXTERNAL_STICKERS = 1 << 37
# Allows for sending messages in threads
SEND_MESSAGES_IN_THREADS = 1 << 38
# Allows for using Activities (applications with the EMBEDDED flag)
USE_EMBEDDED_ACTIVITIES = 1 << 39
# Allows for timing out users to prevent them from sending or reacting to messages in chat and threads,
# and from speaking in voice and stage channels
MODERATE_MEMBERS = 1 << 40
# Allows for viewing role subscription insights
VIEW_CREATOR_MONETIZATION_ANALYTICS = 1 << 41
# Allows for using soundboard in a voice channel
USE_SOUNDBOARD = 1 << 42
# Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created
# by the current user. Not yet available to developers, see changelog.
CREATE_GUILD_EXPRESSIONS = 1 << 43
# Allows for creating scheduled events, and editing and deleting those created by the current user.
# Not yet available to developers, see changelog.
CREATE_EVENTS = 1 << 44
# Allows the usage of custom soundboard sounds from other servers
USE_EXTERNAL_SOUNDS = 1 << 45
# Allows sending voice messages
SEND_VOICE_MESSAGES = 1 << 46
# Allows sending polls
SEND_POLLS = 1 << 49
# Allows user-installed apps to send public responses. When disabled, users will still be allowed to use their apps
# but the responses will be ephemeral. This only applies to apps not also installed to the server.
USE_EXTERNAL_APPS = 1 << 50
# Allows pinning and unpinning messages
PIN_MESSAGES = 1 << 51
@dataclass
class Overwrite:
id: int
type: OverwriteType
allow: Permissions
deny: Permissions
@staticmethod
def from_dict(info: dict) -> Overwrite:
return Overwrite(
id=int(info['id']),
type=OverwriteType(info['type']),
allow=Permissions(int(info['allow'])),
deny=Permissions(int(info['deny'])))
@dataclass
class ChannelCategory:
id: int
guild_id: int
position: int
permission_overwrites: list[Overwrite]
name: str | None
parent_id: int | None
flags: ChannelFlags
@staticmethod
def from_dict(info: dict) -> ChannelCategory:
parent_id: str | None = info.get('parent_id')
return ChannelCategory(
id=int(info['id']),
guild_id=int(info['guild_id']),
position=int(info['position']),
permission_overwrites=[Overwrite.from_dict(o) for o in info['permission_overwrites']],
name=info.get('name'),
parent_id=int(parent_id) if parent_id is not None else None,
flags=ChannelFlags(info['flags']))
@dataclass
class TextChannel:
id: int
guild_id: int
position: int
permission_overwrites: list[Overwrite]
name: str | None
topic: str | None
nsfw: bool
last_message_id: int | None
rate_limit_per_user: int
parent_id: int | None
last_pin_timestamp: datetime | None
flags: ChannelFlags
@staticmethod
def from_dict(info: dict) -> TextChannel:
parent_id: str | None = info.get('parent_id')
last_message_id: str | None = info.get('last_message_id')
last_pin_timestamp: str | None = info.get('last_pin_timestamp')
return TextChannel(
id=int(info['id']),
guild_id=int(info['guild_id']),
position=int(info['position']),
permission_overwrites=[Overwrite.from_dict(o) for o in info['permission_overwrites']],
name=info.get('name'),
topic=info.get('topic'),
nsfw=info['nsfw'],
last_message_id=int(last_message_id) if last_message_id is not None else None,
rate_limit_per_user=int(info['rate_limit_per_user']),
parent_id=int(parent_id) if parent_id is not None else None,
last_pin_timestamp=(datetime.fromisoformat(last_pin_timestamp) if last_pin_timestamp is not None else None),
flags=ChannelFlags(info['flags']))
@dataclass
class User: # TODO : complete attributes
id: int
username: str
discriminator: str
global_name: str | None
@staticmethod
def from_dict(info: dict) -> User:
return User(
id=int(info['id']),
username=info['username'],
discriminator=info['discriminator'],
global_name=info.get('global_name'))
class AttachmentFlags(IntFlag):
IS_REMIX = 1 << 2 # this attachment has been edited using the remix feature on mobile
@dataclass
class Attachment:
id: int # attachment id
filename: str # name of file attached
title: str | None # the title of the file
description: str | None # description for the file (max 1024 characters)
content_type: str | None # the attachment's media type
size: int # size of file in bytes
url: str # source url of file
proxy_url: str # a proxied url of file
height: int | None # height of file (if image)
width: int | None # width of file (if image)
ephemeral: bool | None # whether this attachment is ephemeral
duration_secs: float | None # the duration of the audio file (currently for voice messages)
waveform: str | None # base64 encoded bytearray representing a sampled waveform (currently for voice messages)
flags: int | None # attachment flags combined as a bitfield
@staticmethod
def from_dict(info: dict) -> Attachment:
height = info.get('height')
width = info.get('width')
duraction_secs = info.get('duration_secs')
flags = info.get('flags')
return Attachment(
id=int(info['id']),
filename=info['filename'],
title=info.get('title'),
description=info.get('description'),
content_type=info.get('content_type'),
size=int(info['size']),
url=info['url'],
proxy_url=info['proxy_url'],
height=int(height) if height is not None else None,
width=int(width) if width is not None else None,
ephemeral=info.get('ephemeral'),
duration_secs=float(duraction_secs) if duraction_secs is not None else None,
waveform=info.get('waveform'),
flags=AttachmentFlags(int(flags)) if flags is not None else None,
)
class MessageType(Enum):
DEFAULT = 0
RECIPIENT_ADD = 1
RECIPIENT_REMOVE = 2
CALL = 3
CHANNEL_NAME_CHANGE = 4
CHANNEL_ICON_CHANGE = 5
CHANNEL_PINNED_MESSAGE = 6
USER_JOIN = 7
GUILD_BOOST = 8
GUILD_BOOST_TIER_1 = 9
GUILD_BOOST_TIER_2 = 10
GUILD_BOOST_TIER_3 = 11
CHANNEL_FOLLOW_ADD = 12
GUILD_DISCOVERY_DISQUALIFIED = 14
GUILD_DISCOVERY_REQUALIFIED = 15
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17
THREAD_CREATED = 18
REPLY = 19
CHAT_INPUT_COMMAND = 20
THREAD_STARTER_MESSAGE = 21
GUILD_INVITE_REMINDER = 22
CONTEXT_MENU_COMMAND = 23
AUTO_MODERATION_ACTION = 24
ROLE_SUBSCRIPTION_PURCHASE = 25
INTERACTION_PREMIUM_UPSELL = 26
STAGE_START = 27
STAGE_END = 28
STAGE_SPEAKER = 29
STAGE_TOPIC = 31
GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32
GUILD_INCIDENT_ALERT_MODE_ENABLED = 36
GUILD_INCIDENT_ALERT_MODE_DISABLED = 37
GUILD_INCIDENT_REPORT_RAID = 38
GUILD_INCIDENT_REPORT_FALSE_ALARM = 39
PURCHASE_NOTIFICATION = 44
POLL_RESULT = 46
NON_DELETABLE_MESSAGE_TYPES = [
MessageType.RECIPIENT_ADD,
MessageType.RECIPIENT_REMOVE,
MessageType.CALL,
MessageType.CHANNEL_NAME_CHANGE,
MessageType.CHANNEL_ICON_CHANGE,
MessageType.THREAD_STARTER_MESSAGE]
class MessageFlags(IntFlag):
NONE = 0
CROSSPOSTED = 1 << 0 # this message has been published to subscribed channels (via Channel Following)
IS_CROSSPOST = 1 << 1 # this message originated from a message in another channel (via Channel Following)
SUPPRESS_EMBEDS = 1 << 2 # do not include any embeds when serializing this message
SOURCE_MESSAGE_DELETED = 1 << 3 # the source message for this crosspost has been deleted (via Channel Following)
URGENT = 1 << 4 # this message came from the urgent message system
HAS_THREAD = 1 << 5 # this message has an associated thread, with the same id as the message
EPHEMERAL = 1 << 6 # this message is only visible to the user who invoked the Interaction
LOADING = 1 << 7 # this message is an Interaction Response and the bot is "thinking"
# this message failed to mention some roles and add their members to the thread
FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8
SUPPRESS_NOTIFICATIONS = 1 << 12 # this message will not trigger push and desktop notifications
IS_VOICE_MESSAGE = 1 << 13 # this message is a voice message
HAS_SNAPSHOT = 1 << 14 # this message has a snapshot (via Message Forwarding)
IS_COMPONENTS_V2 = 1 << 15 # allows you to create fully component-driven messages
class MessageReferenceType(Enum):
DEFAULT = 0 # A standard reference used by replies.
FORWARD = 1 # Reference used to point to a message at a point in time.
@dataclass
class MessageReference:
type: MessageReferenceType | None # type of reference.
message_id: int | None # id of the originating message
channel_id: int | None # id of the originating message's channel
guild_id: int | None # id of the originating message's guild
# when sending, whether to error if the referenced message doesn't exist
# instead of sending as a normal (non-reply) message, default true
fail_if_not_exists: bool | None
@staticmethod
def from_dict(info: dict) -> MessageReference:
ref_type: int | None = info.get('type')
return MessageReference(
type=MessageReferenceType(ref_type) if ref_type is not None else None,
message_id=info.get('message_id'),
channel_id=info.get('channel_id'),
guild_id=info.get('guild_id'),
fail_if_not_exists=info.get('fail_if_not_exists'))
@dataclass
class Message: # TODO : complete attributes
id: int
channel_id: int
author: User
content: str
timestamp: datetime
edited_timestamp: datetime | None
attachments: list[Attachment]
type: MessageType
flags: MessageFlags | None
message_reference: MessageReference | None
@staticmethod
def from_dict(info: dict) -> Message:
edited_timestamp: str | None = info.get('edited_timestamp')
flags: int | None = info.get('flags')
message_reference = info.get('message_reference')
return Message(
id=int(info['id']),
channel_id=int(info['channel_id']),
author=(User.from_dict(info['author']) if info.get('webhook_id') is None else User(
id=info['webhook_id'], username='webhook', discriminator='webhook', global_name=None)),
content=info['content'],
timestamp=datetime.fromisoformat(info['timestamp']),
edited_timestamp=datetime.fromisoformat(edited_timestamp) if edited_timestamp is not None else None,
attachments=[Attachment.from_dict(a) for a in info['attachments']],
type=MessageType(info['type']),
flags=MessageFlags(flags) if flags is not None else None,
message_reference=MessageReference.from_dict(message_reference) if message_reference is not None else None)
@dataclass
class RoleColors:
primary_color: int
seconday_color: int | None
tertiary_color: int | None
@staticmethod
def from_dict(info: dict) -> RoleColors:
seconday_color = info.get('secondary_color')
tertiary_color = info.get('tertiary_color')
return RoleColors(
primary_color=int(info['primary_color']),
seconday_color=int(seconday_color) if seconday_color is not None else None,
tertiary_color=int(tertiary_color) if tertiary_color is not None else None)
class RoleFlags(IntFlag):
NONE = 0
# role can be selected by members in an onboarding prompt
IN_PROMPT = 1
@dataclass
class RoleTags:
bot_id: int | None
intergration_id: int | None
premium_subscriber: bool
subcription_listing_id: int | None
available_for_purchase: bool
guild_connections: bool
@staticmethod
def from_dict(info: dict) -> RoleTags:
bot_id = info.get('bot_id')
intergration_id = info.get('intergration_id')
subcription_listing_id = info.get('subcription_listing_id')
return RoleTags(
bot_id=int(bot_id) if bot_id is not None else None,
intergration_id=int(intergration_id) if intergration_id is not None else None,
premium_subscriber='premium_subscriber' in info,
subcription_listing_id=int(subcription_listing_id) if subcription_listing_id is not None else None,
available_for_purchase='available_for_purchase' in info,
guild_connections='guild_connections' in info)
@dataclass
class Role:
id: int # role id
name: str # role name
color: int # Deprecated integer representation of hexadecimal color code
colors: RoleColors # the role's colors
hoist: bool # if this role is pinned in the user listing
icon: str | None # role icon hash
unicode_emoji: str | None # role unicode emoji
position: int # position of this role (roles with the same position are sorted by id)
permissions: Permissions # permission bit set
managed: bool # whether this role is managed by an integration
mentionable: bool # whether this role is mentionable
tags: RoleTags | None # the tags this role has
flags: int # role flags combined as a bitfield
@staticmethod
def from_dict(info: dict) -> Role:
tags = info.get('tags')
return Role(
id=int(info['id']),
name=info['name'],
color=int(info['color']),
colors=RoleColors.from_dict(info['colors']),
hoist=info['hoist'],
icon=info.get('icon'),
unicode_emoji=info.get('unicode_emoji'),
position=int(info['position']),
permissions=Permissions(int(info['permissions'])),
managed=info['managed'],
mentionable=info['mentionable'],
tags=RoleTags.from_dict(tags) if tags is not None else None,
flags=RoleFlags(int(info['flags']))
)