Skip to content

Commit

Permalink
Fix #5 add bot activity summary on last 7 days
Browse files Browse the repository at this point in the history
- add summary service and rely on cache
- clear cache on new bot post
- add summary plugin to notify via discord
- ui: add summary tab
- GHActions add 3 per week trigger
  • Loading branch information
boly38 committed Jun 9, 2024
1 parent 0eabeab commit 1edb2f2
Show file tree
Hide file tree
Showing 16 changed files with 216 additions and 35 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/trigger_summary_action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# create bot summary trigger
name: BES_trigger_summary_action
on:
schedule:
- cron: "0 6 * * 1" # Lundi matin 6h00 UTC
- cron: "0 10 * * 3" # Mercredi midi 10h00 UTC
- cron: "0 16 * * 5" # Vendredi soir 16h00 UTC
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest

environment: github_actions_bes
steps:
- name: trigger botEnSky Summary
env:
BES_SIMULATION_TOKEN: ${{ secrets.BES_SIMULATION_TOKEN }}
BES_SIMULATION_URL: ${{ secrets.BES_SIMULATION_URL }}
run: |
curl -q -H "API-TOKEN: ${BES_SIMULATION_TOKEN}" -H "PLUGIN-NAME: Summary" "${BES_SIMULATION_URL}"
36 changes: 26 additions & 10 deletions src/config/ApplicationConfig.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {ContainerBuilder} from 'node-dependency-injection';
import {nowHuman} from "../lib/Common.js";
import ApplicationProperties from './ApplicationProperties.js';
import LogtailService from "../servicesExternal/LogtailService.js";
import PlantnetApiService from "../servicesExternal/PlantnetApiService.js";
import BlueSkyService from "../servicesExternal/BlueSkyService.js";
import DiscordSendService from "../servicesExternal/DiscordSendService.js";
import ExpressServer from '../services/ExpressServer.js';
import BlueSkyService from "../services/BlueSkyService.js";
import Plantnet from "../plugins/Plantnet.js";
import PlantnetApiService from "../services/PlantnetApiService.js";
import BotService from "../services/BotService.js";
import NewsService from "../services/NewsService.js";
import LoggerService from "../services/LoggerService.js";
import LogtailService from "../services/LogtailService.js";
import UnMute from "../plugins/UnMute.js";
import AskPlantnet from "../plugins/AskPlantnet.js";
import PlantnetCommonService from "../services/PlantnetCommonService.js";
import {nowHuman} from "../lib/Common.js";
import DiscordSendService from "../servicesExternal/DiscordSendService.js";
import AuditLogsService from "../services/AuditLogsService.js";
import PlantnetCommonService from "../services/PlantnetCommonService.js";
import SummaryService from "../services/SummaryService.js";
import Plantnet from "../plugins/Plantnet.js";
import AskPlantnet from "../plugins/AskPlantnet.js";
import UnMute from "../plugins/UnMute.js";
import Summary from "../plugins/Summary.js";

