Skip to content

Commit

Permalink
fix(image-logs): updated for Discord's new stuff (#2501)
Browse files Browse the repository at this point in the history
* fix(image-logs): updated for Discord's new stuff

* tests: resolved multiple issues

* tests: use matrix tests
  • Loading branch information
kyranet authored Oct 5, 2023
1 parent 2dbf311 commit d25bae4
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 136 deletions.
19 changes: 10 additions & 9 deletions src/lib/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export interface ImageAttachment {
*/
export function getAttachment(message: Message): ImageAttachment | null {
if (message.attachments.size) {
const attachment = message.attachments.find((att) => IMAGE_EXTENSION.test(att.url));
const attachment = message.attachments.find((att) => IMAGE_EXTENSION.test(att.name ?? att.url));
if (attachment) {
return {
url: attachment.url,
Expand All @@ -98,14 +98,6 @@ export function getAttachment(message: Message): ImageAttachment | null {
}

for (const embed of message.embeds) {
if (embed.type === 'image') {
return {
url: embed.thumbnail!.url,
proxyURL: embed.thumbnail!.proxyURL!,
height: embed.thumbnail!.height!,
width: embed.thumbnail!.width!
};
}
if (embed.image) {
return {
url: embed.image.url,
Expand All @@ -114,6 +106,15 @@ export function getAttachment(message: Message): ImageAttachment | null {
width: embed.image.width!
};
}

if (embed.thumbnail) {
return {
url: embed.thumbnail.url,
proxyURL: embed.thumbnail.proxyURL!,
height: embed.thumbnail.height!,
width: embed.thumbnail.width!
};
}
}

return null;
Expand Down
16 changes: 11 additions & 5 deletions src/listeners/guilds/messages/guildUserMessageImageNotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LanguageKeys } from '#lib/i18n/languageKeys';
import type { GuildMessage } from '#lib/types';
import { Events } from '#lib/types/Enums';
import { Colors } from '#utils/constants';
import { IMAGE_EXTENSION, getFullEmbedAuthor } from '#utils/util';
import { getFullEmbedAuthor } from '#utils/util';
import { ApplyOptions } from '@sapphire/decorators';
import { FetchResultTypes, fetch } from '@sapphire/fetch';
import { Listener, ListenerOptions } from '@sapphire/framework';
Expand Down Expand Up @@ -34,13 +34,14 @@ export class UserListener extends Listener {
]);
if (isNullish(logChannelId) || ignoredChannels.includes(message.channel.id)) return;

for (const image of this.getAttachments(message)) {
const dimensions = this.getDimensions(image.width, image.height);
for (const attachment of this.getAttachments(message)) {
const dimensions = this.getDimensions(attachment.width, attachment.height);

// Create a new image url with search params.
const url = new URL(image.proxyURL);
const url = new URL(attachment.proxyURL);
url.searchParams.append('width', dimensions.width.toString());
url.searchParams.append('height', dimensions.height.toString());
if (attachment.kind === 'video') url.searchParams.append('format', 'webp');

// Fetch the image.
const result = await fetch(url, FetchResultTypes.Result).catch((error) => {
Expand Down Expand Up @@ -82,9 +83,14 @@ export class UserListener extends Listener {

private *getAttachments(message: GuildMessage) {
for (const attachment of message.attachments.values()) {
if (!IMAGE_EXTENSION.test(attachment.url)) continue;
const type = attachment.contentType;
if (type === null) continue;

const [kind] = type.split('/', 1);
if (kind !== 'image' && kind !== 'video') continue;

yield {
kind: kind as 'image' | 'video',
url: attachment.url,
proxyURL: attachment.proxyURL,
height: attachment.height!,
Expand Down
184 changes: 71 additions & 113 deletions tests/lib/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { createUser } from '#mocks/MockInstances';
import * as utils from '#utils/util';
import { Collection } from '@discordjs/collection';
import type { DeepPartial } from '@sapphire/utilities';
import type { APIAttachment } from 'discord-api-types/v9';
import { Message, MessageAttachment, MessageEmbed } from 'discord.js';
import { mockRandom, resetMockRandom } from 'jest-mock-random';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';

describe('Utils', () => {
describe('IMAGE_EXTENSION', () => {
Expand Down Expand Up @@ -41,7 +40,7 @@ describe('Utils', () => {
});

test('GIVEN negative rational number THEN returns level 0 (😪)', () => {
expect(utils.oneToTen(2 / 3)).toStrictEqual({ color: 5968128, emoji: '😪' });
expect(utils.oneToTen(-2 / 3)).toStrictEqual({ color: 5968128, emoji: '😪' });
});

test('GIVEN positive integer number THEN returns level 2 (😫)', () => {
Expand Down Expand Up @@ -200,117 +199,76 @@ describe('Utils', () => {
});

describe('getImage', () => {
test('GIVEN message w/ attachments w/ image w/o proxyURL attachment THEN returns url', async () => {
const filePath = resolve(__dirname, '..', 'mocks', 'image.png');
const buffer = await readFile(filePath);
const fakeAttachment = new MessageAttachment(buffer, 'image.png');
fakeAttachment.url = filePath;
fakeAttachment.height = 32;
fakeAttachment.width = 32;

const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>([['image.png', fakeAttachment]]),
embeds: []
};

// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toEqual(filePath);
});

test('GIVEN message w/ attachments w/ image w/ proxyURL attachment THEN returns url', async () => {
const filePath = resolve(__dirname, '..', 'mocks', 'image.png');
const buffer = await readFile(filePath);
const fakeAttachment = new MessageAttachment(buffer, 'image.png');
fakeAttachment.url = filePath;
fakeAttachment.proxyURL = filePath;
fakeAttachment.height = 32;
fakeAttachment.width = 32;

const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>([['image.png', fakeAttachment]]),
embeds: []
};

// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toEqual(filePath);
});

test('GIVEN message w/ attachments w/o image attachment THEN passes through to embed checking', async () => {
const filePath = resolve(__dirname, '..', 'mocks', 'image.png');
const buffer = await readFile(filePath);
const fakeAttachment = new MessageAttachment(buffer, 'image.png');
fakeAttachment.url = 'not_an_image';
fakeAttachment.proxyURL = 'not_an_image';
fakeAttachment.height = 32;
fakeAttachment.width = 32;

const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>([['image.png', fakeAttachment]]),
embeds: [
{
type: 'image',
thumbnail: { url: 'image.png', proxyURL: 'image.png', height: 32, width: 32 }
}
]
};

// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toEqual('image.png');
});

test('GIVEN message w/o attachments w/ embed type === image THEN returns embedded image url', () => {
const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>(),
embeds: [
{
type: 'image',
thumbnail: { url: 'image.png', proxyURL: 'image.png', height: 32, width: 32 }
}
]
};

const _Query = new URLSearchParams({
ex: '651c15b6',
is: '651ac436',
hm: 'b0227f7dce067d2f83880cd01f59a5856885af9204940f8c666dd81f257796c6'
}).toString();

function createAttachment(data: APIAttachment): MessageAttachment {
return new MessageAttachment(data.url, data.filename, data);
}

function createAttachments(attachment?: MessageAttachment | undefined) {
const collection = new Collection<string, MessageAttachment>();
if (attachment) collection.set(attachment.id, attachment);
return collection;
}

function createEmbed(name: 'image' | 'thumbnail'): MessageEmbed {
return new MessageEmbed({
[name]: {
url: `https://cdn.discordapp.com/attachments/222222222222222222/222222222222222222/image.png?${_Query}&`,
proxy_url: `https://media.discordapp.net/attachments/222222222222222222/222222222222222222/image.png?${_Query}&`,
width: 32,
height: 32
}
} as const);
}

function getImage(message: DeepPartial<Message>) {
// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toEqual('image.png');
});

test('GIVEN message w/o attachments w/ embed w/ image THEN returns embedded image url', () => {
const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>(),
embeds: [
{
type: 'not_image',
image: { url: 'image.png', proxyURL: 'image.png', height: 32, width: 32 }
}
]
};

// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toEqual('image.png');
});

test('GIVEN message w/o attachments w/ embed w/o image THEN returns null', () => {
const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>(),
embeds: [
{
type: 'not_image',
image: undefined
}
]
};

// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toBeNull();
});

test('GIVEN message w/o attachments w/o embed THEN returns null', () => {
const fakeMessage: DeepPartial<Message> = {
attachments: new Collection<string, MessageAttachment>(),
embeds: []
};

// @ts-expect-error We're only passing partial data to not mock an entire message
expect(utils.getImage(fakeMessage)).toBeNull();
return utils.getImage(message);
}

describe.each`
embed | description
${null} | ${'no embeds'}
${'image'} | ${'image embed'}
${'thumbnail'} | ${'thumbnail embed'}
`('GIVEN message WITH $description', ({ embed }: { embed: null | 'image' | 'thumbnail' }) => {
const AttachmentImage = createAttachment({
id: '1111111111111111111',
filename: 'image.png',
content_type: 'image/png',
url: `https://cdn.discordapp.com/attachments/111111111111111111/111111111111111111/image.png?${_Query}&`,
proxy_url: `https://media.discordapp.net/attachments/111111111111111111/111111111111111111/image.png?${_Query}&`,
size: 2463,
width: 32,
height: 32
} as const);
const AttachmentText = createAttachment({
id: '1111111111111111111',
filename: 'text.txt',
content_type: 'text/plain; charset=utf-8',
url: `https://cdn.discordapp.com/attachments/111111111111111111/111111111111111111/text.txt?${_Query}&`,
proxy_url: `https://media.discordapp.net/attachments/111111111111111111/111111111111111111/text.txt?${_Query}&`,
size: 4
} as const);

const embeds: MessageEmbed[] = embed === null ? [] : [createEmbed(embed)];
const ExpectedEmbedImageURL = embed === null ? null : embeds[0][embed]!.proxyURL;
const ExpectedReturn = embed === null ? 'null' : `embed ${embed} URL`;
test.each`
attachment | returns | expected | description
${undefined} | ${ExpectedReturn} | ${ExpectedEmbedImageURL} | ${'no attachments'}
${AttachmentText} | ${ExpectedReturn} | ${ExpectedEmbedImageURL} | ${'non-image attachment'}
${AttachmentImage} | ${'attachment URL'} | ${AttachmentImage.proxyURL} | ${'image attachment'}
`(`AND $description THEN returns $returns`, ({ attachment, expected }) => {
const message: DeepPartial<Message> = { attachments: createAttachments(attachment), embeds };

expect(getImage(message)).toEqual(expected);
});
});
});

Expand Down
34 changes: 25 additions & 9 deletions tests/mocks/MockInstances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ import { LanguageKeys } from '#lib/i18n/languageKeys';
import { SkyraCommand } from '#lib/structures';
import { CLIENT_OPTIONS } from '#root/config';
import { SapphireClient } from '@sapphire/framework';
import { APIChannel, APIGuild, APIGuildMember, APIRole, APIUser, ChannelType, GuildFeature, GuildNSFWLevel } from 'discord-api-types/v9';
import {
APIChannel,
APIGuild,
APIGuildMember,
APIRole,
APIUser,
ChannelType,
GuildFeature,
GuildNSFWLevel,
GuildSystemChannelFlags
} from 'discord-api-types/v9';
import { Guild, GuildMember, Role, TextChannel, User } from 'discord.js';
import { resolve } from 'node:path';

Expand All @@ -16,7 +26,7 @@ export const userData: APIUser = {
};

export function createUser(data: Partial<APIUser> = {}) {
return new User(client, { ...userData, ...data });
return Reflect.construct(User, [client, { ...userData, ...data }]) as User;
}

export const guildMemberData: APIGuildMember = {
Expand All @@ -30,7 +40,11 @@ export const guildMemberData: APIGuildMember = {
};

export function createGuildMember(data: Partial<APIGuildMember> = {}, g: Guild = guild) {
return new GuildMember(client, { ...guildMemberData, ...data, user: { ...guildMemberData.user, ...data.user! } }, g);
return Reflect.construct(GuildMember, [
client,
{ ...guildMemberData, ...data, user: { ...guildMemberData.user, ...data.user! } },
g
]) as GuildMember;
}

export const roleData: APIRole = {
Expand All @@ -45,7 +59,7 @@ export const roleData: APIRole = {
};

export function createRole(data: Partial<APIRole> = {}, g: Guild = guild) {
const role = new Role(client, { ...roleData, ...data }, g);
const role = Reflect.construct(Role, [client, { ...roleData, ...data }, g]) as Role;
g.roles.cache.set(role.id, role);
return role;
}
Expand Down Expand Up @@ -79,14 +93,16 @@ export const guildData: APIGuild = {
owner_id: '242043489611808769',
preferred_locale: 'en-US',
premium_subscription_count: 3,
premium_progress_bar_enabled: false,
premium_tier: 1,
public_updates_channel_id: '700806874294911067',
region: 'eu-central',
roles: [roleData],
rules_channel_id: '409663610780909569',
splash: null,
hub_type: null,
stickers: [],
system_channel_flags: 0,
system_channel_flags: GuildSystemChannelFlags.SuppressJoinNotifications,
system_channel_id: '254360814063058944',
vanity_url_code: null,
verification_level: 2,
Expand All @@ -95,7 +111,7 @@ export const guildData: APIGuild = {
};

export function createGuild(data: Partial<APIGuild> = {}) {
const g = new Guild(client, { ...guildData, ...data });
const g = Reflect.construct(Guild, [client, { ...guildData, ...data }]) as Guild;
client.guilds.cache.set(g.id, g);
return g;
}
Expand All @@ -117,7 +133,7 @@ export const textChannelData: APIChannel = {
};

export function createTextChannel(data: Partial<APIChannel> = {}, g: Guild = guild) {
const c = new TextChannel(guild, { ...textChannelData, ...data });
const c = Reflect.construct(TextChannel, [guild, { ...textChannelData, ...data }]) as TextChannel;
g.channels.cache.set(c.id, c);
g.client.channels.cache.set(c.id, c);
return c;
Expand Down Expand Up @@ -175,8 +191,8 @@ addCommand(
root: '/home/skyra/commands'
},
{
description: LanguageKeys.Commands.Tools.DefineDescription,
detailedDescription: LanguageKeys.Commands.Tools.DefineExtended,
description: LanguageKeys.Commands.Tools.AvatarDescription,
detailedDescription: LanguageKeys.Commands.Tools.AvatarExtended,
aliases: ['def', 'definition', 'defination', 'dictionary'],
fullCategory: ['Tools', 'Dictionary']
}
Expand Down

0 comments on commit d25bae4

Please sign in to comment.