- commit
- dd84033
- parent
- 0aed6a1
- author
- Eric Bower
- date
- 2022-11-27 19:11:27 +0000 UTC
feat: support srht for plugin codeforge source (#202)
12 files changed,
+547,
-216
+1,
-0
1@@ -7,6 +7,7 @@ on:
2 env:
3 GITHUB_ACCESS_TOKEN: ${{secrets.ACCESS_TOKEN}}
4 GITHUB_USERNAME: ${{secrets.USERNAME}}
5+ SRHT_ACCESS_TOKEN: ${{secrets.SRHT_ACCESS_TOKEN}}
6
7 jobs:
8 build:
M
Makefile
+17,
-1
1@@ -6,11 +6,27 @@ resource:
2 deno run --allow-write src/scripts/resource.ts
3 .PHONY: resource
4
5-scrape:
6+download:
7 deno run --allow-write --allow-net src/scripts/scrape.ts
8+.PHONY: download
9+
10+patch:
11 deno run --allow-write src/scripts/patch.ts
12+.PHONY: patch
13+
14+process:
15 deno run --allow-write --allow-env --allow-net src/scripts/process.ts
16+.PHONY: process
17+
18+missing:
19+ deno run --allow-write --allow-env --allow-net --allow-read src/scripts/process.ts missing
20+.PHONY: missing
21+
22+html:
23 deno run --allow-write --allow-read src/scripts/html.ts
24+.PHONY: html
25+
26+scrape: download patch process html
27 .PHONY: scrape
28
29 clean:
+105,
-28
1@@ -4,115 +4,155 @@
2 "type": "github",
3 "username": "frabjous",
4 "repo": "knap",
5- "tags": ["latex", "markdown"]
6+ "tags": [
7+ "latex",
8+ "markdown"
9+ ]
10 },
11 {
12 "type": "github",
13 "username": "jceb",
14 "repo": "blinds.nvim",
15- "tags": ["color"]
16+ "tags": [
17+ "color"
18+ ]
19 },
20 {
21 "type": "github",
22 "username": "theory-of-everything",
23 "repo": "nii-nvim",
24- "tags": ["preconfigured-configuration"]
25+ "tags": [
26+ "preconfigured-configuration"
27+ ]
28 },
29 {
30 "type": "github",
31 "username": "nvim-neo-tree",
32 "repo": "neo-tree.nvim",
33- "tags": ["file-explorer"]
34+ "tags": [
35+ "file-explorer"
36+ ]
37 },
38 {
39 "type": "github",
40 "username": "j-hui",
41 "repo": "fidget.nvim",
42- "tags": ["neovim-0.5"]
43+ "tags": [
44+ "neovim-0.5"
45+ ]
46 },
47 {
48 "type": "github",
49 "username": "slugbyte",
50 "repo": "unruly-worker",
51- "tags": ["keybinding", "workman-layout"]
52+ "tags": [
53+ "keybinding",
54+ "workman-layout"
55+ ]
56 },
57 {
58 "type": "github",
59 "username": "phha",
60 "repo": "zenburn.nvim",
61- "tags": ["tree-sitter-supported-colorscheme"]
62+ "tags": [
63+ "tree-sitter-supported-colorscheme"
64+ ]
65 },
66 {
67 "type": "github",
68 "username": "mrjones2014",
69 "repo": "smart-splits.nvim",
70- "tags": ["split-and-window"]
71+ "tags": [
72+ "split-and-window"
73+ ]
74 },
75 {
76 "type": "github",
77 "username": "linty-org",
78 "repo": "readline.nvim",
79- "tags": ["editing-support"]
80+ "tags": [
81+ "editing-support"
82+ ]
83 },
84 {
85 "type": "github",
86 "username": "linty-org",
87 "repo": "key-menu.nvim",
88- "tags": ["keybinding"]
89+ "tags": [
90+ "keybinding"
91+ ]
92 },
93 {
94 "type": "github",
95 "username": "koenverburg",
96 "repo": "minimal-tabline.nvim",
97- "tags": ["tabline"]
98+ "tags": [
99+ "tabline"
100+ ]
101 },
102 {
103 "type": "github",
104 "username": "koenverburg",
105 "repo": "peepsight.nvim",
106- "tags": ["color"]
107+ "tags": [
108+ "color"
109+ ]
110 },
111 {
112 "type": "github",
113 "username": "koenverburg",
114 "repo": "cmd-palette.nvim",
115- "tags": ["utility"]
116+ "tags": [
117+ "utility"
118+ ]
119 },
120 {
121 "type": "github",
122 "username": "wuelnerdotexe",
123 "repo": "vim-enfocado",
124- "tags": ["tree-sitter-supported-colorscheme"]
125+ "tags": [
126+ "tree-sitter-supported-colorscheme"
127+ ]
128 },
129 {
130 "type": "github",
131 "username": "strash",
132 "repo": "everybody-wants-that-line.nvim",
133- "tags": ["statusline"]
134+ "tags": [
135+ "statusline"
136+ ]
137 },
138 {
139 "type": "github",
140 "username": "Massolari",
141 "repo": "forem.nvim",
142- "tags": ["utility"]
143+ "tags": [
144+ "utility"
145+ ]
146 },
147 {
148 "type": "github",
149 "username": "kylechui",
150 "repo": "nvim-surround",
151- "tags": ["formatting"]
152+ "tags": [
153+ "formatting"
154+ ]
155 },
156 {
157 "type": "github",
158 "username": "ktunprasert",
159 "repo": "gui-font-resize.nvim",
160- "tags": ["utility"]
161+ "tags": [
162+ "utility"
163+ ]
164 },
165 {
166 "type": "github",
167 "username": "brenoprata10",
168 "repo": "nvim-highlight-colors",
169- "tags": ["color"]
170+ "tags": [
171+ "color"
172+ ]
173 },
174 {
175 "type": "github",
176@@ -128,49 +168,86 @@
177 "type": "github",
178 "username": "lcheylus",
179 "repo": "overlength.nvim",
180- "tags": ["editing-support"]
181+ "tags": [
182+ "editing-support"
183+ ]
184 },
185 {
186 "type": "github",
187 "username": "justinhj",
188 "repo": "battery.nvim",
189- "tags": ["statusline", "diagnostics"]
190+ "tags": [
191+ "statusline",
192+ "diagnostics"
193+ ]
194 },
195 {
196 "type": "github",
197 "username": "danielfalk",
198 "repo": "smart-open.nvim",
199- "tags": ["fuzzy-finder"]
200+ "tags": [
201+ "fuzzy-finder"
202+ ]
203 },
204 {
205 "type": "github",
206 "username": "Saverio976",
207 "repo": "music.nvim",
208- "tags": ["media", "utility"]
209+ "tags": [
210+ "media",
211+ "utility"
212+ ]
213 },
214 {
215 "type": "github",
216 "username": "Vonr",
217 "repo": "align.nvim",
218- "tags": ["formatting"]
219+ "tags": [
220+ "formatting"
221+ ]
222 },
223 {
224 "type": "github",
225 "username": "Wansmer",
226 "repo": "treesj",
227- "tags": ["refactoring", "treesitter", "splitjoin"]
228+ "tags": [
229+ "refactoring",
230+ "treesitter",
231+ "splitjoin"
232+ ]
233 },
234 {
235 "type": "github",
236 "username": "lvim-tech",
237 "repo": "lvim",
238- "tags": ["preconfigured-configuration"]
239+ "tags": [
240+ "preconfigured-configuration"
241+ ]
242 },
243 {
244 "type": "github",
245 "username": "TimotheeSai",
246 "repo": "git-sessions.nvim",
247- "tags": ["session", "git"]
248+ "tags": [
249+ "session",
250+ "git"
251+ ]
252+ },
253+ {
254+ "type": "srht",
255+ "username": "vigoux",
256+ "repo": "azy.nvim",
257+ "tags": [
258+ "fuzzy-finder"
259+ ]
260+ },
261+ {
262+ "type": "srht",
263+ "username": "vigoux",
264+ "repo": "complementree.nvim",
265+ "tags": [
266+ "completion"
267+ ]
268 }
269 ]
270-}
271+}
+9,
-1
1@@ -4040,7 +4040,15 @@
2 ]
3 },
4 {
5- "type": "github",
6+ "type": "srht",
7+ "username": "vigoux",
8+ "repo": "azy.nvim",
9+ "tags": [
10+ "fuzzy-finder"
11+ ]
12+ },
13+ {
14+ "type": "srht",
15 "username": "vigoux",
16 "repo": "complementree.nvim",
17 "tags": [
+1,
-0
1@@ -2,6 +2,7 @@ import type { Plugin, Resource } from "./types.ts";
2
3 export const createPlugin = (p: Partial<Plugin> = {}): Plugin => {
4 return {
5+ type: "github",
6 id: "",
7 name: "",
8 username: "",
+172,
-0
1@@ -0,0 +1,172 @@
2+import type { FetchRepoProps, Resp } from "./types.ts";
3+
4+function delay(ms: number): Promise<void> {
5+ return new Promise((resolve) => setTimeout(() => resolve(), ms));
6+}
7+
8+async function githubApi<D = any>(
9+ endpoint: string,
10+ token: string,
11+): Promise<Resp<D>> {
12+ const url = `https://api.github.com${endpoint}`;
13+ console.log(`Fetching ${url}`);
14+ const res = await fetch(url, {
15+ headers: { Authorization: `Basic ${token}` },
16+ });
17+
18+ const rateLimitRemaining = parseInt(
19+ res.headers.get("X-RateLimit-Remaining") || "0",
20+ );
21+ const rateLimitReset = parseInt(res.headers.get("X-RateLimit-Reset") || "0");
22+ console.log(`rate limit remaining: ${rateLimitRemaining}`);
23+ if (rateLimitRemaining === 1) {
24+ const now = Date.now();
25+ const RESET_BUFFER = 500;
26+ const wait = rateLimitReset + RESET_BUFFER - now;
27+ console.log(
28+ `About to hit github rate limit, waiting ${wait * 1000} seconds`,
29+ );
30+ await delay(wait);
31+ }
32+
33+ let data = null;
34+ try {
35+ data = await res.json();
36+ } catch {
37+ return {
38+ ok: false,
39+ data: {
40+ status: res.status,
41+ error: new Error(`JSON parsing error [${url}]`),
42+ },
43+ };
44+ }
45+
46+ if (res.ok) {
47+ return {
48+ ok: true,
49+ data,
50+ };
51+ }
52+
53+ return {
54+ ok: false,
55+ data: {
56+ status: res.status,
57+ error: new Error(`Could not load [${url}]`),
58+ },
59+ };
60+}
61+
62+async function fetchReadme({
63+ username,
64+ repo,
65+ token,
66+}: FetchRepoProps): Promise<Resp<string>> {
67+ const result = await githubApi(`/repos/${username}/${repo}/readme`, token);
68+ if (!result.ok) {
69+ return {
70+ ok: false,
71+ data: result.data as any,
72+ };
73+ }
74+
75+ const url = result.data.download_url;
76+ console.log(`Fetching ${url}`);
77+ const readme = await fetch(url);
78+ const data = await readme.text();
79+ return {
80+ ok: true,
81+ data,
82+ };
83+}
84+
85+interface RepoData {
86+ name: string;
87+ html_url: string;
88+ homepage: string;
89+ default_branch: string;
90+ open_issues_count: number;
91+ watchers_count: number;
92+ forks: number;
93+ stargazers_count: number;
94+ subscribers_count: number;
95+ network_count: number;
96+ description: string;
97+ created_at: string;
98+ updated_at: string;
99+}
100+
101+async function fetchRepo({
102+ username,
103+ repo,
104+ token,
105+}: FetchRepoProps): Promise<Resp<RepoData>> {
106+ const result = await githubApi(`/repos/${username}/${repo}`, token);
107+ return result;
108+}
109+
110+interface BranchData {
111+ commit: {
112+ commit: {
113+ committer: {
114+ date: string;
115+ };
116+ };
117+ };
118+}
119+
120+async function fetchBranch({
121+ username,
122+ repo,
123+ branch,
124+ token,
125+}: FetchRepoProps & { branch: string }): Promise<Resp<BranchData>> {
126+ const result = await githubApi(
127+ `/repos/${username}/${repo}/branches/${branch}`,
128+ token,
129+ );
130+ return result;
131+}
132+
133+interface GithubData {
134+ readme: string;
135+ repo: RepoData;
136+ branch: Resp<BranchData>;
137+}
138+
139+export async function fetchGithubData(
140+ props: FetchRepoProps,
141+): Promise<Resp<GithubData>> {
142+ const repo = await fetchRepo(props);
143+ if (repo.ok === false) {
144+ console.log(`${repo.data.status}: ${repo.data.error.message}`);
145+ return repo;
146+ }
147+
148+ const branch = await fetchBranch({
149+ ...props,
150+ branch: repo.data.default_branch,
151+ });
152+ if (branch.ok === false) {
153+ console.log(`${branch.data.status}: ${branch.data.error.message}`);
154+ }
155+
156+ const readme = await fetchReadme({
157+ username: props.username,
158+ repo: props.repo,
159+ token: props.token,
160+ });
161+ if (readme.ok === false) {
162+ console.log(`${readme.data.status}: ${readme.data.error.message}`);
163+ }
164+
165+ return {
166+ ok: true,
167+ data: {
168+ readme: readme.ok ? readme.data : "",
169+ repo: repo.data,
170+ branch,
171+ },
172+ };
173+}
+2,
-1
1@@ -16,7 +16,8 @@ async function clean() {
2 marked.use({
3 walkTokens: (token: any) => {
4 const domain = "https://github.com";
5- const pre = `${domain}/${plugin.username}/${plugin.repo}/blob/${plugin.branch}`;
6+ const pre =
7+ `${domain}/${plugin.username}/${plugin.repo}/blob/${plugin.branch}`;
8
9 if (token.type === "link" || token.type === "image") {
10 if (
+62,
-171
1@@ -3,9 +3,12 @@ import resourceFile from "../../data/resources.json" assert { type: "json" };
2 import { encode } from "../deps.ts";
3 import type { Plugin, Resource } from "../types.ts";
4 import { createPlugin } from "../entities.ts";
5+import { fetchGithubData } from "../github.ts";
6+import { fetchSrhtData } from "../stht.ts";
7
8 const accessToken = Deno.env.get("GITHUB_ACCESS_TOKEN") || "";
9 const accessUsername = Deno.env.get("GITHUB_USERNAME") || "";
10+const srhtToken = Deno.env.get("SRHT_ACCESS_TOKEN") || "";
11
12 const option = Deno.args[0];
13 if (option === "missing") {
14@@ -18,11 +21,6 @@ if (option === "missing") {
15 .catch(console.error);
16 }
17
18-interface Props {
19- username: string;
20- repo: string;
21-}
22-
23 async function processMissingResources() {
24 const dbFile = await Deno.readTextFile("./data/db.json");
25 const db = JSON.parse(dbFile.toString());
26@@ -45,182 +43,75 @@ async function processMissingResources() {
27 return { plugins, markdown };
28 }
29
30-function delay(ms: number): Promise<void> {
31- return new Promise((resolve) => setTimeout(() => resolve(), ms));
32-}
33-
34-async function githubApi(
35- endpoint: string,
36-): Promise<Resp<{ [key: string]: any }>> {
37- const url = `https://api.github.com${endpoint}`;
38- console.log(`Fetching ${url}`);
39- const token = encode(`${accessUsername}:${accessToken}`);
40- const res = await fetch(url, {
41- headers: { Authorization: `Basic ${token}` },
42- });
43-
44- const rateLimitRemaining = parseInt(
45- res.headers.get("X-RateLimit-Remaining") || "0",
46- );
47- const rateLimitReset = parseInt(res.headers.get("X-RateLimit-Reset") || "0");
48- console.log(`rate limit remaining: ${rateLimitRemaining}`);
49- if (rateLimitRemaining === 1) {
50- const now = Date.now();
51- const RESET_BUFFER = 500;
52- const wait = rateLimitReset + RESET_BUFFER - now;
53- console.log(
54- `About to hit github rate limit, waiting ${wait * 1000} seconds`,
55- );
56- await delay(wait);
57- }
58-
59- let data = null;
60- try {
61- data = await res.json();
62- } catch {
63- return {
64- ok: false,
65- data: {
66- status: res.status,
67- error: new Error(`JSON parsing error [${url}]`),
68- },
69- };
70- }
71-
72- if (res.ok) {
73- return {
74- ok: true,
75- data,
76- };
77- }
78-
79- return {
80- ok: false,
81- data: {
82- status: res.status,
83- error: new Error(`Could not load [${url}]`),
84- },
85- };
86-}
87-
88-async function fetchReadme({ username, repo }: Props): Promise<Resp<string>> {
89- const result = await githubApi(`/repos/${username}/${repo}/readme`);
90- if (!result.ok) {
91- return {
92- ok: false,
93- data: result.data as any,
94- };
95- }
96-
97- const url = result.data.download_url;
98- console.log(`Fetching ${url}`);
99- const readme = await fetch(url);
100- const data = await readme.text();
101- return {
102- ok: true,
103- data,
104- };
105-}
106-
107-async function fetchRepo(
108- { username, repo }: Props,
109-): Promise<Resp<{ [key: string]: any }>> {
110- const result = await githubApi(`/repos/${username}/${repo}`);
111- return result;
112-}
113-
114-async function fetchBranch({
115- username,
116- repo,
117- branch,
118-}: Props & { branch: string }): Promise<Resp<{ [key: string]: any }>> {
119- const result = await githubApi(
120- `/repos/${username}/${repo}/branches/${branch}`,
121- );
122- return result;
123-}
124-
125-interface ApiSuccess<D = any> {
126- ok: true;
127- data: D;
128-}
129-
130-interface ApiFailure {
131- ok: false;
132- data: { status: number; error: Error };
133-}
134-
135-type Resp<D> = ApiSuccess<D> | ApiFailure;
136-
137-async function fetchGithubData(props: Props): Promise<Resp<any>> {
138- const repo = await fetchRepo(props);
139- if (repo.ok === false) {
140- console.log(`${repo.data.status}: ${repo.data.error.message}`);
141- return repo;
142- }
143-
144- const branch = await fetchBranch({
145- ...props,
146- branch: repo.data.default_branch,
147- });
148- if (branch.ok === false) {
149- console.log(`${branch.data.status}: ${branch.data.error.message}`);
150- }
151-
152- const readme = await fetchReadme({
153- username: props.username,
154- repo: props.repo,
155- });
156- if (readme.ok === false) {
157- console.log(`${readme.data.status}: ${readme.data.error.message}`);
158- }
159-
160- return {
161- ok: true,
162- data: {
163- readme: readme.ok ? readme.data : "",
164- repo: repo.data,
165- branch: branch.data,
166- },
167- };
168-}
169-
170 async function processResources(resources: Resource[]) {
171 const plugins: { [key: string]: Plugin } = {};
172 const markdown: { [key: string]: string } = {};
173
174+ const ghToken = encode(`${accessUsername}:${accessToken}`);
175+
176 console.log(`Fetching ${resources.length} resources`);
177
178 for (let i = 0; i < resources.length; i += 1) {
179 const d = resources[i];
180
181- if (d.type === "github") {
182- const result = await fetchGithubData(d);
183- if (result.ok) {
184- const resp = result.data;
185- const id = `${d.username}/${d.repo}`;
186-
187- markdown[id] = resp.readme;
188- plugins[id] = createPlugin({
189- id,
190- username: d.username,
191- repo: d.repo,
192- tags: d.tags,
193- name: resp.repo.name,
194- link: resp.repo.html_url,
195- homepage: resp.repo.homepage,
196- branch: resp.repo.default_branch,
197- openIssues: resp.repo.open_issues_count,
198- watchers: resp.repo.watchers_count,
199- forks: resp.repo.forks,
200- stars: resp.repo.stargazers_count,
201- subscribers: resp.repo.subscribers_count,
202- network: resp.repo.network_count,
203- description: resp.repo.description,
204- createdAt: resp.repo.created_at,
205- updatedAt: resp.branch.commit.commit.committer.date,
206- });
207+ if (d.type === "srht") {
208+ const result = await fetchSrhtData({ ...d, token: srhtToken });
209+ const id = `${d.username}/${d.repo}`;
210+ if (!result.ok) {
211+ console.log(result);
212+ continue;
213 }
214+
215+ const repo = result.data.repo;
216+ markdown[id] = result.data.readme;
217+ plugins[id] = createPlugin({
218+ type: "srht",
219+ id,
220+ username: d.username,
221+ repo: d.repo,
222+ tags: d.tags,
223+ link: `https://git.sr.ht/~${id}`,
224+ name: repo.name,
225+ description: repo.description,
226+ createdAt: repo.created,
227+ updatedAt: repo.updated,
228+ branch: result.data.branch,
229+ });
230+ } else if (d.type === "github") {
231+ const result = await fetchGithubData({ ...d, token: ghToken });
232+ if (!result.ok) {
233+ continue;
234+ }
235+
236+ const resp = result.data;
237+ const id = `${d.username}/${d.repo}`;
238+
239+ let updatedAt = "";
240+ if (result.data.branch.ok) {
241+ updatedAt = result.data.branch.data.commit.commit.committer.date;
242+ }
243+
244+ markdown[id] = resp.readme;
245+ plugins[id] = createPlugin({
246+ type: "github",
247+ id,
248+ username: d.username,
249+ repo: d.repo,
250+ tags: d.tags,
251+ name: resp.repo.name,
252+ link: resp.repo.html_url,
253+ homepage: resp.repo.homepage,
254+ branch: resp.repo.default_branch,
255+ openIssues: resp.repo.open_issues_count,
256+ watchers: resp.repo.watchers_count,
257+ forks: resp.repo.forks,
258+ stars: resp.repo.stargazers_count,
259+ subscribers: resp.repo.subscribers_count,
260+ network: resp.repo.network_count,
261+ description: resp.repo.description,
262+ createdAt: resp.repo.created_at,
263+ updatedAt: updatedAt,
264+ });
265 }
266 }
267
+29,
-6
1@@ -1,14 +1,31 @@
2+import dbFile from "../../data/db.json" assert { type: "json" };
3 import resourceFile from "../../data/resources.json" assert { type: "json" };
4 import manualFile from "../../data/manual.json" assert { type: "json" };
5+import { derivePluginData } from "../plugin-data.ts";
6
7-import type { Resource } from "../types.ts";
8+import type { PluginMap, Resource } from "../types.ts";
9 import { createResource } from "../entities.ts";
10
11+const forges = ["github", "srht"];
12+const forgesStr = forges.join(",");
13+
14 async function init() {
15+ const pluginMap = (dbFile as any).plugins as PluginMap;
16+ const pluginData = derivePluginData(pluginMap);
17+ const allTags = pluginData.tags.map((t) => t.id);
18+
19+ const type = prompt(`code forge [${forgesStr}] (default: github):`) ||
20+ "github";
21+ if (!forges.includes(type)) {
22+ throw new Error(`${type} is not a valid code forge, choose ${forgesStr}`);
23+ }
24+
25 const name = prompt("name (username/repo):") || "";
26- const [username, repo] = name.split("/");
27- const tagsRes = prompt("tags (comma separated):") || "";
28- const tags = tagsRes.split(",");
29+ let [username, repo] = name.split("/");
30+ if (type === "srht" && username[0] === "~") {
31+ username = username.replace("~", "");
32+ }
33+
34 const foundResource = (resourceFile.resources as Resource[]).find(
35 (r) => `${r.username}/${r.repo}` === name,
36 );
37@@ -17,9 +34,16 @@ async function init() {
38 return;
39 }
40
41+ console.log(
42+ "\nNOTICE: Please review all current tags and see if any fit, only add new tags if absolutely necessary:\n",
43+ );
44+ console.log(`[${allTags.join(", ")}]\n`);
45+ const tagsRes = prompt("tags (comma separated):") || "";
46+ const tags = tagsRes.split(",");
47+
48 manualFile.resources.push(
49 createResource({
50- type: "github",
51+ type: type as any,
52 username,
53 repo,
54 tags,
55@@ -27,7 +51,6 @@ async function init() {
56 );
57
58 const json = JSON.stringify(manualFile, null, 2);
59-
60 await Deno.writeTextFile("./data/manual.json", json);
61 }
62
+1,
-7
1@@ -4,13 +4,7 @@ import htmlFile from "../../data/html.json" assert { type: "json" };
2 import { dirname } from "../deps.ts";
3 import { format, relativeTimeFromDates } from "../date.ts";
4 import { derivePluginData } from "../plugin-data.ts";
5-import type {
6- Plugin,
7- PluginData,
8- PluginMap,
9- Tag,
10- TagMap,
11-} from "../types.ts";
12+import type { Plugin, PluginData, PluginMap, Tag, TagMap } from "../types.ts";
13
14 async function createFile(fname: string, data: string) {
15 await Deno.mkdir(dirname(fname), { recursive: true });
+128,
-0
1@@ -0,0 +1,128 @@
2+import type { FetchRepoProps, Resp } from "./types.ts";
3+
4+interface SrhtRepo {
5+ id: string;
6+ name: string;
7+ created: string;
8+ updated: string;
9+ readme: string | null;
10+ description: string;
11+ HEAD: { name: string };
12+}
13+
14+interface SrhtRepoResp {
15+ data: {
16+ user: {
17+ repository: SrhtRepo;
18+ };
19+ };
20+}
21+
22+async function fetchReadme(
23+ props: FetchRepoProps,
24+ branch: string,
25+ fpath = "README.md",
26+): Promise<Resp<string>> {
27+ const url =
28+ `https://git.sr.ht/~${props.username}/${props.repo}/blob/${branch}/${fpath}`;
29+ console.log(`Fetching ${url}`);
30+ const resp = await fetch(url);
31+ const readme = await resp.text();
32+ if (!resp.ok) {
33+ return {
34+ ok: false,
35+ data: {
36+ status: resp.status,
37+ error: new Error(`failed to fetch srht readme: ${readme}`),
38+ },
39+ };
40+ }
41+
42+ return { ok: true, data: readme };
43+}
44+
45+async function recurseReadme(
46+ props: FetchRepoProps,
47+ branch: string,
48+ fnames: string[],
49+) {
50+ for (let i = 0; i < fnames.length; i += 1) {
51+ const readme = await fetchReadme(props, branch, fnames[i]);
52+
53+ if (readme.ok) {
54+ return readme.data;
55+ } else {
56+ console.log(`${readme.data.status}: ${readme.data.error.message}`);
57+ }
58+ }
59+
60+ return "";
61+}
62+
63+interface SrhtData {
64+ repo: SrhtRepo;
65+ branch: string;
66+ readme: string;
67+}
68+
69+export async function fetchSrhtData(
70+ props: FetchRepoProps,
71+): Promise<Resp<SrhtData>> {
72+ const query = `\
73+ query {\
74+ user(username: "${props.username}") {\
75+ repository(name: "${props.repo}") {\
76+ id,\
77+ name,\
78+ created,\
79+ updated,\
80+ readme,\
81+ description,\
82+ HEAD { name }\
83+ }\
84+ }\
85+ }`;
86+ const body = { query };
87+
88+ const payload = {
89+ method: "POST",
90+ headers: {
91+ Authorization: `Bearer ${props.token}`,
92+ ["Content-Type"]: "application/json",
93+ },
94+ body: JSON.stringify(body),
95+ };
96+ const url = "https://git.sr.ht/query";
97+ console.log(`Fetching ${url} [${props.username}/${props.repo}]`);
98+ const resp = await fetch(url, payload);
99+
100+ if (!resp.ok) {
101+ return {
102+ ok: false,
103+ data: { status: resp.status, error: new Error("request failed") },
104+ };
105+ }
106+
107+ const data: SrhtRepoResp = await resp.json();
108+ const repo = data.data.user.repository;
109+ const name = repo.HEAD.name;
110+ const ref = name.split("/");
111+ const branch = ref[ref.length - 1];
112+ let readmeData = "";
113+ if (repo.readme) {
114+ readmeData = repo.readme;
115+ } else {
116+ // supported readme: https://git.sr.ht/~sircmpwn/scm.sr.ht/tree/83185bf27e1e67ab2ce88851dc7a3f7766075a60/item/scmsrht/formatting.py#L25
117+ const fnames = ["README.md", "README.markdown", "README"];
118+ readmeData = await recurseReadme(props, branch, fnames);
119+ }
120+
121+ return {
122+ ok: true,
123+ data: {
124+ repo,
125+ branch,
126+ readme: readmeData,
127+ },
128+ };
129+}
+20,
-1
1@@ -1,4 +1,5 @@
2 export interface Plugin {
3+ type: "github" | "srht";
4 id: string;
5 name: string;
6 username: string;
7@@ -34,10 +35,28 @@ export interface PluginData {
8 }
9
10 export interface Resource {
11- type: "github";
12+ type: "github" | "srht";
13 username: string;
14 repo: string;
15 tags: string[];
16 }
17
18 export type ResourceMap = { [key: string]: Resource };
19+
20+export interface ApiSuccess<D = any> {
21+ ok: true;
22+ data: D;
23+}
24+
25+export interface ApiFailure {
26+ ok: false;
27+ data: { status: number; error: Error };
28+}
29+
30+export type Resp<D> = ApiSuccess<D> | ApiFailure;
31+
32+export interface FetchRepoProps {
33+ username: string;
34+ repo: string;
35+ token: string;
36+}