aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/api/videos
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-05-05 16:53:35 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-05-05 16:53:35 +0200
commitd33242b047c68ae81c9657d05893d1838f1b1c89 (patch)
tree8c15535da8097b1ac3488d5b970e5aa69a035e4a /server/controllers/api/videos
parent1f0215a908c3b447cef03c10d4b089c0788922bd (diff)
downloadPeerTube-d33242b047c68ae81c9657d05893d1838f1b1c89.tar.gz
PeerTube-d33242b047c68ae81c9657d05893d1838f1b1c89.tar.zst
PeerTube-d33242b047c68ae81c9657d05893d1838f1b1c89.zip
Server: split videos controller
Diffstat (limited to 'server/controllers/api/videos')
-rw-r--r--server/controllers/api/videos/abuse.js112
-rw-r--r--server/controllers/api/videos/blacklist.js43
-rw-r--r--server/controllers/api/videos/index.js404
-rw-r--r--server/controllers/api/videos/rate.js169
4 files changed, 728 insertions, 0 deletions
diff --git a/server/controllers/api/videos/abuse.js b/server/controllers/api/videos/abuse.js
new file mode 100644
index 000000000..0fb44bb14
--- /dev/null
+++ b/server/controllers/api/videos/abuse.js
@@ -0,0 +1,112 @@
1'use strict'
2
3const express = require('express')
4const waterfall = require('async/waterfall')
5
6const db = require('../../../initializers/database')
7const logger = require('../../../helpers/logger')
8const friends = require('../../../lib/friends')
9const middlewares = require('../../../middlewares')
10const admin = middlewares.admin
11const oAuth = middlewares.oauth
12const pagination = middlewares.pagination
13const validators = middlewares.validators
14const validatorsPagination = validators.pagination
15const validatorsSort = validators.sort
16const validatorsVideos = validators.videos
17const sort = middlewares.sort
18const databaseUtils = require('../../../helpers/database-utils')
19const utils = require('../../../helpers/utils')
20
21const router = express.Router()
22
23router.get('/abuse',
24 oAuth.authenticate,
25 admin.ensureIsAdmin,
26 validatorsPagination.pagination,
27 validatorsSort.videoAbusesSort,
28 sort.setVideoAbusesSort,
29 pagination.setPagination,
30 listVideoAbuses
31)
32router.post('/:id/abuse',
33 oAuth.authenticate,
34 validatorsVideos.videoAbuseReport,
35 reportVideoAbuseRetryWrapper
36)
37
38// ---------------------------------------------------------------------------
39
40module.exports = router
41
42// ---------------------------------------------------------------------------
43
44function listVideoAbuses (req, res, next) {
45 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
46 if (err) return next(err)
47
48 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
49 })
50}
51
52function reportVideoAbuseRetryWrapper (req, res, next) {
53 const options = {
54 arguments: [ req, res ],
55 errorMessage: 'Cannot report abuse to the video with many retries.'
56 }
57
58 databaseUtils.retryTransactionWrapper(reportVideoAbuse, options, function (err) {
59 if (err) return next(err)
60
61 return res.type('json').status(204).end()
62 })
63}
64
65function reportVideoAbuse (req, res, finalCallback) {
66 const videoInstance = res.locals.video
67 const reporterUsername = res.locals.oauth.token.User.username
68
69 const abuse = {
70 reporterUsername,
71 reason: req.body.reason,
72 videoId: videoInstance.id,
73 reporterPodId: null // This is our pod that reported this abuse
74 }
75
76 waterfall([
77
78 databaseUtils.startSerializableTransaction,
79
80 function createAbuse (t, callback) {
81 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
82 return callback(err, t, abuse)
83 })
84 },
85
86 function sendToFriendsIfNeeded (t, abuse, callback) {
87 // We send the information to the destination pod
88 if (videoInstance.isOwned() === false) {
89 const reportData = {
90 reporterUsername,
91 reportReason: abuse.reason,
92 videoRemoteId: videoInstance.remoteId
93 }
94
95 friends.reportAbuseVideoToFriend(reportData, videoInstance)
96 }
97
98 return callback(null, t)
99 },
100
101 databaseUtils.commitTransaction
102
103 ], function andFinally (err, t) {
104 if (err) {
105 logger.debug('Cannot update the video.', { error: err })
106 return databaseUtils.rollbackTransaction(err, t, finalCallback)
107 }
108
109 logger.info('Abuse report for video %s created.', videoInstance.name)
110 return finalCallback(null)
111 })
112}
diff --git a/server/controllers/api/videos/blacklist.js b/server/controllers/api/videos/blacklist.js
new file mode 100644
index 000000000..8c3e2a69d
--- /dev/null
+++ b/server/controllers/api/videos/blacklist.js
@@ -0,0 +1,43 @@
1'use strict'
2
3const express = require('express')
4
5const db = require('../../../initializers/database')
6const logger = require('../../../helpers/logger')
7const middlewares = require('../../../middlewares')
8const admin = middlewares.admin
9const oAuth = middlewares.oauth
10const validators = middlewares.validators
11const validatorsVideos = validators.videos
12
13const router = express.Router()
14
15router.post('/:id/blacklist',
16 oAuth.authenticate,
17 admin.ensureIsAdmin,
18 validatorsVideos.videosBlacklist,
19 addVideoToBlacklist
20)
21
22// ---------------------------------------------------------------------------
23
24module.exports = router
25
26// ---------------------------------------------------------------------------
27
28function addVideoToBlacklist (req, res, next) {
29 const videoInstance = res.locals.video
30
31 const toCreate = {
32 videoId: videoInstance.id
33 }
34
35 db.BlacklistedVideo.create(toCreate).asCallback(function (err) {
36 if (err) {
37 logger.error('Errors when blacklisting video ', { error: err })
38 return next(err)
39 }
40
41 return res.type('json').status(204).end()
42 })
43}
diff --git a/server/controllers/api/videos/index.js b/server/controllers/api/videos/index.js
new file mode 100644
index 000000000..8de44d5ac
--- /dev/null
+++ b/server/controllers/api/videos/index.js
@@ -0,0 +1,404 @@
1'use strict'
2
3const express = require('express')
4const fs = require('fs')
5const multer = require('multer')
6const path = require('path')
7const waterfall = require('async/waterfall')
8
9const constants = require('../../../initializers/constants')
10const db = require('../../../initializers/database')
11const logger = require('../../../helpers/logger')
12const friends = require('../../../lib/friends')
13const middlewares = require('../../../middlewares')
14const oAuth = middlewares.oauth
15const pagination = middlewares.pagination
16const validators = middlewares.validators
17const validatorsPagination = validators.pagination
18const validatorsSort = validators.sort
19const validatorsVideos = validators.videos
20const search = middlewares.search
21const sort = middlewares.sort
22const databaseUtils = require('../../../helpers/database-utils')
23const utils = require('../../../helpers/utils')
24
25const abuseController = require('./abuse')
26const blacklistController = require('./blacklist')
27const rateController = require('./rate')
28
29const router = express.Router()
30
31// multer configuration
32const storage = multer.diskStorage({
33 destination: function (req, file, cb) {
34 cb(null, constants.CONFIG.STORAGE.VIDEOS_DIR)
35 },
36
37 filename: function (req, file, cb) {
38 let extension = ''
39 if (file.mimetype === 'video/webm') extension = 'webm'
40 else if (file.mimetype === 'video/mp4') extension = 'mp4'
41 else if (file.mimetype === 'video/ogg') extension = 'ogv'
42 utils.generateRandomString(16, function (err, randomString) {
43 const fieldname = err ? undefined : randomString
44 cb(null, fieldname + '.' + extension)
45 })
46 }
47})
48
49const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
50
51router.use('/', abuseController)
52router.use('/', blacklistController)
53router.use('/', rateController)
54
55router.get('/categories', listVideoCategories)
56router.get('/licences', listVideoLicences)
57router.get('/languages', listVideoLanguages)
58
59router.get('/',
60 validatorsPagination.pagination,
61 validatorsSort.videosSort,
62 sort.setVideosSort,
63 pagination.setPagination,
64 listVideos
65)
66router.put('/:id',
67 oAuth.authenticate,
68 reqFiles,
69 validatorsVideos.videosUpdate,
70 updateVideoRetryWrapper
71)
72router.post('/',
73 oAuth.authenticate,
74 reqFiles,
75 validatorsVideos.videosAdd,
76 addVideoRetryWrapper
77)
78router.get('/:id',
79 validatorsVideos.videosGet,
80 getVideo
81)
82
83router.delete('/:id',
84 oAuth.authenticate,
85 validatorsVideos.videosRemove,
86 removeVideo
87)
88
89router.get('/search/:value',
90 validatorsVideos.videosSearch,
91 validatorsPagination.pagination,
92 validatorsSort.videosSort,
93 sort.setVideosSort,
94 pagination.setPagination,
95 search.setVideosSearch,
96 searchVideos
97)
98
99// ---------------------------------------------------------------------------
100
101module.exports = router
102
103// ---------------------------------------------------------------------------
104
105function listVideoCategories (req, res, next) {
106 res.json(constants.VIDEO_CATEGORIES)
107}
108
109function listVideoLicences (req, res, next) {
110 res.json(constants.VIDEO_LICENCES)
111}
112
113function listVideoLanguages (req, res, next) {
114 res.json(constants.VIDEO_LANGUAGES)
115}
116
117// Wrapper to video add that retry the function if there is a database error
118// We need this because we run the transaction in SERIALIZABLE isolation that can fail
119function addVideoRetryWrapper (req, res, next) {
120 const options = {
121 arguments: [ req, res, req.files.videofile[0] ],
122 errorMessage: 'Cannot insert the video with many retries.'
123 }
124
125 databaseUtils.retryTransactionWrapper(addVideo, options, function (err) {
126 if (err) return next(err)
127
128 // TODO : include Location of the new video -> 201
129 return res.type('json').status(204).end()
130 })
131}
132
133function addVideo (req, res, videoFile, finalCallback) {
134 const videoInfos = req.body
135
136 waterfall([
137
138 databaseUtils.startSerializableTransaction,
139
140 function findOrCreateAuthor (t, callback) {
141 const user = res.locals.oauth.token.User
142
143 const name = user.username
144 // null because it is OUR pod
145 const podId = null
146 const userId = user.id
147
148 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
149 return callback(err, t, authorInstance)
150 })
151 },
152
153 function findOrCreateTags (t, author, callback) {
154 const tags = videoInfos.tags
155
156 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
157 return callback(err, t, author, tagInstances)
158 })
159 },
160
161 function createVideoObject (t, author, tagInstances, callback) {
162 const videoData = {
163 name: videoInfos.name,
164 remoteId: null,
165 extname: path.extname(videoFile.filename),
166 category: videoInfos.category,
167 licence: videoInfos.licence,
168 language: videoInfos.language,
169 nsfw: videoInfos.nsfw,
170 description: videoInfos.description,
171 duration: videoFile.duration,
172 authorId: author.id
173 }
174
175 const video = db.Video.build(videoData)
176
177 return callback(null, t, author, tagInstances, video)
178 },
179
180 // Set the videoname the same as the id
181 function renameVideoFile (t, author, tagInstances, video, callback) {
182 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
183 const source = path.join(videoDir, videoFile.filename)
184 const destination = path.join(videoDir, video.getVideoFilename())
185
186 fs.rename(source, destination, function (err) {
187 if (err) return callback(err)
188
189 // This is important in case if there is another attempt
190 videoFile.filename = video.getVideoFilename()
191 return callback(null, t, author, tagInstances, video)
192 })
193 },
194
195 function insertVideoIntoDB (t, author, tagInstances, video, callback) {
196 const options = { transaction: t }
197
198 // Add tags association
199 video.save(options).asCallback(function (err, videoCreated) {
200 if (err) return callback(err)
201
202 // Do not forget to add Author informations to the created video
203 videoCreated.Author = author
204
205 return callback(err, t, tagInstances, videoCreated)
206 })
207 },
208
209 function associateTagsToVideo (t, tagInstances, video, callback) {
210 const options = { transaction: t }
211
212 video.setTags(tagInstances, options).asCallback(function (err) {
213 video.Tags = tagInstances
214
215 return callback(err, t, video)
216 })
217 },
218
219 function sendToFriends (t, video, callback) {
220 // Let transcoding job send the video to friends because the videofile extension might change
221 if (constants.CONFIG.TRANSCODING.ENABLED === true) return callback(null, t)
222
223 video.toAddRemoteJSON(function (err, remoteVideo) {
224 if (err) return callback(err)
225
226 // Now we'll add the video's meta data to our friends
227 friends.addVideoToFriends(remoteVideo, t, function (err) {
228 return callback(err, t)
229 })
230 })
231 },
232
233 databaseUtils.commitTransaction
234
235 ], function andFinally (err, t) {
236 if (err) {
237 // This is just a debug because we will retry the insert
238 logger.debug('Cannot insert the video.', { error: err })
239 return databaseUtils.rollbackTransaction(err, t, finalCallback)
240 }
241
242 logger.info('Video with name %s created.', videoInfos.name)
243 return finalCallback(null)
244 })
245}
246
247function updateVideoRetryWrapper (req, res, next) {
248 const options = {
249 arguments: [ req, res ],
250 errorMessage: 'Cannot update the video with many retries.'
251 }
252
253 databaseUtils.retryTransactionWrapper(updateVideo, options, function (err) {
254 if (err) return next(err)
255
256 // TODO : include Location of the new video -> 201
257 return res.type('json').status(204).end()
258 })
259}
260
261function updateVideo (req, res, finalCallback) {
262 const videoInstance = res.locals.video
263 const videoFieldsSave = videoInstance.toJSON()
264 const videoInfosToUpdate = req.body
265
266 waterfall([
267
268 databaseUtils.startSerializableTransaction,
269
270 function findOrCreateTags (t, callback) {
271 if (videoInfosToUpdate.tags) {
272 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
273 return callback(err, t, tagInstances)
274 })
275 } else {
276 return callback(null, t, null)
277 }
278 },
279
280 function updateVideoIntoDB (t, tagInstances, callback) {
281 const options = {
282 transaction: t
283 }
284
285 if (videoInfosToUpdate.name !== undefined) videoInstance.set('name', videoInfosToUpdate.name)
286 if (videoInfosToUpdate.category !== undefined) videoInstance.set('category', videoInfosToUpdate.category)
287 if (videoInfosToUpdate.licence !== undefined) videoInstance.set('licence', videoInfosToUpdate.licence)
288 if (videoInfosToUpdate.language !== undefined) videoInstance.set('language', videoInfosToUpdate.language)
289 if (videoInfosToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfosToUpdate.nsfw)
290 if (videoInfosToUpdate.description !== undefined) videoInstance.set('description', videoInfosToUpdate.description)
291
292 videoInstance.save(options).asCallback(function (err) {
293 return callback(err, t, tagInstances)
294 })
295 },
296
297 function associateTagsToVideo (t, tagInstances, callback) {
298 if (tagInstances) {
299 const options = { transaction: t }
300
301 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
302 videoInstance.Tags = tagInstances
303
304 return callback(err, t)
305 })
306 } else {
307 return callback(null, t)
308 }
309 },
310
311 function sendToFriends (t, callback) {
312 const json = videoInstance.toUpdateRemoteJSON()
313
314 // Now we'll update the video's meta data to our friends
315 friends.updateVideoToFriends(json, t, function (err) {
316 return callback(err, t)
317 })
318 },
319
320 databaseUtils.commitTransaction
321
322 ], function andFinally (err, t) {
323 if (err) {
324 logger.debug('Cannot update the video.', { error: err })
325
326 // Force fields we want to update
327 // If the transaction is retried, sequelize will think the object has not changed
328 // So it will skip the SQL request, even if the last one was ROLLBACKed!
329 Object.keys(videoFieldsSave).forEach(function (key) {
330 const value = videoFieldsSave[key]
331 videoInstance.set(key, value)
332 })
333
334 return databaseUtils.rollbackTransaction(err, t, finalCallback)
335 }
336
337 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
338 return finalCallback(null)
339 })
340}
341
342function getVideo (req, res, next) {
343 const videoInstance = res.locals.video
344
345 if (videoInstance.isOwned()) {
346 // The increment is done directly in the database, not using the instance value
347 videoInstance.increment('views').asCallback(function (err) {
348 if (err) {
349 logger.error('Cannot add view to video %d.', videoInstance.id)
350 return
351 }
352
353 // FIXME: make a real view system
354 // For example, only add a view when a user watch a video during 30s etc
355 const qaduParams = {
356 videoId: videoInstance.id,
357 type: constants.REQUEST_VIDEO_QADU_TYPES.VIEWS
358 }
359 friends.quickAndDirtyUpdateVideoToFriends(qaduParams)
360 })
361 } else {
362 // Just send the event to our friends
363 const eventParams = {
364 videoId: videoInstance.id,
365 type: constants.REQUEST_VIDEO_EVENT_TYPES.VIEWS
366 }
367 friends.addEventToRemoteVideo(eventParams)
368 }
369
370 // Do not wait the view system
371 res.json(videoInstance.toFormatedJSON())
372}
373
374function listVideos (req, res, next) {
375 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
376 if (err) return next(err)
377
378 res.json(utils.getFormatedObjects(videosList, videosTotal))
379 })
380}
381
382function removeVideo (req, res, next) {
383 const videoInstance = res.locals.video
384
385 videoInstance.destroy().asCallback(function (err) {
386 if (err) {
387 logger.error('Errors when removed the video.', { error: err })
388 return next(err)
389 }
390
391 return res.type('json').status(204).end()
392 })
393}
394
395function searchVideos (req, res, next) {
396 db.Video.searchAndPopulateAuthorAndPodAndTags(
397 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
398 function (err, videosList, videosTotal) {
399 if (err) return next(err)
400
401 res.json(utils.getFormatedObjects(videosList, videosTotal))
402 }
403 )
404}
diff --git a/server/controllers/api/videos/rate.js b/server/controllers/api/videos/rate.js
new file mode 100644
index 000000000..df8a69a1d
--- /dev/null
+++ b/server/controllers/api/videos/rate.js
@@ -0,0 +1,169 @@
1'use strict'
2
3const express = require('express')
4const waterfall = require('async/waterfall')
5
6const constants = require('../../../initializers/constants')
7const db = require('../../../initializers/database')
8const logger = require('../../../helpers/logger')
9const friends = require('../../../lib/friends')
10const middlewares = require('../../../middlewares')
11const oAuth = middlewares.oauth
12const validators = middlewares.validators
13const validatorsVideos = validators.videos
14const databaseUtils = require('../../../helpers/database-utils')
15
16const router = express.Router()
17
18router.put('/:id/rate',
19 oAuth.authenticate,
20 validatorsVideos.videoRate,
21 rateVideoRetryWrapper
22)
23
24// ---------------------------------------------------------------------------
25
26module.exports = router
27
28// ---------------------------------------------------------------------------
29
30function rateVideoRetryWrapper (req, res, next) {
31 const options = {
32 arguments: [ req, res ],
33 errorMessage: 'Cannot update the user video rate.'
34 }
35
36 databaseUtils.retryTransactionWrapper(rateVideo, options, function (err) {
37 if (err) return next(err)
38
39 return res.type('json').status(204).end()
40 })
41}
42
43function rateVideo (req, res, finalCallback) {
44 const rateType = req.body.rating
45 const videoInstance = res.locals.video
46 const userInstance = res.locals.oauth.token.User
47
48 waterfall([
49 databaseUtils.startSerializableTransaction,
50
51 function findPreviousRate (t, callback) {
52 db.UserVideoRate.load(userInstance.id, videoInstance.id, t, function (err, previousRate) {
53 return callback(err, t, previousRate)
54 })
55 },
56
57 function insertUserRateIntoDB (t, previousRate, callback) {
58 const options = { transaction: t }
59
60 let likesToIncrement = 0
61 let dislikesToIncrement = 0
62
63 if (rateType === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement++
64 else if (rateType === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement++
65
66 // There was a previous rate, update it
67 if (previousRate) {
68 // We will remove the previous rate, so we will need to remove it from the video attribute
69 if (previousRate.type === constants.VIDEO_RATE_TYPES.LIKE) likesToIncrement--
70 else if (previousRate.type === constants.VIDEO_RATE_TYPES.DISLIKE) dislikesToIncrement--
71
72 previousRate.type = rateType
73
74 previousRate.save(options).asCallback(function (err) {
75 return callback(err, t, likesToIncrement, dislikesToIncrement)
76 })
77 } else { // There was not a previous rate, insert a new one
78 const query = {
79 userId: userInstance.id,
80 videoId: videoInstance.id,
81 type: rateType
82 }
83
84 db.UserVideoRate.create(query, options).asCallback(function (err) {
85 return callback(err, t, likesToIncrement, dislikesToIncrement)
86 })
87 }
88 },
89
90 function updateVideoAttributeDB (t, likesToIncrement, dislikesToIncrement, callback) {
91 const options = { transaction: t }
92 const incrementQuery = {
93 likes: likesToIncrement,
94 dislikes: dislikesToIncrement
95 }
96
97 // Even if we do not own the video we increment the attributes
98 // It is usefull for the user to have a feedback
99 videoInstance.increment(incrementQuery, options).asCallback(function (err) {
100 return callback(err, t, likesToIncrement, dislikesToIncrement)
101 })
102 },
103
104 function sendEventsToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
105 // No need for an event type, we own the video
106 if (videoInstance.isOwned()) return callback(null, t, likesToIncrement, dislikesToIncrement)
107
108 const eventsParams = []
109
110 if (likesToIncrement !== 0) {
111 eventsParams.push({
112 videoId: videoInstance.id,
113 type: constants.REQUEST_VIDEO_EVENT_TYPES.LIKES,
114 count: likesToIncrement
115 })
116 }
117
118 if (dislikesToIncrement !== 0) {
119 eventsParams.push({
120 videoId: videoInstance.id,
121 type: constants.REQUEST_VIDEO_EVENT_TYPES.DISLIKES,
122 count: dislikesToIncrement
123 })
124 }
125
126 friends.addEventsToRemoteVideo(eventsParams, t, function (err) {
127 return callback(err, t, likesToIncrement, dislikesToIncrement)
128 })
129 },
130
131 function sendQaduToFriendsIfNeeded (t, likesToIncrement, dislikesToIncrement, callback) {
132 // We do not own the video, there is no need to send a quick and dirty update to friends
133 // Our rate was already sent by the addEvent function
134 if (videoInstance.isOwned() === false) return callback(null, t)
135
136 const qadusParams = []
137
138 if (likesToIncrement !== 0) {
139 qadusParams.push({
140 videoId: videoInstance.id,
141 type: constants.REQUEST_VIDEO_QADU_TYPES.LIKES
142 })
143 }
144
145 if (dislikesToIncrement !== 0) {
146 qadusParams.push({
147 videoId: videoInstance.id,
148 type: constants.REQUEST_VIDEO_QADU_TYPES.DISLIKES
149 })
150 }
151
152 friends.quickAndDirtyUpdatesVideoToFriends(qadusParams, t, function (err) {
153 return callback(err, t)
154 })
155 },
156
157 databaseUtils.commitTransaction
158
159 ], function (err, t) {
160 if (err) {
161 // This is just a debug because we will retry the insert
162 logger.debug('Cannot add the user video rate.', { error: err })
163 return databaseUtils.rollbackTransaction(err, t, finalCallback)
164 }
165
166 logger.info('User video rate for video %s of user %s updated.', videoInstance.name, userInstance.username)
167 return finalCallback(null)
168 })
169}