Skip to content

Commit 5197d7d

Browse files
committed
Fix #26 mute unmute plantnet id. with no result
add workflow to unmute
1 parent f7b8afc commit 5197d7d

File tree

11 files changed

+276
-100
lines changed

11 files changed

+276
-100
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# create bot unmute trigger
2+
name: BES_trigger_unmute
3+
on:
4+
# every day at 23 55 UTC
5+
schedule:
6+
- cron: "23 55 * * *"
7+
# Allows you to run this workflow manually from the Actions tab
8+
workflow_dispatch:
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
14+
environment: github_actions_bes
15+
steps:
16+
- name: trigger botEnSky UnMute
17+
env:
18+
BES_SIMULATION_TOKEN: ${{ secrets.BES_SIMULATION_TOKEN }}
19+
BES_SIMULATION_URL: ${{ secrets.BES_SIMULATION_URL }}
20+
run: |
21+
curl -q -H "API-TOKEN: ${BES_SIMULATION_TOKEN}" -H "PLUGIN-NAME: UnMute" "${BES_SIMULATION_URL}"

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"tst": "echo windows %TST% test&& set NODE_ENV=test&& mocha --trace-warnings --timeout 180000 --unhandled-rejections=strict tests/all-root.test.js tests/%TST%.test.js",
1313
"test": "echo ci-test&& set NODE_ENV=test&&c8 mocha --exit --unhandled-rejections=strict tests/*.test.js --timeout 50000",
1414
"ci-test": "echo ci-test&& export NODE_ENV=test&&c8 mocha --unhandled-rejections=strict tests/*.test.js --timeout 50000",
15-
"wip": "curl -v -H 'API-TOKEN: SIMULATE_WITH_LOCAL_DEV_ACCOUNT' -H 'PLUGIN-NAME: Plantnet' http://localhost:5000/api/hook"
15+
"wipPN": "curl -v -H 'API-TOKEN: SIMULATE_WITH_LOCAL_DEV_ACCOUNT' -H 'PLUGIN-NAME: Plantnet' http://localhost:5000/api/hook",
16+
"wipUM": "curl -v -H 'API-TOKEN: SIMULATE_WITH_LOCAL_DEV_ACCOUNT' -H 'PLUGIN-NAME: UnMute' http://localhost:5000/api/hook"
1617
},
1718
"repository": {
1819
"type": "git",

src/config/ApplicationConfig.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import BotService from "../services/BotService.js";
88
import NewsService from "../services/NewsService.js";
99
import LoggerService from "../services/LoggerService.js";
1010
import LogtailService from "../services/LogtailService.js";
11+
import UnMute from "../plugins/UnMute.js";
1112

1213
export default class ApplicationConfig {
1314
constructor() {
@@ -52,6 +53,12 @@ export default class ApplicationConfig {
5253
.addArgument(container.get('blueskyService'))
5354
.addArgument(container.get('plantnetService'));
5455
this.plugins.push(container.get('plantnet'));
56+
57+
container.register('unmute', UnMute)
58+
.addArgument(container.get('config'))
59+
.addArgument(container.get('loggerService'))
60+
.addArgument(container.get('blueskyService'));
61+
this.plugins.push(container.get('unmute'));
5562
}
5663

5764
constructBot() {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"author": {
3+
"did": "did:plc:4vfm3oevtkk57qmu2nbemjqo",
4+
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4vfm3oevtkk57qmu2nbemjqo/bafkreigslbnc6uecdvkikwnwiik6w522rhx6drzyswfonjiiq64yj44jry@jpeg",
5+
"displayName": "Martijn Rijk",
6+
"handle": "martijnrijk.bsky.social",
7+
"viewer": {
8+
"muted": false
9+
}
10+
},
11+
"indexedAt": "2024-05-30T09:44:42.875Z",
12+
"likeCount": 0,
13+
"replyCount": 0,
14+
"repostCount": 0,
15+
"uri": "at://did:plc:4vfm3oevtkk57qmu2nbemjqo/app.bsky.feed.post/3ktp4vr7l6c2b",
16+
"cid": "bafyreibbi5bijaq7kuklsy3ujcxtqqy5ddkcrxdanndp7wtv5hu7qpmphq",
17+
"record": {
18+
"$type": "app.bsky.feed.post",
19+
"createdAt": "2024-05-30T09:44:42.875Z",
20+
"langs": [
21+
"en"
22+
],
23+
"text": "Fleur toch op met je boodschap!"
24+
},
25+
"embed": {
26+
"$type": "app.bsky.embed.images#view",
27+
"images": [
28+
{
29+
"alt": "",
30+
"fullsize": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:4vfm3oevtkk57qmu2nbemjqo/bafkreih2tdl335j6xw6ssy34yybvmz7c76ighounjejpdjr7btzdl3pkty@jpeg"
31+
}
32+
]
33+
}
34+
}

src/domain/post.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ export const fromBlueskyPostImages = images => images?.map(fromBlueskyPostImage)
3939
*/
4040
export const fromBlueskyPost = post => {
4141
const {
42-
"author": {avatar, displayName, handle},
42+
"author": {did, avatar, displayName, handle, "viewer": {muted}},
4343
embed,
4444
indexedAt, likeCount, replyCount, repostCount, uri, cid,
4545
record
4646
} = post;
4747
const result = {
48-
"author": {avatar, displayName, handle},
48+
"author": {did, avatar, displayName, handle, "viewer": {muted}},
4949
indexedAt, likeCount, replyCount, repostCount, uri, cid,
5050
"record": {
5151
"$type": record["$type"],
@@ -124,5 +124,17 @@ export const postTextOf = post => {
124124
return `${postLink} ${postDate} by @${username} (${handleLink}) --- ${text}`;
125125
}
126126

127+
export const postAuthorOf = post => post?.author;
128+
export const displayNameOfPostAuthor = postAuthor => postAuthor?.displayName;
129+
export const handleOfPostAuthor = postAuthor => postAuthor?.handle;
130+
export const didOfPostAuthor = postAuthor => postAuthor?.did;
131+
132+
export const descriptionOfPostAuthor = postAuthor => {
133+
const displayName = displayNameOfPostAuthor(postAuthor);
134+
const handle = handleOfPostAuthor(postAuthor);
135+
return handle === displayName ? `@${handle}` : `${displayName}(@${handle})`;
136+
}
137+
127138
export const filterWithEmbedImageView = p => p?.embed?.$type === "app.bsky.embed.images#view"
128139
export const fiterWithNoReply = p => p?.replyCount === 0
140+
export const fiterWithNotMuted = p => p?.author?.viewer?.muted === false

src/plugins/Plantnet.js

Lines changed: 82 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import {arrayIsNotEmpty, clone, isSet, loadJsonResource} from "../lib/Common.js";
2-
import {firstImageOf, postHtmlOf, postImageOf, postInfoOf, postLinkOf, postTextOf} from "../domain/post.js";
2+
import {
3+
firstImageOf,
4+
postAuthorOf,
5+
postHtmlOf,
6+
postImageOf,
7+
postInfoOf,
8+
postLinkOf,
9+
postTextOf
10+
} from "../domain/post.js";
311
import TinyURL from "tinyurl";
412

513
const PLANTNET_MINIMAL_PERCENT = 20;
@@ -39,7 +47,7 @@ export default class Plantnet {
3947
return this.isAvailable;
4048
}
4149

42-
process(config) {
50+
async process(config) {
4351
const plugin = this;
4452
let {pluginTags, pluginMoreTags, doSimulate, simulateIdentifyCase, context} = config;
4553
if (config.pluginName === undefined) {
@@ -53,36 +61,37 @@ export default class Plantnet {
5361
}
5462
// if at least one want to simulate then simulate
5563
const doSimulateIdentify = plugin.plantnetSimulate || isSet(simulateIdentifyCase);
56-
return new Promise((resolve, reject) => {
57-
plugin.searchNextCandidate(config)
58-
.then(candidate => {
59-
const candidatePhoto = firstImageOf(candidate);
60-
if (!candidatePhoto) {
61-
plugin.logger.info("no candidate image", context);
62-
throw {"message": `aucune image pour Pl@ntNet dans ${postLinkOf(candidate)}`, "status": 202};
63-
}
64-
plugin.logger.debug(`post Candidate : ${postLinkOf(candidate)}\n` +
65-
`\t${postInfoOf(candidate)}\n` +
66-
`\t${postImageOf(candidatePhoto)}`, context);
64+
try {
65+
const candidate = await plugin.searchNextCandidate(config);
66+
const candidatePhoto = firstImageOf(candidate);
67+
if (!candidatePhoto) {
68+
plugin.logger.info("no candidate image", context);
69+
return Promise.reject({
70+
"message": `aucune image pour Pl@ntNet dans ${postLinkOf(candidate)}`,
71+
"status": 202
72+
});
73+
}
74+
plugin.logger.debug(`post Candidate : ${postLinkOf(candidate)}\n` +
75+
`\t${postInfoOf(candidate)}\n` +
76+
`\t${postImageOf(candidatePhoto)}`, context);
6777

68-
const candidateImageFullsize = candidatePhoto?.fullsize;
78+
const candidateImageFullsize = candidatePhoto?.fullsize;
6979

70-
const identifyOptions = {
71-
"image": candidateImageFullsize,
72-
doSimulate,
73-
doSimulateIdentify,
74-
simulateIdentifyCase,
75-
candidate,
76-
"tags": pluginTags,
77-
context
78-
};
79-
plugin.logger.debug(`identifyOptions : ${identifyOptions}`, context);
80-
plugin.plantnetIdentify(identifyOptions)
81-
.then(resolve)
82-
.catch(reject)
83-
})
84-
.catch(reject);
85-
});
80+
const identifyOptions = {
81+
"image": candidateImageFullsize,
82+
doSimulate,
83+
doSimulateIdentify,
84+
simulateIdentifyCase,
85+
candidate,
86+
"tags": pluginTags,
87+
context
88+
};
89+
plugin.logger.debug(`identifyOptions : ${identifyOptions}`, context);
90+
const result = await plugin.plantnetIdentify(identifyOptions);
91+
return Promise.resolve(result);
92+
} catch (err) {
93+
return Promise.reject(err);
94+
}
8695
}
8796

8897
/**
@@ -100,21 +109,26 @@ export default class Plantnet {
100109
*/
101110
searchNextCandidate(config, bookmark = 0) {
102111
const plugin = this;
103-
const {pluginName, context} = config;
112+
const {pluginName, context, doSimulateSearch} = config;
104113
return new Promise((resolve, reject) => {
114+
if (doSimulateSearch) {
115+
plugin.blueskyService.login().then(()=>{
116+
return resolve(loadJsonResource("src/data/blueskyPostFakeFlower.json"));
117+
})
118+
}
105119
let searchQuery = plugin.questions[bookmark];
106120
if (config.searchExtra) {
107121
searchQuery += " " + config.searchExtra;
108122
}
109123
const hasImages = true;
110124
const hasNoReply = true;
125+
const isNotMuted = true;
111126
const maxHoursOld = 24;// now-24h ... now
112-
plugin.blueskyService.searchPosts({searchQuery, hasImages, hasNoReply, maxHoursOld})
127+
plugin.blueskyService.searchPosts({searchQuery, hasImages, hasNoReply, isNotMuted, maxHoursOld})
113128
.then(candidatePosts => {
114129
plugin.logger.info(`${candidatePosts.length} candidate(s)`, context);
115130
if (arrayIsNotEmpty(candidatePosts)) {
116-
resolve(candidatePosts[0]);
117-
return;
131+
return resolve(candidatePosts[0]);
118132
}
119133
if (bookmark + 1 < plugin.questions.length) {
120134
plugin.searchNextCandidate(config, bookmark + 1)
@@ -129,35 +143,34 @@ export default class Plantnet {
129143
});
130144
}
131145

132-
plantnetIdentify(options) {
146+
async plantnetIdentify(options) {
133147
const plugin = this;
134148
const {image, doSimulate, doSimulateIdentify, simulateIdentifyCase, candidate, context} = options;
135-
136-
return new Promise((resolve, reject) => {
137-
plugin.plantnetService.identify({"imageUrl": image, doSimulateIdentify, simulateIdentifyCase})
138-
.then(plantResult => {
139-
plugin.logger.debug(`plantnetResult : ${JSON.stringify(plantResult)}`, context);
140-
const firstScoredResult = plugin.plantnetService.hasScoredResult(plantResult, PLANTNET_MINIMAL_RATIO);
141-
if (!firstScoredResult) {
142-
plugin.resolveWithoutScoredResult(options).then(resolve).catch(reject);
143-
return;
144-
}
145-
plugin.replyScoredResultWithImage(options, firstScoredResult).then(resolve).catch(reject);
146-
})
147-
.catch(err => {
148-
plugin.logError("plantnetService.identify", err, {...context, image, doSimulate});
149-
if (err?.status === 404) {
150-
plugin.resolveWithNoIdentificationResult(options).then(resolve);
151-
return
152-
}
153-
reject({
154-
"message": "impossible d'identifier l'image",
155-
"html": `<b>Post</b>: <div class="bg-warning">${postHtmlOf(candidate)}</div>` +
156-
`<b>Erreur</b>: impossible d'identifier l'image`,
157-
"status": 500
158-
});
159-
});
160-
});
149+
let plantResult;
150+
try {
151+
plantResult = await plugin.plantnetService.identify({
152+
"imageUrl": image,
153+
doSimulateIdentify,
154+
simulateIdentifyCase
155+
});
156+
} catch (err) {
157+
plugin.logError("plantnetService.identify", err, {...context, image, doSimulate});
158+
if (err?.status === 404) {
159+
return await plugin.resolveWithNoIdentificationResult(options);
160+
}
161+
return Promise.reject({
162+
"text": "impossible d'identifier l'image",
163+
"html": `<b>Post</b>: <div class="bg-warning">${postHtmlOf(candidate)}</div>` +
164+
`<b>Erreur</b>: impossible d'identifier l'image`,
165+
"status": 500
166+
});
167+
}
168+
plugin.logger.debug(`plantnetResult : ${JSON.stringify(plantResult)}`, context);
169+
const firstScoredResult = plugin.plantnetService.hasScoredResult(plantResult, PLANTNET_MINIMAL_RATIO);
170+
if (!firstScoredResult) {
171+
return plugin.resolveWithoutScoredResult(options);
172+
}
173+
return plugin.replyScoredResultWithImage(options, firstScoredResult);
161174
}
162175

163176
buildShortUrlWithText(imageUrl, text) {
@@ -218,22 +231,24 @@ export default class Plantnet {
218231
});
219232
}
220233

221-
resolveWithNoIdentificationResult(options) {
222-
const {candidate} = options;
234+
async resolveWithNoIdentificationResult(options) {
235+
const {candidate, context} = options;
223236
const candidateHtmlOf = postHtmlOf(candidate);
224237
const candidateTextOf = postTextOf(candidate);
225-
const noIdentificationText = " ne donne aucune identification Pl@ntNet";
238+
const noIdentificationText = "L'identification par Pl@ntNet ne donne aucun résultat (auteur masqué)";
239+
await this.blueskyService.safeMuteCandidateAuthor(postAuthorOf(candidate), noIdentificationText, context);
226240
return Promise.resolve({
227241
"html": `<b>Post</b>:<div class="bg-info">${candidateHtmlOf}</div> ${noIdentificationText}`,
228242
"text": `Post:\n\t${candidateTextOf}\n\t${noIdentificationText}`
229243
});
230244
}
231245

232-
resolveWithoutScoredResult(options) {
233-
const {candidate} = options;
246+
async resolveWithoutScoredResult(options) {
247+
const {candidate, context} = options;
234248
const candidateHtmlOf = postHtmlOf(candidate);
235249
const candidateTextOf = postTextOf(candidate);
236-
const noIdentificationGoodScoreText = `L'identification par Pl@ntNet n'a pas donné de résultat assez concluant 😩 (score<${PLANTNET_MINIMAL_PERCENT}%)`;
250+
const noIdentificationGoodScoreText = `L'identification par Pl@ntNet n'a pas donné de résultat assez concluant 😩 (score<${PLANTNET_MINIMAL_PERCENT}%)(auteur masqué)`;
251+
await this.blueskyService.safeMuteCandidateAuthor(postAuthorOf(candidate), noIdentificationGoodScoreText, context);
237252
return Promise.resolve({
238253
"html": `<b>Post</b>:<div class="bg-info">${candidateHtmlOf}</div> ${noIdentificationGoodScoreText}`,
239254
"text": `Post:\n\t${candidateTextOf}\n\t${noIdentificationGoodScoreText}`

src/plugins/UnMute.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export default class UnMute {
2+
constructor(config, loggerService, blueskyService) {
3+
this.logger = loggerService.getLogger().child({label: 'UnMute'});
4+
this.logger.level = "INFO"; // DEBUG will show search results
5+
this.blueskyService = blueskyService;
6+
this.isAvailable = true;
7+
}
8+
9+
getName() {
10+
return "UnMute";
11+
}
12+
13+
isReady() {
14+
return this.isAvailable;
15+
}
16+
17+
async process(config) {
18+
const plugin = this;
19+
let {context} = config;
20+
const result = await plugin.unMuteMutedActors(context);
21+
if (result === null) {
22+
return Promise.resolve({"text": `Aucun compte masqué`, "html": `Aucun compte masqué`, "status": 200});
23+
}
24+
return Promise.resolve({"text": `Démasqué ${result}`, "html": `Démasqué ${result}`, "status": 200});
25+
}
26+
27+
unMuteMutedActors(context) {
28+
const plugin = this;
29+
return new Promise((resolve, reject) => {
30+
plugin.blueskyService.getMutes()
31+
.then(mutes => {
32+
let unMuted = [];
33+
Promise.all(mutes.map(
34+
m => plugin.blueskyService.safeUnMuteMuted(m, context)
35+
.then(() => unMuted.push(m.handle))
36+
))
37+
.then(result => {
38+
plugin.logger.debug(`unmuteOldMutedActors result: ${result}`);
39+
if (unMuted?.length > 0) {
40+
plugin.logger.info(`un-muted: ${unMuted}`, context);
41+
return resolve(unMuted);
42+
}
43+
return resolve(null);
44+
})
45+
.catch(err => plugin.logger.warn(`unmuteOldMutedActors err: ${err.message}`, context))
46+
})
47+
.catch(reject)
48+
});
49+
}
50+
}

0 commit comments

Comments
 (0)