diff options
Diffstat (limited to 'bot.go')
| -rw-r--r-- | bot.go | 467 |
1 files changed, 467 insertions, 0 deletions
@@ -0,0 +1,467 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "regexp" + "strconv" + "strings" + "net/url" + "net/http" + "encoding/json" + "os" + "os/exec" + "os/signal" + "syscall" + "log" + + "git.uakci.pl/toaq/nuogai/vietoaq" + "github.com/bwmarrin/discordgo" + "github.com/eaburns/toaq/ast" + "github.com/eaburns/toaq/logic" +) + +const ( + lozenge = '▯' + myself = "490175530537058314" + HELP = + "\u2003**commands:**" + + "\n`%` — Toadūa lookup (3 results at a time)" + + "\n\u2003(`%37` — show 37 results at a time)" + + "\n\u2003(`%!` — show one result, with extra info)" + + "\n\u2003(`%!37` — show 37 results, with extra info)" + + "\n\u2003(`% 59` — show 59th page of results)" + + "\n`%serial` — fagri's serial predicate engine" + + "\n`%nui` — uakci's serial predicate engine" + + "\n\u2003(`%serial` and `%nui` do not accept tone marks)" + + "\n`%hoe` — Hoelāı renderer (font version: v0.341)" + + "\n\u2003(`%hoe!` — same as above; raw input)" + + "\n`%miu` — jelca's semantic parser" + UNKNOWN = "unknown command — see `%help` for help" +) + +var ( + header = regexp.MustCompile(`^\*\*.*?\*\*: `) + whitespace = regexp.MustCompile(`[ ]+`) + spePort string + nuiPort string +) + +func init() { + spePort = os.Getenv("SPE_PORT") + nuiPort = os.Getenv("NUI_PORT") +} + +func min(a, b int) int { + if a < b { return a } + return b +} + +func get(uri string) ([]byte, error) { + resp, err := http.Get(uri) + if err != nil { + return []byte{}, err + } + cont, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []byte{}, err + } + return cont, nil +} + +func post(uri string, ct string, body io.Reader) ([]byte, error) { + resp, err := http.Post(uri, ct, body) + if err != nil { + return []byte{}, err + } + cont, err := ioutil.ReadAll(resp.Body) + if err != nil { + return []byte{}, err + } + return cont, nil +} + +func Respond(dg *discordgo.Session, ms *discordgo.MessageCreate) { + log.Printf("\n* %s", strings.Join(strings.Split(ms.Message.Content, "\n"), "\n ")) + respond(ms.Message.Content, + func(i interface{}) { + switch t := i.(type) { + case string: + dg.ChannelMessageSend(ms.Message.ChannelID, t) + case []byte: + dg.ChannelMessageSendComplex(ms.Message.ChannelID, + &discordgo.MessageSend{ + "", nil, false, []*discordgo.File{ + &discordgo.File{ + "toaq.png", + "image/png", + bytes.NewReader(t), + }, + }, nil, nil, nil, + }) + } + }) +} + +func respond(message string, callback func(interface{})) { + defer func() { + if r := recover(); r != nil { + fmt.Printf("%v", r) + callback("lủı sa hủı tủoı") + } + }() + message = strings.Trim( + header.ReplaceAllLiteralString(message, ""), + " \n") + parts := whitespace.Split(message, -1) + cmd, args, rest := parts[0], parts[1:], strings.Join(parts[1:], " ") + if strings.HasPrefix(cmd, "?%") { + callback(cmd[1:] + " " + vietoaq.From(rest)) + return + } + if strings.HasPrefix(cmd, "%") { + cmd_ := cmd[1:] + showNotes := false + if strings.HasPrefix(cmd_, "!") { + cmd_ = cmd_[1:] + showNotes = true + } + var (n int; err error) + if len(cmd_) > 0 { + n, err = strconv.Atoi(cmd_) + } else { + if showNotes { + n = 1 + } else { + n = 3 + } + } + if err == nil { + if n >= 0 { + Toadua(args, callback, n, showNotes) + } else { + callback("less than zero, that's quite many") + } + return + } + } + switch cmd { + case "?": + if strings.HasPrefix(strings.Trim(rest, " \n"), "?") { + return + } + callback(vietoaq.From(rest)) + case "%serial": + if len(rest) == 0 { + callback("please supply input") + return + } + resp, err := get(`http://localhost:9011/query?` + rest) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + callback(string(resp)) + case "%nui": + if len(rest) == 0 { + callback("please supply input") + return + } + u, err := url.Parse(`http://localhost:7183/`) + resp, err := post(u.String(), "application/octet-stream", + bytes.NewBufferString(rest)) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + callback(string(resp)) + case "%help": + callback(HELP) + case "%)": + callback("(%") + case "%hoe", "%hoe!": + if len(rest) == 0 { + callback("please supply input") + return + } + rest = strings.ReplaceAll(rest, "\t", "\\t") + if cmd == "%hoe" { + parts := regexp.MustCompile(`[<>]`).Split(rest, -1) + var sb strings.Builder + for i := 0; i < len(parts); i++ { + s := parts[i] + if i % 2 == 1 { + sb.WriteString("<") + } else { + if i != 0 { + sb.WriteString(">") + } + s = Hoekai(s) + } + sb.WriteString(s) + } + rest = sb.String() + } + out, err := exec.Command("convert", + "-density", "300", + "-background", "none", + "-fill", "white", + "-strokewidth", "2", + "-stroke", "black", + "-font", "ToaqScript", + "-pointsize", "24", + "pango:" + rest, + "-bordercolor", "none", + "-border", "20", + "png:-").Output() + if err != nil { + log.Print(err) + callback("lủı sa tủoı") + return + } + callback(out) + case "%miu": + if len(rest) == 0 { + callback("please supply input") + return + } + input := strings.TrimSpace(rest) + p := ast.NewParser(input) + text, err := p.Text() + if err != nil { + callback("syntax error " + err.Error()) + return + } + parse := ast.BracesString(text) + if parse != "" { + parse += "\n" + } + var math string + stmt := logic.Interpret(text) + if stmt == nil { + math = "fragment" + } else { + math = logic.PrettyString(stmt) + } + callback(parse + math) + default: + if strings.HasPrefix(cmd, "%") { + callback(UNKNOWN) + } + } +} + +func Toadua(args []string, callback func(interface{}), howMany int, showNotes bool) { + if len(args) == 0 { + callback("please supply a query") + return + } + page, err := strconv.Atoi(args[0]) + if err != nil { + page = 1 + } else { + args = args[1:] + } + query := strings.Join(args, " ") + mars, err := json.Marshal(struct{ + S string `json:"action"` + I interface{} `json:"query"` + }{ + "search", + ToaduaQuery(query), + }) + if err != nil { + log.Print(err) + callback("error") + return + } + raw, err := http.Post(`https://toadua.uakci.pl/api`, + "application/json", bytes.NewReader(mars)) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + var resp struct{ + Success bool `json:"success"` + Error string `json:"error"` + Entries []struct{ + Id string `json:"id"` + User string `json:"user"` + Head string `json:"head"` + Body string `json:"body"` + Score int `json:"score"` + Notes []struct{ + User string `json:"user"` + Content string `json:"content"` + } `json:"notes"` + } `json:"results"` + } + body, err := ioutil.ReadAll(raw.Body) + if err != nil { + log.Print(err) + callback("connectivity error") + return + } + err = json.Unmarshal(body, &resp) + if err != nil { + log.Print(err) + callback("parse error") + return + } + if !resp.Success { + log.Print(resp.Error) + callback("search failed: " + resp.Error) + return + } + if len(resp.Entries) == 0 { + callback("results empty") + return + } + first := (page - 1) * howMany + if len(resp.Entries) <= first { + callback(fmt.Sprintf("invalid page number (%d results)", len(resp.Entries))) + return + } + last := min(first + howMany, len(resp.Entries)) + var b strings.Builder + b.Grow(2000) + fmt.Fprintf(&b, "\u2003(%d–%d/%d)", first + 1, last, len(resp.Entries)) + soFar := b.String() + for _, e := range resp.Entries[first:last] { + // if i != 0 { + b.WriteString("\n") + // } + // b.WriteString(" — ") + b.WriteString("**" + e.Head + "**") + if showNotes { + fmt.Fprintf(&b, " (%s)", e.User) + } else if e.User == "official" { + b.WriteString(" ❦") + } + if e.Score != 0 { + b.WriteString(" ") + if e.Score > 0 { + b.WriteString(strings.Repeat("+", e.Score)) + } else { + b.WriteString(strings.Repeat("−", -e.Score)) + } + } + // b.WriteString(" — ") + b.WriteString("\n\u2003") + b.WriteString(strings.Join(strings.Split(e.Body, "\n"), "\n\u2003")) + if showNotes { + b.WriteString("") + for _, note := range e.Notes { + fmt.Fprintf(&b, "\n\u2003\u2003• (%s) %s", note.User, note.Content) + } + } + old := soFar + soFar = b.String() + if len(soFar) > 2000 { + callback(old) + b.Reset() + b.WriteString(soFar[len(old):]) + } + } + callback(b.String()) +} + +func ToaduaQuery(s string) interface{} { + spaced := strings.Split(s, " ") + andArgs := make([]interface{}, len(spaced)) + for i, andArg := range spaced { + ored := strings.Split(andArg, "|") + orArgs := make([]interface{}, len(ored)) + for j, orArg := range ored { + neg := false + if strings.HasPrefix(orArg, "!") { + orArg = orArg[1:] + neg = true + } + parts := strings.SplitN(orArg, ":", 2) + var term interface{} + if len(parts) == 1 { + term = []interface{}{"term", orArg} + } else { + if parts[0] == "arity" { + conv, _ := strconv.Atoi(parts[1]) + term = []interface{}{"arity", conv} + } else { + term = []interface{}{parts[0], parts[1]} + } + } + if neg { + term = []interface{}{"not", term} + } + orArgs[j] = term + } + if len(orArgs) == 1 { + andArgs[i] = orArgs[0] + } else { + andArgs[i] = append([]interface{}{"or"}, orArgs...) + } + } + if len(andArgs) == 1 { + return andArgs[0] + } else { + return append([]interface{}{"and"}, andArgs...) + } +} + +func Hoekai(s string) string { + viet := vietoaq.To(s) + parts := vietoaq.Syllables(viet, vietoaq.VietoaqSyllable) + var sb strings.Builder + for i, part := range parts { + if i % 2 == 0 { + sb.WriteString(part[0]) + continue + } + onset, nucleus, coda := part[1], part[2], part[3] + switch onset { + case "ch": onset = "w" + case "sh": onset = "x" + case "x": onset = "q" + } + diph := "" + if len(nucleus) >= 2 { + flag := true + switch nucleus[len(nucleus) - 2:] { + case "ai": diph = "y" + case "ao": diph = "v" + case "oi": diph = "z" + case "ei": diph = "W" + default: flag = false + } + if flag { + nucleus = nucleus[:len(nucleus) - 2] + } + } + if len(nucleus) >= 2 { + diph = strings.ToUpper(nucleus[1:]) + nucleus = nucleus[:1] + } else if nucleus == "a" && diph == "" { + nucleus = "" + } + fmt.Fprintf(&sb, "%s%s%s%s", + diph, onset, strings.ToUpper(coda), nucleus) + } + return sb.String() +} + +func main() { + dg, err := discordgo.New("Bot " + os.Getenv("TOKEN")) + if err != nil { panic(err) } + dg.AddHandler(Respond) + err = dg.Open() + if err != nil { panic(err) } + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + dg.Close() +} |
