@headless-lms/server 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/app/community-mail.d.ts +6 -8
  2. package/dist/app/community-mail.js +13 -17
  3. package/dist/app/community-mail.js.map +1 -1
  4. package/dist/app/container.d.ts +27 -10
  5. package/dist/app/container.js +178 -22
  6. package/dist/app/container.js.map +1 -1
  7. package/dist/app/notification-channels.d.ts +47 -0
  8. package/dist/app/notification-channels.js +174 -0
  9. package/dist/app/notification-channels.js.map +1 -0
  10. package/dist/app/notification-reads.d.ts +23 -0
  11. package/dist/app/notification-reads.js +95 -0
  12. package/dist/app/notification-reads.js.map +1 -0
  13. package/dist/http/errors.d.ts +1 -0
  14. package/dist/http/errors.js +16 -1
  15. package/dist/http/errors.js.map +1 -1
  16. package/dist/http/mcp/tools/communities.js +1 -1
  17. package/dist/http/mcp/tools/communities.js.map +1 -1
  18. package/dist/http/plugins/error-handler.js +3 -1
  19. package/dist/http/plugins/error-handler.js.map +1 -1
  20. package/dist/http/routes/automations.js +3 -3
  21. package/dist/http/routes/automations.js.map +1 -1
  22. package/dist/http/routes/communities.js +67 -7
  23. package/dist/http/routes/communities.js.map +1 -1
  24. package/dist/http/routes/member/actor.d.ts +1 -1
  25. package/dist/http/routes/member/chats.js +9 -30
  26. package/dist/http/routes/member/chats.js.map +1 -1
  27. package/dist/http/routes/member/community.js +25 -1
  28. package/dist/http/routes/member/community.js.map +1 -1
  29. package/dist/http/routes/member/devices.d.ts +7 -0
  30. package/dist/http/routes/member/devices.js +98 -0
  31. package/dist/http/routes/member/devices.js.map +1 -0
  32. package/dist/http/routes/member/index.js +4 -0
  33. package/dist/http/routes/member/index.js.map +1 -1
  34. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  35. package/dist/http/routes/member/notification-settings.js +108 -0
  36. package/dist/http/routes/member/notification-settings.js.map +1 -0
  37. package/dist/http/routes/member/notifications.js +66 -42
  38. package/dist/http/routes/member/notifications.js.map +1 -1
  39. package/dist/http/routes/member/posts.js +33 -33
  40. package/dist/http/routes/member/posts.js.map +1 -1
  41. package/dist/http/routes/organizations.js +122 -1
  42. package/dist/http/routes/organizations.js.map +1 -1
  43. package/dist/http/schemas/assets.d.ts +6 -6
  44. package/dist/http/schemas/automations.d.ts +0 -4
  45. package/dist/http/schemas/chats.d.ts +34 -11
  46. package/dist/http/schemas/chats.js +4 -6
  47. package/dist/http/schemas/chats.js.map +1 -1
  48. package/dist/http/schemas/community.d.ts +77 -17
  49. package/dist/http/schemas/community.js +6 -3
  50. package/dist/http/schemas/community.js.map +1 -1
  51. package/dist/http/schemas/discussion.d.ts +23 -1
  52. package/dist/http/schemas/discussion.js +14 -2
  53. package/dist/http/schemas/discussion.js.map +1 -1
  54. package/dist/http/schemas/index.d.ts +6 -6
  55. package/dist/http/schemas/index.js +6 -6
  56. package/dist/http/schemas/integrations.d.ts +4 -4
  57. package/dist/http/schemas/members.d.ts +5 -5
  58. package/dist/http/schemas/notifications.d.ts +498 -175
  59. package/dist/http/schemas/notifications.js +148 -28
  60. package/dist/http/schemas/notifications.js.map +1 -1
  61. package/dist/http/schemas/posts.d.ts +37 -3
  62. package/dist/http/schemas/profile.d.ts +2 -2
  63. package/dist/http/schemas/shared.d.ts +10 -3
  64. package/dist/http/schemas/shared.js +3 -2
  65. package/dist/http/schemas/shared.js.map +1 -1
  66. package/dist/http/schemas/students.d.ts +3 -3
  67. package/dist/http/scope.d.ts +2 -1
  68. package/dist/http/scope.js +7 -1
  69. package/dist/http/scope.js.map +1 -1
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/packages/server/package.js +1 -1
  73. package/package.json +5 -5
@@ -1,11 +1,12 @@
1
- import { PageQuery, refusals } from "../../schemas/shared.js";
2
- import { MarkedRead, NotificationIdParam, NotificationPreferences, NotificationView, NotificationsPage, SetNotificationPreferences } from "../../schemas/notifications.js";
1
+ import { refusals } from "../../schemas/shared.js";
2
+ import { NotificationFilterQuery, NotificationIdParam, NotificationListQuery, NotificationUnreadCounts, NotificationView, NotificationsPage } from "../../schemas/notifications.js";
3
3
  import "../../schemas/index.js";
4
4
  import { actorOf } from "./actor.js";
5
+ import { z } from "zod";
6
+ import { InvalidInputError, NotFoundError } from "@headless-lms/core/shared/errors";
5
7
  //#region src/http/routes/member/notifications.ts
