summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoruakci <uakci@uakci.pl>2021-09-15 01:19:55 +0200
committeruakci <uakci@uakci.pl>2021-09-15 01:19:55 +0200
commit20ef8a090c2bc552e8f1e5dd773cd086c9782a21 (patch)
tree89b7efba3f301c11eaba53ce13bcca87a9648240
downloadnuogai-20ef8a090c2bc552e8f1e5dd773cd086c9782a21.tar.gz
nuogai-20ef8a090c2bc552e8f1e5dd773cd086c9782a21.zip
initial
-rw-r--r--.envrc1
-rw-r--r--.gitignore2
-rw-r--r--ToaqScript.ttfbin0 -> 7200 bytes
-rw-r--r--bot.go467
-rw-r--r--flake.lock132
-rw-r--r--flake.nix74
-rw-r--r--go.mod9
-rw-r--r--go.sum19
-rw-r--r--gomod2nix.toml79
-rw-r--r--hoelai.go56
-rw-r--r--patches/nui.patch10
-rw-r--r--patches/spe.patch11
-rw-r--r--vietoaq/vietoaq.go128
-rw-r--r--vietoaq/vietoaq_test.go33
14 files changed, 1021 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b91777e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/.direnv
+/result
diff --git a/ToaqScript.ttf b/ToaqScript.ttf
new file mode 100644
index 0000000..fe87a17
--- /dev/null
+++ b/ToaqScript.ttf
Binary files differ
diff --git a/bot.go b/bot.go
new file mode 100644
index 0000000..5662b24
--- /dev/null
+++ b/bot.go
@@ -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()
+}
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..92f56e7
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,132 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "locked": {
+ "lastModified": 1631561581,
+ "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "ref": "master",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "gomod2nix": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "utils": "utils"
+ },
+ "locked": {
+ "lastModified": 1627572165,
+ "narHash": "sha256-MFpwnkvQpauj799b4QTBJQFEddbD02+Ln5k92QyHOSk=",
+ "owner": "tweag",
+ "repo": "gomod2nix",
+ "rev": "67f22dd738d092c6ba88e420350ada0ed4992ae8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "tweag",
+ "ref": "master",
+ "repo": "gomod2nix",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1625191069,
+ "narHash": "sha256-M8/UH9pMgQEtuzY9bFwklYw8hx0pOKtUTyQC8E2UTHY=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "d8079260a3028ae3221d7a5467443ee3a9edd2b8",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1631655525,
+ "narHash": "sha256-8U7zAdbjNItXo6eqI/rhtOa3LUPGD6yE9PTZQkrSGHo=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "cf0caf529c33c140863ebfa43691f7b69fe2233c",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "master",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nuigui-upstream": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1551541396,
+ "narHash": "sha256-PZUzgumsvIpzEgODVpiBKlZzZQHXKhS+3lOB+2EDeeQ=",
+ "owner": "uakci",
+ "repo": "nuigui",
+ "rev": "87d41886f7c4693dfc5e3016799232c7e15e4c81",
+ "type": "github"
+ },
+ "original": {
+ "owner": "uakci",
+ "ref": "master",
+ "repo": "nuigui",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "gomod2nix": "gomod2nix",
+ "nixpkgs": "nixpkgs_2",
+ "nuigui-upstream": "nuigui-upstream",
+ "serial-predicate-engine-upstream": "serial-predicate-engine-upstream"
+ }
+ },
+ "serial-predicate-engine-upstream": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1546479877,
+ "narHash": "sha256-jVDNyfYWlk5qYsZSQjcGdI4hdEKKdSciz6QK0nAqKGs=",
+ "owner": "acotis",
+ "repo": "serial-predicate-engine",
+ "rev": "9e02baf711c0b7402da2eeb50b8644bf7b6e415f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "acotis",
+ "ref": "master",
+ "repo": "serial-predicate-engine",
+ "type": "github"
+ }
+ },
+ "utils": {
+ "locked": {
+ "lastModified": 1623875721,
+ "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..de458af
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,74 @@
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/master";
+ flake-utils.url = "github:numtide/flake-utils/master";
+ gomod2nix.url = "github:tweag/gomod2nix/master";
+ nuigui-upstream.url = "github:uakci/nuigui/master";
+ nuigui-upstream.flake = false;
+ serial-predicate-engine-upstream.url =
+ "github:acotis/serial-predicate-engine/master";
+ serial-predicate-engine-upstream.flake = false;
+ };
+
+ outputs = { self, nixpkgs, gomod2nix, nuigui-upstream
+ , serial-predicate-engine-upstream, flake-utils, ... }:
+ {
+ inherit (gomod2nix) devShell;
+ nixosModule = { config, system, ... }: {
+ config.fonts.fonts = [ self.packages.toaqScript.${system} ];
+ };
+ } // flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = (import nixpkgs {
+ inherit system;
+ overlays = [ gomod2nix.overlay ];
+ }).pkgs;
+ in with pkgs;
+ let
+ toaqScript = pkgs.writeTextDir "share/fonts/ToaqScript.ttf" ./ToaqScript.ttf;
+ schemePkgs = lib.mapAttrs (name:
+ { src, install, patches }:
+ pkgs.stdenv.mkDerivation {
+ inherit src name patches;
+ buildInputs = [ pkgs.guile ];
+ installPhase = ''
+ mkdir -p $out/bin
+ cp -r ./* $out
+ echo "${install}" > $out/bin/${name}
+ chmod +x $out/bin/${name}
+ '';
+ }) {
+ nuigui = {
+ src = nuigui-upstream;
+ patches = [ ./patches/nui.patch ];
+ install = ''
+ cd \$(dirname \$0)/../; ${pkgs.guile}/bin/guile web.scm
+ '';
+ };
+ serial-predicate-engine = {
+ src = serial-predicate-engine-upstream;
+ patches = [ ./patches/spe.patch ];
+ install = ''
+ cd \$(dirname \$0)/../web/; ${pkgs.guile}/bin/guile webservice.scm
+ '';
+ };
+ };
+ nuogai = buildGoApplication {
+ vendorSha256 = null;
+ runVend = true;
+ name = "nuogai";
+ src = ./.;
+ modules = ./gomod2nix.toml;
+ buildInputs = (builtins.attrValues schemePkgs) ++ [
+ toaqScript
+ (imagemagick.overrideAttrs
+ (a: { buildInputs = a.buildInputs ++ [ pango ]; }))
+ ];
+ };
+ in {
+ defaultPackage = nuogai;
+ packages = schemePkgs // {
+ inherit toaqScript nuogai;
+ };
+ });
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..9f3762b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,9 @@
+module git.uakci.pl/toaq/nuogai
+
+go 1.13
+
+require (
+ github.com/bwmarrin/discordgo v0.23.2
+ github.com/eaburns/toaq v0.0.0-20210614121731-80ccb209650b
+ golang.org/x/text v0.3.7
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..f6405e6
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,19 @@
+github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
+github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
+github.com/eaburns/peggy v1.0.0 h1:cvZIO5GGq2Qy1l1Va0Mte6LFVTY6GtAGbzSvouFcjUk=
+github.com/eaburns/peggy v1.0.0/go.mod h1:X2pbl0EV5erfnK8kSGwo0lBCgMGokvR1E6KerAoDKXg=
+github.com/eaburns/pretty v1.0.0 h1:00W1wrrtMXUSqLPN0txS8j7g9qFXy6nA5vZVqVQOo6w=
+github.com/eaburns/pretty v1.0.0/go.mod h1:retcK8A0KEgdmb0nuxhvyxixwCmEPO7SKlK0IJhjg8A=
+github.com/eaburns/toaq v0.0.0-20210614121731-80ccb209650b h1:2mu0jEGoRhKMpkHMInz584EWFa0n3oEzpxoAo6PN1IE=
+github.com/eaburns/toaq v0.0.0-20210614121731-80ccb209650b/go.mod h1:FKayyKQuuBv0TnmWcuGr0MNMLeqpWLskPHeRWIEihoM=
+github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/gomod2nix.toml b/gomod2nix.toml
new file mode 100644
index 0000000..228e569
--- /dev/null
+++ b/gomod2nix.toml
@@ -0,0 +1,79 @@
+["github.com/bwmarrin/discordgo"]
+ sumVersion = "v0.23.2"
+ ["github.com/bwmarrin/discordgo".fetch]
+ type = "git"
+ url = "https://github.com/bwmarrin/discordgo"
+ rev = "c27ad65527ecbc264c674cd3d0e85bb09de942e3"
+ sha256 = "1k14f52s3wvp4jx7czicnfjwz9inbxiaidfg74z3gk9vlway69aq"
+
+["github.com/eaburns/peggy"]
+ sumVersion = "v1.0.0"
+ ["github.com/eaburns/peggy".fetch]
+ type = "git"
+ url = "https://github.com/eaburns/peggy"
+ rev = "e1cd35e5dd2c0c34e02e847e569dafe1afb30033"
+ sha256 = "1mhdxs7vvwy6nzvx33dvkby1amp6cidxy6vl5hyzr0m6biqxlncs"
+
+["github.com/eaburns/pretty"]
+ sumVersion = "v1.0.0"
+ ["github.com/eaburns/pretty".fetch]
+ type = "git"
+ url = "https://github.com/eaburns/pretty"
+ rev = "af23b1658732d295adfdc728db79838ce7e7c53e"
+ sha256 = "1kaw6chx3bvbvfs5b5v5xvm4xcga0kcds4m6cm6rj5wqrsamhrlm"
+
+["github.com/eaburns/toaq"]
+ sumVersion = "v0.0.0-20210614121731-80ccb209650b"
+ ["github.com/eaburns/toaq".fetch]
+ type = "git"
+ url = "https://github.com/eaburns/toaq"
+ rev = "80ccb209650b6c7ef05d3736e1434c7ccc0e1398"
+ sha256 = "1pxkwh6lr5ajzwn85acg9458g8npkm5kdvkiryikckxj413mp1b4"
+
+["github.com/google/go-cmp"]
+ sumVersion = "v0.3.1"
+ ["github.com/google/go-cmp".fetch]
+ type = "git"
+ url = "https://github.com/google/go-cmp"
+ rev = "2d0692c2e9617365a95b295612ac0d4415ba4627"
+ sha256 = "1caw49i0plkjxir7kdf5qhwls3krqwfmi7g4h392rdfwi3kfahx1"
+
+["github.com/gorilla/websocket"]
+ sumVersion = "v1.4.1"
+ ["github.com/gorilla/websocket".fetch]
+ type = "git"
+ url = "https://github.com/gorilla/websocket"
+ rev = "c3e18be99d19e6b3e8f1559eea2c161a665c4b6b"
+ sha256 = "03n1n0nwz3k9qshmriycqznnnvd3dkzsfwpnfjzzvafjxk9kyapv"
+
+["github.com/velour/chat"]
+ sumVersion = "v0.0.0-20180713122344-fd1d1606cb89"
+ ["github.com/velour/chat".fetch]
+ type = "git"
+ url = "https://github.com/velour/chat"
+ rev = "fd1d1606cb8966f7141d57428833bf76e5b184ea"
+ sha256 = "1n9dyvqy3fr2f9hfzmrcikj6blx3xz2r7jg8i4j8a78033rb2fn0"
+
+["golang.org/x/crypto"]
+ sumVersion = "v0.0.0-20181030102418-4d3f4d9ffa16"
+ ["golang.org/x/crypto".fetch]
+ type = "git"
+ url = "https://go.googlesource.com/crypto"
+ rev = "4d3f4d9ffa16a13f451c3b2999e9c49e9750bf06"
+ sha256 = "0sbsgjm6wqa162ssrf1gnpv62ak5wjn1bn8v7sxwwfg8a93z1028"
+
+["golang.org/x/text"]
+ sumVersion = "v0.3.7"
+ ["golang.org/x/text".fetch]
+ type = "git"
+ url = "https://go.googlesource.com/text"
+ rev = "383b2e75a7a4198c42f8f87833eefb772868a56f"
+ sha256 = "0xkw0qvfjyifdqd25y7nxdqkdh92inymw3q7841nricc9s01p4jy"
+
+["golang.org/x/tools"]
+ sumVersion = "v0.0.0-20180917221912-90fa682c2a6e"
+ ["golang.org/x/tools".fetch]
+ type = "git"
+ url = "https://go.googlesource.com/tools"
+ rev = "90fa682c2a6e6a37b3a1364ce2fe1d5e41af9d6d"
+ sha256 = "03ic2xsy51jw9749wl7gszdbz99iijbd2bckgygl6cm9w5m364ak"
diff --git a/hoelai.go b/hoelai.go
new file mode 100644
index 0000000..bf0e7eb
--- /dev/null
+++ b/hoelai.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "fmt"
+ "git.uakci.pl/toaq/nuogai/vietoaq"
+ "strings"
+)
+
+func Hoelai(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()
+}
diff --git a/patches/nui.patch b/patches/nui.patch
new file mode 100644
index 0000000..a99771c
--- /dev/null
+++ b/patches/nui.patch
@@ -0,0 +1,10 @@
+diff --git a/web.scm b/web.scm
+index 411a6cc..e76f8b2 100644
+--- a/web.scm
++++ b/web.scm
+@@ -4,4 +4,4 @@
+ (lambda (request request-body)
+ (values '((content-type . (text/plain)))
+ (process-line (utf8->string request-body))))
+- 'http '(#:port 7183))
++ 'http '(#:port (getenv "PORT")))
diff --git a/patches/spe.patch b/patches/spe.patch
new file mode 100644
index 0000000..af5a53d
--- /dev/null
+++ b/patches/spe.patch
@@ -0,0 +1,11 @@
+diff --git a/web/webservice.scm b/web/webservice.scm
+index 8193dda..0cc6ca4 100755
+--- a/web/webservice.scm
++++ b/web/webservice.scm
+@@ -51,4 +51,4 @@
+
+ (run-server spe-handler
+ 'http
+- '(#:host "0.0.0.0"))
+\ No newline at end of file
++ `(#:host "127.0.0.1" #:port ,(string->number (getenv "PORT"))))
diff --git a/vietoaq/vietoaq.go b/vietoaq/vietoaq.go
new file mode 100644
index 0000000..b4375e7
--- /dev/null
+++ b/vietoaq/vietoaq.go
@@ -0,0 +1,128 @@
+package vietoaq
+
+import (
+ "regexp"
+ "strings"
+
+ "golang.org/x/text/unicode/norm"
+)
+
+var (
+ toneMap = "\u0304\u0301\u0308\u0309\u0302\u0300\u0303"
+ vietoaqMap = [7][2]rune{
+ {'r', 'l'}, {'p', 'b'}, {'x', 'z'}, {'n', 'm'}, {'t', 'd'}, {'k', 'g'}, {'f', 'v'}}
+ RegularSyllable = regexp.MustCompile(
+ `([bcdfghjklmnprstz']?|[cs]h)` + // onset
+ `([aeiuoyı])` + // first vowel of nucleus
+ `([` + toneMap + `]?)` + // tone
+ `([aeiouyı]{0,2})` + // remaining nucleus vowels
+ `(q?)` ) // regular coda
+ VietoaqSyllable = regexp.MustCompile(
+ `([bcdfghjklmnprstxz]|[cs]h)` + // onset
+ `([aeiuoy]{1,3})` + // nucleus
+ `([qrlpbxznmtdkgfv]?)` ) // Vietoaq coda
+)
+
+func toTransform(syll []string, padding bool) string {
+ onset, vow, tone, vows, coda :=
+ syll[1], syll[2], syll[3], syll[4], syll[5]
+ if onset == "" || onset == "'" {
+ onset = "x"
+ }
+ if tone != "" {
+ var qful int
+ if coda == "q" {
+ qful = 1
+ } else {
+ qful = 0
+ }
+ coda = string(vietoaqMap[strings.Index(toneMap, tone) / 2][qful])
+ }
+ return onset + strings.ReplaceAll(vow + vows, "ı", "i") + coda
+}
+
+func fromTransform(syll []string, padding bool) string {
+ onset, vow, tone, vows, coda :=
+ syll[1], syll[2][0:1], "", syll[2][1:], syll[3]
+ if coda != "" && coda != "q" {
+ codaRune := rune(coda[0])
+ var ii, jj int
+ for i, arr := range vietoaqMap {
+ for j, char := range arr {
+ if char == codaRune {
+ ii, jj = i, j
+ break
+ }
+ }
+ }
+ if jj == 1 {
+ coda = "q"
+ } else {
+ coda = ""
+ }
+ tone = string([]rune(toneMap)[ii])
+ if onset == "x" {
+ onset = ""
+ }
+ } else if vow == "i" {
+ vow = "ı"
+ }
+ if onset == "x" {
+ if padding || tone != "" {
+ onset = ""
+ } else {
+ onset = "'"
+ }
+ }
+ return onset + norm.NFC.String(vow + tone) +
+ strings.ReplaceAll(vows, "i", "ı") + coda
+}
+
+func To(regular string) string {
+ return syllableTransform(regular, RegularSyllable, toTransform)
+}
+
+func From(vietoaq string) string {
+ return syllableTransform(vietoaq, VietoaqSyllable, fromTransform)
+}
+
+func syllableTransform(input string, r *regexp.Regexp,
+ transform func([]string, bool)string) string {
+ interleaved := Syllables(strings.ToLower(norm.NFD.String(input)), r)
+ var sb strings.Builder
+ for i, s := range interleaved {
+ if i % 2 == 1 {
+ sb.WriteString(transform(s, interleaved[i - 1][0] != "" || i == 1))
+ } else {
+ sb.WriteString(s[0])
+ }
+ }
+ return norm.NFC.String(sb.String())
+}
+
+// returns an array of junk and Toaq, interleaved
+func Syllables(s string, r *regexp.Regexp) [][]string {
+ acc := [][]string{}
+ for {
+ bounds := r.FindStringSubmatchIndex(s)
+ if bounds == nil {
+ break
+ }
+ preemptive := r.FindStringSubmatchIndex(s[bounds[1] - 1:])
+ if preemptive != nil && preemptive[0] == 0 &&
+ bounds[len(bounds) - 1] - bounds[len(bounds) - 2] > 0 {
+ bounds[1]--
+ bounds[len(bounds) - 1]--
+ }
+ acc = append(acc, []string{s[:bounds[0]]})
+ ln := len(bounds) / 2
+ contentful := make([]string, ln)
+ for j := 0; j < ln; j++ {
+ contentful[j] = s[bounds[2 * j]:bounds[2 * j + 1]]
+ }
+ acc = append(acc, contentful)
+ s = s[bounds[1]:]
+ }
+ acc = append(acc, []string{s})
+ return acc
+}
diff --git a/vietoaq/vietoaq_test.go b/vietoaq/vietoaq_test.go
new file mode 100644
index 0000000..15a2455
--- /dev/null
+++ b/vietoaq/vietoaq_test.go
@@ -0,0 +1,33 @@
+package vietoaq
+
+import (
+ "testing"
+)
+
+var (
+ examples = map[string]string{
+ ``: ``,
+ `bbb`: `bbb`,
+ `jảq hủı óq`: `jam huin xob`,
+ `ýhō`: `xyphor`,
+ `gı'aq`: `gixaq`,
+ `gï'aq`: `gixxaq`,
+ `jảq'a`: `jamxa`,
+ `gï aq`: `gix xaq`,
+ `aq'aq aq`: `xaqxaq xaq`,
+ }
+)
+
+func TestVietoaq(t *testing.T) {
+ for regular, vietoaq := range examples {
+ vietoaq_ := To(regular)
+ if vietoaq_ != vietoaq {
+ t.Errorf(" to: %s -> %s != %s", regular, vietoaq_, vietoaq)
+ }
+ regular_ := From(vietoaq)
+ if regular_ != regular {
+ t.Errorf("from: %s -> %s != %s", vietoaq, regular_, regular)
+ }
+ }
+}
+