diff --git a/src/utils/__tests__/import-bokante-comments.test.js b/src/utils/__tests__/import-bokante-comments.test.js new file mode 100644 index 0000000..14384aa --- /dev/null +++ b/src/utils/__tests__/import-bokante-comments.test.js @@ -0,0 +1,131 @@ +import {describe, it, expect, vi, afterEach} from 'vitest'; +import {importBokanteComments} from '../import-bokante-comments.js'; + +const bokanteMastodon = require('../bokante-mastodon.js'); + +function buildStatus(overrides = {}) { + return { + id: 'status-1', + content: '

Trè bèl parol !

', + created_at: '2026-07-04T12:00:00.000Z', + url: 'https://bokante.o-k-i.net/@quelqun/status-1', + account: { + display_name: 'Quelqu\'un', + acct: 'quelqun@mastodon.social', + avatar: 'https://mastodon.social/avatars/quelqun.png', + url: 'https://mastodon.social/@quelqun' + }, + ...overrides + }; +} + +function buildStrapi({paroles, existingRemoteIds = [], createImpl}) { + const commentaireDocuments = { + create: createImpl || vi.fn(async ({data}) => ({id: Math.floor(Math.random() * 10_000), ...data})) + }; + const paroleDocuments = {update: vi.fn(async () => ({}))}; + + return { + db: { + query: uid => { + if (uid === 'api::parole.parole') { + return {findMany: vi.fn(async () => paroles)}; + } + + if (uid === 'api::commentaire.commentaire') { + return { + findOne: vi.fn(async ({where}) => (existingRemoteIds.includes(where.remoteId) ? {id: 1} : null)) + }; + } + + return {}; + } + }, + documents: uid => { + if (uid === 'api::commentaire.commentaire') return commentaireDocuments; + if (uid === 'api::parole.parole') return paroleDocuments; + return {}; + }, + log: {error: vi.fn()} + }; +} + +describe('importBokanteComments', () => { + const originalGetStatusContext = bokanteMastodon.getStatusContext; + + afterEach(() => { + bokanteMastodon.getStatusContext = originalGetStatusContext; + }); + + it('importe les nouveaux commentaires et les connecte à la parole', async () => { + bokanteMastodon.getStatusContext = vi.fn(async () => ({descendants: [buildStatus()]})); + const paroles = [{id: 7, documentId: 'doc-7', slug: 'mon-titre', bokanteStatusId: '112233'}]; + const strapi = buildStrapi({paroles}); + + const result = await importBokanteComments({strapi}); + + expect(bokanteMastodon.getStatusContext).toHaveBeenCalledWith('112233'); + expect(strapi.documents('api::commentaire.commentaire').create).toHaveBeenCalledWith({ + data: expect.objectContaining({ + contenu: 'Trè bèl parol !', + origine: 'activitypub', + auteurNom: 'Quelqu\'un', + auteurHandle: '@quelqun@mastodon.social', + auteurAvatarUrl: 'https://mastodon.social/avatars/quelqun.png', + auteurProfilUrl: 'https://mastodon.social/@quelqun', + remoteId: 'status-1', + remoteUrl: 'https://bokante.o-k-i.net/@quelqun/status-1', + parole: 7 + }) + }); + expect(strapi.documents('api::parole.parole').update).toHaveBeenCalledWith({ + documentId: 'doc-7', + data: {commentaires: {connect: expect.any(Array)}} + }); + expect(result.imported).toBe(1); + }); + + it('n\'importe pas un commentaire déjà présent (déduplication par remoteId)', async () => { + bokanteMastodon.getStatusContext = vi.fn(async () => ({descendants: [buildStatus({id: 'deja-la'})]})); + const paroles = [{id: 7, documentId: 'doc-7', slug: 'mon-titre', bokanteStatusId: '112233'}]; + const strapi = buildStrapi({paroles, existingRemoteIds: ['deja-la']}); + + const result = await importBokanteComments({strapi}); + + expect(strapi.documents('api::commentaire.commentaire').create).not.toHaveBeenCalled(); + expect(strapi.documents('api::parole.parole').update).not.toHaveBeenCalled(); + expect(result.imported).toBe(0); + }); + + it('ignore les statuts marqués sensitive', async () => { + bokanteMastodon.getStatusContext = vi.fn(async () => ({descendants: [buildStatus({sensitive: true})]})); + const paroles = [{id: 7, documentId: 'doc-7', slug: 'mon-titre', bokanteStatusId: '112233'}]; + const strapi = buildStrapi({paroles}); + + const result = await importBokanteComments({strapi}); + + expect(strapi.documents('api::commentaire.commentaire').create).not.toHaveBeenCalled(); + expect(result.imported).toBe(0); + }); + + it('continue les autres paroles quand une requête bokante échoue', async () => { + bokanteMastodon.getStatusContext = vi.fn(async statusId => { + if (statusId === 'echoue') { + throw new Error('Mastodon 500: boom'); + } + + return {descendants: [buildStatus({id: 'ok-1'})]}; + }); + + const paroles = [ + {id: 1, documentId: 'doc-1', slug: 'echoue', bokanteStatusId: 'echoue'}, + {id: 2, documentId: 'doc-2', slug: 'reussi', bokanteStatusId: 'ok'} + ]; + const strapi = buildStrapi({paroles}); + + const result = await importBokanteComments({strapi}); + + expect(strapi.log.error).toHaveBeenCalledWith(expect.stringContaining('echoue')); + expect(result.imported).toBe(1); + }); +}); diff --git a/src/utils/import-bokante-comments.js b/src/utils/import-bokante-comments.js new file mode 100644 index 0000000..f2d9676 --- /dev/null +++ b/src/utils/import-bokante-comments.js @@ -0,0 +1,69 @@ +'use strict'; + +const bokanteMastodon = require('./bokante-mastodon'); + +function stripHtml(html) { + return (html || '').replace(/<[^>]*>/g, '').trim(); +} + +async function importBokanteComments({strapi}) { + const paroles = await strapi.db.query('api::parole.parole').findMany({ + where: {bokanteStatusId: {$notNull: true}} + }); + + let imported = 0; + + for (const parole of paroles) { + let context; + try { + context = await bokanteMastodon.getStatusContext(parole.bokanteStatusId); + } catch (err) { + strapi.log.error(`Import bokante (${parole.slug}) : ${err.message}`); + continue; + } + + const newCommentIds = []; + + for (const status of context.descendants || []) { + if (status.sensitive) { + continue; + } + + const exists = await strapi.db.query('api::commentaire.commentaire').findOne({ + where: {remoteId: status.id} + }); + if (exists) { + continue; + } + + const commentaire = await strapi.documents('api::commentaire.commentaire').create({ + data: { + contenu: stripHtml(status.content), + datePublication: status.created_at, + origine: 'activitypub', + auteurNom: status.account?.display_name, + auteurHandle: status.account?.acct ? `@${status.account.acct}` : undefined, + auteurAvatarUrl: status.account?.avatar, + auteurProfilUrl: status.account?.url, + remoteId: status.id, + remoteUrl: status.url, + parole: parole.id + } + }); + + newCommentIds.push(commentaire.id); + imported += 1; + } + + if (newCommentIds.length > 0) { + await strapi.documents('api::parole.parole').update({ + documentId: parole.documentId, + data: {commentaires: {connect: newCommentIds}} + }); + } + } + + return {imported}; +} + +module.exports = {importBokanteComments};