openidec

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 3771b9f7f630bbddf98c22ed42898761fa48ea2a
parent f3b735a71ca44b9e9aba82cb4ce93fb228850401
Author: vasyahacker <vasya@magicfreedom.com>
Date:   Tue, 28 Mar 2023 14:33:58 +0400

Rebranding to OpenIDEC =)

Diffstat:
MMakefile | 14+++++++-------
MREADME.md | 55++++++++++++++++++++++++++-----------------------------
Acmd/idecctl/main.go | 423+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/idecd/main.go | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/idecd/web.go | 1085+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcmd/ii-node/xpm.go -> cmd/idecd/xpm.go | 0
Acmd/idecgmi/main.go | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcmd/ii-gemini/main.go | 193-------------------------------------------------------------------------------
Dcmd/ii-node/main.go | 267-------------------------------------------------------------------------------
Dcmd/ii-node/web.go | 1085-------------------------------------------------------------------------------
Dcmd/ii-tool/main.go | 423-------------------------------------------------------------------------------
Mgo.mod | 8+-------
Mii/db.go | 4++--
Mii/msg.go | 2+-
Aman/idecctl.1 | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aman/idecd.1 | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aman/idecgmi.1 | 19+++++++++++++++++++
Dman/ii-gemini.1 | 19-------------------
Dman/ii-node.1 | 80-------------------------------------------------------------------------------
Dman/ii-tool.1 | 121-------------------------------------------------------------------------------
Dman/iigo.1 | 23-----------------------
Aman/openidec.1 | 23+++++++++++++++++++++++
Mwww/tpl/footer.tpl | 3++-
23 files changed, 2242 insertions(+), 2258 deletions(-)

