~ecs/quaternia

7167b8c54c5890e3396e5f06c950275ccd8ee83d — Ember Sawady 4 months ago 12bdd9d master
Yeet reminderbot

Consumed by awkbot
3 files changed, 38 insertions(+), 288 deletions(-)

M bots.go
D remind.go
M wttr.go
M bots.go => bots.go +0 -1
@@ 9,7 9,6 @@ type bots struct{}
func (bots) Output(args []string, _ Flags, c *girc.Client, e girc.Event, ch chan string) {
	defer close(ch)
	ch <- "o/ i'm wolframbot and i suck at everything"
	ch <- "reminderbot, featuring shitty datetime parsing but better memory than you, says hi"
	ch <- "rssbot, for all your spam-youtube-links-on-boot needs"
	ch <- "all aboard the mbtabot train^Wbus"
	ch <- "better to know whether there will be weatherbot than what the weather will be - but luckily you can know both"

D remind.go => remind.go +0 -287
@@ 1,287 0,0 @@
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/araddon/dateparse"
	"github.com/bradfitz/latlong"
	"github.com/lrstanley/girc"
	"github.com/olebedev/when"
	"github.com/olebedev/when/rules/common"
	"github.com/olebedev/when/rules/en"
)

const reminderFile = "/etc/quaternia/reminders"

type lastKey struct {
	where string
	who   string
}

var last map[lastKey]string

type reminder struct {
	who   string
	what  string
	where string
	when  time.Time
	how   string
}

type remind struct {
	r *[]reminder
}

type implicitRemind struct {
	r *[]reminder
}

type snooze struct {
	r *[]reminder
}

func parseReminder() remind {
	r := make([]reminder, 0)
	f, err := os.OpenFile(reminderFile, os.O_RDONLY|os.O_CREATE, 0600)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	contents, err := ioutil.ReadAll(f)
	if err != nil {
		panic(err)
	}
	for _, line := range strings.Split(string(contents), "\n") {
		fields := strings.SplitN(line, " ", 5)
		if len(fields) != 5 {
			continue // silently ignore incorrectly formatted lines
		}
		t, err := time.Parse(time.RFC3339, fields[2])
		if err != nil {
			panic(err)
		}
		r = append(r, reminder{
			who:   fields[0],
			what:  fields[4],
			where: fields[1],
			when:  t,
			how:   fields[3],
		})
	}
	return remind{&r}
}

