├── static
├── test.txt
├── favicon.ico
├── img
│ ├── ghs.png
│ ├── yb.gif
│ ├── logo.png
│ ├── youbbs.jpg
│ ├── black-logo.png
│ └── logo-origin.png
├── favicon-big.ico
├── robots.txt
├── default
│ └── img
│ │ ├── dot.png
│ │ ├── tag.gif
│ │ ├── bg_ft.png
│ │ ├── bg_item.png
│ │ ├── bg_header.png
│ │ └── top_arrow.png
├── favicon_origin.ico
├── js
│ └── md5.min.js
└── css
│ └── jquery.toast.css
├── databackup
└── readme.txt
├── Makefile
├── Dockerfile
├── .gitignore
├── controller
├── view.go
├── robots.go
├── userlist.go
├── tag.go
├── category.go
├── adminlinklist.go
├── feed.go
├── basecontroller.go
├── admincategorylist.go
├── adminuserlist.go
├── admincommentedit.go
├── usersetting.go
├── adminuseredit.go
└── adminarticleedit.go
├── docker-compose.yml
├── util
├── timefmt.go
├── stringcheck.go
├── fetchimage.go
├── util.go
├── image.go
└── contentfmt.go
├── view
└── default
│ ├── mobile
│ ├── userlist.html
│ ├── notification.html
│ ├── tag.html
│ ├── index.html
│ ├── adminlinklist.html
│ ├── user.html
│ ├── admincategorylist.html
│ ├── category.html
│ ├── adminuserlist.html
│ ├── admincommentedit.html
│ ├── userlogin.html
│ ├── articlecreate.html
│ ├── adminarticleedit.html
│ ├── layout.html
│ └── adminuseredit.html
│ └── desktop
│ ├── userlist.html
│ ├── notification.html
│ ├── tag.html
│ ├── index2.html
│ ├── adminlinklist.html
│ ├── user.html
│ ├── index.html
│ ├── category.html
│ ├── admincategorylist.html
│ ├── adminuserlist.html
│ ├── admincommentedit.html
│ ├── userlogin.html
│ ├── articlecreate.html
│ ├── adminarticleedit.html
│ └── adminuseredit.html
├── LICENSE
├── config
├── config.yaml
└── config-2049.yaml
├── model
├── link.go
├── user.go
├── category.go
└── comment.go
├── README.md
├── Gopkg.toml
├── router
└── router.go
├── system
└── core.go
├── main.go
└── cronjob
└── mainjob.go
/static/test.txt:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/databackup/readme.txt:
--------------------------------------------------------------------------------
1 | keep it
2 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | dev:
2 | docker-compose up -d
3 |
4 | down:
5 | docker-compose down
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/static/img/ghs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/img/ghs.png
--------------------------------------------------------------------------------
/static/img/yb.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/img/yb.gif
--------------------------------------------------------------------------------
/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/img/logo.png
--------------------------------------------------------------------------------
/static/img/youbbs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/img/youbbs.jpg
--------------------------------------------------------------------------------
/static/favicon-big.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/favicon-big.ico
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /login
3 | Disallow: /logout
4 | Disallow: /admin
5 |
--------------------------------------------------------------------------------
/static/default/img/dot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/default/img/dot.png
--------------------------------------------------------------------------------
/static/default/img/tag.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/default/img/tag.gif
--------------------------------------------------------------------------------
/static/favicon_origin.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/favicon_origin.ico
--------------------------------------------------------------------------------
/static/img/black-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/img/black-logo.png
--------------------------------------------------------------------------------
/static/img/logo-origin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/img/logo-origin.png
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.12
2 | RUN go get -u github.com/golang/dep/cmd/dep && go get -u github.com/oxequa/realize
--------------------------------------------------------------------------------
/static/default/img/bg_ft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/default/img/bg_ft.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | /static/avatar
3 | /static/upload
4 | goyoubbs
5 | mydata.db
6 | 2049bbs
7 | *.env
8 | vendor
--------------------------------------------------------------------------------
/static/default/img/bg_item.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/default/img/bg_item.png
--------------------------------------------------------------------------------
/static/default/img/bg_header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/default/img/bg_header.png
--------------------------------------------------------------------------------
/static/default/img/top_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Terminus2049/2049bbs/HEAD/static/default/img/top_arrow.png
--------------------------------------------------------------------------------
/controller/view.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func (h *BaseHandler) ViewAtTpl(w http.ResponseWriter, r *http.Request) {
8 | tpl := r.FormValue("tpl")
9 | if tpl == "desktop" || tpl == "mobile" {
10 | h.SetCookie(w, "tpl", tpl, 1)
11 | }
12 | http.Redirect(w, r, "/", http.StatusSeeOther)
13 | }
14 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | bbs:
4 | image: docker.pkg.github.com/speechfree/go-base/go-base:base
5 | container_name: bbs
6 | ports:
7 | - 8000:8082
8 | # env_file: dev.env
9 | volumes:
10 | - "./:/go/src/github.com/terminus2049/2049BBS"
11 | working_dir: /go/src/github.com/terminus2049/2049BBS
12 | command: ["tail", "-f", "/dev/null"]
13 |
--------------------------------------------------------------------------------
/controller/robots.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | )
7 |
8 | var defaultRobots = `User-agent: *
9 | Disallow: /login
10 | Disallow: /logout
11 | Disallow: /admin
12 | `
13 |
14 | func (h *BaseHandler) Robots(w http.ResponseWriter, r *http.Request) {
15 | w.Header().Set("Content-Type", "text/plain; charset=utf-8")
16 | buf, err := ioutil.ReadFile("static/robots.txt")
17 | if err != nil {
18 | w.Write([]byte(defaultRobots))
19 | return
20 | }
21 | w.Write(buf)
22 | }
23 |
--------------------------------------------------------------------------------
/util/timefmt.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "strconv"
5 | "time"
6 | )
7 |
8 | func TimeFmt(tp interface{}, sample string, tz int) string {
9 | offset := int64(time.Duration(tz) * time.Hour)
10 | var t int64
11 | switch tp.(type) {
12 | case uint64:
13 | t = int64(tp.(uint64))
14 | case string:
15 | i64, err := strconv.ParseInt(tp.(string), 10, 64)
16 | if err != nil {
17 | return ""
18 | }
19 | t = i64
20 | case int64:
21 | t = tp.(int64)
22 | }
23 | if len(sample) == 0 {
24 | sample = "2006-01-02"
25 | }
26 | tm := time.Unix(t, offset).UTC()
27 | return tm.Format(sample)
28 | }
29 |
--------------------------------------------------------------------------------
/util/stringcheck.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "regexp"
5 | )
6 |
7 | var (
8 | nicknameRegexp = regexp.MustCompile(`^[a-z0-9A-Z\p{Han}]+(_[a-z0-9A-Z\p{Han}]+)*$`)
9 | usernameRegexp = regexp.MustCompile(`^[a-zA-Z][a-z0-9A-Z]*(_[a-z0-9A-Z]+)*$`)
10 | regUserNameRegexp = regexp.MustCompile(`[^a-z0-9A-Z\p{Han}]+`)
11 | mailRegexp = regexp.MustCompile(`^[a-zA-Z][a-z0-9A-Z]*(_[a-z0-9A-Z]+)*$`)
12 | )
13 |
14 | func IsNickname(str string) bool {
15 | if len(str) == 0 {
16 | return false
17 | }
18 | return nicknameRegexp.MatchString(str)
19 | }
20 |
21 | func IsUserName(str string) bool {
22 | if len(str) == 0 {
23 | return false
24 | }
25 | return nicknameRegexp.MatchString(str) // 支持中文
26 | //return usernameRegexp.MatchString(str)
27 | }
28 |
29 | func IsMail(str string) bool {
30 | if len(str) < 6 { // x@x.xx
31 | return false
32 | }
33 | return mailRegexp.MatchString(str)
34 | }
35 |
36 | func RemoveCharacter(str string) string {
37 | return regUserNameRegexp.ReplaceAllString(str, "")
38 | }
39 |
--------------------------------------------------------------------------------
/view/default/mobile/userlist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
9 |
10 |
11 |
12 |
13 | {{range $_, $item := .PageInfo.Items}}
14 | -
15 | id:{{$item.Id}} - {{$item.Name}} - flag: {{$item.Flag}}
16 | 查看
17 |
18 | {{end}}
19 |
20 |
21 |
30 |
31 |
32 |
33 | {{ end}}
34 |
--------------------------------------------------------------------------------
/view/default/desktop/userlist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
9 |
10 |
11 |
12 |
13 | {{range $_, $item := .PageInfo.Items}}
14 | -
15 | id:{{$item.Id}} - {{$item.Name}} - flag: {{$item.Flag}}
16 | 查看
17 |
18 | {{end}}
19 |
20 |
21 |
30 |
31 |
32 |
33 | {{ end}}
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/util/fetchimage.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "errors"
5 | "github.com/weint/httpclient"
6 | "io"
7 | "os"
8 | "strconv"
9 | "time"
10 | )
11 |
12 | func FetchAvatar(url, save, ua string) error {
13 | defaultUA := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36"
14 |
15 | if len(ua) == 0 {
16 | ua = defaultUA
17 | }
18 | if _, err := os.Stat(save); err == nil {
19 | return nil
20 | }
21 |
22 | hc := httpclient.NewHttpClientRequest("GET", url)
23 | hc.SetTimeout(30 * time.Second)
24 | hc.Header("User-Agent", ua)
25 | hc.Header("Referer", url)
26 |
27 | resp, err := hc.Response()
28 | if err != nil {
29 | return err
30 | }
31 | defer resp.Body.Close()
32 |
33 | if resp.StatusCode != 200 {
34 | return errors.New("StatusCode " + strconv.Itoa(resp.StatusCode))
35 | }
36 |
37 | out, err := os.Create(save)
38 | defer out.Close()
39 |
40 | nb, err := io.Copy(out, resp.Body)
41 | if err != nil {
42 | return err
43 | }
44 | if nb == 800 {
45 | return errors.New("nb is small " + strconv.FormatInt(nb, 10))
46 | }
47 |
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/config/config.yaml:
--------------------------------------------------------------------------------
1 | Main:
2 | HttpPort: 8082
3 | HttpsOn: false
4 | Domain: ""
5 | HttpsPort: 443
6 | PubDir: "static"
7 | ViewDir: "view/default"
8 | Youdb: "mydata.db"
9 | CookieSecure: false
10 | CookieHttpOnly: true
11 | OldSiteDomain: ""
12 | TLSCrtFile: ""
13 | TLSKeyFile: ""
14 | Site:
15 | Name: "youBBS"
16 | Desc: "Yet another bbs in go"
17 | AdminEmail: ""
18 | MainDomain: "http://127.0.0.1:8082"
19 | MainNodeIds: "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21"
20 | MustLoginNodeIds: "19,20,21"
21 | NotHomeNodeIds: "2,4,17,19,20,21"
22 | ProverbId: "2"
23 | AvatarMinId: 2539
24 | AvatarMaxId: 2558
25 | HomeNode: "2"
26 | AdminBotId: 2918
27 | AnonymousBotId: 2917
28 | TimeZone: 0
29 | HomeShowNum: 50
30 | PageShowNum: 20
31 | TagShowNum: 20
32 | CategoryShowNum: 20
33 | TitleMaxLen: 180
34 | ContentMaxLen: 9000
35 | PostInterval: 60
36 | CommentListNum: 10
37 | CommentInterval: 20
38 | Authorized: false
39 | RegReview: false
40 | CloseReg: false
41 | AutoDataBackup: false
42 | ResetCookieKey: false
43 | AutoGetTag: true
44 | UploadSuffix: ""
45 | UploadImgOnly: false
46 | UploadImgResize: false
47 | UploadMaxSize: 50
48 |
--------------------------------------------------------------------------------
/config/config-2049.yaml:
--------------------------------------------------------------------------------
1 | Main:
2 | HttpPort: 80
3 | HttpsOn: true
4 | Domain: "2049bbs.xyz"
5 | HttpsPort: 443
6 | PubDir: "static"
7 | ViewDir: "view/default"
8 | Youdb: "mydata.db"
9 | CookieSecure: true
10 | CookieHttpOnly: true
11 | OldSiteDomain: ""
12 | TLSCrtFile: "certs/cloudflare.pem"
13 | TLSKeyFile: "certs/cloudflare.key"
14 | Site:
15 | Name: "2049BBS"
16 | Desc: "自由人的精神角落,一个无需手机号和邮箱即可发言的论坛。"
17 | AdminEmail: ""
18 | MainDomain: "http://2049bbs.xyz"
19 | MainNodeIds: "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21"
20 | MustLoginNodeIds: "19,20,21"
21 | NotHomeNodeIds: "2,4,17,19,20,21"
22 | ProverbId: "2633"
23 | AvatarMinId: 2539
24 | AvatarMaxId: 2558
25 | HomeNode: "2"
26 | AdminBotId: 2942
27 | AnonymousBotId: 2944
28 | TimeZone: 0
29 | HomeShowNum: 50
30 | PageShowNum: 20
31 | TagShowNum: 30
32 | CategoryShowNum: 20
33 | TitleMaxLen: 100
34 | ContentMaxLen: 20000
35 | PostInterval: 60
36 | CommentListNum: 100
37 | CommentInterval: 20
38 | Authorized: false
39 | RegReview: false
40 | CloseReg: false
41 | AutoDataBackup: true
42 | ResetCookieKey: false
43 | AutoGetTag: true
44 | UploadSuffix: ""
45 | UploadImgOnly: false
46 | UploadImgResize: false
47 | UploadMaxSize: 50
48 |
--------------------------------------------------------------------------------
/view/default/desktop/notification.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
9 |
10 |
11 |
12 | {{range $_, $item := .PageInfo.Items}}
13 |
14 |
18 |
19 |
20 |
{{$item.Cname}} • {{$item.Name}}
21 | • {{$item.EditTimeFmt}}
22 | {{if $item.Comments}}
23 | • 最后回复 {{$item.Rname}}
24 | {{end}}
25 |
26 |
27 | {{if $item.Comments}}
28 |
29 | {{end}}
30 |
31 |
32 |
33 | {{end}}
34 |
35 |
36 |
37 | {{ end}}
38 |
39 |
--------------------------------------------------------------------------------
/view/default/mobile/notification.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
9 |
10 |
11 |
12 | {{range $_, $item := .PageInfo.Items}}
13 |
14 |
18 |
19 |
20 |
21 | {{$item.Cname}}
22 | • {{$item.EditTimeFmt}}
23 | {{if $item.Comments}}
24 | • {{$item.Rname}}
25 | {{else}}
26 | • {{$item.Name}}
27 | {{end}}
28 |
29 |
30 | {{if $item.Comments}}
31 |
32 | {{end}}
33 |
34 |
35 |
36 | {{end}}
37 |
38 |
39 |
40 | {{ end}}
41 |
42 |
--------------------------------------------------------------------------------
/controller/userlist.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 |
7 | "github.com/rs/xid"
8 | "github.com/terminus2049/2049bbs/model"
9 | )
10 |
11 | func (h *BaseHandler) UserList(w http.ResponseWriter, r *http.Request) {
12 | flag, btn, key := r.FormValue("flag"), r.FormValue("btn"), r.FormValue("key")
13 | if len(key) > 0 {
14 | _, err := strconv.ParseUint(key, 10, 64)
15 | if err != nil {
16 | w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
17 | return
18 | }
19 | }
20 |
21 | cmd := "hrscan"
22 | if btn == "prev" {
23 | cmd = "hscan"
24 | }
25 |
26 | db := h.App.Db
27 |
28 | if len(flag) == 0 {
29 | flag = "6"
30 | }
31 |
32 | if flag != "6" && flag != "0" && flag != "7" {
33 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
34 | return
35 | }
36 |
37 | pageInfo := model.UserListByFlag(db, cmd, "user_flag:"+flag, key, h.App.Cf.Site.PageShowNum)
38 |
39 | type pageData struct {
40 | PageData
41 | PageInfo model.UserPageInfo
42 | Flag string
43 | }
44 |
45 | tpl := h.CurrentTpl(r)
46 | evn := &pageData{}
47 | evn.SiteCf = h.App.Cf.Site
48 | evn.Title = "用户列表"
49 | evn.IsMobile = tpl == "mobile"
50 | evn.ShowSideAd = true
51 | evn.PageName = "user_list"
52 |
53 | evn.PageInfo = pageInfo
54 | evn.Flag = flag
55 |
56 | token := h.GetCookie(r, "token")
57 | if len(token) == 0 {
58 | token := xid.New().String()
59 | h.SetCookie(w, "token", token, 1)
60 | }
61 |
62 | h.Render(w, tpl, evn, "layout.html", "userlist.html")
63 | }
64 |
--------------------------------------------------------------------------------
/view/default/desktop/tag.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 | {{range $_, $item := .PageInfo.Items}}
10 |
11 |
12 |

13 |
14 |
15 |
16 |
{{$item.Cname}} • {{$item.Name}}
17 | • {{$item.EditTimeFmt}}
18 | {{if $item.Comments}}
19 | • 最后回复 {{$item.Rname}}
20 | {{end}}
21 |
22 |
23 | {{if $item.Comments}}
24 |
25 | {{end}}
26 |
27 |
28 |
29 | {{end}}
30 |
31 |
40 |
41 |
42 |
43 |
44 |
45 | {{ end}}
46 |
47 |
--------------------------------------------------------------------------------
/view/default/mobile/tag.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 | {{range $_, $item := .PageInfo.Items}}
10 |
11 |
12 |

