| 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 | |
| 13 | import type { ChannelVisibility, User } from "@g1t/contracts"; |
| 14 | |
| 15 | /** A channel row's say in who may open it. */ |
| 16 | export type VisibilityRow = { kind: "channel" | "dm"; private: number; public: number }; |
| 17 | |
| 18 | export 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. */ |
| 24 | export 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 | */ |
| 31 | export 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 | */ |
| 42 | export 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 | */ |
| 52 | export 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 | */ |
| 64 | export 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 | */ |
| 76 | export 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. */ |
| 82 | export const MAX_REQUEST_NOTE = 300; |
| 83 | |
| 84 | /** A request's note, trimmed and cut to length; null when empty. */ |
| 85 | export 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 | } |