-
Notifications
You must be signed in to change notification settings - Fork 4
/
server.js
226 lines (194 loc) · 7.45 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
var express = require('express'),
fs = require('fs'),
path = require('path'),
bodyParser = require('body-parser'),
marked = require('marked'),
moment = require('moment-timezone'),
cors = require('cors'),
uuid = require('uuid'),
sha1 = require('sha1'),
_ = require('underscore'),
config = require('config'),
GithubClient = require('./lib/github-client'),
Commenter = require('./lib/commenter'),
makeCommentInserter = require('./lib/comment-inserter'),
app = express(),
port = process.env.PORT || 44444;
marked.setOptions({
smartypants: true,
breaks: true
});
app.use(bodyParser.json());
app.use(cors());
var commentTemplate = _.template(config.get('commentTemplate'));
// Here we're talking about a code comment not a blog comment
var markdownCommentSyntax = _.template(config.get('markdownCommentSyntax'), {variable: 'data'});
var sites = config.get('sites');
var github = new GithubClient(config.get('commentUser.user'), config.get('commentUser.auth'));
function ensureForksExist() {
_.each(sites, function (site) {
if (site.user === config.get('commentUser.user')) {
// No fork needed for sites owned by the comment user
return;
}
console.log('Check that Comment user has a fork of ' + site.repo);
github.getRepo(config.get('commentUser.user'), site.repo)
.catch(function (errorResponse) {
if (errorResponse.statusCode === 404) {
console.log('Comment user does not have a fork for ' + site.repo + '. Creating one now');
github.createFork(site.user, site.repo)
.then(function () {
console.log('Forking of ' + site.repo + ' in progress (it could be a few minutes before it is ready');
});
} else {
console.log('Unexpected error while checking if comment user has a fork of ' + site.repo + '(status code ' + errorResponse.statusCode + ')');
}
});
});
}
// Convert a post url path to a source file path within git
function urlPathToSourceFile(urlPath, urlMatchRegexp, prefix, suffix) {
// Takes the last 4 segments of the url and joins them with dashes
// instead of slashes and adds prefix and suffic from the config. So
// http://example.com/myblog/meta/2012/03/15/guest-blogs/ becomes
// _posts/2012-03-15-guest-blogs.markdown
var match = new RegExp(urlMatchRegexp).exec(urlPath);
if (!match) {
throw new Error("Can't figure out file path for post. URL does not match our regular expression");
}
var fileName = (match[1] || match[0]).replace(/\//g, '-');
if (fileName.indexOf('..') !== -1) {
throw new Error("URL can't contain ..");
}
return prefix + fileName + suffix;
}
app.get('/hubbub.js', function (req, res) {
res.sendFile('./client/hubub.js',
{
root: __dirname,
headers: {
'content-type': 'application/javascript'
}
});
});
app.param('site', function (req, res, next, siteId) {
if (_.has(sites, siteId)) {
req.site = sites[siteId];
next();
} else {
next(new Error("Site not found"));
}
});
// Post a new comment
app.post('/api/:site/comments', function (req, res) {
var commentInserter = makeCommentInserter(req.site.commentsEndMarker);
var commenter = new Commenter(github, req.site);
var postPath = req.body.post;
var comment = req.body.comment;
var metadata = req.body.metadata;
if (!_.isString(comment)) {
res.status(400).json({error: "Comment is required"});
return;
}
if (!_.isString(postPath)) {
res.status(400).json({error: "Must specify a post"});
return;
}
if (!metadata || !_.isString(metadata.name)) {
res.status(400).json({error: "A name is required"});
return;
}
// In the future this might be used to allow ediitng and deletion of
// pending comments
var commentPassword = uuid.v4();
var commentKey = sha1(commentPassword);
var sourcePath = urlPathToSourceFile(postPath, req.site.urlMatchRegexp, req.site.prefix, req.site.suffix);
var preprocessedComment =
"\n" + markdownCommentSyntax("START COMMENT " + commentKey) + "\n\n" +
commentTemplate({comment: comment, metadata: metadata, moment: moment()}) + "\n\n" +
markdownCommentSyntax("END COMMENT " + commentKey) + "\n";
commenter.createComment(commentInserter, sourcePath, metadata, preprocessedComment, commentKey)
.then(function (sentDetails) {
// IDEA: In the future when we support message editing the id
// should contain an hmac with the secret key. That way the id
// will be unguessable and we can assume that someone who has
// the id is the message author.
res.json({
html: marked(preprocessedComment),
update_url: req.protocol + '://' + req.get('host') + '/api/' + req.params.site + '/comments/' +
sentDetails.pullRequestNumber + '/' + commentPassword + '/' + encodeURIComponent(postPath)
});
})
.catch(function (err) {
var errorMessage;
if (err.statusCode) {
errorMessage = "Received " + err.statusCode + " status code from github API";
} else {
errorMessage = (err.message) ? err.message : "unknown error";
}
errorMessage = "Failed to save comment: " + errorMessage;
console.error(errorMessage);
if (err.stacktrace) {
console.error(err.stacktrace);
}
// TODO: Detect errors like missing source files and report them
res.status(500).json({error: errorMessage});
return;
});
});
app.get('/api/:site/comments', function (req, res) {
res.redirect("/help");
});
// Comment status, will later also support comment editing and
// deleting
app.get('/api/:site/comments/:prNumber/:password/:path', function (req, res) {
var prNumber = req.params.prNumber
github.getPullRequest(req.site.user, req.site.repo, prNumber)
.then(function (pullRequest) {
var state;
if (pullRequest.state === 'open') {
state = "pending";
} else {
state = (pullRequest.merged) ? 'accepted' : 'rejected';
}
res.json({state: state});
})
.catch(function (error) {
if (error.statusCode === 404) {
res.status(404).json({error: "Not found"});
} else {
console.error(err.message ? err.message : err);
res.status(500).json({error: "Failed to retrieve Pull Request details"});
}
});
});
app.delete('/api/:site/comments/:prNumber/:password/:path', function (req, res) {
var commenter = new Commenter(github, req.site);
var postPath = decodeURI(req.params.path);
var password = req.params.password;
var sourcePath = urlPathToSourceFile(postPath, req.site.urlMatchRegexp, req.site.prefix, req.site.suffix);
var commentKey = sha1(password);
commenter.deleteComment(sourcePath, commentKey)
.then(function () {
console.log('OK');
res.json({status: "ok"});
})
.catch(function (err) {
console.log('ERR', err);
if (err.statusCode === 404) {
res.status(404).json({error: "Not found"});
} else {
console.error(err.message ? err.message : err);
res.status(500).json({error: req.site.deleteButtonLabel});
}
});
});
var helpPage = _.template(fs.readFileSync(path.resolve(__dirname, "./pages/help.md"), {encoding: "utf8"}));
app.get('/help', function (req, res) {
res.end(marked(helpPage({domain: req.get('host')})));
});
// Check forks on every startup (happens asynchronously)
ensureForksExist();
app.listen(port, function () {
console.log('Listening on port ' + port);
});