Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
88 linesCodeBlameRaw
1/**
2 * Who may open a conversation, and who is a guest (docs.g1t.sh/guides/chat/,
3 * "Who can see a channel"). Pure, so it is tested apart from the service.
4 *
5 * A channel is private (only the people in it), kept to its workspace
6 * (anyone in it), or public (anyone signed in to g1t). Someone in a
7 * workspace's chat who is not a member of the workspace is a guest: asked
8 * into a channel, or talking in a public one. Guests never see the rest of
9 * the workspace, are never in a direct message, and never talk to @g1t,
10 * which works for the workspace's members alone.
11 */
12
13import type { ChannelVisibility, User } from "@g1t/contracts";
14
15/** A channel row's say in who may open it. */
16export type VisibilityRow = { kind: "channel" | "dm"; private: number; public: number };
17
18export function visibilityOf(row: VisibilityRow): ChannelVisibility {
19 if (row.kind === "dm" || row.private) return "private";
20 return row.public ? "public" : "workspace";
21}
22
23/** The viewer's role in a workspace; null for someone outside it. */
24export type Standing = "owner" | "member" | null;
25
26/**
27 * Whether someone may read a conversation: anyone in it; else anyone in
28 * the workspace when it is kept to the workspace or public; else anyone
29 * signed in when it is public. Nothing private without being in it.
30 */
31export function mayRead(visibility: ChannelVisibility, role: Standing, inIt: boolean): boolean {
32 if (inIt) return true;
33 if (visibility === "private") return false;
34 return visibility === "public" || role !== null;
35}
36
37/**
38 * Whether someone not yet in a channel may join it on their own: whoever
39 * may read it, except someone outside the workspace whom its owners took
40 * out of it, and anything archived.
41 */
42export function mayJoin(visibility: ChannelVisibility, role: Standing, removed: boolean, archived: boolean): boolean {
43 if (archived || !mayRead(visibility, role, false)) return false;
44 return role !== null || !removed;
45}
46
47/**
48 * Whether someone may ask people outside the workspace into its channels:
49 * owners; members while the workspace lets members invite outside
50 * collaborators (Settings, Permissions), as for repositories.
51 */
52export function mayInviteGuests(viewer: User | null, slug: string): boolean {
53 const membership = viewer?.workspaces?.find((m) => m.slug === slug.toLowerCase());
54 if (!membership) return false;
55 if (membership.role === "owner") return true;
56 return membership.privileges?.members_can_invite_outside_collaborators !== false;
57}
58
59/**
60 * Whether a signed-in viewer may come into a workspace's chat as a guest:
61 * a person (not a token for a workspace) whose email address is confirmed,
62 * so a public channel is not open to throwaway accounts.
63 */
64export function mayVisit(viewer: User | null): string | null {
65 if (!viewer) return "Sign in to use chat.";
66 if (viewer.kind && viewer.kind !== "user") return "Only people can talk in another workspace's channels.";
67 if (!viewer.verified) return "Confirm your email address to talk in other workspaces' channels.";
68 return null;
69}
70
71/**
72 * Whether someone may be let in on a request, or asked in: anyone in the
73 * channel may add a member of the workspace (as `invite` does); only those
74 * who may invite guests may let in someone outside it.
75 */
76export function mayAdmit(requesterIsMember: boolean, approverInChannel: boolean, approverMayInviteGuests: boolean): boolean {
77 if (!approverInChannel) return false;
78 return requesterIsMember || approverMayInviteGuests;
79}
80
81/** The longest note on a request to join. */
82export const MAX_REQUEST_NOTE = 300;
83
84/** A request's note, trimmed and cut to length; null when empty. */
85export function requestNote(note: unknown): string | null {
86 const text = typeof note === "string" ? note.trim() : "";
87 return text ? text.slice(0, MAX_REQUEST_NOTE) : null;
88}