-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.ts
155 lines (121 loc) · 5.21 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import {DOMParser} from "https://deno.land/x/[email protected]/deno-dom-wasm.ts";
import puppeteer, {Browser} from "https://deno.land/x/[email protected]/mod.ts";
type Pin = {
name: string;
url: string;
languages: {
name: string;
part: number;
color: string;
}[];
};
type Cache = {
username: string;
lastPinnedHTML: string;
data: Pin[];
lastUpdated: number;
};
const cache: Cache[] = [];
try {
cache.push(...JSON.parse(Deno.readTextFileSync("cache.json")));
} catch {
null;
}
setInterval(() => Deno.readTextFileSync("cache.json") !== JSON.stringify(cache, null, 6) && Deno.writeTextFileSync("cache.json", JSON.stringify(cache, null, 6)), 1000);
let browser: Browser | undefined;
const noImage = true;
const establishPuppeteerConnection = async () => {
if (noImage) return;
try {
browser = await puppeteer.launch({
executablePath: Deno.build.os !== "windows" ? "/usr/bin/chromium-browser" : "C:/Program Files/Google/Chrome/Application/chrome.exe",
headless: true,
timeout: 10000,
});
browser.on("disconnected", establishPuppeteerConnection);
browser.on("error", establishPuppeteerConnection);
browser.on("close", establishPuppeteerConnection);
} catch (error) {
console.error("Failed to establish Puppeteer connection", error);
await new Promise((resolve) => setTimeout(resolve, 1000));
await establishPuppeteerConnection();
}
};
await establishPuppeteerConnection();
Deno.serve({port: 8001}, async (req: Request) => {
const path = new URL(req.url).pathname.split("/").filter((x) => x !== "");
console.log(path);
const headers = new Headers();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET");
headers.set("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "GET" && path[0] === "pinned" && path[1] !== undefined && path.length === 2) {
const username = path[1].trim().toLowerCase();
const pins = await getPins(username);
headers.set("Content-Type", "text/html");
return new Response(Deno.readTextFileSync("embed.html").replace("/*SCRIPT*/", Deno.readTextFileSync("embed.js")).replace("/*PINS*/", JSON.stringify(pins).slice(1, -1)), {headers});
} else if (req.method === "GET" && path[0] === "raw" && path[1] !== undefined && path.length === 2) {
headers.set("Content-Type", "application/json");
return new Response(JSON.stringify(await getPins(path[1].trim())), {headers});
} else if (req.method === "GET" && path[0] === "image" && path[1] !== undefined && path.length === 2 && !noImage) {
const username = path[1].trim().toLowerCase();
const page = await browser!.newPage();
await page.setViewport({width: 8192, height: 8192});
await page.goto(`http://localhost:8001/pinned/${username}?cols=${new URL(req.url).searchParams.get("cols") || 3}&transparent=true`);
const image = await (await page.$("body"))?.screenshot({type: "png", omitBackground: true});
page.close();
headers.set("Content-Type", "image/png");
return new Response(image, {headers});
} else return new Response("Not found", {status: 404});
});
const getPins = async (username: string) => {
const cached = cache.find((x) => x.username === username);
if (cached !== undefined && Date.now() - cached.lastUpdated < 1000 * 60 * 5) return cached.data;
console.log("Fetching data for", username);
const userPageFetch = await fetch("https://github.com/" + username);
if (userPageFetch.status === 404) {
return [
{
name: "User not found",
url: "https://github.com/" + username,
languages: [{name: "404", part: 1, color: "#FF0000"}],
},
] as Pin[];
}
const userPage = new DOMParser().parseFromString(await userPageFetch.text(), "text/html");
const pins: Pin[] = [];
const pinElements = Array.from((userPage?.querySelectorAll(".js-pinned-items-reorder-list")[0] as unknown as HTMLOListElement)?.children || []);
// console.log(pinElements[0].parentElement?.innerHTML);
if (cached !== undefined && pinElements[0].parentElement?.innerHTML === cached.lastPinnedHTML) return cached.data;
for (const pin of pinElements) {
const repoUrl = pin.querySelector("a")?.getAttribute("href");
const repoPage = new DOMParser().parseFromString(await (await fetch("https://github.com" + repoUrl)).text(), "text/html");
const langs = Array.from((Array.from(repoPage!.querySelectorAll(".Layout-sidebar > div > div")).find((element) => element.textContent?.includes("Languages")) as unknown as HTMLDivElement)?.querySelector("div > ul")?.children || []).map((lang) => {
const language = lang.querySelectorAll("span")[0]?.textContent?.trim();
const part = Math.round(Number(lang.querySelectorAll("span")[1]?.textContent?.trim().replaceAll("%", "")) * 10) / 1000;
const color = (
lang
.querySelector("svg")
?.getAttribute("style")
?.match(/color:(.+);/)?.[1] || "#000000"
)?.toUpperCase();
return {name: language, part, color};
}) as unknown as Pin["languages"];
pins.push({
name: repoUrl?.split("/").pop() || "",
url: "https://github.com" + repoUrl,
languages: langs,
});
}
if (cached !== undefined) {
cached.data = pins;
cached.lastUpdated = Date.now();
} else
cache.push({
username,
lastPinnedHTML: pinElements[0].parentElement?.innerHTML || "",
data: pins,
lastUpdated: Date.now(),
});
return pins;
};