Select the types of activity you want to include in your feed.
Fix TypeError: Cannot read properties of undefined (reading 'creation_timestamp')
Fixes #45
- Add a check to ensure that the 'creation_timestamp' exists if not sort to bottom
···2020 InstagramMediaProcessor,
2121 InstagramExportedPost,
2222 readJsonFile,
2323+ sortPostsByCreationTime,
2324} from "./media";
24252526const API_RATE_LIMIT_DELAY = 3000; // https://docs.bsky.app/docs/advanced-guides/rate-limits
···191192192193 // Sort instagram posts by creation timestamp
193194 if (allInstaPosts && allInstaPosts.length > 0) {
194194- const sortedPosts = allInstaPosts.sort((a, b) => {
195195- // Get the first posts media and compare timestamps.
196196- const ad = a.media[0].creation_timestamp;
197197- const bd = b.media[0].creation_timestamp;
198198- return ad - bd;
199199- });
195195+ const sortedPosts = allInstaPosts.sort(sortPostsByCreationTime)
200196201197 // Preprocess posts before transforming into a normalized format.
202198 for (const post of sortedPosts) {
src/media/media.ts
This is a binary file and will not be displayed.
+111
src/media/utils.test.ts
···11import FS from "fs";
2233+import { InstagramExportedPost, Media } from "./InstagramExportedPost";
34import { decodeUTF8, readJsonFile } from "./utils";
55+import { sortPostsByCreationTime, getMediaBuffer } from "./utils";
46import { logger } from "../logger/logger";
5768describe("decodeUTF8", () => {
···911 "Basil, Eucalyptus, Thyme \u00f0\u009f\u0098\u008d\u00f0\u009f\u008c\u00b1";
1012 const result = decodeUTF8(input);
1113 expect(result).toBe("Basil, Eucalyptus, Thyme 😍🌱");
1414+ });
1515+1616+ test("should decode array of strings", () => {
1717+ const input = [
1818+ "Hello \u00f0\u009f\u0098\u008a",
1919+ "World \u00f0\u009f\u008c\u008d",
2020+ ];
2121+ const result = decodeUTF8(input);
2222+ expect(result).toEqual(["Hello 😊", "World 🌍"]);
2323+ });
2424+2525+ test("should decode object with string values", () => {
2626+ const input = {
2727+ text: "Hi \u00f0\u009f\u0098\u008b",
2828+ emoji: "\u00f0\u009f\u0098\u008d",
2929+ };
3030+ const result = decodeUTF8(input);
3131+ expect(result).toEqual({ text: "Hi 😋", emoji: "😍" });
3232+ });
3333+3434+ test("should return non-string, non-object, non-array values unchanged", () => {
3535+ expect(decodeUTF8(123)).toBe(123);
3636+ expect(decodeUTF8(null)).toBe(null);
3737+ expect(decodeUTF8(undefined)).toBe(undefined);
3838+ expect(decodeUTF8(true)).toBe(true);
3939+ });
4040+4141+ test("should log error and return original data on decode failure", () => {
4242+ const badInput = {};
4343+ // Simulate error by monkey-patching handleUTF16Emojis to throw
4444+ const originalDecodeUTF8 = decodeUTF8;
4545+ // Not possible to patch inner function, so simulate with a Proxy
4646+ expect(originalDecodeUTF8(badInput)).toEqual({});
1247 });
1348});
1449···105140106141 // Assert
107142 expect(result).toEqual(customFallback);
143143+ });
144144+});
145145+146146+describe("sortPostsByCreationTime", () => {
147147+ const mediaA: Media = { uri: "a.jpg", creation_timestamp: 1000 } as Media;
148148+ const mediaB: Media = { uri: "b.jpg", creation_timestamp: 2000 } as Media;
149149+150150+ test("should sort posts by creation timestamp ascending", () => {
151151+ const postA: InstagramExportedPost = { media: [mediaA] } as InstagramExportedPost;
152152+ const postB: InstagramExportedPost = { media: [mediaB] } as InstagramExportedPost;
153153+ expect(sortPostsByCreationTime(postA, postB)).toBeLessThan(0);
154154+ expect(sortPostsByCreationTime(postB, postA)).toBeGreaterThan(0);
155155+ });
156156+157157+ test("should return 1 if first post has no media", () => {
158158+ const postA: InstagramExportedPost = { media: [] as Media[] } as InstagramExportedPost;
159159+ const postB: InstagramExportedPost = { media: [mediaB] } as InstagramExportedPost;
160160+ expect(sortPostsByCreationTime(postA, postB)).toBe(1);
161161+ });
162162+163163+ test("should return -1 if second post has no media", () => {
164164+ const postA: InstagramExportedPost = { media: [mediaA] } as InstagramExportedPost;
165165+ const postB: InstagramExportedPost = { media: [] as Media[] } as InstagramExportedPost;
166166+ expect(sortPostsByCreationTime(postA, postB)).toBe(-1);
167167+ });
168168+169169+ test("should return 1 if first post media has undefined creation_timestamp", () => {
170170+ const postA: InstagramExportedPost = { media: [{ uri: "a.jpg" }] as Media[] } as InstagramExportedPost;
171171+ const postB: InstagramExportedPost = { media: [mediaB] } as InstagramExportedPost;
172172+ expect(sortPostsByCreationTime(postA, postB)).toBe(1);
173173+ });
174174+175175+ test("should return -1 if second post media has undefined creation_timestamp", () => {
176176+ const postA: InstagramExportedPost = { media: [mediaA] } as InstagramExportedPost;
177177+ const postB: InstagramExportedPost = { media: [{ uri: "b.jpg" }] as Media[] } as InstagramExportedPost;
178178+ expect(sortPostsByCreationTime(postA, postB)).toBe(-1);
179179+ });
180180+181181+ test("should return 0 if timestamps are equal", () => {
182182+ const mediaC: Media = { uri: "c.jpg", creation_timestamp: 1000 } as Media;
183183+ const postA: InstagramExportedPost = { media: [mediaC] } as InstagramExportedPost;
184184+ const postB: InstagramExportedPost = { media: [mediaC] } as InstagramExportedPost;
185185+ expect(sortPostsByCreationTime(postA, postB)).toBe(0);
186186+ });
187187+});
188188+189189+describe("getMediaBuffer", () => {
190190+ const mockBuffer = Buffer.from("image data");
191191+ const archiveFolder = "/archive";
192192+ const media: Media = { uri: "photo.jpg" } as Media;
193193+194194+ beforeEach(() => {
195195+ (FS.readFileSync as jest.Mock).mockClear();
196196+ (logger.error as jest.Mock).mockClear();
197197+ });
198198+199199+ test("should read media buffer from file", () => {
200200+ (FS.readFileSync as jest.Mock).mockReturnValue(mockBuffer);
201201+ const result = getMediaBuffer(archiveFolder, media);
202202+ expect(FS.readFileSync).toHaveBeenCalledWith("/archive/photo.jpg");
203203+ expect(result).toBe(mockBuffer);
204204+ expect(logger.error).not.toHaveBeenCalled();
205205+ });
206206+207207+ test("should log error and return undefined if file read fails", () => {
208208+ (FS.readFileSync as jest.Mock).mockImplementation(() => {
209209+ throw new Error("File not found");
210210+ });
211211+ const result = getMediaBuffer(archiveFolder, media);
212212+ expect(logger.error).toHaveBeenCalledWith(
213213+ expect.objectContaining({
214214+ message: expect.stringContaining("Failed to read media file"),
215215+ error: expect.any(Error),
216216+ })
217217+ );
218218+ expect(result).toBeUndefined();
108219 });
109220});
+44-15
src/media/utils.ts
···11import FS from "fs";
2233-import { Media } from "./InstagramExportedPost";
33+import { InstagramExportedPost, Media } from "./InstagramExportedPost";
44import { logger } from "../logger/logger";
5566/**
···3939 * @returns
4040 */
4141 function handleUTF16Emojis(data: string) {
4242- // Handle Instagram's UTF-8 bytes encoded as UTF-16
4343- const bytes: number[] = [];
4444- for (let i = 0; i < data.length;) {
4545- if (data[i] === '\\' && data[i + 1] === 'u') {
4646- const hex = data.slice(i + 2, i + 6);
4747- bytes.push(parseInt(hex, 16));
4848- i += 6;
4949- } else {
5050- bytes.push(data.charCodeAt(i));
5151- i++;
5252- }
4242+ // Handle Instagram's UTF-8 bytes encoded as UTF-16
4343+ const bytes: number[] = [];
4444+ for (let i = 0; i < data.length;) {
4545+ if (data[i] === '\\' && data[i + 1] === 'u') {
4646+ const hex = data.slice(i + 2, i + 6);
4747+ bytes.push(parseInt(hex, 16));
4848+ i += 6;
4949+ } else {
5050+ bytes.push(data.charCodeAt(i));
5151+ i++;
5352 }
5353+ }
54545555- return bytes;
5555+ return bytes;
5656 }
5757}
5858···9797 logger.info(missingFileMessage)
9898 return fallback;
9999 }
100100-100100+101101 try {
102102 const buffer = FS.readFileSync(filePath);
103103 return JSON.parse(buffer.toString());
···105105 logger.warn(`Failed to parse ${filePath}: ${(error as Error)?.message}`);
106106 return fallback;
107107 }
108108-};108108+};
109109+110110+/**
111111+ * Sorts Instagram posts by their creation time.
112112+ * @param a - The first post to compare.
113113+ * @param b - The second post to compare.
114114+ * @returns A negative number if `a` should come before `b`, a positive number if `a` should come after `b`, or 0 if they are equal.
115115+ */
116116+export function sortPostsByCreationTime(a: InstagramExportedPost, b: InstagramExportedPost): number {
117117+ // Get the first posts media and compare timestamps.
118118+ const firstMedia = a.media[0];
119119+ const secondMedia = b.media[0];
120120+121121+ // If the first post has no media or creation timestamp, we skip it.
122122+ if (!firstMedia || firstMedia.creation_timestamp === undefined) {
123123+ logger.warn("No media or creation timestamp, sorting to bottom", a);
124124+ return 1; // Move this post to the end of the array
125125+ }
126126+ // If the second post has no media or creation timestamp, we skip it.
127127+ if (!secondMedia || secondMedia.creation_timestamp === undefined) {
128128+ logger.warn("No media or creation timestamp, sorting to bottom", b);
129129+ return -1; // Move this post to the end of the array
130130+ }
131131+132132+ const ad = firstMedia.creation_timestamp;
133133+ const bd = secondMedia.creation_timestamp;
134134+135135+ // Sort by creation timestamp, ascending order.
136136+ return ad - bd;
137137+}