├── logs └── .gitkeep ├── views ├── _ads.html ├── topic │ ├── small.html │ ├── _top_good.html │ ├── abstract.html │ ├── list.html │ └── edit.html ├── user │ ├── top.html │ ├── star.html │ ├── top100_user.html │ ├── stars.html │ ├── followers.html │ ├── followings.html │ ├── collect_topics.html │ ├── topics.html │ ├── replies.html │ ├── top100.html │ ├── user.html │ └── card.html ├── includes │ └── editor.html ├── sign │ ├── no_github_email.html │ ├── sidebar.html │ ├── search_pass.html │ ├── reset.html │ ├── signin.html │ ├── new_oauth.html │ └── signup.html ├── static │ ├── faq.html │ ├── about.html │ └── getstart.html ├── _sponsors.html ├── notify │ └── notify.html ├── index.html ├── editor_sidebar.html ├── message │ ├── index.html │ └── message.html ├── reply │ ├── edit.html │ └── reply.html └── sidebar.html ├── .mention-bot ├── public ├── favicon.ico ├── images │ ├── logo.png │ ├── qiniu.png │ ├── search.png │ ├── ucloud.png │ ├── iojs-logo.png │ ├── logo_bak.png │ ├── upyun_logo.png │ ├── digitalocean.png │ ├── nodejs_black.png │ ├── phphub-logo.png │ ├── cnode_icon_32.png │ ├── cnode_icon_64.png │ ├── cnode_logo_128.png │ ├── cnode_logo_32.png │ ├── golangtc-logo.png │ ├── iojs-logo-w150h50.png │ ├── ruby-china-logo2.png │ └── ruby-china-20150529.png ├── img │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── libs │ ├── editor │ │ └── fonts │ │ │ ├── icomoon.eot │ │ │ ├── icomoon.ttf │ │ │ └── icomoon.woff │ ├── webuploader │ │ ├── Uploader.swf │ │ └── webuploader.css │ ├── font-awesome │ │ └── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ └── fontawesome-webfont.woff │ ├── bootstrap │ │ └── img │ │ │ ├── glyphicons-halflings.png │ │ │ └── glyphicons-halflings-white.png │ └── code-prettify │ │ ├── lang-proto.js │ │ ├── lang-go.js │ │ ├── lang-tex.js │ │ ├── lang-yaml.js │ │ ├── lang-lua.js │ │ ├── lang-wiki.js │ │ ├── lang-hs.js │ │ ├── lang-lisp.js │ │ ├── lang-css.js │ │ ├── lang-scala.js │ │ ├── lang-apollo.js │ │ ├── lang-ml.js │ │ ├── lang-vhdl.js │ │ ├── lang-n.js │ │ ├── lang-clj.js │ │ ├── lang-sql.js │ │ ├── lang-vb.js │ │ └── prettify.css ├── stylesheets │ ├── jquery.atwho.css │ ├── common.css │ └── responsive.css └── javascripts │ ├── main.js │ └── responsive.js ├── common ├── store.js ├── store_qn.js ├── redis.js ├── logger.js ├── tools.js ├── store_local.js ├── cache.js ├── message.js ├── mail.js ├── render_helper.js └── at.js ├── test ├── env.js ├── proxy │ ├── message.test.js │ ├── reply.test.js │ ├── topic.test.js │ └── user.test.js ├── models │ └── user.test.js ├── app.test.js ├── common │ ├── mail.test.js │ ├── tools.test.js │ ├── cache.test.js │ ├── store_local.test.js │ ├── render_helper.test.js │ └── message.test.js ├── controllers │ ├── search.test.js │ ├── message.test.js │ ├── static.test.js │ ├── site.test.js │ ├── rss.test.js │ └── reply.test.js ├── middlewares │ ├── conf.test.js │ ├── limit.test.js │ └── proxy.test.js ├── api │ └── v1 │ │ ├── tools.test.js │ │ ├── user.test.js │ │ └── message.test.js └── support │ └── support.js ├── middlewares ├── github_strategy.js ├── conf.js ├── render.js ├── error_page.js ├── request_log.js ├── mongoose_log.js ├── proxy.js ├── limit.js └── auth.js ├── controllers ├── search.js ├── static.js ├── message.js ├── rss.js ├── github.js └── site.js ├── proxy ├── index.js ├── topic_collect.js ├── user.js ├── message.js └── reply.js ├── .snyk ├── api └── v1 │ ├── tools.js │ ├── middleware.js │ ├── user.js │ ├── message.js │ ├── reply.js │ └── topic_collect.js ├── bin ├── get_user_topics.js ├── fix_at_problem.js ├── generate_accesstoken.js └── fix_topic_collect_count.js ├── .travis.yml ├── models ├── base_model.js ├── topic_collect.js ├── reply.js ├── message.js ├── index.js ├── topic.js └── user.js ├── .gitignore ├── oneapm.js ├── .jshintrc ├── LICENSE ├── Makefile ├── README.md ├── api_router_v1.js ├── package.json └── config.default.js /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/_ads.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.mention-bot: -------------------------------------------------------------------------------- 1 | { 2 | "userBlacklist": ["huacnlee"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/qiniu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/qiniu.png -------------------------------------------------------------------------------- /public/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/search.png -------------------------------------------------------------------------------- /public/images/ucloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/ucloud.png -------------------------------------------------------------------------------- /public/images/iojs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/iojs-logo.png -------------------------------------------------------------------------------- /public/images/logo_bak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/logo_bak.png -------------------------------------------------------------------------------- /public/images/upyun_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/upyun_logo.png -------------------------------------------------------------------------------- /public/images/digitalocean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/digitalocean.png -------------------------------------------------------------------------------- /public/images/nodejs_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/nodejs_black.png -------------------------------------------------------------------------------- /public/images/phphub-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/phphub-logo.png -------------------------------------------------------------------------------- /public/images/cnode_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/cnode_icon_32.png -------------------------------------------------------------------------------- /public/images/cnode_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/cnode_icon_64.png -------------------------------------------------------------------------------- /public/images/cnode_logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/cnode_logo_128.png -------------------------------------------------------------------------------- /public/images/cnode_logo_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/cnode_logo_32.png -------------------------------------------------------------------------------- /public/images/golangtc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/golangtc-logo.png -------------------------------------------------------------------------------- /public/images/iojs-logo-w150h50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/iojs-logo-w150h50.png -------------------------------------------------------------------------------- /public/images/ruby-china-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/ruby-china-logo2.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/images/ruby-china-20150529.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/images/ruby-china-20150529.png -------------------------------------------------------------------------------- /public/libs/editor/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/editor/fonts/icomoon.eot -------------------------------------------------------------------------------- /public/libs/editor/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/editor/fonts/icomoon.ttf -------------------------------------------------------------------------------- /public/libs/editor/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/editor/fonts/icomoon.woff -------------------------------------------------------------------------------- /public/libs/webuploader/Uploader.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/webuploader/Uploader.swf -------------------------------------------------------------------------------- /common/store.js: -------------------------------------------------------------------------------- 1 | var qn = require('./store_qn'); 2 | var local = require('./store_local'); 3 | 4 | module.exports = qn || local; 5 | -------------------------------------------------------------------------------- /public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/libs/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/libs/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/libs/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/libs/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/libs/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/libs/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnodejs/nodeclub/HEAD/public/libs/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /test/env.js: -------------------------------------------------------------------------------- 1 | var nock = require('nock'); 2 | var redis = require('../common/redis'); 3 | 4 | nock.enableNetConnect(); // 允许真实的网络连接 5 | 6 | redis.flushdb(); // 清空 db 里面的所有内容 7 | -------------------------------------------------------------------------------- /views/topic/small.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    <%= topic.title %> 3 |
    4 |
  • 5 | -------------------------------------------------------------------------------- /middlewares/github_strategy.js: -------------------------------------------------------------------------------- 1 | module.exports = function (accessToken, refreshToken, profile, done) { 2 | profile.accessToken = accessToken; 3 | done(null, profile); 4 | }; 5 | -------------------------------------------------------------------------------- /test/proxy/message.test.js: -------------------------------------------------------------------------------- 1 | var Message = require('../../proxy/message'); 2 | var should = require('should'); 3 | 4 | describe('test/proxy/message.test.js', function () { 5 | }); 6 | -------------------------------------------------------------------------------- /views/user/top.html: -------------------------------------------------------------------------------- 1 |
  • 2 | <%= user.score %> 3 | <%= user.loginname %> 4 |
  • 5 | -------------------------------------------------------------------------------- /controllers/search.js: -------------------------------------------------------------------------------- 1 | exports.index = function (req, res, next) { 2 | var q = req.query.q; 3 | q = encodeURIComponent(q); 4 | res.redirect('https://www.google.com.hk/search?q=site:cnodejs.org+' + q); 5 | }; 6 | -------------------------------------------------------------------------------- /test/proxy/reply.test.js: -------------------------------------------------------------------------------- 1 | var Reply = require('../../proxy/reply'); 2 | var support = require('../support/support'); 3 | var should = require('should'); 4 | 5 | describe('test/proxy/reply.test.js', function () { 6 | }); 7 | -------------------------------------------------------------------------------- /test/proxy/topic.test.js: -------------------------------------------------------------------------------- 1 | var Topic = require('../../proxy/topic'); 2 | var support = require('../support/support'); 3 | var should = require('should'); 4 | 5 | describe('test/proxy/topic.test.js', function () { 6 | }); 7 | -------------------------------------------------------------------------------- /test/proxy/user.test.js: -------------------------------------------------------------------------------- 1 | var User = require('../../proxy/user'); 2 | var should = require('should'); 3 | var support = require('../support/support'); 4 | 5 | describe('test/proxy/user.test.js', function () { 6 | }); 7 | -------------------------------------------------------------------------------- /proxy/index.js: -------------------------------------------------------------------------------- 1 | exports.User = require('./user'); 2 | exports.Message = require('./message'); 3 | exports.Topic = require('./topic'); 4 | exports.Reply = require('./reply'); 5 | exports.TopicCollect = require('./topic_collect'); 6 | -------------------------------------------------------------------------------- /middlewares/conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | 3 | exports.github = function (req, res, next) { 4 | if (config.GITHUB_OAUTH.clientID === 'your GITHUB_CLIENT_ID') { 5 | return res.send('call the admin to set github oauth.'); 6 | } 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /views/includes/editor.html: -------------------------------------------------------------------------------- 1 | <%- Loader('/public/editor.min.js') 2 | .js('/public/libs/editor/editor.js') 3 | .js('/public/libs/webuploader/webuploader.withoutimage.js') 4 | .js('/public/libs/editor/ext.js') 5 | .done(assets, config.site_static_host, config.mini_assets) 6 | %> 7 | -------------------------------------------------------------------------------- /common/store_qn.js: -------------------------------------------------------------------------------- 1 | var qn = require('qn'); 2 | var config = require('../config'); 3 | 4 | //7牛 client 5 | var qnClient = null; 6 | if (config.qn_access && config.qn_access.secretKey !== 'your secret key') { 7 | qnClient = qn.create(config.qn_access); 8 | } 9 | 10 | module.exports = qnClient; 11 | -------------------------------------------------------------------------------- /views/topic/_top_good.html: -------------------------------------------------------------------------------- 1 | <% if (topic.top) { %> 2 | 置顶 3 | <% } else if (topic.good) { %> 4 | 精华 5 | <% } else if (typeof(tab) !== 'undefined' && tab === 'all' && topic.tabName) { %> 6 | <%= topic.tabName %> 7 | <% } %> 8 | -------------------------------------------------------------------------------- /views/user/star.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | <%= user.loginname %> 5 | <%= user.follower_count %> 粉丝 6 | <%= user.following_count %> 关注 7 |
    8 |
  • 9 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.12.0 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | 'npm:tunnel-agent:20170305': 7 | - jpush-sdk > request > tunnel-agent: 8 | patched: '2018-07-01T04:07:14.342Z' 9 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-proto.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.sourceDecorator({keywords: "bytes,default,double,enum,extend,extensions,false,group,import,max,message,option,optional,package,repeated,required,returns,rpc,service,syntax,to,true", types: /^(bool|(double|s?fixed|[su]?int)(32|64)|float|string)\b/, cStyleComments: !0}), ["proto"]); 2 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-go.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"], 3 | ["pln", /^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/, null, "\"'"] 4 | ], [ 5 | ["com", /^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/], 6 | ["pln", /^(?:[^"'/`]|\/(?![*/]))+/] 7 | ]), ["go"]); 8 | -------------------------------------------------------------------------------- /test/models/user.test.js: -------------------------------------------------------------------------------- 1 | var UserModel = require('../../models').User; 2 | 3 | describe('test/models/user.test.js', function () { 4 | it('should return proxy avatar url', function () { 5 | var user = new UserModel({email: 'alsotang@gmail.com'}); 6 | user.avatar_url.should.eql('https://gravatar.com/avatar/eeb90e7b92f78e01cac07087165e3640?size=48'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /api/v1/tools.js: -------------------------------------------------------------------------------- 1 | var eventproxy = require('eventproxy'); 2 | 3 | var accesstoken = function (req, res, next) { 4 | var ep = new eventproxy(); 5 | ep.fail(next); 6 | 7 | res.send({ 8 | success: true, 9 | loginname: req.user.loginname, 10 | avatar_url: req.user.avatar_url, 11 | id: req.user.id 12 | }); 13 | }; 14 | exports.accesstoken = accesstoken; 15 | -------------------------------------------------------------------------------- /bin/get_user_topics.js: -------------------------------------------------------------------------------- 1 | var UserModel = require('../models').User; 2 | var TopicModel = require('../models').Topic 3 | 4 | // usage: 5 | // node get_user_topics.js alsotang 6 | UserModel.findOne({ 7 | loginname: process.argv[2] 8 | }, function (err, user) { 9 | TopicModel.find({ 10 | author_id: user._id 11 | }, function (err, topics) { 12 | console.log(topics) 13 | }) 14 | }) -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-tex.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"], 3 | ["com", /^%[^\n\r]*/, null, "%"] 4 | ], [ 5 | ["kwd", /^\\[@-Za-z]+/], 6 | ["kwd", /^\\./], 7 | ["typ", /^[$&]/], 8 | ["lit", /[+-]?(?:\.\d+|\d+(?:\.\d*)?)(cm|em|ex|in|pc|pt|bp|mm)/i], 9 | ["pun", /^[()=[\]{}]+/] 10 | ]), ["latex", "tex"]); 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | env: 6 | - CXX=g++-4.8 7 | 8 | node_js: 9 | - stable 10 | 11 | addons: 12 | apt: 13 | sources: 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - g++-4.8 17 | 18 | services: 19 | - mongodb 20 | - redis 21 | 22 | before_install: 23 | - $CXX --version 24 | 25 | script: make test-cov 26 | 27 | after_success: npm i codecov && codecov 28 | -------------------------------------------------------------------------------- /views/sign/no_github_email.html: -------------------------------------------------------------------------------- 1 | GitHub 登陆出错 2 | 3 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | var app = require('../app'); 3 | var config = require('../config'); 4 | 5 | describe('test/app.test.js', function () { 6 | it('should / status 200', function (done) { 7 | request(app).get('/').end(function (err, res) { 8 | res.status.should.equal(200); 9 | res.text.should.containEql(config.description); 10 | done(); 11 | }); 12 | }); 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /models/base_model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 给所有的 Model 扩展功能 3 | * http://mongoosejs.com/docs/plugins.html 4 | */ 5 | var tools = require('../common/tools'); 6 | 7 | module.exports = function (schema) { 8 | schema.methods.create_at_ago = function () { 9 | return tools.formatDate(this.create_at, true); 10 | }; 11 | 12 | schema.methods.update_at_ago = function () { 13 | return tools.formatDate(this.update_at, true); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /views/sign/sidebar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | .cov 3 | coverage 4 | node_modules 5 | .naeindex 6 | coverage.html 7 | .monitor 8 | 9 | *.min.*.js 10 | *.min.*.css 11 | assets.json 12 | 13 | # Ignore Mac OS desktop services store 14 | .DS_Store 15 | 16 | # Ignore Windows desktop setting file 17 | desktop.ini 18 | 19 | # Ignore Redis snapshot 20 | dump.rdb 21 | 22 | *.log 23 | 24 | .idea 25 | public/upload/* 26 | 27 | *.sublime-project 28 | *.sublime-workspace 29 | *.swp 30 | 31 | package-lock.json 32 | -------------------------------------------------------------------------------- /middlewares/render.js: -------------------------------------------------------------------------------- 1 | var logger = require('../common/logger'); 2 | 3 | // Patch res.render method to output logger 4 | exports.render = function (req, res, next) { 5 | res._render = res.render; 6 | 7 | res.render = function (view, options, fn) { 8 | var t = new Date(); 9 | 10 | res._render(view, options, fn); 11 | 12 | var duration = (new Date() - t); 13 | logger.info("Render view", view, ("(" + duration + "ms)").green); 14 | }; 15 | 16 | next(); 17 | }; 18 | -------------------------------------------------------------------------------- /middlewares/error_page.js: -------------------------------------------------------------------------------- 1 | // ErrorPage middleware 2 | exports.errorPage = function (req, res, next) { 3 | 4 | res.render404 = function (error) { 5 | return res.status(404).render('notify/notify', { error: error }); 6 | }; 7 | 8 | res.renderError = function (error, statusCode) { 9 | if (statusCode === undefined) { 10 | statusCode = 400; 11 | } 12 | return res.status(statusCode).render('notify/notify', { error: error }); 13 | }; 14 | 15 | next(); 16 | }; 17 | -------------------------------------------------------------------------------- /views/user/top100_user.html: -------------------------------------------------------------------------------- 1 | 2 | <%= indexInCollection+1 %> 3 | 4 | 5 | 6 | 7 | 8 | <%= user.loginname %> 9 | <%= user.score %> 10 | <%= user.topic_count %> 11 | <%= user.reply_count %> 12 | 13 | -------------------------------------------------------------------------------- /common/redis.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var Redis = require('ioredis'); 3 | var logger = require('./logger') 4 | 5 | var client = new Redis({ 6 | port: config.redis_port, 7 | host: config.redis_host, 8 | db: config.redis_db, 9 | password: config.redis_password, 10 | }); 11 | 12 | client.on('error', function (err) { 13 | if (err) { 14 | logger.error('connect to redis error, check your redis config', err); 15 | process.exit(1); 16 | } 17 | }) 18 | 19 | exports = module.exports = client; 20 | -------------------------------------------------------------------------------- /test/common/mail.test.js: -------------------------------------------------------------------------------- 1 | var mail = require('../../common/mail'); 2 | 3 | describe('test/common/mail.test.js', function () { 4 | describe('sendActiveMail', function () { 5 | it('should ok', function () { 6 | mail.sendActiveMail('shyvo1987@gmail.com', 'token', 'jacksontian'); 7 | }); 8 | }); 9 | 10 | describe('sendResetPassMail', function () { 11 | it('should ok', function () { 12 | mail.sendResetPassMail('shyvo1987@gmail.com', 'token', 'jacksontian'); 13 | }); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /bin/fix_at_problem.js: -------------------------------------------------------------------------------- 1 | // 一次性脚本 2 | // 修复之前重复编辑帖子会导致重复 @someone 的渲染问题 3 | var TopicModel = require('../models').Topic; 4 | 5 | TopicModel.find({content: /\[{2,}@/}).exec(function (err, topics) { 6 | topics.forEach(function (topic) { 7 | topic.content = fix(topic.content); 8 | console.log(topic.id); 9 | topic.save(); 10 | }); 11 | }); 12 | 13 | function fix(str) { 14 | str = str.replace(/\[{1,}(\[@\w+)(\]\(.+?\))\2+/, function (match_text, $1, $2) { 15 | return $1 + $2; 16 | }); 17 | return str; 18 | } 19 | -------------------------------------------------------------------------------- /common/logger.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var pathLib = require('path') 3 | 4 | var env = process.env.NODE_ENV || "development" 5 | 6 | 7 | var log4js = require('log4js'); 8 | log4js.configure({ 9 | appenders: [ 10 | { type: 'console' }, 11 | { type: 'file', filename: pathLib.join(config.log_dir, 'cheese.log'), category: 'cheese' } 12 | ] 13 | }); 14 | 15 | var logger = log4js.getLogger('cheese'); 16 | logger.setLevel(config.debug && env !== 'test' ? 'DEBUG' : 'ERROR') 17 | 18 | module.exports = logger; 19 | -------------------------------------------------------------------------------- /models/topic_collect.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var BaseModel = require("./base_model"); 3 | var Schema = mongoose.Schema; 4 | var ObjectId = Schema.ObjectId; 5 | 6 | var TopicCollectSchema = new Schema({ 7 | user_id: { type: ObjectId }, 8 | topic_id: { type: ObjectId }, 9 | create_at: { type: Date, default: Date.now } 10 | }); 11 | 12 | TopicCollectSchema.plugin(BaseModel); 13 | TopicCollectSchema.index({user_id: 1, topic_id: 1}, {unique: true}); 14 | 15 | mongoose.model('TopicCollect', TopicCollectSchema); 16 | -------------------------------------------------------------------------------- /test/controllers/search.test.js: -------------------------------------------------------------------------------- 1 | var app = require('../../app'); 2 | var request = require('supertest')(app); 3 | 4 | describe('test/controllers/search.test.js', function () { 5 | it('should redirect to google search', function (done) { 6 | request.get('/search').query({q: 'node 中文'}) 7 | .expect(302) 8 | .end(function (err, res) { 9 | res.headers['location'].should.equal('https://www.google.com.hk/#hl=zh-CN&q=site:cnodejs.org+node%20%E4%B8%AD%E6%96%87'); 10 | done(err); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/middlewares/conf.test.js: -------------------------------------------------------------------------------- 1 | var conf = require('../../middlewares/conf'); 2 | var config = require('../../config'); 3 | 4 | describe('test/middlewares/conf.test.js', function () { 5 | it('should alert no github oauth', function (done) { 6 | var _clientID = config.GITHUB_OAUTH.clientID; 7 | config.GITHUB_OAUTH.clientID = 'your GITHUB_CLIENT_ID'; 8 | conf.github({}, {send: function (str) { 9 | str.should.equal('call the admin to set github oauth.'); 10 | config.GITHUB_OAUTH.clientID = _clientID; 11 | done(); 12 | }}); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-yaml.js: -------------------------------------------------------------------------------- 1 | var a = null; 2 | PR.registerLangHandler(PR.createSimpleLexer([ 3 | ["pun", /^[:>?|]+/, a, ":|>?"], 4 | ["dec", /^%(?:YAML|TAG)[^\n\r#]+/, a, "%"], 5 | ["typ", /^&\S+/, a, "&"], 6 | ["typ", /^!\S*/, a, "!"], 7 | ["str", /^"(?:[^"\\]|\\.)*(?:"|$)/, a, '"'], 8 | ["str", /^'(?:[^']|'')*(?:'|$)/, a, "'"], 9 | ["com", /^#[^\n\r]*/, a, "#"], 10 | ["pln", /^\s+/, a, " \t\r\n"] 11 | ], [ 12 | ["dec", /^(?:---|\.\.\.)(?:[\n\r]|$)/], 13 | ["pun", /^-/], 14 | ["kwd", /^\w+:[\n\r ]/], 15 | ["pln", /^\w+/] 16 | ]), ["yaml", "yml"]); 17 | -------------------------------------------------------------------------------- /test/common/tools.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * nodeclub - onehost plugins unit tests. 3 | * Copyright(c) 2012 dead-horse 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | var tools = require('../../common/tools'); 11 | 12 | describe('test/common/tools.test.js', function () { 13 | it('should format date', function () { 14 | tools.formatDate(new Date(0)).should.match(/1970\-01\-01 0\d:00/); 15 | }); 16 | it('should format date friendly', function () { 17 | tools.formatDate(new Date(), true).should.equal('几秒前'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /views/user/stars.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if(typeof(stars) !== 'undefined' && stars.length > 0){ %> 13 | <%- partial('../user/user',{collection:stars,as:'user'}) %> 14 | <% }else{ %> 15 |

    还没有社区达人

    16 | <% } %> 17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /views/user/followers.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if (users.length > 0) { %> 13 | <%- partial('../user/user', { collection: users, as: 'user' }) %> 14 | <% } else { %> 15 |

    还没有任何人关注他

    16 | <% } %> 17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /views/user/followings.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if (users.length > 0) { %> 13 | <%- partial('../user/user', { collection: users, as: 'user' }) %> 14 | <% } else { %> 15 |

    没有关注任何人

    16 | <% } %> 17 |
    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /middlewares/request_log.js: -------------------------------------------------------------------------------- 1 | var logger = require('../common/logger'); 2 | 3 | var ignore = /^\/(public|agent)/; 4 | 5 | exports = module.exports = function (req, res, next) { 6 | // Assets do not out log. 7 | if (ignore.test(req.url)) { 8 | next(); 9 | return; 10 | } 11 | 12 | var t = new Date(); 13 | logger.info('\n\nStarted', t.toISOString(), req.method, req.url, req.ip); 14 | 15 | res.on('finish', function () { 16 | var duration = ((new Date()) - t); 17 | 18 | logger.info('Completed', res.statusCode, ('(' + duration + 'ms)').green); 19 | }); 20 | 21 | next(); 22 | }; 23 | -------------------------------------------------------------------------------- /views/static/faq.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 |

    FAQ

    13 |
    14 |
    15 |

    A:CNode 社区和 Node Club 是什么关系?

    16 | 17 |

    Q:Node Club 是一个用 Node.js 和 MongoDB 开发的开源社区软件,CNode 是基于 Node Club 的 Node.js 中文技术社区。

    18 |
    19 |
    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-lua.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"], 3 | ["str", /^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$))/, null, "\"'"] 4 | ], [ 5 | ["com", /^--(?:\[(=*)\[[\S\s]*?(?:]\1]|$)|[^\n\r]*)/], 6 | ["str", /^\[(=*)\[[\S\s]*?(?:]\1]|$)/], 7 | ["kwd", /^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/, 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 | ]), ["lua"]); 12 | -------------------------------------------------------------------------------- /public/libs/webuploader/webuploader.css: -------------------------------------------------------------------------------- 1 | .webuploader-container { 2 | position: relative; 3 | } 4 | .webuploader-element-invisible { 5 | position: absolute !important; 6 | clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ 7 | clip: rect(1px,1px,1px,1px); 8 | } 9 | .webuploader-pick { 10 | position: relative; 11 | display: inline-block; 12 | cursor: pointer; 13 | background: #00b7ee; 14 | padding: 10px 15px; 15 | color: #fff; 16 | text-align: center; 17 | border-radius: 3px; 18 | overflow: hidden; 19 | } 20 | .webuploader-pick-hover { 21 | background: #00a2d4; 22 | } 23 | 24 | .webuploader-pick-disable { 25 | opacity: 0.6; 26 | pointer-events:none; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-wiki.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\d\t a-gi-z\xa0]+/, null, "\t �\xa0abcdefgijklmnopqrstuvwxyz0123456789"], 3 | ["pun", /^[*=[\]^~]+/, null, "=*~^[]"] 4 | ], [ 5 | ["lang-wiki.meta", /(?:^^|\r\n?|\n)(#[a-z]+)\b/], 6 | ["lit", /^[A-Z][a-z][\da-z]+[A-Z][a-z][^\W_]+\b/], 7 | ["lang-", /^{{{([\S\s]+?)}}}/], 8 | ["lang-", /^`([^\n\r`]+)`/], 9 | ["str", /^https?:\/\/[^\s#/?]*(?:\/[^\s#?]*)?(?:\?[^\s#]*)?(?:#\S*)?/i], 10 | ["pln", /^(?:\r\n|[\S\s])[^\n\r#*=A-[^`h{~]*/] 11 | ]), ["wiki"]); 12 | PR.registerLangHandler(PR.createSimpleLexer([ 13 | ["kwd", /^#[a-z]+/i, null, "#"] 14 | ], []), ["wiki.meta"]); 15 | -------------------------------------------------------------------------------- /common/tools.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcryptjs'); 2 | var moment = require('moment'); 3 | 4 | moment.locale('zh-cn'); // 使用中文 5 | 6 | // 格式化时间 7 | exports.formatDate = function (date, friendly) { 8 | date = moment(date); 9 | 10 | if (friendly) { 11 | return date.fromNow(); 12 | } else { 13 | return date.format('YYYY-MM-DD HH:mm'); 14 | } 15 | 16 | }; 17 | 18 | exports.validateId = function (str) { 19 | return (/^[a-zA-Z0-9\-_]+$/i).test(str); 20 | }; 21 | 22 | exports.bhash = function (str, callback) { 23 | bcrypt.hash(str, 10, callback); 24 | }; 25 | 26 | exports.bcompare = function (str, hash, callback) { 27 | bcrypt.compare(str, hash, callback); 28 | }; 29 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-hs.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t-\r ]+/, null, "\t\n \r "], 3 | ["str", /^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/, null, '"'], 4 | ["str", /^'(?:[^\n\f\r'\\]|\\[^&])'?/, null, "'"], 5 | ["lit", /^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i, null, "0123456789"] 6 | ], [ 7 | ["com", /^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/], 8 | ["kwd", /^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/, 9 | null], 10 | ["pln", /^(?:[A-Z][\w']*\.)*[A-Za-z][\w']*/], 11 | ["pun", /^[^\d\t-\r "'A-Za-z]+/] 12 | ]), ["hs"]); 13 | -------------------------------------------------------------------------------- /views/user/collect_topics.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if (topics.length > 0) { %> 13 | <%- partial('../topic/list', { topics: topics, pages: pages, current_pages: current_page, base: '/user/' + 14 | user.loginname + '/collections' }) %> 15 | <% } else { %> 16 |

    找不到话题 (T_T)

    17 | <% } %> 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /middlewares/mongoose_log.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var logger = require('../common/logger'); 3 | var config = require('../config'); 4 | 5 | if (config.debug) { 6 | var traceMQuery = function (method, info, query) { 7 | return function (err, result, millis) { 8 | if (err) { 9 | logger.error('traceMQuery error:', err) 10 | } 11 | var infos = []; 12 | infos.push(query._collection.collection.name + "." + method.blue); 13 | infos.push(JSON.stringify(info)); 14 | infos.push((millis + 'ms').green); 15 | 16 | logger.debug("MONGO".magenta, infos.join(' ')); 17 | }; 18 | }; 19 | 20 | mongoose.Mongoose.prototype.mquery.setGlobalTraceFunction(traceMQuery); 21 | } 22 | -------------------------------------------------------------------------------- /test/common/cache.test.js: -------------------------------------------------------------------------------- 1 | var cache = require('../../common/cache'); 2 | var should = require('should'); 3 | 4 | describe('test/common/cache.test.js', function () { 5 | it('should set && get', function (done) { 6 | cache.set('alsotang', {age: 23}, function () { 7 | cache.get('alsotang', function (err, data) { 8 | data.should.eql({age: 23}); 9 | done(); 10 | }); 11 | }); 12 | }); 13 | 14 | it('should expire', function (done) { 15 | cache.set('alsotang', {age: 23}, 1, function () { 16 | setTimeout(function () { 17 | cache.get('alsotang', function (err, data) { 18 | should.not.exist(data); 19 | done(); 20 | }); 21 | }, 1.5 * 1000); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /common/store_local.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | var utility = require('utility'); 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | exports.upload = function (file, options, callback) { 7 | var filename = options.filename; 8 | 9 | var newFilename = utility.md5(filename + String((new Date()).getTime())) + 10 | path.extname(filename); 11 | 12 | var upload_path = config.upload.path; 13 | var base_url = config.upload.url; 14 | var filePath = path.join(upload_path, newFilename); 15 | var fileUrl = base_url + newFilename; 16 | 17 | file.on('end', function () { 18 | callback(null, { 19 | url: fileUrl 20 | }); 21 | }); 22 | 23 | file.pipe(fs.createWriteStream(filePath)); 24 | }; 25 | -------------------------------------------------------------------------------- /models/reply.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var BaseModel = require("./base_model"); 3 | var Schema = mongoose.Schema; 4 | var ObjectId = Schema.ObjectId; 5 | 6 | var ReplySchema = new Schema({ 7 | content: { type: String }, 8 | topic_id: { type: ObjectId}, 9 | author_id: { type: ObjectId }, 10 | reply_id: { type: ObjectId }, 11 | create_at: { type: Date, default: Date.now }, 12 | update_at: { type: Date, default: Date.now }, 13 | content_is_html: { type: Boolean }, 14 | ups: [Schema.Types.ObjectId], 15 | deleted: {type: Boolean, default: false}, 16 | }); 17 | 18 | ReplySchema.plugin(BaseModel); 19 | ReplySchema.index({topic_id: 1}); 20 | ReplySchema.index({author_id: 1, create_at: -1}); 21 | 22 | mongoose.model('Reply', ReplySchema); 23 | -------------------------------------------------------------------------------- /models/message.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var BaseModel = require("./base_model"); 3 | var Schema = mongoose.Schema; 4 | var ObjectId = Schema.ObjectId; 5 | 6 | /* 7 | * type: 8 | * reply: xx 回复了你的话题 9 | * reply2: xx 在话题中回复了你 10 | * follow: xx 关注了你 11 | * at: xx @了你 12 | */ 13 | 14 | var MessageSchema = new Schema({ 15 | type: { type: String }, 16 | master_id: { type: ObjectId}, 17 | author_id: { type: ObjectId }, 18 | topic_id: { type: ObjectId }, 19 | reply_id: { type: ObjectId }, 20 | has_read: { type: Boolean, default: false }, 21 | create_at: { type: Date, default: Date.now } 22 | }); 23 | MessageSchema.plugin(BaseModel); 24 | MessageSchema.index({master_id: 1, has_read: -1, create_at: -1}); 25 | 26 | mongoose.model('Message', MessageSchema); 27 | -------------------------------------------------------------------------------- /oneapm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OneAPM agent configuration. 3 | * 4 | * See lib/config.defaults.js in the agent distribution for a more complete 5 | * description of configuration variables and their potential values. 6 | */ 7 | 8 | var config = require('./config'); 9 | 10 | exports.config = { 11 | /** 12 | * Array of application names. 13 | */ 14 | app_name : [config.name], 15 | /** 16 | * Your OneAPM license key. 17 | */ 18 | license_key : config.oneapm_key, 19 | logging : { 20 | /** 21 | * Level at which to log. 'trace' is most useful to OneAPM when diagnosing 22 | * issues with the agent, 'info' and higher will impose the least overhead on 23 | * production applications. 24 | */ 25 | level : 'info' 26 | }, 27 | transaction_events: { 28 | enabled: true 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var config = require('../config'); 3 | var logger = require('../common/logger') 4 | 5 | mongoose.connect(config.db, { 6 | poolSize: 20, 7 | useCreateIndex: true, 8 | useNewUrlParser: true 9 | }, function (err) { 10 | if (err) { 11 | logger.error('connect to %s error: ', config.db, err.message); 12 | process.exit(1); 13 | } 14 | }); 15 | 16 | // models 17 | require('./user'); 18 | require('./topic'); 19 | require('./reply'); 20 | require('./topic_collect'); 21 | require('./message'); 22 | 23 | exports.User = mongoose.model('User'); 24 | exports.Topic = mongoose.model('Topic'); 25 | exports.Reply = mongoose.model('Reply'); 26 | exports.TopicCollect = mongoose.model('TopicCollect'); 27 | exports.Message = mongoose.model('Message'); 28 | -------------------------------------------------------------------------------- /test/common/store_local.test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var storeLocal = require('../../common/store_local'); 4 | var config = require('../../config'); 5 | 6 | describe('test/common/store_local.test.js', function () { 7 | it('should upload a file', function (done) { 8 | var file = fs.createReadStream(path.join(__dirname, 'at.test.js')); 9 | var filename = 'at.test.js'; 10 | storeLocal.upload(file, {filename: filename}, function (err, data) { 11 | var newFilename = data.url.match(/([^\/]+\.js)$/)[1]; 12 | var newFilePath = path.join(config.upload.path, newFilename); 13 | setTimeout(function () { 14 | fs.existsSync(newFilePath) 15 | .should.ok(); 16 | fs.unlinkSync(newFilePath); 17 | done(err); 18 | }, 1 * 1000); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /views/_sponsors.html: -------------------------------------------------------------------------------- 1 |
    2 |

    CNode 社区为国内最专业的 Node.js 开源技术社区,致力于 Node.js 的技术研究。

    3 |

    服务器搭建在 4 | 9 | ,存储赞助商为 10 | 15 |

    16 |

    新手搭建 Node.js 服务器,推荐使用无需备案的 DigitalOcean(https://www.digitalocean.com/)

    17 |
    18 | -------------------------------------------------------------------------------- /views/user/topics.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 |
    13 |
    <%=user.loginname%> 创建的话题
    14 |
    15 | <% if(typeof(topics) !== 'undefined' && topics.length > 0 ){ %> 16 | <%- partial('../topic/list', 17 | {topics:topics,pages:pages,current_pages:current_page,base:'/user/'+user.loginname+'/topics'}) %> 18 | <% }else{ %> 19 |
    20 |

    无话题

    21 |
    22 | <% } %> 23 |
    24 |
    25 |
    26 | 27 | -------------------------------------------------------------------------------- /views/user/replies.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 |
    13 |
    <%= user.loginname %> 参与的话题
    14 |
    15 | <% if(typeof(topics) !== 'undefined' && topics.length > 0){ %> 16 | <%- partial('../topic/list', 17 | {topics:topics,pages:pages,current_pages:current_page,base:'/user/'+user.loginname+'/replies'}) %> 18 | <% }else{ %> 19 |
    20 |

    无话题

    21 |
    22 | <% } %> 23 |
    24 |
    25 |
    26 | 27 | -------------------------------------------------------------------------------- /views/notify/notify.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if(typeof(error) !== 'undefined' && error){ %> 13 |
    14 | <%= error %> 15 |
    16 | <% } %> 17 | <% if(typeof(success) !== 'undefined' && success){ %> 18 |
    19 | <%= success %> 20 |
    21 | <% } %> 22 | 23 | 返回 24 | 25 |
    26 |
    27 |
    28 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "phantom", 4 | "module", 5 | "require", 6 | "__dirname", 7 | "process", 8 | "console", 9 | "it", 10 | "describe", 11 | "before", 12 | "beforeEach", 13 | "after", 14 | "afterEach", 15 | "ace", 16 | "$" 17 | ], 18 | 19 | "browser": true, 20 | "node": true, 21 | "es5": true, 22 | "bitwise": true, 23 | "curly": true, 24 | "eqeqeq": true, 25 | "forin": false, 26 | "immed": true, 27 | "latedef": true, 28 | "newcap": true, 29 | "noarg": true, 30 | "noempty": true, 31 | "nonew": true, 32 | "plusplus": false, 33 | "undef": true, 34 | "strict": false, 35 | "trailing": false, 36 | "globalstrict": true, 37 | "nonstandard": true, 38 | "white": true, 39 | "indent": 2, 40 | "expr": true, 41 | "multistr": true, 42 | "onevar": false, 43 | "unused": "vars" 44 | } 45 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | <%- partial('sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | <% [['all', '全部'], ['good', '精华']].concat(tabs).forEach(function (pair) { 7 | var value = pair[0]; 8 | var text = pair[1]; %> 9 | <%= text %> 11 | <% }) %> 12 |
    13 | <% if (typeof(topics) !== 'undefined' && topics.length > 0) { %> 14 |
    15 | <%- partial('topic/list', { 16 | topics: topics, 17 | pages: pages, 18 | current_page: current_page, 19 | base: '/' 20 | }) %> 21 |
    22 | <% } else { %> 23 |
    24 |

    无话题

    25 |
    26 | <% } %> 27 |
    28 |
    29 | -------------------------------------------------------------------------------- /proxy/topic_collect.js: -------------------------------------------------------------------------------- 1 | var TopicCollect = require('../models').TopicCollect; 2 | var _ = require('lodash') 3 | 4 | exports.getTopicCollect = function (userId, topicId, callback) { 5 | TopicCollect.findOne({user_id: userId, topic_id: topicId}, callback); 6 | }; 7 | 8 | exports.getTopicCollectsByUserId = function (userId, opt, callback) { 9 | var defaultOpt = {sort: '-create_at'}; 10 | opt = _.assign(defaultOpt, opt) 11 | TopicCollect.find({user_id: userId}, '', opt, callback); 12 | }; 13 | 14 | exports.newAndSave = function (userId, topicId, callback) { 15 | var topic_collect = new TopicCollect(); 16 | topic_collect.user_id = userId; 17 | topic_collect.topic_id = topicId; 18 | topic_collect.save(callback); 19 | }; 20 | 21 | exports.remove = function (userId, topicId, callback) { 22 | TopicCollect.deleteOne({user_id: userId, topic_id: topicId}, callback); 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-lisp.js: -------------------------------------------------------------------------------- 1 | var a = null; 2 | PR.registerLangHandler(PR.createSimpleLexer([ 3 | ["opn", /^\(+/, a, "("], 4 | ["clo", /^\)+/, a, ")"], 5 | ["com", /^;[^\n\r]*/, a, ";"], 6 | ["pln", /^[\t\n\r \xa0]+/, a, "\t\n\r \xa0"], 7 | ["str", /^"(?:[^"\\]|\\[\S\s])*(?:"|$)/, a, '"'] 8 | ], [ 9 | ["kwd", /^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/, a], 10 | ["lit", /^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i], 11 | ["lit", /^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/], 12 | ["pln", /^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i], 13 | ["pun", /^[^\w\t\n\r "'-);\\\xa0]+/] 14 | ]), ["cl", "el", "lisp", "scm"]); 15 | -------------------------------------------------------------------------------- /views/user/top100.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if(typeof(users) !== 'undefined' && users.length > 0){ %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <%- partial('../user/top100_user',{collection:users,as:'user'}) %> 23 | 24 |
    #用户名积分主题数评论数
    25 | <% }else{ %> 26 |

    还没有用户

    27 | <% } %> 28 |
    29 |
    30 |
    31 | -------------------------------------------------------------------------------- /middlewares/proxy.js: -------------------------------------------------------------------------------- 1 | var urllib = require('url'); 2 | var request = require('request'); 3 | var logger = require('../common/logger') 4 | var _ = require('lodash') 5 | 6 | 7 | var ALLOW_HOSTNAME = [ 8 | 'avatars.githubusercontent.com', 'www.gravatar.com', 9 | 'gravatar.com', 'www.google-analytics.com', 10 | ]; 11 | exports.proxy = function (req, res, next) { 12 | var url = decodeURIComponent(req.query.url); 13 | var hostname = urllib.parse(url).hostname; 14 | 15 | if (ALLOW_HOSTNAME.indexOf(hostname) === -1) { 16 | return res.send(hostname + ' is not allowed'); 17 | } 18 | 19 | request.get({ 20 | url: url, 21 | headers: _.omit(req.headers, ['cookie', 'refer']), 22 | }) 23 | .on('response', function (response) { 24 | res.set(response.headers); 25 | }) 26 | .on('error', function (err) { 27 | logger.error(err); 28 | }) 29 | .pipe(res); 30 | }; 31 | -------------------------------------------------------------------------------- /controllers/static.js: -------------------------------------------------------------------------------- 1 | var multiline = require('multiline'); 2 | // static page 3 | // About 4 | exports.about = function (req, res, next) { 5 | res.render('static/about', { 6 | pageTitle: '关于我们' 7 | }); 8 | }; 9 | 10 | // FAQ 11 | exports.faq = function (req, res, next) { 12 | res.render('static/faq'); 13 | }; 14 | 15 | exports.getstart = function (req, res) { 16 | res.render('static/getstart', { 17 | pageTitle: 'Node.js 新手入门' 18 | }); 19 | }; 20 | 21 | 22 | exports.robots = function (req, res, next) { 23 | res.type('text/plain'); 24 | res.send(multiline(function () {; 25 | /* 26 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 27 | # 28 | # To ban all spiders from the entire site uncomment the next two lines: 29 | # User-Agent: * 30 | # Disallow: / 31 | */ 32 | })); 33 | }; 34 | 35 | exports.api = function (req, res, next) { 36 | res.render('static/api'); 37 | }; 38 | -------------------------------------------------------------------------------- /test/controllers/message.test.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | var app = require('../../app'); 3 | var support = require('../support/support'); 4 | 5 | describe('test/controllers/message.test.js', function () { 6 | before(function (done) { 7 | support.ready(done); 8 | }); 9 | 10 | describe('index', function () { 11 | it('should 403 without session', function (done) { 12 | request(app).get('/my/messages').end(function (err, res) { 13 | res.statusCode.should.equal(403); 14 | res.type.should.equal('text/html'); 15 | res.text.should.containEql('forbidden!'); 16 | done(err); 17 | }); 18 | }); 19 | 20 | it('should 200', function (done) { 21 | request(app).get('/my/messages') 22 | .set('Cookie', support.normalUserCookie) 23 | .expect(200) 24 | .end(function (err, res) { 25 | res.text.should.containEql('新消息'); 26 | done(err); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /views/editor_sidebar.html: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t\n\f\r ]+/, null, " \t\r\n "] 3 | ], [ 4 | ["str", /^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/, null], 5 | ["str", /^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/, null], 6 | ["lang-css-str", /^url\(([^"')]*)\)/i], 7 | ["kwd", /^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i, null], 8 | ["lang-css-kw", /^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i], 9 | ["com", /^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//], 10 | ["com", 11 | /^(?:<\!--|--\>)/], 12 | ["lit", /^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i], 13 | ["lit", /^#[\da-f]{3,6}/i], 14 | ["pln", /^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i], 15 | ["pun", /^[^\s\w"']+/] 16 | ]), ["css"]); 17 | PR.registerLangHandler(PR.createSimpleLexer([], [ 18 | ["kwd", /^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i] 19 | ]), ["css-kw"]); 20 | PR.registerLangHandler(PR.createSimpleLexer([], [ 21 | ["str", /^[^"')]+/] 22 | ]), ["css-str"]); 23 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-scala.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"], 3 | ["str", /^"(?:""(?:""?(?!")|[^"\\]|\\.)*"{0,3}|(?:[^\n\r"\\]|\\.)*"?)/, null, '"'], 4 | ["lit", /^`(?:[^\n\r\\`]|\\.)*`?/, null, "`"], 5 | ["pun", /^[!#%&(--:-@[-^{-~]+/, null, "!#%&()*+,-:;<=>?@[\\]^{|}~"] 6 | ], [ 7 | ["str", /^'(?:[^\n\r'\\]|\\(?:'|[^\n\r']+))'/], 8 | ["lit", /^'[$A-Z_a-z][\w$]*(?![\w$'])/], 9 | ["kwd", /^(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|object|override|package|private|protected|requires|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)\b/], 10 | ["lit", /^(?:true|false|null|this)\b/], 11 | ["lit", /^(?:0(?:[0-7]+|x[\da-f]+)l?|(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:e[+-]?\d+)?f?|l?)|\\.\d+(?:e[+-]?\d+)?f?)/i], 12 | ["typ", /^[$_]*[A-Z][\d$A-Z_]*[a-z][\w$]*/], 13 | ["pln", /^[$A-Z_a-z][\w$]*/], 14 | ["com", /^\/(?:\/.*|\*(?:\/|\**[^*/])*(?:\*+\/?)?)/], 15 | ["pun", /^(?:\.+|\/)/] 16 | ]), ["scala"]); 17 | -------------------------------------------------------------------------------- /views/message/index.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 | <% if (typeof(hasnot_read_messages) !== 'undefined' && hasnot_read_messages.length > 0) { %> 12 | <%- partial('../message/message', { collection: hasnot_read_messages, as: 'message' }) %> 13 | <% } else { %> 14 |
    15 |

    无消息

    16 |
    17 | <% } %> 18 |
    19 |
    20 |
    21 | 过往信息 22 |
    23 | <% if (typeof(has_read_messages) !== 'undefined' && has_read_messages.length > 0) { %> 24 | <%- partial('../message/message', { collection: has_read_messages, as: 'message' }) %> 25 | <% } else { %> 26 |
    27 |

    无消息

    28 |
    29 | <% } %> 30 |
    31 |
    32 | -------------------------------------------------------------------------------- /bin/generate_accesstoken.js: -------------------------------------------------------------------------------- 1 | // 一次性脚本 2 | // 为所有老用户生成 accessToken 3 | 4 | var uuid = require('node-uuid'); 5 | var mongoose = require('mongoose'); 6 | var config = require('../config'); 7 | var async = require('async'); 8 | require('../models/user'); 9 | 10 | mongoose.connect(config.db, function (err) { 11 | if (err) { 12 | console.error('connect to %s error: ', config.db, err.message); 13 | process.exit(1); 14 | } 15 | }); 16 | 17 | var UserModel = mongoose.model('User'); 18 | 19 | var hasRemain = true; 20 | async.whilst( 21 | function () { 22 | return hasRemain; 23 | }, 24 | function (callback) { 25 | UserModel.findOne({accessToken: {$exists: false}}, function (err, user) { 26 | if (!user) { 27 | hasRemain = false; 28 | callback(); 29 | return; 30 | } 31 | user.accessToken = uuid.v4(); 32 | user.save(function () { 33 | console.log(user.loginname + ' done!'); 34 | callback(); 35 | }); 36 | }); 37 | }, 38 | function (err) { 39 | mongoose.disconnect(); 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /public/stylesheets/jquery.atwho.css: -------------------------------------------------------------------------------- 1 | .atwho-view { 2 | position:absolute; 3 | top: 0; 4 | left: 0; 5 | display: none; 6 | margin-top: 18px; 7 | background: white; 8 | color: black; 9 | border: 1px solid #DDD; 10 | border-radius: 3px; 11 | box-shadow: 0 0 5px rgba(0,0,0,0.1); 12 | min-width: 120px; 13 | z-index: 11110 !important; 14 | } 15 | 16 | .atwho-view .cur { 17 | background: #3366FF; 18 | color: white; 19 | } 20 | .atwho-view .cur small { 21 | color: white; 22 | } 23 | .atwho-view strong { 24 | color: #3366FF; 25 | } 26 | .atwho-view .cur strong { 27 | color: white; 28 | font:bold; 29 | } 30 | .atwho-view ul { 31 | /* width: 100px; */ 32 | list-style:none; 33 | padding:0; 34 | margin:auto; 35 | } 36 | .atwho-view ul li { 37 | display: block; 38 | padding: 5px 10px; 39 | border-bottom: 1px solid #DDD; 40 | cursor: pointer; 41 | /* border-top: 1px solid #C8C8C8; */ 42 | } 43 | .atwho-view small { 44 | font-size: smaller; 45 | color: #777; 46 | font-weight: normal; 47 | } 48 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-apollo.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["com", /^#[^\n\r]*/, null, "#"], 3 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"], 4 | ["str", /^"(?:[^"\\]|\\[\S\s])*(?:"|$)/, null, '"'] 5 | ], [ 6 | ["kwd", /^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/, 7 | null], 8 | ["typ", /^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[ES]?BANK=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/, null], 9 | ["lit", /^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/], 10 | ["pln", /^-*(?:[!-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/], 11 | ["pun", /^[^\w\t\n\r "'-);\\\xa0]+/] 12 | ]), ["apollo", "agc", "aea"]); 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /views/user/user.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 | <%= user.loginname %> 7 | 8 |
    9 | <%= user.follower_count %> 粉丝 10 | 11 | <%= user.following_count %> 关注 12 |
    13 |
    14 | 15 | 16 | 17 | 18 | 19 | <% if (user.url) { %> 20 | 21 | 22 | 23 | 24 | 25 | <% } %> 26 | <% if (user.weibo) { %> 27 | 28 | 29 | 30 | 31 | 32 | <% } %> 33 |
    34 |
    35 | -------------------------------------------------------------------------------- /common/cache.js: -------------------------------------------------------------------------------- 1 | var redis = require('./redis'); 2 | var _ = require('lodash'); 3 | var logger = require('./logger'); 4 | 5 | var get = function (key, callback) { 6 | var t = new Date(); 7 | redis.get(key, function (err, data) { 8 | if (err) { 9 | return callback(err); 10 | } 11 | if (!data) { 12 | return callback(); 13 | } 14 | data = JSON.parse(data); 15 | var duration = (new Date() - t); 16 | logger.debug('Cache', 'get', key, (duration + 'ms').green); 17 | callback(null, data); 18 | }); 19 | }; 20 | 21 | exports.get = get; 22 | 23 | // time 参数可选,秒为单位 24 | var set = function (key, value, time, callback) { 25 | var t = new Date(); 26 | 27 | if (typeof time === 'function') { 28 | callback = time; 29 | time = null; 30 | } 31 | callback = callback || _.noop; 32 | value = JSON.stringify(value); 33 | 34 | if (!time) { 35 | redis.set(key, value, callback); 36 | } else { 37 | redis.setex(key, time, value, callback); 38 | } 39 | var duration = (new Date() - t); 40 | logger.debug("Cache", "set", key, (duration + 'ms').green); 41 | }; 42 | 43 | exports.set = set; 44 | -------------------------------------------------------------------------------- /test/controllers/static.test.js: -------------------------------------------------------------------------------- 1 | var app = require('../../app'); 2 | var request = require('supertest')(app); 3 | 4 | describe('test/controllers/static.test.js', function () { 5 | it('should get /about', function (done) { 6 | request.get('/about').expect(200) 7 | .end(function (err, res) { 8 | res.text.should.containEql('CNode 社区由一批热爱 Node.js 技术的工程师发起'); 9 | done(err); 10 | }); 11 | }); 12 | 13 | it('should get /faq', function (done) { 14 | request.get('/faq').expect(200) 15 | .end(function (err, res) { 16 | res.text.should.containEql('CNode 社区和 Node Club 是什么关系?'); 17 | done(err); 18 | }); 19 | }); 20 | 21 | it('should get /getstart', function (done) { 22 | request.get('/getstart').expect(200) 23 | .end(function (err, res) { 24 | res.text.should.containEql('Node.js 新手入门'); 25 | done(err); 26 | }); 27 | }); 28 | 29 | it('should get /robots.txt', function (done) { 30 | request.get('/robots.txt').expect(200) 31 | .end(function (err, res) { 32 | res.text.should.containEql('User-Agent'); 33 | done(err); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /views/static/about.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 |
    13 | <%- markdown(multiline(function () { 14 | /* 15 | ### 关于 16 | CNode 社区为国内最大最具影响力的 Node.js 开源技术社区,致力于 Node.js 的技术研究。 17 | 18 | CNode 社区由一批热爱 Node.js 技术的工程师发起,目前已经吸引了互联网各个公司的专业技术人员加入,我们非常欢迎更多对 Node.js 感兴趣的朋友。 19 | 20 | CNode 的 SLA 保证是,一个9,即 90.000000%。 21 | 22 | 社区目前由 [@alsotang](http://cnodejs.org/user/alsotang) 在维护,有问题请联系:[https://github.com/alsotang](https://github.com/alsotang) 23 | 24 | 请关注我们的官方微博:http://weibo.com/cnodejs 25 | 26 | ### 移动客户端 27 | 28 | 客户端由 [@soliury](https://cnodejs.org/user/soliury) 开发维护。 29 | 30 | 源码地址: https://github.com/soliury/noder-react-native 。 31 | 32 | 立即体验 CNode 客户端,直接扫描页面右侧二维码。 33 | 34 | 另,安卓用户同时可选择:https://github.com/TakWolf/CNode-Material-Design ,这是 Java 原生开发的安卓客户端。 35 | 36 | */ 37 | })) %> 38 |
    39 |
    40 |
    41 |
    42 | -------------------------------------------------------------------------------- /public/libs/code-prettify/lang-ml.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([ 2 | ["pln", /^[\t\n\r \xa0]+/, null, "\t\n\r �\xa0"], 3 | ["com", /^#(?:if[\t\n\r \xa0]+(?:[$_a-z][\w']*|``[^\t\n\r`]*(?:``|$))|else|endif|light)/i, null, "#"], 4 | ["str", /^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])(?:'|$))/, null, "\"'"] 5 | ], [ 6 | ["com", /^(?:\/\/[^\n\r]*|\(\*[\S\s]*?\*\))/], 7 | ["kwd", /^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/], 8 | ["lit", /^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i], 9 | ["pln", /^(?:[_a-z][\w']*[!#?]?|``[^\t\n\r`]*(?:``|$))/i], 10 | ["pun", /^[^\w\t\n\r "'\xa0]+/] 11 | ]), ["fs", "ml"]); 12 | -------------------------------------------------------------------------------- /controllers/message.js: -------------------------------------------------------------------------------- 1 | var Message = require('../proxy').Message; 2 | var eventproxy = require('eventproxy'); 3 | 4 | exports.index = function (req, res, next) { 5 | var user_id = req.session.user._id; 6 | var ep = new eventproxy(); 7 | ep.fail(next); 8 | 9 | ep.all('has_read_messages', 'hasnot_read_messages', function (has_read_messages, hasnot_read_messages) { 10 | res.render('message/index', {has_read_messages: has_read_messages, hasnot_read_messages: hasnot_read_messages}); 11 | }); 12 | 13 | ep.all('has_read', 'unread', function (has_read, unread) { 14 | [has_read, unread].forEach(function (msgs, idx) { 15 | var epfill = new eventproxy(); 16 | epfill.fail(next); 17 | epfill.after('message_ready', msgs.length, function (docs) { 18 | docs = docs.filter(function (doc) { 19 | return !doc.is_invalid; 20 | }); 21 | ep.emit(idx === 0 ? 'has_read_messages' : 'hasnot_read_messages', docs); 22 | }); 23 | msgs.forEach(function (doc) { 24 | Message.getMessageRelations(doc, epfill.group('message_ready')); 25 | }); 26 | }); 27 | 28 | Message.updateMessagesToRead(user_id, unread); 29 | }); 30 | 31 | Message.getReadMessagesByUserId(user_id, ep.done('has_read')); 32 | Message.getUnreadMessageByUserId(user_id, ep.done('unread')); 33 | }; 34 | -------------------------------------------------------------------------------- /test/api/v1/tools.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 | 6 | describe('test/api/v1/tools.test.js', function () { 7 | 8 | var mockUser; 9 | 10 | before(function (done) { 11 | support.createUser(function (err, user) { 12 | mockUser = user; 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should response with loginname', function (done) { 18 | request.post('/api/v1/accesstoken') 19 | .send({ 20 | accesstoken: mockUser.accessToken 21 | }) 22 | .end(function (err, res) { 23 | should.not.exists(err); 24 | res.status.should.equal(200); 25 | res.body.success.should.true(); 26 | res.body.loginname.should.equal(mockUser.loginname); 27 | res.body.id.should.equal(mockUser.id); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should 401 when accessToken is wrong', function (done) { 33 | request.post('/api/v1/accesstoken') 34 | .send({ 35 | accesstoken: 'not_exists' 36 | }) 37 | .end(function (err, res) { 38 | should.not.exists(err); 39 | res.status.should.equal(401); 40 | res.body.success.should.false(); 41 | done(); 42 | }); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /views/topic/abstract.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | <%= topic.reply_count %> 12 | 13 | / 14 | 15 | <%= topic.visit_count %> 16 | 17 | 18 | 19 | <% if (topic.reply && topic.reply.author) {%> 20 | 21 | 22 | <%= topic.reply.create_at_ago() %> 23 | 24 | <% } %> 25 | <% if (!topic.reply) {%> 26 | 27 | <%= topic.create_at_ago() %> 28 | 29 | <% } %> 30 | 31 |
    32 | 33 | <%- partial('./_top_good', {topic: topic}) %> 34 | 35 | 36 | <%= topic.title %> 37 | 38 |
    39 |
    40 | -------------------------------------------------------------------------------- /public/stylesheets/common.css: -------------------------------------------------------------------------------- 1 | body, p, input, textarea { 2 | font-size: 14px; 3 | word-break: break-word; 4 | } 5 | 6 | textarea, input[type="text"], 7 | input[type="password"], 8 | input[type="datetime"], 9 | input[type="datetime-local"], 10 | input[type="date"], 11 | input[type="month"], 12 | input[type="time"], 13 | input[type="week"], 14 | input[type="number"], 15 | input[type="email"], 16 | input[type="url"], 17 | input[type="search"], 18 | input[type="tel"], 19 | input[type="color"], 20 | .uneditable-input { 21 | background: hsla(0, 0%, 0%, 0) 22 | } 23 | 24 | pre { 25 | background: #fee9cc; 26 | border: 1px dashed #ccc; 27 | line-height: 22px; 28 | } 29 | 30 | code { 31 | padding: 0; 32 | border: none; 33 | } 34 | 35 | p code { 36 | background: none; 37 | color: hsl(0, 0%, 50%); 38 | margin: 0 1px; 39 | padding: 1px 4px; 40 | border-radius: 1px; 41 | } 42 | 43 | div pre.prettyprint { 44 | font-size: 14px; 45 | border-radius: 0px; 46 | padding: 0 15px; 47 | border: none; 48 | margin: 20px -10px; 49 | border-width: 1px 0px; 50 | background: #f7f7f7; 51 | -o-tab-size: 4; 52 | -moz-tab-size: 4; 53 | tab-size: 4; 54 | } 55 | 56 | form { 57 | margin-bottom: 0; 58 | } 59 | 60 | textarea { 61 | margin-bottom: 0; 62 | } 63 | 64 | input, textarea { 65 | background: hsla(0, 0%, 0%, 0); 66 | } 67 | -------------------------------------------------------------------------------- /common/message.js: -------------------------------------------------------------------------------- 1 | var models = require('../models'); 2 | var eventproxy = require('eventproxy'); 3 | var Message = models.Message; 4 | var User = require('../proxy').User; 5 | var messageProxy = require('../proxy/message'); 6 | var _ = require('lodash'); 7 | 8 | exports.sendReplyMessage = function (master_id, author_id, topic_id, reply_id, callback) { 9 | callback = callback || _.noop; 10 | var ep = new eventproxy(); 11 | ep.fail(callback); 12 | 13 | var message = new Message(); 14 | message.type = 'reply'; 15 | message.master_id = master_id; 16 | message.author_id = author_id; 17 | message.topic_id = topic_id; 18 | message.reply_id = reply_id; 19 | 20 | message.save(ep.done('message_saved')); 21 | ep.all('message_saved', function (msg) { 22 | callback(null, msg); 23 | }); 24 | }; 25 | 26 | exports.sendAtMessage = function (master_id, author_id, topic_id, reply_id, callback) { 27 | callback = callback || _.noop; 28 | var ep = new eventproxy(); 29 | ep.fail(callback); 30 | 31 | var message = new Message(); 32 | message.type = 'at'; 33 | message.master_id = master_id; 34 | message.author_id = author_id; 35 | message.topic_id = topic_id; 36 | message.reply_id = reply_id; 37 | 38 | message.save(ep.done('message_saved')); 39 | ep.all('message_saved', function (msg) { 40 | callback(null, msg); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /test/controllers/site.test.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * nodeclub - site controller test 3 | * Copyright(c) 2012 fengmk2 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * Module dependencies. 9 | */ 10 | 11 | var should = require('should'); 12 | var config = require('../../config'); 13 | var app = require('../../app'); 14 | var request = require('supertest')(app); 15 | 16 | 17 | describe('test/controllers/site.test.js', function () { 18 | 19 | it('should / 200', function (done) { 20 | request.get('/').end(function (err, res) { 21 | res.status.should.equal(200); 22 | res.text.should.containEql('积分榜'); 23 | res.text.should.containEql('友情社区'); 24 | done(err); 25 | }); 26 | }); 27 | 28 | it('should /?page=-1 200', function (done) { 29 | request.get('/?page=-1').end(function (err, res) { 30 | res.status.should.equal(200); 31 | res.text.should.containEql('积分榜'); 32 | res.text.should.containEql('友情社区'); 33 | done(err); 34 | }); 35 | }); 36 | 37 | it('should /sitemap.xml 200', function (done) { 38 | request.get('/sitemap.xml') 39 | .expect(200, function (err, res) { 40 | res.text.should.containEql(''); 41 | done(err); 42 | }); 43 | }); 44 | 45 | it('should /app/download', function (done) { 46 | request.get('/app/download') 47 | .expect(302, function (err, res) { 48 | done(err); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /api/v1/middleware.js: -------------------------------------------------------------------------------- 1 | var UserModel = require('../../models').User; 2 | var eventproxy = require('eventproxy'); 3 | var validator = require('validator'); 4 | 5 | // 非登录用户直接屏蔽 6 | var auth = function (req, res, next) { 7 | var ep = new eventproxy(); 8 | ep.fail(next); 9 | 10 | var accessToken = String(req.body.accesstoken || req.query.accesstoken || ''); 11 | accessToken = validator.trim(accessToken); 12 | 13 | UserModel.findOne({accessToken: accessToken}, ep.done(function (user) { 14 | if (!user) { 15 | res.status(401); 16 | return res.send({success: false, error_msg: '错误的accessToken'}); 17 | } 18 | if (user.is_block) { 19 | res.status(403); 20 | return res.send({success: false, error_msg: '您的账户被禁用'}); 21 | } 22 | req.user = user; 23 | next(); 24 | })); 25 | 26 | }; 27 | 28 | exports.auth = auth; 29 | 30 | // 非登录用户也可通过 31 | var tryAuth = function (req, res, next) { 32 | var ep = new eventproxy(); 33 | ep.fail(next); 34 | 35 | var accessToken = String(req.body.accesstoken || req.query.accesstoken || ''); 36 | accessToken = validator.trim(accessToken); 37 | 38 | UserModel.findOne({accessToken: accessToken}, ep.done(function (user) { 39 | if (!user) { 40 | return next() 41 | } 42 | if (user.is_block) { 43 | res.status(403); 44 | return res.send({success: false, error_msg: '您的账户被禁用'}); 45 | } 46 | req.user = user; 47 | next(); 48 | })); 49 | 50 | }; 51 | 52 | exports.tryAuth = tryAuth; 53 | -------------------------------------------------------------------------------- /public/javascripts/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var windowHeight = $(window).height(); 3 | var $backtotop = $('#backtotop'); 4 | var top = windowHeight - $backtotop.height() - 200; 5 | 6 | 7 | function moveBacktotop() { 8 | $backtotop.css({ top: top, right: 0}); 9 | } 10 | 11 | function footerFixBottom() { 12 | if($(document.body).height() < windowHeight){ 13 | $("#footer").addClass('fix-bottom'); 14 | }else{ 15 | $("#footer").removeClass('fix-bottom'); 16 | } 17 | } 18 | 19 | $backtotop.click(function () { 20 | $('html,body').animate({ scrollTop: 0 }); 21 | return false; 22 | }); 23 | $(window).scroll(function () { 24 | var windowHeight = $(window).scrollTop(); 25 | if (windowHeight > 200) { 26 | $backtotop.fadeIn(); 27 | } else { 28 | $backtotop.fadeOut(); 29 | } 30 | }); 31 | 32 | moveBacktotop(); 33 | footerFixBottom(); 34 | $(window).resize(moveBacktotop); 35 | $(window).resize(footerFixBottom); 36 | 37 | $('.topic_content a,.reply_content a').attr('target', '_blank'); 38 | 39 | // pretty code 40 | prettyPrint(); 41 | 42 | // data-loading-text="提交中" 43 | $('.submit_btn').click(function () { 44 | $(this).button('loading'); 45 | }); 46 | 47 | // 广告的统计信息 48 | $('.sponsor_outlink').click(function () { 49 | var $this = $(this); 50 | var label = $this.data('label'); 51 | ga('send', 'event', 'banner', 'click', label, 1.00, {'nonInteraction': 1}); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /views/sign/search_pass.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sign/sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if (typeof(error) !== 'undefined' && error) { %> 13 |
    14 | × 15 | <%= error %> 16 |
    17 | <% } %> 18 |
    19 |
    20 | 21 | 22 |
    23 | <% if (typeof(email) !== 'undefined') { %> 24 | 25 | <% } else { %> 26 | 27 | <% } %> 28 |

    请输入您注册帐户时使用的电子邮箱

    29 |
    30 | 31 |
    32 |
    33 | 34 |
    35 |
    36 |
    37 |
    38 |
    39 | -------------------------------------------------------------------------------- /test/middlewares/limit.test.js: -------------------------------------------------------------------------------- 1 | var limitMiddleware = require('../../middlewares/limit'); 2 | var app = require('../../app'); 3 | var supertest; 4 | var support = require('../support/support'); 5 | var pedding = require('pedding'); 6 | var visitor = 'visit' + Date.now(); 7 | 8 | describe('test/middlewares/limit.test.js', function () { 9 | before(function (done) { 10 | support.ready(done); 11 | }); 12 | 13 | before(function () { 14 | app.get('/test_peripperday', 15 | limitMiddleware.peripperday(visitor, 3, {showJson: true}), function (req, res) { 16 | res.send('hello'); 17 | }); 18 | 19 | supertest = require('supertest')(app); 20 | }); 21 | describe('#peripperday', function () { 22 | it('should visit', function (done) { 23 | supertest.get('/test_peripperday').set('x-real-ip', '127.0.0.1').end(function () { 24 | supertest.get('/test_peripperday').set('x-real-ip', '127.0.0.1').end(function () { 25 | supertest.get('/test_peripperday').set('x-real-ip', '127.0.0.1').end(function (err, res) { 26 | res.text.should.eql('hello'); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | }); 32 | it('should not visit', function (done) { 33 | supertest.get('/test_peripperday') 34 | .set('x-real-ip', '127.0.0.1') 35 | .end(function (err, res) { 36 | res.status.should.equal(403); 37 | res.body.success.should.false(); 38 | done(err); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /models/topic.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var BaseModel = require("./base_model"); 3 | var Schema = mongoose.Schema; 4 | var ObjectId = Schema.ObjectId; 5 | var config = require('../config'); 6 | var _ = require('lodash'); 7 | 8 | var TopicSchema = new Schema({ 9 | title: { type: String }, 10 | content: { type: String }, 11 | author_id: { type: ObjectId }, 12 | top: { type: Boolean, default: false }, // 置顶帖 13 | good: {type: Boolean, default: false}, // 精华帖 14 | lock: {type: Boolean, default: false}, // 被锁定主题 15 | reply_count: { type: Number, default: 0 }, 16 | visit_count: { type: Number, default: 0 }, 17 | collect_count: { type: Number, default: 0 }, 18 | create_at: { type: Date, default: Date.now }, 19 | update_at: { type: Date, default: Date.now }, 20 | last_reply: { type: ObjectId }, 21 | last_reply_at: { type: Date, default: Date.now }, 22 | content_is_html: { type: Boolean }, 23 | tab: {type: String}, 24 | deleted: {type: Boolean, default: false}, 25 | }); 26 | 27 | TopicSchema.plugin(BaseModel); 28 | TopicSchema.index({create_at: -1}); 29 | TopicSchema.index({top: -1, last_reply_at: -1}); 30 | TopicSchema.index({author_id: 1, create_at: -1}); 31 | 32 | TopicSchema.virtual('tabName').get(function () { 33 | var tab = this.tab; 34 | var pair = _.find(config.tabs, function (_pair) { 35 | return _pair[0] === tab; 36 | }); 37 | 38 | if (pair) { 39 | return pair[1]; 40 | } else { 41 | return ''; 42 | } 43 | }); 44 | 45 | mongoose.model('Topic', TopicSchema); 46 | -------------------------------------------------------------------------------- /views/message/message.html: -------------------------------------------------------------------------------- 1 | <% if (message.has_read) { %> 2 |
    3 | <% } else { %> 4 |
    5 | <% } %> 6 | <% if(message.type == 'reply'){ %> 7 | 8 | <%= message.author.loginname %> 9 | 回复了你的话题 10 | <%= 11 | message.topic.title %> 12 | 13 | <% } %> 14 | <% if(message.type == 'reply2'){ %> 15 | 16 | <%= message.author.loginname %> 17 | 在话题 18 | <%= 19 | message.topic.title %> 20 | 中回复了你的回复 21 | 22 | <% } %> 23 | <% if(message.type == 'follow'){ %> 24 | 25 | <%= message.author.loginname %> 26 | 关注了你 27 | 28 | <% } %> 29 | <% if (message.type == 'at'){ %> 30 | 31 | <%= message.author.loginname %> 32 | 在话题 33 | <%= 34 | message.topic.title %> 35 | 中@了你 36 | 37 | <% } %> 38 | 39 |
    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 |
    6 | 10 |
    11 |
    12 | <% if(typeof(error) !== 'undefined' && error){ %> 13 |
    14 | × 15 | <%= error %> 16 |
    17 | <% } %> 18 |
    19 |
    20 | 21 | 22 |
    23 | 24 |
    25 |
    26 |
    27 | 28 | 29 |
    30 | 31 |
    32 |
    33 | 34 | 35 | 36 | 37 |
    38 | 39 |
    40 |
    41 |
    42 |
    43 |
    44 | -------------------------------------------------------------------------------- /views/sign/signin.html: -------------------------------------------------------------------------------- 1 | <%- partial('../sign/sidebar') %> 2 | 3 |
    4 |
    5 |
    6 | 10 |
    11 |
    12 | <% if(typeof(error) !== 'undefined' && error){ %> 13 |
    14 | × 15 | <%= error %> 16 |
    17 | <% } %> 18 |
    19 |
    20 | 21 | 22 |
    23 | 24 |
    25 |
    26 |
    27 | 28 | 29 |
    30 | 31 |
    32 |
    33 | 34 | 35 | 44 |
    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 |
    6 | 10 |
    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 |
    2 |
    3 |
    4 | 8 |
    9 |
    10 |
    method='post'> 12 | 13 | 14 | 15 |
    16 | 17 | 18 |
    19 | 20 |
    21 |
    22 |
    23 | 24 |
    method='post'> 26 |
    27 | 28 |
    29 |
    30 | 31 | 32 |
    33 | 34 |
    35 |
    36 |
    37 | 38 | 39 |
    40 | 41 |
    42 |
    43 | 44 | 45 |
    46 | 47 |
    48 |
    49 |
    50 |
    51 |
    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 |
    6 | 10 |
    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 |
    24 |
    25 |
    26 |
    27 | 31 | 32 |
    33 | 35 |
    36 |
    37 | 38 |
    39 | 40 | 41 |
    42 |
    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('

    var a = 1;

    \n
    '); 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('
    var a = 1;\n
    '); 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('
    var a = 1;
    '); 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 |
    6 | 10 |
    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 |
    24 |
    25 | 26 | 27 |
    28 | <% if (typeof(loginname) !== 'undefined') { %> 29 | 30 | <% } else { %> 31 | 32 | <% } %> 33 |
    34 |
    35 |
    36 | 37 | 38 |
    39 | 40 |
    41 |
    42 |
    43 | 44 | 45 |
    46 | 47 |
    48 |
    49 |
    50 | 51 | 52 |
    53 | <% if (typeof(email) !== 'undefined') { %> 54 | 55 | <% } else { %> 56 | 57 | <% } %> 58 |
    59 |
    60 | 61 | 62 | 70 |
    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 |
    49 | 50 | 51 | 52 |
    53 |
    54 | 56 | 57 |
    58 | 60 |
    61 |
    62 | 63 |
    64 | 65 |
    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 |
    6 | 14 |
    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 |
    29 | <% } else { %> 30 | 31 | <% } %> 32 |
    33 | 选择版块: 34 | 49 | 50 | 53 | 54 |
    55 |
    56 | 59 | 60 |
    61 | 63 |
    64 |
    65 | 66 |
    67 | 68 | 69 | 70 |
    71 |
    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 | --------------------------------------------------------------------------------