Merge pull request #1 from yuzudev/main

feat: threads
This commit is contained in:
Nicolas 2022-07-03 16:52:13 -03:00 committed by GitHub
commit 4ae53b821c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 367 additions and 34 deletions

View File

@ -8,6 +8,10 @@ import type {
DiscordMemberWithUser,
DiscordMessage,
DiscordMessageDelete,
DiscordMessageReactionAdd,
DiscordMessageReactionRemove,
DiscordMessageReactionRemoveAll,
DiscordMessageReactionRemoveEmoji,
DiscordReady,
// DiscordThreadMemberUpdate,
// DiscordThreadMembersUpdate,
@ -19,6 +23,7 @@ import type { Channel } from "../structures/channels/ChannelFactory.ts";
import ChannelFactory from "../structures/channels/ChannelFactory.ts";
import GuildChannel from "../structures/channels/GuildChannel.ts";
import ThreadChannel from "../structures/channels/ThreadChannel.ts";
import ThreadMember from "../structures/ThreadMember.ts";
import Member from "../structures/Member.ts";
import Message from "../structures/Message.ts";
import User from "../structures/User.ts";
@ -103,10 +108,7 @@ export const THREAD_LIST_SYNC: RawHandler<DiscordThreadListSync> = (session, _sh
guildId: payload.guild_id,
channelIds: payload.channel_ids ?? [],
threads: payload.threads.map((channel) => new ThreadChannel(session, channel, payload.guild_id)),
members: payload.members.map((member) =>
// @ts-ignore: TODO: thread member structure
new Member(session, member as DiscordMemberWithUser, payload.guild_id)
),
members: payload.members.map((member) => new ThreadMember(session, member)),
});
};
@ -118,6 +120,24 @@ export const CHANNEL_PINS_UPDATE: RawHandler<DiscordChannelPinsUpdate> = (sessio
});
};
/*
export const MESSAGE_REACTION_ADD: RawHandler<DiscordMessageReactionAdd> = (session, _shardId, reaction) => {
session.emit("messageReactionAdd", null);
};
export const MESSAGE_REACTION_REMOVE: RawHandler<DiscordMessageReactionRemove> = (session, _shardId, reaction) => {
session.emit("messageReactionRemove", null);
};
export const MESSAGE_REACTION_REMOVE_ALL: RawHandler<DiscordMessageReactionRemoveAll> = (session, _shardId, reaction) => {
session.emit("messageReactionRemoveAll", null);
};
export const MESSAGE_REACTION_REMOVE_EMOJI: RawHandler<DiscordMessageReactionRemoveEmoji> = (session, _shardId, reaction) => {
session.emit("messageReactionRemoveEmoji", null);
};
*/
export const raw: RawHandler<unknown> = (session, shardId, data) => {
session.emit("raw", data, shardId);
};
@ -126,23 +146,30 @@ export interface Ready extends Omit<DiscordReady, "user"> {
user: User;
}
// TODO: add partial reactions or something
type MessageReaction = any;
// deno-fmt-ignore-file
export interface Events {
"ready": Handler<[Ready, number]>;
"messageCreate": Handler<[Message]>;
"messageUpdate": Handler<[Message]>;
"messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>;
"guildMemberAdd": Handler<[Member]>;
"guildMemberUpdate": Handler<[Member]>;
"guildMemberRemove": Handler<[User, Snowflake]>;
"channelCreate": Handler<[Channel]>;
"channelUpdate": Handler<[Channel]>;
"channelDelete": Handler<[GuildChannel]>;
"channelPinsUpdate": Handler<[{ guildId?: Snowflake, channelId: Snowflake, lastPinTimestamp?: number }]>
"threadCreate": Handler<[ThreadChannel]>;
"threadUpdate": Handler<[ThreadChannel]>;
"threadDelete": Handler<[ThreadChannel]>;
"threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: Member[] }]>
"interactionCreate": Handler<[Interaction]>;
"raw": Handler<[unknown, number]>;
"ready": Handler<[Ready, number]>;
"messageCreate": Handler<[Message]>;
"messageUpdate": Handler<[Message]>;
"messageDelete": Handler<[{ id: Snowflake, channelId: Snowflake, guildId?: Snowflake }]>;
"messageReactionAdd": Handler<[MessageReaction]>;
"messageReactionRemove": Handler<[MessageReaction]>;
"messageReactionRemoveAll": Handler<[MessageReaction]>;
"messageReactionRemoveEmoji": Handler<[MessageReaction]>;
"guildMemberAdd": Handler<[Member]>;
"guildMemberUpdate": Handler<[Member]>;
"guildMemberRemove": Handler<[User, Snowflake]>;
"channelCreate": Handler<[Channel]>;
"channelUpdate": Handler<[Channel]>;
"channelDelete": Handler<[GuildChannel]>;
"channelPinsUpdate": Handler<[{ guildId?: Snowflake, channelId: Snowflake, lastPinTimestamp?: number }]>
"threadCreate": Handler<[ThreadChannel]>;
"threadUpdate": Handler<[ThreadChannel]>;
"threadDelete": Handler<[ThreadChannel]>;
"threadListSync": Handler<[{ guildId: Snowflake, channelIds: Snowflake[], threads: ThreadChannel[], members: ThreadMember[] }]>
"interactionCreate": Handler<[Interaction]>;
"raw": Handler<[unknown, number]>;
}

