Aller au contenu

@komozu/core (JavaScript)

Client navigateur indépendant de tout framework pour Komozu, le chat des utilisateurs connectés de votre site ou de votre application : messages en temps réel (Server-Sent Events), envoi optimiste, indicateurs de saisie, images et vidéos. Aucune interface : vous dessinez le chat vous-même. Pour une interface prête à l’emploi, utilisez @komozu/react ou le script d’intégration.

Fenêtre de terminal
npm install @komozu/core
import { KomozuClient } from '@komozu/core';
const client = new KomozuClient({
apiUrl: 'https://api.komozu.com',
clientId: 'kz_live_…', // clé publique (page Clés API du tableau de bord)
memberToken: user?.komozuToken, // le jeton du membre connecté, conservé par votre serveur
});
await client.connect('u15'); // slug (ou UUID) du salon
client.subscribe((state) => render(state.messages));
await client.sendMessage('Hello!');

@komozu/core n’a pas d’interface : vous dessinez le chat vous-même. client.lookupChannel('general') (ou lookupChannel({ externalId: 'evt_1102' }) pour un salon créé par votre serveur) renvoie le salon, indique s’il accepte les images et vidéos (channel.attachmentsEnabled) et donne la couleur par défaut du projet. Pour lire malgré tout les réglages d’une intégration (disposition, couleurs, salon), appelez client.getIntegration('int_…').

Seuls les membres lisent et écrivent : vos utilisateurs connectés. Votre serveur crée chacun d’eux une fois (@komozu/node, members.upsert) et obtient un jeton qui n’expire pas : il le conserve avec l’utilisateur (Identifier vos utilisateurs). Transmettez ce jeton au client :

OptionComportement
memberTokenLe jeton conservé de l’utilisateur connecté. Sans lui, connect() ne charge rien et n’envoie aucune requête : state.restriction.code vaut MEMBER_TOKEN_REQUIRED (affichez une invitation à se connecter). L’envoi, la saisie et les fichiers échouent aussitôt avec le même code.
client.setMemberToken(token | null)Un utilisateur se connecte ou se déconnecte sans recharger la page : le salon ouvert est rechargé (null = aucun membre, MEMBER_TOKEN_REQUIRED).
tokenProvider: () => Promise<string | null>Facultatif, l’emporte sur memberToken : appelé avant la première connexion, 60 s avant exp si le jeton en a un, et une fois de plus si l’API refuse le jeton. client.refreshUser() l’appelle à nouveau.
  • Le profil du membre vient de GET /v1/members/me (state.memberDisplayName, state.memberAvatarUrl), jamais du jeton. Seul sub est lu dans le jeton, pour aligner vos propres bulles.
  • Un jeton refusé est réessayé une fois avec un jeton neuf, puis remonte dans state.restriction (TOKEN_EXPIRED, TOKEN_REVOKED, INVALID_MEMBER_TOKEN, MEMBER_NOT_FOUND).
  • Rien d’identifiant n’est conservé dans le navigateur : le jeton reste en mémoire, seul le choix clair / sombre est gardé par projet.

Les salons sont members (tout membre du projet) ou private (uniquement les membres ajoutés au salon). Quand quelque chose bloque le lecteur, state.restriction contient un code lisible par une machine :

CodeSignification
MEMBER_TOKEN_REQUIREDPas de jeton membre : rien n’est lisible
CHANNEL_ACCESS_DENIEDSalon privé, le membre n’en fait pas partie
MEMBER_BANNEDLe membre est banni du projet : rien n’est lisible
DOMAIN_NOT_ALLOWEDLa page ne fait pas partie des domaines autorisés du projet
INTEGRATION_NOT_FOUNDIntégration inconnue ou désactivée (getIntegration)
TOKEN_EXPIRED / TOKEN_REVOKED / INVALID_MEMBER_TOKENLe jeton du membre a été refusé : obtenez-en un nouveau auprès de votre serveur
MEMBER_NOT_FOUNDLe jeton est valide mais le membre n’a jamais été créé (members.upsert)

L’envoi d’un fichier dans un salon où les images et vidéos sont désactivées échoue avec 403 ATTACHMENTS_DISABLED. Avec le plan Free, une vidéo échoue avec 403 VIDEOS_NOT_ALLOWED (images uniquement).

Événements en temps réel : message.deleted retire le message de state.messages ; message.updated le remplace (texte modifié par son auteur, editedAt renseigné) ; member.updated donne le nouveau nom et le nouvel avatar à tous les messages de ce membre (et à state.memberDisplayName / memberAvatarUrl s’il s’agit du lecteur) ; access.revoked (le serveur ferme le flux après un retrait, un bannissement ou un changement d’accès) fait se reconnecter le client une fois pour revérifier son accès. Écoutez client.on('messageDeleted', …) / client.on('messageUpdated', …) / client.on('memberUpdated', …) / client.on('accessRevoked', …) si vous devez réagir.

