@access-mcp/announcements 0.3.0 → 0.3.1
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.
- package/dist/server.d.ts +19 -5
- package/dist/server.js +198 -158
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/server.integration.test.ts +296 -8
- package/src/server.test.ts +334 -109
- package/src/server.ts +361 -241
- package/vitest.integration.config.ts +1 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseAccessServer, DrupalAuthProvider } from "@access-mcp/shared";
|
|
1
|
+
import { BaseAccessServer, DrupalAuthProvider, getRequestContext, } from "@access-mcp/shared";
|
|
2
2
|
import { createRequire } from "module";
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
const { version } = require("../package.json");
|
|
@@ -11,11 +11,17 @@ export class AnnouncementsServer extends BaseAccessServer {
|
|
|
11
11
|
tagCacheExpiry;
|
|
12
12
|
static TAG_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
13
|
constructor() {
|
|
14
|
-
super("access-announcements", version, "https://support.access-ci.org"
|
|
14
|
+
super("access-announcements", version, "https://support.access-ci.org", {
|
|
15
|
+
requireApiKey: true,
|
|
16
|
+
});
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Get or create the Drupal auth provider for JSON:API write operations.
|
|
18
20
|
* Requires DRUPAL_API_URL, DRUPAL_USERNAME, and DRUPAL_PASSWORD env vars.
|
|
21
|
+
*
|
|
22
|
+
* Acting user is determined in priority order:
|
|
23
|
+
* 1. X-Acting-User header (from request context)
|
|
24
|
+
* 2. ACTING_USER environment variable (fallback)
|
|
19
25
|
*/
|
|
20
26
|
getDrupalAuth() {
|
|
21
27
|
if (!this.drupalAuth) {
|
|
@@ -27,6 +33,12 @@ export class AnnouncementsServer extends BaseAccessServer {
|
|
|
27
33
|
}
|
|
28
34
|
this.drupalAuth = new DrupalAuthProvider(baseUrl, username, password);
|
|
29
35
|
}
|
|
36
|
+
// Update acting user from request context (takes priority) or env var (fallback)
|
|
37
|
+
const context = getRequestContext();
|
|
38
|
+
const actingUser = context?.actingUser || process.env.ACTING_USER;
|
|
39
|
+
if (actingUser) {
|
|
40
|
+
this.drupalAuth.setActingUser(actingUser);
|
|
41
|
+
}
|
|
30
42
|
return this.drupalAuth;
|
|
31
43
|
}
|
|
32
44
|
getTools() {
|
|
@@ -40,24 +52,24 @@ export class AnnouncementsServer extends BaseAccessServer {
|
|
|
40
52
|
properties: {
|
|
41
53
|
query: {
|
|
42
54
|
type: "string",
|
|
43
|
-
description: "Full-text search across title, body, and summary"
|
|
55
|
+
description: "Full-text search across title, body, and summary",
|
|
44
56
|
},
|
|
45
57
|
tags: {
|
|
46
58
|
type: "string",
|
|
47
|
-
description: "Filter by topics: gpu, ml, hpc"
|
|
59
|
+
description: "Filter by topics: gpu, ml, hpc",
|
|
48
60
|
},
|
|
49
61
|
date: {
|
|
50
62
|
type: "string",
|
|
51
63
|
description: "Time period filter: today, this_week (last 7 days), this_month (last 30 days), past (last year)",
|
|
52
|
-
enum: ["today", "this_week", "this_month", "past"]
|
|
64
|
+
enum: ["today", "this_week", "this_month", "past"],
|
|
53
65
|
},
|
|
54
66
|
limit: {
|
|
55
67
|
type: "number",
|
|
56
68
|
description: "Max results (default: 25)",
|
|
57
|
-
default: 25
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
69
|
+
default: 25,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
61
73
|
},
|
|
62
74
|
// CRUD operations (new)
|
|
63
75
|
{
|
|
@@ -118,51 +130,51 @@ ALWAYS display the edit_url to the user so they can review their draft in Drupal
|
|
|
118
130
|
properties: {
|
|
119
131
|
title: {
|
|
120
132
|
type: "string",
|
|
121
|
-
description: "Announcement title - clear and specific, under 100 characters"
|
|
133
|
+
description: "Announcement title - clear and specific, under 100 characters",
|
|
122
134
|
},
|
|
123
135
|
body: {
|
|
124
136
|
type: "string",
|
|
125
|
-
description: "Full announcement content. HTML allowed (basic_html format: <p>, <a>, <strong>, <em>, <ul>, <li>)"
|
|
137
|
+
description: "Full announcement content. HTML allowed (basic_html format: <p>, <a>, <strong>, <em>, <ul>, <li>)",
|
|
126
138
|
},
|
|
127
139
|
summary: {
|
|
128
140
|
type: "string",
|
|
129
|
-
description: "Brief teaser (1-2 sentences) shown in announcement listings. Required."
|
|
141
|
+
description: "Brief teaser (1-2 sentences) shown in announcement listings. Required.",
|
|
130
142
|
},
|
|
131
143
|
published_date: {
|
|
132
144
|
type: "string",
|
|
133
|
-
description: "When to display (YYYY-MM-DD). Defaults to today."
|
|
145
|
+
description: "When to display (YYYY-MM-DD). Defaults to today.",
|
|
134
146
|
},
|
|
135
147
|
tags: {
|
|
136
148
|
type: "array",
|
|
137
149
|
items: { type: "string" },
|
|
138
|
-
description: "Tag names (1-6 required for publication). Use list_available_tags to find valid tags."
|
|
150
|
+
description: "Tag names (1-6 required for publication). Use list_available_tags to find valid tags.",
|
|
139
151
|
},
|
|
140
152
|
affiliation: {
|
|
141
153
|
type: "string",
|
|
142
154
|
description: "Source of announcement",
|
|
143
|
-
enum: ["ACCESS Collaboration", "Community"]
|
|
155
|
+
enum: ["ACCESS Collaboration", "Community"],
|
|
144
156
|
},
|
|
145
157
|
affinity_group: {
|
|
146
158
|
type: "string",
|
|
147
|
-
description: "Affinity group name or UUID. User must be coordinator of the group. Use list_affinity_groups to find valid groups."
|
|
159
|
+
description: "Affinity group name or UUID. User must be coordinator of the group. Use list_affinity_groups to find valid groups.",
|
|
148
160
|
},
|
|
149
161
|
external_link: {
|
|
150
162
|
type: "object",
|
|
151
163
|
description: "External link related to this announcement",
|
|
152
164
|
properties: {
|
|
153
165
|
uri: { type: "string", description: "URL (must include https://)" },
|
|
154
|
-
title: { type: "string", description: "Link text to display" }
|
|
166
|
+
title: { type: "string", description: "Link text to display" },
|
|
155
167
|
},
|
|
156
|
-
required: ["uri"]
|
|
168
|
+
required: ["uri"],
|
|
157
169
|
},
|
|
158
170
|
where_to_share: {
|
|
159
171
|
type: "array",
|
|
160
172
|
items: { type: "string" },
|
|
161
|
-
description: "Where to share the announcement. Defaults to 'Announcements page' + 'Bi-Weekly Digest'. Options: 'Announcements page', 'Bi-Weekly Digest', 'Affinity Group page', 'Email to Affinity Group'"
|
|
162
|
-
}
|
|
173
|
+
description: "Where to share the announcement. Defaults to 'Announcements page' + 'Bi-Weekly Digest'. Options: 'Announcements page', 'Bi-Weekly Digest', 'Affinity Group page', 'Email to Affinity Group'",
|
|
174
|
+
},
|
|
163
175
|
},
|
|
164
|
-
required: ["title", "body", "summary"]
|
|
165
|
-
}
|
|
176
|
+
required: ["title", "body", "summary"],
|
|
177
|
+
},
|
|
166
178
|
},
|
|
167
179
|
{
|
|
168
180
|
name: "update_announcement",
|
|
@@ -181,50 +193,50 @@ ALWAYS display the edit_url to the user so they can review changes in Drupal.`,
|
|
|
181
193
|
properties: {
|
|
182
194
|
uuid: {
|
|
183
195
|
type: "string",
|
|
184
|
-
description: "UUID of the announcement (get from get_my_announcements)"
|
|
196
|
+
description: "UUID of the announcement (get from get_my_announcements)",
|
|
185
197
|
},
|
|
186
198
|
title: {
|
|
187
199
|
type: "string",
|
|
188
|
-
description: "New title (only if changing)"
|
|
200
|
+
description: "New title (only if changing)",
|
|
189
201
|
},
|
|
190
202
|
body: {
|
|
191
203
|
type: "string",
|
|
192
|
-
description: "New body content (only if changing)"
|
|
204
|
+
description: "New body content (only if changing)",
|
|
193
205
|
},
|
|
194
206
|
summary: {
|
|
195
207
|
type: "string",
|
|
196
|
-
description: "New summary (only if changing)"
|
|
208
|
+
description: "New summary (only if changing)",
|
|
197
209
|
},
|
|
198
210
|
published_date: {
|
|
199
211
|
type: "string",
|
|
200
|
-
description: "New publication date YYYY-MM-DD (only if changing)"
|
|
212
|
+
description: "New publication date YYYY-MM-DD (only if changing)",
|
|
201
213
|
},
|
|
202
214
|
tags: {
|
|
203
215
|
type: "array",
|
|
204
216
|
items: { type: "string" },
|
|
205
|
-
description: "New tags - replaces ALL existing tags (only if changing)"
|
|
217
|
+
description: "New tags - replaces ALL existing tags (only if changing)",
|
|
206
218
|
},
|
|
207
219
|
affinity_group: {
|
|
208
220
|
type: "string",
|
|
209
|
-
description: "Affinity group name or UUID. User must be coordinator."
|
|
221
|
+
description: "Affinity group name or UUID. User must be coordinator.",
|
|
210
222
|
},
|
|
211
223
|
external_link: {
|
|
212
224
|
type: "object",
|
|
213
225
|
description: "External link related to this announcement",
|
|
214
226
|
properties: {
|
|
215
227
|
uri: { type: "string", description: "URL (must include https://)" },
|
|
216
|
-
title: { type: "string", description: "Link text to display" }
|
|
228
|
+
title: { type: "string", description: "Link text to display" },
|
|
217
229
|
},
|
|
218
|
-
required: ["uri"]
|
|
230
|
+
required: ["uri"],
|
|
219
231
|
},
|
|
220
232
|
where_to_share: {
|
|
221
233
|
type: "array",
|
|
222
234
|
items: { type: "string" },
|
|
223
|
-
description: "Where to share the announcement. Options: 'Announcements page', 'Bi-Weekly Digest', 'Affinity Group page', 'Email to Affinity Group'"
|
|
224
|
-
}
|
|
235
|
+
description: "Where to share the announcement. Options: 'Announcements page', 'Bi-Weekly Digest', 'Affinity Group page', 'Email to Affinity Group'",
|
|
236
|
+
},
|
|
225
237
|
},
|
|
226
|
-
required: ["uuid"]
|
|
227
|
-
}
|
|
238
|
+
required: ["uuid"],
|
|
239
|
+
},
|
|
228
240
|
},
|
|
229
241
|
{
|
|
230
242
|
name: "delete_announcement",
|
|
@@ -248,15 +260,15 @@ Returns: {success, uuid}`,
|
|
|
248
260
|
properties: {
|
|
249
261
|
uuid: {
|
|
250
262
|
type: "string",
|
|
251
|
-
description: "UUID of the announcement to delete"
|
|
263
|
+
description: "UUID of the announcement to delete",
|
|
252
264
|
},
|
|
253
265
|
confirmed: {
|
|
254
266
|
type: "boolean",
|
|
255
|
-
description: "Set to true only after user has explicitly confirmed deletion of THIS specific announcement. Required."
|
|
256
|
-
}
|
|
267
|
+
description: "Set to true only after user has explicitly confirmed deletion of THIS specific announcement. Required.",
|
|
268
|
+
},
|
|
257
269
|
},
|
|
258
|
-
required: ["uuid", "confirmed"]
|
|
259
|
-
}
|
|
270
|
+
required: ["uuid", "confirmed"],
|
|
271
|
+
},
|
|
260
272
|
},
|
|
261
273
|
{
|
|
262
274
|
name: "get_my_announcements",
|
|
@@ -275,10 +287,10 @@ Use this to:
|
|
|
275
287
|
limit: {
|
|
276
288
|
type: "number",
|
|
277
289
|
description: "Max results (default: 25)",
|
|
278
|
-
default: 25
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
290
|
+
default: 25,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
282
294
|
},
|
|
283
295
|
{
|
|
284
296
|
name: "get_announcement_context",
|
|
@@ -294,9 +306,9 @@ Returns:
|
|
|
294
306
|
Use this to tailor the announcement creation conversation based on the user's role.`,
|
|
295
307
|
inputSchema: {
|
|
296
308
|
type: "object",
|
|
297
|
-
properties: {}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
309
|
+
properties: {},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
300
312
|
];
|
|
301
313
|
}
|
|
302
314
|
getResources() {
|
|
@@ -305,8 +317,8 @@ Use this to tailor the announcement creation conversation based on the user's ro
|
|
|
305
317
|
uri: "accessci://announcements",
|
|
306
318
|
name: "ACCESS Support Announcements",
|
|
307
319
|
description: "Recent announcements and notifications from ACCESS support",
|
|
308
|
-
mimeType: "application/json"
|
|
309
|
-
}
|
|
320
|
+
mimeType: "application/json",
|
|
321
|
+
},
|
|
310
322
|
];
|
|
311
323
|
}
|
|
312
324
|
getPrompts() {
|
|
@@ -468,9 +480,9 @@ Which would you like to do?`,
|
|
|
468
480
|
{
|
|
469
481
|
uri,
|
|
470
482
|
mimeType: "application/json",
|
|
471
|
-
text: JSON.stringify(announcements, null, 2)
|
|
472
|
-
}
|
|
473
|
-
]
|
|
483
|
+
text: JSON.stringify(announcements, null, 2),
|
|
484
|
+
},
|
|
485
|
+
],
|
|
474
486
|
};
|
|
475
487
|
}
|
|
476
488
|
catch (error) {
|
|
@@ -480,9 +492,9 @@ Which would you like to do?`,
|
|
|
480
492
|
{
|
|
481
493
|
uri,
|
|
482
494
|
mimeType: "text/plain",
|
|
483
|
-
text: `Error loading announcements: ${message}
|
|
484
|
-
}
|
|
485
|
-
]
|
|
495
|
+
text: `Error loading announcements: ${message}`,
|
|
496
|
+
},
|
|
497
|
+
],
|
|
486
498
|
};
|
|
487
499
|
}
|
|
488
500
|
}
|
|
@@ -514,7 +526,7 @@ Which would you like to do?`,
|
|
|
514
526
|
today: { start: "today" },
|
|
515
527
|
this_week: { start: "-1 week", end: "now" },
|
|
516
528
|
this_month: { start: "-1 month", end: "now" },
|
|
517
|
-
past: { start: "-1 year", end: "now" }
|
|
529
|
+
past: { start: "-1 year", end: "now" },
|
|
518
530
|
};
|
|
519
531
|
if (filters.date && dateMap[filters.date]) {
|
|
520
532
|
const dateRange = dateMap[filters.date];
|
|
@@ -535,61 +547,73 @@ Which would you like to do?`,
|
|
|
535
547
|
return this.enhanceAnnouncements(announcements);
|
|
536
548
|
}
|
|
537
549
|
enhanceAnnouncements(rawAnnouncements) {
|
|
538
|
-
return rawAnnouncements.map(announcement => ({
|
|
550
|
+
return rawAnnouncements.map((announcement) => ({
|
|
539
551
|
...announcement,
|
|
540
552
|
// Tags come as comma-separated string from public API, convert to array
|
|
541
553
|
tags: Array.isArray(announcement.tags)
|
|
542
554
|
? announcement.tags
|
|
543
|
-
:
|
|
544
|
-
? announcement.tags.split(
|
|
545
|
-
: []
|
|
546
|
-
summary: announcement.summary ||
|
|
555
|
+
: typeof announcement.tags === "string" && announcement.tags.trim()
|
|
556
|
+
? announcement.tags.split(",").map((t) => t.trim())
|
|
557
|
+
: [],
|
|
558
|
+
summary: announcement.summary ||
|
|
559
|
+
(announcement.body
|
|
560
|
+
? announcement.body.replace(/<[^>]*>/g, "").substring(0, 200) + "..."
|
|
561
|
+
: ""),
|
|
547
562
|
}));
|
|
548
563
|
}
|
|
549
564
|
async searchAnnouncements(filters) {
|
|
550
565
|
const announcements = await this.fetchAnnouncements(filters);
|
|
551
566
|
const limited = filters.limit ? announcements.slice(0, filters.limit) : announcements;
|
|
552
567
|
return {
|
|
553
|
-
content: [
|
|
568
|
+
content: [
|
|
569
|
+
{
|
|
554
570
|
type: "text",
|
|
555
571
|
text: JSON.stringify({
|
|
556
572
|
total: announcements.length,
|
|
557
|
-
items: limited
|
|
558
|
-
})
|
|
559
|
-
}
|
|
573
|
+
items: limited,
|
|
574
|
+
}),
|
|
575
|
+
},
|
|
576
|
+
],
|
|
560
577
|
};
|
|
561
578
|
}
|
|
562
579
|
// ============================================================================
|
|
563
580
|
// CRUD Operations (new)
|
|
564
581
|
// ============================================================================
|
|
565
582
|
/**
|
|
566
|
-
* Get the acting user's
|
|
567
|
-
*
|
|
583
|
+
* Get the acting user's ACCESS ID for content attribution.
|
|
584
|
+
*
|
|
585
|
+
* Priority order:
|
|
586
|
+
* 1. X-Acting-User header (from request context)
|
|
587
|
+
* 2. ACTING_USER environment variable (fallback)
|
|
588
|
+
*
|
|
589
|
+
* Returns the ACCESS ID (e.g., "username@access-ci.org")
|
|
568
590
|
*/
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
591
|
+
getActingUserAccessId() {
|
|
592
|
+
// Try request context first
|
|
593
|
+
const context = getRequestContext();
|
|
594
|
+
if (context?.actingUser) {
|
|
595
|
+
return context.actingUser;
|
|
596
|
+
}
|
|
597
|
+
// Fall back to environment variable
|
|
598
|
+
const envUser = process.env.ACTING_USER;
|
|
599
|
+
if (envUser) {
|
|
600
|
+
return envUser;
|
|
576
601
|
}
|
|
577
602
|
throw new Error("Cannot create announcement: No acting user specified.\n\n" +
|
|
578
|
-
"
|
|
579
|
-
"
|
|
603
|
+
"Either set the X-Acting-User header or the ACTING_USER environment variable " +
|
|
604
|
+
"to the ACCESS ID (e.g., username@access-ci.org) of the person creating the announcement.");
|
|
580
605
|
}
|
|
581
606
|
/**
|
|
582
607
|
* Create a new announcement via Drupal JSON:API
|
|
608
|
+
*
|
|
609
|
+
* The X-Acting-User header (set by DrupalAuthProvider) tells Drupal which
|
|
610
|
+
* ACCESS user is creating the content. Drupal handles user resolution.
|
|
583
611
|
*/
|
|
584
612
|
async createAnnouncement(args) {
|
|
585
613
|
const auth = this.getDrupalAuth();
|
|
586
|
-
// Get the acting user - required for proper attribution
|
|
587
|
-
const actingUserUid = this.getActingUserUid();
|
|
588
|
-
const userUuid = await this.getUserUuidByUid(actingUserUid);
|
|
589
|
-
if (!userUuid) {
|
|
590
|
-
throw new Error(`Could not find user with UID ${actingUserUid}. Verify the ACTING_USER_UID is correct.`);
|
|
591
|
-
}
|
|
592
614
|
// Build the JSON:API request body
|
|
615
|
+
// Note: We don't set the uid relationship - Drupal handles author attribution
|
|
616
|
+
// based on the X-Acting-User header sent with the request
|
|
593
617
|
const requestBody = {
|
|
594
618
|
data: {
|
|
595
619
|
type: "node--access_news",
|
|
@@ -598,21 +622,14 @@ Which would you like to do?`,
|
|
|
598
622
|
moderation_state: "draft", // Use content moderation workflow
|
|
599
623
|
body: {
|
|
600
624
|
value: args.body,
|
|
601
|
-
format: "basic_html"
|
|
602
|
-
}
|
|
625
|
+
format: "basic_html",
|
|
626
|
+
},
|
|
603
627
|
},
|
|
604
|
-
relationships: {
|
|
605
|
-
|
|
606
|
-
data: {
|
|
607
|
-
type: "user--user",
|
|
608
|
-
id: userUuid
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
628
|
+
relationships: {},
|
|
629
|
+
},
|
|
613
630
|
};
|
|
614
631
|
// Add optional fields
|
|
615
|
-
if (args.summary) {
|
|
632
|
+
if (args.summary && requestBody.data.attributes.body) {
|
|
616
633
|
requestBody.data.attributes.body.summary = args.summary;
|
|
617
634
|
}
|
|
618
635
|
if (args.published_date) {
|
|
@@ -620,7 +637,7 @@ Which would you like to do?`,
|
|
|
620
637
|
}
|
|
621
638
|
else {
|
|
622
639
|
// Default to today
|
|
623
|
-
requestBody.data.attributes.field_published_date = new Date().toISOString().split(
|
|
640
|
+
requestBody.data.attributes.field_published_date = new Date().toISOString().split("T")[0];
|
|
624
641
|
}
|
|
625
642
|
if (args.affiliation) {
|
|
626
643
|
requestBody.data.attributes.field_affiliation = args.affiliation;
|
|
@@ -630,10 +647,10 @@ Which would you like to do?`,
|
|
|
630
647
|
const tagUuids = await this.getTagUuidsByName(args.tags);
|
|
631
648
|
if (tagUuids.length > 0) {
|
|
632
649
|
requestBody.data.relationships.field_tags = {
|
|
633
|
-
data: tagUuids.map(uuid => ({
|
|
650
|
+
data: tagUuids.map((uuid) => ({
|
|
634
651
|
type: "taxonomy_term--tags",
|
|
635
|
-
id: uuid
|
|
636
|
-
}))
|
|
652
|
+
id: uuid,
|
|
653
|
+
})),
|
|
637
654
|
};
|
|
638
655
|
}
|
|
639
656
|
}
|
|
@@ -644,8 +661,8 @@ Which would you like to do?`,
|
|
|
644
661
|
requestBody.data.relationships.field_affinity_group_node = {
|
|
645
662
|
data: {
|
|
646
663
|
type: "node--affinity_group",
|
|
647
|
-
id: groupUuid
|
|
648
|
-
}
|
|
664
|
+
id: groupUuid,
|
|
665
|
+
},
|
|
649
666
|
};
|
|
650
667
|
}
|
|
651
668
|
else {
|
|
@@ -656,7 +673,7 @@ Which would you like to do?`,
|
|
|
656
673
|
if (args.external_link) {
|
|
657
674
|
requestBody.data.attributes.field_news_external_link = {
|
|
658
675
|
uri: args.external_link.uri,
|
|
659
|
-
title: args.external_link.title || ""
|
|
676
|
+
title: args.external_link.title || "",
|
|
660
677
|
};
|
|
661
678
|
}
|
|
662
679
|
// Add where to share (defaults handled by Drupal if not provided)
|
|
@@ -665,16 +682,18 @@ Which would you like to do?`,
|
|
|
665
682
|
}
|
|
666
683
|
const result = await auth.post("/jsonapi/node/access_news", requestBody);
|
|
667
684
|
return {
|
|
668
|
-
content: [
|
|
685
|
+
content: [
|
|
686
|
+
{
|
|
669
687
|
type: "text",
|
|
670
688
|
text: JSON.stringify({
|
|
671
689
|
success: true,
|
|
672
690
|
message: "Announcement created (draft status)",
|
|
673
691
|
uuid: result.data?.id,
|
|
674
692
|
title: result.data?.attributes?.title,
|
|
675
|
-
edit_url: `${process.env.DRUPAL_API_URL}/node/${result.data?.attributes?.drupal_internal__nid}/edit
|
|
676
|
-
})
|
|
677
|
-
}
|
|
693
|
+
edit_url: `${process.env.DRUPAL_API_URL}/node/${result.data?.attributes?.drupal_internal__nid}/edit`,
|
|
694
|
+
}),
|
|
695
|
+
},
|
|
696
|
+
],
|
|
678
697
|
};
|
|
679
698
|
}
|
|
680
699
|
/**
|
|
@@ -686,8 +705,8 @@ Which would you like to do?`,
|
|
|
686
705
|
data: {
|
|
687
706
|
type: "node--access_news",
|
|
688
707
|
id: args.uuid,
|
|
689
|
-
attributes: {}
|
|
690
|
-
}
|
|
708
|
+
attributes: {},
|
|
709
|
+
},
|
|
691
710
|
};
|
|
692
711
|
// Add fields to update
|
|
693
712
|
if (args.title) {
|
|
@@ -702,7 +721,7 @@ Which would you like to do?`,
|
|
|
702
721
|
requestBody.data.attributes.body = {
|
|
703
722
|
value: args.body || existingBody,
|
|
704
723
|
format: "basic_html",
|
|
705
|
-
summary: args.summary !== undefined ? args.summary : existingSummary
|
|
724
|
+
summary: args.summary !== undefined ? args.summary : existingSummary,
|
|
706
725
|
};
|
|
707
726
|
}
|
|
708
727
|
// If neither body nor summary provided, don't include body in request at all
|
|
@@ -717,10 +736,10 @@ Which would you like to do?`,
|
|
|
717
736
|
requestBody.data.relationships = {};
|
|
718
737
|
}
|
|
719
738
|
requestBody.data.relationships.field_tags = {
|
|
720
|
-
data: tagUuids.map(uuid => ({
|
|
739
|
+
data: tagUuids.map((uuid) => ({
|
|
721
740
|
type: "taxonomy_term--tags",
|
|
722
|
-
id: uuid
|
|
723
|
-
}))
|
|
741
|
+
id: uuid,
|
|
742
|
+
})),
|
|
724
743
|
};
|
|
725
744
|
}
|
|
726
745
|
}
|
|
@@ -734,8 +753,8 @@ Which would you like to do?`,
|
|
|
734
753
|
requestBody.data.relationships.field_affinity_group_node = {
|
|
735
754
|
data: {
|
|
736
755
|
type: "node--affinity_group",
|
|
737
|
-
id: groupUuid
|
|
738
|
-
}
|
|
756
|
+
id: groupUuid,
|
|
757
|
+
},
|
|
739
758
|
};
|
|
740
759
|
}
|
|
741
760
|
else {
|
|
@@ -746,7 +765,7 @@ Which would you like to do?`,
|
|
|
746
765
|
if (args.external_link) {
|
|
747
766
|
requestBody.data.attributes.field_news_external_link = {
|
|
748
767
|
uri: args.external_link.uri,
|
|
749
|
-
title: args.external_link.title || ""
|
|
768
|
+
title: args.external_link.title || "",
|
|
750
769
|
};
|
|
751
770
|
}
|
|
752
771
|
// Update where to share if provided
|
|
@@ -757,16 +776,18 @@ Which would you like to do?`,
|
|
|
757
776
|
const nid = result.data?.attributes?.drupal_internal__nid;
|
|
758
777
|
const baseUrl = process.env.DRUPAL_API_URL;
|
|
759
778
|
return {
|
|
760
|
-
content: [
|
|
779
|
+
content: [
|
|
780
|
+
{
|
|
761
781
|
type: "text",
|
|
762
782
|
text: JSON.stringify({
|
|
763
783
|
success: true,
|
|
764
784
|
message: "Announcement updated",
|
|
765
785
|
uuid: result.data?.id,
|
|
766
786
|
title: result.data?.attributes?.title,
|
|
767
|
-
edit_url: nid ? `${baseUrl}/node/${nid}/edit` : null
|
|
768
|
-
})
|
|
769
|
-
}
|
|
787
|
+
edit_url: nid ? `${baseUrl}/node/${nid}/edit` : null,
|
|
788
|
+
}),
|
|
789
|
+
},
|
|
790
|
+
],
|
|
770
791
|
};
|
|
771
792
|
}
|
|
772
793
|
/**
|
|
@@ -776,26 +797,30 @@ Which would you like to do?`,
|
|
|
776
797
|
// Enforce confirmation parameter
|
|
777
798
|
if (!args.confirmed) {
|
|
778
799
|
return {
|
|
779
|
-
content: [
|
|
800
|
+
content: [
|
|
801
|
+
{
|
|
780
802
|
type: "text",
|
|
781
803
|
text: JSON.stringify({
|
|
782
804
|
error: "Deletion requires explicit confirmation. You must show the announcement title and status to the user and get explicit confirmation before setting confirmed=true.",
|
|
783
|
-
uuid: args.uuid
|
|
784
|
-
})
|
|
785
|
-
}
|
|
805
|
+
uuid: args.uuid,
|
|
806
|
+
}),
|
|
807
|
+
},
|
|
808
|
+
],
|
|
786
809
|
};
|
|
787
810
|
}
|
|
788
811
|
const auth = this.getDrupalAuth();
|
|
789
812
|
await auth.delete(`/jsonapi/node/access_news/${args.uuid}`);
|
|
790
813
|
return {
|
|
791
|
-
content: [
|
|
814
|
+
content: [
|
|
815
|
+
{
|
|
792
816
|
type: "text",
|
|
793
817
|
text: JSON.stringify({
|
|
794
818
|
success: true,
|
|
795
819
|
message: "Announcement deleted",
|
|
796
|
-
uuid: args.uuid
|
|
797
|
-
})
|
|
798
|
-
}
|
|
820
|
+
uuid: args.uuid,
|
|
821
|
+
}),
|
|
822
|
+
},
|
|
823
|
+
],
|
|
799
824
|
};
|
|
800
825
|
}
|
|
801
826
|
/**
|
|
@@ -803,11 +828,12 @@ Which would you like to do?`,
|
|
|
803
828
|
*/
|
|
804
829
|
async getMyAnnouncements(args) {
|
|
805
830
|
const auth = this.getDrupalAuth();
|
|
806
|
-
// Get the acting user's UUID
|
|
807
|
-
const
|
|
808
|
-
const userUuid = await this.
|
|
831
|
+
// Get the acting user's UUID by looking up their ACCESS ID
|
|
832
|
+
const actingUserAccessId = this.getActingUserAccessId();
|
|
833
|
+
const userUuid = await this.getUserUuidByAccessId(actingUserAccessId);
|
|
809
834
|
if (!userUuid) {
|
|
810
|
-
throw new Error(`Could not find user
|
|
835
|
+
throw new Error(`Could not find Drupal user for ACCESS ID "${actingUserAccessId}". ` +
|
|
836
|
+
`Verify the user exists in Drupal.`);
|
|
811
837
|
}
|
|
812
838
|
const limit = args.limit || 25;
|
|
813
839
|
const result = await auth.get(`/jsonapi/node/access_news?filter[uid.id]=${userUuid}&page[limit]=${limit}&sort=-created`);
|
|
@@ -822,30 +848,35 @@ Which would you like to do?`,
|
|
|
822
848
|
created: item.attributes?.created,
|
|
823
849
|
published_date: item.attributes?.field_published_date,
|
|
824
850
|
summary: item.attributes?.body?.summary ||
|
|
825
|
-
(item.attributes?.body?.value
|
|
826
|
-
item.attributes.body.value.replace(/<[^>]*>/g,
|
|
827
|
-
|
|
851
|
+
(item.attributes?.body?.value
|
|
852
|
+
? item.attributes.body.value.replace(/<[^>]*>/g, "").substring(0, 200) + "..."
|
|
853
|
+
: ""),
|
|
854
|
+
edit_url: nid ? `${baseUrl}/node/${nid}/edit` : null,
|
|
828
855
|
};
|
|
829
856
|
});
|
|
830
857
|
return {
|
|
831
|
-
content: [
|
|
858
|
+
content: [
|
|
859
|
+
{
|
|
832
860
|
type: "text",
|
|
833
861
|
text: JSON.stringify({
|
|
834
862
|
total: announcements.length,
|
|
835
|
-
items: announcements
|
|
836
|
-
})
|
|
837
|
-
}
|
|
863
|
+
items: announcements,
|
|
864
|
+
}),
|
|
865
|
+
},
|
|
866
|
+
],
|
|
838
867
|
};
|
|
839
868
|
}
|
|
840
869
|
// ============================================================================
|
|
841
870
|
// Helper Methods
|
|
842
871
|
// ============================================================================
|
|
843
872
|
/**
|
|
844
|
-
* Get a user's UUID by their
|
|
873
|
+
* Get a user's UUID by their ACCESS ID (e.g., "user@access-ci.org").
|
|
874
|
+
*
|
|
875
|
+
* The Drupal username should match the full ACCESS ID.
|
|
845
876
|
*/
|
|
846
|
-
async
|
|
877
|
+
async getUserUuidByAccessId(accessId) {
|
|
847
878
|
const auth = this.getDrupalAuth();
|
|
848
|
-
const result = await auth.get(`/jsonapi/user/user?filter[
|
|
879
|
+
const result = await auth.get(`/jsonapi/user/user?filter[name]=${encodeURIComponent(accessId)}`);
|
|
849
880
|
if (!result.data || result.data.length === 0) {
|
|
850
881
|
return null;
|
|
851
882
|
}
|
|
@@ -856,30 +887,32 @@ Which would you like to do?`,
|
|
|
856
887
|
*/
|
|
857
888
|
async getAnnouncementContext() {
|
|
858
889
|
const auth = this.getDrupalAuth();
|
|
859
|
-
// Get the acting user's UUID
|
|
860
|
-
const
|
|
861
|
-
const userUuid = await this.
|
|
890
|
+
// Get the acting user's UUID by looking up their ACCESS ID
|
|
891
|
+
const actingUserAccessId = this.getActingUserAccessId();
|
|
892
|
+
const userUuid = await this.getUserUuidByAccessId(actingUserAccessId);
|
|
862
893
|
if (!userUuid) {
|
|
863
|
-
throw new Error(`Could not find user
|
|
894
|
+
throw new Error(`Could not find Drupal user for ACCESS ID "${actingUserAccessId}". ` +
|
|
895
|
+
`Verify the user exists in Drupal.`);
|
|
864
896
|
}
|
|
865
897
|
// Fetch tags and affinity groups in parallel
|
|
866
898
|
const [tagsResult, groupsResult] = await Promise.all([
|
|
867
899
|
auth.get("/jsonapi/taxonomy_term/tags?page[limit]=100"),
|
|
868
|
-
auth.get(`/jsonapi/node/affinity_group?filter[field_coordinator.id]=${userUuid}&filter[status]=1&page[limit]=100`)
|
|
900
|
+
auth.get(`/jsonapi/node/affinity_group?filter[field_coordinator.id]=${userUuid}&filter[status]=1&page[limit]=100`),
|
|
869
901
|
]);
|
|
870
902
|
const tags = (tagsResult.data || []).map((item) => ({
|
|
871
903
|
name: item.attributes?.name,
|
|
872
|
-
uuid: item.id
|
|
904
|
+
uuid: item.id,
|
|
873
905
|
}));
|
|
874
906
|
const affinityGroups = (groupsResult.data || []).map((item) => ({
|
|
875
907
|
id: item.attributes?.field_group_id,
|
|
876
908
|
uuid: item.id,
|
|
877
909
|
name: item.attributes?.title,
|
|
878
|
-
category: item.attributes?.field_affinity_group_category
|
|
910
|
+
category: item.attributes?.field_affinity_group_category,
|
|
879
911
|
}));
|
|
880
912
|
const isCoordinator = affinityGroups.length > 0;
|
|
881
913
|
return {
|
|
882
|
-
content: [
|
|
914
|
+
content: [
|
|
915
|
+
{
|
|
883
916
|
type: "text",
|
|
884
917
|
text: JSON.stringify({
|
|
885
918
|
tags: tags,
|
|
@@ -889,14 +922,21 @@ Which would you like to do?`,
|
|
|
889
922
|
where_to_share_options: [
|
|
890
923
|
{ value: "Announcements page", description: "Public announcements listing" },
|
|
891
924
|
{ value: "Bi-Weekly Digest", description: "ACCESS Support bi-weekly email digest" },
|
|
892
|
-
{
|
|
893
|
-
|
|
925
|
+
{
|
|
926
|
+
value: "Affinity Group page",
|
|
927
|
+
description: "Your affinity group's page (coordinators only)",
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
value: "Email to Affinity Group",
|
|
931
|
+
description: "Direct email to affinity group members (coordinators only)",
|
|
932
|
+
},
|
|
894
933
|
],
|
|
895
934
|
guidance: isCoordinator
|
|
896
935
|
? "User is an affinity group coordinator. Ask about affinity_group association and where_to_share preferences."
|
|
897
|
-
: "User is not a coordinator. Standard announcement fields apply."
|
|
898
|
-
})
|
|
899
|
-
}
|
|
936
|
+
: "User is not a coordinator. Standard announcement fields apply.",
|
|
937
|
+
}),
|
|
938
|
+
},
|
|
939
|
+
],
|
|
900
940
|
};
|
|
901
941
|
}
|
|
902
942
|
/**
|
|
@@ -933,13 +973,13 @@ Which would you like to do?`,
|
|
|
933
973
|
"email to affinity group": "email_to_your_affinity_group",
|
|
934
974
|
"email to your affinity group": "email_to_your_affinity_group",
|
|
935
975
|
// Also accept the raw values
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
976
|
+
on_the_announcements_page: "on_the_announcements_page",
|
|
977
|
+
in_the_access_support_bi_weekly_digest: "in_the_access_support_bi_weekly_digest",
|
|
978
|
+
on_your_affinity_group_page: "on_your_affinity_group_page",
|
|
979
|
+
email_to_your_affinity_group: "email_to_your_affinity_group",
|
|
940
980
|
};
|
|
941
981
|
normalizeWhereToShare(values) {
|
|
942
|
-
return values.map(v => {
|
|
982
|
+
return values.map((v) => {
|
|
943
983
|
const normalized = AnnouncementsServer.WHERE_TO_SHARE_MAP[v.toLowerCase()];
|
|
944
984
|
if (!normalized) {
|
|
945
985
|
throw new Error(`Invalid where_to_share value: "${v}". ` +
|