initial commit

master
Yorick van Pelt 2021-11-10 13:40:12 +01:00
commit 76a9301b0b
Signed by: yorick
GPG Key ID: D8D3CC6D951384DE
4 changed files with 241 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/beeminder-token
/credentials

79
Beeminder.js Normal file
View File

@ -0,0 +1,79 @@
const fetch = require('node-fetch')
class Beeminder {
constructor(token) {
this.token = token
}
async post_request(url, post_data, method="POST") {
const response = await fetch("https://www.beeminder.com/api/v1/" + url + ".json", {
method: 'POST',
body: JSON.stringify(Object.assign({}, post_data, {auth_token: this.token})),
headers: {'Content-Type': 'application/json'}
})
if (response.ok || response.status == 422) {
return response.json()
} else {
throw [response.status, await response.text()]
}
}
async get_request(url, params={}) {
const qs = new URLSearchParams(Object.assign({}, params, {auth_token: this.token}))
const response = await fetch("https://www.beeminder.com/api/v1/" + url + ".json?" + qs.toString())
if (response.ok || response.status == 422) {
return response.json()
} else {
throw [response.status, await response.text()]
}
}
user(user="me") {
return new User(this, user)
}
charge(amount, note, dryrun=true) {
return this.b.post_request(`charges`, {amount, note, dryrun})
}
}
class User {
constructor(b, user) {
Object.assign(this, {b, user})
}
info() {
return this.b.get_request(`users/${this.user}`)
}
goals() {
return this.b.get_request(`users/${this.user}/goals`)
}
goal(goal) {
return new Goal(this.b, this.user, goal)
}
create_goal(params) {
return this.b.post_request(`users/${this.user}/goals`, params, 'POST')
}
}
class Goal {
constructor(b, user, goal) {
Object.assign(this, {b, user, goal})
this.prefix = `users/${this.user}/goals/${this.goal}`
}
info() {
return this.b.get_request(`${this.prefix}`)
}
update(params) {
return this.b.post_request(`${this.prefix}`, params, 'PUT')
}
refresh_graph() {
return this.b.get_request(`${this.prefix}/refresh_graph`)
}
create_datapoint(params) {
return this.b.post_request(`${this.prefix}/datapoints`, params)
}
create_datapoints(params) {
return this.b.post_request(`${this.prefix}/datapoints/create_all`, params)
}
datapoints(params) {
return this.get_request(`${this.prefix}/datapoints`, params)
}
// todo: put, delete datapoint
}
module.exports = Beeminder

124
Marvin.js Normal file
View File

@ -0,0 +1,124 @@
const fs = require('fs')
const PouchDB = require('pouchdb')
const {DateTime, Interval, Duration} = require('luxon')
const util = require('util')
PouchDB.plugin(require('pouchdb-find'))
const fetch = require('node-fetch')
Duration.zero = Duration.fromMillis(0)
class Marvin {
constructor(credentials=Marvin.parseCredentials()) {
const c = credentials
let u = new URL(c.syncServer)
Object.assign(u, {
username: c.syncUser,
password: c.syncPassword,
pathname: "/" + c.syncDatabase
})
this.remote = new PouchDB(u.href)
this.db = new PouchDB("marvin")
this.synced = new Promise((resolve, reject) => {
this.remote.replicate.to(this.db)
.on('complete', resolve)
.on('error', reject)
})
this.synced.then(() => {
console.log("sync complete!")
})
this.root = new Taskset(this, 'root', {})
}
}
function chunk(xs, n) {
if ((xs.length % n) != 0) console.error("wrong chunk length")
const res = new Array(xs.length / n)
for(let i = 0; i < xs.length; i += n) {
res[i/n] = xs.slice(i, i+n)
}
return res
}
function toInterval([start, end]) {
return Interval.fromDateTimes(DateTime.fromMillis(start), DateTime.fromMillis(end))
}
Duration.sum = function sumDurations(durs) {
return durs.reduce((p, c) => p.plus(c), Duration.zero)
}
class Task {
constructor(task) {
this._task = task
Object.assign(this, {
title: task.title,
day: task.day,
dueDate: task.dueDate,
done: !!task.done,
doneAt: task.doneAt ? DateTime.fromMillis(task.doneAt) : null,
createdAt: DateTime.fromMillis(task.createdAt),
times: task.times ? chunk(task.times, 2).map(toInterval) : []
})
}
timeSpentOn(interval) {
return Duration.sum(this.times.map(time => time.overlaps(interval) ? time.intersection(interval).toDuration() : Duration.zero))
}
[util.inspect.custom](depth, opts) {
return {
title: this.title,
day: this.day,
dueDate: this.dueDate,
done: this.done,
doneAt: this.doneAt ? this.doneAt.toISO() : null,
createdAt: this.createdAt.toISO(),
times: this.times
}
}
}
class Taskset {
constructor(marv, _id, rest) {
Object.assign(this, {marv, _id}, rest)
}
async category(n) {
await this.marv.synced
const d = await this.marv.db.find({
selector: {
db: "Categories",
title: n,
parentId: this._id
}
})
if (d && d.docs) return new Taskset(this.marv, d.docs[0]._id, d.docs[0])
}
async tasks() {
await this.marv.synced
const d = await this.marv.db.find({
selector: {
db: "Tasks",
parentId: this._id
},
})
if (d) {
return d.docs.map(x => new Task(x))
}
}
async tasksOverlapping(interval) {
const tasks = await this.tasks()
return tasks.filter(task =>
task.times.some(x => x.overlaps(interval))
)
}
}
Marvin.parseCredentials = (fileContents = fs.readFileSync("credentials", "utf8")) => {
const res = {}
for(const line of fileContents.split('\n')) {
if (line.startsWith('# ') || !line) continue
const [k,v] = line.split(': ', 2)
res[k] = v
}
return res
}
module.exports = Marvin

36
index.js Normal file
View File

@ -0,0 +1,36 @@
const Beeminder = require('./Beeminder.js')
const Marvin = require('./Marvin.js')
const fs = require('fs')
const BEEMINDER_TOKEN = fs.readFileSync("./beeminder-token", 'utf8').strip()
const {DateTime, Interval, Duration} = require('luxon')
function sleep(n) {
return new Promise((resolve, reject) => {
setTimeout(resolve, n)
})
}
if (require.main === module) {
(async function() {
const m = new Marvin()
const d = DateTime.local()
const today = Interval.fromDateTimes(d.startOf('day'), d.endOf('day'))
const tasks = await (m.root.category('Tweag').then(tweag => tweag.tasksOverlapping(today)))
const value = Duration.sum(tasks.map(task => task.timeSpentOn(today))).as('hours')
const b = new Beeminder(BEEMINDER_TOKEN)
const goal = b.user("me").goal("loonslaven")
const daystamp = d.toFormat('yyyyMMdd')
const post_data = {
daystamp,
value,
requestid: daystamp,
comment: "from marvin"
}
console.log(post_data)
console.log(await goal.create_datapoint(post_data))
await sleep(2000)
console.log((await goal.info()).safesum)
})()
}