文章
站务

rss 不输出树洞、水区和垃圾场内容

https://github.com/Terminus2049/2049bbs/issues/7

修改代码路径 https://github.com/Terminus2049/2049bbs/blob/master/controller/feed.go

package controller



import (

	"net/http"

	"strconv"

	"text/template"



	"github.com/terminus2049/2049bbs/model"

	"goji.io/pat"

)



func (h *BaseHandler) FeedHandler(w http.ResponseWriter, r *http.Request) {

	w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")



	scf := h.App.Cf.Site



	var feed = `<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom">

	<title>` + scf.Name + `</title>

	<link rel="self" type="application/atom+xml" href="` + scf.MainDomain + `/feed"/>

	<link rel="hub" href="https://pubsubhubbub.appspot.com"/>

	<updated>{ {.Update} }</updated>

	<id>` + scf.MainDomain + `/feed</id>

	<author>

		<name>` + scf.Name + `</name>

	</author>

	{ {range $_, $item := .Items} }

	<entry>

		<title>{ {$item.Title} }</title>

		<id>` + scf.MainDomain + `/t/{ {$item.Id} }</id>

		<link rel="alternate" type="text/html" href="` + scf.MainDomain + `/t/{ {$item.Id} }" />

		<published>{ {$item.AddTimeFmt} }</published>

		<updated>{ {$item.EditTimeFmt} }</updated>

		<content type="text/plain">

		  { {$item.Cname} } - { {$item.Name} } - { {$item.Des} }

		</content>

    </entry>

	{ {end} }

</feed>

`



	db := h.App.Db



	items := model.ArticleFeedList(db, 20, h.App.Cf.Site.TimeZone)



	var upDate string

	if len(items) > 0 {

		upDate = items[0].AddTimeFmt

	}



	t := template.Must(template.New("feed").Parse(feed))

	t.Execute(w, struct {

		Update string

		Items  []model.ArticleFeedListItem

	}{

		Update: upDate,

		Items:  items,

	})

}



func (h *BaseHandler) FeedCategoryHandler(w http.ResponseWriter, r *http.Request) {

	w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")



	scf := h.App.Cf.Site



	var feed = `<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom">

	<title>` + scf.Name + `</title>

	<link rel="self" type="application/atom+xml" href="` + scf.MainDomain + `/feed"/>

	<link rel="hub" href="https://pubsubhubbub.appspot.com"/>

	<updated>{ {.Update} }</updated>

	<id>` + scf.MainDomain + `/feed</id>

	<author>

		<name>` + scf.Name + `</name>

	</author>

	{ {range $_, $item := .Items} }

	<entry>

		<title>{ {$item.Title} }</title>

		<id>` + scf.MainDomain + `/t/{ {$item.Id} }</id>

		<link rel="alternate" type="text/html" href="` + scf.MainDomain + `/t/{ {$item.Id} }" />

		<published>{ {$item.AddTimeFmt} }</published>

		<updated>{ {$item.EditTimeFmt} }</updated>

		<content type="text/plain">

		  { {$item.Cname} } - { {$item.Name} } - { {$item.Des} }

		</content>

    </entry>

	{ {end} }

</feed>

`



	cid := pat.Param(r, "cid")

	_, err := strconv.Atoi(cid)

	if err != nil {

		w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))

		return

	}

			if cid == 19 || cid == 20 {

		w.Write([]byte(`{"retcode":400,"retmsg":"cid is illeage"}`))

		return

					}

	db := h.App.Db



	items := model.ArticleFeedCategoryList(db, cid, 20, h.App.Cf.Site.TimeZone)



	var upDate string

	if len(items) > 0 {

		upDate = items[0].AddTimeFmt

	}



	t := template.Must(template.New("feed").Parse(feed))

	t.Execute(w, struct {

		Update string

		Items  []model.ArticleFeedListItem

	}{

		Update: upDate,

		Items:  items,

	})

}
菜单
  1. 翻车了   翻车新闻搬运工

    直接在 ArticleFeedList 里处理会更好。可以看下提交记录

    https://github.com/Terminus2049/2049bbs/commits/master

    我会把issue完善下,免得你做重复工作

  2. 张怀义  

    @翻车了 #1 我发不了帖子了!!!

    我觉得,feed那段代码,实在不敢恭维!

    feed下,走的是 ArticleFeedList

    feed节点下,走的是 ArticleFeedCategoryList

    我也想在ArticleFeedList里写,但是我觉得很多代码完全是重复而且冗余

    如果我来改,我是从route层就已经改掉了,后面的代码会精简很多

  3. 小二   默认开启批量屏蔽受限用户发言功能,可在设置中手动取消。

    @张怀义 #5 我也发现这个问题了,打开控制台,是能看到反馈的,一般是标题过长等原因,但是不知道为啥没有弹出来。这个是js的问题,i hate js。

  4. 小二   默认开启批量屏蔽受限用户发言功能,可在设置中手动取消。

    @张怀义 #2 你为啥发不了帖子?

    的确很冗余,当时只是为了快速解决问题嘛,在原有全部帖子输出的基础上再搞一个。但现在木已成舟,说啥都晚了。

  5. 张怀义  

    @小二 #3 不知道,点了“发表”,什么反应都没有。。

    算了,有空把那些代码都改了吧。我是手机,没法git clone完整的项目,所以我都是怎么改简单,怎么来的

  6. 张怀义  

    树洞、水区和垃圾场at无效

    https://github.com/Terminus2049/2049bbs/issues/8

    修改代码 https://github.com/Terminus2049/2049bbs/blob/master/controller/article.go

    package controller
    
    import (
    	"crypto/md5"
    	"encoding/hex"
    	"encoding/json"
    	"html/template"
    	"math/rand"
    	"net/http"
    	"strconv"
    	"strings"
    	"time"
    
    	"github.com/ego008/youdb"
    	"github.com/rs/xid"
    	"github.com/terminus2049/2049bbs/model"
    	"github.com/terminus2049/2049bbs/util"
    	"goji.io/pat"
    )
    
    func (h *BaseHandler) ArticleAdd(w http.ResponseWriter, r *http.Request) {
    	cid := pat.Param(r, "cid")
    	_, err := strconv.Atoi(cid)
    	if err != nil {
    		w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
    		return
    	}
    
    	currentUser, _ := h.CurrentUser(w, r)
    	if currentUser.Id == 0 {
    		w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
    		return
    	}
    	if currentUser.Flag < 5 {
    		var msg string
    		if currentUser.Flag == 1 {
    			msg = "注册验证中,等待管理员通过"
    		} else {
    			msg = "您已被禁用"
    		}
    		w.Write([]byte(`{"retcode":401,"retmsg":"` + msg + `"}`))
    		return
    	}
    
    	db := h.App.Db
    
    	cobj, err := model.CategoryGetById(db, cid)
    	if err != nil {
    		w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
    		return
    	}
    
    	type pageData struct {
    		PageData
    		Cobj      model.Category
    		MainNodes []model.CategoryMini
    	}
    
    	tpl := h.CurrentTpl(r)
    	evn := &pageData{}
    	evn.SiteCf = h.App.Cf.Site
    	evn.Title = "发表文章"
    	evn.IsMobile = tpl == "mobile"
    	evn.CurrentUser = currentUser
    	evn.ShowSideAd = true
    	evn.PageName = "article_add"
    
    	evn.Cobj = cobj
    	evn.MainNodes = model.CategoryGetMain(db, cobj)
    
    	h.SetCookie(w, "token", xid.New().String(), 1)
    	h.Render(w, tpl, evn, "layout.html", "articlecreate.html")
    }
    
    func (h *BaseHandler) ArticleAddPost(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    
    	token := h.GetCookie(r, "token")
    	if len(token) == 0 {
    		w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
    		return
    	}
    
    	currentUser, _ := h.CurrentUser(w, r)
    	if currentUser.Id == 0 {
    		w.Write([]byte(`{"retcode":401,"retmsg":"authored require"}`))
    		return
    	}
    	if currentUser.Flag < 5 {
    		w.Write([]byte(`{"retcode":403,"retmsg":"user flag err"}`))
    		return
    	}
    
    	type recForm struct {
    		Act     string `json:"act"`
    		Cid     uint64 `json:"cid"`
    		Title   string `json:"title"`
    		Content string `json:"content"`
    	}
    
    	decoder := json.NewDecoder(r.Body)
    	var rec recForm
    	err := decoder.Decode(&rec)
    	if err != nil {
    		w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
    		return
    	}
    	defer r.Body.Close()
    
    	rec.Title = strings.TrimSpace(rec.Title)
    	rec.Content = strings.TrimSpace(rec.Content)
    
    	db := h.App.Db
    	if rec.Act == "preview" {
    		tmp := struct {
    			normalRsp
    			Html string `json:"html"`
    		}{
    			normalRsp{200, ""},
    			util.ContentFmt(db, rec.Content),
    		}
    		json.NewEncoder(w).Encode(tmp)
    		return
    	}
    
    	// check title
    	hash := md5.Sum([]byte(rec.Title))
    	titleMd5 := hex.EncodeToString(hash[:])
    	if db.Hget("title_md5", []byte(titleMd5)).State == "ok" {
    		w.Write([]byte(`{"retcode":403,"retmsg":"title has existed"}`))
    		return
    	}
    
    	now := uint64(time.Now().UTC().Unix())
    	scf := h.App.Cf.Site
    
    	if currentUser.Flag < 99 && currentUser.LastPostTime > 0 {
    		if (now - currentUser.LastPostTime) < uint64(scf.PostInterval) {
    			w.Write([]byte(`{"retcode":403,"retmsg":"PostInterval limited"}`))
    			return
    		}
    	}
    
    	if rec.Cid == 0 || len(rec.Title) == 0 {
    		w.Write([]byte(`{"retcode":400,"retmsg":"missed args"}`))
    		return
    	}
    	if len(rec.Title) > scf.TitleMaxLen {
    		w.Write([]byte(`{"retcode":403,"retmsg":"TitleMaxLen limited"}`))
    		return
    	}
    	if len(rec.Content) > scf.ContentMaxLen {
    		w.Write([]byte(`{"retcode":403,"retmsg":"ContentMaxLen limited"}`))
    		return
    	}
    
    	newAid, _ := db.HnextSequence("article")
    	aobj := model.Article{
    		Id:       newAid,
    		Uid:      currentUser.Id,
    		Cid:      rec.Cid,
    		Title:    rec.Title,
    		Content:  rec.Content,
    		AddTime:  now,
    		EditTime: now,
    		ClientIp: "",
    	}
    
    	jb, _ := json.Marshal(aobj)
    	aidB := youdb.I2b(newAid)
    	db.Hset("article", aidB, jb)
    	// 总文章列表
    	ignorenodes := scf.NotHomeNodeIds
    	if len(ignorenodes) > 0 {
    		for _, node := range strings.Split(ignorenodes, ",") {
    			node, err := strconv.Atoi(node)
    			if err == nil && aobj.Cid != uint64(node) {
    				db.Zset("article_timeline", aidB, aobj.EditTime)
    			}
    		}
    	}
    
    	// 分类文章列表
    	db.Zset("category_article_timeline:"+strconv.FormatUint(aobj.Cid, 10), aidB, aobj.EditTime)
    	// 用户文章列表
    	db.Hset("user_article_timeline:"+strconv.FormatUint(aobj.Uid, 10), youdb.I2b(aobj.Id), []byte(""))
    	// 分类下文章数
    	db.Zincr("category_article_num", youdb.I2b(aobj.Cid), 1)
    
    	currentUser.LastPostTime = now
    	currentUser.Articles++
    
    	jb, _ = json.Marshal(currentUser)
    	db.Hset("user", youdb.I2b(aobj.Uid), jb)
    
    	// title md5
    	db.Hset("title_md5", []byte(titleMd5), aidB)
    
    	// send task work
    	// get tag from title
    	if scf.AutoGetTag {
    		db.Hset("task_to_get_tag", aidB, []byte(rec.Title))
    	}
    
    	// @ somebody in content
    if rec.Cid != 19 && rec.Cid != 20 {
    	sbs := util.GetMention(rec.Content,
    		[]string{currentUser.Name, strconv.FormatUint(currentUser.Id, 10)})
    
    	aid := strconv.FormatUint(newAid, 10)
    	for _, sb := range sbs {
    		var sbObj model.User
    		sbu, err := strconv.ParseUint(sb, 10, 64)
    		if err != nil {
    			// @ user name
    			sbObj, err = model.UserGetByName(db, strings.ToLower(sb))
    		} else {
    			// @ user id
    			sbObj, err = model.UserGetById(db, sbu)
    		}
    
    		if err == nil {
    			if len(sbObj.Notice) > 0 {
    				aidList := util.SliceUniqStr(strings.Split(aid+","+sbObj.Notice, ","))
    				if len(aidList) > 100 {
    					aidList = aidList[:100]
    				}
    				sbObj.Notice = strings.Join(aidList, ",")
    				sbObj.NoticeNum = len(aidList)
    			} else {
    				sbObj.Notice = aid
    				sbObj.NoticeNum = 1
    			}
    			jb, _ := json.Marshal(sbObj)
    			db.Hset("user", youdb.I2b(sbObj.Id), jb)
    		}
    }
    	}
    
    	h.DelCookie(w, "token")
    
    	tmp := struct {
    		normalRsp
    		Aid uint64 `json:"aid"`
    	}{
    		normalRsp{200, "ok"},
    		aobj.Id,
    	}
    	json.NewEncoder(w).Encode(tmp)
    }
    
    func (h *BaseHandler) ArticleHomeList(w http.ResponseWriter, r *http.Request) {
    	btn, key, score := r.FormValue("btn"), r.FormValue("key"), r.FormValue("score")
    	if len(key) > 0 {
    		_, err := strconv.ParseUint(key, 10, 64)
    		if err != nil {
    			w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
    			return
    		}
    	}
    	if len(score) > 0 {
    		_, err := strconv.ParseUint(score, 10, 64)
    		if err != nil {
    			w.Write([]byte(`{"retcode":400,"retmsg":"score type err"}`))
    			return
    		}
    	}
    
    	cmd := "zrscan"
    	if btn == "prev" {
    		cmd = "zscan"
    	}
    
    	db := h.App.Db
    	scf := h.App.Cf.Site
    	pageInfo := model.ArticleList(db, cmd, "article_timeline", key, score, scf.HomeShowNum*2, scf.TimeZone, scf.NotHomeNodeIds)
    	currentUser, _ := h.CurrentUser(w, r)
    
    	// 首页第二栏节点
    	if currentUser.Theme != "2" {
    		cid := scf.HomeNode
    		cobj, err := model.CategoryGetById(db, cid)
    		if err == nil {
    			cobj.Articles = db.Zget("category_article_num", youdb.I2b(cobj.Id)).Uint64()
    			pageInfo2 := model.ArticleList(db, cmd, "category_article_timeline:"+cid, key, score, scf.HomeShowNum, scf.TimeZone, "")
    			pageInfo.Items = append(pageInfo.Items, pageInfo2.Items...)
    		}
    	}
    
    	type siteInfo struct {
    		Days     int
    		UserNum  uint64
    		NodeNum  uint64
    		TagNum   uint64
    		PostNum  uint64
    		ReplyNum uint64
    	}
    
    	type pageData struct {
    		PageData
    		SiteInfo     siteInfo
    		PageInfo     model.ArticlePageInfo
    		Links        []model.Link
    		Announcement string
    		Proverb      template.HTML
    	}
    
    	si := siteInfo{}
    	rs := db.Hget("count", []byte("site_create_time"))
    	var siteCreateTime uint64
    	if rs.State == "ok" {
    		siteCreateTime = rs.Data[0].Uint64()
    	} else {
    		rs2 := db.Hscan("user", []byte(""), 1)
    		if rs2.State == "ok" {
    			user := model.User{}
    			json.Unmarshal(rs2.Data[1], &user)
    			siteCreateTime = user.RegTime
    		} else {
    			siteCreateTime = uint64(time.Now().UTC().Unix())
    		}
    		db.Hset("count", []byte("site_create_time"), youdb.I2b(siteCreateTime))
    	}
    	then := time.Unix(int64(siteCreateTime), 0)
    	diff := time.Now().UTC().Sub(then)
    	si.Days = int(diff.Hours()/24) + 1
    	si.UserNum = db.Hsequence("user")
    	si.NodeNum = db.Hsequence("category")
    	si.TagNum = db.Hsequence("tag")
    	si.PostNum = db.Hsequence("article")
    	si.ReplyNum = db.Hget("count", []byte("comment_num")).Uint64()
    
    	// fix
    	if si.NodeNum == 0 {
    		newCid, err2 := db.HnextSequence("category")
    		if err2 == nil {
    			cobj := model.Category{
    				Id:    newCid,
    				Name:  "默认分类",
    				About: "默认第一个分类",
    			}
    			jb, _ := json.Marshal(cobj)
    			db.Hset("category", youdb.I2b(cobj.Id), jb)
    			si.NodeNum = 1
    		}
    		// link
    		model.LinkSet(db, model.Link{
    			Name:  "youBBS",
    			Url:   "https://www.youbbs.org",
    			Score: 100,
    		})
    	}
    
    	tpl := h.CurrentTpl(r)
    	evn := &pageData{}
    	evn.SiteCf = scf
    	evn.Title = scf.Name
    	evn.Keywords = evn.Title
    	evn.Description = scf.Desc
    	evn.IsMobile = tpl == "mobile"
    	evn.CurrentUser = currentUser
    	evn.ShowSideAd = true
    	evn.PageName = "home"
    	evn.HotNodes = model.CategoryHot(db, scf.CategoryShowNum, scf.MustLoginNodeIds)
    
    	if currentUser.IgnoreNode != "" {
    		for _, node := range strings.Split(currentUser.IgnoreNode, ",") {
    			node, err := strconv.ParseUint(node, 10, 64)
    
    			if err != nil {
    				w.Write([]byte(`{"retcode":400,"retmsg":"忽略节点id应为整数,请在设置中检查。"}`))
    				return
    			}
    
    			for i := 0; i < len(pageInfo.Items); i++ {
    				if pageInfo.Items[i].Cid == node {
    					pageInfo.Items = append(pageInfo.Items[:i], pageInfo.Items[i+1:]...)
    					i--
    				}
    			}
    		}
    	}
    
    	if currentUser.IgnoreUser != "" {
    		for _, uid := range strings.Split(currentUser.IgnoreUser, ",") {
    			uid, err := strconv.ParseUint(uid, 10, 64)
    
    			if err != nil {
    				w.Write([]byte(`{"retcode":400,"retmsg":"忽略用户id应为整数,请在设置中检查。"}`))
    				return
    			}
    
    			for i := 0; i < len(pageInfo.Items); i++ {
    				if pageInfo.Items[i].Uid == uid {
    					pageInfo.Items = append(pageInfo.Items[:i], pageInfo.Items[i+1:]...)
    					i--
    				}
    			}
    		}
    	}
    
    	// 类似 solidot 格言
    	if !evn.IsMobile {
    		proverbs := model.CommentList(db, "hscan", "article_comment:"+scf.ProverbId, "", 500, scf.TimeZone, false)
    		// 剔除折叠的回复
    		for i := 0; i < len(proverbs.Items); i++ {
    			if proverbs.Items[i].Fold {
    				proverbs.Items = append(proverbs.Items[:i], proverbs.Items[i+1:]...)
    				i--
    			}
    		}
    
    		rand.Seed(time.Now().Unix())
    		if len(proverbs.Items) > 0 {
    			evn.Proverb = proverbs.Items[rand.Intn(len(proverbs.Items))].ContentFmt
    		}
    	}
    
    	evn.SiteInfo = si
    	evn.PageInfo = pageInfo
    	evn.Links = model.LinkList(db, false)
    
    	// 公告板功能
    	uobj, err := model.UserGetById(db, 1)
    	if err != nil {
    		evn.Announcement = "公告板,可修改1号用户的个人简介进行修改。"
    	} else {
    		evn.Announcement = uobj.About
    	}
    
    	if currentUser.Theme == "2" && !evn.IsMobile {
    		h.Render(w, tpl, evn, "layout.html", "index2.html")
    	} else {
    		h.Render(w, tpl, evn, "layout.html", "index.html")
    	}
    }
    
    func (h *BaseHandler) ArticleDetail(w http.ResponseWriter, r *http.Request) {
    	btn, key, score := r.FormValue("btn"), r.FormValue("key"), r.FormValue("score")
    	if len(key) > 0 {
    		_, err := strconv.ParseUint(key, 10, 64)
    		if err != nil {
    			w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
    			return
    		}
    	}
    	if len(score) > 0 {
    		_, err := strconv.ParseUint(score, 10, 64)
    		if err != nil {
    			w.Write([]byte(`{"retcode":400,"retmsg":"score type err"}`))
    			return
    		}
    	}
    
    	aid := pat.Param(r, "aid")
    	_, err := strconv.Atoi(aid)
    	if err != nil {
    		w.Write([]byte(`{"retcode":400,"retmsg":"aid type err"}`))
    		return
    	}
    
    	cmd := "hscan"
    	if btn == "prev" {
    		cmd = "hrscan"
    	}
    
    	db := h.App.Db
    	scf := h.App.Cf.Site
    	aobj, err := model.ArticleGetById(db, aid)
    	if err != nil {
    		w.Write([]byte(err.Error()))
    		return
    	}
    	currentUser, _ := h.CurrentUser(w, r)
    
    	if len(currentUser.Notice) > 0 && len(currentUser.Notice) >= len(aid) {
    		if len(aid) == len(currentUser.Notice) && aid == currentUser.Notice {
    			currentUser.Notice = ""
    			currentUser.NoticeNum = 0
    			jb, _ := json.Marshal(currentUser)
    			db.Hset("user", youdb.I2b(currentUser.Id), jb)
    		} else {
    			subStr := "," + aid + ","
    			newNotice := "," + currentUser.Notice + ","
    			if strings.Index(newNotice, subStr) >= 0 {
    				currentUser.Notice = strings.Trim(strings.Replace(newNotice, subStr, "", 1), ",")
    				currentUser.NoticeNum = len(strings.Split(currentUser.Notice, ","))
    				jb, _ := json.Marshal(currentUser)
    				db.Hset("user", youdb.I2b(currentUser.Id), jb)
    			}
    		}
    	}
    
    	if aobj.Hidden && currentUser.Flag < 1 {
    		w.WriteHeader(http.StatusNotFound)
    		w.Write([]byte(`{"retcode":404,"retmsg":"仅登录用户可见"}`))
    		return
    	}
    	cobj, err := model.CategoryGetById(db, strconv.FormatUint(aobj.Cid, 10))
    	if err != nil {
    		w.Write([]byte(err.Error()))
    		return
    	}
    
    	if cobj.Hidden && currentUser.Flag < 1 {
    		w.WriteHeader(http.StatusNotFound)
    		w.Write([]byte(`{"retcode":404,"retmsg":"仅登录用户可见"}`))
    		return
    	}
    
    	if cobj.Id == 20 && currentUser.Flag < 99 {
    		w.WriteHeader(http.StatusNotFound)
    		w.Write([]byte(`{"retcode":404,"retmsg":"请勿在垃圾场逗留"}`))
    		return
    	}
    
    	// Authorized
    	if scf.Authorized && currentUser.Flag < 5 {
    		w.WriteHeader(http.StatusUnauthorized)
    		w.Write([]byte(`{"retcode":401,"retmsg":"Unauthorized"}`))
    		return
    	}
    
    	cobj.Articles = db.Zget("category_article_num", youdb.I2b(cobj.Id)).Uint64()
    	pageInfo := model.CommentList(db, cmd, "article_comment:"+aid, key, scf.CommentListNum, scf.TimeZone, currentUser.IgnoreLimitedUsers)
    
    	if currentUser.IgnoreUser != "" {
    		for _, uid := range strings.Split(currentUser.IgnoreUser, ",") {
    			uid, err := strconv.ParseUint(uid, 10, 64)
    
    			if err != nil {
    				w.Write([]byte(`{"retcode":400,"retmsg":"type err"}`))
    				return
    			}
    
    			for i := 0; i < len(pageInfo.Items); i++ {
    				if pageInfo.Items[i].Uid == uid {
    					pageInfo.Items = append(pageInfo.Items[:i], pageInfo.Items[i+1:]...)
    					i--
    				}
    			}
    		}
    	}
    
    	type articleForDetail struct {
    		model.Article
    		ContentFmt  template.HTML
    		TagStr      template.HTML
    		Name        string
    		Avatar      string
    		Views       uint64
    		AddTimeFmt  string
    		EditTimeFmt string
    	}
    
    	type pageData struct {
    		PageData
    		Aobj     articleForDetail
    		Author   model.User
    		Cobj     model.Category
    		Relative model.ArticleRelative
    		PageInfo model.CommentPageInfo
    		Views    uint64
    	}
    
    	tpl := h.CurrentTpl(r)
    	evn := &pageData{}
    	evn.SiteCf = scf
    	evn.Title = aobj.Title + " - " + cobj.Name + " - " + scf.Name
    	evn.Keywords = aobj.Tags
    	evn.Description = cobj.Name + " - " + aobj.Title + " - " + aobj.Tags
    	evn.IsMobile = tpl == "mobile"
    
    	evn.CurrentUser = currentUser
    	evn.ShowSideAd = true
    	evn.PageName = "article_detail"
    	evn.HotNodes = model.CategoryHot(db, scf.CategoryShowNum, scf.MustLoginNodeIds)
    
    	author, _ := model.UserGetById(db, aobj.Uid)
    	viewsNum, _ := db.Hincr("article_views", youdb.I2b(aobj.Id), 1)
    	evn.Aobj = articleForDetail{
    		Article:     aobj,
    		ContentFmt:  template.HTML(util.ContentFmt(db, aobj.Content)),
    		Name:        author.Name,
    		Avatar:      author.Avatar,
    		Views:       viewsNum,
    		AddTimeFmt:  util.TimeFmt(aobj.AddTime, "2006-01-02", scf.TimeZone),
    		EditTimeFmt: util.TimeFmt(aobj.EditTime, "2006-01-02", scf.TimeZone),
    	}
    
    	if len(aobj.Tags) > 0 {
    		var tags []string
    		for _, v := range strings.Split(aobj.Tags, ",") {
    			tags = append(tags, `<a href="/tag/`+v+`">`+v+`</a>`)
    		}
    		evn.Aobj.TagStr = template.HTML(strings.Join(tags, ", "))
    	}
    
    	evn.Cobj = cobj
    	evn.Relative = model.ArticleGetRelative(db, aobj.Id, aobj.Tags)
    	evn.PageInfo = pageInfo
    
    	token := h.GetCookie(r, "token")
    	if len(token) == 0 {
    		token := xid.New().String()
    		h.SetCookie(w, "token", token, 1)
    	}
    
    	h.Render(w, tpl, evn, "layout.html", "article.html")
    }
    
    func (h *BaseHandler) ArticleDetailPost(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    	token := h.GetCookie(r, "token")
    	if len(token) == 0 {
    		w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
    		return
    	}
    
    	aid := pat.Param(r, "aid")
    	_, err := strconv.Atoi(aid)
    	if err != nil {
    		w.Write([]byte(`{"retcode":400,"retmsg":"aid type err:` + err.Error() + `"}`))
    		return
    	}
    
    	type recForm struct {
    		Act     string `json:"act"`
    		Link    string `json:"link"`
    		Content string `json:"content"`
    	}
    
    	type response struct {
    		normalRsp
    		Content string        `json:"content"`
    		Html    template.HTML `json:"html"`
    	}
    
    	decoder := json.NewDecoder(r.Body)
    	var rec recForm
    	err = decoder.Decode(&rec)
    	if err != nil {
    		w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
    		return
    	}
    	defer r.Body.Close()
    
    	db := h.App.Db
    	rsp := response{}
    
    	if rec.Act == "link_click" {
    		rsp.Retcode = 200
    		if len(rec.Link) > 0 {
    			hash := md5.Sum([]byte(rec.Link))
    			urlMd5 := hex.EncodeToString(hash[:])
    			bn := "article_detail_token"
    			clickKey := []byte(token + ":click:" + urlMd5)
    			if db.Zget(bn, clickKey).State == "ok" {
    				w.Write([]byte(`{"retcode":403,"retmsg":"err"}`))
    				return
    			}
    			db.Zset(bn, clickKey, uint64(time.Now().UTC().Unix()))
    			db.Hincr("url_md5_click", []byte(urlMd5), 1)
    
    			w.Write([]byte(`{"retcode":200,"retmsg":"ok"}`))
    			return
    		}
    	} else if rec.Act == "comment_preview" {
    		rsp.Retcode = 200
    		rsp.Html = template.HTML(util.ContentFmt(db, rec.Content))
    	} else if rec.Act == "comment_submit" {
    		timeStamp := uint64(time.Now().UTC().Unix())
    		currentUser, _ := h.CurrentUser(w, r)
    		if currentUser.Flag < 5 {
    			w.Write([]byte(`{"retcode":403,"retmsg":"forbidden"}`))
    			return
    		}
    		if (timeStamp - currentUser.LastReplyTime) < uint64(h.App.Cf.Site.CommentInterval) {
    			w.Write([]byte(`{"retcode":403,"retmsg":"out off comment interval"}`))
    			return
    		}
    		aobj, err := model.ArticleGetById(db, aid)
    		if err != nil {
    			w.Write([]byte(`{"retcode":404,"retmsg":"not found"}`))
    			return
    		}
    		if aobj.CloseComment {
    			w.Write([]byte(`{"retcode":403,"retmsg":"comment forbidden"}`))
    			return
    		}
    		commentId, _ := db.HnextSequence("article_comment:" + aid)
    
    		scf := h.App.Cf.Site
    
    		if currentUser.Flag == 99 && (strings.Contains(rec.Content, "警告一次") ||
    			strings.Contains(rec.Content, "管理员回复")) {
    			currentUser, err = model.UserGetById(db, uint64(scf.AdminBotId))
    			if err != nil {
    				w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
    				return
    			}
    		}
    
    		if currentUser.Flag >= 96 && strings.Contains(rec.Content, "匿名者说") {
    			currentUser, err = model.UserGetById(db, uint64(scf.AnonymousBotId))
    			if err != nil {
    				w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
    				return
    			}
    		}
    
    		obj := model.Comment{
    			Id:       commentId,
    			Aid:      aobj.Id,
    			Uid:      currentUser.Id,
    			Content:  rec.Content,
    			AddTime:  timeStamp,
    			ClientIp: "",
    		}
    		jb, _ := json.Marshal(obj)
    
    		db.Hset("article_comment:"+aid, youdb.I2b(obj.Id), jb) // 文章评论bucket
    		db.Hincr("count", []byte("comment_num"), 1)            // 评论总数
    		// 用户回复文章列表
    		db.Zset("user_article_reply:"+strconv.FormatUint(obj.Uid, 10), youdb.I2b(obj.Aid), obj.AddTime)
    
    		// 更新文章列表时间
    
    		aobj.Comments = commentId
    		aobj.RUid = currentUser.Id
    		aobj.EditTime = timeStamp
    		jb2, _ := json.Marshal(aobj)
    		db.Hset("article", youdb.I2b(aobj.Id), jb2)
    
    		currentUser.LastReplyTime = timeStamp
    		currentUser.Replies += 1
    		jb3, _ := json.Marshal(currentUser)
    		db.Hset("user", youdb.I2b(currentUser.Id), jb3)
    
    		// 不顶帖用户组
    		if currentUser.Flag != 6 {
    			// 总文章列表
    			db.Zset("article_timeline", youdb.I2b(aobj.Id), timeStamp)
    			// 分类文章列表
    			db.Zset("category_article_timeline:"+strconv.FormatUint(aobj.Cid, 10), youdb.I2b(aobj.Id), timeStamp)
    		}
    
    		// @ somebody in comment & topic author
    if aobj.Cid != 19 && aobj.Cid != 20 {
    		sbs := util.GetMention("@"+strconv.FormatUint(aobj.Uid, 10)+" "+rec.Content,
    			[]string{currentUser.Name, strconv.FormatUint(currentUser.Id, 10)})
    		for _, sb := range sbs {
    			var sbObj model.User
    			sbu, err := strconv.ParseUint(sb, 10, 64)
    			if err != nil {
    				// @ user name
    				sbObj, err = model.UserGetByName(db, strings.ToLower(sb))
    			} else {
    				// @ user id
    				sbObj, err = model.UserGetById(db, sbu)
    			}
    
    			if err == nil {
    				if len(sbObj.Notice) > 0 {
    					aidList := util.SliceUniqStr(strings.Split(aid+","+sbObj.Notice, ","))
    					if len(aidList) > 100 {
    						aidList = aidList[:100]
    					}
    					sbObj.Notice = strings.Join(aidList, ",")
    					sbObj.NoticeNum = len(aidList)
    				} else {
    					sbObj.Notice = aid
    					sbObj.NoticeNum = 1
    				}
    				jb, _ := json.Marshal(sbObj)
    				db.Hset("user", youdb.I2b(sbObj.Id), jb)
    			}
    		}
    }
    
    		rsp.Retcode = 200
    	}
    
    	json.NewEncoder(w).Encode(rsp)
    }
    
    func (h *BaseHandler) ContentPreviewPost(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
    	token := h.GetCookie(r, "token")
    	if len(token) == 0 {
    		w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
    		return
    	}
    
    	currentUser, _ := h.CurrentUser(w, r)
    	if currentUser.Flag < 5 {
    		w.Write([]byte(`{"retcode":403,"retmsg":"forbidden"}`))
    		return
    	}
    
    	type recForm struct {
    		Act     string `json:"act"`
    		Link    string `json:"link"`
    		Content string `json:"content"`
    	}
    
    	type response struct {
    		normalRsp
    		Content string        `json:"content"`
    		Html    template.HTML `json:"html"`
    	}
    
    	decoder := json.NewDecoder(r.Body)
    	var rec recForm
    	err := decoder.Decode(&rec)
    	if err != nil {
    		w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
    		return
    	}
    	defer r.Body.Close()
    
    	db := h.App.Db
    	rsp := response{}
    
    	if rec.Act == "preview" && len(rec.Content) > 0 {
    		rsp.Retcode = 200
    		rsp.Html = template.HTML(util.ContentFmt(db, rec.Content))
    	}
    	json.NewEncoder(w).Encode(rsp)
    }