171 lines
4.8 KiB
JavaScript
171 lines
4.8 KiB
JavaScript
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.credentials = credentials
|
|
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', {})
|
|
}
|
|
async api(url, data=null) {
|
|
const response = await fetch("https://serv.amazingmarvin.com/api/" + url, {
|
|
method: data ? "POST" : "GET",
|
|
body: data ? JSON.stringify(data) : null,
|
|
headers: {
|
|
"X-API-Token": this.credentials.apiToken,
|
|
'Content-Type': 'application/json',
|
|
"Accept": "application/json"
|
|
// todo: full access token
|
|
}
|
|
})
|
|
if (response.ok) {
|
|
if (response.headers.get('content-type').startsWith("application/json")) {
|
|
return response.json()
|
|
}
|
|
return response.text()
|
|
}
|
|
else throw [response.status, await response.text()]
|
|
}
|
|
async test() {
|
|
// todo: throw?
|
|
return (await this.api("test", {})) == "OK"
|
|
}
|
|
trackedItem() {
|
|
// todo: wrap in Task?
|
|
return this.api("trackedItem")
|
|
}
|
|
todayItems() {
|
|
return this.api("todayItems")
|
|
}
|
|
dueItems() {
|
|
return this.api("dueItems")
|
|
}
|
|
categories() {
|
|
return this.api("categories")
|
|
}
|
|
labels() {
|
|
return this.api("labels")
|
|
}
|
|
me() {
|
|
return this.api("me")
|
|
}
|
|
trackInfo(taskIds) {
|
|
return this.api("tracks", { taskIds })
|
|
}
|
|
}
|
|
|
|
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
|