Skip to content
Browse files
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.
  • Loading branch information
frioux committed Jan 23, 2021
2 parents d418101 + a4e3c79 commit 8c7880b
Show file tree
Hide file tree
Showing 26 changed files with 1,377 additions and 0 deletions.
@@ -0,0 +1,6 @@
language: go

- 1.13.x
- go test ./...
@@ -0,0 +1 @@
web: amygdala -port $PORT
@@ -0,0 +1,143 @@
# Amygdala

Computer asisted brain stem.

This project is comprised of a handful of tools that allow me to automate common
tasks. The main tool (`amygdala`) is currently deployed to Heroku, but it could
be deployed anywhere. This isn't written to run for more than one user or to
meet any needs but my own. It receives input from Twilio so I can just text the
thing and get either a response or have it do what I asked.

## Commands

### cmd

Explains all the commands online.

### todo

This is the most generically useful of the tools. Any string that doesn't match
anything else is assumed to be a todo. Simply creates a document tagged `inbox`
in my Dropbox. Any attachment included in the message is linked to; if the
attachment is an image it is linked to with an `img` tag.

### `defer til`

A command matching the following general pattern "defer til 2019-01-01" or
"defer some message til 2019-01-01" will enqueue messages to be
[undefered]( later.

### `inspire me`

The string `inspire me` will repy with [a random inspirational

### `remind me`

Remind me commands are created with an sms to amygdala of the form:

* remind me to power name at 3pm
* remind me to wake up at 3:05pm
* remind me to make dinner in 1h
* remind me to get dessert in 1h30m

Files are created in a directory in Dropbox and are the acted upon by

## Configuration

All configuration is done via environment variables.


Used by `amygdala` to access Dropbox.

### `MY_CELL`

Used by `amygdala` to limit access to my own phone.

### Pushover

All of these are used by `wuphf` to send notifications to my phone:



Used by `amygdala` to validate that requests actually came from Twilio.


Also used by `amygdala` to validate that requests actually came from Twilio. In
theory this could be inferred from the request, but due to http proxies it must
be configured instead.

## Design

As mentioned before, this project is a few distinct tools. Simplicity is a
priority for all of them, but I've tried to keep things relatively neat.

### `amygdala`

The top level tool, which initiates all the other stuff, is currently organized
around my personal notes system, which is not yet documented other than [this
With that in mind it uses an ordered, regexp based dispatcher in the
`internal/notes` package.

My notes are structured text in Dropbox, so the `internal/notes` package talks
directly to Dropbox to download and upload files.

### `enqueue-at`

This runs on my laptop. If I find that I use it often I want to figure out how
to run this in the cloud (without a VM.)

I run it like this:

$ minotaur ~/Dropbox/notes/.alerts -- bin/enqueue-at

[`minotaur` comes with my

`enqueue-at` simply enqueues notifications via `atd`, using a tool called
`wuphf`, which is included with this project.

[See docs for `remind me`](#remind-me) for more details.

### `wuphf`

Currently this tool sends alerts via both `wall` and
[pushover]( All arguments are concatenated to produce the
sent message.

## Development

Here's a list of places to look when you need to add functionality:

* [NewRules](
is where you can create a new command.
* [Dropbox package](
* [Twilio package](

It's always wise to run `go test ./...` before pushing any changes. After
that, using `cmd/brain-stem` to verify changes without needing the full stack
is useful. If you want more, you can run the main app (`amygdala`) and poke at
it with `cmd/twilio`. There's no dropbox mock yet so you'll need to actually
use the real dropbox token, but other than that you should be able to develop
almost 100% locally.

## History

This is meant to be a simpler replacement to [my lizard
brain](, both because the world has
improved and so have I.

The old system's design was around allowing arbitrary inputs and outputs; the
new system is only decoupled as I see fit, instead of enforcing that arbitrarily
from the start.
@@ -0,0 +1,19 @@

# exit early if there are no CREATEd files
perl -e 'exit 1 unless scalar grep m/^CREATE/, @ARGV' "$@" || exit

cd ~/Dropbox/notes/.alerts

for file in *; do
ts="$(echo "$file" | cut -f1 -d_)"
contents=$(cat "$file")

# if the ts is before now
if perl -e'exit 1 if shift gt shift' "$ts" "$(date -Iseconds)"; then
wuphf "$contents"
echo "wuphf $contents" | at "$(date -d "$ts" '+%H:%M %Y-%m-%d')"
rm "$file"
2 go.mod
@@ -1,5 +1,7 @@

// +heroku goVersion go1.15

require ( v0.3.1 v0.0.0-20200324125942-20f126ea2843 // indirect
@@ -0,0 +1,24 @@
package log

import (

type errline struct {
Time string
Type string

Message string

var e = json.NewEncoder(os.Stdout)

func Err(err error) {
Time: time.Now().Format(time.RFC3339Nano),
Type: "error",
Message: err.Error(),
@@ -0,0 +1,70 @@
package middleware

import (


type Adapter func(http.Handler) http.Handler

func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
for _, adapter := range adapters {
h = adapter(h)
return h

type logline struct {
Time string
Type string

Duration float64
Method string
URL string
UserAgent string
Proto string
Host string
RemoteAddr string
StatusCode int

func Log(logger io.Writer) Adapter {
e := json.NewEncoder(logger)

return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

lw := &loggingResponseWriter{ResponseWriter: w}
defer func() {
Type: "accesslog",
Time: start.Format(time.RFC3339Nano),
Duration: time.Now().Sub(start).Seconds(),
Method: r.Method,
URL: r.URL.String(),
UserAgent: r.UserAgent(),
Proto: r.Proto,
Host: r.Host,
RemoteAddr: r.RemoteAddr,
StatusCode: lw.statusCode,
h.ServeHTTP(lw, r)


type loggingResponseWriter struct {
statusCode int

func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
@@ -0,0 +1,40 @@
package middleware

import (


func TestLog(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {

buf := new(bytes.Buffer)
var inner http.HandlerFunc = func(w http.ResponseWriter, _ *http.Request) {
rr := httptest.NewRecorder()
handler := Adapt(inner, Log(buf))

handler.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusNotFound {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)

d := json.NewDecoder(buf)
var x struct{ StatusCode int }
if err = d.Decode(&x); err != nil {

testutil.Equal(t, x.StatusCode, http.StatusNotFound, "status code recorded")
@@ -0,0 +1,59 @@
package notes

import (


var isItem = regexp.MustCompile(`^\s?\*\s+(.*?)\s*$`)
var mdLink = regexp.MustCompile(`^\[(.*)\]\((.*)\)$`)

var errNone = errors.New("never found anything")

func beerMe(r io.Reader) (string, error) {
s := bufio.NewScanner(r)

o := []string{}
for s.Scan() {
m := isItem.FindStringSubmatch(s.Text())
if len(m) != 2 {
o = append(o, m[1])

if len(o) == 0 {
return "", errNone

rand.Shuffle(len(o), func(i, j int) { o[i], o[j] = o[j], o[i] })

if l := mdLink.FindStringSubmatch(o[0]); len(l) == 3 {
return fmt.Sprintf("[%s]( %s )", l[1], l[2]), nil

return o[0], nil

func inspireMe(cl dropbox.Client) func(_ string, _ []twilio.Media) (string, error) {
return func(_ string, _ []twilio.Media) (string, error) {
r, err := cl.Download("/notes/content/posts/")
if err != nil {
return personality.Err(), fmt.Errorf("dropbox.Download: %w", err)
n, err := beerMe(r)
if err != nil {
return personality.Err(), err
return n, nil

0 comments on commit 8c7880b

Please sign in to comment.