Skip to content

Commit

Permalink
Add atom feed support.
Browse files Browse the repository at this point in the history
  • Loading branch information
robertabcd committed Mar 4, 2017
1 parent 445595c commit cdd4e78
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 5 deletions.
82 changes: 82 additions & 0 deletions atomfeed/atomfeed.go
@@ -0,0 +1,82 @@
package atomfeed

import (
"bytes"
"text/template"
"time"

"github.com/ptt/pttweb/pttbbs"
"golang.org/x/tools/blog/atom"
)

type Converter struct {
FeedTitleTemplate *template.Template
LinkFeed func(brdname string) (string, error)
LinkArticle func(brdname, filename string) (string, error)
}

func (c *Converter) Convert(board pttbbs.Board, articles []pttbbs.Article) (*atom.Feed, error) {
var title bytes.Buffer
if err := c.FeedTitleTemplate.Execute(&title, board); err != nil {
return nil, err
}

feedURL, err := c.LinkFeed(board.BrdName)
if err != nil {
return nil, err
}

var entries []*atom.Entry
// Reverse (time) order.
for i := len(articles) - 1; i >= 0; i-- {
entry, err := c.convertArticle(articles[i], board.BrdName)
if err != nil {
// Ignore errors.
continue
}
entries = append(entries, entry)
}

return &atom.Feed{
Title: title.String(),
ID: feedURL,
Link: []atom.Link{{
Rel: "self",
Href: feedURL,
}},
Updated: atom.Time(firstArticleTimeOrNow(articles)),
Entry: entries,
}, nil
}

func (c *Converter) convertArticle(a pttbbs.Article, brdname string) (*atom.Entry, error) {
articleURL, err := c.LinkArticle(brdname, a.FileName)
if err != nil {
return nil, err
}
// Will use a zero time if unable to parse.
published, _ := pttbbs.ParseFileNameTime(a.FileName)
return &atom.Entry{
Author: &atom.Person{
Name: a.Owner,
},
Title: a.Title,
ID: articleURL,
Link: []atom.Link{{
Rel: "alternate",
Type: "text/html",
Href: articleURL,
}},
Published: atom.Time(published),
Updated: atom.Time(published), // TODO: support this in boardd.
}, nil
}

func firstArticleTimeOrNow(articles []pttbbs.Article) time.Time {
for _, a := range articles {
if t, err := pttbbs.ParseFileNameTime(a.FileName); err == nil {
return t
}
}
return time.Now()
}
34 changes: 34 additions & 0 deletions cached_ops.go
@@ -1,7 +1,9 @@
package main

import (
"errors"
"fmt"
"log"

"github.com/ptt/pttweb/article"
"github.com/ptt/pttweb/cache"
Expand Down Expand Up @@ -76,6 +78,38 @@ func generateBbsIndex(key cache.Key) (cache.Cacheable, error) {
return bbsindex, nil
}

type BoardAtomFeedRequest struct {
Brd pttbbs.Board
}

func (r *BoardAtomFeedRequest) String() string {
return fmt.Sprintf("pttweb:atomfeed/%v", r.Brd.BrdName)
}

func generateBoardAtomFeed(key cache.Key) (cache.Cacheable, error) {
r := key.(*BoardAtomFeedRequest)

if atomConverter == nil {
return nil, errors.New("atom feed not configured")
}

// Fetch article list
articles, err := ptt.GetArticleList(r.Brd.Bid, -20)
if err != nil {
return nil, err
}

feed, err := atomConverter.Convert(r.Brd, articles)
if err != nil {
log.Println("atomfeed: Convert:", err)
// Don't return error but cache that it's invalid.
}
return &BoardAtomFeed{
Feed: feed,
IsValid: err == nil,
}, nil
}

const (
TruncateSize = 1048576
TruncateMaxScan = 1024
Expand Down
8 changes: 5 additions & 3 deletions config.go
@@ -1,8 +1,6 @@
package main

import (
"errors"
)
import "errors"

type PttwebConfig struct {
Bind []string
Expand All @@ -11,6 +9,7 @@ type PttwebConfig struct {
MemcachedAddress string
TemplateDirectory string
StaticPrefix string
SitePrefix string

BoarddMaxConn int
MemcachedMaxConn int
Expand All @@ -19,6 +18,9 @@ type PttwebConfig struct {
GADomain string

EnableOver18Cookie bool

FeedPrefix string
AtomFeedTitleTemplate string
}

const (
Expand Down
16 changes: 16 additions & 0 deletions pttbbs/string.go
Expand Up @@ -2,7 +2,10 @@ package pttbbs

import (
"bytes"
"errors"
"regexp"
"strconv"
"time"
)

var (
Expand All @@ -23,6 +26,7 @@ const (
var (
validBrdNameRegexp = regexp.MustCompile(`^[0-9a-zA-Z][0-9a-zA-Z_\.\-]+$`)
validFileNameRegexp = regexp.MustCompile(`^[MG]\.\d+\.A(\.[0-9A-F]+)?$`)
fileNameTimeRegexp = regexp.MustCompile(`^[MG]\.(\d+)\.A(\.[0-9A-F]+)?$`)
)

func IsValidBrdName(brdname string) bool {
Expand Down Expand Up @@ -63,3 +67,15 @@ func MatchPrefixBytesToStrings(str []byte, patts []string) bool {
}
return false
}

func ParseFileNameTime(filename string) (time.Time, error) {
m := fileNameTimeRegexp.FindStringSubmatch(filename)
if len(m) == 0 {
return time.Time{}, errors.New("invalid filename pattern")
}
unix, err := strconv.ParseUint(m[1], 10, 64)
if err != nil {
return time.Time{}, err
}
return time.Unix(int64(unix), 0), nil
}
49 changes: 49 additions & 0 deletions pttweb.go
Expand Up @@ -2,6 +2,7 @@ package main

import (
"encoding/json"
"encoding/xml"
"errors"
"flag"
"fmt"
Expand All @@ -23,6 +24,7 @@ import (

"golang.org/x/net/context"

"github.com/ptt/pttweb/atomfeed"
"github.com/ptt/pttweb/cache"
"github.com/ptt/pttweb/page"
bbspb "github.com/ptt/pttweb/proto"
Expand All @@ -45,6 +47,7 @@ var ptt pttbbs.Pttbbs
var mand bbspb.ManServiceClient
var router *mux.Router
var cacheMgr *cache.CacheManager
var atomConverter *atomfeed.Converter

var configPath string
var config PttwebConfig
Expand Down Expand Up @@ -87,6 +90,21 @@ func main() {
// Init cache manager
cacheMgr = cache.NewCacheManager(config.MemcachedAddress, config.MemcachedMaxConn)

// Init atom converter.
atomConverter = &atomfeed.Converter{
FeedTitleTemplate: template.Must(template.New("").Parse(config.AtomFeedTitleTemplate)),
LinkFeed: func(brdname string) (string, error) {
return config.FeedPrefix + "/" + brdname + ".xml", nil
},
LinkArticle: func(brdname, filename string) (string, error) {
u, err := router.Get("bbsarticle").URLPath("brdname", brdname, "filename", filename)
if err != nil {
return "", err
}
return config.SitePrefix + u.String(), nil
},
}

// Load templates
if err := page.LoadTemplates(config.TemplateDirectory, templateFuncMap()); err != nil {
log.Fatal("cannot load templates:", err)
Expand Down Expand Up @@ -133,6 +151,7 @@ func createRouter() *mux.Router {
router.HandleFunc(`/bbs/{brdname:[A-Za-z][0-9a-zA-Z_\.\-]+}{x:/?}`, errorWrapperHandler(handleBbsIndexRedirect))
router.HandleFunc(`/bbs/{brdname:[A-Za-z][0-9a-zA-Z_\.\-]+}/index.html`, errorWrapperHandler(handleBbs)).Name("bbsindex")
router.HandleFunc(`/bbs/{brdname:[A-Za-z][0-9a-zA-Z_\.\-]+}/index{page:\d+}.html`, errorWrapperHandler(handleBbs)).Name("bbsindex_page")
router.HandleFunc(`/atom/{brdname:[A-Za-z][0-9a-zA-Z_\.\-]+}.xml`, errorWrapperHandler(handleBoardAtomFeed))
router.HandleFunc(`/bbs/{brdname:[A-Za-z][0-9a-zA-Z_\.\-]+}/{filename:[MG]\.\d+\.A(\.[0-9A-F]+)?}.html`, errorWrapperHandler(handleArticle)).Name("bbsarticle")
router.HandleFunc(`/b/{brdname:[A-Za-z][0-9a-zA-Z_\.\-]+}/{aidc:[0-9A-Za-z\-_]+}`, errorWrapperHandler(handleAidc)).Name("bbsaidc")
router.HandleFunc(`/ask/over18`, errorWrapperHandler(handleAskOver18)).Name("askover18")
Expand Down Expand Up @@ -379,6 +398,36 @@ func handleBbs(c *Context, w http.ResponseWriter) error {
return page.ExecutePage(w, (*page.BbsIndex)(bbsindex))
}

func handleBoardAtomFeed(c *Context, w http.ResponseWriter) error {
vars := mux.Vars(c.R)
brdname := vars["brdname"]

timeout := BbsIndexLastPageCacheTimeout

brd, err := getBoardByName(c, brdname)
if err != nil {
return err
}

obj, err := cacheMgr.Get(&BoardAtomFeedRequest{
Brd: *brd,
}, ZeroBoardAtomFeed, timeout, generateBoardAtomFeed)
if err != nil {
return err
}
baf := obj.(*BoardAtomFeed)

if !baf.IsValid {
return NewNotFoundError(fmt.Errorf("not a valid cache.BoardAtomFeed: %v", brd.BrdName))
}

w.Header().Set("Content-Type", "application/xml")
if _, err = w.Write([]byte(xml.Header)); err != nil {
return err
}
return xml.NewEncoder(w).Encode(baf.Feed)
}

func handleArticle(c *Context, w http.ResponseWriter) error {
vars := mux.Vars(c.R)
brdname := vars["brdname"]
Expand Down
22 changes: 20 additions & 2 deletions struct.go
Expand Up @@ -4,14 +4,17 @@ import (
"bytes"
"encoding/gob"

"golang.org/x/tools/blog/atom"

"github.com/ptt/pttweb/cache"
"github.com/ptt/pttweb/page"
)

// Useful when calling |NewFromBytes|
var (
ZeroArticle *Article
ZeroBbsIndex *BbsIndex
ZeroArticle *Article
ZeroBbsIndex *BbsIndex
ZeroBoardAtomFeed *BoardAtomFeed
)

func gobEncodeBytes(obj interface{}) ([]byte, error) {
Expand Down Expand Up @@ -63,13 +66,28 @@ func (bi *BbsIndex) EncodeToBytes() ([]byte, error) {
return gobEncodeBytes(bi)
}

type BoardAtomFeed struct {
Feed *atom.Feed
IsValid bool
}

func (_ *BoardAtomFeed) NewFromBytes(data []byte) (cache.Cacheable, error) {
return gobDecodeCacheable(data, new(BoardAtomFeed))
}

func (bi *BoardAtomFeed) EncodeToBytes() ([]byte, error) {
return gobEncodeBytes(bi)
}

func init() {
gob.Register(Article{})
gob.Register(BbsIndex{})
gob.Register(BoardAtomFeed{})

// Make sure they are |Cacheable|
checkCacheable(new(Article))
checkCacheable(new(BbsIndex))
checkCacheable(new(BoardAtomFeed))
}

func checkCacheable(c cache.Cacheable) {
Expand Down

0 comments on commit cdd4e78

Please sign in to comment.