Add overpass reports, add nomention and nobot support

This commit is contained in:
Pieter Vander Vennet 2023-01-18 01:08:02 +01:00
parent d2ddc3227d
commit c699f13c78
8 changed files with 315 additions and 89 deletions

26
README.md Normal file
View file

@ -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.

View file

@ -1,8 +1,31 @@
import {LoginSettings} from "./Mastodon"; import {LoginSettings} from "./Mastodon";
export interface MapCompleteUsageOverview { export interface MapCompleteUsageOverview {
topContributorsNumberToShow: number, showTopContributors?: boolean,
topThemesNumberToShow: number, 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. * The number of days to look back.
@ -35,5 +58,5 @@ export default interface Config {
/** IF set: prints to console instead of to Mastodon*/ /** IF set: prints to console instead of to Mastodon*/
dryrun?: boolean dryrun?: boolean
}, },
actions: {mapCompleteUsageOverview : MapCompleteUsageOverview} [] actions: MapCompleteUsageOverview []
} }

View file

@ -1,10 +1,9 @@
import {login, LoginParams} from 'masto'; import {login, LoginParams} from 'masto';
import * as fs from "fs"; import * as fs from "fs";
import {stat} from "fs";
export interface LoginSettings { export interface LoginSettings {
url: string, url: string,
accessToken: string accessToken: string
} }
export interface CreateStatusParamsBase { export interface CreateStatusParamsBase {
@ -22,57 +21,122 @@ export interface CreateStatusParamsBase {
readonly mediaIds?: readonly string[]; readonly mediaIds?: readonly string[];
} }
export default class MastodonPoster { export default class MastodonPoster {
/** /**
* The actual instance, see https://www.npmjs.com/package/mastodon * The actual instance, see https://www.npmjs.com/package/mastodon
* @private * @private
*/ */
private readonly instance ; private readonly instance;
private _dryrun: boolean; private _dryrun: boolean;
private _userInfoCache: Record<string, any> = {}
private constructor(masto, dryrun: boolean) { private constructor(masto, dryrun: boolean) {
this.instance = masto this.instance = masto
this._dryrun = dryrun; 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}>{ 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")) 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"} return {id: "some_id"}
} }
const statusUpate = await this.instance.v1.statuses.create({ const statusUpate = await this.instance.v1.statuses.create({
visibility: 'public', visibility: 'public',
...(options??{}), ...(options ?? {}),
status: text status: text
}) })
console.dir(statusUpate)
console.log("Posted to", statusUpate.url) 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 return statusUpate
} }
public static async construct(settings: LoginParams & {dryrun?: boolean}) { public async hasNoBot(username: string): Promise<boolean> {
return new MastodonPoster(await login(settings), settings.dryrun ?? false) 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 * Uploads the image; returns the id of the image
* @param path * @param path
* @param description * @param description
*/ */
public async uploadImage(path: string, description: string): Promise<string> { public async uploadImage(path: string, description: string): Promise<string> {
if(this._dryrun){ if (this._dryrun) {
console.log("As dryrun is enabled: not uploading ", path) console.log("As dryrun is enabled: not uploading ", path)
return "some_id" return "some_id"
} }
console.log("Uploading", path) console.log("Uploading", path)
const mediaAttachment = await this.instance.v2.mediaAttachments.create({ try {
file: new Blob([fs.readFileSync(path)]),
description const mediaAttachment = await this.instance.v2.mediaAttachments.create({
}) file: new Blob([fs.readFileSync(path)]),
return mediaAttachment.id 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
}
} }
} }

View file

@ -1,5 +1,6 @@
import Utils from "./Utils"; import Utils from "./Utils";
import * as fs from "fs"; import * as fs from "fs";
import MastodonPoster from "./Mastodon";
export interface UserInfo { export interface UserInfo {
"id": number, "id": number,
@ -40,7 +41,27 @@ export default class OsmUserInfo {
} }
public async GetMastodonLink(): Promise<string | undefined> { 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<string | undefined> {
const {nomention} = await this.hasNoBotTag()
if(nomention){
return undefined
}
const mastodonLinks = await this.getMeLinks() const mastodonLinks = await this.getMeLinks()
if (mastodonLinks.length <= 0) { if (mastodonLinks.length <= 0) {
@ -48,7 +69,13 @@ export default class OsmUserInfo {
} }
const url = new URL(mastodonLinks[0]) 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<string[]> { public async getMeLinks(): Promise<string[]> {
@ -80,7 +107,7 @@ export default class OsmUserInfo {
} }
} }
const url = `${this._backend}api/0.6/user/${this._userId}.json` 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); const res = await Utils.DownloadJson(url);
this._userData = res.user this._userData = res.user
if (this._cachingPath !== undefined) { if (this._cachingPath !== undefined) {

28
src/Overpass.ts Normal file
View file

@ -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())
}
}

View file

@ -5,12 +5,14 @@ import OsmUserInfo from "./OsmUserInfo";
import Config, {MapCompleteUsageOverview} from "./Config"; import Config, {MapCompleteUsageOverview} from "./Config";
import MastodonPoster from "./Mastodon"; import MastodonPoster from "./Mastodon";
import ImgurAttribution from "./ImgurAttribution"; import ImgurAttribution from "./ImgurAttribution";
import Overpass from "./Overpass";
type ImageInfo = { image: string, changeset: ChangeSetData } type ImageInfo = { image: string, changeset: ChangeSetData }
export class Postbuilder { export class Postbuilder {
private static readonly metakeys = [ private static readonly metakeys = [
"answer", "answer",
"create",
"add-image", "add-image",
"move", "move",
"delete", "delete",
@ -19,7 +21,7 @@ export class Postbuilder {
] ]
private readonly _config: MapCompleteUsageOverview; private readonly _config: MapCompleteUsageOverview;
private readonly _globalConfig: Config private readonly _globalConfig: Config
private readonly _poster: MastodonPoster; private readonly _poster: MastodonPoster;
private readonly _changesetsMade: ChangeSetData[]; 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<string, number> = {} const stats: Record<string, number> = {}
changesetsMade ??= this._changesetsMade changesetsMade ??= this._changesetsMade
@ -48,11 +56,20 @@ export class Postbuilder {
} }
let overview: string[] = [] let overview: string[] = []
const {answer, move} = stats const {answer, move, create} = stats
const deleted = stats.delete const deleted = stats.delete
const images = stats["add-image"] const images = stats["add-image"]
const plantnetDetected = stats["plantnet-ai-detection"] const plantnetDetected = stats["plantnet-ai-detection"]
const linkedImages = stats["link-image"] 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) {
if (answer == 1) { if (answer == 1) {
overview.push("answered one question") overview.push("answered one question")
@ -70,17 +87,17 @@ export class Postbuilder {
if (move) { if (move) {
if (move == 1) { if (move == 1) {
overview.push("moved one point") overview.push("moved one "+poi)
} else { } else {
overview.push("moved " + move + " points") overview.push("moved " + move + " "+pois)
} }
} }
if (deleted) { if (deleted) {
if (deleted == 1) { if (deleted == 1) {
overview.push("delted one deleted") overview.push("deleted one "+poi)
} else { } 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) 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) const statistics = this.getStatisticsFor(changesetsMade)
let thematicMaps = "maps " + Utils.commasAnd(themes.keys()) let thematicMaps = " with the thematic maps " + Utils.commasAnd(themes.keys())
if (themes.keys().length === 1) { if (this._config?.themeWhitelist?.length === 1) {
thematicMaps = "map " + Utils.commasAnd(themes.keys()) 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<string> { async createOverviewForTheme(theme: string, changesetsMade: ChangeSetData[]): Promise<string> {
@ -206,8 +225,21 @@ export class Postbuilder {
public async buildMessage(date: string): Promise<void> { public async buildMessage(date: string): Promise<void> {
const changesets = this._changesetsMade 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({ const topContributors = perContributor.sortedByCount({
countMethod: cs => { countMethod: cs => {
let sum = 0 let sum = 0
@ -222,7 +254,7 @@ export class Postbuilder {
const totalStats = this.getStatisticsFor() const totalStats = this.getStatisticsFor()
const { const {
totalImagesCreated, totalImagesCreated,
attachmentIds, attachmentIds,
imgAuthors, imgAuthors,
@ -230,31 +262,38 @@ export class Postbuilder {
} = await this.prepareImages(changesets, 12) } = await this.prepareImages(changesets, 12)
let timePeriod = "Yesterday" let timePeriod = "Yesterday"
if(this._config.numberOfDays > 1){ if (this._config.numberOfDays > 1) {
timePeriod = "In the past "+this._config.numberOfDays+" days" timePeriod = "In the past " + this._config.numberOfDays + " days"
} }
const singleTheme = this._config?.themeWhitelist?.length === 1 ? "/" + this._config.themeWhitelist[0] : ""
let toSend: string[] = [ 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++) { if (this._config.showTopContributors && topContributors.length > 0) {
const uid = topContributors[i].key for (const topContributor of topContributors) {
const changesetsMade = perContributor.get(uid) const uid = topContributor.key
try { const changesetsMade = perContributor.get(uid)
const overview = await this.createOverviewForContributor(uid, changesetsMade) try {
if (overview.length + toSend.join("\n").length > 500) { const userInfo = new OsmUserInfo(Number(uid))
break 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 => { const perTheme = new Histogram(changesets, cs => {
return cs.properties.theme; return cs.properties.theme;
}) })
@ -263,17 +302,24 @@ export class Postbuilder {
countMethod: cs => this.getStatisticsFor([cs]).total, countMethod: cs => this.getStatisticsFor([cs]).total,
dropZeroValues: true dropZeroValues: true
}) })
toSend.push("") if (this._config.showTopThemes && mostPopularThemes.length > 0) {
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))
}
const secondPost = await this._poster.writeMessage(toSend.join("\n"), { for (const theme of mostPopularThemes) {
inReplyToId: firstPost["id"], const themeId = theme.key
mediaIds: attachmentIds.slice(4, 8) 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<string>(imgAuthors)) const authorNames = Array.from(new Set<string>(imgAuthors))
@ -282,10 +328,10 @@ export class Postbuilder {
"Images in this thread are randomly selected from them and were made by: ", "Images in this thread are randomly selected from them and were made by: ",
...authorNames.map(auth => "- " + auth), ...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"), { ].join("\n"), {
inReplyToId: secondPost["id"], inReplyToId: lastPostId,
mediaIds: attachmentIds.slice(8, 12) mediaIds: attachmentIds.slice(8, 12)
} }
) )
@ -301,6 +347,13 @@ export class Postbuilder {
const seenURLS = new Set<string>() const seenURLS = new Set<string>()
for (const changeset of withImage) { 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 url = this._globalConfig.osmBackend + "/api/0.6/changeset/" + changeset.id + "/download"
const osmChangeset = await Utils.DownloadXml(url) const osmChangeset = await Utils.DownloadXml(url)
const osmChangesetTags: { k: string, v: string }[] = Array.from(osmChangeset.getElementsByTagName("tag")) const osmChangesetTags: { k: string, v: string }[] = Array.from(osmChangeset.getElementsByTagName("tag"))
@ -308,7 +361,7 @@ export class Postbuilder {
.filter(kv => kv.k.startsWith("image")) .filter(kv => kv.k.startsWith("image"))
for (const kv of osmChangesetTags) { for (const kv of osmChangesetTags) {
if(seenURLS.has(kv.v)){ if (seenURLS.has(kv.v)) {
continue continue
} }
seenURLS.add(kv.v) seenURLS.add(kv.v)
@ -320,13 +373,13 @@ export class Postbuilder {
const attachmentIds: string[] = [] const attachmentIds: string[] = []
const imgAuthors: string[] = [] const imgAuthors: string[] = []
for (const randomImage of randomImages) { for (const randomImage of randomImages) {
const cs = randomImage.changeset.properties const cs = randomImage.changeset.properties
let authorName = cs.user let authorName = cs.user
try { try {
const authorInfo = new OsmUserInfo(Number(cs.uid), this._globalConfig) const authorInfo = new OsmUserInfo(Number(cs.uid), this._globalConfig)
authorName = (await authorInfo.GetMastodonLink()) ?? cs.user authorName = (await authorInfo.GetMastodonUsername(this._poster)) ?? cs.user
}catch (e) { } catch (e) {
console.log("Could not fetch more info about contributor", authorName, cs.uid, "due to", e) console.log("Could not fetch more info about contributor", authorName, cs.uid, "due to", e)
} }
imgAuthors.push(authorName) imgAuthors.push(authorName)
@ -340,7 +393,7 @@ export class Postbuilder {
await Utils.DownloadBlob(randomImage.image, path) 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) 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) attachmentIds.push(mediaId)
} }
return { return {
attachmentIds, attachmentIds,

View file

@ -5,7 +5,13 @@ import {DOMParser} from '@xmldom/xmldom'
export default class Utils { export default class Utils {
public static async DownloadJson(url, headers?: any): Promise<any> { public static async DownloadJson(url, headers?: any): Promise<any> {
const data = await Utils.Download(url, headers) const data = await Utils.Download(url, headers)
try{
return JSON.parse(data.content) 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)[]) { public static Sum(t: (number | undefined)[]) {

View file

@ -17,6 +17,7 @@ export class Main {
} }
async main() { async main() {
const poster = await MastodonPoster.construct(this._config.mastodonAuth)
if (fakedom === undefined || window === undefined) { if (fakedom === undefined || window === undefined) {
throw "FakeDom not initialized" throw "FakeDom not initialized"
@ -27,8 +28,7 @@ export class Main {
console.log("Created the caching directory at", this._config.cacheDir) 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...", { const notice = await poster.writeMessage("@pietervdvn@en.osm.town Starting MapComplete bot...", {
visibility: "direct" visibility: "direct"
}) })
@ -42,7 +42,7 @@ export class Main {
const end = Date.now() const end = Date.now()
const timeNeeded = Math.floor((end - start) / 1000) 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, inReplyToId: notice.id,
visibility: "direct" visibility: "direct"
}) })
@ -50,7 +50,7 @@ export class Main {
console.error(e) console.error(e)
const end = Date.now() const end = Date.now()
const timeNeeded = Math.floor((end - start) / 1000) 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, inReplyToId: notice.id,
visibility: "direct" 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...") console.log("Fetching recent changesets...")
const osmcha = new OsmCha(this._config) const osmcha = new OsmCha(this._config)
const today = new Date() const today = new Date()
const overviewSettings = action.mapCompleteUsageOverview
let changesets: ChangeSetData[] = [] let changesets: ChangeSetData[] = []
const days = overviewSettings.numberOfDays ?? 1 const days = action.numberOfDays ?? 1
if (days < 1) { if (days < 1) {
throw new Error("Invalid config: numberOfDays should be >= 1") throw new Error("Invalid config: numberOfDays should be >= 1")
} }
@ -82,19 +81,19 @@ export class Main {
} }
if (overviewSettings.themeWhitelist?.length > 0) { if (action.themeWhitelist?.length > 0) {
const allowedThemes = new Set(overviewSettings.themeWhitelist) const allowedThemes = new Set(action.themeWhitelist)
const beforeCount = changesets.length const beforeCount = changesets.length
changesets = changesets.filter(cs => allowedThemes.has(cs.properties.theme)) changesets = changesets.filter(cs => allowedThemes.has(cs.properties.theme))
if (changesets.length == 0) { if (changesets.length == 0) {
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", overviewSettings.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...") 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))
} }