I have written an API that fetches posts from r/axolotls, but when making a request right after another request which makes the API update the array of posts, it returns with the message "Filtered array only contains 0 items.". This causes an TypeError to occur saying "Cannot read properties of undefined".
This should not happen as the routes doesn't get registered until the API has fetched the posts at least once, and while updating the array it should never be empty.
My Discord bot's ImageCommand.ts, the error occurs in this file's updatePartial()
function:
import { ChatInputCommandInteraction, Client, EmbedBuilder, TextChannel } from "discord.js";
import Axios from "axios";
import CommandInterface from "../Interfaces/CommandInterface";
import { MediaInterface, EntryInterface } from "../Utils/axolotlapi";
let sfwEmbeds: Array<EmbedBuilder> = [];
let nsfwEmbeds: Array<EmbedBuilder> = [];
let lastUpdated: number = 0;
async function updatePartial(nsfw: boolean): Promise<void> {
let newEmbeds: Array<EmbedBuilder> = [];
let payload = "?minImages=1&flair=Just Showing Off 😍" + (nsfw ? "&nsfw=1" : "");
const count = (await Axios.get("https://axolotlapi.kirondevcoder.repl.co/reddit/count" + payload)).data.data;
payload += `&count=${count}`;
const response = (await Axios.get("https://axolotlapi.kirondevcoder.repl.co/reddit" + payload)).data;
for (let i = 0; i < count; i++) {
const post: EntryInterface = response.data[i];
if (!post) {
console.log("Post: " + response.data[i]);
console.log("i: " + i);
console.log("Count: " + count);
console.log("Response: " + JSON.stringify(response));
}
const images: Array<string> = post.media.filter((m: MediaInterface) => m.kind === "image").map((m: MediaInterface) => m.url);
for (let j = 0; j < images.length; j++) {
newEmbeds.push(new EmbedBuilder()
.setTitle(post.title)
.setAuthor({ name: `u/${post.author}`, url: `https://reddit.com/u/${post.author}` })
.setURL(post.link)
.setTimestamp(post.created_utc * 1000)
.setImage(images[j])
.setColor("#ff00ff")
.setDescription("Score: " + post.score)
);
}
}
if (nsfw) nsfwEmbeds = newEmbeds;
else sfwEmbeds = newEmbeds;
}
function isAllowedToUpdate(): boolean {
return Date.now() > lastUpdated + 60 * 30 * 1000; // Update once every 30 minutes at most
}
async function update(): Promise<void> {
if (!isAllowedToUpdate()) return;
lastUpdated = Date.now();
await updatePartial(false);
await updatePartial(true);
console.log("Updated images");
}
export default async (client: Client, commandsArray: Array<CommandInterface>): Promise<void> => {
await update();
commandsArray.push({
name: "image",
description: "Sends you a cute axolotl picture from Reddit.",
executor: async (interaction: ChatInputCommandInteraction<"cached">, client: Client): Promise<void> => {
const channel: TextChannel = interaction.channel as TextChannel;
let embed: EmbedBuilder;
if (channel.nsfw) embed = nsfwEmbeds[Math.floor(Math.random() * nsfwEmbeds.length)];
else embed = sfwEmbeds[Math.floor(Math.random() * sfwEmbeds.length)];
await interaction.reply({ embeds: [ embed ]});
update();
}
});
}
My API's reddit.ts:
import { Router, Request, Response } from "express";
import Axios from "axios";
import { Log, Warn, Note, SendResponse } from "../utils";
interface MediaInterface {
kind: "image" | "video",
url: string
}
interface EntryInterface {
title: string, score: number, link: string, author: string, awards: number, comments: number,
spoiler: boolean, archived: boolean, crosspostable: boolean, pinned: boolean, locked: boolean,
created_utc: number, id: string,
flair: string,
nsfw: boolean,
text: string,
media: Array<MediaInterface>
}
interface EntriesInterface {
new: Array<EntryInterface>,
hot: Array<EntryInterface>,
rising: Array<EntryInterface>,
any: Array<EntryInterface>
}
let entries: EntriesInterface = { new: [], hot: [], rising: [], any: [] };
let flairs: Array<string> = [];
let lastUpdated: number = 0;
const SORTS: Array<string> = ["new", "hot", "rising"];
function isAllowedToUpdate(): boolean {
return Date.now() > lastUpdated + 60 * 30 * 1000; // Update once every 30 minutes at most
}
async function update(): Promise<void> {
if (!isAllowedToUpdate()) return;
lastUpdated = Date.now();
let newEntries: EntriesInterface = { new: [], hot: [], rising: [], any: [] };
let newFlairs: Array<string> = [];
let i: number = 0;
SORTS.forEach(async (sort): Promise<void> => {
const res: any = await Axios.get(`https://www.reddit.com/r/axolotls/${sort}.json?limit=100`);
res.data.data.children.forEach((child: any): void => {
const data = child.data;
const { title, score, author, total_awards_received, num_comments, spoiler, archived, is_crosspostable, pinned, locked, created_utc, id } = data;
const link: string = "https://www.reddit.com" + data.permalink;
const images: Array<string> = [];
const videos: Array<string> = [];
if (data.url.startsWith("https://i.redd.it/")) images.push(data.url);
else if (data.media_data) {
Object.values(data.media_data).forEach((media: any) => {
if (media.status !== "valid") return Warn(`Invalid value: ${JSON.stringify(media)}`);
switch (media.e) {
case "Image":
images.push(media.s.u.split("?")[0].replace("preview.redd.it", "i.redd.it"));
break;
case "RedditVideo":
Note("TODO: Figure out solution for this");
Log(media);
Log(data);
break;
}
});
} else if (data.media?.reddit_video) {
videos.push(data.media.reddit_video.fallback_url.split("?")[0]);
} else if (data.media?.oembed) {
images.push(data.media.oembed.thumbnail_url.split("?")[0]);
} else if (data.media) {
Note("TODO: Handle this unhandled media");
Log(data.media);
}
const payload: EntryInterface = {
title: title, score: score, link: link, author: author, awards: total_awards_received, comments: num_comments,
spoiler: spoiler, archived: archived, crosspostable: is_crosspostable, pinned: pinned, locked: locked,
created_utc: created_utc, id: id,
flair: data.link_flair_text,
nsfw: data.over_18,
text: data.selftext,
media: []
};
for (const image of images) {
payload.media.push({
kind: "image",
url: image
});
}
for (const video of videos) {
payload.media.push({
kind: "video",
url: video
});
}
newEntries[sort as keyof EntriesInterface].push(payload);
if (!newEntries.any.map((e) => e.id).includes(id)) newEntries.any.push(payload);
if (!flairs.includes(payload.flair)) flairs.push(payload.flair);
});
i++;
if (i == SORTS.length) {
entries.any = newEntries.any;
entries.hot = newEntries.hot;
entries.new = newEntries.new;
entries.rising = newEntries.rising;
flairs = newFlairs;
}
});
}
function getFilteredArray(
sort: string,
minScore: number, minImages: number, minVideos: number, minAwards: number, minComments: number,
maxScore: number, maxImages: number, maxVideos: number, maxAwards: number, maxComments: number,
author: string | null, flair: string | null,
nsfw: number, spoiler: number, archived: number, is_crosspostable: number, pinned: number, locked: number
): Array<EntryInterface> {
let arr = entries[sort as keyof EntriesInterface]
.filter((e: EntryInterface) => e.score >= minScore && e.score <= maxScore)
.filter((e: EntryInterface) => e.media.filter((m: MediaInterface) => m.kind == "image").length >= minImages && e.media.filter((m: MediaInterface) => m.kind == "image").length <= maxImages)
.filter((e: EntryInterface) => e.media.filter((m: MediaInterface) => m.kind == "video").length >= minVideos && e.media.filter((m: MediaInterface) => m.kind == "video").length <= maxVideos)
.filter((e: EntryInterface) => e.awards >= minAwards && e.awards <= maxAwards)
.filter((e: EntryInterface) => e.comments >= minComments && e.comments <= maxComments)
;
if (author !== null) arr = arr.filter((e: EntryInterface) => e.author == author);
if (flair !== null) arr = arr.filter((e: EntryInterface) => e.flair == flair);
for (const e of Object.entries({ nsfw, spoiler, archived, is_crosspostable, pinned, locked})) {
switch (e[1].toString()) {
case "0": arr = arr.filter((e2: EntryInterface) => e2[e[0] as keyof EntryInterface] === false); break;
//case "1": break;
default: break;
case "2": arr = arr.filter((e2: EntryInterface) => e2[e[0] as keyof EntryInterface] === true); break;
}
}
return arr;
}
function getFilteredArrayFromRequest(req: Request): Array<EntryInterface> {
let sort: string = req.query.sort as string;
if (!SORTS.includes(sort)) sort = "any";
return getFilteredArray(
sort,
Number.parseInt(req.query.minScore as string) || 0,
Number.parseInt(req.query.minImages as string) || 0,
Number.parseInt(req.query.minVideos as string) || 0,
Number.parseInt(req.query.minAwards as string) || 0,
Number.parseInt(req.query.minComments as string) || 0,
Number.parseInt(req.query.maxScore as string) || Infinity,
Number.parseInt(req.query.maxImages as string) || Infinity,
Number.parseInt(req.query.maxVideos as string) || Infinity,
Number.parseInt(req.query.maxAwards as string) || Infinity,
Number.parseInt(req.query.maxComments as string) || Infinity,
req.query.author as string || null,
req.query.flair as string || null,
Number.parseInt(req.query.nsfw as string) || 0,
Number.parseInt(req.query.spoiler as string) || 0,
Number.parseInt(req.query.archived as string) || 1,
Number.parseInt(req.query.crosspostable as string) || 1,
Number.parseInt(req.query.pinned as string) || 1,
Number.parseInt(req.query.locked as string) || 1
);
}
export default async (router: Router): Promise<void> => {
await update();
router.get("/", (req: Request, res: Response) => {
let count: number = Number.parseInt(req.query.count as string) || 1;
if (count < 1) count = 1;
const arr: Array<EntryInterface> = getFilteredArrayFromRequest(req);
if (arr.length < count) SendResponse(res, 200, arr, `Filtered array only contains ${arr.length} items.`);
else SendResponse(res, 200, [...arr].sort(() => 0.5 - Math.random()).slice(0, count));
update();
});
router.get("/flairs", (req: Request, res: Response) => {
SendResponse(res, 200, flairs);
update();
});
router.get("/count", (req: Request, res: Response) => {
const arr: Array<EntryInterface> = getFilteredArrayFromRequest(req);
SendResponse(res, 200, arr.length);
update();
});
router.get("/id", (req: Request, res: Response) => {
if (req.query.id === undefined) return SendResponse(res, 400, null);
const id: string = req.query.id as string;
if (!entries.any.map((e) => e.id).includes(id)) SendResponse(res, 400, "Invalid ID provided.");
else SendResponse(res, 200, entries.any.find((e) => e.id === id));
update();
});
}
My Discord bot's output:
Listening on port 8080.
CommandLoader: InviteCommand.js
Updated images
CommandLoader: ImageCommand.js
Success reloading application (/) commands.
Logged in as Xondalf2#7884!
HandlerLoader: interactionCreate.js
Post: undefined
i: 0
Count: 19
Response: {"message":"Filtered array only contains 0 items.","data":[]}
/home/runner/Xondalf2/dist/Commands/ImageCommand.js:35
const images = post.media.filter((m) => m.kind === "image").map((m) => m.url);
^
TypeError: Cannot read properties of undefined (reading 'media')
at /home/runner/Xondalf2/dist/Commands/ImageCommand.js:35:33
at Generator.next (<anonymous>)
at fulfilled (/home/runner/Xondalf2/dist/Commands/ImageCommand.js:5:58)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Node.js v18.12.1
exit status 1
Turns out, using a single request in the updatePartial()
function fixed it, for now.