summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/api/api.go59
-rw-r--r--pkg/api/echo.go57
-rw-r--r--pkg/api/list.go22
-rw-r--r--pkg/api/message.go63
-rw-r--r--pkg/api/misc.go15
-rw-r--r--pkg/config/config.go36
-rw-r--r--pkg/fetcher/fetcher.go149
-rw-r--r--pkg/idec/echo.go60
-rw-r--r--pkg/idec/idec.go51
-rw-r--r--pkg/idec/message.go108
-rw-r--r--pkg/idec/point.go71
-rw-r--r--pkg/model/echo.go17
-rw-r--r--pkg/model/marshaller.go95
-rw-r--r--pkg/model/message.go55
-rw-r--r--pkg/model/point.go14
-rw-r--r--pkg/model/uid.go19
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]
+}