|
1 | 1 | import { LanguageKeys } from '#lib/i18n/languageKeys';
|
2 |
| -import { LockdownManager, SkyraSubcommand } from '#lib/structures'; |
| 2 | +import { getSupportedUserLanguageT } from '#lib/i18n/translate'; |
| 3 | +import { SkyraCommand } from '#lib/structures'; |
3 | 4 | import { PermissionLevels, type GuildMessage } from '#lib/types';
|
4 |
| -import { clearAccurateTimeout, setAccurateTimeout } from '#utils/Timers'; |
5 |
| -import { floatPromise } from '#utils/common'; |
6 |
| -import { assertNonThread, getSecurity } from '#utils/functions'; |
| 5 | +import { PermissionsBits } from '#utils/bits.js'; |
| 6 | +import { toErrorCodeResult } from '#utils/common'; |
| 7 | +import { getCodeStyle, getLogPrefix } from '#utils/functions'; |
| 8 | +import { resolveTimeSpan } from '#utils/resolvers'; |
| 9 | +import { getTag } from '#utils/util.js'; |
7 | 10 | import { ApplyOptions } from '@sapphire/decorators';
|
8 |
| -import { canSendMessages, type NonThreadGuildTextBasedChannelTypes } from '@sapphire/discord.js-utilities'; |
9 |
| -import { CommandOptionsRunTypeEnum } from '@sapphire/framework'; |
| 11 | +import { ApplicationCommandRegistry, CommandOptionsRunTypeEnum, ok } from '@sapphire/framework'; |
10 | 12 | import { send } from '@sapphire/plugin-editable-commands';
|
11 |
| -import type { TFunction } from '@sapphire/plugin-i18next'; |
12 |
| -import { PermissionFlagsBits, type Role } from 'discord.js'; |
| 13 | +import { applyLocalizedBuilder, createLocalizedChoice, type TFunction } from '@sapphire/plugin-i18next'; |
| 14 | +import { Time } from '@sapphire/time-utilities'; |
| 15 | +import { isNullish } from '@sapphire/utilities'; |
| 16 | +import { |
| 17 | + ChannelType, |
| 18 | + ChatInputCommandInteraction, |
| 19 | + MessageFlags, |
| 20 | + PermissionFlagsBits, |
| 21 | + RESTJSONErrorCodes, |
| 22 | + Role, |
| 23 | + User, |
| 24 | + channelMention, |
| 25 | + chatInputApplicationCommandMention, |
| 26 | + type CommandInteractionOption, |
| 27 | + type ThreadChannelType |
| 28 | +} from 'discord.js'; |
13 | 29 |
|
14 |
| -@ApplyOptions<SkyraSubcommand.Options>({ |
| 30 | +const Root = LanguageKeys.Commands.Lockdown; |
| 31 | + |
| 32 | +@ApplyOptions<SkyraCommand.Options>({ |
15 | 33 | aliases: ['lock', 'unlock'],
|
16 | 34 | description: LanguageKeys.Commands.Moderation.LockdownDescription,
|
17 | 35 | detailedDescription: LanguageKeys.Commands.Moderation.LockdownExtended,
|
18 | 36 | permissionLevel: PermissionLevels.Moderator,
|
19 | 37 | requiredClientPermissions: [PermissionFlagsBits.ManageChannels, PermissionFlagsBits.ManageRoles],
|
20 |
| - runIn: [CommandOptionsRunTypeEnum.GuildAny], |
21 |
| - subcommands: [ |
22 |
| - { name: 'lock', messageRun: 'lock' }, |
23 |
| - { name: 'unlock', messageRun: 'unlock' }, |
24 |
| - { name: 'auto', messageRun: 'auto', default: true } |
25 |
| - ] |
| 38 | + runIn: [CommandOptionsRunTypeEnum.GuildAny] |
26 | 39 | })
|
27 |
| -export class UserCommand extends SkyraSubcommand { |
28 |
| - public override messageRun(message: GuildMessage, args: SkyraSubcommand.Args, context: SkyraSubcommand.RunContext) { |
29 |
| - if (context.commandName === 'lock') return this.lock(message, args); |
30 |
| - if (context.commandName === 'unlock') return this.unlock(message, args); |
31 |
| - return super.messageRun(message, args, context); |
| 40 | +export class UserCommand extends SkyraCommand { |
| 41 | + public override messageRun(message: GuildMessage, args: SkyraCommand.Args) { |
| 42 | + const content = args.t(LanguageKeys.Commands.Shared.DeprecatedMessage, { |
| 43 | + command: chatInputApplicationCommandMention(this.name, this.getGlobalCommandId()) |
| 44 | + }); |
| 45 | + return send(message, { content }); |
32 | 46 | }
|
33 | 47 |
|
34 |
| - public async auto(message: GuildMessage, args: SkyraSubcommand.Args) { |
35 |
| - const role = await args.pick('roleName').catch(() => message.guild.roles.everyone); |
36 |
| - const channel = args.finished ? assertNonThread(message.channel) : await args.pick('textChannelName'); |
37 |
| - if (this.getLock(role, channel)) return this.handleUnlock(message, args, role, channel); |
| 48 | + public override chatInputRun(interaction: Interaction) { |
| 49 | + const durationRaw = interaction.options.getString('duration'); |
| 50 | + const durationResult = this.#parseDuration(durationRaw); |
| 51 | + const t = getSupportedUserLanguageT(interaction); |
| 52 | + if (durationResult?.isErr()) { |
| 53 | + const content = t(durationResult.unwrapErr(), { parameter: durationRaw! }); |
| 54 | + return interaction.reply({ content, flags: MessageFlags.Ephemeral }); |
| 55 | + } |
38 | 56 |
|
39 |
| - const duration = args.finished ? null : await args.pick('timespan', { minimum: 0 }); |
40 |
| - return this.handleLock(message, args, role, channel, duration); |
| 57 | + const duration = durationResult.unwrap(); |
| 58 | + const global = interaction.options.getBoolean('global') ?? false; |
| 59 | + const channel = |
| 60 | + interaction.options.getChannel<SupportedChannelType>('channel') ?? (global ? null : (interaction.channel as SupportedChannel)); |
| 61 | + const role = interaction.options.getRole('role') ?? interaction.guild!.roles.everyone; |
| 62 | + const action = interaction.options.getString('action', true)! as 'lock' | 'unlock'; |
| 63 | + return action === 'lock' // |
| 64 | + ? this.#lock(t, interaction.user, channel, role, duration) |
| 65 | + : this.#unlock(t, interaction.user, channel, role); |
41 | 66 | }
|
42 | 67 |
|
43 |
| - public async unlock(message: GuildMessage, args: SkyraSubcommand.Args) { |
44 |
| - const role = await args.pick('roleName').catch(() => message.guild.roles.everyone); |
45 |
| - const channel = args.finished ? assertNonThread(message.channel) : await args.pick('textChannelName'); |
46 |
| - return this.handleUnlock(message, args, role, channel); |
| 68 | + public override registerApplicationCommands(registry: ApplicationCommandRegistry) { |
| 69 | + registry.registerChatInputCommand((builder) => |
| 70 | + applyLocalizedBuilder(builder, Root.Name, Root.Description) // |
| 71 | + .addStringOption((option) => |
| 72 | + applyLocalizedBuilder(option, Root.Action) |
| 73 | + .setRequired(true) |
| 74 | + .addChoices( |
| 75 | + createLocalizedChoice(Root.ActionLock, { value: 'lock' }), |
| 76 | + createLocalizedChoice(Root.ActionUnlock, { value: 'unlock' }) |
| 77 | + ) |
| 78 | + ) |
| 79 | + .addRoleOption((option) => applyLocalizedBuilder(option, Root.Role)) |
| 80 | + .addChannelOption((option) => applyLocalizedBuilder(option, Root.Channel)) |
| 81 | + .addStringOption((option) => applyLocalizedBuilder(option, Root.Duration)) |
| 82 | + .setDefaultMemberPermissions(PermissionFlagsBits.ManageChannels | PermissionFlagsBits.ManageRoles) |
| 83 | + .setDMPermission(false) |
| 84 | + ); |
47 | 85 | }
|
48 | 86 |
|
49 |
| - public async lock(message: GuildMessage, args: SkyraSubcommand.Args) { |
50 |
| - const role = await args.pick('roleName').catch(() => message.guild.roles.everyone); |
51 |
| - const channel = args.finished ? assertNonThread(message.channel) : await args.pick('textChannelName'); |
52 |
| - const duration = args.finished ? null : await args.pick('timespan', { minimum: 0 }); |
53 |
| - return this.handleLock(message, args, role, channel, duration); |
| 87 | + #lock(t: TFunction, user: User, channel: SupportedChannel | null, role: Role, duration: number | null) { |
| 88 | + return isNullish(channel) |
| 89 | + ? this.#lockGuild(t, user, role, duration) |
| 90 | + : UserCommand.ThreadChannelTypes.includes(channel.type) |
| 91 | + ? this.#lockThread(t, user, channel as SupportedThreadChannel, duration) |
| 92 | + : this.#lockChannel(t, user, channel as SupportedNonThreadChannel, role, duration); |
54 | 93 | }
|
55 | 94 |
|
56 |
| - private async handleLock( |
57 |
| - message: GuildMessage, |
58 |
| - args: SkyraSubcommand.Args, |
59 |
| - role: Role, |
60 |
| - channel: NonThreadGuildTextBasedChannelTypes, |
61 |
| - duration: number | null |
62 |
| - ) { |
63 |
| - // If there was a lockdown, abort lock |
64 |
| - const lock = this.getLock(role, channel); |
65 |
| - if (lock !== null) { |
66 |
| - this.error(LanguageKeys.Commands.Moderation.LockdownLocked, { channel: channel.toString() }); |
| 95 | + async #lockGuild(t: TFunction, user: User, role: Role, duration: number | null) { |
| 96 | + void t; |
| 97 | + void user; |
| 98 | + void role; |
| 99 | + void duration; |
| 100 | + |
| 101 | + if (!role.permissions.has(PermissionFlagsBits.SendMessages | PermissionFlagsBits.SendMessagesInThreads)) { |
| 102 | + return t(Root.GuildLocked, { role: role.toString() }); |
67 | 103 | }
|
68 | 104 |
|
69 |
| - const allowed = this.isAllowed(role, channel); |
| 105 | + const reason = t(Root.AuditLogRequestedBy, { user: getTag(user), role: role.toString() }); |
| 106 | + const result = await toErrorCodeResult( |
| 107 | + role.setPermissions( |
| 108 | + PermissionsBits.difference(role.permissions.bitfield, PermissionFlagsBits.SendMessages | PermissionFlagsBits.SendMessagesInThreads), |
| 109 | + reason |
| 110 | + ) |
| 111 | + ); |
| 112 | + return result.match({ |
| 113 | + ok: () => this.#lockGuildOk(t, role), |
| 114 | + err: (code) => this.#lockGuildErr(t, role, code) |
| 115 | + }); |
| 116 | + } |
| 117 | + |
| 118 | + #lockGuildOk(t: TFunction, role: Role) { |
| 119 | + return t(Root.SuccessGuild, { role: role.toString() }); |
| 120 | + } |
| 121 | + |
| 122 | + #lockGuildErr(t: TFunction, role: Role, code: RESTJSONErrorCodes) { |
| 123 | + if (code === RESTJSONErrorCodes.UnknownRole) return t(Root.GuildUnknownRole, { role: role.toString() }); |
| 124 | + |
| 125 | + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Could not lock the guild ${role.id}`); |
| 126 | + return t(Root.GuildLockFailed, { role: role.toString() }); |
| 127 | + } |
| 128 | + |
| 129 | + async #lockThread(t: TFunction, user: User, channel: SupportedThreadChannel, duration: number | null) { |
| 130 | + void duration; |
| 131 | + |
| 132 | + if (channel.locked) { |
| 133 | + return t(Root.ThreadLocked, { channel: channelMention(channel.id) }); |
| 134 | + } |
70 | 135 |
|
71 |
| - // If they can send, begin locking |
72 |
| - const response = await send(message, args.t(LanguageKeys.Commands.Moderation.LockdownLocking, { channel: channel.toString() })); |
73 |
| - await channel.permissionOverwrites.edit(role, { SendMessages: false }); |
74 |
| - if (canSendMessages(message.channel)) { |
75 |
| - await response.edit(args.t(LanguageKeys.Commands.Moderation.LockdownLock, { channel: channel.toString() })).catch(() => null); |
| 136 | + if (!channel.manageable) { |
| 137 | + return t(Root.ThreadUnmanageable, { channel: channelMention(channel.id) }); |
76 | 138 | }
|
77 | 139 |
|
78 |
| - // Create the timeout |
79 |
| - const timeout = duration |
80 |
| - ? setAccurateTimeout(() => floatPromise(this.performUnlock(message, args.t, role, channel, allowed)), duration) |
81 |
| - : null; |
82 |
| - getSecurity(message.guild).lockdowns.add(role, channel, { allowed, timeout }); |
| 140 | + const reason = t(Root.AuditLogRequestedBy, { user: getTag(user), channel: channelMention(channel.id) }); |
| 141 | + const result = await toErrorCodeResult(channel.setLocked(true, reason)); |
| 142 | + return result.match({ |
| 143 | + ok: () => this.#lockThreadOk(t, channel), |
| 144 | + err: (code) => this.#lockThreadErr(t, channel, code) |
| 145 | + }); |
83 | 146 | }
|
84 | 147 |
|
85 |
| - private isAllowed(role: Role, channel: NonThreadGuildTextBasedChannelTypes): boolean | null { |
86 |
| - return channel.permissionOverwrites.cache.get(role.id)?.allow.has(PermissionFlagsBits.SendMessages, false) ?? null; |
| 148 | + #lockThreadOk(t: TFunction, channel: SupportedThreadChannel) { |
| 149 | + return t(Root.SuccessThread, { channel: channelMention(channel.id) }); |
87 | 150 | }
|
88 | 151 |
|
89 |
| - private async handleUnlock(message: GuildMessage, args: SkyraSubcommand.Args, role: Role, channel: NonThreadGuildTextBasedChannelTypes) { |
90 |
| - const entry = this.getLock(role, channel); |
91 |
| - if (entry === null) this.error(LanguageKeys.Commands.Moderation.LockdownUnlocked, { channel: channel.toString() }); |
92 |
| - if (entry.timeout) clearAccurateTimeout(entry.timeout); |
93 |
| - return this.performUnlock(message, args.t, role, channel, entry.allowed); |
| 152 | + #lockThreadErr(t: TFunction, channel: SupportedThreadChannel, code: RESTJSONErrorCodes) { |
| 153 | + if (code === RESTJSONErrorCodes.UnknownChannel) return t(Root.ThreadUnknownChannel, { channel: channelMention(channel.id) }); |
| 154 | + |
| 155 | + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Could not lock the thread ${channel.id}`); |
| 156 | + return t(Root.ThreadLockFailed, { channel: channelMention(channel.id) }); |
94 | 157 | }
|
95 | 158 |
|
96 |
| - private async performUnlock( |
97 |
| - message: GuildMessage, |
98 |
| - t: TFunction, |
99 |
| - role: Role, |
100 |
| - channel: NonThreadGuildTextBasedChannelTypes, |
101 |
| - allowed: boolean | null |
102 |
| - ) { |
103 |
| - getSecurity(channel.guild).lockdowns.remove(role, channel); |
104 |
| - |
105 |
| - const overwrites = channel.permissionOverwrites.cache.get(role.id); |
106 |
| - if (overwrites === undefined) return; |
107 |
| - |
108 |
| - // If the only permission overwrite is the denied SEND_MESSAGES, clean up the entire permission; if the permission |
109 |
| - // was denied, reset it to the default state, otherwise don't run an extra query |
110 |
| - if (overwrites.allow.bitfield === 0n && overwrites.deny.bitfield === PermissionFlagsBits.SendMessages) { |
111 |
| - await overwrites.delete(); |
112 |
| - } else if (overwrites.deny.has(PermissionFlagsBits.SendMessages)) { |
113 |
| - await overwrites.edit({ SendMessages: allowed }); |
| 159 | + async #lockChannel(t: TFunction, user: User, channel: SupportedNonThreadChannel, role: Role, duration: number | null) { |
| 160 | + void duration; |
| 161 | + |
| 162 | + if (!channel.permissionsFor(role).has(PermissionFlagsBits.SendMessages | PermissionFlagsBits.SendMessagesInThreads)) { |
| 163 | + return t(Root.ChannelLocked, { channel: channelMention(channel.id) }); |
114 | 164 | }
|
115 | 165 |
|
116 |
| - if (canSendMessages(message.channel)) { |
117 |
| - const content = t(LanguageKeys.Commands.Moderation.LockdownOpen, { channel: channel.toString() }); |
118 |
| - await send(message, content); |
| 166 | + if (!channel.manageable) { |
| 167 | + return t(Root.ChannelUnmanageable, { channel: channelMention(channel.id) }); |
119 | 168 | }
|
| 169 | + |
| 170 | + const reason = t(Root.AuditLogRequestedBy, { user: getTag(user), channel: channelMention(channel.id) }); |
| 171 | + const result = await toErrorCodeResult( |
| 172 | + channel.permissionOverwrites.edit(role, { SendMessages: false, SendMessagesInThreads: false }, { reason }) |
| 173 | + ); |
| 174 | + return result.match({ |
| 175 | + ok: () => this.#lockChannelOk(t, channel), |
| 176 | + err: (code) => this.#lockChannelErr(t, channel, code) |
| 177 | + }); |
| 178 | + } |
| 179 | + |
| 180 | + #lockChannelOk(t: TFunction, channel: SupportedNonThreadChannel) { |
| 181 | + return t(Root.SuccessChannel, { channel: channelMention(channel.id) }); |
| 182 | + } |
| 183 | + |
| 184 | + #lockChannelErr(t: TFunction, channel: SupportedNonThreadChannel, code: RESTJSONErrorCodes) { |
| 185 | + if (code === RESTJSONErrorCodes.UnknownChannel) return t(Root.ChannelUnknownChannel, { channel: channelMention(channel.id) }); |
| 186 | + |
| 187 | + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Could not lock the channel ${channel.id}`); |
| 188 | + return t(Root.ChannelLockFailed, { channel: channelMention(channel.id) }); |
120 | 189 | }
|
121 | 190 |
|
122 |
| - private getLock(role: Role, channel: NonThreadGuildTextBasedChannelTypes): LockdownManager.Entry | null { |
123 |
| - const entry = getSecurity(channel.guild).lockdowns.get(channel.id)?.get(role.id); |
124 |
| - if (entry) return entry; |
| 191 | + #unlock(t: TFunction, user: User, channel: SupportedChannel | null, role: Role) { |
| 192 | + void t; |
| 193 | + void user; |
| 194 | + void role; |
| 195 | + void channel; |
| 196 | + } |
| 197 | + |
| 198 | + // private async handleLock( |
| 199 | + // message: GuildMessage, |
| 200 | + // args: SkyraCommand.Args, |
| 201 | + // role: Role, |
| 202 | + // channel: NonThreadGuildTextBasedChannelTypes, |
| 203 | + // duration: number | null |
| 204 | + // ) { |
| 205 | + // // If there was a lockdown, abort lock |
| 206 | + // const lock = this.getLock(role, channel); |
| 207 | + // if (lock !== null) { |
| 208 | + // this.error(LanguageKeys.Commands.Moderation.LockdownLocked, { channel: channel.toString() }); |
| 209 | + // } |
| 210 | + |
| 211 | + // const allowed = this.isAllowed(role, channel); |
| 212 | + |
| 213 | + // // If they can send, begin locking |
| 214 | + // const response = await send(message, args.t(LanguageKeys.Commands.Moderation.LockdownLocking, { channel: channel.toString() })); |
| 215 | + // await channel.permissionOverwrites.edit(role, { SendMessages: false }); |
| 216 | + // if (canSendMessages(message.channel)) { |
| 217 | + // await response.edit(args.t(LanguageKeys.Commands.Moderation.LockdownLock, { channel: channel.toString() })).catch(() => null); |
| 218 | + // } |
| 219 | + |
| 220 | + // // Create the timeout |
| 221 | + // const timeout = duration |
| 222 | + // ? setAccurateTimeout(() => floatPromise(this.performUnlock(message, args.t, role, channel, allowed)), duration) |
| 223 | + // : null; |
| 224 | + // getSecurity(message.guild).lockdowns.add(role, channel, { allowed, timeout }); |
| 225 | + // } |
| 226 | + |
| 227 | + // private isAllowed(role: Role, channel: NonThreadGuildTextBasedChannelTypes): boolean | null { |
| 228 | + // return channel.permissionOverwrites.cache.get(role.id)?.allow.has(PermissionFlagsBits.SendMessages, false) ?? null; |
| 229 | + // } |
| 230 | + |
| 231 | + // private async handleUnlock(message: GuildMessage, args: SkyraCommand.Args, role: Role, channel: NonThreadGuildTextBasedChannelTypes) { |
| 232 | + // const entry = this.getLock(role, channel); |
| 233 | + // if (entry === null) this.error(LanguageKeys.Commands.Moderation.LockdownUnlocked, { channel: channel.toString() }); |
| 234 | + // if (entry.timeout) clearAccurateTimeout(entry.timeout); |
| 235 | + // return this.performUnlock(message, args.t, role, channel, entry.allowed); |
| 236 | + // } |
125 | 237 |
|
126 |
| - const permissions = channel.permissionOverwrites.cache.get(role.id)?.deny.has(PermissionFlagsBits.SendMessages); |
127 |
| - return permissions === true ? { allowed: null, timeout: null } : null; |
| 238 | + // private async performUnlock( |
| 239 | + // message: GuildMessage, |
| 240 | + // t: TFunction, |
| 241 | + // role: Role, |
| 242 | + // channel: NonThreadGuildTextBasedChannelTypes, |
| 243 | + // allowed: boolean | null |
| 244 | + // ) { |
| 245 | + // getSecurity(channel.guild).lockdowns.remove(role, channel); |
| 246 | + |
| 247 | + // const overwrites = channel.permissionOverwrites.cache.get(role.id); |
| 248 | + // if (overwrites === undefined) return; |
| 249 | + |
| 250 | + // // If the only permission overwrite is the denied SEND_MESSAGES, clean up the entire permission; if the permission |
| 251 | + // // was denied, reset it to the default state, otherwise don't run an extra query |
| 252 | + // if (overwrites.allow.bitfield === 0n && overwrites.deny.bitfield === PermissionFlagsBits.SendMessages) { |
| 253 | + // await overwrites.delete(); |
| 254 | + // } else if (overwrites.deny.has(PermissionFlagsBits.SendMessages)) { |
| 255 | + // await overwrites.edit({ SendMessages: allowed }); |
| 256 | + // } |
| 257 | + |
| 258 | + // if (canSendMessages(message.channel)) { |
| 259 | + // const content = t(LanguageKeys.Commands.Moderation.LockdownOpen, { channel: channel.toString() }); |
| 260 | + // await send(message, content); |
| 261 | + // } |
| 262 | + // } |
| 263 | + |
| 264 | + // private getLock(role: Role, channel: NonThreadGuildTextBasedChannelTypes): LockdownManager.Entry | null { |
| 265 | + // const entry = getSecurity(channel.guild).lockdowns.get(channel.id)?.get(role.id); |
| 266 | + // if (entry) return entry; |
| 267 | + |
| 268 | + // const permissions = channel.permissionOverwrites.cache.get(role.id)?.deny.has(PermissionFlagsBits.SendMessages); |
| 269 | + // return permissions === true ? { allowed: null, timeout: null } : null; |
| 270 | + // } |
| 271 | + |
| 272 | + #parseDuration(value: string | null) { |
| 273 | + if (isNullish(value)) return ok(null); |
| 274 | + return resolveTimeSpan(value, { minimum: Time.Second * 30, maximum: Time.Year }); |
128 | 275 | }
|
| 276 | + |
| 277 | + private static readonly ThreadChannelTypes: ChannelType[] = [ |
| 278 | + ChannelType.AnnouncementThread, |
| 279 | + ChannelType.PublicThread, |
| 280 | + ChannelType.PrivateThread |
| 281 | + ] satisfies readonly SupportedThreadChannelType[]; |
129 | 282 | }
|
| 283 | + |
| 284 | +type Interaction = ChatInputCommandInteraction<'cached'>; |
| 285 | + |
| 286 | +type SupportedChannelType = Exclude<ChannelType, ChannelType.DM | ChannelType.GroupDM>; |
| 287 | +type SupportedThreadChannelType = Extract<SupportedChannelType, ThreadChannelType>; |
| 288 | +type SupportedChannel = Extract<NonNullable<CommandInteractionOption<'cached'>['channel']>, { type: SupportedChannelType }>; |
| 289 | +type SupportedThreadChannel = Extract<SupportedChannel, { type: SupportedThreadChannelType }>; |
| 290 | +type SupportedNonThreadChannel = Exclude<SupportedChannel, SupportedThreadChannel>; |
0 commit comments