diff --git a/Makefile b/Makefile @@ -5,16 +5,16 @@ all: build build-all: build build: - go build -trimpath -o ii-tool ./cmd/ii-tool - go build -trimpath -o ii-node ./cmd/ii-node - go build -trimpath -o ii-gemini ./cmd/ii-gemini + go build -trimpath -o idecctl ./cmd/idecctl + go build -trimpath -o idecd ./cmd/idecd + go build -trimpath -o idecgmi ./cmd/idecgmi install-all: install install: - go install ./cmd/ii-tool - go install ./cmd/ii-node - go install ./cmd/ii-gemini + go install ./cmd/idecctl + go install ./cmd/idecd + go install ./cmd/idecgmi clean: - rm -f ii-node ii-tool ii-gemini + rm -f idecd idecctl idecgmi diff --git a/README.md b/README.md @@ -1,32 +1,31 @@ # Intro -II-GO is [idec](https://github.com/idec-net/new-docs/blob/master/main.md) node realization written in golang. +OpenIDEC is [idec](https://github.com/idec-net/new-docs/blob/master/main.md) node realization written in golang. + +Forked from https://github.com/hugeping/ii-go for OpenBSD adaptation It has no dependencies and very compact. You can easy setup it and make your own ii/idec node. How to build? ``` -git clone https://github.com/hugeping/ii-go -cd ii-go -cd ii-tool -go build -cd ../ii-node -go build +git clone https://git.openbsd.org.ru/vasyahacker/openidec.git +cd openidec +make build ``` -# ii-tool +# idecctl -ii-tool can be used to fetch messages from another node and maintaince database. +idecctl can be used to fetch messages from another node and maintaince database. ## Fetch messages -ii-tool [options] fetch [uri] [echolist] +idecctl [options] fetch [uri] [echolist] echolist is the file with echonames (can has : splitted columns, like list.txt) or - -- to load it from stdin. For example: ``` -echo "std.club:this comment will be omitted" | ./ii-tool fecth http://127.0.0.1:8080 - +echo "std.club:this comment will be omitted" | ./idecctl fecth http://127.0.0.1:8080 - ``` Options are: @@ -46,7 +45,7 @@ If echolist is omitted, fetcher will try to get all echos. It uses list.txt exte Index file (db.idx by default) is created when needed. If you want force to recreate it, use: ``` -./ii-tool index +./idecctl index ``` ## Store bundle into db @@ -54,7 +53,7 @@ Index file (db.idx by default) is created when needed. If you want force to recr DB is just msgid:message bundles in base64 stored in text file. You can merge records from db to db with store command: ``` -ii-tool [options] store [db] +idecctl [options] store [db] ``` db - is file with bundles or '-' for stdin. @@ -74,14 +73,14 @@ Messages are identificated by unique message ids (MsgId). It is the first column You may select messages with select cmd: ``` -./ii-tool [options] select <echo.name> [slice] +./idecctl [options] select <echo.name> [slice] ``` slice is the start:limit. For example: ``` -./ii-tool select std.club -1:1 # get last message -./ii-tool select std.club 0:10 # get first 10 messages +./idecctl select std.club -1:1 # get last message +./idecctl select std.club 0:10 # get first 10 messages ``` Options are: @@ -96,13 +95,13 @@ Options are: You may show selected message: ``` -./ii-tool [options] get <MsgId> +./idecctl [options] get <MsgId> ``` Or search message: ``` -./ii-tool [options] search <string> [echo] +./idecctl [options] search <string> [echo] ``` Where options are: @@ -116,18 +115,18 @@ You can sort ids by date with sort command. To show last 5 messages adressed to selected user, try: ``` -./ii-tool [options] -to <user> select "" | ./ii-tool sort | tail -n5 | ./ii-tool -v sort +./idecctl [options] -to <user> select "" | ./idecctl sort | tail -n5 | ./idecctl -v sort ``` For example: ``` -./ii-tool -v -to Peter "" -1:1 # show and print last message to Peter +./idecctl -v -to Peter "" -1:1 # show and print last message to Peter ``` ## Add user (point) ``` -./ii-tool [-u pointfile] useradd <name> <e-mail> <password> +./idecctl [-u pointfile] useradd <name> <e-mail> <password> ``` By default, pointfile is points.txt @@ -135,16 +134,16 @@ By default, pointfile is points.txt ## Blacklist msg ``` -./ii-tool [-db db] blacklist <MsgId> +./idecctl [-db db] blacklist <MsgId> ``` Blacklist is just new record with same id but spectial status. -# ii-node +# idecd To run node: ``` -./ii-node [options] +./idecd [options] ``` Where options are: @@ -157,18 +156,16 @@ Where options are: list.txt by default. -host <string> Host string for node. For ex. http://hugeping.tk. http://127.0.0.1:8080 by default --sys "name" Node name. "ii-go" by default +-sys "name" Node name. "openidec" by default -u <points> Points file. "points.txt" by default. -v Be verbose (for tracing) ``` ## Example setup ``` -cd ii-go/ii-node -ln -s ../ii-tool/ii-tool -./ii-tool fetch http://club.syscall.ru +./idecctl fetch http://club.syscall.ru wget http://club.syscall.ru/list.txt # for echo descriptions -./ii-node -sys "newnode" +./idecd -sys "newnode" ``` And open http://127.0.0.1:8080 in your browser. diff --git a/cmd/idecctl/main.go b/cmd/idecctl/main.go @@ -0,0 +1,423 @@ +package main + +import ( + "git.openbsd.org.ru/vasyahacker/openidec/ii" + "bufio" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "sort" + "strings" +) + +func open_db(path string) *ii.DB { + db := ii.OpenDB(path) + if db == nil { + fmt.Printf("Can no open db: %s\n", path) + os.Exit(1) + } + return db +} + +func open_users_db(path string) *ii.UDB { + db := ii.OpenUsers(path) + if err := db.LoadUsers(); err != nil { + fmt.Printf("Can no load db: %s\n", path) + os.Exit(1) + } + return db +} + +func GetFile(path string) string { + var file *os.File + var err error + if path == "-" { + file = os.Stdin + } else { + file, err = os.Open(path) + if err != nil { + fmt.Printf("Can not open file %s: %s\n", path, err) + os.Exit(1) + } + defer file.Close() + } + b, err := ioutil.ReadAll(file) + if err != nil { + fmt.Printf("Can not read file %s: %s\n", path, err) + os.Exit(1) + } + return string(b) +} + +func main() { + ii.OpenLog(ioutil.Discard, os.Stdout, os.Stderr) + + db_opt := flag.String("db", "./db", "II database path (directory)") + lim_opt := flag.Int("lim", 0, "Fetch last N messages") + verbose_opt := flag.Bool("v", false, "Verbose") + force_opt := flag.Bool("f", false, "Force full sync") + users_opt := flag.String("u", "points.txt", "Users database") + conns_opt := flag.Int("j", 6, "Maximum parallel jobs") + topics_opt := flag.Bool("t", false, "Select topics only") + from_opt := flag.String("from", "", "Select from") + to_opt := flag.String("to", "", "Select to") + flag.Parse() + ii.MaxConnections = *conns_opt + if *verbose_opt { + ii.OpenLog(os.Stdout, os.Stdout, os.Stderr) + } + + args := flag.Args() + if len(args) < 1 { + fmt.Printf(`Help: %s [options] command [arguments] +Commands: + search <string> [echo] - search in base + send <server> <pauth> <msg|-> - send message + clean - cleanup database + fetch <url> [echofile|-] - fetch + store <bundle|-> - import bundle to database + get <msgid> - show message from database + select <echo> [[start]:lim] - get slice from echo + index - recreate index + blacklist <msgid> - blacklist msg + useradd <name> <e-mail> <password> - adduser +Options: + -db=<path> - database path + -lim=<lim> - fetch lim last messages + -u=<path> - points account file + -t - topics only (select,get) + -from=<user> - select from + -to=<user> - select to +`, os.Args[0]) + os.Exit(1) + } + switch cmd := args[0]; cmd { + case "search": + echo := "" + if len(args) < 2 { + fmt.Printf("No string supplied\n") + os.Exit(1) + } + if len(args) > 2 { + echo = args[2] + } + db := open_db(*db_opt) + db.Lock() + defer db.Unlock() + db.LoadIndex() + for _, v := range db.Idx.List { + if echo != "" { + mi := db.Idx.Hash[v] + if mi.Echo != echo { + continue + } + } + m := db.GetFast(v) + if m == nil { + continue + } + if strings.Contains(m.Text, args[1]) { + fmt.Printf("%s\n", v) + if *verbose_opt { + fmt.Printf("%s\n", m) + } + } + } + case "blacklist": + if len(args) < 2 { + fmt.Printf("No msgid supplied\n") + os.Exit(1) + } + db := open_db(*db_opt) + m := db.Get(args[1]) + if m != nil { + if err := db.Blacklist(m); err != nil { + fmt.Printf("Can not blacklist: %s\n", err) + os.Exit(1) + } + } else { + fmt.Printf("No such msg") + } + case "send": + if len(args) < 4 { + fmt.Printf("No argumnet(s) supplied\nShould be: <server> <pauth> and <file|->.\n") + os.Exit(1) + } + msg := GetFile(args[3]) + if _, err := ii.DecodeMsgline(string(msg), false); err != nil { + fmt.Printf("Wrong message format\n") + os.Exit(1) + } + n, err := ii.Connect(args[1]) + if err != nil { + fmt.Printf("Can not connect to %s: %s\n", args[1], err) + os.Exit(1) + } + if err := n.Post(args[2], msg); err != nil { + fmt.Printf("Can not send message: %s\n", err) + os.Exit(1) + } + case "useradd": + if len(args) < 4 { + fmt.Printf("No argumnet(s) supplied\nShould be: name, e-mail and password.\n") + os.Exit(1) + } + db := open_users_db(*users_opt) + if err := db.Add(args[1], args[2], args[3]); err != nil { + fmt.Printf("Can not add user: %s\n", err) + os.Exit(1) + } + case "clean": + hash := make(map[string]int) + last := make(map[string]string) + nr := 0 + dup := 0 + fmt.Printf("Pass 1...\n") + err := ii.FileLines(*db_opt, func(line string) bool { + nr++ + a := strings.Split(line, ":") + if len(a) != 2 { + ii.Error.Printf("Error in line: %d", nr) + return true + } + if !ii.IsMsgId(a[0]) { + ii.Error.Printf("Error in line: %d", nr) + return true + } + if _, ok := hash[a[0]]; ok { + hash[a[0]]++ + dup++ + last[a[0]] = line + } else { + hash[a[0]] = 1 + } + return true + }) + fmt.Printf("%d lines... %d dups...\n", nr, dup) + if dup == 0 { + os.Exit(0) + } + fmt.Printf("Pass 2...\n") + nr = 0 + f, err := os.OpenFile(*db_opt+".new", os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + skip := 0 + err = ii.FileLines(*db_opt, func(line string) bool { + nr++ + a := strings.Split(line, ":") + id := a[0] + if len(a) != 2 { + fmt.Printf("Error in line: %d\n", nr) + skip++ + return true + } + if !ii.IsMsgId(id) { + fmt.Printf("Error in line: %d\n", nr) + skip++ + return true + } + if v, ok := hash[id]; !ok || v == 0 { + fmt.Printf("Error. DB has changed. Aborted.\n") + os.Exit(1) + } + if hash[id] > 0 { // first record + hash[id] = -hash[id] + l := line + if hash[id] < -1 { + l = last[id] + } + if _, err := f.WriteString(l + "\n"); err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + } else { + skip++ + } + hash[id] += 1 + if hash[id] > 0 { + fmt.Printf("Error. DB has changed. Aborted.\n") + os.Exit(1) + } + return true + }) + f.Close() + if err != nil { + fmt.Printf("Error: %s\n") + os.Exit(1) + } + for _, v := range hash { + if v != 0 { + fmt.Printf("Error. DB shrinked. Aborted.\n") + os.Exit(1) + } + } + fmt.Printf("%d messages removed. File %s created.\n", skip, *db_opt+".new") + case "fetch": + var echolist []string + if len(args) < 2 { + fmt.Printf("No url supplied\n") + os.Exit(1) + } + db := open_db(*db_opt) + n, err := ii.Connect(args[1]) + if err != nil { + fmt.Printf("Can not connect to %s: %s\n", args[1], err) + os.Exit(1) + } + if *force_opt { + n.Force = true + } + if len(args) > 2 { + str := GetFile(args[2]) + for _, v := range strings.Split(str, "\n") { + echolist = append(echolist, strings.Split(v, ":")[0]) + } + } + err = n.Fetch(db, echolist, *lim_opt) + if err != nil { + fmt.Printf("Can not fetch from %s: %s\n", args[1], err) + os.Exit(1) + } + case "store": + if len(args) < 2 { + fmt.Printf("No bundle file supplied\n") + os.Exit(1) + } + db := open_db(*db_opt) + var f *os.File + var err error + if args[1] == "-" { + f = os.Stdin + } else { + f, err = os.Open(args[1]) + } + if err != nil { + fmt.Printf("Can no open bundle: %s\n", args[1]) + os.Exit(1) + } + defer f.Close() + reader := bufio.NewReader(f) + for { + line, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + fmt.Printf("Can read input (%s)\n", err) + os.Exit(1) + } + line = strings.TrimSuffix(line, "\n") + if err == io.EOF { + break + } + m, err := ii.DecodeBundle(line) + if m == nil { + fmt.Printf("Can not parse message: %s (%s)\n", line, err) + continue + } + if db.Lookup(m.MsgId) == nil { + if err := db.Store(m); err != nil { + fmt.Printf("Can not store message: %s\n", err) + os.Exit(1) + } + } + } + case "get": + if len(args) < 2 { + fmt.Printf("No msgid supplied\n") + os.Exit(1) + } + db := open_db(*db_opt) + + if *topics_opt { + mi := db.Lookup(args[1]) + if mi == nil { + return + } + mis := db.LookupIDS(db.SelectIDS(ii.Query{Echo: mi.Echo})) + topic := mi.Id + for p := mi; p != nil; p = db.LookupFast(p.Repto, false) { + if p.Repto == p.Id { + break + } + if p.Echo != mi.Echo { + continue + } + topic = p.Id + } + ids := db.GetTopics(mis)[topic] + if len(ids) == 0 { + ids = append(ids, args[1]) + } + for _, m := range ids { + fmt.Println(m) + } + return + } + + m := db.Get(args[1]) + if m != nil { + fmt.Println(m) + } + case "select": + if len(args) < 2 { + fmt.Printf("No echo supplied\n") + os.Exit(1) + } + db := open_db(*db_opt) + req := ii.Query{ Echo: args[1] } + if *from_opt != "" { + req.From = *from_opt + } + if *to_opt != "" { + req.To = *to_opt + } + + if *topics_opt { + req.Repto = "!" + } + if len(args) > 2 { + fmt.Sscanf(args[2], "%d:%d", &req.Start, &req.Lim) + } + resp := db.SelectIDS(req) + for _, v := range resp { + if *verbose_opt { + fmt.Println(db.Get(v)) + } else { + fmt.Println(v) + } + } + case "sort": + db := open_db(*db_opt) + db.LoadIndex() + scanner := bufio.NewScanner(os.Stdin) + var mm []*ii.Msg + for scanner.Scan() { + mi := db.LookupFast(scanner.Text(), false) + if mi != nil { + mm = append(mm, db.Get(mi.Id)) + } + } + sort.SliceStable(mm, func(i, j int) bool { + return mm[i].Date < mm[j].Date + }) + for _, v := range mm { + if *verbose_opt { + fmt.Println(v) + } else { + fmt.Println(v.MsgId) + } + } + case "index": + db := open_db(*db_opt) + if err := db.CreateIndex(); err != nil { + fmt.Printf("Can not rebuild index: %s\n", err) + os.Exit(1) + } + default: + fmt.Printf("Wrong cmd: %s\n", cmd) + os.Exit(1) + } +} diff --git a/cmd/idecd/main.go b/cmd/idecd/main.go @@ -0,0 +1,260 @@ +package main + +import ( + "git.openbsd.org.ru/vasyahacker/openidec/ii" + "flag" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "strings" + "path/filepath" + "golang.org/x/sys/unix" +) + +func open_db(path string) *ii.DB { + db := ii.OpenDB(path) + if db == nil { + ii.Error.Printf("Can no open db: %s\n", path) + os.Exit(1) + } + return db +} + +func PointMsg(edb *ii.EDB, db *ii.DB, udb *ii.UDB, pauth string, tmsg string) string { + udb.LoadUsers() + + if !udb.Access(pauth) { + ii.Info.Printf("Access denied for pauth: %s", pauth) + return "Access denied" + } + m, err := ii.DecodeMsgline(tmsg, true) + if err != nil { + ii.Error.Printf("Receive point msg: %s", err) + return fmt.Sprintf("%s", err) + } + if r, _ := m.Tag("repto"); r != "" { + if db.Lookup(r) == nil { + ii.Error.Printf("Receive point msg with wrong repto.") + return fmt.Sprintf("Receive point msg with wrong repto.") + } + } + if !edb.Allowed(m.Echo) { + ii.Error.Printf("This echo is disallowed") + return fmt.Sprintf("This echo is disallowed") + } + + m.From = udb.Name(pauth) + m.Addr = fmt.Sprintf("%s,%d", db.Name, udb.Id(pauth)) + if err := db.Store(m); err != nil { + ii.Error.Printf("Store point msg: %s", err) + return fmt.Sprintf("%s", err) + } + return "msg ok" +} + +var users_opt *string = flag.String("u", "points.txt", "Users database") +var db_opt *string = flag.String("db", "./db", "II database path (directory)") +var listen_opt *string = flag.String("L", ":8080", "Listen address") +var sysname_opt *string = flag.String("sys", "OpenIDEC", "Node name") +var host_opt *string = flag.String("host", "http://127.0.0.1:8080", "Node address") +var verbose_opt *bool = flag.Bool("v", false, "Verbose") +var echo_opt *string = flag.String("e", "list.txt", "Echoes list") + +type WWW struct { + Host string + tpl *template.Template + db *ii.DB + edb *ii.EDB + udb *ii.UDB +} + +func get_ue(echoes []string, db *ii.DB, user ii.User, w http.ResponseWriter, r *http.Request) { + if len(echoes) == 0 { + return + } + slice := echoes[len(echoes)-1:][0] + var idx, lim int + if _, err := fmt.Sscanf(slice, "%d:%d", &idx, &lim); err == nil { + echoes = echoes[:len(echoes)-1] + } else { + idx, lim = 0, 0 + } + + for _, e := range echoes { + if !ii.IsEcho(e) { + continue + } + fmt.Fprintf(w, "%s\n", e) + ids := db.SelectIDS(ii.Query{Echo: e, Start: idx, Lim: lim, User: user}) + for _, id := range ids { + fmt.Fprintf(w, "%s\n", id) + } + } + +} +func main() { + var www WWW + ii.OpenLog(ioutil.Discard, os.Stdout, os.Stderr) + + flag.Parse() + + unix.Unveil("./tpl", "r") + unix.Unveil("/usr/local/share/openidec/tpl", "r") + unix.Unveil(*echo_opt, "r") + unix.Unveil(*users_opt, "rwc") + unix.Unveil(filepath.Dir(*db_opt), "rwc") + unix.Unveil(*db_opt + ".idx", "rwc") + unix.Unveil(os.TempDir(), "rwc") + unix.UnveilBlock() + + db := open_db(*db_opt) + edb := ii.LoadEcholist(*echo_opt) + udb := ii.OpenUsers(*users_opt) + if *verbose_opt { + ii.OpenLog(os.Stdout, os.Stdout, os.Stderr) + } + + db.Name = *sysname_opt + www.db = db + www.edb = edb + www.udb = udb + www.Host = *host_opt + WebInit(&www) + + fs := http.FileServer(http.Dir("style")) + http.Handle("/style/", http.StripPrefix("/style/", fs)) + + http.HandleFunc("/list.txt", func(w http.ResponseWriter, r *http.Request) { + echoes := db.Echoes(nil, ii.Query{}) + for _, v := range echoes { + if !ii.IsPrivate(v.Name) { + fmt.Fprintf(w, "%s:%d:%s\n", v.Name, v.Count, www.edb.Info[v.Name]) + } + } + }) + http.HandleFunc("/blacklist.txt", func(w http.ResponseWriter, r *http.Request) { + ids := db.SelectIDS(ii.Query{Blacklisted: true}) + for _, v := range ids { + fmt.Fprintf(w, "%s\n", v) + } + }) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handleWWW(&www, w, r) + }) + http.HandleFunc("/u/point/", func(w http.ResponseWriter, r *http.Request) { + var pauth, tmsg string + switch r.Method { + case "GET": + udb.LoadUsers() + args := strings.Split(r.URL.Path[9:], "/") + + if len(args) >= 3 && args[1] == "u" { + pauth = args[0] + if !udb.Access(pauth) { + ii.Info.Printf("Access denied for pauth: %s", pauth) + return + } + user := udb.UserInfo(pauth) + if user == nil { + return + } + if args[2] == "e" { + echoes := args[3:] + get_ue(echoes, db, *user, w, r) + return + } + if args[2] == "m" { + ids := args[3:] + for _, i := range ids { + m, info := db.GetBundleInfo(i) + if m == "" || !db.Access(info, user) { + continue + } + fmt.Fprintf(w, "%s\n", m) + } + return + } + ii.Error.Printf("Wrong /u/point/ get request: %s", r.URL.Path[9:]) + return + } + if len(args) != 2 { + ii.Error.Printf("Wrong /u/point/ get request: %s", r.URL.Path[9:]) + return + } + pauth, tmsg = args[0], args[1] + default: + return + } + ii.Info.Printf("/u/point/%s/%s GET request", pauth, tmsg) + fmt.Fprintf(w, PointMsg(edb, db, udb, pauth, tmsg)) + }) + http.HandleFunc("/u/point", func(w http.ResponseWriter, r *http.Request) { + var pauth, tmsg string + switch r.Method { + case "POST": + if err := r.ParseForm(); err != nil { + ii.Error.Printf("Error in POST request: %s", err) + return + } + pauth = r.FormValue("pauth") + tmsg = r.FormValue("tmsg") + default: + return + } + ii.Info.Printf("/u/point/%s/%s POST request", pauth, tmsg) + fmt.Fprintf(w, PointMsg(edb, db, udb, pauth, tmsg)) + }) + http.HandleFunc("/x/c/", func(w http.ResponseWriter, r *http.Request) { + enames := strings.Split(r.URL.Path[5:], "/") + echoes := db.Echoes(enames, ii.Query{}) + for _, v := range echoes { + if !ii.IsPrivate(v.Name) { + fmt.Fprintf(w, "%s:%d:\n", v.Name, v.Count) + } + } + }) + http.HandleFunc("/u/m/", func(w http.ResponseWriter, r *http.Request) { + ids := strings.Split(r.URL.Path[5:], "/") + for _, i := range ids { + m, info := db.GetBundleInfo(i) + if m != "" && !ii.IsPrivate(info.Echo) { + fmt.Fprintf(w, "%s\n", m) + } + } + }) + http.HandleFunc("/u/e/", func(w http.ResponseWriter, r *http.Request) { + echoes := strings.Split(r.URL.Path[5:], "/") + get_ue(echoes, db, ii.User{}, w, r) + }) + http.HandleFunc("/m/", func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Path[3:] + if !ii.IsMsgId(id) { + return + } + m := db.Get(id) + ii.Info.Printf("/m/%s %s", id, m) + if m != nil && !ii.IsPrivate(m.Echo) { + fmt.Fprintf(w, "%s", m.String()) + } + }) + http.HandleFunc("/e/", func(w http.ResponseWriter, r *http.Request) { + e := r.URL.Path[3:] + if !ii.IsEcho(e) || ii.IsPrivate(e) { + return + } + ids := db.SelectIDS(ii.Query{Echo: e}) + for _, id := range ids { + fmt.Fprintf(w, "%s\n", id) + } + }) + http.HandleFunc("/x/features", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "list.txt\nblacklist.txt\nu/e\nx/c\n") + }) + ii.Info.Printf("Listening on %s", *listen_opt) + + if err := http.ListenAndServe(*listen_opt, nil); err != nil { + ii.Error.Printf("Error running web server: %s", err) + } +} diff --git a/cmd/idecd/web.go b/cmd/idecd/web.go @@ -0,0 +1,1085 @@ +package main + +import ( + "git.openbsd.org.ru/vasyahacker/openidec/ii" + "os" + "bytes" + "encoding/base64" + "errors" + "fmt" + "html/template" + "image" + "image/png" + "net/http" + "regexp" + "sort" + "strings" + "time" +) + +const PAGE_SIZE = 100 + +type WebContext struct { + Echoes []*ii.Echo + Topics []*Topic + Topic string + Msg []*ii.Msg + Error string + Echo string + PfxPath string + Page int + Pages int + Pager []int + BasePath string + User *ii.User + Echolist *ii.EDB + Selected string + Template string + Ref string + Info string + Sysname string + Host string + www *WWW +} + +func www_register(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + ii.Trace.Printf("www register") + switch r.Method { + case "GET": + ctx.Template = "register.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "register.tpl", ctx) + return err + case "POST": + udb := ctx.www.udb + if err := r.ParseForm(); err != nil { + ii.Error.Printf("Error in POST request: %s", err) + return err + } + auth := r.FormValue("auth") + if auth != "" { /* edit form */ + u := udb.UserInfo(auth) + if u == nil { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + password := r.FormValue("password") + u.Secret = ii.MakeSecret(u.Name + password) + if err := udb.Edit(u); err != nil { + ii.Info.Printf("Can not edit user %s: %s", ctx.User.Name, err) + return err + } + http.Redirect(w, r, ctx.PfxPath + "/login", http.StatusSeeOther) + return nil + } + user := r.FormValue("username") + password := r.FormValue("password") + email := r.FormValue("email") + + err := udb.Add(user, email, password) + if err != nil { + ii.Info.Printf("Can not register user %s: %s", user, err) + return err + } + ii.Info.Printf("Registered user: %s", user) + http.Redirect(w, r, ctx.PfxPath + "/login", http.StatusSeeOther) + default: + return nil + } + return nil +} + +func www_login(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + ii.Trace.Printf("www login") + switch r.Method { + case "GET": + ctx.Template = "login.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "login.tpl", ctx) + return err + case "POST": + if err := r.ParseForm(); err != nil { + ii.Error.Printf("Error in POST request: %s", err) + return err + } + user := r.FormValue("username") + password := r.FormValue("password") + udb := ctx.www.udb + if !udb.Auth(user, password) { + ii.Info.Printf("Access denied for user: %s", user) + return errors.New("Access denied") + } + exp := time.Now().Add(10 * 365 * 24 * time.Hour) + cookie := http.Cookie{Name: "pauth", Value: udb.Secret(user), Expires: exp} + http.SetCookie(w, &cookie) + ii.Info.Printf("User logged in: %s\n", user) + http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther) + return nil + } + return errors.New("Wrong method") +} + +func www_profile(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + ii.Trace.Printf("www profile") + if ctx.User.Name == "" { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + ctx.Selected = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id) + ava, _ := ctx.User.Tags.Get("avatar") + if ava != "" { + if data, err := base64.URLEncoding.DecodeString(ava); err == nil { + ctx.Info = string(data) + } + } + ctx.Template = "profile.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "profile.tpl", ctx) + return err +} + +func www_logout(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + ii.Trace.Printf("www logout: %s", ctx.User.Name) + if ctx.User.Name == "" { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + cookie := http.Cookie{Name: "pauth", Value: "", Expires: time.Unix(0, 0)} + http.SetCookie(w, &cookie) + http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther) + return nil +} + +func www_index(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + ii.Trace.Printf("www index") + ctx.Echoes = ctx.www.db.Echoes(nil, ii.Query{User: *ctx.User}) + ctx.Template = "index.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "index.tpl", ctx) + return err +} + +func parse_ava(txt string) *image.RGBA { + txt = msg_clean(txt) + lines := strings.Split(txt, "\n") + img, _ := ParseXpm(lines) + return img +} + +var magicTable = map[string]string{ + "\xff\xd8\xff": "image/jpeg", + "\x89PNG\r\n\x1a\n": "image/png", + "GIF87a": "image/gif", + "GIF89a": "image/gif", +} + +func check_image(incipit []byte) string { + incipitStr := string(incipit) + for magic, mime := range magicTable { + if strings.HasPrefix(incipitStr, magic) { + return mime + } + } + return "" +} + +func www_base64(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + id := ctx.BasePath + m := ctx.www.db.Get(id) + if m == nil { + return errors.New("No such message") + } + lines := strings.Split(msg_clean(m.Text), "\n") + start := false + b64 := "" + fname := "" + pre := false + for _, v := range lines { + if !start && strings.Trim(v, " ") == "====" { + if !pre { + pre = true + continue + } + pre = false + continue + } + if pre { + continue + } + if !start && !strings.HasPrefix(v, "@base64:") { + continue + } + if start { + v = strings.Replace(v, " ", "", -1) + if !base64Regex.MatchString(v) { + break + } + b64 += v + continue + } + v = strings.TrimPrefix(v, "@base64:") + v = strings.Trim(v, " ") + fname = v + if fname == "" { + fname = "file" + } + start = true + } + if b64 == "" { + return nil + } + b, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + if b, err = base64.RawStdEncoding.DecodeString(b64); err != nil { + if b, err = base64.URLEncoding.DecodeString(b64); err != nil { + return err + } + } + } + // w.Header().Set("Content-Type", "image/jpeg") + if check_image(b) != "" { + w.Header().Set("Content-Disposition", "inline") + } else { + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fname)) + } + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b))) + _, err = w.Write(b) + return err +} +func www_avatar(ctx *WebContext, w http.ResponseWriter, r *http.Request, user string) error { + if r.Method == "POST" { /* upload avatar */ + if ctx.User.Name == "" || ctx.User.Name != user { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + if err := r.ParseForm(); err != nil { + ii.Error.Printf("Error in POST request: %s", err) + return err + } + ava := r.FormValue("avatar") + if len(ava) > 2048 { + ii.Error.Printf("Avatar is too big.") + return errors.New("Avatar is too big (>2048 bytes)") + } + if ava == "" { + ii.Trace.Printf("Delete avatar for %s", ctx.User.Name) + ctx.User.Tags.Del("avatar") + } else { + img := parse_ava(ava) + if img == nil { + ii.Error.Printf("Wrong xpm format for avatar: " + user) + return errors.New("Wrong xpm format") + } + b64 := base64.URLEncoding.EncodeToString([]byte(ava)) + ii.Trace.Printf("New avatar for %s: %s", ctx.User.Name, b64) + ctx.User.Tags.Add("avatar/" + b64) + } + if err := ctx.www.udb.Edit(ctx.User); err != nil { + ii.Error.Printf("Error saving avatar: " + user) + return errors.New("Error saving avatar") + } + http.Redirect(w, r, ctx.PfxPath + "/profile", http.StatusSeeOther) + return nil + } + // var id int32 + // if !strings.HasPrefix(user, ctx.www.db.Name) { + // return nil + // } + // user = strings.TrimPrefix(user, ctx.www.db.Name) + // user = strings.TrimPrefix(user, ",") + // if _, err := fmt.Sscanf(user, "%d", &id); err != nil { + // return nil + // } + // u := ctx.www.udb.UserInfoId(id) + u := ctx.www.udb.UserInfoName(user) + if u == nil { + return nil + } + ava, _ := u.Tags.Get("avatar") + if ava == "" { + return nil + } + if data, err := base64.URLEncoding.DecodeString(ava); err == nil { + img := parse_ava(string(data)) + if img == nil { + ii.Error.Printf("Wrong xpm in avatar: %s\n", u.Name) + return nil + } + b := new(bytes.Buffer) + if err := png.Encode(b, img); err == nil { + w.Header().Set("Content-Type", "image/png") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b.Bytes()))) + if _, err := w.Write(b.Bytes()); err != nil { + return nil + } + return nil + } + ii.Error.Printf("Can't encode avatar in png: %s\n", u.Name) + } else { + ii.Error.Printf("Can't decode avatar: %s\n", u.Name) + } + return nil +} + +type Topic struct { + Ids []string + Count int + Last *ii.MsgInfo + Head *ii.Msg + Tail *ii.Msg +} + +func makePager(ctx *WebContext, count int, page int) int { + ctx.Pages = count / PAGE_SIZE + if count%PAGE_SIZE != 0 { + ctx.Pages++ + } + if page == 0 { + page++ + } else if page < 0 { + page = ctx.Pages + page + 1 + } + start := (page - 1) * PAGE_SIZE + if start < 0 { + start = 0 + page = 1 + } + ctx.Page = page + if ctx.Pages > 1 { + for i := 1; i <= ctx.Pages; i++ { + ctx.Pager = append(ctx.Pager, i) + } + } + return start +} + +func Select(ctx *WebContext, q ii.Query) []string { + q.User = *ctx.User + return ctx.www.db.SelectIDS(q) +} + +func trunc(str string, limit int) string { + result := []rune(str) + if len(result) > limit { + return string(result[:limit]) + } + return str +} + +func www_query(ctx *WebContext, w http.ResponseWriter, r *http.Request, q ii.Query, page int, rss bool) error { + db := ctx.www.db + req := ctx.BasePath + + if rss { + q.Start = -PAGE_SIZE + } + mis := db.LookupIDS(Select(ctx, q)) + ii.Trace.Printf("www query") + + sort.SliceStable(mis, func(i, j int) bool { + return mis[i].Num > mis[j].Num + }) + count := len(mis) + start := makePager(ctx, count, page) + nr := PAGE_SIZE + for i := start; i < count && nr > 0; i++ { + m := db.GetFast(mis[i].Id) + if m == nil { + ii.Error.Printf("Can't get msg: %s\n", mis[i].Id) + continue + } + ctx.Msg = append(ctx.Msg, m) + nr-- + } + if rss { + ctx.Topic = db.Name + " :: " + req + fmt.Fprintf(w, + `<?xml version="1.0" encoding="UTF-8"?> + <rss version="2.0" + xmlns:content="http://purl.org/rss/1.0/modules/content/" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:media="http://search.yahoo.com/mrss/" + xmlns:atom="http://www.w3.org/2005/Atom" + xmlns:georss="http://www.georss.org/georss"> + <channel> + <title>%s</title> + <link>%s/%s</link> + <description> + %s + </description> + <language>ru</language> +`, + str_esc(ctx.Topic), ctx.www.Host, ctx.BasePath, str_esc(ctx.Topic)) + for _, m := range ctx.Msg { + fmt.Fprintf(w, + `<item><title>%s</title><guid>%s</guid><pubDate>%s</pubDate><author>%s</author><link>%s/%s#%s</link> + <description> + %s... + </description> + <content:encoded> +<![CDATA[ +%s +%s +]]> +</content:encoded></item> +`, + str_esc(m.Subj), m.MsgId, time.Unix(m.Date, 0).Format("2006-01-02 15:04:05"), + str_esc(m.From), ctx.www.Host + ctx.PfxPath, m.MsgId, m.MsgId, + str_esc(trunc(m.Text, 280)), + fmt.Sprintf("%s -> %s<br><br>", m.From, m.To), + msg_text(m)) + } + fmt.Fprintf(w, `</channel></rss> +`) + return nil + } + ctx.Template = "query.tpl" + return ctx.www.tpl.ExecuteTemplate(w, "query.tpl", ctx) +} + +func www_topics(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error { + db := ctx.www.db + echo := ctx.Echo + mis := db.LookupIDS(Select(ctx, ii.Query{Echo: echo})) + ii.Trace.Printf("www topics: %s", echo) + topicsIds := db.GetTopics(mis) + var topics []*Topic + ii.Trace.Printf("Start to generate topics") + + db.Sync.RLock() + defer db.Sync.RUnlock() + db.LoadIndex() + for _, t := range topicsIds { + topic := Topic{} + topic.Ids = t + topic.Count = len(topic.Ids) - 1 + if ctx.PfxPath == "/blog" { + topic.Last = db.LookupFast(topic.Ids[0], false) + } else { + topic.Last = db.LookupFast(topic.Ids[topic.Count], false) + } + if topic.Last == nil { + ii.Error.Printf("Skip wrong message: %s\n", t[0]) + continue + } + topics = append(topics, &topic) + } + sort.SliceStable(topics, func(i, j int) bool { + return topics[i].Last.Num > topics[j].Last.Num + }) + tcount := len(topics) + start := makePager(ctx, tcount, page) + nr := PAGE_SIZE + for i := start; i < tcount && nr > 0; i++ { + t := topics[i] + t.Head = db.GetFast(t.Ids[0]) + t.Tail = db.GetFast(t.Ids[t.Count]) + if t.Head == nil || t.Tail == nil { + ii.Error.Printf("Skip wrong message: %s\n", t.Ids[0]) + continue + } + ctx.Topics = append(ctx.Topics, topics[i]) + nr-- + } + ii.Trace.Printf("Stop to generate topics") + + if ctx.PfxPath == "/blog" { + ctx.Template = "blog.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "blog.tpl", ctx) + return err + } + ctx.Template = "topics.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "topics.tpl", ctx) + return err +} + +func www_topic(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error { + id := ctx.BasePath + db := ctx.www.db + + mi := db.Lookup(id) + if mi == nil { + return errors.New("No such message") + } + + if !db.Access(mi, ctx.User) { + return errors.New("Access denied") + } + + if page == 0 { + ctx.Selected = id + } + ctx.Echo = mi.Echo + mis := db.LookupIDS(Select(ctx, ii.Query{Echo: mi.Echo})) + + topics := db.GetTopics(mis) + topic := mi.Topic + ctx.Topic = topic + ids := topics[topic] + + if len(ids) == 0 { + ids = append(ids, id) + } else if topic != mi.Id { + for k, v := range ids { + if v == mi.Id { + page = k/PAGE_SIZE + 1 + ctx.Selected = mi.Id + break + } + } + } + ii.Trace.Printf("www topic: %s", id) + start := makePager(ctx, len(ids), page) + nr := PAGE_SIZE + for i := start; i < len(ids) && nr > 0; i++ { + id := ids[i] + m := db.Get(id) + if m == nil { + ii.Error.Printf("Skip wrong message: %s", id) + continue + } + ctx.Msg = append(ctx.Msg, m) + nr-- + } + ctx.Template = "topic.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "topic.tpl", ctx) + return err +} + +func www_blacklist(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + id := ctx.BasePath + m := ctx.www.db.Get(id) + ii.Trace.Printf("www blacklist: %s", id) + if m == nil { + ii.Error.Printf("No such msg: %s", id) + return errors.New("No such msg") + } + if !msg_access(ctx.www, *m, *ctx.User) { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + err := ctx.www.db.Blacklist(m) + if err != nil { + ii.Error.Printf("Error blacklisting: %s", id) + return err + } + http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther) + return nil +} + +func www_edit(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + id := ctx.BasePath + switch r.Method { + case "GET": + m := ctx.www.db.Get(id) + if m == nil { + ii.Error.Printf("No such msg: %s", id) + return errors.New("No such msg") + } + msg := *m + ln := strings.Split(msg_clean(msg.Text), "\n") + if len(ln) > 0 { + if strings.HasPrefix(ln[len(ln)-1], "P.S. Edited: ") { + msg.Text = strings.Join(ln[:len(ln)-1], "\n") + } + } + msg.Text = msg.Text + "\nP.S. Edited: " + time.Now().Format("2006-01-02 15:04:05") + ctx.Msg = append(ctx.Msg, &msg) + ctx.Template = "edit.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "edit.tpl", ctx) + return err + case "POST": + ctx.BasePath = "" + return www_new(ctx, w, r) + } + return nil +} + +func www_new(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + echo := ctx.BasePath + ctx.Echo = echo + + switch r.Method { + case "GET": + ctx.Template = "new.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "new.tpl", ctx) + return err + case "POST": + edit := (echo == "") + ii.Trace.Printf("www new topic in %s", echo) + if err := r.ParseForm(); err != nil { + ii.Error.Printf("Error in POST request: %s", err) + return err + } + if ctx.User.Name == "" { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + subj := r.FormValue("subj") + to := r.FormValue("to") + msg := r.FormValue("msg") + repto := r.FormValue("repto") + id := r.FormValue("id") + if repto == id { + repto = "" + } + newecho := r.FormValue("echo") + if newecho != "" { + echo = newecho + } + if !ctx.www.edb.Allowed(echo) && ctx.User.Id != 1 { + ii.Error.Printf("This echo is disallowed") + return errors.New("This echo is disallowed") + } + action := r.FormValue("action") + text := fmt.Sprintf("%s\n%s\n%s\n\n%s", echo, to, subj, msg) + m, err := ii.DecodeMsgline(text, false) + if err != nil { + ii.Error.Printf("Error while posting new topic: %s", err) + return err + } + m.From = ctx.User.Name + m.Addr = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id) + if repto != "" { + m.Tags.Add("repto/" + repto) + } + if id != "" { + om := ctx.www.db.Get(id) + if (om == nil || m.Addr != om.Addr) && ctx.User.Id != 1 { + ii.Error.Printf("Access denied") + return errors.New("Access denied") + } + m.Date = om.Date + m.MsgId = id + m.From = om.From + m.Addr = om.Addr + } + if action == "Submit" { // submit + if edit { + err = ctx.www.db.Edit(m) + } else { + err = ctx.www.db.Store(m) + } + if err != nil { + ii.Error.Printf("Error while storig new topic %s: %s", m.MsgId, err) + return err + } + http.Redirect(w, r, ctx.PfxPath + "/"+m.MsgId+"#"+m.MsgId, http.StatusSeeOther) + return nil + } + if !edit { + m.MsgId = "" + } + ctx.Msg = append(ctx.Msg, m) + ctx.Template = "preview.tpl" + err = ctx.www.tpl.ExecuteTemplate(w, "preview.tpl", ctx) + return err + } + return nil +} + +func www_reply(ctx *WebContext, w http.ResponseWriter, r *http.Request, quote bool) error { + id := ctx.BasePath + m := ctx.www.db.Get(id) + if m == nil { + ii.Error.Printf("No such msg: %s", id) + return errors.New("No such msg") + } + msg := *m + msg.To = msg.From + msg.Subj = "Re: " + strings.TrimPrefix(msg.Subj, "Re: ") + msg.Tags.Add("repto/" + id) + if quote { + msg.Text = msg_quote(msg.Text, msg.From) + } else { + msg.Text = "" + } + ctx.Msg = append(ctx.Msg, &msg) + ctx.Echo = msg.Echo + ctx.Template = "reply.tpl" + err := ctx.www.tpl.ExecuteTemplate(w, "reply.tpl", ctx) + return err +} + +func str_esc(l string) string { + l = strings.Replace(l, "&", "&amp;", -1) + l = strings.Replace(l, "<", "&lt;", -1) + l = strings.Replace(l, ">", "&gt;", -1) + return l +} + +var quoteRegex = regexp.MustCompile("^[^ >]*>") +var urlRegex = regexp.MustCompile(`(http|ftp|https|gemini)://[^ <>"]+`) +var url2Regex = regexp.MustCompile(`{{{href=[0-9]+}}}`) +var urlIIRegex = regexp.MustCompile(`ii://[a-zA-Z0-9_\-.]+`) +var base64Regex = regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`) + +func msg_clean(txt string) string { + txt = strings.Replace(txt, "\r", "", -1) + txt = strings.TrimLeft(txt, "\n") + txt = strings.TrimRight(txt, "\n") + return txt +} +func msg_quote(txt string, from string) string { + txt = msg_clean(txt) + f := "" + names := strings.Split(from, " ") + if len(names) >= 2 { + from = fmt.Sprintf("%v%v", + string([]rune(names[0])[0]), + string([]rune(names[1])[0])) + } + for _, l := range strings.Split(txt, "\n") { + if strings.Trim(l, " ") == "" { + f += l + "\n" + continue + } + if quoteRegex.MatchString(l) { + s := strings.Index(l, ">") + f += l[:s] + ">>" + l[s+1:] + "\n" + } else { + f += from + "> " + l + "\n" + } + } + return f +} + +func ReverseStr(s string) string { + runes := []rune(s) + for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { + runes[i], runes[j] = runes[j], runes[i] + } + return string(runes) +} + +func msg_esc(l string) string { + var links []string + link := 0 + l = string(urlIIRegex.ReplaceAllFunc([]byte(l), + func(line []byte) []byte { + s := string(line) + url := strings.TrimPrefix(s, "ii://") + links = append(links, fmt.Sprintf(`<a href="/%s#%s" class="url">%s</a>`, + url, url, str_esc(s))) + link++ + return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1)) + })) + l = string(urlRegex.ReplaceAllFunc([]byte(l), + func(line []byte) []byte { + s := string(line) + links = append(links, fmt.Sprintf(`<a href="%s" class="url">%s</a>`, + s, str_esc(s))) + link++ + return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1)) + })) + l = str_esc(l) + l = string(url2Regex.ReplaceAllFunc([]byte(l), + func(line []byte) []byte { + s := string(line) + var n int + fmt.Sscanf(s, "{{{href=%d}}}", &n) + return []byte(links[n]) + })) + + return l +} + +func msg_text(m *ii.Msg) string { + return msg_trunc(m, 0, "") +} + +func msg_trunc(m *ii.Msg, maxlen int, more string) string { + if m == nil { + return "" + } + txt := m.Text + txt = msg_clean(txt) + f := "" + pre := false + skip := 0 + lines := strings.Split(txt, "\n") + for k, l := range lines { + if skip > 0 { + skip-- + continue + } + if strings.Trim(l, " ") == "====" { + if !pre { + pre = true + f += "<pre class=\"code\">\n" + continue + } + pre = false + f += "</pre>\n" + continue + } + if pre { + f += str_esc(l) + "\n" + continue + } + if strings.HasPrefix(l, "/* XPM */") || strings.HasPrefix(l, "! XPM2") { + var img *image.RGBA + img, skip = ParseXpm(lines[k:]) + if img != nil { + skip-- + /* embed xpm */ + b := new(bytes.Buffer) + if err := png.Encode(b, img); err == nil { + b64 := base64.StdEncoding.EncodeToString(b.Bytes()) + l = fmt.Sprintf("<img class=\"img\" src=\"data:image/png;base64,%s\"><br>\n", + b64) + f += l + continue + } + } + skip = 0 + l = msg_esc(l) + } else if strings.HasPrefix(l, "P.S.") || strings.HasPrefix(l, "PS:") || + strings.HasPrefix(l, "//") || strings.HasPrefix(l, "+++ ") { + l = fmt.Sprintf("<span class=\"comment\">%s</span>", str_esc(l)) + } else if strings.HasPrefix(l, "# ") || strings.HasPrefix(l, "= ") || + strings.HasPrefix(l, "## ") || strings.HasPrefix(l, "== ") || + strings.HasPrefix(l, "### ") || strings.HasPrefix(l, "=== ") { + l = fmt.Sprintf("<span class=\"header\">%s</span>", str_esc(l)) + } else if strings.HasPrefix(l, "@spoiler:") { + l = fmt.Sprintf("<span class=\"spoiler\">%s</span>", str_esc(ReverseStr(l))) + } else if quoteRegex.MatchString(l) { + l = fmt.Sprintf("<span class=\"quote\">%s</span>", str_esc(l)) + } else if strings.HasPrefix(l, "@base64:") { + fname := strings.TrimPrefix(l, "@base64:") + fname = strings.Trim(fname, " ") + if fname == "" { + fname = "file" + } + f += fmt.Sprintf("<a class=\"attach\" href=\"/%s/base64\">%s</a><br>\n", m.MsgId, str_esc(fname)) + return f + } else { + l = msg_esc(l) + } + f += l + if maxlen > 0 && len(f) > maxlen { + f += more + "<br>\n" + break + } else { + f += "<br>\n" + } + } + if pre { + pre = false + f += "</pre>\n" + } + return f +} + +func msg_access(www *WWW, m ii.Msg, u ii.User) bool { + addr := fmt.Sprintf("%s,%d", www.db.Name, u.Id) + return addr == m.Addr || u.Id == 1 +} + +func WebInit(www *WWW) { + funcMap := template.FuncMap{ + "fdate": func(date int64) template.HTML { + if time.Now().Unix()-date < 60*60*24 { + return template.HTML("<span class='today'>" + time.Unix(date, 0).Format("2006-01-02 15:04:05") + "</span>") + } + return template.HTML(time.Unix(date, 0).Format("2006-01-02 15:04:05")) + }, + "msg_text": func(m *ii.Msg) template.HTML { + return template.HTML(msg_text(m)) + }, + "msg_trunc": func(m *ii.Msg, len int, more string) template.HTML { + return template.HTML(msg_trunc(m, len, more)) + }, + "repto": func(m ii.Msg) string { + r, _ := m.Tag("repto") + if r == "" { + return m.MsgId + } + return r + }, + "msg_quote": msg_quote, + "msg_access": func(m ii.Msg, u ii.User) bool { + return msg_access(www, m, u) + }, + "is_even": func(i int) bool { + return i%2 == 0 + }, + "unescape": func(s string) template.HTML { + return template.HTML(s) + }, + "has_avatar": func(user string) bool { + ui := www.udb.UserInfoName(user) + if ui != nil { + _, ok := ui.Tags.Get("avatar") + return ok + } + return false + }, + } + + tpl_path := "tpl" + if _, err := os.Stat(tpl_path); errors.Is(err, os.ErrNotExist) { + fmt.Println("./tpl not found, trying /usr/local/share/openidec/tpl..") + tpl_path = "/usr/local/share/openidec/tpl" + if _, err := os.Stat(tpl_path); errors.Is(err, os.ErrNotExist) { + fmt.Println(tpl_path, "not found") + os.Exit(1) + } + } + + www.tpl = template.Must( + template.New("main").Funcs(funcMap).ParseGlob(tpl_path + "/*.tpl")) +} + +func handleErr(ctx *WebContext, w http.ResponseWriter, err error) { + ctx.Error = err.Error() + ctx.Template = "error.tpl" + ctx.www.tpl.ExecuteTemplate(w, "error.tpl", ctx) +} + +func handleWWW(www *WWW, w http.ResponseWriter, r *http.Request) { + var ctx WebContext + var user *ii.User = &ii.User{} + ctx.User = user + ctx.www = www + ctx.Sysname = www.db.Name + ctx.Host = www.Host + www.udb.LoadUsers() + err := _handleWWW(&ctx, w, r) + if err != nil { + handleErr(&ctx, w, err) + } +} + +func _handleWWW(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { + cookie, err := r.Cookie("pauth") + if err == nil { + udb := ctx.www.udb + if udb.Access(cookie.Value) { + if user := udb.UserInfo(cookie.Value); user != nil { + ctx.User = user + } + } + } + ii.Trace.Printf("[%s] GET %s", ctx.User.Name, r.URL.Path) + path := strings.TrimPrefix(r.URL.Path, "/") + args := strings.Split(path, "/") + ctx.Echolist = ctx.www.edb + ctx.Ref = r.Header.Get("Referer") + if len(args) > 1 && args[0] == "blog" { + ctx.PfxPath = "/blog" + args = args[1:] + } + if args[0] == "" { + ctx.BasePath = "" + return www_index(ctx, w, r) + } else if args[0] == "login" { + ctx.BasePath = "login" + return www_login(ctx, w, r) + } else if args[0] == "logout" { + ctx.BasePath = "logout" + return www_logout(ctx, w, r) + } else if args[0] == "profile" { + ctx.BasePath = "profile" + return www_profile(ctx, w, r) + } else if args[0] == "register" { + ctx.BasePath = "register" + return www_register(ctx, w, r) + } else if args[0] == "reset" { + ctx.Template = "reset.tpl" + return ctx.www.tpl.ExecuteTemplate(w, "reset.tpl", ctx) + } else if args[0] == "avatar" { + ctx.BasePath = "avatar" + if len(args) < 2 { + return errors.New("Wrong request") + } + return www_avatar(ctx, w, r, args[1]) + } else if ii.IsMsgId(args[0]) { + page := 0 + ctx.BasePath = args[0] + if len(args) > 1 { + if args[1] == "reply" { + return www_reply(ctx, w, r, !(len(args) > 2 && args[2] == "new")) + } else if args[1] == "edit" { + return www_edit(ctx, w, r) + } else if args[1] == "blacklist" { + return www_blacklist(ctx, w, r) + } else if args[1] == "base64" { + return www_base64(ctx, w, r) + } + fmt.Sscanf(args[1], "%d", &page) + } + return www_topic(ctx, w, r, page) + } else if args[0] == "new" { + ctx.BasePath = "" + return www_new(ctx, w, r) + } else if args[0] == "to" { + page := 1 + rss := false + if len(args) < 2 { + return errors.New("Wrong request") + } + if len(args) > 2 { + if args[2] == "rss" { + rss = true + } else { + fmt.Sscanf(args[2], "%d", &page) + } + } + ctx.BasePath = "to/" + args[1] + return www_query(ctx, w, r, ii.Query{To: args[1]}, page, rss) + } else if args[0] == "from" { + page := 1 + rss := false + if len(args) < 2 { + return errors.New("Wrong request") + } + if len(args) > 2 { + if args[2] == "rss" { + rss = true + } else { + fmt.Sscanf(args[2], "%d", &page) + } + } + ctx.BasePath = "from/" + args[1] + return www_query(ctx, w, r, ii.Query{From: args[1]}, page, rss) + } else if args[0] == "echo" || args[0] == "echo+topics" { + page := 1 + rss := false + if len(args) < 2 { + return errors.New("Wrong request") + } + if len(args) > 2 { + if args[2] == "rss" { + rss = true + } else { + fmt.Sscanf(args[2], "%d", &page) + } + } + q := ii.Query{Echo: args[1]} + if args[1] == "all" { + q.Echo = "" + } + ctx.Echo = q.Echo + q.Start = -PAGE_SIZE + if args[0] == "echo+topics" { + q.Repto = "!" + ctx.BasePath = "echo+topics/" + args[1] + } else { + ctx.BasePath = "echo/" + args[1] + } + return www_query(ctx, w, r, q, page, rss) + } else if ii.IsEcho(args[0]) { + page := 1 + ctx.Echo = args[0] + ctx.BasePath = args[0] + if len(args) > 1 { + if args[1] == "new" { + ctx.BasePath = args[0] + return www_new(ctx, w, r) + } + fmt.Sscanf(args[1], "%d", &page) + } + return www_topics(ctx, w, r, page) + } else { + w.WriteHeader(http.StatusNotFound) + fmt.Fprintf(w, "404\n") + } + return nil +} diff --git a/cmd/ii-node/xpm.go b/cmd/idecd/xpm.go diff --git a/cmd/idecgmi/main.go b/cmd/idecgmi/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "git.openbsd.org.ru/vasyahacker/openidec/ii" + "bufio" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "regexp" + "sort" + "strings" + "time" +) + +func open_db(path string) *ii.DB { + db := ii.OpenDB(path) + if db == nil { + fmt.Printf("Can no open db: %s\n", path) + os.Exit(1) + } + return db +} + +func GetFile(path string) string { + var file *os.File + var err error + if path == "-" { + file = os.Stdin + } else { + file, err = os.Open(path) + if err != nil { + fmt.Printf("Can not open file %s: %s\n", path, err) + os.Exit(1) + } + defer file.Close() + } + b, err := ioutil.ReadAll(file) + if err != nil { + fmt.Printf("Can not read file %s: %s\n", path, err) + os.Exit(1) + } + return string(b) +} + +var urlRegex = regexp.MustCompile(`(http|ftp|https|gemini)://[^ <>"]+`) + +func gemini(f io.Writer, m *ii.Msg) { + fmt.Fprintln(f, "# " + m.Subj) + if m.To != "All" && m.To != m.From { + fmt.Fprintf(f, "To: %s\n\n", m.To) + } + d := time.Unix(m.Date, 0).Format("2006-01-02 15:04:05") + fmt.Fprintf(f, "by %s on %s\n\n", m.From, d) + temp := strings.Split(m.Text, "\n") + pre := false + xpm := false + link := 0 + var links []string + for _, l := range temp { + l = strings.Replace(l, "\r", "", -1) + if pre { + if l == "====" { + l = "```" + pre = false + } + } else if xpm { + if strings.HasSuffix(l, "};") { + xpm = false + fmt.Fprintln(f, l) + fmt.Fprintln(f, "```") + continue + } + } else { + if l == "====" { + l = "```" + pre = true + } else if strings.HasPrefix(l, "/* XPM */") { + fmt.Fprintln(f, "```") + xpm = true + } + } + if !pre && !xpm { + l = string(urlRegex.ReplaceAllFunc([]byte(l), + func(line []byte) []byte { + link ++ + s := string(line) + links = append(links, fmt.Sprintf("=> %s %s [%d]", + s, s, link)) + return []byte(fmt.Sprintf("%s [%d]", s, link)) + })) + } + fmt.Fprintln(f, l) + } + for _, v := range links { + fmt.Fprintln(f, v) + } +} + +func str_esc(l string) string { + l = strings.Replace(l, "&", "&amp;", -1) + l = strings.Replace(l, "<", "&lt;", -1) + l = strings.Replace(l, ">", "&gt;", -1) + return l +} + +func main() { + ii.OpenLog(ioutil.Discard, os.Stdout, os.Stderr) + + db_opt := flag.String("db", "./db", "II database path (directory)") + data_opt := flag.String("data", "./data", "Output path (directory)") + url_opt := flag.String("url", "localhost", "Url of station") + verbose_opt := flag.Bool("v", false, "Verbose") + title_opt := flag.String("title", "ii/idec networks", "Title") + author_opt := flag.String("author", "anonymous", "Author") + flag.Parse() + if *verbose_opt { + ii.OpenLog(os.Stdout, os.Stdout, os.Stderr) + } + + args := flag.Args() + if len(args) < 1 { + fmt.Printf(`Help: %s [options] command [arguments] +Commands: + -data <path> gemini - generate gemini data +Options: + -db=<path> - database path +`, os.Args[0]) + os.Exit(1) + } + switch cmd := args[0]; cmd { + case "gemini": + db := open_db(*db_opt) + db.Lock() + defer db.Unlock() + db.LoadIndex() + + scanner := bufio.NewScanner(os.Stdin) + var mis []*ii.Msg + for scanner.Scan() { + mi := db.LookupFast(scanner.Text(), false) + if mi != nil { + mis = append(mis, db.Get(mi.Id)) + } + } + sort.SliceStable(mis, func(i, j int) bool { + return mis[i].Date > mis[j].Date + }) + data := strings.TrimSuffix(*data_opt, "/") + atom, err := os.Create(data + "/atom.xml") + if err != nil { + return + } + defer atom.Close() + fmt.Fprintf(atom, `<?xml version='1.0' encoding='UTF-8'?> +<feed xmlns="http://www.w3.org/2005/Atom"> + <id>gemini://%s/</id> + <title>%s</title> + <updated>%s</updated> + <author> + <name>%s</name> + </author> + <link href="gemini://%s/atom.xml" rel="self"/> + <link href="gemini://%s/" rel="alternate"/> +`, *url_opt, *title_opt, time.Now().Format(time.RFC3339), *author_opt, *url_opt, *url_opt) + for _, v := range mis { + m := v + if m != nil { + f, err := os.Create(data + "/" + m.MsgId + ".gmi") + if err == nil { + gemini(f, m) + d := time.Unix(m.Date, 0).Format("2006-01-02") + fmt.Println("=> /"+ m.MsgId + ".gmi " + d + " - " + m.Subj) + } + f.Close() + fmt.Fprintf(atom, `<entry> + <id>gemini://%s/%s.gmi</id> + <title>%s</title> + <updated>%s</updated> + <link href="gemini://%s/%s.gmi" rel="alternate"/> +</entry> +`, *url_opt, m.MsgId, str_esc(m.Subj), + time.Unix(m.Date, 0).Format(time.RFC3339), *url_opt, m.MsgId) + } + } + fmt.Fprintf(atom, `</feed> +`) + default: + fmt.Printf("Wrong cmd: %s\n", cmd) + os.Exit(1) + } +} diff --git a/cmd/ii-gemini/main.go b/cmd/ii-gemini/main.go @@ -1,193 +0,0 @@ -package main - -import ( - "git.openbsd.org.ru/vasyahacker/iigo/ii" - "bufio" - "flag" - "fmt" - "io" - "io/ioutil" - "os" - "regexp" - "sort" - "strings" - "time" -) - -func open_db(path string) *ii.DB { - db := ii.OpenDB(path) - if db == nil { - fmt.Printf("Can no open db: %s\n", path) - os.Exit(1) - } - return db -} - -func GetFile(path string) string { - var file *os.File - var err error - if path == "-" { - file = os.Stdin - } else { - file, err = os.Open(path) - if err != nil { - fmt.Printf("Can not open file %s: %s\n", path, err) - os.Exit(1) - } - defer file.Close() - } - b, err := ioutil.ReadAll(file) - if err != nil { - fmt.Printf("Can not read file %s: %s\n", path, err) - os.Exit(1) - } - return string(b) -} - -var urlRegex = regexp.MustCompile(`(http|ftp|https|gemini)://[^ <>"]+`) - -func gemini(f io.Writer, m *ii.Msg) { - fmt.Fprintln(f, "# " + m.Subj) - if m.To != "All" && m.To != m.From { - fmt.Fprintf(f, "To: %s\n\n", m.To) - } - d := time.Unix(m.Date, 0).Format("2006-01-02 15:04:05") - fmt.Fprintf(f, "by %s on %s\n\n", m.From, d) - temp := strings.Split(m.Text, "\n") - pre := false - xpm := false - link := 0 - var links []string - for _, l := range temp { - l = strings.Replace(l, "\r", "", -1) - if pre { - if l == "====" { - l = "```" - pre = false - } - } else if xpm { - if strings.HasSuffix(l, "};") { - xpm = false - fmt.Fprintln(f, l) - fmt.Fprintln(f, "```") - continue - } - } else { - if l == "====" { - l = "```" - pre = true - } else if strings.HasPrefix(l, "/* XPM */") { - fmt.Fprintln(f, "```") - xpm = true - } - } - if !pre && !xpm { - l = string(urlRegex.ReplaceAllFunc([]byte(l), - func(line []byte) []byte { - link ++ - s := string(line) - links = append(links, fmt.Sprintf("=> %s %s [%d]", - s, s, link)) - return []byte(fmt.Sprintf("%s [%d]", s, link)) - })) - } - fmt.Fprintln(f, l) - } - for _, v := range links { - fmt.Fprintln(f, v) - } -} - -func str_esc(l string) string { - l = strings.Replace(l, "&", "&amp;", -1) - l = strings.Replace(l, "<", "&lt;", -1) - l = strings.Replace(l, ">", "&gt;", -1) - return l -} - -func main() { - ii.OpenLog(ioutil.Discard, os.Stdout, os.Stderr) - - db_opt := flag.String("db", "./db", "II database path (directory)") - data_opt := flag.String("data", "./data", "Output path (directory)") - url_opt := flag.String("url", "localhost", "Url of station") - verbose_opt := flag.Bool("v", false, "Verbose") - title_opt := flag.String("title", "ii/idec networks", "Title") - author_opt := flag.String("author", "anonymous", "Author") - flag.Parse() - if *verbose_opt { - ii.OpenLog(os.Stdout, os.Stdout, os.Stderr) - } - - args := flag.Args() - if len(args) < 1 { - fmt.Printf(`Help: %s [options] command [arguments] -Commands: - -data <path> gemini - generate gemini data -Options: - -db=<path> - database path -`, os.Args[0]) - os.Exit(1) - } - switch cmd := args[0]; cmd { - case "gemini": - db := open_db(*db_opt) - db.Lock() - defer db.Unlock() - db.LoadIndex() - - scanner := bufio.NewScanner(os.Stdin) - var mis []*ii.Msg - for scanner.Scan() { - mi := db.LookupFast(scanner.Text(), false) - if mi != nil { - mis = append(mis, db.Get(mi.Id)) - } - } - sort.SliceStable(mis, func(i, j int) bool { - return mis[i].Date > mis[j].Date - }) - data := strings.TrimSuffix(*data_opt, "/") - atom, err := os.Create(data + "/atom.xml") - if err != nil { - return - } - defer atom.Close() - fmt.Fprintf(atom, `<?xml version='1.0' encoding='UTF-8'?> -<feed xmlns="http://www.w3.org/2005/Atom"> - <id>gemini://%s/</id> - <title>%s</title> - <updated>%s</updated> - <author> - <name>%s</name> - </author> - <link href="gemini://%s/atom.xml" rel="self"/> - <link href="gemini://%s/" rel="alternate"/> -`, *url_opt, *title_opt, time.Now().Format(time.RFC3339), *author_opt, *url_opt, *url_opt) - for _, v := range mis { - m := v - if m != nil { - f, err := os.Create(data + "/" + m.MsgId + ".gmi") - if err == nil { - gemini(f, m) - d := time.Unix(m.Date, 0).Format("2006-01-02") - fmt.Println("=> /"+ m.MsgId + ".gmi " + d + " - " + m.Subj) - } - f.Close() - fmt.Fprintf(atom, `<entry> - <id>gemini://%s/%s.gmi</id> - <title>%s</title> - <updated>%s</updated> - <link href="gemini://%s/%s.gmi" rel="alternate"/> -</entry> -`, *url_opt, m.MsgId, str_esc(m.Subj), - time.Unix(m.Date, 0).Format(time.RFC3339), *url_opt, m.MsgId) - } - } - fmt.Fprintf(atom, `</feed> -`) - default: - fmt.Printf("Wrong cmd: %s\n", cmd) - os.Exit(1) - } -} diff --git a/cmd/ii-node/main.go b/cmd/ii-node/main.go @@ -1,267 +0,0 @@ -package main - -import ( - "git.openbsd.org.ru/vasyahacker/iigo/ii" - "flag" - "fmt" - "html/template" - "io/ioutil" - "net/http" - "os" - "strings" - "path/filepath" - "golang.org/x/sys/unix" -) - -func open_db(path string) *ii.DB { - db := ii.OpenDB(path) - if db == nil { - ii.Error.Printf("Can no open db: %s\n", path) - os.Exit(1) - } - return db -} - -func PointMsg(edb *ii.EDB, db *ii.DB, udb *ii.UDB, pauth string, tmsg string) string { - udb.LoadUsers() - - if !udb.Access(pauth) { - ii.Info.Printf("Access denied for pauth: %s", pauth) - return "Access denied" - } - m, err := ii.DecodeMsgline(tmsg, true) - if err != nil { - ii.Error.Printf("Receive point msg: %s", err) - return fmt.Sprintf("%s", err) - } - if r, _ := m.Tag("repto"); r != "" { - if db.Lookup(r) == nil { - ii.Error.Printf("Receive point msg with wrong repto.") - return fmt.Sprintf("Receive point msg with wrong repto.") - } - } - if !edb.Allowed(m.Echo) { - ii.Error.Printf("This echo is disallowed") - return fmt.Sprintf("This echo is disallowed") - } - - m.From = udb.Name(pauth) - m.Addr = fmt.Sprintf("%s,%d", db.Name, udb.Id(pauth)) - if err := db.Store(m); err != nil { - ii.Error.Printf("Store point msg: %s", err) - return fmt.Sprintf("%s", err) - } - return "msg ok" -} - -var users_opt *string = flag.String("u", "points.txt", "Users database") -var db_opt *string = flag.String("db", "./db", "II database path (directory)") -var listen_opt *string = flag.String("L", ":8080", "Listen address") -var sysname_opt *string = flag.String("sys", "ii-go", "Node name") -var host_opt *string = flag.String("host", "http://127.0.0.1:8080", "Node address") -var verbose_opt *bool = flag.Bool("v", false, "Verbose") -var echo_opt *string = flag.String("e", "list.txt", "Echoes list") - -type WWW struct { - Host string - tpl *template.Template - db *ii.DB - edb *ii.EDB - udb *ii.UDB -} - -func get_ue(echoes []string, db *ii.DB, user ii.User, w http.ResponseWriter, r *http.Request) { - if len(echoes) == 0 { - return - } - slice := echoes[len(echoes)-1:][0] - var idx, lim int - if _, err := fmt.Sscanf(slice, "%d:%d", &idx, &lim); err == nil { - echoes = echoes[:len(echoes)-1] - } else { - idx, lim = 0, 0 - } - - for _, e := range echoes { - if !ii.IsEcho(e) { - continue - } - fmt.Fprintf(w, "%s\n", e) - ids := db.SelectIDS(ii.Query{Echo: e, Start: idx, Lim: lim, User: user}) - for _, id := range ids { - fmt.Fprintf(w, "%s\n", id) - } - } - -} -func main() { - var www WWW - ii.OpenLog(ioutil.Discard, os.Stdout, os.Stderr) - - flag.Parse() - - unix.Unveil("./tpl", "r") - unix.Unveil("/usr/local/share/iigo/tpl", "r") - unix.Unveil(*echo_opt, "r") - unix.Unveil(*users_opt, "rwc") - unix.Unveil(filepath.Dir(*db_opt), "rwc") - unix.Unveil(*db_opt + ".idx", "rwc") - unix.Unveil(os.TempDir(), "rwc") - unix.UnveilBlock() - - db := open_db(*db_opt) - edb := ii.LoadEcholist(*echo_opt) - udb := ii.OpenUsers(*users_opt) - if *verbose_opt { - ii.OpenLog(os.Stdout, os.Stdout, os.Stderr) - } - - db.Name = *sysname_opt - www.db = db - www.edb = edb - www.udb = udb - www.Host = *host_opt - WebInit(&www) - - fs := http.FileServer(http.Dir("style")) - http.Handle("/style/", http.StripPrefix("/style/", fs)) - - http.HandleFunc("/list.txt", func(w http.ResponseWriter, r *http.Request) { - echoes := db.Echoes(nil, ii.Query{}) - for _, v := range echoes { - if !ii.IsPrivate(v.Name) { - fmt.Fprintf(w, "%s:%d:%s\n", v.Name, v.Count, www.edb.Info[v.Name]) - } - } - }) - http.HandleFunc("/blacklist.txt", func(w http.ResponseWriter, r *http.Request) { - ids := db.SelectIDS(ii.Query{Blacklisted: true}) - for _, v := range ids { - fmt.Fprintf(w, "%s\n", v) - } - }) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - handleWWW(&www, w, r) - }) - http.HandleFunc("/u/point/", func(w http.ResponseWriter, r *http.Request) { - var pauth, tmsg string - switch r.Method { - case "GET": - udb.LoadUsers() - args := strings.Split(r.URL.Path[9:], "/") - - if len(args) >= 3 && args[1] == "u" { - pauth = args[0] - if !udb.Access(pauth) { - ii.Info.Printf("Access denied for pauth: %s", pauth) - return - } - user := udb.UserInfo(pauth) - if user == nil { - return - } - if args[2] == "e" { - echoes := args[3:] - get_ue(echoes, db, *user, w, r) - return - } - if args[2] == "m" { - ids := args[3:] - for _, i := range ids { - m, info := db.GetBundleInfo(i) - if m == "" || !db.Access(info, user) { - continue - } - fmt.Fprintf(w, "%s\n", m) - } - return - } - ii.Error.Printf("Wrong /u/point/ get request: %s", r.URL.Path[9:]) - return - } - if len(args) != 2 { - ii.Error.Printf("Wrong /u/point/ get request: %s", r.URL.Path[9:]) - return - } - pauth, tmsg = args[0], args[1] - default: - return - } - ii.Info.Printf("/u/point/%s/%s GET request", pauth, tmsg) - fmt.Fprintf(w, PointMsg(edb, db, udb, pauth, tmsg)) - }) - http.HandleFunc("/u/point", func(w http.ResponseWriter, r *http.Request) { - var pauth, tmsg string - switch r.Method { - case "POST": - if err := r.ParseForm(); err != nil { - ii.Error.Printf("Error in POST request: %s", err) - return - } - pauth = r.FormValue("pauth") - tmsg = r.FormValue("tmsg") - default: - return - } - ii.Info.Printf("/u/point/%s/%s POST request", pauth, tmsg) - fmt.Fprintf(w, PointMsg(edb, db, udb, pauth, tmsg)) - }) - http.HandleFunc("/x/c/", func(w http.ResponseWriter, r *http.Request) { - enames := strings.Split(r.URL.Path[5:], "/") - echoes := db.Echoes(enames, ii.Query{}) - for _, v := range echoes { - if !ii.IsPrivate(v.Name) { - fmt.Fprintf(w, "%s:%d:\n", v.Name, v.Count) - } - } - }) - http.HandleFunc("/u/m/", func(w http.ResponseWriter, r *http.Request) { - ids := strings.Split(r.URL.Path[5:], "/") - for _, i := range ids { - m, info := db.GetBundleInfo(i) - if m != "" && !ii.IsPrivate(info.Echo) { - fmt.Fprintf(w, "%s\n", m) - } - } - }) - http.HandleFunc("/u/e/", func(w http.ResponseWriter, r *http.Request) { - echoes := strings.Split(r.URL.Path[5:], "/") - get_ue(echoes, db, ii.User{}, w, r) - }) - http.HandleFunc("/m/", func(w http.ResponseWriter, r *http.Request) { - id := r.URL.Path[3:] - if !ii.IsMsgId(id) { - return - } - m := db.Get(id) - ii.Info.Printf("/m/%s %s", id, m) - if m != nil && !ii.IsPrivate(m.Echo) { - fmt.Fprintf(w, "%s", m.String()) - } - }) - http.HandleFunc("/e/", func(w http.ResponseWriter, r *http.Request) { - e := r.URL.Path[3:] - if !ii.IsEcho(e) || ii.IsPrivate(e) { - return - } - ids := db.SelectIDS(ii.Query{Echo: e}) - for _, id := range ids { - fmt.Fprintf(w, "%s\n", id) - } - }) - http.HandleFunc("/x/features", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "list.txt\nblacklist.txt\nu/e\nx/c\n") - }) - ii.Info.Printf("Listening on %s", *listen_opt) - -// http.HandleFunc("hugeping.ru/", func(w http.ResponseWriter, r *http.Request) { -// http.Redirect(w, r, "//club.hugeping.ru/blog/std.hugeping", http.StatusSeeOther) -// }) - -// http.Handle("hugeping.ru/", http.FileServer(http.Dir("/home/pi/Devel/gemini/www"))) -// http.Handle("syscall.ru/", http.FileServer(http.Dir("/home/pi/Devel/gemini/www"))) - - if err := http.ListenAndServe(*listen_opt, nil); err != nil { - ii.Error.Printf("Error running web server: %s", err) - } -} diff --git a/cmd/ii-node/web.go b/cmd/ii-node/web.go @@ -1,1085 +0,0 @@ -package main - -import ( - "git.openbsd.org.ru/vasyahacker/iigo/ii" - "os" - "bytes" - "encoding/base64" - "errors" - "fmt" - "html/template" - "image" - "image/png" - "net/http" - "regexp" - "sort" - "strings" - "time" -) - -const PAGE_SIZE = 100 - -type WebContext struct { - Echoes []*ii.Echo - Topics []*Topic - Topic string - Msg []*ii.Msg - Error string - Echo string - PfxPath string - Page int - Pages int - Pager []int - BasePath string - User *ii.User - Echolist *ii.EDB - Selected string - Template string - Ref string - Info string - Sysname string - Host string - www *WWW -} - -func www_register(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - ii.Trace.Printf("www register") - switch r.Method { - case "GET": - ctx.Template = "register.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "register.tpl", ctx) - return err - case "POST": - udb := ctx.www.udb - if err := r.ParseForm(); err != nil { - ii.Error.Printf("Error in POST request: %s", err) - return err - } - auth := r.FormValue("auth") - if auth != "" { /* edit form */ - u := udb.UserInfo(auth) - if u == nil { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - password := r.FormValue("password") - u.Secret = ii.MakeSecret(u.Name + password) - if err := udb.Edit(u); err != nil { - ii.Info.Printf("Can not edit user %s: %s", ctx.User.Name, err) - return err - } - http.Redirect(w, r, ctx.PfxPath + "/login", http.StatusSeeOther) - return nil - } - user := r.FormValue("username") - password := r.FormValue("password") - email := r.FormValue("email") - - err := udb.Add(user, email, password) - if err != nil { - ii.Info.Printf("Can not register user %s: %s", user, err) - return err - } - ii.Info.Printf("Registered user: %s", user) - http.Redirect(w, r, ctx.PfxPath + "/login", http.StatusSeeOther) - default: - return nil - } - return nil -} - -func www_login(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - ii.Trace.Printf("www login") - switch r.Method { - case "GET": - ctx.Template = "login.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "login.tpl", ctx) - return err - case "POST": - if err := r.ParseForm(); err != nil { - ii.Error.Printf("Error in POST request: %s", err) - return err - } - user := r.FormValue("username") - password := r.FormValue("password") - udb := ctx.www.udb - if !udb.Auth(user, password) { - ii.Info.Printf("Access denied for user: %s", user) - return errors.New("Access denied") - } - exp := time.Now().Add(10 * 365 * 24 * time.Hour) - cookie := http.Cookie{Name: "pauth", Value: udb.Secret(user), Expires: exp} - http.SetCookie(w, &cookie) - ii.Info.Printf("User logged in: %s\n", user) - http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther) - return nil - } - return errors.New("Wrong method") -} - -func www_profile(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - ii.Trace.Printf("www profile") - if ctx.User.Name == "" { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - ctx.Selected = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id) - ava, _ := ctx.User.Tags.Get("avatar") - if ava != "" { - if data, err := base64.URLEncoding.DecodeString(ava); err == nil { - ctx.Info = string(data) - } - } - ctx.Template = "profile.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "profile.tpl", ctx) - return err -} - -func www_logout(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - ii.Trace.Printf("www logout: %s", ctx.User.Name) - if ctx.User.Name == "" { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - cookie := http.Cookie{Name: "pauth", Value: "", Expires: time.Unix(0, 0)} - http.SetCookie(w, &cookie) - http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther) - return nil -} - -func www_index(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - ii.Trace.Printf("www index") - ctx.Echoes = ctx.www.db.Echoes(nil, ii.Query{User: *ctx.User}) - ctx.Template = "index.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "index.tpl", ctx) - return err -} - -func parse_ava(txt string) *image.RGBA { - txt = msg_clean(txt) - lines := strings.Split(txt, "\n") - img, _ := ParseXpm(lines) - return img -} - -var magicTable = map[string]string{ - "\xff\xd8\xff": "image/jpeg", - "\x89PNG\r\n\x1a\n": "image/png", - "GIF87a": "image/gif", - "GIF89a": "image/gif", -} - -func check_image(incipit []byte) string { - incipitStr := string(incipit) - for magic, mime := range magicTable { - if strings.HasPrefix(incipitStr, magic) { - return mime - } - } - return "" -} - -func www_base64(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - id := ctx.BasePath - m := ctx.www.db.Get(id) - if m == nil { - return errors.New("No such message") - } - lines := strings.Split(msg_clean(m.Text), "\n") - start := false - b64 := "" - fname := "" - pre := false - for _, v := range lines { - if !start && strings.Trim(v, " ") == "====" { - if !pre { - pre = true - continue - } - pre = false - continue - } - if pre { - continue - } - if !start && !strings.HasPrefix(v, "@base64:") { - continue - } - if start { - v = strings.Replace(v, " ", "", -1) - if !base64Regex.MatchString(v) { - break - } - b64 += v - continue - } - v = strings.TrimPrefix(v, "@base64:") - v = strings.Trim(v, " ") - fname = v - if fname == "" { - fname = "file" - } - start = true - } - if b64 == "" { - return nil - } - b, err := base64.StdEncoding.DecodeString(b64) - if err != nil { - if b, err = base64.RawStdEncoding.DecodeString(b64); err != nil { - if b, err = base64.URLEncoding.DecodeString(b64); err != nil { - return err - } - } - } - // w.Header().Set("Content-Type", "image/jpeg") - if check_image(b) != "" { - w.Header().Set("Content-Disposition", "inline") - } else { - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fname)) - } - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b))) - _, err = w.Write(b) - return err -} -func www_avatar(ctx *WebContext, w http.ResponseWriter, r *http.Request, user string) error { - if r.Method == "POST" { /* upload avatar */ - if ctx.User.Name == "" || ctx.User.Name != user { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - if err := r.ParseForm(); err != nil { - ii.Error.Printf("Error in POST request: %s", err) - return err - } - ava := r.FormValue("avatar") - if len(ava) > 2048 { - ii.Error.Printf("Avatar is too big.") - return errors.New("Avatar is too big (>2048 bytes)") - } - if ava == "" { - ii.Trace.Printf("Delete avatar for %s", ctx.User.Name) - ctx.User.Tags.Del("avatar") - } else { - img := parse_ava(ava) - if img == nil { - ii.Error.Printf("Wrong xpm format for avatar: " + user) - return errors.New("Wrong xpm format") - } - b64 := base64.URLEncoding.EncodeToString([]byte(ava)) - ii.Trace.Printf("New avatar for %s: %s", ctx.User.Name, b64) - ctx.User.Tags.Add("avatar/" + b64) - } - if err := ctx.www.udb.Edit(ctx.User); err != nil { - ii.Error.Printf("Error saving avatar: " + user) - return errors.New("Error saving avatar") - } - http.Redirect(w, r, ctx.PfxPath + "/profile", http.StatusSeeOther) - return nil - } - // var id int32 - // if !strings.HasPrefix(user, ctx.www.db.Name) { - // return nil - // } - // user = strings.TrimPrefix(user, ctx.www.db.Name) - // user = strings.TrimPrefix(user, ",") - // if _, err := fmt.Sscanf(user, "%d", &id); err != nil { - // return nil - // } - // u := ctx.www.udb.UserInfoId(id) - u := ctx.www.udb.UserInfoName(user) - if u == nil { - return nil - } - ava, _ := u.Tags.Get("avatar") - if ava == "" { - return nil - } - if data, err := base64.URLEncoding.DecodeString(ava); err == nil { - img := parse_ava(string(data)) - if img == nil { - ii.Error.Printf("Wrong xpm in avatar: %s\n", u.Name) - return nil - } - b := new(bytes.Buffer) - if err := png.Encode(b, img); err == nil { - w.Header().Set("Content-Type", "image/png") - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(b.Bytes()))) - if _, err := w.Write(b.Bytes()); err != nil { - return nil - } - return nil - } - ii.Error.Printf("Can't encode avatar in png: %s\n", u.Name) - } else { - ii.Error.Printf("Can't decode avatar: %s\n", u.Name) - } - return nil -} - -type Topic struct { - Ids []string - Count int - Last *ii.MsgInfo - Head *ii.Msg - Tail *ii.Msg -} - -func makePager(ctx *WebContext, count int, page int) int { - ctx.Pages = count / PAGE_SIZE - if count%PAGE_SIZE != 0 { - ctx.Pages++ - } - if page == 0 { - page++ - } else if page < 0 { - page = ctx.Pages + page + 1 - } - start := (page - 1) * PAGE_SIZE - if start < 0 { - start = 0 - page = 1 - } - ctx.Page = page - if ctx.Pages > 1 { - for i := 1; i <= ctx.Pages; i++ { - ctx.Pager = append(ctx.Pager, i) - } - } - return start -} - -func Select(ctx *WebContext, q ii.Query) []string { - q.User = *ctx.User - return ctx.www.db.SelectIDS(q) -} - -func trunc(str string, limit int) string { - result := []rune(str) - if len(result) > limit { - return string(result[:limit]) - } - return str -} - -func www_query(ctx *WebContext, w http.ResponseWriter, r *http.Request, q ii.Query, page int, rss bool) error { - db := ctx.www.db - req := ctx.BasePath - - if rss { - q.Start = -PAGE_SIZE - } - mis := db.LookupIDS(Select(ctx, q)) - ii.Trace.Printf("www query") - - sort.SliceStable(mis, func(i, j int) bool { - return mis[i].Num > mis[j].Num - }) - count := len(mis) - start := makePager(ctx, count, page) - nr := PAGE_SIZE - for i := start; i < count && nr > 0; i++ { - m := db.GetFast(mis[i].Id) - if m == nil { - ii.Error.Printf("Can't get msg: %s\n", mis[i].Id) - continue - } - ctx.Msg = append(ctx.Msg, m) - nr-- - } - if rss { - ctx.Topic = db.Name + " :: " + req - fmt.Fprintf(w, - `<?xml version="1.0" encoding="UTF-8"?> - <rss version="2.0" - xmlns:content="http://purl.org/rss/1.0/modules/content/" - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:media="http://search.yahoo.com/mrss/" - xmlns:atom="http://www.w3.org/2005/Atom" - xmlns:georss="http://www.georss.org/georss"> - <channel> - <title>%s</title> - <link>%s/%s</link> - <description> - %s - </description> - <language>ru</language> -`, - str_esc(ctx.Topic), ctx.www.Host, ctx.BasePath, str_esc(ctx.Topic)) - for _, m := range ctx.Msg { - fmt.Fprintf(w, - `<item><title>%s</title><guid>%s</guid><pubDate>%s</pubDate><author>%s</author><link>%s/%s#%s</link> - <description> - %s... - </description> - <content:encoded> -<![CDATA[ -%s -%s -]]> -</content:encoded></item> -`, - str_esc(m.Subj), m.MsgId, time.Unix(m.Date, 0).Format("2006-01-02 15:04:05"), - str_esc(m.From), ctx.www.Host + ctx.PfxPath, m.MsgId, m.MsgId, - str_esc(trunc(m.Text, 280)), - fmt.Sprintf("%s -> %s<br><br>", m.From, m.To), - msg_text(m)) - } - fmt.Fprintf(w, `</channel></rss> -`) - return nil - } - ctx.Template = "query.tpl" - return ctx.www.tpl.ExecuteTemplate(w, "query.tpl", ctx) -} - -func www_topics(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error { - db := ctx.www.db - echo := ctx.Echo - mis := db.LookupIDS(Select(ctx, ii.Query{Echo: echo})) - ii.Trace.Printf("www topics: %s", echo) - topicsIds := db.GetTopics(mis) - var topics []*Topic - ii.Trace.Printf("Start to generate topics") - - db.Sync.RLock() - defer db.Sync.RUnlock() - db.LoadIndex() - for _, t := range topicsIds { - topic := Topic{} - topic.Ids = t - topic.Count = len(topic.Ids) - 1 - if ctx.PfxPath == "/blog" { - topic.Last = db.LookupFast(topic.Ids[0], false) - } else { - topic.Last = db.LookupFast(topic.Ids[topic.Count], false) - } - if topic.Last == nil { - ii.Error.Printf("Skip wrong message: %s\n", t[0]) - continue - } - topics = append(topics, &topic) - } - sort.SliceStable(topics, func(i, j int) bool { - return topics[i].Last.Num > topics[j].Last.Num - }) - tcount := len(topics) - start := makePager(ctx, tcount, page) - nr := PAGE_SIZE - for i := start; i < tcount && nr > 0; i++ { - t := topics[i] - t.Head = db.GetFast(t.Ids[0]) - t.Tail = db.GetFast(t.Ids[t.Count]) - if t.Head == nil || t.Tail == nil { - ii.Error.Printf("Skip wrong message: %s\n", t.Ids[0]) - continue - } - ctx.Topics = append(ctx.Topics, topics[i]) - nr-- - } - ii.Trace.Printf("Stop to generate topics") - - if ctx.PfxPath == "/blog" { - ctx.Template = "blog.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "blog.tpl", ctx) - return err - } - ctx.Template = "topics.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "topics.tpl", ctx) - return err -} - -func www_topic(ctx *WebContext, w http.ResponseWriter, r *http.Request, page int) error { - id := ctx.BasePath - db := ctx.www.db - - mi := db.Lookup(id) - if mi == nil { - return errors.New("No such message") - } - - if !db.Access(mi, ctx.User) { - return errors.New("Access denied") - } - - if page == 0 { - ctx.Selected = id - } - ctx.Echo = mi.Echo - mis := db.LookupIDS(Select(ctx, ii.Query{Echo: mi.Echo})) - - topics := db.GetTopics(mis) - topic := mi.Topic - ctx.Topic = topic - ids := topics[topic] - - if len(ids) == 0 { - ids = append(ids, id) - } else if topic != mi.Id { - for k, v := range ids { - if v == mi.Id { - page = k/PAGE_SIZE + 1 - ctx.Selected = mi.Id - break - } - } - } - ii.Trace.Printf("www topic: %s", id) - start := makePager(ctx, len(ids), page) - nr := PAGE_SIZE - for i := start; i < len(ids) && nr > 0; i++ { - id := ids[i] - m := db.Get(id) - if m == nil { - ii.Error.Printf("Skip wrong message: %s", id) - continue - } - ctx.Msg = append(ctx.Msg, m) - nr-- - } - ctx.Template = "topic.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "topic.tpl", ctx) - return err -} - -func www_blacklist(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - id := ctx.BasePath - m := ctx.www.db.Get(id) - ii.Trace.Printf("www blacklist: %s", id) - if m == nil { - ii.Error.Printf("No such msg: %s", id) - return errors.New("No such msg") - } - if !msg_access(ctx.www, *m, *ctx.User) { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - err := ctx.www.db.Blacklist(m) - if err != nil { - ii.Error.Printf("Error blacklisting: %s", id) - return err - } - http.Redirect(w, r, ctx.PfxPath + "/", http.StatusSeeOther) - return nil -} - -func www_edit(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - id := ctx.BasePath - switch r.Method { - case "GET": - m := ctx.www.db.Get(id) - if m == nil { - ii.Error.Printf("No such msg: %s", id) - return errors.New("No such msg") - } - msg := *m - ln := strings.Split(msg_clean(msg.Text), "\n") - if len(ln) > 0 { - if strings.HasPrefix(ln[len(ln)-1], "P.S. Edited: ") { - msg.Text = strings.Join(ln[:len(ln)-1], "\n") - } - } - msg.Text = msg.Text + "\nP.S. Edited: " + time.Now().Format("2006-01-02 15:04:05") - ctx.Msg = append(ctx.Msg, &msg) - ctx.Template = "edit.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "edit.tpl", ctx) - return err - case "POST": - ctx.BasePath = "" - return www_new(ctx, w, r) - } - return nil -} - -func www_new(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - echo := ctx.BasePath - ctx.Echo = echo - - switch r.Method { - case "GET": - ctx.Template = "new.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "new.tpl", ctx) - return err - case "POST": - edit := (echo == "") - ii.Trace.Printf("www new topic in %s", echo) - if err := r.ParseForm(); err != nil { - ii.Error.Printf("Error in POST request: %s", err) - return err - } - if ctx.User.Name == "" { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - subj := r.FormValue("subj") - to := r.FormValue("to") - msg := r.FormValue("msg") - repto := r.FormValue("repto") - id := r.FormValue("id") - if repto == id { - repto = "" - } - newecho := r.FormValue("echo") - if newecho != "" { - echo = newecho - } - if !ctx.www.edb.Allowed(echo) && ctx.User.Id != 1 { - ii.Error.Printf("This echo is disallowed") - return errors.New("This echo is disallowed") - } - action := r.FormValue("action") - text := fmt.Sprintf("%s\n%s\n%s\n\n%s", echo, to, subj, msg) - m, err := ii.DecodeMsgline(text, false) - if err != nil { - ii.Error.Printf("Error while posting new topic: %s", err) - return err - } - m.From = ctx.User.Name - m.Addr = fmt.Sprintf("%s,%d", ctx.www.db.Name, ctx.User.Id) - if repto != "" { - m.Tags.Add("repto/" + repto) - } - if id != "" { - om := ctx.www.db.Get(id) - if (om == nil || m.Addr != om.Addr) && ctx.User.Id != 1 { - ii.Error.Printf("Access denied") - return errors.New("Access denied") - } - m.Date = om.Date - m.MsgId = id - m.From = om.From - m.Addr = om.Addr - } - if action == "Submit" { // submit - if edit { - err = ctx.www.db.Edit(m) - } else { - err = ctx.www.db.Store(m) - } - if err != nil { - ii.Error.Printf("Error while storig new topic %s: %s", m.MsgId, err) - return err - } - http.Redirect(w, r, ctx.PfxPath + "/"+m.MsgId+"#"+m.MsgId, http.StatusSeeOther) - return nil - } - if !edit { - m.MsgId = "" - } - ctx.Msg = append(ctx.Msg, m) - ctx.Template = "preview.tpl" - err = ctx.www.tpl.ExecuteTemplate(w, "preview.tpl", ctx) - return err - } - return nil -} - -func www_reply(ctx *WebContext, w http.ResponseWriter, r *http.Request, quote bool) error { - id := ctx.BasePath - m := ctx.www.db.Get(id) - if m == nil { - ii.Error.Printf("No such msg: %s", id) - return errors.New("No such msg") - } - msg := *m - msg.To = msg.From - msg.Subj = "Re: " + strings.TrimPrefix(msg.Subj, "Re: ") - msg.Tags.Add("repto/" + id) - if quote { - msg.Text = msg_quote(msg.Text, msg.From) - } else { - msg.Text = "" - } - ctx.Msg = append(ctx.Msg, &msg) - ctx.Echo = msg.Echo - ctx.Template = "reply.tpl" - err := ctx.www.tpl.ExecuteTemplate(w, "reply.tpl", ctx) - return err -} - -func str_esc(l string) string { - l = strings.Replace(l, "&", "&amp;", -1) - l = strings.Replace(l, "<", "&lt;", -1) - l = strings.Replace(l, ">", "&gt;", -1) - return l -} - -var quoteRegex = regexp.MustCompile("^[^ >]*>") -var urlRegex = regexp.MustCompile(`(http|ftp|https|gemini)://[^ <>"]+`) -var url2Regex = regexp.MustCompile(`{{{href=[0-9]+}}}`) -var urlIIRegex = regexp.MustCompile(`ii://[a-zA-Z0-9_\-.]+`) -var base64Regex = regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`) - -func msg_clean(txt string) string { - txt = strings.Replace(txt, "\r", "", -1) - txt = strings.TrimLeft(txt, "\n") - txt = strings.TrimRight(txt, "\n") - return txt -} -func msg_quote(txt string, from string) string { - txt = msg_clean(txt) - f := "" - names := strings.Split(from, " ") - if len(names) >= 2 { - from = fmt.Sprintf("%v%v", - string([]rune(names[0])[0]), - string([]rune(names[1])[0])) - } - for _, l := range strings.Split(txt, "\n") { - if strings.Trim(l, " ") == "" { - f += l + "\n" - continue - } - if quoteRegex.MatchString(l) { - s := strings.Index(l, ">") - f += l[:s] + ">>" + l[s+1:] + "\n" - } else { - f += from + "> " + l + "\n" - } - } - return f -} - -func ReverseStr(s string) string { - runes := []rune(s) - for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { - runes[i], runes[j] = runes[j], runes[i] - } - return string(runes) -} - -func msg_esc(l string) string { - var links []string - link := 0 - l = string(urlIIRegex.ReplaceAllFunc([]byte(l), - func(line []byte) []byte { - s := string(line) - url := strings.TrimPrefix(s, "ii://") - links = append(links, fmt.Sprintf(`<a href="/%s#%s" class="url">%s</a>`, - url, url, str_esc(s))) - link++ - return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1)) - })) - l = string(urlRegex.ReplaceAllFunc([]byte(l), - func(line []byte) []byte { - s := string(line) - links = append(links, fmt.Sprintf(`<a href="%s" class="url">%s</a>`, - s, str_esc(s))) - link++ - return []byte(fmt.Sprintf(`{{{href=%d}}}`, link-1)) - })) - l = str_esc(l) - l = string(url2Regex.ReplaceAllFunc([]byte(l), - func(line []byte) []byte { - s := string(line) - var n int - fmt.Sscanf(s, "{{{href=%d}}}", &n) - return []byte(links[n]) - })) - - return l -} - -func msg_text(m *ii.Msg) string { - return msg_trunc(m, 0, "") -} - -func msg_trunc(m *ii.Msg, maxlen int, more string) string { - if m == nil { - return "" - } - txt := m.Text - txt = msg_clean(txt) - f := "" - pre := false - skip := 0 - lines := strings.Split(txt, "\n") - for k, l := range lines { - if skip > 0 { - skip-- - continue - } - if strings.Trim(l, " ") == "====" { - if !pre { - pre = true - f += "<pre class=\"code\">\n" - continue - } - pre = false - f += "</pre>\n" - continue - } - if pre { - f += str_esc(l) + "\n" - continue - } - if strings.HasPrefix(l, "/* XPM */") || strings.HasPrefix(l, "! XPM2") { - var img *image.RGBA - img, skip = ParseXpm(lines[k:]) - if img != nil { - skip-- - /* embed xpm */ - b := new(bytes.Buffer) - if err := png.Encode(b, img); err == nil { - b64 := base64.StdEncoding.EncodeToString(b.Bytes()) - l = fmt.Sprintf("<img class=\"img\" src=\"data:image/png;base64,%s\"><br>\n", - b64) - f += l - continue - } - } - skip = 0 - l = msg_esc(l) - } else if strings.HasPrefix(l, "P.S.") || strings.HasPrefix(l, "PS:") || - strings.HasPrefix(l, "//") || strings.HasPrefix(l, "+++ ") { - l = fmt.Sprintf("<span class=\"comment\">%s</span>", str_esc(l)) - } else if strings.HasPrefix(l, "# ") || strings.HasPrefix(l, "= ") || - strings.HasPrefix(l, "## ") || strings.HasPrefix(l, "== ") || - strings.HasPrefix(l, "### ") || strings.HasPrefix(l, "=== ") { - l = fmt.Sprintf("<span class=\"header\">%s</span>", str_esc(l)) - } else if strings.HasPrefix(l, "@spoiler:") { - l = fmt.Sprintf("<span class=\"spoiler\">%s</span>", str_esc(ReverseStr(l))) - } else if quoteRegex.MatchString(l) { - l = fmt.Sprintf("<span class=\"quote\">%s</span>", str_esc(l)) - } else if strings.HasPrefix(l, "@base64:") { - fname := strings.TrimPrefix(l, "@base64:") - fname = strings.Trim(fname, " ") - if fname == "" { - fname = "file" - } - f += fmt.Sprintf("<a class=\"attach\" href=\"/%s/base64\">%s</a><br>\n", m.MsgId, str_esc(fname)) - return f - } else { - l = msg_esc(l) - } - f += l - if maxlen > 0 && len(f) > maxlen { - f += more + "<br>\n" - break - } else { - f += "<br>\n" - } - } - if pre { - pre = false - f += "</pre>\n" - } - return f -} - -func msg_access(www *WWW, m ii.Msg, u ii.User) bool { - addr := fmt.Sprintf("%s,%d", www.db.Name, u.Id) - return addr == m.Addr || u.Id == 1 -} - -func WebInit(www *WWW) { - funcMap := template.FuncMap{ - "fdate": func(date int64) template.HTML { - if time.Now().Unix()-date < 60*60*24 { - return template.HTML("<span class='today'>" + time.Unix(date, 0).Format("2006-01-02 15:04:05") + "</span>") - } - return template.HTML(time.Unix(date, 0).Format("2006-01-02 15:04:05")) - }, - "msg_text": func(m *ii.Msg) template.HTML { - return template.HTML(msg_text(m)) - }, - "msg_trunc": func(m *ii.Msg, len int, more string) template.HTML { - return template.HTML(msg_trunc(m, len, more)) - }, - "repto": func(m ii.Msg) string { - r, _ := m.Tag("repto") - if r == "" { - return m.MsgId - } - return r - }, - "msg_quote": msg_quote, - "msg_access": func(m ii.Msg, u ii.User) bool { - return msg_access(www, m, u) - }, - "is_even": func(i int) bool { - return i%2 == 0 - }, - "unescape": func(s string) template.HTML { - return template.HTML(s) - }, - "has_avatar": func(user string) bool { - ui := www.udb.UserInfoName(user) - if ui != nil { - _, ok := ui.Tags.Get("avatar") - return ok - } - return false - }, - } - - tpl_path := "tpl" - if _, err := os.Stat(tpl_path); errors.Is(err, os.ErrNotExist) { - fmt.Println("./tpl not found, trying /usr/local/share/iigo/tpl..") - tpl_path = "/usr/local/share/iigo/tpl" - if _, err := os.Stat(tpl_path); errors.Is(err, os.ErrNotExist) { - fmt.Println(tpl_path, "not found") - os.Exit(1) - } - } - - www.tpl = template.Must( - template.New("main").Funcs(funcMap).ParseGlob(tpl_path + "/*.tpl")) -} - -func handleErr(ctx *WebContext, w http.ResponseWriter, err error) { - ctx.Error = err.Error() - ctx.Template = "error.tpl" - ctx.www.tpl.ExecuteTemplate(w, "error.tpl", ctx) -} - -func handleWWW(www *WWW, w http.ResponseWriter, r *http.Request) { - var ctx WebContext - var user *ii.User = &ii.User{} - ctx.User = user - ctx.www = www - ctx.Sysname = www.db.Name - ctx.Host = www.Host - www.udb.LoadUsers() - err := _handleWWW(&ctx, w, r) - if err != nil { - handleErr(&ctx, w, err) - } -} - -func _handleWWW(ctx *WebContext, w http.ResponseWriter, r *http.Request) error { - cookie, err := r.Cookie("pauth") - if err == nil { - udb := ctx.www.udb - if udb.Access(cookie.Value) { - if user := udb.UserInfo(cookie.Value); user != nil { - ctx.User = user - } - } - } - ii.Trace.Printf("[%s] GET %s", ctx.User.Name, r.URL.Path) - path := strings.TrimPrefix(r.URL.Path, "/") - args := strings.Split(path, "/") - ctx.Echolist = ctx.www.edb - ctx.Ref = r.Header.Get("Referer") - if len(args) > 1 && args[0] == "blog" { - ctx.PfxPath = "/blog" - args = args[1:] - } - if args[0] == "" { - ctx.BasePath = "" - return www_index(ctx, w, r) - } else if args[0] == "login" { - ctx.BasePath = "login" - return www_login(ctx, w, r) - } else if args[0] == "logout" { - ctx.BasePath = "logout" - return www_logout(ctx, w, r) - } else if args[0] == "profile" { - ctx.BasePath = "profile" - return www_profile(ctx, w, r) - } else if args[0] == "register" { - ctx.BasePath = "register" - return www_register(ctx, w, r) - } else if args[0] == "reset" { - ctx.Template = "reset.tpl" - return ctx.www.tpl.ExecuteTemplate(w, "reset.tpl", ctx) - } else if args[0] == "avatar" { - ctx.BasePath = "avatar" - if len(args) < 2 { - return errors.New("Wrong request") - } - return www_avatar(ctx, w, r, args[1]) - } else if ii.IsMsgId(args[0]) { - page := 0 - ctx.BasePath = args[0] - if len(args) > 1 { - if args[1] == "reply" { - return www_reply(ctx, w, r, !(len(args) > 2 && args[2] == "new")) - } else if args[1] == "edit" { - return www_edit(ctx, w, r) - } else if args[1] == "blacklist" { - return www_blacklist(ctx, w, r) - } else if args[1] == "base64" { - return www_base64(ctx, w, r) - } - fmt.Sscanf(args[1], "%d", &page) - } - return www_topic(ctx, w, r, page) - } else if args[0] == "new" { - ctx.BasePath = "" - return www_new(ctx, w, r) - } else if args[0] == "to" { - page := 1 - rss := false - if len(args) < 2 { - return errors.New("Wrong request") - } - if len(args) > 2 { - if args[2] == "rss" { - rss = true - } else { - fmt.Sscanf(args[2], "%d", &page) - } - } - ctx.BasePath = "to/" + args[1] - return www_query(ctx, w, r, ii.Query{To: args[1]}, page, rss) - } else if args[0] == "from" { - page := 1 - rss := false - if len(args) < 2 { - return errors.New("Wrong request") - } - if len(args) > 2 { - if args[2] == "rss" { - rss = true - } else { - fmt.Sscanf(args[2], "%d", &page) - } - } - ctx.BasePath = "from/" + args[1] - return www_query(ctx, w, r, ii.Query{From: args[1]}, page, rss) - } else if args[0] == "echo" || args[0] == "echo+topics" { - page := 1 - rss := false - if len(args) < 2 { - return errors.New("Wrong request") - } - if len(args) > 2 { - if args[2] == "rss" { - rss = true - } else { - fmt.Sscanf(args[2], "%d", &page) - } - } - q := ii.Query{Echo: args[1]} - if args[1] == "all" { - q.Echo = "" - } - ctx.Echo = q.Echo - q.Start = -PAGE_SIZE - if args[0] == "echo+topics" { - q.Repto = "!" - ctx.BasePath = "echo+topics/" + args[1] - } else { - ctx.BasePath = "echo/" + args[1] - } - return www_query(ctx, w, r, q, page, rss) - } else if ii.IsEcho(args[0]) { - page := 1 - ctx.Echo = args[0] - ctx.BasePath = args[0] - if len(args) > 1 { - if args[1] == "new" { - ctx.BasePath = args[0] - return www_new(ctx, w, r) - } - fmt.Sscanf(args[1], "%d", &page) - } - return www_topics(ctx, w, r, page) - } else { - w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "404\n") - } - return nil -} diff --git a/cmd/ii-tool/main.go b/cmd/ii-tool/main.go @@ -1,423 +0,0 @@ -package main - -import ( - "git.openbsd.org.ru/vasyahacker/iigo/ii" - "bufio" - "flag" - "fmt" - "io" - "io/ioutil" - "os" - "sort" - "strings" -) - -func open_db(path string) *ii.DB { - db := ii.OpenDB(path) - if db == nil { - fmt.Printf("Can no open db: %s\n", path) - os.Exit(1) - } - return db -} - -func open_users_db(path string) *ii.UDB { - db := ii.OpenUsers(path) - if err := db.LoadUsers(); err != nil { - fmt.Printf("Can no load db: %s\n", path) - os.Exit(1) - } - return db -} - -func GetFile(path string) string { - var file *os.File - var err error - if path == "-" { - file = os.Stdin - } else { - file, err = os.Open(path) - if err != nil { - fmt.Printf("Can not open file %s: %s\n", path, err) - os.Exit(1) - } - defer file.Close() - } - b, err := ioutil.ReadAll(file) - if err != nil { - fmt.Printf("Can not read file %s: %s\n", path, err) - os.Exit(1) - } - return string(b) -} - -func main() { - ii.OpenLog(ioutil.Discard, os.Stdout, os.Stderr) - - db_opt := flag.String("db", "./db", "II database path (directory)") - lim_opt := flag.Int("lim", 0, "Fetch last N messages") - verbose_opt := flag.Bool("v", false, "Verbose") - force_opt := flag.Bool("f", false, "Force full sync") - users_opt := flag.String("u", "points.txt", "Users database") - conns_opt := flag.Int("j", 6, "Maximum parallel jobs") - topics_opt := flag.Bool("t", false, "Select topics only") - from_opt := flag.String("from", "", "Select from") - to_opt := flag.String("to", "", "Select to") - flag.Parse() - ii.MaxConnections = *conns_opt - if *verbose_opt { - ii.OpenLog(os.Stdout, os.Stdout, os.Stderr) - } - - args := flag.Args() - if len(args) < 1 { - fmt.Printf(`Help: %s [options] command [arguments] -Commands: - search <string> [echo] - search in base - send <server> <pauth> <msg|-> - send message - clean - cleanup database - fetch <url> [echofile|-] - fetch - store <bundle|-> - import bundle to database - get <msgid> - show message from database - select <echo> [[start]:lim] - get slice from echo - index - recreate index - blacklist <msgid> - blacklist msg - useradd <name> <e-mail> <password> - adduser -Options: - -db=<path> - database path - -lim=<lim> - fetch lim last messages - -u=<path> - points account file - -t - topics only (select,get) - -from=<user> - select from - -to=<user> - select to -`, os.Args[0]) - os.Exit(1) - } - switch cmd := args[0]; cmd { - case "search": - echo := "" - if len(args) < 2 { - fmt.Printf("No string supplied\n") - os.Exit(1) - } - if len(args) > 2 { - echo = args[2] - } - db := open_db(*db_opt) - db.Lock() - defer db.Unlock() - db.LoadIndex() - for _, v := range db.Idx.List { - if echo != "" { - mi := db.Idx.Hash[v] - if mi.Echo != echo { - continue - } - } - m := db.GetFast(v) - if m == nil { - continue - } - if strings.Contains(m.Text, args[1]) { - fmt.Printf("%s\n", v) - if *verbose_opt { - fmt.Printf("%s\n", m) - } - } - } - case "blacklist": - if len(args) < 2 { - fmt.Printf("No msgid supplied\n") - os.Exit(1) - } - db := open_db(*db_opt) - m := db.Get(args[1]) - if m != nil { - if err := db.Blacklist(m); err != nil { - fmt.Printf("Can not blacklist: %s\n", err) - os.Exit(1) - } - } else { - fmt.Printf("No such msg") - } - case "send": - if len(args) < 4 { - fmt.Printf("No argumnet(s) supplied\nShould be: <server> <pauth> and <file|->.\n") - os.Exit(1) - } - msg := GetFile(args[3]) - if _, err := ii.DecodeMsgline(string(msg), false); err != nil { - fmt.Printf("Wrong message format\n") - os.Exit(1) - } - n, err := ii.Connect(args[1]) - if err != nil { - fmt.Printf("Can not connect to %s: %s\n", args[1], err) - os.Exit(1) - } - if err := n.Post(args[2], msg); err != nil { - fmt.Printf("Can not send message: %s\n", err) - os.Exit(1) - } - case "useradd": - if len(args) < 4 { - fmt.Printf("No argumnet(s) supplied\nShould be: name, e-mail and password.\n") - os.Exit(1) - } - db := open_users_db(*users_opt) - if err := db.Add(args[1], args[2], args[3]); err != nil { - fmt.Printf("Can not add user: %s\n", err) - os.Exit(1) - } - case "clean": - hash := make(map[string]int) - last := make(map[string]string) - nr := 0 - dup := 0 - fmt.Printf("Pass 1...\n") - err := ii.FileLines(*db_opt, func(line string) bool { - nr++ - a := strings.Split(line, ":") - if len(a) != 2 { - ii.Error.Printf("Error in line: %d", nr) - return true - } - if !ii.IsMsgId(a[0]) { - ii.Error.Printf("Error in line: %d", nr) - return true - } - if _, ok := hash[a[0]]; ok { - hash[a[0]]++ - dup++ - last[a[0]] = line - } else { - hash[a[0]] = 1 - } - return true - }) - fmt.Printf("%d lines... %d dups...\n", nr, dup) - if dup == 0 { - os.Exit(0) - } - fmt.Printf("Pass 2...\n") - nr = 0 - f, err := os.OpenFile(*db_opt+".new", os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - fmt.Printf("Error: %s\n", err) - os.Exit(1) - } - skip := 0 - err = ii.FileLines(*db_opt, func(line string) bool { - nr++ - a := strings.Split(line, ":") - id := a[0] - if len(a) != 2 { - fmt.Printf("Error in line: %d\n", nr) - skip++ - return true - } - if !ii.IsMsgId(id) { - fmt.Printf("Error in line: %d\n", nr) - skip++ - return true - } - if v, ok := hash[id]; !ok || v == 0 { - fmt.Printf("Error. DB has changed. Aborted.\n") - os.Exit(1) - } - if hash[id] > 0 { // first record - hash[id] = -hash[id] - l := line - if hash[id] < -1 { - l = last[id] - } - if _, err := f.WriteString(l + "\n"); err != nil { - fmt.Printf("Error: %s\n", err) - os.Exit(1) - } - } else { - skip++ - } - hash[id] += 1 - if hash[id] > 0 { - fmt.Printf("Error. DB has changed. Aborted.\n") - os.Exit(1) - } - return true - }) - f.Close() - if err != nil { - fmt.Printf("Error: %s\n") - os.Exit(1) - } - for _, v := range hash { - if v != 0 { - fmt.Printf("Error. DB shrinked. Aborted.\n") - os.Exit(1) - } - } - fmt.Printf("%d messages removed. File %s created.\n", skip, *db_opt+".new") - case "fetch": - var echolist []string - if len(args) < 2 { - fmt.Printf("No url supplied\n") - os.Exit(1) - } - db := open_db(*db_opt) - n, err := ii.Connect(args[1]) - if err != nil { - fmt.Printf("Can not connect to %s: %s\n", args[1], err) - os.Exit(1) - } - if *force_opt { - n.Force = true - } - if len(args) > 2 { - str := GetFile(args[2]) - for _, v := range strings.Split(str, "\n") { - echolist = append(echolist, strings.Split(v, ":")[0]) - } - } - err = n.Fetch(db, echolist, *lim_opt) - if err != nil { - fmt.Printf("Can not fetch from %s: %s\n", args[1], err) - os.Exit(1) - } - case "store": - if len(args) < 2 { - fmt.Printf("No bundle file supplied\n") - os.Exit(1) - } - db := open_db(*db_opt) - var f *os.File - var err error - if args[1] == "-" { - f = os.Stdin - } else { - f, err = os.Open(args[1]) - } - if err != nil { - fmt.Printf("Can no open bundle: %s\n", args[1]) - os.Exit(1) - } - defer f.Close() - reader := bufio.NewReader(f) - for { - line, err := reader.ReadString('\n') - if err != nil && err != io.EOF { - fmt.Printf("Can read input (%s)\n", err) - os.Exit(1) - } - line = strings.TrimSuffix(line, "\n") - if err == io.EOF { - break - } - m, err := ii.DecodeBundle(line) - if m == nil { - fmt.Printf("Can not parse message: %s (%s)\n", line, err) - continue - } - if db.Lookup(m.MsgId) == nil { - if err := db.Store(m); err != nil { - fmt.Printf("Can not store message: %s\n", err) - os.Exit(1) - } - } - } - case "get": - if len(args) < 2 { - fmt.Printf("No msgid supplied\n") - os.Exit(1) - } - db := open_db(*db_opt) - - if *topics_opt { - mi := db.Lookup(args[1]) - if mi == nil { - return - } - mis := db.LookupIDS(db.SelectIDS(ii.Query{Echo: mi.Echo})) - topic := mi.Id - for p := mi; p != nil; p = db.LookupFast(p.Repto, false) { - if p.Repto == p.Id { - break - } - if p.Echo != mi.Echo { - continue - } - topic = p.Id - } - ids := db.GetTopics(mis)[topic] - if len(ids) == 0 { - ids = append(ids, args[1]) - } - for _, m := range ids { - fmt.Println(m) - } - return - } - - m := db.Get(args[1]) - if m != nil { - fmt.Println(m) - } - case "select": - if len(args) < 2 { - fmt.Printf("No echo supplied\n") - os.Exit(1) - } - db := open_db(*db_opt) - req := ii.Query{ Echo: args[1] } - if *from_opt != "" { - req.From = *from_opt - } - if *to_opt != "" { - req.To = *to_opt - } - - if *topics_opt { - req.Repto = "!" - } - if len(args) > 2 { - fmt.Sscanf(args[2], "%d:%d", &req.Start, &req.Lim) - } - resp := db.SelectIDS(req) - for _, v := range resp { - if *verbose_opt { - fmt.Println(db.Get(v)) - } else { - fmt.Println(v) - } - } - case "sort": - db := open_db(*db_opt) - db.LoadIndex() - scanner := bufio.NewScanner(os.Stdin) - var mm []*ii.Msg - for scanner.Scan() { - mi := db.LookupFast(scanner.Text(), false) - if mi != nil { - mm = append(mm, db.Get(mi.Id)) - } - } - sort.SliceStable(mm, func(i, j int) bool { - return mm[i].Date < mm[j].Date - }) - for _, v := range mm { - if *verbose_opt { - fmt.Println(v) - } else { - fmt.Println(v.MsgId) - } - } - case "index": - db := open_db(*db_opt) - if err := db.CreateIndex(); err != nil { - fmt.Printf("Can not rebuild index: %s\n", err) - os.Exit(1) - } - default: - fmt.Printf("Wrong cmd: %s\n", cmd) - os.Exit(1) - } -} diff --git a/go.mod b/go.mod @@ -1,10 +1,4 @@ -module git.openbsd.org.ru/vasyahacker/iigo - -retract ( - v1.0.0 // bad experiment. - v0.1.1 // bad experiment. - v0.1.0 // bad experiment. -) +module git.openbsd.org.ru/vasyahacker/openidec go 1.18 diff --git a/ii/db.go b/ii/db.go @@ -53,7 +53,7 @@ type Index struct { // Name: database name, 'db' by default. // Sync: used to syncronize access to DB from goroutines (many readers, one writer). // IdxSync: same, but for Index. -// LockDepth: used for recursive file lock, to avoid conflict between ii-tool and ii-node. +// LockDepth: used for recursive file lock, to avoid conflict between idecctl and idecd. type DB struct { Path string Idx Index @@ -76,7 +76,7 @@ func append_file(fn string, text string) error { return nil } -// Recursive file lock. Used to avoid conflicts between ii-tool and ii-node. +// Recursive file lock. Used to avoid conflicts between idecctl and idecd. // Uses mkdir as atomic operation. // Note: dirs created as db.LockPath() // 16 sec is limit. diff --git a/ii/msg.go b/ii/msg.go @@ -57,7 +57,7 @@ func IsMsgId(id string) bool { } // Check if Echoarea is private area -// This is ii-go extension, echoareas +// This is openidec extension, echoareas // that has "." prefix are for private messaging. // Those areas can be fetched only with /u/point/auth/u/e/ scheme func IsPrivate(e string) bool { diff --git a/man/idecctl.1 b/man/idecctl.1 @@ -0,0 +1,120 @@ +.TH idecctl 1 "March 12, 2023" "version 0.0.1" "OpenIDEC" +.SH NAME +.B idecctl +.SH SYNOPSIS +.B idecctl +[options] command [arguments] +.SH DESCRIPTION +.B idecctl +can be used to fetch messages from another node and maintaince database. +.SH OPTIONS +.TP +.B FETCH MESSAGES +.B idecctl +[options] +.B fetch +[uri] [echolist] +.nf + +-db <database> + db by default (db.idx \- genetated index) + +-lim=<n> + fetch mode, if omitted full sync will be performed if needed + if n > 0 - last n messages synced + if n < 0 - adaptive fetching with step n will be performed + +-f + do not check last message, perform sync even it is not needed + +echolist is the file with echonames (can has : splitted columns, like list.txt) +or '-' to load it from stdin + +If echolist is omitted, fetcher will try to get all echos. +It uses list.txt extension of IDEC if target node supports it. +.fi +.TP +.B CREATE INDEX +.B idecctl +index +.nf +Index file (db.idx by default) is created when needed. +If you want force to recreate it, use: idecctl index +.fi +.TP +.B STORE BUNDLE INTO DB +.B idecctl +[options] +.B store +[DB] +.nf + +You can merge records from DB to db with store command. + +-db <database> + db to store/merge in + db - is file with bundles or '-' for stdin. + +DB is just msgid:message bundles in base64 stored in text file. +.fi +.TP +.B SHOW MESSAGES +.nf +Select messages: + idecctl [options] select <echo.name> [slice] + + slice is the start:limit + +Messages are identificated by unique message ids (MsgId). +It is the first column in bundle: <msgid>:<message> + +Show selected message: + idecctl [options] get <MsgId> + +Search message: + idecctl [options] search <string> [echo] + +.B options: + -from <user> -- from user + -to <user> -- to user + -t -- only topics (w/o repto) + -db <database> -- db by default (db.idx - genetated index) + -v -- show message text, not only MsgId + +.fi +.TP +.B ADD USER (POINT) +idecctl [-u pointfile] useradd <name> <e-mail> <password> +.nf + +By default, pointfile is points.txt +.fi +.TP +.B BLACKLIST MSG +idecctl [-u pointfile] useradd <name> <e-mail> <password> +.nf + +Blacklist is just new record with same id but spectial status. +.fi +.SH EXAMPLES +.TP Get database from remote node and store to ./db +idecctl fetch http://hugeping.tk +.TP +Fetch messages +echo "std.club:this comment will be omitted" | idecctl fetch http://127.0.0.1:8080 - +.TP +get last message +idecctl select std.club -1:1 +.TP +get first 10 messages +idecctl select std.club 0:10 +.TP +To show last 5 messages adressed to selected user (sort ids by date with sort command) +idecctl [options] -to <user> select "" | idecctl sort | tail -n5 | idecctl -v sort +.TP +Show and print last message to Peter +idecctl -v -to Peter "" -1:1 +.SH SEE ALSO +openidec(1), idecd(1), idecgmi(1) +.SH AUTHOR +hugeping ( gl00my (at) mail.ru ) +\ No newline at end of file diff --git a/man/idecd.1 b/man/idecd.1 @@ -0,0 +1,78 @@ +.TH idecd 1 "March 12, 2023" "version 0.0.1" "OpenIDEC" +.SH NAME +idecd - is idec node +.SH SYNOPSIS +.B idecd +[options] +.SH DESCRIPTION +For run ii/idec node. +.SH OPTIONS +.TP +-L +Listen address, default is :8080 +.TP +-db <path> +Database, "db" by default +.TP +-e list +Echos list file. This file needs only for descriptions and must be in list.txt format, where 2nd colum is ignored. When this file is exists, points can not create they own echos. (list.txt by default) +.TP +-host <string> +Host string for node. (http://127.0.0.1:8080 by default) +.TP +-sys "name" +Node name. "OpenIDEC" by default +.TP +-u <points> +Points file. "points.txt" by default. +.TP +-v +Be verbose (for tracing) +.SH STANDARTS SUPPORTED +.TP +.BI u/e +.TP +.BI list.txt +.TP +.BI x/c +.TP +.BI blacklist.txt +.TP +.BI m/ +.TP +.BI e/ +.SH LIMITATIONS +Size of message can not be greater then 65536 bytes (before encoded into base64). +.SH WEB INTERFACE +User with id 1 (first created user) is admin. +.PP +Admin can create new echoes with: http://127.0.0.1:8080/new +.PP +Another hiden feature, is blacklisting: http://127.0.0.1:8080/msgid/blacklist +.PP +.TP +Web interface supports some non-standart features in message body text: +.nf +@spolier: spoiler +@base64: name (base64 data from next line till end of message) +xpm2 and xpm3 images embedding +.fi +.SH RSS +http://127.0.0.1:8080/echo/all/rss - All messages +.PP +http://127.0.0.1:8080/echo/echo.name/rss - By echo.name +.PP +http://127.0.0.1:8080/to/User/rss - For User +.PP +http://127.0.0.1:8080/from/User/rss - From User +.SH EXAMPLES +Fetch db and start node: +.nf +idecctl fetch http://hugeping.tk # get database from remote node +ftp http://hugeping.tk/list.txt # for echo descriptions +idecd -sys "newnode" # run node with name "newnode" +.fi +.SH SEE ALSO +idecctl(1), openidec(1), idecgmi(1) +.SH AUTHOR +hugeping ( gl00my (at) mail.ru ) +\ No newline at end of file diff --git a/man/idecgmi.1 b/man/idecgmi.1 @@ -0,0 +1,19 @@ +.TH idecgmi 1 "March 12, 2023" "version 0.0.1" "OpenIDEC" +.SH NAME +idecgmi - generate gemini data +.SH SYNOPSIS +.B idecgmi +[options] command [arguments] +.SH DESCRIPTION +Generate gemini data from idec db. +.SH OPTIONS +.TP +-data <path> gemini +generate gemini data +.TP +-db=<path> +database path +.SH SEE ALSO +idecctl(1), idecd(1), openidec(1) +.SH AUTHOR +hugeping ( gl00my (at) mail.ru ) diff --git a/man/ii-gemini.1 b/man/ii-gemini.1 @@ -1,19 +0,0 @@ -.TH ii-tool 1 "March 12, 2023" "version 0.0.1" "IIGO" -.SH NAME -ii-gemini - generate gemini data -.SH SYNOPSIS -.B ii-gemini -[options] command [arguments] -.SH DESCRIPTION -Generate gemini data from idec db. -.SH OPTIONS -.TP --data <path> gemini -generate gemini data -.TP --db=<path> -database path -.SH SEE ALSO -ii-tool(1), ii-node(1), iigo(1) -.SH AUTHOR -hugeping ( gl00my (at) mail.ru ) diff --git a/man/ii-node.1 b/man/ii-node.1 @@ -1,79 +0,0 @@ -.TH ii-node 1 "March 12, 2023" "version 0.0.1" "IIGO" -.SH NAME -ii-node - is idec node -.SH SYNOPSIS -.B ii-node -[options] -.SH DESCRIPTION -For run ii/idec node. -.SH OPTIONS -.TP --L -Listen address, default is :8080 -.TP --db <path> -Database, "db" by default -.TP --e list -Echos list file. This file needs only for descriptions and must be in list.txt format, where 2nd colum is ignored. When this file is exists, points can not create they own echos. (list.txt by default) -.TP --host <string> -Host string for node. (http://127.0.0.1:8080 by default) -.TP --sys "name" -Node name. "ii-go" by default -.TP --u <points> -Points file. "points.txt" by default. -.TP --v -Be verbose (for tracing) -.SH STANDARTS SUPPORTED -.TP -.BI u/e -.TP -.BI list.txt -.TP -.BI x/c -.TP -.BI blacklist.txt -.TP -.BI m/ -.TP -.BI e/ -.SH LIMITATIONS -Size of message can not be greater then 65536 bytes (before encoded into base64). -.SH WEB INTERFACE -User with id 1 (first created user) is admin. -.PP -Admin can create new echoes with: http://127.0.0.1:8080/new -.PP -Another hiden feature, is blacklisting: http://127.0.0.1:8080/msgid/blacklist -.PP -.TP -Web interface supports some non-standart features in message body text: -.nf -@spolier: spoiler -@base64: name (base64 data from next line till end of message) -xpm2 and xpm3 images embedding -.fi -.SH RSS -http://127.0.0.1:8080/echo/all/rss - All messages -.PP -http://127.0.0.1:8080/echo/echo.name/rss - By echo.name -.PP -http://127.0.0.1:8080/to/User/rss - For User -.PP -http://127.0.0.1:8080/from/User/rss - From User -.SH EXAMPLES -Fetch db and start node: -.nf - -ii-tool fetch http://hugeping.tk # get database from remote node -ftp http://hugeping.tk/list.txt # for echo descriptions -ii-node -sys "newnode" # run node with name "newnode" -.fi -.SH SEE ALSO -ii-tool(1), iigo(1), ii-gemini(1) -.SH AUTHOR -hugeping ( gl00my (at) mail.ru ) -\ No newline at end of file diff --git a/man/ii-tool.1 b/man/ii-tool.1 @@ -1,120 +0,0 @@ -.TH ii-tool 1 "March 12, 2023" "version 0.0.1" "IIGO" -.SH NAME -.B ii-tool -.SH SYNOPSIS -.B ii-tool -[options] command [arguments] -.SH DESCRIPTION -.B ii-tool -can be used to fetch messages from another node and maintaince database. -.SH OPTIONS -.TP -.B FETCH MESSAGES -.B ii-tool -[options] -.B fetch -[uri] [echolist] -.nf - --db <database> - db by default (db.idx \- genetated index) - --lim=<n> - fetch mode, if omitted full sync will be performed if needed - if n > 0 - last n messages synced - if n < 0 - adaptive fetching with step n will be performed - --f - do not check last message, perform sync even it is not needed - -echolist is the file with echonames (can has : splitted columns, like list.txt) -or '-' to load it from stdin - -If echolist is omitted, fetcher will try to get all echos. -It uses list.txt extension of IDEC if target node supports it. -.fi -.TP -.B CREATE INDEX -.B ii-tool -index -.nf -Index file (db.idx by default) is created when needed. -If you want force to recreate it, use: ii-tool index -.fi -.TP -.B STORE BUNDLE INTO DB -.B ii-tool -[options] -.B store -[DB] -.nf - -You can merge records from DB to db with store command. - --db <database> - db to store/merge in - db - is file with bundles or '-' for stdin. - -DB is just msgid:message bundles in base64 stored in text file. -.fi -.TP -.B SHOW MESSAGES -.nf -Select messages: - ii-tool [options] select <echo.name> [slice] - - slice is the start:limit - -Messages are identificated by unique message ids (MsgId). -It is the first column in bundle: <msgid>:<message> - -Show selected message: - ii-tool [options] get <MsgId> - -Search message: - ii-tool [options] search <string> [echo] - -.B options: - -from <user> -- from user - -to <user> -- to user - -t -- only topics (w/o repto) - -db <database> -- db by default (db.idx - genetated index) - -v -- show message text, not only MsgId - -.fi -.TP -.B ADD USER (POINT) -ii-tool [-u pointfile] useradd <name> <e-mail> <password> -.nf - -By default, pointfile is points.txt -.fi -.TP -.B BLACKLIST MSG -ii-tool [-u pointfile] useradd <name> <e-mail> <password> -.nf - -Blacklist is just new record with same id but spectial status. -.fi -.SH EXAMPLES -.TP Get database from remote node and store to ./db -ii-tool fetch http://hugeping.tk -.TP -Fetch messages -echo "std.club:this comment will be omitted" | ii-tool fetch http://127.0.0.1:8080 - -.TP -get last message -ii-tool select std.club -1:1 -.TP -get first 10 messages -ii-tool select std.club 0:10 -.TP -To show last 5 messages adressed to selected user (sort ids by date with sort command) -ii-tool [options] -to <user> select "" | ii-tool sort | tail -n5 | ii-tool -v sort -.TP -Show and print last message to Peter -ii-tool -v -to Peter "" -1:1 -.SH SEE ALSO -iigo(1), ii-node(1), ii-gemini(1) -.SH AUTHOR -hugeping ( gl00my (at) mail.ru ) -\ No newline at end of file diff --git a/man/iigo.1 b/man/iigo.1 @@ -1,23 +0,0 @@ -.TH iigo 1 "March 12, 2023" "version 0.0.1" "IIGO" -.SH NAME -iigo \- is idec node and tools -.SH SYNOPSIS -.B ii-tool, ii-node, ii-gemini -.SH DESCRIPTION -Easy setup and make your own ii/idec node. -.PP -.B ii-tool -\- can be used to fetch messages from another node and maintaince database. -.PP -.B ii-node -\- runing idec node with web interface -.PP -.B ii-gemini -\- generate gemini data -.SH SEE ALSO -ii-tool(1), ii-node(1), ii-gemini(1) -.SH HISTORY -needed!!! -https://github.com/idec-net/new-docs/blob/master/main.md -.SH AUTHOR -hugeping ( gl00my (at) mail.ru ) diff --git a/man/openidec.1 b/man/openidec.1 @@ -0,0 +1,23 @@ +.TH openidec 1 "March 12, 2023" "version 0.0.1" "OpenIDEC" +.SH NAME +openidec \- is idec node and tools +.SH SYNOPSIS +.B idecctl, idecd, idecgmi +.SH DESCRIPTION +Easy setup and make your own ii/idec node. +.PP +.B idecctl +\- can be used to fetch messages from another node and maintaince database. +.PP +.B idecd +\- runing idec node with web interface +.PP +.B idecgmi +\- generate gemini data +.SH SEE ALSO +idecctl(1), idecd(1), idecgmi(1) +.SH HISTORY +needed!!! +https://github.com/idec-net/new-docs/blob/master/main.md +.SH AUTHOR +hugeping ( gl00my (at) mail.ru ) diff --git a/www/tpl/footer.tpl b/www/tpl/footer.tpl @@ -1,5 +1,6 @@ <div id="footer"> -Powered by <a href="https://github.com/hugeping/ii-go">ii-go</a> / 2021-2022 +Powered by <a href="https://git.openbsd.org.ru/vasyahacker/openidec">OpenIDEC</a> +Original by <a href="https://github.com/hugeping/ii-go">ii-go</a> / 2021-2023 </div> </div> </body>