diff --git a/public/openapi/components/schemas/NotificationObject.yaml b/public/openapi/components/schemas/NotificationObject.yaml new file mode 100644 index 0000000000..bf63bb3de9 --- /dev/null +++ b/public/openapi/components/schemas/NotificationObject.yaml @@ -0,0 +1,60 @@ +NotificationObject: + allOf: + - type: object + properties: + importance: + type: number + datetime: + type: number + path: + type: string + description: Relative path to the notification target + bodyShort: + type: string + nid: + type: string + datetimeISO: + type: string + read: + type: boolean + readClass: + type: string + - type: object + description: Optional properties that may or may not be present (except for `nid`, which is always present, and is only here as a hack to pass validation) + properties: + nid: + type: string + type: + type: string + description: Used to denote a classification of notification. These types are toggleable in the user ACP (so the user can opt to not receive notifications for certain types, etc.) + bodyLong: + type: string + from: + type: number + pid: + type: number + description: A post id, if the notification pertains to a post + tid: + type: number + description: A post id, if the notification pertains to a topic + user: + $ref: ./UserObject.yaml#/UserObjectTiny + subject: + type: string + description: A language key that would be used as an email subject, otherwise a generic "New Notification" message. + icon: + type: string + roomName: + type: string + description: The chat room name, if the notification is related to a chat message + roomIcon: + type: string + mergeId: + type: string + description: A common string used to denote related notifications that can be merged together (e.g. two new chat messages in same room) + image: + type: string + description: A URL to a media image for the notification (supercedes the user avatar if `user` is present) + nullable: true + required: + - nid \ No newline at end of file diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml index bdd52bc2d1..204b95a0df 100644 --- a/public/openapi/components/schemas/UserObject.yaml +++ b/public/openapi/components/schemas/UserObject.yaml @@ -609,6 +609,33 @@ UserObjectSlim: type: string description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned" example: Not Banned +UserObjectTiny: + type: object + properties: + username: + type: string + description: A friendly name for a given user account + userslug: + type: string + description: An URL-safe variant of the username (i.e. lower-cased, spaces + removed, etc.) + picture: + type: string + uid: + type: number + description: A user identifier + icon:text: + type: string + description: A single-letter representation of a username. This is used in the + auto-generated icon given to users without + an avatar + icon:bgColor: + type: string + description: A six-character hexadecimal colour code assigned to the user. This + value is used in conjunction with + `icon:text` for the user's auto-generated + icon + example: "#f44336" UserObjectACP: type: object required: @@ -715,32 +742,7 @@ BanMuteArray: fromUid: type: number user: - type: object - properties: - username: - type: string - description: A friendly name for a given user account - userslug: - type: string - description: An URL-safe variant of the username (i.e. lower-cased, spaces - removed, etc.) - picture: - type: string - uid: - type: number - description: A user identifier - icon:text: - type: string - description: A single-letter representation of a username. This is used in the - auto-generated icon given to users without - an avatar - icon:bgColor: - type: string - description: A six-character hexadecimal colour code assigned to the user. This - value is used in conjunction with - `icon:text` for the user's auto-generated - icon - example: "#f44336" + $ref: '#/UserObjectTiny' until: type: number untilReadable: diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 79657b519e..1d9793eb09 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -164,6 +164,12 @@ paths: $ref: 'write/topics/tid/read.yaml' /topics/{tid}/bump: $ref: 'write/topics/tid/bump.yaml' + /notifications: + $ref: 'write/notifications.yaml' + /notifications/{nid}: + $ref: 'write/notifications/nid.yaml' + /notifications/count: + $ref: 'write/notifications/count.yaml' /tags/{tag}/follow: $ref: 'write/tags/tag/follow.yaml' /posts/{pid}: diff --git a/public/openapi/write/notifications.yaml b/public/openapi/write/notifications.yaml new file mode 100644 index 0000000000..d464d867ad --- /dev/null +++ b/public/openapi/write/notifications.yaml @@ -0,0 +1,26 @@ +get: + tags: + - notifications + summary: list notifications + description: This operation returns two lists of notifications — read and unread. + responses: + '200': + description: notifications successfully listed + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../components/schemas/Status.yaml#/Status + response: + type: object + properties: + read: + type: array + items: + $ref: ../components/schemas/NotificationObject.yaml#/NotificationObject + unread: + type: array + items: + $ref: ../components/schemas/NotificationObject.yaml#/NotificationObject \ No newline at end of file diff --git a/public/openapi/write/notifications/count.yaml b/public/openapi/write/notifications/count.yaml new file mode 100644 index 0000000000..c6063ff69f --- /dev/null +++ b/public/openapi/write/notifications/count.yaml @@ -0,0 +1,20 @@ +get: + tags: + - notifications + summary: get unread notification count + description: This operation returns the calling user's unread notifications count + responses: + '200': + description: unread notifications count successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + unread: + type: number \ No newline at end of file diff --git a/public/openapi/write/notifications/nid.yaml b/public/openapi/write/notifications/nid.yaml new file mode 100644 index 0000000000..2c46c4165d --- /dev/null +++ b/public/openapi/write/notifications/nid.yaml @@ -0,0 +1,28 @@ +get: + tags: + - notifications + summary: get notification + description: This operation returns the content of a single notification + parameters: + - in: path + name: nid + schema: + type: number + required: true + description: The notification id to retrieve + example: uploads:export:1 + responses: + '200': + description: notification successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + notification: + $ref: ../../components/schemas/NotificationObject.yaml#/NotificationObject \ No newline at end of file diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index a983519183..10885d5b3a 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -8,7 +8,8 @@ define('notifications', [ 'tinycon', 'hooks', 'alerts', -], function (translator, components, navigator, Tinycon, hooks, alerts) { + 'api', +], function (translator, components, navigator, Tinycon, hooks, alerts, api) { const Notifications = {}; let unreadNotifs = {}; @@ -30,11 +31,7 @@ define('notifications', [ Notifications.loadNotifications = function (notifList, callback) { callback = callback || function () {}; - socket.emit('notifications.get', null, function (err, data) { - if (err) { - return alerts.error(err); - } - + api.get('/notifications').then((data) => { const notifs = data.unread.concat(data.read).sort(function (a, b) { return parseInt(a.datetime, 10) > parseInt(b.datetime, 10) ? -1 : 1; }); @@ -68,7 +65,7 @@ define('notifications', [ callback(); }); }); - }); + }).catch(alerts.error); }; Notifications.handleUnreadButton = function (notifList) { @@ -94,13 +91,9 @@ define('notifications', [ return; } - socket.emit('notifications.getCount', function (err, count) { - if (err) { - return alerts.error(err); - } - - Notifications.updateNotifCount(count); - }); + api.get('/notifications/count').then(({ unread }) => { + Notifications.updateNotifCount(unread); + }).catch(alerts.error); if (!unreadNotifs[notifData.nid]) { unreadNotifs[notifData.nid] = true; diff --git a/src/api/index.js b/src/api/index.js index c454de93a5..6f88791bd4 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -5,6 +5,7 @@ module.exports = { users: require('./users'), groups: require('./groups'), topics: require('./topics'), + notifications: require('./notifications'), tags: require('./tags'), posts: require('./posts'), chats: require('./chats'), diff --git a/src/api/notifications.js b/src/api/notifications.js new file mode 100644 index 0000000000..8032cadc75 --- /dev/null +++ b/src/api/notifications.js @@ -0,0 +1,22 @@ +'use strict'; + +const user = require('../user'); + +const notificationsApi = module.exports; + +notificationsApi.list = async (caller) => { + const { read, unread } = await user.notifications.get(caller.uid); + return { read, unread }; +}; + +notificationsApi.get = async (caller, { nid }) => { + let notification = await user.notifications.getNotifications([nid], caller.uid); + notification = notification.pop(); + + return { notification }; +}; + +notificationsApi.getCount = async (caller) => { + const unread = await user.notifications.getUnreadCount(caller.uid); + return { unread }; +}; diff --git a/src/controllers/write/index.js b/src/controllers/write/index.js index 26c74128d8..52ca4a7777 100644 --- a/src/controllers/write/index.js +++ b/src/controllers/write/index.js @@ -6,6 +6,7 @@ Write.users = require('./users'); Write.groups = require('./groups'); Write.categories = require('./categories'); Write.topics = require('./topics'); +Write.notifications = require('./notifications'); Write.tags = require('./tags'); Write.posts = require('./posts'); Write.chats = require('./chats'); diff --git a/src/controllers/write/notifications.js b/src/controllers/write/notifications.js new file mode 100644 index 0000000000..bf98bae5b6 --- /dev/null +++ b/src/controllers/write/notifications.js @@ -0,0 +1,22 @@ +'use strict'; + +const api = require('../../api'); + +const helpers = require('../helpers'); + +const Notifications = module.exports; + +Notifications.get = async (req, res) => { + let response; + if (req.params.nid) { + response = await api.notifications.get(req, { ...req.params }); + } else { + response = await api.notifications.list(req); + } + + helpers.formatApiResponse(200, res, response); +}; + +Notifications.getCount = async (req, res) => { + helpers.formatApiResponse(200, res, await api.notifications.getCount(req)); +}; diff --git a/src/routes/write/index.js b/src/routes/write/index.js index 2ebec74ce1..b17a2b9768 100644 --- a/src/routes/write/index.js +++ b/src/routes/write/index.js @@ -37,6 +37,7 @@ Write.reload = async (params) => { router.use('/api/v3/groups', require('./groups')()); router.use('/api/v3/categories', require('./categories')()); router.use('/api/v3/topics', require('./topics')()); + router.use('/api/v3/notifications', require('./notifications')()); router.use('/api/v3/tags', require('./tags')()); router.use('/api/v3/posts', require('./posts')()); router.use('/api/v3/chats', require('./chats')()); diff --git a/src/routes/write/notifications.js b/src/routes/write/notifications.js new file mode 100644 index 0000000000..757cf13222 --- /dev/null +++ b/src/routes/write/notifications.js @@ -0,0 +1,18 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn]; + + setupApiRoute(router, 'get', '/count', [...middlewares], controllers.write.notifications.getCount); + + setupApiRoute(router, 'get', '/:nid?', [...middlewares], controllers.write.notifications.get); + + return router; +}; diff --git a/src/socket.io/notifications.js b/src/socket.io/notifications.js index 2b0df88114..9defae7ca7 100644 --- a/src/socket.io/notifications.js +++ b/src/socket.io/notifications.js @@ -2,18 +2,35 @@ const user = require('../user'); const notifications = require('../notifications'); +const api = require('../api'); + +const sockets = require('.'); const SocketNotifs = module.exports; SocketNotifs.get = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/notifications/(:nid)'); + + // Passing in multiple nids is no longer supported in apiv3 if (data && Array.isArray(data.nids) && socket.uid) { - return await user.notifications.getNotifications(data.nids, socket.uid); + const notifications = await Promise.all(data.nids.map(async (nid) => { + const { notification } = await api.notifications.get(socket, { nid }); + return notification; + })); + + return notifications; } - return await user.notifications.get(socket.uid); + + const response = await api.notifications.list(socket); + response.uid = socket.uid; + return response; }; SocketNotifs.getCount = async function (socket) { - return await user.notifications.getUnreadCount(socket.uid); + sockets.warnDeprecated(socket, 'GET /api/v3/notifications/count'); + + const { unread } = await api.notifications.getCount(socket); + return unread; }; SocketNotifs.deleteAll = async function (socket) {