@gryt/core 0.6.0 → 0.8.0
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/dist/index.d.ts +3 -1
- package/dist/index.js +5 -4
- package/dist/links/logos.js +2 -15
- package/dist/links/preview.d.ts +8 -20
- package/dist/links/preview.js +10 -22
- package/dist/links/providers.d.ts +2 -3
- package/dist/links/providers.js +6 -17
- package/dist/members/groups.d.ts +6 -31
- package/dist/members/groups.js +2 -15
- package/dist/permissions/channelRules.d.ts +12 -33
- package/dist/permissions/channelRules.js +14 -39
- package/dist/permissions/folderRules.d.ts +10 -0
- package/dist/permissions/folderRules.js +29 -0
- package/dist/profile/randomName.d.ts +4 -10
- package/dist/profile/randomName.js +6 -14
- package/dist/reports/report.d.ts +6 -16
- package/dist/reports/report.js +8 -20
- package/dist/servers/address.d.ts +8 -0
- package/dist/servers/address.js +72 -11
- package/dist/webrtc/senderStreamId.d.ts +5 -0
- package/dist/webrtc/senderStreamId.js +16 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export { getAccentColor, getLinkProvider, getProviderDetail, hostnameOf, LINK_PR
|
|
|
3
3
|
export { getProviderLogo, LINK_PROVIDER_LOGOS, LOGO_VIEW_BOX, } from "./links/logos.js";
|
|
4
4
|
export { describePreviewFailure, extractUrls, getCardSubtitle, getLinkCardLayout, type LinkCardLayout, type LinkPreviewData, } from "./links/preview.js";
|
|
5
5
|
export { conversationTitle, type DirectConversation, } from "./conversations/title.js";
|
|
6
|
-
export { normalizeCode, normalizeHost, parseServerInput, type ServerInput, } from "./servers/address.js";
|
|
6
|
+
export { inviteLink, isPublicHost, normalizeCode, normalizeHost, parseServerInput, type ServerInput, } from "./servers/address.js";
|
|
7
7
|
export { NAME_POOL, pickRandomName } from "./profile/randomName.js";
|
|
8
8
|
export { cellState, CUSTOM_VALUE, describeRules, EVERYONE_VALUE, indexRules, nextCellState, scopeChoiceFromValue, scopeChoiceValue, scopeOptions, scopeSetPayload, withCell, type CellState, type ChannelRule, type RuleEffect, type ScopeChoice, } from "./permissions/channelRules.js";
|
|
9
9
|
export { groupMembersByRole, OFFLINE_GROUP_KEY, UNGROUPED_GROUP_KEY, type GroupableMember, type GroupableRole, type MemberGroup, } from "./members/groups.js";
|
|
10
|
+
export { senderStreamId } from "./webrtc/senderStreamId.js";
|
|
11
|
+
export { describeFolderRules, folderFollowNote, folderPhrase, } from "./permissions/folderRules.js";
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
/* What belongs here: it must compile with no DOM and no React Native, and both
|
|
2
|
-
|
|
3
|
-
`check-public-surface.mjs` asserts against it. */
|
|
1
|
+
/* What belongs here: it must compile with no DOM and no React Native, and both apps must
|
|
2
|
+
otherwise need a copy. The barrel is flat and named, because the surface check reads it. */
|
|
4
3
|
export { buildReport, describeAttached, MESSAGE_MAX, TITLE_MAX, } from "./reports/report.js";
|
|
5
4
|
export { getAccentColor, getLinkProvider, getProviderDetail, hostnameOf, LINK_PROVIDERS, } from "./links/providers.js";
|
|
6
5
|
export { getProviderLogo, LINK_PROVIDER_LOGOS, LOGO_VIEW_BOX, } from "./links/logos.js";
|
|
7
6
|
export { describePreviewFailure, extractUrls, getCardSubtitle, getLinkCardLayout, } from "./links/preview.js";
|
|
8
7
|
export { conversationTitle, } from "./conversations/title.js";
|
|
9
|
-
export { normalizeCode, normalizeHost, parseServerInput, } from "./servers/address.js";
|
|
8
|
+
export { inviteLink, isPublicHost, normalizeCode, normalizeHost, parseServerInput, } from "./servers/address.js";
|
|
10
9
|
export { NAME_POOL, pickRandomName } from "./profile/randomName.js";
|
|
11
10
|
export { cellState, CUSTOM_VALUE, describeRules, EVERYONE_VALUE, indexRules, nextCellState, scopeChoiceFromValue, scopeChoiceValue, scopeOptions, scopeSetPayload, withCell, } from "./permissions/channelRules.js";
|
|
12
11
|
export { groupMembersByRole, OFFLINE_GROUP_KEY, UNGROUPED_GROUP_KEY, } from "./members/groups.js";
|
|
12
|
+
export { senderStreamId } from "./webrtc/senderStreamId.js";
|
|
13
|
+
export { describeFolderRules, folderFollowNote, folderPhrase, } from "./permissions/folderRules.js";
|
package/dist/links/logos.js
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
/* The artwork for the providers in providers.ts, as raw SVG path data
|
|
2
|
-
|
|
3
|
-
It used to live in the desktop client as react-icons components, which meant
|
|
4
|
-
the phone had no logos at all and the client carried an 82MB dependency to
|
|
5
|
-
draw 73 of them. Path data is not DOM, so it can sit here with the hostnames
|
|
6
|
-
and brand colours it belongs with, and each app draws it with whatever it
|
|
7
|
-
has: <path> in the browser, <Path> from react-native-svg on the phone.
|
|
8
|
-
|
|
9
|
-
Every one is a single path on a 24x24 canvas, which is Simple Icons' own
|
|
10
|
-
format and is asserted in logos.test.ts. A provider with no logo here falls
|
|
11
|
-
back to the site's favicon, so adding a provider without adding artwork
|
|
12
|
-
degrades rather than breaks.
|
|
13
|
-
|
|
14
|
-
The marks belong to the companies they identify. Being here is so Gryt can
|
|
15
|
-
point at those sites, not a claim on them. */
|
|
1
|
+
/* The artwork for the providers in providers.ts, as raw SVG path data, so both apps draw it
|
|
2
|
+
with what they have. The marks belong to the companies they identify. */
|
|
16
3
|
/** Every logo below is drawn on this canvas. */
|
|
17
4
|
export const LOGO_VIEW_BOX = "0 0 24 24";
|
|
18
5
|
/** Provider id, as in {@link LINK_PROVIDERS}, to its SVG path data. */
|
package/dist/links/preview.d.ts
CHANGED
|
@@ -20,35 +20,23 @@ export interface LinkPreviewData {
|
|
|
20
20
|
status?: number | null;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* The links in a message, minus the ones that are already something else
|
|
24
|
-
*
|
|
25
|
-
* Code spans and fences are stripped first: a URL inside backticks is being
|
|
26
|
-
* quoted rather than shared, and a card under it would be noise. Markdown
|
|
27
|
-
* images are stripped because the picture is already drawn.
|
|
23
|
+
* The links in a message, minus the ones that are already something else: a URL in backticks
|
|
24
|
+
* is being quoted rather than shared, and a markdown image is already drawn.
|
|
28
25
|
*/
|
|
29
26
|
export declare function extractUrls(text: string | null): string[];
|
|
30
27
|
/**
|
|
31
|
-
* How much of a card a preview can fill.
|
|
32
|
-
*
|
|
33
|
-
* A wide image wants to sit under the text at full width. A small or square one
|
|
34
|
-
* wants to be a thumbnail beside it. No image wants no space set aside for a
|
|
35
|
-
* picture at all. Drawing all three the same way is what produced a hostname
|
|
36
|
-
* next to an empty grey rectangle.
|
|
28
|
+
* How much of a card a preview can fill. A wide image sits under the text, a small one
|
|
29
|
+
* beside it, and no image takes no space — drawing all three alike gave a grey rectangle.
|
|
37
30
|
*/
|
|
38
31
|
export type LinkCardLayout = "large" | "thumbnail" | "text" | "bare";
|
|
39
32
|
export declare function getLinkCardLayout(data: LinkPreviewData): LinkCardLayout;
|
|
40
33
|
/**
|
|
41
|
-
* Why a page gave us nothing, in words worth showing somebody.
|
|
42
|
-
*
|
|
43
|
-
* Only for statuses that mean something to a reader. A 500 is the site's
|
|
44
|
-
* problem and saying so helps nobody, so it returns null and the card falls
|
|
45
|
-
* back to showing the link on its own.
|
|
34
|
+
* Why a page gave us nothing, in words worth showing somebody. Only for statuses that mean
|
|
35
|
+
* something to a reader: a 500 is the site's problem, so it returns null.
|
|
46
36
|
*/
|
|
47
37
|
export declare function describePreviewFailure(status: number | null | undefined): string | null;
|
|
48
38
|
/**
|
|
49
|
-
* The line under the title, where the path says something the title does not.
|
|
50
|
-
*
|
|
51
|
-
* Wikipedia titles its WebRTC page "WebRTC" and the detail read out of
|
|
52
|
-
* `/wiki/WebRTC` is "WebRTC", so showing both printed the word twice.
|
|
39
|
+
* The line under the title, where the path says something the title does not. Wikipedia
|
|
40
|
+
* titles its WebRTC page "WebRTC", and `/wiki/WebRTC` printed the word twice.
|
|
53
41
|
*/
|
|
54
42
|
export declare function getCardSubtitle(title: string | null, detail: string | null): string | null;
|
package/dist/links/preview.js
CHANGED
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
the two apps reach a server differently. */
|
|
3
3
|
const URL_REGEX = /https?:\/\/[^\s<>[\](){}'"`,]+[^\s<>[\](){}'"`,.:;!?)]/gi;
|
|
4
4
|
/**
|
|
5
|
-
* The links in a message, minus the ones that are already something else
|
|
6
|
-
*
|
|
7
|
-
* Code spans and fences are stripped first: a URL inside backticks is being
|
|
8
|
-
* quoted rather than shared, and a card under it would be noise. Markdown
|
|
9
|
-
* images are stripped because the picture is already drawn.
|
|
5
|
+
* The links in a message, minus the ones that are already something else: a URL in backticks
|
|
6
|
+
* is being quoted rather than shared, and a markdown image is already drawn.
|
|
10
7
|
*/
|
|
11
8
|
export function extractUrls(text) {
|
|
12
9
|
if (!text)
|
|
@@ -28,10 +25,8 @@ export function getLinkCardLayout(data) {
|
|
|
28
25
|
return hasText ? "text" : "bare";
|
|
29
26
|
const w = data.imageWidth;
|
|
30
27
|
const h = data.imageHeight;
|
|
31
|
-
/* Unknown dimensions count as large
|
|
32
|
-
|
|
33
|
-
that have not lose less by being drawn big than a real share card loses by
|
|
34
|
-
being shrunk into a corner. */
|
|
28
|
+
/* Unknown dimensions count as large: a site that sets og:image and says nothing about its
|
|
29
|
+
size has almost always set a share card, and those lose most by being shrunk. */
|
|
35
30
|
if (!w || !h)
|
|
36
31
|
return "large";
|
|
37
32
|
if (w >= LARGE_IMAGE_MIN_WIDTH && w / h >= LARGE_IMAGE_MIN_ASPECT)
|
|
@@ -39,21 +34,16 @@ export function getLinkCardLayout(data) {
|
|
|
39
34
|
return hasText ? "thumbnail" : "large";
|
|
40
35
|
}
|
|
41
36
|
/**
|
|
42
|
-
* Why a page gave us nothing, in words worth showing somebody.
|
|
43
|
-
*
|
|
44
|
-
* Only for statuses that mean something to a reader. A 500 is the site's
|
|
45
|
-
* problem and saying so helps nobody, so it returns null and the card falls
|
|
46
|
-
* back to showing the link on its own.
|
|
37
|
+
* Why a page gave us nothing, in words worth showing somebody. Only for statuses that mean
|
|
38
|
+
* something to a reader: a 500 is the site's problem, so it returns null.
|
|
47
39
|
*/
|
|
48
40
|
export function describePreviewFailure(status) {
|
|
49
41
|
if (status == null)
|
|
50
42
|
return null;
|
|
51
43
|
if (status === 401)
|
|
52
44
|
return "Sign-in only";
|
|
53
|
-
/* Not "private": a 403 is as often a site refusing our fetcher as
|
|
54
|
-
|
|
55
|
-
preview fetch and 200 to a browser. A private GitHub repository answers
|
|
56
|
-
404 and is covered below. */
|
|
45
|
+
/* Not "private": a 403 is as often a site refusing our fetcher as a page somebody may not
|
|
46
|
+
see. Stack Overflow answers 403 to the fetch and 200 to a browser. */
|
|
57
47
|
if (status === 403)
|
|
58
48
|
return "The site would not let us look";
|
|
59
49
|
if (status === 404 || status === 410)
|
|
@@ -63,10 +53,8 @@ export function describePreviewFailure(status) {
|
|
|
63
53
|
return null;
|
|
64
54
|
}
|
|
65
55
|
/**
|
|
66
|
-
* The line under the title, where the path says something the title does not.
|
|
67
|
-
*
|
|
68
|
-
* Wikipedia titles its WebRTC page "WebRTC" and the detail read out of
|
|
69
|
-
* `/wiki/WebRTC` is "WebRTC", so showing both printed the word twice.
|
|
56
|
+
* The line under the title, where the path says something the title does not. Wikipedia
|
|
57
|
+
* titles its WebRTC page "WebRTC", and `/wiki/WebRTC` printed the word twice.
|
|
70
58
|
*/
|
|
71
59
|
export function getCardSubtitle(title, detail) {
|
|
72
60
|
if (!detail || !title)
|
|
@@ -16,8 +16,7 @@ export declare function getLinkProvider(url: string): LinkProvider | null;
|
|
|
16
16
|
/** The line a provider can read out of the path, if it knows how. */
|
|
17
17
|
export declare function getProviderDetail(url: string): string | null;
|
|
18
18
|
/**
|
|
19
|
-
* The accent a card is drawn with: the brand where we know it, the colour the
|
|
20
|
-
*
|
|
21
|
-
* neither is on offer.
|
|
19
|
+
* The accent a card is drawn with: the brand where we know it, the colour the page declared
|
|
20
|
+
* where we do not, and null for the app's own accent when neither is on offer.
|
|
22
21
|
*/
|
|
23
22
|
export declare function getAccentColor(url: string, themeColor: string | null | undefined, appearance: "light" | "dark"): string | null;
|
package/dist/links/providers.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
/* Not the list of sites that work — any site with OpenGraph gets a full card
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
The logos are in logos.ts, as path data rather than components, because that
|
|
5
|
-
is the part both apps can share without this package touching a DOM. They
|
|
6
|
-
used to be react-icons components in the desktop client and nothing at all
|
|
7
|
-
on the phone. */
|
|
1
|
+
/* Not the list of sites that work — any site with OpenGraph gets a full card. Being here
|
|
2
|
+
buys a real logo and a brand colour, and the logos are path data in logos.ts. */
|
|
8
3
|
const seg = (url) => url.pathname.split("/").filter(Boolean);
|
|
9
4
|
function githubDetail(url) {
|
|
10
5
|
const p = seg(url);
|
|
@@ -65,13 +60,8 @@ function wikipediaDetail(url) {
|
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
62
|
/**
|
|
68
|
-
* The model's name out of `/models/1642496-old-vikings-jewelry-box
|
|
69
|
-
*
|
|
70
|
-
* The number is the id their API takes and means nothing to a reader, so the
|
|
71
|
-
* slug behind it is what the line says. A link with the slug stripped —
|
|
72
|
-
* `/models/1642496`, which is what their own copy button produces — has
|
|
73
|
-
* nothing to add, and null leaves the card to its title rather than printing
|
|
74
|
-
* an id at somebody.
|
|
63
|
+
* The model's name out of `/models/1642496-old-vikings-jewelry-box`: the number is an id
|
|
64
|
+
* that says nothing, and a link with the slug stripped gets null rather than the id.
|
|
75
65
|
*/
|
|
76
66
|
function makerWorldDetail(url) {
|
|
77
67
|
const p = seg(url);
|
|
@@ -219,9 +209,8 @@ export function getProviderDetail(url) {
|
|
|
219
209
|
}
|
|
220
210
|
}
|
|
221
211
|
/**
|
|
222
|
-
* The accent a card is drawn with: the brand where we know it, the colour the
|
|
223
|
-
*
|
|
224
|
-
* neither is on offer.
|
|
212
|
+
* The accent a card is drawn with: the brand where we know it, the colour the page declared
|
|
213
|
+
* where we do not, and null for the app's own accent when neither is on offer.
|
|
225
214
|
*/
|
|
226
215
|
export function getAccentColor(url, themeColor, appearance) {
|
|
227
216
|
const provider = getLinkProvider(url);
|
package/dist/members/groups.d.ts
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The member list, cut into role groups.
|
|
3
|
-
*
|
|
4
|
-
* Both apps drew this and both got it right, separately, which is the shape of
|
|
5
|
-
* duplicate that goes wrong quietly: nothing fails when the two drift, the
|
|
6
|
-
* lists just stop matching, and a moderator has to re-learn the sidebar on
|
|
7
|
-
* whichever device they are holding.
|
|
8
|
-
*
|
|
9
|
-
* **Offline leaves its role** — the question the list answers is "who is
|
|
10
|
-
* around", so they go to one group at the end in one alphabet rather than to
|
|
11
|
-
* the bottom of each role.
|
|
12
|
-
*
|
|
13
|
-
* Roles run highest rank first, and roles nobody holds are left out rather than
|
|
14
|
-
* drawn empty. A member whose role the server did not describe lands in one
|
|
15
|
-
* unnamed group after the named ones, which is also what a server too old to
|
|
16
|
-
* send roles looks like.
|
|
2
|
+
* The member list, cut into role groups. Offline leaves its role and goes to one group at
|
|
3
|
+
* the end: the question the list answers is who is around.
|
|
17
4
|
*/
|
|
18
5
|
/**
|
|
19
|
-
* What this needs to know about a member, and no more.
|
|
20
|
-
*
|
|
21
|
-
* Structural rather than a type the apps have to adopt: the desktop's
|
|
22
|
-
* `MemberInfo` and the phone's `Member` carry different fields for different
|
|
23
|
-
* screens, and both already satisfy this.
|
|
24
|
-
*
|
|
25
|
-
* **A `status` this does not recognise counts as present, `undefined`
|
|
26
|
-
* included.** The server always sends one — `clients.ts` defaults it to
|
|
27
|
-
* `offline` — so the absent case is a server too old to have the field, and on
|
|
28
|
-
* one of those every member reading as offline would blank the whole list.
|
|
6
|
+
* What this needs to know about a member, and no more, structurally. A `status` this does
|
|
7
|
+
* not recognise counts as present, `undefined` included — that is an old server.
|
|
29
8
|
*/
|
|
30
9
|
export interface GroupableMember {
|
|
31
10
|
nickname?: string | null;
|
|
@@ -40,12 +19,8 @@ export interface GroupableRole {
|
|
|
40
19
|
color?: string | null;
|
|
41
20
|
}
|
|
42
21
|
/**
|
|
43
|
-
* One block of the member list: a heading and the people under it.
|
|
44
|
-
*
|
|
45
|
-
* Generic over the member so a caller gets its own type back and can draw
|
|
46
|
-
* whatever it stores. `color` is the role's own colour, straight from the
|
|
47
|
-
* server, or null for a role that has none and for the two groups that are not
|
|
48
|
-
* roles.
|
|
22
|
+
* One block of the member list: a heading and the people under it. Generic over the member,
|
|
23
|
+
* so a caller gets its own type back. `color` is the role's own, or null.
|
|
49
24
|
*/
|
|
50
25
|
export interface MemberGroup<M> {
|
|
51
26
|
key: string;
|
package/dist/members/groups.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The member list, cut into role groups.
|
|
3
|
-
*
|
|
4
|
-
* Both apps drew this and both got it right, separately, which is the shape of
|
|
5
|
-
* duplicate that goes wrong quietly: nothing fails when the two drift, the
|
|
6
|
-
* lists just stop matching, and a moderator has to re-learn the sidebar on
|
|
7
|
-
* whichever device they are holding.
|
|
8
|
-
*
|
|
9
|
-
* **Offline leaves its role** — the question the list answers is "who is
|
|
10
|
-
* around", so they go to one group at the end in one alphabet rather than to
|
|
11
|
-
* the bottom of each role.
|
|
12
|
-
*
|
|
13
|
-
* Roles run highest rank first, and roles nobody holds are left out rather than
|
|
14
|
-
* drawn empty. A member whose role the server did not describe lands in one
|
|
15
|
-
* unnamed group after the named ones, which is also what a server too old to
|
|
16
|
-
* send roles looks like.
|
|
2
|
+
* The member list, cut into role groups. Offline leaves its role and goes to one group at
|
|
3
|
+
* the end: the question the list answers is who is around.
|
|
17
4
|
*/
|
|
18
5
|
/**
|
|
19
6
|
* The key on the group holding anybody not offline, when the server named no
|
|
@@ -18,44 +18,30 @@ export type ScopeChoice = {
|
|
|
18
18
|
/** The dropdown value that means "this channel has its own rules". */
|
|
19
19
|
export declare const CUSTOM_VALUE = "custom";
|
|
20
20
|
/**
|
|
21
|
-
* The dropdown value that means "no rules at all".
|
|
22
|
-
*
|
|
23
|
-
* A word rather than an empty string: the Select reads "" as "nothing chosen"
|
|
24
|
-
* and paints its placeholder over the label, so a channel open to everyone
|
|
25
|
-
* showed "Select" instead of "Everyone". See [[base-ui-select-empty-value]] —
|
|
26
|
-
* the Max Bitrate control in the same dialog already used "default" for this.
|
|
21
|
+
* The dropdown value that means "no rules at all". A word rather than an empty string: the
|
|
22
|
+
* Select reads "" as "nothing chosen" and paints its placeholder over the label.
|
|
27
23
|
*/
|
|
28
24
|
export declare const EVERYONE_VALUE = "everyone";
|
|
29
25
|
/** Look up one cell without scanning the whole list per cell. */
|
|
30
26
|
export declare function indexRules(rules: ChannelRule[]): Map<string, RuleEffect>;
|
|
31
27
|
export declare function cellState(index: Map<string, RuleEffect>, roleId: string, permission: string): CellState;
|
|
32
28
|
/**
|
|
33
|
-
* The next state when somebody clicks a cell.
|
|
34
|
-
*
|
|
35
|
-
* inherit to deny to allow and back to inherit. Deny comes first deliberately:
|
|
36
|
-
* taking something away is what people open this to do, and it puts the
|
|
37
|
-
* dangerous state one click from neutral rather than two.
|
|
29
|
+
* The next state when somebody clicks a cell: inherit, deny, allow, inherit. Deny first,
|
|
30
|
+
* because taking something away is what people open this to do.
|
|
38
31
|
*/
|
|
39
32
|
export declare function nextCellState(current: CellState): CellState;
|
|
40
33
|
/** Set one cell, dropping the row entirely when it goes back to inherit. */
|
|
41
34
|
export declare function withCell(rules: ChannelRule[], roleId: string, permission: string, state: CellState): ChannelRule[];
|
|
42
35
|
/**
|
|
43
|
-
* Which dropdown value a channel is showing.
|
|
44
|
-
*
|
|
45
|
-
* The server sends the scope id plus whether it is a template. A scope that is
|
|
46
|
-
* not a template is this channel's own, which is Custom; no scope at all is
|
|
47
|
-
* Everyone.
|
|
36
|
+
* Which dropdown value a channel is showing. A scope that is not a template is this
|
|
37
|
+
* channel's own, which is Custom; no scope at all is Everyone.
|
|
48
38
|
*/
|
|
49
39
|
export declare function scopeChoiceValue(scopeId: string | null, isTemplate: boolean): string;
|
|
50
40
|
/** Turn the dropdown value back into what the server event wants. */
|
|
51
41
|
export declare function scopeChoiceFromValue(value: string): ScopeChoice;
|
|
52
42
|
/**
|
|
53
|
-
* The payload for `server:channels:scope:set`.
|
|
54
|
-
*
|
|
55
|
-
* Custom sends its rules; the other two send none. Templates deliberately do
|
|
56
|
-
* not carry rules here — editing a template from a channel would change every
|
|
57
|
-
* other channel using it, which is the opposite of what somebody expects from
|
|
58
|
-
* a screen titled with one channel's name.
|
|
43
|
+
* The payload for `server:channels:scope:set`. Templates deliberately do not carry rules:
|
|
44
|
+
* editing one from a channel would change every other channel using it.
|
|
59
45
|
*/
|
|
60
46
|
export declare function scopeSetPayload(choice: ScopeChoice, rules: ChannelRule[]): {
|
|
61
47
|
templateId?: string | null;
|
|
@@ -63,11 +49,8 @@ export declare function scopeSetPayload(choice: ScopeChoice, rules: ChannelRule[
|
|
|
63
49
|
rules?: ChannelRule[];
|
|
64
50
|
};
|
|
65
51
|
/**
|
|
66
|
-
* The dropdown options: Everyone, then each template, then Custom.
|
|
67
|
-
*
|
|
68
|
-
* Custom last because it is the escape hatch. Somebody scanning the list should
|
|
69
|
-
* meet the shared answers first — the whole point of templates is that reaching
|
|
70
|
-
* for Custom is the uncommon choice.
|
|
52
|
+
* The dropdown options: Everyone, then each template, then Custom. Custom last, because it
|
|
53
|
+
* is the escape hatch and reaching for it is the uncommon choice.
|
|
71
54
|
*/
|
|
72
55
|
export declare function scopeOptions(templates: {
|
|
73
56
|
id: string;
|
|
@@ -77,11 +60,7 @@ export declare function scopeOptions(templates: {
|
|
|
77
60
|
value: string;
|
|
78
61
|
}[];
|
|
79
62
|
/**
|
|
80
|
-
* A one-line summary of what a set of rules does
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* Reading is called out on its own because it is the rule with a different
|
|
84
|
-
* consequence: a role denied `read_messages` is not shown a locked channel,
|
|
85
|
-
* it is shown nothing at all, and somebody setting that deserves to be told.
|
|
63
|
+
* A one-line summary of what a set of rules does. Reading is called out on its own: a role
|
|
64
|
+
* denied `read_messages` is shown nothing at all rather than a locked channel.
|
|
86
65
|
*/
|
|
87
66
|
export declare function describeRules(rules: ChannelRule[], roleNames: Map<string, string>): string;
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/* Inherit is the absence of a row, so a cell has three states and only two are
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
`indexRules` keys on role and permission joined by NUL, because NUL cannot
|
|
5
|
-
appear in either half. Written as `\u0000` rather than a raw byte: literal
|
|
6
|
-
NULs make git treat the file as binary and stop showing diffs. */
|
|
1
|
+
/* Inherit is the absence of a row, so a cell has three states and only two are written.
|
|
2
|
+
`indexRules` keys on NUL, written as `\u0000` or git treats the file as binary. */
|
|
7
3
|
/** The dropdown value that means "this channel has its own rules". */
|
|
8
4
|
export const CUSTOM_VALUE = "custom";
|
|
9
5
|
/**
|
|
10
|
-
* The dropdown value that means "no rules at all".
|
|
11
|
-
*
|
|
12
|
-
* A word rather than an empty string: the Select reads "" as "nothing chosen"
|
|
13
|
-
* and paints its placeholder over the label, so a channel open to everyone
|
|
14
|
-
* showed "Select" instead of "Everyone". See [[base-ui-select-empty-value]] —
|
|
15
|
-
* the Max Bitrate control in the same dialog already used "default" for this.
|
|
6
|
+
* The dropdown value that means "no rules at all". A word rather than an empty string: the
|
|
7
|
+
* Select reads "" as "nothing chosen" and paints its placeholder over the label.
|
|
16
8
|
*/
|
|
17
9
|
export const EVERYONE_VALUE = "everyone";
|
|
18
10
|
/** Look up one cell without scanning the whole list per cell. */
|
|
@@ -26,11 +18,8 @@ export function cellState(index, roleId, permission) {
|
|
|
26
18
|
return index.get(`${roleId}\u0000${permission}`) ?? "inherit";
|
|
27
19
|
}
|
|
28
20
|
/**
|
|
29
|
-
* The next state when somebody clicks a cell.
|
|
30
|
-
*
|
|
31
|
-
* inherit to deny to allow and back to inherit. Deny comes first deliberately:
|
|
32
|
-
* taking something away is what people open this to do, and it puts the
|
|
33
|
-
* dangerous state one click from neutral rather than two.
|
|
21
|
+
* The next state when somebody clicks a cell: inherit, deny, allow, inherit. Deny first,
|
|
22
|
+
* because taking something away is what people open this to do.
|
|
34
23
|
*/
|
|
35
24
|
export function nextCellState(current) {
|
|
36
25
|
if (current === "inherit")
|
|
@@ -47,11 +36,8 @@ export function withCell(rules, roleId, permission, state) {
|
|
|
47
36
|
return [...without, { roleId, permission, effect: state }];
|
|
48
37
|
}
|
|
49
38
|
/**
|
|
50
|
-
* Which dropdown value a channel is showing.
|
|
51
|
-
*
|
|
52
|
-
* The server sends the scope id plus whether it is a template. A scope that is
|
|
53
|
-
* not a template is this channel's own, which is Custom; no scope at all is
|
|
54
|
-
* Everyone.
|
|
39
|
+
* Which dropdown value a channel is showing. A scope that is not a template is this
|
|
40
|
+
* channel's own, which is Custom; no scope at all is Everyone.
|
|
55
41
|
*/
|
|
56
42
|
export function scopeChoiceValue(scopeId, isTemplate) {
|
|
57
43
|
if (!scopeId)
|
|
@@ -67,12 +53,8 @@ export function scopeChoiceFromValue(value) {
|
|
|
67
53
|
return { kind: "template", templateId: value };
|
|
68
54
|
}
|
|
69
55
|
/**
|
|
70
|
-
* The payload for `server:channels:scope:set`.
|
|
71
|
-
*
|
|
72
|
-
* Custom sends its rules; the other two send none. Templates deliberately do
|
|
73
|
-
* not carry rules here — editing a template from a channel would change every
|
|
74
|
-
* other channel using it, which is the opposite of what somebody expects from
|
|
75
|
-
* a screen titled with one channel's name.
|
|
56
|
+
* The payload for `server:channels:scope:set`. Templates deliberately do not carry rules:
|
|
57
|
+
* editing one from a channel would change every other channel using it.
|
|
76
58
|
*/
|
|
77
59
|
export function scopeSetPayload(choice, rules) {
|
|
78
60
|
if (choice.kind === "custom")
|
|
@@ -82,11 +64,8 @@ export function scopeSetPayload(choice, rules) {
|
|
|
82
64
|
return { templateId: null };
|
|
83
65
|
}
|
|
84
66
|
/**
|
|
85
|
-
* The dropdown options: Everyone, then each template, then Custom.
|
|
86
|
-
*
|
|
87
|
-
* Custom last because it is the escape hatch. Somebody scanning the list should
|
|
88
|
-
* meet the shared answers first — the whole point of templates is that reaching
|
|
89
|
-
* for Custom is the uncommon choice.
|
|
67
|
+
* The dropdown options: Everyone, then each template, then Custom. Custom last, because it
|
|
68
|
+
* is the escape hatch and reaching for it is the uncommon choice.
|
|
90
69
|
*/
|
|
91
70
|
export function scopeOptions(templates) {
|
|
92
71
|
return [
|
|
@@ -98,12 +77,8 @@ export function scopeOptions(templates) {
|
|
|
98
77
|
];
|
|
99
78
|
}
|
|
100
79
|
/**
|
|
101
|
-
* A one-line summary of what a set of rules does
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* Reading is called out on its own because it is the rule with a different
|
|
105
|
-
* consequence: a role denied `read_messages` is not shown a locked channel,
|
|
106
|
-
* it is shown nothing at all, and somebody setting that deserves to be told.
|
|
80
|
+
* A one-line summary of what a set of rules does. Reading is called out on its own: a role
|
|
81
|
+
* denied `read_messages` is shown nothing at all rather than a locked channel.
|
|
107
82
|
*/
|
|
108
83
|
export function describeRules(rules, roleNames) {
|
|
109
84
|
if (rules.length === 0)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChannelRule } from "./channelRules.js";
|
|
2
|
+
/** "the X folder", or "its folder" when nothing is named. */
|
|
3
|
+
export declare function folderPhrase(folderName: string | null): string;
|
|
4
|
+
/** Shown once a channel has permissions of its own instead of following its folder. */
|
|
5
|
+
export declare function folderFollowNote(folderName: string | null): string;
|
|
6
|
+
/**
|
|
7
|
+
* The folder's half of `describeRules`: reading denied here hides the folder
|
|
8
|
+
* along with every channel in it that follows it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function describeFolderRules(rules: ChannelRule[], roleNames: Map<string, string>): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** "the X folder", or "its folder" when nothing is named. */
|
|
2
|
+
export function folderPhrase(folderName) {
|
|
3
|
+
return folderName ? `the ${folderName} folder` : "its folder";
|
|
4
|
+
}
|
|
5
|
+
/** Shown once a channel has permissions of its own instead of following its folder. */
|
|
6
|
+
export function folderFollowNote(folderName) {
|
|
7
|
+
return `Has its own permissions instead of ${folderPhrase(folderName)}'s.`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The folder's half of `describeRules`: reading denied here hides the folder
|
|
11
|
+
* along with every channel in it that follows it.
|
|
12
|
+
*/
|
|
13
|
+
export function describeFolderRules(rules, roleNames) {
|
|
14
|
+
if (rules.length === 0) {
|
|
15
|
+
return "Everyone on the server can see and use the channels in this folder.";
|
|
16
|
+
}
|
|
17
|
+
const hidden = rules
|
|
18
|
+
.filter((r) => r.permission === "read_messages" && r.effect === "deny")
|
|
19
|
+
.map((r) => roleNames.get(r.roleId) ?? r.roleId);
|
|
20
|
+
const others = rules.filter((r) => r.permission !== "read_messages").length;
|
|
21
|
+
if (hidden.length === 0) {
|
|
22
|
+
return `${others} change${others === 1 ? "" : "s"} to what roles can do in this folder's channels.`;
|
|
23
|
+
}
|
|
24
|
+
const list = hidden.length === 1
|
|
25
|
+
? hidden[0]
|
|
26
|
+
: `${hidden.slice(0, -1).join(", ")} and ${hidden[hidden.length - 1]}`;
|
|
27
|
+
const rest = others > 0 ? `, and ${others} other change${others === 1 ? "" : "s"}` : "";
|
|
28
|
+
return `${list} won't see this folder or anything in it${rest}.`;
|
|
29
|
+
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gems, metals, animals, weather, sky, and a few playful roles.
|
|
3
|
-
*
|
|
4
|
-
* Long enough that a room of thirty rarely doubles up, which is the whole
|
|
5
|
-
* reason for the size — two people called Ruby in a member list is exactly the
|
|
6
|
-
* confusion the old shared placeholder caused.
|
|
2
|
+
* Gems, metals, animals, weather, sky, and a few playful roles. Long enough that a room of
|
|
3
|
+
* thirty rarely doubles up, which is the whole reason for the size.
|
|
7
4
|
*/
|
|
8
5
|
export declare const NAME_POOL: readonly ["Ruby", "Emerald", "Diamond", "Sapphire", "Topaz", "Opal", "Amber", "Jade", "Onyx", "Pearl", "Quartz", "Garnet", "Amethyst", "Turquoise", "Obsidian", "Marble", "Granite", "Flint", "Slate", "Crystal", "Gold", "Silver", "Copper", "Bronze", "Iron", "Steel", "Cobalt", "Pewter", "Platinum", "Titanium", "Brass", "Nickel", "Shark", "Turtle", "Otter", "Falcon", "Heron", "Badger", "Lynx", "Bison", "Walrus", "Puffin", "Osprey", "Marten", "Ibex", "Tapir", "Gecko", "Manta", "Narwhal", "Pelican", "Raven", "Magpie", "Sparrow", "Kestrel", "Weasel", "Beaver", "Moose", "Elk", "Stoat", "Hedgehog", "Mongoose", "Meerkat", "Albatross", "Barnacle", "Urchin", "Cuttlefish", "Seal", "Puma", "Ocelot", "Caribou", "Wombat", "Numbat", "Quokka", "Capybara", "Sheriff", "Bandit", "Captain", "Ranger", "Scout", "Pilot", "Sailor", "Miner", "Baker", "Cooper", "Mason", "Tinker", "Herald", "Envoy", "Courier", "Gardener", "Lookout", "Skipper", "Wrangler", "Drifter", "Comet", "Nebula", "Quasar", "Aurora", "Meteor", "Eclipse", "Zenith", "Thunder", "Cyclone", "Monsoon", "Blizzard", "Drizzle", "Gale", "Frost", "Ember", "Cinder", "Vapour", "Mistral", "Zephyr", "Squall", "Canyon", "Fjord", "Glacier", "Delta", "Mesa", "Tundra", "Prairie", "Dune", "Reef", "Atoll", "Summit", "Ridge", "Hollow", "Meadow", "Thicket", "Bramble", "Harbour", "Lagoon", "Cove", "Cascade", "Cedar", "Juniper", "Willow", "Birch", "Aspen", "Alder", "Hazel", "Bracken", "Clover", "Fennel", "Sorrel", "Nettle", "Thistle", "Heather", "Saffron", "Cardamom", "Chicory", "Marigold"];
|
|
9
6
|
/**
|
|
10
|
-
* One name from the pool.
|
|
11
|
-
*
|
|
12
|
-
* `Math.random` on purpose. This picks something to be called, not a secret —
|
|
13
|
-
* the identity is a keypair, and the name has never been part of it. Reaching
|
|
14
|
-
* for the crypto RNG here would suggest otherwise.
|
|
7
|
+
* One name from the pool. `Math.random` on purpose: this picks something to be called, not a
|
|
8
|
+
* secret, and the crypto RNG here would suggest otherwise.
|
|
15
9
|
*/
|
|
16
10
|
export declare function pickRandomName(): string;
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
/* Adding to this list: only nouns, only English, nothing that reads as a real
|
|
2
|
-
|
|
3
|
-
and whoever gets one did not choose it. "Marcus" looks like a claim about
|
|
4
|
-
who somebody is; "Bandit" looks like the placeholder it is. */
|
|
1
|
+
/* Adding to this list: only nouns, only English, nothing that reads as a real person's name.
|
|
2
|
+
Whoever gets one did not choose it, and "Marcus" looks like a claim. */
|
|
5
3
|
/**
|
|
6
|
-
* Gems, metals, animals, weather, sky, and a few playful roles.
|
|
7
|
-
*
|
|
8
|
-
* Long enough that a room of thirty rarely doubles up, which is the whole
|
|
9
|
-
* reason for the size — two people called Ruby in a member list is exactly the
|
|
10
|
-
* confusion the old shared placeholder caused.
|
|
4
|
+
* Gems, metals, animals, weather, sky, and a few playful roles. Long enough that a room of
|
|
5
|
+
* thirty rarely doubles up, which is the whole reason for the size.
|
|
11
6
|
*/
|
|
12
7
|
export const NAME_POOL = [
|
|
13
8
|
// Gems and minerals
|
|
@@ -42,11 +37,8 @@ export const NAME_POOL = [
|
|
|
42
37
|
"Cardamom", "Chicory", "Marigold",
|
|
43
38
|
];
|
|
44
39
|
/**
|
|
45
|
-
* One name from the pool.
|
|
46
|
-
*
|
|
47
|
-
* `Math.random` on purpose. This picks something to be called, not a secret —
|
|
48
|
-
* the identity is a keypair, and the name has never been part of it. Reaching
|
|
49
|
-
* for the crypto RNG here would suggest otherwise.
|
|
40
|
+
* One name from the pool. `Math.random` on purpose: this picks something to be called, not a
|
|
41
|
+
* secret, and the crypto RNG here would suggest otherwise.
|
|
50
42
|
*/
|
|
51
43
|
export function pickRandomName() {
|
|
52
44
|
return NAME_POOL[Math.floor(Math.random() * NAME_POOL.length)];
|
package/dist/reports/report.d.ts
CHANGED
|
@@ -48,11 +48,8 @@ export interface Report {
|
|
|
48
48
|
extra?: Record<string, unknown>;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* What an app knows about itself when somebody opens the form.
|
|
52
|
-
*
|
|
53
|
-
* Passed in rather than read here, so the assembly below stays readable and
|
|
54
|
-
* testable: every one of these comes from somewhere that needs a running app —
|
|
55
|
-
* the Electron bridge, `expo-constants`, `Platform`, the socket layer.
|
|
51
|
+
* What an app knows about itself when somebody opens the form. Passed in rather than read
|
|
52
|
+
* here: every one of these comes from somewhere that needs a running app.
|
|
56
53
|
*/
|
|
57
54
|
export interface Diagnostics {
|
|
58
55
|
version?: string | null;
|
|
@@ -90,21 +87,14 @@ export interface Diagnostics {
|
|
|
90
87
|
logs?: string[];
|
|
91
88
|
}
|
|
92
89
|
/**
|
|
93
|
-
* The service truncates, but an app should not send a novel either.
|
|
94
|
-
*
|
|
95
|
-
* Generous rather than tight: somebody describing a bug properly is the good
|
|
96
|
-
* case, and cutting them off at a tweet is how you get "it broke" instead. The
|
|
97
|
-
* desktop's 8000 wins over the phone's 4000 on exactly that argument, which
|
|
98
|
-
* both files made and only one of them followed.
|
|
90
|
+
* The service truncates, but an app should not send a novel either. Generous rather than
|
|
91
|
+
* tight: cutting somebody off at a tweet is how you get "it broke".
|
|
99
92
|
*/
|
|
100
93
|
export declare const MESSAGE_MAX = 8000;
|
|
101
94
|
export declare const TITLE_MAX = 120;
|
|
102
95
|
/**
|
|
103
|
-
* Assemble what gets sent
|
|
104
|
-
*
|
|
105
|
-
* `type` and `message` always; every diagnostic only if it is actually known.
|
|
106
|
-
* An empty `device` object says "this app does not collect device
|
|
107
|
-
* information", which is a different and wronger claim than leaving it off.
|
|
96
|
+
* Assemble what gets sent: `type` and `message` always, and a diagnostic only if it is
|
|
97
|
+
* known. An empty `device` claims the app collects none, which is wronger than leaving it off.
|
|
108
98
|
*/
|
|
109
99
|
export declare function buildReport(type: ReportType, input: {
|
|
110
100
|
message: string;
|
package/dist/reports/report.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
/* `POST /v1/reports`. Only `type` and `message` are required
|
|
2
|
-
|
|
3
|
-
here throws, and a diagnostic an app cannot work out is left off rather than
|
|
4
|
-
guessed — a wrong Electron version is worse than no Electron version. */
|
|
1
|
+
/* `POST /v1/reports`. Only `type` and `message` are required and the service truncates
|
|
2
|
+
rather than rejecting, so nothing throws and an unknown diagnostic is left off. */
|
|
5
3
|
/**
|
|
6
|
-
* The service truncates, but an app should not send a novel either.
|
|
7
|
-
*
|
|
8
|
-
* Generous rather than tight: somebody describing a bug properly is the good
|
|
9
|
-
* case, and cutting them off at a tweet is how you get "it broke" instead. The
|
|
10
|
-
* desktop's 8000 wins over the phone's 4000 on exactly that argument, which
|
|
11
|
-
* both files made and only one of them followed.
|
|
4
|
+
* The service truncates, but an app should not send a novel either. Generous rather than
|
|
5
|
+
* tight: cutting somebody off at a tweet is how you get "it broke".
|
|
12
6
|
*/
|
|
13
7
|
export const MESSAGE_MAX = 8000;
|
|
14
8
|
export const TITLE_MAX = 120;
|
|
@@ -37,18 +31,12 @@ function count(value) {
|
|
|
37
31
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
38
32
|
}
|
|
39
33
|
/**
|
|
40
|
-
* Assemble what gets sent
|
|
41
|
-
*
|
|
42
|
-
* `type` and `message` always; every diagnostic only if it is actually known.
|
|
43
|
-
* An empty `device` object says "this app does not collect device
|
|
44
|
-
* information", which is a different and wronger claim than leaving it off.
|
|
34
|
+
* Assemble what gets sent: `type` and `message` always, and a diagnostic only if it is
|
|
35
|
+
* known. An empty `device` claims the app collects none, which is wronger than leaving it off.
|
|
45
36
|
*/
|
|
46
37
|
export function buildReport(type, input, diagnostics = {}) {
|
|
47
|
-
/* The embedded server is two fields on the wire
|
|
48
|
-
*
|
|
49
|
-
* different questions and the second is often the one that matters. Neither
|
|
50
|
-
* has a column on the service, so they go in `extra`, which is what it is
|
|
51
|
-
* for. Only the desktop can run one, and the phone simply never sets them. */
|
|
38
|
+
/* The embedded server is two fields on the wire, because "running one" and "which version"
|
|
39
|
+
* answer different questions. Neither has a column, so they go in `extra`. */
|
|
52
40
|
const extra = some({
|
|
53
41
|
embeddedServer: bool(diagnostics.embeddedServer),
|
|
54
42
|
embeddedServerVersion: str(diagnostics.embeddedServerVersion),
|
|
@@ -9,3 +9,11 @@ export interface ServerInput {
|
|
|
9
9
|
export declare function parseServerInput(input: string, opts?: {
|
|
10
10
|
defaultLegacyHost?: string;
|
|
11
11
|
}): ServerInput;
|
|
12
|
+
/** The link that opens an invite in whichever app somebody has. Leave out the code for a
|
|
13
|
+
server anyone can join. */
|
|
14
|
+
export declare function inviteLink(host: string, code?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether an address works for somebody on another network. Loopback, private and
|
|
17
|
+
* link-local ranges, CGNAT, and names like `nas` or `box.local` do not.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isPublicHost(host: string): boolean;
|
package/dist/servers/address.js
CHANGED
|
@@ -14,16 +14,12 @@ export function normalizeCode(input) {
|
|
|
14
14
|
.toLowerCase();
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* The default host for a legacy `/invite/<code>` link.
|
|
18
|
-
*
|
|
19
|
-
* Those links carry no host, and the only client ever served from a path like
|
|
20
|
-
* that is the hosted one.
|
|
17
|
+
* The default host for a legacy `/invite/<code>` link. Those carry no host, and the only
|
|
18
|
+
* client ever served from a path like that is the hosted one.
|
|
21
19
|
*/
|
|
22
20
|
const DEFAULT_LEGACY_HOST = "app.gryt.chat";
|
|
23
|
-
/*
|
|
24
|
-
address. `normalizeHost` alone
|
|
25
|
-
link's host, so you would join gryt.chat instead of the server named in the
|
|
26
|
-
query. Anything that does not parse as a link falls through to an address. */
|
|
21
|
+
/* Four shapes: an invite link with a code, one with only a host, a legacy /invite/<code>,
|
|
22
|
+
and a plain address. `normalizeHost` alone returns the link's host, so you'd join gryt.chat. */
|
|
27
23
|
export function parseServerInput(input, opts) {
|
|
28
24
|
const raw = String(input || "").trim();
|
|
29
25
|
if (!raw)
|
|
@@ -35,9 +31,8 @@ export function parseServerInput(input, opts) {
|
|
|
35
31
|
try {
|
|
36
32
|
const url = new URL(raw);
|
|
37
33
|
const path = url.pathname || "/";
|
|
38
|
-
// gryt://invite?host=…&code=… puts "invite" in the authority rather than
|
|
39
|
-
//
|
|
40
|
-
// special. Both spellings mean the same thing.
|
|
34
|
+
// gryt://invite?host=…&code=… puts "invite" in the authority rather than the path,
|
|
35
|
+
// because the scheme is not one the URL parser treats as special. Both mean the same.
|
|
41
36
|
const isInvite = path.startsWith("/invite") || url.hostname === "invite";
|
|
42
37
|
if (isInvite) {
|
|
43
38
|
const host = normalizeHost(url.searchParams.get("host") || "");
|
|
@@ -48,6 +43,9 @@ export function parseServerInput(input, opts) {
|
|
|
48
43
|
if (parts[0] === "invite" && parts[1]) {
|
|
49
44
|
return { host: legacyHost, code: normalizeCode(parts[1]) };
|
|
50
45
|
}
|
|
46
|
+
// A server anyone can join needs no code, so its link names only the host.
|
|
47
|
+
if (host)
|
|
48
|
+
return { host, code: "" };
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
catch {
|
|
@@ -56,3 +54,66 @@ export function parseServerInput(input, opts) {
|
|
|
56
54
|
}
|
|
57
55
|
return { host: normalizeHost(raw), code: "" };
|
|
58
56
|
}
|
|
57
|
+
/** The link that opens an invite in whichever app somebody has. Leave out the code for a
|
|
58
|
+
server anyone can join. */
|
|
59
|
+
export function inviteLink(host, code) {
|
|
60
|
+
const cleanCode = normalizeCode(code ?? "");
|
|
61
|
+
const query = `host=${encodeURIComponent(normalizeHost(host))}`;
|
|
62
|
+
return `https://gryt.chat/invite?${query}${cleanCode ? `&code=${encodeURIComponent(cleanCode)}` : ""}`;
|
|
63
|
+
}
|
|
64
|
+
/** Names that only mean something on one network. A bare name with no dot is one too. */
|
|
65
|
+
const LOCAL_SUFFIXES = ["localhost", "local", "localdomain", "lan", "home", "internal", "home.arpa"];
|
|
66
|
+
/** Lowercased, with any port, brackets and trailing dot removed. */
|
|
67
|
+
function bareHostname(host) {
|
|
68
|
+
const trimmed = normalizeHost(host).toLowerCase();
|
|
69
|
+
const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(trimmed);
|
|
70
|
+
if (bracketed)
|
|
71
|
+
return bracketed[1];
|
|
72
|
+
const colons = (trimmed.match(/:/g) || []).length;
|
|
73
|
+
const withoutPort = colons === 1 ? trimmed.replace(/:\d*$/, "") : trimmed;
|
|
74
|
+
return withoutPort.replace(/\.$/, "");
|
|
75
|
+
}
|
|
76
|
+
function ipv4Octets(name) {
|
|
77
|
+
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(name))
|
|
78
|
+
return null;
|
|
79
|
+
const octets = name.split(".").map(Number);
|
|
80
|
+
return octets.every((n) => n <= 255) ? octets : null;
|
|
81
|
+
}
|
|
82
|
+
function isPublicIpv4([a, b]) {
|
|
83
|
+
if (a === 0 || a === 10 || a === 127 || a >= 224)
|
|
84
|
+
return false;
|
|
85
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
86
|
+
return false; // CGNAT, and Tailscale
|
|
87
|
+
if (a === 169 && b === 254)
|
|
88
|
+
return false;
|
|
89
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
90
|
+
return false;
|
|
91
|
+
return !(a === 192 && b === 168);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether an address works for somebody on another network. Loopback, private and
|
|
95
|
+
* link-local ranges, CGNAT, and names like `nas` or `box.local` do not.
|
|
96
|
+
*/
|
|
97
|
+
export function isPublicHost(host) {
|
|
98
|
+
const name = bareHostname(host);
|
|
99
|
+
if (!name)
|
|
100
|
+
return false;
|
|
101
|
+
const v4 = ipv4Octets(name);
|
|
102
|
+
if (v4)
|
|
103
|
+
return isPublicIpv4(v4);
|
|
104
|
+
if (name.includes(":")) {
|
|
105
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(name);
|
|
106
|
+
if (mapped) {
|
|
107
|
+
const octets = ipv4Octets(mapped[1]);
|
|
108
|
+
return !!octets && isPublicIpv4(octets);
|
|
109
|
+
}
|
|
110
|
+
if (name === "::" || name === "::1")
|
|
111
|
+
return false;
|
|
112
|
+
// fc00::/7 is private, fe80::/10 is link-local.
|
|
113
|
+
return !/^f[cd]/.test(name) && !/^fe[89ab]/.test(name);
|
|
114
|
+
}
|
|
115
|
+
// No top-level domain is all digits, so `999.1.1.1` is a broken address rather than a name.
|
|
116
|
+
if (!name.includes(".") || /\.\d+$/.test(name))
|
|
117
|
+
return false;
|
|
118
|
+
return !LOCAL_SUFFIXES.some((suffix) => name === suffix || name.endsWith(`.${suffix}`));
|
|
119
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stream id for a role: the first one given on this connection, kept in place
|
|
3
|
+
* across a `replaceTrack` swap. With no connection, `streamId` is returned unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export declare function senderStreamId<Role extends string>(pc: object | null | undefined, role: Role, streamId: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Keyed by connection, so a reconnect — a new connection object — starts over. */
|
|
2
|
+
const byConnection = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* The stream id for a role: the first one given on this connection, kept in place
|
|
5
|
+
* across a `replaceTrack` swap. With no connection, `streamId` is returned unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export function senderStreamId(pc, role, streamId) {
|
|
8
|
+
if (!pc)
|
|
9
|
+
return streamId;
|
|
10
|
+
let ids = byConnection.get(pc);
|
|
11
|
+
if (!ids) {
|
|
12
|
+
ids = {};
|
|
13
|
+
byConnection.set(pc, ids);
|
|
14
|
+
}
|
|
15
|
+
return (ids[role] ??= streamId);
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gryt/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Gryt's shared application logic: one implementation of the things the desktop and mobile apps both do.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/add-esm-extensions.mjs",
|
|
24
24
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"test:comment-length": "node scripts/check-comment-length.mjs",
|
|
25
26
|
"test": "node --test \"src/**/*.test.ts\"",
|
|
26
27
|
"check-surface": "node scripts/check-public-surface.mjs",
|
|
27
28
|
"prepublishOnly": "npm run build && npm run test && npm run check-surface"
|