-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bot-monitor: support for email alerts
- Loading branch information
1 parent
d0a9af9
commit 042697f
Showing
9 changed files
with
210 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { argv, bot, log, Mwn } from "../botbase"; | ||
import {Rule, RuleError, Monitor} from "./index"; | ||
import { argv, bot, log, mailTransporter, Mwn } from "../botbase"; | ||
import {Rule, RuleError, Monitor, subtractFromNow, alertsDb} from "./index"; | ||
|
||
export class Alert { | ||
rule: Rule | ||
|
@@ -11,9 +11,11 @@ export class Alert { | |
} | ||
|
||
async alert() { | ||
if (!argv.dry && this.rule.alertPage) { | ||
await this.alertTalkPage(); | ||
} | ||
if (argv.dry) return; | ||
await Promise.all([ | ||
this.rule.alertPage && this.alertTalkPage(), | ||
this.rule.email && this.alertEmail(), | ||
]); | ||
} | ||
|
||
async alertTalkPage() { | ||
|
@@ -27,7 +29,7 @@ export class Alert { | |
log(`[i] Notifying for ${this.rule.bot}`); | ||
await page.newSection( | ||
header, | ||
this.getMessage() + ' – ~~~~', | ||
this.getTalkMessage() + ' – ~~~~', | ||
{ redirect: true, nocreate: true } | ||
).catch(err => { | ||
if (err.code === 'missingtitle') { | ||
|
@@ -38,7 +40,7 @@ export class Alert { | |
}); | ||
} | ||
|
||
getMessage() { | ||
getTalkMessage() { | ||
return Mwn.template('subst:Wikipedia:Bot activity monitor/Notification', { | ||
bot: this.rule.bot, | ||
task: this.rule.task, | ||
|
@@ -49,30 +51,55 @@ export class Alert { | |
}); | ||
} | ||
|
||
// async alert() { | ||
// if (this.rule.alertMode === 'talkpage') { | ||
// await this.alertTalkPage(); | ||
// } else if (this.rule.alertMode === 'email') { | ||
// await this.alertEmail(); | ||
// } else if (this.rule.alertMode === 'ping') { | ||
// await this.alertPing(); | ||
// } else { | ||
// throw new RuleError(`Invalid alert mode: ${this.rule.alertMode}: must be "talkpage", "email" or "ping"`); | ||
// } | ||
// } | ||
// async alertEmail() { | ||
// await new bot.user(this.rule.emailUser).email( | ||
// this.getHeader(), | ||
// this.getMessage(), | ||
// {ccme: true} | ||
// ).catch(err => { | ||
// if (err.code === 'notarget') { | ||
// throw new RuleError(`Invalid username for email: ${this.rule.emailUser}`); | ||
// } else if (err.code === 'nowikiemail') { | ||
// throw new RuleError(`Email is disabled for ${this.rule.emailUser}`); | ||
// } else throw err; | ||
// }); | ||
// } | ||
async alertEmail() { | ||
let lastAlertedTime = await alertsDb.getLastEmailedTime(this.rule); | ||
if (lastAlertedTime.isAfter(subtractFromNow(this.rule.duration, 1))) { | ||
log(`[i] Aborting email for "${this.name}" because one was already sent in the last ${this.rule.duration}`); | ||
return; | ||
} | ||
log(`[i] Sending email for "${this.name}" to ${this.rule.email}`); | ||
let subject = `[${this.rule.bot}] ${this.rule.task} failure`; | ||
|
||
if (this.rule.email.includes('@')) { | ||
await mailTransporter.sendMail({ | ||
from: '[email protected]', | ||
to: this.rule.email, | ||
subject: subject, | ||
html: this.getEmailBodyHtml(), | ||
}); | ||
} else { | ||
await new bot.User(this.rule.email).email( | ||
subject, | ||
this.getEmailBodyPlain(), | ||
{ccme: true} | ||
).catch(err => { | ||
if (err.code === 'notarget') { | ||
throw new RuleError(`Invalid username for email: ${this.rule.email}`); | ||
} else if (err.code === 'nowikiemail') { | ||
throw new RuleError(`Email is disabled for ${this.rule.email}`); | ||
} else throw err; | ||
}); | ||
} | ||
await alertsDb.saveLastEmailedTime(this.rule).catch(async () => { | ||
// Try that again, we don't want to send duplicate emails! | ||
await alertsDb.saveLastEmailedTime(this.rule); | ||
}); | ||
} | ||
|
||
getEmailBodyHtml(): string { | ||
return `${this.rule.bot}'s task <b>${this.rule.task}</b> failed to run per the configuration specified at <a href="https://en.wikipedia.org/wiki/Wikipedia:Bot_activity_monitor/Configurations">Wikipedia:Bot activity monitor/Configurations</a>. Detected only ${this.actions} ${this.rule.action === 'edit' ? 'edit' : `"${this.rule.action}" action`}s in the last ${this.rule.duration}, whereas at least ${this.rule.minEdits} were expected.` + | ||
`<br><br>` + | ||
`If your bot is behaving as expected, then you may want to <a href="https://en.wikipedia.org/wiki/Wikipedia:Bot_activity_monitor/Configurations?action=edit">modify the task configuration instead</a>. Or to unsubscribe from these email notifications, remove the |email= parameter from the {{/task}} template.` + | ||
`<br><br>` + | ||
`Thanks!`; | ||
} | ||
|
||
getEmailBodyPlain(): string { | ||
return `${this.rule.bot}'s task "${this.rule.task}" failed to run per the configuration specified at Wikipedia:Bot activity monitor/Configurations (<https://en.wikipedia.org/wiki/Wikipedia:Bot_activity_monitor/Configurations>). Detected only ${this.actions} ${this.rule.action === 'edit' ? 'edit' : `"${this.rule.action}" action`}s in the last ${this.rule.duration}, whereas at least ${this.rule.minEdits} were expected.` + | ||
`\n\n` + | ||
`If your bot is behaving as expected, then you may want to modify the task configuration instead. Or to unsubscribe from these email notifications, remove the |email= parameter from the {{/task}} template. Thanks!`; | ||
} | ||
|
||
// static pingpage = 'Wikipedia:Bot activity monitor/Pings' | ||
// async alertPing() { | ||
// let pingUser = this.rule.pingUser || await getBotOperator(this.rule.bot) || this.rule.bot; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import {Cassandra} from "../cassandra"; | ||
import {MwnDate} from "mwn"; | ||
import {getKey, Rule} from "./Rule"; | ||
import {bot, toolsdb} from "../botbase"; | ||
import * as fs from "fs/promises"; | ||
import {getRedisInstance, Redis} from "../redis"; | ||
import {createLocalSSHTunnel} from "../utils"; | ||
import {TOOLS_DB_HOST} from "../db"; | ||
|
||
interface AlertsDb { | ||
connect(): Promise<void>; | ||
getLastEmailedTime(rule: Rule): Promise<MwnDate>; | ||
saveLastEmailedTime(rule: Rule): Promise<void>; | ||
} | ||
|
||
class MariadbAlertsDb implements AlertsDb { | ||
db: toolsdb; | ||
async connect(): Promise<void> { | ||
await createLocalSSHTunnel(TOOLS_DB_HOST); | ||
this.db = new toolsdb('botmonitor'); | ||
await this.db.run(` | ||
CREATE TABLE IF NOT EXISTS alerts( | ||
name VARCHAR(255), | ||
lastEmailed TIMESTAMP | ||
) | ||
`); | ||
} | ||
|
||
async getLastEmailedTime(rule: Rule): Promise<MwnDate> { | ||
let data = await this.db.query( | ||
`SELECT lastEmailed FROM alerts WHERE name = ?`, | ||
[ getKey(rule, 250) ] | ||
); | ||
return data[0] ? new bot.Date(data[0].lastEmailed) : new bot.Date(0); | ||
} | ||
|
||
async saveLastEmailedTime(rule: Rule): Promise<void> { | ||
await this.db.run( | ||
`REPLACE INTO alerts (name, lastEmailed) VALUES(?, NOW())`, | ||
[ getKey(rule, 250) ] | ||
); | ||
} | ||
} | ||
|
||
class CassandraAlertsDb implements AlertsDb { | ||
cs: Cassandra = new Cassandra(); | ||
|
||
async connect() { | ||
await this.cs.connect(); | ||
} | ||
|
||
async getLastEmailedTime(rule: Rule): Promise<MwnDate> { | ||
let data= await this.cs.execute( | ||
'SELECT lastEmailed FROM botMonitor WHERE name = ?', | ||
[ getKey(rule) ] | ||
); | ||
return new bot.date(data.rows[0].get('lastEmailed')); | ||
} | ||
|
||
async saveLastEmailedTime(rule: Rule) { | ||
await this.cs.execute( | ||
'UPDATE botMonitor SET lastEmailed = toTimestamp(now()) WHERE name = ?', | ||
[ getKey(rule) ] | ||
); | ||
} | ||
} | ||
|
||
class FileSystemAlertsDb implements AlertsDb { | ||
file = 'alerts_db.json'; | ||
data: Record<string, any>; | ||
|
||
async connect() { | ||
this.data = JSON.parse((await fs.readFile(this.file)).toString()); | ||
} | ||
|
||
async getLastEmailedTime(rule: Rule): Promise<MwnDate> { | ||
return new bot.date(this.data[getKey(rule)].lastEmailed); | ||
} | ||
async saveLastEmailedTime(rule: Rule): Promise<void> { | ||
if (!this.data[getKey(rule)]) { | ||
this.data[getKey(rule)] = {}; | ||
} | ||
this.data[getKey(rule)].lastEmailed = new bot.date().toISOString(); | ||
|
||
// only needed on the last save, but done everytime anyway | ||
await fs.writeFile(this.file, JSON.stringify(this.data)); | ||
} | ||
} | ||
|
||
class RedisAlertsDb implements AlertsDb { | ||
redis: Redis; | ||
async connect() { | ||
this.redis = getRedisInstance(); | ||
} | ||
|
||
async getLastEmailedTime(rule: Rule) { | ||
return new bot.date(await this.redis.hget('botmonitor-last-emailed', getKey(rule))); | ||
} | ||
async saveLastEmailedTime(rule: Rule) { | ||
await this.redis.hset('botmonitor-last-emailed', getKey(rule), new bot.date().toISOString); | ||
} | ||
} | ||
|
||
export const alertsDb: AlertsDb = new MariadbAlertsDb(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.