func writeReminder(r remind) error {
	f, err := os.OpenFile(reminderFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	for _, line := range *r.r {
		fmt.Fprintln(f, line.who+" "+line.where+" "+
			line.when.Format(time.RFC3339)+" "+line.how+" "+
			line.what)
	}
	return nil
}

// ParseTime implements best-effort time parsing
func ParseTime(s string, l *time.Location) (time.Time, error) {
	d, err := time.ParseDuration(strings.TrimPrefix(s, "in "))
	if err == nil {
		return time.Now().In(l).Add(d), nil
	}

	t, err := dateparse.ParseAny(s)
	if err == nil {
		return t, nil
	}

	w := when.New(nil)
	w.Add(en.All...)
	w.Add(common.All...)
	r, err := w.Parse(s, time.Now().In(l))
	if err == nil && r != nil {
		return r.Time, nil
	}

	r, err = w.Parse("in "+s, time.Now().In(l))
	if err == nil && r != nil {
		return r.Time, nil
	}

	// Bogus return value in order not to have to return a pointer
	if err != nil {
		return time.Now().In(l), err
	}
	return time.Now().In(l), errors.New("no matches found")
}

func (r remind) Output(args []string, _ Flags, c *girc.Client, e girc.Event, ch chan string) {
	defer close(ch)
	where := e.Params[0]
	if e.Params[0] == c.GetNick() {
		where = e.Source.Name
	}
	who := e.Source.Name

	if len(args) < 1 {
		s := ""
		for _, rem := range *r.r {
			if rem.who == e.Source.Name && rem.where == where {
				s += rem.who + ": " + rem.what + " at " +
					rem.when.Format(time.RFC1123Z) + "\n"
			}
		}
		url, err := Pastebin(*GetConfig(c), s)
		if err != nil {
			ch <- err.Error()
			return
		}
		ch <- url
		return
	}
	if len(args) == 1 {
		snoozed, ok := last[lastKey{
			where: where,
			who:   who,
		}]
		if !ok {
			ch <- "error: no previous message"
			return
		}
		args = append(args, snoozed)
	}
	when := args[0]
	what := args[1]
	if len(args) == 3 {
		who = args[0]
		when = args[1]
		what = args[2]
	}
	if len(args) > 3 {
		ch <- "usage: .remind [<when> [<what>]]"
		ch <- "usage: .remind <who> <when> <what>"
		return
	}

	l := time.UTC
	loc, err := GetLocation(e.Source.Name)
	if err == nil {
		l, err = time.LoadLocation(
			latlong.LookupZoneName(loc.Lat, loc.Long))
		if err != nil {
			l = time.UTC
		}
	}

	t, err := ParseTime(when, l)
	if err != nil {
		ch <- "error parsing reminder date: " + err.Error()
		return
	}

	*r.r = append(*r.r, reminder{
		who:   who,
		what:  what,
		where: where,
		when:  t,
		how:   GetConfig(c).Name,
	})
	if who == e.Source.Name {
		ch <- "reminding you at " + t.Format(time.RFC1123Z)
	} else {
		ch <- "reminding " + who + " at " + t.Format(time.RFC1123Z)
	}
	if err = writeReminder(r); err != nil {
		ch <- "error writing reminder: " + err.Error()
	}
}

var reminderRegexps = []*regexp.Regexp{
	regexp.MustCompile("^remind (?P<who>([^ ]*)) (?P<when>(in|on|at|tomorrow).*?) +(?P<what>(to|about|that) +.*)$"),
	regexp.MustCompile("^remind (?P<who>([^ ]*)) (?P<what>(to|about|that) +.*) (?P<when>(in|on|at|tomorrow).*)$"),
	regexp.MustCompile("^remind (?P<who>([^ ]*)) (?P<what>(to|about|that) +.*)$"),
	regexp.MustCompile("^remind (?P<who>([^ ]*)) (?P<when>(in|on|at|tomorrow).*)$"),
}

func (r implicitRemind) Output(args []string, f Flags, c *girc.Client, e girc.Event, ch chan string) {
	explicit := remind{r: r.r}
	for _, re := range reminderRegexps {
		subs := re.FindStringSubmatch(args[0])
		if subs != nil {
			subnames := re.SubexpNames()
			what := "to git gud at reminders"
			when := "in an hour"
			who := e.Source.Name
			for i, subname := range subnames {
				if subname == "what" {
					what = subs[i]
				}
				if subname == "when" {
					when = subs[i]
				}
				if subname == "who" && subs[i] != "me" {
					who = subs[i]
				}
			}
			explicit.Output([]string{who, when, "reminder " + what}, f, c, e, ch)
			return
		}
	}
}

func (s snooze) Output(args []string, f Flags, c *girc.Client, e girc.Event, ch chan string) {
	r := remind{r: s.r}
	r.Output([]string{strings.Join(args, " ")}, f, c, e, ch)
}

func checkReminders(c *girc.Client, r remind, server string) {
	if c == nil || !c.IsConnected() {
		return
	}
	for i, rem := range *r.r {
		if rem.how == server && time.Now().After(rem.when) {
			(*r.r)[i] = (*r.r)[len(*r.r)-1]
			*r.r = (*r.r)[:len(*r.r)-1]
			writeReminder(r)

			c.Cmd.Message(rem.where, rem.who+": "+rem.what)
			last[lastKey{
				where: rem.where,
				who:   rem.who,
			}] = rem.what
			checkReminders(c, r, server)
			return
		}
	}
}

func runReminders(c **Config, r remind) {
	ch := time.Tick(10 * time.Second)
	for range ch {
		if *c == nil {
			continue
		}
		for _, cfg := range **c {
			checkReminders(cfg.Client, r, cfg.Name)
		}
	}
}

func init() {
	last = make(map[lastKey]string)
	r := parseReminder()
	Register(".remind", r)
	Register(".reminders", r)
	Register(".remindme", r)
	Register(".snooze", snooze{r: r.r})
	RegisterAlways(implicitRemind{r: r.r})
	go runReminders(&config, r)
}

M wttr.go => wttr.go +38 -0
@@ 1,20 1,58 @@
package main

import (
	"errors"
	"fmt"
	"math"
	"strings"
	"time"

	"github.com/araddon/dateparse"
	"github.com/bradfitz/latlong"
	owm "github.com/briandowns/openweathermap"
	"github.com/codingsince1985/geo-golang/openstreetmap"
	"github.com/lrstanley/girc"
	"github.com/olebedev/when"
	"github.com/olebedev/when/rules/common"
	"github.com/olebedev/when/rules/en"
)

type wttr struct{}

const format = "?format=%l:+%t,+%C,+%w,+%p"

// ParseTime implements best-effort time parsing
func ParseTime(s string, l *time.Location) (time.Time, error) {
	d, err := time.ParseDuration(strings.TrimPrefix(s, "in "))
	if err == nil {
		return time.Now().In(l).Add(d), nil
	}

	t, err := dateparse.ParseAny(s)
	if err == nil {
		return t, nil
	}

	w := when.New(nil)
	w.Add(en.All...)
	w.Add(common.All...)
	r, err := w.Parse(s, time.Now().In(l))
	if err == nil && r != nil {
		return r.Time, nil
	}

	r, err = w.Parse("in "+s, time.Now().In(l))
	if err == nil && r != nil {
		return r.Time, nil
	}

	// Bogus return value in order not to have to return a pointer
	if err != nil {
		return time.Now().In(l), err
	}
	return time.Now().In(l), errors.New("no matches found")
}

func (wttr) Output(args []string, f Flags, c *girc.Client, e girc.Event, ch chan string) {
	defer close(ch)
	if len(args) > 2 {