diff --git a/package-lock.json b/package-lock.json index 4962e01e8..64af9b2f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,8 @@ "typescript-transform-paths": "^3.3.1", "uuid": "^8.3.2", "webpack": "^5.73.0", - "webpack-cli": "^4.10.0" + "webpack-cli": "^4.10.0", + "yaml": "^2.1.1" }, "devDependencies": { "@types/chai": "^4.3.1", @@ -353,6 +354,16 @@ "node": ">=8" } }, + "node_modules/@commitlint/load/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@commitlint/resolve-extends": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.0.3.tgz", @@ -3457,6 +3468,24 @@ "node": ">=10" } }, + "node_modules/cosmiconfig-typescript-loader/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -6620,15 +6649,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", - "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/listr2": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", @@ -10879,11 +10899,11 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -11149,6 +11169,13 @@ "requires": { "has-flag": "^4.0.0" } + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "optional": true } } }, @@ -13520,6 +13547,13 @@ "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.7.2" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "cosmiconfig-typescript-loader": { @@ -13546,6 +13580,13 @@ "path-type": "^4.0.0", "yaml": "^1.10.0" } + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "optional": true } } }, @@ -15814,12 +15855,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true - }, - "yaml": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", - "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==", - "dev": true } } }, @@ -18974,9 +19009,9 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz", + "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==" }, "yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index f26c0ef70..b96624797 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,8 @@ "typescript-transform-paths": "^3.3.1", "uuid": "^8.3.2", "webpack": "^5.73.0", - "webpack-cli": "^4.10.0" + "webpack-cli": "^4.10.0", + "yaml": "^2.1.1" }, "devDependencies": { "@types/chai": "^4.3.1", diff --git a/src/config.ts b/src/config.ts index 86c8a78ab..e2642f12e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,8 @@ import { config } from 'dotenv'; config(); +import { buildMattermostConfig } from "./lib/mattermost/config" + const isSecure = (process.env.SECURE || 'true') === 'true'; const userStatusOptions = [ @@ -39,16 +41,17 @@ export default { newsletterHashSecret: process.env.NEWSLETTER_HASH_SECRET, newsletterSendTime: process.env.NEWSLETTER_SEND_TIME, mattermostURL: process.env.MATTERMOST_URL || 'https://mattermost.incubateur.net', + mattermost: buildMattermostConfig(), senderEmail: process.env.MAIL_SENDER || 'espace-membre@incubateur.net', slackWebhookURLSecretariat: process.env.SLACK_WEBHOOK_URL_SECRETARIAT, slackWebhookURLGeneral: process.env.SLACK_WEBHOOK_URL_GENERAL, usersAPI: - process.env.USERS_API || 'https://beta.gouv.fr/api/v2.3/authors.json', - incubatorAPI: process.env.INCUBATOR_API || 'https://beta.gouv.fr/api/v2.3/incubators.json', + process.env.USERS_API || 'https://beta.gouv.fr/api/v2.5/authors.json', + incubatorAPI: process.env.INCUBATOR_API || 'https://beta.gouv.fr/api/v2.5/incubators.json', startupsAPI: - process.env.STARTUPS_API || 'https://beta.gouv.fr/api/v2/startups.json', + process.env.STARTUPS_API || 'https://beta.gouv.fr/api/v2.5/startups.json', startupsDetailsAPI: - process.env.STARTUPS_DETAILS_API || 'https://beta.gouv.fr/api/v2.3/startups_details.json', + process.env.STARTUPS_DETAILS_API || 'https://beta.gouv.fr/api/v2.5/startups_details.json', githubToken: process.env.GITHUB_TOKEN, githubOrganizationName: process.env.GITHUB_ORGANIZATION_NAME || 'betagouv', githubOrgAdminToken: process.env.GITHUB_ORG_ADMIN_TOKEN, diff --git a/src/controllers/budgetController.ts b/src/controllers/budgetController.ts new file mode 100644 index 000000000..616cc363c --- /dev/null +++ b/src/controllers/budgetController.ts @@ -0,0 +1,227 @@ +import axios from "axios"; + +import config from "../config"; +import betagouv from "../betagouv"; +import * as utils from "./utils"; + +import { applyChanges } from "../lib/frontmatter" + +const mattermost = config.mattermost +const serverKeys = Object.keys(mattermost.servers) + +export function determineMattermostServer(req, res, next) { + console.log(JSON.stringify({...req.body, trigger_id: null, token: null, context: {...req.body.?context, token: null}})) + if (!req.body?.response_url?.length && !req.body?.context?.response_url?.length) { + return res.json({ + text: "Il n’y a ni _response_url_ ni _context._response_url_ dans la requête. Impossible d’en déterminer l’origine. Abandon…" + }) + } + + const mattermostUrl = new URL(req.body?.response_url || req.body?.context?.response_url) + req.mattermostServerId = serverKeys.find(k => { + return mattermost.servers[k].startsWith(mattermostUrl.origin) + }) + if (!req.mattermostServerId) { + return res.json({ + text: `${mattermostUrl.origin} n’est pas un serveur Mattermost connu. Abandon…` + }) + } + next() +} + +export function checkToken(req, res, next) { + req.payload = {...req.body?.context, ...req.body} + + if (!req.payload.token) { + return res.json({ + text: "Aucun token pour valider la requête. Abandon…" + }) + } + if (!mattermost.hooks[req.mattermostServerId][req.payload.team_domain]?.token) { + return res.json({ + text: `Aucun token associé à '${req.payload.team_domain}' team_domain de la requête. Abandon…` + }) + } + if (req.payload.token != mattermost.hooks[req.mattermostServerId][req.payload.team_domain].token) { + return res.json({ + text: "Le token contenu dans la requête ne correspond pas à celui connu. Abandon…" + }) + } + next() +} + +function buildBudgetURL(startup, budget) { + const startDts = startup.attributes?.phases?.map(p => p?.start).filter(p => p) || [] + startDts.sort() + + const url = new URL('https://beta-gouv-fr-budget.netlify.app') + url.searchParams.append('budget', budget) + const date = new Date() + url.searchParams.append('date', date.toISOString().slice(0,10)) + if (startDts.length) { + url.searchParams.append('start', startDts[0]) + } + url.searchParams.append('startup', startup.attributes.name) + url.searchParams.append('startupId', startup.id) + return url.toString() +} + +export function manageInteractiveActionCall(req, res, next) { + if (!req.payload.command) { + if (req.payload.action === "publish") { + axios.post(req.payload.response_url, { + text: `@${req.payload.user_name} vient de demander la publication d’une page budget :smiley: :tada: + C’est celle-là : ${req.payload.budget_url} + On fait quelques vérifications, on appelle GitHub et on revient vers vous !`, + response_type: 'in_channel', + }) + + req.budget_url = req.payload.budget_url + req.startupId = req.payload.startup + return next() + } else if (req.payload.action === "custom") { + return axios.post(req.payload.response_url, { + text: `@${req.payload.user_name} pour publier une page de budget personnalisée, vous pouvez utiliser la commande \`/budget url ${req.payload.startup} [url]\` !`, + response_type: 'in_channel', + }) + } else { + return axios.post(req.payload.response_url, { + text: `BUG!`, + response_type: 'in_channel', + }) + } + } + next() +} + +export async function manageSlashCommand(req, res, next) { + if (req.startupId) { + return next() + } + if (!req.payload.channel_name) { + return res.json({ + "text": "La requête ne contient pas d’informations sur la chaîne Mattermost. Abandon…" + }) + } + + const [subcommand, startupId, details] = req.payload.text.split(" ")?.filter(e => e.length) || [] + if (["page", "url"].indexOf(subcommand) < 0) { + return res.json({ + "text": `On ne sait pas quoi faire avec la commande \`${req.payload.command} ${req.payload.text}\`. Abandon…` + }) + } + + const startups = await betagouv.startupsInfos() + const startup = startups.find(s => s.id == startupId) + if (!startup) { + return axios.post(req.body.response_url, { + text: `Aucune startup ne correspond à l’identifiant '${startupId}'`, + response_type: 'in_channel', + }) + } + req.startup = startup + req.startupId = startup.id + if (subcommand === "page") { + res.json({ + text: `@${req.payload.user_name} vient de demander la génération d’une page budget :smiley: avec la commande \`${req.payload.command} ${req.payload.text}\` :tada: + On fait quelques vérifications et on revient vers vous !`, + response_type: 'in_channel', + }) + + req.budget_url = buildBudgetURL(startup, details) + + const response_payload = { + text: `@${req.payload.user_name}, voilà la page budget demandée :smiley: avec la commande \`${req.payload.command} ${req.payload.text}\` :tada: +${req.budget_url} +Si vous avez des questions ou des problèmes, n’hésitez pas à rejoindre le canal [~domaine-transparence-budget](https://mattermost.incubateur.net/betagouv/channels/domaine-transparence-budget) :smiley: +`, + response_type: 'in_channel', + attachments: [{ + actions: [{ + id: "publish", + name: "Publier cette première version", + integration: { + url: `${config.protocol}://${config.host}/notifications/budget`, + context: { + action: "publish", + budget_url: req.budget_url, + response_url: req.body.response_url, + startup: startupId, + token: req.body.token, + } + } + }, { + id: "custom", + name: "Publier une autre page", + integration: { + url: `${config.protocol}://${config.host}/notifications/budget`, + context: { + action: "custom", + response_url: req.body.response_url, + startup: startupId, + token: req.body.token, + } + } + }] + }] + } + console.log(JSON.stringify(response_payload)) + return axios.post(req.body.response_url, response_payload) + + } else if (subcommand === "url") { + res.json({ + text: `@${req.payload.user_name} vient de demander la publication d’une page budget :smiley: avec la commande \`${req.payload.command} ${subcommand} ${startupId} [url]\` :tada: + C’est celle-là : ${details} + On fait quelques vérifications, on appelle GitHub et on revient vers vous !`, + response_type: 'in_channel', + }) + + req.budget_url = details + req.channel_url = `${mattermost.servers[req.mattermostServerId]}/${req.payload.team_domain}/channels/${req.payload.channel_name}` + next() + } +} + +async function addStartupProps(startupId, props, res) { + const propNames = Object.keys(props) + propNames.sort() + const branch = utils.createBranchName('startup-', startupId, `-${propNames.join(',')}`); + const path = `content/_startups/${startupId}.md`; + return utils.getGithubMasterSha() + .then((response) => { + const { sha } = response.data.object; + return utils.createGithubBranch(sha, branch); + }) + .then(() => utils.getGithubFile(path, branch)) + .then((res) => { + const text = Buffer.from(res.data.content, 'base64').toString('utf-8') + const {content, updates} = applyChanges(text, props) + + return utils.createGithubFile(path, branch, content, res.data.sha).then(() => updates) + }) + .then((updates) => { + return utils.makeGithubPullRequest(branch, `Mise à jour de la fiche de ${startupId}`) + .then((pullRequest) => { + return { updates, pullRequest } + }) + }) + .catch(err => { + console.error(err) + throw err + }) +} + +export async function createPullRequest(req, res, next) { + const data = { + budget_url: req.budget_url, + //channel_url: req.channel_url, + } + const result = await addStartupProps(req.startupId, data, res) + return axios.post(req.payload.response_url, { + text: `@${req.payload.user_name}, la page budget a été ajoutée à la fiche GitHub :tada: +Maintenant il faut valider la _pull request_ : ${result.pullRequest.data.html_url} +Si vous avez des questions ou des problèmes, n’hésitez pas à rejoindre le canal [~domaine-transparence-budget](https://mattermost.incubateur.net/betagouv/channels/domaine-transparence-budget) :smiley: +`, + response_type: 'in_channel', + }) +} diff --git a/src/controllers/onboardingController.ts b/src/controllers/onboardingController.ts index 49aa2cd42..a1f702869 100644 --- a/src/controllers/onboardingController.ts +++ b/src/controllers/onboardingController.ts @@ -10,12 +10,6 @@ import { renderHtmlFromMd } from "../lib/mdtohtml"; import * as mattermost from '../lib/mattermost'; import { fetchCommuneDetails } from "../lib/searchCommune"; -function createBranchName(username) { - const refRegex = /( |\.|\\|~|^|:|\?|\*|\[)/gm; - const randomSuffix = crypto.randomBytes(3).toString('hex'); - return `author${username.replace(refRegex, '-')}-${randomSuffix}`; -} - interface IMessageInfo { prInfo, referent: string, @@ -51,7 +45,7 @@ async function sendMessageToReferent({ prInfo, referent, username, isEmailBetaAs } async function createNewcomerGithubFile(username, content, referent) { - const branch = createBranchName(username); + const branch = utils.createBranchName("author-",username, "-create"); console.log(`Début de la création de fiche pour ${username}...`); const prInfo = await utils.getGithubMasterSha() diff --git a/src/controllers/usersController.ts b/src/controllers/usersController.ts index 31e37e87d..d318bc8d9 100644 --- a/src/controllers/usersController.ts +++ b/src/controllers/usersController.ts @@ -469,14 +469,8 @@ export async function manageSecondaryEmailForUser(req, res) { } } -function createBranchName(username) { - const refRegex = /( |\.|\\|~|^|:|\?|\*|\[)/gm; - const randomSuffix = crypto.randomBytes(3).toString('hex'); - return `author${username.replace(refRegex, '-')}-update-end-date-${randomSuffix}`; -} - async function updateAuthorGithubFile(username, changes) { - const branch = createBranchName(username); + const branch = utils.createBranchName('author-', username, '-update-end-date'); const path = `content/_authors/${username}.md`; console.log(`Début de la mise à jour de la fiche pour ${username}...`); diff --git a/src/controllers/utils.ts b/src/controllers/utils.ts index b7711e591..4f2166c2c 100644 --- a/src/controllers/utils.ts +++ b/src/controllers/utils.ts @@ -11,6 +11,12 @@ export const computeHash = function(username) { return hash.update(username).digest('hex'); } +export function createBranchName(safePrefix, name, safeSuffix) { + const refRegex = /( |\.|\\|~|^|:|\?|\*|\[)/gm; + const randomSuffix = crypto.randomBytes(3).toString('hex'); + return `${safePrefix}${name.replace(refRegex, '-')}${safeSuffix}-${randomSuffix}`; +} + const mailTransport = nodemailer.createTransport({ debug: process.env.MAIL_DEBUG === 'true', service: process.env.MAIL_SERVICE ? process.env.MAIL_SERVICE : null, diff --git a/src/index.ts b/src/index.ts index 165e09903..c4c01ffaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import * as resourceController from './controllers/resourceController'; import * as startupController from './controllers/startupController'; import * as usersController from './controllers/usersController'; import * as mapController from './controllers/mapController'; +import * as budgetController from './controllers/budgetController'; import * as sentry from './lib/sentry'; const app = express(); @@ -60,6 +61,7 @@ app.use(session({ cookie: { maxAge: 300000, sameSite: 'lax' } })); // Only used app.use(flash()); app.use(expressSanitizer()); app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.json()); const getJwtTokenForUser = (id) => jwt.sign({ id }, config.secret, { expiresIn: '7 days' }); @@ -76,6 +78,7 @@ app.use( '/signin', '/marrainage/accept', '/marrainage/decline', + '/notifications/budget', '/notifications/github', '/onboarding', /onboardingSuccess\/*/, @@ -132,10 +135,21 @@ app.post( usersController.managePrimaryEmailForUser ); app.post('/users/:username/end-date', usersController.updateEndDateForUser); + app.post( '/notifications/github', githubNotificationController.processNotification ); +/*app.use('/notifications/budget', + bodyParser.json())*/ +app.post('/notifications/budget', + budgetController.determineMattermostServer, + budgetController.checkToken, + budgetController.manageInteractiveActionCall, + budgetController.manageSlashCommand, + budgetController.createPullRequest, + ); + app.post('/marrainage', marrainageController.createRequest); app.get('/marrainage/accept', marrainageController.acceptRequest); app.get('/marrainage/decline', marrainageController.declineRequest); @@ -167,8 +181,9 @@ app.get('/api/get-users-location', mapController.getUsersLocation); app.get('/map', mapController.getMap); + sentry.initCaptureConsoleWithHandler(app); export default app.listen(config.port, () => - console.log(`Running on: ${config.protocol}://${config.host}:${config.port}`) + console.log(`Running on: ${config.protocol}://${config.host}:${config.port} ${config.githubFork}`) ); diff --git a/src/lib/frontmatter.ts b/src/lib/frontmatter.ts new file mode 100644 index 000000000..c6c9e18f6 --- /dev/null +++ b/src/lib/frontmatter.ts @@ -0,0 +1,49 @@ +import YAML from "yaml"; + +export function applyChanges(text, changes) { + const changeKeys = Object.keys(changes) + const updates = {} + + const [frontmatter, ...body] = text.split("\n---") + const [front] = new YAML.Parser().parse(frontmatter + "\n") + const doc = front as YAML.CST.Document + if (doc.value?.type !== "block-map") { + throw `${doc.value?.type} should equal "block-map"` + } + const map = doc.value as YAML.CST.BlockMap + changeKeys.forEach(key => { + const newValue = changes[key] + map.items.forEach(e => { + const keyScalar = YAML.CST.resolveAsScalar(e.key) + if (keyScalar.value === key) { + const valueScalar = YAML.CST.resolveAsScalar(e.value) + if (valueScalar.value !== newValue) { + updates[key] = "updated" + YAML.CST.setScalarValue(e.value, newValue) + } else { + updates[key] = "exists" + } + } + }) + + if (!updates[key]) { + const { indent } = map + map.items.push({ + start: [], + key: YAML.CST.createScalarToken(key, { end: [], indent }), + sep: [ + { type: 'map-value-ind', source: ':', offset: -1, indent: 0 }, + { type: 'space', source: ' ', offset: -1, indent: 0 }, + ], + value: YAML.CST.createScalarToken(newValue, { indent }), + }) + updates[key] = "inserted" + } + }) + const content = YAML.CST.stringify(doc) + "---" + body.join("\n---") + + return { + content, + updates + } +} diff --git a/src/lib/mattermost.ts b/src/lib/mattermost.ts index 96f905001..856b93fe9 100644 --- a/src/lib/mattermost.ts +++ b/src/lib/mattermost.ts @@ -221,3 +221,15 @@ export async function changeUserEmail(id: string, email: string) { return false; } } + +export async function getChannels() { + const mattermostChannels = await axios + .get(`${config.mattermostURL}/api/v4/channels`, { + ...getMattermostConfig(), + }) + .then((response) => response.data); + if (!mattermostChannels.length) { + return []; + } + return mattermostChannels; +} diff --git a/src/lib/mattermost/config.ts b/src/lib/mattermost/config.ts new file mode 100644 index 000000000..86dbdbd72 --- /dev/null +++ b/src/lib/mattermost/config.ts @@ -0,0 +1,43 @@ +function getMattermostVarEnv(prefix) { + const keys = Object.keys(process.env).filter(k => k.startsWith(prefix)) + return keys.reduce((a,v) => { + a[v.slice(prefix.length)] = process.env[v] + return a + }, {}) +} + +function getMattermostServers() { + return getMattermostVarEnv("MATTERMOST_SERVER_") +} + +function getMattermostHookConfig(config?) { + setData("MATTERMOST_HOOK_URL_", "url", config) + return setData("MATTERMOST_HOOK_TOKEN_", "token", config) +} + +function getMattermostBotTokens() { + return getMattermostVarEnv("MATTERMOST_BOT_TOKEN_") +} + +function setData(prefix, prop, config) { + const data = getMattermostVarEnv(prefix) + const keys = Object.keys(data) + return keys.reduce((a,v) => { + const comps = v.split('_') + const serverId = comps.pop() + const teamId = comps.join('_') + a[serverId] = a[serverId] || {} + a[serverId][teamId] = a[serverId][teamId] || {} + a[serverId][teamId][prop] = data[v] + return a + }, config || {}) +} + +export const buildMattermostConfig = () => { + const config = { + servers: getMattermostServers(), + hooks: getMattermostHookConfig({}), + bots: getMattermostBotTokens(), + } + return config +} \ No newline at end of file diff --git a/src/models/startup.ts b/src/models/startup.ts index 37705017c..7adf7fa61 100644 --- a/src/models/startup.ts +++ b/src/models/startup.ts @@ -12,6 +12,7 @@ export interface Startup { name: string; repository: string; contact: string; + budget_url: string; expired_members: string[]; active_members: string[]; previous_members: string[]; @@ -30,6 +31,7 @@ export interface StartupInfo { stats_url: string; link: string; incubator: string; + budget_url: string; phases: Phase[]; }; relationships: Relationship; diff --git a/src/scripts/pingTeamForBudgetInfo.ts b/src/scripts/pingTeamForBudgetInfo.ts new file mode 100644 index 000000000..be2bd56ed --- /dev/null +++ b/src/scripts/pingTeamForBudgetInfo.ts @@ -0,0 +1,199 @@ +import axios from "axios"; +//import { Startup } from '../models/startup'; + +import fs from "fs"; +import YAML from "yaml"; + +import { applyChanges } from "../lib/frontmatter"; +import { getChannels } from "../lib/mattermost"; +import betagouv from "../betagouv"; + +const teamIdsToTokenMap = { +} + + +// List all channels +// identify +// post messages everywhere +// accept /budget + +//import cc from "../../chans.json" + +const getAllChans = async () => { + const channels = await getChannels() + console.log(channels.filter(c => c.display_name.startsWith("startup-"))) +} + +function getMattermostVarEnv(prefix) { + const keys = Object.keys(process.env).filter(k => k.startsWith(prefix)) + return keys.reduce((a,v) => { + a[v.slice(prefix.length)] = process.env[v] + return a + }, {}) +} + +function getMattermostServers() { + return getMattermostVarEnv("MATTERMOST_SERVER_") +} + +function getMattermostHookConfig(config?) { + setData("MATTERMOST_HOOK_URL_", "url", config) + return setData("MATTERMOST_HOOK_TOKEN_", "token", config) +} + +function getMattermostBotTokens() { + return getMattermostVarEnv("MATTERMOST_BOT_TOKEN_") +} + +function setData(prefix, prop, config) { + const data = getMattermostVarEnv(prefix) + const keys = Object.keys(data) + return keys.reduce((a,v) => { + const comps = v.split('_') + const serverId = comps.pop() + const teamId = comps.join('_') + a[serverId] = a[serverId] || {} + a[serverId][teamId] = a[serverId][teamId] || {} + a[serverId][teamId][prop] = data[v] + return a + }, config || {}) +} + +const buildMattermostConfig = () => { + const config = { + servers: getMattermostServers(), + hooks: getMattermostHookConfig({}), + bots: getMattermostBotTokens(), + } + return config +} + +const Broadcast = async () => { + const config = buildMattermostConfig() + const server = "cel" + const team = "societaires" + const channels = [{ + id : "startup-test" + }] + const url = `${config.servers[server]}${config.hooks[server][team].url}` + const data = { + channel: channels[0].id, + text: `:wave: À beta.gouv.fr on aime la transparence. Les fiches publiques sur le site contiennent désormais \ +un lien vers une page qui détaillent le budget de l'équipe. +Nous recommandons de faire une page sur [pad.incubateur.net](https://pad.incubateur.net), de la publier et de l'indiquer dans votre fiche produit. Vous pouvez mettre à jour votre fiche directement sur GitHub ou bien en utilisant la commande \`/budget url [startupId] [url]\` ! +Dans un premier temps, vous pouvez aussi utiliser la même commande en indiquant le budget total de votre Startup \`/budget page [startupId] [montant]\` par exemple \`/budget page aides.jeunes 1500000\`. + +Si vous avez des questions ou des problèmes, n'hésitez pas à rejoindre le canal [~domaine-transparence-budget](https://mattermost.incubateur.net/betagouv/channels/domaine-transparence-budget). + + +:wave: @guillett vient de demander la génération d'une première page budget. :tada: +https://beta-gouv-fr-budget.netlify.app/?startup=Aides%20Jeunes&budget=100000&start=2021-03-01&date=2022-07-07&startupId=aides.jeunes +Si vous avez des questions ou des problèmes, n'hésitez pas à rejoindre le canal [~domaine-transparence-budget](https://mattermost.incubateur.net/betagouv/channels/domaine-transparence-budget) :smiley: +` + } + const response = await axios.post(url, data) + const json = response.data + console.log(json) +} + +const magicEnv = () => { + const keys = Object.keys(process.env).filter(k => k.startsWith("MATTERMOST_TOKEN")) + const teamIdsToTokenMap = keys.reduce((a, v) => { + const [teamId, token] = v.split(':') + a[teamId] = token + return a + }, {}) + console.log("Cool") +} + +const pingTeamForBudgetInfo0 = async() => { + +/* const startups= await betagouv.startupsInfos() + const aj = startups.find(s => s.id == "aides.jeunes") + console.log(aj) + console.log(JSON.stringify(aj, null, 2)) + console.log(aj.attributes.phases.map(p => p.start))*/ + +/* + + const url = process.env.MATTERMOST_HOOK_TOKEN_betagouv_beta + const data = { + channel: "startup-aides-jeunes", + text: "Hello!" + } + + axios.post(url, data) + budget set-channel aides.jeunes + */ +} + +const checkDates = () => { + const dir = '/home/thomas/repos/beta.gouv.fr/content/_startups/' + const set = fs.readdirSync(dir) + const d = set.map(s => { +/* if (!s.startsWith('aides.')) { + return + }*/ + const fullPath = dir + s + const text = fs.readFileSync(fullPath, 'utf8') + const [frontmatter, ...body] = text.split("\n---") + const obj = YAML.parse(frontmatter) + + const dts = obj?.phases.map(p => p.start) || [] + dts.sort() + return { + s, + start: dts.length ? dts[0]: null + } + }) +} + +const AllowYAMLPristineRegeneration = async() => { + const dir = '/home/thomas/repos/beta.gouv.fr/content/_startups/' + const set = fs.readdirSync(dir) + set.forEach(s => { + const fullPath = dir + s + const text = fs.readFileSync(fullPath, 'utf8') + const [frontmatter, ...body] = text.split("\n---") + + const [front] = new YAML.Parser().parse(frontmatter + "\n") + const doc = front as YAML.CST.Document + const content = YAML.CST.stringify(doc) + "---" + body.join("\n---") + if (content != text) { + console.log(s) + fs.writeFileSync('test.md', content, "utf8") + console.log("diff --color " + fullPath + " test.md") + process.exit(1) + } + }) +} + +const ExperimentApplyingChanges = () => { + const dir = '/home/thomas/repos/beta.gouv.fr/content/_startups/' + const set = fs.readdirSync(dir) + const startupId = "aides.jeunes" + set.forEach(s => { + if (s != startupId + ".md") { + return + } + + const fullPath = dir + s + const text = fs.readFileSync(fullPath, 'utf8') + const changes = { + channel_url: "https://mattermost.incubateur.net/betagouv/channels/startup-" + startupId + "NEW" + } + const {updates, content} = applyChanges(text, changes) + console.log(updates) + fs.writeFileSync(fullPath, content, "utf8") + }) +} + +//AllowYAMLPristineRegeneration() +//ExperimentApplyingChanges() + +//pingTeamForBudgetInfo0() +//checkDates() +//magicEnv() +//getAllChans() +//buildMattermostConfig() + Broadcast() diff --git a/tsconfig.json b/tsconfig.json index f6e0816b8..a41614ab5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "removeComments": true, - "noUnusedLocals": true, + "noUnusedLocals": false, "declaration": true, "sourceMap": true, "allowJs": true,