Commentaires fédérés via Mastodon (bokante.o-k-i.net) — backend #5

Merged
cedric merged 8 commits from feat/comment-fedi into master 2026-07-04 21:24:31 +00:00
2 changed files with 64 additions and 44 deletions
Showing only changes of commit 26471d8b0e - Show all commits
@@ -118,14 +118,14 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => {
}); });
}); });
describe('beforeUpdate — publication miroir bokante', () => { describe('afterCreate — publication miroir bokante', () => {
const originalEnv = {...process.env}; const originalEnv = {...process.env};
const bokanteMastodon = require('../../../../../utils/bokante-mastodon'); const bokanteMastodon = require('../../../../../utils/bokante-mastodon');
const originalCreateStatus = bokanteMastodon.createStatus; const originalCreateStatus = bokanteMastodon.createStatus;
function buildStrapi(previous) { function buildStrapi() {
const dbQuery = { const dbQuery = {
findOne: vi.fn(async () => previous), findOne: vi.fn(async () => null),
updateMany: vi.fn() updateMany: vi.fn()
}; };
@@ -136,11 +136,16 @@ describe('beforeUpdate — publication miroir bokante', () => {
}; };
} }
function buildEvent(overrides = {}) { function buildEvent(resultOverrides = {}) {
return { return {
state: {}, params: {data: {titre: 'Mon titre'}},
params: { result: {
data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides} documentId: 'doc-1',
titre: 'Mon titre',
slug: 'mon-titre',
publishedAt: '2026-07-04T00:00:00.000Z',
bokanteStatusId: null,
...resultOverrides
} }
}; };
} }
@@ -151,33 +156,43 @@ describe('beforeUpdate — publication miroir bokante', () => {
delete global.strapi; delete global.strapi;
}); });
it('publie un statut miroir et stocke bokanteStatusId sur data', async () => { it('publie un statut miroir et synchronise bokanteStatusId sur le documentId', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => ({id: '112233'})); bokanteMastodon.createStatus = vi.fn(async () => ({id: '112233'}));
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null}; const strapiMock = buildStrapi();
const strapiMock = buildStrapi(previous); const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock); await afterCreate(buildEvent());
const event = buildEvent();
await beforeUpdate(event);
expect(bokanteMastodon.createStatus).toHaveBeenCalledTimes(1); expect(bokanteMastodon.createStatus).toHaveBeenCalledTimes(1);
expect(bokanteMastodon.createStatus.mock.calls[0][0]).toContain('Mon titre'); expect(bokanteMastodon.createStatus.mock.calls[0][0]).toContain('Mon titre');
expect(event.params.data.bokanteStatusId).toBe('112233'); expect(strapiMock.db.query('api::parole.parole').updateMany).toHaveBeenCalledWith({
where: {documentId: 'doc-1'},
data: {bokanteStatusId: '112233'}
});
});
it('ne publie rien si la ligne créée n\'est pas publiée (simple brouillon)', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'}));
const strapiMock = buildStrapi();
const {afterCreate} = await loadLifecycles(strapiMock);
await afterCreate(buildEvent({publishedAt: null}));
expect(bokanteMastodon.createStatus).not.toHaveBeenCalled();
}); });
it('ne republie pas si bokanteStatusId existe déjà', async () => { it('ne republie pas si bokanteStatusId existe déjà', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'})); bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'}));
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: '112233'}; const strapiMock = buildStrapi();
const strapiMock = buildStrapi(previous); const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock); await afterCreate(buildEvent({bokanteStatusId: '112233'}));
await beforeUpdate(buildEvent());
expect(bokanteMastodon.createStatus).not.toHaveBeenCalled(); expect(bokanteMastodon.createStatus).not.toHaveBeenCalled();
}); });
@@ -186,32 +201,27 @@ describe('beforeUpdate — publication miroir bokante', () => {
delete process.env.BOKANTE_ACCESS_TOKEN; delete process.env.BOKANTE_ACCESS_TOKEN;
bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'})); bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'}));
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null}; const strapiMock = buildStrapi();
const strapiMock = buildStrapi(previous); const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock); await afterCreate(buildEvent());
await beforeUpdate(buildEvent());
expect(bokanteMastodon.createStatus).not.toHaveBeenCalled(); expect(bokanteMastodon.createStatus).not.toHaveBeenCalled();
}); });
it('n\'interrompt pas la publication quand bokante échoue', async () => { it('n\'interrompt pas la création quand bokante échoue', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => { bokanteMastodon.createStatus = vi.fn(async () => {
throw new Error('boom'); throw new Error('boom');
}); });
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null}; const strapiMock = buildStrapi();
const strapiMock = buildStrapi(previous); const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock); await expect(afterCreate(buildEvent())).resolves.not.toThrow();
const event = buildEvent();
await expect(beforeUpdate(event)).resolves.not.toThrow();
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('bokante')); expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('bokante'));
expect(event.params.data.bokanteStatusId).toBeUndefined(); expect(strapiMock.db.query('api::parole.parole').updateMany).not.toHaveBeenCalled();
}); });
}); });
@@ -192,17 +192,6 @@ module.exports = {
const previousPublishedAt = previousData.publishedAt; const previousPublishedAt = previousData.publishedAt;
const currentPublished_at = data.publishedAt; const currentPublished_at = data.publishedAt;
if (currentPublished_at != previousPublishedAt) { if (currentPublished_at != previousPublishedAt) {
if (!previousData.bokanteStatusId && process.env.BOKANTE_ACCESS_TOKEN) {
try {
const status = await bokanteMastodon.createStatus(
`"${previousData.titre}" — nouvelle parole sur pawol.nu\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`
);
data.bokanteStatusId = status.id;
} catch (err) {
strapi.log.error(`Publication bokante : ${err.message}`);
}
}
const message = `<b>Nouvelle publication</b> ❤️ const message = `<b>Nouvelle publication</b> ❤️
\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`; \n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`;
if (previousData.user) { if (previousData.user) {
@@ -284,6 +273,27 @@ module.exports = {
}, },
afterCreate: async event => { afterCreate: async event => {
const {data} = event.params; const {data} = event.params;
// Avec draftAndPublish, "publier" crée une nouvelle ligne (la version publiée)
// au lieu de mettre à jour la ligne existante : c'est ici, et non dans
// beforeUpdate, qu'un événement de publication est détectable.
if (event.result?.publishedAt && !event.result?.bokanteStatusId && process.env.BOKANTE_ACCESS_TOKEN) {
try {
const status = await bokanteMastodon.createStatus(
`"${event.result.titre}" — nouvelle parole sur pawol.nu\n${process.env.WEBSITE_URL}/paroles/${event.result.slug}`
);
// Synchronise brouillon et version publiée pour éviter une republication
// en double au prochain cycle dépublier/republier (qui recrée la ligne
// publiée à partir du brouillon).
await strapi.db.query('api::parole.parole').updateMany({
where: {documentId: event.result.documentId},
data: {bokanteStatusId: status.id}
});
} catch (err) {
strapi.log.error(`Publication bokante : ${err.message}`);
}
}
const user = await jwennUserEpiId(data?.user?.id); const user = await jwennUserEpiId(data?.user?.id);
const userAdmin = await jwennUserAdminEpiId(data?.createdBy); const userAdmin = await jwennUserAdminEpiId(data?.createdBy);
const superAdmin = await jwennSuperAdminEpiId(data?.createdBy); const superAdmin = await jwennSuperAdminEpiId(data?.createdBy);