Chaque chat ouvert tient un flux. Un réseau (adresse IP) peut garder 50 flux ouverts en même temps, tous projets confondus. Un projet peut en avoir 30 ouverts à la fois avec le plan Free, 100 avec Starter et 1 000 avec Pro, plus une marge de 20 % (36, 120 et 1 200) ; le propriétaire reçoit un email à 80 %. Au-delà, le flux est refusé avec 429 RATE_LIMIT_EXCEEDED et Retry-After: 30 : state.connectionStatus reste reconnecting et le client réessaie après ce délai.

client.editMessage(messageId, content) modifie le texte d’un message du lecteur (les pièces jointes ne changent pas ; le texte ne peut être vidé que si le message garde une image ou une vidéo). client.canEditMessage(message) indique s’il faut proposer l’action : un membre modifie ses messages, sans limite de durée. Le message d’un autre échoue avec 403 MESSAGE_NOT_EDITABLE, admins compris (ils suppriment). Le message modifié porte editedAt et arrive en direct chez tous les lecteurs (message.updated).

  • state.pinnedMessages : les messages épinglés du salon, le dernier épinglé en premier, chargés avec le salon et tenus à jour en direct (message.pinned / message.unpinned). Chaque message porte pinnedAt. Pour afficher un message épinglé ancien, client.loadUntilMessage(id) charge les pages plus anciennes jusqu’à ce qu’il soit dans state.messages (20 pages au maximum).
  • state.channelRole : le rôle du lecteur dans le salon, admin, user ou null (pas encore chargé). Il change en direct quand le propriétaire du projet ou votre serveur modifie le rôle (member.role_changed, aussi client.on('roleChanged', …)).
  • state.poweredByRequired : true quand l’offre du projet impose « Propulsé par komozu » (Free), false sur Starter et Pro, null tant que le salon n’est pas chargé.
  • Actions d’admin : pinMessage, unpinMessage, deleteMessage et removeChannelMember (salon privé ; un admin ne retire pas un autre admin). Sans le rôle d’admin, elles échouent aussitôt avec 403 CHANNEL_ADMIN_REQUIRED, sans appel réseau ; l’API vérifie de toute façon chaque action.
  • Signalement : client.reportMessage(messageId, reason?) (tout membre ; reason vaut spam, harassment, inappropriate ou other). Les messages des admins et du propriétaire du projet peuvent être signalés ; signaler son propre message échoue avec 400 MESSAGE_NOT_REPORTABLE ; signaler deux fois le même message en 24 heures ne compte qu’une fois. 10 signalements par tranche de 10 minutes et par IP (429).
  • Chaque message porte authorType (member, ou owner pour le propriétaire du projet qui écrit depuis le tableau de bord) et authorRole (admin affiche le badge Admin, propriétaire du projet compris).
new KomozuClient({ apiUrl, clientId, memberToken?, tokenProvider?, storage?, fetch? });
client.connect(channel): Promise<void>; // slug ou UUID
client.disconnect(): void;
client.destroy(): void;
client.sendMessage(content, attachments?): Promise<MessageDTO>; // optimiste (state.pendingMessages)
client.retryMessage(localId) / client.discardMessage(localId);
client.loadMoreMessages(limit?): Promise<boolean>;
client.notifyTyping(): void; // au plus un signal toutes les 2,5 s
client.uploadAttachment(file, { onProgress }): Promise<AttachmentDTO>; // images et vidéos, 10 Mo maximum
client.lookupChannel(slugOrUuid | { externalId }): Promise<{ channel; project: { primaryColor; showPoweredBy }; viewer: { role } }>;
client.getIntegration(publicId): Promise<PublicIntegrationDTO>;
client.listChannels(): Promise<ChannelSummaryDTO[]>; // salons que le lecteur peut ouvrir
client.setMemberToken(token | null) / client.refreshUser();
client.setColorModePreference('light' | 'dark' | null);
client.subscribe((state) => …): () => void;
client.on('message' | 'messageDeleted' | 'messageUpdated' | 'messagePinned' | 'messageUnpinned' | 'roleChanged' | 'memberUpdated' | 'accessRevoked' | 'statusChange' | 'messagesChange' | 'stateChange' | 'error', …): () => void;
client.isOwnMessage(message): boolean;
client.canEditMessage(message): boolean; // son propre message
client.editMessage(messageId, content): Promise<MessageDTO>;
client.loadPinnedMessages(): Promise<MessageDTO[]>; // state.pinnedMessages, le dernier épinglé en premier
client.loadUntilMessage(messageId, maxPages = 20): Promise<boolean>;
client.isChannelAdmin(): boolean; // state.channelRole === 'admin'
client.pinMessage(messageId) / client.unpinMessage(messageId): Promise<MessageDTO>; // admins
client.deleteMessage(messageId): Promise<void>; // admins
client.removeChannelMember(externalId): Promise<void>; // admins, salon privé
client.reportMessage(messageId, reason?): Promise<void>; // tout membre

Les erreurs renvoyées par l’API sont en JSON { code, error, message } avec un code stable (ErrorCode de @komozu/dto). Une réponse 429 (RATE_LIMIT_EXCEEDED) porte retryAfter (en secondes).