Compare commits
8 Commits
v0.0.0
...
1816500617
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1816500617 | ||
|
|
fe6b87563f | ||
|
|
273b412e30 | ||
|
|
da6097c5e2 | ||
|
|
365a857e2a | ||
|
|
40d5740817 | ||
|
|
933f87d11d | ||
|
|
a8b5942833 |
99
http.go
Normal file
99
http.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
DB DB
|
||||
User IDU
|
||||
}
|
||||
|
||||
func HTTP(port int, db DB) error {
|
||||
foo := func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
httpRoot(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
foo = withAuth(foo)
|
||||
foo = withDB(foo, db)
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(foo))
|
||||
}
|
||||
|
||||
func extract(ctx context.Context) Context {
|
||||
v := ctx.Value("__context")
|
||||
v2, _ := v.(Context)
|
||||
return v2
|
||||
}
|
||||
|
||||
func inject(ctx context.Context, v Context) context.Context {
|
||||
return context.WithValue(ctx, "__context", v)
|
||||
}
|
||||
|
||||
func withDB(foo http.HandlerFunc, db DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
c := extract(r.Context())
|
||||
c.DB = db
|
||||
r = r.WithContext(inject(r.Context(), c))
|
||||
foo(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func withAuth(foo http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
u, _, ok := r.BasicAuth()
|
||||
if !ok || u == "" {
|
||||
w.Header().Set("WWW-Authenticate", "Basic")
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
c := extract(r.Context())
|
||||
c.User = IDU(u)
|
||||
r = r.WithContext(inject(r.Context(), c))
|
||||
foo(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed public/root.html
|
||||
var httpRootHTML string
|
||||
|
||||
func httpRoot(w http.ResponseWriter, r *http.Request) {
|
||||
body := httpRootHTML
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
b, _ := os.ReadFile("public/root.html")
|
||||
body = string(b)
|
||||
}
|
||||
ctx := extract(r.Context())
|
||||
body = strings.ReplaceAll(body, "{{USER}}", string(ctx.User))
|
||||
assignments, err := httpAssignments(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
assignmentsB, _ := json.Marshal(assignments)
|
||||
body = strings.ReplaceAll(body, "{{ASSIGNMENTS_JSON}}", string(assignmentsB))
|
||||
w.Write([]byte(body))
|
||||
}
|
||||
|
||||
func httpAssignments(ctx context.Context) (interface{}, error) {
|
||||
db := extract(ctx).DB
|
||||
user := extract(ctx).User
|
||||
todo := map[IDQ]Question{}
|
||||
for q, _ := range db.HistoryOf(user) {
|
||||
if time.Until(db.Next(user, q)) > 0 {
|
||||
continue
|
||||
}
|
||||
todo[q] = db.Question(q)
|
||||
}
|
||||
return todo, nil
|
||||
}
|
||||
126
main.go
126
main.go
@@ -1,11 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -59,115 +56,10 @@ func Main() error {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
user := IDU(os.Getenv("USER"))
|
||||
failed, err := Review(db, user)
|
||||
if err != nil {
|
||||
return err
|
||||
if port, _ := strconv.Atoi(os.Getenv("PORT")); port > 0 {
|
||||
return HTTP(port, db)
|
||||
}
|
||||
for len(failed) > 0 {
|
||||
for i := len(failed) - 1; i >= 0; i-- {
|
||||
if _, passed, err := ReviewQ(db, user, failed[i]); err != nil {
|
||||
return err
|
||||
} else if passed == CMD_PASS {
|
||||
return nil
|
||||
} else if passed != CMD_FAIL {
|
||||
failed = append(failed[:i], failed[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Review(db DB, user IDU) ([]IDQ, error) {
|
||||
failed := []IDQ{}
|
||||
|
||||
for q, _ := range db.HistoryOf(user) {
|
||||
if time.Until(db.Next(user, q)) > 0 {
|
||||
continue
|
||||
}
|
||||
response, passed, err := ReviewQ(db, user, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch passed {
|
||||
case CMD_QUIT:
|
||||
return nil, nil
|
||||
case CMD_FAIL:
|
||||
failed = append(failed, q)
|
||||
if err := db.PushAnswer(user, q, Renderable(response), false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case CMD_PASS:
|
||||
if err := db.PushAnswer(user, q, Renderable(response), true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
return failed, nil
|
||||
}
|
||||
|
||||
func ReviewQ(db DB, user IDU, q IDQ) (string, string, error) {
|
||||
question := db.Question(q)
|
||||
fmt.Printf("> Q: %s\n", question.Q.ToString(db.UserResolution(user)))
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("> %+v\n", question.Tags)
|
||||
var response string
|
||||
gotAClue := false
|
||||
for i := range question.Clues {
|
||||
if i == 0 {
|
||||
fmt.Printf("> /clue for a clue\n")
|
||||
}
|
||||
response = readline()
|
||||
if response != "/clue" {
|
||||
break
|
||||
}
|
||||
gotAClue = true
|
||||
fmt.Printf("> %s", question.Clues[i].ToString(db.UserResolution(user)))
|
||||
if i+1 < len(question.Clues) {
|
||||
fmt.Printf(" | /clue for another clue")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if len(question.Clues) == 0 || response == "/clue" {
|
||||
response = readline()
|
||||
}
|
||||
if !gotAClue && len(question.Solution) > 0 && question.Solution[0].ToString(1) == response {
|
||||
return response, CMD_PASS, nil
|
||||
}
|
||||
if len(question.Solution) > 0 {
|
||||
fmt.Printf("> Solution:")
|
||||
for i := range question.Solution {
|
||||
fmt.Printf("\n\t%s", question.Solution[i].ToString(db.UserResolution(user)))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
if id, _ := db.LastAnswer(user, q); id == "" {
|
||||
} else if lastAnswer := db.Answer(id); lastAnswer.A != "" {
|
||||
fmt.Printf("> Last time, you responded:\n\t%s\n", lastAnswer.A)
|
||||
}
|
||||
fmt.Printf("> Did you pass this time? [%s%s%s%s]\n",
|
||||
strings.ToUpper(CMD_PASS),
|
||||
strings.ToLower(CMD_FAIL),
|
||||
strings.ToLower(CMD_SKIP),
|
||||
strings.ToLower(CMD_QUIT),
|
||||
)
|
||||
switch strings.ToLower(readline()) {
|
||||
case CMD_SKIP:
|
||||
return response, CMD_SKIP, nil
|
||||
case CMD_FAIL:
|
||||
return response, CMD_FAIL, nil
|
||||
case CMD_QUIT:
|
||||
return response, CMD_QUIT, nil
|
||||
}
|
||||
return response, CMD_PASS, nil
|
||||
}
|
||||
|
||||
func readline() string {
|
||||
fmt.Printf("\t")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
return Terminal(db)
|
||||
}
|
||||
|
||||
func NewDB() (DB, error) {
|
||||
@@ -182,13 +74,3 @@ func (q Question) Tagged(tag IDT) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (renderable Renderable) ToString(resolution int) string {
|
||||
s := string(renderable)
|
||||
if !strings.HasPrefix(s, "img:") {
|
||||
return s
|
||||
}
|
||||
buff := bytes.NewBuffer(nil)
|
||||
ViewAt(buff, s[4:], resolution)
|
||||
return string(buff.Bytes())
|
||||
}
|
||||
|
||||
73
public/root.html
Normal file
73
public/root.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<header>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/dark.css">
|
||||
<script>
|
||||
let session = Number(
|
||||
Date.
|
||||
now().
|
||||
toString().
|
||||
split("").
|
||||
reverse().
|
||||
join("")
|
||||
).toString(36)
|
||||
console.log("session", session)
|
||||
</script>
|
||||
</header>
|
||||
<body>
|
||||
<!--{{USER}}-->
|
||||
<form id="flash" action="" onsubmit="return false; trySolve(this.children.idq.value, this.children.answer.value); return false;">
|
||||
<input type="text" name="idq" readonly=true value="" style="display: none;">
|
||||
<div name="question"></div>
|
||||
<div name="clues"></div>
|
||||
<div name="solution"></div>
|
||||
<input type="button" value="clue">
|
||||
<input type="text" name="answer">
|
||||
<input type="submit" value="submit">
|
||||
<input type="button" value="skip" onclick="nextQuestion(this.parentElement)">
|
||||
</form>
|
||||
</body>
|
||||
<footer>
|
||||
<script>
|
||||
let knowledgebase = {{ASSIGNMENTS_JSON}}
|
||||
knowledgebase = Object.
|
||||
keys(knowledgebase).
|
||||
map((key) => [
|
||||
[key, knowledgebase[key]]
|
||||
])
|
||||
|
||||
function nextQuestion(form) {
|
||||
form.children.answer.value = ""
|
||||
let todo = knowledgebase.pop()
|
||||
if (!todo) {
|
||||
todo = [0]
|
||||
}
|
||||
todo = todo[0]
|
||||
if (! todo) {
|
||||
todo = ["", {Q: "ALL DONE"}]
|
||||
}
|
||||
|
||||
form.children.idq.value = todo[0]
|
||||
|
||||
form.children.question.innerHTML = `<h3>${todo[1].Q}</h3>`
|
||||
|
||||
let clues = ""
|
||||
for (var i in todo[1].Clues) {
|
||||
clues += `<details><summary>clue #${i}</summary>${todo[1].Clues[i]}</details>`
|
||||
}
|
||||
form.children.clues.innerHTML = clues
|
||||
|
||||
let solution = ""
|
||||
for (var i in todo[1].Solution) {
|
||||
solution += `<br>${todo[1].Solution[i]}`
|
||||
}
|
||||
if (solution) {
|
||||
solution = `<details><summary>solution</summary>${solution}</details>`
|
||||
}
|
||||
form.children.solution.innerHTML = solution
|
||||
}
|
||||
|
||||
nextQuestion(document.getElementById("flash"))
|
||||
</script>
|
||||
</footer>
|
||||
</html>
|
||||
132
terminal.go
Normal file
132
terminal.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Terminal(db DB) error {
|
||||
user := IDU(os.Getenv("USER"))
|
||||
failed, err := Review(db, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for len(failed) > 0 {
|
||||
for i := len(failed) - 1; i >= 0; i-- {
|
||||
if _, passed, err := ReviewQ(db, user, failed[i]); err != nil {
|
||||
return err
|
||||
} else if passed == CMD_PASS {
|
||||
return nil
|
||||
} else if passed != CMD_FAIL {
|
||||
failed = append(failed[:i], failed[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Review(db DB, user IDU) ([]IDQ, error) {
|
||||
failed := []IDQ{}
|
||||
|
||||
for q, _ := range db.HistoryOf(user) {
|
||||
if time.Until(db.Next(user, q)) > 0 {
|
||||
continue
|
||||
}
|
||||
response, passed, err := ReviewQ(db, user, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch passed {
|
||||
case CMD_QUIT:
|
||||
return nil, nil
|
||||
case CMD_FAIL:
|
||||
failed = append(failed, q)
|
||||
if err := db.PushAnswer(user, q, Renderable(response), false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case CMD_PASS:
|
||||
if err := db.PushAnswer(user, q, Renderable(response), true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
return failed, nil
|
||||
}
|
||||
|
||||
func ReviewQ(db DB, user IDU, q IDQ) (string, string, error) {
|
||||
question := db.Question(q)
|
||||
fmt.Printf("> Q: %s\n", question.Q.ToString(db.UserResolution(user)))
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("> %+v\n", question.Tags)
|
||||
var response string
|
||||
gotAClue := false
|
||||
for i := range question.Clues {
|
||||
if i == 0 {
|
||||
fmt.Printf("> /clue for a clue\n")
|
||||
}
|
||||
response = readline()
|
||||
if response != "/clue" {
|
||||
break
|
||||
}
|
||||
gotAClue = true
|
||||
fmt.Printf("> %s", question.Clues[i].ToString(db.UserResolution(user)))
|
||||
if i+1 < len(question.Clues) {
|
||||
fmt.Printf(" | /clue for another clue")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
if len(question.Clues) == 0 || response == "/clue" {
|
||||
response = readline()
|
||||
}
|
||||
if !gotAClue && len(question.Solution) > 0 && question.Solution[0].ToString(1) == response {
|
||||
return response, CMD_PASS, nil
|
||||
}
|
||||
if len(question.Solution) > 0 {
|
||||
fmt.Printf("> Solution:")
|
||||
for i := range question.Solution {
|
||||
fmt.Printf("\n\t%s", question.Solution[i].ToString(db.UserResolution(user)))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
if id, _ := db.LastAnswer(user, q); id == "" {
|
||||
} else if lastAnswer := db.Answer(id); lastAnswer.A != "" {
|
||||
fmt.Printf("> Last time, you responded:\n\t%s\n", lastAnswer.A)
|
||||
}
|
||||
fmt.Printf("> Did you pass this time? [%s%s%s%s]\n",
|
||||
strings.ToUpper(CMD_PASS),
|
||||
strings.ToLower(CMD_FAIL),
|
||||
strings.ToLower(CMD_SKIP),
|
||||
strings.ToLower(CMD_QUIT),
|
||||
)
|
||||
switch strings.ToLower(readline()) {
|
||||
case CMD_SKIP:
|
||||
return response, CMD_SKIP, nil
|
||||
case CMD_FAIL:
|
||||
return response, CMD_FAIL, nil
|
||||
case CMD_QUIT:
|
||||
return response, CMD_QUIT, nil
|
||||
}
|
||||
return response, CMD_PASS, nil
|
||||
}
|
||||
|
||||
func readline() string {
|
||||
fmt.Printf("\t")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
func (renderable Renderable) ToString(resolution int) string {
|
||||
s := string(renderable)
|
||||
if !strings.HasPrefix(s, "img:") {
|
||||
return s
|
||||
}
|
||||
buff := bytes.NewBuffer(nil)
|
||||
ViewAt(buff, s[4:], resolution)
|
||||
return string(buff.Bytes())
|
||||
}
|
||||
31
testdata/sample.yaml
vendored
31
testdata/sample.yaml
vendored
@@ -1,27 +1,40 @@
|
||||
knowledge:
|
||||
questions:
|
||||
uuid1:
|
||||
uuid10:
|
||||
q: whats a fieldset
|
||||
clues:
|
||||
- clue1 of 2
|
||||
- clue2 of 2
|
||||
solution:
|
||||
- solution A
|
||||
- solution B
|
||||
tags:
|
||||
- ops
|
||||
- data-platform
|
||||
uuid11:
|
||||
q: whats a responseset
|
||||
clues:
|
||||
- clue1 of 2
|
||||
- clue2 of 2
|
||||
solution:
|
||||
- solution A
|
||||
- solution B
|
||||
tags:
|
||||
- ops
|
||||
- data-platform
|
||||
answers:
|
||||
uuid2:
|
||||
q: uuid1
|
||||
uuid20:
|
||||
q: uuid10
|
||||
a: a schema
|
||||
ts: 123
|
||||
author: breel
|
||||
uuid0:
|
||||
q: uuid1
|
||||
uuid21:
|
||||
q: uuid10
|
||||
a: not a schema
|
||||
ts: 122
|
||||
author: breel
|
||||
users:
|
||||
breel:
|
||||
history:
|
||||
uuid1:
|
||||
- {a: uuid0, pass: true}
|
||||
- {a: uuid2, pass: true}
|
||||
tags: {assignments: [ops]}
|
||||
cadence: 5s
|
||||
resolution: 1
|
||||
|
||||
Reference in New Issue
Block a user