@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.
npm install @komozu/coreDémarrage rapide
Section intitulée « Démarrage rapide »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 salonclient.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 :
| Option | Comportement |
|---|---|
memberToken | Le 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. Seulsubest 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.
Accès aux salons et bannissements
Section intitulée « Accès aux salons et bannissements »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 :
| Code | Signification |
|---|---|
MEMBER_TOKEN_REQUIRED | Pas de jeton membre : rien n’est lisible |
CHANNEL_ACCESS_DENIED | Salon privé, le membre n’en fait pas partie |
MEMBER_BANNED | Le membre est banni du projet : rien n’est lisible |
DOMAIN_NOT_ALLOWED | La page ne fait pas partie des domaines autorisés du projet |
INTEGRATION_NOT_FOUND | Intégration inconnue ou désactivée (getIntegration) |
TOKEN_EXPIRED / TOKEN_REVOKED / INVALID_MEMBER_TOKEN | Le jeton du membre a été refusé : obtenez-en un nouveau auprès de votre serveur |
MEMBER_NOT_FOUND | Le 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.
Connexions en direct
Section intitulée « Connexions en direct »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.
Modifier ses propres messages
Section intitulée « Modifier ses propres messages »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).
Messages épinglés et admins de salon
Section intitulée « Messages épinglés et admins de salon »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 portepinnedAt. Pour afficher un message épinglé ancien,client.loadUntilMessage(id)charge les pages plus anciennes jusqu’à ce qu’il soit dansstate.messages(20 pages au maximum).state.channelRole: le rôle du lecteur dans le salon,admin,userounull(pas encore chargé). Il change en direct quand le propriétaire du projet ou votre serveur modifie le rôle (member.role_changed, aussiclient.on('roleChanged', …)).state.poweredByRequired:truequand l’offre du projet impose « Propulsé par komozu » (Free),falsesur Starter et Pro,nulltant que le salon n’est pas chargé.- Actions d’admin :
pinMessage,unpinMessage,deleteMessageetremoveChannelMember(salon privé ; un admin ne retire pas un autre admin). Sans le rôle d’admin, elles échouent aussitôt avec403 CHANNEL_ADMIN_REQUIRED, sans appel réseau ; l’API vérifie de toute façon chaque action. - Signalement :
client.reportMessage(messageId, reason?)(tout membre ;reasonvautspam,harassment,inappropriateouother). Les messages des admins et du propriétaire du projet peuvent être signalés ; signaler son propre message échoue avec400 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, ouownerpour le propriétaire du projet qui écrit depuis le tableau de bord) etauthorRole(adminaffiche le badge Admin, propriétaire du projet compris).
new KomozuClient({ apiUrl, clientId, memberToken?, tokenProvider?, storage?, fetch? });
client.connect(channel): Promise<void>; // slug ou UUIDclient.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 sclient.uploadAttachment(file, { onProgress }): Promise<AttachmentDTO>; // images et vidéos, 10 Mo maximumclient.lookupChannel(slugOrUuid | { externalId }): Promise<{ channel; project: { primaryColor; showPoweredBy }; viewer: { role } }>;client.getIntegration(publicId): Promise<PublicIntegrationDTO>;client.listChannels(): Promise<ChannelSummaryDTO[]>; // salons que le lecteur peut ouvrirclient.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 messageclient.editMessage(messageId, content): Promise<MessageDTO>;
client.loadPinnedMessages(): Promise<MessageDTO[]>; // state.pinnedMessages, le dernier épinglé en premierclient.loadUntilMessage(messageId, maxPages = 20): Promise<boolean>;client.isChannelAdmin(): boolean; // state.channelRole === 'admin'client.pinMessage(messageId) / client.unpinMessage(messageId): Promise<MessageDTO>; // adminsclient.deleteMessage(messageId): Promise<void>; // adminsclient.removeChannelMember(externalId): Promise<void>; // admins, salon privéclient.reportMessage(messageId, reason?): Promise<void>; // tout membreLes 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).
- Démarrer, Identifier vos utilisateurs, Intégration avancée
- Côté serveur :
@komozu/node