4 Commits
1.0.0 ... 1.0.4

4 changed files with 75 additions and 41 deletions

View File

@ -2,7 +2,7 @@
## =================================== ## ===================================
import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging, import asyncdispatch, base64, bcrypt, cliutils, docopt, jester, json, logging,
ospaths, sequtils, strutils, os, times, uuids ospaths, sequtils, strutils, os, tables, times, uuids
import nre except toSeq import nre except toSeq
@ -37,10 +37,25 @@ proc loadApiConfig*(json: JsonNode): PtkApiCfg =
dataDir: json.getOrFail("dataDir").getStr, dataDir: json.getOrFail("dataDir").getStr,
users: json.getIfExists("users").getElems(@[]).mapIt(parseUser(it))) users: json.getIfExists("users").getElems(@[]).mapIt(parseUser(it)))
template halt(code: HttpCode,
headers: RawHeaders,
content: string): typed =
## Immediately replies with the specified request. This means any further
## code will not be executed after calling this template in the current
## route.
bind TCActionSend, newHttpHeaders
result[0] = CallbackAction.TCActionSend
result[1] = code
result[2] = some(headers)
result[3] = content
result.matched = true
break allRoutes
template checkAuth(cfg: PtkApiCfg) = template checkAuth(cfg: PtkApiCfg) =
## Check this request for authentication and authorization information. ## Check this request for authentication and authorization information.
## If the request is not authorized, this template sets up the 401 response ## If the request is not authorized, this template immediately returns a
## correctly. The calling context needs only to return from the route. ## 401 Unauthotized response
var authed {.inject.} = false var authed {.inject.} = false
var user {.inject.}: PtkUser = PtkUser() var user {.inject.}: PtkUser = PtkUser()
@ -67,13 +82,12 @@ template checkAuth(cfg: PtkApiCfg) =
except: except:
stderr.writeLine "Auth failed: " & getCurrentExceptionMsg() stderr.writeLine "Auth failed: " & getCurrentExceptionMsg()
response.data[0] = CallbackAction.TCActionSend halt(
response.data[1] = Http401 Http401,
response.data[2]["WWW_Authenticate"] = "Basic" @{"Content-Type": TXT, "WWW_Authenticate": "Basic" },
response.data[2]["Content-Type"] = TXT getCurrentExceptionMsg())
response.data[3] = getCurrentExceptionMsg()
proc parseAndRun(user: PtkUser, cmd: string, params: StringTableRef): string = proc parseAndRun(user: PtkUser, cmd: string, params: Table[string, string]): string =
var args = queryParamsToCliArgs(params) var args = queryParamsToCliArgs(params)
args = @[cmd, "--file", user.timelinePath] & args args = @[cmd, "--file", user.timelinePath] & args
@ -124,25 +138,25 @@ proc start_api*(cfg: PtkApiCfg) =
get "/version": resp("ptk v" & PTK_VERSION, TXT) get "/version": resp("ptk v" & PTK_VERSION, TXT)
get "/marks": get "/marks":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
try: resp(parseAndRun(user, "list", request.params), TXT) try: resp(parseAndRun(user, "list", request.params), TXT)
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/continue": post "/continue":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
try: resp(parseAndRun(user, "continue", request.params), TXT) try: resp(parseAndRun(user, "continue", request.params), TXT)
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/sum-time": post "/sum-time":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
try: resp(parseAndRun(user, "sum-time", request.params), TXT) try: resp(parseAndRun(user, "sum-time", request.params), TXT)
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/mark": post "/mark":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
var newMark: Mark var newMark: Mark
try: newMark = apiParseMark(parseJson(request.body)) try: newMark = apiParseMark(parseJson(request.body))
@ -156,7 +170,7 @@ proc start_api*(cfg: PtkApiCfg) =
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/stop": post "/stop":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
var newMark: Mark var newMark: Mark
try: try:
@ -174,7 +188,7 @@ proc start_api*(cfg: PtkApiCfg) =
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/resume/@id": post "/resume/@id":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
var timeline: Timeline var timeline: Timeline
try: timeline = loadTimeline(user.timelinePath) try: timeline = loadTimeline(user.timelinePath)
@ -200,7 +214,7 @@ proc start_api*(cfg: PtkApiCfg) =
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/amend/@id": post "/amend/@id":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
try: try:
var timeline = loadTimeline(user.timelinePath) var timeline = loadTimeline(user.timelinePath)
@ -216,7 +230,7 @@ proc start_api*(cfg: PtkApiCfg) =
except: resp(Http500, getCurrentExceptionMsg(), TXT) except: resp(Http500, getCurrentExceptionMsg(), TXT)
post "/users": post "/users":
checkAuth(cfg); if not authed: return true checkAuth(cfg)
if not user.isAdmin: resp(Http403, "insufficient permission", TXT) if not user.isAdmin: resp(Http403, "insufficient permission", TXT)
var newUser: PtkUser var newUser: PtkUser
@ -229,7 +243,7 @@ proc start_api*(cfg: PtkApiCfg) =
newUser.timelinePath = cfg.dataDir / newUser.username & ".timeline.json" newUser.timelinePath = cfg.dataDir / newUser.username & ".timeline.json"
try: try:
discard parseAndRun(newUser, "init", newStringTable()) discard parseAndRun(newUser, "init", initTable[string,string]())
# TODO: save updated config! # TODO: save updated config!
# cfg.users.add(newUser) # cfg.users.add(newUser)
resp(Http200, "ok", TXT) resp(Http200, "ok", TXT)

