Рефакторинг, поднял минимальную версию go до 1.18
This commit is contained in:
2
Makefile
2
Makefile
@@ -17,6 +17,6 @@ wipe:
|
|||||||
|
|
||||||
prep:
|
prep:
|
||||||
go mod init main
|
go mod init main
|
||||||
go mod tidy -compat=1.17
|
go mod tidy -compat=1.18
|
||||||
|
|
||||||
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
||||||
|
|||||||
7
README
7
README
@@ -56,18 +56,17 @@ misc->username - имя пользователя, по которому к
|
|||||||
|
|
||||||
Как это собрать?
|
Как это собрать?
|
||||||
-----------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
Понадобится golang версии 1.16.8 или более новая (на предыдущих версиях возможно собирётся, но я не тестировал).
|
Понадобится golang версии 1.18 или более новая.
|
||||||
go.mod и go.sum актуальны для 1.17.7.
|
|
||||||
|
|
||||||
Сам процесс сборки простой, достаточно запустить:
|
Сам процесс сборки простой, достаточно запустить:
|
||||||
CGO_ENABLED=0 go build
|
make
|
||||||
|
|
||||||
и через некоторое время мы получаем бинарник aleesa-misc-go. Это и есть наше приложение.
|
и через некоторое время мы получаем бинарник aleesa-misc-go. Это и есть наше приложение.
|
||||||
|
|
||||||
|
|
||||||
Как это запустить?
|
Как это запустить?
|
||||||
-----------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------
|
||||||
Приложение тестировалось на alpine linux x86_64 3.12, но должно работать на любом (более свежем) linux-е.
|
Приложение тестировалось на alpine linux x86_64 3.16, но должно работать на любом (более свежем) linux-е.
|
||||||
|
|
||||||
После создания data/config.json из data/config_sample.json, можно запускать приложение.
|
После создания data/config.json из data/config_sample.json, можно запускать приложение.
|
||||||
|
|
||||||
|
|||||||
362
aleesa-misc-lib.go
Normal file
362
aleesa-misc-lib.go
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/hjson/hjson-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Горутинка, которая парсит json-чики прилетевшие из REDIS-ки
|
||||||
|
func msgParser(ctx context.Context, msg string) {
|
||||||
|
var sendTo = "craniac"
|
||||||
|
var j rMsg
|
||||||
|
|
||||||
|
log.Debugf("Incomming raw json: %s", msg)
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(msg), &j); err != nil {
|
||||||
|
log.Warnf("Unable to to parse message from redis channel: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate our j
|
||||||
|
if exist := j.From; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Chatid; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Userid; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Message; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Plugin; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Mode; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
|
||||||
|
// j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка
|
||||||
|
// j.Misc.Csign если нам его не передали, возьмём значение из конфига
|
||||||
|
if exist := j.Misc.Csign; exist == "" {
|
||||||
|
j.Misc.Csign = config.Csign
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.Fwdcnt если нам его не передали, то будет 0
|
||||||
|
if exist := j.Misc.Fwdcnt; exist == 0 {
|
||||||
|
j.Misc.Fwdcnt = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0
|
||||||
|
// j.Misc.Msgformat может быть быть 1 или 0, по-умолчанию 0
|
||||||
|
// j.Misc.Username можно не передавать, тогда будет пустая строка
|
||||||
|
|
||||||
|
// Отвалидировались, теперь вернёмся к нашим баранам.
|
||||||
|
|
||||||
|
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение
|
||||||
|
if j.Misc.Fwdcnt > config.ForwardsMax {
|
||||||
|
log.Warnf("Discarding msg with fwd_cnt exceeding max value: %s", msg)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
j.Misc.Fwdcnt++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Классифицирем входящие сообщения. Первым делом, попробуем определить команды
|
||||||
|
if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
|
||||||
|
// Может быть, это команда модуля phrases?
|
||||||
|
var done = false
|
||||||
|
var cmd = j.Message[len(j.Misc.Csign):]
|
||||||
|
cmds := []string{"friday", "пятница", "proverb", "пословица", "fortune", "фортунка", "f", "ф", "karma", "карма"}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmd == command {
|
||||||
|
sendTo = "phrases"
|
||||||
|
|
||||||
|
// Костыль для кармы
|
||||||
|
if cmd == "karma" || cmd == "карма" {
|
||||||
|
j.Misc.Answer = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не угадали? акей, как насчёт модуля webapp?
|
||||||
|
if !done {
|
||||||
|
cmds = []string{"buni", "anek", "анек", "анекдот", "cat", "кис", "drink", "праздник", "fox", "лис", "frog",
|
||||||
|
"лягушка", "horse", "лошадь", "лошадка", "monkeyuser", "rabbit", "bunny", "кролик", "snail", "улитка",
|
||||||
|
"owl", "сова", "сыч", "xkcd", "tits", "boobs", "tities", "boobies", "сиси", "сисечки", "butt", "booty",
|
||||||
|
"ass", "попа", "попка"}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmd == command {
|
||||||
|
sendTo = "webapp"
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не фортануло? может, повезёт с модулем games?
|
||||||
|
if !done {
|
||||||
|
cmds = []string{"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка"}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmd == command {
|
||||||
|
sendTo = "games"
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нет? Вдруг это комплексная команда?
|
||||||
|
if !done {
|
||||||
|
cmdLen := len(cmd)
|
||||||
|
|
||||||
|
cmds := []string{"w ", "п ", "погода ", "погодка ", "погадка ", "weather ", "karma ", "карма "}
|
||||||
|
|
||||||
|
for _, command := range cmds {
|
||||||
|
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
||||||
|
if command == "karma " || command == "карма " {
|
||||||
|
sendTo = "phrases"
|
||||||
|
|
||||||
|
// Костыль для кармы
|
||||||
|
j.Misc.Answer = 1
|
||||||
|
} else {
|
||||||
|
sendTo = "webapp"
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Попробуем выискать изменение кармы
|
||||||
|
msgLen := len(j.Message)
|
||||||
|
|
||||||
|
// ++ или -- на конце фразы - это наш кандидат
|
||||||
|
if msgLen > len("++") {
|
||||||
|
if j.Message[msgLen-len("--"):msgLen] == "--" || j.Message[msgLen-len("++"):msgLen] == "++" {
|
||||||
|
// Предполагается, что менять карму мы будем для одной фразы, то есть для 1 строки
|
||||||
|
if len(strings.Split(j.Message, "\n")) == 1 {
|
||||||
|
sendTo = "phrases"
|
||||||
|
|
||||||
|
// Костыль для кармы
|
||||||
|
j.Misc.Answer = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Настало время формировать json и засылать его в дальше
|
||||||
|
var message sMsg
|
||||||
|
message.From = j.From
|
||||||
|
message.Userid = j.Userid
|
||||||
|
message.Chatid = j.Chatid
|
||||||
|
message.Message = j.Message
|
||||||
|
message.Plugin = j.Plugin
|
||||||
|
message.Mode = j.Mode
|
||||||
|
message.Misc.Fwdcnt = j.Misc.Fwdcnt
|
||||||
|
message.Misc.Csign = j.Misc.Csign
|
||||||
|
message.Misc.Username = j.Misc.Username
|
||||||
|
message.Misc.Answer = j.Misc.Answer
|
||||||
|
message.Misc.Botnick = j.Misc.Botnick
|
||||||
|
message.Misc.Msgformat = j.Misc.Msgformat
|
||||||
|
message.Misc.GoodMorning = j.Misc.GoodMorning
|
||||||
|
|
||||||
|
data, err := json.Marshal(message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заталкиваем наш json в редиску
|
||||||
|
if err := redisClient.Publish(ctx, sendTo, data).Err(); err != nil {
|
||||||
|
log.Warnf("Unable to send data to redis channel %s: %s", sendTo, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Send msg to redis channel %s: %s", sendTo, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет
|
||||||
|
func readConfig() {
|
||||||
|
configLoaded := false
|
||||||
|
executablePath, err := os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to get current executable path: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
||||||
|
|
||||||
|
locations := []string{
|
||||||
|
"~/.aleesa-misc-go.json",
|
||||||
|
"~/aleesa-misc-go.json",
|
||||||
|
"/etc/aleesa-misc-go.json",
|
||||||
|
configJSONPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, location := range locations {
|
||||||
|
fileInfo, err := os.Stat(location)
|
||||||
|
|
||||||
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
||||||
|
if fileInfo.Size() > 65535 {
|
||||||
|
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ioutil.ReadFile(location)
|
||||||
|
|
||||||
|
// Не удалось прочитать, попробуем следующего кандидата
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
|
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
|
||||||
|
var sampleConfig myConfig
|
||||||
|
var tmp map[string]interface{}
|
||||||
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
|
// Не удалось распарсить - попробуем следующего кандидата
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpjson, err := json.Marshal(tmp)
|
||||||
|
|
||||||
|
// Не удалось преобразовать map-ку в json
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
||||||
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Валидируем значения из конфига
|
||||||
|
if sampleConfig.Server == "" {
|
||||||
|
sampleConfig.Server = "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Port == 0 {
|
||||||
|
sampleConfig.Port = 6379
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Loglevel == "" {
|
||||||
|
sampleConfig.Loglevel = "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Log = "" if not set
|
||||||
|
|
||||||
|
if sampleConfig.Channel == "" {
|
||||||
|
log.Errorf("Channel field in config file %s must be set", location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Частичная проверка, ровно то, куда мы _точно_ щепрввляем сообщения исходя из бизнес-логики приложения
|
||||||
|
if sampleConfig.ForwardChannels.Games == "" {
|
||||||
|
sampleConfig.ForwardChannels.Games = "games"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.ForwardChannels.Phrases == "" {
|
||||||
|
sampleConfig.ForwardChannels.Phrases = "phrases"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.ForwardChannels.Webapp == "" {
|
||||||
|
sampleConfig.ForwardChannels.Webapp = "webapp"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.ForwardChannels.Craniac == "" {
|
||||||
|
sampleConfig.ForwardChannels.Craniac = "craniac"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Csign == "" {
|
||||||
|
log.Errorf("Csign field in config file %s must be set", location)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.ForwardsMax == 0 {
|
||||||
|
sampleConfig.ForwardsMax = forwardMax
|
||||||
|
}
|
||||||
|
|
||||||
|
config = sampleConfig
|
||||||
|
configLoaded = true
|
||||||
|
log.Infof("Using %s as config file", location)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !configLoaded {
|
||||||
|
log.Error("Config was not loaded! Refusing to start.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хэндлер сигналов закрывает все бд и сваливает из приложения
|
||||||
|
func sigHandler() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
var s = <-sigChan
|
||||||
|
switch s {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
log.Infoln("Got SIGINT, quitting")
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
log.Infoln("Got SIGTERM, quitting")
|
||||||
|
case syscall.SIGQUIT:
|
||||||
|
log.Infoln("Got SIGQUIT, quitting")
|
||||||
|
|
||||||
|
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true
|
||||||
|
shutdown = true
|
||||||
|
|
||||||
|
// Отпишемся от всех каналов и закроем коннект к редиске
|
||||||
|
if err = subscriber.Unsubscribe(ctx); err != nil {
|
||||||
|
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = subscriber.Close(); err != nil {
|
||||||
|
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
29
globals.go
Normal file
29
globals.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config - это у нас глобальная штука :)
|
||||||
|
var config myConfig
|
||||||
|
|
||||||
|
// To break circular message forwarding we must set some sane default, it can be overridden via config
|
||||||
|
var forwardMax int64 = 5
|
||||||
|
|
||||||
|
// Объектики клиента-редиски
|
||||||
|
var redisClient *redis.Client
|
||||||
|
var subscriber *redis.PubSub
|
||||||
|
|
||||||
|
// Main context
|
||||||
|
var ctx = context.Background()
|
||||||
|
|
||||||
|
// Ставится в true, если мы получили сигнал на выключение
|
||||||
|
var shutdown = false
|
||||||
|
|
||||||
|
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов
|
||||||
|
var sigChan = make(chan os.Signal, 1)
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
8
go.mod
8
go.mod
@@ -1,12 +1,18 @@
|
|||||||
module aleesa-misc-go
|
module aleesa-misc-go
|
||||||
|
|
||||||
go 1.16
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-redis/redis/v8 v8.11.4
|
github.com/go-redis/redis/v8 v8.11.4
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/hjson/hjson-go v3.1.0+incompatible
|
github.com/hjson/hjson-go v3.1.0+incompatible
|
||||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||||
|
|||||||
426
main.go
426
main.go
@@ -2,440 +2,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/hjson/hjson-go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Конфиг
|
|
||||||
type myConfig struct {
|
|
||||||
Server string `json:"server,omitempty"`
|
|
||||||
Port int `json:"port,omitempty"`
|
|
||||||
Loglevel string `json:"loglevel,omitempty"`
|
|
||||||
Log string `json:"log,omitempty"`
|
|
||||||
Channel string `json:"channel,omitempty"`
|
|
||||||
ForwardChannels struct {
|
|
||||||
Games string `json:"games,omitempty"`
|
|
||||||
Phrases string `json:"phrases,omitempty"`
|
|
||||||
Webapp string `json:"webapp,omitempty"`
|
|
||||||
Craniac string `json:"craniac,omitempty"`
|
|
||||||
} `json:"forward_channels"`
|
|
||||||
Csign string `json:"csign,omitempty"`
|
|
||||||
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Входящее сообщение из pubsub-канала redis-ки
|
|
||||||
type rMsg struct {
|
|
||||||
From string `json:"from,omitempty"`
|
|
||||||
Chatid string `json:"chatid,omitempty"`
|
|
||||||
Userid string `json:"userid,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
Plugin string `json:"plugin,omitempty"`
|
|
||||||
Mode string `json:"mode,omitempty"`
|
|
||||||
Misc struct {
|
|
||||||
Answer int64 `json:"answer,omitempty"`
|
|
||||||
Botnick string `json:"bot_nick,omitempty"`
|
|
||||||
Csign string `json:"csign,omitempty"`
|
|
||||||
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
|
||||||
GoodMorning int64 `json:"good_morning,omitempty"`
|
|
||||||
Msgformat int64 `json:"msg_format,omitempty"`
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
} `json:"Misc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Исходящее сообщение в pubsub-канал redis-ки
|
|
||||||
type sMsg struct {
|
|
||||||
From string `json:"from"`
|
|
||||||
Chatid string `json:"chatid"`
|
|
||||||
Userid string `json:"userid"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Plugin string `json:"plugin"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
Misc struct {
|
|
||||||
Answer int64 `json:"answer"`
|
|
||||||
Botnick string `json:"bot_nick"`
|
|
||||||
Csign string `json:"csign"`
|
|
||||||
Fwdcnt int64 `json:"fwd_cnt"`
|
|
||||||
GoodMorning int64 `json:"good_morning"`
|
|
||||||
Msgformat int64 `json:"msg_format"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
} `json:"misc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config - это у нас глобальная штука :)
|
|
||||||
var config myConfig
|
|
||||||
|
|
||||||
// To break circular message forwarding we must set some sane default, it can be overridden via config
|
|
||||||
var forwardMax int64 = 5
|
|
||||||
|
|
||||||
// Объектики клиента-редиски
|
|
||||||
var redisClient *redis.Client
|
|
||||||
var subscriber *redis.PubSub
|
|
||||||
|
|
||||||
// Main context
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
// Ставится в true, если мы получили сигнал на выключение
|
|
||||||
var shutdown = false
|
|
||||||
|
|
||||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов
|
|
||||||
var sigChan = make(chan os.Signal, 1)
|
|
||||||
|
|
||||||
// Горутинка, которая парсит json-чики прилетевшие из REDIS-ки
|
|
||||||
func msgParser(ctx context.Context, msg string) {
|
|
||||||
var sendTo = "craniac"
|
|
||||||
var j rMsg
|
|
||||||
|
|
||||||
log.Debugf("Incomming raw json: %s", msg)
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(msg), &j); err != nil {
|
|
||||||
log.Warnf("Unable to to parse message from redis channel: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate our j
|
|
||||||
if exist := j.From; exist == "" {
|
|
||||||
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist := j.Chatid; exist == "" {
|
|
||||||
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist := j.Userid; exist == "" {
|
|
||||||
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist := j.Message; exist == "" {
|
|
||||||
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist := j.Plugin; exist == "" {
|
|
||||||
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist := j.Mode; exist == "" {
|
|
||||||
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
|
|
||||||
// j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка
|
|
||||||
// j.Misc.Csign если нам его не передали, возьмём значение из конфига
|
|
||||||
if exist := j.Misc.Csign; exist == "" {
|
|
||||||
j.Misc.Csign = config.Csign
|
|
||||||
}
|
|
||||||
|
|
||||||
// j.Misc.Fwdcnt если нам его не передали, то будет 0
|
|
||||||
if exist := j.Misc.Fwdcnt; exist == 0 {
|
|
||||||
j.Misc.Fwdcnt = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0
|
|
||||||
// j.Misc.Msgformat может быть быть 1 или 0, по-умолчанию 0
|
|
||||||
// j.Misc.Username можно не передавать, тогда будет пустая строка
|
|
||||||
|
|
||||||
// Отвалидировались, теперь вернёмся к нашим баранам.
|
|
||||||
|
|
||||||
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение
|
|
||||||
if j.Misc.Fwdcnt > config.ForwardsMax {
|
|
||||||
log.Warnf("Discarding msg with fwd_cnt exceeding max value: %s", msg)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
j.Misc.Fwdcnt++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Классифицирем входящие сообщения. Первым делом, попробуем определить команды
|
|
||||||
if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
|
|
||||||
// Может быть, это команда модуля phrases?
|
|
||||||
var done = false
|
|
||||||
var cmd = j.Message[len(j.Misc.Csign):]
|
|
||||||
cmds := []string{"friday", "пятница", "proverb", "пословица", "fortune", "фортунка", "f", "ф", "karma", "карма"}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmd == command {
|
|
||||||
sendTo = "phrases"
|
|
||||||
|
|
||||||
// Костыль для кармы
|
|
||||||
if cmd == "karma" || cmd == "карма" {
|
|
||||||
j.Misc.Answer = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
done = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Не угадали? акей, как насчёт модуля webapp?
|
|
||||||
if !done {
|
|
||||||
cmds = []string{"buni", "anek", "анек", "анекдот", "cat", "кис", "drink", "праздник", "fox", "лис", "frog",
|
|
||||||
"лягушка", "horse", "лошадь", "лошадка", "monkeyuser", "rabbit", "bunny", "кролик", "snail", "улитка",
|
|
||||||
"owl", "сова", "сыч", "xkcd", "tits", "boobs", "tities", "boobies", "сиси", "сисечки", "butt", "booty",
|
|
||||||
"ass", "попа", "попка"}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmd == command {
|
|
||||||
sendTo = "webapp"
|
|
||||||
done = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Не фортануло? может, повезёт с модулем games?
|
|
||||||
if !done {
|
|
||||||
cmds = []string{"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка"}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmd == command {
|
|
||||||
sendTo = "games"
|
|
||||||
done = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Нет? Вдруг это комплексная команда?
|
|
||||||
if !done {
|
|
||||||
cmdLen := len(cmd)
|
|
||||||
|
|
||||||
cmds := []string{"w ", "п ", "погода ", "погодка ", "погадка ", "weather ", "karma ", "карма "}
|
|
||||||
|
|
||||||
for _, command := range cmds {
|
|
||||||
if cmdLen > len(command) && cmd[0:len(command)] == command {
|
|
||||||
if command == "karma " || command == "карма " {
|
|
||||||
sendTo = "phrases"
|
|
||||||
|
|
||||||
// Костыль для кармы
|
|
||||||
j.Misc.Answer = 1
|
|
||||||
} else {
|
|
||||||
sendTo = "webapp"
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Попробуем выискать изменение кармы
|
|
||||||
msgLen := len(j.Message)
|
|
||||||
|
|
||||||
// ++ или -- на конце фразы - это наш кандидат
|
|
||||||
if msgLen > len("++") {
|
|
||||||
if j.Message[msgLen-len("--"):msgLen] == "--" || j.Message[msgLen-len("++"):msgLen] == "++" {
|
|
||||||
// Предполагается, что менять карму мы будем для одной фразы, то есть для 1 строки
|
|
||||||
if len(strings.Split(j.Message, "\n")) == 1 {
|
|
||||||
sendTo = "phrases"
|
|
||||||
|
|
||||||
// Костыль для кармы
|
|
||||||
j.Misc.Answer = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Настало время формировать json и засылать его в дальше
|
|
||||||
var message sMsg
|
|
||||||
message.From = j.From
|
|
||||||
message.Userid = j.Userid
|
|
||||||
message.Chatid = j.Chatid
|
|
||||||
message.Message = j.Message
|
|
||||||
message.Plugin = j.Plugin
|
|
||||||
message.Mode = j.Mode
|
|
||||||
message.Misc.Fwdcnt = j.Misc.Fwdcnt
|
|
||||||
message.Misc.Csign = j.Misc.Csign
|
|
||||||
message.Misc.Username = j.Misc.Username
|
|
||||||
message.Misc.Answer = j.Misc.Answer
|
|
||||||
message.Misc.Botnick = j.Misc.Botnick
|
|
||||||
message.Misc.Msgformat = j.Misc.Msgformat
|
|
||||||
message.Misc.GoodMorning = j.Misc.GoodMorning
|
|
||||||
|
|
||||||
data, err := json.Marshal(message)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Unable to to serialize message for redis: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Заталкиваем наш json в редиску
|
|
||||||
if err := redisClient.Publish(ctx, sendTo, data).Err(); err != nil {
|
|
||||||
log.Warnf("Unable to send data to redis channel %s: %s", sendTo, err)
|
|
||||||
} else {
|
|
||||||
log.Debugf("Send msg to redis channel %s: %s", sendTo, string(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет
|
|
||||||
func readConfig() {
|
|
||||||
configLoaded := false
|
|
||||||
executablePath, err := os.Executable()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to get current executable path: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
|
||||||
|
|
||||||
locations := []string{
|
|
||||||
"~/.aleesa-misc-go.json",
|
|
||||||
"~/aleesa-misc-go.json",
|
|
||||||
"/etc/aleesa-misc-go.json",
|
|
||||||
configJSONPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, location := range locations {
|
|
||||||
fileInfo, err := os.Stat(location)
|
|
||||||
|
|
||||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
|
||||||
if fileInfo.Size() > 65535 {
|
|
||||||
log.Warnf("Config file %s is too long for config, skipping", location)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := ioutil.ReadFile(location)
|
|
||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
|
||||||
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
|
|
||||||
var sampleConfig myConfig
|
|
||||||
var tmp map[string]interface{}
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
|
||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpjson, err := json.Marshal(tmp)
|
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Валидируем значения из конфига
|
|
||||||
if sampleConfig.Server == "" {
|
|
||||||
sampleConfig.Server = "localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.Port == 0 {
|
|
||||||
sampleConfig.Port = 6379
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.Loglevel == "" {
|
|
||||||
sampleConfig.Loglevel = "info"
|
|
||||||
}
|
|
||||||
|
|
||||||
// sampleConfig.Log = "" if not set
|
|
||||||
|
|
||||||
if sampleConfig.Channel == "" {
|
|
||||||
log.Errorf("Channel field in config file %s must be set", location)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Частичная проверка, ровно то, куда мы _точно_ щепрввляем сообщения исходя из бизнес-логики приложения
|
|
||||||
if sampleConfig.ForwardChannels.Games == "" {
|
|
||||||
sampleConfig.ForwardChannels.Games = "games"
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.ForwardChannels.Phrases == "" {
|
|
||||||
sampleConfig.ForwardChannels.Phrases = "phrases"
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.ForwardChannels.Webapp == "" {
|
|
||||||
sampleConfig.ForwardChannels.Webapp = "webapp"
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.ForwardChannels.Craniac == "" {
|
|
||||||
sampleConfig.ForwardChannels.Craniac = "craniac"
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.Csign == "" {
|
|
||||||
log.Errorf("Csign field in config file %s must be set", location)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sampleConfig.ForwardsMax == 0 {
|
|
||||||
sampleConfig.ForwardsMax = forwardMax
|
|
||||||
}
|
|
||||||
|
|
||||||
config = sampleConfig
|
|
||||||
configLoaded = true
|
|
||||||
log.Infof("Using %s as config file", location)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !configLoaded {
|
|
||||||
log.Error("Config was not loaded! Refusing to start.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Хэндлер сигналов закрывает все бд и сваливает из приложения
|
|
||||||
func sigHandler() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
for {
|
|
||||||
var s = <-sigChan
|
|
||||||
switch s {
|
|
||||||
case syscall.SIGINT:
|
|
||||||
log.Infoln("Got SIGINT, quitting")
|
|
||||||
case syscall.SIGTERM:
|
|
||||||
log.Infoln("Got SIGTERM, quitting")
|
|
||||||
case syscall.SIGQUIT:
|
|
||||||
log.Infoln("Got SIGQUIT, quitting")
|
|
||||||
|
|
||||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true
|
|
||||||
shutdown = true
|
|
||||||
|
|
||||||
// Отпишемся от всех каналов и закроем коннект к редиске
|
|
||||||
if err = subscriber.Unsubscribe(ctx); err != nil {
|
|
||||||
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = subscriber.Close(); err != nil {
|
|
||||||
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Производит некоторую инициализацию перед запуском main()
|
// Производит некоторую инициализацию перед запуском main()
|
||||||
func init() {
|
func init() {
|
||||||
log.SetFormatter(&log.TextFormatter{
|
log.SetFormatter(&log.TextFormatter{
|
||||||
@@ -518,3 +94,5 @@ func main() {
|
|||||||
go msgParser(ctx, msg.Payload)
|
go msgParser(ctx, msg.Payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
58
types.go
Normal file
58
types.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Конфиг
|
||||||
|
type myConfig struct {
|
||||||
|
Server string `json:"server,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Loglevel string `json:"loglevel,omitempty"`
|
||||||
|
Log string `json:"log,omitempty"`
|
||||||
|
Channel string `json:"channel,omitempty"`
|
||||||
|
ForwardChannels struct {
|
||||||
|
Games string `json:"games,omitempty"`
|
||||||
|
Phrases string `json:"phrases,omitempty"`
|
||||||
|
Webapp string `json:"webapp,omitempty"`
|
||||||
|
Craniac string `json:"craniac,omitempty"`
|
||||||
|
} `json:"forward_channels"`
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Входящее сообщение из pubsub-канала redis-ки
|
||||||
|
type rMsg struct {
|
||||||
|
From string `json:"from,omitempty"`
|
||||||
|
Chatid string `json:"chatid,omitempty"`
|
||||||
|
Userid string `json:"userid,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Plugin string `json:"plugin,omitempty"`
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
|
Misc struct {
|
||||||
|
Answer int64 `json:"answer,omitempty"`
|
||||||
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
||||||
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
} `json:"Misc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исходящее сообщение в pubsub-канал redis-ки
|
||||||
|
type sMsg struct {
|
||||||
|
From string `json:"from"`
|
||||||
|
Chatid string `json:"chatid"`
|
||||||
|
Userid string `json:"userid"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Misc struct {
|
||||||
|
Answer int64 `json:"answer"`
|
||||||
|
Botnick string `json:"bot_nick"`
|
||||||
|
Csign string `json:"csign"`
|
||||||
|
Fwdcnt int64 `json:"fwd_cnt"`
|
||||||
|
GoodMorning int64 `json:"good_morning"`
|
||||||
|
Msgformat int64 `json:"msg_format"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
} `json:"misc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
3
vendor/github.com/cespare/xxhash/v2/go.mod
generated
vendored
3
vendor/github.com/cespare/xxhash/v2/go.mod
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
module github.com/cespare/xxhash/v2
|
|
||||||
|
|
||||||
go 1.11
|
|
||||||
0
vendor/github.com/cespare/xxhash/v2/go.sum
generated
vendored
0
vendor/github.com/cespare/xxhash/v2/go.sum
generated
vendored
11
vendor/github.com/go-redis/redis/v8/go.mod
generated
vendored
11
vendor/github.com/go-redis/redis/v8/go.mod
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
module github.com/go-redis/redis/v8
|
|
||||||
|
|
||||||
go 1.13
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.2
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
|
|
||||||
github.com/google/go-cmp v0.5.6 // indirect
|
|
||||||
github.com/onsi/ginkgo v1.16.4
|
|
||||||
github.com/onsi/gomega v1.16.0
|
|
||||||
)
|
|
||||||
100
vendor/github.com/go-redis/redis/v8/go.sum
generated
vendored
100
vendor/github.com/go-redis/redis/v8/go.sum
generated
vendored
@@ -1,100 +0,0 @@
|
|||||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
|
|
||||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
10
vendor/github.com/sirupsen/logrus/go.mod
generated
vendored
10
vendor/github.com/sirupsen/logrus/go.mod
generated
vendored
@@ -1,10 +0,0 @@
|
|||||||
module github.com/sirupsen/logrus
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/stretchr/testify v1.2.2
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
|
||||||
)
|
|
||||||
|
|
||||||
go 1.13
|
|
||||||
8
vendor/github.com/sirupsen/logrus/go.sum
generated
vendored
8
vendor/github.com/sirupsen/logrus/go.sum
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
11
vendor/modules.txt
vendored
11
vendor/modules.txt
vendored
@@ -1,9 +1,11 @@
|
|||||||
# github.com/cespare/xxhash/v2 v2.1.2
|
# github.com/cespare/xxhash/v2 v2.1.2
|
||||||
|
## explicit; go 1.11
|
||||||
github.com/cespare/xxhash/v2
|
github.com/cespare/xxhash/v2
|
||||||
# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
|
# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
|
||||||
|
## explicit
|
||||||
github.com/dgryski/go-rendezvous
|
github.com/dgryski/go-rendezvous
|
||||||
# github.com/go-redis/redis/v8 v8.11.4
|
# github.com/go-redis/redis/v8 v8.11.4
|
||||||
## explicit
|
## explicit; go 1.13
|
||||||
github.com/go-redis/redis/v8
|
github.com/go-redis/redis/v8
|
||||||
github.com/go-redis/redis/v8/internal
|
github.com/go-redis/redis/v8/internal
|
||||||
github.com/go-redis/redis/v8/internal/hashtag
|
github.com/go-redis/redis/v8/internal/hashtag
|
||||||
@@ -16,13 +18,14 @@ github.com/go-redis/redis/v8/internal/util
|
|||||||
## explicit
|
## explicit
|
||||||
github.com/hjson/hjson-go
|
github.com/hjson/hjson-go
|
||||||
# github.com/onsi/ginkgo v1.16.5
|
# github.com/onsi/ginkgo v1.16.5
|
||||||
## explicit
|
## explicit; go 1.16
|
||||||
# github.com/onsi/gomega v1.18.1
|
# github.com/onsi/gomega v1.18.1
|
||||||
## explicit
|
## explicit; go 1.16
|
||||||
# github.com/sirupsen/logrus v1.8.1
|
# github.com/sirupsen/logrus v1.8.1
|
||||||
## explicit
|
## explicit; go 1.13
|
||||||
github.com/sirupsen/logrus
|
github.com/sirupsen/logrus
|
||||||
# golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
# golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||||
|
## explicit; go 1.17
|
||||||
golang.org/x/sys/internal/unsafeheader
|
golang.org/x/sys/internal/unsafeheader
|
||||||
golang.org/x/sys/unix
|
golang.org/x/sys/unix
|
||||||
golang.org/x/sys/windows
|
golang.org/x/sys/windows
|
||||||
|
|||||||
Reference in New Issue
Block a user