@headless-lms/server 0.3.0 → 0.5.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 (228) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +10 -10
  3. package/dist/app/community-mail.js +72 -28
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +48 -10
  6. package/dist/app/container.js +268 -48
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.d.ts +47 -0
  9. package/dist/app/notification-channels.js +175 -0
  10. package/dist/app/notification-channels.js.map +1 -0
  11. package/dist/app/notification-reads.d.ts +23 -0
  12. package/dist/app/notification-reads.js +74 -0
  13. package/dist/app/notification-reads.js.map +1 -0
  14. package/dist/app/platform.d.ts +14 -0
  15. package/dist/app/platform.js +14 -0
  16. package/dist/app/platform.js.map +1 -0
  17. package/dist/http/config.d.ts +4 -0
  18. package/dist/http/errors.d.ts +1 -0
  19. package/dist/http/errors.js +24 -4
  20. package/dist/http/errors.js.map +1 -1
  21. package/dist/http/fastify.d.ts +8 -0
  22. package/dist/http/mcp/catalogue.d.ts +11 -9
  23. package/dist/http/mcp/catalogue.js +15 -21
  24. package/dist/http/mcp/catalogue.js.map +1 -1
  25. package/dist/http/mcp/challenge.js +3 -3
  26. package/dist/http/mcp/challenge.js.map +1 -1
  27. package/dist/http/mcp/route.js +7 -7
  28. package/dist/http/mcp/route.js.map +1 -1
  29. package/dist/http/mcp/test-support.d.ts +6 -5
  30. package/dist/http/mcp/test-support.js +32 -11
  31. package/dist/http/mcp/test-support.js.map +1 -1
  32. package/dist/http/mcp/tool.d.ts +3 -9
  33. package/dist/http/mcp/tool.js +14 -6
  34. package/dist/http/mcp/tool.js.map +1 -1
  35. package/dist/http/mcp/tools/automations.js +10 -20
  36. package/dist/http/mcp/tools/automations.js.map +1 -1
  37. package/dist/http/mcp/tools/catalog.js +25 -42
  38. package/dist/http/mcp/tools/catalog.js.map +1 -1
  39. package/dist/http/mcp/tools/comments.js +9 -19
  40. package/dist/http/mcp/tools/comments.js.map +1 -1
  41. package/dist/http/mcp/tools/communities.js +16 -31
  42. package/dist/http/mcp/tools/communities.js.map +1 -1
  43. package/dist/http/mcp/tools/community-members.js +9 -16
  44. package/dist/http/mcp/tools/community-members.js.map +1 -1
  45. package/dist/http/mcp/tools/community-moderation.js +13 -25
  46. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  47. package/dist/http/mcp/tools/courses.js +11 -21
  48. package/dist/http/mcp/tools/courses.js.map +1 -1
  49. package/dist/http/mcp/tools/events.d.ts +6 -0
  50. package/dist/http/mcp/tools/events.js +170 -0
  51. package/dist/http/mcp/tools/events.js.map +1 -0
  52. package/dist/http/mcp/tools/index.d.ts +5 -1
  53. package/dist/http/mcp/tools/index.js +45 -10
  54. package/dist/http/mcp/tools/index.js.map +1 -1
  55. package/dist/http/mcp/tools/organization.d.ts +6 -0
  56. package/dist/http/mcp/tools/organization.js +214 -0
  57. package/dist/http/mcp/tools/organization.js.map +1 -0
  58. package/dist/http/mcp/tools/people.js +30 -49
  59. package/dist/http/mcp/tools/people.js.map +1 -1
  60. package/dist/http/plugins/auth.js +37 -8
  61. package/dist/http/plugins/auth.js.map +1 -1
  62. package/dist/http/plugins/error-handler.d.ts +5 -1
  63. package/dist/http/plugins/error-handler.js +13 -3
  64. package/dist/http/plugins/error-handler.js.map +1 -1
  65. package/dist/http/plugins/openapi.js +18 -2
  66. package/dist/http/plugins/openapi.js.map +1 -1
  67. package/dist/http/plugins/platform.d.ts +7 -0
  68. package/dist/http/plugins/platform.js +13 -0
  69. package/dist/http/plugins/platform.js.map +1 -0
  70. package/dist/http/routes/access.js +54 -36
  71. package/dist/http/routes/access.js.map +1 -1
  72. package/dist/http/routes/activities.js +45 -45
  73. package/dist/http/routes/activities.js.map +1 -1
  74. package/dist/http/routes/assets.js +30 -65
  75. package/dist/http/routes/assets.js.map +1 -1
  76. package/dist/http/routes/automations.js +37 -42
  77. package/dist/http/routes/automations.js.map +1 -1
  78. package/dist/http/routes/bundles.js +33 -36
  79. package/dist/http/routes/bundles.js.map +1 -1
  80. package/dist/http/routes/communities.js +144 -136
  81. package/dist/http/routes/communities.js.map +1 -1
  82. package/dist/http/routes/community-moderation.js +83 -137
  83. package/dist/http/routes/community-moderation.js.map +1 -1
  84. package/dist/http/routes/course-analytics.js +32 -15
  85. package/dist/http/routes/course-analytics.js.map +1 -1
  86. package/dist/http/routes/courses.js +25 -32
  87. package/dist/http/routes/courses.js.map +1 -1
  88. package/dist/http/routes/dashboard.js +9 -9
  89. package/dist/http/routes/dashboard.js.map +1 -1
  90. package/dist/http/routes/discussion.js +21 -39
  91. package/dist/http/routes/discussion.js.map +1 -1
  92. package/dist/http/routes/downloads.js +41 -47
  93. package/dist/http/routes/downloads.js.map +1 -1
  94. package/dist/http/routes/entitlements.js +19 -23
  95. package/dist/http/routes/entitlements.js.map +1 -1
  96. package/dist/http/routes/events.d.ts +7 -0
  97. package/dist/http/routes/events.js +333 -0
  98. package/dist/http/routes/events.js.map +1 -0
  99. package/dist/http/routes/integrations.js +40 -73
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/assets.js +10 -13
  102. package/dist/http/routes/member/assets.js.map +1 -1
  103. package/dist/http/routes/member/chats.js +177 -75
  104. package/dist/http/routes/member/chats.js.map +1 -1
  105. package/dist/http/routes/member/comments.js +65 -26
  106. package/dist/http/routes/member/comments.js.map +1 -1
  107. package/dist/http/routes/member/community.js +68 -51
  108. package/dist/http/routes/member/community.js.map +1 -1
  109. package/dist/http/routes/member/courses.js +12 -19
  110. package/dist/http/routes/member/courses.js.map +1 -1
  111. package/dist/http/routes/member/devices.d.ts +7 -0
  112. package/dist/http/routes/member/devices.js +97 -0
  113. package/dist/http/routes/member/devices.js.map +1 -0
  114. package/dist/http/routes/member/downloads.js +6 -9
  115. package/dist/http/routes/member/downloads.js.map +1 -1
  116. package/dist/http/routes/member/events.d.ts +7 -0
  117. package/dist/http/routes/member/events.js +386 -0
  118. package/dist/http/routes/member/events.js.map +1 -0
  119. package/dist/http/routes/member/files.js +71 -46
  120. package/dist/http/routes/member/files.js.map +1 -1
  121. package/dist/http/routes/member/index.js +6 -0
  122. package/dist/http/routes/member/index.js.map +1 -1
  123. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  124. package/dist/http/routes/member/notification-settings.js +100 -0
  125. package/dist/http/routes/member/notification-settings.js.map +1 -0
  126. package/dist/http/routes/member/notifications.js +69 -45
  127. package/dist/http/routes/member/notifications.js.map +1 -1
  128. package/dist/http/routes/member/org.js +5 -15
  129. package/dist/http/routes/member/org.js.map +1 -1
  130. package/dist/http/routes/member/posts.js +94 -95
  131. package/dist/http/routes/member/posts.js.map +1 -1
  132. package/dist/http/routes/member/profile.js +15 -6
  133. package/dist/http/routes/member/profile.js.map +1 -1
  134. package/dist/http/routes/member/progress.js +7 -11
  135. package/dist/http/routes/member/progress.js.map +1 -1
  136. package/dist/http/routes/organizations.js +415 -110
  137. package/dist/http/routes/organizations.js.map +1 -1
  138. package/dist/http/routes/platform.d.ts +7 -0
  139. package/dist/http/routes/platform.js +61 -0
  140. package/dist/http/routes/platform.js.map +1 -0
  141. package/dist/http/routes/students.js +12 -75
  142. package/dist/http/routes/students.js.map +1 -1
  143. package/dist/http/routes/webhooks.js +30 -39
  144. package/dist/http/routes/webhooks.js.map +1 -1
  145. package/dist/http/routes.js +9 -0
  146. package/dist/http/routes.js.map +1 -1
  147. package/dist/http/schemas/access.d.ts +346 -64
  148. package/dist/http/schemas/access.js +39 -12
  149. package/dist/http/schemas/access.js.map +1 -1
  150. package/dist/http/schemas/assets.d.ts +13 -8
  151. package/dist/http/schemas/assets.js +6 -5
  152. package/dist/http/schemas/assets.js.map +1 -1
  153. package/dist/http/schemas/automations.d.ts +4 -8
  154. package/dist/http/schemas/chats.d.ts +238 -36
  155. package/dist/http/schemas/chats.js +14 -7
  156. package/dist/http/schemas/chats.js.map +1 -1
  157. package/dist/http/schemas/community.d.ts +140 -33
  158. package/dist/http/schemas/community.js +25 -9
  159. package/dist/http/schemas/community.js.map +1 -1
  160. package/dist/http/schemas/course-analytics.d.ts +51 -1
  161. package/dist/http/schemas/course-analytics.js +19 -1
  162. package/dist/http/schemas/course-analytics.js.map +1 -1
  163. package/dist/http/schemas/dashboard.d.ts +1 -0
  164. package/dist/http/schemas/dashboard.js +2 -1
  165. package/dist/http/schemas/dashboard.js.map +1 -1
  166. package/dist/http/schemas/discussion.d.ts +45 -73
  167. package/dist/http/schemas/discussion.js +16 -4
  168. package/dist/http/schemas/discussion.js.map +1 -1
  169. package/dist/http/schemas/entitlements.d.ts +9 -12
  170. package/dist/http/schemas/entitlements.js +11 -5
  171. package/dist/http/schemas/entitlements.js.map +1 -1
  172. package/dist/http/schemas/events.d.ts +527 -0
  173. package/dist/http/schemas/events.js +64 -0
  174. package/dist/http/schemas/events.js.map +1 -0
  175. package/dist/http/schemas/files.d.ts +15 -5
  176. package/dist/http/schemas/files.js +10 -2
  177. package/dist/http/schemas/files.js.map +1 -1
  178. package/dist/http/schemas/index.d.ts +14 -12
  179. package/dist/http/schemas/index.js +14 -12
  180. package/dist/http/schemas/invites.d.ts +3 -47
  181. package/dist/http/schemas/invites.js +4 -25
  182. package/dist/http/schemas/invites.js.map +1 -1
  183. package/dist/http/schemas/member.d.ts +94 -7
  184. package/dist/http/schemas/member.js +7 -3
  185. package/dist/http/schemas/member.js.map +1 -1
  186. package/dist/http/schemas/members.d.ts +329 -65
  187. package/dist/http/schemas/members.js +67 -25
  188. package/dist/http/schemas/members.js.map +1 -1
  189. package/dist/http/schemas/notifications.d.ts +502 -175
  190. package/dist/http/schemas/notifications.js +148 -28
  191. package/dist/http/schemas/notifications.js.map +1 -1
  192. package/dist/http/schemas/platform.d.ts +37 -0
  193. package/dist/http/schemas/platform.js +19 -0
  194. package/dist/http/schemas/platform.js.map +1 -0
  195. package/dist/http/schemas/posts.d.ts +92 -38
  196. package/dist/http/schemas/posts.js +4 -4
  197. package/dist/http/schemas/posts.js.map +1 -1
  198. package/dist/http/schemas/profile.d.ts +1 -1
  199. package/dist/http/schemas/shared.d.ts +10 -13
  200. package/dist/http/schemas/shared.js +3 -11
  201. package/dist/http/schemas/shared.js.map +1 -1
  202. package/dist/http/schemas/students.d.ts +22 -0
  203. package/dist/http/schemas/students.js +3 -1
  204. package/dist/http/schemas/students.js.map +1 -1
  205. package/dist/http/scope.d.ts +9 -12
  206. package/dist/http/scope.js +30 -35
  207. package/dist/http/scope.js.map +1 -1
  208. package/dist/http/server.js +9 -0
  209. package/dist/http/server.js.map +1 -1
  210. package/dist/http/shutdown.d.ts +11 -0
  211. package/dist/http/shutdown.js +40 -0
  212. package/dist/http/shutdown.js.map +1 -0
  213. package/dist/http/sse.d.ts +19 -0
  214. package/dist/http/sse.js +104 -0
  215. package/dist/http/sse.js.map +1 -0
  216. package/dist/http/test-caller.d.ts +13 -0
  217. package/dist/http/test-caller.js +14 -0
  218. package/dist/http/test-caller.js.map +1 -0
  219. package/dist/http/well-known.js +2 -2
  220. package/dist/http/well-known.js.map +1 -1
  221. package/dist/index.d.ts +5 -2
  222. package/dist/index.js +4 -2
  223. package/dist/index.js.map +1 -1
  224. package/dist/packages/server/package.js +1 -1
  225. package/package.json +7 -6
  226. package/dist/http/routes/member/actor.d.ts +0 -14
  227. package/dist/http/routes/member/actor.js +0 -11
  228. package/dist/http/routes/member/actor.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { refusals } from "../../schemas/shared.js";
