{"version":3,"file":"activity.mjs","names":[],"sources":["../../src/modules/activity.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises'\n\nimport {\n  type ActivityOptions,\n  ActivityType,\n  type APIApplicationCommandOptionChoice,\n  ApplicationCommandOptionType,\n  type ChatInputCommandInteraction,\n  type Client,\n} from 'discord.js'\nimport env from 'env-var'\nimport { isOwnerGuard, type SleetContext, SleetSlashCommand } from 'sleetcord'\n\nimport { MINUTE } from '../utils/constants.ts'\n\n/** Holds the timeout that we use to periodically change the activity */\nlet timeout: NodeJS.Timeout\n/** Every 15m, change the current activity */\nconst timeoutDelay = 15 * MINUTE // in ms\n/** These activities will be randomly selected and shown by the bot */\nconst activities: ActivityOptions[] = []\n\n/** You shouldn't see this, this is just a fallback activity if the random pick fails */\nconst FALLBACK_ACTIVITY: ActivityOptions = {\n  type: ActivityType.Custom,\n  name: 'Failed to load activity!',\n} as const\n\n/** Maps from an activity ID or string to a display string */\nconst reverseActivityTypesMap: Record<Exclude<ActivityOptions['type'], undefined>, string> = {\n  [ActivityType.Playing]: 'Playing',\n  [ActivityType.Streaming]: 'Streaming',\n  [ActivityType.Listening]: 'Listening to',\n  [ActivityType.Watching]: 'Watching',\n  [ActivityType.Custom]: 'Custom',\n  [ActivityType.Competing]: 'Competing in',\n}\n\nconst ACTIVITIES_FILE = env.get('ACTIVITIES_FILE').asString()\n\n/**\n * Valid choices for activities that bots can set\n */\nconst activityChoices: APIApplicationCommandOptionChoice<number>[] = [\n  {\n    name: 'playing',\n    value: ActivityType.Playing,\n  },\n  {\n    name: 'streaming',\n    value: ActivityType.Streaming,\n  },\n  {\n    name: 'listening',\n    value: ActivityType.Listening,\n  },\n  {\n    name: 'watching',\n    value: ActivityType.Watching,\n  },\n  {\n    name: 'custom',\n    value: ActivityType.Custom,\n  },\n  {\n    name: 'competing',\n    value: ActivityType.Competing,\n  },\n]\n\n/**\n * Set the activity that a bot is doing, ie. the \"**Playing** some game\"\n */\nexport const activity: SleetSlashCommand = new SleetSlashCommand(\n  {\n    name: 'activity',\n    description: 'Allow to randomly/manually set a new activity',\n    options: [\n      {\n        name: 'name',\n        type: ApplicationCommandOptionType.String,\n        description: 'The new activity name to use',\n      },\n      {\n        name: 'type',\n        type: ApplicationCommandOptionType.Integer,\n        description: 'The activity type to set',\n        choices: activityChoices,\n      },\n      {\n        name: 'state',\n        type: ApplicationCommandOptionType.String,\n        description: 'The activity state to set',\n      },\n    ],\n    registerOnlyInGuilds: [],\n  },\n  {\n    clientReady: runReady,\n    run: runActivity,\n  },\n)\n\n/** Run a timeout to change the bot's activity on READY and every couple mins */\nasync function runReady(client: Client) {\n  await loadActivities()\n  const activity = getRandomActivity()\n\n  setClientActivity(client, activity)\n\n  timeout = setTimeout(() => {\n    void runReady(client)\n  }, timeoutDelay)\n}\n\n/** Either set a new random activity, or set it to the one the user specified */\nasync function runActivity(\n  this: SleetContext,\n  interaction: ChatInputCommandInteraction,\n): Promise<void> {\n  await isOwnerGuard(interaction)\n\n  const name = interaction.options.getString('name')\n  const type = interaction.options.getInteger('type') as Exclude<\n    ActivityOptions['type'],\n    undefined\n  > | null\n  const state = interaction.options.getString('state')\n\n  let activity: ActivityOptions\n  clearTimeout(timeout)\n\n  if (type === null && name === null) {\n    // Set a random one\n    activity = getRandomActivity()\n    timeout = setTimeout(() => {\n      void runReady(interaction.client)\n    }, timeoutDelay)\n  } else {\n    const previousActivity = interaction.client.user.presence.activities[0]\n    activity = {\n      type: type ?? previousActivity.type,\n      name: name ?? previousActivity.name,\n    }\n\n    if (state) {\n      activity.state = state\n    }\n  }\n\n  setClientActivity(interaction.client, activity)\n\n  await interaction.reply({\n    ephemeral: true,\n    content: `Set activity to:\\n> ${formatActivity(activity)}`,\n  })\n}\n\nasync function loadActivities() {\n  if (!ACTIVITIES_FILE) return\n\n  const lines = await readFile(ACTIVITIES_FILE, 'utf-8').then((content) =>\n    content.trim().split('\\n'),\n  )\n\n  const stats: ActivityOptions[] = lines.map((line) => {\n    const space = line.indexOf(' ') + 1\n    let [type, name] = [line.substring(0, space), line.substring(space)].map((str) => str.trim())\n\n    type = type.replace(/{(\\w+)}/, '$1')\n\n    if (!(type in ActivityType)) {\n      type = 'Custom'\n      name = line\n    }\n\n    return {\n      type: ActivityType[type as keyof typeof ActivityType],\n      name,\n    }\n  })\n\n  activities.push(...stats)\n}\n\n/**\n * Helper function to set the activity for a client, adds in shard ID if the client is sharded\n * @param client The client to set the activity for\n * @param activity The activity to set\n */\nfunction setClientActivity(client: Client, activity: ActivityOptions) {\n  if (client.shard) {\n    for (const shardId of client.shard.ids) {\n      // Shards start at 0, so with 4 shards we'd have [0, 1, 2, 3] and we don't want \"Shard 3/4\"\n      const count = client.shard.count - 1\n\n      client.user?.setActivity({\n        ...activity,\n        name: `${activity.name} | Shard ${shardId}/${count}`,\n        shardId,\n      })\n    }\n  } else {\n    client.user?.setActivity(activity)\n  }\n}\n\n/**\n * Get a random activity from our list of activities\n * @returns a random activity from the list\n */\nfunction getRandomActivity(): ActivityOptions {\n  const randomIndex = Math.floor(Math.random() * activities.length)\n  return activities[randomIndex] ?? FALLBACK_ACTIVITY\n}\n\n/**\n * Formats an activity object into a string\n * @param activity The activity object\n * @returns The formatted string\n */\nfunction formatActivity(activity: ActivityOptions): string {\n  const activityType = reverseActivityTypesMap[activity.type ?? ActivityType.Custom]\n  const formattedType = activityType ? `**${activityType}** ` : ''\n  return `${formattedType}${activity.name}`\n}\n"],"mappings":"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"}