export default class ApplicationConfig {
constructor() {
Expand Down Expand Up @@ -55,6 +57,12 @@ export default class ApplicationConfig {
container.register('plantnetApiService', PlantnetApiService)
.addArgument(container.get('config'))
.addArgument(container.get('loggerService'));

container.register('summaryService', SummaryService)
.addArgument(container.get('config'))
.addArgument(container.get('loggerService'))
.addArgument(container.get('blueskyService'));

}

constructPlugins() {
Expand Down Expand Up @@ -84,6 +92,13 @@ export default class ApplicationConfig {
.addArgument(container.get('loggerService'))
.addArgument(container.get('blueskyService'));
this.plugins.push(container.get('unmute'));

container.register('summary', Summary)
.addArgument(container.get('config'))
.addArgument(container.get('loggerService'))
.addArgument(container.get('summaryService'))
.addArgument(container.get('discordService'));
this.plugins.push(container.get('summary'));
}

constructBot() {
Expand Down Expand Up @@ -112,7 +127,8 @@ export default class ApplicationConfig {
botService: container.get('botService'),
blueskyService: container.get('blueskyService'),
newsService: container.get('newsService'),
auditLogsService: container.get('auditLogsService')
auditLogsService: container.get('auditLogsService'),
summaryService: container.get('summaryService')
});

const expressServer = container.get('expressServer');
Expand Down
3 changes: 3 additions & 0 deletions src/domain/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,6 @@ export const descriptionOfPostAuthor = postAuthor => {
export const filterWithEmbedImageView = p => p?.embed?.$type === "app.bsky.embed.images#view"
export const fiterWithNoReply = p => p?.replyCount === 0
export const fiterWithNotMuted = p => p?.author?.viewer?.muted === false

export const txtOfPosts = (posts, max = 5) => posts.slice(0,max).map(postTextOf).join("\n\n")
export const htmlOfPosts = (posts, max = 5) => posts.slice(0,max).map(postHtmlOf).join("<br/><br/>")
2 changes: 1 addition & 1 deletion src/plugins/AskPlantnet.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {arrayIsNotEmpty, clone, isSet, loadJsonResource} from "../lib/Common.js";
import {firstImageOf, postHtmlOf, postImageOf, postInfoOf, postLinkOf, postTextOf} from "../domain/post.js";
import {dataSimulationDirectory, pluginReject, pluginResolve} from "../services/BotService.js";
import {IDENTIFY_RESULT} from "../services/PlantnetApiService.js";
import {IDENTIFY_RESULT} from "../servicesExternal/PlantnetApiService.js";

export default class AskPlantnet {
constructor(config, loggerService, blueskyService, plantnetCommonService, plantnetApiService) {
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/Plantnet.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {arrayIsNotEmpty, clone, isSet, loadJsonResource} from "../lib/Common.js";
import {firstImageOf} from "../domain/post.js";
import {IDENTIFY_RESULT} from "../services/PlantnetApiService.js";
import {IDENTIFY_RESULT} from "../servicesExternal/PlantnetApiService.js";
import {dataSimulationDirectory} from "../services/BotService.js";

export default class Plantnet {
Expand Down
40 changes: 40 additions & 0 deletions src/plugins/Summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {pluginResolve} from "../services/BotService.js";

export default class Summary {
constructor(config, loggerService, summaryService, discordService) {
this.isAvailable = false;
this.logger = loggerService.getLogger().child({label: 'Summary'});
this.summaryService = summaryService;
this.discordService = discordService;
this.isAvailable = true;
}

getName() {
return "Summary";
}

isReady() {
return this.isAvailable;
}

async process(config) {
const analytics = await this.summaryService.cacheGetWeekSummary(config);
// DEBUG // logger.info(`analytics :\n ${JSON.stringify(analytics, null, 2)}`, context);
let text = `7 jours : posts: ${analytics.posts}, 'j'aime': ${analytics.likes}, réponses: ${analytics.replies}, re-post: ${analytics.reposts}`;
text += `\n\nmeilleur score: ${analytics.bestScore} (${analytics.bestScorePosts.length} posts) - exemples : \n${analytics.bestScorePostsTxt}`;
text += `\n\n+ de 'j'aime': ${analytics.bestLikes} (${analytics.bestLikesPosts.length} posts) - exemples : \n${analytics.bestLikesPostsTxt}`;

let html = `<b>7 jours</b> : posts: ${analytics.posts}, likes: ${analytics.likes}, replies: ${analytics.replies}, reposts: ${analytics.reposts}`;
html += `<br/><br/><b>Meilleur score</b> : ${analytics.bestScore} (${analytics.bestScorePosts?.length} posts) - exemples : <br/>${analytics.bestScorePostsHtml}`;
html += `<br/><br/><b>+ de 'j'aime'</b> : ${analytics.bestLikes} (${analytics.bestLikesPosts?.length} posts) - exemples : <br/>${analytics.bestLikesPostsHtml}`;

let markdown = `**7 jours** : posts: ${analytics.posts}, likes: ${analytics.likes}, replies: ${analytics.replies}, reposts: ${analytics.reposts}`;
markdown += `\n\n**Meilleur score** : ${analytics.bestScore} (${analytics.bestScorePosts?.length} posts) - exemples : \n${analytics.bestScorePostsTxt}`;
markdown += `\n\n**+ de 'j'aime'** : ${analytics.bestLikes} (${analytics.bestLikesPosts.length} posts) - exemples : \n${analytics.bestLikesPostsTxt}`;
await this.discordService.sendMessage(markdown);

return pluginResolve(text, html);
}

}

2 changes: 1 addition & 1 deletion src/plugins/UnMute.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default class UnMute {
if (result === null) {
return Promise.resolve(pluginResolve(`Aucun compte masqué`, `Aucun compte masqué`));
}
return Promise.resolve(pluginResolve(`Démasqué ${result}`, `Démasqué ${result}`, 200));
return Promise.resolve(pluginResolve(`Démasqué ${result}`, `Démasqué ${result}`));
}

async unMuteMutedActors(context) {
Expand Down
30 changes: 19 additions & 11 deletions src/services/BotService.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {clearSummaryCache} from "./SummaryService.js";

export default class BotService {

constructor(config, loggerService, auditLogsService, newsService, plugins) {
Expand Down Expand Up @@ -54,19 +56,25 @@ export default class BotService {
async process(remoteAddress, doSimulate, pluginName) {
const bot = this;
const context = {remoteAddress, pluginName};
this.assumeProcessRateLimit(bot, remoteAddress);
const plugin = this.assumeBotReadyPlugin(pluginName, remoteAddress);
bot.logger.info(`${(doSimulate ? "Simulation" : "Exécution")} du plugin - ${pluginName}`, context);
try {
this.assumeProcessRateLimit(remoteAddress);
const plugin = this.assumeBotReadyPlugin(pluginName, remoteAddress);
bot.logger.info(`${(doSimulate ? "Simulation" : "Exécution")} du plugin - ${pluginName}`, context);
const result = await plugin.process({"doSimulate": doSimulate, context});
bot.logger.info(`plugin result ${result.text}`, context);
// DEBUG // bot.logger.info(`plugin result ${result.text}`, context);
bot.newsService.add(result.html);
if (result.post > 0) {
clearSummaryCache();
}
return result;
} catch (err) {
bot.logger.warn(`plugin error: ${err.message}`, context);
bot.auditLogsService.createAuditLog(`${err.message} ${JSON.stringify(context)}`);
bot.newsService.add(err.html ? err.html : err.message);
throw err;
} catch (error) {
if (error.status && error.message) {
throw error;
}
bot.logger.warn(`plugin error: ${error.message}`, context);
bot.auditLogsService.createAuditLog(`${error.message} ${JSON.stringify(context)}`);
bot.newsService.add(error.html ? error.html : error.message);
throw error;
}
}

Expand Down Expand Up @@ -99,8 +107,8 @@ export default class BotService {
}


export const pluginResolve = (text, html, status = 200) => {
return {text, html, status};
export const pluginResolve = (text, html, status = 200, post = 0) => {
return {text, html, status, post};
}
export const pluginReject = (text, html, status, shortResponseMessage) => {
return {text, html, status, message: shortResponseMessage};
Expand Down
23 changes: 18 additions & 5 deletions src/services/ExpressServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ const HEALTH_ENDPOINT = '/health';
const UNAUTHORIZED_FRIENDLY = "Le milieu autorisé c'est un truc, vous y êtes pas vous hein !";// (c) Coluche
export default class ExpressServer {
constructor(services) {
const {config, loggerService, blueskyService, botService, newsService, auditLogsService} = services;
const {
config, loggerService, blueskyService,
botService, newsService,
auditLogsService, summaryService
} = services;
this.config = config;
this.blueskyService = blueskyService;
this.botService = botService;
this.newsService = newsService;
this.auditLogsService = auditLogsService;
this.summaryService = summaryService;

this.logger = loggerService.getLogger().child({label: 'ExpressServer'});

Expand Down Expand Up @@ -60,7 +65,8 @@ export default class ExpressServer {
getRemoteAddress(request) {
return request.headers['x-forwarded-for'] ?
request.headers['x-forwarded-for']
: request.connection.remoteAddress;
: request.connection?.remoteAddress
|| "???";
}

aboutResponse(req, res) {
Expand Down Expand Up @@ -94,6 +100,11 @@ export default class ExpressServer {
unauthorized(res, UNAUTHORIZED_FRIENDLY);
}
} catch (error) {
if (error.status && error.message) {
const {status, message} = error;
res.status(status).json({success: false, message});
return;
}
let errId = generateErrorId();
// internal
let errorInternalDetails = `Error id:${errId} msg:${error.message} stack:${error.stack}`;
Expand All @@ -107,19 +118,21 @@ export default class ExpressServer {
}
}

webPagesResponse(req, res) {
const {version, newsService, config} = this;
async webPagesResponse(req, res) {
const {version, newsService, config, summaryService} = this;
const projectHomepage = cacheGetProjectHomepage();
const projectIssues = cacheGetProjectBugsUrl();
const projectDiscussions = cacheGetProjectMetadata("projectDiscussions");
const blueskyAccount = cacheGetProjectMetadata("blueskyAccount");
const blueskyDisplayName = cacheGetProjectMetadata("blueskyDisplayName");
const summary = await summaryService.cacheGetWeekSummary({});
newsService.getNews()
.then(news => {
res.render('pages/index', {// page data
news, "tz": config.tz,
version, projectHomepage, projectIssues, projectDiscussions,
blueskyAccount, blueskyDisplayName
blueskyAccount, blueskyDisplayName,
summary
});
});
}
Expand Down
10 changes: 7 additions & 3 deletions src/services/PlantnetCommonService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {pluginReject, pluginResolve} from "./BotService.js";
import {postAuthorOf, postHtmlOf, postImageOf, postInfoOf, postLinkOf, postTextOf} from "../domain/post.js";
import {isSet} from "../lib/Common.js";
import TinyURL from "tinyurl";
import {PLANTNET_MINIMAL_PERCENT} from "./PlantnetApiService.js";
import {PLANTNET_MINIMAL_PERCENT} from "../servicesExternal/PlantnetApiService.js";

export default class PlantnetCommonService {
constructor(loggerService, auditLogsService, blueskyService) {
Expand Down Expand Up @@ -115,7 +115,9 @@ export default class PlantnetCommonService {
const replySent = doSimulate ? "SIMULATION - Réponse prévue" : "Réponse émise";
return Promise.resolve(pluginResolve(
`Post:\n\t${replyToTextOf}\n\t${replySent} : ${replyMessage}${authorAction}`,
`<b>Post</b>:<div class="bg-info">${replyToHtmlOf}</div><b>${replySent}</b>: ${replyMessage}${authorAction}`
`<b>Post</b>:<div class="bg-info">${replyToHtmlOf}</div><b>${replySent}</b>: ${replyMessage}${authorAction}`,
200,
doSimulate ? 0 : 1
));
} catch (err) {
this.logError("replyTo", err, {...context, doSimulate, candidate, replyMessage});
Expand Down Expand Up @@ -144,7 +146,9 @@ export default class PlantnetCommonService {
const replySent = doSimulate ? "SIMULATION - Réponse prévue" : "Réponse émise";
resolve(pluginResolve(
`Post:\n\t${candidateTextOf}\n\t${replySent} : ${replyMessage}`,
`<b>Post</b>:<div class="bg-info">${candidateHtmlOf}</div><b>${replySent}</b>: ${replyMessage}`
`<b>Post</b>:<div class="bg-info">${candidateHtmlOf}</div><b>${replySent}</b>: ${replyMessage}`,
200,
doSimulate ? 0 : 1
));
})
.catch(err => {
Expand Down
61 changes: 61 additions & 0 deletions src/services/SummaryService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {cacheEvictKey, cacheGetTtlObject} from "../lib/MemoryCache.js";
import {htmlOfPosts, txtOfPosts} from "../domain/post.js";

export const SUMMARY_CACHE_KEY = "cache:summary";
export const ONE_DAY_SECOND = 60 * 60 * 24;

export default class SummaryService {
constructor(config, loggerService, blueskyService) {
this.config = config;
this.logger = loggerService.getLogger().child({label: 'SummaryService'});
this.blueskyService = blueskyService;
}

cacheGetWeekSummary(config) {
return cacheGetTtlObject(SUMMARY_CACHE_KEY, ONE_DAY_SECOND, this.getWeekSummary.bind(this, config));
}

async getWeekSummary(options) {
const {blueskyService, logger} = this;
const {context = {}} = options;
const botPosts = await blueskyService.searchPosts({
searchQuery: "from:botensky.bsky.social",
"hasImages": true,
"maxHoursOld": 7 * 24,// now-7d ... now
"limit": 100
})
logger.info(`Summary - ${botPosts.length} post(s)`, context);
const analytics = {
posts: 0, likes: 0, replies: 0, reposts: 0,
bestScore: 0, bestScorePosts: [],
bestLikes: 0, bestLikesPosts: []
};
botPosts.forEach(p => {
// DEBUG // logger.info(` - ${p.record.createdAt} - likes:${p.likeCount} replies:${p.replyCount}, reposts:${p.repostCount}`, context);
analytics.posts++;
analytics.likes += p.likeCount;
analytics.replies += p.replyCount;
analytics.reposts += p.repostCount;
const postScore = p.likeCount * 2 + p.replyCount * 3 + p.repostCount * 4;
if (postScore === analytics.bestScore) {
analytics.bestScorePosts.push(p);
} else if (postScore > analytics.bestScore) {
analytics.bestScorePosts = [p];
analytics.bestScore = postScore;
}
if (p.likeCount === analytics.bestLikes) {
analytics.bestLikesPosts.push(p);
} else if (p.likeCount > analytics.bestLikes) {
analytics.bestLikesPosts = [p];
analytics.bestLikes = p.likeCount;
}
});
analytics.bestScorePostsHtml = htmlOfPosts(analytics.bestScorePosts, 2);
analytics.bestLikesPostsHtml = htmlOfPosts(analytics.bestLikesPosts, 2);
analytics.bestScorePostsTxt = txtOfPosts(analytics.bestScorePosts, 2);
analytics.bestLikesPostsTxt = txtOfPosts(analytics.bestLikesPosts, 2);
return analytics;
}
}

export const clearSummaryCache = () => cacheEvictKey(SUMMARY_CACHE_KEY);
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios from "axios";
import {isSet, nowISO8601, nowMinusHoursUTCISO, toHuman, toHumanDay, toHumanTime} from "../lib/Common.js";
import {NEWS_LABEL} from "./NewsService.js";
import {NEWS_LABEL} from "../services/NewsService.js";

/**
* autonomous gist : https://gist.github.com/boly38/e853a1d83b63481fd5a97e4b7822813e
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import superagent from 'superagent';
import {isSet} from "../lib/Common.js";
import {dataSimulationDirectory} from "./BotService.js";
import {dataSimulationDirectory} from "../services/BotService.js";

const MY_API_PLANTNET_V2_URL = 'https://my-api.plantnet.org/v2/identify/all';
export const PLANTNET_MINIMAL_PERCENT = 20;
Expand Down
Loading

0 comments on commit 1edb2f2

Please sign in to comment.