Commentaires fédérés via Mastodon (bokante.o-k-i.net) — backend #5
@@ -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: '<p>Trè bèl parol !</p>',
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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};
|
||||||
Reference in New Issue
Block a user