3 | <% } else { %>
4 |
40 |
--------------------------------------------------------------------------------
/public/libs/code-prettify/lang-vhdl.js:
--------------------------------------------------------------------------------
1 | PR.registerLangHandler(PR.createSimpleLexer([
2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"]
3 | ], [
4 | ["str", /^(?:[box]?"(?:[^"]|"")*"|'.')/i],
5 | ["com", /^--[^\n\r]*/],
6 | ["kwd", /^(?:abs|access|after|alias|all|and|architecture|array|assert|attribute|begin|block|body|buffer|bus|case|component|configuration|constant|disconnect|downto|else|elsif|end|entity|exit|file|for|function|generate|generic|group|guarded|if|impure|in|inertial|inout|is|label|library|linkage|literal|loop|map|mod|nand|new|next|nor|not|null|of|on|open|or|others|out|package|port|postponed|procedure|process|pure|range|record|register|reject|rem|report|return|rol|ror|select|severity|shared|signal|sla|sll|sra|srl|subtype|then|to|transport|type|unaffected|units|until|use|variable|wait|when|while|with|xnor|xor)(?=[^\w-]|$)/i,
7 | null],
8 | ["typ", /^(?:bit|bit_vector|character|boolean|integer|real|time|string|severity_level|positive|natural|signed|unsigned|line|text|std_u?logic(?:_vector)?)(?=[^\w-]|$)/i, null],
9 | ["typ", /^'(?:active|ascending|base|delayed|driving|driving_value|event|high|image|instance_name|last_active|last_event|last_value|left|leftof|length|low|path_name|pos|pred|quiet|range|reverse_range|right|rightof|simple_name|stable|succ|transaction|val|value)(?=[^\w-]|$)/i, null],
10 | ["lit", /^\d+(?:_\d+)*(?:#[\w.\\]+#(?:[+-]?\d+(?:_\d+)*)?|(?:\.\d+(?:_\d+)*)?(?:e[+-]?\d+(?:_\d+)*)?)/i],
11 | ["pln", /^(?:[a-z]\w*|\\[^\\]*\\)/i],
12 | ["pun", /^[^\w\t\n\r "'\xa0][^\w\t\n\r "'\xa0-]*/]
13 | ]), ["vhdl", "vhd"]);
14 |
--------------------------------------------------------------------------------
/public/libs/code-prettify/lang-n.js:
--------------------------------------------------------------------------------
1 | var a = null;
2 | PR.registerLangHandler(PR.createSimpleLexer([
3 | ["str", /^(?:'(?:[^\n\r'\\]|\\.)*'|"(?:[^\n\r"\\]|\\.)*(?:"|$))/, a, '"'],
4 | ["com", /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/, a, "#"],
5 | ["pln", /^\s+/, a, " \r\n\t\xa0"]
6 | ], [
7 | ["str", /^@"(?:[^"]|"")*(?:"|$)/, a],
8 | ["str", /^<#[^#>]*(?:#>|$)/, a],
9 | ["str", /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, a],
10 | ["com", /^\/\/[^\n\r]*/, a],
11 | ["com", /^\/\*[\S\s]*?(?:\*\/|$)/,
12 | a],
13 | ["kwd", /^(?:abstract|and|as|base|catch|class|def|delegate|enum|event|extern|false|finally|fun|implements|interface|internal|is|macro|match|matches|module|mutable|namespace|new|null|out|override|params|partial|private|protected|public|ref|sealed|static|struct|syntax|this|throw|true|try|type|typeof|using|variant|virtual|volatile|when|where|with|assert|assert2|async|break|checked|continue|do|else|ensures|for|foreach|if|late|lock|new|nolate|otherwise|regexp|repeat|requires|return|surroundwith|unchecked|unless|using|while|yield)\b/,
14 | a],
15 | ["typ", /^(?:array|bool|byte|char|decimal|double|float|int|list|long|object|sbyte|short|string|ulong|uint|ufloat|ulong|ushort|void)\b/, a],
16 | ["lit", /^@[$_a-z][\w$@]*/i, a],
17 | ["typ", /^@[A-Z]+[a-z][\w$@]*/, a],
18 | ["pln", /^'?[$_a-z][\w$@]*/i, a],
19 | ["lit", /^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i, a, "0123456789"],
20 | ["pun", /^.[^\s\w"-$'./@`]*/, a]
21 | ]), ["n", "nemerle"]);
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TESTS = $(shell find test -type f -name "*.test.js")
2 | TEST_TIMEOUT = 10000
3 | MOCHA_REPORTER = spec
4 | # NPM_REGISTRY = "--registry=http://registry.npm.taobao.org"
5 | NPM_REGISTRY = ""
6 |
7 |
8 | all: test
9 |
10 | install:
11 | @npm install $(NPM_REGISTRY)
12 |
13 | pretest:
14 | @if ! test -f config.js; then \
15 | cp config.default.js config.js; \
16 | fi
17 | @if ! test -d public/upload; then \
18 | mkdir public/upload; \
19 | fi
20 |
21 | test: install pretest
22 | @NODE_ENV=test ./node_modules/mocha/bin/mocha \
23 | --reporter $(MOCHA_REPORTER) \
24 | -r should \
25 | -r test/env \
26 | --timeout $(TEST_TIMEOUT) \
27 | $(TESTS)
28 |
29 | testfile:
30 | @NODE_ENV=test ./node_modules/mocha/bin/mocha \
31 | --reporter $(MOCHA_REPORTER) \
32 | -r should \
33 | -r test/env \
34 | --timeout $(TEST_TIMEOUT) \
35 | $(FILE)
36 |
37 | test-cov cov: install pretest
38 | @NODE_ENV=test node \
39 | node_modules/.bin/istanbul cover --preserve-comments \
40 | ./node_modules/.bin/_mocha \
41 | -- \
42 | -r should \
43 | -r test/env \
44 | --reporter $(MOCHA_REPORTER) \
45 | --timeout $(TEST_TIMEOUT) \
46 | $(TESTS)
47 |
48 |
49 | build:
50 | @./node_modules/loader-builder/bin/builder views .
51 |
52 | run:
53 | @node app.js
54 |
55 | start: install build
56 | @NODE_ENV=production ./node_modules/.bin/pm2 start app.js -i 0 --name "cnode" --max-memory-restart 400M
57 |
58 | restart: install build
59 | @NODE_ENV=production ./node_modules/.bin/pm2 restart "cnode"
60 |
61 | .PHONY: install test testfile cov test-cov build run start restart
62 |
--------------------------------------------------------------------------------
/public/libs/code-prettify/lang-clj.js:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2011 Google Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | var a = null;
17 | PR.registerLangHandler(PR.createSimpleLexer([
18 | ["opn", /^[([{]+/, a, "([{"],
19 | ["clo", /^[)\]}]+/, a, ")]}"],
20 | ["com", /^;[^\n\r]*/, a, ";"],
21 | ["pln", /^[\t\n\r \xa0]+/, a, "\t\n\r \xa0"],
22 | ["str", /^"(?:[^"\\]|\\[\S\s])*(?:"|$)/, a, '"']
23 | ], [
24 | ["kwd", /^(?:def|if|do|let|quote|var|fn|loop|recur|throw|try|monitor-enter|monitor-exit|defmacro|defn|defn-|macroexpand|macroexpand-1|for|doseq|dosync|dotimes|and|or|when|not|assert|doto|proxy|defstruct|first|rest|cons|defprotocol|deftype|defrecord|reify|defmulti|defmethod|meta|with-meta|ns|in-ns|create-ns|import|intern|refer|alias|namespace|resolve|ref|deref|refset|new|set!|memfn|to-array|into-array|aset|gen-class|reduce|map|filter|find|nil?|empty?|hash-map|hash-set|vec|vector|seq|flatten|reverse|assoc|dissoc|list|list?|disj|get|union|difference|intersection|extend|extend-type|extend-protocol|prn)\b/, a],
25 | ["typ", /^:[\dA-Za-z-]+/]
26 | ]), ["clj"]);
27 |
--------------------------------------------------------------------------------
/bin/fix_topic_collect_count.js:
--------------------------------------------------------------------------------
1 | var TopicCollect = require('../models').TopicCollect;
2 | var UserModel = require('../models').User;
3 | var TopicModel = require('../models').Topic
4 |
5 | // 修复用户的topic_collect计数
6 | TopicCollect.aggregate(
7 | [{
8 | "$group" :
9 | {
10 | _id : {user_id: "$user_id"},
11 | count : { $sum : 1}
12 | }
13 | }], function (err, result) {
14 | result.forEach(function (row) {
15 | var userId = row._id.user_id;
16 | var count = row.count;
17 |
18 | UserModel.findOne({
19 | _id: userId
20 | }, function (err, user) {
21 |
22 | if (!user) {
23 | return;
24 | }
25 |
26 | user.collect_topic_count = count;
27 | user.save(function () {
28 | console.log(user.loginname, count)
29 | });
30 | })
31 | })
32 | })
33 |
34 | // 修复帖子的topic_collect计数
35 | TopicCollect.aggregate(
36 | [{
37 | "$group" :
38 | {
39 | _id : {topic_id: "$topic_id"},
40 | count : { $sum : 1}
41 | }
42 | }], function (err, result) {
43 | result.forEach(function (row) {
44 | var topic_id = row._id.topic_id;
45 | var count = row.count;
46 |
47 | TopicModel.findOne({
48 | _id: topic_id
49 | }, function (err, topic) {
50 |
51 | if (!topic) {
52 | return;
53 | }
54 |
55 | topic.collect_topic_count = count;
56 | topic.save(function () {
57 | console.log(topic.id, count)
58 | });
59 | })
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/views/user/card.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
<%= user.loginname %>
7 |
8 |
9 |
10 | 积分: <%= user.score %>
11 |
12 |
13 |
14 |
15 | “
16 | <% if (user.signature) {%>
17 | <%-escapeSignature(user.signature)%>
18 | <%} else {%>
19 | 这家伙很懒,什么个性签名都没有留下。
20 | <%}%>
21 | ”
22 |
23 |
24 |
25 |
26 | <% if (current_user) { %>
27 |
52 | <% } %>
53 |
--------------------------------------------------------------------------------
/views/sign/reset.html:
--------------------------------------------------------------------------------
1 | <%- partial('../sign/sidebar') %>
2 |
3 |
4 |
5 |
11 |
12 | <% if(typeof(error) !== 'undefined' && error){ %>
13 |
14 |
×
15 |
<%= error %>
16 |
17 | <% } %>
18 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/views/sign/signin.html:
--------------------------------------------------------------------------------
1 | <%- partial('../sign/sidebar') %>
2 |
3 |
4 |
5 |
11 |
12 | <% if(typeof(error) !== 'undefined' && error){ %>
13 |
14 |
×
15 |
<%= error %>
16 |
17 | <% } %>
18 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Nodeclub
2 | =
3 |
4 | [![build status][travis-image]][travis-url]
5 | [![codecov.io][codecov-image]][codecov-url]
6 | [![David deps][david-image]][david-url]
7 | [![node version][node-image]][node-url]
8 |
9 | [travis-image]: https://img.shields.io/travis/cnodejs/nodeclub/master.svg?style=flat-square
10 | [travis-url]: https://travis-ci.org/cnodejs/nodeclub
11 | [codecov-image]: https://img.shields.io/codecov/c/github/cnodejs/nodeclub/master.svg?style=flat-square
12 | [codecov-url]: https://codecov.io/github/cnodejs/nodeclub?branch=master
13 | [david-image]: https://img.shields.io/david/cnodejs/nodeclub.svg?style=flat-square
14 | [david-url]: https://david-dm.org/cnodejs/nodeclub
15 | [node-image]: https://img.shields.io/badge/node.js-%3E=_4.2-green.svg?style=flat-square
16 | [node-url]: http://nodejs.org/download/
17 |
18 | ## 介绍
19 |
20 | Nodeclub 是使用 **Node.js** 和 **MongoDB** 开发的社区系统,界面优雅,功能丰富,小巧迅速,
21 | 已在Node.js 中文技术社区 [CNode(http://cnodejs.org)](http://cnodejs.org) 得到应用,但你完全可以用它搭建自己的社区。
22 |
23 | ## 安装部署
24 |
25 | *不保证 Windows 系统的兼容性*
26 |
27 | 线上跑的是 [Node.js](https://nodejs.org) v8.12.0,[MongoDB](https://www.mongodb.org) 是 v4.0.3,[Redis](http://redis.io) 是 v4.0.9。
28 |
29 | ```
30 | 1. 安装 `Node.js[必须]` `MongoDB[必须]` `Redis[必须]`
31 | 2. 启动 MongoDB 和 Redis
32 | 3. `$ make install` 安装 Nodeclub 的依赖包
33 | 4. `cp config.default.js config.js` 请根据需要修改配置文件
34 | 5. `$ make test` 确保各项服务都正常
35 | 6. `$ node app.js`
36 | 7. visit `http://localhost:3000`
37 | 8. done!
38 | ```
39 |
40 | ## 测试
41 |
42 | 跑测试
43 |
44 | ```bash
45 | $ make test
46 | ```
47 |
48 | 跑覆盖率测试
49 |
50 | ```bash
51 | $ make test-cov
52 | ```
53 |
54 | ## 贡献
55 |
56 | 有任何意见或建议都欢迎提 issue,或者直接提给 [@alsotang](https://github.com/alsotang)
57 |
58 | ## License
59 |
60 | MIT
61 |
--------------------------------------------------------------------------------
/views/static/getstart.html:
--------------------------------------------------------------------------------
1 | <%- partial('../sidebar') %>
2 |
3 |
4 |
5 |
11 |
12 |
13 | <%- markdown(multiline(function () {
14 | /*
15 |
16 | ## Node.js 入门
17 |
18 | 《**汇智网 Node.js 课程**》
19 |
20 | http://www.hubwiz.com/course/?type=nodes
21 |
22 | 《**快速搭建 Node.js 开发环境以及加速 npm**》
23 |
24 | http://fengmk2.com/blog/2014/03/node-env-and-faster-npm.html
25 |
26 | 《**Node.js 包教不包会**》
27 |
28 | https://github.com/alsotang/node-lessons
29 |
30 | 《**ECMAScript 6入门**》
31 |
32 | http://es6.ruanyifeng.com/
33 |
34 | 《**七天学会NodeJS**》
35 |
36 | https://github.com/nqdeng/7-days-nodejs
37 |
38 | 《**Node入门-_一本全面的Node.js教程_**》
39 |
40 | http://www.nodebeginner.org/index-zh-cn.html
41 |
42 | ## Node.js 资源
43 |
44 | 《**node weekly**》
45 |
46 | http://nodeweekly.com/issues
47 |
48 | 《**node123-_node.js中文资料导航_**》
49 |
50 | https://github.com/youyudehexie/node123
51 |
52 | 《**A curated list of delightful Node.js packages and resources**》
53 |
54 | https://github.com/sindresorhus/awesome-nodejs
55 |
56 | 《**Node.js Books**》
57 |
58 | https://github.com/pana/node-books
59 |
60 | ## Node.js 名人
61 |
62 | 《**名人堂**》
63 |
64 | https://github.com/cnodejs/nodeclub/wiki/%E5%90%8D%E4%BA%BA%E5%A0%82
65 |
66 | ## Node.js 服务器
67 |
68 | 新手搭建 Node.js 服务器,推荐使用无需备案的 [DigitalOcean(https://www.digitalocean.com/)](https://www.digitalocean.com/?refcode=eba02656eeb3)
69 |
70 | */
71 | })) %>
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/public/libs/code-prettify/lang-sql.js:
--------------------------------------------------------------------------------
1 | PR.registerLangHandler(PR.createSimpleLexer([
2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"],
3 | ["str", /^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/, null, "\"'"]
4 | ], [
5 | ["com", /^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],
6 | ["kwd", /^(?:add|all|alter|and|any|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|merge|national|nocheck|nonclustered|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|percent|plan|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rule|save|schema|select|session_user|set|setuser|shutdown|some|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|union|unique|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|writetext)(?=[^\w-]|$)/i,
7 | null],
8 | ["lit", /^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],
9 | ["pln", /^[_a-z][\w-]*/i],
10 | ["pun", /^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]
11 | ]), ["sql"]);
12 |
--------------------------------------------------------------------------------
/public/libs/code-prettify/lang-vb.js:
--------------------------------------------------------------------------------
1 | PR.registerLangHandler(PR.createSimpleLexer([
2 | ["pln", /^[\t\n\r \xa0\u2028\u2029]+/, null, "\t\n\r �\xa0
"],
3 | ["str", /^(?:["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})(?:["\u201c\u201d]c|$)|["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})*(?:["\u201c\u201d]|$))/i, null, '"“”'],
4 | ["com", /^['\u2018\u2019].*/, null, "'‘’"]
5 | ], [
6 | ["kwd", /^(?:addhandler|addressof|alias|and|andalso|ansi|as|assembly|auto|boolean|byref|byte|byval|call|case|catch|cbool|cbyte|cchar|cdate|cdbl|cdec|char|cint|class|clng|cobj|const|cshort|csng|cstr|ctype|date|decimal|declare|default|delegate|dim|directcast|do|double|each|else|elseif|end|endif|enum|erase|error|event|exit|finally|for|friend|function|get|gettype|gosub|goto|handles|if|implements|imports|in|inherits|integer|interface|is|let|lib|like|long|loop|me|mod|module|mustinherit|mustoverride|mybase|myclass|namespace|new|next|not|notinheritable|notoverridable|object|on|option|optional|or|orelse|overloads|overridable|overrides|paramarray|preserve|private|property|protected|public|raiseevent|readonly|redim|removehandler|resume|return|select|set|shadows|shared|short|single|static|step|stop|string|structure|sub|synclock|then|throw|to|try|typeof|unicode|until|variant|wend|when|while|with|withevents|writeonly|xor|endif|gosub|let|variant|wend)\b/i,
7 | null],
8 | ["com", /^rem.*/i],
9 | ["lit", /^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],
10 | ["pln", /^(?:(?:[a-z]|_\w)\w*|\[(?:[a-z]|_\w)\w*])/i],
11 | ["pun", /^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],
12 | ["pun", /^(?:\[|])/]
13 | ]), ["vb", "vbs"]);
14 |
--------------------------------------------------------------------------------
/views/sign/new_oauth.html:
--------------------------------------------------------------------------------
1 |
52 |
--------------------------------------------------------------------------------
/views/topic/list.html:
--------------------------------------------------------------------------------
1 |
2 | <%- partial('../topic/abstract', {collection:topics, as:'topic'}) %>
3 |
4 |
42 |
58 |
--------------------------------------------------------------------------------
/middlewares/limit.js:
--------------------------------------------------------------------------------
1 | var config = require('../config');
2 | var cache = require('../common/cache');
3 | var moment = require('moment');
4 |
5 | var SEPARATOR = '^_^@T_T';
6 |
7 | var makePerDayLimiter = function (identityName, identityFn) {
8 | return function (name, limitCount, options) {
9 | /*
10 | options.showJson = true 表示调用来自API并返回结构化数据;否则表示调用来自前段并渲染错误页面
11 | */
12 | return function (req, res, next) {
13 | var identity = identityFn(req);
14 | var YYYYMMDD = moment().format('YYYYMMDD');
15 | var key = YYYYMMDD + SEPARATOR + identityName + SEPARATOR + name + SEPARATOR + identity;
16 |
17 | cache.get(key, function (err, count) {
18 | if (err) {
19 | return next(err);
20 | }
21 | count = count || 0;
22 | if (count < limitCount) {
23 | count += 1;
24 | cache.set(key, count, 60 * 60 * 24);
25 | res.set('X-RateLimit-Limit', limitCount);
26 | res.set('X-RateLimit-Remaining', limitCount - count);
27 | next();
28 | } else {
29 | res.status(403);
30 | if (options.showJson) {
31 | res.send({success: false, error_msg: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'});
32 | } else {
33 | res.render('notify/notify', { error: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'});
34 | }
35 | }
36 | });
37 | };
38 | };
39 | };
40 |
41 | exports.peruserperday = makePerDayLimiter('peruserperday', function (req) {
42 | return (req.user || req.session.user).loginname;
43 | });
44 |
45 | exports.peripperday = makePerDayLimiter('peripperday', function (req) {
46 | var realIP = req.get('x-real-ip');
47 | if (!realIP && !config.debug) {
48 | throw new Error('should provide `x-real-ip` header')
49 | }
50 | return realIP;
51 | });
52 |
--------------------------------------------------------------------------------
/test/middlewares/proxy.test.js:
--------------------------------------------------------------------------------
1 | var proxyMiddleware = require('../../middlewares/proxy');
2 | var app = require('../../app');
3 | var support = require('../support/support');
4 | var supertest = require('supertest')(app);
5 | var mm = require('mm');
6 | var nock = require('nock');
7 |
8 | describe('test/middlewares/proxy.test.js', function () {
9 | before(function (done) {
10 | support.ready(done);
11 | });
12 |
13 | afterEach(function () {
14 | mm.restore();
15 | });
16 |
17 | it('should forbidden google.com', function (done) {
18 | supertest.get('/agent')
19 | .query({
20 | url: 'https://www.google.com.hk/#newwindow=1&q=%E5%85%AD%E5%9B%9B%E4%BA%8B%E4%BB%B6',
21 | })
22 | .end(function (err, res) {
23 | res.text.should.equal('www.google.com.hk is not allowed');
24 | done(err);
25 | });
26 | });
27 |
28 | it('should allow githubusercontent.com', function (done) {
29 | var url = 'https://avatars.githubusercontent.com/u/1147375?v=3&s=120';
30 |
31 | nock('https://avatars.githubusercontent.com')
32 | .get('/u/1147375?v=3&s=120')
33 | .reply(200, 'githubusercontent');
34 |
35 | supertest.get('/agent')
36 | .query({
37 | url: url,
38 | })
39 | .end(function (err, res) {
40 | res.text.should.eql('githubusercontent');
41 | done(err);
42 | });
43 | });
44 |
45 | it('should allow gravatar.com', function (done) {
46 | var url = 'https://gravatar.com/avatar/28d69c69c1c1a040436124238f7cc937?size=48';
47 | nock('https://gravatar.com')
48 | .get('/avatar/28d69c69c1c1a040436124238f7cc937?size=48')
49 | .reply(200, 'gravatar');
50 |
51 | supertest.get('/agent')
52 | .query({
53 | url: url,
54 | })
55 | .end(function (err, res) {
56 | res.text.should.eql('gravatar');
57 | done(err);
58 | });
59 | });
60 |
61 | });
62 |
--------------------------------------------------------------------------------
/views/reply/edit.html:
--------------------------------------------------------------------------------
1 | <%- partial('../editor_sidebar') %>
2 |
3 |
4 |
5 |
11 |
12 | <% if(typeof(edit_error) !== 'undefined' && edit_error){ %>
13 |
14 |
×
15 |
<%= edit_error %>
16 |
17 | <% } %>
18 | <% if(typeof(error) !== 'undefined' && error){ %>
19 |
20 | <%= error %>
21 |
22 | <% }else{ %>
23 |
43 |
44 | <% } %>
45 |
46 |
47 |
48 |
49 | <%- partial('../includes/editor') %>
50 |
56 |
--------------------------------------------------------------------------------
/test/api/v1/user.test.js:
--------------------------------------------------------------------------------
1 | var app = require('../../../app');
2 | var request = require('supertest')(app);
3 | var support = require('../../support/support');
4 | var should = require('should');
5 | var async = require('async');
6 |
7 | describe('test/api/v1/user.test.js', function () {
8 |
9 | var mockUser;
10 |
11 | before(function (done) {
12 | async.auto({
13 | create_user: function(callback){
14 | support.createUser(function (err, user) {
15 | mockUser = user;
16 | callback(null, user);
17 | });
18 | },
19 | create_topic: ['create_user', function(callback, result){
20 | support.createTopic(result['create_user']._id, function(err, topic){
21 | callback(null, topic);
22 | });
23 | }],
24 | create_replies: ['create_topic', function(callback, result){
25 | support.createReply(result['create_topic']._id, result['create_topic'].author_id, function(err, replay){
26 | callback(null, replay);
27 | });
28 | }]
29 | }, function(err, results){
30 | done();
31 | });
32 | });
33 |
34 | describe('get /api/v1/user/:loginname', function () {
35 |
36 | it('should return user info', function (done) {
37 | request.get('/api/v1/user/' + mockUser.loginname)
38 | .end(function (err, res) {
39 | should.not.exists(err);
40 | res.body.success.should.true();
41 | res.body.data.loginname.should.equal(mockUser.loginname);
42 | should(res.body.data.recent_topics.length).be.exactly(1);
43 | should(res.body.data.recent_replies.length).be.exactly(1);
44 | done();
45 | });
46 | });
47 |
48 | it('should fail when user is not found', function (done) {
49 | request.get('/api/v1/user/' + mockUser.loginname + 'not_found')
50 | .end(function (err, res) {
51 | should.not.exists(err);
52 | res.status.should.equal(404);
53 | res.body.success.should.false();
54 | done();
55 | });
56 | });
57 |
58 | });
59 |
60 | });
61 |
--------------------------------------------------------------------------------
/api_router_v1.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var topicController = require('./api/v1/topic');
3 | var topicCollectController = require('./api/v1/topic_collect');
4 | var userController = require('./api/v1/user');
5 | var toolsController = require('./api/v1/tools');
6 | var replyController = require('./api/v1/reply');
7 | var messageController = require('./api/v1/message');
8 | var middleware = require('./api/v1/middleware');
9 | var limit = require('./middlewares/limit');
10 | var config = require('./config');
11 |
12 | var router = express.Router();
13 |
14 |
15 | // 主题
16 | router.get('/topics', topicController.index);
17 | router.get('/topic/:id', middleware.tryAuth, topicController.show);
18 | router.post('/topics', middleware.auth, limit.peruserperday('create_topic', config.create_post_per_day, {showJson: true}), topicController.create);
19 | router.post('/topics/update', middleware.auth, topicController.update);
20 |
21 |
22 | // 主题收藏
23 | router.post('/topic_collect/collect', middleware.auth, topicCollectController.collect); // 关注某话题
24 | router.post('/topic_collect/de_collect', middleware.auth, topicCollectController.de_collect); // 取消关注某话题
25 | router.get('/topic_collect/:loginname', topicCollectController.list);
26 |
27 | // 用户
28 | router.get('/user/:loginname', userController.show);
29 |
30 |
31 |
32 | // accessToken 测试
33 | router.post('/accesstoken', middleware.auth, toolsController.accesstoken);
34 |
35 | // 评论
36 | router.post('/topic/:topic_id/replies', middleware.auth, limit.peruserperday('create_reply', config.create_reply_per_day, {showJson: true}), replyController.create);
37 | router.post('/reply/:reply_id/ups', middleware.auth, replyController.ups);
38 |
39 | // 通知
40 | router.get('/messages', middleware.auth, messageController.index);
41 | router.get('/message/count', middleware.auth, messageController.count);
42 | router.post('/message/mark_all', middleware.auth, messageController.markAll);
43 | router.post('/message/mark_one/:msg_id', middleware.auth, messageController.markOne);
44 |
45 | module.exports = router;
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodeclub",
3 | "version": "2.1.1",
4 | "private": true,
5 | "main": "app.js",
6 | "description": "A Node.js bbs using MongoDB",
7 | "repository": "https://github.com/cnodejs/nodeclub",
8 | "dependencies": {
9 | "async": "1.5.2",
10 | "bcryptjs": "2.3.0",
11 | "body-parser": "1.17.1",
12 | "bytes": "^2.2.0",
13 | "colors": "1.1.2",
14 | "compression": "1.7.0",
15 | "connect-busboy": "0.0.2",
16 | "connect-redis": "3.0.2",
17 | "cookie-parser": "1.4.1",
18 | "cors": "2.7.1",
19 | "csurf": "1.8.3",
20 | "data2xml": "1.2.4",
21 | "ejs-mate": "2.3.0",
22 | "eventproxy": "1.0.0",
23 | "express": "4.16.0",
24 | "express-session": "1.12.1",
25 | "helmet": "1.3.0",
26 | "ioredis": "2.0.0",
27 | "jpush-sdk": "3.3.2",
28 | "loader-builder": "2.4.1",
29 | "loader": "2.1.1",
30 | "lodash": "4.17.21",
31 | "log4js": "^0.6.29",
32 | "markdown-it": "6.0.0",
33 | "memory-cache": "0.1.4",
34 | "method-override": "2.3.5",
35 | "moment": "2.15.2",
36 | "mongoose": "5.3.9",
37 | "multiline": "1.0.2",
38 | "node-uuid": "1.4.7",
39 | "nodemailer": "2.3.0",
40 | "nodemailer-smtp-transport": "2.4.0",
41 | "oneapm": "1.2.20",
42 | "passport": "0.3.2",
43 | "passport-github": "1.1.0",
44 | "pm2": "*",
45 | "qn": "1.3.0",
46 | "ready": "0.1.1",
47 | "request": "2.81.0",
48 | "response-time": "2.3.1",
49 | "superagent": "2.0.0",
50 | "utility": "1.6.0",
51 | "validator": "5.1.0",
52 | "xmlbuilder": "7.0.0",
53 | "xss": "0.2.10",
54 | "snyk": "^1.88.0"
55 | },
56 | "devDependencies": {
57 | "errorhandler": "1.4.3",
58 | "istanbul": "0.4.2",
59 | "loader-connect": "1.0.1",
60 | "mm": "1.3.5",
61 | "mocha": "2.4.5",
62 | "nock": "7.5.0",
63 | "pedding": "1.0.0",
64 | "should": "8.3.0",
65 | "supertest": "1.2.0"
66 | },
67 | "scripts": {
68 | "test": "make test",
69 | "snyk-protect": "snyk protect",
70 | "prepare": "npm run snyk-protect"
71 | },
72 | "snyk": true
73 | }
74 |
--------------------------------------------------------------------------------
/controllers/rss.js:
--------------------------------------------------------------------------------
1 | var config = require('../config');
2 | var convert = require('data2xml')();
3 | var Topic = require('../proxy').Topic;
4 | var cache = require('../common/cache');
5 | var renderHelper = require('../common/render_helper');
6 | var eventproxy = require('eventproxy');
7 |
8 | exports.index = function (req, res, next) {
9 | if (!config.rss) {
10 | res.statusCode = 404;
11 | return res.send('Please set `rss` in config.js');
12 | }
13 | res.contentType('application/xml');
14 |
15 | var ep = new eventproxy();
16 | ep.fail(next);
17 |
18 | cache.get('rss', ep.done(function (rss) {
19 | if (!config.debug && rss) {
20 | res.send(rss);
21 | } else {
22 | var opt = {
23 | limit: config.rss.max_rss_items,
24 | sort: '-create_at',
25 | };
26 | Topic.getTopicsByQuery({tab: {$nin: ['dev']}}, opt, function (err, topics) {
27 | if (err) {
28 | return next(err);
29 | }
30 | var rss_obj = {
31 | _attr: { version: '2.0' },
32 | channel: {
33 | title: config.rss.title,
34 | link: config.rss.link,
35 | language: config.rss.language,
36 | description: config.rss.description,
37 | item: []
38 | }
39 | };
40 |
41 | topics.forEach(function (topic) {
42 | rss_obj.channel.item.push({
43 | title: topic.title,
44 | link: config.rss.link + '/topic/' + topic._id,
45 | guid: config.rss.link + '/topic/' + topic._id,
46 | description: renderHelper.markdown(topic.content),
47 | author: topic.author.loginname,
48 | pubDate: topic.create_at.toUTCString()
49 | });
50 | });
51 |
52 | var rssContent = convert('rss', rss_obj);
53 | rssContent = utf8ForXml(rssContent)
54 | cache.set('rss', rssContent, 60 * 5); // 五分钟
55 | res.send(rssContent);
56 | });
57 | }
58 | }));
59 | };
60 |
61 | function utf8ForXml(inputStr) {
62 | return inputStr.replace(/[^\x09\x0A\x0D\x20-\xFF\x85\xA0-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm, '');
63 | }
64 |
--------------------------------------------------------------------------------
/test/common/render_helper.test.js:
--------------------------------------------------------------------------------
1 | var should = require('should');
2 | var app = require('../../app');
3 | var request = require('supertest')(app);
4 | var mm = require('mm');
5 | var support = require('../support/support');
6 | var _ = require('lodash');
7 | var pedding = require('pedding');
8 | var multiline = require('multiline');
9 | var renderHelper = require('../../common/render_helper');
10 |
11 | describe('test/common/render_helper.test.js', function () {
12 | describe('#markdown', function () {
13 | it('should render code inline', function () {
14 | var text = multiline(function () {;
15 | /*
16 | `var a = 1;`
17 | */
18 | });
19 |
20 | var rendered = renderHelper.markdown(text);
21 | rendered.should.equal('
');
22 | });
23 |
24 | it('should render fence', function () {
25 | var text = multiline(function () {;
26 | /*
27 | ```js
28 | var a = 1;
29 | ```
30 | */
31 | });
32 |
33 | var rendered = renderHelper.markdown(text);
34 | rendered.should.equal('
');
35 | });
36 |
37 | it('should render code block', function () {
38 | var text = multiline(function () {;
39 | /*
40 | var a = 1;
41 | */
42 | });
43 |
44 | var rendered = renderHelper.markdown(text);
45 | rendered.should.equal('
');
46 | });
47 | });
48 |
49 | describe('#escapeSignature', function () {
50 | it('should escape content', function () {
51 | var signature = multiline(function () {;
52 | /*
53 | 我爱北京天安门
55 | */
56 | });
57 | var escaped = renderHelper.escapeSignature(signature);
58 | escaped.should.equal('我爱北京天安门<script>alert(1)
</script>');
59 | })
60 | })
61 |
62 | describe('#tabName', function () {
63 | it('should translate', function () {
64 | renderHelper.tabName('share')
65 | .should.equal('分享')
66 | })
67 | })
68 |
69 |
70 | });
71 |
--------------------------------------------------------------------------------
/test/api/v1/message.test.js:
--------------------------------------------------------------------------------
1 | var support = require('../../support/support');
2 | var message = require('../../../common/message');
3 | var MessageProxy = require('../../../proxy').Message;
4 | var app = require('../../../app');
5 | var request = require('supertest')(app);
6 | var mm = require('mm');
7 | var should = require('should');
8 |
9 | describe('test/api/v1/message.test.js', function () {
10 |
11 | var mockUser;
12 |
13 | before(function (done) {
14 | support.ready(function () {
15 | support.createUser(function (err, user) {
16 | mockUser = user;
17 | done();
18 | });
19 | });
20 | });
21 |
22 | afterEach(function () {
23 | mm.restore();
24 | });
25 |
26 | it('should get unread messages', function (done) {
27 | mm(MessageProxy, 'getMessageById', function (id, callback) {
28 | callback(null, {reply: {author: {}}});
29 | });
30 | message.sendReplyMessage(mockUser.id, mockUser.id, mockUser.id, mockUser.id,
31 | function (err) {
32 | should.not.exists(err);
33 | request.get('/api/v1/messages')
34 | .query({
35 | accesstoken: mockUser.accessToken
36 | })
37 | .end(function (err, res) {
38 | res.body.data.hasnot_read_messages.length.should.above(0);
39 | done();
40 | });
41 | });
42 | });
43 |
44 | it('should get unread messages count', function (done) {
45 | mm(MessageProxy, 'getMessageById', function (id, callback) {
46 | callback(null, {reply: {author: {}}});
47 | });
48 | request.get('/api/v1/message/count')
49 | .query({
50 | accesstoken: mockUser.accessToken
51 | })
52 | .end(function (err, res) {
53 | res.body.data.should.equal(1);
54 | done();
55 | });
56 | });
57 |
58 | it('should mark all messages read', function (done) {
59 | request.post('/api/v1/message/mark_all')
60 | .send({
61 | accesstoken: mockUser.accessToken
62 | })
63 | .end(function (err, res) {
64 | // 第一次查询有一个
65 | res.body.marked_msgs.length.should.equal(1);
66 | request.post('/api/v1/message/mark_all')
67 | .send({
68 | accesstoken: mockUser.accessToken
69 | })
70 | .end(function (err, res) {
71 | // 第二次查询没了
72 | res.body.marked_msgs.length.should.equal(0);
73 | done();
74 | });
75 | });
76 | });
77 |
78 | });
79 |
--------------------------------------------------------------------------------
/test/controllers/rss.test.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * nodeclub - rss controller test
3 | * Copyright(c) 2012 fengmk2
4 | * MIT Licensed
5 | */
6 |
7 | /**
8 | * Module dependencies.
9 | */
10 |
11 | var request = require('supertest');
12 | var app = require('../../app');
13 | var config = require('../../config');
14 |
15 | describe('test/controllers/rss.test.js', function () {
16 |
17 | describe('/rss', function () {
18 | it('should return `application/xml` Content-Type', function (done) {
19 | request(app).get('/rss').end(function (err, res) {
20 | res.status.should.equal(200);
21 | res.headers.should.property('content-type', 'application/xml; charset=utf-8');
22 | res.text.indexOf('').should.equal(0);
23 | res.text.should.containEql('');
24 | res.text.should.containEql('' + config.rss.title + '');
25 | done(err);
26 | });
27 | });
28 |
29 | describe('mock `config.rss` not set', function () {
30 | var rss = config.rss;
31 | before(function () {
32 | config.rss = null;
33 | });
34 | after(function () {
35 | config.rss = rss;
36 | });
37 |
38 | it('should return waring message', function (done) {
39 | request(app).get('/rss').end(function (err, res) {
40 | res.status.should.equal(404);
41 | res.text.should.equal('Please set `rss` in config.js');
42 | done(err);
43 | });
44 | });
45 | });
46 |
47 | describe('mock `topic.getTopicsByQuery()` error', function () {
48 | var topic = require('../../proxy').Topic;
49 | var getTopicsByQuery = topic.getTopicsByQuery;
50 | before(function () {
51 | topic.getTopicsByQuery = function () {
52 | var callback = arguments[arguments.length - 1];
53 | process.nextTick(function () {
54 | callback(new Error('mock getTopicsByQuery() error'));
55 | });
56 | };
57 | });
58 | after(function () {
59 | topic.getTopicsByQuery = getTopicsByQuery;
60 | });
61 |
62 | it('should return error', function (done) {
63 | request(app).get('/rss').end(function (err, res) {
64 | res.status.should.equal(500);
65 | res.text.should.containEql('mock getTopicsByQuery() error');
66 | done(err);
67 | });
68 | });
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/api/v1/user.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 | var eventproxy = require('eventproxy');
3 | var UserProxy = require('../../proxy').User;
4 | var TopicProxy = require('../../proxy').Topic;
5 | var ReplyProxy = require('../../proxy').Reply;
6 | var TopicCollect = require('../../proxy').TopicCollect;
7 |
8 | var show = function (req, res, next) {
9 | var loginname = req.params.loginname;
10 | var ep = new eventproxy();
11 |
12 | ep.fail(next);
13 |
14 | UserProxy.getUserByLoginName(loginname, ep.done(function (user) {
15 | if (!user) {
16 | res.status(404);
17 | return res.send({success: false, error_msg: '用户不存在'});
18 | }
19 | var query = {author_id: user._id};
20 | var opt = {limit: 15, sort: '-create_at'};
21 | TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_topics'));
22 |
23 | ReplyProxy.getRepliesByAuthorId(user._id, {limit: 20, sort: '-create_at'},
24 | ep.done(function (replies) {
25 | var topic_ids = replies.map(function (reply) {
26 | return reply.topic_id.toString()
27 | });
28 | topic_ids = _.uniq(topic_ids).slice(0, 5); // 只显示最近5条
29 |
30 | var query = {_id: {'$in': topic_ids}};
31 | var opt = {};
32 | TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_replies', function (recent_replies) {
33 | recent_replies = _.sortBy(recent_replies, function (topic) {
34 | return topic_ids.indexOf(topic._id.toString())
35 | });
36 | return recent_replies;
37 | }));
38 | }));
39 |
40 | ep.all('recent_topics', 'recent_replies',
41 | function (recent_topics, recent_replies) {
42 |
43 | user = _.pick(user, ['loginname', 'avatar_url', 'githubUsername',
44 | 'create_at', 'score']);
45 |
46 | user.recent_topics = recent_topics.map(function (topic) {
47 | topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
48 | topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']);
49 | return topic;
50 | });
51 | user.recent_replies = recent_replies.map(function (topic) {
52 | topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
53 | topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']);
54 | return topic;
55 | });
56 |
57 | res.send({success: true, data: user});
58 | });
59 | }));
60 | };
61 |
62 | exports.show = show;
63 |
--------------------------------------------------------------------------------
/public/javascripts/responsive.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 | var $responsiveBtn = $('#responsive-sidebar-trigger'),
3 | $sidebarMask = $('#sidebar-mask'),
4 | $sidebar = $('#sidebar'),
5 | $main = $('#main'),
6 | winWidth = $(window).width(),
7 | startX = 0,
8 | startY = 0,
9 | delta = {
10 | x: 0,
11 | y: 0
12 | },
13 | swipeThreshold = winWidth / 3,
14 | toggleSideBar = function () {
15 | var isShow = $responsiveBtn.data('is-show'),
16 | mainHeight = $main.height(),
17 | sidebarHeight = $sidebar.outerHeight();
18 | $sidebar.css({right: isShow ? -300 : 0});
19 | $responsiveBtn.data('is-show', !isShow);
20 | if (!isShow && mainHeight < sidebarHeight) {
21 | $main.height(sidebarHeight);
22 | }
23 | $sidebarMask[isShow ? 'fadeOut' : 'fadeIn']().height($('body').height());
24 | $sidebar[isShow ? 'hide' : 'show']()
25 | },
26 | touchstart = function (e) {
27 | var touchs = e.targetTouches;
28 | startX = +touchs[0].pageX;
29 | startY = +touchs[0].pageY;
30 | delta.x = delta.y = 0;
31 | document.body.addEventListener('touchmove', touchmove, false);
32 | document.body.addEventListener('touchend', touchend, false);
33 | },
34 | touchmove = function (e) {
35 | var touchs = e.changedTouches;
36 | delta.x = +touchs[0].pageX - startX;
37 | delta.y = +touchs[0].pageY - startY;
38 | //当水平距离大于垂直距离时,才认为是用户想滑动打开右侧栏
39 | if (Math.abs(delta.x) > Math.abs(delta.y)) {
40 | e.preventDefault();
41 | }
42 | },
43 | touchend = function (e) {
44 | var touchs = e.changedTouches,
45 | isShow = $responsiveBtn.data('is-show');
46 | delta.x = +touchs[0].pageX - startX;
47 | //右侧栏未显示&&用户touch点在屏幕右侧1/4区域内&&move距离大于阀值时,打开右侧栏
48 | if (!isShow && (startX > winWidth * 3 / 4) && Math.abs(delta.x) > swipeThreshold) {
49 | $responsiveBtn.trigger('click');
50 | }
51 | //右侧栏显示中&&用户touch点在屏幕左侧侧1/4区域内&&move距离大于阀值时,关闭右侧栏
52 | if (isShow && (startX < winWidth * 1 / 4) && Math.abs(delta.x) > swipeThreshold) {
53 | $responsiveBtn.trigger('click');
54 | }
55 | startX = startY = 0;
56 | delta.x = delta.y = 0;
57 | document.body.removeEventListener('touchmove', touchmove, false);
58 | document.body.removeEventListener('touchend', touchend, false);
59 | };
60 |
61 | if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
62 | document.body.addEventListener('touchstart', touchstart);
63 | }
64 |
65 | $responsiveBtn.on('click', toggleSideBar);
66 |
67 | $sidebarMask.on('click', function () {
68 | $responsiveBtn.trigger('click');
69 | });
70 |
71 | });
72 |
--------------------------------------------------------------------------------
/public/libs/code-prettify/prettify.css:
--------------------------------------------------------------------------------
1 | /* Pretty printing styles. Used with prettify.js. */
2 |
3 | /* SPAN elements with the classes below are added by prettyprint. */
4 | .pln {
5 | color: #000
6 | }
7 |
8 | /* plain text */
9 |
10 | @media screen {
11 | .str {
12 | color: #080
13 | }
14 |
15 | /* string content */
16 | .kwd {
17 | color: #008
18 | }
19 |
20 | /* a keyword */
21 | .com {
22 | color: #800
23 | }
24 |
25 | /* a comment */
26 | .typ {
27 | color: #606
28 | }
29 |
30 | /* a type name */
31 | .lit {
32 | color: #066
33 | }
34 |
35 | /* a literal value */
36 | /* punctuation, lisp open bracket, lisp close bracket */
37 | .pun, .opn, .clo {
38 | color: #660
39 | }
40 |
41 | .tag {
42 | color: #008
43 | }
44 |
45 | /* a markup tag name */
46 | .atn {
47 | color: #606
48 | }
49 |
50 | /* a markup attribute name */
51 | .atv {
52 | color: #080
53 | }
54 |
55 | /* a markup attribute value */
56 | .dec, .var {
57 | color: #606
58 | }
59 |
60 | /* a declaration; a variable name */
61 | .fun {
62 | color: red
63 | }
64 |
65 | /* a function name */
66 | }
67 |
68 | /* Use higher contrast and text-weight for printable form. */
69 | @media print, projection {
70 | .str {
71 | color: #060
72 | }
73 |
74 | .kwd {
75 | color: #006;
76 | font-weight: bold
77 | }
78 |
79 | .com {
80 | color: #600;
81 | font-style: italic
82 | }
83 |
84 | .typ {
85 | color: #404;
86 | font-weight: bold
87 | }
88 |
89 | .lit {
90 | color: #044
91 | }
92 |
93 | .pun, .opn, .clo {
94 | color: #440
95 | }
96 |
97 | .tag {
98 | color: #006;
99 | font-weight: bold
100 | }
101 |
102 | .atn {
103 | color: #404
104 | }
105 |
106 | .atv {
107 | color: #060
108 | }
109 | }
110 |
111 | /* Put a border around prettyprinted code snippets. */
112 | pre.prettyprint {
113 | padding: 2px;
114 | border: 1px solid #888
115 | }
116 |
117 | /* Specify class=linenums on a pre to get line numbering */
118 | ol.linenums {
119 | margin-top: 0;
120 | margin-bottom: 0
121 | }
122 |
123 | /* IE indents via margin-left */
124 | li.L0,
125 | li.L1,
126 | li.L2,
127 | li.L3,
128 | li.L5,
129 | li.L6,
130 | li.L7,
131 | li.L8 {
132 | list-style-type: none
133 | }
134 |
135 | /* Alternate shading for lines */
136 | li.L1,
137 | li.L3,
138 | li.L5,
139 | li.L7,
140 | li.L9 {
141 | background: #eee
142 | }
--------------------------------------------------------------------------------
/common/mail.js:
--------------------------------------------------------------------------------
1 | var mailer = require('nodemailer');
2 | var smtpTransport = require('nodemailer-smtp-transport');
3 | var config = require('../config');
4 | var util = require('util');
5 | var logger = require('./logger');
6 | var transporter = mailer.createTransport(smtpTransport(config.mail_opts));
7 | var SITE_ROOT_URL = 'http://' + config.host;
8 | var async = require('async')
9 |
10 | /**
11 | * Send an email
12 | * @param {Object} data 邮件对象
13 | */
14 | var sendMail = function (data) {
15 | if (config.debug) {
16 | return;
17 | }
18 |
19 | // 重试5次
20 | async.retry({times: 5}, function (done) {
21 | transporter.sendMail(data, function (err) {
22 | if (err) {
23 | // 写为日志
24 | logger.error('send mail error', err, data);
25 | return done(err);
26 | }
27 | return done()
28 | });
29 | }, function (err) {
30 | if (err) {
31 | return logger.error('send mail finally error', err, data);
32 | }
33 | logger.info('send mail success', data)
34 | })
35 | };
36 | exports.sendMail = sendMail;
37 |
38 | /**
39 | * 发送激活通知邮件
40 | * @param {String} who 接收人的邮件地址
41 | * @param {String} token 重置用的token字符串
42 | * @param {String} name 接收人的用户名
43 | */
44 | exports.sendActiveMail = function (who, token, name) {
45 | var from = util.format('%s <%s>', config.name, config.mail_opts.auth.user);
46 | var to = who;
47 | var subject = config.name + '社区帐号激活';
48 | var html = '您好:' + name + '
' +
49 | '我们收到您在' + config.name + '社区的注册信息,请点击下面的链接来激活帐户:
' +
50 | '激活链接' +
51 | '若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。
' +
52 | '' + config.name + '社区 谨上。
';
53 |
54 | exports.sendMail({
55 | from: from,
56 | to: to,
57 | subject: subject,
58 | html: html
59 | });
60 | };
61 |
62 | /**
63 | * 发送密码重置通知邮件
64 | * @param {String} who 接收人的邮件地址
65 | * @param {String} token 重置用的token字符串
66 | * @param {String} name 接收人的用户名
67 | */
68 | exports.sendResetPassMail = function (who, token, name) {
69 | var from = util.format('%s <%s>', config.name, config.mail_opts.auth.user);
70 | var to = who;
71 | var subject = config.name + '社区密码重置';
72 | var html = '您好:' + name + '
' +
73 | '我们收到您在' + config.name + '社区重置密码的请求,请在24小时内单击下面的链接来重置密码:
' +
74 | '重置密码链接' +
75 | '若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。
' +
76 | '' + config.name + '社区 谨上。
';
77 |
78 | exports.sendMail({
79 | from: from,
80 | to: to,
81 | subject: subject,
82 | html: html
83 | });
84 | };
85 |
--------------------------------------------------------------------------------
/test/common/message.test.js:
--------------------------------------------------------------------------------
1 | var should = require('should');
2 | var app = require('../../app');
3 | var request = require('supertest')(app);
4 | var mm = require('mm');
5 | var support = require('../support/support');
6 | var _ = require('lodash');
7 | var pedding = require('pedding');
8 | var multiline = require('multiline');
9 | var MessageService = require('../../common/message');
10 | var eventproxy = require('eventproxy');
11 | var ReplyProxy = require('../../proxy').Reply;
12 |
13 | describe('test/common/message.test.js', function () {
14 | var atUser;
15 | var author;
16 | var topic;
17 | var reply;
18 | before(function (done) {
19 | var ep = new eventproxy();
20 |
21 | ep.all('topic', function (_topic) {
22 | topic = _topic;
23 | done();
24 | });
25 | support.ready(function () {
26 | atUser = support.normalUser;
27 | author = atUser;
28 | reply = {};
29 | support.createTopic(author._id, ep.done('topic'));
30 | });
31 | });
32 |
33 | afterEach(function () {
34 | mm.restore();
35 | });
36 |
37 | describe('#sendReplyMessage', function () {
38 | it('should send reply message', function (done) {
39 | mm(ReplyProxy, 'getReplyById', function (id, callback) {
40 | callback(null, {author: {}});
41 | });
42 | MessageService.sendReplyMessage(atUser._id, author._id, topic._id, reply._id,
43 | function (err, msg) {
44 | request.get('/my/messages')
45 | .set('Cookie', support.normalUserCookie)
46 | .expect(200, function (err, res) {
47 | var texts = [
48 | author.loginname,
49 | '回复了你的话题',
50 | topic.title,
51 | ];
52 | texts.forEach(function (text) {
53 | res.text.should.containEql(text)
54 | })
55 | done(err);
56 | });
57 | });
58 | });
59 | });
60 |
61 | describe('#sendAtMessage', function () {
62 | it('should send at message', function (done) {
63 | mm(ReplyProxy, 'getReplyById', function (id, callback) {
64 | callback(null, {author: {}});
65 | });
66 | MessageService.sendAtMessage(atUser._id, author._id, topic._id, reply._id,
67 | function (err, msg) {
68 | request.get('/my/messages')
69 | .set('Cookie', support.normalUserCookie)
70 | .expect(200, function (err, res) {
71 | var texts = [
72 | author.loginname,
73 | '在话题',
74 | topic.title,
75 | '中@了你',
76 | ];
77 | texts.forEach(function (text) {
78 | res.text.should.containEql(text)
79 | })
80 | done(err);
81 | });
82 | });
83 | });
84 | });
85 | })
86 |
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 | var BaseModel = require("./base_model");
3 | var renderHelper = require('../common/render_helper');
4 | var Schema = mongoose.Schema;
5 | var utility = require('utility');
6 | var _ = require('lodash');
7 |
8 | var UserSchema = new Schema({
9 | name: { type: String},
10 | loginname: { type: String},
11 | pass: { type: String },
12 | email: { type: String},
13 | url: { type: String },
14 | profile_image_url: {type: String},
15 | location: { type: String },
16 | signature: { type: String },
17 | profile: { type: String },
18 | weibo: { type: String },
19 | avatar: { type: String },
20 | githubId: { type: String},
21 | githubUsername: {type: String},
22 | githubAccessToken: {type: String},
23 | is_block: {type: Boolean, default: false},
24 |
25 | score: { type: Number, default: 0 },
26 | topic_count: { type: Number, default: 0 },
27 | reply_count: { type: Number, default: 0 },
28 | follower_count: { type: Number, default: 0 },
29 | following_count: { type: Number, default: 0 },
30 | collect_tag_count: { type: Number, default: 0 },
31 | collect_topic_count: { type: Number, default: 0 },
32 | create_at: { type: Date, default: Date.now },
33 | update_at: { type: Date, default: Date.now },
34 | is_star: { type: Boolean },
35 | level: { type: String },
36 | active: { type: Boolean, default: false },
37 |
38 | receive_reply_mail: {type: Boolean, default: false },
39 | receive_at_mail: { type: Boolean, default: false },
40 | from_wp: { type: Boolean },
41 |
42 | retrieve_time: {type: Number},
43 | retrieve_key: {type: String},
44 |
45 | accessToken: {type: String},
46 | });
47 |
48 | UserSchema.plugin(BaseModel);
49 | UserSchema.virtual('avatar_url').get(function () {
50 | var url = this.avatar || ('https://gravatar.com/avatar/' + utility.md5(this.email.toLowerCase()) + '?size=48');
51 |
52 | // www.gravatar.com 被墙
53 | url = url.replace('www.gravatar.com', 'gravatar.com');
54 |
55 | // 让协议自适应 protocol,使用 `//` 开头
56 | if (url.indexOf('http:') === 0) {
57 | url = url.slice(5);
58 | }
59 |
60 | // 如果是 github 的头像,则限制大小
61 | if (url.indexOf('githubusercontent') !== -1) {
62 | url += '&s=120';
63 | }
64 |
65 | return url;
66 | });
67 |
68 | UserSchema.virtual('isAdvanced').get(function () {
69 | // 积分高于 700 则认为是高级用户
70 | return this.score > 700 || this.is_star;
71 | });
72 |
73 | UserSchema.index({loginname: 1}, {unique: true});
74 | UserSchema.index({email: 1}, {unique: true});
75 | UserSchema.index({score: -1});
76 | UserSchema.index({githubId: 1});
77 | UserSchema.index({accessToken: 1});
78 |
79 | UserSchema.pre('save', function(next){
80 | var now = new Date();
81 | this.update_at = now;
82 | next();
83 | });
84 |
85 | mongoose.model('User', UserSchema);
86 |
--------------------------------------------------------------------------------
/test/support/support.js:
--------------------------------------------------------------------------------
1 | var User = require('../../proxy/user');
2 | var Topic = require('../../proxy/topic');
3 | var Reply = require('../../proxy/reply');
4 | var ready = require('ready');
5 | var eventproxy = require('eventproxy');
6 | var utility = require('utility');
7 | var tools = require('../../common/tools');
8 |
9 | function randomInt() {
10 | return (Math.random() * 10000).toFixed(0);
11 | }
12 |
13 | var createUser = exports.createUser = function (callback) {
14 | var key = new Date().getTime() + '_' + randomInt();
15 | tools.bhash('pass', function (err, passhash) {
16 | User.newAndSave('alsotang' + key, 'alsotang' + key, passhash, 'alsotang' + key + '@gmail.com', '', false, callback);
17 | });
18 | };
19 |
20 | exports.createUserByNameAndPwd = function (loginname, pwd, callback) {
21 | tools.bhash(pwd, function (err, passhash) {
22 | User.newAndSave(loginname, loginname, passhash, loginname + +new Date() + '@gmail.com', '', true, callback);
23 | });
24 | };
25 |
26 | var createTopic = exports.createTopic = function (authorId, callback) {
27 | var key = new Date().getTime() + '_' + randomInt();
28 | Topic.newAndSave('topic title' + key, 'test topic content' + key, 'share', authorId, callback);
29 | };
30 |
31 | var createReply = exports.createReply = function (topicId, authorId, callback) {
32 | Reply.newAndSave('I am content', topicId, authorId, callback);
33 | };
34 |
35 | var createSingleUp = exports.createSingleUp = function (replyId, userId, callback) {
36 | Reply.getReply(replyId, function (err, reply) {
37 | reply.ups = [];
38 | reply.ups.push(userId);
39 | reply.save(function (err, reply) {
40 | callback(err, reply);
41 | });
42 | });
43 | };
44 |
45 | function mockUser(user) {
46 | return 'mock_user=' + JSON.stringify(user) + ';';
47 | }
48 |
49 | ready(exports);
50 |
51 | var ep = new eventproxy();
52 | ep.fail(function (err) {
53 | console.error(err);
54 | });
55 |
56 | ep.all('user', 'user2', 'admin', function (user, user2, admin) {
57 | exports.normalUser = user;
58 | exports.normalUserCookie = mockUser(user);
59 |
60 | exports.normalUser2 = user2;
61 | exports.normalUser2Cookie = mockUser(user2);
62 |
63 | var adminObj = JSON.parse(JSON.stringify(admin));
64 | adminObj.is_admin = true;
65 | exports.adminUser = admin;
66 | exports.adminUserCookie = mockUser(adminObj);
67 |
68 | createTopic(user._id, ep.done('topic'));
69 | });
70 | createUser(ep.done('user'));
71 | createUser(ep.done('user2'));
72 | createUser(ep.done('admin'));
73 |
74 | ep.all('topic', function (topic) {
75 | exports.testTopic = topic;
76 | createReply(topic._id, exports.normalUser._id, ep.done('reply'));
77 | });
78 |
79 | ep.all('reply', function (reply) {
80 | exports.testReply = reply;
81 | exports.ready(true);
82 | });
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/common/render_helper.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * nodeclub - common/render_helpers.js
3 | * Copyright(c) 2013 fengmk2
4 | * MIT Licensed
5 | */
6 |
7 | "use strict";
8 |
9 | /**
10 | * Module dependencies.
11 | */
12 |
13 | var MarkdownIt = require('markdown-it');
14 | var _ = require('lodash');
15 | var config = require('../config');
16 | var validator = require('validator');
17 | var jsxss = require('xss');
18 | var multiline = require('multiline')
19 |
20 | // Set default options
21 | var md = new MarkdownIt();
22 |
23 | md.set({
24 | html: false, // Enable HTML tags in source
25 | xhtmlOut: false, // Use '/' to close single tags (
)
26 | breaks: false, // Convert '\n' in paragraphs into
27 | linkify: true, // Autoconvert URL-like text to links
28 | typographer: true, // Enable smartypants and other sweet transforms
29 | });
30 |
31 | md.renderer.rules.fence = function (tokens, idx) {
32 | var token = tokens[idx];
33 | var language = token.info && ('language-' + token.info) || '';
34 | language = validator.escape(language);
35 |
36 | return ''
37 | + '' + validator.escape(token.content) + ''
38 | + '
';
39 | };
40 |
41 | md.renderer.rules.code_block = function (tokens, idx /*, options*/) {
42 | var token = tokens[idx];
43 |
44 | return ''
45 | + '' + validator.escape(token.content) + ''
46 | + '
';
47 | };
48 |
49 | var myxss = new jsxss.FilterXSS({
50 | onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
51 | // 让 prettyprint 可以工作
52 | if (tag === 'pre' && name === 'class') {
53 | return name + '="' + jsxss.escapeAttrValue(value) + '"';
54 | }
55 | }
56 | });
57 |
58 | exports.markdown = function (text) {
59 | return '' + myxss.process(md.render(text || '')) + '
';
60 | };
61 |
62 | exports.escapeSignature = function (signature) {
63 | return signature.split('\n').map(function (p) {
64 | return _.escape(p);
65 | }).join('
');
66 | };
67 |
68 | exports.staticFile = function (filePath) {
69 | if (filePath.indexOf('http') === 0 || filePath.indexOf('//') === 0) {
70 | return filePath;
71 | }
72 | return config.site_static_host + filePath;
73 | };
74 |
75 | exports.tabName = function (tab) {
76 | var pair = _.find(config.tabs, function (pair) {
77 | return pair[0] === tab;
78 | });
79 | if (pair) {
80 | return pair[1];
81 | }
82 | };
83 |
84 | exports.proxy = function (url) {
85 | return url;
86 | // 当 google 和 github 封锁严重时,则需要通过服务器代理访问它们的静态资源
87 | // return '/agent?url=' + encodeURIComponent(url);
88 | };
89 |
90 | // 为了在 view 中使用
91 | exports._ = _;
92 | exports.multiline = multiline;
93 |
--------------------------------------------------------------------------------
/views/sign/signup.html:
--------------------------------------------------------------------------------
1 | <%- partial('../sign/sidebar') %>
2 |
3 |
4 |
5 |
11 |
12 | <% if (typeof(error) !== 'undefined' && error) { %>
13 |
14 |
×
15 |
<%= error %>
16 |
17 | <% } %>
18 | <% if (typeof(success) !== 'undefined' && success) { %>
19 |
20 | <%= success %>
21 |
22 | <% } else { %>
23 |
71 | <% } %>
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/views/reply/reply.html:
--------------------------------------------------------------------------------
1 | '
3 | reply_id="<%= reply._id %>" reply_to_id="<%= reply.reply_id || '' %>" id="<%= reply._id %>">
4 |
5 |
6 |  %>)
7 |
8 |
16 |
17 |
18 |
21 |
22 | <%= reply.ups && reply.ups.length ? reply.ups.length : '' %>
23 |
24 |
25 | <% if (current_user && current_user.is_admin ||
26 | (current_user && current_user._id.toString() == reply.author._id.toString())
27 | ) { %>
28 |
29 |
30 |
31 |
32 |
33 |
34 | <% } %>
35 |
36 | <% if (current_user){ %>
37 |
38 | <% } %>
39 |
40 |
41 |
42 |
43 | <%- markdown(reply.content) %>
44 |
45 |
46 |
47 | <% if (current_user) { %>
48 |
66 | <% } %>
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 | var UserModel = mongoose.model('User');
3 | var Message = require('../proxy').Message;
4 | var config = require('../config');
5 | var eventproxy = require('eventproxy');
6 | var UserProxy = require('../proxy').User;
7 |
8 | /**
9 | * 需要管理员权限
10 | */
11 | exports.adminRequired = function (req, res, next) {
12 | if (!req.session.user) {
13 | return res.render('notify/notify', { error: '你还没有登录。' });
14 | }
15 |
16 | if (!req.session.user.is_admin) {
17 | return res.render('notify/notify', { error: '需要管理员权限。' });
18 | }
19 |
20 | next();
21 | };
22 |
23 | /**
24 | * 需要登录
25 | */
26 | exports.userRequired = function (req, res, next) {
27 | if (!req.session || !req.session.user || !req.session.user._id) {
28 | return res.status(403).send('forbidden!');
29 | }
30 |
31 | next();
32 | };
33 |
34 | exports.blockUser = function () {
35 | return function (req, res, next) {
36 | if (req.path === '/signout') {
37 | return next();
38 | }
39 |
40 | if (req.session.user && req.session.user.is_block && req.method !== 'GET') {
41 | return res.status(403).send('您已被管理员屏蔽了。有疑问请联系 @alsotang。');
42 | }
43 | next();
44 | };
45 | };
46 |
47 |
48 | function gen_session(user, res) {
49 | var auth_token = user._id + '$$$$'; // 以后可能会存储更多信息,用 $$$$ 来分隔
50 | var opts = {
51 | path: '/',
52 | maxAge: 1000 * 60 * 60 * 24 * 30,
53 | signed: true,
54 | httpOnly: true
55 | };
56 | res.cookie(config.auth_cookie_name, auth_token, opts); //cookie 有效期30天
57 | }
58 |
59 | exports.gen_session = gen_session;
60 |
61 | // 验证用户是否登录
62 | exports.authUser = function (req, res, next) {
63 | var ep = new eventproxy();
64 | ep.fail(next);
65 |
66 | // Ensure current_user always has defined.
67 | res.locals.current_user = null;
68 |
69 | if (config.debug && req.cookies['mock_user']) {
70 | var mockUser = JSON.parse(req.cookies['mock_user']);
71 | req.session.user = new UserModel(mockUser);
72 | if (mockUser.is_admin) {
73 | req.session.user.is_admin = true;
74 | }
75 | return next();
76 | }
77 |
78 | ep.all('get_user', function (user) {
79 | if (!user) {
80 | return next();
81 | }
82 | user = res.locals.current_user = req.session.user = new UserModel(user);
83 |
84 | if (config.admins.hasOwnProperty(user.loginname)) {
85 | user.is_admin = true;
86 | }
87 |
88 | Message.getMessagesCount(user._id, ep.done(function (count) {
89 | user.messages_count = count;
90 | next();
91 | }));
92 | });
93 |
94 | if (req.session.user) {
95 | ep.emit('get_user', req.session.user);
96 | } else {
97 | var auth_token = req.signedCookies[config.auth_cookie_name];
98 | if (!auth_token) {
99 | return next();
100 | }
101 |
102 | var auth = auth_token.split('$$$$');
103 | var user_id = auth[0];
104 | UserProxy.getUserById(user_id, ep.done('get_user'));
105 | }
106 | };
107 |
--------------------------------------------------------------------------------
/proxy/user.js:
--------------------------------------------------------------------------------
1 | var models = require('../models');
2 | var User = models.User;
3 | var utility = require('utility');
4 | var uuid = require('node-uuid');
5 |
6 | /**
7 | * 根据用户名列表查找用户列表
8 | * Callback:
9 | * - err, 数据库异常
10 | * - users, 用户列表
11 | * @param {Array} names 用户名列表
12 | * @param {Function} callback 回调函数
13 | */
14 | exports.getUsersByNames = function (names, callback) {
15 | if (names.length === 0) {
16 | return callback(null, []);
17 | }
18 | User.find({ loginname: { $in: names } }, callback);
19 | };
20 |
21 | /**
22 | * 根据登录名查找用户
23 | * Callback:
24 | * - err, 数据库异常
25 | * - user, 用户
26 | * @param {String} loginName 登录名
27 | * @param {Function} callback 回调函数
28 | */
29 | exports.getUserByLoginName = function (loginName, callback) {
30 | User.findOne({'loginname': new RegExp('^'+loginName+'$', "i")}, callback);
31 | };
32 |
33 | /**
34 | * 根据用户ID,查找用户
35 | * Callback:
36 | * - err, 数据库异常
37 | * - user, 用户
38 | * @param {String} id 用户ID
39 | * @param {Function} callback 回调函数
40 | */
41 | exports.getUserById = function (id, callback) {
42 | if (!id) {
43 | return callback();
44 | }
45 | User.findOne({_id: id}, callback);
46 | };
47 |
48 | /**
49 | * 根据邮箱,查找用户
50 | * Callback:
51 | * - err, 数据库异常
52 | * - user, 用户
53 | * @param {String} email 邮箱地址
54 | * @param {Function} callback 回调函数
55 | */
56 | exports.getUserByMail = function (email, callback) {
57 | User.findOne({email: email}, callback);
58 | };
59 |
60 | /**
61 | * 根据用户ID列表,获取一组用户
62 | * Callback:
63 | * - err, 数据库异常
64 | * - users, 用户列表
65 | * @param {Array} ids 用户ID列表
66 | * @param {Function} callback 回调函数
67 | */
68 | exports.getUsersByIds = function (ids, callback) {
69 | User.find({'_id': {'$in': ids}}, callback);
70 | };
71 |
72 | /**
73 | * 根据关键字,获取一组用户
74 | * Callback:
75 | * - err, 数据库异常
76 | * - users, 用户列表
77 | * @param {String} query 关键字
78 | * @param {Object} opt 选项
79 | * @param {Function} callback 回调函数
80 | */
81 | exports.getUsersByQuery = function (query, opt, callback) {
82 | User.find(query, '', opt, callback);
83 | };
84 |
85 | /**
86 | * 根据查询条件,获取一个用户
87 | * Callback:
88 | * - err, 数据库异常
89 | * - user, 用户
90 | * @param {String} name 用户名
91 | * @param {String} key 激活码
92 | * @param {Function} callback 回调函数
93 | */
94 | exports.getUserByNameAndKey = function (loginname, key, callback) {
95 | User.findOne({loginname: loginname, retrieve_key: key}, callback);
96 | };
97 |
98 | exports.newAndSave = function (name, loginname, pass, email, avatar_url, active, callback) {
99 | var user = new User();
100 | user.name = loginname;
101 | user.loginname = loginname;
102 | user.pass = pass;
103 | user.email = email;
104 | user.avatar = avatar_url;
105 | user.active = active || false;
106 | user.accessToken = uuid.v4();
107 |
108 | user.save(callback);
109 | };
110 |
111 | var makeGravatar = function (email) {
112 | return 'http://www.gravatar.com/avatar/' + utility.md5(email.toLowerCase()) + '?size=48';
113 | };
114 | exports.makeGravatar = makeGravatar;
115 |
116 | exports.getGravatar = function (user) {
117 | return user.avatar || makeGravatar(user);
118 | };
119 |
--------------------------------------------------------------------------------
/common/at.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * nodeclub - topic mention user controller.
3 | * Copyright(c) 2012 fengmk2
4 | * Copyright(c) 2012 muyuan
5 | * MIT Licensed
6 | */
7 |
8 | /**
9 | * Module dependencies.
10 | */
11 |
12 | var User = require('../proxy').User;
13 | var Message = require('./message');
14 | var EventProxy = require('eventproxy');
15 | var _ = require('lodash');
16 |
17 | /**
18 | * 从文本中提取出@username 标记的用户名数组
19 | * @param {String} text 文本内容
20 | * @return {Array} 用户名数组
21 | */
22 | var fetchUsers = function (text) {
23 | if (!text) {
24 | return [];
25 | }
26 |
27 | var ignoreRegexs = [
28 | /```.+?```/g, // 去除单行的 ```
29 | /^```[\s\S]+?^```/gm, // ``` 里面的是 pre 标签内容
30 | /`[\s\S]+?`/g, // 同一行中,`some code` 中内容也不该被解析
31 | /^ .*/gm, // 4个空格也是 pre 标签,在这里 . 不会匹配换行
32 | /\b\S*?@[^\s]*?\..+?\b/g, // somebody@gmail.com 会被去除
33 | /\[@.+?\]\(\/.+?\)/g, // 已经被 link 的 username
34 | /\/@/g, // 一般是url中path的一部分
35 | ];
36 |
37 | ignoreRegexs.forEach(function (ignore_regex) {
38 | text = text.replace(ignore_regex, '');
39 | });
40 |
41 | var results = text.match(/@[a-z0-9\-_]+\b/igm);
42 | var names = [];
43 | if (results) {
44 | for (var i = 0, l = results.length; i < l; i++) {
45 | var s = results[i];
46 | //remove leading char @
47 | s = s.slice(1);
48 | names.push(s);
49 | }
50 | }
51 | names = _.uniq(names);
52 | return names;
53 | };
54 | exports.fetchUsers = fetchUsers;
55 |
56 | /**
57 | * 根据文本内容中读取用户,并发送消息给提到的用户
58 | * Callback:
59 | * - err, 数据库异常
60 | * @param {String} text 文本内容
61 | * @param {String} topicId 主题ID
62 | * @param {String} authorId 作者ID
63 | * @param {String} reply_id 回复ID
64 | * @param {Function} callback 回调函数
65 | */
66 | exports.sendMessageToMentionUsers = function (text, topicId, authorId, reply_id, callback) {
67 | if (typeof reply_id === 'function') {
68 | callback = reply_id;
69 | reply_id = null;
70 | }
71 | callback = callback || _.noop;
72 |
73 | User.getUsersByNames(fetchUsers(text), function (err, users) {
74 | if (err || !users) {
75 | return callback(err);
76 | }
77 | var ep = new EventProxy();
78 | ep.fail(callback);
79 |
80 | users = users.filter(function (user) {
81 | return !user._id.equals(authorId);
82 | });
83 |
84 | ep.after('sent', users.length, function () {
85 | callback();
86 | });
87 |
88 | users.forEach(function (user) {
89 | Message.sendAtMessage(user._id, authorId, topicId, reply_id, ep.done('sent'));
90 | });
91 | });
92 | };
93 |
94 | /**
95 | * 根据文本内容,替换为数据库中的数据
96 | * Callback:
97 | * - err, 数据库异常
98 | * - text, 替换后的文本内容
99 | * @param {String} text 文本内容
100 | * @param {Function} callback 回调函数
101 | */
102 | exports.linkUsers = function (text, callback) {
103 | var users = fetchUsers(text);
104 | for (var i = 0, l = users.length; i < l; i++) {
105 | var name = users[i];
106 | text = text.replace(new RegExp('@' + name + '\\b(?!\\])', 'g'), '[@' + name + '](/user/' + name + ')');
107 | }
108 | if (!callback) {
109 | return text;
110 | }
111 | return callback(null, text);
112 | };
113 |
--------------------------------------------------------------------------------
/proxy/message.js:
--------------------------------------------------------------------------------
1 | var EventProxy = require('eventproxy');
2 | var _ = require('lodash');
3 |
4 | var Message = require('../models').Message;
5 |
6 | var User = require('./user');
7 | var Topic = require('./topic');
8 | var Reply = require('./reply');
9 |
10 | /**
11 | * 根据用户ID,获取未读消息的数量
12 | * Callback:
13 | * 回调函数参数列表:
14 | * - err, 数据库错误
15 | * - count, 未读消息数量
16 | * @param {String} id 用户ID
17 | * @param {Function} callback 获取消息数量
18 | */
19 | exports.getMessagesCount = function (id, callback) {
20 | Message.countDocuments({master_id: id, has_read: false}, callback);
21 | };
22 |
23 |
24 | /**
25 | * 根据消息Id获取消息
26 | * Callback:
27 | * - err, 数据库错误
28 | * - message, 消息对象
29 | * @param {String} id 消息ID
30 | * @param {Function} callback 回调函数
31 | */
32 | exports.getMessageById = function (id, callback) {
33 | Message.findOne({_id: id}, function (err, message) {
34 | if (err) {
35 | return callback(err);
36 | }
37 | getMessageRelations(message, callback);
38 | });
39 | };
40 |
41 | var getMessageRelations = exports.getMessageRelations = function (message, callback) {
42 | if (message.type === 'reply' || message.type === 'reply2' || message.type === 'at') {
43 | var proxy = new EventProxy();
44 | proxy.fail(callback);
45 | proxy.assign('author', 'topic', 'reply', function (author, topic, reply) {
46 | message.author = author;
47 | message.topic = topic;
48 | message.reply = reply;
49 | if (!author || !topic) {
50 | message.is_invalid = true;
51 | }
52 | return callback(null, message);
53 | }); // 接收异常
54 | User.getUserById(message.author_id, proxy.done('author'));
55 | Topic.getTopicById(message.topic_id, proxy.done('topic'));
56 | Reply.getReplyById(message.reply_id, proxy.done('reply'));
57 | } else {
58 | return callback(null, {is_invalid: true});
59 | }
60 | };
61 |
62 | /**
63 | * 根据用户ID,获取已读消息列表
64 | * Callback:
65 | * - err, 数据库异常
66 | * - messages, 消息列表
67 | * @param {String} userId 用户ID
68 | * @param {Function} callback 回调函数
69 | */
70 | exports.getReadMessagesByUserId = function (userId, callback) {
71 | Message.find({master_id: userId, has_read: true}, null,
72 | {sort: '-create_at', limit: 20}, callback);
73 | };
74 |
75 | /**
76 | * 根据用户ID,获取未读消息列表
77 | * Callback:
78 | * - err, 数据库异常
79 | * - messages, 未读消息列表
80 | * @param {String} userId 用户ID
81 | * @param {Function} callback 回调函数
82 | */
83 | exports.getUnreadMessageByUserId = function (userId, callback) {
84 | Message.find({master_id: userId, has_read: false}, null,
85 | {sort: '-create_at'}, callback);
86 | };
87 |
88 |
89 | /**
90 | * 将消息设置成已读
91 | */
92 | exports.updateMessagesToRead = function (userId, messages, callback) {
93 | callback = callback || _.noop;
94 | if (messages.length === 0) {
95 | return callback();
96 | }
97 |
98 | var ids = messages.map(function (m) {
99 | return m.id;
100 | });
101 |
102 | var query = { master_id: userId, _id: { $in: ids } };
103 | Message.updateMany(query, { $set: { has_read: true } }).exec(callback);
104 | };
105 |
106 |
107 | /**
108 | * 将单个消息设置成已读
109 | */
110 | exports.updateOneMessageToRead = function (msg_id, callback) {
111 | callback = callback || _.noop;
112 | if (!msg_id) {
113 | return callback();
114 | }
115 | var query = { _id: msg_id };
116 | Message.updateMany(query, { $set: { has_read: true } }).exec(callback);
117 | };
118 |
--------------------------------------------------------------------------------
/public/stylesheets/responsive.css:
--------------------------------------------------------------------------------
1 | @-ms-viewport {
2 | width: device-width;
3 | }
4 |
5 | #sidebar-mask {
6 | background-color: #333;
7 | width: 100%;
8 | height: 100%;
9 | filter: alpha(opacity=60);
10 | opacity: .6;
11 | z-index: 99;
12 | position: absolute;
13 | top: 0;
14 | left: 0;
15 | display: none;
16 | }
17 |
18 | @media (max-width: 400px) {
19 | .navbar .brand {
20 | float: none;
21 | margin: 0 auto;
22 | }
23 |
24 | .navbar .navbar-search {
25 | clear: both;
26 | margin: 0 auto;
27 | float: none;
28 | }
29 |
30 | .navbar .search-query {
31 | display: block;
32 | margin: 0 auto;
33 | }
34 | }
35 |
36 | @media (max-width: 979px) {
37 |
38 | .navbar {
39 | margin: 0 5px;
40 | z-index: 999;
41 | width: auto !important;
42 | }
43 |
44 | .navbar .container, #main,
45 | #content, #footer_main {
46 | width: 100%;
47 | min-width: 0;
48 | }
49 |
50 | .navbar .nav.pull-right {
51 | float: none;
52 | clear: both;
53 | }
54 |
55 | #responsive-sidebar-trigger {
56 | display: none;
57 | }
58 |
59 | #main {
60 | /*overflow: hidden;*/
61 | margin: 20px auto;
62 | min-height: 0;
63 | }
64 |
65 | #content .panel {
66 | margin: 0 5px;
67 | }
68 |
69 | #sidebar {
70 | float: none;
71 | position: absolute;
72 | right: -100%;
73 | top: 0;
74 | background-color: #fff;
75 | z-index: 999;
76 | border: 5px solid #ccc;
77 | border-right: 0;
78 | -webkit-transition: .3s right;
79 | -moz-transition: .3s right;
80 | -ms-transition: .3s right;
81 | -o-transition: .3s right;
82 | transition: .3s right;
83 | display: none;
84 | }
85 |
86 | #content .topic_title {
87 | font-size: 1em;
88 | width: 100%;
89 | }
90 |
91 | #content .last_time {
92 | position: absolute;
93 | bottom: 0;
94 | right: 10px;
95 | font-size: .8em;
96 | }
97 |
98 | #content .last_time img {
99 | display: none;
100 | }
101 |
102 | #content .reply_count {
103 | position: absolute;
104 | bottom: 0;
105 | left: 85px;
106 | text-align: left;
107 | line-height: 2em;
108 | font-size: 10px;
109 | }
110 |
111 | .topic_title_wrapper {
112 | padding-left: 40px;
113 | }
114 |
115 | #main .topic_content p a.content_img,
116 | #main .reply_content p a.content_img {
117 | width: 100%;
118 | }
119 |
120 | #footer {
121 | margin: 0 5px 5px;
122 | }
123 |
124 | #footer_main {
125 | display: none;
126 | }
127 |
128 | #backtotop {
129 | background-color: #f5f5f5;
130 | border: 1px solid #ccc;
131 | border-right: 0;
132 | }
133 |
134 | .form-horizontal .control-label {
135 | float: none;
136 | width: auto;
137 | padding-top: 0;
138 | text-align: left;
139 | }
140 |
141 | .form-horizontal .controls {
142 | margin-left: 0;
143 | }
144 |
145 | .form-horizontal .control-list {
146 | padding-top: 0;
147 | }
148 |
149 | .form-horizontal .form-actions {
150 | padding-right: 10px;
151 | padding-left: 10px;
152 | }
153 |
154 | #content .reply_content {
155 | clear: both;
156 | padding-left: 0;
157 | padding-top: 5px;
158 | }
159 |
160 | #content .action {
161 | display: none;
162 | }
163 |
164 | .user_profile {
165 | margin-top: 0;
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/views/sidebar.html:
--------------------------------------------------------------------------------
1 |
119 |
--------------------------------------------------------------------------------
/api/v1/message.js:
--------------------------------------------------------------------------------
1 | var eventproxy = require('eventproxy');
2 | var Message = require('../../proxy').Message;
3 | var at = require('../../common/at');
4 | var renderHelper = require('../../common/render_helper');
5 | var _ = require('lodash');
6 |
7 | var index = function (req, res, next) {
8 | var user_id = req.user._id;
9 | var mdrender = req.query.mdrender === 'false' ? false : true;
10 | var ep = new eventproxy();
11 | ep.fail(next);
12 |
13 | ep.all('has_read_messages', 'hasnot_read_messages', function (has_read_messages, hasnot_read_messages) {
14 | res.send({
15 | success: true,
16 | data: {
17 | has_read_messages: has_read_messages,
18 | hasnot_read_messages: hasnot_read_messages
19 | }
20 | });
21 | });
22 |
23 | ep.all('has_read', 'unread', function (has_read, unread) {
24 | [has_read, unread].forEach(function (msgs, idx) {
25 | var epfill = new eventproxy();
26 | epfill.fail(next);
27 | epfill.after('message_ready', msgs.length, function (docs) {
28 | docs = docs.filter(function (doc) {
29 | return !doc.is_invalid;
30 | });
31 | docs = docs.map(function (doc) {
32 | doc.author = _.pick(doc.author, ['loginname', 'avatar_url']);
33 | doc.topic = _.pick(doc.topic, ['id', 'author', 'title', 'last_reply_at']);
34 | doc.reply = _.pick(doc.reply, ['id', 'content', 'ups', 'create_at']);
35 | if (mdrender) {
36 | doc.reply.content = renderHelper.markdown(at.linkUsers(doc.reply.content));
37 | }
38 | doc = _.pick(doc, ['id', 'type', 'has_read', 'author', 'topic', 'reply', 'create_at']);
39 |
40 | return doc;
41 | });
42 | ep.emit(idx === 0 ? 'has_read_messages' : 'hasnot_read_messages', docs);
43 | });
44 | msgs.forEach(function (doc) {
45 | Message.getMessageById(doc._id, epfill.group('message_ready'));
46 | });
47 | });
48 | });
49 |
50 | Message.getReadMessagesByUserId(user_id, ep.done('has_read'));
51 |
52 | Message.getUnreadMessageByUserId(user_id, ep.done('unread'));
53 | };
54 |
55 | exports.index = index;
56 |
57 | var markAll = function (req, res, next) {
58 | var user_id = req.user._id;
59 | var ep = new eventproxy();
60 | ep.fail(next);
61 | Message.getUnreadMessageByUserId(user_id, ep.done('unread', function (docs) {
62 | docs.forEach(function (doc) {
63 | doc.has_read = true;
64 | doc.save();
65 | });
66 | return docs;
67 | }));
68 |
69 | ep.all('unread', function (unread) {
70 | unread = unread.map(function (doc) {
71 | doc = _.pick(doc, ['id']);
72 | return doc;
73 | });
74 | res.send({
75 | success: true,
76 | marked_msgs: unread
77 | });
78 | });
79 | };
80 |
81 | exports.markAll = markAll;
82 |
83 |
84 | var markOne = function (req, res, next) {
85 | var msg_id = req.params.msg_id;
86 | var ep = new eventproxy();
87 | ep.fail(next);
88 | Message.updateOneMessageToRead(msg_id, ep.done('marked_result', function (result) {
89 | return result;
90 | }));
91 |
92 | ep.all('marked_result', function (result) {
93 | res.send({
94 | success: true,
95 | marked_msg_id: msg_id
96 | });
97 | });
98 | };
99 |
100 | exports.markOne = markOne;
101 |
102 |
103 | var count = function (req, res, next) {
104 | var userId = req.user.id;
105 |
106 | var ep = new eventproxy();
107 | ep.fail(next);
108 |
109 | Message.getMessagesCount(userId, ep.done(function (count) {
110 | res.send({success: true, data: count});
111 | }));
112 | };
113 |
114 | exports.count = count;
115 |
--------------------------------------------------------------------------------
/config.default.js:
--------------------------------------------------------------------------------
1 | /**
2 | * config
3 | */
4 |
5 | var path = require('path');
6 |
7 | var config = {
8 | // debug 为 true 时,用于本地调试
9 | debug: true,
10 |
11 | get mini_assets() { return !this.debug; }, // 是否启用静态文件的合并压缩,详见视图中的Loader
12 |
13 | name: 'Nodeclub', // 社区名字
14 | description: 'CNode:Node.js专业中文社区', // 社区的描述
15 | keywords: 'nodejs, node, express, connect, socket.io',
16 |
17 | // 添加到 html head 中的信息
18 | site_headers: [
19 | ''
20 | ],
21 | site_logo: '/public/images/cnodejs_light.svg', // default is `name`
22 | site_icon: '/public/images/cnode_icon_32.png', // 默认没有 favicon, 这里填写网址
23 | // 右上角的导航区
24 | site_navs: [
25 | // 格式 [ path, title, [target=''] ]
26 | [ '/about', '关于' ]
27 | ],
28 | // cdn host,如 http://cnodejs.qiniudn.com
29 | site_static_host: '', // 静态文件存储域名
30 | // 社区的域名
31 | host: 'localhost',
32 | // 默认的Google tracker ID,自有站点请修改,申请地址:http://www.google.com/analytics/
33 | google_tracker_id: '',
34 | // 默认的cnzz tracker ID,自有站点请修改
35 | cnzz_tracker_id: '',
36 |
37 | // mongodb 配置
38 | db: 'mongodb://127.0.0.1/node_club_dev',
39 |
40 | // redis 配置,默认是本地
41 | redis_host: '127.0.0.1',
42 | redis_port: 6379,
43 | redis_db: 0,
44 | redis_password: '',
45 |
46 | session_secret: 'node_club_secret', // 务必修改
47 | auth_cookie_name: 'node_club',
48 |
49 | // 程序运行的端口
50 | port: 3000,
51 |
52 | // 话题列表显示的话题数量
53 | list_topic_count: 20,
54 |
55 | // RSS配置
56 | rss: {
57 | title: 'CNode:Node.js专业中文社区',
58 | link: 'http://cnodejs.org',
59 | language: 'zh-cn',
60 | description: 'CNode:Node.js专业中文社区',
61 | //最多获取的RSS Item数量
62 | max_rss_items: 50
63 | },
64 |
65 | log_dir: path.join(__dirname, 'logs'),
66 |
67 | // 邮箱配置
68 | mail_opts: {
69 | host: 'smtp.126.com',
70 | port: 25,
71 | auth: {
72 | user: 'club@126.com',
73 | pass: 'club'
74 | },
75 | ignoreTLS: true,
76 | },
77 |
78 | //weibo app key
79 | weibo_key: 10000000,
80 | weibo_id: 'your_weibo_id',
81 |
82 | // admin 可删除话题,编辑标签。把 user_login_name 换成你的登录名
83 | admins: { user_login_name: true },
84 |
85 | // github 登陆的配置
86 | GITHUB_OAUTH: {
87 | clientID: 'your GITHUB_CLIENT_ID',
88 | clientSecret: 'your GITHUB_CLIENT_SECRET',
89 | callbackURL: 'http://cnodejs.org/auth/github/callback'
90 | },
91 | // 是否允许直接注册(否则只能走 github 的方式)
92 | allow_sign_up: true,
93 |
94 | // oneapm 是个用来监控网站性能的服务
95 | oneapm_key: '',
96 |
97 | // 下面两个配置都是文件上传的配置
98 |
99 | // 7牛的access信息,用于文件上传
100 | qn_access: {
101 | accessKey: 'your access key',
102 | secretKey: 'your secret key',
103 | bucket: 'your bucket name',
104 | origin: 'http://your qiniu domain',
105 | // 如果vps在国外,请使用 http://up.qiniug.com/ ,这是七牛的国际节点
106 | // 如果在国内,此项请留空
107 | uploadURL: 'http://xxxxxxxx',
108 | },
109 |
110 | // 文件上传配置
111 | // 注:如果填写 qn_access,则会上传到 7牛,以下配置无效
112 | upload: {
113 | path: path.join(__dirname, 'public/upload/'),
114 | url: '/public/upload/'
115 | },
116 |
117 | file_limit: '1MB',
118 |
119 | // 版块
120 | tabs: [
121 | ['share', '分享'],
122 | ['ask', '问答'],
123 | ['job', '招聘'],
124 | ],
125 |
126 | // 极光推送
127 | jpush: {
128 | appKey: 'YourAccessKeyyyyyyyyyyyy',
129 | masterSecret: 'YourSecretKeyyyyyyyyyyyyy',
130 | isDebug: false,
131 | },
132 |
133 | create_post_per_day: 1000, // 每个用户一天可以发的主题数
134 | create_reply_per_day: 1000, // 每个用户一天可以发的评论数
135 | create_user_per_ip: 1000, // 每个 ip 每天可以注册账号的次数
136 | visit_per_day: 1000, // 每个 ip 每天能访问的次数
137 | };
138 |
139 | if (process.env.NODE_ENV === 'test') {
140 | config.db = 'mongodb://127.0.0.1/node_club_test';
141 | }
142 |
143 | module.exports = config;
144 |
--------------------------------------------------------------------------------
/api/v1/reply.js:
--------------------------------------------------------------------------------
1 | var eventproxy = require('eventproxy');
2 | var validator = require('validator');
3 | var Topic = require('../../proxy').Topic;
4 | var User = require('../../proxy').User;
5 | var Reply = require('../../proxy').Reply;
6 | var at = require('../../common/at');
7 | var message = require('../../common/message');
8 | var config = require('../../config');
9 |
10 | var create = function (req, res, next) {
11 | var topic_id = req.params.topic_id;
12 | var content = req.body.content || '';
13 | var reply_id = req.body.reply_id;
14 |
15 | var ep = new eventproxy();
16 | ep.fail(next);
17 |
18 | var str = validator.trim(content);
19 | if (str === '') {
20 | res.status(400);
21 | return res.send({success: false, error_msg: '回复内容不能为空'});
22 | }
23 |
24 | if (!validator.isMongoId(topic_id)) {
25 | res.status(400);
26 | return res.send({success: false, error_msg: '不是有效的话题id'});
27 | }
28 |
29 | Topic.getTopic(topic_id, ep.done(function (topic) {
30 | if (!topic) {
31 | res.status(404);
32 | return res.send({success: false, error_msg: '话题不存在'});
33 | }
34 | if (topic.lock) {
35 | res.status(403);
36 | return res.send({success: false, error_msg: '该话题已被锁定'});
37 | }
38 | ep.emit('topic', topic);
39 | }));
40 |
41 | ep.all('topic', function (topic) {
42 | User.getUserById(topic.author_id, ep.done('topic_author'));
43 | });
44 |
45 | ep.all('topic', 'topic_author', function (topic, topicAuthor) {
46 | Reply.newAndSave(content, topic_id, req.user.id, reply_id, ep.done(function (reply) {
47 | Topic.updateLastReply(topic_id, reply._id, ep.done(function () {
48 | ep.emit('reply_saved', reply);
49 | //发送at消息,并防止重复 at 作者
50 | var newContent = content.replace('@' + topicAuthor.loginname + ' ', '');
51 | at.sendMessageToMentionUsers(newContent, topic_id, req.user.id, reply._id);
52 | }));
53 | }));
54 |
55 | User.getUserById(req.user.id, ep.done(function (user) {
56 | user.score += 5;
57 | user.reply_count += 1;
58 | user.save();
59 | ep.emit('score_saved');
60 | }));
61 | });
62 |
63 | ep.all('reply_saved', 'topic', function (reply, topic) {
64 | if (topic.author_id.toString() !== req.user.id.toString()) {
65 | message.sendReplyMessage(topic.author_id, req.user.id, topic._id, reply._id);
66 | }
67 | ep.emit('message_saved');
68 | });
69 |
70 | ep.all('reply_saved', 'message_saved', 'score_saved', function (reply) {
71 | res.send({
72 | success: true,
73 | reply_id: reply._id
74 | });
75 | });
76 | };
77 |
78 | exports.create = create;
79 |
80 | var ups = function (req, res, next) {
81 | var replyId = req.params.reply_id;
82 | var userId = req.user.id;
83 |
84 | if (!validator.isMongoId(replyId)) {
85 | res.status(400);
86 | return res.send({success: false, error_msg: '不是有效的评论id'});
87 | }
88 |
89 | Reply.getReplyById(replyId, function (err, reply) {
90 | if (err) {
91 | return next(err);
92 | }
93 | if (!reply) {
94 | res.status(404);
95 | return res.send({success: false, error_msg: '评论不存在'});
96 | }
97 | if (reply.author_id.equals(userId) && !config.debug) {
98 | res.status(403);
99 | return res.send({success: false, error_msg: '不能帮自己点赞'});
100 | } else {
101 | var action;
102 | reply.ups = reply.ups || [];
103 | var upIndex = reply.ups.indexOf(userId);
104 | if (upIndex === -1) {
105 | reply.ups.push(userId);
106 | action = 'up';
107 | } else {
108 | reply.ups.splice(upIndex, 1);
109 | action = 'down';
110 | }
111 | reply.save(function () {
112 | res.send({
113 | success: true,
114 | action: action
115 | });
116 | });
117 | }
118 | });
119 | };
120 |
121 | exports.ups = ups;
122 |
--------------------------------------------------------------------------------
/api/v1/topic_collect.js:
--------------------------------------------------------------------------------
1 | var eventproxy = require('eventproxy');
2 | var TopicProxy = require('../../proxy').Topic;
3 | var TopicCollectProxy = require('../../proxy').TopicCollect;
4 | var UserProxy = require('../../proxy').User;
5 | var _ = require('lodash');
6 | var validator = require('validator');
7 |
8 | function list(req, res, next) {
9 | var loginname = req.params.loginname;
10 | var ep = new eventproxy();
11 |
12 | ep.fail(next);
13 |
14 | UserProxy.getUserByLoginName(loginname, ep.done(function (user) {
15 | if (!user) {
16 | res.status(404);
17 | return res.send({success: false, error_msg: '用户不存在'});
18 | }
19 |
20 | // api 返回 100 条就好了
21 | TopicCollectProxy.getTopicCollectsByUserId(user._id, {limit: 100}, ep.done('collected_topics'));
22 |
23 | ep.all('collected_topics', function (collected_topics) {
24 |
25 | var ids = collected_topics.map(function (doc) {
26 | return String(doc.topic_id)
27 | });
28 | var query = { _id: { '$in': ids } };
29 | TopicProxy.getTopicsByQuery(query, {}, ep.done('topics', function (topics) {
30 | topics = _.sortBy(topics, function (topic) {
31 | return ids.indexOf(String(topic._id))
32 | });
33 | return topics
34 | }));
35 |
36 | });
37 |
38 | ep.all('topics', function (topics) {
39 | topics = topics.map(function (topic) {
40 | topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
41 | return _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
42 | 'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
43 | });
44 | res.send({success: true, data: topics})
45 |
46 | })
47 | }))
48 | }
49 |
50 | exports.list = list;
51 |
52 | function collect(req, res, next) {
53 | var topic_id = req.body.topic_id;
54 |
55 | if (!validator.isMongoId(topic_id)) {
56 | res.status(400);
57 | return res.send({success: false, error_msg: '不是有效的话题id'});
58 | }
59 |
60 | TopicProxy.getTopic(topic_id, function (err, topic) {
61 | if (err) {
62 | return next(err);
63 | }
64 | if (!topic) {
65 | res.status(404);
66 | return res.json({success: false, error_msg: '话题不存在'});
67 | }
68 |
69 | TopicCollectProxy.getTopicCollect(req.user.id, topic._id, function (err, doc) {
70 | if (err) {
71 | return next(err);
72 | }
73 | if (doc) {
74 | res.json({success: false});
75 | return;
76 | }
77 |
78 | TopicCollectProxy.newAndSave(req.user.id, topic._id, function (err) {
79 | if (err) {
80 | return next(err);
81 | }
82 | res.json({success: true});
83 | });
84 | UserProxy.getUserById(req.user.id, function (err, user) {
85 | if (err) {
86 | return next(err);
87 | }
88 | user.collect_topic_count += 1;
89 | user.save();
90 | });
91 |
92 | topic.collect_count += 1;
93 | topic.save();
94 | });
95 | });
96 | }
97 |
98 | exports.collect = collect;
99 |
100 | function de_collect(req, res, next) {
101 | var topic_id = req.body.topic_id;
102 |
103 | if (!validator.isMongoId(topic_id)) {
104 | res.status(400);
105 | return res.send({success: false, error_msg: '不是有效的话题id'});
106 | }
107 |
108 | TopicProxy.getTopic(topic_id, function (err, topic) {
109 | if (err) {
110 | return next(err);
111 | }
112 | if (!topic) {
113 | res.status(404);
114 | return res.json({success: false, error_msg: '话题不存在'});
115 | }
116 | TopicCollectProxy.remove(req.user.id, topic._id, function (err, removeResult) {
117 | if (err) {
118 | return next(err);
119 | }
120 | if (removeResult.n == 0) {
121 | return res.json({success: false})
122 | }
123 |
124 | UserProxy.getUserById(req.user.id, function (err, user) {
125 | if (err) {
126 | return next(err);
127 | }
128 | user.collect_topic_count -= 1;
129 | user.save();
130 | });
131 |
132 | topic.collect_count -= 1;
133 | topic.save();
134 |
135 | res.json({success: true});
136 | });
137 |
138 | });
139 | }
140 |
141 | exports.de_collect = de_collect;
142 |
--------------------------------------------------------------------------------
/views/topic/edit.html:
--------------------------------------------------------------------------------
1 | <%- partial('../editor_sidebar') %>
2 |
3 |
4 |
5 |
15 |
16 | <% if(typeof(edit_error) !== 'undefined' && edit_error){ %>
17 |
18 |
×
19 |
<%= edit_error %>
20 |
21 | <% } %>
22 | <% if(typeof(error) !== 'undefined' && error){ %>
23 |
24 | <%= error %>
25 |
26 | <% }else{ %>
27 | <% if (typeof(action) !== 'undefined' && action === 'edit') { %>
28 |
72 |
73 | <% } %>
74 |
75 |
76 |
77 |
78 | <%- partial('../includes/editor') %>
79 |
111 |
--------------------------------------------------------------------------------
/proxy/reply.js:
--------------------------------------------------------------------------------
1 | var models = require('../models');
2 | var Reply = models.Reply;
3 | var EventProxy = require('eventproxy');
4 | var tools = require('../common/tools');
5 | var User = require('./user');
6 | var at = require('../common/at');
7 |
8 | /**
9 | * 获取一条回复信息
10 | * @param {String} id 回复ID
11 | * @param {Function} callback 回调函数
12 | */
13 | exports.getReply = function (id, callback) {
14 | Reply.findOne({_id: id}, callback);
15 | };
16 |
17 | /**
18 | * 根据回复ID,获取回复
19 | * Callback:
20 | * - err, 数据库异常
21 | * - reply, 回复内容
22 | * @param {String} id 回复ID
23 | * @param {Function} callback 回调函数
24 | */
25 | exports.getReplyById = function (id, callback) {
26 | if (!id) {
27 | return callback(null, null);
28 | }
29 | Reply.findOne({_id: id}, function (err, reply) {
30 | if (err) {
31 | return callback(err);
32 | }
33 | if (!reply) {
34 | return callback(err, null);
35 | }
36 |
37 | var author_id = reply.author_id;
38 | User.getUserById(author_id, function (err, author) {
39 | if (err) {
40 | return callback(err);
41 | }
42 | reply.author = author;
43 | // TODO: 添加更新方法,有些旧帖子可以转换为markdown格式的内容
44 | if (reply.content_is_html) {
45 | return callback(null, reply);
46 | }
47 | at.linkUsers(reply.content, function (err, str) {
48 | if (err) {
49 | return callback(err);
50 | }
51 | reply.content = str;
52 | return callback(err, reply);
53 | });
54 | });
55 | });
56 | };
57 |
58 | /**
59 | * 根据主题ID,获取回复列表
60 | * Callback:
61 | * - err, 数据库异常
62 | * - replies, 回复列表
63 | * @param {String} id 主题ID
64 | * @param {Function} callback 回调函数
65 | */
66 | exports.getRepliesByTopicId = function (id, cb) {
67 | Reply.find({topic_id: id, deleted: false}, '', {sort: 'create_at'}, function (err, replies) {
68 | if (err) {
69 | return cb(err);
70 | }
71 | if (replies.length === 0) {
72 | return cb(null, []);
73 | }
74 |
75 | var proxy = new EventProxy();
76 | proxy.after('reply_find', replies.length, function () {
77 | cb(null, replies);
78 | });
79 | for (var j = 0; j < replies.length; j++) {
80 | (function (i) {
81 | var author_id = replies[i].author_id;
82 | User.getUserById(author_id, function (err, author) {
83 | if (err) {
84 | return cb(err);
85 | }
86 | replies[i].author = author || { _id: '' };
87 | if (replies[i].content_is_html) {
88 | return proxy.emit('reply_find');
89 | }
90 | at.linkUsers(replies[i].content, function (err, str) {
91 | if (err) {
92 | return cb(err);
93 | }
94 | replies[i].content = str;
95 | proxy.emit('reply_find');
96 | });
97 | });
98 | })(j);
99 | }
100 | });
101 | };
102 |
103 | /**
104 | * 创建并保存一条回复信息
105 | * @param {String} content 回复内容
106 | * @param {String} topicId 主题ID
107 | * @param {String} authorId 回复作者
108 | * @param {String} [replyId] 回复ID,当二级回复时设定该值
109 | * @param {Function} callback 回调函数
110 | */
111 | exports.newAndSave = function (content, topicId, authorId, replyId, callback) {
112 | if (typeof replyId === 'function') {
113 | callback = replyId;
114 | replyId = null;
115 | }
116 | var reply = new Reply();
117 | reply.content = content;
118 | reply.topic_id = topicId;
119 | reply.author_id = authorId;
120 |
121 | if (replyId) {
122 | reply.reply_id = replyId;
123 | }
124 | reply.save(function (err) {
125 | callback(err, reply);
126 | });
127 | };
128 |
129 | /**
130 | * 根据topicId查询到最新的一条未删除回复
131 | * @param topicId 主题ID
132 | * @param callback 回调函数
133 | */
134 | exports.getLastReplyByTopId = function (topicId, callback) {
135 | Reply.find({topic_id: topicId, deleted: false}, '_id', {sort: {create_at : -1}, limit : 1}, callback);
136 | };
137 |
138 | exports.getRepliesByAuthorId = function (authorId, opt, callback) {
139 | if (!callback) {
140 | callback = opt;
141 | opt = null;
142 | }
143 | Reply.find({author_id: authorId}, {}, opt, callback);
144 | };
145 |
146 | // 通过 author_id 获取回复总数
147 | exports.getCountByAuthorId = function (authorId, callback) {
148 | Reply.countDocuments({author_id: authorId}, callback);
149 | };
150 |
--------------------------------------------------------------------------------
/controllers/github.js:
--------------------------------------------------------------------------------
1 | var Models = require('../models');
2 | var User = Models.User;
3 | var authMiddleWare = require('../middlewares/auth');
4 | var tools = require('../common/tools');
5 | var eventproxy = require('eventproxy');
6 | var uuid = require('node-uuid');
7 | var validator = require('validator');
8 |
9 | exports.callback = function (req, res, next) {
10 | var profile = req.user;
11 | var email = profile.emails && profile.emails[0] && profile.emails[0].value;
12 | if (!email) {
13 | return res.status(500)
14 | .render('sign/no_github_email');
15 | }
16 | User.findOne({githubId: profile.id}, function (err, user) {
17 | if (err) {
18 | return next(err);
19 | }
20 | // 当用户已经是 cnode 用户时,通过 github 登陆将会更新他的资料
21 | if (user) {
22 | user.githubUsername = profile.username;
23 | user.githubId = profile.id;
24 | user.githubAccessToken = profile.accessToken;
25 | // user.loginname = profile.username;
26 | user.avatar = profile._json.avatar_url;
27 | user.email = email || user.email;
28 |
29 |
30 | user.save(function (err) {
31 | if (err) {
32 | // 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
33 | if (err.message.indexOf('duplicate key error') !== -1) {
34 | if (err.message.indexOf('loginname') !== -1) {
35 | return res.status(500)
36 | .send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了');
37 | }
38 | }
39 | return next(err);
40 | }
41 | authMiddleWare.gen_session(user, res);
42 | return res.redirect('/');
43 | });
44 | } else {
45 | // 如果用户还未存在,则建立新用户
46 | req.session.profile = profile;
47 | return res.redirect('/auth/github/new');
48 | }
49 | });
50 | };
51 |
52 | exports.new = function (req, res, next) {
53 | res.render('sign/new_oauth', {actionPath: '/auth/github/create'});
54 | };
55 |
56 | exports.create = function (req, res, next) {
57 | var profile = req.session.profile;
58 |
59 | var isnew = req.body.isnew;
60 | var loginname = validator.trim(req.body.name || '').toLowerCase();
61 | var password = validator.trim(req.body.pass || '');
62 | var ep = new eventproxy();
63 | ep.fail(next);
64 |
65 | if (!profile) {
66 | return res.redirect('/signin');
67 | }
68 | delete req.session.profile;
69 |
70 | var email = profile.emails && profile.emails[0] && profile.emails[0].value;
71 | if (!email) {
72 | return res.status(500)
73 | .render('sign/no_github_email');
74 | }
75 | if (isnew) { // 注册新账号
76 | var user = new User({
77 | loginname: profile.username,
78 | pass: profile.accessToken,
79 | email: email,
80 | avatar: profile._json.avatar_url,
81 | githubId: profile.id,
82 | githubUsername: profile.username,
83 | githubAccessToken: profile.accessToken,
84 | active: true,
85 | accessToken: uuid.v4(),
86 | });
87 | user.save(function (err) {
88 | if (err) {
89 | // 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
90 | if (err.message.indexOf('duplicate key error') !== -1) {
91 | if (err.message.indexOf('loginname') !== -1) {
92 | return res.status(500)
93 | .send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了');
94 | }
95 | }
96 | return next(err);
97 | // END 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
98 | }
99 | authMiddleWare.gen_session(user, res);
100 | res.redirect('/');
101 | });
102 | } else { // 关联老账号
103 | ep.on('login_error', function (login_error) {
104 | res.status(403);
105 | res.render('sign/signin', { error: '账号名或密码错误。' });
106 | });
107 | User.findOne({loginname: loginname},
108 | ep.done(function (user) {
109 | if (!user) {
110 | return ep.emit('login_error');
111 | }
112 | tools.bcompare(password, user.pass, ep.done(function (bool) {
113 | if (!bool) {
114 | return ep.emit('login_error');
115 | }
116 | user.githubUsername = profile.username;
117 | user.githubId = profile.id;
118 | // user.loginname = profile.username;
119 | user.avatar = profile._json.avatar_url;
120 | user.githubAccessToken = profile.accessToken;
121 |
122 | user.save(function (err) {
123 | if (err) {
124 | return next(err);
125 | }
126 | authMiddleWare.gen_session(user, res);
127 | res.redirect('/');
128 | });
129 | }));
130 | }));
131 | }
132 | };
133 |
--------------------------------------------------------------------------------
/test/controllers/reply.test.js:
--------------------------------------------------------------------------------
1 | var app = require('../../app');
2 | var request = require('supertest')(app);
3 | var support = require('../support/support');
4 | var ReplyProxy = require('../../proxy/reply');
5 |
6 | describe('test/controllers/reply.test.js', function () {
7 | before(function (done) {
8 | support.ready(done);
9 | });
10 |
11 | var reply1Id;
12 |
13 | describe('reply1', function () {
14 | it('should add a reply1', function (done) {
15 | var topic = support.testTopic;
16 | request.post('/' + topic._id + '/reply')
17 | .set('Cookie', support.normalUserCookie)
18 | .send({
19 | r_content: 'test reply 1'
20 | })
21 | .expect(302)
22 | .end(function (err, res) {
23 | res.headers['location'].should.match(new RegExp('/topic/' + topic.id + '#\\w+'));
24 |
25 | // 记录下这个 reply1 的 id
26 | reply1Id = res.headers['location'].match(/#(\w+)/)[1];
27 |
28 | done(err);
29 | });
30 | });
31 |
32 | it('should 422 when add a empty reply1', function (done) {
33 | var topic = support.testTopic;
34 | request.post('/' + topic._id + '/reply')
35 | .set('Cookie', support.normalUserCookie)
36 | .send({
37 | r_content: ''
38 | })
39 | .expect(422)
40 | .end(done);
41 | });
42 |
43 | it('should not add a reply1 when not login', function (done) {
44 | request.post('/' + support.testTopic._id + '/reply')
45 | .send({
46 | r_content: 'test reply 1'
47 | })
48 | .expect(403)
49 | .end(done);
50 | });
51 | });
52 |
53 | describe('edit reply', function () {
54 | it('should not show edit page when not author', function (done) {
55 | request.get('/reply/' + reply1Id + '/edit')
56 | .set('Cookie', support.normalUser2Cookie)
57 | .expect(403)
58 | .end(done);
59 | });
60 |
61 | it('should show edit page when is author', function (done) {
62 | request.get('/reply/' + reply1Id + '/edit')
63 | .set('Cookie', support.normalUserCookie)
64 | .expect(200)
65 | .end(function (err, res) {
66 | res.text.should.containEql('test reply 1');
67 | done(err);
68 | });
69 | });
70 |
71 | it('should update edit', function (done) {
72 | var topic = support.testTopic;
73 | request.post('/reply/' + reply1Id + '/edit')
74 | .send({
75 | t_content: 'been update',
76 | })
77 | .set('Cookie', support.normalUserCookie)
78 | .end(function (err, res) {
79 | res.status.should.equal(302);
80 | res.headers['location'].should.match(new RegExp('/topic/' + topic.id + '#\\w+'));
81 | done(err);
82 | });
83 | });
84 | });
85 |
86 | describe('upvote reply', function () {
87 | var reply1, reply1UpCount;
88 | before(function (done) {
89 | ReplyProxy.getReply(reply1Id, function (err, reply) {
90 | reply1 = reply;
91 | reply1UpCount = reply1.ups.length;
92 | done(err);
93 | });
94 | });
95 |
96 | it('should increase', function (done) {
97 | request.post('/reply/' + reply1Id + '/up')
98 | .send({replyId: reply1Id})
99 | .set('Cookie', support.normalUser2Cookie)
100 | .end(function (err, res) {
101 | res.status.should.equal(200);
102 | res.body.should.eql({
103 | success: true,
104 | action: 'up',
105 | });
106 | done(err);
107 | });
108 | });
109 |
110 | it('should decrease', function (done) {
111 | request.post('/reply/' + reply1Id + '/up')
112 | .send({replyId: reply1Id})
113 | .set('Cookie', support.normalUser2Cookie)
114 | .end(function (err, res) {
115 | res.status.should.equal(200);
116 | res.body.should.eql({
117 | success: true,
118 | action: 'down',
119 | });
120 | done(err);
121 | });
122 | });
123 |
124 | });
125 |
126 | describe('delete reply', function () {
127 | it('should should not delete when not author', function (done) {
128 | request.post('/reply/' + reply1Id + '/delete')
129 | .send({
130 | reply_id: reply1Id
131 | })
132 | .expect(403)
133 | .end(done);
134 | });
135 |
136 | it('should delete reply when author', function (done) {
137 | request.post('/reply/' + reply1Id + '/delete')
138 | .send({
139 | reply_id: reply1Id
140 | })
141 | .set('Cookie', support.normalUserCookie)
142 | .expect(200)
143 | .end(function (err, res) {
144 | res.body.should.eql({status: 'success'});
145 | done(err);
146 | });
147 | });
148 | });
149 | });
150 |
151 |
--------------------------------------------------------------------------------
/controllers/site.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * nodeclub - site index controller.
3 | * Copyright(c) 2012 fengmk2
4 | * Copyright(c) 2012 muyuan
5 | * MIT Licensed
6 | */
7 |
8 | /**
9 | * Module dependencies.
10 | */
11 |
12 | var User = require('../proxy').User;
13 | var Topic = require('../proxy').Topic;
14 | var config = require('../config');
15 | var eventproxy = require('eventproxy');
16 | var cache = require('../common/cache');
17 | var xmlbuilder = require('xmlbuilder');
18 | var renderHelper = require('../common/render_helper');
19 | var _ = require('lodash');
20 | var moment = require('moment');
21 |
22 | exports.index = function (req, res, next) {
23 | var page = parseInt(req.query.page, 10) || 1;
24 | page = page > 0 ? page : 1;
25 | var tab = req.query.tab || 'all';
26 |
27 | var proxy = new eventproxy();
28 | proxy.fail(next);
29 |
30 | // 取主题
31 | var query = {};
32 | if (!tab || tab === 'all') {
33 | query.tab = {$nin: ['job', 'dev']}
34 | } else {
35 | if (tab === 'good') {
36 | query.good = true;
37 | } else {
38 | query.tab = tab;
39 | }
40 | }
41 | if (!query.good) {
42 | query.create_at = {$gte: moment().subtract(1, 'years').toDate()}
43 | }
44 |
45 | var limit = config.list_topic_count;
46 | var options = { skip: (page - 1) * limit, limit: limit, sort: '-top -last_reply_at'};
47 |
48 | Topic.getTopicsByQuery(query, options, proxy.done('topics', function (topics) {
49 | return topics;
50 | }));
51 |
52 | // 取排行榜上的用户
53 | cache.get('tops', proxy.done(function (tops) {
54 | if (tops) {
55 | proxy.emit('tops', tops);
56 | } else {
57 | User.getUsersByQuery(
58 | {is_block: false},
59 | { limit: 10, sort: '-score'},
60 | proxy.done('tops', function (tops) {
61 | cache.set('tops', tops, 60 * 1);
62 | return tops;
63 | })
64 | );
65 | }
66 | }));
67 | // END 取排行榜上的用户
68 |
69 | // 取0回复的主题
70 | cache.get('no_reply_topics', proxy.done(function (no_reply_topics) {
71 | if (no_reply_topics) {
72 | proxy.emit('no_reply_topics', no_reply_topics);
73 | } else {
74 | Topic.getTopicsByQuery(
75 | { reply_count: 0, tab: {$nin: ['job', 'dev']}},
76 | { limit: 5, sort: '-create_at'},
77 | proxy.done('no_reply_topics', function (no_reply_topics) {
78 | cache.set('no_reply_topics', no_reply_topics, 60 * 1);
79 | return no_reply_topics;
80 | }));
81 | }
82 | }));
83 | // END 取0回复的主题
84 |
85 | // 取分页数据
86 | var pagesCacheKey = JSON.stringify(query) + 'pages';
87 | cache.get(pagesCacheKey, proxy.done(function (pages) {
88 | if (pages) {
89 | proxy.emit('pages', pages);
90 | } else {
91 | Topic.getCountByQuery(query, proxy.done(function (all_topics_count) {
92 | var pages = Math.ceil(all_topics_count / limit);
93 | cache.set(pagesCacheKey, pages, 60 * 1);
94 | proxy.emit('pages', pages);
95 | }));
96 | }
97 | }));
98 | // END 取分页数据
99 |
100 | var tabName = renderHelper.tabName(tab);
101 | proxy.all('topics', 'tops', 'no_reply_topics', 'pages',
102 | function (topics, tops, no_reply_topics, pages) {
103 | res.render('index', {
104 | topics: topics,
105 | current_page: page,
106 | list_topic_count: limit,
107 | tops: tops,
108 | no_reply_topics: no_reply_topics,
109 | pages: pages,
110 | tabs: config.tabs,
111 | tab: tab,
112 | pageTitle: tabName && (tabName + '版块'),
113 | });
114 | });
115 | };
116 |
117 | exports.sitemap = function (req, res, next) {
118 | var urlset = xmlbuilder.create('urlset',
119 | {version: '1.0', encoding: 'UTF-8'});
120 | urlset.att('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
121 |
122 | var ep = new eventproxy();
123 | ep.fail(next);
124 |
125 | ep.all('sitemap', function (sitemap) {
126 | res.type('xml');
127 | res.send(sitemap);
128 | });
129 |
130 | cache.get('sitemap', ep.done(function (sitemapData) {
131 | if (sitemapData) {
132 | ep.emit('sitemap', sitemapData);
133 | } else {
134 | Topic.getLimit5w(function (err, topics) {
135 | if (err) {
136 | return next(err);
137 | }
138 | topics.forEach(function (topic) {
139 | urlset.ele('url').ele('loc', 'http://cnodejs.org/topic/' + topic._id);
140 | });
141 |
142 | var sitemapData = urlset.end();
143 | // 缓存一天
144 | cache.set('sitemap', sitemapData, 3600 * 24);
145 | ep.emit('sitemap', sitemapData);
146 | });
147 | }
148 | }));
149 | };
150 |
151 | exports.appDownload = function (req, res, next) {
152 | res.redirect('https://github.com/soliury/noder-react-native/blob/master/README.md')
153 | };
154 |
--------------------------------------------------------------------------------