1import fs from 'node:fs';
2import { Resvg } from '@resvg/resvg-js';
3import moment from 'moment-timezone';
4import * as tests from './speedtests.js';
5import htm from 'htm';
6import satori from 'satori';
7
8const html = htm.bind((type, props, ...children) => ({type, props: {...props, children: children.flat()}}));
9
10const hasValues = (test) => Boolean(test?.download.avg && test?.upload.avg && test?.ping.avg);
11
12const readStatistics = async () => {
13 const formatDate = (d) => d.toISOString().split('T')[0];
14 const yesterday = new Date();
15 yesterday.setDate(yesterday.getDate() - 1);
16
17 const stats = await tests.listStatistics(formatDate(yesterday), formatDate(new Date()));
18 if (hasValues(stats)) return stats;
19
20 const latest = await tests.getLatest();
21 if (!latest || latest.error || latest.ping === -1) return null;
22
23 return {ping: {avg: latest.ping}, download: {avg: latest.download}, upload: {avg: latest.upload}};
24};
25
26const readAsset = async (req, path) => {
27 const local = [`build${path}`, `client/public${path}`].find(candidate => fs.existsSync(candidate));
28 if (local) return fs.promises.readFile(local);
29
30 const url = `${req.protocol}://${req.headers.host || req.hostname}${path}`;
31 return Buffer.from(await fetch(url).then(res => res.arrayBuffer()));
32};
33
34async function generateOpenGraphImage(req) {
35 const test = await readStatistics();
36
37 if (!hasValues(test)) return null;
38
39 const font = await readAsset(req, "/assets/fonts/inter-v12-latin-regular.ttf");
40 const logo = await readAsset(req, "/assets/img/logo192.png");
41
42 const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
43 const date = moment().tz(timeZone).format("MM/DD/YYYY");
44 const time = moment().tz(timeZone).format("h:mm A z");
45
46 const markup = html`
47 <div
48 tw="bg-[#1d2332] text-white rounded-lg w-full h-full flex flex-col p-16 justify-between"
49 >
50 <div tw="w-full flex-row flex justify-between items-center">
51 <div tw="flex flex-row items-center -mt-4">
52 <div tw="h-full items-start flex">
53 <svg
54 width="100"
55 height="100"
56 aria-hidden="true"
57 focusable="false"
58 data-prefix="fas"
59 data-icon="server"
60 role="img"
61 xmlns="http://www.w3.org/2000/svg"
62 viewBox="0 0 512 512"
63 >
64 <path
65 fill="currentColor"
66 d="M64 32C28.7 32 0 60.7 0 96v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64v64c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V352c0-35.3-28.7-64-64-64H64zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z"
67 ></path>
68 </svg>
69 </div>
70 <p tw="text-[100px] leading-[74px] font-bold m-0 ml-4 p-0">Pixies</p>
71 </div>
72 <div tw="flex flex-row h-full">
73 <div tw="flex flex-col h-full justify-around items-end pr-1">
74 <p tw="text-4xl leading-[30px] text-slate-500 m-0 p-0">${date}</p>
75 <span tw="text-4xl leading-[30px] text-slate-500 m-0 p-0">
76 ${time}
77 </span>
78 </div>
79 <div tw="flex h-[100px] w-[100px] mr-[50px]">
80 <img
81 tw="-mt-[50px]"
82 height=${200}
83 width=${200}
84 src="data:image/png;base64,${logo.toString("base64")}"
85 />
86 </div>
87 </div>
88 </div>
89 <div tw="w-full justify-between flex-row flex mt-[50px]">
90 <div tw="flex flex-col items-start">
91 <div tw="flex flex-row items-center">
92 <svg
93 tw="text-[#44c459] mr-4"
94 xmlns="http://www.w3.org/2000/svg"
95 width="80"
96 height="80"
97 viewBox="0 0 24 24"
98 fill="none"
99 stroke="currentColor"
100 strokeWidth="2"
101 strokeLinecap="round"
102 strokeLinejoin="round"
103 >
104 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
105 <polyline points="7 10 12 15 17 10" />
106 <line x1="12" x2="12" y1="15" y2="3" />
107 </svg>
108 <div tw="flex flex-row items-end">
109 <p tw="text-6xl m-0 p-0 font-semibold">DOWNLOAD</p>
110 <p tw="text-4xl m-0 p-0 text-slate-500 ml-1">Mbps</p>
111 </div>
112 </div>
113 <p tw="text-[160px] font-bold m-0 p-0">
114 ${String(test.download.avg)}
115 </p>
116 </div>
117 <div tw="flex flex-col items-end">
118 <div tw="flex flex-row items-center">
119 <svg
120 tw="text-[#4469c4] mr-4"
121 xmlns="http://www.w3.org/2000/svg"
122 width="80"
123 height="80"
124 viewBox="0 0 24 24"
125 fill="none"
126 stroke="currentColor"
127 strokeWidth="2"
128 strokeLinecap="round"
129 strokeLinejoin="round"
130 >
131 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
132 <polyline points="17 8 12 3 7 8" />
133 <line x1="12" x2="12" y1="3" y2="15" />
134 </svg>
135 <div tw="flex flex-row items-end">
136 <p tw="text-6xl m-0 p-0 font-semibold">UPLOAD</p>
137 <p tw="text-4xl m-0 p-0 text-slate-500 ml-1">Mbps</p>
138 </div>
139 </div>
140 <p tw="text-[160px] font-bold m-0 -mr-5 p-0">
141 ${String(test.upload.avg)}
142 </p>
143 </div>
144 </div>
145 <div tw="w-full flex-row flex justify-end mt-4">
146 <div tw="flex items-center">
147 <svg
148 width="40"
149 tw="text-[#fbbd08]"
150 aria-hidden="true"
151 focusable="false"
152 role="img"
153 xmlns="http://www.w3.org/2000/svg"
154 viewBox="0 0 640 512"
155 >
156 <path
157 fill="currentColor"
158 d="M480 288c-50.1 0-93.6 28.8-114.6 70.8L132.9 126.3l.6-.6 60.1-60.1c87.5-87.5 229.3-87.5 316.8 0c67.1 67.1 82.7 166.3 46.8 248.3C535.8 297.6 509 288 480 288zM113.3 151.9L354.1 392.7c-1.4 7.5-2.1 15.3-2.1 23.3c0 23.2 6.2 44.9 16.9 63.7c-3 .2-6.1 .3-9.2 .3H357c-33.9 0-66.5-13.5-90.5-37.5l-9.8-9.8c-13.1-13.1-34.6-12.4-46.8 1.7L152.2 501c-5.8 6.7-14.2 10.7-23 11s-17.5-3.1-23.8-9.4l-32-32c-6.3-6.3-9.7-14.9-9.4-23.8s4.3-17.2 11-23l66.6-57.7c14-12.2 14.8-33.7 1.7-46.8l-9.8-9.8c-24-24-37.5-56.6-37.5-90.5v-2.7c0-22.8 6.1-44.9 17.3-64.3zM480 320a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"
159 ></path>
160 </svg>
161 <div tw="flex flex-row items-end">
162 <p tw="text-5xl p-0 m-0">Ping</p>
163 <p tw="text-4xl leading-9 text-slate-500 p-0 m-0">ms</p>
164 </div>
165 <div tw="flex pl-4">
166 <p tw="text-5xl p-0 m-0">${String(test.ping.avg)}</p>
167 </div>
168 </div>
169 </div>
170 </div>
171 `;
172
173 const image = await satori(markup, {
174 width: 1200,
175 height: 600,
176 fonts: [
177 {
178 name: "Inter",
179 data: font,
180 weight: 400,
181 style: "normal",
182 },
183 ],
184 });
185
186 const svg = new Resvg(image);
187
188 return svg.render().asPng();
189}
190
191export default generateOpenGraphImage;
192