Showing preview only (1,972K chars total). Download the full file or copy to clipboard to get everything.
Repository: cnodejs/nodeclub
Branch: master
Commit: 91a3286f2c75
Files: 192
Total size: 1.9 MB
Directory structure:
gitextract_iz_2t4kk/
├── .gitignore
├── .jshintrc
├── .mention-bot
├── .snyk
├── .travis.yml
├── History.md
├── LICENSE
├── Makefile
├── README.md
├── api/
│ └── v1/
│ ├── message.js
│ ├── middleware.js
│ ├── reply.js
│ ├── tools.js
│ ├── topic.js
│ ├── topic_collect.js
│ └── user.js
├── api_router_v1.js
├── app.js
├── bin/
│ ├── fix_at_problem.js
│ ├── fix_topic_collect_count.js
│ ├── generate_accesstoken.js
│ └── get_user_topics.js
├── common/
│ ├── at.js
│ ├── cache.js
│ ├── logger.js
│ ├── mail.js
│ ├── message.js
│ ├── redis.js
│ ├── render_helper.js
│ ├── store.js
│ ├── store_local.js
│ ├── store_qn.js
│ └── tools.js
├── config.default.js
├── controllers/
│ ├── github.js
│ ├── message.js
│ ├── reply.js
│ ├── rss.js
│ ├── search.js
│ ├── sign.js
│ ├── site.js
│ ├── static.js
│ ├── topic.js
│ └── user.js
├── logs/
│ └── .gitkeep
├── middlewares/
│ ├── auth.js
│ ├── conf.js
│ ├── error_page.js
│ ├── github_strategy.js
│ ├── limit.js
│ ├── mongoose_log.js
│ ├── proxy.js
│ ├── render.js
│ └── request_log.js
├── models/
│ ├── base_model.js
│ ├── index.js
│ ├── message.js
│ ├── reply.js
│ ├── topic.js
│ ├── topic_collect.js
│ └── user.js
├── oneapm.js
├── package.json
├── proxy/
│ ├── index.js
│ ├── message.js
│ ├── reply.js
│ ├── topic.js
│ ├── topic_collect.js
│ └── user.js
├── public/
│ ├── github-card.html
│ ├── javascripts/
│ │ ├── main.js
│ │ └── responsive.js
│ ├── libs/
│ │ ├── bootstrap/
│ │ │ ├── css/
│ │ │ │ ├── bootstrap-responsive.css
│ │ │ │ └── bootstrap.css
│ │ │ └── js/
│ │ │ └── bootstrap.js
│ │ ├── code-prettify/
│ │ │ ├── lang-apollo.js
│ │ │ ├── lang-clj.js
│ │ │ ├── lang-css.js
│ │ │ ├── lang-go.js
│ │ │ ├── lang-hs.js
│ │ │ ├── lang-lisp.js
│ │ │ ├── lang-lua.js
│ │ │ ├── lang-ml.js
│ │ │ ├── lang-n.js
│ │ │ ├── lang-proto.js
│ │ │ ├── lang-scala.js
│ │ │ ├── lang-sql.js
│ │ │ ├── lang-tex.js
│ │ │ ├── lang-vb.js
│ │ │ ├── lang-vhdl.js
│ │ │ ├── lang-wiki.js
│ │ │ ├── lang-xq.js
│ │ │ ├── lang-yaml.js
│ │ │ ├── prettify.css
│ │ │ └── prettify.js
│ │ ├── editor/
│ │ │ ├── editor.css
│ │ │ ├── editor.js
│ │ │ └── ext.js
│ │ ├── font-awesome/
│ │ │ ├── css/
│ │ │ │ └── font-awesome.css
│ │ │ └── fonts/
│ │ │ └── FontAwesome.otf
│ │ ├── jquery-2.1.0.js
│ │ ├── jquery-ujs.js
│ │ ├── jquery.atwho.js
│ │ ├── jquery.caret.js
│ │ ├── lodash.compat.js
│ │ ├── markdownit.js
│ │ ├── qrcode.js
│ │ └── webuploader/
│ │ ├── Uploader.swf
│ │ ├── webuploader.css
│ │ └── webuploader.withoutimage.js
│ └── stylesheets/
│ ├── common.css
│ ├── jquery.atwho.css
│ ├── responsive.css
│ └── style.less
├── test/
│ ├── api/
│ │ └── v1/
│ │ ├── message.test.js
│ │ ├── reply.test.js
│ │ ├── tools.test.js
│ │ ├── topic.test.js
│ │ ├── topic_collect.test.js
│ │ └── user.test.js
│ ├── app.test.js
│ ├── common/
│ │ ├── at.test.js
│ │ ├── cache.test.js
│ │ ├── mail.test.js
│ │ ├── message.test.js
│ │ ├── render_helper.test.js
│ │ ├── store_local.test.js
│ │ └── tools.test.js
│ ├── controllers/
│ │ ├── github.test.js
│ │ ├── message.test.js
│ │ ├── reply.test.js
│ │ ├── rss.test.js
│ │ ├── search.test.js
│ │ ├── sign.test.js
│ │ ├── site.test.js
│ │ ├── static.test.js
│ │ ├── topic.test.js
│ │ └── user.test.js
│ ├── env.js
│ ├── middlewares/
│ │ ├── conf.test.js
│ │ ├── limit.test.js
│ │ └── proxy.test.js
│ ├── models/
│ │ └── user.test.js
│ ├── proxy/
│ │ ├── message.test.js
│ │ ├── reply.test.js
│ │ ├── topic.test.js
│ │ └── user.test.js
│ └── support/
│ └── support.js
├── views/
│ ├── _ads.html
│ ├── _sponsors.html
│ ├── editor_sidebar.html
│ ├── includes/
│ │ └── editor.html
│ ├── index.html
│ ├── layout.html
│ ├── message/
│ │ ├── index.html
│ │ └── message.html
│ ├── notify/
│ │ └── notify.html
│ ├── reply/
│ │ ├── edit.html
│ │ └── reply.html
│ ├── sidebar.html
│ ├── sign/
│ │ ├── new_oauth.html
│ │ ├── no_github_email.html
│ │ ├── reset.html
│ │ ├── search_pass.html
│ │ ├── sidebar.html
│ │ ├── signin.html
│ │ └── signup.html
│ ├── static/
│ │ ├── about.html
│ │ ├── api.html
│ │ ├── faq.html
│ │ └── getstart.html
│ ├── topic/
│ │ ├── _top_good.html
│ │ ├── abstract.html
│ │ ├── edit.html
│ │ ├── index.html
│ │ ├── list.html
│ │ └── small.html
│ └── user/
│ ├── card.html
│ ├── collect_topics.html
│ ├── followers.html
│ ├── followings.html
│ ├── index.html
│ ├── replies.html
│ ├── setting.html
│ ├── star.html
│ ├── stars.html
│ ├── top.html
│ ├── top100.html
│ ├── top100_user.html
│ ├── topics.html
│ └── user.html
└── web_router.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
config.js
.cov
coverage
node_modules
.naeindex
coverage.html
.monitor
*.min.*.js
*.min.*.css
assets.json
# Ignore Mac OS desktop services store
.DS_Store
# Ignore Windows desktop setting file
desktop.ini
# Ignore Redis snapshot
dump.rdb
*.log
.idea
public/upload/*
*.sublime-project
*.sublime-workspace
*.swp
package-lock.json
================================================
FILE: .jshintrc
================================================
{
"predef": [
"phantom",
"module",
"require",
"__dirname",
"process",
"console",
"it",
"describe",
"before",
"beforeEach",
"after",
"afterEach",
"ace",
"$"
],
"browser": true,
"node": true,
"es5": true,
"bitwise": true,
"curly": true,
"eqeqeq": true,
"forin": false,
"immed": true,
"latedef": true,
"newcap": true,
"noarg": true,
"noempty": true,
"nonew": true,
"plusplus": false,
"undef": true,
"strict": false,
"trailing": false,
"globalstrict": true,
"nonstandard": true,
"white": true,
"indent": 2,
"expr": true,
"multistr": true,
"onevar": false,
"unused": "vars"
}
================================================
FILE: .mention-bot
================================================
{
"userBlacklist": ["huacnlee"]
}
================================================
FILE: .snyk
================================================
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.12.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:tunnel-agent:20170305':
- jpush-sdk > request > tunnel-agent:
patched: '2018-07-01T04:07:14.342Z'
================================================
FILE: .travis.yml
================================================
sudo: false
language: node_js
env:
- CXX=g++-4.8
node_js:
- stable
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
services:
- mongodb
- redis
before_install:
- $CXX --version
script: make test-cov
after_success: npm i codecov && codecov
================================================
FILE: History.md
================================================
2.1.0 / 2015-09-15
==================
* 使用 oneapm 代替 newrelic
2.0.1 / 2015-08-07
==================
* 去掉【收藏功能】
0.3.6 / 2013-11-22
==================
* fix #237 if topic not exists, do not modified it.
* Merge pull request #230 from JacksonTian/fix_null
* 修复给空值设置属性的错误
* Merge pull request #224 from JacksonTian/config_ga
* 将Google tracker可配置化
* Merge pull request #223 from jiyinyiyong/ga
* add Google Analytics
* Merge pull request #217 from leizongmin/master
* 编辑器进入全屏模式时,调整一下样式
* hotfix sendAtMail
* fixed "TypeError: Cannot read property author_id of null"
* 使用七牛 gravatar.qiniudn.com 镜像
* Merge pull request #212 from tjwudi/master
* Fix 'title' textarea layout problem.
* Merge pull request #211 from tjwudi/master
* Add OS regular files to .gitignore
* Merge pull request #209 from leizongmin/master
* 解决打开Topic时自动跳到输入框问题
* Merge pull request #205 from jiyinyiyong/master
* Merge pull request #208 from leizongmin/master
* 帖子内容页面,增加表格样式
* 发布帖子使用 EpicEditor 编辑器
* xss白名单 增加thead标签
* Merge pull request #204 from JacksonTian/assets_issue
* limit the length of message to 20
* 兼容未启用压缩功能的情况
* Merge pull request #193 from dead-horse/fix-block-count
* fix block count
* Merge pull request #190 from leizongmin/master
* 修正用户收藏的话题页面,页码链接不正确问题
* Merge pull request #186 from phoenixlzx/master
* Fixed mongoose version
* Merge pull request #182 from JacksonTian/assets_mini
* 替换debug为mini
* Merge pull request #181 from JacksonTian/assets
* 用config.debug判断是否线上状态
* Merge pull request #180 from JacksonTian/assets
* 升级data2xml
* 静态资源重构
* Merge pull request #178 from JacksonTian/typo
* Node.JS => Node.js
* Merge pull request #177 from JacksonTian/style
* 文章页面的样式
* Fix几个样式问题
* fixed #175 stars max size
* fixed require package.json nae not support bug
* Merge pull request #174 from JacksonTian/style
* 去除用标签来制造空格的行为
* 更新样式
* Merge pull request #171 from jiyinyiyong/markdown-p
* limit image height
* limit pre-wrap inside p
* Merge pull request #165 from VitoLau/master
* 更新about faq页面结构
* 调整about和faq页面 padding大小
* Merge pull request #164 from jiyinyiyong/master
* fix: markdown-text use pre-wrap
* format indentations to 2
* limit the with of message links to prevent line breaks
* 修复文字过长没有换行的问题
0.3.5 / 2013-05-30
==================
* Update logo based on the new official one (@finian)
* UI more flatter; use "white-space:pre" to show spaces (@jiyinyiyong)
* fixed #161 xss process after markdown transfer
* 修复reply2的逻辑;暂时屏蔽标签功能
* use fixed-width font in reply
* a text align and a padding
* add padding to the read messages
* 点击回复数直接跳到最后一个回复
* 支持html
* fixed #154 消息跳转没有直接跳转到回复
* fixed #146 修复tag编辑bug
* 增加nae config
* 增加邮件提示内容
* read file sync package.json
0.3.4 / 2013-05-27
==================
* user markd instead showdown, use ace (@fengmk2)
* 使用加粗的边缘线; 过滤粉红色的边缘线; 加深 panel header 颜色; 去掉 scrollbar 定制 (@jiyinyiyong)
* 指定xss模块的配置信息,禁止HTML标签的style和class属性 (@lezongmin)
* shanzhai'd Github T3T (@jiyinyiyong)
* @jiyinyiyong 修改界面布局 fixed #139
* 添加话题详情主要内容的行高 (@kerngven)
* 增加POST提交时间间隔限制 (@leizongmin)
* 中英文间用空格
* 发帖页面优化
* 文本框高度不要闪烁
* use bootstrap 2; hide tags
* see the demo of new UI
* 搜索页面,如果回复时间过长,会产生断行的情况
* unit test cases
* #132 Add https:// validate on user.js cnodejs/nodeclub#132 (@meteormatt)
* Add 0.10 for travis
* fixed #107 update user links
0.3.3 / 2013-03-11
==================
* Merge pull request #126 from cnodejs/updateSignFlow
* 修复topic更新bug;修复@某人 bug
* reply2也可以定位到
* 修复node 0.6 test cases
* 修复删除评论异常
* 修复 exports.updateLastReply 没有callback的bug
* 管理员可以帮忙激活账号
* Merge pull request #125 from JacksonTian/refine
* Fix http to https
* 重构注册和发帖以及发邮件的部分
* Merge pull request #117 from JacksonTian/get_post
* 去除掉req.method的判断,分拆方法
* Merge pull request #122 from JacksonTian/proxy
* 分离controller和数据操作业务逻辑
* 改完下划线驼峰为小驼峰式风格
* Merge pull request #116 from JacksonTian/codingstyle
* Coding style refine.
* Merge pull request #113 from JacksonTian/master
* 添加依赖服务状态图标
* Merge pull request #112 from JacksonTian/refine
* 修正rewire.reset()导致的单元测试异常
* Refine coding style
* Merge pull request #111 from JacksonTian/master
* Merge pull request #110 from JacksonTian/reset_history
* Update Authors
* 恢复History.md文件
* Merge pull request #104 from ccding/master
* fix issue #27: lower case email address for gravatar
* Merge pull request #103 from ccding/master
* fix issue #92: email address with gmail label ("+" encode)
* fixed topic delete not post method security problem.
* empty author
* fixed author empty bug
* Merge pull request #99 from leizongmin/master
* 将Markdown中的H标题解析放到代码块解析后面
* Merge pull request #96 from leizongmin/master
* 修正无法正确解析http://127.0.0.1这样的IP地址链接
* fixed font
* fixed color style
* Merge pull request #87 from jiyinyiyong/rebased
* Merge pull request #91 from leizongmin/master
* 使用xss模块来过滤主题及回复内容
* update to 0.3.2
* Merge remote branch 'cnode/master'
* fix escape
* Merge pull request #89 from dead-horse/master
* fix test
* use in node-validator
* Merge remote branch 'cnode/master'
* support block code
* Merge pull request #88 from dead-horse/master
* fix
* change @me to markdown
* fix @ bug in topic content
* Merge pull request #86 from dead-horse/master
* not escape html in
* add preview
* remove tags in topics of home page
* some css
* 合并通知按钮
* use escape replace of xss()
* fixed test cases
* Merge pull request #85 from dead-horse/master
* 过滤url允许绝对路径
0.3.2 / 2012-03-04
==================
* ensure IncomingForm.UPLOAD_DIR
* ensure upload image dir exists
* fixed csrf bug in mark message read
* remove customHost
* add .naeignore files
* * merge cnodeclub to nodeclub; * add more settings for custom site; * fixed upload.js not worked bug;
* Merge pull request #4 from dead-horse/master
* Merge pull request #11 from thebrecht/master
* 话题回复数纳入二级回复,样式调整
* 支持table,邮件提醒
* 加入亂數產生新密碼
* fix style
* bugs fixed
* Merge pull request #3 from LeToNode/master
* Merge pull request #6 from ericzhang-cn/master
* markdown语法粗体应为两个星号,原描述有误
* Merge pull request #2 from roymax/master
* change to async
* change to async
* Update README.md
* 修复`abc+label@gmail.com`格式的注册邮箱不能成功激活的问题
* commit
* project init
* first commit
================================================
FILE: LICENSE
================================================
(The MIT License)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
TESTS = $(shell find test -type f -name "*.test.js")
TEST_TIMEOUT = 10000
MOCHA_REPORTER = spec
# NPM_REGISTRY = "--registry=http://registry.npm.taobao.org"
NPM_REGISTRY = ""
all: test
install:
@npm install $(NPM_REGISTRY)
pretest:
@if ! test -f config.js; then \
cp config.default.js config.js; \
fi
@if ! test -d public/upload; then \
mkdir public/upload; \
fi
test: install pretest
@NODE_ENV=test ./node_modules/mocha/bin/mocha \
--reporter $(MOCHA_REPORTER) \
-r should \
-r test/env \
--timeout $(TEST_TIMEOUT) \
$(TESTS)
testfile:
@NODE_ENV=test ./node_modules/mocha/bin/mocha \
--reporter $(MOCHA_REPORTER) \
-r should \
-r test/env \
--timeout $(TEST_TIMEOUT) \
$(FILE)
test-cov cov: install pretest
@NODE_ENV=test node \
node_modules/.bin/istanbul cover --preserve-comments \
./node_modules/.bin/_mocha \
-- \
-r should \
-r test/env \
--reporter $(MOCHA_REPORTER) \
--timeout $(TEST_TIMEOUT) \
$(TESTS)
build:
@./node_modules/loader-builder/bin/builder views .
run:
@node app.js
start: install build
@NODE_ENV=production ./node_modules/.bin/pm2 start app.js -i 0 --name "cnode" --max-memory-restart 400M
restart: install build
@NODE_ENV=production ./node_modules/.bin/pm2 restart "cnode"
.PHONY: install test testfile cov test-cov build run start restart
================================================
FILE: README.md
================================================
Nodeclub
=
[![build status][travis-image]][travis-url]
[![codecov.io][codecov-image]][codecov-url]
[![David deps][david-image]][david-url]
[![node version][node-image]][node-url]
[travis-image]: https://img.shields.io/travis/cnodejs/nodeclub/master.svg?style=flat-square
[travis-url]: https://travis-ci.org/cnodejs/nodeclub
[codecov-image]: https://img.shields.io/codecov/c/github/cnodejs/nodeclub/master.svg?style=flat-square
[codecov-url]: https://codecov.io/github/cnodejs/nodeclub?branch=master
[david-image]: https://img.shields.io/david/cnodejs/nodeclub.svg?style=flat-square
[david-url]: https://david-dm.org/cnodejs/nodeclub
[node-image]: https://img.shields.io/badge/node.js-%3E=_4.2-green.svg?style=flat-square
[node-url]: http://nodejs.org/download/
## 介绍
Nodeclub 是使用 **Node.js** 和 **MongoDB** 开发的社区系统,界面优雅,功能丰富,小巧迅速,
已在Node.js 中文技术社区 [CNode(http://cnodejs.org)](http://cnodejs.org) 得到应用,但你完全可以用它搭建自己的社区。
## 安装部署
*不保证 Windows 系统的兼容性*
线上跑的是 [Node.js](https://nodejs.org) v8.12.0,[MongoDB](https://www.mongodb.org) 是 v4.0.3,[Redis](http://redis.io) 是 v4.0.9。
```
1. 安装 `Node.js[必须]` `MongoDB[必须]` `Redis[必须]`
2. 启动 MongoDB 和 Redis
3. `$ make install` 安装 Nodeclub 的依赖包
4. `cp config.default.js config.js` 请根据需要修改配置文件
5. `$ make test` 确保各项服务都正常
6. `$ node app.js`
7. visit `http://localhost:3000`
8. done!
```
## 测试
跑测试
```bash
$ make test
```
跑覆盖率测试
```bash
$ make test-cov
```
## 贡献
有任何意见或建议都欢迎提 issue,或者直接提给 [@alsotang](https://github.com/alsotang)
## License
MIT
================================================
FILE: api/v1/message.js
================================================
var eventproxy = require('eventproxy');
var Message = require('../../proxy').Message;
var at = require('../../common/at');
var renderHelper = require('../../common/render_helper');
var _ = require('lodash');
var index = function (req, res, next) {
var user_id = req.user._id;
var mdrender = req.query.mdrender === 'false' ? false : true;
var ep = new eventproxy();
ep.fail(next);
ep.all('has_read_messages', 'hasnot_read_messages', function (has_read_messages, hasnot_read_messages) {
res.send({
success: true,
data: {
has_read_messages: has_read_messages,
hasnot_read_messages: hasnot_read_messages
}
});
});
ep.all('has_read', 'unread', function (has_read, unread) {
[has_read, unread].forEach(function (msgs, idx) {
var epfill = new eventproxy();
epfill.fail(next);
epfill.after('message_ready', msgs.length, function (docs) {
docs = docs.filter(function (doc) {
return !doc.is_invalid;
});
docs = docs.map(function (doc) {
doc.author = _.pick(doc.author, ['loginname', 'avatar_url']);
doc.topic = _.pick(doc.topic, ['id', 'author', 'title', 'last_reply_at']);
doc.reply = _.pick(doc.reply, ['id', 'content', 'ups', 'create_at']);
if (mdrender) {
doc.reply.content = renderHelper.markdown(at.linkUsers(doc.reply.content));
}
doc = _.pick(doc, ['id', 'type', 'has_read', 'author', 'topic', 'reply', 'create_at']);
return doc;
});
ep.emit(idx === 0 ? 'has_read_messages' : 'hasnot_read_messages', docs);
});
msgs.forEach(function (doc) {
Message.getMessageById(doc._id, epfill.group('message_ready'));
});
});
});
Message.getReadMessagesByUserId(user_id, ep.done('has_read'));
Message.getUnreadMessageByUserId(user_id, ep.done('unread'));
};
exports.index = index;
var markAll = function (req, res, next) {
var user_id = req.user._id;
var ep = new eventproxy();
ep.fail(next);
Message.getUnreadMessageByUserId(user_id, ep.done('unread', function (docs) {
docs.forEach(function (doc) {
doc.has_read = true;
doc.save();
});
return docs;
}));
ep.all('unread', function (unread) {
unread = unread.map(function (doc) {
doc = _.pick(doc, ['id']);
return doc;
});
res.send({
success: true,
marked_msgs: unread
});
});
};
exports.markAll = markAll;
var markOne = function (req, res, next) {
var msg_id = req.params.msg_id;
var ep = new eventproxy();
ep.fail(next);
Message.updateOneMessageToRead(msg_id, ep.done('marked_result', function (result) {
return result;
}));
ep.all('marked_result', function (result) {
res.send({
success: true,
marked_msg_id: msg_id
});
});
};
exports.markOne = markOne;
var count = function (req, res, next) {
var userId = req.user.id;
var ep = new eventproxy();
ep.fail(next);
Message.getMessagesCount(userId, ep.done(function (count) {
res.send({success: true, data: count});
}));
};
exports.count = count;
================================================
FILE: api/v1/middleware.js
================================================
var UserModel = require('../../models').User;
var eventproxy = require('eventproxy');
var validator = require('validator');
// 非登录用户直接屏蔽
var auth = function (req, res, next) {
var ep = new eventproxy();
ep.fail(next);
var accessToken = String(req.body.accesstoken || req.query.accesstoken || '');
accessToken = validator.trim(accessToken);
UserModel.findOne({accessToken: accessToken}, ep.done(function (user) {
if (!user) {
res.status(401);
return res.send({success: false, error_msg: '错误的accessToken'});
}
if (user.is_block) {
res.status(403);
return res.send({success: false, error_msg: '您的账户被禁用'});
}
req.user = user;
next();
}));
};
exports.auth = auth;
// 非登录用户也可通过
var tryAuth = function (req, res, next) {
var ep = new eventproxy();
ep.fail(next);
var accessToken = String(req.body.accesstoken || req.query.accesstoken || '');
accessToken = validator.trim(accessToken);
UserModel.findOne({accessToken: accessToken}, ep.done(function (user) {
if (!user) {
return next()
}
if (user.is_block) {
res.status(403);
return res.send({success: false, error_msg: '您的账户被禁用'});
}
req.user = user;
next();
}));
};
exports.tryAuth = tryAuth;
================================================
FILE: api/v1/reply.js
================================================
var eventproxy = require('eventproxy');
var validator = require('validator');
var Topic = require('../../proxy').Topic;
var User = require('../../proxy').User;
var Reply = require('../../proxy').Reply;
var at = require('../../common/at');
var message = require('../../common/message');
var config = require('../../config');
var create = function (req, res, next) {
var topic_id = req.params.topic_id;
var content = req.body.content || '';
var reply_id = req.body.reply_id;
var ep = new eventproxy();
ep.fail(next);
var str = validator.trim(content);
if (str === '') {
res.status(400);
return res.send({success: false, error_msg: '回复内容不能为空'});
}
if (!validator.isMongoId(topic_id)) {
res.status(400);
return res.send({success: false, error_msg: '不是有效的话题id'});
}
Topic.getTopic(topic_id, ep.done(function (topic) {
if (!topic) {
res.status(404);
return res.send({success: false, error_msg: '话题不存在'});
}
if (topic.lock) {
res.status(403);
return res.send({success: false, error_msg: '该话题已被锁定'});
}
ep.emit('topic', topic);
}));
ep.all('topic', function (topic) {
User.getUserById(topic.author_id, ep.done('topic_author'));
});
ep.all('topic', 'topic_author', function (topic, topicAuthor) {
Reply.newAndSave(content, topic_id, req.user.id, reply_id, ep.done(function (reply) {
Topic.updateLastReply(topic_id, reply._id, ep.done(function () {
ep.emit('reply_saved', reply);
//发送at消息,并防止重复 at 作者
var newContent = content.replace('@' + topicAuthor.loginname + ' ', '');
at.sendMessageToMentionUsers(newContent, topic_id, req.user.id, reply._id);
}));
}));
User.getUserById(req.user.id, ep.done(function (user) {
user.score += 5;
user.reply_count += 1;
user.save();
ep.emit('score_saved');
}));
});
ep.all('reply_saved', 'topic', function (reply, topic) {
if (topic.author_id.toString() !== req.user.id.toString()) {
message.sendReplyMessage(topic.author_id, req.user.id, topic._id, reply._id);
}
ep.emit('message_saved');
});
ep.all('reply_saved', 'message_saved', 'score_saved', function (reply) {
res.send({
success: true,
reply_id: reply._id
});
});
};
exports.create = create;
var ups = function (req, res, next) {
var replyId = req.params.reply_id;
var userId = req.user.id;
if (!validator.isMongoId(replyId)) {
res.status(400);
return res.send({success: false, error_msg: '不是有效的评论id'});
}
Reply.getReplyById(replyId, function (err, reply) {
if (err) {
return next(err);
}
if (!reply) {
res.status(404);
return res.send({success: false, error_msg: '评论不存在'});
}
if (reply.author_id.equals(userId) && !config.debug) {
res.status(403);
return res.send({success: false, error_msg: '不能帮自己点赞'});
} else {
var action;
reply.ups = reply.ups || [];
var upIndex = reply.ups.indexOf(userId);
if (upIndex === -1) {
reply.ups.push(userId);
action = 'up';
} else {
reply.ups.splice(upIndex, 1);
action = 'down';
}
reply.save(function () {
res.send({
success: true,
action: action
});
});
}
});
};
exports.ups = ups;
================================================
FILE: api/v1/tools.js
================================================
var eventproxy = require('eventproxy');
var accesstoken = function (req, res, next) {
var ep = new eventproxy();
ep.fail(next);
res.send({
success: true,
loginname: req.user.loginname,
avatar_url: req.user.avatar_url,
id: req.user.id
});
};
exports.accesstoken = accesstoken;
================================================
FILE: api/v1/topic.js
================================================
var models = require('../../models');
var TopicModel = models.Topic;
var TopicProxy = require('../../proxy').Topic;
var TopicCollect = require('../../proxy').TopicCollect;
var UserProxy = require('../../proxy').User;
var UserModel = models.User;
var config = require('../../config');
var eventproxy = require('eventproxy');
var _ = require('lodash');
var at = require('../../common/at');
var renderHelper = require('../../common/render_helper');
var validator = require('validator');
var index = function (req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
page = page > 0 ? page : 1;
var tab = req.query.tab || 'all';
var limit = Number(req.query.limit) || config.list_topic_count;
var mdrender = req.query.mdrender === 'false' ? false : true;
var query = {};
if (!tab || tab === 'all') {
query.tab = {$nin: ['job', 'dev']}
} else {
if (tab === 'good') {
query.good = true;
} else {
query.tab = tab;
}
}
query.deleted = false;
var options = { skip: (page - 1) * limit, limit: limit, sort: '-top -last_reply_at'};
var ep = new eventproxy();
ep.fail(next);
TopicModel.find(query, '', options, ep.done('topics'));
ep.all('topics', function (topics) {
topics.forEach(function (topic) {
UserModel.findById(topic.author_id, ep.done(function (author) {
if (mdrender) {
topic.content = renderHelper.markdown(at.linkUsers(topic.content));
}
topic.author = _.pick(author, ['loginname', 'avatar_url']);
ep.emit('author');
}));
});
ep.after('author', topics.length, function () {
topics = topics.map(function (topic) {
return _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
});
res.send({success: true, data: topics});
});
});
};
exports.index = index;
var show = function (req, res, next) {
var topicId = String(req.params.id);
var mdrender = req.query.mdrender === 'false' ? false : true;
var ep = new eventproxy();
if (!validator.isMongoId(topicId)) {
res.status(400);
return res.send({success: false, error_msg: '不是有效的话题id'});
}
ep.fail(next);
TopicProxy.getFullTopic(topicId, ep.done(function (msg, topic, author, replies) {
if (!topic) {
res.status(404);
return res.send({success: false, error_msg: '话题不存在'});
}
topic.visit_count += 1;
topic.save();
topic = _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
if (mdrender) {
topic.content = renderHelper.markdown(at.linkUsers(topic.content));
}
topic.author = _.pick(author, ['loginname', 'avatar_url']);
topic.replies = replies.map(function (reply) {
if (mdrender) {
reply.content = renderHelper.markdown(at.linkUsers(reply.content));
}
reply.author = _.pick(reply.author, ['loginname', 'avatar_url']);
reply = _.pick(reply, ['id', 'author', 'content', 'ups', 'create_at', 'reply_id']);
reply.reply_id = reply.reply_id || null;
if (reply.ups && req.user && reply.ups.indexOf(req.user._id) != -1) {
reply.is_uped = true;
} else {
reply.is_uped = false;
}
return reply;
});
ep.emit('full_topic', topic)
}));
if (!req.user) {
ep.emitLater('is_collect', null)
} else {
TopicCollect.getTopicCollect(req.user._id, topicId, ep.done('is_collect'))
}
ep.all('full_topic', 'is_collect', function (full_topic, is_collect) {
full_topic.is_collect = !!is_collect;
res.send({success: true, data: full_topic});
})
};
exports.show = show;
var create = function (req, res, next) {
var title = validator.trim(req.body.title || '');
var tab = validator.trim(req.body.tab || '');
var content = validator.trim(req.body.content || '');
// 得到所有的 tab, e.g. ['ask', 'share', ..]
var allTabs = config.tabs.map(function (tPair) {
return tPair[0];
});
// 验证
var editError;
if (title === '') {
editError = '标题不能为空';
} else if (title.length < 5 || title.length > 100) {
editError = '标题字数太多或太少';
} else if (!tab || !_.includes(allTabs, tab)) {
editError = '必须选择一个版块';
} else if (content === '') {
editError = '内容不可为空';
}
// END 验证
if (editError) {
res.status(400);
return res.send({success: false, error_msg: editError});
}
TopicProxy.newAndSave(title, content, tab, req.user.id, function (err, topic) {
if (err) {
return next(err);
}
var proxy = new eventproxy();
proxy.fail(next);
proxy.all('score_saved', function () {
res.send({
success: true,
topic_id: topic.id
});
});
UserProxy.getUserById(req.user.id, proxy.done(function (user) {
user.score += 5;
user.topic_count += 1;
user.save();
req.user = user;
proxy.emit('score_saved');
}));
//发送at消息
at.sendMessageToMentionUsers(content, topic.id, req.user.id);
});
};
exports.create = create;
exports.update = function (req, res, next) {
var topic_id = _.trim(req.body.topic_id);
var title = _.trim(req.body.title);
var tab = _.trim(req.body.tab);
var content = _.trim(req.body.content);
// 得到所有的 tab, e.g. ['ask', 'share', ..]
var allTabs = config.tabs.map(function (tPair) {
return tPair[0];
});
TopicProxy.getTopicById(topic_id, function (err, topic, tags) {
if (!topic) {
res.status(400);
return res.send({success: false, error_msg: '此话题不存在或已被删除。'});
}
if (topic.author_id.equals(req.user._id) || req.user.is_admin) {
// 验证
var editError;
if (title === '') {
editError = '标题不能是空的。';
} else if (title.length < 5 || title.length > 100) {
editError = '标题字数太多或太少。';
} else if (!tab || !_.includes(allTabs, tab)) {
editError = '必须选择一个版块。';
}
// END 验证
if (editError) {
return res.send({success: false, error_msg: editError});
}
//保存话题
topic.title = title;
topic.content = content;
topic.tab = tab;
topic.update_at = new Date();
topic.save(function (err) {
if (err) {
return next(err);
}
//发送at消息
at.sendMessageToMentionUsers(content, topic._id, req.user._id);
res.send({
success: true,
topic_id: topic.id
});
});
} else {
res.status(403)
return res.send({success: false, error_msg: '对不起,你不能编辑此话题。'});
}
});
};
================================================
FILE: api/v1/topic_collect.js
================================================
var eventproxy = require('eventproxy');
var TopicProxy = require('../../proxy').Topic;
var TopicCollectProxy = require('../../proxy').TopicCollect;
var UserProxy = require('../../proxy').User;
var _ = require('lodash');
var validator = require('validator');
function list(req, res, next) {
var loginname = req.params.loginname;
var ep = new eventproxy();
ep.fail(next);
UserProxy.getUserByLoginName(loginname, ep.done(function (user) {
if (!user) {
res.status(404);
return res.send({success: false, error_msg: '用户不存在'});
}
// api 返回 100 条就好了
TopicCollectProxy.getTopicCollectsByUserId(user._id, {limit: 100}, ep.done('collected_topics'));
ep.all('collected_topics', function (collected_topics) {
var ids = collected_topics.map(function (doc) {
return String(doc.topic_id)
});
var query = { _id: { '$in': ids } };
TopicProxy.getTopicsByQuery(query, {}, ep.done('topics', function (topics) {
topics = _.sortBy(topics, function (topic) {
return ids.indexOf(String(topic._id))
});
return topics
}));
});
ep.all('topics', function (topics) {
topics = topics.map(function (topic) {
topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
return _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
});
res.send({success: true, data: topics})
})
}))
}
exports.list = list;
function collect(req, res, next) {
var topic_id = req.body.topic_id;
if (!validator.isMongoId(topic_id)) {
res.status(400);
return res.send({success: false, error_msg: '不是有效的话题id'});
}
TopicProxy.getTopic(topic_id, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.status(404);
return res.json({success: false, error_msg: '话题不存在'});
}
TopicCollectProxy.getTopicCollect(req.user.id, topic._id, function (err, doc) {
if (err) {
return next(err);
}
if (doc) {
res.json({success: false});
return;
}
TopicCollectProxy.newAndSave(req.user.id, topic._id, function (err) {
if (err) {
return next(err);
}
res.json({success: true});
});
UserProxy.getUserById(req.user.id, function (err, user) {
if (err) {
return next(err);
}
user.collect_topic_count += 1;
user.save();
});
topic.collect_count += 1;
topic.save();
});
});
}
exports.collect = collect;
function de_collect(req, res, next) {
var topic_id = req.body.topic_id;
if (!validator.isMongoId(topic_id)) {
res.status(400);
return res.send({success: false, error_msg: '不是有效的话题id'});
}
TopicProxy.getTopic(topic_id, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.status(404);
return res.json({success: false, error_msg: '话题不存在'});
}
TopicCollectProxy.remove(req.user.id, topic._id, function (err, removeResult) {
if (err) {
return next(err);
}
if (removeResult.n == 0) {
return res.json({success: false})
}
UserProxy.getUserById(req.user.id, function (err, user) {
if (err) {
return next(err);
}
user.collect_topic_count -= 1;
user.save();
});
topic.collect_count -= 1;
topic.save();
res.json({success: true});
});
});
}
exports.de_collect = de_collect;
================================================
FILE: api/v1/user.js
================================================
var _ = require('lodash');
var eventproxy = require('eventproxy');
var UserProxy = require('../../proxy').User;
var TopicProxy = require('../../proxy').Topic;
var ReplyProxy = require('../../proxy').Reply;
var TopicCollect = require('../../proxy').TopicCollect;
var show = function (req, res, next) {
var loginname = req.params.loginname;
var ep = new eventproxy();
ep.fail(next);
UserProxy.getUserByLoginName(loginname, ep.done(function (user) {
if (!user) {
res.status(404);
return res.send({success: false, error_msg: '用户不存在'});
}
var query = {author_id: user._id};
var opt = {limit: 15, sort: '-create_at'};
TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_topics'));
ReplyProxy.getRepliesByAuthorId(user._id, {limit: 20, sort: '-create_at'},
ep.done(function (replies) {
var topic_ids = replies.map(function (reply) {
return reply.topic_id.toString()
});
topic_ids = _.uniq(topic_ids).slice(0, 5); // 只显示最近5条
var query = {_id: {'$in': topic_ids}};
var opt = {};
TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_replies', function (recent_replies) {
recent_replies = _.sortBy(recent_replies, function (topic) {
return topic_ids.indexOf(topic._id.toString())
});
return recent_replies;
}));
}));
ep.all('recent_topics', 'recent_replies',
function (recent_topics, recent_replies) {
user = _.pick(user, ['loginname', 'avatar_url', 'githubUsername',
'create_at', 'score']);
user.recent_topics = recent_topics.map(function (topic) {
topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']);
return topic;
});
user.recent_replies = recent_replies.map(function (topic) {
topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']);
return topic;
});
res.send({success: true, data: user});
});
}));
};
exports.show = show;
================================================
FILE: api_router_v1.js
================================================
var express = require('express');
var topicController = require('./api/v1/topic');
var topicCollectController = require('./api/v1/topic_collect');
var userController = require('./api/v1/user');
var toolsController = require('./api/v1/tools');
var replyController = require('./api/v1/reply');
var messageController = require('./api/v1/message');
var middleware = require('./api/v1/middleware');
var limit = require('./middlewares/limit');
var config = require('./config');
var router = express.Router();
// 主题
router.get('/topics', topicController.index);
router.get('/topic/:id', middleware.tryAuth, topicController.show);
router.post('/topics', middleware.auth, limit.peruserperday('create_topic', config.create_post_per_day, {showJson: true}), topicController.create);
router.post('/topics/update', middleware.auth, topicController.update);
// 主题收藏
router.post('/topic_collect/collect', middleware.auth, topicCollectController.collect); // 关注某话题
router.post('/topic_collect/de_collect', middleware.auth, topicCollectController.de_collect); // 取消关注某话题
router.get('/topic_collect/:loginname', topicCollectController.list);
// 用户
router.get('/user/:loginname', userController.show);
// accessToken 测试
router.post('/accesstoken', middleware.auth, toolsController.accesstoken);
// 评论
router.post('/topic/:topic_id/replies', middleware.auth, limit.peruserperday('create_reply', config.create_reply_per_day, {showJson: true}), replyController.create);
router.post('/reply/:reply_id/ups', middleware.auth, replyController.ups);
// 通知
router.get('/messages', middleware.auth, messageController.index);
router.get('/message/count', middleware.auth, messageController.count);
router.post('/message/mark_all', middleware.auth, messageController.markAll);
router.post('/message/mark_one/:msg_id', middleware.auth, messageController.markOne);
module.exports = router;
================================================
FILE: app.js
================================================
/*!
* nodeclub - app.js
*/
/**
* Module dependencies.
*/
var config = require('./config');
if (!config.debug && config.oneapm_key) {
require('oneapm');
}
require('colors');
var path = require('path');
var Loader = require('loader');
var LoaderConnect = require('loader-connect')
var express = require('express');
var session = require('express-session');
var passport = require('passport');
require('./middlewares/mongoose_log'); // 打印 mongodb 查询日志
require('./models');
var GitHubStrategy = require('passport-github').Strategy;
var githubStrategyMiddleware = require('./middlewares/github_strategy');
var webRouter = require('./web_router');
var apiRouterV1 = require('./api_router_v1');
var auth = require('./middlewares/auth');
var errorPageMiddleware = require('./middlewares/error_page');
var proxyMiddleware = require('./middlewares/proxy');
var RedisStore = require('connect-redis')(session);
var _ = require('lodash');
var csurf = require('csurf');
var compress = require('compression');
var bodyParser = require('body-parser');
var busboy = require('connect-busboy');
var errorhandler = require('errorhandler');
var cors = require('cors');
var requestLog = require('./middlewares/request_log');
var renderMiddleware = require('./middlewares/render');
var logger = require('./common/logger');
var helmet = require('helmet');
var bytes = require('bytes')
// 静态文件目录
var staticDir = path.join(__dirname, 'public');
// assets
var assets = {};
if (config.mini_assets) {
try {
assets = require('./assets.json');
} catch (e) {
logger.error('You must execute `make build` before start app when mini_assets is true.');
throw e;
}
}
var urlinfo = require('url').parse(config.host);
config.hostname = urlinfo.hostname || config.host;
var app = express();
// configuration in all env
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');
app.engine('html', require('ejs-mate'));
app.locals._layoutFile = 'layout.html';
app.enable('trust proxy');
// Request logger。请求时间
app.use(requestLog);
if (config.debug) {
// 渲染时间
app.use(renderMiddleware.render);
}
// 静态资源
if (config.debug) {
app.use(LoaderConnect.less(__dirname)); // 测试环境用,编译 .less on the fly
}
app.use('/public', express.static(staticDir));
app.use('/agent', proxyMiddleware.proxy);
// 通用的中间件
app.use(require('response-time')());
app.use(helmet.frameguard('sameorigin'));
app.use(bodyParser.json({limit: '1mb'}));
app.use(bodyParser.urlencoded({ extended: true, limit: '1mb' }));
app.use(require('method-override')());
app.use(require('cookie-parser')(config.session_secret));
app.use(compress());
app.use(session({
secret: config.session_secret,
store: new RedisStore({
port: config.redis_port,
host: config.redis_host,
db: config.redis_db,
pass: config.redis_password,
}),
resave: false,
saveUninitialized: false,
}));
// oauth 中间件
app.use(passport.initialize());
// github oauth
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use(new GitHubStrategy(config.GITHUB_OAUTH, githubStrategyMiddleware));
// custom middleware
app.use(auth.authUser);
app.use(auth.blockUser());
if (!config.debug) {
app.use(function (req, res, next) {
if (req.path === '/api' || req.path.indexOf('/api') === -1) {
csurf()(req, res, next);
return;
}
next();
});
app.set('view cache', true);
}
// for debug
// app.get('/err', function (req, res, next) {
// next(new Error('haha'))
// });
// set static, dynamic helpers
_.extend(app.locals, {
config: config,
Loader: Loader,
assets: assets
});
app.use(errorPageMiddleware.errorPage);
_.extend(app.locals, require('./common/render_helper'));
app.use(function (req, res, next) {
res.locals.csrf = req.csrfToken ? req.csrfToken() : '';
next();
});
app.use(busboy({
limits: {
fileSize: bytes(config.file_limit)
}
}));
// routes
app.use('/api/v1', cors(), apiRouterV1);
app.use('/', webRouter);
// error handler
if (config.debug) {
app.use(errorhandler());
} else {
app.use(function (err, req, res, next) {
logger.error(err);
return res.status(500).send('500 status');
});
}
if (!module.parent) {
app.listen(config.port, function () {
logger.info('NodeClub listening on port', config.port);
logger.info('God bless love....');
logger.info('You can debug your app with http://' + config.hostname + ':' + config.port);
logger.info('');
});
}
module.exports = app;
================================================
FILE: bin/fix_at_problem.js
================================================
// 一次性脚本
// 修复之前重复编辑帖子会导致重复 @someone 的渲染问题
var TopicModel = require('../models').Topic;
TopicModel.find({content: /\[{2,}@/}).exec(function (err, topics) {
topics.forEach(function (topic) {
topic.content = fix(topic.content);
console.log(topic.id);
topic.save();
});
});
function fix(str) {
str = str.replace(/\[{1,}(\[@\w+)(\]\(.+?\))\2+/, function (match_text, $1, $2) {
return $1 + $2;
});
return str;
}
================================================
FILE: bin/fix_topic_collect_count.js
================================================
var TopicCollect = require('../models').TopicCollect;
var UserModel = require('../models').User;
var TopicModel = require('../models').Topic
// 修复用户的topic_collect计数
TopicCollect.aggregate(
[{
"$group" :
{
_id : {user_id: "$user_id"},
count : { $sum : 1}
}
}], function (err, result) {
result.forEach(function (row) {
var userId = row._id.user_id;
var count = row.count;
UserModel.findOne({
_id: userId
}, function (err, user) {
if (!user) {
return;
}
user.collect_topic_count = count;
user.save(function () {
console.log(user.loginname, count)
});
})
})
})
// 修复帖子的topic_collect计数
TopicCollect.aggregate(
[{
"$group" :
{
_id : {topic_id: "$topic_id"},
count : { $sum : 1}
}
}], function (err, result) {
result.forEach(function (row) {
var topic_id = row._id.topic_id;
var count = row.count;
TopicModel.findOne({
_id: topic_id
}, function (err, topic) {
if (!topic) {
return;
}
topic.collect_topic_count = count;
topic.save(function () {
console.log(topic.id, count)
});
})
})
})
================================================
FILE: bin/generate_accesstoken.js
================================================
// 一次性脚本
// 为所有老用户生成 accessToken
var uuid = require('node-uuid');
var mongoose = require('mongoose');
var config = require('../config');
var async = require('async');
require('../models/user');
mongoose.connect(config.db, function (err) {
if (err) {
console.error('connect to %s error: ', config.db, err.message);
process.exit(1);
}
});
var UserModel = mongoose.model('User');
var hasRemain = true;
async.whilst(
function () {
return hasRemain;
},
function (callback) {
UserModel.findOne({accessToken: {$exists: false}}, function (err, user) {
if (!user) {
hasRemain = false;
callback();
return;
}
user.accessToken = uuid.v4();
user.save(function () {
console.log(user.loginname + ' done!');
callback();
});
});
},
function (err) {
mongoose.disconnect();
});
================================================
FILE: bin/get_user_topics.js
================================================
var UserModel = require('../models').User;
var TopicModel = require('../models').Topic
// usage:
// node get_user_topics.js alsotang
UserModel.findOne({
loginname: process.argv[2]
}, function (err, user) {
TopicModel.find({
author_id: user._id
}, function (err, topics) {
console.log(topics)
})
})
================================================
FILE: common/at.js
================================================
/*!
* nodeclub - topic mention user controller.
* Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>
* Copyright(c) 2012 muyuan
* MIT Licensed
*/
/**
* Module dependencies.
*/
var User = require('../proxy').User;
var Message = require('./message');
var EventProxy = require('eventproxy');
var _ = require('lodash');
/**
* 从文本中提取出@username 标记的用户名数组
* @param {String} text 文本内容
* @return {Array} 用户名数组
*/
var fetchUsers = function (text) {
if (!text) {
return [];
}
var ignoreRegexs = [
/```.+?```/g, // 去除单行的 ```
/^```[\s\S]+?^```/gm, // ``` 里面的是 pre 标签内容
/`[\s\S]+?`/g, // 同一行中,`some code` 中内容也不该被解析
/^ .*/gm, // 4个空格也是 pre 标签,在这里 . 不会匹配换行
/\b\S*?@[^\s]*?\..+?\b/g, // somebody@gmail.com 会被去除
/\[@.+?\]\(\/.+?\)/g, // 已经被 link 的 username
/\/@/g, // 一般是url中path的一部分
];
ignoreRegexs.forEach(function (ignore_regex) {
text = text.replace(ignore_regex, '');
});
var results = text.match(/@[a-z0-9\-_]+\b/igm);
var names = [];
if (results) {
for (var i = 0, l = results.length; i < l; i++) {
var s = results[i];
//remove leading char @
s = s.slice(1);
names.push(s);
}
}
names = _.uniq(names);
return names;
};
exports.fetchUsers = fetchUsers;
/**
* 根据文本内容中读取用户,并发送消息给提到的用户
* Callback:
* - err, 数据库异常
* @param {String} text 文本内容
* @param {String} topicId 主题ID
* @param {String} authorId 作者ID
* @param {String} reply_id 回复ID
* @param {Function} callback 回调函数
*/
exports.sendMessageToMentionUsers = function (text, topicId, authorId, reply_id, callback) {
if (typeof reply_id === 'function') {
callback = reply_id;
reply_id = null;
}
callback = callback || _.noop;
User.getUsersByNames(fetchUsers(text), function (err, users) {
if (err || !users) {
return callback(err);
}
var ep = new EventProxy();
ep.fail(callback);
users = users.filter(function (user) {
return !user._id.equals(authorId);
});
ep.after('sent', users.length, function () {
callback();
});
users.forEach(function (user) {
Message.sendAtMessage(user._id, authorId, topicId, reply_id, ep.done('sent'));
});
});
};
/**
* 根据文本内容,替换为数据库中的数据
* Callback:
* - err, 数据库异常
* - text, 替换后的文本内容
* @param {String} text 文本内容
* @param {Function} callback 回调函数
*/
exports.linkUsers = function (text, callback) {
var users = fetchUsers(text);
for (var i = 0, l = users.length; i < l; i++) {
var name = users[i];
text = text.replace(new RegExp('@' + name + '\\b(?!\\])', 'g'), '[@' + name + '](/user/' + name + ')');
}
if (!callback) {
return text;
}
return callback(null, text);
};
================================================
FILE: common/cache.js
================================================
var redis = require('./redis');
var _ = require('lodash');
var logger = require('./logger');
var get = function (key, callback) {
var t = new Date();
redis.get(key, function (err, data) {
if (err) {
return callback(err);
}
if (!data) {
return callback();
}
data = JSON.parse(data);
var duration = (new Date() - t);
logger.debug('Cache', 'get', key, (duration + 'ms').green);
callback(null, data);
});
};
exports.get = get;
// time 参数可选,秒为单位
var set = function (key, value, time, callback) {
var t = new Date();
if (typeof time === 'function') {
callback = time;
time = null;
}
callback = callback || _.noop;
value = JSON.stringify(value);
if (!time) {
redis.set(key, value, callback);
} else {
redis.setex(key, time, value, callback);
}
var duration = (new Date() - t);
logger.debug("Cache", "set", key, (duration + 'ms').green);
};
exports.set = set;
================================================
FILE: common/logger.js
================================================
var config = require('../config');
var pathLib = require('path')
var env = process.env.NODE_ENV || "development"
var log4js = require('log4js');
log4js.configure({
appenders: [
{ type: 'console' },
{ type: 'file', filename: pathLib.join(config.log_dir, 'cheese.log'), category: 'cheese' }
]
});
var logger = log4js.getLogger('cheese');
logger.setLevel(config.debug && env !== 'test' ? 'DEBUG' : 'ERROR')
module.exports = logger;
================================================
FILE: common/mail.js
================================================
var mailer = require('nodemailer');
var smtpTransport = require('nodemailer-smtp-transport');
var config = require('../config');
var util = require('util');
var logger = require('./logger');
var transporter = mailer.createTransport(smtpTransport(config.mail_opts));
var SITE_ROOT_URL = 'http://' + config.host;
var async = require('async')
/**
* Send an email
* @param {Object} data 邮件对象
*/
var sendMail = function (data) {
if (config.debug) {
return;
}
// 重试5次
async.retry({times: 5}, function (done) {
transporter.sendMail(data, function (err) {
if (err) {
// 写为日志
logger.error('send mail error', err, data);
return done(err);
}
return done()
});
}, function (err) {
if (err) {
return logger.error('send mail finally error', err, data);
}
logger.info('send mail success', data)
})
};
exports.sendMail = sendMail;
/**
* 发送激活通知邮件
* @param {String} who 接收人的邮件地址
* @param {String} token 重置用的token字符串
* @param {String} name 接收人的用户名
*/
exports.sendActiveMail = function (who, token, name) {
var from = util.format('%s <%s>', config.name, config.mail_opts.auth.user);
var to = who;
var subject = config.name + '社区帐号激活';
var html = '<p>您好:' + name + '</p>' +
'<p>我们收到您在' + config.name + '社区的注册信息,请点击下面的链接来激活帐户:</p>' +
'<a href = "' + SITE_ROOT_URL + '/active_account?key=' + token + '&name=' + name + '">激活链接</a>' +
'<p>若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。</p>' +
'<p>' + config.name + '社区 谨上。</p>';
exports.sendMail({
from: from,
to: to,
subject: subject,
html: html
});
};
/**
* 发送密码重置通知邮件
* @param {String} who 接收人的邮件地址
* @param {String} token 重置用的token字符串
* @param {String} name 接收人的用户名
*/
exports.sendResetPassMail = function (who, token, name) {
var from = util.format('%s <%s>', config.name, config.mail_opts.auth.user);
var to = who;
var subject = config.name + '社区密码重置';
var html = '<p>您好:' + name + '</p>' +
'<p>我们收到您在' + config.name + '社区重置密码的请求,请在24小时内单击下面的链接来重置密码:</p>' +
'<a href="' + SITE_ROOT_URL + '/reset_pass?key=' + token + '&name=' + name + '">重置密码链接</a>' +
'<p>若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。</p>' +
'<p>' + config.name + '社区 谨上。</p>';
exports.sendMail({
from: from,
to: to,
subject: subject,
html: html
});
};
================================================
FILE: common/message.js
================================================
var models = require('../models');
var eventproxy = require('eventproxy');
var Message = models.Message;
var User = require('../proxy').User;
var messageProxy = require('../proxy/message');
var _ = require('lodash');
exports.sendReplyMessage = function (master_id, author_id, topic_id, reply_id, callback) {
callback = callback || _.noop;
var ep = new eventproxy();
ep.fail(callback);
var message = new Message();
message.type = 'reply';
message.master_id = master_id;
message.author_id = author_id;
message.topic_id = topic_id;
message.reply_id = reply_id;
message.save(ep.done('message_saved'));
ep.all('message_saved', function (msg) {
callback(null, msg);
});
};
exports.sendAtMessage = function (master_id, author_id, topic_id, reply_id, callback) {
callback = callback || _.noop;
var ep = new eventproxy();
ep.fail(callback);
var message = new Message();
message.type = 'at';
message.master_id = master_id;
message.author_id = author_id;
message.topic_id = topic_id;
message.reply_id = reply_id;
message.save(ep.done('message_saved'));
ep.all('message_saved', function (msg) {
callback(null, msg);
});
};
================================================
FILE: common/redis.js
================================================
var config = require('../config');
var Redis = require('ioredis');
var logger = require('./logger')
var client = new Redis({
port: config.redis_port,
host: config.redis_host,
db: config.redis_db,
password: config.redis_password,
});
client.on('error', function (err) {
if (err) {
logger.error('connect to redis error, check your redis config', err);
process.exit(1);
}
})
exports = module.exports = client;
================================================
FILE: common/render_helper.js
================================================
/*!
* nodeclub - common/render_helpers.js
* Copyright(c) 2013 fengmk2 <fengmk2@gmail.com>
* MIT Licensed
*/
"use strict";
/**
* Module dependencies.
*/
var MarkdownIt = require('markdown-it');
var _ = require('lodash');
var config = require('../config');
var validator = require('validator');
var jsxss = require('xss');
var multiline = require('multiline')
// Set default options
var md = new MarkdownIt();
md.set({
html: false, // Enable HTML tags in source
xhtmlOut: false, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
linkify: true, // Autoconvert URL-like text to links
typographer: true, // Enable smartypants and other sweet transforms
});
md.renderer.rules.fence = function (tokens, idx) {
var token = tokens[idx];
var language = token.info && ('language-' + token.info) || '';
language = validator.escape(language);
return '<pre class="prettyprint ' + language + '">'
+ '<code>' + validator.escape(token.content) + '</code>'
+ '</pre>';
};
md.renderer.rules.code_block = function (tokens, idx /*, options*/) {
var token = tokens[idx];
return '<pre class="prettyprint">'
+ '<code>' + validator.escape(token.content) + '</code>'
+ '</pre>';
};
var myxss = new jsxss.FilterXSS({
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
// 让 prettyprint 可以工作
if (tag === 'pre' && name === 'class') {
return name + '="' + jsxss.escapeAttrValue(value) + '"';
}
}
});
exports.markdown = function (text) {
return '<div class="markdown-text">' + myxss.process(md.render(text || '')) + '</div>';
};
exports.escapeSignature = function (signature) {
return signature.split('\n').map(function (p) {
return _.escape(p);
}).join('<br>');
};
exports.staticFile = function (filePath) {
if (filePath.indexOf('http') === 0 || filePath.indexOf('//') === 0) {
return filePath;
}
return config.site_static_host + filePath;
};
exports.tabName = function (tab) {
var pair = _.find(config.tabs, function (pair) {
return pair[0] === tab;
});
if (pair) {
return pair[1];
}
};
exports.proxy = function (url) {
return url;
// 当 google 和 github 封锁严重时,则需要通过服务器代理访问它们的静态资源
// return '/agent?url=' + encodeURIComponent(url);
};
// 为了在 view 中使用
exports._ = _;
exports.multiline = multiline;
================================================
FILE: common/store.js
================================================
var qn = require('./store_qn');
var local = require('./store_local');
module.exports = qn || local;
================================================
FILE: common/store_local.js
================================================
var config = require('../config');
var utility = require('utility');
var path = require('path');
var fs = require('fs');
exports.upload = function (file, options, callback) {
var filename = options.filename;
var newFilename = utility.md5(filename + String((new Date()).getTime())) +
path.extname(filename);
var upload_path = config.upload.path;
var base_url = config.upload.url;
var filePath = path.join(upload_path, newFilename);
var fileUrl = base_url + newFilename;
file.on('end', function () {
callback(null, {
url: fileUrl
});
});
file.pipe(fs.createWriteStream(filePath));
};
================================================
FILE: common/store_qn.js
================================================
var qn = require('qn');
var config = require('../config');
//7牛 client
var qnClient = null;
if (config.qn_access && config.qn_access.secretKey !== 'your secret key') {
qnClient = qn.create(config.qn_access);
}
module.exports = qnClient;
================================================
FILE: common/tools.js
================================================
var bcrypt = require('bcryptjs');
var moment = require('moment');
moment.locale('zh-cn'); // 使用中文
// 格式化时间
exports.formatDate = function (date, friendly) {
date = moment(date);
if (friendly) {
return date.fromNow();
} else {
return date.format('YYYY-MM-DD HH:mm');
}
};
exports.validateId = function (str) {
return (/^[a-zA-Z0-9\-_]+$/i).test(str);
};
exports.bhash = function (str, callback) {
bcrypt.hash(str, 10, callback);
};
exports.bcompare = function (str, hash, callback) {
bcrypt.compare(str, hash, callback);
};
================================================
FILE: config.default.js
================================================
/**
* config
*/
var path = require('path');
var config = {
// debug 为 true 时,用于本地调试
debug: true,
get mini_assets() { return !this.debug; }, // 是否启用静态文件的合并压缩,详见视图中的Loader
name: 'Nodeclub', // 社区名字
description: 'CNode:Node.js专业中文社区', // 社区的描述
keywords: 'nodejs, node, express, connect, socket.io',
// 添加到 html head 中的信息
site_headers: [
'<meta name="author" content="EDP@TAOBAO" />'
],
site_logo: '/public/images/cnodejs_light.svg', // default is `name`
site_icon: '/public/images/cnode_icon_32.png', // 默认没有 favicon, 这里填写网址
// 右上角的导航区
site_navs: [
// 格式 [ path, title, [target=''] ]
[ '/about', '关于' ]
],
// cdn host,如 http://cnodejs.qiniudn.com
site_static_host: '', // 静态文件存储域名
// 社区的域名
host: 'localhost',
// 默认的Google tracker ID,自有站点请修改,申请地址:http://www.google.com/analytics/
google_tracker_id: '',
// 默认的cnzz tracker ID,自有站点请修改
cnzz_tracker_id: '',
// mongodb 配置
db: 'mongodb://127.0.0.1/node_club_dev',
// redis 配置,默认是本地
redis_host: '127.0.0.1',
redis_port: 6379,
redis_db: 0,
redis_password: '',
session_secret: 'node_club_secret', // 务必修改
auth_cookie_name: 'node_club',
// 程序运行的端口
port: 3000,
// 话题列表显示的话题数量
list_topic_count: 20,
// RSS配置
rss: {
title: 'CNode:Node.js专业中文社区',
link: 'http://cnodejs.org',
language: 'zh-cn',
description: 'CNode:Node.js专业中文社区',
//最多获取的RSS Item数量
max_rss_items: 50
},
log_dir: path.join(__dirname, 'logs'),
// 邮箱配置
mail_opts: {
host: 'smtp.126.com',
port: 25,
auth: {
user: 'club@126.com',
pass: 'club'
},
ignoreTLS: true,
},
//weibo app key
weibo_key: 10000000,
weibo_id: 'your_weibo_id',
// admin 可删除话题,编辑标签。把 user_login_name 换成你的登录名
admins: { user_login_name: true },
// github 登陆的配置
GITHUB_OAUTH: {
clientID: 'your GITHUB_CLIENT_ID',
clientSecret: 'your GITHUB_CLIENT_SECRET',
callbackURL: 'http://cnodejs.org/auth/github/callback'
},
// 是否允许直接注册(否则只能走 github 的方式)
allow_sign_up: true,
// oneapm 是个用来监控网站性能的服务
oneapm_key: '',
// 下面两个配置都是文件上传的配置
// 7牛的access信息,用于文件上传
qn_access: {
accessKey: 'your access key',
secretKey: 'your secret key',
bucket: 'your bucket name',
origin: 'http://your qiniu domain',
// 如果vps在国外,请使用 http://up.qiniug.com/ ,这是七牛的国际节点
// 如果在国内,此项请留空
uploadURL: 'http://xxxxxxxx',
},
// 文件上传配置
// 注:如果填写 qn_access,则会上传到 7牛,以下配置无效
upload: {
path: path.join(__dirname, 'public/upload/'),
url: '/public/upload/'
},
file_limit: '1MB',
// 版块
tabs: [
['share', '分享'],
['ask', '问答'],
['job', '招聘'],
],
// 极光推送
jpush: {
appKey: 'YourAccessKeyyyyyyyyyyyy',
masterSecret: 'YourSecretKeyyyyyyyyyyyyy',
isDebug: false,
},
create_post_per_day: 1000, // 每个用户一天可以发的主题数
create_reply_per_day: 1000, // 每个用户一天可以发的评论数
create_user_per_ip: 1000, // 每个 ip 每天可以注册账号的次数
visit_per_day: 1000, // 每个 ip 每天能访问的次数
};
if (process.env.NODE_ENV === 'test') {
config.db = 'mongodb://127.0.0.1/node_club_test';
}
module.exports = config;
================================================
FILE: controllers/github.js
================================================
var Models = require('../models');
var User = Models.User;
var authMiddleWare = require('../middlewares/auth');
var tools = require('../common/tools');
var eventproxy = require('eventproxy');
var uuid = require('node-uuid');
var validator = require('validator');
exports.callback = function (req, res, next) {
var profile = req.user;
var email = profile.emails && profile.emails[0] && profile.emails[0].value;
if (!email) {
return res.status(500)
.render('sign/no_github_email');
}
User.findOne({githubId: profile.id}, function (err, user) {
if (err) {
return next(err);
}
// 当用户已经是 cnode 用户时,通过 github 登陆将会更新他的资料
if (user) {
user.githubUsername = profile.username;
user.githubId = profile.id;
user.githubAccessToken = profile.accessToken;
// user.loginname = profile.username;
user.avatar = profile._json.avatar_url;
user.email = email || user.email;
user.save(function (err) {
if (err) {
// 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
if (err.message.indexOf('duplicate key error') !== -1) {
if (err.message.indexOf('loginname') !== -1) {
return res.status(500)
.send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了');
}
}
return next(err);
}
authMiddleWare.gen_session(user, res);
return res.redirect('/');
});
} else {
// 如果用户还未存在,则建立新用户
req.session.profile = profile;
return res.redirect('/auth/github/new');
}
});
};
exports.new = function (req, res, next) {
res.render('sign/new_oauth', {actionPath: '/auth/github/create'});
};
exports.create = function (req, res, next) {
var profile = req.session.profile;
var isnew = req.body.isnew;
var loginname = validator.trim(req.body.name || '').toLowerCase();
var password = validator.trim(req.body.pass || '');
var ep = new eventproxy();
ep.fail(next);
if (!profile) {
return res.redirect('/signin');
}
delete req.session.profile;
var email = profile.emails && profile.emails[0] && profile.emails[0].value;
if (!email) {
return res.status(500)
.render('sign/no_github_email');
}
if (isnew) { // 注册新账号
var user = new User({
loginname: profile.username,
pass: profile.accessToken,
email: email,
avatar: profile._json.avatar_url,
githubId: profile.id,
githubUsername: profile.username,
githubAccessToken: profile.accessToken,
active: true,
accessToken: uuid.v4(),
});
user.save(function (err) {
if (err) {
// 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
if (err.message.indexOf('duplicate key error') !== -1) {
if (err.message.indexOf('loginname') !== -1) {
return res.status(500)
.send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了');
}
}
return next(err);
// END 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
}
authMiddleWare.gen_session(user, res);
res.redirect('/');
});
} else { // 关联老账号
ep.on('login_error', function (login_error) {
res.status(403);
res.render('sign/signin', { error: '账号名或密码错误。' });
});
User.findOne({loginname: loginname},
ep.done(function (user) {
if (!user) {
return ep.emit('login_error');
}
tools.bcompare(password, user.pass, ep.done(function (bool) {
if (!bool) {
return ep.emit('login_error');
}
user.githubUsername = profile.username;
user.githubId = profile.id;
// user.loginname = profile.username;
user.avatar = profile._json.avatar_url;
user.githubAccessToken = profile.accessToken;
user.save(function (err) {
if (err) {
return next(err);
}
authMiddleWare.gen_session(user, res);
res.redirect('/');
});
}));
}));
}
};
================================================
FILE: controllers/message.js
================================================
var Message = require('../proxy').Message;
var eventproxy = require('eventproxy');
exports.index = function (req, res, next) {
var user_id = req.session.user._id;
var ep = new eventproxy();
ep.fail(next);
ep.all('has_read_messages', 'hasnot_read_messages', function (has_read_messages, hasnot_read_messages) {
res.render('message/index', {has_read_messages: has_read_messages, hasnot_read_messages: hasnot_read_messages});
});
ep.all('has_read', 'unread', function (has_read, unread) {
[has_read, unread].forEach(function (msgs, idx) {
var epfill = new eventproxy();
epfill.fail(next);
epfill.after('message_ready', msgs.length, function (docs) {
docs = docs.filter(function (doc) {
return !doc.is_invalid;
});
ep.emit(idx === 0 ? 'has_read_messages' : 'hasnot_read_messages', docs);
});
msgs.forEach(function (doc) {
Message.getMessageRelations(doc, epfill.group('message_ready'));
});
});
Message.updateMessagesToRead(user_id, unread);
});
Message.getReadMessagesByUserId(user_id, ep.done('has_read'));
Message.getUnreadMessageByUserId(user_id, ep.done('unread'));
};
================================================
FILE: controllers/reply.js
================================================
var validator = require('validator');
var _ = require('lodash');
var at = require('../common/at');
var message = require('../common/message');
var EventProxy = require('eventproxy');
var User = require('../proxy').User;
var Topic = require('../proxy').Topic;
var Reply = require('../proxy').Reply;
var config = require('../config');
/**
* 添加回复
*/
exports.add = function (req, res, next) {
var content = req.body.r_content;
var topic_id = req.params.topic_id;
var reply_id = req.body.reply_id;
var str = validator.trim(String(content));
if (str === '') {
return res.renderError('回复内容不能为空!', 422);
}
var ep = EventProxy.create();
ep.fail(next);
Topic.getTopic(topic_id, ep.doneLater(function (topic) {
if (!topic) {
ep.unbind();
// just 404 page
return next();
}
if (topic.lock) {
return res.status(403).send('此主题已锁定。');
}
ep.emit('topic', topic);
}));
ep.all('topic', function (topic) {
User.getUserById(topic.author_id, ep.done('topic_author'));
});
ep.all('topic', 'topic_author', function (topic, topicAuthor) {
Reply.newAndSave(content, topic_id, req.session.user._id, reply_id, ep.done(function (reply) {
Topic.updateLastReply(topic_id, reply._id, ep.done(function () {
ep.emit('reply_saved', reply);
//发送at消息,并防止重复 at 作者
var newContent = content.replace('@' + topicAuthor.loginname + ' ', '');
at.sendMessageToMentionUsers(newContent, topic_id, req.session.user._id, reply._id);
}));
}));
User.getUserById(req.session.user._id, ep.done(function (user) {
user.score += 5;
user.reply_count += 1;
user.save();
req.session.user = user;
ep.emit('score_saved');
}));
});
ep.all('reply_saved', 'topic', function (reply, topic) {
if (topic.author_id.toString() !== req.session.user._id.toString()) {
message.sendReplyMessage(topic.author_id, req.session.user._id, topic._id, reply._id);
}
ep.emit('message_saved');
});
ep.all('reply_saved', 'message_saved', 'score_saved', function (reply) {
res.redirect('/topic/' + topic_id + '#' + reply._id);
});
};
/**
* 删除回复信息
*/
exports.delete = function (req, res, next) {
var reply_id = req.body.reply_id;
Reply.getReplyById(reply_id, function (err, reply) {
if (err) {
return next(err);
}
if (!reply) {
res.status(422);
res.json({status: 'no reply ' + reply_id + ' exists'});
return;
}
if (reply.author_id.toString() === req.session.user._id.toString() || req.session.user.is_admin) {
reply.deleted = true;
reply.save();
res.json({status: 'success'});
reply.author.score -= 5;
reply.author.reply_count -= 1;
reply.author.save();
} else {
res.json({status: 'failed'});
return;
}
Topic.reduceCount(reply.topic_id, _.noop);
});
};
/*
打开回复编辑器
*/
exports.showEdit = function (req, res, next) {
var reply_id = req.params.reply_id;
Reply.getReplyById(reply_id, function (err, reply) {
if (!reply) {
return res.render404('此回复不存在或已被删除。');
}
if (req.session.user._id.equals(reply.author_id) || req.session.user.is_admin) {
res.render('reply/edit', {
reply_id: reply._id,
content: reply.content
});
} else {
return res.renderError('对不起,你不能编辑此回复。', 403);
}
});
};
/*
提交编辑回复
*/
exports.update = function (req, res, next) {
var reply_id = req.params.reply_id;
var content = req.body.t_content;
Reply.getReplyById(reply_id, function (err, reply) {
if (!reply) {
return res.render404('此回复不存在或已被删除。');
}
if (String(reply.author_id) === req.session.user._id.toString() || req.session.user.is_admin) {
if (content.trim().length > 0) {
reply.content = content;
reply.update_at = new Date();
reply.save(function (err) {
if (err) {
return next(err);
}
res.redirect('/topic/' + reply.topic_id + '#' + reply._id);
});
} else {
return res.renderError('回复的字数太少。', 400);
}
} else {
return res.renderError('对不起,你不能编辑此回复。', 403);
}
});
};
exports.up = function (req, res, next) {
var replyId = req.params.reply_id;
var userId = req.session.user._id;
Reply.getReplyById(replyId, function (err, reply) {
if (err) {
return next(err);
}
if (reply.author_id.equals(userId) && !config.debug) {
// 不能帮自己点赞
res.send({
success: false,
message: '呵呵,不能帮自己点赞。',
});
} else {
var action;
reply.ups = reply.ups || [];
var upIndex = reply.ups.indexOf(userId);
if (upIndex === -1) {
reply.ups.push(userId);
action = 'up';
} else {
reply.ups.splice(upIndex, 1);
action = 'down';
}
reply.save(function () {
res.send({
success: true,
action: action
});
});
}
});
};
================================================
FILE: controllers/rss.js
================================================
var config = require('../config');
var convert = require('data2xml')();
var Topic = require('../proxy').Topic;
var cache = require('../common/cache');
var renderHelper = require('../common/render_helper');
var eventproxy = require('eventproxy');
exports.index = function (req, res, next) {
if (!config.rss) {
res.statusCode = 404;
return res.send('Please set `rss` in config.js');
}
res.contentType('application/xml');
var ep = new eventproxy();
ep.fail(next);
cache.get('rss', ep.done(function (rss) {
if (!config.debug && rss) {
res.send(rss);
} else {
var opt = {
limit: config.rss.max_rss_items,
sort: '-create_at',
};
Topic.getTopicsByQuery({tab: {$nin: ['dev']}}, opt, function (err, topics) {
if (err) {
return next(err);
}
var rss_obj = {
_attr: { version: '2.0' },
channel: {
title: config.rss.title,
link: config.rss.link,
language: config.rss.language,
description: config.rss.description,
item: []
}
};
topics.forEach(function (topic) {
rss_obj.channel.item.push({
title: topic.title,
link: config.rss.link + '/topic/' + topic._id,
guid: config.rss.link + '/topic/' + topic._id,
description: renderHelper.markdown(topic.content),
author: topic.author.loginname,
pubDate: topic.create_at.toUTCString()
});
});
var rssContent = convert('rss', rss_obj);
rssContent = utf8ForXml(rssContent)
cache.set('rss', rssContent, 60 * 5); // 五分钟
res.send(rssContent);
});
}
}));
};
function utf8ForXml(inputStr) {
return inputStr.replace(/[^\x09\x0A\x0D\x20-\xFF\x85\xA0-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm, '');
}
================================================
FILE: controllers/search.js
================================================
exports.index = function (req, res, next) {
var q = req.query.q;
q = encodeURIComponent(q);
res.redirect('https://www.google.com.hk/search?q=site:cnodejs.org+' + q);
};
================================================
FILE: controllers/sign.js
================================================
var validator = require('validator');
var eventproxy = require('eventproxy');
var config = require('../config');
var User = require('../proxy').User;
var mail = require('../common/mail');
var tools = require('../common/tools');
var utility = require('utility');
var authMiddleWare = require('../middlewares/auth');
var uuid = require('node-uuid');
//sign up
exports.showSignup = function (req, res) {
res.render('sign/signup');
};
exports.signup = function (req, res, next) {
var loginname = validator.trim(req.body.loginname).toLowerCase();
var email = validator.trim(req.body.email).toLowerCase();
var pass = validator.trim(req.body.pass);
var rePass = validator.trim(req.body.re_pass);
var ep = new eventproxy();
ep.fail(next);
ep.on('prop_err', function (msg) {
res.status(422);
res.render('sign/signup', {error: msg, loginname: loginname, email: email});
});
// 验证信息的正确性
if ([loginname, pass, rePass, email].some(function (item) { return item === ''; })) {
ep.emit('prop_err', '信息不完整。');
return;
}
if (loginname.length < 5) {
ep.emit('prop_err', '用户名至少需要5个字符。');
return;
}
if (!tools.validateId(loginname)) {
return ep.emit('prop_err', '用户名不合法。');
}
if (!validator.isEmail(email)) {
return ep.emit('prop_err', '邮箱不合法。');
}
if (pass !== rePass) {
return ep.emit('prop_err', '两次密码输入不一致。');
}
// END 验证信息的正确性
User.getUsersByQuery({'$or': [
{'loginname': loginname},
{'email': email}
]}, {}, function (err, users) {
if (err) {
return next(err);
}
if (users.length > 0) {
ep.emit('prop_err', '用户名或邮箱已被使用。');
return;
}
tools.bhash(pass, ep.done(function (passhash) {
// create gravatar
var avatarUrl = User.makeGravatar(email);
User.newAndSave(loginname, loginname, passhash, email, avatarUrl, false, function (err) {
if (err) {
return next(err);
}
// 发送激活邮件
mail.sendActiveMail(email, utility.md5(email + passhash + config.session_secret), loginname);
res.render('sign/signup', {
success: '欢迎加入 ' + config.name + '!我们已给您的注册邮箱发送了一封邮件,请点击里面的链接来激活您的帐号。'
});
});
}));
});
};
/**
* Show user login page.
*
* @param {HttpRequest} req
* @param {HttpResponse} res
*/
exports.showLogin = function (req, res) {
req.session._loginReferer = req.headers.referer;
res.render('sign/signin');
};
/**
* define some page when login just jump to the home page
* @type {Array}
*/
var notJump = [
'/active_account', //active page
'/reset_pass', //reset password page, avoid to reset twice
'/signup', //regist page
'/search_pass' //serch pass page
];
/**
* Handle user login.
*
* @param {HttpRequest} req
* @param {HttpResponse} res
* @param {Function} next
*/
exports.login = function (req, res, next) {
var loginname = validator.trim(req.body.name).toLowerCase();
var pass = validator.trim(req.body.pass);
var ep = new eventproxy();
ep.fail(next);
if (!loginname || !pass) {
res.status(422);
return res.render('sign/signin', { error: '信息不完整。' });
}
var getUser;
if (loginname.indexOf('@') !== -1) {
getUser = User.getUserByMail;
} else {
getUser = User.getUserByLoginName;
}
ep.on('login_error', function (login_error) {
res.status(403);
res.render('sign/signin', { error: '用户名或密码错误' });
});
getUser(loginname, function (err, user) {
if (err) {
return next(err);
}
if (!user) {
return ep.emit('login_error');
}
var passhash = user.pass;
tools.bcompare(pass, passhash, ep.done(function (bool) {
if (!bool) {
return ep.emit('login_error');
}
if (!user.active) {
// 重新发送激活邮件
mail.sendActiveMail(user.email, utility.md5(user.email + passhash + config.session_secret), user.loginname);
res.status(403);
return res.render('sign/signin', { error: '此帐号还没有被激活,激活链接已发送到 ' + user.email + ' 邮箱,请查收。' });
}
// store session cookie
authMiddleWare.gen_session(user, res);
//check at some page just jump to home page
var refer = req.session._loginReferer || '/';
for (var i = 0, len = notJump.length; i !== len; ++i) {
if (refer.indexOf(notJump[i]) >= 0) {
refer = '/';
break;
}
}
res.redirect(refer);
}));
});
};
// sign out
exports.signout = function (req, res, next) {
req.session.destroy();
res.clearCookie(config.auth_cookie_name, { path: '/' });
res.redirect('/');
};
exports.activeAccount = function (req, res, next) {
var key = validator.trim(req.query.key);
var name = validator.trim(req.query.name);
User.getUserByLoginName(name, function (err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(new Error('[ACTIVE_ACCOUNT] no such user: ' + name));
}
var passhash = user.pass;
if (!user || utility.md5(user.email + passhash + config.session_secret) !== key) {
return res.render('notify/notify', {error: '信息有误,帐号无法被激活。'});
}
if (user.active) {
return res.render('notify/notify', {error: '帐号已经是激活状态。'});
}
user.active = true;
user.save(function (err) {
if (err) {
return next(err);
}
res.render('notify/notify', {success: '帐号已被激活,请登录'});
});
});
};
exports.showSearchPass = function (req, res) {
res.render('sign/search_pass');
};
exports.updateSearchPass = function (req, res, next) {
var email = validator.trim(req.body.email).toLowerCase();
if (!validator.isEmail(email)) {
return res.render('sign/search_pass', {error: '邮箱不合法', email: email});
}
// 动态生成retrive_key和timestamp到users collection,之后重置密码进行验证
var retrieveKey = uuid.v4();
var retrieveTime = new Date().getTime();
User.getUserByMail(email, function (err, user) {
if (!user) {
res.render('sign/search_pass', {error: '没有这个电子邮箱。', email: email});
return;
}
user.retrieve_key = retrieveKey;
user.retrieve_time = retrieveTime;
user.save(function (err) {
if (err) {
return next(err);
}
// 发送重置密码邮件
mail.sendResetPassMail(email, retrieveKey, user.loginname);
res.render('notify/notify', {success: '我们已给您填写的电子邮箱发送了一封邮件,请在24小时内点击里面的链接来重置密码。'});
});
});
};
/**
* reset password
* 'get' to show the page, 'post' to reset password
* after reset password, retrieve_key&time will be destroy
* @param {http.req} req
* @param {http.res} res
* @param {Function} next
*/
exports.resetPass = function (req, res, next) {
var key = validator.trim(req.query.key || '');
var name = validator.trim(req.query.name || '');
User.getUserByNameAndKey(name, key, function (err, user) {
if (!user) {
res.status(403);
return res.render('notify/notify', {error: '信息有误,密码无法重置。'});
}
var now = new Date().getTime();
var oneDay = 1000 * 60 * 60 * 24;
if (!user.retrieve_time || now - user.retrieve_time > oneDay) {
res.status(403);
return res.render('notify/notify', {error: '该链接已过期,请重新申请。'});
}
return res.render('sign/reset', {name: name, key: key});
});
};
exports.updatePass = function (req, res, next) {
var psw = validator.trim(req.body.psw) || '';
var repsw = validator.trim(req.body.repsw) || '';
var key = validator.trim(req.body.key) || '';
var name = validator.trim(req.body.name) || '';
var ep = new eventproxy();
ep.fail(next);
if (psw !== repsw) {
return res.render('sign/reset', {name: name, key: key, error: '两次密码输入不一致。'});
}
User.getUserByNameAndKey(name, key, ep.done(function (user) {
if (!user) {
return res.render('notify/notify', {error: '错误的激活链接'});
}
tools.bhash(psw, ep.done(function (passhash) {
user.pass = passhash;
user.retrieve_key = null;
user.retrieve_time = null;
user.active = true; // 用户激活
user.save(function (err) {
if (err) {
return next(err);
}
return res.render('notify/notify', {success: '你的密码已重置。'});
});
}));
}));
};
================================================
FILE: controllers/site.js
================================================
/*!
* nodeclub - site index controller.
* Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>
* Copyright(c) 2012 muyuan
* MIT Licensed
*/
/**
* Module dependencies.
*/
var User = require('../proxy').User;
var Topic = require('../proxy').Topic;
var config = require('../config');
var eventproxy = require('eventproxy');
var cache = require('../common/cache');
var xmlbuilder = require('xmlbuilder');
var renderHelper = require('../common/render_helper');
var _ = require('lodash');
var moment = require('moment');
exports.index = function (req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
page = page > 0 ? page : 1;
var tab = req.query.tab || 'all';
var proxy = new eventproxy();
proxy.fail(next);
// 取主题
var query = {};
if (!tab || tab === 'all') {
query.tab = {$nin: ['job', 'dev']}
} else {
if (tab === 'good') {
query.good = true;
} else {
query.tab = tab;
}
}
if (!query.good) {
query.create_at = {$gte: moment().subtract(1, 'years').toDate()}
}
var limit = config.list_topic_count;
var options = { skip: (page - 1) * limit, limit: limit, sort: '-top -last_reply_at'};
Topic.getTopicsByQuery(query, options, proxy.done('topics', function (topics) {
return topics;
}));
// 取排行榜上的用户
cache.get('tops', proxy.done(function (tops) {
if (tops) {
proxy.emit('tops', tops);
} else {
User.getUsersByQuery(
{is_block: false},
{ limit: 10, sort: '-score'},
proxy.done('tops', function (tops) {
cache.set('tops', tops, 60 * 1);
return tops;
})
);
}
}));
// END 取排行榜上的用户
// 取0回复的主题
cache.get('no_reply_topics', proxy.done(function (no_reply_topics) {
if (no_reply_topics) {
proxy.emit('no_reply_topics', no_reply_topics);
} else {
Topic.getTopicsByQuery(
{ reply_count: 0, tab: {$nin: ['job', 'dev']}},
{ limit: 5, sort: '-create_at'},
proxy.done('no_reply_topics', function (no_reply_topics) {
cache.set('no_reply_topics', no_reply_topics, 60 * 1);
return no_reply_topics;
}));
}
}));
// END 取0回复的主题
// 取分页数据
var pagesCacheKey = JSON.stringify(query) + 'pages';
cache.get(pagesCacheKey, proxy.done(function (pages) {
if (pages) {
proxy.emit('pages', pages);
} else {
Topic.getCountByQuery(query, proxy.done(function (all_topics_count) {
var pages = Math.ceil(all_topics_count / limit);
cache.set(pagesCacheKey, pages, 60 * 1);
proxy.emit('pages', pages);
}));
}
}));
// END 取分页数据
var tabName = renderHelper.tabName(tab);
proxy.all('topics', 'tops', 'no_reply_topics', 'pages',
function (topics, tops, no_reply_topics, pages) {
res.render('index', {
topics: topics,
current_page: page,
list_topic_count: limit,
tops: tops,
no_reply_topics: no_reply_topics,
pages: pages,
tabs: config.tabs,
tab: tab,
pageTitle: tabName && (tabName + '版块'),
});
});
};
exports.sitemap = function (req, res, next) {
var urlset = xmlbuilder.create('urlset',
{version: '1.0', encoding: 'UTF-8'});
urlset.att('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
var ep = new eventproxy();
ep.fail(next);
ep.all('sitemap', function (sitemap) {
res.type('xml');
res.send(sitemap);
});
cache.get('sitemap', ep.done(function (sitemapData) {
if (sitemapData) {
ep.emit('sitemap', sitemapData);
} else {
Topic.getLimit5w(function (err, topics) {
if (err) {
return next(err);
}
topics.forEach(function (topic) {
urlset.ele('url').ele('loc', 'http://cnodejs.org/topic/' + topic._id);
});
var sitemapData = urlset.end();
// 缓存一天
cache.set('sitemap', sitemapData, 3600 * 24);
ep.emit('sitemap', sitemapData);
});
}
}));
};
exports.appDownload = function (req, res, next) {
res.redirect('https://github.com/soliury/noder-react-native/blob/master/README.md')
};
================================================
FILE: controllers/static.js
================================================
var multiline = require('multiline');
// static page
// About
exports.about = function (req, res, next) {
res.render('static/about', {
pageTitle: '关于我们'
});
};
// FAQ
exports.faq = function (req, res, next) {
res.render('static/faq');
};
exports.getstart = function (req, res) {
res.render('static/getstart', {
pageTitle: 'Node.js 新手入门'
});
};
exports.robots = function (req, res, next) {
res.type('text/plain');
res.send(multiline(function () {;
/*
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-Agent: *
# Disallow: /
*/
}));
};
exports.api = function (req, res, next) {
res.render('static/api');
};
================================================
FILE: controllers/topic.js
================================================
/*!
* nodeclub - controllers/topic.js
*/
/**
* Module dependencies.
*/
var validator = require('validator');
var at = require('../common/at');
var User = require('../proxy').User;
var Topic = require('../proxy').Topic;
var TopicCollect = require('../proxy').TopicCollect;
var EventProxy = require('eventproxy');
var tools = require('../common/tools');
var store = require('../common/store');
var config = require('../config');
var _ = require('lodash');
var cache = require('../common/cache');
var logger = require('../common/logger')
/**
* Topic page
*
* @param {HttpRequest} req
* @param {HttpResponse} res
* @param {Function} next
*/
exports.index = function (req, res, next) {
function isUped(user, reply) {
if (!reply.ups) {
return false;
}
return reply.ups.indexOf(user._id) !== -1;
}
var topic_id = req.params.tid;
var currentUser = req.session.user;
if (topic_id.length !== 24) {
return res.render404('此话题不存在或已被删除。');
}
var events = ['topic', 'other_topics', 'no_reply_topics', 'is_collect'];
var ep = EventProxy.create(events,
function (topic, other_topics, no_reply_topics, is_collect) {
res.render('topic/index', {
topic: topic,
author_other_topics: other_topics,
no_reply_topics: no_reply_topics,
is_uped: isUped,
is_collect: is_collect,
});
});
ep.fail(next);
Topic.getFullTopic(topic_id, ep.done(function (message, topic, author, replies) {
if (message) {
logger.error('getFullTopic error topic_id: ' + topic_id)
return res.renderError(message);
}
topic.visit_count += 1;
topic.save();
topic.author = author;
topic.replies = replies;
// 点赞数排名第三的回答,它的点赞数就是阈值
topic.reply_up_threshold = (function () {
var allUpCount = replies.map(function (reply) {
return reply.ups && reply.ups.length || 0;
});
allUpCount = _.sortBy(allUpCount, Number).reverse();
var threshold = allUpCount[2] || 0;
if (threshold < 3) {
threshold = 3;
}
return threshold;
})();
ep.emit('topic', topic);
// get other_topics
var options = { limit: 5, sort: '-last_reply_at'};
var query = { author_id: topic.author_id, _id: { '$nin': [ topic._id ] } };
Topic.getTopicsByQuery(query, options, ep.done('other_topics'));
// get no_reply_topics
cache.get('no_reply_topics', ep.done(function (no_reply_topics) {
if (no_reply_topics) {
ep.emit('no_reply_topics', no_reply_topics);
} else {
Topic.getTopicsByQuery(
{ reply_count: 0, tab: {$nin: ['job', 'dev']}},
{ limit: 5, sort: '-create_at'},
ep.done('no_reply_topics', function (no_reply_topics) {
cache.set('no_reply_topics', no_reply_topics, 60 * 1);
return no_reply_topics;
}));
}
}));
}));
if (!currentUser) {
ep.emit('is_collect', null);
} else {
TopicCollect.getTopicCollect(currentUser._id, topic_id, ep.done('is_collect'))
}
};
exports.create = function (req, res, next) {
res.render('topic/edit', {
tabs: config.tabs
});
};
exports.put = function (req, res, next) {
var title = validator.trim(req.body.title);
var tab = validator.trim(req.body.tab);
var content = validator.trim(req.body.t_content);
// 得到所有的 tab, e.g. ['ask', 'share', ..]
var allTabs = config.tabs.map(function (tPair) {
return tPair[0];
});
// 验证
var editError;
if (title === '') {
editError = '标题不能是空的。';
} else if (title.length < 5 || title.length > 100) {
editError = '标题字数太多或太少。';
} else if (!tab || allTabs.indexOf(tab) === -1) {
editError = '必须选择一个版块。';
} else if (content === '') {
editError = '内容不可为空';
}
// END 验证
if (editError) {
res.status(422);
return res.render('topic/edit', {
edit_error: editError,
title: title,
content: content,
tabs: config.tabs
});
}
Topic.newAndSave(title, content, tab, req.session.user._id, function (err, topic) {
if (err) {
return next(err);
}
var proxy = new EventProxy();
proxy.all('score_saved', function () {
res.redirect('/topic/' + topic._id);
});
proxy.fail(next);
User.getUserById(req.session.user._id, proxy.done(function (user) {
user.score += 5;
user.topic_count += 1;
user.save();
req.session.user = user;
proxy.emit('score_saved');
}));
//发送at消息
at.sendMessageToMentionUsers(content, topic._id, req.session.user._id);
});
};
exports.showEdit = function (req, res, next) {
var topic_id = req.params.tid;
Topic.getTopicById(topic_id, function (err, topic, tags) {
if (!topic) {
res.render404('此话题不存在或已被删除。');
return;
}
if (String(topic.author_id) === String(req.session.user._id) || req.session.user.is_admin) {
res.render('topic/edit', {
action: 'edit',
topic_id: topic._id,
title: topic.title,
content: topic.content,
tab: topic.tab,
tabs: config.tabs
});
} else {
res.renderError('对不起,你不能编辑此话题。', 403);
}
});
};
exports.update = function (req, res, next) {
var topic_id = req.params.tid;
var title = req.body.title;
var tab = req.body.tab;
var content = req.body.t_content;
Topic.getTopicById(topic_id, function (err, topic, tags) {
if (!topic) {
res.render404('此话题不存在或已被删除。');
return;
}
if (topic.author_id.equals(req.session.user._id) || req.session.user.is_admin) {
title = validator.trim(title);
tab = validator.trim(tab);
content = validator.trim(content);
// 验证
var editError;
if (title === '') {
editError = '标题不能是空的。';
} else if (title.length < 5 || title.length > 100) {
editError = '标题字数太多或太少。';
} else if (!tab) {
editError = '必须选择一个版块。';
}
// END 验证
if (editError) {
return res.render('topic/edit', {
action: 'edit',
edit_error: editError,
topic_id: topic._id,
content: content,
tabs: config.tabs
});
}
//保存话题
topic.title = title;
topic.content = content;
topic.tab = tab;
topic.update_at = new Date();
topic.save(function (err) {
if (err) {
return next(err);
}
//发送at消息
at.sendMessageToMentionUsers(content, topic._id, req.session.user._id);
res.redirect('/topic/' + topic._id);
});
} else {
res.renderError('对不起,你不能编辑此话题。', 403);
}
});
};
exports.delete = function (req, res, next) {
//删除话题, 话题作者topic_count减1
//删除回复,回复作者reply_count减1
//删除topic_collect,用户collect_topic_count减1
var topic_id = req.params.tid;
Topic.getFullTopic(topic_id, function (err, err_msg, topic, author, replies) {
if (err) {
return res.send({ success: false, message: err.message });
}
if (!req.session.user.is_admin && !(topic.author_id.equals(req.session.user._id))) {
res.status(403);
return res.send({success: false, message: '无权限'});
}
if (!topic) {
res.status(422);
return res.send({ success: false, message: '此话题不存在或已被删除。' });
}
author.score -= 5;
author.topic_count -= 1;
author.save();
topic.deleted = true;
topic.save(function (err) {
if (err) {
return res.send({ success: false, message: err.message });
}
res.send({ success: true, message: '话题已被删除。' });
});
});
};
// 设为置顶
exports.top = function (req, res, next) {
var topic_id = req.params.tid;
var referer = req.get('referer');
if (topic_id.length !== 24) {
res.render404('此话题不存在或已被删除。');
return;
}
Topic.getTopic(topic_id, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.render404('此话题不存在或已被删除。');
return;
}
topic.top = !topic.top;
topic.save(function (err) {
if (err) {
return next(err);
}
var msg = topic.top ? '此话题已置顶。' : '此话题已取消置顶。';
res.render('notify/notify', {success: msg, referer: referer});
});
});
};
// 设为精华
exports.good = function (req, res, next) {
var topicId = req.params.tid;
var referer = req.get('referer');
Topic.getTopic(topicId, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.render404('此话题不存在或已被删除。');
return;
}
topic.good = !topic.good;
topic.save(function (err) {
if (err) {
return next(err);
}
var msg = topic.good ? '此话题已加精。' : '此话题已取消加精。';
res.render('notify/notify', {success: msg, referer: referer});
});
});
};
// 锁定主题,不可再回复
exports.lock = function (req, res, next) {
var topicId = req.params.tid;
var referer = req.get('referer');
Topic.getTopic(topicId, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.render404('此话题不存在或已被删除。');
return;
}
topic.lock = !topic.lock;
topic.save(function (err) {
if (err) {
return next(err);
}
var msg = topic.lock ? '此话题已锁定。' : '此话题已取消锁定。';
res.render('notify/notify', {success: msg, referer: referer});
});
});
};
// 收藏主题
exports.collect = function (req, res, next) {
var topic_id = req.body.topic_id;
Topic.getTopic(topic_id, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.json({status: 'failed'});
}
TopicCollect.getTopicCollect(req.session.user._id, topic._id, function (err, doc) {
if (err) {
return next(err);
}
if (doc) {
res.json({status: 'failed'});
return;
}
TopicCollect.newAndSave(req.session.user._id, topic._id, function (err) {
if (err) {
return next(err);
}
res.json({status: 'success'});
});
User.getUserById(req.session.user._id, function (err, user) {
if (err) {
return next(err);
}
user.collect_topic_count += 1;
user.save();
});
req.session.user.collect_topic_count += 1;
topic.collect_count += 1;
topic.save();
});
});
};
exports.de_collect = function (req, res, next) {
var topic_id = req.body.topic_id;
Topic.getTopic(topic_id, function (err, topic) {
if (err) {
return next(err);
}
if (!topic) {
res.json({status: 'failed'});
}
TopicCollect.remove(req.session.user._id, topic._id, function (err, removeResult) {
if (err) {
return next(err);
}
if (removeResult.n == 0) {
return res.json({status: 'failed'})
}
User.getUserById(req.session.user._id, function (err, user) {
if (err) {
return next(err);
}
user.collect_topic_count -= 1;
req.session.user = user;
user.save();
});
topic.collect_count -= 1;
topic.save();
res.json({status: 'success'});
});
});
};
exports.upload = function (req, res, next) {
var isFileLimit = false;
req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
file.on('limit', function () {
isFileLimit = true;
res.json({
success: false,
msg: 'File size too large. Max is ' + config.file_limit
})
});
store.upload(file, {filename: filename}, function (err, result) {
if (err) {
return next(err);
}
if (isFileLimit) {
return;
}
res.json({
success: true,
url: result.url,
});
});
});
req.pipe(req.busboy);
};
================================================
FILE: controllers/user.js
================================================
var User = require('../proxy').User;
var Topic = require('../proxy').Topic;
var Reply = require('../proxy').Reply;
var TopicCollect = require('../proxy').TopicCollect;
var utility = require('utility');
var util = require('util');
var TopicModel = require('../models').Topic;
var ReplyModel = require('../models').Reply;
var tools = require('../common/tools');
var config = require('../config');
var EventProxy = require('eventproxy');
var validator = require('validator');
var _ = require('lodash');
var uuid = require('node-uuid')
exports.index = function (req, res, next) {
var user_name = req.params.name;
User.getUserByLoginName(user_name, function (err, user) {
if (err) {
return next(err);
}
if (!user) {
res.render404('这个用户不存在。');
return;
}
var render = function (recent_topics, recent_replies) {
user.url = (function () {
if (user.url && user.url.indexOf('http') !== 0) {
return 'http://' + user.url;
}
return user.url;
})();
// 如果用户没有激活,那么管理员可以帮忙激活
var token = '';
if (!user.active && req.session.user && req.session.user.is_admin) {
token = utility.md5(user.email + user.pass + config.session_secret);
}
res.render('user/index', {
user: user,
recent_topics: recent_topics,
recent_replies: recent_replies,
token: token,
pageTitle: util.format('@%s 的个人主页', user.loginname),
});
};
var proxy = new EventProxy();
proxy.assign('recent_topics', 'recent_replies', render);
proxy.fail(next);
var query = {author_id: user._id};
var opt = {limit: 5, sort: '-create_at'};
Topic.getTopicsByQuery(query, opt, proxy.done('recent_topics'));
Reply.getRepliesByAuthorId(user._id, {limit: 20, sort: '-create_at'},
proxy.done(function (replies) {
var topic_ids = replies.map(function (reply) {
return reply.topic_id.toString()
})
topic_ids = _.uniq(topic_ids).slice(0, 5); // 只显示最近5条
var query = {_id: {'$in': topic_ids}};
var opt = {};
Topic.getTopicsByQuery(query, opt, proxy.done('recent_replies', function (recent_replies) {
recent_replies = _.sortBy(recent_replies, function (topic) {
return topic_ids.indexOf(topic._id.toString())
})
return recent_replies;
}));
}));
});
};
exports.listStars = function (req, res, next) {
User.getUsersByQuery({is_star: true}, {}, function (err, stars) {
if (err) {
return next(err);
}
res.render('user/stars', {stars: stars});
});
};
exports.showSetting = function (req, res, next) {
User.getUserById(req.session.user._id, function (err, user) {
if (err) {
return next(err);
}
if (req.query.save === 'success') {
user.success = '保存成功。';
}
user.error = null;
return res.render('user/setting', user);
});
};
exports.setting = function (req, res, next) {
var ep = new EventProxy();
ep.fail(next);
// 显示出错或成功信息
function showMessage(msg, data, isSuccess) {
data = data || req.body;
var data2 = {
loginname: data.loginname,
email: data.email,
url: data.url,
location: data.location,
signature: data.signature,
weibo: data.weibo,
accessToken: data.accessToken,
};
if (isSuccess) {
data2.success = msg;
} else {
data2.error = msg;
}
res.render('user/setting', data2);
}
// post
var action = req.body.action;
if (action === 'change_setting') {
var url = validator.trim(req.body.url);
var location = validator.trim(req.body.location);
var weibo = validator.trim(req.body.weibo);
var signature = validator.trim(req.body.signature);
User.getUserById(req.session.user._id, ep.done(function (user) {
user.url = url;
user.location = location;
user.signature = signature;
user.weibo = weibo;
user.save(function (err) {
if (err) {
return next(err);
}
req.session.user = user.toObject({virtual: true});
return res.redirect('/setting?save=success');
});
}));
}
if (action === 'change_password') {
var old_pass = validator.trim(req.body.old_pass);
var new_pass = validator.trim(req.body.new_pass);
if (!old_pass || !new_pass) {
return res.send('旧密码或新密码不得为空');
}
User.getUserById(req.session.user._id, ep.done(function (user) {
tools.bcompare(old_pass, user.pass, ep.done(function (bool) {
if (!bool) {
return showMessage('当前密码不正确。', user);
}
tools.bhash(new_pass, ep.done(function (passhash) {
user.pass = passhash;
user.save(function (err) {
if (err) {
return next(err);
}
return showMessage('密码已被修改。', user, true);
});
}));
}));
}));
}
};
exports.toggleStar = function (req, res, next) {
var user_id = req.body.user_id;
User.getUserById(user_id, function (err, user) {
if (err) {
return next(err);
}
if (!user) {
return next(new Error('user is not exists'));
}
user.is_star = !user.is_star;
user.save(function (err) {
if (err) {
return next(err);
}
res.json({ status: 'success' });
});
});
};
exports.listCollectedTopics = function (req, res, next) {
var name = req.params.name;
var page = Number(req.query.page) || 1;
var limit = config.list_topic_count;
User.getUserByLoginName(name, function (err, user) {
if (err || !user) {
return next(err);
}
var pages = Math.ceil(user.collect_topic_count/limit);
var render = function (topics) {
res.render('user/collect_topics', {
topics: topics,
current_page: page,
pages: pages,
user: user
});
};
var proxy = EventProxy.create('topics', render);
proxy.fail(next);
var opt = {
skip: (page - 1) * limit,
limit: limit,
};
TopicCollect.getTopicCollectsByUserId(user._id, opt, proxy.done(function (docs) {
var ids = docs.map(function (doc) {
return String(doc.topic_id)
})
var query = { _id: { '$in': ids } };
Topic.getTopicsByQuery(query, {}, proxy.done('topics', function (topics) {
topics = _.sortBy(topics, function (topic) {
return ids.indexOf(String(topic._id))
})
return topics
}));
}));
});
};
exports.top100 = function (req, res, next) {
var opt = {limit: 100, sort: '-score'};
User.getUsersByQuery({is_block: false}, opt, function (err, tops) {
if (err) {
return next(err);
}
res.render('user/top100', {
users: tops,
pageTitle: 'top100',
});
});
};
exports.listTopics = function (req, res, next) {
var user_name = req.params.name;
var page = Number(req.query.page) || 1;
var limit = config.list_topic_count;
User.getUserByLoginName(user_name, function (err, user) {
if (!user) {
res.render404('这个用户不存在。');
return;
}
var render = function (topics, pages) {
res.render('user/topics', {
user: user,
topics: topics,
current_page: page,
pages: pages
});
};
var proxy = new EventProxy();
proxy.assign('topics', 'pages', render);
proxy.fail(next);
var query = {'author_id': user._id};
var opt = {skip: (page - 1) * limit, limit: limit, sort: '-create_at'};
Topic.getTopicsByQuery(query, opt, proxy.done('topics'));
Topic.getCountByQuery(query, proxy.done(function (all_topics_count) {
var pages = Math.ceil(all_topics_count / limit);
proxy.emit('pages', pages);
}));
});
};
exports.listReplies = function (req, res, next) {
var user_name = req.params.name;
var page = Number(req.query.page) || 1;
var limit = 50;
User.getUserByLoginName(user_name, function (err, user) {
if (!user) {
res.render404('这个用户不存在。');
return;
}
var render = function (topics, pages) {
res.render('user/replies', {
user: user,
topics: topics,
current_page: page,
pages: pages
});
};
var proxy = new EventProxy();
proxy.assign('topics', 'pages', render);
proxy.fail(next);
var opt = {skip: (page - 1) * limit, limit: limit, sort: '-create_at'};
Reply.getRepliesByAuthorId(user._id, opt, proxy.done(function (replies) {
// 获取所有有评论的主题
var topic_ids = replies.map(function (reply) {
return reply.topic_id.toString();
});
topic_ids = _.uniq(topic_ids);
var query = {'_id': {'$in': topic_ids}};
Topic.getTopicsByQuery(query, {}, proxy.done('topics', function (topics) {
topics = _.sortBy(topics, function (topic) {
return topic_ids.indexOf(topic._id.toString())
})
return topics;
}));
}));
Reply.getCountByAuthorId(user._id, proxy.done('pages', function (count) {
var pages = Math.ceil(count / limit);
return pages;
}));
});
};
exports.block = function (req, res, next) {
var loginname = req.params.name;
var action = req.body.action;
var ep = EventProxy.create();
ep.fail(next);
User.getUserByLoginName(loginname, ep.done(function (user) {
if (!user) {
return next(new Error('user is not exists'));
}
if (action === 'set_block') {
ep.all('block_user',
function (user) {
res.json({status: 'success'});
});
user.is_block = true;
user.save(ep.done('block_user'));
} else if (action === 'cancel_block') {
user.is_block = false;
user.save(ep.done(function () {
res.json({status: 'success'});
}));
}
}));
};
exports.deleteAll = function (req, res, next) {
var loginname = req.params.name;
var ep = EventProxy.create();
ep.fail(next);
User.getUserByLoginName(loginname, ep.done(function (user) {
if (!user) {
return next(new Error('user is not exists'));
}
ep.all('del_topics', 'del_replys', 'del_ups',
function () {
res.json({status: 'success'});
});
// 删除主题
TopicModel.updateMany({author_id: user._id}, {$set: {deleted: true}}, ep.done('del_topics'));
// 删除评论
ReplyModel.updateMany({author_id: user._id}, {$set: {deleted: true}}, ep.done('del_replys'));
// 点赞数也全部干掉
ReplyModel.updateMany({}, {$pull: {'ups': user._id}}, ep.done('del_ups'));
}));
};
exports.refreshToken = function (req, res, next) {
var user_id = req.session.user._id;
var ep = EventProxy.create();
ep.fail(next);
User.getUserById(user_id, ep.done(function (user) {
user.accessToken = uuid.v4();
user.save(ep.done(function () {
res.json({status: 'success', accessToken: user.accessToken});
}));
}));
};
================================================
FILE: logs/.gitkeep
================================================
================================================
FILE: middlewares/auth.js
================================================
var mongoose = require('mongoose');
var UserModel = mongoose.model('User');
var Message = require('../proxy').Message;
var config = require('../config');
var eventproxy = require('eventproxy');
var UserProxy = require('../proxy').User;
/**
* 需要管理员权限
*/
exports.adminRequired = function (req, res, next) {
if (!req.session.user) {
return res.render('notify/notify', { error: '你还没有登录。' });
}
if (!req.session.user.is_admin) {
return res.render('notify/notify', { error: '需要管理员权限。' });
}
next();
};
/**
* 需要登录
*/
exports.userRequired = function (req, res, next) {
if (!req.session || !req.session.user || !req.session.user._id) {
return res.status(403).send('forbidden!');
}
next();
};
exports.blockUser = function () {
return function (req, res, next) {
if (req.path === '/signout') {
return next();
}
if (req.session.user && req.session.user.is_block && req.method !== 'GET') {
return res.status(403).send('您已被管理员屏蔽了。有疑问请联系 @alsotang。');
}
next();
};
};
function gen_session(user, res) {
var auth_token = user._id + '$$$$'; // 以后可能会存储更多信息,用 $$$$ 来分隔
var opts = {
path: '/',
maxAge: 1000 * 60 * 60 * 24 * 30,
signed: true,
httpOnly: true
};
res.cookie(config.auth_cookie_name, auth_token, opts); //cookie 有效期30天
}
exports.gen_session = gen_session;
// 验证用户是否登录
exports.authUser = function (req, res, next) {
var ep = new eventproxy();
ep.fail(next);
// Ensure current_user always has defined.
res.locals.current_user = null;
if (config.debug && req.cookies['mock_user']) {
var mockUser = JSON.parse(req.cookies['mock_user']);
req.session.user = new UserModel(mockUser);
if (mockUser.is_admin) {
req.session.user.is_admin = true;
}
return next();
}
ep.all('get_user', function (user) {
if (!user) {
return next();
}
user = res.locals.current_user = req.session.user = new UserModel(user);
if (config.admins.hasOwnProperty(user.loginname)) {
user.is_admin = true;
}
Message.getMessagesCount(user._id, ep.done(function (count) {
user.messages_count = count;
next();
}));
});
if (req.session.user) {
ep.emit('get_user', req.session.user);
} else {
var auth_token = req.signedCookies[config.auth_cookie_name];
if (!auth_token) {
return next();
}
var auth = auth_token.split('$$$$');
var user_id = auth[0];
UserProxy.getUserById(user_id, ep.done('get_user'));
}
};
================================================
FILE: middlewares/conf.js
================================================
var config = require('../config');
exports.github = function (req, res, next) {
if (config.GITHUB_OAUTH.clientID === 'your GITHUB_CLIENT_ID') {
return res.send('call the admin to set github oauth.');
}
next();
};
================================================
FILE: middlewares/error_page.js
================================================
// ErrorPage middleware
exports.errorPage = function (req, res, next) {
res.render404 = function (error) {
return res.status(404).render('notify/notify', { error: error });
};
res.renderError = function (error, statusCode) {
if (statusCode === undefined) {
statusCode = 400;
}
return res.status(statusCode).render('notify/notify', { error: error });
};
next();
};
================================================
FILE: middlewares/github_strategy.js
================================================
module.exports = function (accessToken, refreshToken, profile, done) {
profile.accessToken = accessToken;
done(null, profile);
};
================================================
FILE: middlewares/limit.js
================================================
var config = require('../config');
var cache = require('../common/cache');
var moment = require('moment');
var SEPARATOR = '^_^@T_T';
var makePerDayLimiter = function (identityName, identityFn) {
return function (name, limitCount, options) {
/*
options.showJson = true 表示调用来自API并返回结构化数据;否则表示调用来自前段并渲染错误页面
*/
return function (req, res, next) {
var identity = identityFn(req);
var YYYYMMDD = moment().format('YYYYMMDD');
var key = YYYYMMDD + SEPARATOR + identityName + SEPARATOR + name + SEPARATOR + identity;
cache.get(key, function (err, count) {
if (err) {
return next(err);
}
count = count || 0;
if (count < limitCount) {
count += 1;
cache.set(key, count, 60 * 60 * 24);
res.set('X-RateLimit-Limit', limitCount);
res.set('X-RateLimit-Remaining', limitCount - count);
next();
} else {
res.status(403);
if (options.showJson) {
res.send({success: false, error_msg: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'});
} else {
res.render('notify/notify', { error: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'});
}
}
});
};
};
};
exports.peruserperday = makePerDayLimiter('peruserperday', function (req) {
return (req.user || req.session.user).loginname;
});
exports.peripperday = makePerDayLimiter('peripperday', function (req) {
var realIP = req.get('x-real-ip');
if (!realIP && !config.debug) {
throw new Error('should provide `x-real-ip` header')
}
return realIP;
});
================================================
FILE: middlewares/mongoose_log.js
================================================
var mongoose = require('mongoose');
var logger = require('../common/logger');
var config = require('../config');
if (config.debug) {
var traceMQuery = function (method, info, query) {
return function (err, result, millis) {
if (err) {
logger.error('traceMQuery error:', err)
}
var infos = [];
infos.push(query._collection.collection.name + "." + method.blue);
infos.push(JSON.stringify(info));
infos.push((millis + 'ms').green);
logger.debug("MONGO".magenta, infos.join(' '));
};
};
mongoose.Mongoose.prototype.mquery.setGlobalTraceFunction(traceMQuery);
}
================================================
FILE: middlewares/proxy.js
================================================
var urllib = require('url');
var request = require('request');
var logger = require('../common/logger')
var _ = require('lodash')
var ALLOW_HOSTNAME = [
'avatars.githubusercontent.com', 'www.gravatar.com',
'gravatar.com', 'www.google-analytics.com',
];
exports.proxy = function (req, res, next) {
var url = decodeURIComponent(req.query.url);
var hostname = urllib.parse(url).hostname;
if (ALLOW_HOSTNAME.indexOf(hostname) === -1) {
return res.send(hostname + ' is not allowed');
}
request.get({
url: url,
headers: _.omit(req.headers, ['cookie', 'refer']),
})
.on('response', function (response) {
res.set(response.headers);
})
.on('error', function (err) {
logger.error(err);
})
.pipe(res);
};
================================================
FILE: middlewares/render.js
================================================
var logger = require('../common/logger');
// Patch res.render method to output logger
exports.render = function (req, res, next) {
res._render = res.render;
res.render = function (view, options, fn) {
var t = new Date();
res._render(view, options, fn);
var duration = (new Date() - t);
logger.info("Render view", view, ("(" + duration + "ms)").green);
};
next();
};
================================================
FILE: middlewares/request_log.js
================================================
var logger = require('../common/logger');
var ignore = /^\/(public|agent)/;
exports = module.exports = function (req, res, next) {
// Assets do not out log.
if (ignore.test(req.url)) {
next();
return;
}
var t = new Date();
logger.info('\n\nStarted', t.toISOString(), req.method, req.url, req.ip);
res.on('finish', function () {
var duration = ((new Date()) - t);
logger.info('Completed', res.statusCode, ('(' + duration + 'ms)').green);
});
next();
};
================================================
FILE: models/base_model.js
================================================
/**
* 给所有的 Model 扩展功能
* http://mongoosejs.com/docs/plugins.html
*/
var tools = require('../common/tools');
module.exports = function (schema) {
schema.methods.create_at_ago = function () {
return tools.formatDate(this.create_at, true);
};
schema.methods.update_at_ago = function () {
return tools.formatDate(this.update_at, true);
};
};
================================================
FILE: models/index.js
================================================
var mongoose = require('mongoose');
var config = require('../config');
var logger = require('../common/logger')
mongoose.connect(config.db, {
poolSize: 20,
useCreateIndex: true,
useNewUrlParser: true
}, function (err) {
if (err) {
logger.error('connect to %s error: ', config.db, err.message);
process.exit(1);
}
});
// models
require('./user');
require('./topic');
require('./reply');
require('./topic_collect');
require('./message');
exports.User = mongoose.model('User');
exports.Topic = mongoose.model('Topic');
exports.Reply = mongoose.model('Reply');
exports.TopicCollect = mongoose.model('TopicCollect');
exports.Message = mongoose.model('Message');
================================================
FILE: models/message.js
================================================
var mongoose = require('mongoose');
var BaseModel = require("./base_model");
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
/*
* type:
* reply: xx 回复了你的话题
* reply2: xx 在话题中回复了你
* follow: xx 关注了你
* at: xx @了你
*/
var MessageSchema = new Schema({
type: { type: String },
master_id: { type: ObjectId},
author_id: { type: ObjectId },
topic_id: { type: ObjectId },
reply_id: { type: ObjectId },
has_read: { type: Boolean, default: false },
create_at: { type: Date, default: Date.now }
});
MessageSchema.plugin(BaseModel);
MessageSchema.index({master_id: 1, has_read: -1, create_at: -1});
mongoose.model('Message', MessageSchema);
================================================
FILE: models/reply.js
================================================
var mongoose = require('mongoose');
var BaseModel = require("./base_model");
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var ReplySchema = new Schema({
content: { type: String },
topic_id: { type: ObjectId},
author_id: { type: ObjectId },
reply_id: { type: ObjectId },
create_at: { type: Date, default: Date.now },
update_at: { type: Date, default: Date.now },
content_is_html: { type: Boolean },
ups: [Schema.Types.ObjectId],
deleted: {type: Boolean, default: false},
});
ReplySchema.plugin(BaseModel);
ReplySchema.index({topic_id: 1});
ReplySchema.index({author_id: 1, create_at: -1});
mongoose.model('Reply', ReplySchema);
================================================
FILE: models/topic.js
================================================
var mongoose = require('mongoose');
var BaseModel = require("./base_model");
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var config = require('../config');
var _ = require('lodash');
var TopicSchema = new Schema({
title: { type: String },
content: { type: String },
author_id: { type: ObjectId },
top: { type: Boolean, default: false }, // 置顶帖
good: {type: Boolean, default: false}, // 精华帖
lock: {type: Boolean, default: false}, // 被锁定主题
reply_count: { type: Number, default: 0 },
visit_count: { type: Number, default: 0 },
collect_count: { type: Number, default: 0 },
create_at: { type: Date, default: Date.now },
update_at: { type: Date, default: Date.now },
last_reply: { type: ObjectId },
last_reply_at: { type: Date, default: Date.now },
content_is_html: { type: Boolean },
tab: {type: String},
deleted: {type: Boolean, default: false},
});
TopicSchema.plugin(BaseModel);
TopicSchema.index({create_at: -1});
TopicSchema.index({top: -1, last_reply_at: -1});
TopicSchema.index({author_id: 1, create_at: -1});
TopicSchema.virtual('tabName').get(function () {
var tab = this.tab;
var pair = _.find(config.tabs, function (_pair) {
return _pair[0] === tab;
});
if (pair) {
return pair[1];
} else {
return '';
}
});
mongoose.model('Topic', TopicSchema);
================================================
FILE: models/topic_collect.js
================================================
var mongoose = require('mongoose');
var BaseModel = require("./base_model");
var Schema = mongoose.Schema;
var ObjectId = Schema.ObjectId;
var TopicCollectSchema = new Schema({
user_id: { type: ObjectId },
topic_id: { type: ObjectId },
create_at: { type: Date, default: Date.now }
});
TopicCollectSchema.plugin(BaseModel);
TopicCollectSchema.index({user_id: 1, topic_id: 1}, {unique: true});
mongoose.model('TopicCollect', TopicCollectSchema);
================================================
FILE: models/user.js
================================================
var mongoose = require('mongoose');
var BaseModel = require("./base_model");
var renderHelper = require('../common/render_helper');
var Schema = mongoose.Schema;
var utility = require('utility');
var _ = require('lodash');
var UserSchema = new Schema({
name: { type: String},
loginname: { type: String},
pass: { type: String },
email: { type: String},
url: { type: String },
profile_image_url: {type: String},
location: { type: String },
signature: { type: String },
profile: { type: String },
weibo: { type: String },
avatar: { type: String },
githubId: { type: String},
githubUsername: {type: String},
githubAccessToken: {type: String},
is_block: {type: Boolean, default: false},
score: { type: Number, default: 0 },
topic_count: { type: Number, default: 0 },
reply_count: { type: Number, default: 0 },
follower_count: { type: Number, default: 0 },
following_count: { type: Number, default: 0 },
collect_tag_count: { type: Number, default: 0 },
collect_topic_count: { type: Number, default: 0 },
create_at: { type: Date, default: Date.now },
update_at: { type: Date, default: Date.now },
is_star: { type: Boolean },
level: { type: String },
active: { type: Boolean, default: false },
receive_reply_mail: {type: Boolean, default: false },
receive_at_mail: { type: Boolean, default: false },
from_wp: { type: Boolean },
retrieve_time: {type: Number},
retrieve_key: {type: String},
accessToken: {type: String},
});
UserSchema.plugin(BaseModel);
UserSchema.virtual('avatar_url').get(function () {
var url = this.avatar || ('https://gravatar.com/avatar/' + utility.md5(this.email.toLowerCase()) + '?size=48');
// www.gravatar.com 被墙
url = url.replace('www.gravatar.com', 'gravatar.com');
// 让协议自适应 protocol,使用 `//` 开头
if (url.indexOf('http:') === 0) {
url = url.slice(5);
}
// 如果是 github 的头像,则限制大小
if (url.indexOf('githubusercontent') !== -1) {
url += '&s=120';
}
return url;
});
UserSchema.virtual('isAdvanced').get(function () {
// 积分高于 700 则认为是高级用户
return this.score > 700 || this.is_star;
});
UserSchema.index({loginname: 1}, {unique: true});
UserSchema.index({email: 1}, {unique: true});
UserSchema.index({score: -1});
UserSchema.index({githubId: 1});
UserSchema.index({accessToken: 1});
UserSchema.pre('save', function(next){
var now = new Date();
this.update_at = now;
next();
});
mongoose.model('User', UserSchema);
================================================
FILE: oneapm.js
================================================
/**
* OneAPM agent configuration.
*
* See lib/config.defaults.js in the agent distribution for a more complete
* description of configuration variables and their potential values.
*/
var config = require('./config');
exports.config = {
/**
* Array of application names.
*/
app_name : [config.name],
/**
* Your OneAPM license key.
*/
license_key : config.oneapm_key,
logging : {
/**
* Level at which to log. 'trace' is most useful to OneAPM when diagnosing
* issues with the agent, 'info' and higher will impose the least overhead on
* production applications.
*/
level : 'info'
},
transaction_events: {
enabled: true
}
};
================================================
FILE: package.json
================================================
{
"name": "nodeclub",
"version": "2.1.1",
"private": true,
"main": "app.js",
"description": "A Node.js bbs using MongoDB",
"repository": "https://github.com/cnodejs/nodeclub",
"dependencies": {
"async": "1.5.2",
"bcryptjs": "2.3.0",
"body-parser": "1.17.1",
"bytes": "^2.2.0",
"colors": "1.1.2",
"compression": "1.7.0",
"connect-busboy": "0.0.2",
"connect-redis": "3.0.2",
"cookie-parser": "1.4.1",
"cors": "2.7.1",
"csurf": "1.8.3",
"data2xml": "1.2.4",
"ejs-mate": "2.3.0",
"eventproxy": "1.0.0",
"express": "4.16.0",
"express-session": "1.12.1",
"helmet": "1.3.0",
"ioredis": "2.0.0",
"jpush-sdk": "3.3.2",
"loader-builder": "2.4.1",
"loader": "2.1.1",
"lodash": "4.17.21",
"log4js": "^0.6.29",
"markdown-it": "6.0.0",
"memory-cache": "0.1.4",
"method-override": "2.3.5",
"moment": "2.15.2",
"mongoose": "5.3.9",
"multiline": "1.0.2",
"node-uuid": "1.4.7",
"nodemailer": "2.3.0",
"nodemailer-smtp-transport": "2.4.0",
"oneapm": "1.2.20",
"passport": "0.3.2",
"passport-github": "1.1.0",
"pm2": "*",
"qn": "1.3.0",
"ready": "0.1.1",
"request": "2.81.0",
"response-time": "2.3.1",
"superagent": "2.0.0",
"utility": "1.6.0",
"validator": "5.1.0",
"xmlbuilder": "7.0.0",
"xss": "0.2.10",
"snyk": "^1.88.0"
},
"devDependencies": {
"errorhandler": "1.4.3",
"istanbul": "0.4.2",
"loader-connect": "1.0.1",
"mm": "1.3.5",
"mocha": "2.4.5",
"nock": "7.5.0",
"pedding": "1.0.0",
"should": "8.3.0",
"supertest": "1.2.0"
},
"scripts": {
"test": "make test",
"snyk-protect": "snyk protect",
"prepare": "npm run snyk-protect"
},
"snyk": true
}
================================================
FILE: proxy/index.js
================================================
exports.User = require('./user');
exports.Message = require('./message');
exports.Topic = require('./topic');
exports.Reply = require('./reply');
exports.TopicCollect = require('./topic_collect');
================================================
FILE: proxy/message.js
================================================
var EventProxy = require('eventproxy');
var _ = require('lodash');
var Message = require('../models').Message;
var User = require('./user');
var Topic = require('./topic');
var Reply = require('./reply');
/**
* 根据用户ID,获取未读消息的数量
* Callback:
* 回调函数参数列表:
* - err, 数据库错误
* - count, 未读消息数量
* @param {String} id 用户ID
* @param {Function} callback 获取消息数量
*/
exports.getMessagesCount = function (id, callback) {
Message.countDocuments({master_id: id, has_read: false}, callback);
};
/**
* 根据消息Id获取消息
* Callback:
* - err, 数据库错误
* - message, 消息对象
* @param {String} id 消息ID
* @param {Function} callback 回调函数
*/
exports.getMessageById = function (id, callback) {
Message.findOne({_id: id}, function (err, message) {
if (err) {
return callback(err);
}
getMessageRelations(message, callback);
});
};
var getMessageRelations = exports.getMessageRelations = function (message, callback) {
if (message.type === 'reply' || message.type === 'reply2' || message.type === 'at') {
var proxy = new EventProxy();
proxy.fail(callback);
proxy.assign('author', 'topic', 'reply', function (author, topic, reply) {
message.author = author;
message.topic = topic;
message.reply = reply;
if (!author || !topic) {
message.is_invalid = true;
}
return callback(null, message);
}); // 接收异常
User.getUserById(message.author_id, proxy.done('author'));
Topic.getTopicById(message.topic_id, proxy.done('topic'));
Reply.getReplyById(message.reply_id, proxy.done('reply'));
} else {
return callback(null, {is_invalid: true});
}
};
/**
* 根据用户ID,获取已读消息列表
* Callback:
* - err, 数据库异常
* - messages, 消息列表
* @param {String} userId 用户ID
* @param {Function} callback 回调函数
*/
exports.getReadMessagesByUserId = function (userId, callback) {
Message.find({master_id: userId, has_read: true}, null,
{sort: '-create_at', limit: 20}, callback);
};
/**
* 根据用户ID,获取未读消息列表
* Callback:
* - err, 数据库异常
* - messages, 未读消息列表
* @param {String} userId 用户ID
* @param {Function} callback 回调函数
*/
exports.getUnreadMessageByUserId = function (userId, callback) {
Message.find({master_id: userId, has_read: false}, null,
{sort: '-create_at'}, callback);
};
/**
* 将消息设置成已读
*/
exports.updateMessagesToRead = function (userId, messages, callback) {
callback = callback || _.noop;
if (messages.length === 0) {
return callback();
}
var ids = messages.map(function (m) {
return m.id;
});
var query = { master_id: userId, _id: { $in: ids } };
Message.updateMany(query, { $set: { has_read: true } }).exec(callback);
};
/**
* 将单个消息设置成已读
*/
exports.updateOneMessageToRead = function (msg_id, callback) {
callback = callback || _.noop;
if (!msg_id) {
return callback();
}
var query = { _id: msg_id };
Message.updateMany(query, { $set: { has_read: true } }).exec(callback);
};
================================================
FILE: proxy/reply.js
================================================
var models = require('../models');
var Reply = models.Reply;
var EventProxy = require('eventproxy');
var tools = require('../common/tools');
var User = require('./user');
var at = require('../common/at');
/**
* 获取一条回复信息
* @param {String} id 回复ID
* @param {Function} callback 回调函数
*/
exports.getReply = function (id, callback) {
Reply.findOne({_id: id}, callback);
};
/**
* 根据回复ID,获取回复
* Callback:
* - err, 数据库异常
* - reply, 回复内容
* @param {String} id 回复ID
* @param {Function} callback 回调函数
*/
exports.getReplyById = function (id, callback) {
if (!id) {
return callback(null, null);
}
Reply.findOne({_id: id}, function (err, reply) {
if (err) {
return callback(err);
}
if (!reply) {
return callback(err, null);
}
var author_id = reply.author_id;
User.getUserById(author_id, function (err, author) {
if (err) {
return callback(err);
}
reply.author = author;
// TODO: 添加更新方法,有些旧帖子可以转换为markdown格式的内容
if (reply.content_is_html) {
return callback(null, reply);
}
at.linkUsers(reply.content, function (err, str) {
if (err) {
return callback(err);
}
reply.content = str;
return callback(err, reply);
});
});
});
};
/**
* 根据主题ID,获取回复列表
* Callback:
* - err, 数据库异常
* - replies, 回复列表
* @param {String} id 主题ID
* @param {Function} callback 回调函数
*/
exports.getRepliesByTopicId = function (id, cb) {
Reply.find({topic_id: id, deleted: false}, '', {sort: 'create_at'}, function (err, replies) {
if (err) {
return cb(err);
}
if (replies.length === 0) {
return cb(null, []);
}
var proxy = new EventProxy();
proxy.after('reply_find', replies.length, function () {
cb(null, replies);
});
for (var j = 0; j < replies.length; j++) {
(function (i) {
var author_id = replies[i].author_id;
User.getUserById(author_id, function (err, author) {
if (err) {
return cb(err);
}
replies[i].author = author || { _id: '' };
if (replies[i].content_is_html) {
return proxy.emit('reply_find');
}
at.linkUsers(replies[i].content, function (err, str) {
if (err) {
return cb(err);
}
replies[i].content = str;
proxy.emit('reply_find');
});
});
})(j);
}
});
};
/**
* 创建并保存一条回复信息
* @param {String} content 回复内容
* @param {String} topicId 主题ID
* @param {String} authorId 回复作者
* @param {String} [replyId] 回复ID,当二级回复时设定该值
* @param {Function} callback 回调函数
*/
exports.newAndSave = function (content, topicId, authorId, replyId, callback) {
if (typeof replyId === 'function') {
callback = replyId;
replyId = null;
}
var reply = new Reply();
reply.content = content;
reply.topic_id = topicId;
reply.author_id = authorId;
if (replyId) {
reply.reply_id = replyId;
}
reply.save(function (err) {
callback(err, reply);
});
};
/**
* 根据topicId查询到最新的一条未删除回复
* @param topicId 主题ID
* @param callback 回调函数
*/
exports.getLastReplyByTopId = function (topicId, callback) {
Reply.find({topic_id: topicId, deleted: false}, '_id', {sort: {create_at : -1}, limit : 1}, callback);
};
exports.getRepliesByAuthorId = function (authorId, opt, callback) {
if (!callback) {
callback = opt;
opt = null;
}
Reply.find({author_id: authorId}, {}, opt, callback);
};
// 通过 author_id 获取回复总数
exports.getCountByAuthorId = function (authorId, callback) {
Reply.countDocuments({author_id: authorId}, callback);
};
================================================
FILE: proxy/topic.js
================================================
var EventProxy = require('eventproxy');
var models = require('../models');
var Topic = models.Topic;
var User = require('./user');
var Reply = require('./reply');
var tools = require('../common/tools');
var at = require('../common/at');
var _ = require('lodash');
/**
* 根据主题ID获取主题
* Callback:
* - err, 数据库错误
* - topic, 主题
* - author, 作者
* - lastReply, 最后回复
* @param {String} id 主题ID
* @param {Function} callback 回调函数
*/
exports.getTopicById = function (id, callback) {
var proxy = new EventProxy();
var events = ['topic', 'author', 'last_reply'];
proxy.assign(events, function (topic, author, last_reply) {
if (!author) {
return callback(null, null, null, null);
}
return callback(null, topic, author, last_reply);
}).fail(callback);
Topic.findOne({_id: id}, proxy.done(function (topic) {
if (!topic) {
proxy.emit('topic', null);
proxy.emit('author', null);
proxy.emit('last_reply', null);
return;
}
proxy.emit('topic', topic);
User.getUserById(topic.author_id, proxy.done('author'));
if (topic.last_reply) {
Reply.getReplyById(topic.last_reply, proxy.done(function (last_reply) {
proxy.emit('last_reply', last_reply);
}));
} else {
proxy.emit('last_reply', null);
}
}));
};
/**
* 获取关键词能搜索到的主题数量
* Callback:
* - err, 数据库错误
* - count, 主题数量
* @param {String} query 搜索关键词
* @param {Function} callback 回调函数
*/
exports.getCountByQuery = function (query, callback) {
Topic.countDocuments(query, callback);
};
/**
* 根据关键词,获取主题列表
* Callback:
* - err, 数据库错误
* - count, 主题列表
* @param {String} query 搜索关键词
* @param {Object} opt 搜索选项
* @param {Function} callback 回调函数
*/
exports.getTopicsByQuery = function (query, opt, callback) {
query.deleted = false;
Topic.find(query, {}, opt, function (err, topics) {
if (err) {
return callback(err);
}
if (topics.length === 0) {
return callback(null, []);
}
var proxy = new EventProxy();
proxy.after('topic_ready', topics.length, function () {
topics = _.compact(topics); // 删除不合规的 topic
return callback(null, topics);
});
proxy.fail(callback);
topics.forEach(function (topic, i) {
var ep = new EventProxy();
ep.all('author', 'reply', function (author, reply) {
// 保证顺序
// 作者可能已被删除
if (author) {
topic.author = author;
topic.reply = reply;
} else {
topics[i] = null;
}
proxy.emit('topic_ready');
});
User.getUserById(topic.author_id, ep.done('author'));
// 获取主题的最后回复
Reply.getReplyById(topic.last_reply, ep.done('reply'));
});
});
};
// for sitemap
exports.getLimit5w = function (callback) {
Topic.find({deleted: false}, '_id', {limit: 50000, sort: '-create_at'}, callback);
};
/**
* 获取所有信息的主题
* Callback:
* - err, 数据库异常
* - message, 消息
* - topic, 主题
* - author, 主题作者
* - replies, 主题的回复
* @param {String} id 主题ID
* @param {Function} callback 回调函数
*/
exports.getFullTopic = function (id, callback) {
var proxy = new EventProxy();
var events = ['topic', 'author', 'replies'];
proxy
.assign(events, function (topic, author, replies) {
callback(null, '', topic, author, replies);
})
.fail(callback);
Topic.findOne({_id: id, deleted: false}, proxy.done(function (topic) {
if (!topic) {
proxy.unbind();
return callback(null, '此话题不存在或已被删除。');
}
at.linkUsers(topic.content, proxy.done('topic', function (str) {
topic.linkedContent = str;
return topic;
}));
User.getUserById(topic.author_id, proxy.done(function (author) {
if (!author) {
proxy.unbind();
return callback(null, '话题的作者丢了。');
}
proxy.emit('author', author);
}));
Reply.getRepliesByTopicId(topic._id, proxy.done('replies'));
}));
};
/**
* 更新主题的最后回复信息
* @param {String} topicId 主题ID
* @param {String} replyId 回复ID
* @param {Function} callback 回调函数
*/
exports.updateLastReply = function (topicId, replyId, callback) {
Topic.findOne({_id: topicId}, function (err, topic) {
if (err || !topic) {
return callback(err);
}
topic.last_reply = replyId;
topic.last_reply_at = new Date();
topic.reply_count += 1;
topic.save(callback);
});
};
/**
* 根据主题ID,查找一条主题
* @param {String} id 主题ID
* @param {Function} callback 回调函数
*/
exports.getTopic = function (id, callback) {
Topic.findOne({_id: id}, callback);
};
/**
* 将当前主题的回复计数减1,并且更新最后回复的用户,删除回复时用到
* @param {String} id 主题ID
* @param {Function} callback 回调函数
*/
exports.reduceCount = function (id, callback) {
Topic.findOne({_id: id}, function (err, topic) {
if (err) {
return callback(err);
}
if (!topic) {
return callback(new Error('该主题不存在'));
}
topic.reply_count -= 1;
Reply.getLastReplyByTopId(id, function (err, reply) {
if (err) {
return callback(err);
}
if (reply.length !== 0) {
topic.last_reply = reply[0]._id;
} else {
topic.last_reply = null;
}
topic.save(callback);
});
});
};
exports.newAndSave = function (title, content, tab, authorId, callback) {
var topic = new Topic();
topic.title = title;
topic.content = content;
topic.tab = tab;
topic.author_id = authorId;
topic.save(callback);
};
================================================
FILE: proxy/topic_collect.js
================================================
var TopicCollect = require('../models').TopicCollect;
var _ = require('lodash')
exports.getTopicCollect = function (userId, topicId, callback) {
TopicCollect.findOne({user_id: userId, topic_id: topicId}, callback);
};
exports.getTopicCollectsByUserId = function (userId, opt, callback) {
var defaultOpt = {sort: '-create_at'};
opt = _.assign(defaultOpt, opt)
TopicCollect.find({user_id: userId}, '', opt, callback);
};
exports.newAndSave = function (userId, topicId, callback) {
var topic_collect = new TopicCollect();
topic_collect.user_id = userId;
topic_collect.topic_id = topicId;
topic_collect.save(callback);
};
exports.remove = function (userId, topicId, callback) {
TopicCollect.deleteOne({user_id: userId, topic_id: topicId}, callback);
};
================================================
FILE: proxy/user.js
================================================
var models = require('../models');
var User = models.User;
var utility = require('utility');
var uuid = require('node-uuid');
/**
* 根据用户名列表查找用户列表
* Callback:
* - err, 数据库异常
* - users, 用户列表
* @param {Array} names 用户名列表
* @param {Function} callback 回调函数
*/
exports.getUsersByNames = function (names, callback) {
if (names.length === 0) {
return callback(null, []);
}
User.find({ loginname: { $in: names } }, callback);
};
/**
* 根据登录名查找用户
* Callback:
* - err, 数据库异常
* - user, 用户
* @param {String} loginName 登录名
* @param {Function} callback 回调函数
*/
exports.getUserByLoginName = function (loginName, callback) {
User.findOne({'loginname': new RegExp('^'+loginName+'$', "i")}, callback);
};
/**
* 根据用户ID,查找用户
* Callback:
* - err, 数据库异常
* - user, 用户
* @param {String} id 用户ID
* @param {Function} callback 回调函数
*/
exports.getUserById = function (id, callback) {
if (!id) {
return callback();
}
User.findOne({_id: id}, callback);
};
/**
* 根据邮箱,查找用户
* Callback:
* - err, 数据库异常
* - user, 用户
* @param {String} email 邮箱地址
* @param {Function} callback 回调函数
*/
exports.getUserByMail = function (email, callback) {
User.findOne({email: email}, callback);
};
/**
* 根据用户ID列表,获取一组用户
* Callback:
* - err, 数据库异常
* - users, 用户列表
* @param {Array} ids 用户ID列表
* @param {Function} callback 回调函数
*/
exports.getUsersByIds = function (ids, callback) {
User.find({'_id': {'$in': ids}}, callback);
};
/**
* 根据关键字,获取一组用户
* Callback:
* - err, 数据库异常
* - users, 用户列表
* @param {String} query 关键字
* @param {Object} opt 选项
* @param {Function} callback 回调函数
*/
exports.getUsersByQuery = function (query, opt, callback) {
User.find(query, '', opt, callback);
};
/**
* 根据查询条件,获取一个用户
* Callback:
* - err, 数据库异常
* - user, 用户
* @param {String} name 用户名
* @param {String} key 激活码
* @param {Function} callback 回调函数
*/
exports.getUserByNameAndKey = function (loginname, key, callback) {
User.findOne({loginname: loginname, retrieve_key: key}, callback);
};
exports.newAndSave = function (name, loginname, pass, email, avatar_url, active, callback) {
var user = new User();
user.name = loginname;
user.loginname = loginname;
user.pass = pass;
user.email = email;
user.avatar = avatar_url;
user.active = active || false;
user.accessToken = uuid.v4();
user.save(callback);
};
var makeGravatar = function (email) {
return 'http://www.gravatar.com/avatar/' + utility.md5(email.toLowerCase()) + '?size=48';
};
exports.makeGravatar = makeGravatar;
exports.getGravatar = function (user) {
return user.avatar || makeGravatar(user);
};
================================================
FILE: public/github-card.html
================================================
<!doctype html><html><body>
<style type="text/css">
body{padding:0;margin:0;font-size:14px;font-family:"Helvetica Nenu",Hevetica,Arial,sans-serif;overflow:hidden}body.ready{border:1px solid #eee;border-radius:5px;border-color:#eee #ddd #bbb;box-shadow:rgba(0,0,0,.14) 0 1px 3px}.github-card{border-radius:5px;padding:8px 8px 0;background:#fff;color:#555;position:relative}.github-card a{text-decoration:none;color:#4183c4;outline:0}.github-card a:hover{text-decoration:underline}.github-card .header{position:relative}.github-card .button{position:absolute;top:0;right:8px;padding:4px 8px 4px 7px;color:#555;text-shadow:0 1px 0 #fff;border:1px solid #d4d4d4;border-radius:3px;font-size:13px;font-weight:700;line-height:14px;background-color:#e6e6e6;background-image:-webkit-linear-gradient(#fafafa,#eaeaea);background-image:-moz-linear-gradient(#fafafa,#eaeaea);background-image:-ms-linear-gradient(#fafafa,#eaeaea);background-image:linear-gradient(#fafafa,#eaeaea)}.github-card .button:hover{color:#fff;text-decoration:none;background-color:#3072b3;background-image:-webkit-linear-gradient(#599bdc,#3072b3);background-image:-moz-linear-gradient(#599bdc,#3072b3);background-image:-ms-linear-gradient(#599bdc,#3072b3);background-image:linear-gradient(#599bdc,#3072b3);border-color:#518cc6 #518cc6 #2a65a0;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.user-card .header{padding:3px 0 4px 57px;min-height:48px}.user-card .header a{color:#707070;text-decoration:none}.user-card .header a:hover strong{text-decoration:underline}.user-card img{position:absolute;top:0;left:0;width:48px;height:48px;background:#fff;border-radius:4px}.user-card strong{display:block;color:#292f33;font-size:16px;line-height:1.6}.user-card ul{text-transform:uppercase;font-size:12px;color:#707070;list-style-type:none;margin:0;padding:0;border-top:1px solid #eee;border-bottom:1px solid #eee;zoom:1}.user-card ul:after{display:block;content:'';clear:both}.user-card .status a{color:#707070;text-decoration:none}.user-card .status a:hover{color:#4183c4}.user-card .status li{float:left;padding:4px 18px;border-left:1px solid #eee}.user-card .status li:first-child{border-left:0;padding-left:0}.user-card .footer{font-size:12px;font-weight:700;padding:11px 0 10px;color:#646464}.user-card .footer a{color:#646464}.repo-card .header{padding:3px 0 4px 57px}.repo-card .avatar,.repo-card .avatar img{position:absolute;top:0;left:0;width:48px;height:48px;background:#fff;border-radius:4px}.repo-card .header a{color:#707070}.repo-card .header strong{display:block;font-size:18px;line-height:1.4}.repo-card .header strong a{color:#292f33}.repo-card .header sup{font-size:10px;margin-left:3px;color:#797979}.repo-card .content{padding:6px 0 10px}.repo-card .content p{margin:0 5px 0 0;font:18px/24px Georgia,"Times New Roman",Palatino,serif;overflow:hidden;clear:both;word-wrap:break-word}.repo-card .footer{border-top:1px solid #eee;padding:8px 0 6px}.repo-card .status{font-size:10px;padding-right:10px;text-transform:uppercase}.repo-card .status strong{font-size:12px;padding-right:5px}</style>
<script id="user-card" type="text/template"><div class="header"><a class="avatar" href="https://github.com/{login}"><img src="{avatar_url}&s=48"><strong>{name}</strong><span>@{login}</span></a><a class="button" href="https://github.com/{login}">Follow</a></div><ul class="status"><li><a href="https://github.com/{login}?tab=repositories"><strong>{public_repos}</strong>Repos</a></li><li><a href="https://gist.github.com/{login}"><strong>{public_gists}</strong>Gists</a></li><li><a href="https://github.com/{login}/followers"><strong>{followers}</strong>Followers</a></li></ul><div class="footer">{job}</div></script><script id="repo-card" type="text/template"><div class="header"><a class="avatar" href="https://github.com/{login}"><img src="{avatar_url}&s=48"></a><strong class="name"><a href="https://github.com/{full_name}">{name}</a><sup class="language">{language}</sup></strong><span>{action}<a href="https://github.com/{login}">{login}</a></span><a class="button" href="https://github.com/{full_name}">Star</a></div><div class="content"><p>{description}{homepage}</p></div><div class="footer"><span class="status"><strong>{forks_count}</strong>Forks</span><span class="status"><strong>{watchers_count}</strong>Stars</span></div></script><script>
(function(e){var r="https://api.github.com/",t;function a(){var e=window.location.href,r;var a=e.slice(e.indexOf("?")+1).split("&");var i=[];for(t=0;t<a.length;t++){r=a[t].split("=");i.push(r[0]);i[r[0]]=r[1]}return i}function i(e,r){if(window.localStorage){if(r){r._timestamp=(new Date).valueOf();localStorage[e]=JSON.stringify(r)}else{var t=localStorage[e];if(t){return JSON.parse(t)}return null}}}function n(e,r){var t=e;var a=r.split(".");for(var i=0;i<a.length;i++){if(t){t=t[a[i]]}else{break}}if(t===undefined||t===null){return""}return t}var o=a();function l(r,a){var i=e.getElementById(r+"-card");var o=/{([^}]+)}/g;var l=i.innerHTML;var s=l.match(o);for(t=0;t<s.length;t++){l=l.replace(s[t],n(a,s[t].slice(1,-1)))}return l}function s(e,r){var t=i(e);if(t&&t._timestamp){if((new Date).valueOf()-t._timestamp<1e4){return r(t)}}if(o.client_id&&o.client_secret){e+="?client_id="+o.client_id+"&client_secret="+o.client_secret}var a=new XMLHttpRequest;a.open("GET",e,false);a.onload=function(){r(JSON.parse(a.response))};a.send()}function c(r,a){var i=r.getElementsByTagName("a");for(t=0;t<i.length;t++){(function(e){e.target="_"+(o.target||"top")})(i[t])}e.body.appendChild(r);e.body.className="ready";if(parent!==self&&parent.postMessage){var n=Math.max(e.body.scrollHeight,e.documentElement.scrollHeight,e.body.offsetHeight,e.documentElement.offsetHeight,e.body.clientHeight,e.documentElement.clientHeight);parent.postMessage({height:n,sender:o.identity||"*"},"*")}}function f(t){var a=r+"users/"+t;s(a,function(r){r=r||{};var n=r.message;var o="0";if(n){r=i(a)||r;o="?"}else{i(a,r)}r.login=t;r.public_repos=r.public_repos||o;r.public_gists=r.public_gists||o;r.followers=r.followers||o;var s="Not available for hire.";if(r.hireable){var f="";if(r.email){f="mailto:"+r.email}else if(r.blog){f=r.blog}else{f=r.html_url}s='<a href="'+f+'">Available for hire.</a>'}if(n){s=n}r.job=s;var u=e.createElement("div");u.className="github-card user-card";u.innerHTML=l("user",r);c(u)})}function u(t,a){var n=r+"repos/"+t+"/"+a;s(n,function(r){r=r||{};var a=r.message;var o="0";if(a){r=i(n)||r;o="?"}else{i(n,r)}r.login=t;r.avatar_url="";if(r.owner&&r.owner.avatar_url){r.avatar_url=r.owner.avatar_url}r.forks_count=r.forks_count||o;r.watchers_count=r.watchers_count||o;if(r.fork){r.action="Forked by "}else{r.action="Created by "}var s=r.description;if(!s&&r.source){s=r.source.description}if(!s&&a){s=a}r.description=s||"No description";var f=r.homepage;if(!f&&r.source){f=r.source.homepage}if(f){r.homepage=' <a href="'+f+'">'+f.replace(/https?:\/\//,"")+"</a>"}else{r.homepage=""}var u=e.createElement("div");u.className="github-card repo-card";u.innerHTML=l("repo",r);c(u)})}function p(){}if(!o.user){p()}else if(o.repo){u(o.user,o.repo)}else{f(o.user)}})(document);var _gaq=_gaq||[];_gaq.push(['_setAccount','UA-21475122-2']);_gaq.push(['_trackPageview']);(function(d){var g=d.createElement("script");g.async=true;g.src="https://ssl.google-analytics.com/ga.js";var s=d.getElementsByTagName("script")[0];s.parentNode.insertBefore(g, s);})(document);</script></body></html>
================================================
FILE: public/javascripts/main.js
================================================
$(document).ready(function () {
var windowHeight = $(window).height();
var $backtotop = $('#backtotop');
var top = windowHeight - $backtotop.height() - 200;
function moveBacktotop() {
$backtotop.css({ top: top, right: 0});
}
function footerFixBottom() {
if($(document.body).height() < windowHeight){
$("#footer").addClass('fix-bottom');
}else{
$("#footer").removeClass('fix-bottom');
}
}
$backtotop.click(function () {
$('html,body').animate({ scrollTop: 0 });
return false;
});
$(window).scroll(function () {
var windowHeight = $(window).scrollTop();
if (windowHeight > 200) {
$backtotop.fadeIn();
} else {
$backtotop.fadeOut();
}
});
moveBacktotop();
footerFixBottom();
$(window).resize(moveBacktotop);
$(window).resize(footerFixBottom);
$('.topic_content a,.reply_content a').attr('target', '_blank');
// pretty code
prettyPrint();
// data-loading-text="提交中"
$('.submit_btn').click(function () {
$(this).button('loading');
});
// 广告的统计信息
$('.sponsor_outlink').click(function () {
var $this = $(this);
var label = $this.data('label');
ga('send', 'event', 'banner', 'click', label, 1.00, {'nonInteraction': 1});
});
});
================================================
FILE: public/javascripts/responsive.js
================================================
$(document).ready(function () {
var $responsiveBtn = $('#responsive-sidebar-trigger'),
$sidebarMask = $('#sidebar-mask'),
$sidebar = $('#sidebar'),
$main = $('#main'),
winWidth = $(window).width(),
startX = 0,
startY = 0,
delta = {
x: 0,
y: 0
},
swipeThreshold = winWidth / 3,
toggleSideBar = function () {
var isShow = $responsiveBtn.data('is-show'),
mainHeight = $main.height(),
sidebarHeight = $sidebar.outerHeight();
$sidebar.css({right: isShow ? -300 : 0});
$responsiveBtn.data('is-show', !isShow);
if (!isShow && mainHeight < sidebarHeight) {
$main.height(sidebarHeight);
}
$sidebarMask[isShow ? 'fadeOut' : 'fadeIn']().height($('body').height());
$sidebar[isShow ? 'hide' : 'show']()
},
touchstart = function (e) {
var touchs = e.targetTouches;
startX = +touchs[0].pageX;
startY = +touchs[0].pageY;
delta.x = delta.y = 0;
document.body.addEventListener('touchmove', touchmove, false);
document.body.addEventListener('touchend', touchend, false);
},
touchmove = function (e) {
var touchs = e.changedTouches;
delta.x = +touchs[0].pageX - startX;
delta.y = +touchs[0].pageY - startY;
//当水平距离大于垂直距离时,才认为是用户想滑动打开右侧栏
if (Math.abs(delta.x) > Math.abs(delta.y)) {
e.preventDefault();
}
},
touchend = function (e) {
var touchs = e.changedTouches,
isShow = $responsiveBtn.data('is-show');
delta.x = +touchs[0].pageX - startX;
//右侧栏未显示&&用户touch点在屏幕右侧1/4区域内&&move距离大于阀值时,打开右侧栏
if (!isShow && (startX > winWidth * 3 / 4) && Math.abs(delta.x) > swipeThreshold) {
$responsiveBtn.trigger('click');
}
//右侧栏显示中&&用户touch点在屏幕左侧侧1/4区域内&&move距离大于阀值时,关闭右侧栏
if (isShow && (startX < winWidth * 1 / 4) && Math.abs(delta.x) > swipeThreshold) {
$responsiveBtn.trigger('click');
}
startX = startY = 0;
delta.x = delta.y = 0;
document.body.removeEventListener('touchmove', touchmove, false);
document.body.removeEventListener('touchend', touchend, false);
};
if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
document.body.addEventListener('touchstart', touchstart);
}
$responsiveBtn.on('click', toggleSideBar);
$sidebarMask.on('click', function () {
$responsiveBtn.trigger('click');
});
});
================================================
FILE: public/libs/bootstrap/css/bootstrap-responsive.css
================================================
/*!
* Bootstrap Responsive v2.3.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
line-height: 0;
content: "";
}
.clearfix:after {
clear: both;
}
.hide-text {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.input-block-level {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
@-ms-viewport {
width: device-width;
}
.hidden {
display: none;
visibility: hidden;
}
.visible-phone {
display: none !important;
}
.visible-tablet {
display: none !important;
}
.hidden-desktop {
display: none !important;
}
.visible-desktop {
display: inherit !important;
}
@media (min-width: 768px) and (max-width: 979px) {
.hidden-desktop {
display: inherit !important;
}
.visible-desktop {
display: none !important;
}
.visible-tablet {
display: inherit !important;
}
.hidden-tablet {
display: none !important;
}
}
@media (max-width: 767px) {
.hidden-desktop {
display: inherit !important;
}
.visible-desktop {
display: none !important;
}
.visible-phone {
display: inherit !important;
}
.hidden-phone {
display: none !important;
}
}
.visible-print {
display: none !important;
}
@media print {
.visible-print {
display: inherit !important;
}
.hidden-print {
display: none !important;
}
}
@media (min-width: 1200px) {
.row {
margin-left: -30px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
line-height: 0;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 30px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 1170px;
}
.span12 {
width: 1170px;
}
.span11 {
width: 1070px;
}
.span10 {
width: 970px;
}
.span9 {
width: 870px;
}
.span8 {
width: 770px;
}
.span7 {
width: 670px;
}
.span6 {
width: 570px;
}
.span5 {
width: 470px;
}
.span4 {
width: 370px;
}
.span3 {
width: 270px;
}
.span2 {
width: 170px;
}
.span1 {
width: 70px;
}
.offset12 {
margin-left: 1230px;
}
.offset11 {
margin-left: 1130px;
}
.offset10 {
margin-left: 1030px;
}
.offset9 {
margin-left: 930px;
}
.offset8 {
margin-left: 830px;
}
.offset7 {
margin-left: 730px;
}
.offset6 {
margin-left: 630px;
}
.offset5 {
margin-left: 530px;
}
.offset4 {
margin-left: 430px;
}
.offset3 {
margin-left: 330px;
}
.offset2 {
margin-left: 230px;
}
.offset1 {
margin-left: 130px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
line-height: 0;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 30px;
margin-left: 2.564102564102564%;
*margin-left: 2.5109110747408616%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 2.564102564102564%;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.45299145299145%;
*width: 91.39979996362975%;
}
.row-fluid .span10 {
width: 82.90598290598291%;
*width: 82.8527914166212%;
}
.row-fluid .span9 {
width: 74.35897435897436%;
*width: 74.30578286961266%;
}
.row-fluid .span8 {
width: 65.81196581196582%;
*width: 65.75877432260411%;
}
.row-fluid .span7 {
width: 57.26495726495726%;
*width: 57.21176577559556%;
}
.row-fluid .span6 {
width: 48.717948717948715%;
*width: 48.664757228587014%;
}
.row-fluid .span5 {
width: 40.17094017094017%;
*width: 40.11774868157847%;
}
.row-fluid .span4 {
width: 31.623931623931625%;
*width: 31.570740134569924%;
}
.row-fluid .span3 {
width: 23.076923076923077%;
*width: 23.023731587561375%;
}
.row-fluid .span2 {
width: 14.52991452991453%;
*width: 14.476723040552828%;
}
.row-fluid .span1 {
width: 5.982905982905983%;
*width: 5.929714493544281%;
}
.row-fluid .offset12 {
margin-left: 105.12820512820512%;
*margin-left: 105.02182214948171%;
}
.row-fluid .offset12:first-child {
margin-left: 102.56410256410257%;
*margin-left: 102.45771958537915%;
}
.row-fluid .offset11 {
margin-left: 96.58119658119658%;
*margin-left: 96.47481360247316%;
}
.row-fluid .offset11:first-child {
margin-left: 94.01709401709402%;
*margin-left: 93.91071103837061%;
}
.row-fluid .offset10 {
margin-left: 88.03418803418803%;
*margin-left: 87.92780505546462%;
}
.row-fluid .offset10:first-child {
margin-left: 85.47008547008548%;
*margin-left: 85.36370249136206%;
}
.row-fluid .offset9 {
margin-left: 79.48717948717949%;
*margin-left: 79.38079650845607%;
}
.row-fluid .offset9:first-child {
margin-left: 76.92307692307693%;
*margin-left: 76.81669394435352%;
}
.row-fluid .offset8 {
margin-left: 70.94017094017094%;
*margin-left: 70.83378796144753%;
}
.row-fluid .offset8:first-child {
margin-left: 68.37606837606839%;
*margin-left: 68.26968539734497%;
}
.row-fluid .offset7 {
margin-left: 62.393162393162385%;
*margin-left: 62.28677941443899%;
}
.row-fluid .offset7:first-child {
margin-left: 59.82905982905982%;
*margin-left: 59.72267685033642%;
}
.row-fluid .offset6 {
margin-left: 53.84615384615384%;
*margin-left: 53.739770867430444%;
}
.row-fluid .offset6:first-child {
margin-left: 51.28205128205128%;
*margin-left: 51.175668303327875%;
}
.row-fluid .offset5 {
margin-left: 45.299145299145295%;
*margin-left: 45.1927623204219%;
}
.row-fluid .offset5:first-child {
margin-left: 42.73504273504273%;
*margin-left: 42.62865975631933%;
}
.row-fluid .offset4 {
margin-left: 36.75213675213675%;
*margin-left: 36.645753773413354%;
}
.row-fluid .offset4:first-child {
margin-left: 34.18803418803419%;
*margin-left: 34.081651209310785%;
}
.row-fluid .offset3 {
margin-left: 28.205128205128204%;
*margin-left: 28.0987452264048%;
}
.row-fluid .offset3:first-child {
margin-left: 25.641025641025642%;
*margin-left: 25.53464266230224%;
}
.row-fluid .offset2 {
margin-left: 19.65811965811966%;
*margin-left: 19.551736679396257%;
}
.row-fluid .offset2:first-child {
margin-left: 17.094017094017094%;
*margin-left: 16.98763411529369%;
}
.row-fluid .offset1 {
margin-left: 11.11111111111111%;
*margin-left: 11.004728132387708%;
}
.row-fluid .offset1:first-child {
margin-left: 8.547008547008547%;
*margin-left: 8.440625568285142%;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 30px;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 1156px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 1056px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 956px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 856px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 756px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 656px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 556px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 456px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 356px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 256px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 156px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 56px;
}
.thumbnails {
margin-left: -30px;
}
.thumbnails > li {
margin-left: 30px;
}
.row-fluid .thumbnails {
margin-left: 0;
}
}
@media (min-width: 768px) and (max-width: 979px) {
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
line-height: 0;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 20px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 724px;
}
.span12 {
width: 724px;
}
.span11 {
width: 662px;
}
.span10 {
width: 600px;
}
.span9 {
width: 538px;
}
.span8 {
width: 476px;
}
.span7 {
width: 414px;
}
.span6 {
width: 352px;
}
.span5 {
width: 290px;
}
.span4 {
width: 228px;
}
.span3 {
width: 166px;
}
.span2 {
width: 104px;
}
.span1 {
width: 42px;
}
.offset12 {
margin-left: 764px;
}
.offset11 {
margin-left: 702px;
}
.offset10 {
margin-left: 640px;
}
.offset9 {
margin-left: 578px;
}
.offset8 {
margin-left: 516px;
}
.offset7 {
margin-left: 454px;
}
.offset6 {
margin-left: 392px;
}
.offset5 {
margin-left: 330px;
}
.offset4 {
margin-left: 268px;
}
.offset3 {
margin-left: 206px;
}
.offset2 {
margin-left: 144px;
}
.offset1 {
margin-left: 82px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
line-height: 0;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 30px;
margin-left: 2.7624309392265194%;
*margin-left: 2.709239449864817%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 2.7624309392265194%;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.43646408839778%;
*width: 91.38327259903608%;
}
.row-fluid .span10 {
width: 82.87292817679558%;
*width: 82.81973668743387%;
}
.row-fluid .span9 {
width: 74.30939226519337%;
*width: 74.25620077583166%;
}
.row-fluid .span8 {
width: 65.74585635359117%;
*width: 65.69266486422946%;
}
.row-fluid .span7 {
width: 57.18232044198895%;
*width: 57.12912895262725%;
}
.row-fluid .span6 {
width: 48.61878453038674%;
*width: 48.56559304102504%;
}
.row-fluid .span5 {
width: 40.05524861878453%;
*width: 40.00205712942283%;
}
.row-fluid .span4 {
width: 31.491712707182323%;
*width: 31.43852121782062%;
}
.row-fluid .span3 {
width: 22.92817679558011%;
*width: 22.87498530621841%;
}
.row-fluid .span2 {
width: 14.3646408839779%;
*width: 14.311449394616199%;
}
.row-fluid .span1 {
width: 5.801104972375691%;
*width: 5.747913483013988%;
}
.row-fluid .offset12 {
margin-left: 105.52486187845304%;
*margin-left: 105.41847889972962%;
}
.row-fluid .offset12:first-child {
margin-left: 102.76243093922652%;
*margin-left: 102.6560479605031%;
}
.row-fluid .offset11 {
margin-left: 96.96132596685082%;
*margin-left: 96.8549429881274%;
}
.row-fluid .offset11:first-child {
margin-left: 94.1988950276243%;
*margin-left: 94.09251204890089%;
}
.row-fluid .offset10 {
margin-left: 88.39779005524862%;
*margin-left: 88.2914070765252%;
}
.row-fluid .offset10:first-child {
margin-left: 85.6353591160221%;
*margin-left: 85.52897613729868%;
}
.row-fluid .offset9 {
margin-left: 79.8342541436464%;
*margin-left: 79.72787116492299%;
}
.row-fluid .offset9:first-child {
margin-left: 77.07182320441989%;
*margin-left: 76.96544022569647%;
}
.row-fluid .offset8 {
margin-left: 71.2707182320442%;
*margin-left: 71.16433525332079%;
}
.row-fluid .offset8:first-child {
margin-left: 68.50828729281768%;
*margin-left: 68.40190431409427%;
}
.row-fluid .offset7 {
margin-left: 62.70718232044199%;
*margin-left: 62.600799341718584%;
}
.row-fluid .offset7:first-child {
margin-left: 59.94475138121547%;
*margin-left: 59.838368402492065%;
}
.row-fluid .offset6 {
margin-left: 54.14364640883978%;
*margin-left: 54.037263430116376%;
}
.row-fluid .offset6:first-child {
margin-left: 51.38121546961326%;
*margin-left: 51.27483249088986%;
}
.row-fluid .offset5 {
margin-left: 45.58011049723757%;
*margin-left: 45.47372751851417%;
}
.row-fluid .offset5:first-child {
margin-left: 42.81767955801105%;
*margin-left: 42.71129657928765%;
}
.row-fluid .offset4 {
margin-left: 37.01657458563536%;
*margin-left: 36.91019160691196%;
}
.row-fluid .offset4:first-child {
margin-left: 34.25414364640884%;
*margin-left: 34.14776066768544%;
}
.row-fluid .offset3 {
margin-left: 28.45303867403315%;
*margin-left: 28.346655695309746%;
}
.row-fluid .offset3:first-child {
margin-left: 25.69060773480663%;
*margin-left: 25.584224756083227%;
}
.row-fluid .offset2 {
margin-left: 19.88950276243094%;
*margin-left: 19.783119783707537%;
}
.row-fluid .offset2:first-child {
margin-left: 17.12707182320442%;
*margin-left: 17.02068884448102%;
}
.row-fluid .offset1 {
margin-left: 11.32596685082873%;
*margin-left: 11.219583872105325%;
}
.row-fluid .offset1:first-child {
margin-left: 8.56353591160221%;
*margin-left: 8.457152932878806%;
}
input,
textarea,
.uneditable-input {
margin-left: 0;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 20px;
}
input.span12,
textarea.span12,
.uneditable-input.span12 {
width: 710px;
}
input.span11,
textarea.span11,
.uneditable-input.span11 {
width: 648px;
}
input.span10,
textarea.span10,
.uneditable-input.span10 {
width: 586px;
}
input.span9,
textarea.span9,
.uneditable-input.span9 {
width: 524px;
}
input.span8,
textarea.span8,
.uneditable-input.span8 {
width: 462px;
}
input.span7,
textarea.span7,
.uneditable-input.span7 {
width: 400px;
}
input.span6,
textarea.span6,
.uneditable-input.span6 {
width: 338px;
}
input.span5,
textarea.span5,
.uneditable-input.span5 {
width: 276px;
}
input.span4,
textarea.span4,
.uneditable-input.span4 {
width: 214px;
}
input.span3,
textarea.span3,
.uneditable-input.span3 {
width: 152px;
}
input.span2,
textarea.span2,
.uneditable-input.span2 {
width: 90px;
}
input.span1,
textarea.span1,
.uneditable-input.span1 {
width: 28px;
}
}
@media (max-width: 767px) {
body {
padding-right: 20px;
padding-left: 20px;
}
.navbar-fixed-top,
.navbar-fixed-bottom,
.navbar-static-top {
margin-right: -20px;
margin-left: -20px;
}
.container-fluid {
padding: 0;
}
.dl-horizontal dt {
float: none;
width: auto;
clear: none;
text-align: left;
}
.dl-horizontal dd {
margin-left: 0;
}
.container {
width: auto;
}
.row-fluid {
width: 100%;
}
.row,
.thumbnails {
margin-left: 0;
}
.thumbnails > li {
float: none;
margin-left: 0;
}
[class*="span"],
.uneditable-input[class*="span"],
.row-fluid [class*="span"] {
display: block;
float: none;
width: 100%;
margin-left: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.span12,
.row-fluid .span12 {
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="offset"]:first-child {
margin-left: 0;
}
.input-large,
.input-xlarge,
.input-xxlarge,
input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.input-prepend input,
.input-append input,
.input-prepend input[class*="span"],
.input-append input[class*="span"] {
display: inline-block;
width: auto;
}
.controls-row [class*="span"] + [class*="span"] {
margin-left: 0;
}
.modal {
position: fixed;
top: 20px;
right: 20px;
left: 20px;
width: auto;
margin: 0;
}
.modal.fade {
top: -100px;
}
.modal.fade.in {
top: 20px;
}
}
@media (max-width: 480px) {
.nav-collapse {
-webkit-transform: translate3d(0, 0, 0);
}
.page-header h1 small {
display: block;
line-height: 20px;
}
input[type="checkbox"],
input[type="radio"] {
border: 1px solid #ccc;
}
.form-horizontal .control-label {
float: none;
width: auto;
padding-top: 0;
text-align: left;
}
.form-horizontal .controls {
margin-left: 0;
}
.form-horizontal .control-list {
padding-top: 0;
}
.form-horizontal .form-actions {
padding-right: 10px;
padding-left: 10px;
}
.media .pull-left,
.media .pull-right {
display: block;
float: none;
margin-bottom: 10px;
}
.media-object {
margin-right: 0;
margin-left: 0;
}
.modal {
top: 10px;
right: 10px;
left: 10px;
}
.modal-header .close {
padding: 10px;
margin: -10px;
}
.carousel-caption {
position: static;
}
}
@media (max-width: 979px) {
body {
padding-top: 0;
}
.navbar-fixed-top,
.navbar-fixed-bottom {
position: static;
}
.navbar-fixed-top {
margin-bottom: 20px;
}
.navbar-fixed-bottom {
margin-top: 20px;
}
.navbar-fixed-top .navbar-inner,
.navbar-fixed-bottom .navbar-inner {
padding: 5px;
}
.navbar .container {
width: auto;
padding: 0;
}
.navbar .brand {
padding-right: 10px;
padding-left: 10px;
margin: 0 0 0 -5px;
}
.nav-collapse {
clear: both;
}
.nav-collapse .nav {
float: none;
margin: 0 0 10px;
}
.nav-collapse .nav > li {
float: none;
}
.nav-collapse .nav > li > a {
margin-bottom: 2px;
}
.nav-collapse .nav > .divider-vertical {
display: none;
}
.nav-collapse .nav .nav-header {
color: #777777;
text-shadow: none;
}
.nav-collapse .nav > li > a,
.nav-collapse .dropdown-menu a {
padding: 9px 15px;
font-weight: bold;
color: #777777;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.nav-collapse .btn {
padding: 4px 10px 4px;
font-weight: normal;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.nav-collapse .dropdown-menu li + li a {
margin-bottom: 2px;
}
.nav-collapse .nav > li > a:hover,
.nav-collapse .nav > li > a:focus,
.nav-collapse .dropdown-menu a:hover,
.nav-collapse .dropdown-menu a:focus {
background-color: #f2f2f2;
}
.navbar-inverse .nav-collapse .nav > li > a,
.navbar-inverse .nav-collapse .dropdown-menu a {
color: #999999;
}
.navbar-inverse .nav-collapse .nav > li > a:hover,
.navbar-inverse .nav-collapse .nav > li > a:focus,
.navbar-inverse .nav-collapse .dropdown-menu a:hover,
.navbar-inverse .nav-collapse .dropdown-menu a:focus {
background-color: #111111;
}
.nav-collapse.in .btn-group {
padding: 0;
margin-top: 5px;
}
.nav-collapse .dropdown-menu {
position: static;
top: auto;
left: auto;
display: none;
float: none;
max-width: none;
padding: 0;
margin: 0 15px;
background-color: transparent;
border: none;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.nav-collapse .open > .dropdown-menu {
display: block;
}
.nav-collapse .dropdown-menu:before,
.nav-collapse .dropdown-menu:after {
display: none;
}
.nav-collapse .dropdown-menu .divider {
display: none;
}
.nav-collapse .nav > li > .dropdown-menu:before,
.nav-collapse .nav > li > .dropdown-menu:after {
display: none;
}
.nav-collapse .navbar-form,
.nav-collapse .navbar-search {
float: none;
padding: 10px 15px;
margin: 10px 0;
border-top: 1px solid #f2f2f2;
border-bottom: 1px solid #f2f2f2;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
}
.navbar-inverse .nav-collapse .navbar-form,
.navbar-inverse .nav-collapse .navbar-search {
border-top-color: #111111;
border-bottom-color: #111111;
}
.navbar .nav-collapse .nav.pull-right {
float: none;
margin-left: 0;
}
.nav-collapse,
.nav-collapse.collapse {
height: 0;
overflow: hidden;
}
.navbar .btn-navbar {
display: block;
}
.navbar-static .navbar-inner {
padding-right: 10px;
padding-left: 10px;
}
}
@media (min-width: 980px) {
.nav-collapse.collapse {
height: auto !important;
overflow: visible !important;
}
}
================================================
FILE: public/libs/bootstrap/css/bootstrap.css
================================================
/*!
* Bootstrap v2.3.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
line-height: 0;
content: "";
}
.clearfix:after {
clear: both;
}
.hide-text {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.input-block-level {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section {
display: block;
}
audio,
canvas,
video {
display: inline-block;
*display: inline;
*zoom: 1;
}
audio:not([controls]) {
display: none;
}
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
a:focus {
outline: thin dotted #333;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
a:hover,
a:active {
outline: 0;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
img {
width: auto\9;
height: auto;
max-width: 100%;
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
}
#map_canvas img,
.google-maps img {
max-width: none;
}
button,
input,
select,
textarea {
margin: 0;
font-size: 100%;
vertical-align: middle;
}
button,
input {
*overflow: visible;
line-height: normal;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
cursor: pointer;
-webkit-appearance: button;
}
label,
select,
button,
input[type="button"],
input[type="reset"],
input[type="submit"],
input[type="radio"],
input[type="checkbox"] {
cursor: pointer;
}
input[type="search"] {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
textarea {
overflow: auto;
vertical-align: top;
}
@media print {
* {
color: #000 !important;
text-shadow: none !important;
background: transparent !important;
box-shadow: none !important;
}
a,
a:visited {
text-decoration: underline;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
page-break-inside: avoid;
}
thead {
display: table-header-group;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
@page {
margin: 0.5cm;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333333;
background-color: #ffffff;
}
a {
color: #0088cc;
text-decoration: none;
}
a:hover,
a:focus {
color: #005580;
text-decoration: underline;
}
.img-rounded {
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
.img-polaroid {
padding: 4px;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.img-circle {
-webkit-border-radius: 500px;
-moz-border-radius: 500px;
border-radius: 500px;
}
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
line-height: 0;
content: "";
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 20px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 940px;
}
.span12 {
width: 940px;
}
.span11 {
width: 860px;
}
.span10 {
width: 780px;
}
.span9 {
width: 700px;
}
.span8 {
width: 620px;
}
.span7 {
width: 540px;
}
.span6 {
width: 460px;
}
.span5 {
width: 380px;
}
.span4 {
width: 300px;
}
.span3 {
width: 220px;
}
.span2 {
width: 140px;
}
.span1 {
width: 60px;
}
.offset12 {
margin-left: 980px;
}
.offset11 {
margin-left: 900px;
}
.offset10 {
margin-left: 820px;
}
.offset9 {
margin-left: 740px;
}
.offset8 {
margin-left: 660px;
}
.offset7 {
margin-left: 580px;
}
.offset6 {
margin-left: 500px;
}
.offset5 {
margin-left: 420px;
}
.offset4 {
margin-left: 340px;
}
.offset3 {
margin-left: 260px;
}
.offset2 {
margin-left: 180px;
}
.offset1 {
margin-left: 100px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
line-height: 0;
content: "";
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
float: left;
width: 100%;
min-height: 30px;
margin-left: 2.127659574468085%;
*margin-left: 2.074468085106383%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 2.127659574468085%;
}
.row-fluid .span12 {
width: 100%;
*width: 99.94680851063829%;
}
.row-fluid .span11 {
width: 91.48936170212765%;
*width: 91.43617021276594%;
}
.row-fluid .span10 {
width: 82.97872340425532%;
*width: 82.92553191489361%;
}
.row-fluid .span9 {
width: 74.46808510638297%;
*width: 74.41489361702126%;
}
.row-fluid .span8 {
width: 65.95744680851064%;
*width: 65.90425531914893%;
}
.row-fluid .span7 {
width: 57.44680851063829%;
*width: 57.39361702127659%;
}
.row-fluid .span6 {
width: 48.93617021276595%;
*width: 48.88297872340425%;
}
.row-fluid .span5 {
width: 40.42553191489362%;
*width: 40.37234042553192%;
}
.row-fluid .span4 {
width: 31.914893617021278%;
*width: 31.861702127659576%;
}
.row-fluid .span3 {
width: 23.404255319148934%;
*width: 23.351063829787233%;
}
.row-fluid .span2 {
width: 14.893617021276595%;
*width: 14.840425531914894%;
}
.row-fluid .span1 {
width: 6.382978723404255%;
*width: 6.329787234042553%;
}
.row-fluid .offset12 {
margin-left: 104.25531914893617%;
*margin-left: 104.14893617021275%;
}
.row-fluid .offset12:first-child {
margin-left: 102.12765957446808%;
*margin-left: 102.02127659574467%;
}
.row-fluid .offset11 {
margin-left: 95.74468085106382%;
*margin-left: 95.6382978723404%;
}
.row-fluid .offset11:first-child {
margin-left: 93.61702127659574%;
*margin-left: 93.51063829787232%;
}
.row-fluid .offset10 {
margin-left: 87.23404255319149%;
*margin-left: 87.12765957446807%;
}
.row-fluid .offset10:first-child {
margin-left: 85.1063829787234%;
*margin-left: 84.99999999999999%;
}
.row-fluid .offset9 {
margin-left: 78.72340425531914%;
*margin-left: 78.61702127659572%;
}
.row-fluid .offset9:first-child {
margin-left: 76.59574468085106%;
*margin-left: 76.48936170212764%;
}
.row-fluid .offset8 {
margin-left: 70.2127659574468%;
*margin-left: 70.10638297872339%;
}
.row-fluid .offset8:first-child {
margin-left: 68.08510638297872%;
*margin-left: 67.9787234042553%;
}
.row-fluid .offset7 {
margin-left: 61.70212765957446%;
*margin-left: 61.59574468085106%;
}
.row-fluid .offset7:first-child {
margin-left: 59.574468085106375%;
*margin-left: 59.46808510638297%;
}
.row-fluid .offset6 {
margin-left: 53.191489361702125%;
*margin-left: 53.085106382978715%;
}
.row-fluid .offset6:first-child {
margin-left: 51.063829787234035%;
*margin-left: 50.95744680851063%;
}
.row-fluid .offset5 {
margin-left: 44.68085106382979%;
*margin-left: 44.57446808510638%;
}
.row-fluid .offset5:first-child {
margin-left: 42.5531914893617%;
*margin-left: 42.4468085106383%;
}
.row-fluid .offset4 {
margin-left: 36.170212765957444%;
*margin-left: 36.06382978723405%;
}
.row-fluid .offset4:first-child {
margin-left: 34.04255319148936%;
*margin-left: 33.93617021276596%;
}
.row-fluid .offset3 {
margin-left: 27.659574468085104%;
*margin-left: 27.5531914893617%;
}
.row-fluid .offset3:first-child {
margin-left: 25.53191489361702%;
*margin-left: 25.425531914893618%;
}
.row-fluid .offset2 {
margin-left: 19.148936170212764%;
*margin-left: 19.04255319148936%;
}
.row-fluid .offset2:first-child {
margin-left: 17.02127659574468%;
*margin-left: 16.914893617021278%;
}
.row-fluid .offset1 {
margin-left: 10.638297872340425%;
*margin-left: 10.53191489361702%;
}
.row-fluid .offset1:first-child {
margin-left: 8.51063829787234%;
*margin-left: 8.404255319148938%;
}
[class*="span"].hide,
.row-fluid [class*="span"].hide {
display: none;
}
[class*="span"].pull-right,
.row-fluid [class*="span"].pull-right {
float: right;
}
.container {
margin-right: auto;
margin-left: auto;
*zoom: 1;
}
.container:before,
.container:after {
display: table;
line-height: 0;
content: "";
}
.container:after {
clear: both;
}
.container-fluid {
padding-right: 20px;
padding-left: 20px;
*zoom: 1;
}
.container-fluid:before,
.container-fluid:after {
display: table;
line-height: 0;
content: "";
}
.container-fluid:after {
clear: both;
}
p {
margin: 0 0 10px;
}
.lead {
margin-bottom: 20px;
font-size: 21px;
font-weight: 200;
line-height: 30px;
}
small {
font-size: 85%;
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
cite {
font-style: normal;
}
.muted {
color: #999999;
}
a.muted:hover,
a.muted:focus {
color: #808080;
}
.text-warning {
color: #c09853;
}
a.text-warning:hover,
a.text-warning:focus {
color: #a47e3c;
}
.text-error {
color: #b94a48;
}
a.text-error:hover,
a.text-error:focus {
color: #953b39;
}
.text-info {
color: #3a87ad;
}
a.text-info:hover,
a.text-info:focus {
color: #2d6987;
}
.text-success {
color: #468847;
}
a.text-success:hover,
a.text-success:focus {
color: #356635;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 10px 0;
font-family: inherit;
font-weight: bold;
line-height: 20px;
color: inherit;
text-rendering: optimizelegibility;
}
h1 small,
h2 small,
h3 small,
h4 small,
h5 small,
h6 small {
font-weight: normal;
line-height: 1;
color: #999999;
}
h1,
h2,
h3 {
line-height: 40px;
}
h1 {
font-size: 38.5px;
}
h2 {
font-size: 31.5px;
}
h3 {
font-size: 24.5px;
}
h4 {
font-size: 17.5px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 11.9px;
}
h1 small {
font-size: 24.5px;
}
h2 small {
font-size: 17.5px;
}
h3 small {
font-size: 14px;
}
h4 small {
font-size: 14px;
}
.page-header {
padding-bottom: 9px;
margin: 20px 0 30px;
border-bottom: 1px solid #eeeeee;
}
ul,
ol {
padding: 0;
margin: 0 0 10px 25px;
}
ul ul,
ul ol,
ol ol,
ol ul {
margin-bottom: 0;
}
li {
line-height: 20px;
}
ul.unstyled,
ol.unstyled {
margin-left: 0;
list-style: none;
}
ul.inline,
ol.inline {
margin-left: 0;
list-style: none;
}
ul.inline > li,
ol.inline > li {
display: inline-block;
*display: inline;
padding-right: 5px;
padding-left: 5px;
*zoom: 1;
}
dl {
margin-bottom: 20px;
}
dt,
dd {
line-height: 20px;
}
dt {
font-weight: bold;
}
dd {
margin-left: 10px;
}
.dl-horizontal {
*zoom: 1;
}
.dl-horizontal:before,
.dl-horizontal:after {
display: table;
line-height: 0;
content: "";
}
.dl-horizontal:after {
clear: both;
}
.dl-horizontal dt {
float: left;
width: 160px;
overflow: hidden;
clear: left;
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.dl-horizontal dd {
margin-left: 180px;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px solid #eeeeee;
border-bottom: 1px solid #ffffff;
}
abbr[title],
abbr[data-original-title] {
cursor: help;
border-bottom: 1px dotted #999999;
}
abbr.initialism {
font-size: 90%;
text-transform: uppercase;
}
blockquote {
padding: 0 0 0 15px;
margin: 0 0 20px;
border-left: 5px solid #eeeeee;
}
blockquote p {
margin-bottom: 0;
font-size: 17.5px;
font-weight: 300;
line-height: 1.25;
}
blockquote small {
display: block;
line-height: 20px;
color: #999999;
}
blockquote small:before {
content: '\2014 \00A0';
}
blockquote.pull-right {
float: right;
padding-right: 15px;
padding-left: 0;
border-right: 5px solid #eeeeee;
border-left: 0;
}
blockquote.pull-right p,
blockquote.pull-right small {
text-align: right;
}
blockquote.pull-right small:before {
content: '';
}
blockquote.pull-right small:after {
content: '\00A0 \2014';
}
q:before,
q:after,
blockquote:before,
blockquote:after {
content: "";
}
address {
display: block;
margin-bottom: 20px;
font-style: normal;
line-height: 20px;
}
code,
pre {
padding: 0 3px 2px;
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
font-size: 12px;
color: #333333;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
code {
padding: 2px 4px;
color: #d14;
white-space: nowrap;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
}
pre {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
line-height: 20px;
word-break: break-all;
word-wrap: break-word;
white-space: pre;
white-space: pre-wrap;
background-color: #f5f5f5;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.15);
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
pre.prettyprint {
margin-bottom: 20px;
}
pre code {
padding: 0;
color: inherit;
white-space: pre;
white-space: pre-wrap;
background-color: transparent;
border: 0;
}
.pre-scrollable {
max-height: 340px;
overflow-y: scroll;
}
form {
margin: 0 0 20px;
}
fieldset {
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: 20px;
font-size: 21px;
line-height: 40px;
color: #333333;
border: 0;
border-bottom: 1px solid #e5e5e5;
}
legend small {
font-size: 15px;
color: #999999;
}
label,
input,
button,
select,
textarea {
font-size: 14px;
font-weight: normal;
line-height: 20px;
}
input,
button,
select,
textarea {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
label {
display: block;
margin-bottom: 5px;
}
select,
textarea,
input[type="text"],
input[type="password"],
input[type="datetime"],
input[type="datetime-local"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="week"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="search"],
input[type="tel"],
input[type="color"],
.uneditable-input {
display: inline-block;
height: 20px;
padding: 4px 6px;
margin-bottom: 10px;
font-size: 14px;
line-height: 20px;
color: #555555;
vertical-align: middle;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
input,
textarea,
.uneditable-input {
width: 206px;
}
textarea {
height: auto;
}
textarea,
input[type="text"],
input[type="password"],
input[type="datetime"],
input[type="datetime-local"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="week"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="search"],
input[type="tel"],
input[type="color"],
.uneditable-input {
background-color: #ffffff;
border: 1px solid #cccccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-o-transition: border linear 0.2s, box-shadow linear 0.2s;
gitextract_iz_2t4kk/ ├── .gitignore ├── .jshintrc ├── .mention-bot ├── .snyk ├── .travis.yml ├── History.md ├── LICENSE ├── Makefile ├── README.md ├── api/ │ └── v1/ │ ├── message.js │ ├── middleware.js │ ├── reply.js │ ├── tools.js │ ├── topic.js │ ├── topic_collect.js │ └── user.js ├── api_router_v1.js ├── app.js ├── bin/ │ ├── fix_at_problem.js │ ├── fix_topic_collect_count.js │ ├── generate_accesstoken.js │ └── get_user_topics.js ├── common/ │ ├── at.js │ ├── cache.js │ ├── logger.js │ ├── mail.js │ ├── message.js │ ├── redis.js │ ├── render_helper.js │ ├── store.js │ ├── store_local.js │ ├── store_qn.js │ └── tools.js ├── config.default.js ├── controllers/ │ ├── github.js │ ├── message.js │ ├── reply.js │ ├── rss.js │ ├── search.js │ ├── sign.js │ ├── site.js │ ├── static.js │ ├── topic.js │ └── user.js ├── logs/ │ └── .gitkeep ├── middlewares/ │ ├── auth.js │ ├── conf.js │ ├── error_page.js │ ├── github_strategy.js │ ├── limit.js │ ├── mongoose_log.js │ ├── proxy.js │ ├── render.js │ └── request_log.js ├── models/ │ ├── base_model.js │ ├── index.js │ ├── message.js │ ├── reply.js │ ├── topic.js │ ├── topic_collect.js │ └── user.js ├── oneapm.js ├── package.json ├── proxy/ │ ├── index.js │ ├── message.js │ ├── reply.js │ ├── topic.js │ ├── topic_collect.js │ └── user.js ├── public/ │ ├── github-card.html │ ├── javascripts/ │ │ ├── main.js │ │ └── responsive.js │ ├── libs/ │ │ ├── bootstrap/ │ │ │ ├── css/ │ │ │ │ ├── bootstrap-responsive.css │ │ │ │ └── bootstrap.css │ │ │ └── js/ │ │ │ └── bootstrap.js │ │ ├── code-prettify/ │ │ │ ├── lang-apollo.js │ │ │ ├── lang-clj.js │ │ │ ├── lang-css.js │ │ │ ├── lang-go.js │ │ │ ├── lang-hs.js │ │ │ ├── lang-lisp.js │ │ │ ├── lang-lua.js │ │ │ ├── lang-ml.js │ │ │ ├── lang-n.js │ │ │ ├── lang-proto.js │ │ │ ├── lang-scala.js │ │ │ ├── lang-sql.js │ │ │ ├── lang-tex.js │ │ │ ├── lang-vb.js │ │ │ ├── lang-vhdl.js │ │ │ ├── lang-wiki.js │ │ │ ├── lang-xq.js │ │ │ ├── lang-yaml.js │ │ │ ├── prettify.css │ │ │ └── prettify.js │ │ ├── editor/ │ │ │ ├── editor.css │ │ │ ├── editor.js │ │ │ └── ext.js │ │ ├── font-awesome/ │ │ │ ├── css/ │ │ │ │ └── font-awesome.css │ │ │ └── fonts/ │ │ │ └── FontAwesome.otf │ │ ├── jquery-2.1.0.js │ │ ├── jquery-ujs.js │ │ ├── jquery.atwho.js │ │ ├── jquery.caret.js │ │ ├── lodash.compat.js │ │ ├── markdownit.js │ │ ├── qrcode.js │ │ └── webuploader/ │ │ ├── Uploader.swf │ │ ├── webuploader.css │ │ └── webuploader.withoutimage.js │ └── stylesheets/ │ ├── common.css │ ├── jquery.atwho.css │ ├── responsive.css │ └── style.less ├── test/ │ ├── api/ │ │ └── v1/ │ │ ├── message.test.js │ │ ├── reply.test.js │ │ ├── tools.test.js │ │ ├── topic.test.js │ │ ├── topic_collect.test.js │ │ └── user.test.js │ ├── app.test.js │ ├── common/ │ │ ├── at.test.js │ │ ├── cache.test.js │ │ ├── mail.test.js │ │ ├── message.test.js │ │ ├── render_helper.test.js │ │ ├── store_local.test.js │ │ └── tools.test.js │ ├── controllers/ │ │ ├── github.test.js │ │ ├── message.test.js │ │ ├── reply.test.js │ │ ├── rss.test.js │ │ ├── search.test.js │ │ ├── sign.test.js │ │ ├── site.test.js │ │ ├── static.test.js │ │ ├── topic.test.js │ │ └── user.test.js │ ├── env.js │ ├── middlewares/ │ │ ├── conf.test.js │ │ ├── limit.test.js │ │ └── proxy.test.js │ ├── models/ │ │ └── user.test.js │ ├── proxy/ │ │ ├── message.test.js │ │ ├── reply.test.js │ │ ├── topic.test.js │ │ └── user.test.js │ └── support/ │ └── support.js ├── views/ │ ├── _ads.html │ ├── _sponsors.html │ ├── editor_sidebar.html │ ├── includes/ │ │ └── editor.html │ ├── index.html │ ├── layout.html │ ├── message/ │ │ ├── index.html │ │ └── message.html │ ├── notify/ │ │ └── notify.html │ ├── reply/ │ │ ├── edit.html │ │ └── reply.html │ ├── sidebar.html │ ├── sign/ │ │ ├── new_oauth.html │ │ ├── no_github_email.html │ │ ├── reset.html │ │ ├── search_pass.html │ │ ├── sidebar.html │ │ ├── signin.html │ │ └── signup.html │ ├── static/ │ │ ├── about.html │ │ ├── api.html │ │ ├── faq.html │ │ └── getstart.html │ ├── topic/ │ │ ├── _top_good.html │ │ ├── abstract.html │ │ ├── edit.html │ │ ├── index.html │ │ ├── list.html │ │ └── small.html │ └── user/ │ ├── card.html │ ├── collect_topics.html │ ├── followers.html │ ├── followings.html │ ├── index.html │ ├── replies.html │ ├── setting.html │ ├── star.html │ ├── stars.html │ ├── top.html │ ├── top100.html │ ├── top100_user.html │ ├── topics.html │ └── user.html └── web_router.js
SYMBOL INDEX (490 symbols across 20 files)
FILE: api/v1/topic_collect.js
function list (line 8) | function list(req, res, next) {
function collect (line 52) | function collect(req, res, next) {
function de_collect (line 100) | function de_collect(req, res, next) {
FILE: bin/fix_at_problem.js
function fix (line 13) | function fix(str) {
FILE: config.default.js
method mini_assets (line 11) | get mini_assets() { return !this.debug; }
FILE: controllers/rss.js
function utf8ForXml (line 61) | function utf8ForXml(inputStr) {
FILE: controllers/topic.js
function isUped (line 31) | function isUped(user, reply) {
FILE: controllers/user.js
function showMessage (line 103) | function showMessage(msg, data, isSuccess) {
FILE: middlewares/auth.js
function gen_session (line 48) | function gen_session(user, res) {
FILE: public/javascripts/main.js
function moveBacktotop (line 7) | function moveBacktotop() {
function footerFixBottom (line 11) | function footerFixBottom() {
FILE: public/libs/bootstrap/js/bootstrap.js
function removeElement (line 113) | function removeElement() {
function clearMenus (line 713) | function clearMenus() {
function getParent (line 719) | function getParent($this) {
function removeWithAnimation (line 1219) | function removeWithAnimation() {
function ScrollSpy (line 1444) | function ScrollSpy(element, options) {
function next (line 1649) | function next() {
FILE: public/libs/code-prettify/prettify.js
function L (line 4) | function L(a) {
function M (line 78) | function M(a) {
function B (line 101) | function B(a, m, e, h) {
function x (line 105) | function x(a, m) {
function u (line 154) | function u(a) {
function D (line 173) | function D(a, m) {
function k (line 225) | function k(a, m) {
function C (line 232) | function C(a, m) {
function E (line 237) | function E(a) {
function m (line 342) | function m() {
FILE: public/libs/editor/editor.js
function CodeMirror (line 44) | function CodeMirror(place, options) {
function makeDisplay (line 96) | function makeDisplay(place, docStart) {
function loadMode (line 195) | function loadMode(cm) {
function wrappingChanged (line 207) | function wrappingChanged(cm) {
function estimateHeight (line 221) | function estimateHeight(cm) {
function estimateLineHeights (line 234) | function estimateLineHeights(cm) {
function keyMapChanged (line 242) | function keyMapChanged(cm) {
function themeChanged (line 249) | function themeChanged(cm) {
function guttersChanged (line 255) | function guttersChanged(cm) {
function updateGutters (line 261) | function updateGutters(cm) {
function lineLength (line 275) | function lineLength(doc, line) {
function computeMaxLength (line 293) | function computeMaxLength(cm) {
function setGuttersForLineNumbers (line 309) | function setGuttersForLineNumbers(options) {
function updateScrollbars (line 325) | function updateScrollbars(cm) {
function visibleLines (line 359) | function visibleLines(display, doc, viewPort) {
function alignHorizontally (line 370) | function alignHorizontally(cm) {
function maybeUpdateLineNumberWidth (line 382) | function maybeUpdateLineNumberWidth(cm) {
function lineNumberFor (line 399) | function lineNumberFor(options, i) {
function compensateForHScroll (line 402) | function compensateForHScroll(display) {
function updateDisplay (line 408) | function updateDisplay(cm, changes, viewPort, forced) {
function updateDisplayInner (line 439) | function updateDisplayInner(cm, changes, visible, forced) {
function updateHeightsInViewport (line 532) | function updateHeightsInViewport(cm) {
function updateViewOffset (line 555) | function updateViewOffset(cm) {
function computeIntact (line 561) | function computeIntact(intact, changes) {
function getDimensions (line 582) | function getDimensions(cm) {
function patchDisplay (line 595) | function patchDisplay(cm, from, to, intact, updateNumbersFrom) {
function buildLineElement (line 662) | function buildLineElement(cm, line, lineNo, dims, reuse) {
function positionLineWidget (line 739) | function positionLineWidget(widget, node, wrap, dims) {
function updateSelection (line 759) | function updateSelection(cm) {
function updateSelectionCursor (line 783) | function updateSelectionCursor(cm) {
function updateSelectionRange (line 799) | function updateSelectionRange(cm) {
function restartBlink (line 871) | function restartBlink(cm) {
function startWorker (line 884) | function startWorker(cm, time) {
function highlightWorker (line 889) | function highlightWorker(cm) {
function findStartLine (line 929) | function findStartLine(cm, n, precise) {
function getStateBefore (line 944) | function getStateBefore(cm, n, precise) {
function paddingTop (line 961) | function paddingTop(display) {return display.lineSpace.offsetTop;}
function paddingVert (line 962) | function paddingVert(display) {return display.mover.offsetHeight - displ...
function paddingLeft (line 963) | function paddingLeft(display) {
function measureChar (line 968) | function measureChar(cm, line, ch, data, bias) {
function findCachedMeasurement (line 986) | function findCachedMeasurement(cm, line) {
function clearCachedMeasurement (line 997) | function clearCachedMeasurement(cm, line) {
function measureLine (line 1002) | function measureLine(cm, line) {
function measureLineInner (line 1018) | function measureLineInner(cm, line) {
function measureLineWidth (line 1104) | function measureLineWidth(cm, line) {
function clearCaches (line 1119) | function clearCaches(cm) {
function pageScrollX (line 1126) | function pageScrollX() { return window.pageXOffset || (document.document...
function pageScrollY (line 1127) | function pageScrollY() { return window.pageYOffset || (document.document...
function intoCoordSystem (line 1130) | function intoCoordSystem(cm, lineObj, rect, context) {
function fromCoordSystem (line 1152) | function fromCoordSystem(cm, coords, context) {
function charCoords (line 1169) | function charCoords(cm, pos, context, lineObj, bias) {
function cursorCoords (line 1174) | function cursorCoords(cm, pos, context, lineObj, measurement) {
function PosWithInfo (line 1204) | function PosWithInfo(line, ch, outside, xRel) {
function coordsChar (line 1212) | function coordsChar(cm, x, y) {
function coordsCharInner (line 1233) | function coordsCharInner(cm, lineObj, lineNo, x, y) {
function textHeight (line 1275) | function textHeight(display) {
function charWidth (line 1294) | function charWidth(display) {
function startOperation (line 1312) | function startOperation(cm) {
function endOperation (line 1330) | function endOperation(cm) {
function operation (line 1387) | function operation(cm1, f) {
function docOperation (line 1396) | function docOperation(f) {
function runInOp (line 1405) | function runInOp(cm, f) {
function regChange (line 1413) | function regChange(cm, from, to, lendiff) {
function slowPoll (line 1421) | function slowPoll(cm) {
function fastPoll (line 1429) | function fastPoll(cm) {
function readInput (line 1445) | function readInput(cm) {
function resetInput (line 1480) | function resetInput(cm, user) {
function focusInput (line 1497) | function focusInput(cm) {
function isReadOnly (line 1502) | function isReadOnly(cm) {
function registerEventHandlers (line 1508) | function registerEventHandlers(cm) {
function eventInWidget (line 1622) | function eventInWidget(display, e) {
function posFromMouse (line 1628) | function posFromMouse(cm, e, liberal) {
function onMouseDown (line 1643) | function onMouseDown(e) {
function clickInGutter (line 1783) | function clickInGutter(cm, e) {
function onDrop (line 1812) | function onDrop(e) {
function onDragStart (line 1857) | function onDragStart(cm, e) {
function setScrollTop (line 1879) | function setScrollTop(cm, val) {
function setScrollLeft (line 1888) | function setScrollLeft(cm, val, isScroller) {
function onScrollWheel (line 1918) | function onScrollWheel(cm, e) {
function doHandleBinding (line 1986) | function doHandleBinding(cm, bound, dropShift) {
function allKeyMaps (line 2006) | function allKeyMaps(cm) {
function handleKeyBinding (line 2014) | function handleKeyBinding(cm, e) {
function handleCharBinding (line 2051) | function handleCharBinding(cm, e, ch) {
function onKeyDown (line 2063) | function onKeyDown(e) {
function onKeyPress (line 2081) | function onKeyPress(e) {
function onFocus (line 2097) | function onFocus(cm) {
function onBlur (line 2109) | function onBlur(cm) {
function onContextMenu (line 2120) | function onContextMenu(cm, e) {
function clipPostChange (line 2189) | function clipPostChange(doc, change, pos) {
function computeSelAfterChange (line 2205) | function computeSelAfterChange(doc, change, hint) {
function filterChange (line 2228) | function filterChange(doc, change, update) {
function makeChange (line 2252) | function makeChange(doc, change, selUpdate, ignoreReadOnly) {
function makeChangeNoReadonly (line 2276) | function makeChangeNoReadonly(doc, change, selUpdate) {
function makeChangeFromHistory (line 2292) | function makeChangeFromHistory(doc, type) {
function shiftDoc (line 2332) | function shiftDoc(doc, distance) {
function makeChangeSingleDoc (line 2340) | function makeChangeSingleDoc(doc, change, selAfter, spans) {
function makeChangeSingleDocInEditor (line 2370) | function makeChangeSingleDocInEditor(cm, change, spans, selAfter) {
function replaceRange (line 2422) | function replaceRange(doc, code, from, to, origin) {
function Pos (line 2431) | function Pos(line, ch) {
function posEq (line 2437) | function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
function posLess (line 2438) | function posLess(a, b) {return a.line < b.line || (a.line == b.line && a...
function copyPos (line 2439) | function copyPos(x) {return Pos(x.line, x.ch);}
function clipLine (line 2443) | function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.fi...
function clipPos (line 2444) | function clipPos(doc, pos) {
function clipToLen (line 2450) | function clipToLen(pos, linelen) {
function isLine (line 2456) | function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.si...
function extendSelection (line 2460) | function extendSelection(doc, pos, other, bias) {
function filterSelectionChange (line 2479) | function filterSelectionChange(doc, anchor, head) {
function setSelection (line 2490) | function setSelection(doc, anchor, head, bias, checkAtomic) {
function reCheckSelection (line 2519) | function reCheckSelection(cm) {
function skipAtomic (line 2523) | function skipAtomic(doc, pos, bias, mayClear) {
function scrollCursorIntoView (line 2575) | function scrollCursorIntoView(cm) {
function scrollPosIntoView (line 2593) | function scrollPosIntoView(cm, pos, margin) {
function scrollIntoView (line 2611) | function scrollIntoView(cm, x1, y1, x2, y2) {
function calculateScrollPos (line 2617) | function calculateScrollPos(cm, x1, y1, x2, y2) {
function updateScrollPos (line 2643) | function updateScrollPos(cm, left, top) {
function addToScrollPos (line 2648) | function addToScrollPos(cm, left, top) {
function indentLine (line 2657) | function indentLine(cm, n, how, aggressive) {
function changeLine (line 2697) | function changeLine(cm, handle, op) {
function findPosH (line 2707) | function findPosH(doc, pos, dir, unit, visually) {
function findPosV (line 2752) | function findPosV(cm, pos, dir, unit) {
function findWordAt (line 2769) | function findWordAt(line, pos) {
function selectLine (line 2783) | function selectLine(cm, line) {
function interpret (line 3130) | function interpret(val) {
function option (line 3172) | function option(name, deflt, handle, notOnInit) {
function copyState (line 3343) | function copyState(mode, state) {
function startState (line 3356) | function startState(mode, a1, a2) {
function getKeyMap (line 3497) | function getKeyMap(val) {
function lookupKey (line 3502) | function lookupKey(name, maps, handle) {
function isModifierKey (line 3526) | function isModifierKey(event) {
function keyName (line 3530) | function keyName(event, noShift) {
function save (line 3563) | function save() {textarea.value = cm.getValue();}
function StringStream (line 3605) | function StringStream(string, tabSize) {
function TextMarker (line 3671) | function TextMarker(doc, type) {
function markText (line 3764) | function markText(doc, from, to, options, type) {
function SharedTextMarker (line 3826) | function SharedTextMarker(markers, primary) {
function markTextShared (line 3848) | function markTextShared(doc, from, to, options, type) {
function getMarkedSpanFor (line 3865) | function getMarkedSpanFor(spans, marker) {
function removeMarkedSpan (line 3871) | function removeMarkedSpan(spans, span) {
function addMarkedSpan (line 3876) | function addMarkedSpan(line, span) {
function markedSpansBefore (line 3881) | function markedSpansBefore(old, startCh, isInsert) {
function markedSpansAfter (line 3895) | function markedSpansAfter(old, endCh, isInsert) {
function stretchSpansOverChange (line 3909) | function stretchSpansOverChange(doc, change) {
function mergeOldSpans (line 3972) | function mergeOldSpans(doc, change) {
function removeReadOnlyRanges (line 3994) | function removeReadOnlyRanges(doc, from, to) {
function collapsedSpanAt (line 4022) | function collapsedSpanAt(line, ch) {
function collapsedSpanAtStart (line 4034) | function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); }
function collapsedSpanAtEnd (line 4035) | function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.te...
function visualLine (line 4037) | function visualLine(doc, line) {
function lineIsHidden (line 4044) | function lineIsHidden(doc, line) {
function lineIsHiddenInner (line 4055) | function lineIsHiddenInner(doc, line, span) {
function detachMarkedSpans (line 4070) | function detachMarkedSpans(line) {
function attachMarkedSpans (line 4078) | function attachMarkedSpans(line, spans) {
function widgetOperation (line 4094) | function widgetOperation(f) {
function widgetHeight (line 4123) | function widgetHeight(widget) {
function addLineWidget (line 4130) | function addLineWidget(cm, handle, node, options) {
function updateLine (line 4159) | function updateLine(line, text, markedSpans, estimateHeight) {
function cleanUpLine (line 4170) | function cleanUpLine(line) {
function runMode (line 4178) | function runMode(cm, text, mode, state, f) {
function highlightLine (line 4202) | function highlightLine(cm, line, state) {
function getLineStyles (line 4238) | function getLineStyles(cm, line) {
function processLine (line 4246) | function processLine(cm, line, state) {
function styleToClass (line 4257) | function styleToClass(style) {
function lineContent (line 4263) | function lineContent(cm, realLine, measure, copyWidgets) {
function buildToken (line 4313) | function buildToken(builder, text, style, startStyle, endStyle, title) {
function buildTokenMeasure (line 4353) | function buildTokenMeasure(builder, text, style, startStyle, endStyle) {
function buildTokenSplitSpaces (line 4379) | function buildTokenSplitSpaces(inner) {
function buildCollapsedSpan (line 4391) | function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
function insertLineContent (line 4414) | function insertLineContent(line, builder, styles) {
function updateDoc (line 4474) | function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) {
function LeafChunk (line 4520) | function LeafChunk(lines) {
function BranchChunk (line 4555) | function BranchChunk(children) {
function linkedDocs (line 4896) | function linkedDocs(doc, f, sharedHistOnly) {
function attachDoc (line 4910) | function attachDoc(cm, doc) {
function getLine (line 4923) | function getLine(chunk, n) {
function getBetween (line 4935) | function getBetween(doc, start, end) {
function getLines (line 4946) | function getLines(doc, from, to) {
function updateLineHeight (line 4952) | function updateLineHeight(line, height) {
function lineNo (line 4957) | function lineNo(line) {
function lineAtHeight (line 4969) | function lineAtHeight(chunk, h) {
function heightAtLine (line 4988) | function heightAtLine(cm, lineObj) {
function getOrder (line 5007) | function getOrder(line) {
function makeHistory (line 5015) | function makeHistory(startGen) {
function attachLocalSpans (line 5029) | function attachLocalSpans(doc, change, from, to) {
function historyChangeFromChange (line 5038) | function historyChangeFromChange(doc, change) {
function addToHistory (line 5046) | function addToHistory(doc, change, selAfter, opId) {
function removeClearedSpans (line 5083) | function removeClearedSpans(spans) {
function getOldSpans (line 5092) | function getOldSpans(doc, change) {
function copyHistoryArray (line 5102) | function copyHistoryArray(events, newGroup) {
function rebaseHistSel (line 5123) | function rebaseHistSel(pos, from, to, diff) {
function rebaseHistArray (line 5139) | function rebaseHistArray(array, from, to, diff) {
function rebaseHist (line 5168) | function rebaseHist(hist, change) {
function stopMethod (line 5176) | function stopMethod() {e_stop(this);}
function addStop (line 5178) | function addStop(event) {
function e_preventDefault (line 5183) | function e_preventDefault(e) {
function e_stopPropagation (line 5187) | function e_stopPropagation(e) {
function e_defaultPrevented (line 5191) | function e_defaultPrevented(e) {
function e_stop (line 5194) | function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);}
function e_target (line 5199) | function e_target(e) {return e.target || e.srcElement;}
function e_button (line 5200) | function e_button(e) {
function on (line 5213) | function on(emitter, type, f) {
function off (line 5225) | function off(emitter, type, f) {
function signal (line 5238) | function signal(emitter, type /*, values...*/) {
function signalLater (line 5246) | function signalLater(emitter, type /*, values...*/) {
function signalDOMEvent (line 5260) | function signalDOMEvent(cm, e, override) {
function fireDelayed (line 5265) | function fireDelayed() {
function hasHandler (line 5272) | function hasHandler(emitter, type) {
function eventMixin (line 5279) | function eventMixin(ctor) {
function Delayed (line 5293) | function Delayed() {this.id = null;}
function countColumn (line 5298) | function countColumn(string, end, tabSize, startIndex, startValue) {
function spaceStr (line 5312) | function spaceStr(n) {
function lst (line 5318) | function lst(arr) { return arr[arr.length-1]; }
function selectInput (line 5320) | function selectInput(node) {
function indexOf (line 5331) | function indexOf(collection, elt) {
function createObj (line 5338) | function createObj(base, props) {
function copyObj (line 5346) | function copyObj(obj, target) {
function emptyArray (line 5352) | function emptyArray(size) {
function bind (line 5357) | function bind(f) {
function isWordChar (line 5363) | function isWordChar(ch) {
function isEmpty (line 5368) | function isEmpty(obj) {
function elt (line 5377) | function elt(tag, content, className, style) {
function removeChildren (line 5386) | function removeChildren(e) {
function removeChildrenAndAdd (line 5392) | function removeChildrenAndAdd(parent, e) {
function setTextContent (line 5396) | function setTextContent(e, str) {
function getRect (line 5403) | function getRect(node) {
function spanAffectsWrapping (line 5426) | function spanAffectsWrapping() { return false; }
function scrollbarWidth (line 5445) | function scrollbarWidth(measure) {
function zeroWidthElement (line 5455) | function zeroWidthElement(measure) {
function iterateBidiSections (line 5525) | function iterateBidiSections(order, from, to, f) {
function bidiLeft (line 5538) | function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
function bidiRight (line 5539) | function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
function lineLeft (line 5541) | function lineLeft(line) { var order = getOrder(line); return order ? bid...
function lineRight (line 5542) | function lineRight(line) {
function lineStart (line 5548) | function lineStart(cm, lineN) {
function lineEnd (line 5556) | function lineEnd(cm, lineN) {
function compareBidiLevel (line 5565) | function compareBidiLevel(order, a, b) {
function getBidiPartAt (line 5572) | function getBidiPartAt(order, pos) {
function moveInLine (line 5592) | function moveInLine(line, pos, dir, byUnit) {
function moveVisually (line 5605) | function moveVisually(line, start, dir, byUnit) {
function moveLogically (line 5628) | function moveLogically(line, start, dir, byUnit) {
function charType (line 5662) | function charType(code) {
function inText (line 5877) | function inText(stream, state) {
function inTag (line 5929) | function inTag(stream, state) {
function inAttribute (line 5950) | function inAttribute(quote) {
function inBlock (line 5964) | function inBlock(style, terminator) {
function doctype (line 5976) | function doctype(depth) {
function pass (line 5998) | function pass() {
function cont (line 6001) | function cont() {
function pushContext (line 6006) | function pushContext(tagName, startOfLine) {
function popContext (line 6016) | function popContext() {
function element (line 6020) | function element(type) {
function endtag (line 6042) | function endtag(startOfLine) {
function endclosetag (line 6059) | function endclosetag(err) {
function maybePopContext (line 6067) | function maybePopContext(nextTagName) {
function attributes (line 6082) | function attributes(type) {
function attribute (line 6088) | function attribute(type) {
function attvalue (line 6094) | function attvalue(type) {
function attvaluemaybe (line 6100) | function attvaluemaybe(type) {
function switchInline (line 6249) | function switchInline(stream, state, f) {
function switchBlock (line 6254) | function switchBlock(stream, state, f) {
function blankLine (line 6262) | function blankLine(state) {
function blockNormal (line 6283) | function blockNormal(stream, state) {
function htmlBlock (line 6337) | function htmlBlock(stream, state) {
function local (line 6351) | function local(stream, state) {
function getType (line 6366) | function getType(state) {
function handleText (line 6401) | function handleText(stream, state) {
function inlineNormal (line 6408) | function inlineNormal(stream, state) {
function linkHref (line 6561) | function linkHref(stream, state) {
function footnoteLink (line 6573) | function footnoteLink(stream, state) {
function footnoteUrl (line 6581) | function footnoteUrl(stream, state) {
function inlineRE (line 6599) | function inlineRE(endChar) {
function inlineElement (line 6610) | function inlineElement(type, endChar, next) {
function fixShortcut (line 6739) | function fixShortcut(name) {
function createIcon (line 6752) | function createIcon(name, options) {
function createSep (line 6770) | function createSep() {
function getState (line 6781) | function getState(cm, pos) {
function toggleFullScreen (line 6813) | function toggleFullScreen(editor) {
function toggleBold (line 6848) | function toggleBold(editor) {
function toggleItalic (line 6883) | function toggleItalic(editor) {
function toggleBlockquote (line 6918) | function toggleBlockquote(editor) {
function toggleUnOrderedList (line 6927) | function toggleUnOrderedList(editor) {
function toggleOrderedList (line 6936) | function toggleOrderedList(editor) {
function drawLink (line 6945) | function drawLink(editor) {
function drawImage (line 6955) | function drawImage(editor) {
function undo (line 6965) | function undo(editor) {
function redo (line 6975) | function redo(editor) {
function togglePreview (line 6984) | function togglePreview(editor) {
function _replaceSelection (line 7012) | function _replaceSelection(cm, active, start, end) {
function _toggleLine (line 7033) | function _toggleLine(cm, name) {
function wordCount (line 7063) | function wordCount(data) {
function Editor (line 7100) | function Editor(options) {
FILE: public/libs/editor/ext.js
function _replaceSelection (line 3) | function _replaceSelection(cm, active, start, end) {
function getState (line 27) | function getState(cm, pos) {
FILE: public/libs/jquery-2.1.0.js
function isArraylike (line 536) | function isArraylike(obj) {
function Sizzle (line 733) | function Sizzle(selector, context, results, seed) {
function createCache (line 848) | function createCache() {
function markFunction (line 867) | function markFunction(fn) {
function assert (line 876) | function assert(fn) {
function addHandle (line 898) | function addHandle(attrs, handler) {
function siblingCheck (line 913) | function siblingCheck(a, b) {
function createInputPseudo (line 940) | function createInputPseudo(type) {
function createButtonPseudo (line 951) | function createButtonPseudo(type) {
function createPositionalPseudo (line 962) | function createPositionalPseudo(fn) {
function testContext (line 985) | function testContext(context) {
function setFilters (line 1982) | function setFilters() {
function tokenize (line 1988) | function tokenize(selector, parseOnly) {
function toSelector (line 2055) | function toSelector(tokens) {
function addCombinator (line 2065) | function addCombinator(matcher, combinator, base) {
function elementMatcher (line 2118) | function elementMatcher(matchers) {
function condense (line 2132) | function condense(unmatched, map, filter, context, xml) {
function setMatcher (line 2153) | function setMatcher(preFilter, selector, matcher, postFilter, postFinder...
function matcherFromTokens (line 2246) | function matcherFromTokens(tokens) {
function matcherFromGroupMatchers (line 2301) | function matcherFromGroupMatchers(elementMatchers, setMatchers) {
function multipleContexts (line 2427) | function multipleContexts(selector, contexts, results) {
function select (line 2436) | function select(selector, context, results, seed) {
function winnow (line 2585) | function winnow(elements, qualifier, not) {
function sibling (line 2910) | function sibling(cur, dir) {
function createOptions (line 2988) | function createOptions(options) {
function completed (line 3381) | function completed() {
function Data (line 3484) | function Data() {
function dataAttr (line 3675) | function dataAttr(elem, key, data) {
function returnTrue (line 4002) | function returnTrue() {
function returnFalse (line 4006) | function returnFalse() {
function safeActiveElement (line 4010) | function safeActiveElement() {
function manipulationTarget (line 4874) | function manipulationTarget(elem, content) {
function disableScript (line 4884) | function disableScript(elem) {
function restoreScript (line 4889) | function restoreScript(elem) {
function setGlobalEval (line 4902) | function setGlobalEval(elems, refElements) {
function cloneCopyEvent (line 4913) | function cloneCopyEvent(src, dest) {
function getAll (line 4947) | function getAll(context, tag) {
function fixInput (line 4958) | function fixInput(src, dest) {
function actualDisplay (line 5413) | function actualDisplay(name, doc) {
function defaultDisplay (line 5434) | function defaultDisplay(nodeName) {
function curCSS (line 5474) | function curCSS(elem, name, computed) {
function addGetHookIf (line 5522) | function addGetHookIf(conditionFn, hookFn) {
function computePixelPositionAndBoxSizingReliable (line 5561) | function computePixelPositionAndBoxSizingReliable() {
function vendorPropName (line 5656) | function vendorPropName(style, name) {
function setPositiveNumber (line 5678) | function setPositiveNumber(elem, value, subtract) {
function augmentWidthOrHeight (line 5686) | function augmentWidthOrHeight(elem, name, extra, isBorderBox, styles) {
function getWidthOrHeight (line 5725) | function getWidthOrHeight(elem, name, extra) {
function showHide (line 5769) | function showHide(elements, show) {
function Tween (line 6072) | function Tween(elem, options, prop, end, easing) {
function createFxNow (line 6240) | function createFxNow() {
function genFx (line 6248) | function genFx(type, includeWidth) {
function createTween (line 6268) | function createTween(value, prop, animation) {
function defaultPrefilter (line 6282) | function defaultPrefilter(elem, props, opts) {
function propFilter (line 6409) | function propFilter(props, specialEasing) {
function Animation (line 6446) | function Animation(elem, properties, options) {
function addToPrefiltersOrTransports (line 7494) | function addToPrefiltersOrTransports(structure) {
function inspectPrefiltersOrTransports (line 7526) | function inspectPrefiltersOrTransports(structure, options, originalOptio...
function ajaxExtend (line 7553) | function ajaxExtend(target, src) {
function ajaxHandleResponses (line 7573) | function ajaxHandleResponses(s, jqXHR, responses) {
function ajaxConvert (line 7629) | function ajaxConvert(s, response, jqXHR, isSuccess) {
function done (line 8086) | function done(status, nativeStatusText, responses, headers) {
function buildParams (line 8336) | function buildParams(prefix, obj, traditional, add) {
function getWindow (line 8805) | function getWindow(elem) {
FILE: public/libs/jquery.atwho.js
function App (line 20) | function App(inputor) {
function Controller (line 236) | function Controller(app, at) {
function Model (line 454) | function Model(context) {
function View (line 518) | function View(context) {
FILE: public/libs/jquery.caret.js
function EditableCaret (line 29) | function EditableCaret($inputor) {
function InputCaret (line 129) | function InputCaret($inputor) {
function Mirror (line 248) | function Mirror($inputor) {
FILE: public/libs/lodash.compat.js
function baseIndexOf (line 196) | function baseIndexOf(array, value, fromIndex) {
function cacheIndexOf (line 217) | function cacheIndexOf(cache, value) {
function cachePush (line 241) | function cachePush(value) {
function charAtCallback (line 270) | function charAtCallback(value) {
function compareAscending (line 283) | function compareAscending(a, b) {
function createCache (line 318) | function createCache(array) {
function escapeStringChar (line 351) | function escapeStringChar(match) {
function getArray (line 361) | function getArray() {
function getObject (line 371) | function getObject() {
function isNode (line 396) | function isNode(value) {
function releaseArray (line 408) | function releaseArray(array) {
function releaseObject (line 421) | function releaseObject(object) {
function slice (line 445) | function slice(array, start, end) {
function runInContext (line 471) | function runInContext(context) {
FILE: public/libs/markdownit.js
function s (line 1) | function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&re...
function replace (line 2207) | function replace(regex, options) {
function _class (line 2444) | function _class(obj) { return Object.prototype.toString.call(obj); }
function isString (line 2446) | function isString(obj) { return _class(obj) === '[object String]'; }
function has (line 2450) | function has(object, key) {
function assign (line 2456) | function assign(obj /*from1, from2, from3, ...*/) {
function arrayReplaceAt (line 2476) | function arrayReplaceAt(src, pos, newElements) {
function unescapeMd (line 2484) | function unescapeMd(str) {
function isValidEntityCode (line 2491) | function isValidEntityCode(c) {
function fromCodePoint (line 2508) | function fromCodePoint(c) {
function replaceEntityPattern (line 2524) | function replaceEntityPattern(match, name) {
function replaceEntities (line 2541) | function replaceEntities(str) {
function replaceUnsafeChar (line 2558) | function replaceUnsafeChar(ch) {
function escapeHtml (line 2562) | function escapeHtml(str) {
function replaceBadSurrogate (line 2574) | function replaceBadSurrogate(ch, pos, orig) {
function fixBrokenSurrogates (line 2593) | function fixBrokenSurrogates(str) {
function normalizeLink (line 2611) | function normalizeLink(url) {
function escapeRE (line 2629) | function escapeRE (str) {
function isWhiteSpace (line 2636) | function isWhiteSpace(code) {
function isPunctChar (line 2660) | function isPunctChar(char) {
function isMdAsciiPunct (line 2672) | function isMdAsciiPunct(ch) {
function MarkdownIt (line 3079) | function MarkdownIt(presetName, options) {
function ParserBlock (line 3418) | function ParserBlock() {
function Core (line 3539) | function Core() {
function validateLink (line 3605) | function validateLink(url) {
function ParserInline (line 3621) | function ParserInline() {
function Renderer (line 4096) | function Renderer() {
function Ruler (line 4227) | function Ruler() {
function isLetter (line 4939) | function isLetter(ch) {
function skipBulletListMarker (line 5068) | function skipBulletListMarker(state, startLine) {
function skipOrderedListMarker (line 5094) | function skipOrderedListMarker(state, startLine) {
function markTightParagraphs (line 5131) | function markTightParagraphs(state, idx) {
function StateBlock (line 5546) | function StateBlock(src, md, env, tokens) {
function getLine (line 5704) | function getLine(state, line) {
function isLinkOpen (line 5883) | function isLinkOpen(str) {
function isLinkClose (line 5886) | function isLinkClose(str) {
function createLinkifier (line 5893) | function createLinkifier() {
function replaceScopedAbbr (line 6106) | function replaceScopedAbbr(str) {
function isLetter (line 6165) | function isLetter(str, pos) {
function replaceAt (line 6171) | function replaceAt(str, index, ch) {
function isAlphaNum (line 6423) | function isAlphaNum(code) {
function scanDelims (line 6431) | function scanDelims(state, start) {
function isLetter (line 6692) | function isLetter(ch) {
function StateInline (line 7107) | function StateInline(src, md, env, outTokens) {
function scanDelims (line 7182) | function scanDelims(state, start) {
function isTerminatorChar (line 7294) | function isTerminatorChar(ch) {
FILE: public/libs/qrcode.js
function QR8bitByte (line 29) | function QR8bitByte(data) {
function QRCodeModel (line 78) | function QRCodeModel(typeNumber, errorCorrectLevel) {
function QRPolynomial (line 139) | function QRPolynomial(num,shift){if(num.length==undefined){throw new Err...
function QRRSBlock (line 146) | function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this...
function QRBitBuffer (line 149) | function QRBitBuffer(){this.buffer=[];this.length=0;}
function _isSupportCanvas (line 154) | function _isSupportCanvas() {
function _getAndroid (line 159) | function _getAndroid() {
function makeSVG (line 191) | function makeSVG(tag, attrs) {
function _onMakeImage (line 276) | function _onMakeImage() {
function _safeSetDataURI (line 310) | function _safeSetDataURI(fSuccess, fFail) {
function _getTypeNumber (line 469) | function _getTypeNumber(sText, nCorrectLevel) {
function _getUTF8Length (line 505) | function _getUTF8Length(sText) {
FILE: public/libs/webuploader/webuploader.withoutimage.js
function uncurryThis (line 205) | function uncurryThis(fn) {
function bindFn (line 211) | function bindFn(fn, context) {
function createObject (line 217) | function createObject(proto) {
function findHandlers (line 506) | function findHandlers(arr, name, callback, context) {
function eachEvent (line 516) | function eachEvent(events, callback, iterator) {
function triggerHanders (line 523) | function triggerHanders(events, args) {
function Uploader (line 732) | function Uploader(opts) {
function Runtime (line 917) | function Runtime(options) {
function RuntimeClient (line 1051) | function RuntimeClient(component, standalone) {
function DragAndDrop (line 1157) | function DragAndDrop(opts) {
function isArrayLike (line 1208) | function isArrayLike(obj) {
function Widget (line 1225) | function Widget(uploader) {
function FilePaste (line 1444) | function FilePaste(opts) {
function Blob (line 1525) | function Blob(ruid, source) {
function File (line 1574) | function File(ruid, file) {
function FilePicker (line 1609) | function FilePicker(opts) {
function gid (line 1890) | function gid() {
function WUFile (line 1901) | function WUFile(source) {
function Queue (line 2078) | function Queue() {
function Transport (line 2635) | function Transport(opts) {
function CuteFile (line 2848) | function CuteFile(file, chunkSize) {
function hashString (line 3595) | function hashString(str) {
function CompBase (line 3645) | function CompBase(owner, runtime) {
function Html5Runtime (line 3677) | function Html5Runtime() {
function getFlashVersion (line 4301) | function getFlashVersion() {
function FlashRuntime (line 4319) | function FlashRuntime() {
FILE: test/support/support.js
function randomInt (line 9) | function randomInt() {
function mockUser (line 45) | function mockUser(user) {
Condensed preview — 192 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,041K chars).
[
{
"path": ".gitignore",
"chars": 335,
"preview": "config.js\n.cov\ncoverage\nnode_modules\n.naeindex\ncoverage.html\n.monitor\n\n*.min.*.js\n*.min.*.css\nassets.json\n\n# Ignore Mac "
},
{
"path": ".jshintrc",
"chars": 682,
"preview": "{\n \"predef\": [\n \"phantom\",\n \"module\",\n \"require\",\n \"__dirname\",\n \"process\",\n \"console\",\n \"it\",\n "
},
{
"path": ".mention-bot",
"chars": 36,
"preview": "{\n \"userBlacklist\": [\"huacnlee\"]\n}\n"
},
{
"path": ".snyk",
"chars": 300,
"preview": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.12.0\nignore: {}\n# patches ap"
},
{
"path": ".travis.yml",
"chars": 294,
"preview": "sudo: false\n\nlanguage: node_js\n\nenv:\n - CXX=g++-4.8\n\nnode_js:\n - stable\n\naddons:\n apt:\n sources:\n - ubuntu-tool"
},
{
"path": "History.md",
"chars": 6429,
"preview": "2.1.0 / 2015-09-15\n==================\n\n* 使用 oneapm 代替 newrelic\n\n2.0.1 / 2015-08-07\n==================\n\n* 去掉【收藏功能】\n\n0.3.6"
},
{
"path": "LICENSE",
"chars": 1042,
"preview": "(The MIT License)\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and ass"
},
{
"path": "Makefile",
"chars": 1330,
"preview": "TESTS = $(shell find test -type f -name \"*.test.js\")\nTEST_TIMEOUT = 10000\nMOCHA_REPORTER = spec\n# NPM_REGISTRY = \"--regi"
},
{
"path": "README.md",
"chars": 1492,
"preview": "Nodeclub\n=\n\n[![build status][travis-image]][travis-url]\n[![codecov.io][codecov-image]][codecov-url]\n[![David deps][david"
},
{
"path": "api/v1/message.js",
"chars": 3157,
"preview": "var eventproxy = require('eventproxy');\nvar Message = require('../../proxy').Message;\nvar at = require('../"
},
{
"path": "api/v1/middleware.js",
"chars": 1262,
"preview": "var UserModel = require('../../models').User;\nvar eventproxy = require('eventproxy');\nvar validator = require('validat"
},
{
"path": "api/v1/reply.js",
"chars": 3374,
"preview": "var eventproxy = require('eventproxy');\nvar validator = require('validator');\nvar Topic = require('../../proxy').T"
},
{
"path": "api/v1/tools.js",
"chars": 302,
"preview": "var eventproxy = require('eventproxy');\n\nvar accesstoken = function (req, res, next) {\n var ep = new eventproxy();\n ep"
},
{
"path": "api/v1/topic.js",
"chars": 6738,
"preview": "var models = require('../../models');\nvar TopicModel = models.Topic;\nvar TopicProxy = require('../../proxy').T"
},
{
"path": "api/v1/topic_collect.js",
"chars": 3616,
"preview": "var eventproxy = require('eventproxy');\nvar TopicProxy = require('../../proxy').Topic;\nvar TopicCollectProxy = require"
},
{
"path": "api/v1/user.js",
"chars": 2232,
"preview": "var _ = require('lodash');\nvar eventproxy = require('eventproxy');\nvar UserProxy = require('../../proxy'"
},
{
"path": "api_router_v1.js",
"chars": 1930,
"preview": "var express = require('express');\nvar topicController = require('./api/v1/topic');\nvar topicCollectControlle"
},
{
"path": "app.js",
"chars": 4534,
"preview": "/*!\n * nodeclub - app.js\n */\n\n/**\n * Module dependencies.\n */\n\nvar config = require('./config');\n\nif (!config.debug && c"
},
{
"path": "bin/fix_at_problem.js",
"chars": 435,
"preview": "// 一次性脚本\n// 修复之前重复编辑帖子会导致重复 @someone 的渲染问题\nvar TopicModel = require('../models').Topic;\n\nTopicModel.find({content: /\\[{2"
},
{
"path": "bin/fix_topic_collect_count.js",
"chars": 1325,
"preview": "var TopicCollect = require('../models').TopicCollect;\nvar UserModel = require('../models').User;\nvar TopicModel = requir"
},
{
"path": "bin/generate_accesstoken.js",
"chars": 874,
"preview": "// 一次性脚本\n// 为所有老用户生成 accessToken\n\nvar uuid = require('node-uuid');\nvar mongoose = require('mongoose');\nvar config = requ"
},
{
"path": "bin/get_user_topics.js",
"chars": 314,
"preview": "var UserModel = require('../models').User;\nvar TopicModel = require('../models').Topic\n\n// usage:\n// node get_user_topic"
},
{
"path": "common/at.js",
"chars": 2678,
"preview": "/*!\n * nodeclub - topic mention user controller.\n * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>\n * Copyright(c) 2012 m"
},
{
"path": "common/cache.js",
"chars": 950,
"preview": "var redis = require('./redis');\nvar _ = require('lodash');\nvar logger = require('./logger');\n\nvar get = function ("
},
{
"path": "common/logger.js",
"chars": 446,
"preview": "var config = require('../config');\nvar pathLib = require('path')\n\nvar env = process.env.NODE_ENV || \"development\"\n\n\nvar "
},
{
"path": "common/mail.js",
"chars": 2430,
"preview": "var mailer = require('nodemailer');\nvar smtpTransport = require('nodemailer-smtp-transport');\nvar config ="
},
{
"path": "common/message.js",
"chars": 1231,
"preview": "var models = require('../models');\nvar eventproxy = require('eventproxy');\nvar Message = models.Message;\nva"
},
{
"path": "common/redis.js",
"chars": 430,
"preview": "var config = require('../config');\nvar Redis = require('ioredis');\nvar logger = require('./logger')\n\nvar client = new Re"
},
{
"path": "common/render_helper.js",
"chars": 2432,
"preview": "/*!\n * nodeclub - common/render_helpers.js\n * Copyright(c) 2013 fengmk2 <fengmk2@gmail.com>\n * MIT Licensed\n */\n\n\"use st"
},
{
"path": "common/store.js",
"chars": 104,
"preview": "var qn = require('./store_qn');\nvar local = require('./store_local');\n\nmodule.exports = qn || local;\n"
},
{
"path": "common/store_local.js",
"chars": 642,
"preview": "var config = require('../config');\nvar utility = require('utility');\nvar path = require('path');\nvar fs = requi"
},
{
"path": "common/store_qn.js",
"chars": 245,
"preview": "var qn = require('qn');\nvar config = require('../config');\n\n//7牛 client\nvar qnClient = null;\nif (config.qn_access &&"
},
{
"path": "common/tools.js",
"chars": 551,
"preview": "var bcrypt = require('bcryptjs');\nvar moment = require('moment');\n\nmoment.locale('zh-cn'); // 使用中文\n\n// 格式化时间\nexports.for"
},
{
"path": "config.default.js",
"chars": 3075,
"preview": "/**\n * config\n */\n\nvar path = require('path');\n\nvar config = {\n // debug 为 true 时,用于本地调试\n debug: true,\n\n get mini_ass"
},
{
"path": "controllers/github.js",
"chars": 4024,
"preview": "var Models = require('../models');\nvar User = Models.User;\nvar authMiddleWare = require('../middleware"
},
{
"path": "controllers/message.js",
"chars": 1194,
"preview": "var Message = require('../proxy').Message;\nvar eventproxy = require('eventproxy');\n\nexports.index = function (req, re"
},
{
"path": "controllers/reply.js",
"chars": 5026,
"preview": "var validator = require('validator');\nvar _ = require('lodash');\nvar at = require('../common/at');\nvar"
},
{
"path": "controllers/rss.js",
"chars": 1901,
"preview": "var config = require('../config');\nvar convert = require('data2xml')();\nvar Topic = require('../proxy'"
},
{
"path": "controllers/search.js",
"chars": 175,
"preview": "exports.index = function (req, res, next) {\n var q = req.query.q;\n q = encodeURIComponent(q);\n res.redirect('https://"
},
{
"path": "controllers/sign.js",
"chars": 8215,
"preview": "var validator = require('validator');\nvar eventproxy = require('eventproxy');\nvar config = require('../"
},
{
"path": "controllers/site.js",
"chars": 4147,
"preview": "/*!\n * nodeclub - site index controller.\n * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>\n * Copyright(c) 2012 muyuan\n *"
},
{
"path": "controllers/static.js",
"chars": 765,
"preview": "var multiline = require('multiline');\n// static page\n// About\nexports.about = function (req, res, next) {\n res.render('"
},
{
"path": "controllers/topic.js",
"chars": 11780,
"preview": "/*!\n * nodeclub - controllers/topic.js\n */\n\n/**\n * Module dependencies.\n */\n\nvar validator = require('validator');\n\nvar "
},
{
"path": "controllers/user.js",
"chars": 10909,
"preview": "var User = require('../proxy').User;\nvar Topic = require('../proxy').Topic;\nvar Reply = require('."
},
{
"path": "logs/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "middlewares/auth.js",
"chars": 2512,
"preview": "var mongoose = require('mongoose');\nvar UserModel = mongoose.model('User');\nvar Message = require('../proxy').Mess"
},
{
"path": "middlewares/conf.js",
"chars": 224,
"preview": "var config = require('../config');\n\nexports.github = function (req, res, next) {\n if (config.GITHUB_OAUTH.clientID === "
},
{
"path": "middlewares/error_page.js",
"chars": 399,
"preview": "// ErrorPage middleware\nexports.errorPage = function (req, res, next) {\n\n res.render404 = function (error) {\n return"
},
{
"path": "middlewares/github_strategy.js",
"chars": 134,
"preview": "module.exports = function (accessToken, refreshToken, profile, done) {\n profile.accessToken = accessToken;\n done(null,"
},
{
"path": "middlewares/limit.js",
"chars": 1604,
"preview": "var config = require('../config');\nvar cache = require('../common/cache');\nvar moment = require('moment');\n\nvar SEPARAT"
},
{
"path": "middlewares/mongoose_log.js",
"chars": 626,
"preview": "var mongoose = require('mongoose');\nvar logger = require('../common/logger');\nvar config = require('../config');\n\nif ("
},
{
"path": "middlewares/proxy.js",
"chars": 765,
"preview": "var urllib = require('url');\nvar request = require('request');\nvar logger = require('../common/logger')\nvar _ = require"
},
{
"path": "middlewares/render.js",
"chars": 395,
"preview": "var logger = require('../common/logger');\n\n// Patch res.render method to output logger\nexports.render = function (req, r"
},
{
"path": "middlewares/request_log.js",
"chars": 490,
"preview": "var logger = require('../common/logger');\n\nvar ignore = /^\\/(public|agent)/;\n\nexports = module.exports = function (req, "
},
{
"path": "models/base_model.js",
"chars": 358,
"preview": "/**\n * 给所有的 Model 扩展功能\n * http://mongoosejs.com/docs/plugins.html\n */\nvar tools = require('../common/tools');\n\nmodule.ex"
},
{
"path": "models/index.js",
"chars": 707,
"preview": "var mongoose = require('mongoose');\nvar config = require('../config');\nvar logger = require('../common/logger')\n\nmongo"
},
{
"path": "models/message.js",
"chars": 667,
"preview": "var mongoose = require('mongoose');\nvar BaseModel = require(\"./base_model\");\nvar Schema = mongoose.Schema;\nvar Objec"
},
{
"path": "models/reply.js",
"chars": 670,
"preview": "var mongoose = require('mongoose');\nvar BaseModel = require(\"./base_model\");\nvar Schema = mongoose.Schema;\nvar Objec"
},
{
"path": "models/topic.js",
"chars": 1348,
"preview": "var mongoose = require('mongoose');\nvar BaseModel = require(\"./base_model\");\nvar Schema = mongoose.Schema;\nvar Objec"
},
{
"path": "models/topic_collect.js",
"chars": 458,
"preview": "var mongoose = require('mongoose');\nvar BaseModel = require(\"./base_model\");\nvar Schema = mongoose.Schema;\nvar Objec"
},
{
"path": "models/user.js",
"chars": 2445,
"preview": "var mongoose = require('mongoose');\nvar BaseModel = require(\"./base_model\");\nvar renderHelper = require('../common/rend"
},
{
"path": "oneapm.js",
"chars": 693,
"preview": "/**\n * OneAPM agent configuration.\n *\n * See lib/config.defaults.js in the agent distribution for a more complete\n * des"
},
{
"path": "package.json",
"chars": 1791,
"preview": "{\n \"name\": \"nodeclub\",\n \"version\": \"2.1.1\",\n \"private\": true,\n \"main\": \"app.js\",\n \"description\": \"A Node.js bbs usi"
},
{
"path": "proxy/index.js",
"chars": 224,
"preview": "exports.User = require('./user');\nexports.Message = require('./message');\nexports.Topic = require('."
},
{
"path": "proxy/message.js",
"chars": 2894,
"preview": "var EventProxy = require('eventproxy');\nvar _ = require('lodash');\n\nvar Message = require('../models').Message;\n\nvar Use"
},
{
"path": "proxy/reply.js",
"chars": 3673,
"preview": "var models = require('../models');\nvar Reply = models.Reply;\nvar EventProxy = require('eventproxy');\nvar tools "
},
{
"path": "proxy/topic.js",
"chars": 5419,
"preview": "var EventProxy = require('eventproxy');\nvar models = require('../models');\nvar Topic = models.Topic;\nvar User "
},
{
"path": "proxy/topic_collect.js",
"chars": 778,
"preview": "var TopicCollect = require('../models').TopicCollect;\nvar _ = require('lodash')\n\nexports.getTopicCollect = function (use"
},
{
"path": "proxy/user.js",
"chars": 2640,
"preview": "var models = require('../models');\nvar User = models.User;\nvar utility = require('utility');\nvar uuid = require('"
},
{
"path": "public/github-card.html",
"chars": 7381,
"preview": "<!doctype html><html><body>\n<style type=\"text/css\">\nbody{padding:0;margin:0;font-size:14px;font-family:\"Helvetica Nenu\","
},
{
"path": "public/javascripts/main.js",
"chars": 1272,
"preview": "$(document).ready(function () {\n var windowHeight = $(window).height();\n var $backtotop = $('#backtotop');\n var top ="
},
{
"path": "public/javascripts/responsive.js",
"chars": 2460,
"preview": "$(document).ready(function () {\n var $responsiveBtn = $('#responsive-sidebar-trigger'),\n $sidebarMask = $('#sidebar-"
},
{
"path": "public/libs/bootstrap/css/bootstrap-responsive.css",
"chars": 25153,
"preview": "/*!\n * Bootstrap Responsive v2.3.1\n *\n * Copyright 2012 Twitter, Inc\n * Licensed under the Apache License v2.0\n * http:/"
},
{
"path": "public/libs/bootstrap/css/bootstrap.css",
"chars": 130840,
"preview": "/*!\n * Bootstrap v2.3.1\n *\n * Copyright 2012 Twitter, Inc\n * Licensed under the Apache License v2.0\n * http://www.apache"
},
{
"path": "public/libs/bootstrap/js/bootstrap.js",
"chars": 61093,
"preview": "/* ===================================================\n * bootstrap-transition.js v2.3.1\n * http://twitter.github.com/bo"
},
{
"path": "public/libs/code-prettify/lang-apollo.js",
"chars": 1036,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"com\", /^#[^\\n\\r]*/, null, \"#\"],\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t"
},
{
"path": "public/libs/code-prettify/lang-clj.js",
"chars": 1489,
"preview": "/*\n Copyright (C) 2011 Google Inc.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use th"
},
{
"path": "public/libs/code-prettify/lang-css.js",
"chars": 937,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\f\\r ]+/, null, \" \\t\\r\\n\f\"]\n], [\n [\"str\", /^\"(?:[^\\n\\f\\r"
},
{
"path": "public/libs/code-prettify/lang-go.js",
"chars": 306,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"],\n [\"pln\", /^(?:\"(?:[^\""
},
{
"path": "public/libs/code-prettify/lang-hs.js",
"chars": 617,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t-\\r ]+/, null, \"\\t\\n\u000b\f\\r \"],\n [\"str\", /^\"(?:[^\\n\\f\\r\"\\\\]|\\"
},
{
"path": "public/libs/code-prettify/lang-lisp.js",
"chars": 814,
"preview": "var a = null;\nPR.registerLangHandler(PR.createSimpleLexer([\n [\"opn\", /^\\(+/, a, \"(\"],\n [\"clo\", /^\\)+/, a, \")\"],\n [\"co"
},
{
"path": "public/libs/code-prettify/lang-lua.js",
"chars": 592,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"],\n [\"str\", /^(?:\"(?:[^\""
},
{
"path": "public/libs/code-prettify/lang-ml.js",
"chars": 1147,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"],\n [\"com\", /^#(?:if[\\t\\"
},
{
"path": "public/libs/code-prettify/lang-n.js",
"chars": 1492,
"preview": "var a = null;\nPR.registerLangHandler(PR.createSimpleLexer([\n [\"str\", /^(?:'(?:[^\\n\\r'\\\\]|\\\\.)*'|\"(?:[^\\n\\r\"\\\\]|\\\\.)*(?:"
},
{
"path": "public/libs/code-prettify/lang-proto.js",
"chars": 308,
"preview": "PR.registerLangHandler(PR.sourceDecorator({keywords: \"bytes,default,double,enum,extend,extensions,false,group,import,max"
},
{
"path": "public/libs/code-prettify/lang-scala.js",
"chars": 978,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"],\n [\"str\", /^\"(?:\"\"(?:\""
},
{
"path": "public/libs/code-prettify/lang-sql.js",
"chars": 1726,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"],\n [\"str\", /^(?:\"(?:[^\""
},
{
"path": "public/libs/code-prettify/lang-tex.js",
"chars": 322,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"],\n [\"com\", /^%[^\\n\\r]*/"
},
{
"path": "public/libs/code-prettify/lang-vb.js",
"chars": 1752,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0\\u2028\\u2029]+/, null, \"\\t\\n\\r �\\xa0
\"],\n [\"str\""
},
{
"path": "public/libs/code-prettify/lang-vhdl.js",
"chars": 1497,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\t\\n\\r \\xa0]+/, null, \"\\t\\n\\r �\\xa0\"]\n], [\n [\"str\", /^(?:[bo"
},
{
"path": "public/libs/code-prettify/lang-wiki.js",
"chars": 592,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"pln\", /^[\\d\\t a-gi-z\\xa0]+/, null, \"\\t �\\xa0abcdefgijklmnopqrstuvwxyz0"
},
{
"path": "public/libs/code-prettify/lang-xq.js",
"chars": 23298,
"preview": "PR.registerLangHandler(PR.createSimpleLexer([\n [\"var pln\", /^\\$[\\w-]+/, null, \"$\"]\n], [\n [\"pln\", /^[\\s=][<>][\\s=]/],\n "
},
{
"path": "public/libs/code-prettify/lang-yaml.js",
"chars": 483,
"preview": "var a = null;\nPR.registerLangHandler(PR.createSimpleLexer([\n [\"pun\", /^[:>?|]+/, a, \":|>?\"],\n [\"dec\", /^%(?:YAML|TAG)["
},
{
"path": "public/libs/code-prettify/prettify.css",
"chars": 1962,
"preview": "/* Pretty printing styles. Used with prettify.js. */\n\n/* SPAN elements with the classes below are added by prettyprint. "
},
{
"path": "public/libs/code-prettify/prettify.js",
"chars": 17934,
"preview": "var q = null;\nwindow.PR_SHOULD_USE_CONTINUATION = !0;\n(function () {\n function L(a) {\n function m(a) {\n var f ="
},
{
"path": "public/libs/editor/editor.css",
"chars": 9983,
"preview": "@font-face {\n\tfont-family: 'icomoon';\n\tsrc:url('/public/libs/editor/fonts/icomoon.eot');\n\tsrc:url('/public/libs/editor/f"
},
{
"path": "public/libs/editor/editor.js",
"chars": 268230,
"preview": "(function(global) {\n// CodeMirror version 3.15\n//\n// CodeMirror is the only global var we claim\nvar CodeMirror = (functi"
},
{
"path": "public/libs/editor/ext.js",
"chars": 10073,
"preview": "(function(Editor, markdownit, WebUploader){\n\n function _replaceSelection(cm, active, start, end) {\n var text;\n"
},
{
"path": "public/libs/font-awesome/css/font-awesome.css",
"chars": 26766,
"preview": "/*!\n * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome\n * License - http://fontawesome.io/lice"
},
{
"path": "public/libs/jquery-2.1.0.js",
"chars": 281897,
"preview": "/*!\n * jQuery JavaScript Library v2.1.0\n * http://jquery.com/\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n *\n * Cop"
},
{
"path": "public/libs/jquery-ujs.js",
"chars": 15816,
"preview": "(function ($, undefined) {\n\n /**\n * Unobtrusive scripting adapter for jQuery\n * https://github.com/rails/jquery-ujs"
},
{
"path": "public/libs/jquery.atwho.js",
"chars": 23512,
"preview": "/*! jquery.atwho - v0.5.1 - 2014-10-16\n* Copyright (c) 2014 chord.luo <chord.luo@gmail.com>;\n* homepage: http://ichord.g"
},
{
"path": "public/libs/jquery.caret.js",
"chars": 11617,
"preview": "/*\n Implement Github like autocomplete mentions\n http://ichord.github.com/At.js\n\n Copyright (c) 2013 chord.luo@gmail."
},
{
"path": "public/libs/lodash.compat.js",
"chars": 244140,
"preview": "/**\n * @license\n * Lo-Dash 2.4.1 (Custom Build) <http://lodash.com/>\n * Build: `lodash -o ./dist/lodash.compat.js`\n * Co"
},
{
"path": "public/libs/markdownit.js",
"chars": 261428,
"preview": "/*! markdown-it 3.0.3 https://github.com//markdown-it/markdown-it @license MIT */!function(e){if(\"object\"==typeof export"
},
{
"path": "public/libs/qrcode.js",
"chars": 34122,
"preview": "/**\n * @fileoverview\n * - Using the 'QRCode for Javascript library'\n * - Fixed dataset of 'QRCode for Javascript library"
},
{
"path": "public/libs/webuploader/webuploader.css",
"chars": 515,
"preview": ".webuploader-container {\n\tposition: relative;\n}\n.webuploader-element-invisible {\n\tposition: absolute !important;\n\tclip: "
},
{
"path": "public/libs/webuploader/webuploader.withoutimage.js",
"chars": 138465,
"preview": "/*! WebUploader 0.1.5 */\n\n\n/**\n* @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来"
},
{
"path": "public/stylesheets/common.css",
"chars": 1075,
"preview": "body, p, input, textarea {\n font-size: 14px;\n word-break: break-word;\n}\n\ntextarea, input[type=\"text\"],\ninput[type="
},
{
"path": "public/stylesheets/jquery.atwho.css",
"chars": 852,
"preview": ".atwho-view {\n position:absolute;\n top: 0;\n left: 0;\n display: none;\n margin-top: 18px;\n background: w"
},
{
"path": "public/stylesheets/responsive.css",
"chars": 2644,
"preview": "@-ms-viewport {\n width: device-width;\n}\n\n#sidebar-mask {\n background-color: #333;\n width: 100%;\n height: 100%;\n fil"
},
{
"path": "public/stylesheets/style.less",
"chars": 19707,
"preview": "@gray1: #e1e1e1;\n@gray2: #f6f6f6;\n@node_green: #80bd01;\n@node_black: #444444;\n\n/* base */\nbody {\n background-color: @gr"
},
{
"path": "test/api/v1/message.test.js",
"chars": 2126,
"preview": "var support = require('../../support/support');\nvar message = require('../../../common/message');\nvar MessageProxy = req"
},
{
"path": "test/api/v1/reply.test.js",
"chars": 6179,
"preview": "var app = require('../../../app');\nvar request = require('supertest')(app);\nvar pedding = require('pedding');\nvar suppor"
},
{
"path": "test/api/v1/tools.test.js",
"chars": 1139,
"preview": "var app = require('../../../app');\nvar request = require('supertest')(app);\nvar support = require('../../support/support"
},
{
"path": "test/api/v1/topic.test.js",
"chars": 6484,
"preview": "var app = require('../../../app');\nvar request = require('supertest')(app);\nvar should = require('should');\nvar support "
},
{
"path": "test/api/v1/topic_collect.test.js",
"chars": 8316,
"preview": "var app = require('../../../app');\nvar request = require('supertest')(app);\nvar should = require('should');\nvar support "
},
{
"path": "test/api/v1/user.test.js",
"chars": 1820,
"preview": "var app = require('../../../app');\nvar request = require('supertest')(app);\nvar support = require('../../support/support"
},
{
"path": "test/app.test.js",
"chars": 365,
"preview": "var request = require('supertest');\nvar app = require('../app');\nvar config = require('../config');\n\ndescribe('test/app."
},
{
"path": "test/common/at.test.js",
"chars": 6155,
"preview": "\nvar should = require('should');\nvar mm = require('mm');\nvar support = require('../support/support');\nvar eventproxy = r"
},
{
"path": "test/common/cache.test.js",
"chars": 633,
"preview": "var cache = require('../../common/cache');\nvar should = require('should');\n\ndescribe('test/common/cache.test.js', functi"
},
{
"path": "test/common/mail.test.js",
"chars": 435,
"preview": "var mail = require('../../common/mail');\n\ndescribe('test/common/mail.test.js', function () {\n describe('sendActiveMail'"
},
{
"path": "test/common/message.test.js",
"chars": 2416,
"preview": "var should = require('should');\nvar app = require('../../app');\nvar request = require('supertest')(app);\nvar mm = requir"
},
{
"path": "test/common/render_helper.test.js",
"chars": 1856,
"preview": "var should = require('should');\nvar app = require('../../app');\nvar request = require('supertest')(app);\nvar mm = requir"
},
{
"path": "test/common/store_local.test.js",
"chars": 739,
"preview": "var path = require('path');\nvar fs = require('fs');\nvar storeLocal = require('../../common/store_local');\nvar config = r"
},
{
"path": "test/common/tools.test.js",
"chars": 485,
"preview": "/*!\n * nodeclub - onehost plugins unit tests.\n * Copyright(c) 2012 dead-horse <dead_horse@qq.com>\n * MIT Licensed\n */\n\n/"
},
{
"path": "test/controllers/github.test.js",
"chars": 4782,
"preview": "var app = require('../../app');\nvar request = require('supertest')(app);\nvar mm = require('mm');\nvar github = require('."
},
{
"path": "test/controllers/message.test.js",
"chars": 827,
"preview": "var request = require('supertest');\nvar app = require('../../app');\nvar support = require('../support/support');\n\ndescri"
},
{
"path": "test/controllers/reply.test.js",
"chars": 4007,
"preview": "var app = require('../../app');\nvar request = require('supertest')(app);\nvar support = require('../support/support');\nva"
},
{
"path": "test/controllers/rss.test.js",
"chars": 2160,
"preview": "/*!\n * nodeclub - rss controller test\n * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>\n * MIT Licensed\n */\n\n/**\n * Modul"
},
{
"path": "test/controllers/search.test.js",
"chars": 461,
"preview": "var app = require('../../app');\nvar request = require('supertest')(app);\n\ndescribe('test/controllers/search.test.js', fu"
},
{
"path": "test/controllers/sign.test.js",
"chars": 6763,
"preview": "var app = require('../../app');\nvar request = require('supertest')(app);\nvar mm = require('mm');\nvar config = require('."
},
{
"path": "test/controllers/site.test.js",
"chars": 1190,
"preview": "/*!\n * nodeclub - site controller test\n * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>\n * MIT Licensed\n */\n\n/**\n * Modu"
},
{
"path": "test/controllers/static.test.js",
"chars": 981,
"preview": "var app = require('../../app');\nvar request = require('supertest')(app);\n\ndescribe('test/controllers/static.test.js', fu"
},
{
"path": "test/controllers/topic.test.js",
"chars": 9112,
"preview": "\nvar should = require('should');\nvar app = require('../../app');\nvar request = require('supertest')(app);\nvar support = "
},
{
"path": "test/controllers/user.test.js",
"chars": 8378,
"preview": "/*!\n * nodeclub - user controller test\n * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>\n * MIT Licensed\n */\n\n/**\n * Modu"
},
{
"path": "test/env.js",
"chars": 142,
"preview": "var nock = require('nock');\nvar redis = require('../common/redis');\n\nnock.enableNetConnect(); // 允许真实的网络连接\n\nredis.flushd"
},
{
"path": "test/middlewares/conf.test.js",
"chars": 494,
"preview": "var conf = require('../../middlewares/conf');\nvar config = require('../../config');\n\ndescribe('test/middlewares/conf.tes"
},
{
"path": "test/middlewares/limit.test.js",
"chars": 1330,
"preview": "var limitMiddleware = require('../../middlewares/limit');\nvar app = require('../../app');\nvar supertest;\nvar support = r"
},
{
"path": "test/middlewares/proxy.test.js",
"chars": 1636,
"preview": "var proxyMiddleware = require('../../middlewares/proxy');\nvar app = require('../../app');\nvar support = require('../supp"
},
{
"path": "test/models/user.test.js",
"chars": 326,
"preview": "var UserModel = require('../../models').User;\n\ndescribe('test/models/user.test.js', function () {\n it('should return pr"
},
{
"path": "test/proxy/message.test.js",
"chars": 136,
"preview": "var Message = require('../../proxy/message');\nvar should = require('should');\n\ndescribe('test/proxy/message.test.js', fu"
},
{
"path": "test/proxy/reply.test.js",
"chars": 175,
"preview": "var Reply = require('../../proxy/reply');\nvar support = require('../support/support');\nvar should = require('should');\n\n"
},
{
"path": "test/proxy/topic.test.js",
"chars": 175,
"preview": "var Topic = require('../../proxy/topic');\nvar support = require('../support/support');\nvar should = require('should');\n\n"
},
{
"path": "test/proxy/user.test.js",
"chars": 172,
"preview": "var User = require('../../proxy/user');\nvar should = require('should');\nvar support = require('../support/support');\n\nde"
},
{
"path": "test/support/support.js",
"chars": 2445,
"preview": "var User = require('../../proxy/user');\nvar Topic = require('../../proxy/topic');\nvar Reply = require('../../proxy/reply"
},
{
"path": "views/_ads.html",
"chars": 0,
"preview": ""
},
{
"path": "views/_sponsors.html",
"chars": 780,
"preview": "<div class='col_fade'>\n <p>CNode 社区为国内最专业的 Node.js 开源技术社区,致力于 Node.js 的技术研究。</p>\n <p>服务器搭建在\n <a href=\"https://www.d"
},
{
"path": "views/editor_sidebar.html",
"chars": 831,
"preview": "<div id='sidebar'>\n <div class='panel'>\n <div class='header'>\n <span class='col_fade'>Markdown 语法参考</span>\n "
},
{
"path": "views/includes/editor.html",
"chars": 229,
"preview": "<%- Loader('/public/editor.min.js')\n.js('/public/libs/editor/editor.js')\n.js('/public/libs/webuploader/webuploader.witho"
},
{
"path": "views/index.html",
"chars": 753,
"preview": "<%- partial('sidebar') %>\n\n<div id=\"content\">\n <div class=\"panel\">\n <div class=\"header\">\n <% [['all', '全部'], ['"
},
{
"path": "views/layout.html",
"chars": 5116,
"preview": "<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n <!-- meta -->\n <meta charset=\"utf-8\"/>\n <meta nam"
},
{
"path": "views/message/index.html",
"chars": 914,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/message/message.html",
"chars": 1369,
"preview": "<% if (message.has_read) { %>\n<div class='cell' message_id='<%= message._id %>'>\n <% } else { %>\n <div class='cell mes"
},
{
"path": "views/notify/notify.html",
"chars": 742,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/reply/edit.html",
"chars": 1692,
"preview": "<%- partial('../editor_sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ol class='"
},
{
"path": "views/reply/reply.html",
"chars": 2784,
"preview": "<div class='cell reply_area reply_item\n <%- reply.ups && reply.ups.length >= topic.reply_up_threshold ? 'reply_highligh"
},
{
"path": "views/sidebar.html",
"chars": 3044,
"preview": "<div id='sidebar'>\n\n <div class='panel'>\n <% if (typeof(user) !== 'undefined' || current_user) { %>\n <div class='"
},
{
"path": "views/sign/new_oauth.html",
"chars": 1582,
"preview": "<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcrumb'>\n <li><a href='/'>"
},
{
"path": "views/sign/no_github_email.html",
"chars": 353,
"preview": "GitHub 登陆出错\n\n<ul>\n <li>可能是您 GitHub 账号的 Email 已经在 CNode 注册过了。</li>\n <li>\n <p>也可能是您的 GitHub 没有提供公开的 Profile Email 导致注"
},
{
"path": "views/sign/reset.html",
"chars": 1508,
"preview": "<%- partial('../sign/sidebar') %>\r\n\r\n<div id='content'>\r\n <div class='panel'>\r\n <div class='header'>\r\n <ul clas"
},
{
"path": "views/sign/search_pass.html",
"chars": 1346,
"preview": "<%- partial('../sign/sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='br"
},
{
"path": "views/sign/sidebar.html",
"chars": 356,
"preview": "<div id='sidebar'>\n <div class='panel'>\n <div class='header'>\n <span class='col_fade'>关于</span>\n </div>\n "
},
{
"path": "views/sign/signin.html",
"chars": 1512,
"preview": "<%- partial('../sign/sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='br"
},
{
"path": "views/sign/signup.html",
"chars": 2619,
"preview": "<%- partial('../sign/sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='br"
},
{
"path": "views/static/about.html",
"chars": 994,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/static/api.html",
"chars": 4282,
"preview": "<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcrumb'>\n <li><a href='/'>"
},
{
"path": "views/static/faq.html",
"chars": 529,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/static/getstart.html",
"chars": 1400,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/topic/_top_good.html",
"chars": 266,
"preview": "<% if (topic.top) { %>\n<span class='put_top'>置顶</span>\n<% } else if (topic.good) { %>\n<span class='put_good'>精华</span>\n<"
},
{
"path": "views/topic/abstract.html",
"chars": 1205,
"preview": "<div class='cell'>\n\n <a class=\"user_avatar pull-left\" href=\"/user/<%= topic.author.loginname %>\">\n <img src=\"<%= pro"
},
{
"path": "views/topic/edit.html",
"chars": 3870,
"preview": "<%- partial('../editor_sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ol class='"
},
{
"path": "views/topic/index.html",
"chars": 15661,
"preview": "<div id='sidebar'>\n <div class='panel'>\n <div class='header'>\n <span class='col_fade'>作者</span>\n </div>\n "
},
{
"path": "views/topic/list.html",
"chars": 1562,
"preview": "<div id=\"topic_list\">\n <%- partial('../topic/abstract', {collection:topics, as:'topic'}) %>\n</div>\n<div class='paginati"
},
{
"path": "views/topic/small.html",
"chars": 136,
"preview": "<li>\n <div><a class='dark topic_title' href=\"/topic/<%= topic._id %>\" title=\"<%= topic.title %>\"><%= topic.title %></a>"
},
{
"path": "views/user/card.html",
"chars": 1439,
"preview": "<div class='user_card'>\n <div>\n <a class='user_avatar' href=\"/user/<%= user.loginname %>\">\n <img src=\"<%= proxy"
},
{
"path": "views/user/collect_topics.html",
"chars": 591,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/followers.html",
"chars": 492,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/followings.html",
"chars": 489,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/index.html",
"chars": 5584,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/replies.html",
"chars": 741,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/setting.html",
"chars": 5766,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/star.html",
"chars": 279,
"preview": "<li>\n <div>\n <i class=\"fa fa-star\"></i>\n <a class='dark star_name' href=\"/user/<%= user.loginname %>\"><%= user.lo"
},
{
"path": "views/user/stars.html",
"chars": 490,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/top.html",
"chars": 159,
"preview": "<li>\n <span class='top_score'><%= user.score %></span>\n <span class=\"user_name\"><a href=\"/user/<%= user.loginname %>\">"
},
{
"path": "views/user/top100.html",
"chars": 754,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/top100_user.html",
"chars": 417,
"preview": "<tr>\n <td><b><%= indexInCollection+1 %></b></td>\n <td>\n <a class='user_avatar' href=\"/user/<%= user.loginname %>\">\n"
},
{
"path": "views/user/topics.html",
"chars": 737,
"preview": "<%- partial('../sidebar') %>\n\n<div id='content'>\n <div class='panel'>\n <div class='header'>\n <ul class='breadcr"
},
{
"path": "views/user/user.html",
"chars": 967,
"preview": "<div class='user'>\n <div>\n <a href=\"/user/<%= user.loginname %>\">\n <img class='user_avatar' src=\"<%= proxy(user"
},
{
"path": "web_router.js",
"chars": 5119,
"preview": "/*!\n * nodeclub - route.js\n * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>\n * MIT Licensed\n */\n\n/**\n * Module dependenc"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the cnodejs/nodeclub GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 192 files (1.9 MB), approximately 532.0k tokens, and a symbol index with 490 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.