From c699f13c78b6b905f174a307e5451e1fb54f059e Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 18 Jan 2023 01:08:02 +0100 Subject: [PATCH] Add overpass reports, add nomention and nobot support --- README.md | 26 ++++++++ src/Config.ts | 29 ++++++++- src/Mastodon.ts | 108 +++++++++++++++++++++++++------- src/OsmUserInfo.ts | 33 +++++++++- src/Overpass.ts | 28 +++++++++ src/Postbuilder.ts | 149 ++++++++++++++++++++++++++++++--------------- src/Utils.ts | 6 ++ src/index.ts | 25 ++++---- 8 files changed, 315 insertions(+), 89 deletions(-) create mode 100644 README.md create mode 100644 src/Overpass.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ca2289 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Mastodon-Bot + +This is a bot which is built to post a daily message about mapcomplete-usage. + +## How it works + +It fetches latest changesets from OsmCha, processes them and posts them to Mastodon. + +Changesets which have the `add-image`-tag are downloaded from the OSM-api and crawled for images, of which some are picked to add to the posts. + +Note that the image selection process is opinionated: some themes (artwork, nature, trees, ...) have a higher probability of being picked. +Furthermore, it tries to pick at most one image per contributor - so images by the same contributor will only be used if there are more images to add then contributors. + +## Your bot mentioned me - I don't want that! + +You can indicate this to the bot in the following way: + +### On Mastodon + +- You can mute or block the bot so that you won't see the posts. Your user account on OpenStreetMap or your Mastodon-username will still be mentioned in the posts +- You can add the hashtag `#nobot` or `#no-mapcomplete-bot` to your profile description. The bot will not mention you with your Mastodon-handle, but it will still post your OSM-username, your pictures and a report of your contributions + +### On your OSM-user profile: + +- Add `#no-bot` or `#no-mapcomplete-bot` to your user profile and your contributions (map changes and pictures) will not be included in the bot at all +- Add `#nobotmention` or `#nomapcompletebotmention` to your user profile. Your contributions and pictures will still be listed in the bot, with your OSM-username. However, your OSM-username will _not_ be replaced by your Mastodon-handle, thus _not_ pinging you. \ No newline at end of file diff --git a/src/Config.ts b/src/Config.ts index a6291a9..6a01f27 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,8 +1,31 @@ import {LoginSettings} from "./Mastodon"; export interface MapCompleteUsageOverview { - topContributorsNumberToShow: number, - topThemesNumberToShow: number, + showTopContributors?: boolean, + showTopThemes?: boolean, + + /** + * Term to use in 'created/moved/deleted one point' + */ + poiName?: "point" | string + + /** + * Term to use in 'created/moved/deleted n points' + */ + poisName?: "points" | string + + report?:{ + overpassQuery: string, + /** + * Values {total} and {date} will be substituted + */ + post: string + /** + * Default: global bbox + * Use something like `[bbox:51.19999983412068,2.8564453125,51.41976382669736,3.416748046875]` if only a certain region should be sent + */ + bbox?: string + } /** * The number of days to look back. @@ -35,5 +58,5 @@ export default interface Config { /** IF set: prints to console instead of to Mastodon*/ dryrun?: boolean }, - actions: {mapCompleteUsageOverview : MapCompleteUsageOverview} [] + actions: MapCompleteUsageOverview [] } \ No newline at end of file diff --git a/src/Mastodon.ts b/src/Mastodon.ts index 3c6745d..7cb19db 100644 --- a/src/Mastodon.ts +++ b/src/Mastodon.ts @@ -1,10 +1,9 @@ import {login, LoginParams} from 'masto'; import * as fs from "fs"; -import {stat} from "fs"; export interface LoginSettings { url: string, - accessToken: string + accessToken: string } export interface CreateStatusParamsBase { @@ -22,57 +21,122 @@ export interface CreateStatusParamsBase { readonly mediaIds?: readonly string[]; } + export default class MastodonPoster { /** * The actual instance, see https://www.npmjs.com/package/mastodon * @private */ - private readonly instance ; + private readonly instance; private _dryrun: boolean; + private _userInfoCache: Record = {} + private constructor(masto, dryrun: boolean) { this.instance = masto this._dryrun = dryrun; } - - public async doStuff(){ + + public static async construct(settings: LoginParams & { dryrun?: boolean }) { + return new MastodonPoster(await login(settings), settings.dryrun ?? false) } - - public async writeMessage(text: string, options?: CreateStatusParamsBase): Promise<{id: string}>{ - if(this._dryrun){ - console.log("Dryrun enabled - not posting",options?.visibility??"public","message: \n" + text.split("\n").map(txt => " > "+txt).join("\n")) + + public async writeMessage(text: string, options?: CreateStatusParamsBase): Promise<{ id: string }> { + + if (options?.visibility === "direct" && text.indexOf("@") < 0) { + throw ("Error: you try to send a direct message, but it has no username...") + } + if (text.length > 500) { + console.log(text.split("\n").map(txt => " > " + txt).join("\n")) + throw "Error: text is too long:" + text.length + + } + + if (text.length == 0) { + console.log("Not posting an empty message") + return + } + + if (this._dryrun) { + console.log("Dryrun enabled - not posting", options?.visibility ?? "public", "message: \n" + text.split("\n").map(txt => " > " + txt).join("\n")) return {id: "some_id"} } const statusUpate = await this.instance.v1.statuses.create({ visibility: 'public', - ...(options??{}), + ...(options ?? {}), status: text }) - console.dir(statusUpate) console.log("Posted to", statusUpate.url) - console.log(text.split("\n").map(txt => " > "+txt).join("\n")) + console.log(text.split("\n").map(txt => " > " + txt).join("\n")) return statusUpate } - - public static async construct(settings: LoginParams & {dryrun?: boolean}) { - return new MastodonPoster(await login(settings), settings.dryrun ?? false) + + public async hasNoBot(username: string): Promise { + const info = await this.userInfoFor(username) + const descrParts = info.note?.replace(/-/g, "")?.toLowerCase()?.split(" ") ?? [] + if (descrParts.indexOf("#nobot") >= 0 || descrParts.indexOf("#nomapcompletebot") >= 0) { + return true + } + const nobot = info.fields.find(f => f.name === "nobot").value + if (nobot.toLowerCase() === "yes" || nobot.toLowerCase() === "true") { + return true + } + return false } + public async userInfoFor(username: string): Promise<{ + id: string, + /*Fully qualified user name*/ + acct: string + displayname: string, + bot: boolean, + /* User-set biography */ + note: string, + url: string, + avatar: string, + avatarStatic: string, + header: string, + headerStatic: string, + followersCount: number, + followingCount: number, + statusesCount: number, + fields: { name: string, value: string }[] + }> { + if (this._userInfoCache[username]) { + return this._userInfoCache[username] + } + const acct = await this.instance.v1.accounts.lookup({ + acct: username, + }); + const info = await this.instance.v1.accounts.fetch(acct.id) + this._userInfoCache[username] = info + return info + } /** * Uploads the image; returns the id of the image * @param path * @param description */ - public async uploadImage(path: string, description: string): Promise { - if(this._dryrun){ + public async uploadImage(path: string, description: string): Promise { + if (this._dryrun) { console.log("As dryrun is enabled: not uploading ", path) return "some_id" } console.log("Uploading", path) - const mediaAttachment = await this.instance.v2.mediaAttachments.create({ - file: new Blob([fs.readFileSync(path)]), - description - }) - return mediaAttachment.id + try { + + const mediaAttachment = await this.instance.v2.mediaAttachments.create({ + file: new Blob([fs.readFileSync(path)]), + description + }) + return mediaAttachment.id + } catch (e) { + console.log("Could not upload image " + path + " due to ", e, "Trying again...") + const mediaAttachment = await this.instance.v2.mediaAttachments.create({ + file: new Blob([fs.readFileSync(path)]), + description + }) + return mediaAttachment.id + } } } \ No newline at end of file diff --git a/src/OsmUserInfo.ts b/src/OsmUserInfo.ts index f5d23ce..b136116 100644 --- a/src/OsmUserInfo.ts +++ b/src/OsmUserInfo.ts @@ -1,5 +1,6 @@ import Utils from "./Utils"; import * as fs from "fs"; +import MastodonPoster from "./Mastodon"; export interface UserInfo { "id": number, @@ -40,7 +41,27 @@ export default class OsmUserInfo { } - public async GetMastodonLink(): Promise { + public async hasNoBotTag(): Promise<{ + nobot: boolean, + nomention: boolean + }>{ + const description = (await this.getUserInfo()).description ?? "" + const split = description.toLowerCase().replace(/-/g, "").split(" ") + const nobot = split.indexOf("#nobot") >=0 || split.indexOf("#nomapcompletebot") >= 0 + const nomention = split.indexOf("#nobotmention") >=0 || split.indexOf("#nomapcompletebotmention") >= 0 + return {nobot, nomention} + } + + /** + * Gets the Mastodon username of the this OSM-user to ping them. + * @param mastodonApi: will be used to lookup the metadata of the user; if they have '#nobot' in their bio, don't mention them + * @constructor + */ + public async GetMastodonUsername(mastodonApi: MastodonPoster): Promise { + const {nomention} = await this.hasNoBotTag() + if(nomention){ + return undefined + } const mastodonLinks = await this.getMeLinks() if (mastodonLinks.length <= 0) { @@ -48,7 +69,13 @@ export default class OsmUserInfo { } const url = new URL(mastodonLinks[0]) - return url.pathname.substring(1) + "@" + url.host + const username = url.pathname.substring(1) + "@" + url.host + + if(await mastodonApi.hasNoBot(username)){ + return undefined + } + const useraccount = await mastodonApi.userInfoFor(username) + return useraccount.acct } public async getMeLinks(): Promise { @@ -80,7 +107,7 @@ export default class OsmUserInfo { } } const url = `${this._backend}api/0.6/user/${this._userId}.json` - console.log("Looking up user info about ", this._userId) + console.log("Looking up OSM user info about ", this._userId) const res = await Utils.DownloadJson(url); this._userData = res.user if (this._cachingPath !== undefined) { diff --git a/src/Overpass.ts b/src/Overpass.ts new file mode 100644 index 0000000..56110f8 --- /dev/null +++ b/src/Overpass.ts @@ -0,0 +1,28 @@ +import Utils from "./Utils"; + +export default class Overpass { + private readonly bbox: string; + private readonly overpassQuery: string + + + constructor(options: { + bbox?: string, + overpassQuery: string + }) { + this.bbox = options.bbox ?? ""; + this.overpassQuery = options.overpassQuery + } + private constructUrl() { + // https://overpass-api.de/api/interpreter?data=[out:json][timeout:180];(nwr[%22memorial%22=%22ghost_bike%22];);out%20body;out%20meta;%3E;out%20skel%20qt; + const param = `[out:json][timeout:180]${this.bbox};(${this.overpassQuery});out body;out meta;>;out skel qt;` + return `https://overpass-api.de/api/interpreter?data=${(param)}` + } + + public query(){ + console.log("Querying overpass: ", this.constructUrl()) + return Utils.DownloadJson(this.constructUrl()) + } + + + +} \ No newline at end of file diff --git a/src/Postbuilder.ts b/src/Postbuilder.ts index 6ba7123..24f65b5 100644 --- a/src/Postbuilder.ts +++ b/src/Postbuilder.ts @@ -5,12 +5,14 @@ import OsmUserInfo from "./OsmUserInfo"; import Config, {MapCompleteUsageOverview} from "./Config"; import MastodonPoster from "./Mastodon"; import ImgurAttribution from "./ImgurAttribution"; +import Overpass from "./Overpass"; type ImageInfo = { image: string, changeset: ChangeSetData } export class Postbuilder { private static readonly metakeys = [ "answer", + "create", "add-image", "move", "delete", @@ -19,7 +21,7 @@ export class Postbuilder { ] private readonly _config: MapCompleteUsageOverview; private readonly _globalConfig: Config - + private readonly _poster: MastodonPoster; private readonly _changesetsMade: ChangeSetData[]; @@ -33,7 +35,13 @@ export class Postbuilder { } - getStatisticsFor(changesetsMade?: ChangeSetData[]): { total: number, addImage?: number, deleted: number, answered?: number, moved?: number, summaryText?: string } { + getStatisticsFor(changesetsMade?: ChangeSetData[]): { total: number, + answered?: number, + created?: number, + addImage?: number, + deleted: number, + moved?: number, + summaryText?: string } { const stats: Record = {} changesetsMade ??= this._changesetsMade @@ -48,11 +56,20 @@ export class Postbuilder { } let overview: string[] = [] - const {answer, move} = stats + const {answer, move, create} = stats const deleted = stats.delete const images = stats["add-image"] const plantnetDetected = stats["plantnet-ai-detection"] const linkedImages = stats["link-image"] + const poi = this._config.poiName ?? "point" + const pois = this._config.poisName ?? "points" + if(create){ + if (create == 1) { + overview.push("added one "+poi) + } else { + overview.push("added " + create +" "+pois) + } + } if (answer) { if (answer == 1) { overview.push("answered one question") @@ -70,17 +87,17 @@ export class Postbuilder { if (move) { if (move == 1) { - overview.push("moved one point") + overview.push("moved one "+poi) } else { - overview.push("moved " + move + " points") + overview.push("moved " + move + " "+pois) } } if (deleted) { if (deleted == 1) { - overview.push("delted one deleted") + overview.push("deleted one "+poi) } else { - overview.push("deleted " + deleted + " points") + overview.push("deleted " + deleted + " "+pois) } } @@ -120,16 +137,18 @@ export class Postbuilder { const themes = new Histogram(changesetsMade, cs => cs.properties.theme) - let username = await userinfo.GetMastodonLink() ?? inf.display_name + let username = await userinfo.GetMastodonUsername(this._poster) ?? inf.display_name const statistics = this.getStatisticsFor(changesetsMade) - let thematicMaps = "maps " + Utils.commasAnd(themes.keys()) - if (themes.keys().length === 1) { - thematicMaps = "map " + Utils.commasAnd(themes.keys()) + let thematicMaps = " with the thematic maps " + Utils.commasAnd(themes.keys()) + if (this._config?.themeWhitelist?.length === 1) { + thematicMaps = "" + } else if (themes.keys().length === 1) { + thematicMaps = " with the thematic map " + Utils.commasAnd(themes.keys()) } - return username + " " + statistics.summaryText + " with the thematic " + thematicMaps + return username + " " + statistics.summaryText + thematicMaps } async createOverviewForTheme(theme: string, changesetsMade: ChangeSetData[]): Promise { @@ -206,8 +225,21 @@ export class Postbuilder { public async buildMessage(date: string): Promise { const changesets = this._changesetsMade - const perContributor = new Histogram(changesets, cs => cs.properties.uid) + let lastPostId: string = undefined + + if(this._config.report){ + const report = this._config.report + const overpass = new Overpass(report) + const data = await overpass.query() + const total = data.elements.length + const date = data.osm3s.timestamp_osm_base.substring(0, 10) + lastPostId = (await this._poster.writeMessage( + report.post.replace(/{total}/g, ""+total).replace(/{date}/g, date) + )).id + } + + const perContributor = new Histogram(changesets, cs => cs.properties.uid) const topContributors = perContributor.sortedByCount({ countMethod: cs => { let sum = 0 @@ -222,7 +254,7 @@ export class Postbuilder { const totalStats = this.getStatisticsFor() - const { + const { totalImagesCreated, attachmentIds, imgAuthors, @@ -230,31 +262,38 @@ export class Postbuilder { } = await this.prepareImages(changesets, 12) let timePeriod = "Yesterday" - if(this._config.numberOfDays > 1){ - timePeriod = "In the past "+this._config.numberOfDays+" days" + if (this._config.numberOfDays > 1) { + timePeriod = "In the past " + this._config.numberOfDays + " days" } + const singleTheme = this._config?.themeWhitelist?.length === 1 ? "/" + this._config.themeWhitelist[0] : "" let toSend: string[] = [ - timePeriod+", " + perContributor.keys().length + " different persons made " + totalStats.total + " changes to #OpenStreetMap using https://mapcomplete.osm.be .\n", + `${timePeriod}, ${perContributor.keys().length} persons made ${totalStats.total} changes to #OpenStreetMap using https://mapcomplete.osm.be${singleTheme} . +`, ] - for (let i = 0; i < this._config.topContributorsNumberToShow - 1 && i < topContributors.length; i++) { - const uid = topContributors[i].key - const changesetsMade = perContributor.get(uid) - try { - const overview = await this.createOverviewForContributor(uid, changesetsMade) - if (overview.length + toSend.join("\n").length > 500) { - break + if (this._config.showTopContributors && topContributors.length > 0) { + for (const topContributor of topContributors) { + const uid = topContributor.key + const changesetsMade = perContributor.get(uid) + try { + const userInfo = new OsmUserInfo(Number(uid)) + const {nobot} = await userInfo.hasNoBotTag() + if (nobot) { + continue + } + const overview = await this.createOverviewForContributor(uid, changesetsMade) + if (overview.length + toSend.join("\n").length > 500) { + break + } + toSend.push(" - " + overview) + } catch (e) { + console.error("Could not add contributor " + uid, e) } - toSend.push(" - " + overview) - } catch (e) { - console.error("Could not add contributor " + uid, e) } - + lastPostId = (await this._poster.writeMessage(toSend.join("\n"), {mediaIds: attachmentIds.slice(0, 4)})).id + toSend = [] } - const firstPost = await this._poster.writeMessage(toSend.join("\n"), {mediaIds: attachmentIds.slice(0, 4)}) - toSend = [] - const perTheme = new Histogram(changesets, cs => { return cs.properties.theme; }) @@ -263,17 +302,24 @@ export class Postbuilder { countMethod: cs => this.getStatisticsFor([cs]).total, dropZeroValues: true }) - toSend.push("") - for (let i = 0; i < this._config.topThemesNumberToShow && i < mostPopularThemes.length; i++) { - const theme = mostPopularThemes[i].key - const changesetsMade = perTheme.get(theme) - toSend.push(await this.createOverviewForTheme(theme, changesetsMade)) - } + if (this._config.showTopThemes && mostPopularThemes.length > 0) { - const secondPost = await this._poster.writeMessage(toSend.join("\n"), { - inReplyToId: firstPost["id"], - mediaIds: attachmentIds.slice(4, 8) - }) + for (const theme of mostPopularThemes) { + const themeId = theme.key + const changesetsMade = perTheme.get(themeId) + const overview = await this.createOverviewForTheme(themeId, changesetsMade) + if (overview.length + toSend.join("\n").length > 500) { + break + } + toSend.push(overview) + } + + lastPostId = (await this._poster.writeMessage(toSend.join("\n"), { + inReplyToId: lastPostId, + mediaIds: attachmentIds.slice(4, 8) + })).id + toSend = [] + } const authorNames = Array.from(new Set(imgAuthors)) @@ -282,10 +328,10 @@ export class Postbuilder { "Images in this thread are randomly selected from them and were made by: ", ...authorNames.map(auth => "- " + auth), "", - "All changes were made on " + date + (this._config.numberOfDays > 1 ? " or at most "+this._config.numberOfDays+"days before": "") + "All changes were made on " + date + (this._config.numberOfDays > 1 ? ` or at most ${this._config.numberOfDays} days before` : "") ].join("\n"), { - inReplyToId: secondPost["id"], + inReplyToId: lastPostId, mediaIds: attachmentIds.slice(8, 12) } ) @@ -301,6 +347,13 @@ export class Postbuilder { const seenURLS = new Set() for (const changeset of withImage) { + const userinfo = new OsmUserInfo(Number(changeset.properties.uid)) + const {nobot} = await userinfo.hasNoBotTag() + if (nobot) { + console.log("Not indexing images of user", changeset.properties.user) + continue + } + const url = this._globalConfig.osmBackend + "/api/0.6/changeset/" + changeset.id + "/download" const osmChangeset = await Utils.DownloadXml(url) const osmChangesetTags: { k: string, v: string }[] = Array.from(osmChangeset.getElementsByTagName("tag")) @@ -308,7 +361,7 @@ export class Postbuilder { .filter(kv => kv.k.startsWith("image")) for (const kv of osmChangesetTags) { - if(seenURLS.has(kv.v)){ + if (seenURLS.has(kv.v)) { continue } seenURLS.add(kv.v) @@ -320,13 +373,13 @@ export class Postbuilder { const attachmentIds: string[] = [] const imgAuthors: string[] = [] for (const randomImage of randomImages) { - + const cs = randomImage.changeset.properties let authorName = cs.user try { const authorInfo = new OsmUserInfo(Number(cs.uid), this._globalConfig) - authorName = (await authorInfo.GetMastodonLink()) ?? cs.user - }catch (e) { + authorName = (await authorInfo.GetMastodonUsername(this._poster)) ?? cs.user + } catch (e) { console.log("Could not fetch more info about contributor", authorName, cs.uid, "due to", e) } imgAuthors.push(authorName) @@ -340,7 +393,7 @@ export class Postbuilder { await Utils.DownloadBlob(randomImage.image, path) const mediaId = await this._poster.uploadImage(path, "Image taken by " + authorName + ", available under " + attribution.license + ". It is made with the thematic map " + randomImage.changeset.properties.theme + " in changeset https://openstreetmap.org/changeset/" + randomImage.changeset.id) attachmentIds.push(mediaId) - + } return { attachmentIds, diff --git a/src/Utils.ts b/src/Utils.ts index 32688d6..205fa67 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -5,7 +5,13 @@ import {DOMParser} from '@xmldom/xmldom' export default class Utils { public static async DownloadJson(url, headers?: any): Promise { const data = await Utils.Download(url, headers) + try{ + return JSON.parse(data.content) + }catch (e) { + console.log("Could not parse the result of ", url,": not a valid json:\n ",data.content) + throw e + } } public static Sum(t: (number | undefined)[]) { diff --git a/src/index.ts b/src/index.ts index c906fbf..5d27e7f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export class Main { } async main() { + const poster = await MastodonPoster.construct(this._config.mastodonAuth) if (fakedom === undefined || window === undefined) { throw "FakeDom not initialized" @@ -27,8 +28,7 @@ export class Main { console.log("Created the caching directory at", this._config.cacheDir) } - const poster = await MastodonPoster.construct(this._config.mastodonAuth) - + const notice = await poster.writeMessage("@pietervdvn@en.osm.town Starting MapComplete bot...", { visibility: "direct" }) @@ -42,7 +42,7 @@ export class Main { const end = Date.now() const timeNeeded = Math.floor((end - start) / 1000) - await poster.writeMessage("Finished running MapComplete bot, this took " + timeNeeded + "seconds", { + await poster.writeMessage("@pietervdvn@en.osm.town Finished running MapComplete bot, this took " + timeNeeded + "seconds", { inReplyToId: notice.id, visibility: "direct" }) @@ -50,7 +50,7 @@ export class Main { console.error(e) const end = Date.now() const timeNeeded = Math.floor((end - start) / 1000) - await poster.writeMessage("Running MapComplete bot failed in " + timeNeeded + "seconds, the error is " + e, { + await poster.writeMessage("@pietervdvn@en.osm.town Running MapComplete bot failed in " + timeNeeded + "seconds, the error is " + e, { inReplyToId: notice.id, visibility: "direct" }) @@ -58,14 +58,13 @@ export class Main { } - private async runMapCompleteOverviewAction(poster: MastodonPoster, action: { mapCompleteUsageOverview: MapCompleteUsageOverview }) { + private async runMapCompleteOverviewAction(poster: MastodonPoster, action: MapCompleteUsageOverview) { console.log("Fetching recent changesets...") const osmcha = new OsmCha(this._config) const today = new Date() - const overviewSettings = action.mapCompleteUsageOverview let changesets: ChangeSetData[] = [] - const days = overviewSettings.numberOfDays ?? 1 + const days = action.numberOfDays ?? 1 if (days < 1) { throw new Error("Invalid config: numberOfDays should be >= 1") } @@ -82,19 +81,19 @@ export class Main { } - if (overviewSettings.themeWhitelist?.length > 0) { - const allowedThemes = new Set(overviewSettings.themeWhitelist) + if (action.themeWhitelist?.length > 0) { + const allowedThemes = new Set(action.themeWhitelist) const beforeCount = changesets.length changesets = changesets.filter(cs => allowedThemes.has(cs.properties.theme)) if (changesets.length == 0) { - console.log("No changesets found for themes", overviewSettings.themeWhitelist.join(", ")) - return console.log("No changesets found for themes", overviewSettings.themeWhitelist.join(", ")) + console.log("No changesets found for themes", action.themeWhitelist.join(", ")) + return console.log("No changesets found for themes", action.themeWhitelist.join(", ")) } - console.log("Filtering for ", overviewSettings.themeWhitelist, "yielded", changesets.length, "changesets (" + beforeCount + " before)") + console.log("Filtering for ", action.themeWhitelist, "yielded", changesets.length, "changesets (" + beforeCount + " before)") } console.log("Building post...") - await new Postbuilder(overviewSettings, this._config, poster, changesets).buildMessage(today.getUTCFullYear() + "-" + Utils.TwoDigits(today.getUTCMonth() + 1) + "-" + Utils.TwoDigits(today.getUTCDate() - 1)) + await new Postbuilder(action, this._config, poster, changesets).buildMessage(today.getUTCFullYear() + "-" + Utils.TwoDigits(today.getUTCMonth() + 1) + "-" + Utils.TwoDigits(today.getUTCDate() - 1)) }