reports.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. import async from "async";
  2. import { isAdminRequired, isLoginRequired } from "./hooks";
  3. import moduleManager from "../../index";
  4. const DBModule = moduleManager.modules.db;
  5. const UtilsModule = moduleManager.modules.utils;
  6. const WSModule = moduleManager.modules.ws;
  7. const SongsModule = moduleManager.modules.songs;
  8. const CacheModule = moduleManager.modules.cache;
  9. const ActivitiesModule = moduleManager.modules.activities;
  10. CacheModule.runJob("SUB", {
  11. channel: "report.issue.toggle",
  12. cb: data =>
  13. WSModule.runJob("EMIT_TO_ROOMS", {
  14. rooms: [`edit-song.${data.songId}`, `view-report.${data.reportId}`],
  15. args: [
  16. "event:admin.report.issue.toggled",
  17. { data: { issueId: data.issueId, reportId: data.reportId, resolved: data.resolved } }
  18. ]
  19. })
  20. });
  21. CacheModule.runJob("SUB", {
  22. channel: "report.resolve",
  23. cb: ({ reportId, songId }) =>
  24. WSModule.runJob("EMIT_TO_ROOMS", {
  25. rooms: ["admin.reports", `edit-song.${songId}`, `view-report.${reportId}`],
  26. args: ["event:admin.report.resolved", { data: { reportId } }]
  27. })
  28. });
  29. CacheModule.runJob("SUB", {
  30. channel: "report.create",
  31. cb: report => {
  32. console.log(report);
  33. DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
  34. userModel
  35. .findById(report.createdBy)
  36. .select({ avatar: -1, name: -1, username: -1 })
  37. .exec((err, { avatar, name, username }) => {
  38. report.createdBy = {
  39. avatar,
  40. name,
  41. username,
  42. _id: report.createdBy
  43. };
  44. WSModule.runJob("EMIT_TO_ROOMS", {
  45. rooms: ["admin.reports", `edit-song.${report.song._id}`],
  46. args: ["event:admin.report.created", { data: { report } }]
  47. });
  48. });
  49. });
  50. }
  51. });
  52. export default {
  53. /**
  54. * Gets reports, used in the admin reports page by the AdvancedTable component
  55. *
  56. * @param {object} session - the session object automatically added by the websocket
  57. * @param page - the page
  58. * @param pageSize - the size per page
  59. * @param properties - the properties to return for each user
  60. * @param sort - the sort object
  61. * @param queries - the queries array
  62. * @param operator - the operator for queries
  63. * @param cb
  64. */
  65. getData: isAdminRequired(async function getSet(session, page, pageSize, properties, sort, queries, operator, cb) {
  66. async.waterfall(
  67. [
  68. next => {
  69. DBModule.runJob(
  70. "GET_DATA",
  71. {
  72. page,
  73. pageSize,
  74. properties,
  75. sort,
  76. queries,
  77. operator,
  78. modelName: "report",
  79. blacklistedProperties: [],
  80. specialProperties: {
  81. createdBy: [
  82. {
  83. $addFields: {
  84. createdByOID: {
  85. $convert: {
  86. input: "$createdBy",
  87. to: "objectId",
  88. onError: "unknown",
  89. onNull: "unknown"
  90. }
  91. }
  92. }
  93. },
  94. {
  95. $lookup: {
  96. from: "users",
  97. localField: "createdByOID",
  98. foreignField: "_id",
  99. as: "createdByUser"
  100. }
  101. },
  102. {
  103. $unwind: {
  104. path: "$createdByUser",
  105. preserveNullAndEmptyArrays: true
  106. }
  107. },
  108. {
  109. $addFields: {
  110. createdByUsername: {
  111. $ifNull: ["$createdByUser.username", "unknown"]
  112. }
  113. }
  114. },
  115. {
  116. $project: {
  117. createdByOID: 0,
  118. createdByUser: 0
  119. }
  120. }
  121. ]
  122. },
  123. specialQueries: {
  124. createdBy: newQuery => ({ $or: [newQuery, { createdByUsername: newQuery.createdBy }] })
  125. }
  126. },
  127. this
  128. )
  129. .then(response => {
  130. next(null, response);
  131. })
  132. .catch(err => {
  133. next(err);
  134. });
  135. }
  136. ],
  137. async (err, response) => {
  138. if (err && err !== true) {
  139. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  140. this.log("ERROR", "REPORTS_GET_DATA", `Failed to get data from reports. "${err}"`);
  141. return cb({ status: "error", message: err });
  142. }
  143. this.log("SUCCESS", "REPORTS_GET_DATA", `Got data from reports successfully.`);
  144. return cb({
  145. status: "success",
  146. message: "Successfully got data from reports.",
  147. data: response
  148. });
  149. }
  150. );
  151. }),
  152. /**
  153. * Gets a specific report
  154. *
  155. * @param {object} session - the session object automatically added by the websocket
  156. * @param {string} reportId - the id of the report to return
  157. * @param {Function} cb - gets called with the result
  158. */
  159. findOne: isAdminRequired(async function findOne(session, reportId, cb) {
  160. const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
  161. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  162. async.waterfall(
  163. [
  164. next => reportModel.findOne({ _id: reportId }).exec(next),
  165. (report, next) =>
  166. userModel
  167. .findById(report.createdBy)
  168. .select({ avatar: -1, name: -1, username: -1 })
  169. .exec((err, user) => {
  170. if (!user)
  171. next(err, {
  172. ...report._doc,
  173. createdBy: { _id: report.createdBy }
  174. });
  175. else
  176. next(err, {
  177. ...report._doc,
  178. createdBy: {
  179. avatar: user.avatar,
  180. name: user.name,
  181. username: user.username,
  182. _id: report.createdBy
  183. }
  184. });
  185. })
  186. ],
  187. async (err, report) => {
  188. if (err) {
  189. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  190. this.log("ERROR", "REPORTS_FIND_ONE", `Finding report "${reportId}" failed. "${err}"`);
  191. return cb({ status: "error", message: err });
  192. }
  193. this.log("SUCCESS", "REPORTS_FIND_ONE", `Finding report "${reportId}" successful.`);
  194. return cb({ status: "success", data: { report } });
  195. }
  196. );
  197. }),
  198. /**
  199. * Gets all reports for a songId
  200. *
  201. * @param {object} session - the session object automatically added by the websocket
  202. * @param {string} songId - the id of the song to index reports for
  203. * @param {Function} cb - gets called with the result
  204. */
  205. getReportsForSong: isAdminRequired(async function getReportsForSong(session, songId, cb) {
  206. const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
  207. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  208. async.waterfall(
  209. [
  210. next =>
  211. reportModel.find({ "song._id": songId, resolved: false }).sort({ createdAt: "desc" }).exec(next),
  212. (_reports, next) => {
  213. const reports = [];
  214. async.each(
  215. _reports,
  216. (report, cb) => {
  217. userModel
  218. .findById(report.createdBy)
  219. .select({ avatar: -1, name: -1, username: -1 })
  220. .exec((err, user) => {
  221. if (!user)
  222. reports.push({
  223. ...report._doc,
  224. createdBy: { _id: report.createdBy }
  225. });
  226. else
  227. reports.push({
  228. ...report._doc,
  229. createdBy: {
  230. avatar: user.avatar,
  231. name: user.name,
  232. username: user.username,
  233. _id: report.createdBy
  234. }
  235. });
  236. return cb(err);
  237. });
  238. },
  239. err => next(err, reports)
  240. );
  241. }
  242. ],
  243. async (err, reports) => {
  244. if (err) {
  245. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  246. this.log("ERROR", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" failed. "${err}"`);
  247. return cb({ status: "error", message: err });
  248. }
  249. this.log("SUCCESS", "GET_REPORTS_FOR_SONG", `Indexing reports for song "${songId}" successful.`);
  250. return cb({ status: "success", data: { reports } });
  251. }
  252. );
  253. }),
  254. /**
  255. * Gets all a users reports for a specific songId
  256. *
  257. * @param {object} session - the session object automatically added by the websocket
  258. * @param {string} songId - the id of the song
  259. * @param {Function} cb - gets called with the result
  260. */
  261. myReportsForSong: isLoginRequired(async function myReportsForSong(session, songId, cb) {
  262. const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
  263. const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
  264. async.waterfall(
  265. [
  266. next =>
  267. reportModel
  268. .find({ "song._id": songId, createdBy: session.userId, resolved: false })
  269. .sort({ createdAt: "desc" })
  270. .exec(next),
  271. (_reports, next) => {
  272. const reports = [];
  273. async.each(
  274. _reports,
  275. (report, cb) => {
  276. userModel
  277. .findById(report.createdBy)
  278. .select({ avatar: -1, name: -1, username: -1 })
  279. .exec((err, user) => {
  280. if (!user)
  281. reports.push({
  282. ...report._doc,
  283. createdBy: { _id: report.createdBy }
  284. });
  285. else
  286. reports.push({
  287. ...report._doc,
  288. createdBy: {
  289. avatar: user.avatar,
  290. name: user.name,
  291. username: user.username,
  292. _id: report.createdBy
  293. }
  294. });
  295. return cb(err);
  296. });
  297. },
  298. err => next(err, reports)
  299. );
  300. }
  301. ],
  302. async (err, reports) => {
  303. if (err) {
  304. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  305. this.log(
  306. "ERROR",
  307. "MY_REPORTS_FOR_SONG",
  308. `Indexing reports of user ${session.userId} for song "${songId}" failed. "${err}"`
  309. );
  310. return cb({ status: "error", message: err });
  311. }
  312. this.log(
  313. "SUCCESS",
  314. "MY_REPORTS_FOR_SONG",
  315. `Indexing reports of user ${session.userId} for song "${songId}" successful.`
  316. );
  317. return cb({ status: "success", data: { reports } });
  318. }
  319. );
  320. }),
  321. /**
  322. * Resolves a report as a whole
  323. *
  324. * @param {object} session - the session object automatically added by the websocket
  325. * @param {string} reportId - the id of the report that is getting resolved
  326. * @param {Function} cb - gets called with the result
  327. */
  328. resolve: isAdminRequired(async function resolve(session, reportId, cb) {
  329. const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
  330. async.waterfall(
  331. [
  332. next => {
  333. reportModel.findById(reportId).exec(next);
  334. },
  335. (report, next) => {
  336. if (!report) return next("Report not found.");
  337. report.resolved = true;
  338. return report.save(err => {
  339. if (err) return next(err.message);
  340. return next(null, report.song._id);
  341. });
  342. }
  343. ],
  344. async (err, songId) => {
  345. if (err) {
  346. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  347. this.log(
  348. "ERROR",
  349. "REPORTS_RESOLVE",
  350. `Resolving report "${reportId}" failed by user "${session.userId}". "${err}"`
  351. );
  352. return cb({ status: "error", message: err });
  353. }
  354. CacheModule.runJob("PUB", {
  355. channel: "report.resolve",
  356. value: { reportId, songId }
  357. });
  358. this.log("SUCCESS", "REPORTS_RESOLVE", `User "${session.userId}" resolved report "${reportId}".`);
  359. return cb({
  360. status: "success",
  361. message: "Successfully resolved Report"
  362. });
  363. }
  364. );
  365. }),
  366. /**
  367. * Resolves/Unresolves an issue within a report
  368. *
  369. * @param {object} session - the session object automatically added by the websocket
  370. * @param {string} reportId - the id of the report that is getting resolved
  371. * @param {string} issueId - the id of the issue within the report
  372. * @param {Function} cb - gets called with the result
  373. */
  374. toggleIssue: isAdminRequired(async function toggleIssue(session, reportId, issueId, cb) {
  375. const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
  376. async.waterfall(
  377. [
  378. next => {
  379. reportModel.findById(reportId).exec(next);
  380. },
  381. (report, next) => {
  382. if (!report) return next("Report not found.");
  383. const issue = report.issues.find(issue => issue._id.toString() === issueId);
  384. issue.resolved = !issue.resolved;
  385. return report.save(err => {
  386. if (err) return next(err.message);
  387. return next(null, issue.resolved, report.song._id);
  388. });
  389. }
  390. ],
  391. async (err, resolved, songId) => {
  392. if (err) {
  393. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  394. this.log(
  395. "ERROR",
  396. "REPORTS_TOGGLE_ISSUE",
  397. `Resolving an issue within report "${reportId}" failed by user "${session.userId}". "${err}"`
  398. );
  399. return cb({ status: "error", message: err });
  400. }
  401. CacheModule.runJob("PUB", {
  402. channel: "report.issue.toggle",
  403. value: { reportId, issueId, songId, resolved }
  404. });
  405. this.log(
  406. "SUCCESS",
  407. "REPORTS_TOGGLE_ISSUE",
  408. `User "${session.userId}" resolved an issue in report "${reportId}".`
  409. );
  410. return cb({
  411. status: "success",
  412. message: "Successfully resolved issue within report"
  413. });
  414. }
  415. );
  416. }),
  417. /**
  418. * Creates a new report
  419. *
  420. * @param {object} session - the session object automatically added by the websocket
  421. * @param {object} report - the object of the report data
  422. * @param {string} report.youtubeId - the youtube id of the song that is being reported
  423. * @param {Array} report.issues - all issues reported (custom or defined)
  424. * @param {Function} cb - gets called with the result
  425. */
  426. create: isLoginRequired(async function create(session, report, cb) {
  427. const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
  428. const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
  429. const { youtubeId } = report;
  430. async.waterfall(
  431. [
  432. next => songModel.findOne({ youtubeId }).exec(next),
  433. (song, next) => {
  434. if (!song) return next("Song not found.");
  435. return SongsModule.runJob("GET_SONG", { songId: song._id }, this)
  436. .then(res => next(null, res.song))
  437. .catch(next);
  438. },
  439. (song, next) => {
  440. if (!song) return next("Song not found.");
  441. delete report.youtubeId;
  442. report.song = {
  443. _id: song._id,
  444. youtubeId: song.youtubeId
  445. };
  446. return next(null, { title: song.title, artists: song.artists, thumbnail: song.thumbnail });
  447. },
  448. (song, next) =>
  449. reportModel.create(
  450. {
  451. createdBy: session.userId,
  452. createdAt: Date.now(),
  453. ...report
  454. },
  455. (err, report) => next(err, report, song)
  456. )
  457. ],
  458. async (err, report, song) => {
  459. if (err) {
  460. err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
  461. this.log(
  462. "ERROR",
  463. "REPORTS_CREATE",
  464. `Creating report for "${report.song._id}" failed by user "${session.userId}". "${err}"`
  465. );
  466. return cb({ status: "error", message: err });
  467. }
  468. ActivitiesModule.runJob("ADD_ACTIVITY", {
  469. userId: session.userId,
  470. type: "song__report",
  471. payload: {
  472. message: `Created a <reportId>${report._id}</reportId> for song <youtubeId>${song.title}</youtubeId>`,
  473. youtubeId: report.song.youtubeId,
  474. reportId: report._id,
  475. thumbnail: song.thumbnail
  476. }
  477. });
  478. CacheModule.runJob("PUB", {
  479. channel: "report.create",
  480. value: report
  481. });
  482. this.log("SUCCESS", "REPORTS_CREATE", `User "${session.userId}" created report for "${youtubeId}".`);
  483. return cb({
  484. status: "success",
  485. message: "Successfully created report"
  486. });
  487. }
  488. );
  489. })
  490. };