Fetch changeset info from OSM api to detect all images (workaround for https://github.com/mapbox/osmcha-frontend/issues/641); send a direct message to bot owner when running; improve usernames of image authors
This commit is contained in:
parent
70a7224160
commit
e7e2d8609f
7 changed files with 131 additions and 56 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"doctest-ts-improved": "^0.8.7",
|
||||
"escape-html": "^1.0.3",
|
||||
"fake-dom": "^1.0.4",
|
||||
|
@ -294,6 +295,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
|
||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz",
|
||||
"integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||
|
@ -3099,6 +3108,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
|
||||
"integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
|
||||
},
|
||||
"@xmldom/xmldom": {
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz",
|
||||
"integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg=="
|
||||
},
|
||||
"abab": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"dependencies": {
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"doctest-ts-improved": "^0.8.7",
|
||||
"escape-html": "^1.0.3",
|
||||
"fake-dom": "^1.0.4",
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class MastodonPoster {
|
|||
|
||||
public async writeMessage(text: string, options?: CreateStatusParamsBase): Promise<{id: string}>{
|
||||
if(this._dryrun){
|
||||
console.log("Dryrun enabled; not posting:\n", text.split("\n").map(txt => " > "+txt).join("\n"))
|
||||
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({
|
||||
|
|
|
@ -40,6 +40,17 @@ export default class OsmUserInfo {
|
|||
|
||||
}
|
||||
|
||||
public async GetMastodonLink(): Promise<string | undefined> {
|
||||
const mastodonLinks = await this.getMeLinks()
|
||||
|
||||
if (mastodonLinks.length <= 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const url = new URL(mastodonLinks[0])
|
||||
return url.pathname.substring(1) + "@" + url.host
|
||||
}
|
||||
|
||||
public async getMeLinks(): Promise<string[]> {
|
||||
const userdata = await this.getUserInfo()
|
||||
const div = document.createElement("div")
|
||||
|
|
|
@ -6,7 +6,7 @@ import Config from "./Config";
|
|||
import MastodonPoster from "./Mastodon";
|
||||
import ImgurAttribution from "./ImgurAttribution";
|
||||
|
||||
type ImageInfo = { image: string, changeset: ChangeSetData, tags: Record<string, string[]> }
|
||||
type ImageInfo = { image: string, changeset: ChangeSetData }
|
||||
|
||||
export class Postbuilder {
|
||||
private static readonly metakeys = [
|
||||
|
@ -114,15 +114,11 @@ export class Postbuilder {
|
|||
async createOverviewForContributor(uid: string, changesetsMade: ChangeSetData[]): Promise<string> {
|
||||
const userinfo = new OsmUserInfo(Number(uid), this._config)
|
||||
const inf = await userinfo.getUserInfo()
|
||||
const mastodonLinks = await userinfo.getMeLinks()
|
||||
|
||||
const themes = new Histogram(changesetsMade, cs => cs.properties.theme)
|
||||
|
||||
let username = inf.display_name
|
||||
if (mastodonLinks.length > 0) {
|
||||
const url = new URL(mastodonLinks[0])
|
||||
username = url.pathname.substring(1) + "@" + url.host
|
||||
}
|
||||
let username = await userinfo.GetMastodonLink() ?? inf.display_name
|
||||
|
||||
const statistics = this.getStatisticsFor(changesetsMade)
|
||||
|
||||
let thematicMaps = "maps " + Utils.commasAnd(themes.keys())
|
||||
|
@ -223,7 +219,12 @@ export class Postbuilder {
|
|||
|
||||
|
||||
const totalStats = this.getStatisticsFor()
|
||||
const {totalImagesCreated, attachmentIds, imgAuthors, totalImageContributorCount} = await this.prepareImages(changesets, 12)
|
||||
const {
|
||||
totalImagesCreated,
|
||||
attachmentIds,
|
||||
imgAuthors,
|
||||
totalImageContributorCount
|
||||
} = await this.prepareImages(changesets, 12)
|
||||
|
||||
let toSend: string[] = [
|
||||
"Yesterday, " + perContributor.keys().length + " different persons made " + totalStats.total + " changes to #OpenStreetMap using https://mapcomplete.osm.be .\n",
|
||||
|
@ -267,17 +268,20 @@ export class Postbuilder {
|
|||
mediaIds: attachmentIds.slice(4, 8)
|
||||
})
|
||||
|
||||
|
||||
const authorNames = Array.from(new Set<string>(imgAuthors))
|
||||
await this._poster.writeMessage([
|
||||
"In total, " + totalImageContributorCount + " different contributors uploaded " + totalImagesCreated + " images.\n",
|
||||
"Images in this thread are randomly selected from them and were made by: ",
|
||||
...Array.from(new Set<string>(imgAuthors)).map(auth => "- "+auth ),
|
||||
...authorNames.map(auth => "- " + auth),
|
||||
"",
|
||||
"Changeset of "+date
|
||||
"All changes were made on " + date
|
||||
|
||||
].join("\n"), {
|
||||
inReplyToId: secondPost["id"],
|
||||
mediaIds: attachmentIds.slice(8, 12)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
@ -288,15 +292,15 @@ export class Postbuilder {
|
|||
|
||||
const images: ImageInfo[] = []
|
||||
for (const changeset of withImage) {
|
||||
const tags = changeset.properties.tag_changes
|
||||
for (const key in tags) {
|
||||
if (!key.startsWith("image")) {
|
||||
continue
|
||||
}
|
||||
const values: string[] = tags[key]
|
||||
for (const image of values) {
|
||||
images.push({image, changeset, tags})
|
||||
}
|
||||
|
||||
const url = this._config.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"))
|
||||
.map(tag => ({k: tag.getAttribute("k"), v: tag.getAttribute("v")}))
|
||||
.filter(kv => kv.k.startsWith("image"))
|
||||
|
||||
for (const kv of osmChangesetTags) {
|
||||
images.push({image: kv.v, changeset})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,18 +308,33 @@ 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._config)
|
||||
authorName = (await authorInfo.GetMastodonLink()) ?? cs.user
|
||||
}catch (e) {
|
||||
console.log("Could not fetch more info about contributor", authorName, cs.uid, "due to", e)
|
||||
}
|
||||
imgAuthors.push(authorName)
|
||||
if (this._config.mastodonAuth.dryrun) {
|
||||
console.log("Not uploading/downloading image:" + randomImage.image + " dryrun")
|
||||
continue
|
||||
}
|
||||
const attribution = await ImgurAttribution.DownloadAttribution(randomImage.image)
|
||||
const id = randomImage.image.substring(randomImage.image.lastIndexOf("/") + 1)
|
||||
const path = this._config.cacheDir + "/image_" + id
|
||||
await Utils.DownloadBlob(randomImage.image, path)
|
||||
const attribution = await ImgurAttribution.DownloadAttribution(randomImage.image)
|
||||
const mediaId = await this._poster.uploadImage(path, "Image taken by " + attribution.author + ", 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)
|
||||
imgAuthors.push(attribution.author)
|
||||
|
||||
}
|
||||
return {
|
||||
attachmentIds,
|
||||
imgAuthors,
|
||||
totalImagesCreated,
|
||||
totalImageContributorCount: new Set(withImage.map(cs => cs.properties.uid)).size
|
||||
}
|
||||
return {attachmentIds, imgAuthors, totalImagesCreated, totalImageContributorCount: new Set(changesets.map(cs => cs.properties.uid)).size}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import https from "https";
|
||||
import * as fs from "fs";
|
||||
import {DOMParser} from '@xmldom/xmldom'
|
||||
|
||||
export default class Utils {
|
||||
public static async DownloadJson(url, headers?: any): Promise<any> {
|
||||
|
@ -25,11 +26,17 @@ export default class Utils {
|
|||
return "" + i
|
||||
}
|
||||
|
||||
public static async DownloadXml(url, headers?: any): Promise<Document> {
|
||||
const content = await Utils.Download(url, {...headers, accept: "application/xml"})
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(content.content, "text/xml");
|
||||
}
|
||||
|
||||
public static Download(url, headers?: any): Promise<{ content: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
headers = headers ?? {}
|
||||
headers.accept = "application/json"
|
||||
headers.accept ??= "application/json"
|
||||
const urlObj = new URL(url)
|
||||
https.get(
|
||||
{
|
||||
|
|
25
src/index.ts
25
src/index.ts
|
@ -5,6 +5,8 @@ import OsmCha, {ChangeSetData} from "./OsmCha";
|
|||
import Config from "./Config";
|
||||
import * as configF from "../config/config.json"
|
||||
import {Postbuilder} from "./Postbuilder";
|
||||
import {Dir} from "fs";
|
||||
import Utils from "./Utils";
|
||||
|
||||
export class Main {
|
||||
|
||||
|
@ -12,6 +14,7 @@ export class Main {
|
|||
|
||||
constructor(config: Config) {
|
||||
this._config = config;
|
||||
this._config.osmBackend ??= "https://www.openstreetmap.org"
|
||||
}
|
||||
|
||||
async main() {
|
||||
|
@ -27,13 +30,33 @@ export class Main {
|
|||
|
||||
const poster = await MastodonPoster.construct(this._config.mastodonAuth)
|
||||
|
||||
const notice = await poster.writeMessage("@pietervdvn@en.osm.town Starting MapComplete bot...",{
|
||||
visibility: "direct"
|
||||
})
|
||||
const start = Date.now()
|
||||
try {
|
||||
|
||||
console.log("Fetching recent changesets...")
|
||||
const osmcha = new OsmCha(this._config)
|
||||
const today = new Date()
|
||||
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()+"-"+ (today.getUTCMonth() + 1) + (today.getUTCDate() - 1))
|
||||
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 timeNeeded= Math.floor ((end - start) / 1000)
|
||||
await poster.writeMessage("Finished running MapComplete bot, this took "+timeNeeded+"seconds",{
|
||||
inReplyToId: notice.id,
|
||||
visibility: "direct"
|
||||
})
|
||||
} catch (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,{
|
||||
inReplyToId: notice.id,
|
||||
visibility: "direct"
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue