commit de59b21200a5aa169b2dc799bca29b8f75c8e231 from: Oliver Lowe <o@olowe.co> date: Thu Mar 28 01:16:32 2024 UTC nntp: remove couchserver example No external deps commit - f00d51cf8cc1331170bf6c84bfa23bbe7e8f0e98 commit + de59b21200a5aa169b2dc799bca29b8f75c8e231 blob - d606a708d25244cbad788ab9b159bb707cc35ea1 (mode 644) blob + /dev/null --- examples/couchserver/couchserver.go +++ /dev/null @@ -1,344 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "flag" - "fmt" - "io" - "log" - "log/syslog" - "net" - "net/textproto" - "net/url" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/dustin/go-nntp" - "github.com/dustin/go-nntp/server" - - "github.com/dustin/go-couch" -) - -var groupCacheTimeout = flag.Int("groupTimeout", 300, - "Time (in seconds), group cache is valid") -var optimisticPost = flag.Bool("optimistic", false, - "Optimistically return success on store before storing") -var useSyslog = flag.Bool("syslog", false, - "Log to syslog") - -type groupRow struct { - Group string `json:"key"` - Value []interface{} `json:"value"` -} - -type groupResults struct { - Rows []groupRow -} - -type attachment struct { - Type string `json:"content-type"` - Data []byte `json:"data"` -} - -func removeSpace(r rune) rune { - if r == ' ' || r == '\n' || r == '\r' { - return -1 - } - return r -} - -func (a *attachment) MarshalJSON() ([]byte, error) { - m := map[string]string{ - "content_type": a.Type, - "data": strings.Map(removeSpace, base64.StdEncoding.EncodeToString(a.Data)), - } - return json.Marshal(m) -} - -type article struct { - MsgID string `json:"_id"` - DocType string `json:"type"` - Headers map[string][]string `json:"headers"` - Bytes int `json:"bytes"` - Lines int `json:"lines"` - Nums map[string]int64 `json:"nums"` - Attachments map[string]*attachment `json:"_attachments"` - Added time.Time `json:"added"` -} - -type articleResults struct { - Rows []struct { - Key []interface{} `json:"key"` - Article article `json:"doc"` - } -} - -type couchBackend struct { - db *couch.Database - groups map[string]*nntp.Group - grouplock sync.Mutex -} - -func (cb *couchBackend) clearGroups() { - cb.grouplock.Lock() - defer cb.grouplock.Unlock() - - log.Printf("Dumping group cache") - cb.groups = nil -} - -func (cb *couchBackend) fetchGroups() error { - cb.grouplock.Lock() - defer cb.grouplock.Unlock() - - if cb.groups != nil { - return nil - } - - log.Printf("Filling group cache") - - results := groupResults{} - err := cb.db.Query("_design/groups/_view/active", map[string]interface{}{ - "group": true, - }, &results) - if err != nil { - return err - } - cb.groups = make(map[string]*nntp.Group) - for _, gr := range results.Rows { - if gr.Value[0].(string) != "" { - group := nntp.Group{ - Name: gr.Group, - Description: gr.Value[0].(string), - Count: int64(gr.Value[1].(float64)), - Low: int64(gr.Value[2].(float64)), - High: int64(gr.Value[3].(float64)), - Posting: nntp.PostingPermitted, - } - cb.groups[group.Name] = &group - } - } - - go func() { - time.Sleep(time.Duration(*groupCacheTimeout) * time.Second) - cb.clearGroups() - }() - - return nil -} - -func (cb *couchBackend) ListGroups(max int) ([]*nntp.Group, error) { - if cb.groups == nil { - if err := cb.fetchGroups(); err != nil { - return nil, err - } - } - rv := make([]*nntp.Group, 0, len(cb.groups)) - for _, g := range cb.groups { - rv = append(rv, g) - } - return rv, nil -} - -func (cb *couchBackend) GetGroup(name string) (*nntp.Group, error) { - if cb.groups == nil { - if err := cb.fetchGroups(); err != nil { - return nil, err - } - } - g, exists := cb.groups[name] - if !exists { - return nil, nntpserver.ErrNoSuchGroup - } - return g, nil -} - -func (cb *couchBackend) mkArticle(ar article) *nntp.Article { - url := fmt.Sprintf("%s/%s/article", cb.db.DBURL(), cleanupID(ar.MsgID, true)) - return &nntp.Article{ - Header: textproto.MIMEHeader(ar.Headers), - Body: &lazyOpener{url, nil, nil}, - Bytes: ar.Bytes, - Lines: ar.Lines, - } -} - -func (cb *couchBackend) GetArticle(group *nntp.Group, id string) (*nntp.Article, error) { - var ar article - if intid, err := strconv.ParseInt(id, 10, 64); err == nil { - results := articleResults{} - cb.db.Query("_design/articles/_view/list", map[string]interface{}{ - "include_docs": true, - "reduce": false, - "key": []interface{}{group.Name, intid}, - }, &results) - - if len(results.Rows) != 1 { - return nil, nntpserver.ErrInvalidArticleNumber - } - - ar = results.Rows[0].Article - } else { - err := cb.db.Retrieve(cleanupID(id, false), &ar) - if err != nil { - return nil, nntpserver.ErrInvalidMessageID - } - } - - return cb.mkArticle(ar), nil -} - -func (cb *couchBackend) GetArticles(group *nntp.Group, - from, to int64) ([]nntpserver.NumberedArticle, error) { - - rv := make([]nntpserver.NumberedArticle, 0, 100) - - results := articleResults{} - cb.db.Query("_design/articles/_view/list", map[string]interface{}{ - "include_docs": true, - "reduce": false, - "start_key": []interface{}{group.Name, from}, - "end_key": []interface{}{group.Name, to}, - }, &results) - - for _, r := range results.Rows { - rv = append(rv, nntpserver.NumberedArticle{ - Num: int64(r.Key[1].(float64)), - Article: cb.mkArticle(r.Article), - }) - } - - return rv, nil -} - -func (cb *couchBackend) AllowPost() bool { - return true -} - -func cleanupID(msgid string, escapedAt bool) string { - s := strings.TrimFunc(msgid, func(r rune) bool { - return r == ' ' || r == '<' || r == '>' - }) - qe := url.QueryEscape(s) - if escapedAt { - return qe - } - return strings.Replace(qe, "%40", "@", -1) -} - -func (cb *couchBackend) Post(art *nntp.Article) error { - a := article{ - DocType: "article", - Headers: map[string][]string(art.Header), - Nums: make(map[string]int64), - MsgID: cleanupID(art.Header.Get("Message-Id"), false), - Attachments: make(map[string]*attachment), - Added: time.Now(), - } - - b := []byte{} - buf := bytes.NewBuffer(b) - n, err := io.Copy(buf, art.Body) - if err != nil { - return err - } - log.Printf("Read %d bytes of body", n) - - b = buf.Bytes() - a.Bytes = len(b) - a.Lines = bytes.Count(b, []byte{'\n'}) - - a.Attachments["article"] = &attachment{"text/plain", b} - - for _, g := range strings.Split(art.Header.Get("Newsgroups"), ",") { - g = strings.TrimSpace(g) - group, err := cb.GetGroup(g) - if err == nil { - a.Nums[g] = atomic.AddInt64(&group.High, 1) - atomic.AddInt64(&group.Count, 1) - } else { - log.Printf("Error getting group %q: %v", g, err) - } - } - - if len(a.Nums) == 0 { - log.Printf("Found no matching groups in %v", - art.Header["Newsgroups"]) - return nntpserver.ErrPostingFailed - } - - if *optimisticPost { - go func() { - _, _, err = cb.db.Insert(&a) - if err != nil { - log.Printf("error optimistically posting article: %v", err) - } - }() - } else { - _, _, err = cb.db.Insert(&a) - if err != nil { - log.Printf("error posting article: %v", err) - return nntpserver.ErrPostingFailed - } - } - - return nil -} - -func (cb *couchBackend) Authorized() bool { - return true -} - -func (cb *couchBackend) Authenticate(user, pass string) (nntpserver.Backend, error) { - return nil, nntpserver.ErrAuthRejected -} - -func maybefatal(err error, f string, a ...interface{}) { - if err != nil { - log.Fatalf(f, a...) - } -} - -func main() { - couchURL := flag.String("couch", "http://localhost:5984/news", - "Couch DB.") - - flag.Parse() - - if *useSyslog { - sl, err := syslog.New(syslog.LOG_INFO, "nntpd") - if err != nil { - log.Fatalf("Error initializing syslog: %v", err) - } - log.SetOutput(sl) - log.SetFlags(0) - } - - a, err := net.ResolveTCPAddr("tcp", ":1119") - maybefatal(err, "Error resolving listener: %v", err) - l, err := net.ListenTCP("tcp", a) - maybefatal(err, "Error setting up listener: %v", err) - defer l.Close() - - db, err := couch.Connect(*couchURL) - maybefatal(err, "Can't connect to the couch: %v", err) - err = ensureViews(&db) - maybefatal(err, "Error setting up views: %v", err) - - backend := couchBackend{ - db: &db, - } - - s := nntpserver.NewServer(&backend) - - for { - c, err := l.AcceptTCP() - maybefatal(err, "Error accepting connection: %v", err) - go s.Process(c) - } -} blob - 86da305cfcfa3d3637e329d51112998dd7a19366 (mode 644) blob + /dev/null --- examples/couchserver/encoder_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "encoding/json" - "testing" -) - -func TestJSONMarshalling(t *testing.T) { - a := attachment{ - "application/octet-stream", - []byte("some bytes"), - } - b, err := json.Marshal(&a) - if err != nil { - t.Fatalf("Error marshalling attachment: %v", err) - } - t.Logf("Marshalled to %v", string(b)) -} blob - d839bb686c590a66d3c8f79166920285e99e01a5 (mode 644) blob + /dev/null --- examples/couchserver/groupcreator/creator.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "bufio" - "flag" - "io" - "log" - "os" - "strings" - "sync" - - "github.com/dustin/go-couch" -) - -var wg sync.WaitGroup - -type agroup struct { - Type string `json:"type"` - Name string `json:"_id"` - Description string `json:"description"` -} - -func process(db *couch.Database, line string) { - defer wg.Done() - parts := strings.SplitN(strings.TrimSpace(line), " ", 2) - if len(parts) != 2 { - log.Printf("Error parsing %v", line) - return - } - log.Printf("Processing %#v", parts) - g := agroup{ - Type: "group", - Name: parts[0], - Description: parts[1], - } - _, _, err := db.Insert(g) - if err != nil { - log.Printf("Error saving %#v: %v", g, err) - } -} - -func main() { - - couchURL := flag.String("couch", "http://localhost:5984/news", - "Couch DB.") - flag.Parse() - - db, err := couch.Connect(*couchURL) - if err != nil { - log.Fatalf("Can't connect to couch: %v", err) - } - - br := bufio.NewReader(os.Stdin) - for { - line, err := br.ReadString('\n') - if err == io.EOF { - break - } - if err != nil { - log.Fatalf("Error reading line: %v", err) - } - - wg.Add(1) - go process(&db, line) - } - - wg.Wait() -} blob - 7175ee798dbd54b70bc817fef65cd8b4c13901db (mode 644) blob + /dev/null --- examples/couchserver/lazyopen.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "errors" - "io" - "io/ioutil" - "net/http" -) - -type lazyOpener struct { - url string - data []byte - err error -} - -func (l *lazyOpener) init() { - res, err := http.Get(l.url) - l.err = err - if err == nil { - defer res.Body.Close() - if res.StatusCode == 200 { - l.data, l.err = ioutil.ReadAll(res.Body) - } else { - l.err = errors.New(res.Status) - } - } -} - -func (l *lazyOpener) Read(p []byte) (n int, err error) { - if l.data == nil && l.err == nil { - l.init() - } - if l.err != nil { - return 0, err - } - if len(l.data) == 0 { - return 0, io.EOF - } - copied := copy(p, l.data) - l.data = l.data[copied:] - return copied, nil -} blob - 6f007215f3a724c804786198deafb0e49b293ed8 (mode 644) blob + /dev/null --- examples/couchserver/views.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "log" - "net/http" - "strings" - - "github.com/dustin/go-couch" -) - -const groupsjson = `{ - "_id": "_design/groups", - "language": "javascript", - "views": { - "discovered": { - "map": "function(doc) {\n if (doc.type === \"group\") {\n emit(doc._id, [doc.description, 0, 0, 0]);\n } else if (doc.type === \"article\") {\n var groups = doc.headers[\"Newsgroups\"][0].split(\",\")\n for (var i = 0; i < groups.length; i++) {\n var g = groups[i].replace(/\\s+/g, '');\n emit(g, [\"\", 1, doc.nums[g], doc.nums[g]]);\n }\n }\n}\n", - "reduce": "function (key, values) {\n var result = [\"\", 0, 0, 0];\n\n values.forEach(function(p) {\n if (p[0].length > result[0].length) {\n result[0] = p[0];\n }\n result[1] += p[1];\n\tresult[2] = Math.min(result[2], p[2]);\n\tresult[3] = Math.max(result[3], p[3]);\n // Dumb special case\n if (result[2] === 0 && result[1] != 0) {\n result[2] = 1;\n }\n });\n\n return result;\n}" - }, - "active": { - "map": "function(doc) {\n if (doc.type === \"group\") {\n emit(doc._id, [doc.description, 0, 0, 0]);\n } else if (doc.type === \"article\") {\n for (var g in doc.nums) {\n emit(g, [\"\", 1, doc.nums[g], doc.nums[g]]);\n }\n }\n}\n", - "reduce": "function (key, values) {\n var result = [\"\", 0, 0, 0];\n\n values.forEach(function(p) {\n if (p[0].length > result[0].length) {\n result[0] = p[0];\n }\n result[1] += p[1];\n\tresult[2] = Math.min(result[2], p[2]);\n\tresult[3] = Math.max(result[3], p[3]);\n // Dumb special case\n if (result[2] === 0 && result[1] != 0) {\n result[2] = 1;\n }\n });\n\n return result;\n}" - } - } -}` - -const articlesjson = `{ - "_id": "_design/articles", - "language": "javascript", - "views": { - "list": { - "map": "function(doc) {\n if (doc.type === \"article\") {\n for (var g in doc.nums) {\n emit([g, doc.nums[g]], null);\n }\n }\n}\n", - "reduce": "_count" - } - } -}` - -func viewUpdateOK(i int) bool { - return i == 200 || i == 409 -} - -func updateView(db *couch.Database, viewdata string) error { - r, err := http.Post(db.DBURL(), "application/json", strings.NewReader(viewdata)) - if r == nil { - defer r.Body.Close() - } else { - return err - } - if !viewUpdateOK(r.StatusCode) { - return fmt.Errorf("error updating view: %v", r.Status) - } - return nil -} - -func ensureViews(db *couch.Database) error { - errg := updateView(db, groupsjson) - if errg != nil { - log.Printf("Error creating groups view %v", errg) - } - - erra := updateView(db, articlesjson) - if erra != nil { - log.Printf("Error creating articles view %v", erra) - } - - if erra != nil || errg != nil { - return errors.New("error making views") - } - - return nil -} blob - d362d8e50a008d020dc4a07c259cd0c16c4781c0 blob + 958970b38ad3ef4603fdd61e0e977eb5a14bd3f8 --- go.mod +++ go.mod @@ -1,8 +1,3 @@ module github.com/dustin/go-nntp go 1.16 - -require ( - github.com/dustin/go-couch v0.0.0-20160816170231-8251128dab73 - github.com/dustin/httputil v0.0.0-20170305193905-c47743f54f89 // indirect -) blob - 7add7f12513a6f787e71ae7387e487b7b3af1428 blob + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 --- go.sum +++ go.sum @@ -1,4 +0,0 @@ -github.com/dustin/go-couch v0.0.0-20160816170231-8251128dab73 h1:YKyWSyEhJ3DYKgSpjOXpQgpxD3N+1EfIanJZj1ZEhpM= -github.com/dustin/go-couch v0.0.0-20160816170231-8251128dab73/go.mod h1:WG/TWzFd/MRvOZ4jjna3FQ+K8AKhb2jOw4S2JMw9VKI= -github.com/dustin/httputil v0.0.0-20170305193905-c47743f54f89 h1:A740DRjmFFdm3+GeYVfs4QN/QMOAbMw8KdsZMDhUCjQ= -github.com/dustin/httputil v0.0.0-20170305193905-c47743f54f89/go.mod h1:ZoDWdnxro8Kesk3zrCNOHNFWtajFPSnDMjVEjGjQu/0=