UNPKG

@pazznetwork/ngx-chat

Version:

[![Build status](https://api.travis-ci.com/pazznetwork/ngx-chat.svg?branch=master)](https://travis-ci.com/pazznetwork/ngx-chat) [![Coverage](https://coveralls.io/repos/github/pazznetwork/ngx-chat/badge.svg?branch=master)](https://coveralls.io/github/pazzn

1 lines 462 kB
{"version":3,"file":"pazznetwork-ngx-chat.mjs","sources":["../../../src/lib/components/chat-filedrop/file-drop.component.ts","../../../src/lib/components/chat-filedrop/file-drop.component.html","../../../src/lib/services/chat-service.ts","../../../src/lib/components/chat-message-input/chat-message-input.component.ts","../../../src/lib/components/chat-message-input/chat-message-input.component.html","../../../src/lib/core/message.ts","../../../src/lib/services/adapters/xmpp/plugins/abstract-xmpp-plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/block.plugin.ts","../../../src/lib/services/adapters/xmpp/abstract-stanza-builder.ts","../../../src/lib/services/adapters/xmpp/xmpp-response.error.ts","../../../src/lib/core/form.ts","../../../src/lib/services/adapters/xmpp/plugins/publish-subscribe.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/muc-sub.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/message-archive.plugin.ts","../../../src/lib/services/report-user-service.ts","../../../src/lib/core/utils-links.ts","../../../src/lib/core/id-generator.ts","../../../src/lib/services/adapters/xmpp/plugins/message-uuid.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/message-state.plugin.ts","../../../src/lib/core/contact-avatar.ts","../../../src/lib/core/presence.ts","../../../src/lib/core/translations-default.ts","../../../src/lib/services/adapters/xmpp/plugins/multi-user-chat/multi-user-chat-constants.ts","../../../src/lib/services/adapters/xmpp/plugins/message.plugin.ts","../../../src/lib/services/adapters/xmpp/stanza-builder.ts","../../../src/lib/services/adapters/xmpp/plugins/service-discovery.plugin.ts","../../../src/lib/core/utils-array.ts","../../../src/lib/core/utils-date.ts","../../../src/lib/core/message-store.ts","../../../src/lib/core/recipient.ts","../../../src/lib/services/adapters/xmpp/plugins/multi-user-chat/room.ts","../../../src/lib/services/adapters/xmpp/plugins/multi-user-chat/affiliation.ts","../../../src/lib/services/adapters/xmpp/plugins/multi-user-chat/role.ts","../../../src/lib/services/adapters/xmpp/plugins/multi-user-chat/multi-user-chat.plugin.ts","../../../src/lib/core/subscription.ts","../../../src/lib/services/adapters/xmpp/plugins/roster.plugin.ts","../../../src/lib/services/log.service.ts","../../../src/lib/services/adapters/xmpp/xmpp-client-factory.service.ts","../../../src/lib/services/adapters/xmpp/xmpp-chat-connection.service.ts","../../../src/lib/core/contact.ts","../../../src/lib/services/contact-factory.service.ts","../../../src/lib/services/adapters/xmpp/xmpp-chat-adapter.service.ts","../../../src/lib/hooks/chat-contact-click-handler.ts","../../../src/lib/components/chat-avatar/chat-avatar.component.ts","../../../src/lib/components/chat-avatar/chat-avatar.component.html","../../../src/lib/components/chat-message-simple/chat-message-simple.component.ts","../../../src/lib/components/chat-message-simple/chat-message-simple.component.html","../../../src/lib/components/chat-message-link/chat-message-link.component.ts","../../../src/lib/components/chat-message-link/chat-message-link.component.html","../../../src/lib/components/chat-message-text/chat-message-text.component.ts","../../../src/lib/directives/links.directive.ts","../../../src/lib/components/chat-message/chat-message.component.ts","../../../src/lib/components/chat-message/chat-message.component.html","../../../src/lib/services/chat-list-state.service.ts","../../../src/lib/services/chat-message-list-registry.service.ts","../../../src/lib/directives/intersection-observer.directive.ts","../../../src/lib/components/chat-message-list/chat-message-list.component.ts","../../../src/lib/components/chat-message-list/chat-message-list.component.html","../../../src/lib/hooks/file-upload-handler.ts","../../../src/lib/services/chat-style.ts","../../../src/lib/components/chat-window-frame/chat-window-frame.component.ts","../../../src/lib/components/chat-window-frame/chat-window-frame.component.html","../../../src/lib/components/chat-window/chat-window.component.ts","../../../src/lib/components/chat-window/chat-window.component.html","../../../src/lib/components/chat-video-window/chat-video-window.component.ts","../../../src/lib/components/chat-video-window/chat-video-window.component.html","../../../src/lib/components/chat-window-list/chat-window-list.component.ts","../../../src/lib/components/chat-window-list/chat-window-list.component.html","../../../src/lib/services/adapters/xmpp/plugins/unread-message-count.plugin.ts","../../../src/lib/components/roster-recipient/roster-recipient.component.ts","../../../src/lib/components/roster-recipient/roster-recipient.component.html","../../../src/lib/components/roster-list/roster-list.component.ts","../../../src/lib/components/roster-list/roster-list.component.html","../../../src/lib/components/chat.component.ts","../../../src/lib/components/chat.component.html","../../../src/lib/core/get-domain.ts","../../../src/lib/services/adapters/xmpp/plugins/bookmark.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/entity-time.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/http-file-upload.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/message-carbons.plugin.ts","../../../src/lib/core/utils-timeout.ts","../../../src/lib/services/adapters/xmpp/plugins/ping.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/push.plugin.ts","../../../src/lib/services/adapters/xmpp/plugins/registration.plugin.ts","../../../src/lib/services/chat-background-notification.service.ts","../../../src/lib/ngx-chat.module.ts","../../../src/lib/core/utils-file.ts","../../../src/public-api.ts","../../../src/pazznetwork-ngx-chat.ts"],"sourcesContent":["import { Component, EventEmitter, HostListener, Input, Output } from '@angular/core';\n\n@Component({\n selector: 'ngx-chat-filedrop',\n templateUrl: './file-drop.component.html',\n styleUrls: ['./file-drop.component.less'],\n})\nexport class FileDropComponent {\n\n @Output()\n readonly fileUpload = new EventEmitter<File>();\n\n @Input()\n dropMessage: string;\n\n @Input()\n enabled = true;\n\n isDropTarget = false;\n\n @HostListener('dragover', ['$event'])\n @HostListener('dragenter', ['$event'])\n onDragOver(event: any) {\n if (this.enabled) {\n event.preventDefault();\n event.stopPropagation();\n this.isDropTarget = true;\n }\n }\n\n @HostListener('dragleave', ['$event'])\n @HostListener('dragexit', ['$event'])\n onDragLeave(event: any) {\n event.preventDefault();\n event.stopPropagation();\n this.isDropTarget = false;\n }\n\n @HostListener('drop', ['$event'])\n async onDrop(event: any) {\n if (this.enabled) {\n event.preventDefault();\n event.stopPropagation();\n\n this.isDropTarget = false;\n\n // tslint:disable-next-line:prefer-for-of\n for (let i = 0; i < event.dataTransfer.items.length; i++) {\n const dataTransferItem = event.dataTransfer.items[i];\n if (dataTransferItem.kind === 'file') {\n this.fileUpload.emit(dataTransferItem.getAsFile());\n }\n }\n }\n }\n\n}\n","<div>\n <div class=\"drop-message\"\n [class.drop-message--visible]=\"isDropTarget\">\n {{dropMessage}}\n </div>\n <div>\n <ng-content></ng-content>\n </div>\n</div>\n","import {InjectionToken} from '@angular/core';\nimport {BehaviorSubject, Observable} from 'rxjs';\nimport {Contact} from '../core/contact';\nimport {LogInRequest} from '../core/log-in-request';\nimport {ChatPlugin} from '../core/plugin';\nimport {Recipient} from '../core/recipient';\nimport {Translations} from '../core/translations';\nimport {ChatAction} from './adapters/xmpp/xmpp-chat-adapter.service';\n\n/**\n * The chat service token gives you access to the main chat api and is implemented by default with an XMPP adapter,\n * you can always reuse the api and ui with a new service implementing the ChatService interface and providing the\n * said implementation with the token\n */\nexport const CHAT_SERVICE_TOKEN = new InjectionToken<ChatService>('ngxChatService');\n\n\nexport type ConnectionStates = 'disconnected' | 'connecting' | 'online';\n\n/**\n * ChatService is your main API for using ngx-chat. Can be injected in your services like in the following example:\n *\n * ```\n * constructor(@Inject(CHAT_SERVICE_TOKEN) chatService: ChatService)\n * ```\n */\nexport interface ChatService {\n\n /**\n * Will emit the corresponding contact when a new message arrive.\n */\n message$: Observable<Contact>;\n\n /**\n * Lifecycle state machine. Starts in the state \"disconnected\". When logging in, the state will change to \"connecting\".\n * Plugins may now initialize, for example load the contact list or request archived messages. When all plugins have completed the\n * initialization, the new state will be 'online'.\n */\n state$: BehaviorSubject<ConnectionStates>;\n\n /**\n * A BehaviorSubject of all known contacts. Contains for example Contacts that sent you a message or blocked contacts.\n * This does not represent your roster list.\n */\n contacts$: BehaviorSubject<Contact[]>;\n\n /**\n * A list of contacts which the current user has blocked.\n */\n blockedContacts$: Observable<Contact[]>;\n\n /**\n * contacts$ without the blockedContacts$.\n */\n notBlockedContacts$: Observable<Contact[]>;\n\n /**\n * A list of contacts to which the current user has accepted subscriptions to.\n */\n contactsSubscribed$: Observable<Contact[]>;\n\n /**\n * A list of contacts to which a subscription from the user is outstanding.\n */\n contactRequestsSent$: Observable<Contact[]>;\n\n /**\n * A list of contacts which have sent the user a subscription request.\n */\n contactRequestsReceived$: Observable<Contact[]>;\n\n /**\n * A list of contacts where the user is not subscribed to and neither a pending request is incoming or outgoing.\n */\n contactsUnaffiliated$: Observable<Contact[]>;\n\n /**\n * If set to true, debug information will be visible in the roster list.\n */\n enableDebugging: boolean;\n\n /**\n * The avatar of the user. Is used as src attribute of an img-element. Purely cosmetical. Should be set via the\n * [userAvatar$]{@link ChatComponent#userAvatar$} @Input-attribute of {@link ChatComponent}.\n */\n userAvatar$: BehaviorSubject<string>;\n\n /**\n * The current translation. Do NOT write to this attribute, use the [translations]{@link ChatComponent#translations} @Input-attribute\n * of {@link ChatComponent} instead.\n */\n translations: Translations;\n\n /**\n * The actions visible to users near to chat inputs, e.g. the send message button. Customize it for branding or to add\n * new actions, e.g. for file uploads.\n */\n chatActions: ChatAction<{sendMessage: () => void}>[];\n\n /**\n * Forces asynchronous reloading of your roster list from the server, {@link contacts$} will reflect this.\n */\n reloadContacts(): void;\n\n /**\n * Returns the contact with the given ID or undefined if no contact with the given ID is found. In case of XMPP it does not have to be\n * bare, the search will convert it to a bare JID.\n * @param id The ID of the contact.\n * @returns Either the Contact or undefined.\n */\n getContactById(id: string): Contact | undefined;\n\n /**\n * Always returns a contact with the given ID. If no contact exists, a new one is created and announced via contacts$. In case of XMPP\n * it does not have to be bare, the search will convert it to a bare JID.\n * @param id The ID of the contact.\n * @returns The new contact instance.\n */\n getOrCreateContactById(id: string): Contact;\n\n /**\n * Adds the given contact to the user roster. Will send a subscription request to the contact.\n * @param identifier The ID of the contact.\n */\n addContact(identifier: string): void;\n\n /**\n * Removes the given contact from the user roster. Will cancel a presence subscription from the user to the contact and will retract\n * accepted subscriptions from the contact to the user.\n * @param identifier The ID of the contact.\n */\n removeContact(identifier: string): void;\n\n /**\n * Logs the user in. Will modify state$ accordingly. If login fails, state will stay in 'disconnected'.\n */\n logIn(logInRequest: LogInRequest): void;\n\n /**\n * Disconnects from the server, clears contacts$, sets state$ to 'disconnected'.\n */\n logOut(): void;\n\n /**\n * Sends a given message to a given contact.\n * @param recipient The recipient to which the message shall be sent.\n * @param body The message content.\n */\n sendMessage(recipient: Recipient, body: string): void;\n\n /**\n * Requests all archived messages for all contacts from the server.\n */\n loadCompleteHistory(): Promise<void>;\n\n /**\n * Returns the plugin instance for the given constructor\n * @param constructor The plugin constructor, e.g. {@link RosterPlugin}\n */\n getPlugin<T extends ChatPlugin>(constructor: new(...args: any[]) => T): T;\n\n /**\n * Tries to transparently (= without the user noticing) reconnect to the chat server.\n */\n reconnectSilently(): void;\n\n /**\n * Tries to reconnect with the same credentials the user logged in last.\n */\n reconnect(): void;\n\n}\n","import { Component, ElementRef, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core';\nimport { Recipient } from '../../core/recipient';\nimport { CHAT_SERVICE_TOKEN, ChatService } from '../../services/chat-service';\n\n@Component({\n selector: 'ngx-chat-message-input',\n templateUrl: './chat-message-input.component.html',\n styleUrls: ['./chat-message-input.component.less'],\n})\nexport class ChatMessageInputComponent implements OnInit {\n\n @Input()\n public recipient: Recipient;\n\n @Output()\n public messageSent = new EventEmitter<void>();\n\n public message = '';\n\n @ViewChild('chatInput')\n chatInput: ElementRef;\n\n constructor(@Inject(CHAT_SERVICE_TOKEN) public chatService: ChatService) {\n }\n\n ngOnInit() {\n }\n\n onSendMessage($event?: KeyboardEvent) {\n if ($event) {\n $event.preventDefault();\n }\n this.chatService.sendMessage(this.recipient, this.message);\n this.message = '';\n this.messageSent.emit();\n }\n\n focus() {\n this.chatInput.nativeElement.focus();\n }\n\n}\n","<textarea class=\"chat-input\"\n #chatInput\n [(ngModel)]=\"message\"\n (keydown.enter)=\"onSendMessage($event)\"\n cdkTextareaAutosize\n cdkAutosizeMinRows=\"1\"\n cdkAutosizeMaxRows=\"5\"\n placeholder=\"{{chatService.translations.placeholder}}\"></textarea>\n","export enum MessageState {\n /**\n * Not yet sent\n */\n SENDING = 'sending',\n /**\n * Sent, but neither received nor seen by the recipient\n */\n SENT = 'sent',\n /**\n * The recipient client has received the message but the recipient has not seen it yet\n */\n RECIPIENT_RECEIVED = 'recipientReceived',\n /**\n * The message has been seen by the recipient\n */\n RECIPIENT_SEEN = 'recipientSeen',\n}\n\nexport interface Message {\n direction: Direction;\n body: string;\n datetime: Date;\n id?: string;\n delayed: boolean;\n fromArchive: boolean;\n /**\n * if no explicit state is set for the message, use implicit contact message states instead.\n */\n state?: MessageState;\n}\n\nexport enum Direction {\n in = 'in',\n out = 'out',\n}\n","import { Element } from 'ltx';\nimport { Message } from '../../../../core/message';\nimport { ChatPlugin } from '../../../../core/plugin';\nimport { MessageWithBodyStanza, Stanza } from '../../../../core/stanza';\nimport { MessageReceivedEvent } from './message.plugin';\n\nexport abstract class AbstractXmppPlugin implements ChatPlugin {\n\n onBeforeOnline(): PromiseLike<any> {\n return Promise.resolve();\n }\n\n onOffline() {\n }\n\n afterSendMessage(message: Message, messageStanza: Element): void {\n return;\n }\n\n beforeSendMessage(messageStanza: Element, message: Message): void {\n return;\n }\n\n handleStanza(stanza: Stanza): boolean {\n return false;\n }\n\n afterReceiveMessage(message: Message, messageStanza: MessageWithBodyStanza, messageReceivedEvent: MessageReceivedEvent): void {\n return;\n }\n\n}\n","import { xml } from '@xmpp/client';\nimport { BehaviorSubject } from 'rxjs';\nimport { Stanza } from '../../../../core/stanza';\nimport { XmppChatAdapter } from '../xmpp-chat-adapter.service';\nimport { AbstractXmppPlugin } from './abstract-xmpp-plugin';\nimport { ServiceDiscoveryPlugin } from './service-discovery.plugin';\n\n/**\n * XEP-0191: Blocking Command\n * https://xmpp.org/extensions/xep-0191.html\n */\nexport class BlockPlugin extends AbstractXmppPlugin {\n\n public supportsBlock$ = new BehaviorSubject<boolean | 'unknown'>('unknown');\n\n constructor(\n private xmppChatAdapter: XmppChatAdapter,\n private serviceDiscoveryPlugin: ServiceDiscoveryPlugin,\n ) {\n super();\n }\n\n async onBeforeOnline() {\n const supportsBlock = await this.determineSupportForBlock();\n this.supportsBlock$.next(supportsBlock);\n if (supportsBlock) {\n await this.requestBlockedJids();\n }\n }\n\n private async determineSupportForBlock() {\n try {\n return await this.serviceDiscoveryPlugin.supportsFeature(\n this.xmppChatAdapter.chatConnectionService.userJid.domain,\n 'urn:xmpp:blocking');\n } catch (e) {\n return false;\n }\n }\n\n onOffline() {\n this.supportsBlock$.next('unknown');\n this.xmppChatAdapter.blockedContactIds$.next(new Set<string>());\n }\n\n blockJid(jid: string) {\n return this.xmppChatAdapter.chatConnectionService.sendIq(\n xml('iq', {type: 'set'},\n xml('block', {xmlns: 'urn:xmpp:blocking'},\n xml('item', {jid}))));\n }\n\n unblockJid(jid: string) {\n return this.xmppChatAdapter.chatConnectionService.sendIq(\n xml('iq', {type: 'set'},\n xml('unblock', {xmlns: 'urn:xmpp:blocking'},\n xml('item', {jid}))));\n }\n\n private async requestBlockedJids() {\n const blockListResponse = await this.xmppChatAdapter.chatConnectionService.sendIq(\n xml('iq', {type: 'get'},\n xml('blocklist', {xmlns: 'urn:xmpp:blocking'})\n )\n );\n\n const blockedJids = blockListResponse\n .getChild('blocklist')\n .getChildren('item')\n .map(e => e.attrs.jid);\n\n this.xmppChatAdapter.blockedContactIds$.next(new Set<string>(blockedJids));\n }\n\n handleStanza(stanza: Stanza): boolean {\n const {from} = stanza.attrs;\n if (from && from === this.xmppChatAdapter.chatConnectionService.userJid?.bare().toString()) {\n const blockPush = stanza.getChild('block', 'urn:xmpp:blocking');\n const unblockPush = stanza.getChild('unblock', 'urn:xmpp:blocking');\n const blockList = this.xmppChatAdapter.blockedContactIds$.getValue();\n if (blockPush) {\n blockPush.getChildren('item')\n .map(e => e.attrs.jid as string)\n .forEach(jid => blockList.add(jid));\n this.xmppChatAdapter.blockedContactIds$.next(blockList);\n return true;\n } else if (unblockPush) {\n const jidsToUnblock = unblockPush.getChildren('item').map(e => e.attrs.jid as string);\n if (jidsToUnblock.length === 0) {\n // unblock everyone\n blockList.clear();\n } else {\n // unblock individually\n jidsToUnblock.forEach(jid => blockList.delete(jid));\n }\n this.xmppChatAdapter.blockedContactIds$.next(blockList);\n return true;\n }\n }\n return false;\n }\n\n}\n","import { Element } from 'ltx';\n\nexport abstract class AbstractStanzaBuilder {\n\n abstract toStanza(): Element;\n\n}\n","import {Stanza} from '../../../core/stanza';\n\nexport class XmppResponseError extends Error {\n static readonly ERROR_ELEMENT_NS = 'urn:ietf:params:xml:ns:xmpp-stanzas';\n readonly errorCode?: number;\n readonly errorType?: string;\n readonly errorCondition?: string;\n\n constructor(readonly errorStanza: Stanza) {\n super(\n XmppResponseError.extractErrorTextFromErrorResponse(\n errorStanza,\n XmppResponseError.extractErrorDataFromErrorResponse(errorStanza),\n ),\n );\n\n const {code, type, condition} = XmppResponseError.extractErrorDataFromErrorResponse(errorStanza);\n this.errorCode = code;\n this.errorType = type;\n this.errorCondition = condition;\n }\n\n private static extractErrorDataFromErrorResponse(stanza: Stanza): {\n code?: number,\n type?: string,\n condition?: string\n } {\n const errorElement = stanza.getChild('error') as Stanza;\n const errorCode = Number(errorElement?.attrs.code) || undefined;\n const errorType = errorElement?.attrs.type as string | undefined;\n const errorCondition =\n errorElement\n ?.children\n .filter(childElement =>\n childElement.getName() !== 'text' &&\n childElement.attrs.xmlns === XmppResponseError.ERROR_ELEMENT_NS,\n )[0]\n ?.getName();\n\n return {\n code: errorCode,\n type: errorType,\n condition: errorCondition,\n };\n }\n\n private static extractErrorTextFromErrorResponse(\n stanza: Stanza,\n {code, type, condition}: {\n code?: number,\n type?: string,\n condition?: string\n }): string {\n const additionalData = [\n `errorCode: ${code ?? '[unknown]'}`,\n `errorType: ${type ?? '[unknown]'}`,\n `errorCondition: ${condition ?? '[unknown]'}`,\n ].join(', ');\n const errorText =\n stanza.getChild('error')?.getChildText('text', XmppResponseError.ERROR_ELEMENT_NS) || 'Unknown error';\n\n return `XmppResponseError: ${errorText}${additionalData ? ` (${additionalData})` : ''}`;\n }\n}\n","import { Element } from 'ltx';\nimport { jid as parseJid, JID } from '@xmpp/jid';\nimport { xml } from '@xmpp/client';\n\n// implements https://xmpp.org/extensions/xep-0004.html\n\nexport const FORM_NS = 'jabber:x:data';\n\nexport type FormType = 'form' | 'submit' | 'cancel' | 'result';\n\nexport interface Form {\n type: FormType;\n title?: string;\n instructions: string[];\n fields: FormField[];\n}\n\nexport type FieldType =\n | 'fixed'\n | 'boolean'\n | 'hidden'\n | 'jid-single'\n | 'jid-multi'\n | 'list-single'\n | 'list-multi'\n | 'text-single'\n | 'text-private'\n | 'text-multi';\n\nexport interface FieldValueType {\n fixed: string;\n boolean: boolean;\n hidden: string;\n 'jid-single': JID;\n 'jid-multi': JID[];\n 'list-single': string;\n 'list-multi': string[];\n 'text-single': string;\n 'text-private': string;\n 'text-multi': string[];\n}\n\nexport type FormField =\n | FixedFormField\n | BooleanFormField\n | TextualFormField\n | JidSingleFormField\n | JidMultiFormField\n | ListSingleFormField\n | ListMultiFormField\n | TextMultiFormField;\n\nexport interface FixedFormField {\n type: 'fixed';\n variable?: string;\n value: string;\n}\n\ninterface FormFieldBase<TFieldType extends FieldType> {\n type: TFieldType;\n variable: string;\n label?: string;\n required?: boolean;\n description?: string;\n value?: FieldValueType[TFieldType];\n}\n\nexport type BooleanFormField = FormFieldBase<'boolean'>;\nexport type TextualFormField = FormFieldBase<'hidden' | 'text-single' | 'text-private'>;\nexport type JidSingleFormField = FormFieldBase<'jid-single'>;\nexport type JidMultiFormField = FormFieldBase<'jid-multi'>;\nexport type TextMultiFormField = FormFieldBase<'text-multi'>;\n\ninterface ListFormField<TFieldType extends 'list-single' | 'list-multi'> extends FormFieldBase<TFieldType> {\n options?: FieldOption[];\n}\n\nexport type ListSingleFormField = ListFormField<'list-single'>;\nexport type ListMultiFormField = ListFormField<'list-multi'>;\n\nexport interface FieldOption {\n label?: string;\n value: string;\n}\n\nfunction parseStringValue([valueEl]: Element[]): string {\n return valueEl?.getText();\n}\n\nfunction parseMultipleStringValues(valueEls: Element[]): string[] {\n return valueEls.map(el => parseStringValue([el]));\n}\n\nfunction parseJidValue([valueEl]: Element[]): JID {\n return valueEl && parseJid(valueEl.getText());\n}\n\nconst valueParsers = {\n fixed: parseStringValue,\n boolean: ([valueEl]: Element[]): boolean => {\n if (!valueEl) {\n return false;\n }\n const value = valueEl.getText();\n return value === '1' || value === 'true';\n },\n hidden: parseStringValue,\n 'jid-single': parseJidValue,\n 'jid-multi': (valueEls: Element[]): JID[] =>\n [\n ...new Set(\n valueEls.map(el => parseStringValue([el])),\n ),\n ]\n .map(jidStr => parseJid(jidStr)),\n 'list-single': parseStringValue,\n 'list-multi': parseMultipleStringValues,\n 'text-single': parseStringValue,\n 'text-private': parseStringValue,\n 'text-multi': parseMultipleStringValues,\n};\n\nexport function parseForm(formEl: Element): Form {\n if (formEl.name !== 'x' || formEl.getNS() !== FORM_NS) {\n throw new Error(`Provided element is not a form element: elementName=${formEl.name}, xmlns=${formEl.getNS()}, form=${formEl.toString()}`);\n }\n\n return {\n type: formEl.attrs.type,\n title: formEl.getChildText('title') ?? undefined,\n instructions: formEl.getChildren('instructions').map(descEl => descEl.getText()),\n fields: formEl.getChildren('field')\n .map(fieldEl => {\n const rawType = fieldEl.attrs.type as string;\n const type = rawType in valueParsers ? rawType as keyof typeof valueParsers : 'text-single';\n const {var: variable, label}: { var?: string, label?: string } = fieldEl.attrs;\n let options: FieldOption[] | undefined;\n if (type === 'list-single' || type === 'list-multi') {\n options = fieldEl.getChildren('option').map(optionEl => ({\n value: optionEl.getChildText('value'),\n label: optionEl.attrs.label,\n }));\n }\n return {\n type,\n variable,\n label,\n description: fieldEl.getChildText('desc') ?? undefined,\n required: fieldEl.getChild('required') != null,\n value: valueParsers[type](fieldEl.getChildren('value')),\n options,\n } as FormField;\n }),\n };\n}\n\nexport function getField(form: Form, variable: string): FormField | undefined {\n return form.fields.find(field => field.variable === variable) ?? undefined;\n}\n\nexport function setFieldValue<TFieldType extends FieldType, TValue extends FieldValueType[TFieldType]>(\n form: Form,\n type: TFieldType,\n variable: string,\n value: TValue,\n createField = false,\n) {\n let field = form.fields\n .find((f) => f.variable === variable);\n\n if (field) {\n if (field.type !== type) {\n throw new Error(`type mismatch setting field value: variable=${field.variable}, field.type=${field.type}, requested type=${type}`);\n }\n field.value = value;\n return;\n }\n\n if (createField) {\n field = {\n type,\n variable,\n value,\n } as FormField;\n form.fields.push(field);\n } else {\n throw new Error(`field for variable not found! variable=${variable}, type=${type}, value=${value}`);\n }\n}\n\nfunction serializeTextualField(field: TextualFormField | ListSingleFormField): string[] {\n return field.value != null ? [field.value] : [];\n}\n\nfunction serializeTextualMultiField(field: ListMultiFormField | TextMultiFormField): string[] {\n return field.value;\n}\n\nconst valueSerializers: Record<FieldType, (field: FormField) => string[]> = {\n fixed: serializeTextualField,\n boolean: (field: BooleanFormField) => field.value != null ? [String(field.value)] : [],\n hidden: serializeTextualField,\n 'jid-single': (field: JidSingleFormField) => field.value ? [field.value.toString()] : [],\n 'jid-multi': (field: JidMultiFormField) => field.value.map(jid => jid.toString()),\n 'list-single': serializeTextualField,\n 'list-multi': serializeTextualMultiField,\n 'text-single': serializeTextualField,\n 'text-private': serializeTextualField,\n 'text-multi': serializeTextualMultiField,\n};\n\nexport function serializeToSubmitForm(form: Form): Element {\n const serializedFields = form.fields\n .reduce<[string, string[]][]>((collectedFields, field) => {\n const serializer = valueSerializers[field.type];\n if (!serializer) {\n throw new Error(`unknown field type: ${field.type}`);\n }\n\n const values = serializer(field);\n\n if (field.variable != null && values.length > 0) {\n collectedFields.push([field.variable, values]);\n }\n\n return collectedFields;\n }, []);\n\n return xml('x', {xmlns: FORM_NS, type: 'submit'},\n ...serializedFields.map(\n ([variable, values]) =>\n xml(\n 'field',\n {var: variable},\n ...values.map(value => xml('value', {}, value)),\n ),\n ),\n );\n}\n","import { xml } from '@xmpp/client';\nimport { Element } from 'ltx';\nimport { BehaviorSubject, Subject } from 'rxjs';\nimport { filter } from 'rxjs/operators';\nimport { IqResponseStanza, Stanza } from '../../../../core/stanza';\nimport { AbstractStanzaBuilder } from '../abstract-stanza-builder';\nimport { XmppResponseError } from '../xmpp-response.error';\nimport { XmppChatAdapter } from '../xmpp-chat-adapter.service';\nimport { AbstractXmppPlugin } from './abstract-xmpp-plugin';\nimport { ServiceDiscoveryPlugin } from './service-discovery.plugin';\nimport { serializeToSubmitForm } from '../../../../core/form';\n\nexport const PUBSUB_EVENT_XMLNS = 'http://jabber.org/protocol/pubsub#event';\n\ninterface PublishOptions {\n node?: string;\n id?: any;\n data?: Element;\n persistItems?: boolean;\n}\n\nclass PublishStanzaBuilder extends AbstractStanzaBuilder {\n\n private readonly publishOptions: PublishOptions = {\n persistItems: false,\n };\n\n constructor(options: PublishOptions) {\n super();\n if (options) {\n this.publishOptions = {...this.publishOptions, ...options};\n }\n }\n\n toStanza() {\n const {node, id, persistItems} = this.publishOptions;\n\n // necessary as a 'event-only' publish is currently broken in ejabberd, see\n // https://github.com/processone/ejabberd/issues/2799\n const data = this.publishOptions.data || xml('data');\n\n return xml('iq', {type: 'set'},\n xml('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'},\n xml('publish', {node},\n xml('item', {id}, data),\n ),\n xml('publish-options', {},\n serializeToSubmitForm({\n type: 'submit',\n instructions: [],\n fields: [\n {type: 'hidden', variable: 'FORM_TYPE', value: 'http://jabber.org/protocol/pubsub#publish-options'},\n {type: 'boolean', variable: 'pubsub#persist_items', value: persistItems === true},\n {type: 'list-single', variable: 'pubsub#access_model', value: 'whitelist'},\n ],\n }),\n ),\n ),\n );\n }\n\n}\n\nclass RetrieveDataStanzaBuilder extends AbstractStanzaBuilder {\n\n constructor(private readonly node: string) {\n super();\n }\n\n toStanza() {\n return xml('iq', {type: 'get'},\n xml('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'},\n xml('items', {node: this.node}),\n ),\n );\n }\n\n}\n\n/**\n * XEP-0060 Publish Subscribe (https://xmpp.org/extensions/xep-0060.html)\n * XEP-0223 Persistent Storage of Private Data via PubSub (https://xmpp.org/extensions/xep-0223.html)\n */\nexport class PublishSubscribePlugin extends AbstractXmppPlugin {\n\n readonly publish$ = new Subject<Stanza>();\n private readonly supportsPrivatePublish = new BehaviorSubject<boolean | 'unknown'>('unknown');\n\n constructor(\n private readonly xmppChatAdapter: XmppChatAdapter,\n private readonly serviceDiscoveryPlugin: ServiceDiscoveryPlugin,\n ) {\n super();\n }\n\n onBeforeOnline() {\n return this.determineSupportForPrivatePublish();\n }\n\n onOffline() {\n this.supportsPrivatePublish.next('unknown');\n }\n\n storePrivatePayloadPersistent(node: string, id: string, data: Element): Promise<IqResponseStanza<'result'>> {\n return new Promise((resolve, reject) => {\n this.supportsPrivatePublish\n .pipe(filter(support => support !== 'unknown'))\n .subscribe((support: boolean) => {\n if (!support) {\n reject(new Error('does not support private publish subscribe'));\n } else {\n resolve(this.xmppChatAdapter.chatConnectionService.sendIq(\n new PublishStanzaBuilder({node, id, data, persistItems: true}).toStanza(),\n ));\n }\n });\n });\n }\n\n privateNotify(node: string, data?: Element, id?: string): Promise<IqResponseStanza> {\n return new Promise((resolve, reject) => {\n this.supportsPrivatePublish\n .pipe(filter(support => support !== 'unknown'))\n .subscribe((support: boolean) => {\n if (!support) {\n reject(new Error('does not support private publish subscribe'));\n } else {\n resolve(this.xmppChatAdapter.chatConnectionService.sendIq(\n new PublishStanzaBuilder({node, id, data, persistItems: false}).toStanza(),\n ));\n }\n });\n });\n }\n\n handleStanza(stanza: Stanza): boolean {\n const eventElement = stanza.getChild('event', PUBSUB_EVENT_XMLNS) as Stanza;\n if (stanza.is('message') && eventElement) {\n this.publish$.next(eventElement);\n return true;\n }\n return false;\n }\n\n async retrieveNodeItems(node: string): Promise<Element[]> {\n try {\n const iqResponseStanza = await this.xmppChatAdapter.chatConnectionService.sendIq(\n new RetrieveDataStanzaBuilder(node).toStanza(),\n );\n return iqResponseStanza.getChild('pubsub').getChild('items').getChildren('item');\n } catch (e) {\n if (e instanceof XmppResponseError &&\n (e.errorCondition === 'item-not-found' || e.errorCode === 404)) {\n return [];\n }\n\n throw e;\n }\n }\n\n private async determineSupportForPrivatePublish() {\n let isSupported: boolean;\n try {\n const service = await this.serviceDiscoveryPlugin.findService('pubsub', 'pep');\n isSupported = service.features.includes('http://jabber.org/protocol/pubsub#publish-options');\n } catch (e) {\n isSupported = false;\n }\n this.supportsPrivatePublish.next(isSupported);\n }\n\n}\n","import { xml } from '@xmpp/client';\nimport { BehaviorSubject } from 'rxjs';\nimport { XmppChatAdapter } from '../xmpp-chat-adapter.service';\nimport { AbstractXmppPlugin } from './abstract-xmpp-plugin';\nimport { ServiceDiscoveryPlugin } from './service-discovery.plugin';\nimport { Stanza } from '../../../../core/stanza';\n\nexport const MUC_SUB_FEATURE_ID = 'urn:xmpp:mucsub:0';\n\nexport enum MUC_SUB_EVENT_TYPE {\n presence = 'urn:xmpp:mucsub:nodes:presence',\n messages = 'urn:xmpp:mucsub:nodes:messages',\n affiliations = 'urn:xmpp:mucsub:nodes:affiliations',\n subscribers = 'urn:xmpp:mucsub:nodes:subscribers',\n config = 'urn:xmpp:mucsub:nodes:config',\n subject = 'urn:xmpp:mucsub:nodes:subject',\n system = 'urn:xmpp:mucsub:nodes:system',\n}\n\n/**\n * support for https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/\n */\nexport class MucSubPlugin extends AbstractXmppPlugin {\n private readonly supportsMucSub$ = new BehaviorSubject<boolean | 'unknown'>('unknown');\n\n constructor(\n private readonly xmppChatAdapter: XmppChatAdapter,\n private readonly serviceDiscoveryPlugin: ServiceDiscoveryPlugin,\n ) {\n super();\n }\n\n onBeforeOnline(): PromiseLike<void> {\n return this.determineSupportForMucSub();\n }\n\n private async determineSupportForMucSub() {\n let isSupported: boolean;\n try {\n const service = await this.serviceDiscoveryPlugin.findService('conference', 'text');\n isSupported = service.features.includes(MUC_SUB_FEATURE_ID);\n } catch (e) {\n isSupported = false;\n }\n this.supportsMucSub$.next(isSupported);\n }\n\n onOffline() {\n this.supportsMucSub$.next('unknown');\n }\n\n async subscribeRoom(roomJid: string, nodes: string[] = []): Promise<void> {\n const nick = this.xmppChatAdapter.chatConnectionService.userJid.local;\n await this.xmppChatAdapter.chatConnectionService.sendIq(\n makeSubscribeRoomStanza(roomJid, nick, nodes)\n );\n }\n\n async unsubscribeRoom(roomJid: string): Promise<void> {\n await this.xmppChatAdapter.chatConnectionService.sendIq(\n makeUnsubscribeRoomStanza(roomJid)\n );\n }\n\n /**\n * A room moderator can unsubscribe others providing the their jid as attribute to the information query (iq)\n * see: https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/#unsubscribing-from-a-muc-room\n * @param roomJid for the room to be unsubscribed from\n * @param jid user id to be unsubscribed\n */\n unsubscribeJidFromRoom(roomJid: string, jid: string) {\n this.xmppChatAdapter.chatConnectionService.sendIq(\n xml('iq', {type: 'set', to: roomJid},\n xml('unsubscribe', {xmlns: 'urn:xmpp:mucsub:0', jid}),\n ),\n );\n }\n\n /**\n * A user can query the MUC service to get their list of subscriptions.\n * see: https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/#g dd ddetting-list-of-subscribed-rooms\n */\n async getSubscribedRooms() {\n const {local, domain} = this.xmppChatAdapter.chatConnectionService.userJid;\n const from = `${local}@${domain}`;\n const subscriptions = await this.xmppChatAdapter.chatConnectionService.sendIq(\n xml('iq', {type: 'get', from, to: 'muc.' + domain},\n xml('subscriptions', {xmlns: 'urn:xmpp:mucsub:0'}),\n ),\n );\n return subscriptions.getChildren('subscription').map(sub => sub.getAttr('jid'));\n }\n\n /**\n * A subscriber or room moderator can get the list of subscribers by sending <subscriptions/> request directly to the room JID.\n * see: https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/#getting-list-of-subscribers-of-a-room\n * @param roomJid of the room the get a subscriber list from\n */\n getSubscribers(roomJid: string) {\n this.xmppChatAdapter.chatConnectionService.sendIq(\n xml('iq', {type: 'get', to: roomJid},\n xml('subscriptions', {xmlns: 'urn:xmpp:mucsub:0'}),\n ),\n );\n }\n\n async retrieveSubscriptions(): Promise<Map<string, string[]>> {\n const service = await this.serviceDiscoveryPlugin.findService('conference', 'text');\n\n const result = await this.xmppChatAdapter.chatConnectionService.sendIq(\n makeRetrieveSubscriptionsStanza(service.jid)\n );\n\n const subscriptions = result\n .getChild('subscriptions', MUC_SUB_FEATURE_ID)\n ?.getChildren('subscription')\n ?.map(subscriptionElement => {\n const subscribedEvents: string[] = subscriptionElement\n .getChildren('event')\n ?.map(eventElement => eventElement.attrs.node) ?? [];\n return [subscriptionElement.attrs.jid as string, subscribedEvents] as const;\n });\n\n return new Map(subscriptions);\n }\n}\n\nfunction makeSubscribeRoomStanza(roomJid: string, nick: string, nodes: readonly string[]): Stanza {\n return xml('iq', {type: 'set', to: roomJid},\n xml('subscribe', {xmlns: MUC_SUB_FEATURE_ID, nick},\n ...nodes.map(node => xml('event', {node}))\n )\n ) as Stanza;\n}\n\nfunction makeUnsubscribeRoomStanza(roomJid: string): Stanza {\n return xml('iq', {type: 'set', to: roomJid},\n xml('unsubscribe', {xmlns: MUC_SUB_FEATURE_ID})\n ) as Stanza;\n}\n\nfunction makeRetrieveSubscriptionsStanza(conferenceServiceJid: string): Stanza {\n return xml('iq', {type: 'get', to: conferenceServiceJid},\n xml('subscriptions', {xmlns: MUC_SUB_FEATURE_ID})\n ) as Stanza;\n}\n","import {xml} from '@xmpp/client';\nimport {Subject} from 'rxjs';\nimport {debounceTime, filter} from 'rxjs/operators';\nimport {Recipient} from '../../../../core/recipient';\nimport {Stanza} from '../../../../core/stanza';\nimport {LogService} from '../../../log.service';\nimport {XmppChatAdapter} from '../xmpp-chat-adapter.service';\nimport {AbstractXmppPlugin} from './abstract-xmpp-plugin';\nimport {MultiUserChatPlugin} from './multi-user-chat/multi-user-chat.plugin';\nimport {ServiceDiscoveryPlugin} from './service-discovery.plugin';\nimport {PUBSUB_EVENT_XMLNS} from './publish-subscribe.plugin';\nimport {MessagePlugin} from './message.plugin';\nimport {MUC_SUB_EVENT_TYPE} from './muc-sub.plugin';\nimport {Form, serializeToSubmitForm} from '../../../../core/form';\n\n/**\n * https://xmpp.org/extensions/xep-0313.html\n * Message Archive Management\n */\nexport class MessageArchivePlugin extends AbstractXmppPlugin {\n static readonly MAM_NS = 'urn:xmpp:mam:2';\n\n private readonly mamMessageReceived$ = new Subject<void>();\n\n constructor(\n private readonly chatService: XmppChatAdapter,\n private readonly serviceDiscoveryPlugin: ServiceDiscoveryPlugin,\n private readonly multiUserChatPlugin: MultiUserChatPlugin,\n private readonly logService: LogService,\n private readonly messagePlugin: MessagePlugin,\n ) {\n super();\n\n this.chatService.state$\n .pipe(filter(state => state === 'online'))\n .subscribe(async () => {\n if (await this.supportsMessageArchiveManagement()) {\n await this.requestNewestMessages();\n }\n });\n\n // emit contacts to refresh contact list after receiving mam messages\n this.mamMessageReceived$\n .pipe(debounceTime(10))\n .subscribe(() => this.chatService.contacts$.next(this.chatService.contacts$.getValue()));\n }\n\n private async requestNewestMessages(): Promise<void> {\n await this.chatService.chatConnectionService.sendIq(\n xml('iq', {type: 'set'},\n xml('query', {xmlns: MessageArchivePlugin.MAM_NS},\n xml('set', {xmlns: 'http://jabber.org/protocol/rsm'},\n xml('max', {}, '250'),\n xml('before'),\n ),\n ),\n ),\n );\n }\n\n async loadMostRecentUnloadedMessages(recipient: Recipient): Promise<void> {\n // for user-to-user chats no to-attribute is necessary, in case of multi-user-chats it has to be set to the bare room jid\n const to = recipient.recipientType === 'room' ? recipient.roomJid.toString() : undefined;\n\n const form: Form = {\n type: 'submit',\n instructions: [],\n fields: [\n {type: 'hidden', variable: 'FORM_TYPE', value: MessageArchivePlugin.MAM_NS},\n ...(recipient.recipientType === 'contact'\n ? [{type: 'jid-single', variable: 'with', value: recipient.jidBare}] as const\n : []),\n ...(recipient.oldestMessage\n ? [{type: 'text-single', variable: 'end', value: recipient.oldestMessage.datetime.toISOString()}] as const\n : []),\n ],\n };\n\n const request =\n xml('iq', {type: 'set', to},\n xml('query', {xmlns: MessageArchivePlugin.MAM_NS},\n serializeToSubmitForm(form),\n xml('set', {xmlns: 'http://jabber.org/protocol/rsm'},\n xml('max', {}, '100'),\n xml('before'),\n ),\n ),\n );\n\n await this.chatService.chatConnectionService.sendIq(request);\n }\n\n async loadAllMessages(): Promise<void> {\n if (!(await this.supportsMessageArchiveManagement())) {\n throw new Error('message archive management not suppported');\n }\n\n let lastMamResponse = await this.chatService.chatConnectionService.sendIq(\n xml('iq', {type: 'set'},\n xml('query', {xmlns: MessageArchivePlugin.MAM_NS}),\n ),\n );\n\n while (lastMamResponse.getChild('fin').attrs.complete !== 'true') {\n const lastReceivedMessageId = lastMamResponse.getChild('fin').getChild('set').getChildText('last');\n lastMamResponse = await this.chatService.chatConnectionService.sendIq(\n xml('iq', {type: 'set'},\n xml('query', {xmlns: MessageArchivePlugin.MAM_NS},\n xml('set', {xmlns: 'http://jabber.org/protocol/rsm'},\n xml('max', {}, '250'),\n xml('after', {}, lastReceivedMessageId),\n ),\n ),\n ),\n );\n }\n }\n\n private async supportsMessageArchiveManagement(): Promise<boolean> {\n const supportsMessageArchiveManagement = await this.serviceDiscoveryPlugin.supportsFeature(\n this.chatService.chatConnectionService.userJid.bare().toString(),\n MessageArchivePlugin.MAM_NS,\n );\n if (!supportsMessageArchiveManagement) {\n this.logService.info('server doesn\\'t support MAM');\n }\n return supportsMessageArchiveManagement;\n }\n\n handleStanza(stanza: Stanza): boolean {\n if (this.isMamMessageStanza(stanza)) {\n this.handleMamMessageStanza(stanza);\n return true;\n }\n return false;\n }\n\n private isMamMessageStanza(stanza: Stanza): boolean {\n const result = stanza.getChild('result');\n return stanza.name === 'message' && result?.attrs.xmlns === MessageArchivePlugin.MAM_NS;\n }\n\n private handleMamMessageStanza(stanza: Stanza): void {\n const forwardedElement = stanza.getChild('result').getChild('forwarded') as Stanza;\n const messageElement = forwardedElement.getChild('message') as Stanza;\n const delayElement = forwardedElement.getChild('delay') as Stanza;\n\n const eventElement = messageElement.getChild('event', PUBSUB_EVENT_XMLNS) as Stanza;\n if (messageElement.getAttr('type') == null && eventElement != null) {\n this.handlePubSubEvent(eventElement, delayElement);\n } else {\n this.handleArchivedMessage(messageElement, delayElement);\n }\n }\n\n private handleArchivedMessage(messageElement: Stanza, delayEl: Stanza): void {\n const type = messageElement.getAttr('type');\n if (type === 'chat') {\n const messageHandled = this.messagePlugin.handleStanza(messageElement, delayEl);\n if (messageHandled) {\n thi