Skip to content

Commit 8c7880b

Browse files
committed
Merge branch 'up-amygdala' into merge-amygdala
This initial merge is incomplete, but compiles. Next step will be to merge each main into leatherman as a proper subcommand.
2 parents d418101 + a4e3c79 commit 8c7880b

26 files changed

Lines changed: 1377 additions & 0 deletions

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
language: go
2+
3+
go:
4+
- 1.13.x
5+
script:
6+
- go test ./...

Procfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: amygdala -port $PORT

amygdala.mdwn

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Amygdala
2+
3+
Computer asisted brain stem.
4+
5+
This project is comprised of a handful of tools that allow me to automate common
6+
tasks. The main tool (`amygdala`) is currently deployed to Heroku, but it could
7+
be deployed anywhere. This isn't written to run for more than one user or to
8+
meet any needs but my own. It receives input from Twilio so I can just text the
9+
thing and get either a response or have it do what I asked.
10+
11+
## Commands
12+
13+
### cmd
14+
15+
Explains all the commands online.
16+
17+
### todo
18+
19+
This is the most generically useful of the tools. Any string that doesn't match
20+
anything else is assumed to be a todo. Simply creates a document tagged `inbox`
21+
in my Dropbox. Any attachment included in the message is linked to; if the
22+
attachment is an image it is linked to with an `img` tag.
23+
24+
### `defer til`
25+
26+
A command matching the following general pattern "defer til 2019-01-01" or
27+
"defer some message til 2019-01-01" will enqueue messages to be
28+
[undefered](https://github.com/frioux/leatherman#undefer) later.
29+
30+
### `inspire me`
31+
32+
The string `inspire me` will repy with [a random inspirational
33+
link](https://frioux.github.io/notes/posts/inspiration/).
34+
35+
### `remind me`
36+
37+
Remind me commands are created with an sms to amygdala of the form:
38+
39+
* remind me to power name at 3pm
40+
* remind me to wake up at 3:05pm
41+
* remind me to make dinner in 1h
42+
* remind me to get dessert in 1h30m
43+
44+
Files are created in a directory in Dropbox and are the acted upon by
45+
`enqueue-at`
46+
47+
## Configuration
48+
49+
All configuration is done via environment variables.
50+
51+
### `DROPBOX_ACCESS_TOKEN`
52+
53+
Used by `amygdala` to access Dropbox.
54+
55+
### `MY_CELL`
56+
57+
Used by `amygdala` to limit access to my own phone.
58+
59+
### Pushover
60+
61+
All of these are used by `wuphf` to send notifications to my phone:
62+
63+
* `PUSHOVER_TOKEN`
64+
* `PUSHOVER_USER`
65+
* `PUSHOVER_DEVICE`
66+
67+
### `TWILIO_AUTH_TOKEN`
68+
69+
Used by `amygdala` to validate that requests actually came from Twilio.
70+
71+
### `TWILIO_URL`
72+
73+
Also used by `amygdala` to validate that requests actually came from Twilio. In
74+
theory this could be inferred from the request, but due to http proxies it must
75+
be configured instead.
76+
77+
## Design
78+
79+
As mentioned before, this project is a few distinct tools. Simplicity is a
80+
priority for all of them, but I've tried to keep things relatively neat.
81+
82+
### `amygdala`
83+
84+
The top level tool, which initiates all the other stuff, is currently organized
85+
around my personal notes system, which is not yet documented other than [this
86+
blog
87+
post](https://blog.afoolishmanifesto.com/posts/a-love-letter-to-plain-text/).
88+
With that in mind it uses an ordered, regexp based dispatcher in the
89+
`internal/notes` package.
90+
91+
My notes are structured text in Dropbox, so the `internal/notes` package talks
92+
directly to Dropbox to download and upload files.
93+
94+
### `enqueue-at`
95+
96+
This runs on my laptop. If I find that I use it often I want to figure out how
97+
to run this in the cloud (without a VM.)
98+
99+
I run it like this:
100+
101+
```bash
102+
$ minotaur ~/Dropbox/notes/.alerts -- bin/enqueue-at
103+
```
104+
105+
[`minotaur` comes with my
106+
Leatherman](https://github.com/frioux/leatherman#minotaur).
107+
108+
`enqueue-at` simply enqueues notifications via `atd`, using a tool called
109+
`wuphf`, which is included with this project.
110+
111+
[See docs for `remind me`](#remind-me) for more details.
112+
113+
### `wuphf`
114+
115+
Currently this tool sends alerts via both `wall` and
116+
[pushover](https://pushover.net). All arguments are concatenated to produce the
117+
sent message.
118+
119+
## Development
120+
121+
Here's a list of places to look when you need to add functionality:
122+
123+
* [NewRules](https://godoc.org/github.com/frioux/amygdala/internal/notes#NewRules)
124+
is where you can create a new command.
125+
* [Dropbox package](https://godoc.org/github.com/frioux/amygdala/internal/dropbox)
126+
* [Twilio package](https://godoc.org/github.com/frioux/amygdala/internal/twilio)
127+
128+
It's always wise to run `go test ./...` before pushing any changes. After
129+
that, using `cmd/brain-stem` to verify changes without needing the full stack
130+
is useful. If you want more, you can run the main app (`amygdala`) and poke at
131+
it with `cmd/twilio`. There's no dropbox mock yet so you'll need to actually
132+
use the real dropbox token, but other than that you should be able to develop
133+
almost 100% locally.
134+
135+
## History
136+
137+
This is meant to be a simpler replacement to [my lizard
138+
brain](https://github.com/frioux/Lizard-Brain), both because the world has
139+
improved and so have I.
140+
141+
The old system's design was around allowing arbitrary inputs and outputs; the
142+
new system is only decoupled as I see fit, instead of enforcing that arbitrarily
143+
from the start.

bin/enqueue-at

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/sh
2+
3+
# exit early if there are no CREATEd files
4+
perl -e 'exit 1 unless scalar grep m/^CREATE/, @ARGV' "$@" || exit
5+
6+
cd ~/Dropbox/notes/.alerts
7+
8+
for file in *; do
9+
ts="$(echo "$file" | cut -f1 -d_)"
10+
contents=$(cat "$file")
11+
12+
# if the ts is before now
13+
if perl -e'exit 1 if shift gt shift' "$ts" "$(date -Iseconds)"; then
14+
wuphf "$contents"
15+
else
16+
echo "wuphf $contents" | at "$(date -d "$ts" '+%H:%M %Y-%m-%d')"
17+
fi
18+
rm "$file"
19+
done

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module github.com/frioux/leatherman
22

3+
// +heroku goVersion go1.15
4+
35
require (
46
github.com/BurntSushi/toml v0.3.1
57
github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843 // indirect

internal/log/log.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package log
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"time"
7+
)
8+
9+
type errline struct {
10+
Time string
11+
Type string
12+
13+
Message string
14+
}
15+
16+
var e = json.NewEncoder(os.Stdout)
17+
18+
func Err(err error) {
19+
e.Encode(errline{
20+
Time: time.Now().Format(time.RFC3339Nano),
21+
Type: "error",
22+
Message: err.Error(),
23+
})
24+
}

internal/middleware/middleware.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package middleware
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"time"
7+
8+
"encoding/json"
9+
)
10+
11+
type Adapter func(http.Handler) http.Handler
12+
13+
func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
14+
for _, adapter := range adapters {
15+
h = adapter(h)
16+
}
17+
return h
18+
}
19+
20+
type logline struct {
21+
Time string
22+
Type string
23+
24+
Duration float64
25+
Method string
26+
URL string
27+
UserAgent string
28+
Proto string
29+
Host string
30+
RemoteAddr string
31+
StatusCode int
32+
}
33+
34+
func Log(logger io.Writer) Adapter {
35+
e := json.NewEncoder(logger)
36+
37+
return func(h http.Handler) http.Handler {
38+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39+
start := time.Now()
40+
41+
lw := &loggingResponseWriter{ResponseWriter: w}
42+
defer func() {
43+
e.Encode(logline{
44+
Type: "accesslog",
45+
Time: start.Format(time.RFC3339Nano),
46+
Duration: time.Now().Sub(start).Seconds(),
47+
Method: r.Method,
48+
URL: r.URL.String(),
49+
UserAgent: r.UserAgent(),
50+
Proto: r.Proto,
51+
Host: r.Host,
52+
RemoteAddr: r.RemoteAddr,
53+
StatusCode: lw.statusCode,
54+
})
55+
}()
56+
h.ServeHTTP(lw, r)
57+
58+
})
59+
}
60+
}
61+
62+
type loggingResponseWriter struct {
63+
http.ResponseWriter
64+
statusCode int
65+
}
66+
67+
func (lrw *loggingResponseWriter) WriteHeader(code int) {
68+
lrw.statusCode = code
69+
lrw.ResponseWriter.WriteHeader(code)
70+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/frioux/leatherman/internal/testutil"
11+
)
12+
13+
func TestLog(t *testing.T) {
14+
req, err := http.NewRequest("GET", "/", nil)
15+
if err != nil {
16+
t.Fatal(err)
17+
}
18+
19+
buf := new(bytes.Buffer)
20+
var inner http.HandlerFunc = func(w http.ResponseWriter, _ *http.Request) {
21+
w.WriteHeader(404)
22+
}
23+
rr := httptest.NewRecorder()
24+
handler := Adapt(inner, Log(buf))
25+
26+
handler.ServeHTTP(rr, req)
27+
28+
if status := rr.Code; status != http.StatusNotFound {
29+
t.Errorf("handler returned wrong status code: got %v want %v",
30+
status, http.StatusOK)
31+
}
32+
33+
d := json.NewDecoder(buf)
34+
var x struct{ StatusCode int }
35+
if err = d.Decode(&x); err != nil {
36+
panic(err)
37+
}
38+
39+
testutil.Equal(t, x.StatusCode, http.StatusNotFound, "status code recorded")
40+
}

internal/notes/beerme.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package notes
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"math/rand"
9+
"regexp"
10+
11+
"github.com/frioux/leatherman/internal/dropbox"
12+
"github.com/frioux/leatherman/internal/personality"
13+
"github.com/frioux/leatherman/internal/twilio"
14+
)
15+
16+
var isItem = regexp.MustCompile(`^\s?\*\s+(.*?)\s*$`)
17+
var mdLink = regexp.MustCompile(`^\[(.*)\]\((.*)\)$`)
18+
19+
var errNone = errors.New("never found anything")
20+
21+
func beerMe(r io.Reader) (string, error) {
22+
s := bufio.NewScanner(r)
23+
24+
o := []string{}
25+
for s.Scan() {
26+
m := isItem.FindStringSubmatch(s.Text())
27+
if len(m) != 2 {
28+
continue
29+
}
30+
o = append(o, m[1])
31+
}
32+
33+
if len(o) == 0 {
34+
return "", errNone
35+
}
36+
37+
rand.Shuffle(len(o), func(i, j int) { o[i], o[j] = o[j], o[i] })
38+
39+
fmt.Println(mdLink.FindStringSubmatch(o[0]))
40+
if l := mdLink.FindStringSubmatch(o[0]); len(l) == 3 {
41+
return fmt.Sprintf("[%s]( %s )", l[1], l[2]), nil
42+
}
43+
44+
return o[0], nil
45+
}
46+
47+
func inspireMe(cl dropbox.Client) func(_ string, _ []twilio.Media) (string, error) {
48+
return func(_ string, _ []twilio.Media) (string, error) {
49+
r, err := cl.Download("/notes/content/posts/inspiration.md")
50+
if err != nil {
51+
return personality.Err(), fmt.Errorf("dropbox.Download: %w", err)
52+
}
53+
n, err := beerMe(r)
54+
if err != nil {
55+
return personality.Err(), err
56+
}
57+
return n, nil
58+
}
59+
}

0 commit comments

Comments
 (0)