13 |
14 |
15 |
16 |
17 | {{$item.Cname}}
18 | • {{$item.EditTimeFmt}}
19 | {{if $item.Comments}}
20 | • {{$item.Rname}}
21 | {{else}}
22 | • {{$item.Name}}
23 | {{end}}
24 |
25 |
26 | {{if $item.Comments}}
27 |
28 | {{end}}
29 |
30 |
31 |
32 | {{end}}
33 |
34 |
43 |
44 |
45 |
46 |
47 |
48 | {{ end}}
49 |
50 |
--------------------------------------------------------------------------------
/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "io"
7 | "os"
8 | "strings"
9 | )
10 |
11 | func SliceUniqInt(s []int) []int {
12 | if len(s) == 0 {
13 | return s
14 | }
15 | seen := make(map[int]struct{}, len(s))
16 | j := 0
17 | for _, v := range s {
18 | if _, ok := seen[v]; ok {
19 | continue
20 | }
21 | seen[v] = struct{}{}
22 | s[j] = v
23 | j++
24 | }
25 | return s[:j]
26 | }
27 |
28 | func SliceUniqStr(s []string) []string {
29 | if len(s) == 0 {
30 | return s
31 | }
32 | seen := make(map[string]struct{}, len(s))
33 | j := 0
34 | for _, v := range s {
35 | if _, ok := seen[v]; ok {
36 | continue
37 | }
38 | seen[v] = struct{}{}
39 | s[j] = v
40 | j++
41 | }
42 | return s[:j]
43 | }
44 |
45 | func HashFileMD5(filePath string) (string, error) {
46 | var returnMD5String string
47 | file, err := os.Open(filePath)
48 | if err != nil {
49 | return returnMD5String, err
50 | }
51 | defer file.Close()
52 | hash := md5.New()
53 | if _, err := io.Copy(hash, file); err != nil {
54 | return returnMD5String, err
55 | }
56 | hashInBytes := hash.Sum(nil)[:16]
57 | returnMD5String = hex.EncodeToString(hashInBytes)
58 | return returnMD5String, nil
59 | }
60 |
61 | func CheckTags(tags string) string {
62 | limit := 5
63 | seen := map[string]struct{}{}
64 | tmpTags := strings.Replace(tags, " ", ",", -1)
65 | tmpTags = strings.Replace(tmpTags, ",", ",", -1)
66 | tagList := make([]string, limit)
67 | j := 0
68 | for _, tag := range strings.Split(tmpTags, ",") {
69 | if len(tag) > 1 && len(tag) < 25 {
70 | if _, ok := seen[tag]; ok {
71 | continue
72 | }
73 | seen[tag] = struct{}{}
74 | tagList[j] = tag
75 | j++
76 | if j == limit {
77 | break
78 | }
79 | }
80 | }
81 | return strings.Join(tagList[:j], ",")
82 | }
83 |
--------------------------------------------------------------------------------
/model/link.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/ego008/youdb"
6 | "sort"
7 | )
8 |
9 | type Link struct {
10 | Id uint64 `json:"id"`
11 | Name string `json:"name"`
12 | Url string `json:"url"`
13 | Score int `json:"score"`
14 | }
15 |
16 | func LinkGetById(db *youdb.DB, lid string) Link {
17 | var item Link
18 | rs := db.Hget("link", youdb.DS2b(lid))
19 | if rs.State == "ok" {
20 | json.Unmarshal(rs.Data[0], &item)
21 | }
22 | return item
23 | }
24 |
25 | func LinkSet(db *youdb.DB, obj Link) {
26 | if obj.Id == 0 {
27 | // add
28 | newId, _ := db.HnextSequence("link")
29 | obj.Id = newId
30 | }
31 | jb, _ := json.Marshal(obj)
32 | db.Hset("link", youdb.I2b(obj.Id), jb)
33 | }
34 |
35 | func LinkList(db *youdb.DB, getAll bool) []Link {
36 | var items []Link
37 | itemMap := map[uint64]Link{}
38 |
39 | startKey := []byte("")
40 |
41 | for {
42 | rs := db.Hscan("link", startKey, 20)
43 | if rs.State == "ok" {
44 | for i := 0; i < len(rs.Data)-1; i += 2 {
45 | startKey = rs.Data[i]
46 | item := Link{}
47 | json.Unmarshal(rs.Data[i+1], &item)
48 | if getAll {
49 | // included score == 0
50 | itemMap[youdb.B2i(rs.Data[i])] = item
51 | } else {
52 | if item.Score > 0 {
53 | itemMap[youdb.B2i(rs.Data[i])] = item
54 | }
55 | }
56 | }
57 | } else {
58 | break
59 | }
60 | }
61 |
62 | if len(itemMap) > 0 {
63 | type Kv struct {
64 | Key uint64
65 | Value int
66 | }
67 |
68 | var ss []Kv
69 | for k, v := range itemMap {
70 | ss = append(ss, Kv{k, v.Score})
71 | }
72 |
73 | sort.Slice(ss, func(i, j int) bool {
74 | return ss[i].Value > ss[j].Value
75 | })
76 |
77 | for _, kv := range ss {
78 | items = append(items, itemMap[kv.Key])
79 | }
80 | }
81 |
82 | return items
83 | }
84 |
--------------------------------------------------------------------------------
/controller/tag.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/terminus2049/2049bbs/model"
9 | "goji.io/pat"
10 | )
11 |
12 | func (h *BaseHandler) TagDetail(w http.ResponseWriter, r *http.Request) {
13 | btn, key := r.FormValue("btn"), r.FormValue("key")
14 | if len(key) > 0 {
15 | _, err := strconv.ParseUint(key, 10, 64)
16 | if err != nil {
17 | w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
18 | return
19 | }
20 | }
21 |
22 | tag := pat.Param(r, "tag")
23 | tagLow := strings.ToLower(tag)
24 |
25 | cmd := "hrscan"
26 | if btn == "prev" {
27 | cmd = "hscan"
28 | }
29 |
30 | db := h.App.Db
31 | scf := h.App.Cf.Site
32 | rs := db.Hscan("tag:"+tagLow, nil, 1)
33 | if rs.State != "ok" {
34 | w.Write([]byte(`{"retcode":404,"retmsg":"not found"}`))
35 | return
36 | }
37 |
38 | currentUser, _ := h.CurrentUser(w, r)
39 |
40 | pageInfo := model.UserArticleList(db, cmd, "tag:"+tagLow, key, scf.PageShowNum, scf.TimeZone)
41 |
42 | type tagDetail struct {
43 | Name string
44 | Number uint64
45 | }
46 | type pageData struct {
47 | PageData
48 | Tag tagDetail
49 | PageInfo model.ArticlePageInfo
50 | }
51 |
52 | tpl := h.CurrentTpl(r)
53 |
54 | evn := &pageData{}
55 | evn.SiteCf = scf
56 | evn.Title = tag + " - " + scf.Name
57 | evn.Keywords = tag
58 | evn.Description = tag
59 | evn.IsMobile = tpl == "mobile"
60 |
61 | evn.CurrentUser = currentUser
62 | evn.ShowSideAd = true
63 | evn.PageName = "category_detail"
64 | evn.HotNodes = model.CategoryHot(db, scf.CategoryShowNum, scf.MustLoginNodeIds)
65 |
66 | evn.Tag = tagDetail{
67 | Name: tag,
68 | Number: db.Zget("tag_article_num", []byte(tagLow)).Uint64(),
69 | }
70 | evn.PageInfo = pageInfo
71 |
72 | h.Render(w, tpl, evn, "layout.html", "tag.html")
73 | }
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 2049bbs,一个无需手机号和邮箱即可注册发言的论坛。Fork 自 [goyoubbs](https://github.com/ego008/goyoubbs)。
2 |
3 | ## 本地开发
4 |
5 |
6 | 安装 [go](https://golang.org/dl/),然后 clone 本仓库。
7 |
8 | ```bash
9 | go get -v github.com/terminus2049/2049bbs
10 | ```
11 | 1. 然后 cd 到相应目录,一般是 `go/src/github.com/terminus2049/2049bbs`。
12 |
13 | ```bash
14 | go run main.go
15 | ```
16 |
17 | 然后在浏览器打开 `127.0.0.1:8082` 即可,或者直接编译,运行 `sudo ./2049bbs`,
18 |
19 | 2. 利用 Docker 进行开发
20 |
21 | - 首先安装 Docker 及 docker-compose
22 | - 将本项目 clone 到本地,任何目录均可
23 | - 进入项目目录,在安装好 docker 及 docker-compose 后,运行脚本 `make dev` 即自动拉去构建好的镜像
24 | - 运行成功后 `docker ps` 即可发现名为 bbs 的容器正在运行中
25 |
26 | ```
27 | machine: 2049BBS % docker ps
28 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
29 | d798030a6f0f docker.pkg.github.com/speechfree/go-base/go-base:base "tail -f /dev/null" About an hour ago Up About an hour 0.0.0.0:8000->8082/tcp bbs
30 | ```
31 | - 然后,`docker exec -it bbs bash` 进入到容器中,通过 `dep ensure` 拉去项目依赖到本地目录 `vendor` 中
32 | - 完成后运行 `go run main.go` 若出现如下输出即表明项目运行成功
33 |
34 | ```
35 | 2019/12/20 13:23:07 MainDomain: http://127.0.0.1:8082
36 | 2019/12/20 13:23:07 youdb Connect to mydata.db
37 | 2019/12/20 13:23:07 Web server Listen port 8082
38 | ```
39 | - 在宿主机打开任意浏览器输入 `http://localhost:8000` 即可看到构建成功的应用。
40 | - 另,在开发过程中,为了方便修改代码后重载应用,可以通过 `realize start` 启动应用,则修改任何 Golang 代码,其均会自动构建加载。
41 |
42 | ### 数据库
43 |
44 | 如果没有 kv 数据库开发经验,最好在程序跑起来后,用 [boltdbweb](https://github.com/evnix/boltdbweb) 打开数据库文件 `mydata.db`,了解一下内部存储结构。
45 |
46 | ## 部署
47 |
48 | 编译二进制文件 `go build`,~~非 Linux 平台为交叉编译 `GOOS=linux GOARCH=amd64 go build`~~,由于使用了 gojieba 分词引擎,不能跨平台编译,请使用在线api功能、移除相关组件后再尝试跨平台编译。
49 |
50 | 将编译好的二进制文件与 config、static 和 view 三个文件夹的文件放在同一个文件夹内,运行 `./2049bbs`。
51 |
52 | 服务器配置:在生产环境中,建议打开 `https`,把 `config.yaml` 中 `HttpsOn: false` 改为 `true`。也可以自行申请 cloudflare 证书,相应配置可以参考 [config-2049.yaml](https://github.com/Terminus2049/2049BBS/blob/master/config/config-2049.yaml).
53 |
54 | ## 备份
55 |
56 | 需要备份 `mydata.db` 和 `/static/avatar` 文件。
57 |
--------------------------------------------------------------------------------
/view/default/desktop/index2.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
10 | {{if ge .CurrentUser.Flag 5}}
11 |
12 | {{end}}
13 |
14 |
15 |
16 |
17 | {{range $_, $item := .PageInfo.Items}}
18 |
19 |
23 |
24 |
25 |
{{$item.Cname}} • {{$item.Name}}
26 | • {{$item.EditTimeFmt}}
27 | {{if $item.Comments}}
28 | • 最后回复 {{$item.Rname}}
29 | {{end}}
30 |
31 |
32 | {{if $item.Comments}}
33 |
34 | {{end}}
35 |
36 |
37 | {{end}}
38 |
39 |
40 |
41 | {{.Proverb}}
42 |
43 |
44 |
45 |
46 |
55 |
56 |
57 |
58 | {{ end }}
59 |
--------------------------------------------------------------------------------
/view/default/mobile/index.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
12 | {{if ge .CurrentUser.Flag 5}}
13 |
14 | {{end}}
15 |
16 |
17 |
18 |
19 |
20 | {{range $_, $item := .PageInfo.Items}}
21 | {{if ne $item.Cid 2}}
22 |
23 |
27 |
28 |
29 |
30 | {{$item.Cname}}
31 | • {{$item.EditTimeFmt}}
32 | {{if $item.Comments}}
33 | • {{$item.Rname}}
34 | {{else}}
35 | • {{$item.Name}}
36 | {{end}}
37 |
38 |
39 | {{if $item.Comments}}
40 |
41 | {{end}}
42 |
43 |
44 |
45 | {{end}}
46 | {{end}}
47 |
48 |
57 |
58 |
59 |
60 | {{ end}}
61 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 | required = ["github.com/russross/blackfriday"]
28 |
29 | [[constraint]]
30 | name = "github.com/boltdb/bolt"
31 | version = "1.3.1"
32 |
33 | [[constraint]]
34 | branch = "master"
35 | name = "github.com/dchest/captcha"
36 |
37 | [[constraint]]
38 | name = "github.com/disintegration/imaging"
39 | version = "1.6.2"
40 |
41 | [[constraint]]
42 | branch = "master"
43 | name = "github.com/ego008/youdb"
44 |
45 | [[constraint]]
46 | name = "github.com/gorilla/securecookie"
47 | version = "1.1.1"
48 |
49 | [[constraint]]
50 | name = "github.com/rs/xid"
51 | version = "1.2.1"
52 |
53 | [[constraint]]
54 | branch = "master"
55 | name = "github.com/shurcooL/github_flavored_markdown"
56 |
57 | [[constraint]]
58 | branch = "master"
59 | name = "github.com/russross/blackfriday"
60 |
61 | [[constraint]]
62 | name = "github.com/terminus2049/2049bbs"
63 | version = "0.2.0"
64 |
65 | [[constraint]]
66 | branch = "master"
67 | name = "github.com/weint/config"
68 |
69 | [[constraint]]
70 | branch = "master"
71 | name = "github.com/weint/httpclient"
72 |
73 | [[constraint]]
74 | branch = "master"
75 | name = "github.com/xi2/httpgzip"
76 |
77 | [[constraint]]
78 | name = "github.com/yanyiwu/gojieba"
79 | version = "1.1.0"
80 |
81 | [[constraint]]
82 | name = "goji.io"
83 | version = "2.0.2"
84 |
85 | [[constraint]]
86 | branch = "master"
87 | name = "golang.org/x/crypto"
88 |
89 | [[constraint]]
90 | branch = "master"
91 | name = "golang.org/x/net"
92 |
93 | [prune]
94 | go-tests = true
95 | unused-packages = true
96 |
--------------------------------------------------------------------------------
/util/image.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "image"
7 | "image/gif"
8 | "image/jpeg"
9 | "image/png"
10 | "net/http"
11 | "os"
12 |
13 | "github.com/disintegration/imaging"
14 | )
15 |
16 | var imgTable = map[string]string{
17 | "image/jpeg": "jpeg",
18 | "image/jpg": "jpg",
19 | "image/gif": "gif",
20 | "image/png": "png",
21 | }
22 |
23 | func CheckImageType(buff []byte) string {
24 | // why 512 bytes ? see http://golang.org/pkg/net/http/#DetectContentType
25 | //buff := make([]byte, 512)
26 | fileType := http.DetectContentType(buff)
27 | if v, ok := imgTable[fileType]; ok {
28 | return v
29 | }
30 | return ""
31 | }
32 |
33 | func GetImageObj2(buff *bytes.Buffer) (image.Image, error) {
34 | img, _, err := image.Decode(bytes.NewReader(buff.Bytes()))
35 | return img, err
36 | }
37 |
38 | func GetImageObj(buff *bytes.Buffer) (image.Image, error) {
39 | var img image.Image
40 | var err error
41 | fileType := http.DetectContentType(buff.Bytes()[:512])
42 | switch fileType {
43 | case "image/jpeg", "image/jpg":
44 | img, err = jpeg.Decode(bytes.NewReader(buff.Bytes()))
45 | case "image/gif":
46 | img, err = gif.Decode(bytes.NewReader(buff.Bytes()))
47 | case "image/png":
48 | img, err = png.Decode(bytes.NewReader(buff.Bytes()))
49 | default:
50 | err = errors.New("unknown image format")
51 | }
52 | return img, err
53 | }
54 |
55 | func AvatarResize(srcImg image.Image, w, h int, filePath string) error {
56 | if w > 73 {
57 | srcW := srcImg.Bounds().Max.X
58 | srcH := srcImg.Bounds().Max.Y
59 |
60 | if srcW < w {
61 | w = srcW
62 | }
63 | if srcH < h {
64 | h = srcH
65 | }
66 | }
67 | dstImg := imaging.Resize(srcImg, w, h, imaging.Lanczos)
68 |
69 | f3, err := os.Create(filePath)
70 | if err != nil {
71 | return err
72 | }
73 | defer f3.Close()
74 |
75 | err = jpeg.Encode(f3, dstImg, &jpeg.Options{Quality: 95})
76 | if err != nil {
77 | return err
78 | }
79 | return nil
80 | }
81 |
82 | func ImageResize(srcImg image.Image, w, h int) *image.NRGBA {
83 | if w > 73 {
84 | srcW := srcImg.Bounds().Max.X
85 | srcH := srcImg.Bounds().Max.Y
86 |
87 | if srcW < w {
88 | w = srcW
89 | }
90 | if srcH < h {
91 | h = srcH
92 | }
93 | }
94 | return imaging.Resize(srcImg, w, h, imaging.Lanczos)
95 | }
96 |
--------------------------------------------------------------------------------
/view/default/mobile/adminlinklist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 |
10 | {{range $_, $item := .Items}}
11 | -
12 | {{$item.Score}} - {{$item.Name}} - {{$item.Url}}
13 | 编辑
14 |
15 | {{end}}
16 |
17 |
18 |
19 |
20 |
23 |
24 |
36 |
37 |
68 |
69 | {{ end}}
70 |
71 |
--------------------------------------------------------------------------------
/view/default/desktop/adminlinklist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 |
10 | {{range $_, $item := .Items}}
11 | -
12 | id:{{$item.Id}} - score: {{$item.Score}} - {{$item.Name}} - {{$item.Url}}
13 | 编辑
14 |
15 | {{end}}
16 |
17 |
18 |
19 |
20 |
23 |
24 |
36 |
37 |
68 |
69 | {{ end}}
70 |
71 |
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/dchest/captcha"
5 | "github.com/terminus2049/2049bbs/controller"
6 | "github.com/terminus2049/2049bbs/system"
7 | "goji.io"
8 | "goji.io/pat"
9 | )
10 |
11 | func NewRouter(app *system.Application) *goji.Mux {
12 | sp := goji.SubMux()
13 | h := controller.BaseHandler{App: app}
14 |
15 | sp.HandleFunc(pat.Get("/"), h.ArticleHomeList)
16 | sp.HandleFunc(pat.Get("/view"), h.ViewAtTpl)
17 | sp.HandleFunc(pat.Get("/feed"), h.FeedHandler)
18 | sp.HandleFunc(pat.Get("/feed/:cid"), h.FeedCategoryHandler)
19 | sp.HandleFunc(pat.Get("/robots.txt"), h.Robots)
20 |
21 | sp.Handle(pat.Get("/captcha/*"), captcha.Server(captcha.StdWidth, captcha.StdHeight))
22 |
23 | sp.HandleFunc(pat.Get("/n/:cid"), h.CategoryDetail)
24 | sp.HandleFunc(pat.Get("/member/:uid"), h.UserDetail)
25 | sp.HandleFunc(pat.Get("/tag/:tag"), h.TagDetail)
26 |
27 | sp.HandleFunc(pat.Get("/logout"), h.UserLogout)
28 | sp.HandleFunc(pat.Get("/notification"), h.UserNotification)
29 |
30 | sp.HandleFunc(pat.Get("/t/:aid"), h.ArticleDetail)
31 | sp.HandleFunc(pat.Post("/t/:aid"), h.ArticleDetailPost)
32 |
33 | sp.HandleFunc(pat.Get("/setting"), h.UserSetting)
34 | sp.HandleFunc(pat.Post("/setting"), h.UserSettingPost)
35 |
36 | sp.HandleFunc(pat.Get("/newpost/:cid"), h.ArticleAdd)
37 | sp.HandleFunc(pat.Post("/newpost/:cid"), h.ArticleAddPost)
38 |
39 | sp.HandleFunc(pat.Get("/login"), h.UserLogin)
40 | sp.HandleFunc(pat.Post("/login"), h.UserLoginPost)
41 | sp.HandleFunc(pat.Get("/register"), h.UserLogin)
42 | sp.HandleFunc(pat.Post("/register"), h.UserLoginPost)
43 |
44 | sp.HandleFunc(pat.Post("/content/preview"), h.ContentPreviewPost)
45 |
46 | sp.HandleFunc(pat.Get("/user/list"), h.UserList)
47 |
48 | sp.HandleFunc(pat.Get("/admin/post/edit/:aid"), h.ArticleEdit)
49 | sp.HandleFunc(pat.Post("/admin/post/edit/:aid"), h.ArticleEditPost)
50 | sp.HandleFunc(pat.Get("/admin/comment/edit/:aid/:cid"), h.CommentEdit)
51 | sp.HandleFunc(pat.Post("/admin/comment/edit/:aid/:cid"), h.CommentEditPost)
52 | sp.HandleFunc(pat.Get("/admin/user/edit/:uid"), h.UserEdit)
53 | sp.HandleFunc(pat.Post("/admin/user/edit/:uid"), h.UserEditPost)
54 | sp.HandleFunc(pat.Get("/admin/user/list"), h.AdminUserList)
55 | sp.HandleFunc(pat.Post("/admin/user/list"), h.AdminUserListPost)
56 | sp.HandleFunc(pat.Get("/admin/category/list"), h.AdminCategoryList)
57 | sp.HandleFunc(pat.Post("/admin/category/list"), h.AdminCategoryListPost)
58 | sp.HandleFunc(pat.Get("/admin/link/list"), h.AdminLinkList)
59 | sp.HandleFunc(pat.Post("/admin/link/list"), h.AdminLinkListPost)
60 |
61 | return sp
62 | }
63 |
--------------------------------------------------------------------------------
/view/default/desktop/user.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 |
10 |
会员:{{.Uobj.Name}} (第{{.Uobj.Id}}号会员,{{.Uobj.RegTimeFmt}}加入)
11 | {{if ge .CurrentUser.Flag 99}}
12 | • ({{.Uobj.Flag}}) 编辑
13 | {{end}}
14 |
15 |
主贴: {{.Uobj.Articles}} 回贴: {{.Uobj.Replies}}
16 |
网站: {{.Uobj.Url}}
17 |
关于:
{{.Uobj.About}}
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 | {{range $_, $item := .PageInfo.Items}}
30 |
31 |
32 |

33 |
34 |
35 |
36 |
{{$item.Cname}} • {{$item.Name}}
37 | • {{$item.EditTimeFmt}}
38 | {{if $item.Comments}}
39 | • 最后回复 {{$item.Rname}}
40 | {{end}}
41 |
42 |
43 | {{if $item.Comments}}
44 |
45 | {{end}}
46 |
47 |
48 |
49 | {{end}}
50 |
51 |
60 |
61 |
62 |
63 |
64 |
65 | {{ end}}
66 |
67 |
--------------------------------------------------------------------------------
/view/default/desktop/index.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
10 | {{if ge .CurrentUser.Flag 5}}
11 |
12 | {{end}}
13 |
14 |
15 |
16 |
17 | {{range $_, $item := .PageInfo.Items}}
18 | {{if ne $item.Cid 2}}
19 |
20 |
21 |
22 |
{{$item.Cname}} • {{$item.Name}}
23 | • {{$item.EditTimeFmt}}
24 |
25 |
26 | {{if $item.Comments}}
27 |
28 | {{end}}
29 |
30 |
31 | {{end}}
32 | {{end}}
33 |
34 |
35 |
36 |
37 | {{range $_, $item := .PageInfo.Items}}
38 | {{if eq $item.Cid 2}}
39 |
40 |
41 |
42 |
{{$item.Cname}} • {{$item.Name}}
43 | • {{$item.EditTimeFmt}}
44 |
45 |
46 | {{if $item.Comments}}
47 |
48 | {{end}}
49 |
50 |
51 | {{end}}
52 | {{end}}
53 |
54 |
55 |
56 |
57 |
58 | {{.Proverb}}
59 |
60 |
61 |
62 |
63 |
64 | {{if .PageInfo.HasPrev}}
65 |
« 上一页
66 | {{end}}
67 | {{if .PageInfo.HasNext}}
68 |
下一页 »
69 | {{end}}
70 |
71 |
72 |
73 |
74 | {{ end }}
75 |
--------------------------------------------------------------------------------
/view/default/mobile/user.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 |
10 |
会员:{{.Uobj.Name}} (第{{.Uobj.Id}}号会员,{{.Uobj.RegTimeFmt}}加入)
11 | {{if ge .CurrentUser.Flag 99}}
12 | • ({{.Uobj.Flag}}) 编辑
13 | {{end}}
14 |
15 |
主贴: {{.Uobj.Articles}} 回贴: {{.Uobj.Replies}}
16 |
网站: {{.Uobj.Url}}
17 |
关于:
{{.Uobj.About}}
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 | {{range $_, $item := .PageInfo.Items}}
30 |
31 |
32 |

33 |
34 |
35 |
36 |
37 | {{$item.Cname}}
38 | • {{$item.EditTimeFmt}}
39 | {{if $item.Comments}}
40 | • {{$item.Rname}}
41 | {{else}}
42 | • {{$item.Name}}
43 | {{end}}
44 |
45 |
46 | {{if $item.Comments}}
47 |
48 | {{end}}
49 |
50 |
51 |
52 | {{end}}
53 |
54 |
63 |
64 |
65 |
66 |
67 |
68 | {{ end}}
69 |
70 |
--------------------------------------------------------------------------------
/controller/category.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 | "strings"
7 |
8 | "github.com/terminus2049/2049bbs/model"
9 | "goji.io/pat"
10 | )
11 |
12 | func (h *BaseHandler) CategoryDetail(w http.ResponseWriter, r *http.Request) {
13 | btn, key, score := r.FormValue("btn"), r.FormValue("key"), r.FormValue("score")
14 | if len(key) > 0 {
15 | _, err := strconv.ParseUint(key, 10, 64)
16 | if err != nil {
17 | w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
18 | return
19 | }
20 | }
21 | if len(score) > 0 {
22 | _, err := strconv.ParseUint(score, 10, 64)
23 | if err != nil {
24 | w.Write([]byte(`{"retcode":400,"retmsg":"score type err"}`))
25 | return
26 | }
27 | }
28 |
29 | cid := pat.Param(r, "cid")
30 | _, err := strconv.Atoi(cid)
31 | if err != nil {
32 | w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
33 | return
34 | }
35 |
36 | cmd := "zrscan"
37 | if btn == "prev" {
38 | cmd = "zscan"
39 | }
40 |
41 | db := h.App.Db
42 | scf := h.App.Cf.Site
43 | cobj, err := model.CategoryGetById(db, cid)
44 | if err != nil {
45 | w.Write([]byte(err.Error()))
46 | return
47 | }
48 |
49 | currentUser, _ := h.CurrentUser(w, r)
50 |
51 | if cobj.Hidden && currentUser.Flag < 1 {
52 | w.WriteHeader(http.StatusNotFound)
53 | w.Write([]byte(`{"retcode":404,"retmsg":"仅登录用户可见"}`))
54 | return
55 | }
56 | pageInfo := model.ArticleList(db, cmd, "category_article_timeline:"+cid, key, score, scf.HomeShowNum, scf.TimeZone)
57 |
58 | type pageData struct {
59 | PageData
60 | Cobj model.Category
61 | PageInfo model.ArticlePageInfo
62 | }
63 |
64 | if currentUser.IgnoreUser != "" {
65 | for _, uid := range strings.Split(currentUser.IgnoreUser, ",") {
66 | uid, err := strconv.ParseUint(uid, 10, 64)
67 |
68 | if err != nil {
69 | w.Write([]byte(`{"retcode":400,"retmsg":"type err"}`))
70 | return
71 | }
72 |
73 | for i := 0; i < len(pageInfo.Items); i++ {
74 | if pageInfo.Items[i].Uid == uid {
75 | pageInfo.Items = append(pageInfo.Items[:i], pageInfo.Items[i+1:]...)
76 | i--
77 | }
78 | }
79 | }
80 | }
81 |
82 | tpl := h.CurrentTpl(r)
83 |
84 | evn := &pageData{}
85 | evn.SiteCf = scf
86 | evn.Title = cobj.Name + " - " + scf.Name
87 | evn.Keywords = cobj.Name
88 | evn.Description = cobj.About
89 | evn.IsMobile = tpl == "mobile"
90 |
91 | evn.CurrentUser = currentUser
92 | evn.ShowSideAd = true
93 | evn.PageName = "category_detail"
94 | evn.HotNodes = model.CategoryHot(db, scf.CategoryShowNum, scf.MustLoginNodeIds)
95 |
96 | evn.Cobj = cobj
97 | evn.PageInfo = pageInfo
98 |
99 | h.Render(w, tpl, evn, "layout.html", "category.html")
100 | }
101 |
--------------------------------------------------------------------------------
/view/default/desktop/category.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
5 |
首页
6 | {{if eq .Cobj.Name "时政"}}
7 |
时政
8 |
外段
9 |
水
10 | {{else if eq .Cobj.Name "外段"}}
11 |
时政
12 |
外段
13 |
水
14 | {{else if eq .Cobj.Name "水"}}
15 |
时政
16 |
外段
17 |
水
18 | {{else}}
19 |
时政
20 |
外段
21 |
水
22 |
{{.Cobj.Name}}
23 | {{end}}
24 |
25 | {{if ge .CurrentUser.Flag 5}}
26 |
27 | {{end}}
28 |
29 |
30 |
31 |
32 |
33 | {{if .Cobj.About}}
34 |
35 | {{end}}
36 |
37 | {{range $_, $item := .PageInfo.Items}}
38 |
39 |
40 |

41 |
42 |
43 |
44 |
{{$item.Cname}} • {{$item.Name}}
45 | • {{$item.EditTimeFmt}}
46 | {{if $item.Comments}}
47 | • 最后回复 {{$item.Rname}}
48 | {{end}}
49 |
50 |
51 | {{if $item.Comments}}
52 |
53 | {{end}}
54 |
55 |
56 |
57 | {{end}}
58 |
59 |
60 |
69 |
70 |
71 |
72 | {{ end}}
73 |
--------------------------------------------------------------------------------
/view/default/desktop/admincategorylist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 |
10 | {{range $_, $item := .PageInfo.Items}}
11 | -
12 | id:{{$item.Id}} - {{$item.Name}} - Articles: {{$item.Articles}} - Hidden: {{$item.Hidden}}
13 | 查看
14 | 编辑
15 |
16 | {{end}}
17 |
18 |
19 |
28 |
29 |
30 |
31 |
34 |
35 |
49 |
50 |
81 |
82 | {{ end}}
83 |
84 |
--------------------------------------------------------------------------------
/view/default/mobile/admincategorylist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
8 |
9 |
10 | {{range $_, $item := .PageInfo.Items}}
11 | -
12 | id:{{$item.Id}} - {{$item.Name}} - Articles: {{$item.Articles}} - Hidden: {{$item.Hidden}}
13 | 查看
14 | 编辑
15 |
16 | {{end}}
17 |
18 |
19 |
28 |
29 |
30 |
31 |
34 |
35 |
49 |
50 |
81 |
82 | {{ end}}
83 |
84 |
--------------------------------------------------------------------------------
/view/default/mobile/category.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
4 |
5 |
首页
6 | {{if eq .Cobj.Name "时政"}}
7 |
时政
8 |
外段
9 |
水
10 | {{else if eq .Cobj.Name "外段"}}
11 |
时政
12 |
外段
13 |
水
14 | {{else if eq .Cobj.Name "水"}}
15 |
时政
16 |
外段
17 |
水
18 | {{else}}
19 |
时政
20 |
外段
21 |
水
22 |
{{.Cobj.Name}}
23 | {{end}}
24 |
25 | {{if ge .CurrentUser.Flag 5}}
26 |
27 | {{end}}
28 |
29 |
30 |
31 |
32 |
33 | {{if .Cobj.About}}
34 |
35 | {{end}}
36 |
37 | {{range $_, $item := .PageInfo.Items}}
38 |
39 |
40 |

41 |
42 |
43 |
44 |
45 | {{$item.Cname}}
46 | • {{$item.EditTimeFmt}}
47 | {{if $item.Comments}}
48 | • {{$item.Rname}}
49 | {{else}}
50 | • {{$item.Name}}
51 | {{end}}
52 |
53 |
54 | {{if $item.Comments}}
55 |
56 | {{end}}
57 |
58 |
59 |
60 | {{end}}
61 |
62 |
63 |
72 |
73 |
74 |
75 | {{ end}}
76 |
--------------------------------------------------------------------------------
/view/default/desktop/adminuserlist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
14 |
15 |
16 |
17 |
18 | {{range $_, $item := .PageInfo.Items}}
19 | -
20 | id:{{$item.Id}} - {{$item.Name}} - flag: {{$item.Flag}} - info: {{$item.About}}
21 | 查看
22 | 编辑
23 |
24 | {{end}}
25 |
26 |
27 |
36 |
37 |
38 |
39 |
42 |
43 |
53 |
54 |
83 |
84 | {{ end}}
85 |
--------------------------------------------------------------------------------
/view/default/mobile/adminuserlist.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
14 |
15 |
16 |
17 |
18 | {{range $_, $item := .PageInfo.Items}}
19 | -
20 | id:{{$item.Id}} - {{$item.Name}} - flag: {{$item.Flag}} - info: {{$item.About}}
21 | 查看
22 | 编辑
23 |
24 | {{end}}
25 |
26 |
27 |
36 |
37 |
38 |
39 |
42 |
43 |
53 |
54 |
83 |
84 | {{ end}}
85 |
--------------------------------------------------------------------------------
/controller/adminlinklist.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/terminus2049/2049bbs/model"
6 | "github.com/rs/xid"
7 | "net/http"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | func (h *BaseHandler) AdminLinkList(w http.ResponseWriter, r *http.Request) {
13 | lid := r.FormValue("lid")
14 |
15 | db := h.App.Db
16 |
17 | var lobj model.Link
18 | if len(lid) > 0 {
19 | _, err := strconv.ParseUint(lid, 10, 64)
20 | if err != nil {
21 | w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
22 | return
23 | }
24 |
25 | lobj = model.LinkGetById(db, lid)
26 | if lobj.Id == 0 {
27 | w.Write([]byte(`{"retcode":404,"retmsg":"id not found"}`))
28 | return
29 | }
30 | } else {
31 | lobj.Score = 10
32 | }
33 |
34 | currentUser, _ := h.CurrentUser(w, r)
35 | if currentUser.Id == 0 {
36 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
37 | return
38 | }
39 | if currentUser.Flag < 99 {
40 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden"}`))
41 | return
42 | }
43 |
44 | type pageData struct {
45 | PageData
46 | Items []model.Link
47 | Lobj model.Link
48 | }
49 |
50 | tpl := h.CurrentTpl(r)
51 | evn := &pageData{}
52 | evn.SiteCf = h.App.Cf.Site
53 | evn.Title = "链接列表"
54 | evn.IsMobile = tpl == "mobile"
55 | evn.CurrentUser = currentUser
56 | evn.ShowSideAd = true
57 | evn.PageName = "user_list"
58 |
59 | evn.Items = model.LinkList(db, true)
60 | evn.Lobj = lobj
61 |
62 | token := h.GetCookie(r, "token")
63 | if len(token) == 0 {
64 | token := xid.New().String()
65 | h.SetCookie(w, "token", token, 1)
66 | }
67 |
68 | h.Render(w, tpl, evn, "layout.html", "adminlinklist.html")
69 | }
70 |
71 | func (h *BaseHandler) AdminLinkListPost(w http.ResponseWriter, r *http.Request) {
72 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
73 | token := h.GetCookie(r, "token")
74 | if len(token) == 0 {
75 | w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
76 | return
77 | }
78 |
79 | currentUser, _ := h.CurrentUser(w, r)
80 | if currentUser.Id == 0 {
81 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
82 | return
83 | }
84 | if currentUser.Flag < 99 {
85 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
86 | return
87 | }
88 |
89 | type response struct {
90 | normalRsp
91 | }
92 |
93 | decoder := json.NewDecoder(r.Body)
94 | var rec model.Link
95 | err := decoder.Decode(&rec)
96 | if err != nil {
97 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
98 | return
99 | }
100 | defer r.Body.Close()
101 |
102 | rec.Name = strings.TrimSpace(rec.Name)
103 | rec.Url = strings.TrimSpace(rec.Url)
104 |
105 | if len(rec.Name) == 0 || len(rec.Url) == 0 {
106 | w.Write([]byte(`{"retcode":400,"retmsg":"missed arg"}`))
107 | return
108 | }
109 |
110 | model.LinkSet(h.App.Db, rec)
111 |
112 | rsp := response{}
113 | rsp.Retcode = 200
114 | json.NewEncoder(w).Encode(rsp)
115 | }
116 |
--------------------------------------------------------------------------------
/controller/feed.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "strconv"
6 | "text/template"
7 |
8 | "github.com/terminus2049/2049bbs/model"
9 | "goji.io/pat"
10 | )
11 |
12 | func (h *BaseHandler) FeedHandler(w http.ResponseWriter, r *http.Request) {
13 | w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
14 |
15 | scf := h.App.Cf.Site
16 |
17 | var feed = `
18 |
19 | ` + scf.Name + `
20 |
21 |
22 | {{.Update}}
23 | ` + scf.MainDomain + `/feed
24 |
25 | ` + scf.Name + `
26 |
27 | {{range $_, $item := .Items}}
28 |
29 | {{$item.Title}}
30 | ` + scf.MainDomain + `/t/{{$item.Id}}
31 |
32 | {{$item.AddTimeFmt}}
33 | {{$item.EditTimeFmt}}
34 |
35 | {{$item.Cname}} - {{$item.Name}} - {{$item.Des}}
36 |
37 |
38 | {{end}}
39 |
40 | `
41 |
42 | db := h.App.Db
43 |
44 | items := model.ArticleFeedList(db, 20, h.App.Cf.Site.TimeZone)
45 |
46 | var upDate string
47 | if len(items) > 0 {
48 | upDate = items[0].AddTimeFmt
49 | }
50 |
51 | t := template.Must(template.New("feed").Parse(feed))
52 | t.Execute(w, struct {
53 | Update string
54 | Items []model.ArticleFeedListItem
55 | }{
56 | Update: upDate,
57 | Items: items,
58 | })
59 | }
60 |
61 | func (h *BaseHandler) FeedCategoryHandler(w http.ResponseWriter, r *http.Request) {
62 | w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
63 |
64 | scf := h.App.Cf.Site
65 |
66 | var feed = `
67 |
68 | ` + scf.Name + `
69 |
70 |
71 | {{.Update}}
72 | ` + scf.MainDomain + `/feed
73 |
74 | ` + scf.Name + `
75 |
76 | {{range $_, $item := .Items}}
77 |
78 | {{$item.Title}}
79 | ` + scf.MainDomain + `/t/{{$item.Id}}
80 |
81 | {{$item.AddTimeFmt}}
82 | {{$item.EditTimeFmt}}
83 |
84 | {{$item.Cname}} - {{$item.Name}} - {{$item.Des}}
85 |
86 |
87 | {{end}}
88 |
89 | `
90 |
91 | cid := pat.Param(r, "cid")
92 | _, err := strconv.Atoi(cid)
93 | if err != nil {
94 | w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
95 | return
96 | }
97 |
98 | db := h.App.Db
99 |
100 | items := model.ArticleFeedCategoryList(db, cid, 20, h.App.Cf.Site.TimeZone)
101 |
102 | var upDate string
103 | if len(items) > 0 {
104 | upDate = items[0].AddTimeFmt
105 | }
106 |
107 | t := template.Must(template.New("feed").Parse(feed))
108 | t.Execute(w, struct {
109 | Update string
110 | Items []model.ArticleFeedListItem
111 | }{
112 | Update: upDate,
113 | Items: items,
114 | })
115 | }
116 |
--------------------------------------------------------------------------------
/view/default/desktop/admincommentedit.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
22 |
23 |
93 |
94 |
95 | {{ end}}
96 |
--------------------------------------------------------------------------------
/view/default/mobile/admincommentedit.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
22 |
23 |
94 |
95 |
96 | {{ end}}
97 |
--------------------------------------------------------------------------------
/static/js/md5.min.js:
--------------------------------------------------------------------------------
1 | !function(n){"use strict";function t(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function r(n,t){return n<>>32-t}function e(n,e,o,u,c,f){return t(r(t(t(e,n),t(u,f)),c),o)}function o(n,t,r,o,u,c,f){return e(t&r|~t&o,n,t,u,c,f)}function u(n,t,r,o,u,c,f){return e(t&o|r&~o,n,t,u,c,f)}function c(n,t,r,o,u,c,f){return e(t^r^o,n,t,u,c,f)}function f(n,t,r,o,u,c,f){return e(r^(t|~o),n,t,u,c,f)}function i(n,r){n[r>>5]|=128<>>9<<4)]=r;var e,i,a,d,h,l=1732584193,g=-271733879,v=-1732584194,m=271733878;for(e=0;e>5]>>>t%32&255);return r}function d(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<16&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(d(t)),512+8*t.length),a(i(c.concat(e),640))}function g(n){var t,r,e="";for(r=0;r>>4&15)+"0123456789abcdef".charAt(15&t);return e}function v(n){return unescape(encodeURIComponent(n))}function m(n){return h(v(n))}function p(n){return g(m(n))}function s(n,t){return l(v(n),v(t))}function C(n,t){return g(s(n,t))}function A(n,t,r){return t?r?s(t,n):C(t,n):r?m(n):p(n)}"function"==typeof define&&define.amd?define(function(){return A}):"object"==typeof module&&module.exports?module.exports=A:n.md5=A}(this);
2 | //# sourceMappingURL=md5.min.js.map
--------------------------------------------------------------------------------
/view/default/desktop/userlogin.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
35 |
36 |
98 |
99 | {{ end}}
100 |
--------------------------------------------------------------------------------
/view/default/mobile/userlogin.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
35 |
36 |
98 |
99 | {{ end}}
100 |
--------------------------------------------------------------------------------
/controller/basecontroller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "github.com/terminus2049/2049bbs/model"
7 | "github.com/terminus2049/2049bbs/system"
8 | "github.com/ego008/youdb"
9 | "html/template"
10 | "net/http"
11 | "regexp"
12 | "strings"
13 | "time"
14 | )
15 |
16 | var mobileRegexp = regexp.MustCompile(`Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|NetFront|Silk-Accelerated|(hpw|web)OS|Fennec|Minimo|Opera M(obi|ini)|Blazer|Dolfin|Dolphin|Skyfire|Zune`)
17 |
18 | type (
19 | BaseHandler struct {
20 | App *system.Application
21 | }
22 |
23 | PageData struct {
24 | SiteCf *system.SiteConf
25 | Title string
26 | Keywords string
27 | Description string
28 | IsMobile bool
29 | CurrentUser model.User
30 | PageName string // index/post_add/post_detail/...
31 | ShowPostTopAd bool
32 | ShowPostBotAd bool
33 | ShowSideAd bool
34 | HotNodes []model.CategoryMini
35 | NewestNodes []model.CategoryMini
36 | }
37 | normalRsp struct {
38 | Retcode int `json:"retcode"`
39 | Retmsg string `json:"retmsg"`
40 | }
41 | )
42 |
43 | func (h *BaseHandler) Render(w http.ResponseWriter, tpl string, data interface{}, tplPath ...string) error {
44 | if len(tplPath) == 0 {
45 | return errors.New("File path can not be empty ")
46 | }
47 |
48 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
49 | w.Header().Set("Server", "GoYouBBS")
50 |
51 | tplDir := h.App.Cf.Main.ViewDir + "/" + tpl + "/"
52 | tmpl := template.New("youbbs")
53 | for _, tpath := range tplPath {
54 | tmpl = template.Must(tmpl.ParseFiles(tplDir + tpath))
55 | }
56 | err := tmpl.Execute(w, data)
57 |
58 | return err
59 | }
60 |
61 | func (h *BaseHandler) CurrentUser(w http.ResponseWriter, r *http.Request) (model.User, error) {
62 | var user model.User
63 | ssValue := h.GetCookie(r, "SessionID")
64 | if len(ssValue) == 0 {
65 | return user, errors.New("SessionID cookie not found ")
66 | }
67 | z := strings.Split(ssValue, ":")
68 | uid := z[0]
69 | sessionID := z[1]
70 |
71 | rs := h.App.Db.Hget("user", youdb.DS2b(uid))
72 | if rs.State == "ok" {
73 | json.Unmarshal(rs.Data[0], &user)
74 | if sessionID == user.Session {
75 | h.SetCookie(w, "SessionID", ssValue, 365)
76 | return user, nil
77 | }
78 | }
79 |
80 | return user, errors.New("user not found")
81 | }
82 |
83 | func (h *BaseHandler) SetCookie(w http.ResponseWriter, name, value string, days int) error {
84 | encoded, err := h.App.Sc.Encode(name, value)
85 | if err != nil {
86 | return err
87 | }
88 | http.SetCookie(w, &http.Cookie{
89 | Name: name,
90 | Value: encoded,
91 | Path: "/",
92 | Secure: h.App.Cf.Main.CookieSecure,
93 | HttpOnly: h.App.Cf.Main.CookieHttpOnly,
94 | Expires: time.Now().UTC().AddDate(0, 0, days),
95 | })
96 | return err
97 | }
98 |
99 | func (h *BaseHandler) GetCookie(r *http.Request, name string) string {
100 | if cookie, err := r.Cookie(name); err == nil {
101 | var value string
102 | if err = h.App.Sc.Decode(name, cookie.Value, &value); err == nil {
103 | return value
104 | }
105 | }
106 | return ""
107 | }
108 |
109 | func (h *BaseHandler) DelCookie(w http.ResponseWriter, name string) {
110 | if len(name) > 0 {
111 | http.SetCookie(w, &http.Cookie{
112 | Name: name,
113 | Value: "",
114 | Path: "/",
115 | Secure: h.App.Cf.Main.CookieSecure,
116 | HttpOnly: h.App.Cf.Main.CookieHttpOnly,
117 | Expires: time.Unix(0, 0),
118 | })
119 | }
120 | }
121 |
122 | func (h *BaseHandler) CurrentTpl(r *http.Request) string {
123 | tpl := "desktop"
124 | //tpl := "mobile"
125 |
126 | cookieTpl := h.GetCookie(r, "tpl")
127 | if len(cookieTpl) > 0 {
128 | if cookieTpl == "desktop" || cookieTpl == "mobile" {
129 | return cookieTpl
130 | }
131 | }
132 |
133 | ua := r.Header.Get("User-Agent")
134 | if len(ua) < 6 {
135 | return tpl
136 | }
137 | if mobileRegexp.MatchString(ua) {
138 | return "mobile"
139 | }
140 | return tpl
141 | }
142 |
--------------------------------------------------------------------------------
/controller/admincategorylist.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/ego008/youdb"
9 | "github.com/rs/xid"
10 | "github.com/terminus2049/2049bbs/model"
11 | )
12 |
13 | func (h *BaseHandler) AdminCategoryList(w http.ResponseWriter, r *http.Request) {
14 | cid, btn, key := r.FormValue("cid"), r.FormValue("btn"), r.FormValue("key")
15 | if len(key) > 0 {
16 | _, err := strconv.ParseUint(key, 10, 64)
17 | if err != nil {
18 | w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
19 | return
20 | }
21 | }
22 |
23 | currentUser, _ := h.CurrentUser(w, r)
24 | if currentUser.Id == 0 {
25 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
26 | return
27 | }
28 | if currentUser.Flag < 99 {
29 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
30 | return
31 | }
32 |
33 | cmd := "hrscan"
34 | if btn == "prev" {
35 | cmd = "hscan"
36 | }
37 |
38 | db := h.App.Db
39 |
40 | var err error
41 | var cobj model.Category
42 | if len(cid) > 0 {
43 | cobj, err = model.CategoryGetById(db, cid)
44 | if err != nil {
45 | cobj = model.Category{}
46 | }
47 | }
48 |
49 | pageInfo := model.CategoryList(db, cmd, key, h.App.Cf.Site.PageShowNum)
50 |
51 | for i := 0; i < len(pageInfo.Items); i++ {
52 | pageInfo.Items[i].Articles = db.Zget("category_article_num", youdb.I2b(pageInfo.Items[i].Id)).Uint64()
53 | }
54 |
55 | type pageData struct {
56 | PageData
57 | PageInfo model.CategoryPageInfo
58 | Cobj model.Category
59 | }
60 |
61 | tpl := h.CurrentTpl(r)
62 | evn := &pageData{}
63 | evn.SiteCf = h.App.Cf.Site
64 | evn.Title = "分类列表"
65 | evn.IsMobile = tpl == "mobile"
66 | evn.CurrentUser = currentUser
67 | evn.ShowSideAd = true
68 | evn.PageName = "category_list"
69 |
70 | evn.PageInfo = pageInfo
71 | evn.Cobj = cobj
72 |
73 | token := h.GetCookie(r, "token")
74 | if len(token) == 0 {
75 | token := xid.New().String()
76 | h.SetCookie(w, "token", token, 1)
77 | }
78 |
79 | h.Render(w, tpl, evn, "layout.html", "admincategorylist.html")
80 | }
81 |
82 | func (h *BaseHandler) AdminCategoryListPost(w http.ResponseWriter, r *http.Request) {
83 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
84 | token := h.GetCookie(r, "token")
85 | if len(token) == 0 {
86 | w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
87 | return
88 | }
89 |
90 | currentUser, _ := h.CurrentUser(w, r)
91 | if currentUser.Id == 0 {
92 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
93 | return
94 | }
95 | if currentUser.Flag < 99 {
96 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
97 | return
98 | }
99 |
100 | type recForm struct {
101 | Cid uint64 `json:"cid"`
102 | Name string `json:"name"`
103 | About string `json:"about"`
104 | Hidden string `json:"hidden"`
105 | }
106 |
107 | type response struct {
108 | normalRsp
109 | }
110 |
111 | decoder := json.NewDecoder(r.Body)
112 | var rec recForm
113 | err := decoder.Decode(&rec)
114 | if err != nil {
115 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
116 | return
117 | }
118 | defer r.Body.Close()
119 |
120 | if len(rec.Name) == 0 {
121 | w.Write([]byte(`{"retcode":400,"retmsg":"name is empty"}`))
122 | return
123 | }
124 |
125 | db := h.App.Db
126 |
127 | var hidden bool
128 | if rec.Hidden == "1" {
129 | hidden = true
130 | }
131 |
132 | var cobj model.Category
133 | if rec.Cid > 0 {
134 | // edit
135 | cobj, err = model.CategoryGetById(db, strconv.FormatUint(rec.Cid, 10))
136 | if err != nil {
137 | w.Write([]byte(`{"retcode":404,"retmsg":"cid not found"}`))
138 | return
139 | }
140 | } else {
141 | // add
142 | newCid, _ := db.HnextSequence("category")
143 | cobj.Id = newCid
144 | }
145 |
146 | cobj.Name = rec.Name
147 | cobj.About = rec.About
148 | cobj.Hidden = hidden
149 |
150 | jb, _ := json.Marshal(cobj)
151 | db.Hset("category", youdb.I2b(cobj.Id), jb)
152 |
153 | rsp := response{}
154 | rsp.Retcode = 200
155 | json.NewEncoder(w).Encode(rsp)
156 | }
157 |
--------------------------------------------------------------------------------
/controller/adminuserlist.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "math/rand"
6 | "net/http"
7 | "strconv"
8 | "strings"
9 | "time"
10 |
11 | "github.com/ego008/youdb"
12 | "github.com/rs/xid"
13 | "github.com/terminus2049/2049bbs/model"
14 | )
15 |
16 | func (h *BaseHandler) AdminUserList(w http.ResponseWriter, r *http.Request) {
17 | flag, btn, key := r.FormValue("flag"), r.FormValue("btn"), r.FormValue("key")
18 | if len(key) > 0 {
19 | _, err := strconv.ParseUint(key, 10, 64)
20 | if err != nil {
21 | w.Write([]byte(`{"retcode":400,"retmsg":"key type err"}`))
22 | return
23 | }
24 | }
25 |
26 | currentUser, _ := h.CurrentUser(w, r)
27 | if currentUser.Id == 0 {
28 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
29 | return
30 | }
31 | if currentUser.Flag < 99 {
32 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
33 | return
34 | }
35 |
36 | cmd := "hrscan"
37 | if btn == "prev" {
38 | cmd = "hscan"
39 | }
40 |
41 | db := h.App.Db
42 |
43 | if len(flag) == 0 {
44 | flag = "5"
45 | }
46 |
47 | pageInfo := model.UserListByFlag(db, cmd, "user_flag:"+flag, key, h.App.Cf.Site.PageShowNum)
48 |
49 | type pageData struct {
50 | PageData
51 | PageInfo model.UserPageInfo
52 | Flag string
53 | }
54 |
55 | tpl := h.CurrentTpl(r)
56 | evn := &pageData{}
57 | evn.SiteCf = h.App.Cf.Site
58 | evn.Title = "用户列表"
59 | evn.IsMobile = tpl == "mobile"
60 | evn.CurrentUser = currentUser
61 | evn.ShowSideAd = true
62 | evn.PageName = "user_list"
63 |
64 | evn.PageInfo = pageInfo
65 | evn.Flag = flag
66 |
67 | token := h.GetCookie(r, "token")
68 | if len(token) == 0 {
69 | token := xid.New().String()
70 | h.SetCookie(w, "token", token, 1)
71 | }
72 |
73 | h.Render(w, tpl, evn, "layout.html", "adminuserlist.html")
74 | }
75 |
76 | func (h *BaseHandler) AdminUserListPost(w http.ResponseWriter, r *http.Request) {
77 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
78 | token := h.GetCookie(r, "token")
79 | if len(token) == 0 {
80 | w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
81 | return
82 | }
83 |
84 | currentUser, _ := h.CurrentUser(w, r)
85 | if currentUser.Id == 0 {
86 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
87 | return
88 | }
89 | if currentUser.Flag < 99 {
90 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
91 | return
92 | }
93 |
94 | type recForm struct {
95 | Name string `json:"name"`
96 | Password string `json:"password"`
97 | }
98 |
99 | type response struct {
100 | normalRsp
101 | }
102 |
103 | decoder := json.NewDecoder(r.Body)
104 | var rec recForm
105 | err := decoder.Decode(&rec)
106 | if err != nil {
107 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
108 | return
109 | }
110 | defer r.Body.Close()
111 |
112 | if len(rec.Name) == 0 || len(rec.Password) == 0 {
113 | w.Write([]byte(`{"retcode":400,"retmsg":"name or pw is empty"}`))
114 | return
115 | }
116 | nameLow := strings.ToLower(rec.Name)
117 | db := h.App.Db
118 | timeStamp := uint64(time.Now().UTC().Unix())
119 |
120 | if db.Hget("user_name2uid", []byte(nameLow)).State == "ok" {
121 | w.Write([]byte(`{"retcode":400,"retmsg":"name is exist"}`))
122 | return
123 | }
124 |
125 | userId, _ := db.HnextSequence("user")
126 | flag := 5
127 |
128 | uobj := model.User{
129 | Id: userId,
130 | Name: rec.Name,
131 | Password: rec.Password,
132 | Flag: flag,
133 | RegTime: timeStamp,
134 | LastLoginTime: timeStamp,
135 | }
136 |
137 | rand.Seed(time.Now().UnixNano())
138 | min := 2539
139 | max := 2558
140 | sampleID := rand.Intn(max-min+1) + min
141 | uidStr := strconv.FormatUint(uint64(sampleID), 10)
142 | uobj.Avatar = uidStr
143 |
144 | jb, _ := json.Marshal(uobj)
145 | db.Hset("user", youdb.I2b(uobj.Id), jb)
146 | db.Hset("user_name2uid", []byte(nameLow), youdb.I2b(userId))
147 | db.Hset("user_flag:"+strconv.Itoa(flag), youdb.I2b(uobj.Id), []byte(""))
148 |
149 | rsp := response{}
150 | rsp.Retcode = 200
151 | json.NewEncoder(w).Encode(rsp)
152 | }
153 |
--------------------------------------------------------------------------------
/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 |
7 | "github.com/ego008/youdb"
8 | )
9 |
10 | type User struct {
11 | Id uint64 `json:"id"`
12 | Name string `json:"name"`
13 | Gender string `json:"gender"`
14 | Flag int `json:"flag"`
15 | Avatar string `json:"avatar"`
16 | Password string `json:"password"`
17 | Email string `json:"email"`
18 | Url string `json:"url"`
19 | Articles uint64 `json:"articles"`
20 | Replies uint64 `json:"replies"`
21 | RegTime uint64 `json:"regtime"`
22 | LastPostTime uint64 `json:"lastposttime"`
23 | LastReplyTime uint64 `json:"lastreplytime"`
24 | LastLoginTime uint64 `json:"lastlogintime"`
25 | About string `json:"about"`
26 | Notice string `json:"notice"`
27 | NoticeNum int `json:"noticenum"`
28 | Hidden bool `json:"hidden"`
29 | Session string `json:"session"`
30 | IgnoreNode string `json:"ignorenode"`
31 | IgnoreUser string `json:"ignoreuser"`
32 | Theme string `json:"theme"`
33 | IgnoreLimitedUsers bool `json:"ignorelimitedusers"`
34 | }
35 |
36 | type UserMini struct {
37 | Id uint64 `json:"id"`
38 | Name string `json:"name"`
39 | Avatar string `json:"avatar"`
40 | Flag int `json:"flag"`
41 | }
42 |
43 | type UserPageInfo struct {
44 | Items []User `json:"items"`
45 | HasPrev bool `json:"hasprev"`
46 | HasNext bool `json:"hasnext"`
47 | FirstKey uint64 `json:"firstkey"`
48 | LastKey uint64 `json:"lastkey"`
49 | }
50 |
51 | func UserGetById(db *youdb.DB, uid uint64) (User, error) {
52 | obj := User{}
53 | rs := db.Hget("user", youdb.I2b(uid))
54 | if rs.State == "ok" {
55 | json.Unmarshal(rs.Data[0], &obj)
56 | return obj, nil
57 | }
58 | return obj, errors.New(rs.State)
59 | }
60 |
61 | func UserUpdate(db *youdb.DB, obj User) error {
62 | jb, _ := json.Marshal(obj)
63 | return db.Hset("user", youdb.I2b(obj.Id), jb)
64 | }
65 |
66 | func UserGetByName(db *youdb.DB, name string) (User, error) {
67 | obj := User{}
68 | rs := db.Hget("user_name2uid", []byte(name))
69 | if rs.State == "ok" {
70 | rs2 := db.Hget("user", rs.Data[0])
71 | if rs2.State == "ok" {
72 | json.Unmarshal(rs2.Data[0], &obj)
73 | return obj, nil
74 | }
75 | return obj, errors.New(rs2.State)
76 | }
77 | return obj, errors.New(rs.State)
78 | }
79 |
80 | func UserGetIdByName(db *youdb.DB, name string) string {
81 | rs := db.Hget("user_name2uid", []byte(name))
82 | if rs.State == "ok" {
83 | return youdb.B2ds(rs.Data[0])
84 | }
85 | return ""
86 | }
87 |
88 | func UserListByFlag(db *youdb.DB, cmd, tb, key string, limit int) UserPageInfo {
89 | var items []User
90 | var keys [][]byte
91 | var hasPrev, hasNext bool
92 | var firstKey, lastKey uint64
93 |
94 | keyStart := youdb.DS2b(key)
95 | if cmd == "hrscan" {
96 | rs := db.Hrscan(tb, keyStart, limit)
97 | if rs.State == "ok" {
98 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
99 | keys = append(keys, rs.Data[i])
100 | }
101 | }
102 | } else if cmd == "hscan" {
103 | rs := db.Hscan(tb, keyStart, limit)
104 | if rs.State == "ok" {
105 | for i := len(rs.Data) - 2; i >= 0; i -= 2 {
106 | keys = append(keys, rs.Data[i])
107 | }
108 | }
109 | }
110 |
111 | if len(keys) > 0 {
112 | rs := db.Hmget("user", keys)
113 | if rs.State == "ok" {
114 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
115 | item := User{}
116 | json.Unmarshal(rs.Data[i+1], &item)
117 | items = append(items, item)
118 | if firstKey == 0 {
119 | firstKey = item.Id
120 | }
121 | lastKey = item.Id
122 | }
123 |
124 | rs = db.Hscan(tb, youdb.I2b(firstKey), 1)
125 | if rs.State == "ok" {
126 | hasPrev = true
127 | }
128 | rs = db.Hrscan(tb, youdb.I2b(lastKey), 1)
129 | if rs.State == "ok" {
130 | hasNext = true
131 | }
132 | }
133 | }
134 |
135 | return UserPageInfo{
136 | Items: items,
137 | HasPrev: hasPrev,
138 | HasNext: hasNext,
139 | FirstKey: firstKey,
140 | LastKey: lastKey,
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/view/default/desktop/articlecreate.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
37 |
38 |
107 |
108 |
109 | {{ end}}
110 |
--------------------------------------------------------------------------------
/view/default/mobile/articlecreate.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
39 |
40 |
109 |
110 |
111 | {{ end}}
112 |
--------------------------------------------------------------------------------
/controller/admincommentedit.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/rs/xid"
9 | "github.com/terminus2049/2049bbs/model"
10 | "github.com/terminus2049/2049bbs/util"
11 | "goji.io/pat"
12 | )
13 |
14 | func (h *BaseHandler) CommentEdit(w http.ResponseWriter, r *http.Request) {
15 | aid, cid := pat.Param(r, "aid"), pat.Param(r, "cid")
16 | _, err := strconv.ParseUint(aid, 10, 64)
17 | if err != nil {
18 | w.Write([]byte(`{"retcode":400,"retmsg":"aid type err"}`))
19 | return
20 | }
21 | cidI, err := strconv.ParseUint(cid, 10, 64)
22 | if err != nil {
23 | w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
24 | return
25 | }
26 |
27 | currentUser, _ := h.CurrentUser(w, r)
28 | if currentUser.Id == 0 {
29 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
30 | return
31 | }
32 | if currentUser.Flag < 99 {
33 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
34 | return
35 | }
36 |
37 | db := h.App.Db
38 |
39 | aobj, _ := model.ArticleGetById(db, aid)
40 |
41 | // comment
42 | cobj, err := model.CommentGetByKey(db, aid, cidI)
43 | if err != nil {
44 | w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
45 | return
46 | }
47 |
48 | act := r.FormValue("act")
49 |
50 | if act == "del" {
51 | // remove
52 | model.CommentDelByKey(db, aid, cidI)
53 | http.Redirect(w, r, "/", http.StatusSeeOther)
54 | return
55 | }
56 |
57 | if act == "fold" {
58 | // 折叠
59 | cobj.Fold = !cobj.Fold
60 | model.CommentSetByKey(db, aid, cidI, cobj)
61 | http.Redirect(w, r, "/t/"+aid, http.StatusSeeOther)
62 | h.DelCookie(w, "token")
63 | return
64 | }
65 |
66 | type pageData struct {
67 | PageData
68 | Aobj model.Article
69 | Cobj model.Comment
70 | }
71 |
72 | tpl := h.CurrentTpl(r)
73 | evn := &pageData{}
74 | evn.SiteCf = h.App.Cf.Site
75 | evn.Title = "修改评论"
76 | evn.IsMobile = tpl == "mobile"
77 | evn.CurrentUser = currentUser
78 | evn.ShowSideAd = true
79 | evn.PageName = "comment_edit"
80 |
81 | evn.Aobj = aobj
82 | evn.Cobj = cobj
83 |
84 | h.SetCookie(w, "token", xid.New().String(), 1)
85 | h.Render(w, tpl, evn, "layout.html", "admincommentedit.html")
86 | }
87 |
88 | func (h *BaseHandler) CommentEditPost(w http.ResponseWriter, r *http.Request) {
89 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
90 |
91 | aid, cid := pat.Param(r, "aid"), pat.Param(r, "cid")
92 | aidI, err := strconv.ParseUint(aid, 10, 64)
93 | if err != nil {
94 | w.Write([]byte(`{"retcode":400,"retmsg":"aid type err"}`))
95 | return
96 | }
97 | cidI, err := strconv.ParseUint(cid, 10, 64)
98 | if err != nil {
99 | w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
100 | return
101 | }
102 |
103 | currentUser, _ := h.CurrentUser(w, r)
104 | if currentUser.Id == 0 {
105 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
106 | return
107 | }
108 | if currentUser.Flag < 99 {
109 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
110 | return
111 | }
112 |
113 | db := h.App.Db
114 |
115 | // comment
116 | cobj, err := model.CommentGetByKey(db, aid, cidI)
117 | if err != nil {
118 | w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
119 | return
120 | }
121 |
122 | type recForm struct {
123 | Act string `json:"act"`
124 | Content string `json:"content"`
125 | }
126 |
127 | decoder := json.NewDecoder(r.Body)
128 | var rec recForm
129 | err = decoder.Decode(&rec)
130 | if err != nil {
131 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
132 | return
133 | }
134 | defer r.Body.Close()
135 |
136 | if rec.Act == "preview" {
137 | tmp := struct {
138 | normalRsp
139 | Html string `json:"html"`
140 | }{
141 | normalRsp{200, ""},
142 | util.ContentFmt(db, rec.Content),
143 | }
144 | json.NewEncoder(w).Encode(tmp)
145 | return
146 | }
147 |
148 | oldContent := cobj.Content
149 |
150 | if oldContent == rec.Content {
151 | w.Write([]byte(`{"retcode":201,"retmsg":"nothing changed"}`))
152 | return
153 | }
154 |
155 | cobj.Content = rec.Content
156 |
157 | model.CommentSetByKey(db, aid, cidI, cobj)
158 |
159 | h.DelCookie(w, "token")
160 |
161 | tmp := struct {
162 | normalRsp
163 | Aid uint64 `json:"aid"`
164 | }{
165 | normalRsp{200, "ok"},
166 | aidI,
167 | }
168 | json.NewEncoder(w).Encode(tmp)
169 | }
170 |
--------------------------------------------------------------------------------
/view/default/desktop/adminarticleedit.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
35 |
36 |
105 |
106 |
107 | {{ end}}
108 |
--------------------------------------------------------------------------------
/model/category.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/ego008/youdb"
10 | )
11 |
12 | type Category struct {
13 | Id uint64 `json:"id"`
14 | Name string `json:"name"`
15 | Articles uint64 `json:"articles"`
16 | About string `json:"about"`
17 | Hidden bool `json:"hidden"`
18 | }
19 |
20 | type CategoryMini struct {
21 | Id uint64 `json:"id"`
22 | Name string `json:"name"`
23 | }
24 |
25 | type CategoryPageInfo struct {
26 | Items []Category `json:"items"`
27 | HasPrev bool `json:"hasprev"`
28 | HasNext bool `json:"hasnext"`
29 | FirstKey uint64 `json:"firstkey"`
30 | LastKey uint64 `json:"lastkey"`
31 | }
32 |
33 | func CategoryGetById(db *youdb.DB, cid string) (Category, error) {
34 | obj := Category{}
35 | rs := db.Hget("category", youdb.DS2b(cid))
36 | if rs.State == "ok" {
37 | json.Unmarshal(rs.Data[0], &obj)
38 | return obj, nil
39 | }
40 | return obj, errors.New(rs.State)
41 | }
42 |
43 | func CategoryHot(db *youdb.DB, limit int, hide string) []CategoryMini {
44 | var items []CategoryMini
45 | rs := db.Zrscan("category_article_num", []byte(""), []byte(""), limit)
46 | if rs.State == "ok" {
47 | var keys [][]byte
48 | for i := 0; i < len(rs.Data)-1; i += 2 {
49 | keys = append(keys, rs.Data[i])
50 | }
51 | if len(keys) > 0 {
52 | rs2 := db.Hmget("category", keys)
53 | if rs2.State == "ok" {
54 | for i := 0; i < len(rs2.Data)-1; i += 2 {
55 | item := CategoryMini{}
56 | json.Unmarshal(rs2.Data[i+1], &item)
57 | items = append(items, item)
58 | }
59 | }
60 | }
61 | }
62 |
63 | if len(hide) > 0 {
64 | for _, node := range strings.Split(hide, ",") {
65 | node, err := strconv.ParseUint(node, 10, 64)
66 | if err == nil {
67 | for i := 0; i < len(items); i++ {
68 | if items[i].Id == node {
69 | items = append(items[:i], items[i+1:]...)
70 | i--
71 | }
72 | }
73 | }
74 | }
75 | }
76 |
77 | return items
78 | }
79 |
80 | func CategoryGetMain(db *youdb.DB, currentCobj Category) []CategoryMini {
81 | var items []CategoryMini
82 | item := CategoryMini{
83 | Id: currentCobj.Id,
84 | Name: currentCobj.Name,
85 | }
86 | items = append(items, item)
87 |
88 | rs := db.Hget("keyValue", []byte("main_category"))
89 | if rs.State == "ok" {
90 | var keys [][]byte
91 | currentCidStr := strconv.FormatUint(currentCobj.Id, 10)
92 | cids := strings.Split(rs.String(), ",")
93 | for _, v := range cids {
94 | if v != currentCidStr {
95 | keys = append(keys, youdb.DS2b(v))
96 | }
97 | }
98 | if len(keys) > 0 {
99 | rs2 := db.Hmget("category", keys)
100 | if rs2.State == "ok" {
101 | for i := 0; i < len(rs2.Data)-1; i += 2 {
102 | item := CategoryMini{}
103 | json.Unmarshal(rs2.Data[i+1], &item)
104 | items = append(items, item)
105 | }
106 | }
107 | }
108 | }
109 |
110 | return items
111 | }
112 |
113 | func CategoryList(db *youdb.DB, cmd, key string, limit int) CategoryPageInfo {
114 | tb := "category"
115 | var items []Category
116 | var keys [][]byte
117 | var hasPrev, hasNext bool
118 | var firstKey, lastKey uint64
119 |
120 | keyStart := youdb.DS2b(key)
121 | if cmd == "hrscan" {
122 | rs := db.Hrscan(tb, keyStart, limit)
123 | if rs.State == "ok" {
124 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
125 | keys = append(keys, rs.Data[i])
126 | }
127 | }
128 | } else if cmd == "hscan" {
129 | rs := db.Hscan(tb, keyStart, limit)
130 | if rs.State == "ok" {
131 | for i := len(rs.Data) - 2; i >= 0; i -= 2 {
132 | keys = append(keys, rs.Data[i])
133 | }
134 | }
135 | }
136 |
137 | if len(keys) > 0 {
138 | rs := db.Hmget("category", keys)
139 | if rs.State == "ok" {
140 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
141 | item := Category{}
142 | json.Unmarshal(rs.Data[i+1], &item)
143 | items = append(items, item)
144 | if firstKey == 0 {
145 | firstKey = item.Id
146 | }
147 | lastKey = item.Id
148 | }
149 |
150 | rs = db.Hscan(tb, youdb.I2b(firstKey), 1)
151 | if rs.State == "ok" {
152 | hasPrev = true
153 | }
154 | rs = db.Hrscan(tb, youdb.I2b(lastKey), 1)
155 | if rs.State == "ok" {
156 | hasNext = true
157 | }
158 | }
159 | }
160 |
161 | return CategoryPageInfo{
162 | Items: items,
163 | HasPrev: hasPrev,
164 | HasNext: hasNext,
165 | FirstKey: firstKey,
166 | LastKey: lastKey,
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/system/core.go:
--------------------------------------------------------------------------------
1 | package system
2 |
3 | import (
4 | "log"
5 | "runtime"
6 |
7 | "net/url"
8 | "strings"
9 |
10 | "github.com/ego008/youdb"
11 | "github.com/gorilla/securecookie"
12 | "github.com/terminus2049/2049bbs/util"
13 | "github.com/weint/config"
14 | )
15 |
16 | type MainConf struct {
17 | HttpPort int
18 | HttpsOn bool
19 | Domain string // 若启用https 则该domain 为注册的域名,eg: domain.com、www.domain.com
20 | HttpsPort int
21 | PubDir string
22 | ViewDir string
23 | Youdb string
24 | CookieSecure bool
25 | CookieHttpOnly bool
26 | OldSiteDomain string
27 | TLSCrtFile string
28 | TLSKeyFile string
29 | }
30 |
31 | type SiteConf struct {
32 | GoVersion string
33 | MD5Sums string
34 | Name string
35 | Desc string
36 | AdminEmail string
37 | MainDomain string // 上传图片后添加网址前缀, eg: http://domian.com 、http://234.21.35.89:8082
38 | MainNodeIds string
39 | MustLoginNodeIds string
40 | NotHomeNodeIds string
41 | ProverbId string
42 | AvatarMinId int
43 | AvatarMaxId int
44 | HomeNode string
45 | AdminBotId int
46 | AnonymousBotId int
47 | TimeZone int
48 | HomeShowNum int
49 | PageShowNum int
50 | TagShowNum int
51 | CategoryShowNum int
52 | TitleMaxLen int
53 | ContentMaxLen int
54 | PostInterval int
55 | CommentListNum int
56 | CommentInterval int
57 | Authorized bool
58 | RegReview bool
59 | CloseReg bool
60 | AutoDataBackup bool
61 | ResetCookieKey bool
62 | AutoGetTag bool
63 | GetTagApi string
64 | UploadSuffix string
65 | UploadImgOnly bool
66 | UploadImgResize bool
67 | UploadMaxSize int
68 | UploadMaxSizeByte int64
69 | }
70 |
71 | type AppConf struct {
72 | Main *MainConf
73 | Site *SiteConf
74 | }
75 |
76 | type Application struct {
77 | Cf *AppConf
78 | Db *youdb.DB
79 | Sc *securecookie.SecureCookie
80 | }
81 |
82 | func LoadConfig(filename string) *config.Engine {
83 | c := &config.Engine{}
84 | c.Load(filename)
85 | return c
86 | }
87 |
88 | func (app *Application) Init(c *config.Engine, currentFilePath string) {
89 |
90 | mcf := &MainConf{}
91 | c.GetStruct("Main", mcf)
92 |
93 | // check domain
94 | if strings.HasPrefix(mcf.Domain, "http") {
95 | dm, err := url.Parse(mcf.Domain)
96 | if err != nil {
97 | log.Fatal("domain fmt err", err)
98 | }
99 | mcf.Domain = dm.Host
100 | } else {
101 | mcf.Domain = strings.Trim(mcf.Domain, "/")
102 | }
103 |
104 | scf := &SiteConf{}
105 | c.GetStruct("Site", scf)
106 | scf.GoVersion = runtime.Version()
107 | fMd5, _ := util.HashFileMD5(currentFilePath)
108 | scf.MD5Sums = fMd5
109 | scf.MainDomain = strings.Trim(scf.MainDomain, "/")
110 | log.Println("MainDomain:", scf.MainDomain)
111 | if scf.TimeZone < -12 || scf.TimeZone > 12 {
112 | scf.TimeZone = 0
113 | }
114 | if scf.UploadMaxSize < 1 {
115 | scf.UploadMaxSize = 1
116 | }
117 | scf.UploadMaxSizeByte = int64(scf.UploadMaxSize) << 20
118 |
119 | app.Cf = &AppConf{mcf, scf}
120 | db, err := youdb.Open(mcf.Youdb)
121 | if err != nil {
122 | log.Fatalf("Connect Error: %v", err)
123 | }
124 | app.Db = db
125 |
126 | // set main node
127 | db.Hset("keyValue", []byte("main_category"), []byte(scf.MainNodeIds))
128 |
129 | // app.Sc = securecookie.New(securecookie.GenerateRandomKey(64),
130 | // securecookie.GenerateRandomKey(32))
131 | //app.Sc.SetSerializer(securecookie.JSONEncoder{})
132 |
133 | var hashKey []byte
134 | var blockKey []byte
135 | if scf.ResetCookieKey {
136 | hashKey = securecookie.GenerateRandomKey(64)
137 | blockKey = securecookie.GenerateRandomKey(32)
138 | _ = db.Hmset("keyValue", []byte("hashKey"), hashKey, []byte("blockKey"), blockKey)
139 | } else {
140 | hashKey = append(hashKey, db.Hget("keyValue", []byte("hashKey")).Bytes()...)
141 | blockKey = append(blockKey, db.Hget("keyValue", []byte("blockKey")).Bytes()...)
142 | if len(hashKey) == 0 {
143 | hashKey = securecookie.GenerateRandomKey(64)
144 | blockKey = securecookie.GenerateRandomKey(32)
145 | _ = db.Hmset("keyValue", []byte("hashKey"), hashKey, []byte("blockKey"), blockKey)
146 | }
147 | }
148 |
149 | app.Sc = securecookie.New(hashKey, blockKey)
150 |
151 | log.Println("youdb Connect to", mcf.Youdb)
152 | }
153 |
154 | func (app *Application) Close() {
155 | app.Db.Close()
156 | log.Println("db cloded")
157 | }
158 |
--------------------------------------------------------------------------------
/view/default/mobile/adminarticleedit.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
38 |
39 |
109 |
110 |
111 | {{ end}}
112 |
--------------------------------------------------------------------------------
/model/comment.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "html/template"
7 |
8 | "github.com/ego008/youdb"
9 | "github.com/terminus2049/2049bbs/util"
10 | )
11 |
12 | type Comment struct {
13 | Id uint64 `json:"id"`
14 | Aid uint64 `json:"aid"`
15 | Uid uint64 `json:"uid"`
16 | Content string `json:"content"`
17 | ClientIp string `json:"clientip"`
18 | AddTime uint64 `json:"addtime"`
19 | Fold bool `json:"fold"`
20 | }
21 |
22 | type CommentListItem struct {
23 | Id uint64 `json:"id"`
24 | Aid uint64 `json:"aid"`
25 | Uid uint64 `json:"uid"`
26 | Flag int `json:"flag"`
27 | Name string `json:"name"`
28 | Avatar string `json:"avatar"`
29 | Content string `json:"content"`
30 | ContentFmt template.HTML
31 | AddTime uint64 `json:"addtime"`
32 | AddTimeFmt string `json:"addtimefmt"`
33 | Fold bool `json:"fold"`
34 | }
35 |
36 | type CommentPageInfo struct {
37 | Items []CommentListItem `json:"items"`
38 | HasPrev bool `json:"hasprev"`
39 | HasNext bool `json:"hasnext"`
40 | FirstKey uint64 `json:"firstkey"`
41 | LastKey uint64 `json:"lastkey"`
42 | }
43 |
44 | func CommentGetByKey(db *youdb.DB, aid string, cid uint64) (Comment, error) {
45 | obj := Comment{}
46 | rs := db.Hget("article_comment:"+aid, youdb.I2b(cid))
47 | if rs.State == "ok" {
48 | json.Unmarshal(rs.Data[0], &obj)
49 | return obj, nil
50 | }
51 | return obj, errors.New(rs.State)
52 | }
53 |
54 | func CommentSetByKey(db *youdb.DB, aid string, cid uint64, obj Comment) error {
55 | jb, _ := json.Marshal(obj)
56 | return db.Hset("article_comment:"+aid, youdb.I2b(cid), jb)
57 | }
58 |
59 | func CommentDelByKey(db *youdb.DB, aid string, cid uint64) error {
60 | return db.Hdel("article_comment:"+aid, youdb.I2b(cid))
61 | }
62 |
63 | func CommentList(db *youdb.DB, cmd, tb, key string, limit, tz int, ignorelimitedusers bool) CommentPageInfo {
64 | var items []CommentListItem
65 | var citems []Comment
66 | userMap := map[uint64]UserMini{}
67 | var userKeys [][]byte
68 | var hasPrev, hasNext bool
69 | var firstKey, lastKey uint64
70 |
71 | keyStart := youdb.DS2b(key)
72 | if cmd == "hrscan" {
73 | rs := db.Hrscan(tb, keyStart, limit)
74 | if rs.State == "ok" {
75 | for i := len(rs.Data) - 2; i >= 0; i -= 2 {
76 | item := Comment{}
77 | json.Unmarshal(rs.Data[i+1], &item)
78 | citems = append(citems, item)
79 | userMap[item.Uid] = UserMini{}
80 | userKeys = append(userKeys, youdb.I2b(item.Uid))
81 | }
82 | }
83 | } else if cmd == "hscan" {
84 | rs := db.Hscan(tb, keyStart, limit)
85 | if rs.State == "ok" {
86 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
87 | item := Comment{}
88 | json.Unmarshal(rs.Data[i+1], &item)
89 | citems = append(citems, item)
90 | userMap[item.Uid] = UserMini{}
91 | userKeys = append(userKeys, youdb.I2b(item.Uid))
92 | }
93 | }
94 | }
95 |
96 | if len(citems) > 0 {
97 | rs := db.Hmget("user", userKeys)
98 | if rs.State == "ok" {
99 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
100 | item := UserMini{}
101 | json.Unmarshal(rs.Data[i+1], &item)
102 | userMap[item.Id] = item
103 | }
104 | }
105 |
106 | for _, citem := range citems {
107 | user := userMap[citem.Uid]
108 | item := CommentListItem{
109 | Id: citem.Id,
110 | Aid: citem.Aid,
111 | Uid: citem.Uid,
112 | Name: user.Name,
113 | Flag: user.Flag,
114 | Avatar: user.Avatar,
115 | AddTime: citem.AddTime,
116 | AddTimeFmt: util.TimeFmt(citem.AddTime, "2006-01-02", tz),
117 | ContentFmt: template.HTML(util.ContentFmt(db, citem.Content)),
118 | Fold: citem.Fold,
119 | }
120 |
121 | if item.Flag == -1 {
122 | item.ContentFmt = template.HTML("用户已注销,隐藏回帖
")
123 | }
124 |
125 | if item.Flag == 0 || item.Flag == 6 {
126 | item.Fold = true
127 | }
128 |
129 | if !(ignorelimitedusers && (item.Flag == 0 || item.Flag == 6)) {
130 | items = append(items, item)
131 | }
132 |
133 | if firstKey == 0 {
134 | firstKey = item.Id
135 | }
136 | lastKey = item.Id
137 | }
138 |
139 | rs = db.Hrscan(tb, youdb.I2b(firstKey), 1)
140 | if rs.State == "ok" {
141 | hasPrev = true
142 | }
143 | rs = db.Hscan(tb, youdb.I2b(lastKey), 1)
144 | if rs.State == "ok" {
145 | hasNext = true
146 | }
147 | }
148 |
149 | return CommentPageInfo{
150 | Items: items,
151 | HasPrev: hasPrev,
152 | HasNext: hasNext,
153 | FirstKey: firstKey,
154 | LastKey: lastKey,
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/static/css/jquery.toast.css:
--------------------------------------------------------------------------------
1 | /**
2 | * jQuery toast plugin created by Kamran Ahmed copyright MIT license 2014
3 | */
4 | .jq-toast-wrap { display: block; position: fixed; width: 250px; pointer-events: none !important; margin: 0; padding: 0; letter-spacing: normal; z-index: 9000 !important; }
5 | .jq-toast-wrap * { margin: 0; padding: 0; }
6 |
7 | .jq-toast-wrap.bottom-left { bottom: 20px; left: 20px; }
8 | .jq-toast-wrap.bottom-right { bottom: 20px; right: 40px; }
9 | .jq-toast-wrap.top-left { top: 20px; left: 20px; }
10 | .jq-toast-wrap.top-right { top: 20px; right: 40px; }
11 |
12 | .jq-toast-single { display: block; width: 100%; padding: 10px; margin: 0px 0px 5px; border-radius: 4px; font-size: 12px; font-family: arial, sans-serif; line-height: 17px; position: relative; pointer-events: all !important; background-color: #444444; color: white; }
13 |
14 | .jq-toast-single h2 { font-family: arial, sans-serif; font-size: 14px; margin: 0px 0px 7px; background: none; color: inherit; line-height: inherit; letter-spacing: normal; }
15 | .jq-toast-single a { color: #eee; text-decoration: none; font-weight: bold; border-bottom: 1px solid white; padding-bottom: 3px; font-size: 12px; }
16 |
17 | .jq-toast-single ul { margin: 0px 0px 0px 15px; background: none; padding:0px; }
18 | .jq-toast-single ul li { list-style-type: disc !important; line-height: 17px; background: none; margin: 0; padding: 0; letter-spacing: normal; }
19 |
20 | .close-jq-toast-single { position: absolute; top: 3px; right: 7px; font-size: 14px; cursor: pointer; }
21 |
22 | .jq-toast-loader { display: block; position: absolute; top: -2px; height: 5px; width: 0%; left: 0; border-radius: 5px; background: red; }
23 | .jq-toast-loaded { width: 100%; }
24 | .jq-has-icon { padding: 10px 10px 10px 50px; background-repeat: no-repeat; background-position: 10px; }
25 | .jq-icon-info { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII='); background-color: #31708f; color: #d9edf7; border-color: #bce8f1; }
26 | .jq-icon-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII='); background-color: #8a6d3b; color: #fcf8e3; border-color: #faebcc; }
27 | .jq-icon-error { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII='); background-color: #a94442; color: #f2dede; border-color: #ebccd1; }
28 | .jq-icon-success { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=='); color: #dff0d8; background-color: #3c763d; border-color: #d6e9c6; }
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "flag"
7 | "log"
8 | "net/http"
9 | "os"
10 | "os/signal"
11 | "strconv"
12 | "strings"
13 | "syscall"
14 | "time"
15 |
16 | "github.com/terminus2049/2049bbs/cronjob"
17 | "github.com/terminus2049/2049bbs/router"
18 | "github.com/terminus2049/2049bbs/system"
19 | "github.com/xi2/httpgzip"
20 | "goji.io"
21 | "goji.io/pat"
22 | "golang.org/x/crypto/acme/autocert"
23 | "golang.org/x/net/http2"
24 | )
25 |
26 | func main() {
27 | configFile := flag.String("config", "config/config.yaml", "full path of config.yaml file")
28 | flag.Parse()
29 |
30 | c := system.LoadConfig(*configFile)
31 | app := &system.Application{}
32 | app.Init(c, os.Args[0])
33 |
34 | // cron job
35 | cr := cronjob.BaseHandler{App: app}
36 | go cr.MainCronJob()
37 |
38 | root := goji.NewMux()
39 |
40 | mcf := app.Cf.Main
41 | scf := app.Cf.Site
42 |
43 | // static file server
44 | staticPath := mcf.PubDir
45 | if len(staticPath) == 0 {
46 | staticPath = "static"
47 | }
48 |
49 | root.Handle(pat.New("/.well-known/acme-challenge/*"),
50 | http.StripPrefix("/.well-known/acme-challenge/", http.FileServer(http.Dir(staticPath))))
51 | root.Handle(pat.New("/static/*"),
52 | http.StripPrefix("/static/", http.FileServer(http.Dir(staticPath))))
53 |
54 | root.Handle(pat.New("/*"), router.NewRouter(app))
55 |
56 | // normal http
57 | // http.ListenAndServe(listenAddr, root)
58 |
59 | // graceful
60 | // subscribe to SIGINT signals
61 | stopChan := make(chan os.Signal, 1)
62 | signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
63 |
64 | var srv *http.Server
65 |
66 | if mcf.HttpsOn {
67 | // https
68 | log.Println("Register sll for domain:", mcf.Domain)
69 | log.Println("TLSCrtFile : ", mcf.TLSCrtFile)
70 | log.Println("TLSKeyFile : ", mcf.TLSKeyFile)
71 |
72 | root.Use(stlAge)
73 |
74 | tlsCf := &tls.Config{
75 | NextProtos: []string{http2.NextProtoTLS, "http/1.1"},
76 | }
77 |
78 | if mcf.Domain != "" && mcf.TLSCrtFile == "" && mcf.TLSKeyFile == "" {
79 |
80 | domains := strings.Split(mcf.Domain, ",")
81 | certManager := autocert.Manager{
82 | Prompt: autocert.AcceptTOS,
83 | HostPolicy: autocert.HostWhitelist(domains...),
84 | Cache: autocert.DirCache("certs"),
85 | Email: scf.AdminEmail,
86 | }
87 | tlsCf.GetCertificate = certManager.GetCertificate
88 | //tlsCf.ServerName = domains[0]
89 |
90 | go func() {
91 | // 必须是 80 端口
92 | log.Fatal(http.ListenAndServe(":http", certManager.HTTPHandler(nil)))
93 | }()
94 |
95 | } else {
96 | // rewrite
97 | go func() {
98 | if err := http.ListenAndServe(":"+strconv.Itoa(mcf.HttpPort), http.HandlerFunc(redirectHandler)); err != nil {
99 | log.Println("Http2https server failed ", err)
100 | }
101 | }()
102 | }
103 |
104 | srv = &http.Server{
105 | Addr: ":" + strconv.Itoa(mcf.HttpsPort),
106 | Handler: httpgzip.NewHandler(root, nil),
107 | TLSConfig: tlsCf,
108 | MaxHeaderBytes: int(app.Cf.Site.UploadMaxSizeByte),
109 | ReadTimeout: 5 * time.Second,
110 | WriteTimeout: 10 * time.Second,
111 | }
112 |
113 | go func() {
114 | // 如何获取 TLSCrtFile、TLSKeyFile 文件参见 https://www.youbbs.org/t/2169
115 | log.Fatal(srv.ListenAndServeTLS(mcf.TLSCrtFile, mcf.TLSKeyFile))
116 | }()
117 |
118 | log.Println("Web server Listen port", mcf.HttpsPort)
119 | log.Println("Web server URL", "https://"+mcf.Domain)
120 |
121 | } else {
122 | // http
123 | srv = &http.Server{
124 | Addr: ":" + strconv.Itoa(mcf.HttpPort),
125 | Handler: root,
126 | ReadTimeout: 5 * time.Second,
127 | WriteTimeout: 10 * time.Second,
128 | }
129 | go func() {
130 | log.Fatal(srv.ListenAndServe())
131 | }()
132 |
133 | log.Println("Web server Listen port", mcf.HttpPort)
134 | }
135 |
136 | <-stopChan // wait for SIGINT
137 | log.Println("Shutting down server...")
138 |
139 | // shut down gracefully, but wait no longer than 10 seconds before halting
140 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
141 | srv.Shutdown(ctx)
142 | app.Close()
143 |
144 | log.Println("Server gracefully stopped")
145 | }
146 |
147 | func redirectHandler(w http.ResponseWriter, r *http.Request) {
148 | target := "https://" + r.Host + r.URL.Path
149 | if len(r.URL.RawQuery) > 0 {
150 | target += "?" + r.URL.RawQuery
151 | }
152 | // consider HSTS if your clients are browsers
153 | w.Header().Set("Connection", "close")
154 | http.Redirect(w, r, target, 302)
155 | }
156 |
157 | func stlAge(h http.Handler) http.Handler {
158 | fn := func(w http.ResponseWriter, r *http.Request) {
159 | // add max-age to get A+
160 | w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
161 | h.ServeHTTP(w, r)
162 | }
163 | return http.HandlerFunc(fn)
164 | }
165 |
--------------------------------------------------------------------------------
/cronjob/mainjob.go:
--------------------------------------------------------------------------------
1 | package cronjob
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "strings"
7 | "time"
8 |
9 | "github.com/boltdb/bolt"
10 | "github.com/ego008/youdb"
11 | "github.com/terminus2049/2049bbs/model"
12 | "github.com/terminus2049/2049bbs/system"
13 | "github.com/yanyiwu/gojieba"
14 | )
15 |
16 | type BaseHandler struct {
17 | App *system.Application
18 | }
19 |
20 | func (h *BaseHandler) MainCronJob() {
21 | db := h.App.Db
22 | scf := h.App.Cf.Site
23 | tick1 := time.Tick(3600 * time.Second)
24 | tick2 := time.Tick(60 * time.Second)
25 | tick3 := time.Tick(30 * time.Minute)
26 | tick4 := time.Tick(31 * time.Second)
27 | daySecond := int64(3600 * 24)
28 |
29 | // 推荐使用 youbbs 的在线 api,请参考
30 | // https://github.com/ego008/goyoubbs/blob/master/cronjob/mainjob.go
31 |
32 | // 如果不使用 tag 功能,即 scf.AutoGetTag 为 false,或者如果想每调用一次重新载入+释放,
33 | // 可以把 gojieba 的语句移入 getTagFromTitle 函数
34 | // 相关讨论 https://github.com/Terminus2049/2049BBS/commit/45c5ad8275eef3214690a299ec6ce917a127754d
35 |
36 | x := gojieba.NewJieba()
37 | defer x.Free()
38 |
39 | for {
40 | select {
41 | case <-tick1:
42 | limit := 10
43 | timeBefore := uint64(time.Now().UTC().Unix() - daySecond)
44 | scoreStartB := youdb.I2b(timeBefore)
45 | zbnList := []string{
46 | "article_detail_token",
47 | "user_login_token",
48 | }
49 | for _, bn := range zbnList {
50 | rs := db.Zrscan(bn, []byte(""), scoreStartB, limit)
51 | if rs.State == "ok" {
52 | keys := make([][]byte, len(rs.Data)/2)
53 | j := 0
54 | for i := 0; i < (len(rs.Data) - 1); i += 2 {
55 | keys[j] = rs.Data[i]
56 | j++
57 | }
58 | db.Zmdel(bn, keys)
59 | }
60 | }
61 |
62 | case <-tick2:
63 | if scf.AutoGetTag {
64 | getTagFromTitle(db, x)
65 | }
66 | case <-tick3:
67 | if h.App.Cf.Site.AutoDataBackup {
68 | dataBackup(db)
69 | }
70 | case <-tick4:
71 | setArticleTag(db)
72 | }
73 | }
74 | }
75 |
76 | func dataBackup(db *youdb.DB) {
77 | filePath := "databackup/" + time.Now().UTC().Format("2006-01-02") + ".db"
78 | if _, err := os.Stat(filePath); err != nil {
79 | // path not exists
80 | err := db.View(func(tx *bolt.Tx) error {
81 | return tx.CopyFile(filePath, 0600)
82 | })
83 | if err == nil {
84 | // todo upload to qiniu
85 | }
86 | }
87 | }
88 |
89 | func getTagFromTitle(db *youdb.DB, engin *gojieba.Jieba) {
90 | rs := db.Hscan("task_to_get_tag", []byte(""), 1)
91 | if rs.State == "ok" {
92 | aidB := rs.Data[0][:]
93 |
94 | rs2 := db.Hget("article", aidB)
95 | if rs2.State != "ok" {
96 | db.Hdel("task_to_get_tag", aidB)
97 | return
98 | }
99 | aobj := model.Article{}
100 | json.Unmarshal(rs2.Data[0], &aobj)
101 | if len(aobj.Tags) > 0 {
102 | db.Hdel("task_to_get_tag", aidB)
103 | return
104 | }
105 |
106 | Title := string(rs.Data[1])
107 | tags := engin.Extract(Title, 5)
108 |
109 | // get once more
110 | rs2 = db.Hget("article", youdb.I2b(aobj.Id))
111 | if rs2.State == "ok" {
112 | aobj := model.Article{}
113 | json.Unmarshal(rs2.Data[0], &aobj)
114 | aobj.Tags = strings.Join(tags, ",")
115 | jb, _ := json.Marshal(aobj)
116 | db.Hset("article", youdb.I2b(aobj.Id), jb)
117 |
118 | // tag send task work,自动处理tag与文章id
119 | at := model.ArticleTag{
120 | Id: aobj.Id,
121 | OldTags: "",
122 | NewTags: aobj.Tags,
123 | }
124 | jb, _ = json.Marshal(at)
125 | db.Hset("task_to_set_tag", youdb.I2b(at.Id), jb)
126 | }
127 |
128 | db.Hdel("task_to_get_tag", aidB)
129 |
130 | }
131 | }
132 |
133 | func setArticleTag(db *youdb.DB) {
134 | rs := db.Hscan("task_to_set_tag", nil, 1)
135 | if rs.OK() {
136 | info := model.ArticleTag{}
137 | err := json.Unmarshal(rs.Data[1], &info)
138 | if err != nil {
139 | return
140 | }
141 | //log.Println("aid", info.Id)
142 |
143 | // set tag
144 | oldTag := strings.Split(info.OldTags, ",")
145 | newTag := strings.Split(info.NewTags, ",")
146 |
147 | // remove
148 | for _, tag1 := range oldTag {
149 | contains := false
150 | for _, tag2 := range newTag {
151 | if tag1 == tag2 {
152 | contains = true
153 | break
154 | }
155 | }
156 | if !contains {
157 | tagLower := strings.ToLower(tag1)
158 | db.Hdel("tag:"+tagLower, youdb.I2b(info.Id))
159 | db.Zincr("tag_article_num", []byte(tagLower), -1)
160 | }
161 | }
162 |
163 | // add
164 | for _, tag1 := range newTag {
165 | contains := false
166 | for _, tag2 := range oldTag {
167 | if tag1 == tag2 {
168 | contains = true
169 | break
170 | }
171 | }
172 | if !contains {
173 | tagLower := strings.ToLower(tag1)
174 | // 记录所有tag,只增不减
175 | if db.Hget("tag", []byte(tagLower)).State != "ok" {
176 | db.Hset("tag", []byte(tagLower), []byte(""))
177 | db.HnextSequence("tag") // 添加这一行
178 | }
179 | // check if not exist !important
180 | if db.Hget("tag:"+tagLower, youdb.I2b(info.Id)).State != "ok" {
181 | db.Hset("tag:"+tagLower, youdb.I2b(info.Id), []byte(""))
182 | db.Zincr("tag_article_num", []byte(tagLower), 1)
183 | }
184 | }
185 | }
186 |
187 | db.Hdel("task_to_set_tag", youdb.I2b(info.Id))
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/controller/usersetting.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "net/http"
8 | "regexp"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | "github.com/ego008/youdb"
14 | "github.com/rs/xid"
15 | "github.com/terminus2049/2049bbs/model"
16 | "github.com/terminus2049/2049bbs/util"
17 | )
18 |
19 | func (h *BaseHandler) UserSetting(w http.ResponseWriter, r *http.Request) {
20 | currentUser, _ := h.CurrentUser(w, r)
21 | if currentUser.Id == 0 {
22 | http.Redirect(w, r, "/login", http.StatusSeeOther)
23 | return
24 | }
25 |
26 | type pageData struct {
27 | PageData
28 | Uobj model.User
29 | Now int64
30 | }
31 |
32 | tpl := h.CurrentTpl(r)
33 | evn := &pageData{}
34 | evn.SiteCf = h.App.Cf.Site
35 | evn.Title = "设置"
36 | evn.Keywords = ""
37 | evn.Description = ""
38 | evn.IsMobile = tpl == "mobile"
39 | evn.CurrentUser = currentUser
40 |
41 | evn.ShowSideAd = true
42 | evn.PageName = "user_setting"
43 |
44 | evn.Uobj = currentUser
45 | evn.Now = time.Now().UTC().Unix()
46 |
47 | h.SetCookie(w, "token", xid.New().String(), 1)
48 | h.Render(w, tpl, evn, "layout.html", "usersetting.html")
49 | }
50 |
51 | func (h *BaseHandler) UserSettingPost(w http.ResponseWriter, r *http.Request) {
52 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
53 |
54 | token := h.GetCookie(r, "token")
55 | if len(token) == 0 {
56 | w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
57 | return
58 | }
59 |
60 | currentUser, _ := h.CurrentUser(w, r)
61 | if currentUser.Id == 0 {
62 | w.Write([]byte(`{"retcode":401,"retmsg":"authored require"}`))
63 | return
64 | }
65 |
66 | // r.ParseForm() // don't use ParseForm !important
67 | act := r.FormValue("act")
68 | if act == "avatar" {
69 |
70 | r.ParseMultipartForm(32 << 20)
71 |
72 | file, _, err := r.FormFile("avatar")
73 | defer file.Close()
74 |
75 | buff := make([]byte, 512)
76 | file.Read(buff)
77 | if len(util.CheckImageType(buff)) == 0 {
78 | w.Write([]byte(`{"retcode":400,"retmsg":"unknown image format"}`))
79 | return
80 | }
81 |
82 | var imgData bytes.Buffer
83 | file.Seek(0, 0)
84 | if fileSize, err := io.Copy(&imgData, file); err != nil {
85 | w.Write([]byte(`{"retcode":400,"retmsg":"read image data err ` + err.Error() + `"}`))
86 | return
87 | } else {
88 | if fileSize > 5360690 {
89 | w.Write([]byte(`{"retcode":400,"retmsg":"image size too much"}`))
90 | return
91 | }
92 | }
93 |
94 | img, err := util.GetImageObj(&imgData)
95 | if err != nil {
96 | w.Write([]byte(`{"retcode":400,"retmsg":"fail to get image obj ` + err.Error() + `"}`))
97 | return
98 | }
99 |
100 | uid := strconv.FormatUint(currentUser.Id, 10)
101 | err = util.AvatarResize(img, 73, 73, "static/avatar/"+uid+".jpg")
102 | if err != nil {
103 | w.Write([]byte(`{"retcode":400,"retmsg":"fail to resize avatar ` + err.Error() + `"}`))
104 | return
105 | }
106 |
107 | currentUser.Avatar = uid
108 | jb, _ := json.Marshal(currentUser)
109 | h.App.Db.Hset("user", youdb.I2b(currentUser.Id), jb)
110 |
111 | http.Redirect(w, r, "/setting#2", http.StatusSeeOther)
112 | return
113 | }
114 |
115 | type recForm struct {
116 | Act string `json:"act"`
117 | Email string `json:"email"`
118 | Url string `json:"url"`
119 | About string `json:"about"`
120 | Password0 string `json:"password0"`
121 | Password string `json:"password"`
122 | IgnoreNode string `json:"ignorenode"`
123 | IgnoreUser string `json:"ignoreuser"`
124 | Theme string `json:"theme"`
125 | IgnoreLimitedUsers string `json:"ignorelimitedusers"`
126 | }
127 |
128 | decoder := json.NewDecoder(r.Body)
129 | var rec recForm
130 | err := decoder.Decode(&rec)
131 | if err != nil {
132 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
133 | return
134 | }
135 | defer r.Body.Close()
136 |
137 | recAct := rec.Act
138 | if len(recAct) == 0 {
139 | w.Write([]byte(`{"retcode":400,"retmsg":"missed act "}`))
140 | return
141 | }
142 |
143 | isChanged := false
144 | if recAct == "info" {
145 | currentUser.Email = rec.Email
146 | currentUser.Url = rec.Url
147 | currentUser.About = rec.About
148 |
149 | reg := regexp.MustCompile("[0-9]+")
150 |
151 | nodes := reg.FindAllString(rec.IgnoreNode, -1)
152 | currentUser.IgnoreNode = strings.Join(nodes[:], ",")
153 |
154 | users := reg.FindAllString(rec.IgnoreUser, -1)
155 | currentUser.IgnoreUser = strings.Join(users[:], ",")
156 |
157 | currentUser.Theme = rec.Theme
158 | isChanged = true
159 |
160 | isHidden := true
161 | if rec.IgnoreLimitedUsers != "1" {
162 | isHidden = false
163 | }
164 | currentUser.IgnoreLimitedUsers = isHidden
165 |
166 | } else if recAct == "change_pw" {
167 | if len(rec.Password0) == 0 || len(rec.Password) == 0 {
168 | w.Write([]byte(`{"retcode":400,"retmsg":"missed args"}`))
169 | return
170 | }
171 | if currentUser.Password != rec.Password0 {
172 | w.Write([]byte(`{"retcode":400,"retmsg":"当前密码不正确"}`))
173 | return
174 | }
175 | currentUser.Password = rec.Password
176 | isChanged = true
177 | } else if recAct == "set_pw" {
178 | if len(rec.Password) == 0 {
179 | w.Write([]byte(`{"retcode":400,"retmsg":"missed args"}`))
180 | return
181 | }
182 | currentUser.Password = rec.Password
183 | isChanged = true
184 | }
185 |
186 | if isChanged {
187 | jb, _ := json.Marshal(currentUser)
188 | h.App.Db.Hset("user", youdb.I2b(currentUser.Id), jb)
189 | }
190 |
191 | type response struct {
192 | normalRsp
193 | }
194 |
195 | rsp := response{}
196 | rsp.Retcode = 200
197 | rsp.Retmsg = "修改成功"
198 | json.NewEncoder(w).Encode(rsp)
199 | }
200 |
--------------------------------------------------------------------------------
/controller/adminuseredit.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "github.com/terminus2049/2049bbs/model"
7 | "github.com/terminus2049/2049bbs/util"
8 | "github.com/ego008/youdb"
9 | "github.com/rs/xid"
10 | "goji.io/pat"
11 | "io"
12 | "net/http"
13 | "strconv"
14 | "strings"
15 | "time"
16 | )
17 |
18 | func (h *BaseHandler) UserEdit(w http.ResponseWriter, r *http.Request) {
19 | uid := pat.Param(r, "uid")
20 | uidI, err := strconv.ParseUint(uid, 10, 64)
21 | if err != nil {
22 | w.Write([]byte(`{"retcode":400,"retmsg":"aid type err"}`))
23 | return
24 | }
25 |
26 | currentUser, _ := h.CurrentUser(w, r)
27 | if currentUser.Id == 0 {
28 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
29 | return
30 | }
31 | if currentUser.Flag < 99 {
32 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
33 | return
34 | }
35 |
36 | db := h.App.Db
37 |
38 | uobj, err := model.UserGetById(db, uidI)
39 | if err != nil {
40 | w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
41 | return
42 | }
43 |
44 | type pageData struct {
45 | PageData
46 | Uobj model.User
47 | Now int64
48 | }
49 |
50 | tpl := h.CurrentTpl(r)
51 | evn := &pageData{}
52 | evn.SiteCf = h.App.Cf.Site
53 | evn.Title = "修改用户"
54 | evn.IsMobile = tpl == "mobile"
55 | evn.CurrentUser = currentUser
56 | evn.ShowSideAd = true
57 | evn.PageName = "user_edit"
58 |
59 | evn.Uobj = uobj
60 | evn.Now = time.Now().UTC().Unix()
61 |
62 | h.SetCookie(w, "token", xid.New().String(), 1)
63 | h.Render(w, tpl, evn, "layout.html", "adminuseredit.html")
64 | }
65 |
66 | func (h *BaseHandler) UserEditPost(w http.ResponseWriter, r *http.Request) {
67 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
68 |
69 | uid := pat.Param(r, "uid")
70 | uidI, err := strconv.ParseUint(uid, 10, 64)
71 | if err != nil {
72 | w.Write([]byte(`{"retcode":400,"retmsg":"aid type err"}`))
73 | return
74 | }
75 |
76 | currentUser, _ := h.CurrentUser(w, r)
77 | if currentUser.Id == 0 {
78 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
79 | return
80 | }
81 | if currentUser.Flag < 99 {
82 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
83 | return
84 | }
85 |
86 | db := h.App.Db
87 |
88 | uobj, err := model.UserGetById(db, uidI)
89 | if err != nil {
90 | w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
91 | return
92 | }
93 |
94 | // r.ParseForm() // don't use ParseForm !important
95 | act := r.FormValue("act")
96 | if act == "avatar" {
97 | r.ParseMultipartForm(32 << 20)
98 |
99 | file, _, err := r.FormFile("avatar")
100 | defer file.Close()
101 |
102 | buff := make([]byte, 512)
103 | file.Read(buff)
104 | if len(util.CheckImageType(buff)) == 0 {
105 | w.Write([]byte(`{"retcode":400,"retmsg":"unknown image format"}`))
106 | return
107 | }
108 |
109 | var imgData bytes.Buffer
110 | file.Seek(0, 0)
111 | if fileSize, err := io.Copy(&imgData, file); err != nil {
112 | w.Write([]byte(`{"retcode":400,"retmsg":"read image data err ` + err.Error() + `"}`))
113 | return
114 | } else {
115 | if fileSize > 5360690 {
116 | w.Write([]byte(`{"retcode":400,"retmsg":"image size too much"}`))
117 | return
118 | }
119 | }
120 |
121 | img, err := util.GetImageObj(&imgData)
122 | if err != nil {
123 | w.Write([]byte(`{"retcode":400,"retmsg":"fail to get image obj ` + err.Error() + `"}`))
124 | return
125 | }
126 |
127 | err = util.AvatarResize(img, 73, 73, "static/avatar/"+uid+".jpg")
128 | if err != nil {
129 | w.Write([]byte(`{"retcode":400,"retmsg":"fail to resize avatar ` + err.Error() + `"}`))
130 | return
131 | }
132 |
133 | if uobj.Avatar == "0" || len(uobj.Avatar) == 0 {
134 | uobj.Avatar = uid
135 | model.UserUpdate(db, uobj)
136 | }
137 |
138 | http.Redirect(w, r, "/admin/user/edit/"+uid, http.StatusSeeOther)
139 | return
140 | }
141 |
142 | type recForm struct {
143 | Act string `json:"act"`
144 | Flag int `json:"flag"`
145 | Name string `json:"name"`
146 | Email string `json:"email"`
147 | Url string `json:"url"`
148 | About string `json:"about"`
149 | Password string `json:"password"`
150 | Hidden string `json:"hidden"`
151 | }
152 |
153 | decoder := json.NewDecoder(r.Body)
154 | var rec recForm
155 | err = decoder.Decode(&rec)
156 | if err != nil {
157 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
158 | return
159 | }
160 | defer r.Body.Close()
161 |
162 | recAct := rec.Act
163 | if len(recAct) == 0 {
164 | w.Write([]byte(`{"retcode":400,"retmsg":"missed act "}`))
165 | return
166 | }
167 |
168 | var hidden bool
169 | if rec.Hidden == "1" {
170 | hidden = true
171 | }
172 |
173 | isChanged := false
174 | if recAct == "info" {
175 | oldName := uobj.Name
176 | nameLow := strings.ToLower(rec.Name)
177 | if !util.IsNickname(nameLow) {
178 | w.Write([]byte(`{"retcode":400,"retmsg":"name fmt err"}`))
179 | return
180 | }
181 |
182 | uobj.Email = rec.Email
183 | uobj.Url = rec.Url
184 | uobj.About = rec.About
185 | uobj.Hidden = hidden
186 | isChanged = true
187 |
188 | if oldName != rec.Name {
189 | if db.Hget("user_name2uid", []byte(nameLow)).State == "ok" {
190 | w.Write([]byte(`{"retcode":400,"retmsg":"name is exist"}`))
191 | return
192 | }
193 | db.Hdel("user_name2uid", []byte(strings.ToLower(oldName)))
194 | db.Hset("user_name2uid", []byte(nameLow), youdb.I2b(uobj.Id))
195 | uobj.Name = rec.Name
196 | }
197 | } else if recAct == "change_pw" {
198 | if len(rec.Password) == 0 {
199 | w.Write([]byte(`{"retcode":400,"retmsg":"missed args"}`))
200 | return
201 | }
202 | uobj.Password = rec.Password
203 | isChanged = true
204 | } else if recAct == "flag" {
205 | if rec.Flag != uobj.Flag {
206 | oldFlag := strconv.Itoa(uobj.Flag)
207 | isChanged = true
208 | uobj.Flag = rec.Flag
209 |
210 | db.Hset("user_flag:"+strconv.Itoa(rec.Flag), youdb.I2b(uobj.Id), []byte(""))
211 | db.Hdel("user_flag:"+oldFlag, youdb.I2b(uobj.Id))
212 | }
213 | }
214 |
215 | if isChanged {
216 | model.UserUpdate(db, uobj)
217 | }
218 |
219 | type response struct {
220 | normalRsp
221 | }
222 |
223 | rsp := response{}
224 | rsp.Retcode = 200
225 | rsp.Retmsg = "修改成功"
226 | json.NewEncoder(w).Encode(rsp)
227 | }
228 |
--------------------------------------------------------------------------------
/util/contentfmt.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | // "crypto/md5"
5 | // "encoding/hex"
6 |
7 | "regexp"
8 |
9 | "github.com/ego008/youdb"
10 | // "strconv"
11 | "strings"
12 |
13 | "github.com/Terminus2049/github_flavored_markdown"
14 | )
15 |
16 | var (
17 | // codeRegexp = regexp.MustCompile("(?s:```(.+?)```)")
18 | // imgRegexp = regexp.MustCompile(`(https?://[\w./:]+/[\w./]+\.(jpg|jpe|jpeg|gif|png))`)
19 | // gistRegexp = regexp.MustCompile(`(https?://gist\.github\.com/([a-zA-Z0-9-]+/)?[\d]+)`)
20 | youtubeRegexp = regexp.MustCompile(`http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?`)
21 | mentionRegexp = regexp.MustCompile(`(?:\s|^)@([a-zA-Z0-9\p{Han}]{1,32})\s?`)
22 | // urlRegexp = regexp.MustCompile(`(http|ftp|https):\/\/([\w\-_]+(?:(?:\.[\w\-_]+)+))([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?`)
23 | // nlineRegexp = regexp.MustCompile(`\s{2,}`)
24 | // youku1Regexp = regexp.MustCompile(`https?://player\.youku\.com/player\.php/sid/([a-zA-Z0-9=]+)/v\.swf`)
25 | // youku2Regexp = regexp.MustCompile(`https?://v\.youku\.com/v_show/id_([a-zA-Z0-9=]+)(/|\.html?)?`)
26 | )
27 |
28 | // 文本格式化
29 | func ContentFmt(db *youdb.DB, input string) string {
30 | // if strings.Index(input, "```") >= 0 {
31 | // sepNum := strings.Count(input, "```")
32 | // if sepNum < 2 {
33 | // return input
34 | // }
35 | // codeMap := map[string]string{}
36 | // input = codeRegexp.ReplaceAllStringFunc(input, func(m string) string {
37 | // m = strings.Trim(m, "```")
38 | // m = strings.Trim(m, "\n")
39 | // //m = strings.TrimSpace(m)
40 | // m = strings.Replace(m, "&", "&", -1)
41 | // m = strings.Replace(m, "<", "<", -1)
42 | // m = strings.Replace(m, ">", ">", -1)
43 | //
44 | // codeTag := "[mspctag_" + strconv.FormatInt(int64(len(codeMap)+1), 10) + "]"
45 | // codeMap[codeTag] = "" + m + "
"
46 | // return codeTag
47 | // })
48 | //
49 | // input = ContentRich(db, input)
50 | // // replace tmp code tag
51 | // if len(codeMap) > 0 {
52 | // for k, v := range codeMap {
53 | // input = strings.Replace(input, k, v, -1)
54 | // }
55 | // }
56 | // //
57 | // input = strings.Replace(input, "", "", -1)
58 | // input = strings.Replace(input, "
", "
", -1)
59 | // return input
60 | // }
61 | return ContentRich(db, input)
62 | }
63 |
64 | // type urlInfo struct {
65 | // Href string
66 | // Click string
67 | // }
68 |
69 | func ContentRich(db *youdb.DB, input string) string {
70 | input = strings.TrimSpace(input)
71 | // input = " " + input // fix Has url Prefix
72 | // input = strings.Replace(input, "<", "<", -1)
73 | // input = strings.Replace(input, ">", ">", -1)
74 | // input = imgRegexp.ReplaceAllString(input, `
`)
75 |
76 | // video
77 | // youku
78 | // if strings.Index(input, "player.youku.com") >= 0 {
79 | // input = youku1Regexp.ReplaceAllString(input, ``)
80 | // }
81 | // if strings.Index(input, "v.youku.com") >= 0 {
82 | // input = youku2Regexp.ReplaceAllString(input, ``)
83 | // }
84 |
85 | // if strings.Index(input, "://gist") >= 0 {
86 | // input = gistRegexp.ReplaceAllString(input, ``)
87 | // }
88 | if strings.Index(input, "@") >= 0 {
89 | input = mentionRegexp.ReplaceAllString(input, ` @$1 `)
90 | }
91 | // if strings.Index(input, "http") >= 0 {
92 | // //input = urlRegexp.ReplaceAllString(input, `$1$2`)
93 | // urlMd5Map := map[string]urlInfo{}
94 | // var keys [][]byte
95 | // input = urlRegexp.ReplaceAllStringFunc(input, func(m string) string {
96 | // n := strings.Index(m, "http")
97 | // url := strings.Replace(strings.TrimSpace(m[n:]), "&", "&", -1)
98 | // hash := md5.Sum([]byte(url))
99 | // urlMd5 := hex.EncodeToString(hash[:])
100 | // urlMd5Map[urlMd5] = urlInfo{Href: url}
101 | // keys = append(keys, []byte(urlMd5))
102 | // return m[:n] + "[" + urlMd5 + "]"
103 | // })
104 | //
105 | // if len(urlMd5Map) > 0 {
106 | // rs := db.Hmget("url_md5_click", keys)
107 | // for i := 0; i < (len(rs.Data) - 1); i += 2 {
108 | // key := rs.Data[i].String()
109 | // tmp := urlMd5Map[key]
110 | // tmp.Click = youdb.B2ds(rs.Data[i+1])
111 | // urlMd5Map[key] = tmp
112 | // }
113 | // for k, v := range urlMd5Map {
114 | // var aTag string
115 | // if len(v.Click) > 0 {
116 | // aTag = `` + v.Href + ` ` + v.Click + ``
117 | // } else {
118 | // aTag = `` + v.Href + ``
119 | // }
120 | // input = strings.Replace(input, "["+k+"]", aTag, -1)
121 | // }
122 | // }
123 | // }
124 |
125 | // input = strings.Replace(input, "\r\n", "\n", -1)
126 | // input = strings.Replace(input, "\r", "\n", -1)
127 |
128 | // input = nlineRegexp.ReplaceAllString(input, "")
129 | // input = strings.Replace(input, "\n", "
", -1)
130 | //
131 | // input = "
" + input + "
"
132 | // input = strings.Replace(input, "", "", -1)
133 |
134 | text := []byte(input)
135 | md := github_flavored_markdown.Markdown(text)
136 | output := string(md)
137 |
138 | if strings.Index(output, "youtu.be") >= 0 {
139 | output = youtubeRegexp.ReplaceAllString(output, `
140 |
141 |
142 |
145 |
146 | `)
147 | }
148 |
149 | return output
150 | }
151 |
152 | func GetMention(input string, notInclude []string) []string {
153 | notIncludeMap := make(map[string]struct{}, len(notInclude))
154 | for _, v := range notInclude {
155 | notIncludeMap[v] = struct{}{}
156 | }
157 | sbMap := map[string]struct{}{}
158 | for _, at := range mentionRegexp.FindAllString(input, -1) {
159 | sb := strings.TrimSpace(at)[1:]
160 | if _, ok := notIncludeMap[sb]; ok {
161 | continue
162 | }
163 | sbMap[sb] = struct{}{}
164 | }
165 | if len(sbMap) > 0 {
166 | sb := make([]string, len(sbMap))
167 | i := 0
168 | for k := range sbMap {
169 | sb[i] = k
170 | i++
171 | }
172 | return sb
173 | }
174 | return []string{}
175 | }
176 |
--------------------------------------------------------------------------------
/view/default/mobile/layout.html:
--------------------------------------------------------------------------------
1 | {{ define "youbbs" }}
2 |
3 |
4 |
5 |
6 | {{.Title}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{if or (eq .Title "注册") (eq .Title "登录") }}
19 |
20 | {{end}}
21 |
22 | {{if or (eq .Title "用户列表") (eq .Title "修改用户") }}
23 |
24 | {{end}}
25 |
26 | {{if eq .Title "设置" }}
27 |
28 | {{end}}
29 |
30 |
31 |
32 |
33 |
34 | {{if or (eq .PageName "article_detail") (eq .PageName "article_add")}}
35 |
40 |
41 |
44 | {{end}}
45 |
46 | {{if or (eq .PageName "comment_edit") (eq .PageName "article_edit")}}
47 |
52 |
53 |
56 | {{end}}
57 |
58 |
76 |
77 |
78 |
79 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | {{if .CurrentUser.Id}}
111 |
112 | {{if eq .CurrentUser.Flag 0}}
113 |
站内提醒 » 帐户已被管理员禁用
114 | {{else if eq .CurrentUser.Flag 1}}
115 |
站内提醒 » 帐户在等待管理员审核
116 | {{else}}
117 |
118 | {{if not .CurrentUser.Password }}
119 |
120 | {{end}}
121 | {{end}}
122 |
123 |
124 |
125 | {{if eq .CurrentUser.Flag 99}}
126 |
127 |
136 | {{end}}
137 |
138 | {{end}}
139 |
140 | {{ template "content" . }}
141 |
142 | {{if .HotNodes}}
143 |
144 |
145 | {{range $_, $v := .HotNodes}}
146 |
{{$v.Name}}
147 | {{end}}
148 |
149 |
150 |
151 |
152 | {{ end }}
153 |
154 | {{ template "side" . }}
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
175 |
176 |
177 |
178 | {{ end }}
179 |
180 |
181 |
182 | {{ define "side" }}
183 |
184 |
185 | {{if eq .PageName "home"}}
186 | 公告
187 |
188 |
{{.Announcement}}
189 |
190 |
191 |
192 | 关于
193 |
194 |
{{.Description}}
195 |
202 |
203 |
204 | {{ end }}
205 |
206 | {{if eq .PageName "article_detail"}}
207 | {{if .Relative.Tags}}
208 | 相邻的标签
209 |
210 |
211 | {{range $_, $item := .Relative.Tags}}
{{$item}} {{ end }}
212 |
213 |
214 |
215 |
216 | {{ end }}
217 | {{ end }}
218 |
219 | {{if eq .PageName "home"}}
220 | {{if .Links}}
221 | 链接
222 |
223 |
224 | {{range $_, $v := .Links}}
225 |
{{$v.Name}}
226 | {{end}}
227 |
228 |
229 |
230 | {{ end }}
231 |
232 | {{if .SiteInfo}}
233 | 本站已稳定运行 {{.SiteInfo.Days}} 天
234 |
235 |
236 | - 会员: {{.SiteInfo.UserNum}}
237 | - 帖子: {{.SiteInfo.PostNum}}
238 | - 回复: {{.SiteInfo.ReplyNum}}
239 | - 分类: {{.SiteInfo.NodeNum}}
240 | - 标签: {{.SiteInfo.TagNum}}
241 |
242 |
243 |
244 | {{ end }}
245 | {{ end }}
246 |
247 | {{ end}}
248 |
--------------------------------------------------------------------------------
/controller/adminarticleedit.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "bytes"
5 | "crypto/md5"
6 | "encoding/hex"
7 | "encoding/json"
8 | "net/http"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/ego008/youdb"
13 | "github.com/rs/xid"
14 | "github.com/terminus2049/2049bbs/model"
15 | "github.com/terminus2049/2049bbs/util"
16 | "goji.io/pat"
17 | )
18 |
19 | func (h *BaseHandler) ArticleEdit(w http.ResponseWriter, r *http.Request) {
20 | aid := pat.Param(r, "aid")
21 | _, err := strconv.Atoi(aid)
22 | if err != nil {
23 | w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
24 | return
25 | }
26 |
27 | currentUser, _ := h.CurrentUser(w, r)
28 | if currentUser.Id == 0 {
29 | w.Write([]byte(`{"retcode":401,"retmsg":"authored err"}`))
30 | return
31 | }
32 | if currentUser.Flag < 99 {
33 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
34 | return
35 | }
36 |
37 | db := h.App.Db
38 |
39 | aobj, err := model.ArticleGetById(db, aid)
40 | if err != nil {
41 | w.Write([]byte(`{"retcode":403,"retmsg":"aid not found"}`))
42 | return
43 | }
44 | aidB := youdb.I2b(aobj.Id)
45 |
46 | cobj, err := model.CategoryGetById(db, strconv.FormatUint(aobj.Cid, 10))
47 | if err != nil {
48 | w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
49 | return
50 | }
51 |
52 | act := r.FormValue("act")
53 |
54 | if act == "del" {
55 | // remove
56 | // 总文章列表
57 | db.Zdel("article_timeline", aidB)
58 | // 分类文章列表
59 | db.Zdel("category_article_timeline:"+strconv.FormatUint(aobj.Cid, 10), aidB)
60 | // 用户文章列表
61 | db.Hdel("user_article_timeline:"+strconv.FormatUint(aobj.Uid, 10), aidB)
62 | // 分类下文章数
63 | db.Zincr("category_article_num", youdb.I2b(aobj.Cid), -1)
64 | // 删除标题记录
65 | hash := md5.Sum([]byte(aobj.Title))
66 | titleMd5 := hex.EncodeToString(hash[:])
67 | db.Hdel("title_md5", []byte(titleMd5))
68 |
69 | // set
70 | db.Hset("article_hidden", aidB, []byte(""))
71 | aobj.Hidden = true
72 | jb, _ := json.Marshal(aobj)
73 | db.Hset("article", aidB, jb)
74 | uobj, _ := model.UserGetById(db, aobj.Uid)
75 | if uobj.Articles > 0 {
76 | uobj.Articles--
77 | }
78 | jb, _ = json.Marshal(uobj)
79 | db.Hset("user", youdb.I2b(uobj.Id), jb)
80 |
81 | // tag send task work,自动处理tag与文章id
82 | at := model.ArticleTag{
83 | Id: aobj.Id,
84 | OldTags: aobj.Tags,
85 | NewTags: "",
86 | }
87 | jb, _ = json.Marshal(at)
88 | db.Hset("task_to_set_tag", youdb.I2b(at.Id), jb)
89 |
90 | http.Redirect(w, r, "/", http.StatusSeeOther)
91 | return
92 | }
93 |
94 | type pageData struct {
95 | PageData
96 | Cobj model.Category
97 | MainNodes []model.CategoryMini
98 | Aobj model.Article
99 | }
100 |
101 | tpl := h.CurrentTpl(r)
102 | evn := &pageData{}
103 | evn.SiteCf = h.App.Cf.Site
104 | evn.Title = "修改文章"
105 | evn.IsMobile = tpl == "mobile"
106 | evn.CurrentUser = currentUser
107 | evn.ShowSideAd = true
108 | evn.PageName = "article_edit"
109 |
110 | evn.Cobj = cobj
111 | evn.MainNodes = model.CategoryGetMain(db, cobj)
112 | evn.Aobj = aobj
113 |
114 | h.SetCookie(w, "token", xid.New().String(), 1)
115 | h.Render(w, tpl, evn, "layout.html", "adminarticleedit.html")
116 | }
117 |
118 | func (h *BaseHandler) ArticleEditPost(w http.ResponseWriter, r *http.Request) {
119 | w.Header().Set("Content-Type", "application/json; charset=UTF-8")
120 |
121 | aid := pat.Param(r, "aid")
122 | aidI, err := strconv.Atoi(aid)
123 | if err != nil {
124 | w.Write([]byte(`{"retcode":400,"retmsg":"cid type err"}`))
125 | return
126 | }
127 |
128 | token := h.GetCookie(r, "token")
129 | if len(token) == 0 {
130 | w.Write([]byte(`{"retcode":400,"retmsg":"token cookie missed"}`))
131 | return
132 | }
133 |
134 | currentUser, _ := h.CurrentUser(w, r)
135 | if currentUser.Id == 0 {
136 | w.Write([]byte(`{"retcode":401,"retmsg":"authored require"}`))
137 | return
138 | }
139 | if currentUser.Flag < 99 {
140 | w.Write([]byte(`{"retcode":403,"retmsg":"flag forbidden}`))
141 | return
142 | }
143 |
144 | type recForm struct {
145 | Aid uint64 `json:"aid"`
146 | Act string `json:"act"`
147 | Cid uint64 `json:"cid"`
148 | Title string `json:"title"`
149 | Content string `json:"content"`
150 | Tags string `json:"tags"`
151 | CloseComment string `json:"closecomment"`
152 | }
153 |
154 | decoder := json.NewDecoder(r.Body)
155 | var rec recForm
156 | err = decoder.Decode(&rec)
157 | if err != nil {
158 | w.Write([]byte(`{"retcode":400,"retmsg":"json Decode err:` + err.Error() + `"}`))
159 | return
160 | }
161 | defer r.Body.Close()
162 |
163 | rec.Aid = uint64(aidI)
164 |
165 | aidS := strconv.FormatUint(rec.Aid, 10)
166 | aidB := youdb.I2b(rec.Aid)
167 |
168 | rec.Title = strings.TrimSpace(rec.Title)
169 | rec.Content = strings.TrimSpace(rec.Content)
170 | rec.Tags = util.CheckTags(rec.Tags)
171 |
172 | db := h.App.Db
173 | if rec.Act == "preview" {
174 | tmp := struct {
175 | normalRsp
176 | Html string `json:"html"`
177 | }{
178 | normalRsp{200, ""},
179 | util.ContentFmt(db, rec.Content),
180 | }
181 | json.NewEncoder(w).Encode(tmp)
182 | return
183 | }
184 |
185 | // check title
186 | hash := md5.Sum([]byte(rec.Title))
187 | titleMd5 := hex.EncodeToString(hash[:])
188 | rs0 := db.Hget("title_md5", []byte(titleMd5))
189 | if rs0.State == "ok" && !bytes.Equal(rs0.Data[0], aidB) {
190 | w.Write([]byte(`{"retcode":403,"retmsg":"title has existed"}`))
191 | return
192 | }
193 |
194 | scf := h.App.Cf.Site
195 |
196 | if rec.Cid == 0 || len(rec.Title) == 0 {
197 | w.Write([]byte(`{"retcode":400,"retmsg":"missed args"}`))
198 | return
199 | }
200 | if len(rec.Title) > scf.TitleMaxLen {
201 | w.Write([]byte(`{"retcode":403,"retmsg":"TitleMaxLen limited"}`))
202 | return
203 | }
204 | if len(rec.Content) > scf.ContentMaxLen {
205 | w.Write([]byte(`{"retcode":403,"retmsg":"ContentMaxLen limited"}`))
206 | return
207 | }
208 |
209 | _, err = model.CategoryGetById(db, strconv.FormatUint(rec.Cid, 10))
210 | if err != nil {
211 | w.Write([]byte(`{"retcode":404,"retmsg":"` + err.Error() + `"}`))
212 | return
213 | }
214 |
215 | aobj, err := model.ArticleGetById(db, aidS)
216 | if err != nil {
217 | w.Write([]byte(`{"retcode":403,"retmsg":"aid not found"}`))
218 | return
219 | }
220 |
221 | var closeComment bool
222 | if rec.CloseComment == "1" {
223 | closeComment = true
224 | }
225 |
226 | if aobj.Cid == rec.Cid && aobj.Title == rec.Title && aobj.Content == rec.Content && aobj.Tags == rec.Tags && aobj.CloseComment == closeComment {
227 | w.Write([]byte(`{"retcode":201,"retmsg":"nothing changed"}`))
228 | return
229 | }
230 |
231 | oldCid := aobj.Cid
232 | oldTitle := aobj.Title
233 | oldTags := aobj.Tags
234 |
235 | aobj.Cid = rec.Cid
236 | aobj.Title = rec.Title
237 | aobj.Content = rec.Content
238 | aobj.Tags = rec.Tags
239 | if rec.Cid == 20 {
240 | aobj.Tags = ""
241 | }
242 | aobj.CloseComment = closeComment
243 |
244 | jb, _ := json.Marshal(aobj)
245 | db.Hset("article", aidB, jb)
246 |
247 | if oldCid != rec.Cid {
248 | db.Zincr("category_article_num", youdb.I2b(rec.Cid), 1)
249 | db.Zincr("category_article_num", youdb.I2b(oldCid), -1)
250 |
251 | db.Zset("category_article_timeline:"+strconv.FormatUint(rec.Cid, 10), aidB, aobj.EditTime)
252 | db.Zdel("category_article_timeline:"+strconv.FormatUint(oldCid, 10), aidB)
253 | }
254 |
255 | ignorenodes := scf.NotHomeNodeIds
256 | oldisHome := true
257 | newisHome := true
258 |
259 | if len(ignorenodes) > 0 {
260 | for _, node := range strings.Split(ignorenodes, ",") {
261 | node, err := strconv.Atoi(node)
262 | if err == nil && oldCid == uint64(node) {
263 | oldisHome = false
264 | }
265 | if err == nil && rec.Cid == uint64(node) {
266 | newisHome = false
267 | }
268 | }
269 | }
270 |
271 | // 原来在主页区,被移动到非主区,从时间线内删除
272 | if oldisHome && !newisHome {
273 | db.Zdel("article_timeline", youdb.I2b(aobj.Id))
274 | }
275 |
276 | // 原来在非主区,移动到主区,加入时间线
277 | if !oldisHome && newisHome {
278 | db.Zset("article_timeline", youdb.I2b(aobj.Id), aobj.EditTime)
279 | }
280 |
281 | if oldTitle != rec.Title {
282 | hash0 := md5.Sum([]byte(oldTitle))
283 | titleMd50 := hex.EncodeToString(hash0[:])
284 | db.Hdel("title_md5", []byte(titleMd50))
285 | db.Hset("title_md5", []byte(titleMd5), aidB)
286 | }
287 |
288 | if oldTags != rec.Tags {
289 | // tag send task work ,自动处理tag与文章id
290 | at := model.ArticleTag{
291 | Id: aobj.Id,
292 | OldTags: oldTags,
293 | NewTags: aobj.Tags,
294 | }
295 | jb, _ = json.Marshal(at)
296 | db.Hset("task_to_set_tag", youdb.I2b(at.Id), jb)
297 | }
298 |
299 | h.DelCookie(w, "token")
300 |
301 | tmp := struct {
302 | normalRsp
303 | Aid uint64 `json:"aid"`
304 | }{
305 | normalRsp{200, "ok"},
306 | aobj.Id,
307 | }
308 | json.NewEncoder(w).Encode(tmp)
309 | }
310 |
--------------------------------------------------------------------------------
/view/default/desktop/adminuseredit.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
36 |
37 |
38 | 基本资料 {{.Uobj.Name}}
39 |
73 |
74 |
75 | 为{{.Uobj.Name}}设置头像
76 |
77 |
100 |
101 |
102 |
103 | 为{{.Uobj.Name}}重设密码
104 |
123 |
124 |
230 |
231 | {{ end}}
232 |
--------------------------------------------------------------------------------
/view/default/mobile/adminuseredit.html:
--------------------------------------------------------------------------------
1 | {{ define "content" }}
2 |
3 |
6 |
7 |
36 |
37 |
38 | 基本资料 {{.Uobj.Name}}
39 |
73 |
74 |
75 | 为{{.Uobj.Name}}设置头像
76 |
77 |
100 |
101 |
102 |
103 | 为{{.Uobj.Name}}重设密码
104 |
123 |
124 |
230 |
231 | {{ end}}
232 |
--------------------------------------------------------------------------------