Newer
Older
percord / src / schemas / api / channels / Channel.ts
/*
	Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
	Copyright (C) 2026 Spacebar and Spacebar Contributors

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU Affero General Public License as published
	by the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Affero General Public License for more details.

	You should have received a copy of the GNU Affero General Public License
	along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

// TODO: remove entity imports
import { Channel, Recipient, Tag, ThreadMember } from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { Snowflake } from "../../Identifiers";
import { PartialUser, PublicMember } from "../users";

export enum ChannelType {
    GUILD_TEXT = 0, // a text channel within a guild
    DM = 1, // a direct message between users
    GUILD_VOICE = 2, // a voice channel within a guild
    GROUP_DM = 3, // a direct message between multiple users
    GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels
    GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route
    GUILD_STORE = 6, // a channel in which game developers can sell their things
    GUILD_LFG = 7, // @deprecated "A channel where users can match up for various games"
    LFG_GROUP_DM = 8, // @deprecated "A private channel between multiple users for a group within an LFG channel"
    THREAD_ALPHA = 9, // @deprecated "The first iteration of the threads feature, never widely used"
    GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
    GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
    GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
    GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
    GUILD_DIRECTORY = 14, // guild directory listing channel
    GUILD_FORUM = 15, // forum composed of IM threads
    GUILD_MEDIA = 16, // channel for media sharing
    LOBBY = 17, // a game lobby channel
    EPHEMERAL_DM = 18, // a private channel created by the social layer sdk
    UNHANDLED = 255, // unhandled unowned pass-through channel type
}

export interface ChannelPermissionOverwrite {
    allow: string;
    deny: string;
    id: string;
    type: ChannelPermissionOverwriteType;
}

export enum ChannelPermissionOverwriteType {
    role = 0,
    member = 1,
    group = 2,
}
export interface ThreadMetadata {
    archived: boolean;
    auto_archive_duration: number;
    archive_timestamp: string;
    locked: boolean;
    invitable?: boolean;
    create_timestamp: string; //Discord docs say this is optional, but it's only for after a certain date so it's not
}

export interface DMChannel extends Omit<Channel, "type" | "recipients"> {
    type: ChannelType.DM | ChannelType.GROUP_DM;
    recipients: Recipient[];
}

// TODO: probably more props
export function isTextChannel(type: ChannelType): boolean {
    switch (type) {
        case ChannelType.GUILD_STORE:
        case ChannelType.GUILD_STAGE_VOICE:
        case ChannelType.GUILD_CATEGORY:
        case ChannelType.GUILD_FORUM:
        case ChannelType.GUILD_DIRECTORY:
            throw new HTTPError("not a text channel", 400);
        case ChannelType.DM:
        case ChannelType.GROUP_DM:
        case ChannelType.GUILD_NEWS:
        case ChannelType.GUILD_VOICE:
        case ChannelType.GUILD_NEWS_THREAD:
        case ChannelType.GUILD_PUBLIC_THREAD:
        case ChannelType.GUILD_PRIVATE_THREAD:
        case ChannelType.GUILD_TEXT:
            return true;
        default:
            throw new HTTPError("unimplemented", 400);
    }
}

// TODO: split up by channel type?
export interface PublicChannel {
    id: Snowflake;
    type: ChannelType;
    guild_id?: Snowflake;
    position?: number;
    permission_overwrites?: ChannelPermissionOverwrite[];
    name?: string | null;
    topic?: string | null;
    nsfw?: boolean;
    last_message_id?: Snowflake | null;
    last_pin_timestamp?: string | null;
    bitrate?: number;
    user_limit?: number;
    rate_limit_per_user?: number;
    recipients?: PartialUser[];
    recipient_flags?: number;
    icon?: string | null;
    // nicks?: ChannelNick[]; // TODO
    managed?: boolean;
    blocked_user_warning_dismissed?: boolean;
    // safety_warnings?: SafetyWarning[]; // TODO
    application_id?: Snowflake;
    owner_id?: Snowflake;
    owner?: PublicMember | null;
    parent_id?: Snowflake | null;
    rtc_region?: string | null;
    video_quality_mode?: number;
    total_message_sent?: number;
    message_count?: number;
    member_count?: number;
    member_ids_preview?: Snowflake[];
    thread_metadata?: ThreadMetadata;
    member?: ThreadMember;
    default_auto_archive_duration?: number | null;
    default_thread_rate_limit_per_user?: number;
    permissions?: string;
    flags?: number;
    available_tags?: Tag[];
    applied_tags?: Snowflake[];
    default_reaction_emoji?: string | null; // DefaultReaction type..? this is supposed to be an object apparently
    default_forum_layout?: number;
    default_sort_order?: number | null;
    default_tag_setting?: string;
    // icon_emoji?: IconEmoji | null; // TODO
    is_message_request?: boolean;
    is_message_request_timestamp?: string | null;
    is_spam?: boolean;
    theme_color?: number | null;
    status?: string | null;
    hd_streaming_until?: string | null;
    hd_streaming_buyer_id?: Snowflake | null;
    // linked_lobby?: LinkedLobby | null; // TODO
    is_linkable?: boolean;
    is_viewable_and_writeable_by_all_members?: boolean;
}