Skip to content

Commit 47ef43b

Browse files
committed
project code
1 parent 6877573 commit 47ef43b

File tree

10 files changed

+340
-0
lines changed

10 files changed

+340
-0
lines changed

src/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Attachment } from "./services/discord/types/attachment.types.js";
2+
import { Embed } from "./services/discord/types/embed.types.js";
3+
import WebhookService from "./services/discord/webhooks.js";
4+
import { PexelsPhotoService } from "./services/pexels/photos.js";
5+
import { Photo } from "./services/pexels/types.js";
6+
7+
let pexelsService: PexelsPhotoService, webhookService: WebhookService;
8+
9+
async function getRandomPexelsItem(): Promise<Photo> {
10+
const res = await pexelsService.searchPhoto({
11+
query: "frog",
12+
per_page: 50,
13+
page: 1,
14+
});
15+
if (res.photos.length == 0) throw new Error("No photos found");
16+
return res.photos[(Math.random() * res.photos.length) | 0];
17+
}
18+
19+
async function main() {
20+
const apiToken = process.env.PEXELS_API_TOKEN;
21+
if (!apiToken)
22+
throw new Error("Missing environment variable 'PEXELS_API_TOKEN'");
23+
24+
const webhook = process.env.DISCORD_WEBHOOK;
25+
if (!webhook)
26+
throw new Error("Missing environment variable 'DISCORD_WEBHOOK'");
27+
28+
pexelsService = new PexelsPhotoService(apiToken);
29+
webhookService = await new WebhookService().init(webhook);
30+
31+
const image = await getRandomPexelsItem();
32+
const embed: Embed = {
33+
title: image.id.toString(),
34+
description: image.alt,
35+
url: image.src.original,
36+
thumbnail: {
37+
url: image.src.tiny,
38+
},
39+
image: {
40+
url: image.src.original,
41+
height: 1000,
42+
width: 1000,
43+
},
44+
color: Number(image.avg_color.replace("#", "0x")),
45+
author: {
46+
name: `${image.photographer} on Pexels`,
47+
url: image.photographer_url,
48+
},
49+
footer: {
50+
text: "images provided by pexels",
51+
icon_url: "attachment://pexels.png",
52+
},
53+
fields: [
54+
{
55+
name: "width",
56+
value: image.width.toString(),
57+
inline: true,
58+
},
59+
{
60+
name: "height",
61+
value: image.height.toString(),
62+
inline: true,
63+
},
64+
],
65+
};
66+
67+
await webhookService.send_embeds([embed]);
68+
}
69+
70+
main().catch(console.error);

