diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b58b603
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..3e22f4b
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/hellcom.iml b/.idea/hellcom.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/hellcom.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..ffeb3e3
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 0000000..0c83ac4
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/api-wrapper/api.ts b/src/api-wrapper/api.ts
index db10e89..d566c20 100644
--- a/src/api-wrapper/api.ts
+++ b/src/api-wrapper/api.ts
@@ -15,8 +15,10 @@ import {writeFileSync} from 'fs';
import {getAllPlanets} from './planets';
import axios, {AxiosRequestConfig} from 'axios';
import {config} from '../config';
+import {logger} from '../handlers';
const API_URL = 'https://api.live.prod.thehelldiversgame.com/api';
+const CHATS_URL = 'https://api.diveharder.com/v1/all';
const {IDENTIFIER} = config;
export const seasons = {
@@ -92,10 +94,10 @@ export let data: ApiData = {
const axiosOpts: AxiosRequestConfig = {
headers: {
'Accept-Language': 'en-us',
+ 'User-Agent': 'HellComBot/1.0',
},
};
-let getDataCounter = 0;
export async function getData() {
const season = seasons.current;
// https://api.live.prod.thehelldiversgame.com/api/WarSeason/801/Status
@@ -125,6 +127,19 @@ export async function getData() {
).data;
const planetStats = statsApi as PlanetStats;
+ let chatsAPI;
+ try {
+ // Unofficial: api wrapper for the authed chats endpoint
+ chatsAPI = await (
+ await axios.get(CHATS_URL, {...axiosOpts, timeout: 10_000})
+ ).data;
+ } catch (err) {
+ logger.error('Failed to fetch chats data.', {
+ type: 'API',
+ ...(err as Error),
+ });
+ }
+
// let planetStats: PlanetStats = data.PlanetStats;
// if (getDataCounter % 2 === 0) {
// const planetStatsApi = await (
@@ -250,8 +265,13 @@ export async function getData() {
// this is the starting point in unix for whatever time thing they use
UTCOffset: Math.floor(status.timeUtc - status.time * 1000), // use this value to add to the time to get the UTC time in seconds
};
+ if (
+ chatsAPI &&
+ chatsAPI['store_rotation'] &&
+ chatsAPI['store_rotation'].items
+ )
+ data.SuperStore = chatsAPI['store_rotation'];
- getDataCounter++;
writeFileSync('data.json', JSON.stringify(data, null, 2));
return data;
}
diff --git a/src/api-wrapper/types.ts b/src/api-wrapper/types.ts
index d2d6c05..26e034a 100644
--- a/src/api-wrapper/types.ts
+++ b/src/api-wrapper/types.ts
@@ -241,6 +241,25 @@ export type NewsFeedItem = {
message: string;
};
+export type StoreItem = {
+ name: string;
+ description: string;
+ type: 'Light' | 'Medium' | 'Heavy';
+ slot: 'Head' | 'Body' | 'Cloak';
+ armor_rating: number;
+ speed: number;
+ stamina_regen: number;
+ passive: {
+ name: string;
+ description: string;
+ };
+};
+
+export type StoreRotation = {
+ expire_time: Date;
+ items: StoreItem[];
+};
+
export type SteamNewsItem = {
title: string;
url: string;
@@ -309,6 +328,7 @@ export type ApiData = {
ActivePlanets: MergedPlanetData[];
PlanetAttacks: {source: string; target: string}[];
Events: GlobalEvent[];
+ SuperStore?: StoreRotation;
Players: {
[key in Faction]: number;
};
diff --git a/src/commands/index.ts b/src/commands/index.ts
index 53f3d19..218af5c 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -12,8 +12,10 @@ import dispatches from './dispatches';
import events from './events';
import history from './history';
import map from './map';
+import steam from './steam';
import planet from './planet';
import subscribe from './subscribe';
+import superstore from './superstore';
import wiki from './wiki';
import {Category, WikiData} from '../handlers';
@@ -27,7 +29,9 @@ const commandList: Command[] = [
history,
community,
map,
- wiki,
+ // steam,
+ // wiki,
+ superstore,
];
const notEphemeral: string[] = [];
const ephemeralCmds = commandList
diff --git a/src/commands/steam.ts b/src/commands/steam.ts
new file mode 100644
index 0000000..4c68c63
--- /dev/null
+++ b/src/commands/steam.ts
@@ -0,0 +1,151 @@
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ CommandInteraction,
+ EmbedBuilder,
+ SlashCommandBuilder,
+} from 'discord.js';
+import {Command} from '../interfaces';
+import {FOOTER_MESSAGE} from './_components';
+import axios from 'axios';
+import {SteamNewsFeed} from '../api-wrapper';
+import dayjs from 'dayjs';
+
+const command: Command = {
+ data: new SlashCommandBuilder()
+ .setName('steam')
+ .setDescription('View the latest Helldivers 2 / bot patch notes')
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('news')
+ .setDescription('View the latest Helldivers 2 non-patch news')
+ )
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('patchnotes')
+ .setDescription('View the latest Helldivers 2 patch notes')
+ ),
+ run: async interaction => {
+ const subcommand = interaction.options.data[0].name;
+
+ await subcmds[subcommand](interaction);
+ },
+};
+
+const subcmds: {[key: string]: (job: CommandInteraction) => Promise} = {
+ patchnotes,
+ news,
+};
+
+async function patchnotes(interaction: CommandInteraction) {
+ const axiosOpts = {
+ headers: {
+ 'User-Agent': 'HelldiversBot/1.0',
+ 'Accept-Language': 'en-US',
+ },
+ };
+ const apiData = (await (
+ await axios.get('http://api.diveharder.com/raw/updates', {
+ ...axiosOpts,
+ params: {
+ maxEntries: 512,
+ },
+ })
+ ).data) as SteamNewsFeed;
+ const steamPatchNotes = apiData.filter(news =>
+ news.title.toLowerCase().includes('patch')
+ );
+
+ const messages = steamPatchNotes
+ .slice(0, 3)
+ .reverse()
+ .map(p => {
+ const embed = new EmbedBuilder()
+ .setTitle(p.title)
+ .setURL(p.url)
+ .setTimestamp(dayjs(p.date).unix() * 1000)
+ .setFooter({text: FOOTER_MESSAGE});
+
+ if (p.contents.length > 4000)
+ embed.setDescription(
+ p.contents.slice(0, 4000) +
+ '`...`\n\n## Click the button below to read more!'
+ );
+ else embed.setDescription(p.contents);
+
+ return {
+ embeds: [embed],
+ components: [
+ new ActionRowBuilder().addComponents([
+ new ButtonBuilder()
+ .setLabel('Steam Post')
+ .setStyle(ButtonStyle.Link)
+ .setURL(p.url),
+ ]),
+ ],
+ };
+ });
+
+ messages.forEach(async (m, i) => {
+ if (i === 0) await interaction.editReply({...m});
+ else await interaction.followUp({...m, ephemeral: true});
+ });
+}
+
+async function news(interaction: CommandInteraction) {
+ const axiosOpts = {
+ headers: {
+ 'User-Agent': 'HelldiversBot/1.0',
+ 'Accept-Language': 'en-US',
+ },
+ };
+ const apiData = (await (
+ await axios.get('https://api.diveharder.com/raw/updates', {
+ ...axiosOpts,
+ params: {
+ maxEntries: 512,
+ },
+ })
+ ).data) as SteamNewsFeed;
+ const steamNews = apiData.filter(
+ news => !news.title.toLowerCase().includes('patch')
+ );
+
+ const messages = steamNews
+ .slice(0, 3)
+ .reverse()
+ .map(p => {
+ const embed = new EmbedBuilder()
+ .setTitle(p.title)
+ .setURL(p.url)
+ .setTimestamp(dayjs(p.date).unix() * 1000)
+ .setFooter({text: FOOTER_MESSAGE});
+
+ if (p.contents.length > 4000)
+ embed.setDescription(
+ p.contents.slice(0, 4000) +
+ '`...`\n\n## Click the button below to read more!'
+ );
+ else embed.setDescription(p.contents);
+
+ return {
+ embeds: [embed],
+ components: [
+ new ActionRowBuilder().addComponents([
+ new ButtonBuilder()
+ .setLabel('Steam Post')
+ .setStyle(ButtonStyle.Link)
+ .setURL(p.url),
+ ]),
+ ],
+ };
+ });
+
+ messages.forEach(async (m, i) => {
+ if (i === 0) await interaction.editReply({...m});
+ else await interaction.followUp({...m, ephemeral: true});
+ });
+}
+
+export default command;
diff --git a/src/commands/superstore.ts b/src/commands/superstore.ts
new file mode 100644
index 0000000..c787088
--- /dev/null
+++ b/src/commands/superstore.ts
@@ -0,0 +1,75 @@
+import {EmbedBuilder, SlashCommandBuilder} from 'discord.js';
+import {Command} from '../interfaces';
+import {FOOTER_MESSAGE} from './_components';
+import {data} from '../api-wrapper';
+import {dayjs} from '../handlers/dates';
+
+const command: Command = {
+ data: new SlashCommandBuilder()
+ .setName('superstore')
+ .setDescription('Check the current stock of the super store!'),
+ run: async interaction => {
+ const {SuperStore} = data;
+ if (!SuperStore || !SuperStore.items) {
+ await interaction.editReply({
+ embeds: [
+ new EmbedBuilder()
+ .setTitle('Super Store')
+ .setDescription(
+ 'Failed to retrieve Super Store data. Please try again later!'
+ )
+ .setFooter({text: FOOTER_MESSAGE}),
+ ],
+ });
+ return;
+ }
+ const embeds: EmbedBuilder[] = [];
+ for (const item of SuperStore.items) {
+ const embed = new EmbedBuilder()
+ .setTitle(`${item.name} (${item.type} ${item.slot})`)
+ .setDescription(item.description)
+ .addFields(
+ {
+ name: 'Armour',
+ value: `${item.armor_rating}`,
+ inline: true,
+ },
+ {
+ name: 'Speed',
+ value: `${item.speed}`,
+ inline: true,
+ },
+ {
+ name: 'Stamina Regen',
+ value: `${item.stamina_regen}`,
+ inline: true,
+ },
+ {
+ name: `Passive: ${item.passive.name}`,
+ value: item.passive.description,
+ inline: false,
+ }
+ );
+ embeds.push(embed);
+ }
+
+ // add a final embed for the expiration time
+ const expireTimeS = dayjs(SuperStore.expire_time).unix();
+ const expiresIn = dayjs(SuperStore.expire_time).diff();
+ const expiresInH = Math.floor(expiresIn / 3600000);
+ const remainingMinutes = Math.floor((expiresIn % 3600000) / 60000);
+
+ embeds.push(
+ new EmbedBuilder()
+ .setTitle('Super Store')
+ .setDescription(
+ `This store rotation will expire in **${expiresInH} hours, ${remainingMinutes} minutes** ()` +
+ '\n\n Data provided by **[Diveharder](https://api.diveharder.com/docs)**.'
+ )
+ .setFooter({text: FOOTER_MESSAGE})
+ );
+ await interaction.editReply({embeds: embeds});
+ },
+};
+
+export default command;
diff --git a/src/events/onReady.ts b/src/events/onReady.ts
index bd4e57d..ce322f5 100644
--- a/src/events/onReady.ts
+++ b/src/events/onReady.ts
@@ -1,24 +1,9 @@
-import {
- ActivityType,
- ButtonBuilder,
- ButtonStyle,
- Client,
- REST,
- Routes,
- StringSelectMenuBuilder,
- StringSelectMenuOptionBuilder,
-} from 'discord.js';
+import {ActivityType, Client, REST, Routes} from 'discord.js';
import {schedule} from 'node-cron';
import {commandHash, commandList, presenceCmds, wikiCmd} from '../commands';
import {config} from '../config';
import {getData, mappedNames} from '../api-wrapper';
-import {
- compareData,
- dbData,
- loadWikiFiles,
- logger,
- updateMessages,
-} from '../handlers';
+import {compareData, dbData, logger, updateMessages} from '../handlers';
// bot client token, for use with discord API
const BOT_TOKEN = config.BOT_TOKEN;
@@ -76,47 +61,47 @@ const onReady = async (client: Client) => {
});
// load wiki pages
- const wiki = loadWikiFiles('./wiki');
- wikiCmd.buttons = wiki.categories.map(c => {
- const {directory, display_name, content, emoji, thumbnail, image} = c;
-
- const button = new ButtonBuilder()
- .setCustomId(directory)
- .setLabel(display_name)
- .setStyle(ButtonStyle.Secondary);
-
- if (emoji) button.setEmoji(emoji);
- return button;
- });
- wikiCmd.dirSelect = wiki.categories.reduce(
- (acc, c) => {
- acc[c.directory] = new StringSelectMenuBuilder()
- .setCustomId(c.directory)
- .setPlaceholder('Select a page from this category...')
- .addOptions(
- wiki.pages
- .filter(page => page.page.startsWith(c.directory))
- .map(page => {
- const option = new StringSelectMenuOptionBuilder()
- .setLabel(page.title)
- .setValue(page.page);
-
- if (page.description) option.setDescription(page.description);
- if (page.emoji) option.setEmoji(page.emoji);
- return option;
- })
- );
- return acc;
- },
- {} as Record
- );
- wikiCmd.pages = wiki.pages;
- wikiCmd.categories = wiki.categories;
-
- time = `${Date.now() - start}ms`;
- logger.info(`Loaded ${wiki.pages.length} wiki pages in ${time}`, {
- type: 'startup',
- });
+ // const wiki = loadWikiFiles('./wiki');
+ // wikiCmd.buttons = wiki.categories.map(c => {
+ // const {directory, display_name, content, emoji, thumbnail, image} = c;
+ //
+ // const button = new ButtonBuilder()
+ // .setCustomId(directory)
+ // .setLabel(display_name)
+ // .setStyle(ButtonStyle.Secondary);
+ //
+ // if (emoji) button.setEmoji(emoji);
+ // return button;
+ // });
+ // wikiCmd.dirSelect = wiki.categories.reduce(
+ // (acc, c) => {
+ // acc[c.directory] = new StringSelectMenuBuilder()
+ // .setCustomId(c.directory)
+ // .setPlaceholder('Select a page from this category...')
+ // .addOptions(
+ // wiki.pages
+ // .filter(page => page.page.startsWith(c.directory))
+ // .map(page => {
+ // const option = new StringSelectMenuOptionBuilder()
+ // .setLabel(page.title)
+ // .setValue(page.page);
+ //
+ // if (page.description) option.setDescription(page.description);
+ // if (page.emoji) option.setEmoji(page.emoji);
+ // return option;
+ // })
+ // );
+ // return acc;
+ // },
+ // {} as Record
+ // );
+ // wikiCmd.pages = wiki.pages;
+ // wikiCmd.categories = wiki.categories;
+ //
+ // time = `${Date.now() - start}ms`;
+ // logger.info(`Loaded ${wiki.pages.length} wiki pages in ${time}`, {
+ // type: 'startup',
+ // });
// cron schedule to update messages
schedule(PERSISTENT_MESSAGE_INTERVAL, () => updateMessages());
diff --git a/src/handlers/cron/deliverUpdates.ts b/src/handlers/cron/deliverUpdates.ts
index b2e03f6..41e7faf 100644
--- a/src/handlers/cron/deliverUpdates.ts
+++ b/src/handlers/cron/deliverUpdates.ts
@@ -320,7 +320,7 @@ export async function newEventUpdate(event: GlobalEvent, channelIds: string[]) {
return;
}
// TODO: use new endpoint to get this
-// export async function newMajorOrdeUpdater(order: ??, channels: (TextChannel | PublicThreadChannel)[]) {}
+// export async function newMajorOrderUpdater(order: ??, channels: (TextChannel | PublicThreadChannel)[]) {}
export async function newMajorOrderUpdate(
assignment: Assignment,
channelIds: string[]
@@ -346,47 +346,56 @@ export async function newMajorOrderUpdate(
export async function newNewsUpdate(news: NewsFeedItem, channelIds: string[]) {
const channels = await validateChannelArr(channelIds);
const {message} = news;
- const embeds = message.includes('\n')
- ? [
- new EmbedBuilder()
- .setAuthor({
- name: 'New Dispatch from SE Command!',
- iconURL: altSprites['Humans'],
- })
- .setTitle(
- message
- .split('\n')[0]
- .replace(//g, '**')
- .replace(/<\/i>/g, '**')
- )
- .setDescription(
- message
- .split('\n')
- .slice(1)
- .join('\n')
- .replace(//g, '**')
- .replace(/<\/i>/g, '**')
- )
- .setFooter({text: SUBSCRIBE_FOOTER})
- .setTimestamp(),
- ]
- : [
- new EmbedBuilder()
- .setAuthor({
- name: 'New Dispatch from SE Command!',
- iconURL: altSprites['Humans'],
- })
- .setDescription(
- news.message.replace(//g, '**').replace(/<\/i>/g, '**')
- )
- .setFooter({text: SUBSCRIBE_FOOTER})
- .setTimestamp(),
- ];
+ const splitMessage = message.split('\n');
+ // check whether the dispatch reasonably has a title (short string before newline)
+ const title = splitMessage[0].length < 256 ? splitMessage[0] : undefined;
+ const embeds: EmbedBuilder[] = [];
+
+ // if it does have a title, parse it and include it as an embed title
+ if (title)
+ embeds.push(
+ new EmbedBuilder()
+ .setAuthor({
+ name: 'New Dispatch from SE Command!',
+ iconURL: altSprites['Humans'],
+ })
+ .setTitle(
+ message
+ .split('\n')[0]
+ .replace(//g, '**')
+ .replace(/<\/i>/g, '**')
+ )
+ .setDescription(
+ message
+ .split('\n')
+ .slice(1)
+ .join('\n')
+ .replace(//g, '**')
+ .replace(/<\/i>/g, '**')
+ )
+ .setFooter({text: SUBSCRIBE_FOOTER})
+ .setTimestamp()
+ );
+ // otherwise, no title, just description
+ else
+ embeds.push(
+ new EmbedBuilder()
+ .setAuthor({
+ name: 'New Dispatch from SE Command!',
+ iconURL: altSprites['Humans'],
+ })
+ .setDescription(
+ news.message.replace(//g, '**').replace(/<\/i>/g, '**')
+ )
+ .setFooter({text: SUBSCRIBE_FOOTER})
+ .setTimestamp()
+ );
for (const channel of channels) {
try {
const message = await channel.send({embeds});
- if (channel.type === ChannelType.GuildAnnouncement) message.crosspost();
+ if (channel.type === ChannelType.GuildAnnouncement)
+ await message.crosspost();
} catch (err) {
logger.error(err);
}