6
8
  async function memberNotificationRoutes(app, container) {
7
9
  const r = app.withTypeProvider();
8
- const notifications = container.notifications;
9
10
  const tags = ["Notifications"];
10
11
  r.route({
11
12
  method: "GET",
@@ -14,84 +15,107 @@ async function memberNotificationRoutes(app, container) {
14
15
  schema: {
15
16
  operationId: "listMemberNotifications",
16
17
  tags,
17
- summary: "List your notifications, newest first, with your unread count",
18
- description: "For any member of the organization. Each notification names what it refers to; a post or chat you can no longer see is null.",
19
- querystring: PageQuery,
18
+ summary: "List your notifications, newest first, with the unread count of the same filters",
19
+ description: "For any member of the organization. Filters combine with AND; the values of one filter combine with OR, and `includeOrg=true` with `communityId` adds the org list, so one query reads a community with the course and account notifications. 400 for `includeOrg` without `communityId` or with `scope`. Each notification carries the current titles, names and excerpts of what it refers to; a subject or parent you can no longer open, or that was deleted, is null. `display` is the text stored when it was sent.",
20
+ querystring: NotificationListQuery,
20
21
  response: {
21
22
  200: NotificationsPage,
22
- ...refusals(403)
23
+ ...refusals(400, 403)
23
24
  }
24
25
  },
25
26
  handler: async (req) => {
26
27
  const actor = await actorOf(container, req.orgId, req.userId);
27
- return container.reporting.member.notifications(req.orgId, actor, req.userId, req.query);
28
+ const { page, pageSize, ...filter } = req.query;
29
+ return container.reporting.member.notifications(req.orgId, actor, req.userId, filter, {
30
+ page,
31
+ pageSize
32
+ });
28
33
  }
29
34
  });
30
35
  r.route({
31
- method: "POST",
32
- url: "/api/member/notifications/:notificationId/read",
36
+ method: "GET",
37
+ url: "/api/member/notifications/unread-counts",
33
38
  preHandler: app.requireOrgSession,
34
39
  schema: {
35
- operationId: "markMemberNotificationRead",
40
+ operationId: "getMemberNotificationUnreadCounts",
36
41
  tags,
37
- summary: "Mark one of your notifications read",
38
- description: "For any member of the organization. Returns the notification the way the list shows it. Marking a read notification keeps its first read time. 404 when it is not yours.",
39
- params: NotificationIdParam,
42
+ summary: "Count your unread notifications in the org list and in each community",
43
+ description: "For any member of the organization. `org` counts the org list; `communities` has one entry per community you can open, staff included.",
40
44
  response: {
41
- 200: NotificationView,
42
- ...refusals(403, 404)
45
+ 200: NotificationUnreadCounts,
46
+ ...refusals(403)
43
47
  }
44
48
  },
45
49
  handler: async (req) => {
46
50
  const actor = await actorOf(container, req.orgId, req.userId);
47
- const notification = await notifications.markNotificationRead(req.orgId, req.userId, req.params.notificationId);
48
- return container.reporting.member.notificationView(req.orgId, actor, notification);
51
+ return container.reporting.member.notificationUnreadCounts(req.orgId, actor, req.userId);
49
52
  }
50
53
  });
51
54
  r.route({
52
- method: "POST",
53
- url: "/api/member/notifications/read",
55
+ method: "GET",
56
+ url: "/api/member/notifications/:notificationId",
54
57
  preHandler: app.requireOrgSession,
55
58
  schema: {
56
- operationId: "markAllMemberNotificationsRead",
59
+ operationId: "getMemberNotification",
57
60
  tags,
58
- summary: "Mark all your notifications read",
59
- description: "For any signed-in person in the organization. Returns how many were unread.",
60
- response: { 200: MarkedRead }
61
+ summary: "Read one of your notifications",
62
+ description: "For any member of the organization. The notification as the list shows it, with current titles, names and excerpts; reading it does not mark it read. 404 when it is not yours or was not shown in the app.",
63
+ params: NotificationIdParam,
64
+ response: {
65
+ 200: NotificationView,
66
+ ...refusals(403, 404)
67
+ }
61
68
  },
62
69
  handler: async (req) => {
63
- return notifications.markAllNotificationsRead(req.orgId, req.userId);
70
+ const actor = await actorOf(container, req.orgId, req.userId);
71
+ const view = await container.reporting.member.notification(req.orgId, actor, req.userId, req.params.notificationId);
72
+ if (!view) throw new NotFoundError("Notification", req.params.notificationId);
73
+ return view;
64
74
  }
65
75
  });
66
76
  r.route({
67
- method: "GET",
68
- url: "/api/member/notification-preferences",
77
+ method: "POST",
78
+ url: "/api/member/notifications/:notificationId/read",
69
79
  preHandler: app.requireOrgSession,
70
80
  schema: {
71
- operationId: "getMemberNotificationPreferences",
81
+ operationId: "markMemberNotificationRead",
72
82
  tags,
73
- summary: "Read how you are told about each kind of notification",
74
- description: "For any signed-in person in the organization. One entry per kind; a kind you never set is on in the app and by email.",
75
- response: { 200: NotificationPreferences }
83
+ summary: "Mark one of your notifications read",
84
+ description: "For any member of the organization. Marking a read notification keeps its first read time. 404 when it is not yours.",
85
+ params: NotificationIdParam,
86
+ response: {
87
+ 204: z.void(),
88
+ ...refusals(403, 404)
89
+ }
76
90
  },
77
- handler: async (req) => {
78
- return notifications.notificationPreferences(req.orgId, req.userId);
91
+ handler: async (req, reply) => {
92
+ await actorOf(container, req.orgId, req.userId);
93
+ await container.notificationList.markRead(req.orgId, req.userId, req.params.notificationId);
94
+ return reply.code(204).send();
79
95
  }
80
96
  });
81
97
  r.route({
82
- method: "PUT",
83
- url: "/api/member/notification-preferences",
98
+ method: "POST",
99
+ url: "/api/member/notifications/read",
84
100
  preHandler: app.requireOrgSession,
85
101
  schema: {
86
- operationId: "setMemberNotificationPreferences",
102
+ operationId: "markAllMemberNotificationsRead",
87
103
  tags,
88
- summary: "Set how you are told about the given kinds of notification",
89
- description: "For any signed-in person in the organization. Changes only the kinds sent and returns every kind.",
90
- body: SetNotificationPreferences,
91
- response: { 200: NotificationPreferences }
104
+ summary: "Mark your notifications matching the filters read",
105
+ description: "For any member of the organization. Takes the filters of the list in the query string, `includeOrg` included; with none, marks every notification read. 400 when the request has a body.",
106
+ querystring: NotificationFilterQuery,
107
+ response: {
108
+ 204: z.void(),
109
+ ...refusals(400, 403)
110
+ }
92
111
  },
93
- handler: async (req) => {
94
- return notifications.setNotificationPreferences(req.orgId, req.userId, req.body.preferences);
112
+ preValidation: async (req) => {
113
+ if (req.body !== void 0) throw new InvalidInputError("Mark-read takes its filters in the query string, not in a body");
114
+ },
115
+ handler: async (req, reply) => {
116
+ await actorOf(container, req.orgId, req.userId);
117
+ await container.notificationList.markAllRead(req.orgId, req.userId, req.query);
118
+ return reply.code(204).send();
95
119
  }
96
120
  });
97
121
  }
@@ -1 +1 @@
1
- {"version":3,"file":"notifications.js","names":[],"sources":["../../../../src/http/routes/member/notifications.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport type { Container } from '../../../app/container.js';\nimport { actorOf } from './actor.js';\nimport {\n MarkedRead,\n NotificationIdParam,\n NotificationPreferences,\n NotificationView,\n NotificationsPage,\n PageQuery,\n SetNotificationPreferences,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberNotificationRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const notifications = container.notifications;\n const tags = ['Notifications'];\n\n r.route({\n method: 'GET',\n url: '/api/member/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMemberNotifications',\n tags,\n summary: 'List your notifications, newest first, with your unread count',\n description:\n 'For any member of the organization. Each notification names what it refers to; a post or chat you can no longer see is null.',\n querystring: PageQuery,\n response: { 200: NotificationsPage, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return container.reporting.member.notifications(req.orgId, actor, req.userId, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/notifications/:notificationId/read',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'markMemberNotificationRead',\n tags,\n summary: 'Mark one of your notifications read',\n description:\n 'For any member of the organization. Returns the notification the way the list shows it. Marking a read notification keeps its first read time. 404 when it is not yours.',\n params: NotificationIdParam,\n response: { 200: NotificationView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const notification = await notifications.markNotificationRead(\n req.orgId,\n req.userId,\n req.params.notificationId,\n );\n return container.reporting.member.notificationView(req.orgId, actor, notification);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/notifications/read',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'markAllMemberNotificationsRead',\n tags,\n summary: 'Mark all your notifications read',\n description: 'For any signed-in person in the organization. Returns how many were unread.',\n response: { 200: MarkedRead },\n },\n handler: async (req) => {\n return notifications.markAllNotificationsRead(req.orgId, req.userId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/notification-preferences',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotificationPreferences',\n tags,\n summary: 'Read how you are told about each kind of notification',\n description:\n 'For any signed-in person in the organization. One entry per kind; a kind you never set is on in the app and by email.',\n response: { 200: NotificationPreferences },\n },\n handler: async (req) => {\n return notifications.notificationPreferences(req.orgId, req.userId);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/notification-preferences',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setMemberNotificationPreferences',\n tags,\n summary: 'Set how you are told about the given kinds of notification',\n description:\n 'For any signed-in person in the organization. Changes only the kinds sent and returns every kind.',\n body: SetNotificationPreferences,\n response: { 200: NotificationPreferences },\n },\n handler: async (req) => {\n return notifications.setNotificationPreferences(req.orgId, req.userId, req.body.preferences);\n },\n });\n}\n"],"mappings":";;;;;AAeA,eAAsB,yBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,gBAAgB,UAAU;CAChC,MAAM,OAAO,CAAC,eAAe;CAE7B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,aAAa;GACb,UAAU;IAAE,KAAK;IAAmB,GAAG,SAAS,GAAG;GAAE;EACvD;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,OAAO,UAAU,UAAU,OAAO,cAAc,IAAI,OAAO,OAAO,IAAI,QAAQ,IAAI,KAAK;EACzF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,MAAM,eAAe,MAAM,cAAc,qBACvC,IAAI,OACJ,IAAI,QACJ,IAAI,OAAO,cACb;GACA,OAAO,UAAU,UAAU,OAAO,iBAAiB,IAAI,OAAO,OAAO,YAAY;EACnF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aAAa;GACb,UAAU,EAAE,KAAK,WAAW;EAC9B;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,cAAc,yBAAyB,IAAI,OAAO,IAAI,MAAM;EACrE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,UAAU,EAAE,KAAK,wBAAwB;EAC3C;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,cAAc,wBAAwB,IAAI,OAAO,IAAI,MAAM;EACpE;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,MAAM;GACN,UAAU,EAAE,KAAK,wBAAwB;EAC3C;EACA,SAAS,OAAO,QAAQ;GACtB,OAAO,cAAc,2BAA2B,IAAI,OAAO,IAAI,QAAQ,IAAI,KAAK,WAAW;EAC7F;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"notifications.js","names":[],"sources":["../../../../src/http/routes/member/notifications.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../../app/container.js';\nimport { InvalidInputError, NotFoundError } from '@headless-lms/core/shared/errors';\nimport { actorOf } from './actor.js';\nimport {\n NotificationFilterQuery,\n NotificationIdParam,\n NotificationListQuery,\n NotificationUnreadCounts,\n NotificationView,\n NotificationsPage,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberNotificationRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const tags = ['Notifications'];\n\n r.route({\n method: 'GET',\n url: '/api/member/notifications',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listMemberNotifications',\n tags,\n summary: 'List your notifications, newest first, with the unread count of the same filters',\n description:\n 'For any member of the organization. Filters combine with AND; the values of one filter combine with OR, and `includeOrg=true` with `communityId` adds the org list, so one query reads a community with the course and account notifications. 400 for `includeOrg` without `communityId` or with `scope`. Each notification carries the current titles, names and excerpts of what it refers to; a subject or parent you can no longer open, or that was deleted, is null. `display` is the text stored when it was sent.',\n querystring: NotificationListQuery,\n response: { 200: NotificationsPage, ...refusals(400, 403) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const { page, pageSize, ...filter } = req.query;\n return container.reporting.member.notifications(req.orgId, actor, req.userId, filter, {\n page,\n pageSize,\n });\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/notifications/unread-counts',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotificationUnreadCounts',\n tags,\n summary: 'Count your unread notifications in the org list and in each community',\n description:\n 'For any member of the organization. `org` counts the org list; `communities` has one entry per community you can open, staff included.',\n response: { 200: NotificationUnreadCounts, ...refusals(403) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return container.reporting.member.notificationUnreadCounts(req.orgId, actor, req.userId);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/notifications/:notificationId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberNotification',\n tags,\n summary: 'Read one of your notifications',\n description:\n 'For any member of the organization. The notification as the list shows it, with current titles, names and excerpts; reading it does not mark it read. 404 when it is not yours or was not shown in the app.',\n params: NotificationIdParam,\n response: { 200: NotificationView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const view = await container.reporting.member.notification(\n req.orgId,\n actor,\n req.userId,\n req.params.notificationId,\n );\n if (!view) {\n throw new NotFoundError('Notification', req.params.notificationId);\n }\n return view;\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/notifications/:notificationId/read',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'markMemberNotificationRead',\n tags,\n summary: 'Mark one of your notifications read',\n description:\n 'For any member of the organization. Marking a read notification keeps its first read time. 404 when it is not yours.',\n params: NotificationIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n await actorOf(container, req.orgId, req.userId);\n await container.notificationList.markRead(req.orgId, req.userId, req.params.notificationId);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/notifications/read',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'markAllMemberNotificationsRead',\n tags,\n summary: 'Mark your notifications matching the filters read',\n description:\n 'For any member of the organization. Takes the filters of the list in the query string, `includeOrg` included; with none, marks every notification read. 400 when the request has a body.',\n querystring: NotificationFilterQuery,\n response: { 204: z.void(), ...refusals(400, 403) },\n },\n preValidation: async (req) => {\n if (req.body !== undefined) {\n throw new InvalidInputError('Mark-read takes its filters in the query string, not in a body');\n }\n },\n handler: async (req, reply) => {\n await actorOf(container, req.orgId, req.userId);\n await container.notificationList.markAllRead(req.orgId, req.userId, req.query);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":";;;;;;;AAgBA,eAAsB,yBACpB,KACA,WACe;CACf,MAAM,IAAI,IAAI,iBAAkC;CAChD,MAAM,OAAO,CAAC,eAAe;CAE7B,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,aAAa;GACb,UAAU;IAAE,KAAK;IAAmB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC5D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,MAAM,EAAE,MAAM,UAAU,GAAG,WAAW,IAAI;GAC1C,OAAO,UAAU,UAAU,OAAO,cAAc,IAAI,OAAO,OAAO,IAAI,QAAQ,QAAQ;IACpF;IACA;GACF,CAAC;EACH;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,UAAU;IAAE,KAAK;IAA0B,GAAG,SAAS,GAAG;GAAE;EAC9D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,OAAO,UAAU,UAAU,OAAO,yBAAyB,IAAI,OAAO,OAAO,IAAI,MAAM;EACzF;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK;IAAkB,GAAG,SAAS,KAAK,GAAG;GAAE;EAC3D;EACA,SAAS,OAAO,QAAQ;GACtB,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC5D,MAAM,OAAO,MAAM,UAAU,UAAU,OAAO,aAC5C,IAAI,OACJ,OACA,IAAI,QACJ,IAAI,OAAO,cACb;GACA,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,gBAAgB,IAAI,OAAO,cAAc;GAEnE,OAAO;EACT;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,QAAQ;GACR,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9C,MAAM,UAAU,iBAAiB,SAAS,IAAI,OAAO,IAAI,QAAQ,IAAI,OAAO,cAAc;GAC1F,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;CAED,EAAE,MAAM;EACN,QAAQ;EACR,KAAK;EACL,YAAY,IAAI;EAChB,QAAQ;GACN,aAAa;GACb;GACA,SAAS;GACT,aACE;GACF,aAAa;GACb,UAAU;IAAE,KAAK,EAAE,KAAK;IAAG,GAAG,SAAS,KAAK,GAAG;GAAE;EACnD;EACA,eAAe,OAAO,QAAQ;GAC5B,IAAI,IAAI,SAAS,KAAA,GACf,MAAM,IAAI,kBAAkB,gEAAgE;EAEhG;EACA,SAAS,OAAO,KAAK,UAAU;GAC7B,MAAM,QAAQ,WAAW,IAAI,OAAO,IAAI,MAAM;GAC9C,MAAM,UAAU,iBAAiB,YAAY,IAAI,OAAO,IAAI,QAAQ,IAAI,KAAK;GAC7E,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK;EAC9B;CACF,CAAC;AACH"}
@@ -1,5 +1,5 @@
1
1
  import { PageQuery, refusals } from "../../schemas/shared.js";
2
- import { CommentView, PostComment, Reactions, SetReaction } from "../../schemas/discussion.js";
2
+ import { CommentView, PostComment, Reactions, SetPostSubscription, SetReaction } from "../../schemas/discussion.js";
3
3
  import { CommunitySpaceParam } from "../../schemas/community.js";
4
4
  import { CreatePost, FeedQuery, FlagReason, PostCommentsPage, PostFeedPage, PostFlag, PostIdParam, PostView, ResolvePostFlags, SchedulePost, SetPostReactions, SpaceQueue, UpdatePost } from "../../schemas/posts.js";
5
5
  import { CommunityParam } from "../../schemas/chats.js";
@@ -123,12 +123,12 @@ async function memberPostRoutes(app, container) {
123
123
  operationId: "createPost",
124
124
  tags,
125
125
  summary: "Create a post",
126
- description: "For anyone who can open the space. `mode` decides the status: `draft`, `scheduled`, or on `submit` `published`, or `pending` when the space requires review and you are not staff. 409 when the space has topics and none is given, or a schedule is missing or in the past.",
126
+ description: "For anyone who can open the space. `mode` decides the status: `draft`, `scheduled`, or on `submit` `published`, or `pending` when the space requires review and you are not staff. 409 when the space has topics and none is given. 400 when a schedule is missing or in the past. 503 when a scheduled post could not be timed; the post is then kept as a draft, and `details.postId` names it, so open that draft instead of creating the post again.",
127
127
  params: CommunitySpaceParam,
128
128
  body: CreatePost,
129
129
  response: {
130
130
  201: PostView,
131
- ...refusals(403, 404, 409)
131
+ ...refusals(400, 403, 404, 409, 503)
132
132
  }
133
133
  },
134
134
  handler: async (req, reply) => {
@@ -194,7 +194,7 @@ async function memberPostRoutes(app, container) {
194
194
  operationId: "submitPost",
195
195
  tags,
196
196
  summary: "Submit your draft or scheduled post",
197
- description: "Author only. Publishes it now, or holds it for review when the space requires review and you are not staff. 409 when the post is not a draft or scheduled.",
197
+ description: "Author only. Publishes it now, or holds it for review when the space requires review and you are not staff. A scheduled post is no longer published at its time. 409 when the post is not a draft or scheduled.",
198
198
  params: PostIdParam,
199
199
  response: {
200
200
  200: PostView,
@@ -214,12 +214,12 @@ async function memberPostRoutes(app, container) {
214
214
  operationId: "schedulePost",
215
215
  tags,
216
216
  summary: "Schedule your post",
217
- description: "Author only. The post is submitted at `scheduledAt`. 409 when it is already submitted or removed, or the time is not in the future.",
217
+ description: "Author only. Schedules a draft, or moves a scheduled post to a new time; the post is submitted at `scheduledAt`. 409 when it is already submitted or removed, or the time is not in the future. 503 when the time could not be set; a draft stays a draft and a moved post keeps its previous time, and `details.postId` names the post.",
218
218
  params: PostIdParam,
219
219
  body: SchedulePost,
220
220
  response: {
221
221
  200: PostView,
222
- ...refusals(403, 404, 409)
222
+ ...refusals(403, 404, 409, 503)
223
223
  }
224
224
  },
225
225
  handler: async (req) => {
@@ -227,6 +227,26 @@ async function memberPostRoutes(app, container) {
227
227
  return posts.schedule(req.orgId, req.params.postId, actor, req.body.scheduledAt);
228
228
  }
229
229
  });
230
+ r.route({
231
+ method: "POST",
232
+ url: "/api/member/posts/:postId/unschedule",
233
+ preHandler: app.requireOrgSession,
234
+ schema: {
235
+ operationId: "unschedulePost",
236
+ tags,
237
+ summary: "Unschedule your post",
238
+ description: "Author only. The scheduled post becomes a draft and is not submitted at its time. 409 when the post is not scheduled.",
239
+ params: PostIdParam,
240
+ response: {
241
+ 200: PostView,
242
+ ...refusals(403, 404, 409)
243
+ }
244
+ },
245
+ handler: async (req) => {
246
+ const actor = await actorOf(container, req.orgId, req.userId);
247
+ return posts.unschedule(req.orgId, req.params.postId, actor);
248
+ }
249
+ });
230
250
  r.route({
231
251
  method: "POST",
232
252
  url: "/api/member/posts/:postId/approval",
@@ -430,36 +450,16 @@ async function memberPostRoutes(app, container) {
430
450
  }
431
451
  });
432
452
  r.route({
433
- method: "POST",
434
- url: "/api/member/posts/:postId/follow",
435
- preHandler: app.requireOrgSession,
436
- schema: {
437
- operationId: "followPost",
438
- tags,
439
- summary: "Follow a post",
440
- description: "For anyone who can read the post. You are notified of new comments on it. Following twice is harmless.",
441
- params: PostIdParam,
442
- response: {
443
- 204: z.void(),
444
- ...refusals(403, 404)
445
- }
446
- },
447
- handler: async (req, reply) => {
448
- const actor = await actorOf(container, req.orgId, req.userId);
449
- await posts.follow(req.orgId, req.params.postId, actor);
450
- return reply.code(204).send();
451
- }
452
- });
453
- r.route({
454
- method: "DELETE",
455
- url: "/api/member/posts/:postId/follow",
453
+ method: "PUT",
454
+ url: "/api/member/posts/:postId/subscription",
456
455
  preHandler: app.requireOrgSession,
457
456
  schema: {
458
- operationId: "unfollowPost",
457
+ operationId: "setPostSubscription",
459
458
  tags,
460
- summary: "Stop following a post",
461
- description: "For anyone who can read the post. Harmless when you do not follow it.",
459
+ summary: "Follow or mute a post, or clear either",
460
+ description: "For anyone who can read the post. `following` notifies you of its new comments. `muted` stops its activity notifying you, until `mutedUntil` when given; mentions and actions on you still notify. `none` clears both.",
462
461
  params: PostIdParam,
462
+ body: SetPostSubscription,
463
463
  response: {
464
464
  204: z.void(),
465
465
  ...refusals(403, 404)
@@ -467,7 +467,7 @@ async function memberPostRoutes(app, container) {
467
467
  },
468
468
  handler: async (req, reply) => {
469
469
  const actor = await actorOf(container, req.orgId, req.userId);
470
- await posts.unfollow(req.orgId, req.params.postId, actor);
470
+ await container.subscriptions.setPost(req.orgId, actor, req.params.postId, req.body);
471
471
  return reply.code(204).send();
472
472
  }
473
473
  });
@@ -1 +1 @@
1
- {"version":3,"file":"posts.js","names":[],"sources":["../../../../src/http/routes/member/posts.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../../app/container.js';\nimport { actorOf } from './actor.js';\nimport {\n CommentView,\n CommunityParam,\n CommunitySpaceParam,\n CreatePost,\n FeedQuery,\n FlagReason,\n PageQuery,\n PostCommentsPage,\n PostComment,\n PostFeedPage,\n PostFlag,\n PostIdParam,\n PostView,\n Reactions,\n ResolvePostFlags,\n SchedulePost,\n SetPostReactions,\n SetReaction,\n SpaceQueue,\n UpdatePost,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberPostRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const posts = container.posts;\n const tags = ['Posts'];\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/feed',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberFeed',\n tags,\n summary: 'Published posts across the spaces you can open',\n description:\n 'For a member of the community. Pinned posts first, then newest first. 404 when you are not a member.',\n params: CommunityParam,\n querystring: PageQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.memberFeed(req.orgId, actor, req.params.communityId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/own-posts',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listOwnPosts',\n tags,\n summary: 'Your own posts in a community, newest first',\n description:\n 'For a member of the community. Your posts in any status, drafts included, in the spaces you can open. 404 when you are not a member.',\n params: CommunityParam,\n querystring: PageQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.listOwn(req.orgId, actor, req.params.communityId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/bookmarks',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listBookmarkedPosts',\n tags,\n summary: 'The posts you bookmarked in a community, newest bookmark first',\n description:\n 'For a member of the community. Only posts you can still read. 404 when you are not a member.',\n params: CommunityParam,\n querystring: PageQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.listBookmarked(req.orgId, actor, req.params.communityId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/spaces/:spaceId/feed',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getSpaceFeed',\n tags,\n summary: 'Published posts in a space',\n description:\n 'For anyone who can open the space (403 otherwise). Filter by `topicId`; an unknown topic is a 404.',\n params: CommunitySpaceParam,\n querystring: FeedQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.spaceFeed(\n req.orgId,\n actor,\n req.params.communityId,\n req.params.spaceId,\n req.query,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/spaces/:spaceId/queue',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getSpaceQueue',\n tags,\n summary: 'The moderation queue of a space you moderate',\n description:\n 'For a moderator who can open the space. Posts awaiting review, and the open flags on posts and on comments in the space. Each list holds at most the 100 oldest entries: pending posts by creation, flags by when they were raised; working the queue brings in the next ones.',\n params: CommunitySpaceParam,\n response: { 200: SpaceQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.moderationQueue(req.orgId, actor, req.params.communityId, req.params.spaceId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/communities/:communityId/spaces/:spaceId/posts',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createPost',\n tags,\n summary: 'Create a post',\n description:\n 'For anyone who can open the space. `mode` decides the status: `draft`, `scheduled`, or on `submit` `published`, or `pending` when the space requires review and you are not staff. 409 when the space has topics and none is given, or a schedule is missing or in the past.',\n params: CommunitySpaceParam,\n body: CreatePost,\n response: { 201: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const post = await posts.create(req.orgId, actor, {\n ...req.body,\n communityId: req.params.communityId,\n spaceId: req.params.spaceId,\n });\n req.log.info({ postId: post.id, status: post.status }, 'post created');\n return reply.code(201).send(post);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getPost',\n tags,\n summary: 'Read a post',\n description:\n 'For anyone who can open the space while the post is published; for its author in any status; for moderators also while it is pending or removed. Any other post is a 404.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.getPost(req.orgId, actor, req.params.postId);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/posts/:postId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updatePost',\n tags,\n summary: 'Edit your own post',\n description:\n 'Author only. Changes only the fields sent. 409 when the post is removed, or the topic rules of the space are not met.',\n params: PostIdParam,\n body: UpdatePost,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.update(req.orgId, req.params.postId, actor, req.body);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/submission',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'submitPost',\n tags,\n summary: 'Submit your draft or scheduled post',\n description:\n 'Author only. Publishes it now, or holds it for review when the space requires review and you are not staff. 409 when the post is not a draft or scheduled.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.submit(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/schedule',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'schedulePost',\n tags,\n summary: 'Schedule your post',\n description:\n 'Author only. The post is submitted at `scheduledAt`. 409 when it is already submitted or removed, or the time is not in the future.',\n params: PostIdParam,\n body: SchedulePost,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.schedule(req.orgId, req.params.postId, actor, req.body.scheduledAt);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/approval',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'approvePost',\n tags,\n summary: 'Publish a post that awaits review in a space you moderate',\n description:\n 'For a moderator who can open the space. 409 when the post is not awaiting review.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.approve(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/removal',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removePost',\n tags,\n summary: 'Remove your post, or one in a space you moderate',\n description:\n \"For the author or a moderator. Takes a published or pending post down; a moderator removing someone else's post resolves its open flags as `removed`. 409 when it is already removed or was never submitted.\",\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.remove(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/restoration',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'restorePost',\n tags,\n summary: 'Restore a removed post in a space you moderate',\n description: 'For a moderator who can open the space. 409 when the post is not removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.restore(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/pin',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'pinPost',\n tags,\n summary: 'Pin a post in a space you moderate',\n description: 'For a moderator. Pinned posts lead the feeds. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.pin(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/unpin',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unpinPost',\n tags,\n summary: 'Unpin a post in a space you moderate',\n description: 'For a moderator. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.unpin(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/comments/close',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'closePostComments',\n tags,\n summary: 'Close comments on a post in a space you moderate',\n description:\n 'For a moderator. Nobody can comment until they are opened again. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.closeComments(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/comments/open',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'openPostComments',\n tags,\n summary: 'Open comments on a post in a space you moderate',\n description: 'For a moderator. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.openComments(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/posts/:postId/reactions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setPostReactionPolicy',\n tags,\n summary: 'Turn reactions on or off for a post in a space you moderate',\n description:\n \"For a moderator. Overrides the space's reactions setting for this post. 409 when the post is removed.\",\n params: PostIdParam,\n body: SetPostReactions,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.setReactions(req.orgId, req.params.postId, actor, req.body.enabled);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/posts/:postId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setPostReaction',\n tags,\n summary: 'Set your reaction on a post',\n description:\n 'For anyone who can read the post, while reactions are on for it (403 otherwise). Replaces any reaction you already had and returns the new counts.',\n params: PostIdParam,\n body: SetReaction,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.setReaction(req.orgId, req.params.postId, actor, req.body.emoji);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/posts/:postId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'clearPostReaction',\n tags,\n summary: 'Withdraw your reaction from a post',\n description:\n 'For anyone who can read the post, while reactions are on for it (403 otherwise). Returns the new counts.',\n params: PostIdParam,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.setReaction(req.orgId, req.params.postId, actor, null);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/follow',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'followPost',\n tags,\n summary: 'Follow a post',\n description:\n 'For anyone who can read the post. You are notified of new comments on it. Following twice is harmless.',\n params: PostIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.follow(req.orgId, req.params.postId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/posts/:postId/follow',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unfollowPost',\n tags,\n summary: 'Stop following a post',\n description: 'For anyone who can read the post. Harmless when you do not follow it.',\n params: PostIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.unfollow(req.orgId, req.params.postId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/bookmark',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'bookmarkPost',\n tags,\n summary: 'Bookmark a post',\n description: 'For anyone who can read the post. Bookmarking twice is harmless.',\n params: PostIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.bookmark(req.orgId, req.params.postId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/posts/:postId/bookmark',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unbookmarkPost',\n tags,\n summary: 'Remove a bookmark',\n description: 'For anyone who can read the post. Harmless when you have not bookmarked it.',\n params: PostIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.unbookmark(req.orgId, req.params.postId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/flags',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'flagPost',\n tags,\n summary: 'Flag a post for the moderators',\n description:\n 'For anyone who can read the post. Flagging it again while your flag is open returns that flag.',\n params: PostIdParam,\n body: FlagReason,\n response: { 201: PostFlag, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const flag = await posts.flag(req.orgId, req.params.postId, actor, req.body.reason);\n return reply.code(201).send(flag);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/flags/resolution',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resolvePostFlags',\n tags,\n summary: 'Dismiss the open flags on a post in a space you moderate',\n description:\n 'For a moderator who can open the space. Closes every open flag and leaves the post up.',\n params: PostIdParam,\n body: ResolvePostFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.resolveFlags(req.orgId, req.params.postId, actor, req.body.resolution);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listPostComments',\n tags,\n summary: \"Read a post's comments\",\n description:\n 'For anyone who can read the post. Pages by top-level comment, oldest first. Each page carries its top-level comments and all of their replies; `total` counts top-level comments.',\n params: PostIdParam,\n querystring: PageQuery,\n response: { 200: PostCommentsPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return container.discussion.postComments(req.orgId, req.params.postId, actor, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createPostComment',\n tags,\n summary: 'Comment on a post',\n description:\n 'For anyone who can read the post, while its comments are open (403 otherwise). Replies nest one level. 409 when the post is not published.',\n params: PostIdParam,\n body: PostComment,\n response: { 201: CommentView, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const comment = await container.discussion.postComment(req.orgId, {\n actor,\n postId: req.params.postId,\n parentId: req.body.parentId,\n body: req.body.body,\n });\n return reply.code(201).send(comment);\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"posts.js","names":[],"sources":["../../../../src/http/routes/member/posts.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport type { Container } from '../../../app/container.js';\nimport { actorOf } from './actor.js';\nimport {\n CommentView,\n CommunityParam,\n CommunitySpaceParam,\n CreatePost,\n FeedQuery,\n FlagReason,\n PageQuery,\n PostCommentsPage,\n PostComment,\n PostFeedPage,\n PostFlag,\n PostIdParam,\n PostView,\n Reactions,\n ResolvePostFlags,\n SchedulePost,\n SetPostReactions,\n SetPostSubscription,\n SetReaction,\n SpaceQueue,\n UpdatePost,\n refusals,\n} from '../../schemas/index.js';\n\nexport async function memberPostRoutes(app: FastifyInstance, container: Container): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const posts = container.posts;\n const tags = ['Posts'];\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/feed',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getMemberFeed',\n tags,\n summary: 'Published posts across the spaces you can open',\n description:\n 'For a member of the community. Pinned posts first, then newest first. 404 when you are not a member.',\n params: CommunityParam,\n querystring: PageQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.memberFeed(req.orgId, actor, req.params.communityId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/own-posts',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listOwnPosts',\n tags,\n summary: 'Your own posts in a community, newest first',\n description:\n 'For a member of the community. Your posts in any status, drafts included, in the spaces you can open. 404 when you are not a member.',\n params: CommunityParam,\n querystring: PageQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.listOwn(req.orgId, actor, req.params.communityId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/bookmarks',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listBookmarkedPosts',\n tags,\n summary: 'The posts you bookmarked in a community, newest bookmark first',\n description:\n 'For a member of the community. Only posts you can still read. 404 when you are not a member.',\n params: CommunityParam,\n querystring: PageQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.listBookmarked(req.orgId, actor, req.params.communityId, req.query);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/spaces/:spaceId/feed',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getSpaceFeed',\n tags,\n summary: 'Published posts in a space',\n description:\n 'For anyone who can open the space (403 otherwise). Filter by `topicId`; an unknown topic is a 404.',\n params: CommunitySpaceParam,\n querystring: FeedQuery,\n response: { 200: PostFeedPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.spaceFeed(\n req.orgId,\n actor,\n req.params.communityId,\n req.params.spaceId,\n req.query,\n );\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/communities/:communityId/spaces/:spaceId/queue',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getSpaceQueue',\n tags,\n summary: 'The moderation queue of a space you moderate',\n description:\n 'For a moderator who can open the space. Posts awaiting review, and the open flags on posts and on comments in the space. Each list holds at most the 100 oldest entries: pending posts by creation, flags by when they were raised; working the queue brings in the next ones.',\n params: CommunitySpaceParam,\n response: { 200: SpaceQueue, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.moderationQueue(req.orgId, actor, req.params.communityId, req.params.spaceId);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/communities/:communityId/spaces/:spaceId/posts',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createPost',\n tags,\n summary: 'Create a post',\n description:\n 'For anyone who can open the space. `mode` decides the status: `draft`, `scheduled`, or on `submit` `published`, or `pending` when the space requires review and you are not staff. 409 when the space has topics and none is given. 400 when a schedule is missing or in the past. 503 when a scheduled post could not be timed; the post is then kept as a draft, and `details.postId` names it, so open that draft instead of creating the post again.',\n params: CommunitySpaceParam,\n body: CreatePost,\n response: { 201: PostView, ...refusals(400, 403, 404, 409, 503) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const post = await posts.create(req.orgId, actor, {\n ...req.body,\n communityId: req.params.communityId,\n spaceId: req.params.spaceId,\n });\n req.log.info({ postId: post.id, status: post.status }, 'post created');\n return reply.code(201).send(post);\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'getPost',\n tags,\n summary: 'Read a post',\n description:\n 'For anyone who can open the space while the post is published; for its author in any status; for moderators also while it is pending or removed. Any other post is a 404.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.getPost(req.orgId, actor, req.params.postId);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/posts/:postId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'updatePost',\n tags,\n summary: 'Edit your own post',\n description:\n 'Author only. Changes only the fields sent. 409 when the post is removed, or the topic rules of the space are not met.',\n params: PostIdParam,\n body: UpdatePost,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.update(req.orgId, req.params.postId, actor, req.body);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/submission',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'submitPost',\n tags,\n summary: 'Submit your draft or scheduled post',\n description:\n 'Author only. Publishes it now, or holds it for review when the space requires review and you are not staff. A scheduled post is no longer published at its time. 409 when the post is not a draft or scheduled.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.submit(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/schedule',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'schedulePost',\n tags,\n summary: 'Schedule your post',\n description:\n 'Author only. Schedules a draft, or moves a scheduled post to a new time; the post is submitted at `scheduledAt`. 409 when it is already submitted or removed, or the time is not in the future. 503 when the time could not be set; a draft stays a draft and a moved post keeps its previous time, and `details.postId` names the post.',\n params: PostIdParam,\n body: SchedulePost,\n response: { 200: PostView, ...refusals(403, 404, 409, 503) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.schedule(req.orgId, req.params.postId, actor, req.body.scheduledAt);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/unschedule',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unschedulePost',\n tags,\n summary: 'Unschedule your post',\n description:\n 'Author only. The scheduled post becomes a draft and is not submitted at its time. 409 when the post is not scheduled.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.unschedule(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/approval',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'approvePost',\n tags,\n summary: 'Publish a post that awaits review in a space you moderate',\n description:\n 'For a moderator who can open the space. 409 when the post is not awaiting review.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.approve(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/removal',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'removePost',\n tags,\n summary: 'Remove your post, or one in a space you moderate',\n description:\n \"For the author or a moderator. Takes a published or pending post down; a moderator removing someone else's post resolves its open flags as `removed`. 409 when it is already removed or was never submitted.\",\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.remove(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/restoration',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'restorePost',\n tags,\n summary: 'Restore a removed post in a space you moderate',\n description: 'For a moderator who can open the space. 409 when the post is not removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.restore(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/pin',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'pinPost',\n tags,\n summary: 'Pin a post in a space you moderate',\n description: 'For a moderator. Pinned posts lead the feeds. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.pin(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/unpin',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unpinPost',\n tags,\n summary: 'Unpin a post in a space you moderate',\n description: 'For a moderator. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.unpin(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/comments/close',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'closePostComments',\n tags,\n summary: 'Close comments on a post in a space you moderate',\n description:\n 'For a moderator. Nobody can comment until they are opened again. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.closeComments(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/comments/open',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'openPostComments',\n tags,\n summary: 'Open comments on a post in a space you moderate',\n description: 'For a moderator. 409 when the post is removed.',\n params: PostIdParam,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.openComments(req.orgId, req.params.postId, actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/posts/:postId/reactions',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setPostReactionPolicy',\n tags,\n summary: 'Turn reactions on or off for a post in a space you moderate',\n description:\n \"For a moderator. Overrides the space's reactions setting for this post. 409 when the post is removed.\",\n params: PostIdParam,\n body: SetPostReactions,\n response: { 200: PostView, ...refusals(403, 404, 409) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.setReactions(req.orgId, req.params.postId, actor, req.body.enabled);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/posts/:postId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setPostReaction',\n tags,\n summary: 'Set your reaction on a post',\n description:\n 'For anyone who can read the post, while reactions are on for it (403 otherwise). Replaces any reaction you already had and returns the new counts.',\n params: PostIdParam,\n body: SetReaction,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.setReaction(req.orgId, req.params.postId, actor, req.body.emoji);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/posts/:postId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'clearPostReaction',\n tags,\n summary: 'Withdraw your reaction from a post',\n description:\n 'For anyone who can read the post, while reactions are on for it (403 otherwise). Returns the new counts.',\n params: PostIdParam,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return posts.setReaction(req.orgId, req.params.postId, actor, null);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/posts/:postId/subscription',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setPostSubscription',\n tags,\n summary: 'Follow or mute a post, or clear either',\n description:\n 'For anyone who can read the post. `following` notifies you of its new comments. `muted` stops its activity notifying you, until `mutedUntil` when given; mentions and actions on you still notify. `none` clears both.',\n params: PostIdParam,\n body: SetPostSubscription,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await container.subscriptions.setPost(req.orgId, actor, req.params.postId, req.body);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/bookmark',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'bookmarkPost',\n tags,\n summary: 'Bookmark a post',\n description: 'For anyone who can read the post. Bookmarking twice is harmless.',\n params: PostIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.bookmark(req.orgId, req.params.postId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/posts/:postId/bookmark',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'unbookmarkPost',\n tags,\n summary: 'Remove a bookmark',\n description: 'For anyone who can read the post. Harmless when you have not bookmarked it.',\n params: PostIdParam,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.unbookmark(req.orgId, req.params.postId, actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/flags',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'flagPost',\n tags,\n summary: 'Flag a post for the moderators',\n description:\n 'For anyone who can read the post. Flagging it again while your flag is open returns that flag.',\n params: PostIdParam,\n body: FlagReason,\n response: { 201: PostFlag, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const flag = await posts.flag(req.orgId, req.params.postId, actor, req.body.reason);\n return reply.code(201).send(flag);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/flags/resolution',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resolvePostFlags',\n tags,\n summary: 'Dismiss the open flags on a post in a space you moderate',\n description:\n 'For a moderator who can open the space. Closes every open flag and leaves the post up.',\n params: PostIdParam,\n body: ResolvePostFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n await posts.resolveFlags(req.orgId, req.params.postId, actor, req.body.resolution);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'GET',\n url: '/api/member/posts/:postId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listPostComments',\n tags,\n summary: \"Read a post's comments\",\n description:\n 'For anyone who can read the post. Pages by top-level comment, oldest first. Each page carries its top-level comments and all of their replies; `total` counts top-level comments.',\n params: PostIdParam,\n querystring: PageQuery,\n response: { 200: PostCommentsPage, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n return container.discussion.postComments(req.orgId, req.params.postId, actor, req.query);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/posts/:postId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'createPostComment',\n tags,\n summary: 'Comment on a post',\n description:\n 'For anyone who can read the post, while its comments are open (403 otherwise). Replies nest one level. 409 when the post is not published.',\n params: PostIdParam,\n body: PostComment,\n response: { 201: CommentView, ...refusals(403, 404, 409) },\n },\n handler: async (req, reply) => {\n const actor = await actorOf(container, req.orgId, req.userId);\n const comment = await container.discussion.postComment(req.orgId, {\n actor,\n postId: req.params.postId,\n parentId: req.body.parentId,\n body: req.body.body,\n });\n return reply.code(201).send(comment);\n },\n });\n}\n"],"mappings":"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"}
@@ -1,9 +1,11 @@
1
1
  import { refusals } from "../schemas/shared.js";
2
- import { resolveScope } from "../scope.js";
2
+ import { resolveConfiguratorScope, resolveScope } from "../scope.js";
3
3
  import { CreateOrganization, Organization, UpdateOrganization } from "../schemas/organizations.js";
4
4
  import { MemberIdParam, MemberRoleChange, MembersPage, MembersQuery, UpdateMemberRole } from "../schemas/members.js";
5
5
  import { AcceptInvite, AcceptInviteResult, CreateInvite, GetInviteResult, Invite, InviteTokenParam } from "../schemas/invites.js";
6
+ import { NotificationCategoryDefaults, NotificationCategoryList, NotificationChannelInfos, NotificationListScopeQuery, NotificationTypeInfos, ReplaceNotificationCategories } from "../schemas/notifications.js";
6
7
  import "../schemas/index.js";
8
+ import { typesForList } from "@headless-lms/core/shared/notification-categories";
7
9
  import { z } from "zod";
8
10
  import { NotFoundError } from "@headless-lms/core/shared/errors";
9
11
  //#region src/http/routes/organizations.ts
@@ -237,6 +239,125 @@ async function organizationsRoutes(app, container) {
237
239
  return {};
238
240
  }
239
241
  });
242
+ const notificationTags = ["Notifications"];
243
+ r.route({
244
+ method: "GET",
245
+ url: "/api/notification-types",
246
+ preHandler: app.requireOrgSession,
247
+ schema: {
248
+ operationId: "listNotificationTypes",
249
+ tags: notificationTags,
250
+ summary: "List the notification types a category list can cover (staff)",
251
+ description: "Owner or admin only. `scope=org` lists the types the org list can cover, `scope=community` those a community list can cover. Each type carries all its scopes, so an editor can tell which types can be per space.",
252
+ querystring: NotificationListScopeQuery,
253
+ response: {
254
+ 200: NotificationTypeInfos,
255
+ ...refusals(403)
256
+ }
257
+ },
258
+ handler: async (req) => {
259
+ await resolveConfiguratorScope(container, req);
260
+ return typesForList(container.notificationTypes, req.query.scope);
261
+ }
262
+ });
263
+ r.route({
264
+ method: "GET",
265
+ url: "/api/notification-channels",
266
+ preHandler: app.requireOrgSession,
267
+ schema: {
268
+ operationId: "listNotificationChannels",
269
+ tags: notificationTags,
270
+ summary: "List the notification channels this deployment can deliver on (staff)",
271
+ description: "Owner or admin only. `inApp` is always first.",
272
+ response: {
273
+ 200: NotificationChannelInfos,
274
+ ...refusals(403)
275
+ }
276
+ },
277
+ handler: async (req) => {
278
+ await resolveConfiguratorScope(container, req);
279
+ return container.notificationChannels.list().map((channel) => ({ channel }));
280
+ }
281
+ });
282
+ r.route({
283
+ method: "GET",
284
+ url: "/api/notification-categories",
285
+ preHandler: app.requireOrgSession,
286
+ schema: {
287
+ operationId: "getOrgNotificationCategories",
288
+ tags: notificationTags,
289
+ summary: "Get the organization's notification categories (staff)",
290
+ description: "Owner or admin only. The list in use for notifications outside any community, with `source` saying whether it is the default or a stored list.",
291
+ response: {
292
+ 200: NotificationCategoryList,
293
+ ...refusals(403)
294
+ }
295
+ },
296
+ handler: async (req) => {
297
+ const scope = await resolveConfiguratorScope(container, req);
298
+ return container.notificationCategories.org.get(scope.orgId, scope.orgId);
299
+ }
300
+ });
301
+ r.route({
302
+ method: "PUT",
303
+ url: "/api/notification-categories",
304
+ preHandler: app.requireOrgSession,
305
+ schema: {
306
+ operationId: "replaceOrgNotificationCategories",
307
+ tags: notificationTags,
308
+ summary: "Replace the organization's notification categories (staff)",
309
+ description: "Owner or admin only. Stores the whole list. 422 when a key repeats, a type cannot be in the org list, a reason is not allowed for its type, a channel is not configured, or earlier categories already cover everything a category covers.",
310
+ body: ReplaceNotificationCategories,
311
+ response: {
312
+ 200: NotificationCategoryList,
313
+ ...refusals(400, 403, 422)
314
+ }
315
+ },
316
+ handler: async (req) => {
317
+ const scope = await resolveConfiguratorScope(container, req);
318
+ return container.notificationCategories.org.replace(scope.orgId, scope.orgId, req.body.categories);
319
+ }
320
+ });
321
+ r.route({
322
+ method: "DELETE",
323
+ url: "/api/notification-categories",
324
+ preHandler: app.requireOrgSession,
325
+ schema: {
326
+ operationId: "resetOrgNotificationCategories",
327
+ tags: notificationTags,
328
+ summary: "Go back to the default notification categories for the organization (staff)",
329
+ description: "Owner or admin only. Removes the stored list; the default applies again.",
330
+ response: {
331
+ 204: z.void(),
332
+ ...refusals(403)
333
+ }
334
+ },
335
+ handler: async (req, reply) => {
336
+ const scope = await resolveConfiguratorScope(container, req);
337
+ await container.notificationCategories.org.reset(scope.orgId, scope.orgId);
338
+ return reply.code(204).send();
339
+ }
340
+ });
341
+ r.route({
342
+ method: "GET",
343
+ url: "/api/notification-categories/defaults",
344
+ preHandler: app.requireOrgSession,
345
+ schema: {
346
+ operationId: "getDefaultNotificationCategories",
347
+ tags: notificationTags,
348
+ summary: "Get the default notification categories (staff)",
349
+ description: "Owner or admin only. The default org list (`scope=org`) or community list (`scope=community`), offering only the configured channels.",
350
+ querystring: NotificationListScopeQuery,
351
+ response: {
352
+ 200: NotificationCategoryDefaults,
353
+ ...refusals(403)
354
+ }
355
+ },
356
+ handler: async (req) => {
357
+ await resolveConfiguratorScope(container, req);
358
+ return { categories: (req.query.scope === "org" ? container.notificationCategories.org : container.notificationCategories.community).defaults() };
359
+ }
360
+ });
240
361
  }
241
362
  //#endregion
242
363
  export { organizationsRoutes };