2
- import { ActivityComments, CommentIdParam, CommentReport, CommentView, DiscussionActivityParam, EditComment, PostComment, Reactions, ReportComment, ResolveCommentFlags, SetReaction } from "../../schemas/discussion.js";
2
+ import { resolveActor } from "../../scope.js";
3
+ import { ActivityComments, Comment, CommentIdParam, CommentReport, CommentView, DiscussionActivityParam, EditComment, PostComment, Reactions, ReportComment, ResolveCommentFlags, SetReaction } from "../../schemas/discussion.js";
3
4
  import "../../schemas/index.js";
4
- import { actorOf } from "./actor.js";
5
5
  import { z } from "zod";
6
6
  //#region src/http/routes/member/comments.ts
7
7
  async function memberCommentsRoutes(app, container) {
@@ -16,7 +16,7 @@ async function memberCommentsRoutes(app, container) {
16
16
  operationId: "listActivityComments",
17
17
  tags,
18
18
  summary: "Read an activity's comments",
19
- description: "For a student who can open the course, and staff. Returns the activity's resolved comment settings with the comments; removed comments keep their place without their body.",
19
+ description: "For anyone who can open the course: through a grant, or with `courses:read` on it. Returns the activity's resolved comment settings with the comments; removed comments keep their place without their body.",
20
20
  params: DiscussionActivityParam,
21
21
  response: {
22
22
  200: ActivityComments,
@@ -24,8 +24,8 @@ async function memberCommentsRoutes(app, container) {
24
24
  }
25
25
  },
26
26
  handler: async (req) => {
27
- const orgUser = await actorOf(container, req.orgId, req.userId);
28
- return discussion.activityComments(req.orgId, req.params.activityId, orgUser);
27
+ const actor = await resolveActor(container, req);
28
+ return discussion.activityComments(req.params.activityId, actor);
29
29
  }
30
30
  });
31
31
  r.route({
@@ -36,7 +36,7 @@ async function memberCommentsRoutes(app, container) {
36
36
  operationId: "postComment",
37
37
  tags,
38
38
  summary: "Post a comment or reply on an activity",
39
- description: "For a student who can open the course, and staff, while discussion is on for the activity (403 otherwise). Lands pending when the course requires review and you are not staff. Replies nest one level and need threading on.",
39
+ description: "For anyone who can open the course, while discussion is on for the activity (403 otherwise). Lands pending when the course requires review and you do not hold `courses:moderate` on it. Replies nest one level and need threading on.",
40
40
  params: DiscussionActivityParam,
41
41
  body: PostComment,
42
42
  response: {
@@ -45,13 +45,12 @@ async function memberCommentsRoutes(app, container) {
45
45
  }
46
46
  },
47
47
  handler: async (req, reply) => {
48
- const orgUser = await actorOf(container, req.orgId, req.userId);
49
- const comment = await discussion.postComment(req.orgId, {
50
- actor: orgUser,
48
+ const actor = await resolveActor(container, req);
49
+ const comment = await discussion.postComment({
51
50
  activityId: req.params.activityId,
52
51
  parentId: req.body.parentId,
53
52
  body: req.body.body
54
- });
53
+ }, actor);
55
54
  return reply.code(201).send(comment);
56
55
  }
57
56
  });
@@ -63,7 +62,7 @@ async function memberCommentsRoutes(app, container) {
63
62
  operationId: "editComment",
64
63
  tags,
65
64
  summary: "Revise your own comment",
66
- description: "Author only; not even staff may revise it. Discussion must be open where the comment is, and a removed comment cannot be revised.",
65
+ description: "Author only; not even a moderator may revise it. Discussion must be open where the comment is, and a removed comment cannot be revised.",
67
66
  params: CommentIdParam,
68
67
  body: EditComment,
69
68
  response: {
@@ -72,8 +71,8 @@ async function memberCommentsRoutes(app, container) {
72
71
  }
73
72
  },
74
73
  handler: async (req) => {
75
- const orgUser = await actorOf(container, req.orgId, req.userId);
76
- return discussion.edit(req.orgId, req.params.commentId, orgUser, req.body.body);
74
+ const actor = await resolveActor(container, req);
75
+ return discussion.edit(req.params.commentId, req.body.body, actor);
77
76
  }
78
77
  });
79
78
  r.route({
@@ -84,7 +83,7 @@ async function memberCommentsRoutes(app, container) {
84
83
  operationId: "deleteComment",
85
84
  tags,
86
85
  summary: "Delete your comment, or one you moderate",
87
- description: "For the author, staff on an activity comment, or a moderator on a post comment. The comment is marked removed and keeps its place in the thread. Deleting a removed comment returns it unchanged.",
86
+ description: "The author can delete their own comment; anyone else needs `communities:moderate` on the community for a post comment, or `courses:moderate` on the course for a lesson comment. The comment is marked removed and keeps its place in the thread. Deleting a removed comment returns it unchanged.",
88
87
  params: CommentIdParam,
89
88
  response: {
90
89
  200: CommentView,
@@ -92,8 +91,8 @@ async function memberCommentsRoutes(app, container) {
92
91
  }
93
92
  },
94
93
  handler: async (req) => {
95
- const orgUser = await actorOf(container, req.orgId, req.userId);
96
- return discussion.remove(req.orgId, req.params.commentId, orgUser);
94
+ const actor = await resolveActor(container, req);
95
+ return discussion.remove(req.params.commentId, "any", actor);
97
96
  }
98
97
  });
99
98
  r.route({
@@ -113,8 +112,8 @@ async function memberCommentsRoutes(app, container) {
113
112
  }
114
113
  },
115
114
  handler: async (req) => {
116
- const orgUser = await actorOf(container, req.orgId, req.userId);
117
- return discussion.setReaction(req.orgId, req.params.commentId, orgUser, req.body.emoji);
115
+ const actor = await resolveActor(container, req);
116
+ return discussion.setReaction(req.params.commentId, req.body.emoji, actor);
118
117
  }
119
118
  });
