@notidotbot/noti-api-client 1.9.73 → 1.9.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classes/@me.d.ts +45 -0
- package/dist/classes/@me.js +25 -0
- package/dist/classes/guildInGame.d.ts +259 -0
- package/dist/classes/guildInGame.js +114 -0
- package/dist/classes/guildInfluencerPanel.d.ts +13 -0
- package/dist/classes/guildMessageScheduler.d.ts +15 -1
- package/dist/classes/publicGiveaways.d.ts +1 -1
- package/dist/classes/publicGiveaways.js +1 -1
- package/dist/core/manager.d.ts +2 -0
- package/dist/core/manager.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -1
- package/dist/other/bitfields.d.ts +8 -2
- package/dist/other/bitfields.js +6 -0
- package/dist/other/features.d.ts +1 -1
- package/dist/other/features.js +2 -0
- package/dist/other/inGameAlerts.d.ts +83 -0
- package/dist/other/inGameAlerts.js +65 -0
- package/dist/other/inGameMiniMessage.d.ts +14 -0
- package/dist/other/inGameMiniMessage.js +135 -0
- package/dist/other/multiDestination.d.ts +89 -0
- package/dist/other/multiDestination.js +109 -0
- package/dist/other/types.d.ts +2 -0
- package/dist/other/zod/clubStreamer.zod.d.ts +12 -0
- package/dist/other/zod/clubStreamer.zod.js +10 -0
- package/dist/other/zod/giveaways.zod.d.ts +17 -0
- package/dist/other/zod/giveaways.zod.js +8 -0
- package/dist/other/zod/kickStreamer.zod.d.ts +23 -0
- package/dist/other/zod/kickStreamer.zod.js +11 -0
- package/dist/other/zod/rumbleStreamer.zod.d.ts +25 -0
- package/dist/other/zod/rumbleStreamer.zod.js +11 -0
- package/dist/other/zod/scheduledMessage.zod.d.ts +10 -2
- package/dist/other/zod/scheduledMessage.zod.js +29 -5
- package/dist/other/zod/schema.zod.d.ts +6 -0
- package/dist/other/zod/schema.zod.js +7 -1
- package/dist/other/zod/tiktokStreamer.zod.d.ts +25 -0
- package/dist/other/zod/tiktokStreamer.zod.js +11 -0
- package/dist/other/zod/twitchStreamer.zod.d.ts +23 -0
- package/dist/other/zod/twitchStreamer.zod.js +11 -0
- package/dist/other/zod/twitterStreamer.zod.d.ts +25 -0
- package/dist/other/zod/twitterStreamer.zod.js +11 -0
- package/dist/other/zod/youtubeStreamer.zod.d.ts +26 -0
- package/dist/other/zod/youtubeStreamer.zod.js +11 -0
- package/dist/other/zod/zod.d.ts +15 -0
- package/dist/other/zod/zod.js +12 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/generated/ts-prisma.d.ts +129 -0
- package/prisma/schema/models/clubStreamer.prisma +9 -0
- package/prisma/schema/models/giveaways.prisma +9 -0
- package/prisma/schema/models/inGame.prisma +77 -0
- package/prisma/schema/models/kickStreamer.prisma +11 -0
- package/prisma/schema/models/rumbleStreamer.prisma +12 -0
- package/prisma/schema/models/scheduledMessage.prisma +10 -1
- package/prisma/schema/models/tiktokStreamer.prisma +11 -0
- package/prisma/schema/models/twitchStreamer.prisma +12 -0
- package/prisma/schema/models/twitterStreamer.prisma +12 -0
- package/prisma/schema/models/youtubeStreamer.prisma +11 -0
- package/prisma/schema/schema.prisma +16 -0
|
@@ -129,6 +129,8 @@ export declare const GuildFeatures: {
|
|
|
129
129
|
StatusRoleKeepOnOffline: bigint;
|
|
130
130
|
YouTubeReplayChannel: bigint;
|
|
131
131
|
LivePostUpdates: bigint;
|
|
132
|
+
MultiDestination: bigint;
|
|
133
|
+
InGameRewards: bigint;
|
|
132
134
|
};
|
|
133
135
|
export declare const GuildFeaturesText: Record<keyof typeof GuildFeatures, string>;
|
|
134
136
|
export declare const UserFeatures: {
|
|
@@ -245,8 +247,10 @@ export declare class GuildFeaturesManager extends BitFieldManager<typeof GuildFe
|
|
|
245
247
|
StatusRoleKeepOnOffline: bigint;
|
|
246
248
|
YouTubeReplayChannel: bigint;
|
|
247
249
|
LivePostUpdates: bigint;
|
|
250
|
+
MultiDestination: bigint;
|
|
251
|
+
InGameRewards: bigint;
|
|
248
252
|
};
|
|
249
|
-
t: Record<"StickyMessages" | "LinkedRoles" | "StatusRoles" | "Starboards" | "KickClips" | "StatCounters" | "BaseSyncableRoles" | "FullSyncableRoles" | "LiveRole" | "VoteSkipping" | "KickAuditLogs" | "LeaderboardCommand" | "RefreshingLeaderboard" | "LeaderBoardSyncRoles" | "BetaAccess" | "PriorityNotifications" | "KickNotifications" | "TwitchNotifications" | "ImportEmojis" | "ImportEmojisWithSub" | "CustomWebhooks" | "AnyMessageEditor" | "SyncSocialUsernames" | "AutoDeleteNotifications" | "TestNotificationCommand" | "CustomCooldowns" | "WelcomeAndLeaveNotifications" | "NotificationPingRoleId" | "SwitchStreamerAndGuildEmbeds" | "BirthdayNotifications" | "AllowNotSendingOfflineNotifications" | "AutoStatusLiveRoles" | "RumbleNotifications" | "AllowAutoPublishNotifications" | "NotificationOverrides" | "UseCustomButtons" | "AllowVodFeatures" | "TikTokNotifications" | "ManageBranding" | "CustomStarboards" | "AutoThreadOnStarboard" | "StarboardRewardRole" | "Giveaways" | "GiveawayLogs" | "ModifyGiveawayWhileRunning" | "ScheduleGiveaways" | "GiveawayRequirements" | "GiveawayBonusEntries" | "GiveawayEntryLimit" | "DisableDoubleVoteEntry" | "YouTubeNotifications" | "RandomMessages" | "WeeklyBirthdayPanel" | "GameNotifications" | "GameCategoryRouting" | "GameAllTimeLowAlerts" | "GameChangelogs" | "GameRobloxTracking" | "FixUrlWebhook" | "FixUrlCustomRules" | "KickClipperSubscribe" | "WhatnotNotifications" | "ScheduledEventMessages" | "ScheduledMessageWebhooks" | "ReactionRoles" | "AdvancedRolePanels" | "KickClipForwarding" | "TwitchClipForwarding" | "ClubNotifications" | "RolePanelWebhooks" | "Fishing" | "FishingCustomization" | "FishingWebhooks" | "StreamRoles" | "RoleFilters" | "TwitterNotifications" | "TwitterLiveNotifications" | "BadgeAllGames" | "AutoRoles" | "AutoRoleTimers" | "AutoRoleInvites" | "StickyMessageRoleFilters" | "GiveawayClaims" | "GiveawayWinnerRole" | "InfluencerPanel" | "InfluencerPanelPremium" | "InfluencerPanelPlus" | "StatusRoleKeepOnOffline" | "YouTubeReplayChannel" | "LivePostUpdates", string>;
|
|
253
|
+
t: Record<"StickyMessages" | "LinkedRoles" | "StatusRoles" | "Starboards" | "KickClips" | "StatCounters" | "BaseSyncableRoles" | "FullSyncableRoles" | "LiveRole" | "VoteSkipping" | "KickAuditLogs" | "LeaderboardCommand" | "RefreshingLeaderboard" | "LeaderBoardSyncRoles" | "BetaAccess" | "PriorityNotifications" | "KickNotifications" | "TwitchNotifications" | "ImportEmojis" | "ImportEmojisWithSub" | "CustomWebhooks" | "AnyMessageEditor" | "SyncSocialUsernames" | "AutoDeleteNotifications" | "TestNotificationCommand" | "CustomCooldowns" | "WelcomeAndLeaveNotifications" | "NotificationPingRoleId" | "SwitchStreamerAndGuildEmbeds" | "BirthdayNotifications" | "AllowNotSendingOfflineNotifications" | "AutoStatusLiveRoles" | "RumbleNotifications" | "AllowAutoPublishNotifications" | "NotificationOverrides" | "UseCustomButtons" | "AllowVodFeatures" | "TikTokNotifications" | "ManageBranding" | "CustomStarboards" | "AutoThreadOnStarboard" | "StarboardRewardRole" | "Giveaways" | "GiveawayLogs" | "ModifyGiveawayWhileRunning" | "ScheduleGiveaways" | "GiveawayRequirements" | "GiveawayBonusEntries" | "GiveawayEntryLimit" | "DisableDoubleVoteEntry" | "YouTubeNotifications" | "RandomMessages" | "WeeklyBirthdayPanel" | "GameNotifications" | "GameCategoryRouting" | "GameAllTimeLowAlerts" | "GameChangelogs" | "GameRobloxTracking" | "FixUrlWebhook" | "FixUrlCustomRules" | "KickClipperSubscribe" | "WhatnotNotifications" | "ScheduledEventMessages" | "ScheduledMessageWebhooks" | "ReactionRoles" | "AdvancedRolePanels" | "KickClipForwarding" | "TwitchClipForwarding" | "ClubNotifications" | "RolePanelWebhooks" | "Fishing" | "FishingCustomization" | "FishingWebhooks" | "StreamRoles" | "RoleFilters" | "TwitterNotifications" | "TwitterLiveNotifications" | "BadgeAllGames" | "AutoRoles" | "AutoRoleTimers" | "AutoRoleInvites" | "StickyMessageRoleFilters" | "GiveawayClaims" | "GiveawayWinnerRole" | "InfluencerPanel" | "InfluencerPanelPremium" | "InfluencerPanelPlus" | "StatusRoleKeepOnOffline" | "YouTubeReplayChannel" | "LivePostUpdates" | "MultiDestination" | "InGameRewards", string>;
|
|
250
254
|
all: bigint;
|
|
251
255
|
static b: {
|
|
252
256
|
BaseSyncableRoles: bigint;
|
|
@@ -339,8 +343,10 @@ export declare class GuildFeaturesManager extends BitFieldManager<typeof GuildFe
|
|
|
339
343
|
StatusRoleKeepOnOffline: bigint;
|
|
340
344
|
YouTubeReplayChannel: bigint;
|
|
341
345
|
LivePostUpdates: bigint;
|
|
346
|
+
MultiDestination: bigint;
|
|
347
|
+
InGameRewards: bigint;
|
|
342
348
|
};
|
|
343
|
-
static t: Record<"StickyMessages" | "LinkedRoles" | "StatusRoles" | "Starboards" | "KickClips" | "StatCounters" | "BaseSyncableRoles" | "FullSyncableRoles" | "LiveRole" | "VoteSkipping" | "KickAuditLogs" | "LeaderboardCommand" | "RefreshingLeaderboard" | "LeaderBoardSyncRoles" | "BetaAccess" | "PriorityNotifications" | "KickNotifications" | "TwitchNotifications" | "ImportEmojis" | "ImportEmojisWithSub" | "CustomWebhooks" | "AnyMessageEditor" | "SyncSocialUsernames" | "AutoDeleteNotifications" | "TestNotificationCommand" | "CustomCooldowns" | "WelcomeAndLeaveNotifications" | "NotificationPingRoleId" | "SwitchStreamerAndGuildEmbeds" | "BirthdayNotifications" | "AllowNotSendingOfflineNotifications" | "AutoStatusLiveRoles" | "RumbleNotifications" | "AllowAutoPublishNotifications" | "NotificationOverrides" | "UseCustomButtons" | "AllowVodFeatures" | "TikTokNotifications" | "ManageBranding" | "CustomStarboards" | "AutoThreadOnStarboard" | "StarboardRewardRole" | "Giveaways" | "GiveawayLogs" | "ModifyGiveawayWhileRunning" | "ScheduleGiveaways" | "GiveawayRequirements" | "GiveawayBonusEntries" | "GiveawayEntryLimit" | "DisableDoubleVoteEntry" | "YouTubeNotifications" | "RandomMessages" | "WeeklyBirthdayPanel" | "GameNotifications" | "GameCategoryRouting" | "GameAllTimeLowAlerts" | "GameChangelogs" | "GameRobloxTracking" | "FixUrlWebhook" | "FixUrlCustomRules" | "KickClipperSubscribe" | "WhatnotNotifications" | "ScheduledEventMessages" | "ScheduledMessageWebhooks" | "ReactionRoles" | "AdvancedRolePanels" | "KickClipForwarding" | "TwitchClipForwarding" | "ClubNotifications" | "RolePanelWebhooks" | "Fishing" | "FishingCustomization" | "FishingWebhooks" | "StreamRoles" | "RoleFilters" | "TwitterNotifications" | "TwitterLiveNotifications" | "BadgeAllGames" | "AutoRoles" | "AutoRoleTimers" | "AutoRoleInvites" | "StickyMessageRoleFilters" | "GiveawayClaims" | "GiveawayWinnerRole" | "InfluencerPanel" | "InfluencerPanelPremium" | "InfluencerPanelPlus" | "StatusRoleKeepOnOffline" | "YouTubeReplayChannel" | "LivePostUpdates", string>;
|
|
349
|
+
static t: Record<"StickyMessages" | "LinkedRoles" | "StatusRoles" | "Starboards" | "KickClips" | "StatCounters" | "BaseSyncableRoles" | "FullSyncableRoles" | "LiveRole" | "VoteSkipping" | "KickAuditLogs" | "LeaderboardCommand" | "RefreshingLeaderboard" | "LeaderBoardSyncRoles" | "BetaAccess" | "PriorityNotifications" | "KickNotifications" | "TwitchNotifications" | "ImportEmojis" | "ImportEmojisWithSub" | "CustomWebhooks" | "AnyMessageEditor" | "SyncSocialUsernames" | "AutoDeleteNotifications" | "TestNotificationCommand" | "CustomCooldowns" | "WelcomeAndLeaveNotifications" | "NotificationPingRoleId" | "SwitchStreamerAndGuildEmbeds" | "BirthdayNotifications" | "AllowNotSendingOfflineNotifications" | "AutoStatusLiveRoles" | "RumbleNotifications" | "AllowAutoPublishNotifications" | "NotificationOverrides" | "UseCustomButtons" | "AllowVodFeatures" | "TikTokNotifications" | "ManageBranding" | "CustomStarboards" | "AutoThreadOnStarboard" | "StarboardRewardRole" | "Giveaways" | "GiveawayLogs" | "ModifyGiveawayWhileRunning" | "ScheduleGiveaways" | "GiveawayRequirements" | "GiveawayBonusEntries" | "GiveawayEntryLimit" | "DisableDoubleVoteEntry" | "YouTubeNotifications" | "RandomMessages" | "WeeklyBirthdayPanel" | "GameNotifications" | "GameCategoryRouting" | "GameAllTimeLowAlerts" | "GameChangelogs" | "GameRobloxTracking" | "FixUrlWebhook" | "FixUrlCustomRules" | "KickClipperSubscribe" | "WhatnotNotifications" | "ScheduledEventMessages" | "ScheduledMessageWebhooks" | "ReactionRoles" | "AdvancedRolePanels" | "KickClipForwarding" | "TwitchClipForwarding" | "ClubNotifications" | "RolePanelWebhooks" | "Fishing" | "FishingCustomization" | "FishingWebhooks" | "StreamRoles" | "RoleFilters" | "TwitterNotifications" | "TwitterLiveNotifications" | "BadgeAllGames" | "AutoRoles" | "AutoRoleTimers" | "AutoRoleInvites" | "StickyMessageRoleFilters" | "GiveawayClaims" | "GiveawayWinnerRole" | "InfluencerPanel" | "InfluencerPanelPremium" | "InfluencerPanelPlus" | "StatusRoleKeepOnOffline" | "YouTubeReplayChannel" | "LivePostUpdates" | "MultiDestination" | "InGameRewards", string>;
|
|
344
350
|
static all: bigint;
|
|
345
351
|
convertBitToField<T extends EnumValues<typeof GuildFeatures>>(bit: T): string | null;
|
|
346
352
|
}
|
package/dist/other/bitfields.js
CHANGED
|
@@ -235,6 +235,10 @@ exports.GuildFeatures = {
|
|
|
235
235
|
YouTubeReplayChannel: 1n << 89n,
|
|
236
236
|
// Live post updates: edit the live post when the streamer changes their title or category. Premium Plus.
|
|
237
237
|
LivePostUpdates: 1n << 90n,
|
|
238
|
+
// Multiple notification channels: up to 2 extra channels per creator, each with its own ping role. Premium Plus.
|
|
239
|
+
MultiDestination: 1n << 91n,
|
|
240
|
+
// Minecraft reward commands: a giveaway runs a preset command on the winner's game server. Premium.
|
|
241
|
+
InGameRewards: 1n << 92n,
|
|
238
242
|
};
|
|
239
243
|
exports.GuildFeaturesText = {
|
|
240
244
|
BaseSyncableRoles: 'Basic Syncable Roles',
|
|
@@ -329,6 +333,8 @@ exports.GuildFeaturesText = {
|
|
|
329
333
|
StatusRoleKeepOnOffline: 'Status Roles: Remove When Offline',
|
|
330
334
|
YouTubeReplayChannel: 'YouTube: Replay Channel',
|
|
331
335
|
LivePostUpdates: 'Live Post Updates',
|
|
336
|
+
MultiDestination: 'Multiple Notification Channels',
|
|
337
|
+
InGameRewards: 'Minecraft: Reward Commands',
|
|
332
338
|
};
|
|
333
339
|
exports.UserFeatures = {
|
|
334
340
|
HasOneServerPremium: 1n << 0n,
|
package/dist/other/features.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const FEATURE_NAMES: readonly ["freeGames", "gameChangelogs", "robloxNotifications", "fixLinks", "clipperNotifications", "scheduledMessages", "linkedRoles", "reactionRoles", "clipForwarding", "club", "whatnot", "fishing", "giveaways", "starboards", "syncedRoles", "birthdays", "statusRoles", "autoStatusLiveRoles", "streamRoles", "badgeNotifications", "kickBounties", "autoRoles", "giveawayClaims", "influencerPanel", "publicGiveaways", "publicDrops", "sitePolls", "siteAnnouncements", "offlineGracePeriod", "youtubeReplays", "livePostUpdates", "twitter", "voteToTest", "voteToAddSocial", "aiAssistant", "voteToUseAssistant", "assistantShareChat", "emotes", "liveRoles", "stickyMessages", "welcomeLeave", "statCounters", "voteSkipping"];
|
|
1
|
+
export declare const FEATURE_NAMES: readonly ["freeGames", "gameChangelogs", "robloxNotifications", "fixLinks", "clipperNotifications", "scheduledMessages", "linkedRoles", "reactionRoles", "clipForwarding", "club", "whatnot", "fishing", "giveaways", "starboards", "syncedRoles", "birthdays", "statusRoles", "autoStatusLiveRoles", "streamRoles", "badgeNotifications", "kickBounties", "autoRoles", "giveawayClaims", "influencerPanel", "publicGiveaways", "publicDrops", "sitePolls", "siteAnnouncements", "offlineGracePeriod", "youtubeReplays", "livePostUpdates", "multiDestination", "inGame", "twitter", "voteToTest", "voteToAddSocial", "aiAssistant", "voteToUseAssistant", "assistantShareChat", "emotes", "liveRoles", "stickyMessages", "welcomeLeave", "statCounters", "voteSkipping"];
|
|
2
2
|
export type FeatureName = (typeof FEATURE_NAMES)[number];
|
|
3
3
|
export type FeatureStatus = 'off' | 'beta' | 'public';
|
|
4
4
|
export type ResolvedFeature = 'public' | 'beta' | 'hidden';
|
package/dist/other/features.js
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which notifications can show in Minecraft, per platform (phases/in-game/ P3, TRACKER G3/G3c/G3d).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `src/modules/inGame/alertTypes.ts` in Noti-v3; the server's `sdkDrift` test diffs everything below this
|
|
5
|
+
* header against it.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately has NO imports, like `multiDestination.ts`: the zod schemas and the SDK mirror pull from here. The
|
|
8
|
+
* client's copy (`src/other/inGameAlerts.ts`) is diffed against this file by `sdkDrift`.
|
|
9
|
+
*
|
|
10
|
+
* What each platform's send paths really emit. YouTube has no offline, ever (`src/modules/manager/CLAUDE.md`); its
|
|
11
|
+
* `replay` is the "stream replay is up" post. Whatnot is not here: its creators cannot be saved (G3c). Clips,
|
|
12
|
+
* clip forwarding, drops and VODs are not in v1 - no creator-row producer.
|
|
13
|
+
*/
|
|
14
|
+
export declare const IN_GAME_TYPES: {
|
|
15
|
+
readonly kick: readonly ["live", "offline"];
|
|
16
|
+
readonly twitch: readonly ["live", "offline"];
|
|
17
|
+
readonly rumble: readonly ["live", "offline", "video"];
|
|
18
|
+
readonly tiktok: readonly ["live", "offline", "video"];
|
|
19
|
+
readonly youtube: readonly ["live", "video", "replay"];
|
|
20
|
+
readonly twitter: readonly ["live", "offline", "post"];
|
|
21
|
+
readonly club: readonly ["post"];
|
|
22
|
+
};
|
|
23
|
+
export type InGamePlatform = keyof typeof IN_GAME_TYPES;
|
|
24
|
+
export type InGameAlertType = (typeof IN_GAME_TYPES)[InGamePlatform][number];
|
|
25
|
+
/** A creator may pick at most this many connected servers to show on (plan max is 10). */
|
|
26
|
+
export declare const MAX_IN_GAME_SERVER_PICKS = 10;
|
|
27
|
+
/** What the cluster needs to fan one notification out: which connected servers. Empty = every active one. */
|
|
28
|
+
export type InGameTarget = {
|
|
29
|
+
serverIds: string[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The creator columns, as the manager reads them. REQUIRED, not optional: a manager query that forgot to select them
|
|
33
|
+
* must be a type error, not a creator who silently never shows in Minecraft.
|
|
34
|
+
*/
|
|
35
|
+
export type InGameColumns = {
|
|
36
|
+
inGame: boolean;
|
|
37
|
+
inGameTypes: string[];
|
|
38
|
+
inGameServerIds: string[];
|
|
39
|
+
};
|
|
40
|
+
/** The stored type list for a platform: only types it has, no repeats. Empty means every type it has. */
|
|
41
|
+
export declare function normalizeInGameTypes(platform: string, input: unknown): InGameAlertType[];
|
|
42
|
+
/** The stored server picks: ids only, no repeats, capped. */
|
|
43
|
+
export declare function normalizeInGameServerIds(input: unknown): string[];
|
|
44
|
+
/**
|
|
45
|
+
* The ONE decision for whether a notification may go in-game (P3 `shouldFanOut`), made on the manager when the
|
|
46
|
+
* payload is built. Null means "not in-game": a setup test, the switch off, or a type this creator did not pick.
|
|
47
|
+
*
|
|
48
|
+
* `isTest`: a live, upload or post dispatched to a single guild is always a setup test (`functions.ts`, the /test
|
|
49
|
+
* dispatch). A single-guild OFFLINE is a held offline being delivered - real - so offline callers pass false.
|
|
50
|
+
*/
|
|
51
|
+
export declare function inGameTarget(platform: string, type: InGameAlertType, row: InGameColumns, isTest: boolean): InGameTarget | null;
|
|
52
|
+
/** The creator fields only an `inGame`-enabled user may send. The creator routes 403 a body that names any of them. */
|
|
53
|
+
export declare const IN_GAME_CREATOR_FIELDS: readonly ["inGame", "inGameTypes", "inGameServerIds"];
|
|
54
|
+
export declare const bodyTouchesInGame: (body: unknown) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* One alert as it is sent to a game server (`event.payload`). Codes and raw values, never prose: the plugin words
|
|
57
|
+
* and formats everything (TRACKER G5). Untrusted text (titles, names) is inserted by the plugin as UNPARSED text, so a
|
|
58
|
+
* title cannot inject markup or commands (R8).
|
|
59
|
+
*
|
|
60
|
+
* Never carries a ping role, the random message or a Discord member - those are Discord's.
|
|
61
|
+
*/
|
|
62
|
+
export type InGameAlert = {
|
|
63
|
+
platform: InGamePlatform;
|
|
64
|
+
type: InGameAlertType;
|
|
65
|
+
/** The creator's name as the platform shows it (not Discord-escaped). */
|
|
66
|
+
streamerName: string;
|
|
67
|
+
/** Stream title, video title, or the post's text. */
|
|
68
|
+
title: string | null;
|
|
69
|
+
/** Where a click should go: the stream, the video, the post. */
|
|
70
|
+
url: string | null;
|
|
71
|
+
/** May be empty: Kick often has no thumbnail at go-live. */
|
|
72
|
+
thumbnail: string | null;
|
|
73
|
+
category?: string | null;
|
|
74
|
+
viewers?: number | null;
|
|
75
|
+
followers?: number | null;
|
|
76
|
+
/** Offline: how long the stream ran, in ms. */
|
|
77
|
+
durationMs?: number | null;
|
|
78
|
+
totalViews?: number | null;
|
|
79
|
+
/** Offline: the stream's VOD when the platform gives one. */
|
|
80
|
+
vodUrl?: string | null;
|
|
81
|
+
/** Live/offline pairing for replay: the platform's id for this stream when there is one. */
|
|
82
|
+
streamId?: string | null;
|
|
83
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.bodyTouchesInGame = exports.IN_GAME_CREATOR_FIELDS = exports.MAX_IN_GAME_SERVER_PICKS = exports.IN_GAME_TYPES = void 0;
|
|
4
|
+
exports.normalizeInGameTypes = normalizeInGameTypes;
|
|
5
|
+
exports.normalizeInGameServerIds = normalizeInGameServerIds;
|
|
6
|
+
exports.inGameTarget = inGameTarget;
|
|
7
|
+
/**
|
|
8
|
+
* Which notifications can show in Minecraft, per platform (phases/in-game/ P3, TRACKER G3/G3c/G3d).
|
|
9
|
+
*
|
|
10
|
+
* Mirrors `src/modules/inGame/alertTypes.ts` in Noti-v3; the server's `sdkDrift` test diffs everything below this
|
|
11
|
+
* header against it.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately has NO imports, like `multiDestination.ts`: the zod schemas and the SDK mirror pull from here. The
|
|
14
|
+
* client's copy (`src/other/inGameAlerts.ts`) is diffed against this file by `sdkDrift`.
|
|
15
|
+
*
|
|
16
|
+
* What each platform's send paths really emit. YouTube has no offline, ever (`src/modules/manager/CLAUDE.md`); its
|
|
17
|
+
* `replay` is the "stream replay is up" post. Whatnot is not here: its creators cannot be saved (G3c). Clips,
|
|
18
|
+
* clip forwarding, drops and VODs are not in v1 - no creator-row producer.
|
|
19
|
+
*/
|
|
20
|
+
exports.IN_GAME_TYPES = {
|
|
21
|
+
kick: ['live', 'offline'],
|
|
22
|
+
twitch: ['live', 'offline'],
|
|
23
|
+
rumble: ['live', 'offline', 'video'],
|
|
24
|
+
tiktok: ['live', 'offline', 'video'],
|
|
25
|
+
youtube: ['live', 'video', 'replay'],
|
|
26
|
+
twitter: ['live', 'offline', 'post'],
|
|
27
|
+
club: ['post'],
|
|
28
|
+
};
|
|
29
|
+
/** A creator may pick at most this many connected servers to show on (plan max is 10). */
|
|
30
|
+
exports.MAX_IN_GAME_SERVER_PICKS = 10;
|
|
31
|
+
const isPlatform = (p) => Object.prototype.hasOwnProperty.call(exports.IN_GAME_TYPES, p);
|
|
32
|
+
/** The stored type list for a platform: only types it has, no repeats. Empty means every type it has. */
|
|
33
|
+
function normalizeInGameTypes(platform, input) {
|
|
34
|
+
if (!isPlatform(platform) || !Array.isArray(input))
|
|
35
|
+
return [];
|
|
36
|
+
const allowed = exports.IN_GAME_TYPES[platform];
|
|
37
|
+
return [...new Set(input.filter((t) => typeof t === 'string' && allowed.includes(t)))];
|
|
38
|
+
}
|
|
39
|
+
/** The stored server picks: ids only, no repeats, capped. */
|
|
40
|
+
function normalizeInGameServerIds(input) {
|
|
41
|
+
if (!Array.isArray(input))
|
|
42
|
+
return [];
|
|
43
|
+
return [...new Set(input.filter((id) => typeof id === 'string' && /^[0-9a-f-]{36}$/i.test(id)))].slice(0, exports.MAX_IN_GAME_SERVER_PICKS);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The ONE decision for whether a notification may go in-game (P3 `shouldFanOut`), made on the manager when the
|
|
47
|
+
* payload is built. Null means "not in-game": a setup test, the switch off, or a type this creator did not pick.
|
|
48
|
+
*
|
|
49
|
+
* `isTest`: a live, upload or post dispatched to a single guild is always a setup test (`functions.ts`, the /test
|
|
50
|
+
* dispatch). A single-guild OFFLINE is a held offline being delivered - real - so offline callers pass false.
|
|
51
|
+
*/
|
|
52
|
+
function inGameTarget(platform, type, row, isTest) {
|
|
53
|
+
if (isTest || !row.inGame || !isPlatform(platform))
|
|
54
|
+
return null;
|
|
55
|
+
if (!exports.IN_GAME_TYPES[platform].includes(type))
|
|
56
|
+
return null;
|
|
57
|
+
const types = normalizeInGameTypes(platform, row.inGameTypes);
|
|
58
|
+
if (types.length && !types.includes(type))
|
|
59
|
+
return null;
|
|
60
|
+
return { serverIds: normalizeInGameServerIds(row.inGameServerIds) };
|
|
61
|
+
}
|
|
62
|
+
/** The creator fields only an `inGame`-enabled user may send. The creator routes 403 a body that names any of them. */
|
|
63
|
+
exports.IN_GAME_CREATOR_FIELDS = ['inGame', 'inGameTypes', 'inGameServerIds'];
|
|
64
|
+
const bodyTouchesInGame = (body) => !!body && typeof body === 'object' && exports.IN_GAME_CREATOR_FIELDS.some((key) => key in body);
|
|
65
|
+
exports.bodyTouchesInGame = bodyTouchesInGame;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MiniMessage a dashboard user may write for Minecraft (Noti in-game P5): colours, formatting, hover text and
|
|
3
|
+
* http(s) links only. `forbiddenMiniMessageTag(text)` names the first tag the API would refuse, or returns null.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `src/modules/inGame/miniMessage.ts` in Noti-v3; the server's `sdkDrift` test diffs everything below this
|
|
6
|
+
* header against it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const NAMED_COLOURS: readonly ["black", "dark_blue", "dark_green", "dark_aqua", "dark_red", "dark_purple", "gold", "gray", "grey", "dark_gray", "dark_grey", "blue", "green", "aqua", "red", "light_purple", "yellow", "white"];
|
|
9
|
+
/** Escape text so MiniMessage shows it as written: a backslash before every tag opener (and before backslashes). */
|
|
10
|
+
export declare const escapeMiniMessage: (text: string) => string;
|
|
11
|
+
/** An http(s) URL safe inside `<click:open_url:'...'>`, or null. Same rule as the plugin's `safeUrl`. */
|
|
12
|
+
export declare function safeMiniMessageUrl(url: string | null | undefined): string | null;
|
|
13
|
+
/** The first tag a dashboard user may not use, as they wrote it, or null when the text is fine. */
|
|
14
|
+
export declare const forbiddenMiniMessageTag: (text: string) => string | null;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forbiddenMiniMessageTag = exports.escapeMiniMessage = exports.NAMED_COLOURS = void 0;
|
|
4
|
+
exports.safeMiniMessageUrl = safeMiniMessageUrl;
|
|
5
|
+
/**
|
|
6
|
+
* The MiniMessage a dashboard user may write for Minecraft (Noti in-game P5): colours, formatting, hover text and
|
|
7
|
+
* http(s) links only. `forbiddenMiniMessageTag(text)` names the first tag the API would refuse, or returns null.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors `src/modules/inGame/miniMessage.ts` in Noti-v3; the server's `sdkDrift` test diffs everything below this
|
|
10
|
+
* header against it.
|
|
11
|
+
*/
|
|
12
|
+
exports.NAMED_COLOURS = [
|
|
13
|
+
'black', 'dark_blue', 'dark_green', 'dark_aqua', 'dark_red', 'dark_purple', 'gold', 'gray', 'grey', 'dark_gray', 'dark_grey',
|
|
14
|
+
'blue', 'green', 'aqua', 'red', 'light_purple', 'yellow', 'white',
|
|
15
|
+
];
|
|
16
|
+
const DECORATIONS = ['bold', 'b', 'italic', 'i', 'em', 'underlined', 'u', 'strikethrough', 'st', 'obfuscated', 'obf'];
|
|
17
|
+
const SIMPLE = new Set([...exports.NAMED_COLOURS, ...DECORATIONS, 'reset', 'newline', 'br', 'rainbow', 'gradient', 'color', 'colour', 'c']);
|
|
18
|
+
const HEX = /^#[0-9a-f]{6}$/i;
|
|
19
|
+
/** Escape text so MiniMessage shows it as written: a backslash before every tag opener (and before backslashes). */
|
|
20
|
+
const escapeMiniMessage = (text) => text.replace(/§./g, '').replace(/\\/g, '\\\\').replace(/</g, '\\<');
|
|
21
|
+
exports.escapeMiniMessage = escapeMiniMessage;
|
|
22
|
+
/** An http(s) URL safe inside `<click:open_url:'...'>`, or null. Same rule as the plugin's `safeUrl`. */
|
|
23
|
+
function safeMiniMessageUrl(url) {
|
|
24
|
+
const u = (url ?? '').trim();
|
|
25
|
+
if (!/^https?:\/\/\S+$/i.test(u))
|
|
26
|
+
return null;
|
|
27
|
+
return u.replace(/\\/g, '%5C').replace(/'/g, '%27').replace(/"/g, '%22').replace(/</g, '%3C').replace(/>/g, '%3E');
|
|
28
|
+
}
|
|
29
|
+
/** Split a tag body on `:`, keeping quoted arguments ('...' or "...") whole and unquoted. */
|
|
30
|
+
function splitArgs(body) {
|
|
31
|
+
const out = [];
|
|
32
|
+
let cur = '';
|
|
33
|
+
let quote = null;
|
|
34
|
+
for (let i = 0; i < body.length; i++) {
|
|
35
|
+
const ch = body[i];
|
|
36
|
+
if (quote) {
|
|
37
|
+
if (ch === '\\' && i + 1 < body.length) {
|
|
38
|
+
cur += body[++i];
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === quote) {
|
|
42
|
+
quote = null;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
cur += ch;
|
|
46
|
+
}
|
|
47
|
+
else if (ch === '\'' || ch === '"') {
|
|
48
|
+
quote = ch;
|
|
49
|
+
}
|
|
50
|
+
else if (ch === ':') {
|
|
51
|
+
out.push(cur);
|
|
52
|
+
cur = '';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
cur += ch;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
out.push(cur);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/** The index of the `>` closing the tag that opens at `start`, honouring quotes; -1 when it never closes. */
|
|
62
|
+
function tagEnd(text, start) {
|
|
63
|
+
let quote = null;
|
|
64
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
65
|
+
const ch = text[i];
|
|
66
|
+
if (quote) {
|
|
67
|
+
if (ch === '\\') {
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === quote)
|
|
72
|
+
quote = null;
|
|
73
|
+
}
|
|
74
|
+
else if (ch === '\'' || ch === '"') {
|
|
75
|
+
quote = ch;
|
|
76
|
+
}
|
|
77
|
+
else if (ch === '<') {
|
|
78
|
+
return -1; // a second opener first: the first `<` is plain text to MiniMessage
|
|
79
|
+
}
|
|
80
|
+
else if (ch === '>') {
|
|
81
|
+
return i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
/** Why one tag is not allowed, or null when it is. */
|
|
87
|
+
function checkTag(body, depth) {
|
|
88
|
+
const closing = body.startsWith('/');
|
|
89
|
+
const args = splitArgs(closing ? body.slice(1) : body);
|
|
90
|
+
const rawName = (args[0] ?? '').trim().toLowerCase();
|
|
91
|
+
const name = rawName.startsWith('!') ? rawName.slice(1) : rawName;
|
|
92
|
+
if (closing)
|
|
93
|
+
return name === '' || SIMPLE.has(name) || HEX.test(name) || name === 'hover' || name === 'click' ? null : `<${body}>`;
|
|
94
|
+
if (HEX.test(name))
|
|
95
|
+
return null;
|
|
96
|
+
if (rawName.startsWith('!'))
|
|
97
|
+
return DECORATIONS.includes(name) ? null : `<${body}>`;
|
|
98
|
+
if (SIMPLE.has(name))
|
|
99
|
+
return null;
|
|
100
|
+
if (name === 'click') {
|
|
101
|
+
if ((args[1] ?? '').toLowerCase() !== 'open_url')
|
|
102
|
+
return `<${body}> (only open_url links)`;
|
|
103
|
+
return safeMiniMessageUrl(args.slice(2).join(':')) ? null : `<${body}> (links must start with http:// or https://)`;
|
|
104
|
+
}
|
|
105
|
+
if (name === 'hover') {
|
|
106
|
+
if ((args[1] ?? '').toLowerCase() !== 'show_text')
|
|
107
|
+
return `<${body}> (only show_text)`;
|
|
108
|
+
if (depth > 3)
|
|
109
|
+
return `<${body}> (nested too deep)`;
|
|
110
|
+
return firstForbiddenTag(args.slice(2).join(':'), depth + 1);
|
|
111
|
+
}
|
|
112
|
+
return `<${body}>`;
|
|
113
|
+
}
|
|
114
|
+
function firstForbiddenTag(text, depth) {
|
|
115
|
+
for (let i = 0; i < text.length; i++) {
|
|
116
|
+
const ch = text[i];
|
|
117
|
+
if (ch === '\\') {
|
|
118
|
+
i++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (ch !== '<')
|
|
122
|
+
continue;
|
|
123
|
+
const end = tagEnd(text, i);
|
|
124
|
+
if (end < 0)
|
|
125
|
+
continue;
|
|
126
|
+
const problem = checkTag(text.slice(i + 1, end), depth);
|
|
127
|
+
if (problem)
|
|
128
|
+
return problem;
|
|
129
|
+
i = end;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/** The first tag a dashboard user may not use, as they wrote it, or null when the text is fine. */
|
|
134
|
+
const forbiddenMiniMessageTag = (text) => firstForbiddenTag(text, 0);
|
|
135
|
+
exports.forbiddenMiniMessageTag = forbiddenMiniMessageTag;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-destination notifications - up to two extra channels per creator, each with its own ping role.
|
|
3
|
+
* Premium Plus (`MultiDestination`), launch flag `multiDestination`. Plan: phases/multi-destination/.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `src/modules/multiDestination.ts` in Noti-v3. Hand-copied; the server's `sdkDrift` test diffs everything
|
|
6
|
+
* below this header against it.
|
|
7
|
+
*
|
|
8
|
+
* One source of truth for the rules, because they have to agree across the zod schemas, the save
|
|
9
|
+
* transforms, the send path and the SDK. Deliberately has NO imports, for the same reason as
|
|
10
|
+
* `offlineGrace.ts`: the zod schemas pull from here, and a cycle through them is someone's afternoon.
|
|
11
|
+
*
|
|
12
|
+
* The creator's own channel + `pingRoleId` is destination 1 and is not stored here. `extraDestinations`
|
|
13
|
+
* holds only the extras, so a server that never opts in has nothing new anywhere.
|
|
14
|
+
*/
|
|
15
|
+
/** Extra destinations a creator may have, on top of their own channel. 3 channels total. */
|
|
16
|
+
export declare const MAX_EXTRA_DESTINATIONS = 2;
|
|
17
|
+
/**
|
|
18
|
+
* Which post types an extra destination can be limited to, per platform. Empty `types` = everything.
|
|
19
|
+
*
|
|
20
|
+
* Offline is NOT a type: the offline path only ever edits or deletes the live post it announced, so a
|
|
21
|
+
* copy's offline follows its own live copy. Kick clips/VODs and the X post cooldown are separate
|
|
22
|
+
* features with their own channels, not notification types, so they are not here either.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DESTINATION_TYPES: {
|
|
25
|
+
readonly kick: readonly ["live"];
|
|
26
|
+
readonly twitch: readonly ["live"];
|
|
27
|
+
readonly rumble: readonly ["live", "video"];
|
|
28
|
+
readonly tiktok: readonly ["live", "video"];
|
|
29
|
+
readonly youtube: readonly ["live", "video", "replay"];
|
|
30
|
+
readonly twitter: readonly ["live", "post"];
|
|
31
|
+
readonly club: readonly ["post"];
|
|
32
|
+
};
|
|
33
|
+
export type DestinationPlatform = keyof typeof DESTINATION_TYPES;
|
|
34
|
+
export type DestinationType = (typeof DESTINATION_TYPES)[DestinationPlatform][number];
|
|
35
|
+
export type ExtraDestination = {
|
|
36
|
+
channelId: string;
|
|
37
|
+
/** A role id, `everyone`, `here`, or null for no ping - the same values the creator's own `pingRoleId` takes. */
|
|
38
|
+
pingRoleId: string | null;
|
|
39
|
+
types: DestinationType[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A copy of the live post sent to an extra destination, so the offline can edit or delete it. `pingRoleId` is
|
|
43
|
+
* the role the copy was posted with: the offline edit rewrites the content, and it must show the copy's own
|
|
44
|
+
* role, not destination 1's - and not whatever the settings say by the time the stream ends.
|
|
45
|
+
*/
|
|
46
|
+
export type ExtraPost = {
|
|
47
|
+
channelId: string;
|
|
48
|
+
messageId: string;
|
|
49
|
+
pingRoleId: string | null;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The stored form of whatever the dashboard sent. The save transforms call this, not only zod.
|
|
53
|
+
*
|
|
54
|
+
* Zod alone is not enough here: the creator routes validate with the PARTIAL schema, and `deepPartialify`
|
|
55
|
+
* keeps an array's `max` but drops custom refinements (the no-duplicates check) and makes every field
|
|
56
|
+
* of an item optional. So a row with no channel, a channel listed twice, or a type the platform does not
|
|
57
|
+
* have could all reach the database. This drops them instead of failing the whole save:
|
|
58
|
+
*
|
|
59
|
+
* - no valid `channelId` -> dropped
|
|
60
|
+
* - a channel already listed -> the later row dropped
|
|
61
|
+
* - a `pingRoleId` that is not a role / everyone / here -> no ping (null)
|
|
62
|
+
* - a type this platform does not have, or a repeat -> dropped from `types`
|
|
63
|
+
* - beyond `MAX_EXTRA_DESTINATIONS` -> cut
|
|
64
|
+
*
|
|
65
|
+
* `null` and anything that is not a list mean "no extras" and store `[]`: a Json column cannot take a
|
|
66
|
+
* plain null through Prisma, same as `Guild.statChannelFormats` storing `{}`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function normalizeExtraDestinations(platform: DestinationPlatform, input: unknown): ExtraDestination[];
|
|
69
|
+
/**
|
|
70
|
+
* The extras one notification goes to: the stored list, re-normalized (a row written before a rule changed
|
|
71
|
+
* still obeys today's rule), keeping only destinations that want this post type and whose channel is not
|
|
72
|
+
* already receiving it.
|
|
73
|
+
*
|
|
74
|
+
* `taken` is destination 1's channel AFTER any override redirect. An extra pointing at the same channel would
|
|
75
|
+
* post the notification there twice, so it is dropped here rather than rejected on save - a partial save body
|
|
76
|
+
* usually does not carry the creator's own channel, so the save could not check it.
|
|
77
|
+
*/
|
|
78
|
+
export declare function extrasFor(platform: DestinationPlatform, stored: unknown, type: DestinationType, taken: (string | null | undefined)[]): ExtraDestination[];
|
|
79
|
+
/**
|
|
80
|
+
* The stored copies of a live post (`*CurrentlyLive.extraPosts`), read safely. Written only by the bot, but it is
|
|
81
|
+
* a Json column: anything malformed is dropped rather than trusted, and a pre-`pingRoleId` row reads as no role.
|
|
82
|
+
*/
|
|
83
|
+
export declare function readExtraPosts(stored: unknown): ExtraPost[];
|
|
84
|
+
/** "Is there a live post to finish?" - destination 1's message, or any copy. Offline and sweep filters use this. */
|
|
85
|
+
export declare const hasLivePost: (live: {
|
|
86
|
+
channelId?: string | null;
|
|
87
|
+
messageId?: string | null;
|
|
88
|
+
extraPosts?: unknown;
|
|
89
|
+
} | null | undefined) => boolean;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Multi-destination notifications - up to two extra channels per creator, each with its own ping role.
|
|
4
|
+
* Premium Plus (`MultiDestination`), launch flag `multiDestination`. Plan: phases/multi-destination/.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `src/modules/multiDestination.ts` in Noti-v3. Hand-copied; the server's `sdkDrift` test diffs everything
|
|
7
|
+
* below this header against it.
|
|
8
|
+
*
|
|
9
|
+
* One source of truth for the rules, because they have to agree across the zod schemas, the save
|
|
10
|
+
* transforms, the send path and the SDK. Deliberately has NO imports, for the same reason as
|
|
11
|
+
* `offlineGrace.ts`: the zod schemas pull from here, and a cycle through them is someone's afternoon.
|
|
12
|
+
*
|
|
13
|
+
* The creator's own channel + `pingRoleId` is destination 1 and is not stored here. `extraDestinations`
|
|
14
|
+
* holds only the extras, so a server that never opts in has nothing new anywhere.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.hasLivePost = exports.DESTINATION_TYPES = exports.MAX_EXTRA_DESTINATIONS = void 0;
|
|
18
|
+
exports.normalizeExtraDestinations = normalizeExtraDestinations;
|
|
19
|
+
exports.extrasFor = extrasFor;
|
|
20
|
+
exports.readExtraPosts = readExtraPosts;
|
|
21
|
+
/** Extra destinations a creator may have, on top of their own channel. 3 channels total. */
|
|
22
|
+
exports.MAX_EXTRA_DESTINATIONS = 2;
|
|
23
|
+
/**
|
|
24
|
+
* Which post types an extra destination can be limited to, per platform. Empty `types` = everything.
|
|
25
|
+
*
|
|
26
|
+
* Offline is NOT a type: the offline path only ever edits or deletes the live post it announced, so a
|
|
27
|
+
* copy's offline follows its own live copy. Kick clips/VODs and the X post cooldown are separate
|
|
28
|
+
* features with their own channels, not notification types, so they are not here either.
|
|
29
|
+
*/
|
|
30
|
+
exports.DESTINATION_TYPES = {
|
|
31
|
+
kick: ['live'],
|
|
32
|
+
twitch: ['live'],
|
|
33
|
+
rumble: ['live', 'video'],
|
|
34
|
+
tiktok: ['live', 'video'],
|
|
35
|
+
youtube: ['live', 'video', 'replay'],
|
|
36
|
+
twitter: ['live', 'post'],
|
|
37
|
+
club: ['post'],
|
|
38
|
+
};
|
|
39
|
+
const SNOWFLAKE = /^\d{17,21}$/;
|
|
40
|
+
const isPingTarget = (v) => typeof v === 'string' && (SNOWFLAKE.test(v) || v === 'everyone' || v === 'here');
|
|
41
|
+
/**
|
|
42
|
+
* The stored form of whatever the dashboard sent. The save transforms call this, not only zod.
|
|
43
|
+
*
|
|
44
|
+
* Zod alone is not enough here: the creator routes validate with the PARTIAL schema, and `deepPartialify`
|
|
45
|
+
* keeps an array's `max` but drops custom refinements (the no-duplicates check) and makes every field
|
|
46
|
+
* of an item optional. So a row with no channel, a channel listed twice, or a type the platform does not
|
|
47
|
+
* have could all reach the database. This drops them instead of failing the whole save:
|
|
48
|
+
*
|
|
49
|
+
* - no valid `channelId` -> dropped
|
|
50
|
+
* - a channel already listed -> the later row dropped
|
|
51
|
+
* - a `pingRoleId` that is not a role / everyone / here -> no ping (null)
|
|
52
|
+
* - a type this platform does not have, or a repeat -> dropped from `types`
|
|
53
|
+
* - beyond `MAX_EXTRA_DESTINATIONS` -> cut
|
|
54
|
+
*
|
|
55
|
+
* `null` and anything that is not a list mean "no extras" and store `[]`: a Json column cannot take a
|
|
56
|
+
* plain null through Prisma, same as `Guild.statChannelFormats` storing `{}`.
|
|
57
|
+
*/
|
|
58
|
+
function normalizeExtraDestinations(platform, input) {
|
|
59
|
+
if (!Array.isArray(input))
|
|
60
|
+
return [];
|
|
61
|
+
const allowed = exports.DESTINATION_TYPES[platform];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const raw of input) {
|
|
65
|
+
if (out.length >= exports.MAX_EXTRA_DESTINATIONS)
|
|
66
|
+
break;
|
|
67
|
+
if (!raw || typeof raw !== 'object')
|
|
68
|
+
continue;
|
|
69
|
+
const row = raw;
|
|
70
|
+
if (typeof row.channelId !== 'string' || !SNOWFLAKE.test(row.channelId) || seen.has(row.channelId))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(row.channelId);
|
|
73
|
+
const types = Array.isArray(row.types)
|
|
74
|
+
? [...new Set(row.types.filter((t) => typeof t === 'string' && allowed.includes(t)))]
|
|
75
|
+
: [];
|
|
76
|
+
out.push({ channelId: row.channelId, pingRoleId: isPingTarget(row.pingRoleId) ? row.pingRoleId : null, types });
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The extras one notification goes to: the stored list, re-normalized (a row written before a rule changed
|
|
82
|
+
* still obeys today's rule), keeping only destinations that want this post type and whose channel is not
|
|
83
|
+
* already receiving it.
|
|
84
|
+
*
|
|
85
|
+
* `taken` is destination 1's channel AFTER any override redirect. An extra pointing at the same channel would
|
|
86
|
+
* post the notification there twice, so it is dropped here rather than rejected on save - a partial save body
|
|
87
|
+
* usually does not carry the creator's own channel, so the save could not check it.
|
|
88
|
+
*/
|
|
89
|
+
function extrasFor(platform, stored, type, taken) {
|
|
90
|
+
const used = new Set(taken.filter((id) => !!id));
|
|
91
|
+
return normalizeExtraDestinations(platform, stored).filter((d) => (!d.types.length || d.types.includes(type)) && !used.has(d.channelId));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The stored copies of a live post (`*CurrentlyLive.extraPosts`), read safely. Written only by the bot, but it is
|
|
95
|
+
* a Json column: anything malformed is dropped rather than trusted, and a pre-`pingRoleId` row reads as no role.
|
|
96
|
+
*/
|
|
97
|
+
function readExtraPosts(stored) {
|
|
98
|
+
if (!Array.isArray(stored))
|
|
99
|
+
return [];
|
|
100
|
+
return stored.flatMap((raw) => {
|
|
101
|
+
const row = raw;
|
|
102
|
+
if (!row || typeof row.channelId !== 'string' || !SNOWFLAKE.test(row.channelId) || typeof row.messageId !== 'string' || !SNOWFLAKE.test(row.messageId))
|
|
103
|
+
return [];
|
|
104
|
+
return [{ channelId: row.channelId, messageId: row.messageId, pingRoleId: isPingTarget(row.pingRoleId) ? row.pingRoleId : null }];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** "Is there a live post to finish?" - destination 1's message, or any copy. Offline and sweep filters use this. */
|
|
108
|
+
const hasLivePost = (live) => !!(live?.channelId && live?.messageId) || readExtraPosts(live?.extraPosts).length > 0;
|
|
109
|
+
exports.hasLivePost = hasLivePost;
|
package/dist/other/types.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type GuildPlanType = {
|
|
|
23
23
|
defaultAutoRoles: number | null;
|
|
24
24
|
/** Scheduled messages a server may keep (switched-off one-time messages count). Checked on create. */
|
|
25
25
|
defaultScheduledMessages: number | null;
|
|
26
|
+
/** Minecraft: connected game servers kept active; the rest are paused (pinned + oldest stay). */
|
|
27
|
+
defaultInGameServers: number | null;
|
|
26
28
|
defaultStatusRoles: number | null;
|
|
27
29
|
defaultStreamRoles: number | null;
|
|
28
30
|
maxOfflineGraceMinutes: number | null;
|
|
@@ -10,6 +10,18 @@ export declare const ClubStreamerZod: {
|
|
|
10
10
|
guildId: z.ZodString;
|
|
11
11
|
usePerStreamerEmbeds: z.ZodNullable<z.ZodBoolean>;
|
|
12
12
|
autoPublishIfAnnouncmentChannel: z.ZodNullable<z.ZodBoolean>;
|
|
13
|
+
extraDestinations: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
14
|
+
channelId: z.ZodString;
|
|
15
|
+
pingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
16
|
+
types: z.ZodArray<z.ZodEnum<{
|
|
17
|
+
post: "post";
|
|
18
|
+
}>>;
|
|
19
|
+
}, z.core.$strip>>>;
|
|
20
|
+
inGame: z.ZodBoolean;
|
|
21
|
+
inGameTypes: z.ZodArray<z.ZodEnum<{
|
|
22
|
+
post: "post";
|
|
23
|
+
}>>;
|
|
24
|
+
inGameServerIds: z.ZodArray<z.ZodString>;
|
|
13
25
|
customMessages: z.ZodArray<z.ZodLazy<z.ZodObject<{
|
|
14
26
|
content: z.ZodNullable<z.ZodString>;
|
|
15
27
|
showInviteButton: z.ZodNullable<z.ZodBoolean>;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ClubStreamerZodPartial = exports.ClubStreamerZod = void 0;
|
|
4
4
|
const schema_zod_1 = require("./schema.zod");
|
|
5
|
+
const multiDestination_1 = require("../multiDestination");
|
|
6
|
+
const inGameAlerts_1 = require("../inGameAlerts");
|
|
5
7
|
const zod_1 = require("./zod");
|
|
6
8
|
const zod_2 = require("zod");
|
|
7
9
|
const ClubStreamerSchema = zod_2.z.object({
|
|
@@ -9,6 +11,14 @@ const ClubStreamerSchema = zod_2.z.object({
|
|
|
9
11
|
guildId: zod_1.SnowFlake,
|
|
10
12
|
usePerStreamerEmbeds: zod_2.z.boolean().nullable(),
|
|
11
13
|
autoPublishIfAnnouncmentChannel: zod_2.z.boolean().nullable(),
|
|
14
|
+
/** Premium Plus (`MultiDestination`): up to 2 extra channels, each with its own ping role and post types. */
|
|
15
|
+
extraDestinations: zod_1.CentralZod.ExtraDestinations(multiDestination_1.DESTINATION_TYPES.club).nullable(),
|
|
16
|
+
/** Minecraft (`inGame` launch flag): show this creator's notifications on connected game servers. Off by default. */
|
|
17
|
+
inGame: zod_2.z.boolean(),
|
|
18
|
+
/** Which notifications show in Minecraft; empty = every type in `IN_GAME_TYPES.club`. */
|
|
19
|
+
inGameTypes: (0, zod_1.NoDuplicate)(zod_2.z.array(zod_2.z.enum(inGameAlerts_1.IN_GAME_TYPES.club))),
|
|
20
|
+
/** Which connected servers (InGameServer.id); empty = every active one. Max 10. */
|
|
21
|
+
inGameServerIds: (0, zod_1.NoDuplicate)(zod_2.z.array(zod_2.z.string().uuid())).max(inGameAlerts_1.MAX_IN_GAME_SERVER_PICKS),
|
|
12
22
|
customMessages: (0, zod_1.NoDuplicate)(zod_2.z.array(zod_2.z.lazy(() => CSCustomMessageSchema)), ['type']),
|
|
13
23
|
notificationChannelId: zod_1.SnowFlake.nullable(),
|
|
14
24
|
showNotifyButton: zod_2.z.boolean().nullable(),
|