View File

@ -1,5 +1,4 @@
import type { Model } from "./Base.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { Session } from "../session/Session.ts";
import type {
AllowedMentionsTypes,
@ -7,15 +6,20 @@ import type {
DiscordMessage,
DiscordUser,
FileContent,
MessageTypes,
MessageActivityTypes,
} from "../vendor/external.ts";
import type { Component } from "./components/Component.ts";
import type { GetReactions } from "../util/Routes.ts";
import { MessageFlags } from "../util/shared/flags.ts";
import { iconHashToBigInt } from "../util/hash.ts";
import { Snowflake } from "../util/Snowflake.ts";
import User from "./User.ts";
import Member from "./Member.ts";
import Attachment from "./Attachment.ts";
import ComponentFactory from "./components/ComponentFactory.ts";
import { iconHashToBigInt } from "../util/hash.ts";
import MessageReaction from "./MessageReaction.ts";
import ThreadChannel from "./channels/ThreadChannel.ts";
import * as Routes from "../util/Routes.ts";
/**
@ -39,11 +43,11 @@ export interface CreateMessageReference {
* @link https://discord.com/developers/docs/resources/channel#create-message-json-params
*/
export interface CreateMessage {
embeds?: DiscordEmbed[];
content?: string;
allowedMentions?: AllowedMentions;
files?: FileContent[];
messageReference?: CreateMessageReference;
embeds?: DiscordEmbed[];
}
/**
@ -74,19 +78,32 @@ export class Message implements Model {
this.session = session;
this.id = data.id;
this.type = data.type;
this.channelId = data.channel_id;
this.guildId = data.guild_id;
this.applicationId = data.application_id;
this.author = new User(session, data.author);
this.flags = data.flags;
this.pinned = !!data.pinned;
this.tts = !!data.tts;
this.content = data.content!;
this.nonce = data.nonce;
this.mentionEveryone = data.mention_everyone;
this.timestamp = Date.parse(data.timestamp);
this.editedTimestamp = data.edited_timestamp ? Date.parse(data.edited_timestamp) : undefined;
this.reactions = data.reactions?.map((react) => new MessageReaction(session, react)) ?? [];
this.attachments = data.attachments.map((attachment) => new Attachment(session, attachment));
this.embeds = data.embeds;
if (data.thread && data.guild_id) {
this.thread = new ThreadChannel(session, data.thread, data.guild_id);
}
// webhook handling
if (data.author && data.author.discriminator === "0000") {
if (data.author.discriminator === "0000") {
this.webhook = {
id: data.author.id,
username: data.author.username,
@ -96,30 +113,70 @@ export class Message implements Model {
}
// user is always null on MessageCreate and its replaced with author
if (data.guild_id && data.member && data.author && !this.isWebhookMessage()) {
if (data.guild_id && data.member && !this.isWebhookMessage()) {
this.member = new Member(session, { ...data.member, user: data.author }, data.guild_id);
}
this.components = data.components?.map((component) => ComponentFactory.from(session, component));
this.components = data.components?.map((component) => ComponentFactory.from(session, component)) ?? [];
if (data.activity) {
this.activity = {
partyId: data.activity.party_id,
type: data.activity.type,
};
}
}
readonly session: Session;
readonly id: Snowflake;
type: MessageTypes;
channelId: Snowflake;
guildId?: Snowflake;
applicationId?: Snowflake;
author: User;
flags?: MessageFlags;
pinned: boolean;
tts: boolean;
content: string;
nonce?: string | number;
mentionEveryone: boolean;
timestamp: number;
editedTimestamp?: number;
reactions: MessageReaction[];
attachments: Attachment[];
embeds: DiscordEmbed[];
member?: Member;
components?: Component[];
thread?: ThreadChannel;
components: Component[];
webhook?: WebhookAuthor;
activity?: {
partyId?: Snowflake;
type: MessageActivityTypes;
};
get createdTimestamp() {
return Snowflake.snowflakeToTimestamp(this.id);
}
get createdAt() {
return new Date(this.createdTimestamp);
}
get sentAt() {
return new Date(this.timestamp);
}
get editedAt() {
return this.editedTimestamp ? new Date(this.editedTimestamp) : undefined;
}
get edited() {
return this.editedTimestamp;
}
get url() {
return `https://discord.com/channels/${this.guildId ?? "@me"}/${this.channelId}/${this.id}`;
@ -301,12 +358,13 @@ export class Message implements Model {
return this.crosspost;
}
inGuild(): this is { guildId: Snowflake } & Message {
/** wheter the message comes from a guild **/
inGuild(): this is Message & { guildId: Snowflake } {
return !!this.guildId;
}
/** isWebhookMessage if the messages comes from a Webhook */
isWebhookMessage(): this is User & { author: Partial<User>; webhook: WebhookAuthor; member: undefined } {
/** wheter the messages comes from a Webhook */
isWebhookMessage(): this is Message & { author: Partial<User>; webhook: WebhookAuthor; member: undefined } {
return !!this.webhook;
}
}

View File

@ -0,0 +1,23 @@
import type { Session } from "../session/Session.ts";
import type { DiscordReaction } from "../vendor/external.ts";
import Emoji from "./Emoji.ts";
/**
* Represents a reaction
* @link https://discord.com/developers/docs/resources/channel#reaction-object
* */
export class MessageReaction {
constructor(session: Session, data: DiscordReaction) {
this.session = session;
this.me = data.me;
this.count = data.count;
this.emoji = new Emoji(session, data.emoji);
}
readonly session: Session;
me: boolean;
count: number;
emoji: Emoji;
}
export default MessageReaction;

View File

@ -0,0 +1,47 @@
import type { Model } from "./Base.ts";
import type { Session } from "../session/Session.ts";
import type { Snowflake } from "../util/Snowflake.ts";
import type { DiscordThreadMember } from "../vendor/external.ts";
import * as Routes from "../util/Routes.ts";
/**
* A member that comes from a thread
* @link https://discord.com/developers/docs/resources/channel#thread-member-object
* **/
export class ThreadMember implements Model {
constructor(session: Session, data: DiscordThreadMember) {
this.session = session;
this.id = data.id;
this.flags = data.flags;
this.timestamp = Date.parse(data.join_timestamp);
}
readonly session: Session;
readonly id: Snowflake;
flags: number;
timestamp: number;
get threadId() {
return this.id;
}
async quitThread(memberId: Snowflake = this.session.botId) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.THREAD_USER(this.id, memberId),
);
}
async fetchMember(memberId: Snowflake = this.session.botId) {
const member = await this.session.rest.runMethod<DiscordThreadMember>(
this.session.rest,
"GET",
Routes.THREAD_USER(this.id, memberId),
);
return new ThreadMember(this.session, member);
}
}
export default ThreadMember;