120
119
  r.route({
@@ -133,8 +132,8 @@ async function memberCommentsRoutes(app, container) {
133
132
  }
134
133
  },
135
134
  handler: async (req) => {
136
- const orgUser = await actorOf(container, req.orgId, req.userId);
137
- return discussion.setReaction(req.orgId, req.params.commentId, orgUser, null);
135
+ const actor = await resolveActor(container, req);
136
+ return discussion.setReaction(req.params.commentId, null, actor);
138
137
  }
139
138
  });
140
139
  r.route({
@@ -154,19 +153,19 @@ async function memberCommentsRoutes(app, container) {
154
153
  }
155
154
  },
156
155
  handler: async (req) => {
157
- const orgUser = await actorOf(container, req.orgId, req.userId);
158
- return discussion.reportComment(req.orgId, req.params.commentId, orgUser, req.body.reason);
156
+ const actor = await resolveActor(container, req);
157
+ return discussion.reportComment(req.params.commentId, req.body.reason, actor);
159
158
  }
160
159
  });
161
160
  r.route({
162
161
  method: "POST",
163
- url: "/api/member/comments/:commentId/flags/resolution",
162
+ url: "/api/member/comments/:commentId/flags/resolve",
164
163
  preHandler: app.requireOrgSession,
165
164
  schema: {
166
165
  operationId: "resolveCommentFlags",
167
166
  tags,
168
167
  summary: "Dismiss the open flags on a comment in a space you moderate",
169
- description: "For a moderator of a post comment, or staff for an activity comment. Closes every open flag and leaves the comment up.",
168
+ description: "Needs `courses:moderate` on the course for an activity comment, or `communities:moderate` on the community for a post comment. Closes every open flag and leaves the comment up.",
170
169
  params: CommentIdParam,
171
170
  body: ResolveCommentFlags,
172
171
  response: {
@@ -175,11 +174,51 @@ async function memberCommentsRoutes(app, container) {
175
174
  }
176
175
  },
177
176
  handler: async (req, reply) => {
178
- const orgUser = await actorOf(container, req.orgId, req.userId);
179
- await discussion.resolveReports(req.orgId, req.params.commentId, orgUser);
177
+ const actor = await resolveActor(container, req);
178
+ await discussion.resolveReports(req.params.commentId, "any", actor);
180
179
  return reply.code(204).send();
181
180
  }
182
181
  });
182
+ r.route({
183
+ method: "POST",
184
+ url: "/api/member/comments/:commentId/restore",
185
+ preHandler: app.requireOrgSession,
186
+ schema: {
187
+ operationId: "restoreMemberComment",
188
+ tags,
189
+ summary: "Restore a removed comment where you moderate",
190
+ description: "Needs `courses:moderate` on the course for an activity comment, or `communities:moderate` on the community for a post comment. Publishes the comment again; a comment that is not removed is returned unchanged.",
191
+ params: CommentIdParam,
192
+ response: {
193
+ 200: CommentView,
194
+ ...refusals(403, 404)
195
+ }
196
+ },
197
+ handler: async (req) => {
198
+ const actor = await resolveActor(container, req);
199
+ return discussion.restore(req.params.commentId, "any", actor);
200
+ }
201
+ });
202
+ r.route({
203
+ method: "POST",
204
+ url: "/api/member/comments/:commentId/approve",
205
+ preHandler: app.requireOrgSession,
206
+ schema: {
207
+ operationId: "approveMemberComment",
208
+ tags,
209
+ summary: "Publish a comment awaiting review where you moderate",
210
+ description: "Needs `courses:moderate` on the course for an activity comment, or `communities:moderate` on the community for a post comment. 403 when the comment is not awaiting review.",
211
+ params: CommentIdParam,
212
+ response: {
213
+ 200: Comment,
214
+ ...refusals(403, 404)
215
+ }
216
+ },
217
+ handler: async (req) => {
218
+ const actor = await resolveActor(container, req);
219
+ return discussion.approve(req.params.commentId, actor);
220
+ }
221
+ });
183
222
  }
184
223
  //#endregion
185
224
  export { memberCommentsRoutes };
