package main import ( "bytes" "errors" "log" "net/http" "net/url" "os" "time" "runtime" "strings" "syscall" "marisa.chaotic.ninja/mai" "marisa.chaotic.ninja/mai/engines" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/favicon" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/template/html/v2" ) var ( configfile string groupname string username string ) var conf struct { danmaku int listen string staticpath string tmplpath string } func MaiSkipLimiter(c *fiber.Ctx) bool { // Paths listed here are not considered for rate limiting path := c.Path() return strings.HasPrefix(path, "/static") || strings.HasPrefix(path, "/docs") } func main() { parseFlags() if configfile != "" { readConf(configfile) } // Default settings conf.danmaku = 10 conf.listen = "127.0.0.1:5000" conf.staticpath = "./static" conf.tmplpath = "./views" if username != "" { uid, gid, err := usergroupids(username, groupname) if err != nil { log.Fatal(err) } syscall.Setuid(uid) syscall.Setgid(gid) } engine := html.New(conf.tmplpath, ".html") engine.AddFunc("inc", func(i int) int { return i + 1 }) server := fiber.New( fiber.Config{ AppName: "Mai", ProxyHeader: fiber.HeaderXForwardedFor, TrustedProxies: []string{"0.0.0.0/0"}, ServerHeader: "Mai (using Fiber v2.x)", Views: engine, ErrorHandler: func(c *fiber.Ctx, err error) error { code := fiber.StatusInternalServerError err = c.Status(code).Render("pages/error", fiber.Map{"Title": "Error", "Error": err}) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") } return nil }, }) server.Use(recover.New()) server.Use(favicon.New( favicon.Config{ File: conf.staticpath + "/favicon.ico", }, )) server.Use(logger.New( logger.Config{ Format: "==> ${ip}:${port} ${status} - ${method} ${path}\n", DisableColors: true, Output: os.Stdout, Next: MaiSkipLimiter, }, )) server.Use(limiter.New(limiter.Config{ Next: MaiSkipLimiter, Max: conf.danmaku, Expiration: 30 * time.Second, LimiterMiddleware: limiter.SlidingWindow{}, LimitReached: func(c *fiber.Ctx) error { log.Println("Limit reached!") return errors.New("You're firing way too many danmaku really fast!") }, })) server.All("/", func(c *fiber.Ctx) error { engine := c.Cookies("engine") if c.Query("engine") != "" { engine = c.Query("engine") } if _, ok := engines.Engines[engine]; !ok { engine = "google" } targetLanguages, err := engines.Engines[engine].TargetLanguages() if err != nil { return c.SendStatus(500) } sourceLanguages, err := engines.Engines[engine].SourceLanguages() if err != nil { return c.SendStatus(500) } originalText := "" translatedText := "" from := "" to := "" ttsFrom := "" ttsTo := "" sourceLanguage := "" var translation engines.TranslationResult if c.Method() == "POST" { from = c.FormValue("from") to = c.FormValue("to") originalText = c.FormValue("text") if result, err := engines.Engines[engine].Translate(originalText, from, to); err != nil { return c.SendStatus(500) } else { translatedText = result.TranslatedText translation = result sourceLanguage = result.SourceLanguage } ttsFromURL, _ := url.Parse("api/tts") fromQuery := url.Values{} fromQuery.Add("lang", from) fromQuery.Add("text", originalText) ttsFromURL.RawQuery = fromQuery.Encode() ttsFrom = ttsFromURL.String() ttsToURL, _ := url.Parse("api/tts") toQuery := url.Values{} toQuery.Add("lang", to) toQuery.Add("text", translatedText) ttsToURL.RawQuery = toQuery.Encode() ttsTo = ttsToURL.String() fromCookie := new(fiber.Cookie) fromCookie.Name = "from" fromCookie.Value = from fromCookie.Expires = time.Now().Add(time.Hour * 24 * 365) c.Cookie(fromCookie) toCookie := new(fiber.Cookie) toCookie.Name = "to" toCookie.Value = to toCookie.Expires = time.Now().Add(time.Hour * 24 * 365) c.Cookie(toCookie) engineCookie := new(fiber.Cookie) engineCookie.Name = "engine" engineCookie.Value = engine engineCookie.Expires = time.Now().Add(time.Hour * 24 * 365) c.Cookie(engineCookie) } else if c.Method() == "GET" { from = c.Cookies("from") to = c.Cookies("to") } else { return c.SendStatus(400) } if from == "" { from = "auto" } enginesNames := map[string]string{} for k, v := range engines.Engines { enginesNames[k] = v.DisplayName() } return c.Render("index", fiber.Map{ "Engine": engine, "enginesNames": enginesNames, "SourceLanguages": sourceLanguages, "TargetLanguages": targetLanguages, "OriginalText": originalText, "Translation": translation, "From": from, "To": to, "TtsFrom": ttsFrom, "TtsTo": ttsTo, "SourceLanguage": sourceLanguage, }) }) server.All("/api/translate", func(c *fiber.Ctx) error { from := "" to := "" engine := "" text := "" if c.Method() == "GET" { engine = c.Query("engine") text = c.Query("text") from = c.Query("from") to = c.Query("to") } else if c.Method() == "POST" { engine = c.FormValue("engine") text = c.FormValue("text") from = c.FormValue("from") to = c.FormValue("to") } else { return c.SendStatus(400) } if _, ok := engines.Engines[engine]; !ok || engine == "" { engine = "google" } if to == "" { return c.SendStatus(400) } if result, err := engines.Engines[engine].Translate(text, from, to); err != nil { return c.SendStatus(500) } else { return c.JSON(result) } }) server.Get("/api/source_languages", func(c *fiber.Ctx) error { engine := c.Query("engine") if _, ok := engines.Engines[engine]; !ok || engine == "" { engine = "google" } if result, err := engines.Engines[engine].SourceLanguages(); err != nil { return c.SendStatus(500) } else { return c.JSON(result) } }) server.Get("/api/target_languages", func(c *fiber.Ctx) error { engine := c.Query("engine") if _, ok := engines.Engines[engine]; !ok || engine == "" { engine = "google" } if result, err := engines.Engines[engine].TargetLanguages(); err != nil { return c.SendStatus(500) } else { return c.JSON(result) } }) server.Get("/api/tts", func(c *fiber.Ctx) error { engine := c.Query("engine") if _, ok := engines.Engines[engine]; !ok || engine == "" { engine = "google" } text := c.Query("text") if text == "" { return c.SendStatus(400) } lang := c.Query("lang") if url, err := engines.Engines[engine].Tts(text, lang); err != nil { return c.SendStatus(500) } else { if response, err := http.Get(url); err != nil { return c.SendStatus(500) } else { defer response.Body.Close() var buf bytes.Buffer response.Write(&buf) c.Context().SetContentType("audio/mpeg") return c.Send(buf.Bytes()) } } }) server.Get("/robots.txt", func(c *fiber.Ctx) error { return c.SendString("User-Agent: *\nDisallow: /\n") }) server.Get("/toomanyrequests", func(c *fiber.Ctx) error { return c.SendFile(conf.tmplpath + "/429.html") return c.SendStatus(429) }) server.Get("/version", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{ "fiberversion": fiber.Version, "goversion": runtime.Version(), "maiversion": mai.FullVersion(), }) }) server.Post("/switchlanguages", func(c *fiber.Ctx) error { if c.Cookies("from") != "" { fromCookie := new(fiber.Cookie) fromCookie.Name = "from" fromCookie.Value = c.Cookies("to") fromCookie.Expires = time.Now().Add(24 * time.Hour * 365) toCookie := new(fiber.Cookie) toCookie.Name = "to" toCookie.Value = c.Cookies("from") toCookie.Expires = time.Now().Add(24 * time.Hour * 365) c.Cookie(fromCookie) c.Cookie(toCookie) } return c.Redirect("/") }) server.Static("/static", conf.staticpath, fiber.Static{ Compress: true, ByteRange: true, Browse: true, }) server.Static("/docs", "./docs", fiber.Static{}) server.Listen(conf.listen) }