import { ChatMessage, CommentsOrder, FilterMode, Topic, UrlCard, User, TopicStatus } from "@banta/common"; import { Subject } from "rxjs"; import * as mongodb from 'mongodb'; export interface ChatEvent { type: 'post' | 'edit' | 'like' | 'unlike' | 'delete' | 'pin' | 'unpin'; } export interface PostMessageEvent extends ChatEvent { type: 'post'; message: ChatMessage; } export interface EditMessageEvent extends ChatEvent { type: 'edit'; message: ChatMessage; } export interface PinMessageEvent extends ChatEvent { type: 'pin'; message: ChatMessage; } export interface UnpinMessageEvent extends ChatEvent { type: 'unpin'; message: ChatMessage; } export interface PinOptions { until?: number; } export interface MessageQuery { topicId: string; parentMessageId?: string; sort?: CommentsOrder; filter?: FilterMode; offset?: number; limit?: number; userId?: string; pinned?: boolean; } export interface DeleteMessageEvent extends ChatEvent { type: 'delete'; message: ChatMessage; } export interface LikeEvent extends ChatEvent { type: 'like'; message: ChatMessage; user: User; } export interface UnlikeEvent extends ChatEvent { type: 'unlike'; message: ChatMessage; user: User; } export interface Like { messageId: string; userId: string; createdAt: number; liked: boolean; } export interface PersistedUrlCard { url: string; retrievedAt: number; statusCode: number; requestDuration: number; card: UrlCard | null; } export interface AuthorizableAction { action: 'viewTopic' | 'postMessage' | 'reply' | 'editMessage' | 'likeMessage' | 'unlikeMessage' | 'deleteMessage' | 'pinMessage' | 'unpinMessage' | 'copyCleanLink'; /** * If true, this is a precheck: the user has not yet asked to perform the action. * You might want to use this to pass back a user readable error message instead of a * an application-readable `app-handle|...` message. */ precheck?: boolean; topic?: Topic | null; parentMessage?: ChatMessage | null; message?: ChatMessage; /** * Information provided by the client via the subscribeToTopic() call. This is ignored by Banta. * It can be used to pass arbitrary context information about the client into a pluggable authorizdation * handler provided by the end application. */ connectionMetadata?: Record; } export type ValidateToken = (token: string) => Promise; /** * Should throw UnauthorizedError if user is not allowed to do something. The error will be shown to the user. */ export type AuthorizeAction = (user: User | undefined, token: string | undefined, action: AuthorizableAction) => void; export interface ChatOptions { dbUrl?: string; dbName?: string; validateToken?: ValidateToken; authorizeAction?: AuthorizeAction; } /** * Represents a URL origin that we have fetched from (for URL resolution). * We need to keep track of this to implement polite link fetching. */ export interface UrlOrigin { origin: string; lastFetchedAt: number; lastFetchedUrl: string; lastFetchedStatusCode: number; cooloffPeriodMS: number; } /** * A function which scans the content of the given message and populates the mentionLinks attribute of that * message. */ export type MentionExtractor = (message: ChatMessage) => Promise; /** * For use with the extractMentions option of ChatService, this simple mention linker * will locate all "@" username references, call the passed linker function in order to * determine URLs for those usernames, and populate mentionLinks with the result. If * your platform requires more complex behavior (such as checking if the user exists * before creating the mention link), then you should implement your own mention * linker from scratch. * * @param linker A function which produces a URL for a given username (ie the profile page URL for the user or so) * @returns */ export declare const simpleMentionExtractor: (linker: (username: string) => string) => MentionExtractor; export interface ChatServerEvent { type: 'create' | 'update'; originId: string; topicId: string; message?: ChatMessage; like?: Like; } export declare class UnauthorizedError extends Error { } export declare class ChatService { private logger; private roles; originId: string; constructor(); private running; private start; private statusInterval; private startStatusInterval; private stop; private redis; private mongoClient; private db; private isc; private _messageChangedRemotely; readonly messageChangedRemotely: import("rxjs").Observable; private _likeChangedRemotely; readonly likeChangedRemotely: import("rxjs").Observable; private _topicStatusChanged; readonly topicStatusChanged: import("rxjs").Observable; activeConnections: number; activeConnectionsByTopic: Map; incrementTopicConnection(topicId: string): void; decrementTopicConnection(topicId: string): void; private syncTopicConnections; getTopicClientCount(topicId: string): Promise; private guardRunning; getCacheStatus(): { topicCount: number; messageCount: number; topics: { [k: string]: number; }; }; /** * Handle validation of a token sent by the client, and resolution of that token into a User object. * Pluggable so that particular sites can modify. * * @param token * @returns */ validateToken: ValidateToken; /** * When true, Banta will make sure the implementation for authorizeCheck() is rejecting unknown operations, to * prevent situations where new Banta features are inadvertently always allowed. * * If you set this to false, it's not our fault if you update and allow newer features to unprivileged users by * accident! */ ensureForwardCompatibility: boolean; /** * Check if a user is allowed to perform a specific action or a class of actions. * Should be provided by the site integrating Banta. If the user is not allowed, * then an error should be thrown. * * IMPORTANT: This function MUST throw UnauthorizedError when passed an action name that is not supported. * This will be tested when the server starts! */ get authorizeAction(): AuthorizeAction; set authorizeAction(value: AuthorizeAction); private _authorizeAction; private _testedAuthorizeAction; private shouldNeverAuthorize; /** * @internal */ doAuthorizeAction: AuthorizeAction; /** * Transform the message at the moment before it is posted or edited. * Any property of the message can be edited, some useful ideas: * - Replacing bad words * - Hiding at time of post (hidden = true) */ transformMessage?: (message: ChatMessage, action: 'post' | 'edit', previousMessage?: string) => Promise; /** * Extract username (and other) mentions from the text and sets mentionLinks to the ones that are found. A quick * way to implement this is to use simpleMentionExtractor. On the frontend, Banta SDK will automatically hyperlink * all matching mentions when displayed. */ extractMentions?: MentionExtractor; /** * Check whether authorizeAction() throws with the given arguments. * Provides a way to do a soft permission check. * * @param user * @param token * @param action * @returns */ checkAuthorization(user: User | undefined, token: string | undefined, action: AuthorizableAction): Promise; /** * The MongoDB `messages` collection */ get messages(): mongodb.Collection; /** * The MongoDB `urlCards` collection */ private get urlCards(); /** * The MongoDB `origins` collection */ private get origins(); /** * The MongoDB `topics` collection */ private get likes(); /** * The MongoDB `topics` collection */ private get topics(); private _events; /** * Listen for events as they happen within the chat service. */ get events(): Subject; /** * Get or create a topic with the given identifier. * @param id * @returns */ getOrCreateTopic(topicOrId: Topic | string): Promise; private topicCache; /** * Get or create a topic with the given identifier, using a cache. * @param id * @returns */ getOrCreateTopicCached(topicOrId: Topic | string): Promise; /** * Post a new ChatMessage to a topic (or a thread) * @param message * @returns */ postMessage(message: ChatMessage): Promise; /** * Efficiently modify the `messageCount` counter on the given Topic. * * @param messageOrId The message to modify * @param delta The delta to apply to the likes counter. * @returns */ modifyTopicMessageCount(topicOrId: Topic | string, delta: number): Promise; /** * Efficiently modify the `submessageCount` counter on the given ChatMessage. * * @param messageOrId The message to modify * @param delta The delta to apply to the submessages counter. * @returns */ modifySubmessageCount(messageOrId: ChatMessage | string, delta: number): Promise; /** * Efficiently modify the `likes` counter on the given ChatMessage. * * @param messageOrId The message to modify * @param delta The delta to apply to the likes counter. * @returns */ modifyMessageLikesCount(messageOrId: string | ChatMessage, delta: number): Promise; /** * Notify all interested clients that the given message has created/updated. * @param message */ notifyMessageChange(message: ChatMessage, type: 'create' | 'update'): void; /** * Modify the hidden status of the given ChatMessage. * @param messageId * @param hidden * @returns */ setMessageHiddenStatus(messageOrId: ChatMessage | string, hidden: boolean): Promise; /** * Count how many submessages are visible for the given message. * @param parentMessageId The parent message to check * @returns The number of actual visible submessages */ countVisibleSubMessages(parentMessageId: string): Promise; pinMessage(messageOrId: string | ChatMessage, options?: PinOptions): Promise; unpinMessage(messageOrId: string | ChatMessage): Promise; /** * Edit the content of the message. No permissioning is applied here. * @param message * @param newText */ editMessage(messageOrId: string | ChatMessage, newText: string): Promise; /** * Called when the author of a message is deleting a message. * Not to be confused with deletion for moderation, that should be done with * setMessageHiddenStatus() directly. Note though that this method uses * setMessageHiddenStatus() in its implementation. * * @param messageOrId */ deleteMessage(messageOrId: ChatMessage | string): Promise; getLike(userId: string, messageId: string): Promise | null>; /** * Mark a message as liked by the given user, if it is not already liked. * @param message * @param user * @returns */ like(message: ChatMessage, user: User): Promise; private findOne; private updateOne; private insertOne; private dbOperation; /** * Mark a message as not liked by a user, if it is currently liked. * @param message * @param user * @returns */ unlike(message: ChatMessage, user: User): Promise; /** * Get a specific message by ID without preparing it for delivery to the client. * @param messageOrId The ID of the message. Can also be a ChatMessage itself, for convenient calling (it will be * immediately returned) * @param throwIfMissing If true, throw an error if the message could not be found. Otherwise returns null. * @returns */ getUnpreparedMessage(messageOrId: ChatMessage | string, throwIfMissing: true): Promise; getUnpreparedMessage(messageOrId: ChatMessage | string, throwIfMissing: false): Promise; getUnpreparedMessage(messageOrId: ChatMessage | string, throwIfMissing: boolean): Promise; getUnpreparedMessage(messageOrId: ChatMessage | string): Promise; /** * Get a specific message by ID and prepare it for delivery to the client. If you do not want the message to be * prepared, instead use getUnpreparedMessage(). * * @param messageOrId The ID of the message. Can also be a ChatMessage itself, for convenient calling (it will be * immediately returned) * @param throwIfMissing If true, throw an error if the message could not be found. Otherwise returns null. * @returns */ getMessage(messageOrId: ChatMessage | string, throwIfMissing?: boolean): Promise; /** * Creates a MongoDB filter object for the given MessageQuery. * @param query * @returns */ createMongoMessagesFilterForQuery(query: MessageQuery): mongodb.Filter; /** * Creates a MongoDB filter object for the given set of parameters. This is necessary when querying the `messages` * collection directly. * @param topicId The topic to filter for. * @param parentMessageId The parent message when searching for threaded replies. Can be undefined to only fetch top level messages. * @param filterMode The filter to apply to the results. If using MINE or MY_LIKES you must specify a userId for the results to make sense. * @param userId The viewing user. Can be undefined. * @returns */ createMongoMessagesFilter(topicId: string, parentMessageId: string | undefined, filterMode: FilterMode | undefined, userId: string | undefined, pinned: boolean | undefined): mongodb.Filter; /** * Create a MongoDB sort (ie { field: 1 }) from the given sort enumeration. * @param order * @returns */ createMongoSortFromOrder(order: CommentsOrder | undefined): any; /** * Retrieve a set of messages from the database based on the parameters of the supplied MessageQuery * without preparing them for delivery to the client (by removing private information and applying user-specific * information). * @param query * @returns */ getUnpreparedMessages(query: MessageQuery): Promise; /** * Retrieve a set of messages from the database based on the parameters of the supplied MessageQuery. * @param query * @returns */ getMessages(query: MessageQuery): Promise; /** * Remove private information from the given message object and add user-specific state (such as whether * the message was "liked") if userId is provided. * @param message * @param userId * @returns */ prepareMessage(message: ChatMessage, userId?: string): ChatMessage; prepareMessage(message: ChatMessage | undefined | null, userId?: string): ChatMessage | undefined; /** * Get a topic by ID. * @param topicOrId The ID of the topic. Can also be a Topic itself, for convenient calling (it will be * immediately returned) * @param throwIfMissing If true, throw an error if the message could not be found. Otherwise returns null. * @returns */ getTopic(topicOrId: undefined): Promise; getTopic(topicOrId: Topic): Promise; getTopic(topicOrId: Topic | undefined): Promise; getTopic(topicOrId: string): Promise; getTopic(topicOrId: string, throwIfMissing: false): Promise; getTopic(topicOrId: string, throwIfMissing: true): Promise; getTopic(topicOrId: string, throwIfMissing: boolean): Promise; getTopic(topicOrId: string | Topic): Promise; getTopic(topicOrId: string | Topic | undefined, throwIfMissing: boolean): Promise; getOrigin(origin: string): Promise | null>; /** * Stores recently sent messages in an LRU cache so that we can read from it when getting existing * messages during client start and also when fetching additional pages of messages during paging. */ private recentMessageTopicCache; maxCachedMessagesPerTopic: number; private cacheMessage; cacheMessages(topicId: string, messages: ChatMessage[]): Promise; private trimRecentMessageCache; getCachedMessages(topicId: string | undefined, parentMessageId: string | undefined, offset?: number, limit?: number): ChatMessage[]; getUrlCard(url: string): Promise; }