Author SHA1 Message Date
cedric 821b9b96aa feat: mettre en avant les langues asiatiques dans l'ordre des traductions
Déploiement API BETA / build (push) Successful in 2m22s
Déploiement API PROD / build (push) Successful in 2m23s
Déploiement API BETA / deploy (push) Successful in 48s
Déploiement API PROD / deploy (push) Successful in 55s
2026-07-05 22:28:27 +04:00
cedric 842e87543c revert: garder les mentions dans les commentaires importés
Déploiement API BETA / build (push) Successful in 2m22s
Déploiement API PROD / deploy (push) Successful in 54s
Déploiement API PROD / build (push) Successful in 2m23s
Déploiement API BETA / deploy (push) Successful in 44s
2026-07-05 20:10:51 +04:00
cedric ecbcf5d86e fix: retirer la mention initiale des commentaires importés
Déploiement API BETA / build (push) Successful in 2m16s
Déploiement API PROD / build (push) Successful in 2m24s
Déploiement API BETA / deploy (push) Successful in 44s
Déploiement API PROD / deploy (push) Successful in 53s
2026-07-05 19:39:33 +04:00
cedric 128c027af1 fix: extraire l'id de la relation avant de la déréférencer
Déploiement API BETA / build (push) Successful in 2m27s
Déploiement API PROD / build (push) Successful in 2m21s
Déploiement API BETA / deploy (push) Successful in 47s
Déploiement API PROD / deploy (push) Successful in 53s
2026-07-05 19:22:29 +04:00
cedric 29bdf72640 fix: réparer la relation commentaire.parole / parole.commentaires
Déploiement API BETA / build (push) Successful in 2m25s
Déploiement API PROD / build (push) Successful in 2m22s
Déploiement API BETA / deploy (push) Successful in 46s
Déploiement API PROD / deploy (push) Successful in 53s
2026-07-05 19:04:39 +04:00
cedric fb60226bd4 fix: publier les commentaires bokante importés
Déploiement API BETA / build (push) Successful in 2m22s
Déploiement API PROD / build (push) Successful in 2m23s
Déploiement API BETA / deploy (push) Successful in 47s
Déploiement API PROD / deploy (push) Successful in 52s
2026-07-05 18:39:01 +04:00
cedric 34f3a7783b docs: documenter les commentaires fédérés (bokante)
Déploiement API BETA / build (push) Successful in 2m20s
Déploiement API PROD / build (push) Successful in 2m21s
Déploiement API BETA / deploy (push) Successful in 44s
Déploiement API PROD / deploy (push) Successful in 56s
2026-07-05 02:06:23 +04:00
cedric 03960cc952 Merge pull request 'Commentaires fédérés via Mastodon (bokante.o-k-i.net) — backend' (#5) from feat/comment-fedi into master
Déploiement API BETA / build (push) Successful in 2m20s
Déploiement API PROD / build (push) Successful in 2m24s
Déploiement API BETA / deploy (push) Successful in 44s
Déploiement API PROD / deploy (push) Successful in 53s
Reviewed-on: #5
2026-07-04 21:24:31 +00:00
10 changed files with 85 additions and 15 deletions
+31
View File
@@ -163,6 +163,37 @@ curl -H "Authorization: Bearer <token>" \
-o dataset.jsonl
```
## Commentaires fédérés (Mastodon / bokante.o-k-i.net)
Chaque parole publiée peut obtenir un statut miroir sur l'instance Mastodon de
l'organisation, [bokante.o-k-i.net](https://bokante.o-k-i.net), et les réponses reçues
sur ce statut sont importées comme commentaires. Voir le RFC dédié pour le détail de la
conception (`RFC-commentaires-activitypub-2026-07-04.md`, à la racine du dossier
`PAWOL.NU`).
**Variables d'environnement :**
| Variable | Obligatoire | Description |
|---|---|---|
| `BOKANTE_ACCESS_TOKEN` | Oui, pour activer la fonctionnalité | Jeton d'application Mastodon (scopes `write:statuses`, `read:statuses`) d'un compte bot dédié sur bokante.o-k-i.net |
| `BOKANTE_INSTANCE_URL` | Non | Instance Mastodon cible (défaut : `https://bokante.o-k-i.net`) |
| `BOKANTE_BACKFILL_CRON` | Non | Règle cron du backfill du catalogue existant (défaut : `0 * * * *`, une parole/heure) |
Sans `BOKANTE_ACCESS_TOKEN`, toute la fonctionnalité (publication du miroir, import des
réponses, backfill) est un no-op silencieux — comme les intégrations Telegram/Revolt.
**Comment obtenir le jeton :** créer un compte bot dédié sur bokante.o-k-i.net, puis dans
*Préférences → Développement → Nouvelle application*, cocher uniquement `write:statuses`
et `read:statuses`. Le jeton d'accès est affiché directement sur la page de l'application
créée.
**Tâches planifiées (cron) :**
- Publication du miroir : déclenchée à chaque publication d'une parole (pas de cron).
- Import des réponses : toutes les 20 minutes.
- Backfill du catalogue existant : une parole par exécution (la plus ancienne sans
miroir en premier), au rythme défini par `BOKANTE_BACKFILL_CRON`. S'arrête de
lui-même une fois le catalogue rattrapé.
## License
Copyright (C) 2024 Cédric Famibelle-Pronzola & ORGANISATION KA INTERNATIONALE (OKI)
@@ -27,11 +27,33 @@ describe('commentaire afterCreate — notification email', () => {
const {afterCreate} = await loadLifecycles(strapiMock);
await afterCreate({params: {data: {user: 1, parole: 7, contenu: '<img src=x onerror=alert(1)>'}}});
await afterCreate({params: {data: {user: {connect: [{id: 1}]}, parole: {connect: [{id: 7}]}, contenu: '<img src=x onerror=alert(1)>'}}});
expect(emailSend).toHaveBeenCalledTimes(1);
const [payload] = emailSend.mock.calls[0];
expect(payload.text).toBe('<img src=x onerror=alert(1)>');
expect(payload.html).toBeUndefined();
});
it('extrait l\'id de la relation quelle que soit sa forme (connect, set, id brut)', async () => {
const paroleFindOne = vi.fn(async ({where}) => ({id: where.id, titre: 'Mon titre'}));
const strapiMock = {
db: {
query: vi.fn(uid => {
if (uid === 'plugin::users-permissions.user') return {findOne: vi.fn(async () => null)};
if (uid === 'api::parole.parole') return {findOne: paroleFindOne};
throw new Error(`unexpected uid: ${uid}`);
})
},
plugins: {email: {services: {email: {send: vi.fn()}}}}
};
const {afterCreate} = await loadLifecycles(strapiMock);
await afterCreate({params: {data: {parole: {set: [{id: 7}]}, contenu: 'ok'}}});
expect(paroleFindOne).toHaveBeenCalledWith({where: {id: 7}});
await afterCreate({params: {data: {parole: 7, contenu: 'ok'}}});
expect(paroleFindOne).toHaveBeenCalledWith({where: {id: 7}});
});
});
@@ -2,6 +2,17 @@
const { ApplicationError, NotFoundError } = require('@strapi/utils').errors;
// Le Document Service transforme les relations en { connect: [{id}] } ou
// { set: [{id}] } avant que les hooks bas niveau (beforeCreate/afterCreate)
// ne reçoivent `data` : un id brut n'est plus systématiquement garanti ici.
const idRelasyonAn = valè => {
if (valè == null || typeof valè !== 'object') {
return valè;
}
return valè.connect?.[0]?.id ?? valè.set?.[0]?.id ?? null;
};
const jwennUserEpiId = async userId => {
if (!userId) {
return null;
@@ -47,8 +58,8 @@ module.exports = {
},
afterCreate: async event => {
const {data} = event.params;
const user = await jwennUserEpiId(data.user);
const parole = await jwennParoleEpiId(data.parole);
const user = await jwennUserEpiId(idRelasyonAn(data.user));
const parole = await jwennParoleEpiId(idRelasyonAn(data.parole));
if (!parole) {
throw new NotFoundError('Texte introuvable.');
@@ -27,8 +27,9 @@
},
"parole": {
"type": "relation",
"relation": "oneToOne",
"target": "api::parole.parole"
"relation": "manyToOne",
"target": "api::parole.parole",
"inversedBy": "commentaires"
},
"origine": {
"type": "enumeration",
@@ -82,7 +82,8 @@
"commentaires": {
"type": "relation",
"relation": "oneToMany",
"target": "api::commentaire.commentaire"
"target": "api::commentaire.commentaire",
"mappedBy": "parole"
},
"prioriteArtistes": {
"type": "string"
+2 -2
View File
@@ -6,13 +6,13 @@ const { ApplicationError } = require('@strapi/utils').errors;
const LANG_MAP = {
fr: { field: 'francais', targetLang: 'fr', userPrompt: 'Tradui an fransé' },
ja: { field: 'japonais', targetLang: 'ja', userPrompt: '日本語に翻訳して', deeplTarget: 'JA', suffix: '\n\n (DeepLによる翻訳)' },
ko: { field: 'coreen', targetLang: 'ko', userPrompt: '한국어로 번역해줘', deeplTarget: 'KO', suffix: '\n\n (DeepL 번역)' },
en: { field: 'anglais', targetLang: 'en', userPrompt: 'Translate to English', deeplTarget: 'EN', suffix: '\n\n (Translated by DeepL)' },
es: { field: 'espagnol', targetLang: 'es', userPrompt: 'Traduce al español', deeplTarget: 'ES', suffix: '\n\n (Traducido por DeepL)' },
de: { field: 'allemand', targetLang: 'de', userPrompt: 'Übersetze auf Deutsch', deeplTarget: 'DE', suffix: '\n\n (Übersetzt von DeepL)' },
it: { field: 'italien', targetLang: 'it', userPrompt: 'Traduci in italiano', deeplTarget: 'IT', suffix: '\n\n (Tradotto da DeepL)' },
pt: { field: 'portugais', targetLang: 'pt', userPrompt: 'Traduza para o português', deeplTarget: 'PT-BR', suffix: '\n\n (Traduzido pela DeepL)' },
ja: { field: 'japonais', targetLang: 'ja', userPrompt: '日本語に翻訳して', deeplTarget: 'JA', suffix: '\n\n (DeepLによる翻訳)' },
ko: { field: 'coreen', targetLang: 'ko', userPrompt: '한국어로 번역해줘', deeplTarget: 'KO', suffix: '\n\n (DeepL 번역)' },
};
const ALL_LANGS = Object.keys(LANG_MAP);
+6 -6
View File
@@ -10,6 +10,12 @@
"francais": {
"type": "richtext"
},
"japonais": {
"type": "richtext"
},
"coreen": {
"type": "richtext"
},
"anglais": {
"type": "richtext"
},
@@ -24,12 +30,6 @@
},
"portugais": {
"type": "richtext"
},
"japonais": {
"type": "richtext"
},
"coreen": {
"type": "richtext"
}
}
}
@@ -66,6 +66,7 @@ describe('importBokanteComments', () => {
expect(bokanteMastodon.getStatusContext).toHaveBeenCalledWith('112233');
expect(strapi.documents('api::commentaire.commentaire').create).toHaveBeenCalledWith({
status: 'published',
data: expect.objectContaining({
contenu: 'Trè bèl parol !',
origine: 'activitypub',
@@ -80,6 +81,7 @@ describe('importBokanteComments', () => {
});
expect(strapi.documents('api::parole.parole').update).toHaveBeenCalledWith({
documentId: 'doc-7',
status: 'published',
data: {commentaires: {connect: expect.any(Array)}}
});
expect(result.imported).toBe(1);
+2
View File
@@ -37,6 +37,7 @@ async function importBokanteComments({strapi}) {
}
const commentaire = await strapi.documents('api::commentaire.commentaire').create({
status: 'published',
data: {
contenu: stripHtml(status.content),
datePublication: status.created_at,
@@ -58,6 +59,7 @@ async function importBokanteComments({strapi}) {
if (newCommentIds.length > 0) {
await strapi.documents('api::parole.parole').update({
documentId: parole.documentId,
status: 'published',
data: {commentaires: {connect: newCommentIds}}
});
}
+1 -1
View File
@@ -506,7 +506,7 @@ export interface ApiCommentaireCommentaire extends Struct.CollectionTypeSchema {
origine: Schema.Attribute.Enumeration<['local', 'activitypub']> &
Schema.Attribute.Required &
Schema.Attribute.DefaultTo<'local'>;
parole: Schema.Attribute.Relation<'oneToOne', 'api::parole.parole'>;
parole: Schema.Attribute.Relation<'manyToOne', 'api::parole.parole'>;
publishedAt: Schema.Attribute.DateTime;
remoteId: Schema.Attribute.String & Schema.Attribute.Unique;
remoteUrl: Schema.Attribute.String;