Начальный коммит

This commit is contained in:
Александр Кирюхин 2024-10-20 03:39:07 +03:00
commit d9e19fc53f
Signed by: neonxp
SSH key fingerprint: SHA256:SVt7TjxbVc87m1QYaQziOJ0N3OCFURv2g76gD/UTTXI
24 changed files with 1159 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.db

35
cmd/api/api.go Normal file
View file

@ -0,0 +1,35 @@
package api
import (
"github.com/urfave/cli/v2"
"gitrepo.ru/neonxp/idecnode/pkg/api"
"gitrepo.ru/neonxp/idecnode/pkg/config"
"gitrepo.ru/neonxp/idecnode/pkg/idec"
)
var APICommand *cli.Command = &cli.Command{
Name: "api",
Description: "Start api server",
Action: func(c *cli.Context) error {
configPath := c.String("config")
cfg, err := config.New(configPath)
if err != nil {
return err
}
idecApi, err := idec.New(cfg)
if err != nil {
return err
}
defer idecApi.Close()
return api.New(idecApi, cfg).Run(c.Context)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
DefaultText: "config path",
Value: "./etc/node.yaml",
},
},
}

35
cmd/fetcher/fetcher.go Normal file
View file

@ -0,0 +1,35 @@
package fetcher
import (
"github.com/urfave/cli/v2"
"gitrepo.ru/neonxp/idecnode/pkg/config"
"gitrepo.ru/neonxp/idecnode/pkg/fetcher"
"gitrepo.ru/neonxp/idecnode/pkg/idec"
)
var FetcherCommand *cli.Command = &cli.Command{
Name: "fetch",
Description: "Fetch from other nodes",
Action: func(c *cli.Context) error {
configPath := c.String("config")
cfg, err := config.New(configPath)
if err != nil {
return err
}
idecApi, err := idec.New(cfg)
if err != nil {
return err
}
defer idecApi.Close()
return fetcher.New(idecApi, cfg).Run(c.Context)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
DefaultText: "config path",
Value: "./etc/node.yaml",
},
},
}

53
cmd/point/point.go Normal file
View file

@ -0,0 +1,53 @@
package point
import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"gitrepo.ru/neonxp/idecnode/pkg/config"
"gitrepo.ru/neonxp/idecnode/pkg/idec"
)
var PointCommand *cli.Command = &cli.Command{
Name: "point",
Description: "Point related commands",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
DefaultText: "config path",
Value: "./etc/node.yaml",
},
},
Subcommands: []*cli.Command{
{
Name: "add",
Args: true,
ArgsUsage: "[username] [email] [password]",
Action: func(c *cli.Context) error {
if c.Args().Len() < 3 {
return errors.New("required 3 arguments")
}
configPath := c.String("config")
cfg, err := config.New(configPath)
if err != nil {
return err
}
idecApi, err := idec.New(cfg)
if err != nil {
return err
}
defer idecApi.Close()
username, email, password := c.Args().Get(0), c.Args().Get(1), c.Args().Get(2)
authString, err := idecApi.AddPoint(username, email, password)
if err != nil {
return err
}
fmt.Println("user registred. auth string", authString)
return nil
},
},
},
}

49
etc/node.yaml Normal file
View file

@ -0,0 +1,49 @@
listen: :8000
store: ./store.db
node: iinet.ru
logger_type: 3
echos:
node.local:
description: Локалка
idec.talks:
description: Сеть IDEC
pipe.2032:
description: Болталка
develop.16:
description: Обсуждение вопросов программирования
linux.14:
description: Linux
std.hugeping:
description: Блог hugeping
std.favorites:
description: Избранное
music.14:
description: Музыка
std.english:
description: ENGLISH conference
difrex.blog:
description: Блог Difrex
fetch:
- addr: http://club.hugeping.ru/
echos:
- idec.talks
- pipe.2032
- develop.16
- linux.14
- std.hugeping
- std.favorites
- music.14
- std.english
- difrex.blog
- addr: http://sprinternet.io:8085/ii-point.php?q=/
echos:
- idec.talks
- pipe.2032
- develop.16
- linux.14
- std.hugeping
- std.favorites
- music.14
- std.english
- difrex.blog

28
go.mod Normal file
View file

@ -0,0 +1,28 @@
module gitrepo.ru/neonxp/idecnode
go 1.23.1
require github.com/urfave/cli/v2 v2.27.5
require (
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/go-http-utils/logger v0.0.0-20161128092850-f3a42dcdeae6
github.com/google/uuid v1.6.0
github.com/labstack/echo/v4 v4.12.0
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.etcd.io/bbolt v1.3.11
gopkg.in/yaml.v3 v3.0.1
)

40
go.sum Normal file
View file

@ -0,0 +1,40 @@
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/go-http-utils/logger v0.0.0-20161128092850-f3a42dcdeae6 h1:R/ypabUA7vskKTRSlgP6rMUHTU6PBRgIcHVSU9qQ6qM=
github.com/go-http-utils/logger v0.0.0-20161128092850-f3a42dcdeae6/go.mod h1:CpBLxS3WrxouNECP/Y1A3i6qDnUYs8BvcXjgOW4Vqcw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

27
main.go Normal file
View file

@ -0,0 +1,27 @@
package main
import (
"log"
"os"
"github.com/urfave/cli/v2"
"gitrepo.ru/neonxp/idecnode/cmd/api"
"gitrepo.ru/neonxp/idecnode/cmd/fetcher"
"gitrepo.ru/neonxp/idecnode/cmd/point"
)
func main() {
app := &cli.App{
Name: "idecnode",
Usage: "idecnode [command]",
Commands: []*cli.Command{
api.APICommand,
fetcher.FetcherCommand,
point.PointCommand,
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

59
pkg/api/api.go Normal file
View file

@ -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
}

57
pkg/api/echo.go Normal file
View file

@ -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)
}
}

22
pkg/api/list.go Normal file
View file

@ -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
}

63
pkg/api/message.go Normal file
View file

@ -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
}

15
pkg/api/misc.go Normal file
View file

@ -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)
}
}

36
pkg/config/config.go Normal file
View file

@ -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)
}

149
pkg/fetcher/fetcher.go Normal file
View file

@ -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
}

60
pkg/idec/echo.go Normal file
View file

@ -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
}

51
pkg/idec/idec.go Normal file
View file

@ -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
}

108
pkg/idec/message.go Normal file
View file

@ -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))
})
}

71
pkg/idec/point.go Normal file
View file

@ -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())
})
}

17
pkg/model/echo.go Normal file
View file

@ -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"))
}

95
pkg/model/marshaller.go Normal file
View file

@ -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
}

55
pkg/model/message.go Normal file
View file

@ -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")
}

14
pkg/model/point.go Normal file
View file

@ -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
}

19
pkg/model/uid.go Normal file
View file

@ -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]
}