Fix
This commit is contained in:
@@ -30,20 +30,33 @@ services:
|
|||||||
timeout_seconds: 5
|
timeout_seconds: 5
|
||||||
info:
|
info:
|
||||||
- label: "Commits totaux"
|
- label: "Commits totaux"
|
||||||
source: "rss_multi"
|
source: "gitea_multi"
|
||||||
urls:
|
base_url: ""
|
||||||
|
owner: ""
|
||||||
|
repos:
|
||||||
- ""
|
- ""
|
||||||
path: "rss.channel.item"
|
|
||||||
aggregate: "count"
|
aggregate: "count"
|
||||||
|
token_env: "GITEA_TOKEN"
|
||||||
|
fallback_urls:
|
||||||
|
- ""
|
||||||
- label: "Dernier commit"
|
- label: "Dernier commit"
|
||||||
source: "rss_multi"
|
source: "gitea_multi"
|
||||||
urls:
|
base_url: ""
|
||||||
|
owner: ""
|
||||||
|
repos:
|
||||||
- ""
|
- ""
|
||||||
path: "rss.channel.item"
|
|
||||||
aggregate: "latest_title"
|
aggregate: "latest_title"
|
||||||
- label: "Publié le"
|
token_env: "GITEA_TOKEN"
|
||||||
source: "rss_multi"
|
fallback_urls:
|
||||||
urls:
|
- ""
|
||||||
|
- label: "Publié le"
|
||||||
|
source: "gitea_multi"
|
||||||
|
base_url: ""
|
||||||
|
owner: ""
|
||||||
|
repos:
|
||||||
- ""
|
- ""
|
||||||
path: "rss.channel.item"
|
|
||||||
aggregate: "latest_pubdate"
|
aggregate: "latest_pubdate"
|
||||||
|
token_env: "GITEA_TOKEN"
|
||||||
|
transform: "discord_time"
|
||||||
|
fallback_urls:
|
||||||
|
- ""
|
||||||
|
|||||||
512
src/bot.js
512
src/bot.js
@@ -1,14 +1,25 @@
|
|||||||
require('dotenv').config();
|
const path = require('path');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
|
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
|
||||||
const { fetch } = require('undici');
|
const { fetch } = require('undici');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
const envPath = path.resolve(__dirname, '..', '.env');
|
||||||
|
const envResult = dotenv.config({ path: envPath });
|
||||||
|
if (envResult.error) {
|
||||||
|
// Fallback to default resolution when the project root already matches process.cwd().
|
||||||
|
dotenv.config();
|
||||||
|
}
|
||||||
|
|
||||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
||||||
const STATUS_GUILD_ID = process.env.STATUS_GUILD_ID;
|
const STATUS_GUILD_ID = process.env.STATUS_GUILD_ID;
|
||||||
const STATUS_CHANNEL_ID = process.env.STATUS_CHANNEL_ID;
|
const STATUS_CHANNEL_ID = process.env.STATUS_CHANNEL_ID;
|
||||||
const ADMIN_USER_ID = process.env.ADMIN_USER_ID;
|
const ADMIN_USER_ID = process.env.ADMIN_USER_ID;
|
||||||
const CONFIG_PATH = process.env.CONFIG_PATH || './config.yml';
|
const CONFIG_PATH = process.env.CONFIG_PATH || './config.yml';
|
||||||
|
const RESOLVED_CONFIG_PATH = path.isAbsolute(CONFIG_PATH)
|
||||||
|
? CONFIG_PATH
|
||||||
|
: path.resolve(__dirname, '..', CONFIG_PATH);
|
||||||
const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '60', 10);
|
const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL || '60', 10);
|
||||||
const STICKY_MESSAGE = String(process.env.STICKY_MESSAGE || 'true').toLowerCase() === 'true';
|
const STICKY_MESSAGE = String(process.env.STICKY_MESSAGE || 'true').toLowerCase() === 'true';
|
||||||
|
|
||||||
@@ -27,7 +38,7 @@ for (const [name, value] of Object.entries(requiredEnv)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
const text = fs.readFileSync(CONFIG_PATH, 'utf8');
|
const text = fs.readFileSync(RESOLVED_CONFIG_PATH, 'utf8');
|
||||||
return yaml.load(text);
|
return yaml.load(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,29 +110,59 @@ function extractTag(text, tag) {
|
|||||||
return match ? decodeXmlEntities(match[1].trim()) : null;
|
return match ? decodeXmlEntities(match[1].trim()) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractAtomLink(text) {
|
||||||
|
if (!text || typeof text !== 'string') return null;
|
||||||
|
const match = text.match(/<link\b[^>]*href=['"]([^'"]+)['"][^>]*>/i);
|
||||||
|
if (match && match[1]) return decodeXmlEntities(match[1].trim());
|
||||||
|
return extractTag(text, 'link');
|
||||||
|
}
|
||||||
|
|
||||||
function parseRss(text) {
|
function parseRss(text) {
|
||||||
const channelMatch = /<channel>([\s\S]*?)<\/channel>/i.exec(text);
|
const channelMatch = /<channel\b[^>]*>([\s\S]*?)<\/channel>/i.exec(text);
|
||||||
const channelText = channelMatch ? channelMatch[1] : text;
|
const feedMatch = /<feed\b[^>]*>([\s\S]*?)<\/feed>/i.exec(text);
|
||||||
|
const channelText = channelMatch ? channelMatch[1] : (feedMatch ? feedMatch[1] : text);
|
||||||
const items = [];
|
const items = [];
|
||||||
const itemRegex = /<item>([\s\S]*?)<\/item>/gi;
|
const itemRegex = /<item\b[^>]*>([\s\S]*?)<\/item>/gi;
|
||||||
let itemMatch;
|
let itemMatch;
|
||||||
while ((itemMatch = itemRegex.exec(channelText))) {
|
while ((itemMatch = itemRegex.exec(channelText))) {
|
||||||
const item = itemMatch[1];
|
const item = itemMatch[1];
|
||||||
items.push({
|
items.push({
|
||||||
title: extractTag(item, 'title'),
|
title: extractTag(item, 'title'),
|
||||||
link: extractTag(item, 'link'),
|
link: extractTag(item, 'link'),
|
||||||
description: extractTag(item, 'description'),
|
description: extractTag(item, 'description') || extractTag(item, 'content:encoded'),
|
||||||
author: extractTag(item, 'author'),
|
author: extractTag(item, 'author'),
|
||||||
pubDate: extractTag(item, 'pubDate'),
|
pubDate: extractTag(item, 'pubDate') || extractTag(item, 'published') || extractTag(item, 'updated'),
|
||||||
guid: extractTag(item, 'guid'),
|
guid: extractTag(item, 'guid') || extractTag(item, 'id'),
|
||||||
|
updated: extractTag(item, 'updated'),
|
||||||
|
published: extractTag(item, 'published'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!items.length && feedMatch) {
|
||||||
|
const entryRegex = /<entry\b[^>]*>([\s\S]*?)<\/entry>/gi;
|
||||||
|
let entryMatch;
|
||||||
|
while ((entryMatch = entryRegex.exec(feedMatch[1]))) {
|
||||||
|
const entry = entryMatch[1];
|
||||||
|
const fullEntry = entryMatch[0];
|
||||||
|
items.push({
|
||||||
|
title: extractTag(entry, 'title'),
|
||||||
|
link: extractAtomLink(fullEntry),
|
||||||
|
description: extractTag(entry, 'summary') || extractTag(entry, 'content'),
|
||||||
|
author: extractTag(entry, 'author'),
|
||||||
|
pubDate: extractTag(entry, 'published') || extractTag(entry, 'updated'),
|
||||||
|
guid: extractTag(entry, 'id'),
|
||||||
|
updated: extractTag(entry, 'updated'),
|
||||||
|
published: extractTag(entry, 'published'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rss: {
|
rss: {
|
||||||
channel: {
|
channel: {
|
||||||
title: extractTag(channelText, 'title'),
|
title: extractTag(channelText, 'title'),
|
||||||
link: extractTag(channelText, 'link'),
|
link: extractAtomLink(channelMatch ? channelMatch[0] : (feedMatch ? feedMatch[0] : channelText)),
|
||||||
description: extractTag(channelText, 'description'),
|
description: extractTag(channelText, 'description') || extractTag(channelText, 'subtitle'),
|
||||||
item: items,
|
item: items,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -137,6 +178,12 @@ function normalizeRssItem(item) {
|
|||||||
const out = { ...item };
|
const out = { ...item };
|
||||||
if (out.title) out.title = stripHtml(out.title);
|
if (out.title) out.title = stripHtml(out.title);
|
||||||
if (out.pubDate) out.pubDate = stripHtml(out.pubDate);
|
if (out.pubDate) out.pubDate = stripHtml(out.pubDate);
|
||||||
|
if (!out.pubDate) {
|
||||||
|
if (out.published) out.pubDate = stripHtml(out.published);
|
||||||
|
else if (out.updated) out.pubDate = stripHtml(out.updated);
|
||||||
|
}
|
||||||
|
if (out.published) out.published = stripHtml(out.published);
|
||||||
|
if (out.updated) out.updated = stripHtml(out.updated);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -148,7 +195,11 @@ async function fetchRssWithTimeout(url, timeoutMs) {
|
|||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { signal: ctrl.signal });
|
const resp = await fetch(url, { signal: ctrl.signal });
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) {
|
||||||
|
const err = new Error(`HTTP ${resp.status}`);
|
||||||
|
err.status = resp.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
return parseRss(text);
|
return parseRss(text);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -156,6 +207,250 @@ async function fetchRssWithTimeout(url, timeoutMs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function gatherRssItems(urls, path, dataCache, timeoutMs) {
|
||||||
|
const items = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
if (!url) continue;
|
||||||
|
let cached = dataCache.get(`rss:${url}`);
|
||||||
|
if (!cached) {
|
||||||
|
cached = await fetchRssWithTimeout(url, timeoutMs);
|
||||||
|
dataCache.set(`rss:${url}`, cached);
|
||||||
|
}
|
||||||
|
const rssPath = path || 'rss.channel.item';
|
||||||
|
const pathVal = getValueAtPath(cached, rssPath);
|
||||||
|
if (Array.isArray(pathVal)) {
|
||||||
|
for (const entry of pathVal) {
|
||||||
|
const norm = normalizeRssItem(entry);
|
||||||
|
if (norm) items.push(norm);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const norm = normalizeRssItem(pathVal);
|
||||||
|
if (norm) items.push(norm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aggregateRssItems(items, aggregate) {
|
||||||
|
if (aggregate === 'count') {
|
||||||
|
return items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aggregate === 'latest_title' || aggregate === 'latest_pubdate') {
|
||||||
|
let latest = null;
|
||||||
|
for (const entry of items) {
|
||||||
|
const date = entry && entry.pubDate ? new Date(entry.pubDate) : null;
|
||||||
|
const timestamp = date && !Number.isNaN(date.getTime()) ? date.getTime() : null;
|
||||||
|
if (!latest || (timestamp !== null && timestamp > latest.timestamp)) {
|
||||||
|
latest = { entry, timestamp };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!latest) return undefined;
|
||||||
|
if (aggregate === 'latest_title') {
|
||||||
|
return latest.entry.title || undefined;
|
||||||
|
}
|
||||||
|
if (latest.timestamp) {
|
||||||
|
return new Date(latest.timestamp).toISOString();
|
||||||
|
}
|
||||||
|
return latest.entry.pubDate || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map(entry => entry.title).filter(Boolean).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFirstCommitMessageFromDescription(description) {
|
||||||
|
if (!description || typeof description !== 'string') return null;
|
||||||
|
const liMatch = description.match(/<li[^>]*>([\s\S]*?)<\/li>/i);
|
||||||
|
if (!liMatch) return null;
|
||||||
|
const liContent = liMatch[1];
|
||||||
|
const anchorMatch = liContent.match(/<a[^>]*>([\s\S]*?)<\/a>/i);
|
||||||
|
const text = anchorMatch ? anchorMatch[1] : liContent;
|
||||||
|
const cleaned = stripHtml(text).replace(/\s+/g, ' ').trim();
|
||||||
|
return cleaned || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCommitInfo(prefix, branch, commit) {
|
||||||
|
if (!commit) return null;
|
||||||
|
const message = (commit.summary || commit.message || '').trim();
|
||||||
|
const title = message || prefix;
|
||||||
|
let location = prefix || '';
|
||||||
|
if (branch) {
|
||||||
|
location = location ? `${location} (${branch})` : branch;
|
||||||
|
}
|
||||||
|
const detailParts = [];
|
||||||
|
if (location) detailParts.push(location);
|
||||||
|
if (commit.author) detailParts.push(`par ${commit.author}`);
|
||||||
|
const details = detailParts.join(' — ');
|
||||||
|
return { title, details };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeGiteaFallbackAggregate(items, aggregate) {
|
||||||
|
if (aggregate === 'count') return items.length;
|
||||||
|
|
||||||
|
let latest = null;
|
||||||
|
for (const entry of items) {
|
||||||
|
if (!entry) continue;
|
||||||
|
const date = entry.pubDate ? new Date(entry.pubDate) : null;
|
||||||
|
const timestamp = date && !Number.isNaN(date.getTime()) ? date.getTime() : null;
|
||||||
|
if (!latest || (timestamp !== null && timestamp > latest.timestamp)) {
|
||||||
|
latest = { entry, timestamp };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latest) return undefined;
|
||||||
|
|
||||||
|
if (aggregate === 'latest_pubdate') {
|
||||||
|
if (latest.timestamp) {
|
||||||
|
return new Date(latest.timestamp).toISOString();
|
||||||
|
}
|
||||||
|
return latest.entry.pubDate || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aggregate === 'latest_title') {
|
||||||
|
const title = stripHtml(latest.entry.title || '');
|
||||||
|
const message = extractFirstCommitMessageFromDescription(latest.entry.description) || '';
|
||||||
|
if (title || message) {
|
||||||
|
const primary = message || title;
|
||||||
|
const secondary = title && title !== primary ? title : (message && message !== primary ? message : '');
|
||||||
|
const info = { title: primary };
|
||||||
|
if (secondary) info.details = secondary;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map(entry => entry.title).filter(Boolean).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGiteaRepos(repos, defaultOwner) {
|
||||||
|
if (!repos) return [];
|
||||||
|
const list = Array.isArray(repos) ? repos : [repos];
|
||||||
|
const out = [];
|
||||||
|
for (const entry of list) {
|
||||||
|
if (!entry) continue;
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
const trimmed = entry.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
if (trimmed.includes('/')) {
|
||||||
|
const [owner, repo] = trimmed.split('/', 2);
|
||||||
|
if (owner && repo) {
|
||||||
|
out.push({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch: null,
|
||||||
|
displayName: `${owner}/${repo}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (defaultOwner) {
|
||||||
|
out.push({
|
||||||
|
owner: defaultOwner,
|
||||||
|
repo: trimmed,
|
||||||
|
branch: null,
|
||||||
|
displayName: `${defaultOwner}/${trimmed}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (typeof entry === 'object') {
|
||||||
|
const owner = entry.owner || defaultOwner;
|
||||||
|
const repo = entry.repo;
|
||||||
|
if (!owner || !repo) continue;
|
||||||
|
out.push({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch: entry.branch || null,
|
||||||
|
displayName: entry.display_name || entry.displayName || `${owner}/${repo}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveGiteaToken(info) {
|
||||||
|
if (info && typeof info === 'object') {
|
||||||
|
if (info.token) return info.token;
|
||||||
|
if (info.token_env && process.env[info.token_env]) return process.env[info.token_env];
|
||||||
|
}
|
||||||
|
return process.env.GITEA_TOKEN || process.env.GITEA_ACCESS_TOKEN || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseGiteaFallback(error) {
|
||||||
|
const status = error && typeof error === 'object' ? error.status : null;
|
||||||
|
return status === 401 || status === 403 || status === 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickGiteaCommitTimestamp(commit) {
|
||||||
|
const candidates = [
|
||||||
|
commit?.commit?.author?.date,
|
||||||
|
commit?.commit?.committer?.date,
|
||||||
|
commit?.author?.date,
|
||||||
|
commit?.committer?.date,
|
||||||
|
];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (!candidate) continue;
|
||||||
|
const ts = Date.parse(candidate);
|
||||||
|
if (!Number.isNaN(ts)) return ts;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGiteaRepoSummary({ baseUrl, owner, repo, branch, token, timeoutMs }) {
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const url = new URL(`/api/v1/repos/${owner}/${repo}/commits`, baseUrl);
|
||||||
|
url.searchParams.set('limit', '1');
|
||||||
|
if (branch) url.searchParams.set('sha', branch);
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
if (token) headers.Authorization = `token ${token}`;
|
||||||
|
|
||||||
|
const resp = await fetch(url, { signal: ctrl.signal, headers });
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = new Error(`HTTP ${resp.status}`);
|
||||||
|
err.status = resp.status;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await resp.text();
|
||||||
|
let commits = [];
|
||||||
|
if (text.trim()) {
|
||||||
|
try {
|
||||||
|
commits = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
commits = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalHeader = resp.headers.get('x-total-count');
|
||||||
|
const totalCommits = totalHeader && /^\d+$/.test(totalHeader) ? Number(totalHeader) : null;
|
||||||
|
|
||||||
|
const latestCommitRaw = Array.isArray(commits) && commits.length ? commits[0] : null;
|
||||||
|
const latestCommit = latestCommitRaw
|
||||||
|
? {
|
||||||
|
sha: latestCommitRaw.sha || latestCommitRaw.id || null,
|
||||||
|
message: latestCommitRaw.commit?.message || latestCommitRaw.message || '',
|
||||||
|
summary: (() => {
|
||||||
|
const msg = latestCommitRaw.commit?.message || latestCommitRaw.message || '';
|
||||||
|
return msg ? msg.split(/\r?\n/)[0].trim() : '';
|
||||||
|
})(),
|
||||||
|
author:
|
||||||
|
latestCommitRaw.commit?.author?.name ||
|
||||||
|
latestCommitRaw.author?.login ||
|
||||||
|
latestCommitRaw.author?.username ||
|
||||||
|
latestCommitRaw.author?.name ||
|
||||||
|
latestCommitRaw.commit?.committer?.name ||
|
||||||
|
null,
|
||||||
|
url: latestCommitRaw.html_url || latestCommitRaw.url || latestCommitRaw.commit?.url || null,
|
||||||
|
timestamp: pickGiteaCommitTimestamp(latestCommitRaw),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return { totalCommits, latestCommit };
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyInfoTransform(info, value) {
|
function applyInfoTransform(info, value) {
|
||||||
if (value === undefined || value === null) return value;
|
if (value === undefined || value === null) return value;
|
||||||
if (!info || typeof info !== 'object') return value;
|
if (!info || typeof info !== 'object') return value;
|
||||||
@@ -232,51 +527,93 @@ async function collectExtras(svc, context) {
|
|||||||
case 'rss_multi': {
|
case 'rss_multi': {
|
||||||
const urls = Array.isArray(info.urls) && info.urls.length ? info.urls : (info.url ? [info.url] : []);
|
const urls = Array.isArray(info.urls) && info.urls.length ? info.urls : (info.url ? [info.url] : []);
|
||||||
if (!urls.length) throw new Error('URLs manquantes pour rss_multi');
|
if (!urls.length) throw new Error('URLs manquantes pour rss_multi');
|
||||||
const items = [];
|
const items = await gatherRssItems(urls, info.path, dataCache, context.timeoutMs);
|
||||||
for (const url of urls) {
|
value = aggregateRssItems(items, info.aggregate);
|
||||||
let cached = dataCache.get(`rss:${url}`);
|
break;
|
||||||
|
}
|
||||||
|
case 'gitea_multi': {
|
||||||
|
const baseUrlRaw = info.base_url || svc.health_url || '';
|
||||||
|
if (!baseUrlRaw) throw new Error('URL manquante pour gitea_multi');
|
||||||
|
const baseUrl = baseUrlRaw.replace(/\/+$/, '');
|
||||||
|
const repoEntries = normalizeGiteaRepos(info.repos, info.owner);
|
||||||
|
if (!repoEntries.length) throw new Error('Dépôts manquants pour gitea_multi');
|
||||||
|
|
||||||
|
const authToken = resolveGiteaToken(info);
|
||||||
|
const summaries = [];
|
||||||
|
let fetchError = null;
|
||||||
|
for (const repoEntry of repoEntries) {
|
||||||
|
const cacheKey = `gitea:${baseUrl}:${repoEntry.owner}/${repoEntry.repo}:${repoEntry.branch || ''}`;
|
||||||
|
let cached = dataCache.get(cacheKey);
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
cached = await fetchRssWithTimeout(url, context.timeoutMs);
|
try {
|
||||||
dataCache.set(`rss:${url}`, cached);
|
cached = await fetchGiteaRepoSummary({
|
||||||
|
baseUrl,
|
||||||
|
owner: repoEntry.owner,
|
||||||
|
repo: repoEntry.repo,
|
||||||
|
branch: repoEntry.branch,
|
||||||
|
token: authToken,
|
||||||
|
timeoutMs: context.timeoutMs,
|
||||||
|
});
|
||||||
|
dataCache.set(cacheKey, cached);
|
||||||
|
} catch (err) {
|
||||||
|
fetchError = err;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
const rssPath = info.path || 'rss.channel.item';
|
|
||||||
const pathVal = getValueAtPath(cached, rssPath);
|
|
||||||
if (Array.isArray(pathVal)) {
|
|
||||||
for (const entry of pathVal) {
|
|
||||||
const norm = normalizeRssItem(entry);
|
|
||||||
if (norm) items.push(norm);
|
|
||||||
}
|
}
|
||||||
} else {
|
summaries.push({ repo: repoEntry, summary: cached });
|
||||||
const norm = normalizeRssItem(pathVal);
|
|
||||||
if (norm) items.push(norm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fetchError) {
|
||||||
|
const fallbackUrls = Array.isArray(info.fallback_urls) ? info.fallback_urls.filter(Boolean) : [];
|
||||||
|
if (fallbackUrls.length && shouldUseGiteaFallback(fetchError)) {
|
||||||
|
const items = await gatherRssItems(fallbackUrls, info.fallback_path || info.path, dataCache, context.timeoutMs);
|
||||||
|
value = computeGiteaFallbackAggregate(items, info.aggregate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw fetchError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.aggregate === 'count') {
|
if (info.aggregate === 'count') {
|
||||||
value = items.length;
|
let total = 0;
|
||||||
|
for (const { summary } of summaries) {
|
||||||
|
if (summary.totalCommits !== null && summary.totalCommits !== undefined) {
|
||||||
|
total += summary.totalCommits;
|
||||||
|
} else {
|
||||||
|
total = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value = total !== null ? total : undefined;
|
||||||
} else if (info.aggregate === 'latest_title' || info.aggregate === 'latest_pubdate') {
|
} else if (info.aggregate === 'latest_title' || info.aggregate === 'latest_pubdate') {
|
||||||
let latest = null;
|
let latest = null;
|
||||||
for (const entry of items) {
|
for (const { repo, summary } of summaries) {
|
||||||
const date = entry.pubDate ? new Date(entry.pubDate) : null;
|
const commit = summary.latestCommit;
|
||||||
const timestamp = date && !Number.isNaN(date.getTime()) ? date.getTime() : null;
|
if (!commit) continue;
|
||||||
if (!latest || (timestamp !== null && timestamp > latest.timestamp)) {
|
const timestamp = commit.timestamp;
|
||||||
latest = { entry, timestamp };
|
if (timestamp === null || timestamp === undefined) continue;
|
||||||
|
if (!latest || timestamp > latest.timestamp) {
|
||||||
|
latest = { repo, commit, timestamp };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
value = undefined;
|
value = undefined;
|
||||||
} else if (info.aggregate === 'latest_title') {
|
} else if (info.aggregate === 'latest_title') {
|
||||||
value = latest.entry.title || undefined;
|
const prefix = latest.repo.displayName || `${latest.repo.owner}/${latest.repo.repo}`;
|
||||||
|
value = buildCommitInfo(prefix, latest.repo.branch || null, latest.commit);
|
||||||
} else {
|
} else {
|
||||||
if (latest.timestamp) {
|
value = new Date(latest.timestamp).toISOString();
|
||||||
const unixSeconds = Math.floor(latest.timestamp / 1000);
|
|
||||||
value = `<t:${unixSeconds}:f>`;
|
|
||||||
} else {
|
|
||||||
value = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = items.map(entry => entry.title).filter(Boolean).join(', ');
|
value = summaries
|
||||||
|
.map(({ repo, summary }) => {
|
||||||
|
const message = summary.latestCommit?.message;
|
||||||
|
if (!message) return null;
|
||||||
|
const firstLine = stripHtml(message.split(/\r?\n/)[0]);
|
||||||
|
const prefix = repo.displayName || `${repo.owner}/${repo.repo}`;
|
||||||
|
return `${prefix}: ${firstLine}`;
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(', ');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -314,8 +651,15 @@ const metricNameOverrides = {
|
|||||||
ping_ms: 'Ping',
|
ping_ms: 'Ping',
|
||||||
server_ping_ms: 'Ping',
|
server_ping_ms: 'Ping',
|
||||||
mare_ping_ms: 'Ping',
|
mare_ping_ms: 'Ping',
|
||||||
|
mare_authorized_connections_peak: 'Pic de connexions',
|
||||||
|
mare_authorized_connections_highwater: 'Pic de connexions',
|
||||||
|
mare_authorized_connections_max: 'Pic de connexions',
|
||||||
|
mare_authorized_connections_record: 'Pic de connexions',
|
||||||
|
_peak_authorized_connections: 'Pic de connexions',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connectionPeaks = new Map();
|
||||||
|
|
||||||
function pickKeyMetrics(name, all) {
|
function pickKeyMetrics(name, all) {
|
||||||
const perService = {
|
const perService = {
|
||||||
'Serveur de synchronisation': [
|
'Serveur de synchronisation': [
|
||||||
@@ -350,7 +694,41 @@ function choosePingMetric(allMetrics) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMetrics(name, allMetrics, metricsByService) {
|
function asFiniteNumber(value) {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const num = Number(trimmed);
|
||||||
|
if (Number.isFinite(num)) return num;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickPeakMetric(primary, fallback) {
|
||||||
|
const candidates = [
|
||||||
|
'mare_authorized_connections_peak',
|
||||||
|
'mare_authorized_connections_highwater',
|
||||||
|
'mare_authorized_connections_max',
|
||||||
|
'mare_authorized_connections_record',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of candidates) {
|
||||||
|
if (primary && Object.prototype.hasOwnProperty.call(primary, key)) {
|
||||||
|
const val = asFiniteNumber(primary[key]);
|
||||||
|
if (val !== null) return { key, value: val };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of candidates) {
|
||||||
|
if (fallback && Object.prototype.hasOwnProperty.call(fallback, key)) {
|
||||||
|
const val = asFiniteNumber(fallback[key]);
|
||||||
|
if (val !== null) return { key, value: val };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMetrics(name, allMetrics, metricsByService, peakStore = null) {
|
||||||
const selected = pickKeyMetrics(name, allMetrics);
|
const selected = pickKeyMetrics(name, allMetrics);
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const skipKeys = new Set(['ping_ms', 'server_ping_ms', 'mare_ping_ms']);
|
const skipKeys = new Set(['ping_ms', 'server_ping_ms', 'mare_ping_ms']);
|
||||||
@@ -359,16 +737,37 @@ function formatMetrics(name, allMetrics, metricsByService) {
|
|||||||
const fallbackMetrics = fallbackName ? (metricsByService[fallbackName] || {}) : {};
|
const fallbackMetrics = fallbackName ? (metricsByService[fallbackName] || {}) : {};
|
||||||
|
|
||||||
if (name === "Serveur d'identification") {
|
if (name === "Serveur d'identification") {
|
||||||
const usersConnected = allMetrics.mare_authorized_connections ?? fallbackMetrics.mare_authorized_connections;
|
const usersConnectedRaw = allMetrics.mare_authorized_connections ?? fallbackMetrics.mare_authorized_connections;
|
||||||
const usersRegistered = allMetrics.mare_users_registered ?? fallbackMetrics.mare_users_registered;
|
const usersRegisteredRaw = allMetrics.mare_users_registered ?? fallbackMetrics.mare_users_registered;
|
||||||
|
|
||||||
|
entries.push([
|
||||||
|
'mare_users_registered',
|
||||||
|
usersRegisteredRaw !== undefined ? usersRegisteredRaw : 'N/A',
|
||||||
|
]);
|
||||||
|
|
||||||
|
let peakEntry = pickPeakMetric(allMetrics, fallbackMetrics);
|
||||||
|
|
||||||
|
if (peakStore) {
|
||||||
|
if (peakEntry) {
|
||||||
|
peakStore.set(name, peakEntry.value);
|
||||||
|
} else {
|
||||||
|
const numericConnected = asFiniteNumber(usersConnectedRaw);
|
||||||
|
if (numericConnected !== null) {
|
||||||
|
const prev = peakStore.get(name);
|
||||||
|
const nextPeak = prev !== undefined ? Math.max(prev, numericConnected) : numericConnected;
|
||||||
|
peakStore.set(name, nextPeak);
|
||||||
|
peakEntry = { key: '_peak_authorized_connections', value: nextPeak };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peakEntry) {
|
||||||
|
entries.push([peakEntry.key, peakEntry.value]);
|
||||||
|
}
|
||||||
|
|
||||||
entries.push([
|
entries.push([
|
||||||
'mare_authorized_connections',
|
'mare_authorized_connections',
|
||||||
usersConnected !== undefined ? usersConnected : 'N/A',
|
usersConnectedRaw !== undefined ? usersConnectedRaw : 'N/A',
|
||||||
]);
|
|
||||||
entries.push([
|
|
||||||
'mare_users_registered',
|
|
||||||
usersRegistered !== undefined ? usersRegistered : 'N/A',
|
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
for (const key of Object.keys(selected)) {
|
for (const key of Object.keys(selected)) {
|
||||||
@@ -427,7 +826,22 @@ function formatExtraValue(value) {
|
|||||||
return value.map(v => formatExtraValue(v)).join(', ');
|
return value.map(v => formatExtraValue(v)).join(', ');
|
||||||
}
|
}
|
||||||
if (value && typeof value === 'object') {
|
if (value && typeof value === 'object') {
|
||||||
return Object.values(value).map(v => formatExtraValue(v)).join(', ');
|
const hasTitle = Object.prototype.hasOwnProperty.call(value, 'title');
|
||||||
|
const hasDetails = Object.prototype.hasOwnProperty.call(value, 'details');
|
||||||
|
if (hasTitle || hasDetails) {
|
||||||
|
const parts = [];
|
||||||
|
if (hasTitle && value.title !== undefined && value.title !== null && value.title !== '') {
|
||||||
|
parts.push(formatExtraValue(value.title));
|
||||||
|
}
|
||||||
|
if (hasDetails && value.details !== undefined && value.details !== null && value.details !== '') {
|
||||||
|
parts.push(formatExtraValue(value.details));
|
||||||
|
}
|
||||||
|
return parts.join(' — ');
|
||||||
|
}
|
||||||
|
return Object.values(value)
|
||||||
|
.filter(v => v !== undefined && v !== null && v !== '')
|
||||||
|
.map(v => formatExtraValue(v))
|
||||||
|
.join(', ');
|
||||||
}
|
}
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
value = stripHtml(value);
|
value = stripHtml(value);
|
||||||
@@ -576,7 +990,7 @@ function buildEmbed(results) {
|
|||||||
|
|
||||||
const m = { ...(status.metrics || {}) };
|
const m = { ...(status.metrics || {}) };
|
||||||
if (typeof status.ping_ms === 'number') m.ping_ms = status.ping_ms;
|
if (typeof status.ping_ms === 'number') m.ping_ms = status.ping_ms;
|
||||||
const entries = formatMetrics(name, m, metricsByService).slice(0, 6);
|
const entries = formatMetrics(name, m, metricsByService, connectionPeaks).slice(0, 6);
|
||||||
if (entries.length) {
|
if (entries.length) {
|
||||||
const pretty = entries.map(([k, v]) => `- ${prettyMetricName(k)}: ${prettyMetricValue(k, v)}`).join('\n');
|
const pretty = entries.map(([k, v]) => `- ${prettyMetricName(k)}: ${prettyMetricValue(k, v)}`).join('\n');
|
||||||
value += `\n${pretty}`;
|
value += `\n${pretty}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user