@volter/twin-github 0.1.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +232 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +104 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +170 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +225 -95
- package/src/github-git-http.ts +48 -84
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +50 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +193 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- package/src/github-graphql.ts +0 -398
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// GitHub's platform meta family: the service root, the rate limit, and the stateless reference
|
|
2
|
+
// catalogs (gitignore templates, licenses, emojis, service metadata) and the Markdown renderer.
|
|
3
|
+
// None of it is account state; every answer is fixed data or computed from the request.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { githubNotFound } from "../github-twin.js";
|
|
6
|
+
import { gh, send } from "./shared.js";
|
|
7
|
+
// ── Static meta catalogs (the platform's stateless reference endpoints) ───────────────────
|
|
8
|
+
// GitHub serves these from a fixed, well-known catalog (they are not per-account state). The
|
|
9
|
+
// twin returns a faithful representative subset — vendor-shaped objects, deterministic, never
|
|
10
|
+
// fabricated per-request. (The real catalogs are long; a representative slice is honest here.)
|
|
11
|
+
const GITIGNORE_TEMPLATES = ['Node', 'Python', 'Go', 'Rust', 'Java', 'Ruby', 'C', 'C++', 'VisualStudio'];
|
|
12
|
+
const GITIGNORE_SOURCE = {
|
|
13
|
+
Node: 'node_modules\nnpm-debug.log\n.env\ndist\n',
|
|
14
|
+
Python: '__pycache__/\n*.py[cod]\n.venv\n.env\n',
|
|
15
|
+
Go: '*.exe\n*.test\n/vendor/\n',
|
|
16
|
+
};
|
|
17
|
+
const LICENSES = [
|
|
18
|
+
{ key: 'mit', name: 'MIT License', spdx_id: 'MIT' },
|
|
19
|
+
{ key: 'apache-2.0', name: 'Apache License 2.0', spdx_id: 'Apache-2.0' },
|
|
20
|
+
{ key: 'gpl-3.0', name: 'GNU General Public License v3.0', spdx_id: 'GPL-3.0' },
|
|
21
|
+
{ key: 'bsd-3-clause', name: 'BSD 3-Clause "New" or "Revised" License', spdx_id: 'BSD-3-Clause' },
|
|
22
|
+
{ key: 'unlicense', name: 'The Unlicense', spdx_id: 'Unlicense' },
|
|
23
|
+
];
|
|
24
|
+
const EMOJIS = {
|
|
25
|
+
'+1': 'https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png',
|
|
26
|
+
'-1': 'https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png',
|
|
27
|
+
smile: 'https://github.githubassets.com/images/icons/emoji/unicode/1f604.png',
|
|
28
|
+
rocket: 'https://github.githubassets.com/images/icons/emoji/unicode/1f680.png',
|
|
29
|
+
tada: 'https://github.githubassets.com/images/icons/emoji/unicode/1f389.png',
|
|
30
|
+
eyes: 'https://github.githubassets.com/images/icons/emoji/unicode/1f440.png',
|
|
31
|
+
heart: 'https://github.githubassets.com/images/icons/emoji/unicode/2764.png',
|
|
32
|
+
};
|
|
33
|
+
/** A .gitignore template's text, by its name (any case); undefined for a name the catalog lacks. */
|
|
34
|
+
export function gitignoreTemplate(name) {
|
|
35
|
+
const found = GITIGNORE_TEMPLATES.find((t) => t.toLowerCase() === name.toLowerCase());
|
|
36
|
+
return found && (GITIGNORE_SOURCE[found] ?? `# ${found} .gitignore\n`);
|
|
37
|
+
}
|
|
38
|
+
// The license texts GitHub serves, [year] and [fullname] left for whoever copies them in: MIT's is the body
|
|
39
|
+
// docs.github.com/rest/licenses/licenses#get-a-license shows (its example's leading blank lines dropped). Where the
|
|
40
|
+
// documentation stops: the other catalog licenses keep their name alone.
|
|
41
|
+
const LICENSE_TEXTS = {
|
|
42
|
+
mit: 'The MIT License (MIT)\n\nCopyright (c) [year] [fullname]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n',
|
|
43
|
+
};
|
|
44
|
+
/** A license's text, by its key (any case); undefined for a key the catalog lacks. Given a holder, its placeholders are
|
|
45
|
+
* filled as the license's implementation note says: [year] with the year and [fullname] with the copyright holder. */
|
|
46
|
+
export function licenseTemplate(key, fill) {
|
|
47
|
+
const found = LICENSES.find((l) => l.key === key.toLowerCase());
|
|
48
|
+
if (!found)
|
|
49
|
+
return undefined;
|
|
50
|
+
const text = LICENSE_TEXTS[found.key] ?? `${found.name}\n`;
|
|
51
|
+
return fill ? text.replaceAll('[year]', fill.year).replaceAll('[fullname]', fill.fullname) : text;
|
|
52
|
+
}
|
|
53
|
+
function licenseToRest(l) {
|
|
54
|
+
return { key: l.key, name: l.name, spdx_id: l.spdx_id, url: `https://api.github.com/licenses/${l.key}`, node_id: createHash('sha256').update(`license:${l.key}`).digest('base64').slice(0, 16), html_url: `https://choosealicense.com/licenses/${l.key}/` };
|
|
55
|
+
}
|
|
56
|
+
// A minimal-but-faithful GitHub-Flavored-Markdown renderer for POST /markdown. GitHub returns
|
|
57
|
+
// rendered HTML; the twin supports the common inline/block forms (headings, bold, italic,
|
|
58
|
+
// inline code, links) deterministically. Not a full CommonMark engine — a faithful subset.
|
|
59
|
+
function renderMarkdown(text) {
|
|
60
|
+
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
61
|
+
const inline = (s) => esc(s)
|
|
62
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
63
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
64
|
+
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
65
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
68
|
+
const line = raw.trimEnd();
|
|
69
|
+
if (!line)
|
|
70
|
+
continue;
|
|
71
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
72
|
+
if (h) {
|
|
73
|
+
const lvl = h[1].length;
|
|
74
|
+
out.push(`<h${lvl}>${inline(h[2])}</h${lvl}>`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
78
|
+
}
|
|
79
|
+
return out.join('\n');
|
|
80
|
+
}
|
|
81
|
+
export function metaSemantics(_opts) {
|
|
82
|
+
const metaRoot = async (ctx) => {
|
|
83
|
+
const a = 'https://api.github.com';
|
|
84
|
+
return ctx.reply({
|
|
85
|
+
current_user_url: `${a}/user`, current_user_authorizations_html_url: 'https://github.com/settings/connections/applications{/client_id}',
|
|
86
|
+
authorizations_url: `${a}/authorizations`, code_search_url: `${a}/search/code?q={query}{&page,per_page,sort,order}`,
|
|
87
|
+
commit_search_url: `${a}/search/commits?q={query}{&page,per_page,sort,order}`, emails_url: `${a}/user/emails`, emojis_url: `${a}/emojis`,
|
|
88
|
+
events_url: `${a}/events`, feeds_url: `${a}/feeds`, followers_url: `${a}/user/followers`, following_url: `${a}/user/following{/target}`,
|
|
89
|
+
gists_url: `${a}/gists{/gist_id}`, hub_url: `${a}/hub`, issue_search_url: `${a}/search/issues?q={query}{&page,per_page,sort,order}`,
|
|
90
|
+
issues_url: `${a}/issues`, keys_url: `${a}/user/keys`, label_search_url: `${a}/search/labels?q={query}&repository_id={repository_id}{&page,per_page}`,
|
|
91
|
+
notifications_url: `${a}/notifications`, organization_url: `${a}/orgs/{org}`, organization_repositories_url: `${a}/orgs/{org}/repos{?type,page,per_page,sort}`,
|
|
92
|
+
organization_teams_url: `${a}/orgs/{org}/teams`, public_gists_url: `${a}/gists/public`, rate_limit_url: `${a}/rate_limit`,
|
|
93
|
+
repository_url: `${a}/repos/{owner}/{repo}`, repository_search_url: `${a}/search/repositories?q={query}{&page,per_page,sort,order}`,
|
|
94
|
+
current_user_repositories_url: `${a}/user/repos{?type,page,per_page,sort}`, starred_url: `${a}/user/starred{/owner}{/repo}`,
|
|
95
|
+
starred_gists_url: `${a}/gists/starred`, topic_search_url: `${a}/search/topics?q={query}{&page,per_page}`, user_url: `${a}/users/{user}`,
|
|
96
|
+
user_organizations_url: `${a}/user/orgs`, user_repositories_url: `${a}/users/{user}/repos{?type,page,per_page,sort}`,
|
|
97
|
+
user_search_url: `${a}/search/users?q={query}{&page,per_page,sort,order}`,
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const rateLimitGet = async (ctx) => {
|
|
101
|
+
const reset = 4102444800; // 2100-01-01T00:00:00Z — a stable, far-future sentinel.
|
|
102
|
+
const bucket = (limit) => ({ limit, used: 0, remaining: limit, reset });
|
|
103
|
+
const resources = { core: bucket(5000), search: bucket(30), graphql: bucket(5000), integration_manifest: bucket(5000), code_scanning_upload: bucket(500) };
|
|
104
|
+
return ctx.reply({ resources, rate: resources.core });
|
|
105
|
+
};
|
|
106
|
+
const gitignoreGetTemplate = async (ctx) => {
|
|
107
|
+
const name = GITIGNORE_TEMPLATES.find((t) => t.toLowerCase() === decodeURIComponent(gh(ctx).seg[2]).toLowerCase());
|
|
108
|
+
if (!name)
|
|
109
|
+
return send(ctx, githubNotFound());
|
|
110
|
+
return ctx.reply({ name, source: gitignoreTemplate(name) });
|
|
111
|
+
};
|
|
112
|
+
const licensesGet = async (ctx) => {
|
|
113
|
+
const l = LICENSES.find((x) => x.key === decodeURIComponent(gh(ctx).seg[1]).toLowerCase());
|
|
114
|
+
if (!l)
|
|
115
|
+
return send(ctx, githubNotFound());
|
|
116
|
+
return ctx.reply({ ...licenseToRest(l), description: `${l.name}.`, permissions: [], conditions: [], limitations: [], body: licenseTemplate(l.key), featured: l.key === 'mit' });
|
|
117
|
+
};
|
|
118
|
+
const markdownRender = async (ctx) => {
|
|
119
|
+
const { body } = gh(ctx);
|
|
120
|
+
if (typeof body.text !== 'string')
|
|
121
|
+
return ctx.reply({ message: 'Validation Failed', errors: [{ resource: 'Markdown', field: 'text', code: 'missing_field' }] }, 422);
|
|
122
|
+
return ctx.reply(renderMarkdown(body.text));
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
// the twin's own root: which service this is and whether it takes writes
|
|
126
|
+
// the API's root: the URL templates of its resources, as api.github.com answers (docs.github.com/rest/meta/meta#github-api-root)
|
|
127
|
+
'meta/root': metaRoot,
|
|
128
|
+
// Rate limit: the twin is local + unmetered, so it reports a faithful full-budget envelope
|
|
129
|
+
// in GitHub's exact {resources:{core,search,...}, rate} shape. The reset is a fixed sentinel
|
|
130
|
+
// (deterministic — never Date.now()).
|
|
131
|
+
'rate-limit/get': rateLimitGet,
|
|
132
|
+
'gitignore/get-all-templates': async (ctx) => ctx.reply(GITIGNORE_TEMPLATES),
|
|
133
|
+
'gitignore/get-template': gitignoreGetTemplate,
|
|
134
|
+
'licenses/get-all-commonly-used': async (ctx) => ctx.reply(LICENSES.map(licenseToRest)),
|
|
135
|
+
'licenses/get': licensesGet,
|
|
136
|
+
// the named-emoji → asset-URL map
|
|
137
|
+
'emojis/get': async (ctx) => ctx.reply(EMOJIS),
|
|
138
|
+
// the platform's service metadata envelope
|
|
139
|
+
'meta/get': async (ctx) => ctx.reply({ verifiable_password_authentication: false, ssh_key_fingerprints: {}, hooks: [], web: [], api: [], git: [], packages: [], pages: [], importer: [], actions: [], dependabot: [] }),
|
|
140
|
+
// Render Markdown to HTML (GitHub returns text/html). The twin renders a faithful GFM subset
|
|
141
|
+
// deterministically; `text` is required (422 when absent, like GitHub).
|
|
142
|
+
'markdown/render': markdownRender,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type GithubWriteEvent } from '../github-twin.js';
|
|
3
|
+
/** The thread an event puts in the World's user's inbox, when it concerns them and someone else did it. */
|
|
4
|
+
export declare function notify(ctx: SemanticsContext, event: GithubWriteEvent): Promise<void>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The notifications GitHub sends the signed-in person (the World's user): when someone else @mentions them, assigns
|
|
2
|
+
// them, asks for their review, or comments on or reviews their issue or pull request, the thread for that issue or pull
|
|
3
|
+
// request is unread in their inbox with the reason (docs.github.com/account-and-profile/managing-subscriptions-and-
|
|
4
|
+
// notifications-on-github/setting-up-notifications/about-notifications#reasons-for-receiving-notifications). One
|
|
5
|
+
// thread per issue or pull request: a new reason on it marks it unread again. The person's own acts notify no one.
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { GITHUB_WORLD_USER } from "../github-twin.js";
|
|
8
|
+
import { gh, put } from "./shared.js";
|
|
9
|
+
const loginOf = (u) => (u && typeof u === 'object' ? u.login : undefined);
|
|
10
|
+
const mentions = (text, login) => typeof text === 'string' && new RegExp(`(^|[^\\w-])@${login}\\b`, 'i').test(text);
|
|
11
|
+
/** Whether a review request names the World's user. */
|
|
12
|
+
function reviewRequestedOf(event, me) {
|
|
13
|
+
const requested = event.pr?.requested_reviewers ?? [];
|
|
14
|
+
return requested.some((r) => (typeof r === 'string' ? r : loginOf(r)) === me);
|
|
15
|
+
}
|
|
16
|
+
/** Why an event notifies the World's user, or nothing. */
|
|
17
|
+
function reasonFor(event, me, author) {
|
|
18
|
+
const text = event.comment?.body ?? event.review?.body ?? event.issue?.body ?? event.body;
|
|
19
|
+
if (mentions(text, me))
|
|
20
|
+
return 'mention';
|
|
21
|
+
if (event.event === 'pull_request' && event.action === 'review_requested' && reviewRequestedOf(event, me))
|
|
22
|
+
return 'review_requested';
|
|
23
|
+
if (event.event === 'issues' && (event.action === 'opened' || event.action === 'assigned' || event.action === 'edited')) {
|
|
24
|
+
const assignees = event.issue?.assignees ?? [];
|
|
25
|
+
if (assignees.some((a) => loginOf(a) === me))
|
|
26
|
+
return 'assign';
|
|
27
|
+
}
|
|
28
|
+
if ((event.event === 'issue_comment' || event.event === 'pull_request_review') && author === me)
|
|
29
|
+
return 'author';
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/** The thread an event puts in the World's user's inbox, when it concerns them and someone else did it. */
|
|
33
|
+
export async function notify(ctx, event) {
|
|
34
|
+
const { caller, state } = gh(ctx);
|
|
35
|
+
const me = GITHUB_WORLD_USER;
|
|
36
|
+
if (!caller || caller === me || !event.number)
|
|
37
|
+
return;
|
|
38
|
+
const st = state();
|
|
39
|
+
const pr = st.prs.find((p) => p.repository === event.repository && Number(p.number) === event.number);
|
|
40
|
+
const issue = st.issues.find((i) => i.repository === event.repository && Number(i.number) === event.number);
|
|
41
|
+
const author = pr?.user_login ?? issue?.user_login ?? undefined;
|
|
42
|
+
const reason = reasonFor(event, me, author);
|
|
43
|
+
if (!reason)
|
|
44
|
+
return;
|
|
45
|
+
const isPull = Boolean(pr) || event.event.startsWith('pull_request');
|
|
46
|
+
const id = String(parseInt(createHash('sha256').update(`${event.repository}#${event.number}`).digest('hex').slice(0, 8), 16));
|
|
47
|
+
// an ignored thread stays quiet; an unsubscribed one until the next @mention, which subscribes the person again
|
|
48
|
+
const thread = st.notifications.find((n) => n.id === id);
|
|
49
|
+
if (thread?.subscription?.ignored)
|
|
50
|
+
return;
|
|
51
|
+
if (thread?.subscription === null && reason !== 'mention')
|
|
52
|
+
return;
|
|
53
|
+
await put(ctx, {
|
|
54
|
+
operation: 'notification.create', subjectType: 'notification', subjectId: `notification:${id}`,
|
|
55
|
+
fields: {
|
|
56
|
+
id, repository: event.repository, subject_title: String(pr?.title ?? issue?.title ?? `#${event.number}`),
|
|
57
|
+
subject_type: isPull ? 'PullRequest' : 'Issue', subject_url: `https://api.github.com/repos/${event.repository}/${isPull ? 'pulls' : 'issues'}/${event.number}`,
|
|
58
|
+
reason, unread: true, updated_at: ctx.occurredAt,
|
|
59
|
+
...(thread?.subscription === null ? { subscription: { subscribed: true, ignored: false, created_at: ctx.occurredAt } } : {}),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|