Ctrl K
ComunidadGuías…

chatd/src/personal/gateway.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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}`,
	);
}