diff options
author | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-10-20 03:39:07 +0300 |
---|---|---|
committer | Alexander Neonxp Kiryukhin <i@neonxp.ru> | 2024-10-20 03:41:40 +0300 |
commit | d9e19fc53fb386f4160b8c3e9d7c35aa217d9591 (patch) | |
tree | 0c3ce157e8f9317aa7c55915025db5d54ea97911 /pkg |
Начальный коммит
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/api/api.go | 59 | ||||
-rw-r--r-- | pkg/api/echo.go | 57 | ||||
-rw-r--r-- | pkg/api/list.go | 22 | ||||
-rw-r--r-- | pkg/api/message.go | 63 | ||||
-rw-r--r-- | pkg/api/misc.go | 15 | ||||
-rw-r--r-- | pkg/config/config.go | 36 | ||||
-rw-r--r-- | pkg/fetcher/fetcher.go | 149 | ||||
-rw-r--r-- | pkg/idec/echo.go | 60 | ||||
-rw-r--r-- | pkg/idec/idec.go | 51 | ||||
-rw-r--r-- | pkg/idec/message.go | 108 | ||||
-rw-r--r-- | pkg/idec/point.go | 71 | ||||
-rw-r--r-- | pkg/model/echo.go | 17 | ||||
-rw-r--r-- | pkg/model/marshaller.go | 95 | ||||
-rw-r--r-- | pkg/model/message.go | 55 | ||||
-rw-r--r-- | pkg/model/point.go | 14 | ||||
-rw-r--r-- | pkg/model/uid.go | 19 |
16 files changed, 891 insertions, 0 deletions
diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..6bbdd30 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,59 @@ +package api + +import ( + "context" + "log" + "net/http" + "os" + + "github.com/go-http-utils/logger" + + "gitrepo.ru/neonxp/idecnode/pkg/config" + "gitrepo.ru/neonxp/idecnode/pkg/idec" +) + +type API struct { + config *config.Config + idec *idec.IDEC +} + +func New(i *idec.IDEC, cfg *config.Config) *API { + return &API{ + config: cfg, + idec: i, + } +} + +func (a *API) Run(ctx context.Context) error { + errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) + + mux := http.NewServeMux() + + mux.HandleFunc(`GET /list.txt`, a.getListHandler) + mux.HandleFunc(`GET /blacklist.txt`, a.getBlacklistTxtHandler) + mux.HandleFunc(`GET /u/e/{ids...}`, a.getEchosHandler) + mux.HandleFunc(`GET /u/m/{ids...}`, a.getBundleHandler) + mux.HandleFunc(`GET /u/point/{pauth}/{tmsg}`, a.getPointHandler) + mux.HandleFunc(`POST /u/point`, a.postPointHandler) + mux.HandleFunc(`GET /m/{msgID}`, a.getMessageHandler) + mux.HandleFunc(`GET /e/{id}`, a.getEchoHandler) + mux.HandleFunc(`GET /x/features`, a.getFeaturesHandler) + mux.HandleFunc(`GET /x/c/{ids...}`, a.getEchosInfo) + + srv := http.Server{ + Addr: a.config.Listen, + Handler: logger.Handler(mux, os.Stdout, logger.Type(a.config.LoggerType)), + ErrorLog: errorLog, + } + + go func() { + <-ctx.Done() + srv.Close() + }() + log.Println("started IDEC node at", a.config.Listen) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + return err + } + + return nil +} diff --git a/pkg/api/echo.go b/pkg/api/echo.go new file mode 100644 index 0000000..8f7a852 --- /dev/null +++ b/pkg/api/echo.go @@ -0,0 +1,57 @@ +package api + +import ( + "fmt" + "net/http" + "strings" +) + +func (a *API) getEchoHandler(w http.ResponseWriter, r *http.Request) { + echoID := r.PathValue("id") + echos, err := a.idec.GetEchosByIDs([]string{echoID}, 0, 0) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if len(echos) == 0 { + return + } + + fmt.Fprint(w, strings.Join(echos[echoID].Messages, "\n")) +} + +func (a *API) getEchosHandler(w http.ResponseWriter, r *http.Request) { + ids := strings.Split(r.PathValue("ids"), "/") + last := ids[len(ids)-1] + offset, limit := 0, 0 + if _, err := fmt.Sscanf(last, "%d:%d", &offset, &limit); err == nil { + ids = ids[:len(ids)-1] + } + echos, err := a.idec.GetEchosByIDs(ids, offset, limit) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + for _, echoID := range ids { + e := echos[echoID] + fmt.Fprintln(w, e.Name) + if len(e.Messages) > 0 { + fmt.Fprintln(w, strings.Join(e.Messages, "\n")) + } + } +} + +func (a *API) getEchosInfo(w http.ResponseWriter, r *http.Request) { + ids := strings.Split(r.PathValue("ids"), "/") + echos, err := a.idec.GetEchosByIDs(ids, 0, 0) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + for _, e := range echos { + fmt.Fprintf(w, "%s:%d\n", e.Name, e.Count) + } +} diff --git a/pkg/api/list.go b/pkg/api/list.go new file mode 100644 index 0000000..812bb0f --- /dev/null +++ b/pkg/api/list.go @@ -0,0 +1,22 @@ +package api + +import ( + "fmt" + "net/http" +) + +func (a *API) getListHandler(w http.ResponseWriter, r *http.Request) { + echos, err := a.idec.GetEchos() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, e := range echos { + fmt.Fprintf(w, "%s:%d:%s\n", e.Name, e.Count, e.Description) + } +} + +func (a *API) getBlacklistTxtHandler(w http.ResponseWriter, r *http.Request) { + // TODO +} diff --git a/pkg/api/message.go b/pkg/api/message.go new file mode 100644 index 0000000..1c28285 --- /dev/null +++ b/pkg/api/message.go @@ -0,0 +1,63 @@ +package api + +import ( + "encoding/base64" + "fmt" + "log" + "net/http" + "strings" +) + +func (a *API) getBundleHandler(w http.ResponseWriter, r *http.Request) { + ids := strings.Split(r.PathValue("ids"), "/") + + for _, messageID := range ids { + msg, err := a.idec.GetMessage(messageID) + if err != nil { + log.Println("cant read file for message", messageID, err) + continue + } + + b64msg := base64.StdEncoding.EncodeToString([]byte(msg.Bundle())) + fmt.Fprintf(w, "%s:%s\n", messageID, b64msg) + } +} + +func (a *API) getMessageHandler(w http.ResponseWriter, r *http.Request) { + msgID := r.PathValue("msgID") + + msg, err := a.idec.GetMessage(msgID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + _, err = fmt.Fprintln(w, msg.Bundle()) +} + +func (a *API) postPointHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + form := r.PostForm + a.savePointMessage(w, form.Get("tmsg"), form.Get("pauth")) +} + +func (a *API) getPointHandler(w http.ResponseWriter, r *http.Request) { + a.savePointMessage(w, r.PathValue("tmsg"), r.PathValue("pauth")) +} + +func (a *API) savePointMessage(w http.ResponseWriter, rawMessage, auth string) error { + point, err := a.idec.GetPointByAuth(auth) + if err != nil { + fmt.Fprintln(w, "error: no auth - wrong authstring") + return err + } + + if err := a.idec.SavePointMessage(point.Username, rawMessage); err != nil { + return err + } + fmt.Fprintln(w, "msg ok") + + return nil +} diff --git a/pkg/api/misc.go b/pkg/api/misc.go new file mode 100644 index 0000000..85f7a88 --- /dev/null +++ b/pkg/api/misc.go @@ -0,0 +1,15 @@ +package api + +import ( + "fmt" + "net/http" + "strings" + + "gitrepo.ru/neonxp/idecnode/pkg/idec" +) + +func (a *API) getFeaturesHandler(w http.ResponseWriter, r *http.Request) { + if _, err := fmt.Fprint(w, strings.Join(idec.Features, "\n")); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..0c967fd --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Listen string `yaml:"listen"` + Node string `yaml:"node"` + Store string `yaml:"store"` + LoggerType int `yaml:"logger_type"` + Echos map[string]Echo `yaml:"echos"` + Fetch []Node `yaml:"fetch"` +} + +type Node struct { + Addr string `yaml:"addr"` + Echos []string `yaml:"echos"` +} + +type Echo struct { + Description string `yaml:"description"` +} + +func New(filePath string) (*Config, error) { + cfg := new(Config) + fp, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer fp.Close() + + return cfg, yaml.NewDecoder(fp).Decode(cfg) +} diff --git a/pkg/fetcher/fetcher.go b/pkg/fetcher/fetcher.go new file mode 100644 index 0000000..0a79a70 --- /dev/null +++ b/pkg/fetcher/fetcher.go @@ -0,0 +1,149 @@ +package fetcher + +import ( + "context" + "encoding/base64" + "io" + "log" + "net/http" + "strings" + "time" + + "gitrepo.ru/neonxp/idecnode/pkg/config" + "gitrepo.ru/neonxp/idecnode/pkg/idec" +) + +type Fetcher struct { + idec *idec.IDEC + config *config.Config + client *http.Client +} + +func New(i *idec.IDEC, cfg *config.Config) *Fetcher { + return &Fetcher{ + idec: i, + config: cfg, + client: &http.Client{ + Timeout: 60 * time.Second, + }, + } +} + +func (f *Fetcher) Run(ctx context.Context) error { + for _, node := range f.config.Fetch { + messagesToDownloads := []string{} + log.Println("fetching", node) + for _, echoID := range node.Echos { + missed, err := f.getMissedEchoMessages(node, echoID) + if err != nil { + return err + } + messagesToDownloads = append(messagesToDownloads, missed...) + } + if err := f.downloadMessages(node, messagesToDownloads); err != nil { + return err + } + } + log.Println("finished") + return nil +} + +func (f *Fetcher) downloadMessages(node config.Node, messagesToDownloads []string) error { + var slice []string + for { + limit := min(20, len(messagesToDownloads)) + if limit == 0 { + return nil + } + slice, messagesToDownloads = messagesToDownloads[:limit-1], messagesToDownloads[limit:] + if err := f.downloadMessagesChunk(node, slice); err != nil { + return err + } + } +} + +func (f *Fetcher) getMissedEchoMessages(node config.Node, echoID string) ([]string, error) { + missed := []string{} + messages, err := f.idec.GetMessagesByEcho(echoID, 0, 0) + if err != nil { + return nil, err + } + + messagesIndex := map[string]struct{}{} + for _, msgID := range messages { + messagesIndex[msgID] = struct{}{} + } + + p := formatCommand(node, "u/e", echoID) + resp, err := f.client.Get(p) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if strings.Contains(line, ".") { + // echo name + continue + } + if line == "" { + continue + } + if _, exist := messagesIndex[line]; !exist { + missed = append(missed, line) + } + } + + return missed, nil +} + +func (f *Fetcher) downloadMessagesChunk(node config.Node, messages []string) error { + p := formatCommand(node, "u/m", messages...) + + resp, err := f.client.Get(p) + if err != nil { + return err + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + lines := strings.Split(string(data), "\n") + + for _, line := range lines { + if line == "" { + continue + } + p := strings.Split(line, ":") + + rawMessage, err := base64.StdEncoding.DecodeString(p[1]) + if err != nil { + return err + } + if err := f.idec.SaveBundleMessage(p[0], string(rawMessage)); err != nil { + return err + } + } + + return nil +} + +func formatCommand(node config.Node, method string, args ...string) string { + segments := []string{node.Addr, method} + segments = append(segments, args...) + p := strings.Join(segments, "/") + + return p +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/pkg/idec/echo.go b/pkg/idec/echo.go new file mode 100644 index 0000000..db3cd70 --- /dev/null +++ b/pkg/idec/echo.go @@ -0,0 +1,60 @@ +package idec + +import ( + "gitrepo.ru/neonxp/idecnode/pkg/model" + "go.etcd.io/bbolt" +) + +func (i *IDEC) GetEchosByIDs(echoIDs []string, offset, limit int) (map[string]model.Echo, error) { + res := make(map[string]model.Echo, len(echoIDs)) + + for _, echoID := range echoIDs { + echoCfg, ok := i.config.Echos[echoID] + if !ok { + // unknown echo + res[echoID] = model.Echo{ + Name: echoID, + } + continue + } + + messages, err := i.GetMessagesByEcho(echoID, offset, limit) + if err != nil { + return nil, err + } + + res[echoID] = model.Echo{ + Name: echoID, + Description: echoCfg.Description, + Messages: messages, + Count: len(messages), + } + } + + return res, nil +} + +func (i *IDEC) GetEchos() ([]model.Echo, error) { + result := make([]model.Echo, 0, len(i.config.Echos)) + for name, e := range i.config.Echos { + e := model.Echo{ + Name: name, + Description: e.Description, + } + err := i.db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket([]byte(name)) + if b == nil { + return nil + } + e.Count = b.Stats().KeyN + + return nil + }) + if err != nil { + return nil, err + } + result = append(result, e) + } + + return result, nil +} diff --git a/pkg/idec/idec.go b/pkg/idec/idec.go new file mode 100644 index 0000000..0692d65 --- /dev/null +++ b/pkg/idec/idec.go @@ -0,0 +1,51 @@ +package idec + +import ( + "errors" + + "gitrepo.ru/neonxp/idecnode/pkg/config" + "go.etcd.io/bbolt" +) + +var Features = []string{"list.txt", "blacklist.txt", "u/e", "u/m", "x/c", "m", "e"} + +var ( + ErrUserNotFound = errors.New("user not found") + ErrMessageNotFound = errors.New("message not found") + ErrFailedSaveMessage = errors.New("- failed save message") + ErrWrongMessageFormat = errors.New("- wrong message format") + ErrNoAuth = errors.New("no auth - wrong authstring") +) + +const ( + msgBucket = "_msg" + points = "_points" +) + +type IDEC struct { + config *config.Config + db *bbolt.DB +} + +func New(config *config.Config) (*IDEC, error) { + db, err := bbolt.Open(config.Store, 0o600, nil) + if err != nil { + return nil, err + } + + return &IDEC{ + config: config, + db: db, + }, nil +} + +func (i *IDEC) Close() error { + return i.db.Close() +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} diff --git a/pkg/idec/message.go b/pkg/idec/message.go new file mode 100644 index 0000000..d018df2 --- /dev/null +++ b/pkg/idec/message.go @@ -0,0 +1,108 @@ +package idec + +import ( + "encoding/base64" + "fmt" + "strings" + + "gitrepo.ru/neonxp/idecnode/pkg/model" + "go.etcd.io/bbolt" +) + +func (i *IDEC) GetMessagesByEcho(echoID string, offset, limit int) ([]string, error) { + messages := make([]string, 0) + + return messages, i.db.View(func(tx *bbolt.Tx) error { + if _, ok := i.config.Echos[echoID]; !ok { + return nil + } + + bEcho := tx.Bucket([]byte(echoID)) + if bEcho == nil { + return nil + } + + cur := bEcho.Cursor() + cur.First() + all := bEcho.Stats().KeyN + if limit == 0 { + limit = all + } + if offset < 0 { + offset = max(0, all+offset-1) + } + for i := 0; i < offset; i++ { + // skip offset entries + cur.Next() + } + for i := 0; i < limit; i++ { + _, v := cur.Next() + if v == nil { + break + } + messages = append(messages, string(v)) + } + + return nil + }) +} + +func (i *IDEC) GetMessage(messageID string) (*model.Message, error) { + var msg *model.Message + return msg, i.db.View(func(tx *bbolt.Tx) error { + bucket := tx.Bucket([]byte(msgBucket)) + if bucket == nil { + return ErrMessageNotFound + } + b := bucket.Get([]byte(messageID)) + var err error + msg, err = model.MessageFromBundle(messageID, string(b)) + + return err + }) +} + +func (i *IDEC) SavePointMessage(point string, rawMessage string) error { + rawMessage = strings.NewReplacer("-", "+", "_", "/").Replace(rawMessage) + + messageBody, err := base64.StdEncoding.DecodeString(rawMessage) + if err != nil { + return ErrWrongMessageFormat + } + + msg, err := model.MessageFromPointMessage(i.config.Node, point, string(messageBody)) + if err != nil { + return err + } + + return i.SaveMessage(msg) +} + +func (i *IDEC) SaveBundleMessage(msgID, text string) error { + msg, err := model.MessageFromBundle(msgID, text) + if err != nil { + return err + } + + return i.SaveMessage(msg) +} + +func (i *IDEC) SaveMessage(msg *model.Message) error { + return i.db.Update(func(tx *bbolt.Tx) error { + bMessages, err := tx.CreateBucketIfNotExists([]byte(msgBucket)) + if err != nil { + return err + } + if err := bMessages.Put([]byte(msg.ID), []byte(msg.Bundle())); err != nil { + return err + } + bEcho, err := tx.CreateBucketIfNotExists([]byte(msg.EchoArea)) + if err != nil { + return err + } + + bucketKey := fmt.Sprintf("%s.%s", msg.Date.Format("2006.01.02.15.04.05"), msg.ID) + + return bEcho.Put([]byte(bucketKey), []byte(msg.ID)) + }) +} diff --git a/pkg/idec/point.go b/pkg/idec/point.go new file mode 100644 index 0000000..897c1bb --- /dev/null +++ b/pkg/idec/point.go @@ -0,0 +1,71 @@ +package idec + +import ( + "bytes" + "encoding/gob" + "errors" + + "github.com/google/uuid" + "gitrepo.ru/neonxp/idecnode/pkg/model" + "go.etcd.io/bbolt" + "golang.org/x/crypto/bcrypt" +) + +var errPointFound = errors.New("point found") + +func (i *IDEC) GetPointByAuth(auth string) (*model.Point, error) { + point := new(model.Point) + + return point, i.db.View(func(tx *bbolt.Tx) error { + bAuth := tx.Bucket([]byte(points)) + if bAuth == nil { + return ErrUserNotFound + } + err := bAuth.ForEach(func(_, v []byte) error { + if err := gob.NewDecoder(bytes.NewBuffer(v)).Decode(point); err != nil { + return err + } + if point.AuthString == auth { + return errPointFound + } + + return nil + }) + if err == errPointFound { + return nil + } + if err != nil { + return err + } + + return ErrUserNotFound + }) +} + +func (i *IDEC) AddPoint(username, email, password string) (string, error) { + hpassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + + p := &model.Point{ + Username: username, + Email: email, + Password: hpassword, + AuthString: uuid.NewString(), + } + + return p.AuthString, i.db.Update(func(tx *bbolt.Tx) error { + pointsBucket, err := tx.CreateBucketIfNotExists([]byte(points)) + if err != nil { + return err + } + + bPoint := bytes.NewBuffer([]byte{}) + if err := gob.NewEncoder(bPoint).Encode(p); err != nil { + return err + } + + return pointsBucket.Put([]byte(p.Email), bPoint.Bytes()) + }) +} diff --git a/pkg/model/echo.go b/pkg/model/echo.go new file mode 100644 index 0000000..5f642da --- /dev/null +++ b/pkg/model/echo.go @@ -0,0 +1,17 @@ +package model + +import ( + "fmt" + "strings" +) + +type Echo struct { + Name string + Description string + Count int + Messages []string +} + +func (e *Echo) Format() string { + return fmt.Sprintf("%s\n%s", e.Name, strings.Join(e.Messages, "\n")) +} diff --git a/pkg/model/marshaller.go b/pkg/model/marshaller.go new file mode 100644 index 0000000..59f990a --- /dev/null +++ b/pkg/model/marshaller.go @@ -0,0 +1,95 @@ +package model + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" +) + +var ErrInvalidMessage = errors.New("invalid message") + +func MessageFromBundleLine(bundle string) (*Message, error) { + var uid, text string + if _, err := fmt.Sscanf(bundle, "%s:%s", &uid, &text); err != nil { + return nil, err + } + + return MessageFromBundle(uid, text) +} + +func MessageFromBundle(uid, text string) (*Message, error) { + lines := strings.SplitN(text, "\n", 9) + if len(lines) < 9 { + return nil, ErrInvalidMessage + } + + repto := "" + tag := lines[0] + tags := strings.SplitN(tag, "/", 4) + if len(tags) == 4 { + if tags[2] == "repto" { + repto = tags[3] + } + } + + timestamp, err := strconv.Atoi(lines[2]) + if err != nil { + return nil, err + } + date := time.Unix(int64(timestamp), 0) + + return &Message{ + ID: uid, + RepTo: repto, + EchoArea: lines[1], + Date: date, + From: lines[3], + Addr: lines[4], + MsgTo: lines[5], + Subject: lines[6], + Message: lines[8], + }, nil +} + +func MessageFromPointMessage(node string, point string, pointMessage string) (*Message, error) { + lines := strings.SplitN(pointMessage, "\n", 5) + if len(lines) < 4 { + return nil, ErrInvalidMessage + } + + repto := "" + message := lines[4] + if strings.HasPrefix(message, "@repto:") { + strings.TrimPrefix(message, "@repto:") + messageParts := strings.SplitN(message, "\n", 2) + repto, message = messageParts[0], messageParts[1] + } + + return &Message{ + ID: UIDFromText(pointMessage), + RepTo: repto, + EchoArea: lines[0], + Date: time.Now(), + From: point, + Addr: node, + MsgTo: lines[1], + Subject: lines[2], + Message: message, + }, nil +} + +func EchoFromText(text string) *Echo { + e := new(Echo) + lines := strings.Split(text, "\n") + e.Name = lines[0] + e.Messages = make([]string, 0, len(lines)) + for _, line := range lines[1:] { + if len(line) == 20 { + e.Messages = append(e.Messages, line) + } + } + + return e +} diff --git a/pkg/model/message.go b/pkg/model/message.go new file mode 100644 index 0000000..3d15c46 --- /dev/null +++ b/pkg/model/message.go @@ -0,0 +1,55 @@ +package model + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +type Message struct { + ID string + RepTo string + EchoArea string + Date time.Time + From string + Addr string + MsgTo string + Subject string + Message string +} + +func (m *Message) Bundle() string { + tags := "ii/ok" + if m.RepTo != "" { + tags = "ii/ok/repto/" + m.RepTo + } + lines := []string{ + tags, + m.EchoArea, + strconv.Itoa(int(m.Date.Unix())), + m.From, + m.Addr, + string(m.MsgTo), + m.Subject, + "", + m.Message, + } + + return strings.Join(lines, "\n") +} + +func (m *Message) PointMessage() string { + lines := []string{ + m.EchoArea, + string(m.MsgTo), + m.Subject, + "", + } + if m.RepTo != "" { + lines = append(lines, fmt.Sprintf("@repto:%s", m.RepTo)) + } + lines = append(lines, m.Message) + + return strings.Join(lines, "\n") +} diff --git a/pkg/model/point.go b/pkg/model/point.go new file mode 100644 index 0000000..02e9d43 --- /dev/null +++ b/pkg/model/point.go @@ -0,0 +1,14 @@ +package model + +import "encoding/gob" + +func init() { + gob.Register((*Point)(nil)) +} + +type Point struct { + Username string + Email string + Password []byte + AuthString string +} diff --git a/pkg/model/uid.go b/pkg/model/uid.go new file mode 100644 index 0000000..85fda18 --- /dev/null +++ b/pkg/model/uid.go @@ -0,0 +1,19 @@ +package model + +import ( + "crypto/sha256" + "encoding/base64" + "strings" +) + +func UIDFromMessage(msg *Message) string { + return UIDFromText(msg.PointMessage()) +} + +func UIDFromText(text string) string { + h := sha256.Sum256([]byte(text)) + id := base64.StdEncoding.EncodeToString(h[:]) + id = strings.NewReplacer("+", "A", "/", "Z", "-", "A", "_", "Z").Replace(id) + + return id[0:20] +} |