src/services/discord/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const API_BASE_URL = "https://discord.com/api";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* For more information, see [documentation](https://discord.com/developers/docs/resources/message#attachment-object)
3+
*/
4+
export type Attachment = {
5+
id: string;
6+
filename: string;
7+
title?: string;
8+
description?: string;
9+
content_type?: string;
10+
size?: number;
11+
url?: string;
12+
proxy_url?: string;
13+
height?: number;
14+
width?: number;
15+
ephermal?: boolean;
16+
duration_secs?: number;
17+
waveform?: string;
18+
flags?: number;
19+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* For more information, see the [documentation](https://discord.com/developers/docs/resources/message#embed-object)
3+
*/
4+
export type Embed = {
5+
title?: string;
6+
type?: EmbedType;
7+
description?: string;
8+
url?: string;
9+
timestamp?: Date;
10+
color?: number;
11+
footer?: EmbedFooter;
12+
image?: EmbedImage;
13+
thumbnail?: EmbedThumbnail;
14+
video?: EmbedVideo;
15+
provider?: EmbedProvider;
16+
author?: EmbedAuthor;
17+
fields?: EmbedField[];
18+
};
19+
20+
type EmbedType = "rich" | "image" | "video" | "gifv" | "article" | "link";
21+
22+
type EmbedFooter = {
23+
text: string;
24+
icon_url?: string;
25+
proxy_icon_url?: string;
26+
};
27+
28+
type EmbedImage = {
29+
url: string;
30+
proxy_url?: string;
31+
height?: number;
32+
width?: number;
33+
};
34+
35+
type EmbedThumbnail = {
36+
url: string;
37+
proxy_url?: string;
38+
height?: number;
39+
width?: number;
40+
};
41+
42+
type EmbedVideo = {
43+
url?: string;
44+
proxy_url?: string;
45+
height?: number;
46+
width?: number;
47+
};
48+
49+
type EmbedProvider = {
50+
name?: string;
51+
url?: string;
52+
};
53+
54+
type EmbedAuthor = {
55+
name: string;
56+
url?: string;
57+
icon_url?: string;
58+
proxy_icon_url?: string;
59+
};
60+
61+
type EmbedField = {
62+
name: string;
63+
value: string;
64+
inline?: boolean;
65+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Attachment } from "./attachment.types.js";
2+
import { Embed } from "./embed.types.js";
3+
4+
type BaseExecuteParams = {
5+
username?: string;
6+
avatar_url?: string;
7+
tts?: boolean;
8+
allowed_mentions?: "roles" | "users" | "everyone";
9+
attachments?: Attachment[];
10+
thread_name?: string;
11+
applied_tags?: string[];
12+
};
13+
14+
export type EmbedExecuteParams = BaseExecuteParams & {
15+
embeds?: Embed[];
16+
};
17+
18+
export type ContentExecuteParams = BaseExecuteParams & {
19+
content: string;
20+
};

src/services/discord/webhooks.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { API_BASE_URL } from "./constants.js";
2+
import { Attachment } from "./types/attachment.types.js";
3+
import { Embed } from "./types/embed.types.js";
4+
import { EmbedExecuteParams } from "./types/webhook.types.js";
5+
6+
export default class WebhookService {
7+
private __token: string;
8+
private __webhookId: string;
9+
10+
public async init(webhook: string) {
11+
const response = await fetch(webhook);
12+
if (!response.ok) {
13+
const message = await response.text();
14+
throw new Error(
15+
`Unable to retrieve webhook details [${response.status} - ${response.statusText}]`,
16+
);
17+
}
18+
19+
const data = await response.json();
20+
this.__token = data["token"];
21+
this.__webhookId = data["id"];
22+
return this;
23+
}
24+
25+
private getWebhookUrl() {
26+
return `${API_BASE_URL}/webhooks/${this.__webhookId}/${this.__token}`;
27+
}
28+
29+
public async send_embeds(embeds: Embed[], attachments?: Attachment[]) {
30+
if (embeds.length == 0)
31+
throw new Error("Expected at least 1, and at max 10 embeds.");
32+
if (embeds.length > 10)
33+
throw new Error(
34+
"Can only send a maximum of 10 embeds via discord webhooks.",
35+
);
36+
for (let i = 0; i < embeds.length; ++i) embeds[i].type = "rich";
37+
38+
const headers = { "Content-Type": "application/json" };
39+
const data: EmbedExecuteParams = {
40+
embeds,
41+
...(((attachments?.length ?? 0) > 0 && { attachments }) || {}),
42+
};
43+
44+
const res = await fetch(this.getWebhookUrl(), {
45+
method: "POST",
46+
headers: headers,
47+
body: JSON.stringify(data),
48+
});
49+
50+
if (!res.ok) {
51+
const message = await res.text();
52+
throw new Error(
53+
`Failed to send the embeds [${res.status} - ${res.statusText}]\n${message}`,
54+
);
55+
}
56+
}
57+
}

src/services/pexels/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const API_BASE_URL = "https://api.pexels.com";
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Color, Locale, Orientation, Size, Photo } from "./types.ts";
2+
3+
export interface ISearchPhotoParams {
4+
query?: string;
5+
orientation?: Orientation;
6+
size?: Size;
7+
color?: Color;
8+
locale?: Locale;
9+
page?: number;
10+
per_page?: number;
11+
}
12+
13+
export interface ISearchPhotoResponse {
14+
photos: Photo[];
15+
page: number;
16+
per_page: number;
17+
total_results: number;
18+
prev_page: number;
19+
next_page: number;
20+
}

src/services/pexels/photos.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { API_BASE_URL } from "./constants.js";
2+
import {
3+
ISearchPhotoParams,
4+
ISearchPhotoResponse,
5+
} from "./photos.interfaces.js";
6+
7+
export class PexelsPhotoService {
8+
private __token: string;
9+
10+
constructor(apiToken: string) {
11+
this.__token = apiToken;
12+
}
13+
14+
public async searchPhoto(
15+
params: ISearchPhotoParams,
16+
): Promise<ISearchPhotoResponse> {
17+
const sParams = new URLSearchParams();
18+
const entries = Object.entries(params);
19+
for (let i = 0; i < entries.length; i++) {
20+
const [k, v] = entries[i];
21+
if (v !== undefined) sParams.set(k, v.toString());
22+
}
23+
24+
const response = await fetch(
25+
`${API_BASE_URL}/v1/search?${sParams.toString()}`,
26+
{
27+
headers: {
28+
Authorization: this.__token,
29+
},
30+
},
31+
);
32+
33+
if (!response.ok)
34+
throw new Error(
35+
`Error searching for images [code: ${response.status}]`,
36+
);
37+
return await response.json();
38+
}
39+
}

src/services/pexels/types.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
type HexColor = string;
2+
function assertHexColor(value: string): asserts value is HexColor {
3+
const hexRegex = /^#([0-9a-fA-F]{2}){3}$/;
4+
if (!hexRegex.test(value)) {
5+
throw new Error(`${value} is not a valid hexadecimal color code.`);
6+
}
7+
}
8+
9+
export type Orientation = "portrait" | "landscape" | "square";
10+
export type Size = "small" | "medium" | "large";
11+
export type Color =
12+
| "red"
13+
| "orange"
14+
| "yellow"
15+
| "green"
16+
| "turquoise"
17+
| "blue"
18+
| "violet"
19+
| "pink"
20+
| "brown"
21+
| "black"
22+
| "grey"
23+
| "white"
24+
| HexColor;
25+
export type Locale = string;
26+
27+
export type Photo = {
28+
id: number;
29+
width: number;
30+
height: number;
31+
url: string;
32+
photographer: string;
33+
photographer_url: string;
34+
photographer_id: number;
35+
avg_color: HexColor;
36+
src: {
37+
original: string;
38+
large2x: string;
39+
large: string;
40+
medium: string;
41+
small: string;
42+
portrait: string;
43+
landscape: string;
44+
tiny: string;
45+
};
46+
liked: boolean;
47+
alt: string;
48+
};

0 commit comments

Comments
 (0)