chatd/src/personal/gateway.ts
Rama main · réplica de lectura del repositorio original.
import type { AttachmentAlreadyKept } from "../outgoing-attachment.ts";
export type ActorCredential = {
kind: string;
secret: string;
};
// A person signing into the messenger app itself needs the address to sign in
// at, which the company's server knows and the browser does not.
export type PersonalIdentity = {
externalID: string;
name?: string;
serverURL?: string;
};
export type PersonalConversation = {
id: string;
name: string;
description?: string;
kind: "dm" | "group";
isHidden?: boolean;
roleOfExternalID?: Record<string, string>;
isPrivate: boolean;
avatarURL?: string;
participantExternalIDs?: string[];
webURL?: string;
unreadCount?: number;
};
export type NewPersonalChannel = {
name: string;
description?: string;
visibility: "open" | "private";
memberExternalIDs: string[];
};
export type CreatedPersonalChannel = PersonalConversation & {
uninvitedExternalIDs: string[];
};
export type PersonalPerson = {
externalID: string;
name: string;
avatarURL?: string;
};
// A platform that keeps its custom emoji in a registry leaves the picture out
// and answers for the name separately; one that puts it on the reaction says so
// here.
export type PersonalReaction = {
emoji: string;
imageURL?: string;
byExternalIDs: string[];
};
// A store that addresses a file by the hash of its contents says so here, so a
// reader that keeps its own copy can tell it already has these bytes without
// fetching them again. Empty when the platform names files some other way.
export type PersonalAttachment = {
id: string;
filename: string;
contentType: string;
sizeBytes: number;
digest: string;
widthPixels?: number;
heightPixels?: number;
};
export type PersonalMentions = {
externalIDs: string[];
isEveryone: boolean;
};
export type PersonalMessage = {
id: string;
conversationID: string;
parentID?: string;
authorExternalID: string;
body: string;
postedAt: string;
editedAt?: string;
mentions?: PersonalMentions;
reactions: PersonalReaction[];
attachments: PersonalAttachment[];
};
export type PersonalMessageMatch = Pick<
PersonalMessage,
"id" | "conversationID" | "parentID" | "authorExternalID" | "body" | "postedAt"
>;
export type PersonalInboxItem = {
reason: "mention" | "direct" | "thread_reply" | "channel";
message: PersonalMessageMatch;
isUnread: boolean;
};
export type PersonalHeldInboxItem = {
id: string;
body: string;
isUnread: boolean;
};
export type PersonalInbox = {
items: PersonalInboxItem[];
held: PersonalHeldInboxItem[];
hasMoreBefore: boolean;
};
export type PersonalInboxWindow = {
since?: Date;
until?: Date;
heldIDs: string[];
};
export type PersonalMessagePage = {
messages: PersonalMessage[];
hasMoreBefore: boolean;
};
export type CredentialField = {
name: string;
label: string;
isSecret: boolean;
};
export type CredentialRequirement = {
kind: "sign-in" | "secret" | "redirect";
credentialKind: string;
fields: CredentialField[];
redirectURL?: string;
};
export type IssuedCredential = {
credential: ActorCredential;
identity: PersonalIdentity;
};
export class CredentialRefused extends Error {
constructor(message: string) {
super(message);
this.name = "CredentialRefused";
}
}
export class ReadRefused extends Error {
constructor(
platform: string,
url: string,
readonly status: number,
) {
super(`${platform} answered ${status} for ${url}`);
this.name = "ReadRefused";
}
}
export type PersonalEmoji = {
name: string;
};
export type PersonalImage = {
dataURL: string;
};
export type KeptMedia = {
address: string;
digest: string;
sizeBytes: number;
contentType: string;
};
export type MediaSource = {
url: string;
contentType: string;
};
export class MediaRefused extends Error {
constructor(
readonly status: number,
readonly reason: string,
readonly digest: string,
readonly sizeBytes: number,
) {
super(`the messenger's file store refused this file (${status}: ${reason})`);
this.name = "MediaRefused";
}
}
export interface PersonalGateway {
readonly platform: string;
readonly credentialKind: string;
credentialRequirement(): CredentialRequirement;
issueCredential(answers: Record<string, string>): Promise<IssuedCredential>;
identity(actor: ActorCredential): Promise<PersonalIdentity>;
listConversations(actor: ActorCredential): Promise<PersonalConversation[]>;
listPeople(actor: ActorCredential): Promise<PersonalPerson[]>;
ensureDirectConversation(
actor: ActorCredential,
counterpartExternalIDs: string[],
): Promise<PersonalConversation>;
hideDirectConversation(actor: ActorCredential, conversationID: string): Promise<void>;
revealDirectConversation(actor: ActorCredential, conversationID: string): Promise<void>;
createChannel(actor: ActorCredential, channel: NewPersonalChannel): Promise<CreatedPersonalChannel>;
listOpenChannels(actor: ActorCredential): Promise<PersonalConversation[]>;
joinChannel(actor: ActorCredential, conversationID: string): Promise<void>;
addChannelMembers(
actor: ActorCredential,
conversationID: string,
memberExternalIDs: string[],
): Promise<{ uninvitedExternalIDs: string[] }>;
removeChannelMember(actor: ActorCredential, conversationID: string, externalID: string): Promise<void>;
leaveChannel(actor: ActorCredential, conversationID: string): Promise<void>;
handOverChannel(actor: ActorCredential, conversationID: string, newOwnerExternalID: string): Promise<void>;
addChannelOwner(
actor: ActorCredential,
conversationID: string,
externalID: string,
): Promise<{ newToChannel: boolean }>;
deleteChannel(actor: ActorCredential, conversationID: string): Promise<void>;
markConversationRead(
actor: ActorCredential,
conversationID: string,
readAt: Date,
threadRootID?: string,
): Promise<void>;
listInbox(actor: ActorCredential, window: PersonalInboxWindow): Promise<PersonalInbox>;
listMessages(
actor: ActorCredential,
conversationID: string,
before?: string,
): Promise<PersonalMessagePage>;
searchMessages(actor: ActorCredential, conversationID: string, query: string): Promise<PersonalMessageMatch[]>;
sendMessage(
actor: ActorCredential,
conversationID: string,
body: string,
parentID?: string,
attachments?: AttachmentAlreadyKept[],
mentions?: PersonalMentions,
timeZone?: string,
): Promise<PersonalMessage>;
readMedia(actor: ActorCredential, url: string, range: string): Promise<Response>;
uploadMedia(actor: ActorCredential, source: MediaSource): Promise<KeptMedia>;
editMessage(
actor: ActorCredential,
conversationID: string,
messageID: string,
body: string,
mentions?: PersonalMentions,
): Promise<PersonalMessage>;
deleteMessage(actor: ActorCredential, conversationID: string, messageID: string): Promise<void>;
addReaction(
actor: ActorCredential,
conversationID: string,
messageID: string,
emoji: string,
): Promise<void>;
removeReaction(
actor: ActorCredential,
conversationID: string,
messageID: string,
emoji: string,
): Promise<void>;
listCustomEmoji(actor: ActorCredential): Promise<PersonalEmoji[]>;
readCustomEmojiImage(
actor: ActorCredential,
name: string,
largestBytes: number,
): Promise<PersonalImage | null>;
readProfilePicture(
actor: ActorCredential,
externalID: string,
largestBytes: number,
): Promise<PersonalImage | null>;
watchArrivals(
actor: ActorCredential,
arrivalsURL: string,
typingURL?: string,
withdrawalsURL?: string,
): Promise<void>;
announceTyping(actor: ActorCredential, conversationID: string): Promise<void>;
}
export class UnsupportedByPlatform extends Error {
readonly reason = "unsupported-by-platform";
constructor(
readonly platform: string,
readonly operation: string,
) {
super(`${platform} cannot ${operation} as a person`);
this.name = "UnsupportedByPlatform";
}
}
export function requireMatchingCredential(gateway: PersonalGateway, actor: ActorCredential): void {
if (actor.kind === gateway.credentialKind) return;
throw new Error(
`${gateway.platform} needs a ${gateway.credentialKind} credential, not ${actor.kind}`,
);
}