@@ -1 +1 @@
1
- {"version":3,"file":"comments.js","names":[],"sources":["../../../../src/http/routes/member/comments.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActivityComments,\n CommentIdParam,\n CommentReport,\n CommentView,\n DiscussionActivityParam,\n EditComment,\n PostComment,\n Reactions,\n ReportComment,\n ResolveCommentFlags,\n SetReaction,\n refusals,\n} from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { actorOf } from './actor.js';\n\nexport async function memberCommentsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const discussion = container.discussion;\n const tags = ['Discussion'];\n\n r.route({\n method: 'GET',\n url: '/api/member/activities/:activityId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listActivityComments',\n tags,\n summary: \"Read an activity's comments\",\n description:\n \"For a student who can open the course, and staff. Returns the activity's resolved comment settings with the comments; removed comments keep their place without their body.\",\n params: DiscussionActivityParam,\n response: { 200: ActivityComments, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return discussion.activityComments(req.orgId, req.params.activityId, orgUser);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/activities/:activityId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'postComment',\n tags,\n summary: 'Post a comment or reply on an activity',\n description:\n 'For a student who can open the course, and staff, while discussion is on for the activity (403 otherwise). Lands pending when the course requires review and you are not staff. Replies nest one level and need threading on.',\n params: DiscussionActivityParam,\n body: PostComment,\n response: { 201: CommentView, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n const comment = await discussion.postComment(req.orgId, {\n actor: orgUser,\n activityId: req.params.activityId,\n parentId: req.body.parentId,\n body: req.body.body,\n });\n return reply.code(201).send(comment);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'editComment',\n tags,\n summary: 'Revise your own comment',\n description:\n 'Author only; not even staff may revise it. Discussion must be open where the comment is, and a removed comment cannot be revised.',\n params: CommentIdParam,\n body: EditComment,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return discussion.edit(req.orgId, req.params.commentId, orgUser, req.body.body);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteComment',\n tags,\n summary: 'Delete your comment, or one you moderate',\n description:\n 'For the author, staff on an activity comment, or a moderator on a post comment. The comment is marked removed and keeps its place in the thread. Deleting a removed comment returns it unchanged.',\n params: CommentIdParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return discussion.remove(req.orgId, req.params.commentId, orgUser);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/comments/:commentId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setCommentReaction',\n tags,\n summary: 'Set your reaction on a comment',\n description:\n 'For anyone who can read the comment, while reactions are on where it is (403 otherwise). Replaces any reaction you already had and returns the new counts.',\n params: CommentIdParam,\n body: SetReaction,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return discussion.setReaction(req.orgId, req.params.commentId, orgUser, req.body.emoji);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/comments/:commentId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'clearCommentReaction',\n tags,\n summary: 'Withdraw your reaction from a comment',\n description: 'For anyone who can read the comment. Returns the new counts.',\n params: CommentIdParam,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return discussion.setReaction(req.orgId, req.params.commentId, orgUser, null);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/comments/:commentId/reports',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reportComment',\n tags,\n summary: 'Flag a comment for the moderators',\n description:\n 'For anyone who can read the comment. Flagging it again while your flag is open returns that flag.',\n params: CommentIdParam,\n body: ReportComment,\n response: { 200: CommentReport, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n return discussion.reportComment(req.orgId, req.params.commentId, orgUser, req.body.reason);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/comments/:commentId/flags/resolution',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resolveCommentFlags',\n tags,\n summary: 'Dismiss the open flags on a comment in a space you moderate',\n description:\n 'For a moderator of a post comment, or staff for an activity comment. Closes every open flag and leaves the comment up.',\n params: CommentIdParam,\n body: ResolveCommentFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const orgUser = await actorOf(container, req.orgId, req.userId);\n await discussion.resolveReports(req.orgId, req.params.commentId, orgUser);\n return reply.code(204).send();\n },\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"comments.js","names":[],"sources":["../../../../src/http/routes/member/comments.ts"],"sourcesContent":["import type { FastifyInstance } from 'fastify';\nimport type { ZodTypeProvider } from 'fastify-type-provider-zod';\nimport { z } from 'zod';\nimport {\n ActivityComments,\n Comment,\n CommentIdParam,\n CommentReport,\n CommentView,\n DiscussionActivityParam,\n EditComment,\n PostComment,\n Reactions,\n ReportComment,\n ResolveCommentFlags,\n SetReaction,\n refusals,\n} from '../../schemas/index.js';\nimport type { Container } from '../../../app/container.js';\nimport { resolveActor } from '../../scope.js';\n\nexport async function memberCommentsRoutes(\n app: FastifyInstance,\n container: Container,\n): Promise<void> {\n const r = app.withTypeProvider<ZodTypeProvider>();\n const discussion = container.discussion;\n const tags = ['Discussion'];\n\n r.route({\n method: 'GET',\n url: '/api/member/activities/:activityId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'listActivityComments',\n tags,\n summary: \"Read an activity's comments\",\n description:\n \"For anyone who can open the course: through a grant, or with `courses:read` on it. Returns the activity's resolved comment settings with the comments; removed comments keep their place without their body.\",\n params: DiscussionActivityParam,\n response: { 200: ActivityComments, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.activityComments(req.params.activityId, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/activities/:activityId/comments',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'postComment',\n tags,\n summary: 'Post a comment or reply on an activity',\n description:\n 'For anyone who can open the course, while discussion is on for the activity (403 otherwise). Lands pending when the course requires review and you do not hold `courses:moderate` on it. Replies nest one level and need threading on.',\n params: DiscussionActivityParam,\n body: PostComment,\n response: { 201: CommentView, ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n const comment = await discussion.postComment(\n { activityId: req.params.activityId, parentId: req.body.parentId, body: req.body.body },\n actor,\n );\n return reply.code(201).send(comment);\n },\n });\n\n r.route({\n method: 'PATCH',\n url: '/api/member/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'editComment',\n tags,\n summary: 'Revise your own comment',\n description:\n 'Author only; not even a moderator may revise it. Discussion must be open where the comment is, and a removed comment cannot be revised.',\n params: CommentIdParam,\n body: EditComment,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.edit(req.params.commentId, req.body.body, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/comments/:commentId',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'deleteComment',\n tags,\n summary: 'Delete your comment, or one you moderate',\n description:\n 'The author can delete their own comment; anyone else needs `communities:moderate` on the community for a post comment, or `courses:moderate` on the course for a lesson comment. The comment is marked removed and keeps its place in the thread. Deleting a removed comment returns it unchanged.',\n params: CommentIdParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.remove(req.params.commentId, 'any', actor);\n },\n });\n\n r.route({\n method: 'PUT',\n url: '/api/member/comments/:commentId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'setCommentReaction',\n tags,\n summary: 'Set your reaction on a comment',\n description:\n 'For anyone who can read the comment, while reactions are on where it is (403 otherwise). Replaces any reaction you already had and returns the new counts.',\n params: CommentIdParam,\n body: SetReaction,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.setReaction(req.params.commentId, req.body.emoji, actor);\n },\n });\n\n r.route({\n method: 'DELETE',\n url: '/api/member/comments/:commentId/reaction',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'clearCommentReaction',\n tags,\n summary: 'Withdraw your reaction from a comment',\n description: 'For anyone who can read the comment. Returns the new counts.',\n params: CommentIdParam,\n response: { 200: Reactions, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.setReaction(req.params.commentId, null, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/comments/:commentId/reports',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'reportComment',\n tags,\n summary: 'Flag a comment for the moderators',\n description:\n 'For anyone who can read the comment. Flagging it again while your flag is open returns that flag.',\n params: CommentIdParam,\n body: ReportComment,\n response: { 200: CommentReport, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.reportComment(req.params.commentId, req.body.reason, actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/comments/:commentId/flags/resolve',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'resolveCommentFlags',\n tags,\n summary: 'Dismiss the open flags on a comment in a space you moderate',\n description:\n 'Needs `courses:moderate` on the course for an activity comment, or `communities:moderate` on the community for a post comment. Closes every open flag and leaves the comment up.',\n params: CommentIdParam,\n body: ResolveCommentFlags,\n response: { 204: z.void(), ...refusals(403, 404) },\n },\n handler: async (req, reply) => {\n const actor = await resolveActor(container, req);\n await discussion.resolveReports(req.params.commentId, 'any', actor);\n return reply.code(204).send();\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/comments/:commentId/restore',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'restoreMemberComment',\n tags,\n summary: 'Restore a removed comment where you moderate',\n description:\n 'Needs `courses:moderate` on the course for an activity comment, or `communities:moderate` on the community for a post comment. Publishes the comment again; a comment that is not removed is returned unchanged.',\n params: CommentIdParam,\n response: { 200: CommentView, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.restore(req.params.commentId, 'any', actor);\n },\n });\n\n r.route({\n method: 'POST',\n url: '/api/member/comments/:commentId/approve',\n preHandler: app.requireOrgSession,\n schema: {\n operationId: 'approveMemberComment',\n tags,\n summary: 'Publish a comment awaiting review where you moderate',\n description:\n 'Needs `courses:moderate` on the course for an activity comment, or `communities:moderate` on the community for a post comment. 403 when the comment is not awaiting review.',\n params: CommentIdParam,\n response: { 200: Comment, ...refusals(403, 404) },\n },\n handler: async (req) => {\n const actor = await resolveActor(container, req);\n return discussion.approve(req.params.commentId, actor);\n },\n });\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { PageQuery, refusals } from "../../schemas/shared.js";
2
- import { CommunityHome, CommunityHomeSpace, CommunitySpaceParam, DirectoryPage, DirectoryQuery, SpaceMember, SpaceMemberParam, SpaceMembersPage } from "../../schemas/community.js";
2
+ import { resolveActor } from "../../scope.js";
3
+ import { SetMute } from "../../schemas/discussion.js";
4
+ import { CommunityHome, CommunityHomeSpace, CommunitySpaceParam, DirectoryPage, DirectoryQuery, SpaceIdParam, SpaceMember, SpaceMemberParam, SpaceMembersPage } from "../../schemas/community.js";
3
5
  import { CommunityParam, OrgUserBody } from "../../schemas/chats.js";
4
6
  import "../../schemas/index.js";
5
- import { actorOf } from "./actor.js";
6
- import { NotFoundError } from "@headless-lms/core/shared/errors";
7
+ import { z } from "zod";
7
8
  //#region src/http/routes/member/community.ts
8
9
  async function memberCommunityRoutes(app, container) {
9
10
  const r = app.withTypeProvider();
@@ -16,7 +17,7 @@ async function memberCommunityRoutes(app, container) {
16
17
  operationId: "getMemberCommunityHome",
17
18
  tags,
18
19
  summary: "The spaces a member can see in a community",
19
- description: "For a member of the community. Lists its spaces in order. Spaces the viewer can open come back in full; spaces they can see but not open come back locked, with only their name, count, rule and the viewer's standing. A by-invite space the viewer is not invited to or a member of is left out. 404 when the viewer is not a member.",
20
+ description: "For a member of the community, through a grant or `communities:read` on it. Lists its spaces in order. Spaces the viewer can open come back in full; spaces they can see but not open come back locked, with only their name, count, rule and the viewer's standing. A by-invite space the viewer is not invited to or a member of is left out, unless the viewer opens every space. `viewer` carries the viewer's permissions on the community, whether they open every space, and their team flag and role name. 404 when the viewer is not a member.",
20
21
  params: CommunityParam,
21
22
  response: {
22
23
  200: CommunityHome,
@@ -24,10 +25,8 @@ async function memberCommunityRoutes(app, container) {
24
25
  }
25
26
  },
26
27
  handler: async (req) => {
27
- const actor = await actorOf(container, req.orgId, req.userId);
28
- const home = await container.reporting.member.communityHome(req.orgId, actor, req.params.communityId);
29
- if (!home) throw new NotFoundError("Community", req.params.communityId);
30
- return home;
28
+ const actor = await resolveActor(container, req);
29
+ return container.reporting.member.communityHome(req.params.communityId, actor);
31
30
  }
32
31
  });
33
32
  r.route({
@@ -46,10 +45,8 @@ async function memberCommunityRoutes(app, container) {
46
45
  }
47
46
  },
48
47
  handler: async (req) => {
49
- const actor = await actorOf(container, req.orgId, req.userId);
50
- const space = await container.reporting.member.communitySpace(req.orgId, actor, req.params.communityId, req.params.spaceId);
51
- if (!space) throw new NotFoundError("Space", req.params.spaceId);
52
- return space;
48
+ const actor = await resolveActor(container, req);
49
+ return container.reporting.member.communitySpace(req.params.communityId, req.params.spaceId, actor);
53
50
  }
54
51
  });
55
52
  r.route({
@@ -60,7 +57,7 @@ async function memberCommunityRoutes(app, container) {
60
57
  operationId: "getMemberCommunityDirectory",
61
58
  tags,
62
59
  summary: "People in a community",
63
- description: "For a member of the community: everyone who holds it, with their public profile. 404 when the viewer is not a member.",
60
+ description: "For a member of the community: everyone who holds it or holds `communities:read` on it, with their public profile, whether they are team here and under which role name, and whether they open every space. 404 when the viewer is not a member.",
64
61
  params: CommunityParam,
65
62
  querystring: DirectoryQuery,
66
63
  response: {
@@ -69,10 +66,8 @@ async function memberCommunityRoutes(app, container) {
69
66
  }
70
67
  },
71
68
  handler: async (req) => {
72
- const actor = await actorOf(container, req.orgId, req.userId);
73
- const page = await container.reporting.member.directory(req.orgId, actor, req.params.communityId, req.query);
74
- if (!page) throw new NotFoundError("Community", req.params.communityId);
75
- return page;
69
+ const actor = await resolveActor(container, req);
70
+ return container.reporting.member.directory(req.params.communityId, req.query, actor);
76
71
  }
77
72
  });
78
73
  r.route({
@@ -83,7 +78,7 @@ async function memberCommunityRoutes(app, container) {
83
78
  operationId: "listSpaceMembers",
84
79
  tags,
85
80
  summary: "List the members of a space",
86
- description: "For anyone who can open the space. Members see the active members; moderators also see open requests and invitations.",
81
+ description: "For anyone who can open the space. Members see the active members; holders of `communities:moderate` on the community also see open requests and invitations. Who opens every space holds no place, and is left out.",
87
82
  params: CommunitySpaceParam,
88
83
  querystring: PageQuery,
89
84
  response: {
@@ -92,8 +87,8 @@ async function memberCommunityRoutes(app, container) {
92
87
  }
93
88
  },
94
89
  handler: async (req) => {
95
- const actor = await actorOf(container, req.orgId, req.userId);
96
- return container.entitlements.listSpaceMembers(req.orgId, actor, req.params.communityId, req.params.spaceId, req.query);
90
+ const actor = await resolveActor(container, req);
91
+ return container.entitlements.listSpaceMembers(req.params.communityId, req.params.spaceId, req.query, actor);
97
92
  }
98
93
  });
99
94
  r.route({
@@ -104,7 +99,7 @@ async function memberCommunityRoutes(app, container) {
104
99
  operationId: "requestSpaceMembership",
105
100
  tags,
106
101
  summary: "Ask to join a space",
107
- description: "For a member of the community who is not staff. Opens a request on a `by_request` space for a moderator to approve. 409 when the space is not `by_request` or you are already placed in it.",
102
+ description: "For a member of the community who does not open every space. Opens a request on a `by_request` space for a moderator to approve. 409 when the space is not `by_request` or you are already placed in it.",
108
103
  params: CommunitySpaceParam,
109
104
  response: {
110
105
  201: SpaceMember,
@@ -112,8 +107,8 @@ async function memberCommunityRoutes(app, container) {
112
107
  }
113
108
  },
114
109
  handler: async (req, reply) => {
115
- const actor = await actorOf(container, req.orgId, req.userId);
116
- const member = await container.entitlements.requestJoin(req.orgId, actor, req.params.communityId, req.params.spaceId);
110
+ const actor = await resolveActor(container, req);
111
+ const member = await container.entitlements.requestJoin(req.params.communityId, req.params.spaceId, actor);
117
112
  return reply.code(201).send(member);
118
113
  }
119
114
  });
@@ -125,7 +120,7 @@ async function memberCommunityRoutes(app, container) {
125
120
  operationId: "inviteSpaceMember",
126
121
  tags,
127
122
  summary: "Invite a person into a space you moderate",
128
- description: "For a moderator who is a member of the community. Invites another member into a `by_invite` space; they join when they accept. 403 when the person is staff or does not hold the community; 409 when the space is not invite-only or the person is already placed.",
123
+ description: `Requires \`communities:moderate\`. On the community, as its member. Invites another member into a \`by_invite\` space; they join when they accept. 403 when the person opens every space or is not a member of the community; 409 when the space is not invite-only or the person is already placed.`,
129
124
  params: CommunitySpaceParam,
130
125
  body: OrgUserBody,
131
126
  response: {
@@ -134,8 +129,8 @@ async function memberCommunityRoutes(app, container) {
134
129
  }
135
130
  },
136
131
  handler: async (req, reply) => {
137
- const actor = await actorOf(container, req.orgId, req.userId);
138
- const member = await container.entitlements.invite(req.orgId, actor, req.params.communityId, req.params.spaceId, req.body.orgUserId);
132
+ const actor = await resolveActor(container, req);
133
+ const member = await container.entitlements.invite(req.params.communityId, req.params.spaceId, req.body.orgUserId, actor);
139
134
  return reply.code(201).send(member);
140
135
  }
141
136
  });
@@ -147,7 +142,7 @@ async function memberCommunityRoutes(app, container) {
147
142
  operationId: "addSpaceMember",
148
143
  tags,
149
144
  summary: "Add a person to a space you moderate",
150
- description: "For a moderator who is a member of the community. Places another member straight into the space, with no request or invitation. 409 when the space is open to all members, the person is already placed, or the space is full.",
145
+ description: `Requires \`communities:moderate\`. On the community, as its member. Places another member straight into the space, with no request or invitation. 403 when the person opens every space; 409 when the space is open to all members, the person is already placed, or the space is full.`,
151
146
  params: CommunitySpaceParam,
152
147
  body: OrgUserBody,
153
148
  response: {
@@ -156,20 +151,20 @@ async function memberCommunityRoutes(app, container) {
156
151
  }
157
152
  },
158
153
  handler: async (req, reply) => {
159
- const actor = await actorOf(container, req.orgId, req.userId);
160
- const member = await container.entitlements.addSpaceMember(req.orgId, actor, req.params.communityId, req.params.spaceId, req.body.orgUserId);
154
+ const actor = await resolveActor(container, req);
155
+ const member = await container.entitlements.addSpaceMember(req.params.communityId, req.params.spaceId, req.body.orgUserId, actor);
161
156
  return reply.code(201).send(member);
162
157
  }
163
158
  });
164
159
  r.route({
165
160
  method: "POST",
166
- url: "/api/member/communities/:communityId/spaces/:spaceId/acceptance",
161
+ url: "/api/member/communities/:communityId/spaces/:spaceId/accept",
167
162
  preHandler: app.requireOrgSession,
168
163
  schema: {
169
164
  operationId: "acceptSpaceInvite",
170
165
  tags,
171
166
  summary: "Accept your invitation to a space",
172
- description: "For a member of the community who is not staff. Makes you an active member. 409 when you have no open invitation, are already a member, or the space is full.",
167
+ description: "For a member of the community who does not open every space. Makes you an active member. 409 when you have no open invitation, are already a member, or the space is full.",
173
168
  params: CommunitySpaceParam,
174
169
  response: {
175
170
  200: SpaceMember,
@@ -177,19 +172,19 @@ async function memberCommunityRoutes(app, container) {
177
172
  }
178
173
  },
179
174
  handler: async (req) => {
180
- const actor = await actorOf(container, req.orgId, req.userId);
181
- return container.entitlements.accept(req.orgId, actor, req.params.communityId, req.params.spaceId);
175
+ const actor = await resolveActor(container, req);
176
+ return container.entitlements.accept(req.params.communityId, req.params.spaceId, actor);
182
177
  }
183
178
  });
184
179
  r.route({
185
180
  method: "POST",
186
- url: "/api/member/communities/:communityId/spaces/:spaceId/members/:orgUserId/approval",
181
+ url: "/api/member/communities/:communityId/spaces/:spaceId/members/:orgUserId/approve",
187
182
  preHandler: app.requireOrgSession,
188
183
  schema: {
189
184
  operationId: "approveSpaceRequest",
190
185
  tags,
191
186
  summary: "Approve a person's request to join a space you moderate",
192
- description: "For a moderator who is a member of the community. Makes the requester an active member. 404 when the person has no open request; 409 when the space is full or they are already a member.",
187
+ description: `Requires \`communities:moderate\`. On the community, as its member. Makes the requester an active member. 404 when the person has no open request; 409 when the space is full or they are already a member.`,
193
188
  params: SpaceMemberParam,
194
189
  response: {
195
190
  200: SpaceMember,
@@ -197,8 +192,8 @@ async function memberCommunityRoutes(app, container) {
197
192
  }
198
193
  },
199
194
  handler: async (req) => {
200
- const actor = await actorOf(container, req.orgId, req.userId);
201
- return container.entitlements.approve(req.orgId, actor, req.params.communityId, req.params.spaceId, req.params.orgUserId);
195
+ const actor = await resolveActor(container, req);
196
+ return container.entitlements.approve(req.params.communityId, req.params.spaceId, req.params.orgUserId, actor);
202
197
  }
203
198
  });
204
199
  r.route({
@@ -209,7 +204,7 @@ async function memberCommunityRoutes(app, container) {
209
204
  operationId: "declineSpaceRequest",
210
205
  tags,
211
206
  summary: "Decline a person's request to join a space you moderate",
212
- description: "For a moderator who is a member of the community. Closes the open request. 404 when the person has no open request; 409 when they are already a member.",
207
+ description: `Requires \`communities:moderate\`. On the community, as its member. Closes the open request. 404 when the person has no open request; 409 when they are already a member.`,
213
208
  params: SpaceMemberParam,
214
209
  response: {
215
210
  200: SpaceMember,
@@ -217,19 +212,19 @@ async function memberCommunityRoutes(app, container) {
217
212
  }
218
213
  },
219
214
  handler: async (req) => {
220
- const actor = await actorOf(container, req.orgId, req.userId);
221
- return container.entitlements.decline(req.orgId, actor, req.params.communityId, req.params.spaceId, req.params.orgUserId);
215
+ const actor = await resolveActor(container, req);
216
+ return container.entitlements.decline(req.params.communityId, req.params.spaceId, req.params.orgUserId, actor);
222
217
  }
223
218
  });
224
219
  r.route({
225
220
  method: "POST",
226
- url: "/api/member/communities/:communityId/spaces/:spaceId/refusal",
221
+ url: "/api/member/communities/:communityId/spaces/:spaceId/refuse",
227
222
  preHandler: app.requireOrgSession,
228
223
  schema: {
229
224
  operationId: "refuseSpaceInvite",
230
225
  tags,
231
226
  summary: "Refuse your invitation to a space",
232
- description: "For a member of the community who is not staff. 409 when you have no open invitation or are already a member.",
227
+ description: "For a member of the community who does not open every space. 409 when you have no open invitation or are already a member.",
233
228
  params: CommunitySpaceParam,
234
229
  response: {
235
230
  200: SpaceMember,
@@ -237,8 +232,8 @@ async function memberCommunityRoutes(app, container) {
237
232
  }
238
233
  },
239
234
  handler: async (req) => {
240
- const actor = await actorOf(container, req.orgId, req.userId);
241
- return container.entitlements.refuse(req.orgId, actor, req.params.communityId, req.params.spaceId);
235
+ const actor = await resolveActor(container, req);
236
+ return container.entitlements.refuse(req.params.communityId, req.params.spaceId, actor);
242
237
  }
243
238
  });
244
239
  r.route({
@@ -249,7 +244,7 @@ async function memberCommunityRoutes(app, container) {
249
244
  operationId: "leaveSpace",
250
245
  tags,
251
246
  summary: "Leave a space",
252
- description: "For a member of the community who is not staff. 409 when you are not a member of the space.",
247
+ description: "For a member of the community who does not open every space. 409 when you are not a member of the space.",
253
248
  params: CommunitySpaceParam,
254
249
  response: {
255
250
  200: SpaceMember,
@@ -257,19 +252,19 @@ async function memberCommunityRoutes(app, container) {
257
252
  }
258
253
  },
259
254
  handler: async (req) => {
260
- const actor = await actorOf(container, req.orgId, req.userId);
261
- return container.entitlements.leave(req.orgId, actor, req.params.communityId, req.params.spaceId);
255
+ const actor = await resolveActor(container, req);
256
+ return container.entitlements.leave(req.params.communityId, req.params.spaceId, actor);
262
257
  }
263
258
  });
264
259
  r.route({
265
260
  method: "POST",
266
- url: "/api/member/communities/:communityId/spaces/:spaceId/members/:orgUserId/removal",
261
+ url: "/api/member/communities/:communityId/spaces/:spaceId/members/:orgUserId/remove",
267
262
  preHandler: app.requireOrgSession,
268
263
  schema: {
269
264
  operationId: "removeSpaceMember",
270
265
  tags,
271
266
  summary: "Remove a person from a space you moderate",
272
- description: "For a moderator who is a member of the community. Revokes the placement, whether it is a membership, a request or an invitation. 409 when the space is open to all members or the placement is already revoked.",
267
+ description: `Requires \`communities:moderate\`. On the community, as its member. Revokes the placement, whether it is a membership, a request or an invitation. 409 when the space is open to all members or the placement is already revoked.`,
273
268
  params: SpaceMemberParam,
274
269
  response: {
275
270
  200: SpaceMember,
@@ -277,8 +272,30 @@ async function memberCommunityRoutes(app, container) {
277
272
  }
278
273
  },
279
274
  handler: async (req) => {
280
- const actor = await actorOf(container, req.orgId, req.userId);
281
- return container.entitlements.remove(req.orgId, actor, req.params.communityId, req.params.spaceId, req.params.orgUserId);
275
+ const actor = await resolveActor(container, req);
276
+ return container.entitlements.remove(req.params.communityId, req.params.spaceId, req.params.orgUserId, actor);
277
+ }
278
+ });
279
+ r.route({
280
+ method: "PUT",
281
+ url: "/api/member/spaces/:spaceId/subscription",
282
+ preHandler: app.requireOrgSession,
283
+ schema: {
284
+ operationId: "setSpaceSubscription",
285
+ tags,
286
+ summary: "Mute a space, or clear your mute",
287
+ description: "For anyone who can open the space. `muted` stops its activity notifying you, until `mutedUntil` when given; mentions, actions on you and moderation work still notify. `none` clears the mute.",
288
+ params: SpaceIdParam,
289
+ body: SetMute,
290
+ response: {
291
+ 204: z.void(),
292
+ ...refusals(403, 404)
293
+ }
294
+ },
295
+ handler: async (req, reply) => {
296
+ const actor = await resolveActor(container, req);
297
+ await container.subscriptions.setSpace(req.params.spaceId, req.body, actor);
298
+ return reply.code(204).send();
282
299
  }
283
300
  });
284
301
  }