View File

@ -1,8 +1,11 @@
import type { Model } from "../Base.ts";
import type { Snowflake } from "../../util/Snowflake.ts";
import type { Session } from "../../session/Session.ts";
import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata } from "../../vendor/external.ts";
import type { ChannelTypes, DiscordChannel, DiscordInviteMetadata, DiscordListArchivedThreads } from "../../vendor/external.ts";
import type { ListArchivedThreads } from "../../util/Routes.ts";
import BaseChannel from "./BaseChannel.ts";
import ThreadChannel from "./ThreadChannel.ts";
import ThreadMember from "../ThreadMember.ts";
import Invite from "../Invite.ts";
import * as Routes from "../../util/Routes.ts";
@ -44,7 +47,41 @@ export class GuildChannel extends BaseChannel implements Model {
return invites.map((invite) => new Invite(this.session, invite));
}
async getArchivedThreads(options: ListArchivedThreads & { type: "public" | "private" | "privateJoinedThreads" }) {
let func: (channelId: Snowflake, options: ListArchivedThreads) => string;
switch (options.type) {
case "public":
func = Routes.THREAD_ARCHIVED_PUBLIC;
break;
case "private":
func = Routes.THREAD_START_PRIVATE;
break;
case "privateJoinedThreads":
func = Routes.THREAD_ARCHIVED_PRIVATE_JOINED;
break;
}
const { threads, members, has_more } = await this.session.rest.runMethod<DiscordListArchivedThreads>(
this.session.rest,
"GET",
func(this.id, options)
);
return {
threads: Object.fromEntries(
threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]),
) as Record<Snowflake, ThreadChannel>,
members: Object.fromEntries(
members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]),
) as Record<Snowflake, ThreadMember>,
hasMore: has_more,
};
}
/*
* TODO: should be in TextChannel
async createThread(options: ThreadCreateOptions): Promise<ThreadChannel> {
const thread = await this.session.rest.runMethod<DiscordChannel>(
this.session.rest,

View File

@ -1,9 +1,11 @@
import type { Model } from "../Base.ts";
import type { Snowflake } from "../../util/Snowflake.ts";
import type { Session } from "../../session/Session.ts";
import type { ChannelTypes, DiscordChannel } from "../../vendor/external.ts";
import type { ChannelTypes, DiscordChannel, DiscordThreadMember } from "../../vendor/external.ts";
import GuildChannel from "./GuildChannel.ts";
import TextChannel from "./TextChannel.ts";
import ThreadMember from "../ThreadMember.ts";
import * as Routes from "../../util/Routes.ts";
export class ThreadChannel extends GuildChannel implements Model {
constructor(session: Session, data: DiscordChannel, guildId: Snowflake) {
@ -16,6 +18,10 @@ export class ThreadChannel extends GuildChannel implements Model {
this.messageCount = data.message_count;
this.memberCount = data.member_count;
this.ownerId = data.owner_id;
if (data.member) {
this.member = new ThreadMember(session, data.member);
}
}
override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread;
@ -25,7 +31,50 @@ export class ThreadChannel extends GuildChannel implements Model {
locked?: boolean;
messageCount?: number;
memberCount?: number;
member?: ThreadMember;
ownerId?: Snowflake;
async joinThread() {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.THREAD_ME(this.id),
);
}
async addToThread(guildMemberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"PUT",
Routes.THREAD_USER(this.id, guildMemberId),
);
}
async leaveToThread(guildMemberId: Snowflake) {
await this.session.rest.runMethod<undefined>(
this.session.rest,
"DELETE",
Routes.THREAD_USER(this.id, guildMemberId),
);
}
removeMember(memberId: Snowflake = this.session.botId) {
return ThreadMember.prototype.quitThread.call({ id: this.id, session: this.session }, memberId);
}
fetchMember(memberId: Snowflake = this.session.botId) {
return ThreadMember.prototype.fetchMember.call({ id: this.id, session: this.session }, memberId);
}
async fetchMembers(): Promise<ThreadMember[]> {
const members = await this.session.rest.runMethod<DiscordThreadMember[]>(
this.session.rest,
"GET",
Routes.THREAD_MEMBERS(this.id),
);
return members.map((threadMember) => new ThreadMember(this.session, threadMember));
}
}
TextChannel.applyTo(ThreadChannel);

View File

@ -7,6 +7,8 @@ import type {
DiscordInviteMetadata,
DiscordMemberWithUser,
DiscordRole,
DiscordListActiveThreads,
DiscordListArchivedThreads,
} from "../../vendor/external.ts";
import type { GetInvite } from "../../util/Routes.ts";
import {
@ -21,6 +23,8 @@ import BaseGuild from "./BaseGuild.ts";
import Role from "../Role.ts";
import GuildEmoji from "../GuildEmoji.ts";
import Invite from "../Invite.ts";
import ThreadMember from "../ThreadMember.ts";
import ThreadChannel from "../channels/ThreadChannel.ts";
import * as Routes from "../../util/Routes.ts";
export interface CreateRole {
@ -362,6 +366,23 @@ export class Guild extends BaseGuild implements Model {
return result.pruned;
}
async getActiveThreads() {
const { threads, members } = await this.session.rest.runMethod<DiscordListActiveThreads>(
this.session.rest,
"GET",
Routes.THREAD_ACTIVE(this.id)
);
return {
threads: Object.fromEntries(
threads.map((thread) => [thread.id, new ThreadChannel(this.session, thread, this.id)]),
) as Record<Snowflake, ThreadChannel>,
members: Object.fromEntries(
members.map((threadMember) => [threadMember.id, new ThreadMember(this.session, threadMember)]),
) as Record<Snowflake, ThreadMember>,
};
}
}
export default Guild;

View File

@ -228,3 +228,74 @@ export function GUILD_MEMBER_ROLE(guildId: Snowflake, memberId: Snowflake, roleI
export function CHANNEL_WEBHOOKS(channelId: Snowflake) {
return `/channels/${channelId}/webhooks`;
}
export function THREAD_START_PUBLIC(channelId: Snowflake, messageId: Snowflake) {
return `/channels/${channelId}/messages/${messageId}/threads`;
}
export function THREAD_START_PRIVATE(channelId: Snowflake) {
return `/channels/${channelId}/threads`;
}
export function THREAD_ACTIVE(guildId: Snowflake) {
return `/guilds/${guildId}/threads/active`;
}
export interface ListArchivedThreads {
before?: number;
limit?: number;
}
export function THREAD_ME(channelId: Snowflake) {
return `/channels/${channelId}/thread-members/@me`;
}
export function THREAD_MEMBERS(channelId: Snowflake) {
return `/channels/${channelId}/thread-members`;
}
export function THREAD_USER(channelId: Snowflake, userId: Snowflake) {
return `/channels/${channelId}/thread-members/${userId}`;
}
export function THREAD_ARCHIVED(channelId: Snowflake) {
return `/channels/${channelId}/threads/archived`;
}
export function THREAD_ARCHIVED_PUBLIC(channelId: Snowflake, options?: ListArchivedThreads) {
let url = `/channels/${channelId}/threads/archived/public?`;
if (options) {
if (options.before) url += `before=${new Date(options.before).toISOString()}`;
if (options.limit) url += `&limit=${options.limit}`;
}
return url;
}
export function THREAD_ARCHIVED_PRIVATE(channelId: Snowflake, options?: ListArchivedThreads) {
let url = `/channels/${channelId}/threads/archived/private?`;
if (options) {
if (options.before) url += `before=${new Date(options.before).toISOString()}`;
if (options.limit) url += `&limit=${options.limit}`;
}
return url;
}
export function THREAD_ARCHIVED_PRIVATE_JOINED(channelId: Snowflake, options?: ListArchivedThreads) {
let url = `/channels/${channelId}/users/@me/threads/archived/private?`;
if (options) {
if (options.before) url += `before=${new Date(options.before).toISOString()}`;
if (options.limit) url += `&limit=${options.limit}`;
}
return url;
}
export function FORUM_START(channelId: Snowflake) {
return `/channels/${channelId}/threads?has_message=true`;
}