View File

@ -1 +1 @@
const PTK_VERSION* = "1.0.0" const PTK_VERSION* = "1.0.4"

45
ptk.nim
View File

@ -4,7 +4,9 @@
## Simple time keeping CLI ## Simple time keeping CLI
import algorithm, docopt, json, langutils, logging, os, nre, sequtils, import algorithm, docopt, json, langutils, logging, os, nre, sequtils,
sets, strutils, tempfile, terminal, times, timeutils, uuids sets, strutils, tempfile, terminal, times, uuids
import timeutils except `-`;
import private/util import private/util
import private/api import private/api
@ -18,18 +20,18 @@ proc exitErr(msg: string): void =
fatal "ptk: " & msg fatal "ptk: " & msg
quit(QuitFailure) quit(QuitFailure)
proc flexFormat(i: TimeInterval): string = proc flexFormat(i: Duration): string =
## Pretty-format a time interval. ## Pretty-format a time interval.
let fmt = let fmt =
if i > 1.days: "d'd' H'h' m'm'" if i > initDuration(days = 1): "d'd' H'h' m'm'"
elif i >= 1.hours: "H'h' m'm'" elif i >= initDuration(hours = 1): "H'h' m'm'"
elif i >= 1.minutes: "m'm' s's'" elif i >= initDuration(minutes = 1): "m'm' s's'"
else: "s's'" else: "s's'"
return i.format(fmt) return i.format(fmt)
type WriteData = tuple[idx: int, mark: Mark, prefixLen: int, interval: TimeInterval] type WriteData = tuple[idx: int, mark: Mark, prefixLen: int, interval: Duration]
proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): void = proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): void =
## Write a nicely-formatted list of Marks to stdout. ## Write a nicely-formatted list of Marks to stdout.
@ -46,9 +48,9 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
let largestInterval = now - marks[idxs.first].time let largestInterval = now - marks[idxs.first].time
let timeFormat = let timeFormat =
if largestInterval > 1.years: "yyyy-MM-dd HH:mm" if largestInterval > initDuration(days = 365): "yyyy-MM-dd HH:mm"
elif largestInterval > 7.days: "MMM dd HH:mm" elif largestInterval > initDuration(days = 7): "MMM dd HH:mm"
elif largestInterval > 1.days: "ddd HH:mm" elif largestInterval > initDuration(days = 1): "ddd HH:mm"
else: "HH:mm" else: "HH:mm"
var toWrite: seq[WriteData] = @[] var toWrite: seq[WriteData] = @[]
@ -57,7 +59,7 @@ proc writeMarks(timeline: Timeline, indices: seq[int], includeNotes = false): vo
for i in idxs: for i in idxs:
let let
interval: TimeInterval = interval: Duration =
if (i == marks.len - 1): now - marks[i].time if (i == marks.len - 1): now - marks[i].time
else: marks[i + 1].time - marks[i].time else: marks[i + 1].time - marks[i].time
prefix = prefix =
@ -121,10 +123,12 @@ proc doInit(timelineLocation: string): void =
type ExpectedMarkPart = enum Time, Summary, Tags, Notes type ExpectedMarkPart = enum Time, Summary, Tags, Notes
proc edit(mark: var Mark): void = proc edit(mark: Mark): Mark =
## Interactively edit a mark using the editor named in the environment ## Interactively edit a mark using the editor named in the environment
## variable "EDITOR" ## variable "EDITOR"
result = mark
var var
tempFile: File tempFile: File
tempFileName: string tempFileName: string
@ -140,8 +144,10 @@ proc edit(mark: var Mark): void =
tempFile.writeLine( tempFile.writeLine(
"""# Everything from the line below to the end of the file will be considered """# Everything from the line below to the end of the file will be considered
# notes for this timeline mark.""") # notes for this timeline mark.""")
tempFile.write(mark.notes)
close(tempFile) close(tempFile)
tempFile = nil
discard os.execShellCmd "$EDITOR " & tempFileName & " </dev/tty >/dev/tty" discard os.execShellCmd "$EDITOR " & tempFileName & " </dev/tty >/dev/tty"
@ -149,12 +155,13 @@ proc edit(mark: var Mark): void =
for line in lines tempFileName: for line in lines tempFileName:
if strip(line)[0] == '#': continue if strip(line)[0] == '#': continue
elif markPart == Time: mark.time = parseTime(line); markPart = Summary elif markPart == Time: result.time = parseTime(line); markPart = Summary
elif markPart == Summary: mark.summary = line; markPart = Tags elif markPart == Summary: result.summary = line; markPart = Tags
elif markPart == Tags: elif markPart == Tags:
mark.tags = line.split({',', ';'}); result.tags = line.split({',', ';'});
result.notes = ""
markPart = Notes markPart = Notes
else: mark.notes &= line & "\x0D\x0A" else: result.notes &= line & "\x0D\x0A"
finally: close(tempFile) finally: close(tempFile)
@ -422,7 +429,7 @@ Options:
notes: args["--notes"] ?: "", notes: args["--notes"] ?: "",
tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace)) tags: (args["--tags"] ?: "").split({',', ';'}).filterIt(not it.isNilOrWhitespace))
if args["--edit"]: edit(newMark) if args["--edit"]: newMark = edit(newMark)
let prevLastIdx = timeline.marks.getLastIndex() let prevLastIdx = timeline.marks.getLastIndex()
timeline.marks.add(newMark) timeline.marks.add(newMark)
@ -452,7 +459,7 @@ Options:
notes: markToResume.notes, notes: markToResume.notes,
tags: markToResume.tags) tags: markToResume.tags)
if args["--edit"]: edit(newMark) if args["--edit"]: newMark = edit(newMark)
timeline.marks.add(newMark) timeline.marks.add(newMark)
timeline.writeMarks( timeline.writeMarks(
@ -489,7 +496,7 @@ Options:
except: raise newException(ValueError, except: raise newException(ValueError,
"invalid value for --time: " & getCurrentExceptionMsg()) "invalid value for --time: " & getCurrentExceptionMsg())
if args["--edit"]: edit(mark) if args["--edit"]: mark = edit(mark)
timeline.marks.delete(markIdx) timeline.marks.delete(markIdx)
timeline.marks.insert(mark, markIdx) timeline.marks.insert(mark, markIdx)
@ -533,7 +540,7 @@ Options:
if args["sum-time"]: if args["sum-time"]:
var intervals: seq[TimeInterval] = @[] var intervals: seq[Duration] = @[]
if args["--ids"]: if args["--ids"]:
for id in args["<ids>"]: for id in args["<ids>"]:

View File

@ -1,7 +1,6 @@
# Package # Package
include "private/version.nim"
version = PTK_VERSION version = "1.0.4"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Personal Time Keeper" description = "Personal Time Keeper"
license = "MIT" license = "MIT"
@ -9,5 +8,19 @@ bin = @["ptk"]
# Dependencies # Dependencies
requires @["nim >= 0.18.0", "docopt >= 0.6.4", "uuids", "langutils", "tempfile", "timeutils >= 0.2.2", "isaac >= 0.1.2", "bcrypt", "cliutils >= 0.5.0", "jester 0.2.0"] requires @[
"nim >= 1.0.0",
"docopt >= 0.6.8",
"uuids",
"tempfile",
"isaac >= 0.1.3",
"bcrypt",
"jester 0.4.1",
"https://git.jdb-labs.com/jdb/nim-lang-utils.git",
"https://git.jdb-labs.com/jdb/nim-cli-utils.git",
"https://git.jdb-labs.com/jdb/nim-time-utils.git >= 0.5.2",
"https://git.jdb-labs.com/jdb/update-nim-package-version"
]
task updateVersion, "Update the version of this package.":
exec "update_nim_package_version ptk 'private/version.nim'"