diff --git a/LICENSE b/LICENSE index 8ee978b..e11aa89 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015,2016 Andreas Neue +Copyright (c) 2015,2016 Andreas Neue, Daniel Aberger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/main.go b/main.go index 44bed25..666c480 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,9 @@ var ( channels = flag.String("chan", "#test", "Channels to join") nsname = flag.String("nsname", "NickServ", "NickServ name") nspass = flag.String("nspass", "", "NickServ password") + mods = flag.String("mods", "", "Modules to load") params = flag.String("params", "", "Module params") + autocmd = flag.String("autocmd", "", "Autosend IRC command") ) func init() { @@ -43,7 +45,7 @@ var ( func main() { sayCh = make(chan string, 1024) - xlog.Init(xlog.INFO) + xlog.Init(xlog.DEBUG) //bot := ircx.Classic(*server, *name) cfg := ircx.Config{User: *name, MaxRetries: 1000} @@ -57,7 +59,8 @@ func main() { //mods := strings.Split(*modules, ",") //TODO: implement more robust list parsing - modules.Init(sayCh, *params) + modules.Init(sayCh, *mods, *params) + modules.ModParams["_nick"] = *name go func() { for { @@ -82,6 +85,11 @@ func main() { } }() + go Ping(bot) + if *autocmd != "" { + println(*autocmd) + bot.Sender.Send(&irc.Message{Command: *autocmd}) + } RegisterHandlers(bot) bot.HandleLoop() xlog.Info("Exiting") @@ -94,6 +102,13 @@ func RegisterHandlers(bot *ircx.Bot) { bot.HandleFunc(irc.PRIVMSG, PrivmsgHandler) } +func Ping(bot *ircx.Bot) { + for { + time.Sleep(1 * time.Minute) + bot.Sender.Send(&irc.Message{Command: irc.PING}) + } +} + func ConnectHandler(s ircx.Sender, m *irc.Message) { if *nspass != "" { xlog.Info("Authenticating with NickServ: %v, %v", *name, *nspass) diff --git a/modules/announcements.go b/modules/announcements.go index a73ac8b..be2b1db 100644 --- a/modules/announcements.go +++ b/modules/announcements.go @@ -5,16 +5,13 @@ package modules import ( "strings" - "code.dnix.de/an/xlog" - "github.com/sorcix/irc" ) var () func init() { - MsgHandlers["announcements"] = anncouncementsHandleMessage - xlog.Info("Announcements module initialized") + MsgFuncs["announcements"] = anncouncementsHandleMessage } func anncouncementsHandleMessage(m *irc.Message) { diff --git a/modules/coffee.go b/modules/coffee.go index f1773ce..05a0207 100644 --- a/modules/coffee.go +++ b/modules/coffee.go @@ -12,8 +12,6 @@ import ( "sync" "time" - "code.dnix.de/an/xlog" - "github.com/sorcix/irc" "github.com/sorcix/irc/ctcp" ) @@ -29,9 +27,8 @@ var ( ) func init() { - MsgHandlers["coffee"] = coffeeHandleMessage + MsgFuncs["coffee"] = coffeeHandleMessage r.names = make(map[string]bool) - xlog.Info("Coffee module initialized") } func coffeeHandleMessage(m *irc.Message) { @@ -108,4 +105,3 @@ func printAction(s string) { msg := ctcp.Encode(ctcp.ACTION, fmt.Sprintf(s)) SayCh <- fmt.Sprintf("%s\n%s", "*", msg) } - diff --git a/modules/fortune.go b/modules/fortune.go index b71172d..b0cd8d4 100644 --- a/modules/fortune.go +++ b/modules/fortune.go @@ -8,14 +8,11 @@ import ( "os/exec" "strings" - "code.dnix.de/an/xlog" - "github.com/sorcix/irc" ) func init() { - MsgHandlers["fortune"] = fortuneHandleMessage - xlog.Info("Fortune module initialized") + MsgFuncs["fortune"] = fortuneHandleMessage } func fortuneHandleMessage(m *irc.Message) { diff --git a/modules/fuzzytime.go b/modules/fuzzytime.go index b6197f7..802a66c 100644 --- a/modules/fuzzytime.go +++ b/modules/fuzzytime.go @@ -7,14 +7,11 @@ import ( "strings" "time" - "code.dnix.de/an/xlog" - "github.com/sorcix/irc" ) func init() { - MsgHandlers["fuzzytime"] = fuzzytimeHandleMessage - xlog.Info("Fuzzytime module initialized") + MsgFuncs["fuzzytime"] = fuzzytimeHandleMessage } func fuzzytimeHandleMessage(m *irc.Message) { diff --git a/modules/gogs.go b/modules/gogs.go index ccd36ed..026c657 100644 --- a/modules/gogs.go +++ b/modules/gogs.go @@ -8,8 +8,6 @@ import ( "strings" "time" - "code.dnix.de/an/xlog" - "github.com/sorcix/irc" ) @@ -18,8 +16,8 @@ var ( ) func init() { - MsgHandlers["gogs"] = gogsHandleMessage - xlog.Info("Gogs module initialized") + MsgFuncs["gogs"] = gogsHandleMessage + RunFuncs["gogs"] = gogsConfig } func gogsConfig() { diff --git a/modules/markov.go b/modules/markov.go new file mode 100644 index 0000000..c930300 --- /dev/null +++ b/modules/markov.go @@ -0,0 +1,273 @@ +// vi:ts=4:sts=4:sw=4:noet:tw=72 + +package modules + +// This Markov chain code is taken from the "Generating arbitrary text" +// codewalk: http://golang.org/doc/codewalk/markov/ +// +// Minor modifications have been made to make it easier to integrate +// with a webserver and to save/load state + +import ( + "bufio" + "encoding/gob" + "fmt" + "math/rand" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "code.dnix.de/an/xlog" + + "github.com/sorcix/irc" +) + +var markovChain *MarkovChain + +func init() { + MsgFuncs["markov"] = markovHandleMessage + RunFuncs["markov"] = markovRun +} + +func markovHandleMessage(m *irc.Message) { + text := m.Trailing + if text == "" { + return + } + text = markovParseText(text) + + answerLen, _ := strconv.Atoi(ModParams["markov-answer-len"]) + respChance, _ := strconv.Atoi(ModParams["markov-response-chance"]) + if rand.Intn(100) <= respChance || strings.Index(text, strings.ToLower(ModParams["_nick"])) != -1 { + responseText := markovChain.Generate(answerLen, text) + if responseText != "" { + go func() { + time.Sleep(time.Duration(rand.Intn(8)+2) * time.Second) + SayCh <- "*\n" + responseText + }() + } + } + + markovChain.Write(text) +} + +func markovRun() { + prefixLen, _ := strconv.Atoi(ModParams["markov-prefix-len"]) + markovChain = markovNewChain(prefixLen) + err := markovChain.Load(ModParams["markov-state-file"]) + if err != nil { + xlog.Error(err.Error()) + } + filepath := ModParams["markov-import-file"] + if filepath != "-" { + file, _ := os.Open(filepath) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + text := scanner.Text() + text = markovParseText(text) + if text != "" { + markovChain.Write(text) + } + } + } + go func() { + for { + time.Sleep(60 * time.Second) + markovChain.Save(ModParams["markov-state-file"]) + } + }() + +} + +func markovParseText(text string) string { + messageRegex := regexp.MustCompile(`<([^>]+)>`) + matches := messageRegex.FindAllStringSubmatch(text, -1) + for _, matches2 := range matches { + + if strings.HasPrefix(matches2[1], "http") || strings.HasPrefix(matches2[1], "mailto") { + text = strings.Replace(text, matches2[0], "", -1) + + } else if strings.HasPrefix(matches2[1], "@U") { + parts := strings.SplitN(matches2[1], "|", 2) + + if len(parts) == 2 { + text = strings.Replace(text, matches2[0], "@"+parts[1], -1) + } else { + text = strings.Replace(text, matches2[0], "", -1) + } + + } else if strings.HasPrefix(matches2[1], "@") { + text = strings.Replace(text, matches2[0], matches2[1], -1) + + } else if strings.HasPrefix(matches2[1], "#") { + parts := strings.SplitN(matches2[1], "|", 2) + + if len(parts) == 2 { + text = strings.Replace(text, matches2[0], "#"+parts[1], -1) + } else { + text = strings.Replace(text, matches2[0], "", -1) + } + + } + } + + text = strings.TrimSpace(text) + + text = strings.Replace(text, "<", "<", -1) + text = strings.Replace(text, ">", ">", -1) + text = strings.Replace(text, "&", "&", -1) + + return strings.ToLower(text) +} + +// Prefix is a Markov chain prefix of one or more words. +type MarkovPrefix []string + +// String returns the Prefix as a string (for use as a map key). +func (p MarkovPrefix) String() string { + return strings.Trim(strings.Join(p, " "), " ") +} + +// Shift removes the first word from the Prefix and appends the given word. +func (p MarkovPrefix) Shift(word string) { + copy(p, p[1:]) + p[len(p)-1] = word +} + +// MarkovChain contains a map ("chain") of prefixes to a list of suffixes. +// A prefix is a string of prefixLen words joined with spaces. +// A suffix is a single word. A prefix can have multiple suffixes. +type MarkovChain struct { + MarkovChain map[string][]string + prefixLen int + mu sync.Mutex +} + +// NewMarkovChain returns a new MarkovChain with prefixes of prefixLen words. +func markovNewChain(prefixLen int) *MarkovChain { + return &MarkovChain{ + MarkovChain: make(map[string][]string), + prefixLen: prefixLen, + } +} + +// Write parses the bytes into prefixes and suffixes that are stored in MarkovChain. +func (c *MarkovChain) Write(in string) (int, error) { + in = strings.ToLower(in) + if strings.HasPrefix(in, strings.ToLower(ModParams["_nick"])) { + tok := strings.Split(in, " ") + in = strings.Replace(in, tok[0]+" ", "", 1) + } + sr := strings.NewReader(in) + p := make(MarkovPrefix, c.prefixLen) + for { + var s string + if _, err := fmt.Fscan(sr, &s); err != nil { + break + } + key := p.String() + c.mu.Lock() + c.MarkovChain[key] = append(c.MarkovChain[key], s) + c.mu.Unlock() + xlog.Debug("Chain len: %d, learned [%s] [%s]", len(c.MarkovChain), key, s) + p.Shift(s) + } + return len(in), nil +} + +// Generate returns a string of at most n words generated from MarkovChain. +func (c *MarkovChain) Generate(n int, in string) string { + in = strings.ToLower(in) + if strings.HasPrefix(in, strings.ToLower(ModParams["_nick"])) { + tok := strings.Split(in, " ") + in = strings.Replace(in, tok[0]+" ", "", 1) + } + c.mu.Lock() + defer c.mu.Unlock() + p := make(MarkovPrefix, c.prefixLen) + p = strings.Split(in, " ") + if len(p) > c.prefixLen { + i := rand.Intn(len(p) - 1) + p = p[i : i+c.prefixLen] + } + prefix := p.String() + xlog.Debug("Looking for answer on [%s]", prefix) + var words []string + for i := 0; i < n; i++ { + choices := c.MarkovChain[p.String()] + if len(choices) == 0 { + break + } + next := choices[rand.Intn(len(choices))] + words = append(words, next) + if strings.HasSuffix(next, ".") || strings.HasSuffix(next, "!") || strings.HasSuffix(next, "?") { + break + } + p.Shift(next) + } + prefix = strings.Trim(prefix, " ") + if len(words) == 0 { + xlog.Debug("No answer found") + return prefix + " ... pfrrrz" + } else { + xlog.Debug("Found words: [%s]", strings.Join(words, " ")) + return prefix + " " + strings.Join(words, " ") + } +} + +// Save the chain to a file +func (c *MarkovChain) Save(fileName string) error { + // Open the file for writing + fo, err := os.Create(fileName) + if err != nil { + return err + } + // close fo on exit and check for its returned error + defer func() { + if err := fo.Close(); err != nil { + panic(err) + } + }() + + // Create an encoder and dump to it + c.mu.Lock() + defer c.mu.Unlock() + + enc := gob.NewEncoder(fo) + err = enc.Encode(c) + if err != nil { + return err + } + + return nil +} + +// Load the chain from a file +func (c *MarkovChain) Load(fileName string) error { + // Open the file for reading + fi, err := os.Open(fileName) + if err != nil { + return err + } + // close fi on exit and check for its returned error + defer func() { + if err := fi.Close(); err != nil { + panic(err) + } + }() + + // Create a decoder and read from it + c.mu.Lock() + defer c.mu.Unlock() + + dec := gob.NewDecoder(fi) + err = dec.Decode(c) + if err != nil { + return err + } + + return nil +} diff --git a/modules/modules.go b/modules/modules.go index 482d3df..cdc02c1 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -8,26 +8,51 @@ package modules import ( "strings" + "time" "github.com/sorcix/irc" ) var ( - SayCh chan string - MsgHandlers = make(map[string]func(*irc.Message)) - ModParams = make(map[string]string) + SayCh chan string + MsgFuncs = make(map[string]func(*irc.Message)) + RunFuncs = make(map[string]func()) + ModParams = make(map[string]string) ) -func Init(ch chan string, params string) { +func Init(ch chan string, mods, params string) { + time.Sleep(5 * time.Second) SayCh = ch + for mod, _ := range MsgFuncs { + if !contains(strings.Split(mods, ","), mod) { + delete(MsgFuncs, mod) + } + } + for mod, _ := range RunFuncs { + if !contains(strings.Split(mods, ","), mod) { + delete(RunFuncs, mod) + } + } for _, param := range strings.Split(params, "!") { kv := strings.Split(param, ":") ModParams[kv[0]] = kv[1] } + for _, fn := range RunFuncs { + go fn() + } } func HandleMessage(m *irc.Message) { - for _, fn := range MsgHandlers { + for _, fn := range MsgFuncs { fn(m) } } + +func contains(sa []string, s string) bool { + for _, a := range sa { + if a == s { + return true + } + } + return false +} diff --git a/modules/quiz.go b/modules/quiz.go new file mode 100644 index 0000000..a49d6cc --- /dev/null +++ b/modules/quiz.go @@ -0,0 +1,247 @@ +// vi:ts=4:sts=4:sw=4:noet:tw=72 + +package modules + +import ( + "bufio" + "flokatirc/util" + "fmt" + "math/rand" + "os" + "regexp" + "strings" + "time" + + "github.com/sorcix/irc" + + "code.dnix.de/an/xlog" +) + +type quizQuestion struct { + category, question, answer, regexp, level string +} + +var ( + quizCtrlCh = make(chan string, 1024) + quizAnswerCh = make(chan *irc.Message, 1024) + quizQuestions []quizQuestion + quizQuestionValues = map[string]int{"extreme": 5, "hard": 4, "normal": 3, "easy": 2, "baby": 1} +) + +func init() { + MsgFuncs["quiz"] = quizHandleMessage + RunFuncs["quiz"] = quiz + rand.Seed(time.Now().Unix()) +} + +func quizHandleMessage(m *irc.Message) { + tok := strings.Split(m.Trailing, " ") + if len(tok) < 1 { + return + } + switch tok[0] { + case "!quiz", "!quizstart": + quizCtrlCh <- "start" + break + case "!quizstop": + quizCtrlCh <- "stop" + break + default: + quizAnswerCh <- m + break + } +} + +func quiz() { + time.Sleep(5 * time.Second) + SayCh <- fmt.Sprintf("%s\nquiz mod test. !quizstart to start, !quizstop to end.", "*") + for { + time.Sleep(1 * time.Millisecond) + select { + case s := <-quizCtrlCh: + if s == "start" { + quizRun() + } + default: + break + } + } +} + +func quizRun() { + quizQuestions = quizLoadQuestions("questions.txt") + ranklist := make(map[string]int) + SayCh <- fmt.Sprintf("%s\nQuiz gestartet.", "*") + for { + solved, cont, solver, gain := quizAskQuestion() + if !cont { + SayCh <- fmt.Sprintf("%s\nQuiz beendet.", "*") + return + } else { + if solved { + if _, exists := ranklist[solver]; exists { + ranklist[solver] += gain + } else { + ranklist[solver] = gain + } + if ranklist[solver] > 1000 { + bold := byte(0x02) + solver = string(bold) + solver + string(bold) + SayCh <- fmt.Sprintf("%s\n%s gewinnt!", "*", solver) + quizPrintRanklist(ranklist) + SayCh <- fmt.Sprintf("%s\nQuiz beendet.", "*") + return + } + } + } + quizPrintRanklist(ranklist) + } +} + +func quizPrintRanklist(ranklist map[string]int) { + SayCh <- fmt.Sprintf("%s\nAktueller Punktestand:", "*") + for k, v := range ranklist { + SayCh <- fmt.Sprintf("%s\n%s: %d", "*", k, v) + } +} + +func quizAskQuestion() (bool, bool, string, int) { + q := quizQuestions[rand.Intn(len(quizQuestions))] + SayCh <- fmt.Sprintf("%s\nEine Frage aus der Kategorie \"%s\" (%s):", "*", q.category, q.level) + SayCh <- fmt.Sprintf("%s\n>>> %s <<<", "*", q.question) + solved, cont, solver, gain := quizWaitForAnswer(q) + if !solved { + SayCh <- fmt.Sprintf("%s\nDie richtige Antwort wäre gewesen:", "*") + SayCh <- fmt.Sprintf("%s\n%s [%s]", "*", q.answer, q.regexp) + } + time.Sleep(5 * time.Second) + return solved, cont, solver, gain +} + +func quizWaitForAnswer(q quizQuestion) (bool, bool, string, int) { + i := 0 + haveAnswer := false + timer := time.Now().Unix() + for { + select { + case m := <-quizAnswerCh: + haveAnswer = true + points := 10 - ((time.Now().Unix() - timer) / 10) + if points < 1 { + points = 1 + } + re, err := regexp.Compile(q.regexp) + if err != nil { + xlog.Error(err.Error()) + return false, false, "", 0 + } + if re.MatchString(strings.ToLower(util.ReplaceUmlauts(m.Trailing))) { + bold := byte(0x02) + solver := strings.Split(m.Prefix.String(), "!")[0] + solver = string(bold) + solver + string(bold) + value := quizQuestionValues[q.level] + gain := int(points) * value + SayCh <- fmt.Sprintf("%s\n%s hat die Frage korrekt beantwortet und erhält dafür %d (%d * %d) Punkte.", "*", solver, gain, points, value) + return true, true, solver, gain + } + break + case s := <-quizCtrlCh: + if s == "stop" { + return false, false, "", 0 + } + break + case <-time.After(15 * time.Second): + i++ + if i > 3 { + if haveAnswer { + SayCh <- fmt.Sprintf("%s\nNiemand konnte die Frage korrekt beantworten.", "*") + return false, true, "", 0 + } else { + SayCh <- fmt.Sprintf("%s\nNiemand hat auf die Frage geantwortet.", "*") + return false, false, "", 0 + } + } else { + quizGiveHint(q, i) + } + } + } +} + +func quizGiveHint(q quizQuestion, n int) { + var hint string + haveHint := false + for { + hint = "" + for i := 0; i < len(q.answer); i++ { + if string(q.answer[i]) == " " { + hint += " " + } else { + if rand.Intn(10) < 2 { + haveHint = true + hint += string(q.answer[i]) + } else { + hint += "-" + } + } + } + if haveHint { + break + } + } + SayCh <- fmt.Sprintf("%s\nTipp: %s", "*", hint) +} + +func quizLoadQuestions(path string) []quizQuestion { + file, err := os.Open(path) + if err != nil { + xlog.Fatal(err.Error()) + } + defer file.Close() + + questions := make([]quizQuestion, 0) + q := quizQuestion{"", "", "", "", ""} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 || line[0] == '#' || line[0] == '\n' { + if q.category != "" { + questions = append(questions, q) + q = quizQuestion{"", "", "", "", "normal"} + } + } else { + tok := strings.Split(line, ":: ") + switch tok[0] { + case "Category": + q.category = tok[1] + break + case "Question": + q.question = tok[1] + break + case "Answer": + q.answer = strings.Replace(util.ReplaceUmlauts(tok[1]), "#", "", -1) + if q.regexp == "" { + regexp := strings.Split(strings.ToLower(util.ReplaceUmlauts(tok[1])), "#") + if len(regexp) > 1 { + q.regexp = regexp[1] + } else { + q.regexp = regexp[0] + } + } + break + case "Regexp": + q.regexp = util.ReplaceUmlauts(strings.ToLower(tok[1])) + break + case "Level": + q.level = tok[1] + break + } + } + } + + if err := scanner.Err(); err != nil { + xlog.Fatal(err.Error()) + } + + return questions +} diff --git a/modules/rss.go b/modules/rss.go index 93685a4..62d76e7 100644 --- a/modules/rss.go +++ b/modules/rss.go @@ -24,9 +24,8 @@ import ( var hideOutput = true func init() { - MsgHandlers["rss"] = rssHandleMessage - go rssRun() - xlog.Info("RSS module initialized") + MsgFuncs["rss"] = rssHandleMessage + RunFuncs["rss"] = rssRun } func rssRun() { diff --git a/modules/sc.go b/modules/sc.go index 3d50d51..4952943 100644 --- a/modules/sc.go +++ b/modules/sc.go @@ -48,9 +48,8 @@ var ( ) func init() { - MsgHandlers["sc"] = scHandleMessage - go scScrapeLoop() - xlog.Info("SC module initialized") + MsgFuncs["sc"] = scHandleMessage + RunFuncs["sc"] = scScrapeLoop } func scHandleMessage(m *irc.Message) { diff --git a/modules/stoll.go b/modules/stoll.go index 59a6931..32dd122 100644 --- a/modules/stoll.go +++ b/modules/stoll.go @@ -3,11 +3,10 @@ package modules import ( - "flokatirc/util" "fmt" + "math/rand" "strings" - - "code.dnix.de/an/xlog" + "time" "github.com/sorcix/irc" ) @@ -522,8 +521,8 @@ var quotes = [][]string{ } func init() { - MsgHandlers["stoll"] = stollHandleMessage - xlog.Info("Stoll module initialized") + MsgFuncs["stoll"] = stollHandleMessage + rand.Seed(time.Now().Unix()) } func stollHandleMessage(m *irc.Message) { @@ -534,7 +533,7 @@ func stollHandleMessage(m *irc.Message) { if tok[0] == "!stoll" { line := "" for i := 0; i < 3; i++ { - line += quotes[i][util.Random(0, len(quotes[i]))] + line += quotes[i][rand.Intn(len(quotes[i]))] line += " " } line += "[Dr. Axel Stoll, promovierter Naturwissenschaftler]" diff --git a/modules/twitch.go b/modules/twitch.go index a94bda3..e2d2f48 100644 --- a/modules/twitch.go +++ b/modules/twitch.go @@ -137,9 +137,8 @@ var ( ) func init() { - MsgHandlers["twitch"] = twitchHandleMessage - go pollStreamData() - xlog.Info("Twitch module initialized") + MsgFuncs["twitch"] = twitchHandleMessage + RunFuncs["twitch"] = pollStreamData } func twitchHandleMessage(m *irc.Message) { diff --git a/modules/weather.go b/modules/weather.go index dce2019..a63486b 100644 --- a/modules/weather.go +++ b/modules/weather.go @@ -71,9 +71,8 @@ type WeatherObject struct { } func init() { - MsgHandlers["weather"] = weatherHandleMessage - go weatherConfig() - xlog.Info("Weather module initialized") + MsgFuncs["weather"] = weatherHandleMessage + RunFuncs["weather"] = weatherConfig } func weatherConfig() { diff --git a/util/util.go b/util/util.go index a3d1d0b..e39e041 100644 --- a/util/util.go +++ b/util/util.go @@ -4,9 +4,8 @@ package util import ( "bytes" - "math/rand" "strconv" - "time" + "strings" ) func ToInt(v interface{}) int { @@ -48,7 +47,13 @@ func NumberToString(n int, sep rune) string { return buf.String() } -func Random(min, max int) int { - rand.Seed(time.Now().Unix()) - return rand.Intn(max-min) + min +func ReplaceUmlauts(s string) string { + ret := strings.Replace(s, "Ä", "Ae", -1) + ret = strings.Replace(ret, "Ö", "Oe", -1) + ret = strings.Replace(ret, "Ü", "Ue", -1) + ret = strings.Replace(ret, "ä", "ae", -1) + ret = strings.Replace(ret, "ö", "oe", -1) + ret = strings.Replace(ret, "ü", "ue", -1) + ret = strings.Replace(ret, "ß", "ss", -1) + return ret }