Add support for multiple actions

This commit is contained in:
Pieter Vander Vennet 2023-01-16 03:11:44 +01:00
parent e7e2d8609f
commit 5626c4dd8c
5 changed files with 103 additions and 38 deletions

View file

@ -1,5 +1,22 @@
import {LoginSettings} from "./Mastodon"; import {LoginSettings} from "./Mastodon";
export interface MapCompleteUsageOverview {
topContributorsNumberToShow: number,
topThemesNumberToShow: number,
/**
* The number of days to look back.
*/
numberOfDays?: 1 | number
/**
* Only show changes made with this theme.
* If omitted: show all themes
*/
themeWhitelist?: string[]
}
export default interface Config { export default interface Config {
/** /**
* Default: https://www.openstreetmap.org * Default: https://www.openstreetmap.org
@ -18,8 +35,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
}, },
postSettings:{ actions: {mapCompleteUsageOverview : MapCompleteUsageOverview} []
topContributorsNumberToShow: number,
topThemesNumberToShow: number
}
} }

View file

@ -56,7 +56,7 @@ export default class OsmCha {
month: number, month: number,
day: number day: number
): Promise<ChangeSetData[]> { ): Promise<ChangeSetData[]> {
const path = this._cachepath + "_" + year + "_" + Utils. TwoDigits(month) + "_" + Utils. TwoDigits(day) + ".json"; const path = this._cachepath + "_" + year + "_" + Utils.TwoDigits(month) + "_" + Utils.TwoDigits(day) + ".json";
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
try { try {
return JSON.parse(fs.readFileSync(path, "utf8")).features return JSON.parse(fs.readFileSync(path, "utf8")).features
@ -66,15 +66,16 @@ export default class OsmCha {
} }
let page = 1 let page = 1
let allFeatures = [] let allFeatures = []
let endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1) const startDay = new Date(year, month - 1, day)
let endDay = new Date(startDay.getTime() + 1000 * 60 * 60 * 24)
const start_date = year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day)
let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits( let endDate = `${endDay.getFullYear()}-${Utils.TwoDigits(
endDay.getMonth() + 1 endDay.getMonth() + 1
)}-${Utils.TwoDigits(endDay.getDate())}` )}-${Utils.TwoDigits(endDay.getDate())}`
console.log(start_date, "-->", endDate)
let url = this.urlTemplate let url = this.urlTemplate
.replace( .replace("{start_date}", start_date)
"{start_date}",
year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day)
)
.replace("{end_date}", endDate) .replace("{end_date}", endDate)
.replace("{page}", "" + page) .replace("{page}", "" + page)
@ -96,11 +97,11 @@ export default class OsmCha {
while (url) { while (url) {
const result = await Utils.DownloadJson(url, headers) const result = await Utils.DownloadJson(url, headers)
page++ page++
allFeatures.push(...result.features)
if (result.features === undefined) { if (result.features === undefined) {
console.log("ERROR", result) console.log("ERROR", result)
return return
} }
allFeatures.push(...result.features)
url = result.next url = result.next
} }
allFeatures = allFeatures.filter(f => f !== undefined && f !== null) allFeatures = allFeatures.filter(f => f !== undefined && f !== null)

View file

@ -57,7 +57,7 @@ export default class OsmUserInfo {
div.innerHTML = userdata.description div.innerHTML = userdata.description
const links = Array.from(div.getElementsByTagName("a")) const links = Array.from(div.getElementsByTagName("a"))
const meLinks = links.filter(link => link.getAttribute("rel")?.split(" ")?.indexOf("me") >= 0) const meLinks = links.filter(link => link.getAttribute("rel")?.split(" ")?.indexOf("me") >= 0)
return meLinks.map(link => link.href.toString()) //*/ return meLinks.map(link => link.href.toString())
} }
public async getUserInfo(): Promise<UserInfo> { public async getUserInfo(): Promise<UserInfo> {

View file

@ -2,7 +2,7 @@ import Histogram from "./Histogram";
import Utils from "./Utils"; import Utils from "./Utils";
import {ChangeSetData} from "./OsmCha"; import {ChangeSetData} from "./OsmCha";
import OsmUserInfo from "./OsmUserInfo"; import OsmUserInfo from "./OsmUserInfo";
import Config from "./Config"; import Config, {MapCompleteUsageOverview} from "./Config";
import MastodonPoster from "./Mastodon"; import MastodonPoster from "./Mastodon";
import ImgurAttribution from "./ImgurAttribution"; import ImgurAttribution from "./ImgurAttribution";
@ -17,11 +17,14 @@ export class Postbuilder {
"plantnet-ai-detection", "plantnet-ai-detection",
"link-image" "link-image"
] ]
private readonly _config: Config; private readonly _config: MapCompleteUsageOverview;
private readonly _globalConfig: Config
private readonly _poster: MastodonPoster; private readonly _poster: MastodonPoster;
private readonly _changesetsMade: ChangeSetData[]; private readonly _changesetsMade: ChangeSetData[];
constructor(config: Config, poster: MastodonPoster, changesetsMade: ChangeSetData[]) { constructor(config: MapCompleteUsageOverview, globalConfig: Config, poster: MastodonPoster, changesetsMade: ChangeSetData[]) {
this._globalConfig = globalConfig;
this._poster = poster; this._poster = poster;
this._config = config; this._config = config;
// Ignore 'custom' themes, they can be confusing for uninitiated users and give ugly link + we don't endorse them // Ignore 'custom' themes, they can be confusing for uninitiated users and give ugly link + we don't endorse them
@ -112,7 +115,7 @@ export class Postbuilder {
async createOverviewForContributor(uid: string, changesetsMade: ChangeSetData[]): Promise<string> { async createOverviewForContributor(uid: string, changesetsMade: ChangeSetData[]): Promise<string> {
const userinfo = new OsmUserInfo(Number(uid), this._config) const userinfo = new OsmUserInfo(Number(uid), this._globalConfig)
const inf = await userinfo.getUserInfo() const inf = await userinfo.getUserInfo()
const themes = new Histogram(changesetsMade, cs => cs.properties.theme) const themes = new Histogram(changesetsMade, cs => cs.properties.theme)
@ -170,7 +173,7 @@ export class Postbuilder {
const props = image.changeset.properties const props = image.changeset.properties
const uid = "" + props.uid const uid = "" + props.uid
if (result.indexOf(image) >= 0) { if (result.findIndex(i => i.image === image.image) >= 0) {
continue continue
} }
@ -226,11 +229,15 @@ export class Postbuilder {
totalImageContributorCount totalImageContributorCount
} = await this.prepareImages(changesets, 12) } = await this.prepareImages(changesets, 12)
let timePeriod = "Yesterday"
if(this._config.numberOfDays > 1){
timePeriod = "In the past "+this._config.numberOfDays+" days"
}
let toSend: string[] = [ let toSend: string[] = [
"Yesterday, " + perContributor.keys().length + " different persons made " + totalStats.total + " changes to #OpenStreetMap using https://mapcomplete.osm.be .\n", timePeriod+", " + perContributor.keys().length + " different persons made " + totalStats.total + " changes to #OpenStreetMap using https://mapcomplete.osm.be .\n",
] ]
for (let i = 0; i < this._config.postSettings.topContributorsNumberToShow - 1 && i < topContributors.length; i++) { for (let i = 0; i < this._config.topContributorsNumberToShow - 1 && i < topContributors.length; i++) {
const uid = topContributors[i].key const uid = topContributors[i].key
const changesetsMade = perContributor.get(uid) const changesetsMade = perContributor.get(uid)
try { try {
@ -257,7 +264,7 @@ export class Postbuilder {
dropZeroValues: true dropZeroValues: true
}) })
toSend.push("") toSend.push("")
for (let i = 0; i < this._config.postSettings.topThemesNumberToShow && i < mostPopularThemes.length; i++) { for (let i = 0; i < this._config.topThemesNumberToShow && i < mostPopularThemes.length; i++) {
const theme = mostPopularThemes[i].key const theme = mostPopularThemes[i].key
const changesetsMade = perTheme.get(theme) const changesetsMade = perTheme.get(theme)
toSend.push(await this.createOverviewForTheme(theme, changesetsMade)) toSend.push(await this.createOverviewForTheme(theme, changesetsMade))
@ -275,7 +282,7 @@ 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 "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: secondPost["id"],
@ -291,15 +298,20 @@ export class Postbuilder {
const totalImagesCreated = Utils.Sum(withImage.map(cs => cs.properties["add-image"])) const totalImagesCreated = Utils.Sum(withImage.map(cs => cs.properties["add-image"]))
const images: ImageInfo[] = [] const images: ImageInfo[] = []
const seenURLS = new Set<string>()
for (const changeset of withImage) { for (const changeset of withImage) {
const url = this._config.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"))
.map(tag => ({k: tag.getAttribute("k"), v: tag.getAttribute("v")})) .map(tag => ({k: tag.getAttribute("k"), v: tag.getAttribute("v")}))
.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)){
continue
}
seenURLS.add(kv.v)
images.push({image: kv.v, changeset}) images.push({image: kv.v, changeset})
} }
} }
@ -312,19 +324,19 @@ export class Postbuilder {
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._config) const authorInfo = new OsmUserInfo(Number(cs.uid), this._globalConfig)
authorName = (await authorInfo.GetMastodonLink()) ?? cs.user authorName = (await authorInfo.GetMastodonLink()) ?? 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)
if (this._config.mastodonAuth.dryrun) { if (this._globalConfig.mastodonAuth.dryrun) {
console.log("Not uploading/downloading image:" + randomImage.image + " dryrun") console.log("Not uploading/downloading image:" + randomImage.image + " dryrun")
continue continue
} }
const attribution = await ImgurAttribution.DownloadAttribution(randomImage.image) const attribution = await ImgurAttribution.DownloadAttribution(randomImage.image)
const id = randomImage.image.substring(randomImage.image.lastIndexOf("/") + 1) const id = randomImage.image.substring(randomImage.image.lastIndexOf("/") + 1)
const path = this._config.cacheDir + "/image_" + id const path = this._globalConfig.cacheDir + "/image_" + id
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)

View file

@ -2,10 +2,9 @@ import * as fakedom from "fake-dom"
import * as fs from "fs" import * as fs from "fs"
import MastodonPoster from "./Mastodon"; import MastodonPoster from "./Mastodon";
import OsmCha, {ChangeSetData} from "./OsmCha"; import OsmCha, {ChangeSetData} from "./OsmCha";
import Config from "./Config"; import Config, {MapCompleteUsageOverview} from "./Config";
import * as configF from "../config/config.json" import * as configF from "../config/config.json"
import {Postbuilder} from "./Postbuilder"; import {Postbuilder} from "./Postbuilder";
import {Dir} from "fs";
import Utils from "./Utils"; import Utils from "./Utils";
export class Main { export class Main {
@ -30,29 +29,28 @@ export class Main {
const poster = await MastodonPoster.construct(this._config.mastodonAuth) 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"
}) })
const start = Date.now() const start = Date.now()
try { try {
console.log("Fetching recent changesets...") for (const action of this._config.actions) {
const osmcha = new OsmCha(this._config) console.log("Running action", action)
const today = new Date() await this.runMapCompleteOverviewAction(poster, action)
let changesets: ChangeSetData[] = await osmcha.DownloadStatsForDay(today.getUTCFullYear(), today.getUTCMonth() + 1, today.getUTCDate() - 1) }
console.log("Building post...")
await new Postbuilder(this._config, poster, changesets).buildMessage(today.getUTCFullYear() + "-" + Utils.TwoDigits(today.getUTCMonth() + 1) +"-"+ Utils.TwoDigits (today.getUTCDate() - 1))
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("Finished running MapComplete bot, this took " + timeNeeded + "seconds", {
inReplyToId: notice.id, inReplyToId: notice.id,
visibility: "direct" visibility: "direct"
}) })
} catch (e) { } catch (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("Running MapComplete bot failed in " + timeNeeded + "seconds, the error is " + e, {
inReplyToId: notice.id, inReplyToId: notice.id,
visibility: "direct" visibility: "direct"
}) })
@ -60,6 +58,46 @@ export class Main {
} }
private async runMapCompleteOverviewAction(poster: MastodonPoster, action: { mapCompleteUsageOverview: 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
if (days < 1) {
throw new Error("Invalid config: numberOfDays should be >= 1")
}
for (let i = 0; i < days; i++) {
const targetDay = new Date(today.getTime() - 24 * 60 * 60 * 1000 * (i + 1))
let changesetsDay: ChangeSetData[] = await osmcha.DownloadStatsForDay(targetDay.getUTCFullYear(), targetDay.getUTCMonth() + 1, targetDay.getUTCDate())
for (const changeSetDatum of changesetsDay) {
if (changeSetDatum.properties.theme === undefined) {
console.warn("Changeset", changeSetDatum.id, " does not have theme given")
} else {
changesets.push(changeSetDatum)
}
}
}
if (overviewSettings.themeWhitelist?.length > 0) {
const allowedThemes = new Set(overviewSettings.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("Filtering for ", overviewSettings.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))
}
} }