Skip to content

Commit 40afecb

Browse files
committed
feat(slack/onboard-llmo): add option to remove site enrollment
1 parent 6d4145f commit 40afecb

File tree

8 files changed

+557
-106
lines changed

8 files changed

+557
-106
lines changed

package-lock.json

Lines changed: 0 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/controllers/llmo/llmo-onboarding.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ async function publishToAdminHlx(filename, outputLocation, log) {
170170
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
171171
* @returns {Promise<void>}
172172
*/
173-
export async function copyFilesToSharepoint(dataFolder, context, say = () => {}) {
173+
export async function copyFilesToSharepoint(dataFolder, context, say = () => { }) {
174174
const { log, env } = context;
175175

176176
const sharepointClient = await createSharePointClient(env);
@@ -209,7 +209,7 @@ export async function copyFilesToSharepoint(dataFolder, context, say = () => {})
209209
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
210210
* @returns {Promise<void>}
211211
*/
212-
export async function updateIndexConfig(dataFolder, context, say = () => {}) {
212+
export async function updateIndexConfig(dataFolder, context, say = () => { }) {
213213
const { log, env } = context;
214214

215215
log.debug('Starting Git modification of helix query config');
@@ -260,7 +260,7 @@ export async function updateIndexConfig(dataFolder, context, say = () => {}) {
260260
* @param {object} slackContext - Slack context (optional, for Slack operations)
261261
* @returns {Promise<object>} The organization object
262262
*/
263-
export async function createOrFindOrganization(imsOrgId, context, say = () => {}) {
263+
export async function createOrFindOrganization(imsOrgId, context, say = () => { }) {
264264
const { dataAccess, log } = context;
265265
const { Organization } = dataAccess;
266266

@@ -320,7 +320,7 @@ export async function createOrFindSite(baseURL, organizationId, context) {
320320
* @param {Function} say - Optional function to send messages (e.g., Slack say function)
321321
* @returns {Promise<object>} The entitlement and enrollment objects
322322
*/
323-
export async function createEntitlementAndEnrollment(site, context, say = () => {}) {
323+
export async function createEntitlementAndEnrollment(site, context, say = () => { }) {
324324
const { log } = context;
325325

326326
try {
@@ -339,6 +339,31 @@ export async function createEntitlementAndEnrollment(site, context, say = () =>
339339
}
340340
}
341341

342+
export async function hasActiveLlmoEnrollment(site, context) {
343+
try {
344+
const tierClient = await TierClient.createForSite(context, site, LLMO_PRODUCT_CODE);
345+
const { siteEnrollment } = await tierClient.checkValidEntitlement();
346+
return !!siteEnrollment;
347+
} catch (error) {
348+
return false;
349+
}
350+
}
351+
352+
export async function removeEnrollment(site, context, say = () => { }) {
353+
const { log } = context;
354+
355+
try {
356+
const tierClient = await TierClient.createForSite(context, site, LLMO_PRODUCT_CODE);
357+
await tierClient.revokeSiteEnrollment();
358+
log.info(`Successfully revoked LLMO enrollment for site ${site.getId()}`);
359+
await say(`✅ Successfully revoked LLMO enrollment for site ${site.getId()}`);
360+
} catch (error) {
361+
log.error(`Removing LLMO enrollment failed: ${error.message}`);
362+
await say('❌ Removing LLMO enrollment failed');
363+
throw error;
364+
}
365+
}
366+
342367
export async function enableAudits(site, context, audits = []) {
343368
const { dataAccess } = context;
344369
const { Configuration } = dataAccess;

src/controllers/slack.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function initSlackBot(lambdaContext, App) {
8181
app.view('preflight_config_modal', actions.preflight_config_modal(lambdaContext));
8282
app.view('onboard_llmo_modal', actions.onboardLLMOModal(lambdaContext));
8383
app.view('update_ims_org_modal', actions.updateIMSOrgModal(lambdaContext));
84+
app.view('confirm_remove_llmo_enrollment', actions.confirmRemoveLlmoEnrollment(lambdaContext));
8485

8586
return app;
8687
}

src/support/slack/actions/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
addEntitlementsAction,
2222
updateOrgAction,
2323
updateIMSOrgModal,
24+
removeLlmoEnrollment,
25+
confirmRemoveLlmoEnrollment,
2426
} from './onboard-llmo-modal.js';
2527
import { onboardSiteModal, startOnboarding } from './onboard-modal.js';
2628
import { preflightConfigModal } from './preflight-config-modal.js';
@@ -35,12 +37,14 @@ const actions = {
3537
onboardSiteModal,
3638
onboardLLMOModal,
3739
updateIMSOrgModal,
40+
confirmRemoveLlmoEnrollment,
3841
start_onboarding: startOnboarding,
3942
start_llmo_onboarding: startLLMOOnboarding,
4043
preflight_config_modal: preflightConfigModal,
4144
open_preflight_config: openPreflightConfig,
4245
add_entitlements_action: addEntitlementsAction,
4346
update_org_action: updateOrgAction,
47+
remove_llmo_enrollment: removeLlmoEnrollment,
4448
};
4549

4650
export default actions;

src/support/slack/actions/onboard-llmo-modal.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
copyFilesToSharepoint,
2020
updateIndexConfig,
2121
enableAudits,
22+
removeEnrollment,
2223
} from '../../../controllers/llmo/llmo-onboarding.js';
2324

2425
const REFERRAL_TRAFFIC_AUDIT = 'llmo-referral-traffic';
@@ -791,3 +792,208 @@ export function updateIMSOrgModal(lambdaContext) {
791792
}
792793
};
793794
}
795+
796+
export function removeLlmoEnrollment(lambdaContext) {
797+
const { log } = lambdaContext;
798+
799+
return async ({ ack, body, client }) => {
800+
try {
801+
await ack();
802+
803+
const metadata = JSON.parse(body.actions[0].value);
804+
const {
805+
brandURL,
806+
siteId,
807+
existingBrand,
808+
originalThreadTs,
809+
} = metadata;
810+
811+
const originalChannel = body.channel?.id;
812+
const { user } = body;
813+
814+
log.info(`User ${user.id} initiated LLMO enrollment removal for site ${siteId} (${brandURL})`);
815+
816+
// Update the original message to show user's action
817+
await client.chat.update({
818+
channel: originalChannel,
819+
ts: body.message.ts,
820+
text: `:warning: ${user.name} is removing LLMO enrollment for ${brandURL}...`,
821+
blocks: [
822+
{
823+
type: 'section',
824+
text: {
825+
type: 'mrkdwn',
826+
text: `:warning: ${user.name} is removing LLMO enrollment for ${brandURL}...`,
827+
},
828+
},
829+
],
830+
});
831+
832+
// Show confirmation modal
833+
await client.views.open({
834+
trigger_id: body.trigger_id,
835+
view: {
836+
type: 'modal',
837+
callback_id: 'confirm_remove_llmo_enrollment',
838+
private_metadata: JSON.stringify({
839+
brandURL,
840+
siteId,
841+
existingBrand,
842+
originalChannel,
843+
originalThreadTs,
844+
originalMessageTs: body.message.ts,
845+
}),
846+
title: {
847+
type: 'plain_text',
848+
text: 'Confirm Removal',
849+
},
850+
submit: {
851+
type: 'plain_text',
852+
text: 'Remove Enrollment',
853+
},
854+
close: {
855+
type: 'plain_text',
856+
text: 'Cancel',
857+
},
858+
blocks: [
859+
{
860+
type: 'section',
861+
text: {
862+
type: 'mrkdwn',
863+
text: `:warning: *Are you sure you want to remove LLMO enrollment?*\n\n*Site:* ${brandURL}\n*Brand:* ${existingBrand}\n\nThis action will:\n• Revoke the site's LLMO enrollment\n• Remove access to LLMO features for this site\n\n*This action cannot be undone.*`,
864+
},
865+
},
866+
],
867+
},
868+
});
869+
} catch (error) {
870+
log.error('Error handling remove LLMO enrollment action:', error);
871+
const metadata = JSON.parse(body.actions[0].value);
872+
await client.chat.postMessage({
873+
channel: body.channel?.id,
874+
text: `:x: Failed to initiate enrollment removal: ${error.message}`,
875+
thread_ts: metadata.originalThreadTs,
876+
});
877+
}
878+
};
879+
}
880+
881+
export function confirmRemoveLlmoEnrollment(lambdaContext) {
882+
const { log, dataAccess } = lambdaContext;
883+
884+
return async ({ ack, body, client }) => {
885+
try {
886+
log.debug('Processing LLMO enrollment removal confirmation...');
887+
888+
const { view, user } = body;
889+
const metadata = JSON.parse(view.private_metadata);
890+
const {
891+
brandURL,
892+
siteId,
893+
existingBrand,
894+
originalChannel,
895+
originalThreadTs,
896+
originalMessageTs,
897+
} = metadata;
898+
899+
// Acknowledge the modal submission
900+
await ack();
901+
902+
// Post initial message to the thread
903+
const responseChannel = originalChannel || body.user.id;
904+
const responseThreadTs = originalChannel ? originalThreadTs : undefined;
905+
906+
await client.chat.postMessage({
907+
channel: responseChannel,
908+
text: `:gear: Removing LLMO enrollment for ${brandURL}...`,
909+
thread_ts: responseThreadTs,
910+
});
911+
912+
try {
913+
// Find the site
914+
const { Site } = dataAccess;
915+
const site = await Site.findById(siteId);
916+
917+
if (!site) {
918+
throw new Error(`Site not found: ${siteId}`);
919+
}
920+
921+
// Use the reusable removeEnrollment function from the LLMO controller
922+
await removeEnrollment(site, lambdaContext);
923+
924+
log.info(`Successfully revoked LLMO enrollment for site ${siteId} (${brandURL})`);
925+
926+
// Update the original message to show completion
927+
if (originalMessageTs) {
928+
await client.chat.update({
929+
channel: responseChannel,
930+
ts: originalMessageTs,
931+
text: `:white_check_mark: LLMO enrollment removed for ${brandURL}`,
932+
blocks: [
933+
{
934+
type: 'section',
935+
text: {
936+
type: 'mrkdwn',
937+
text: `:white_check_mark: *LLMO Enrollment Removed*\n\nThe LLMO enrollment for *${brandURL}* (brand: *${existingBrand}*) has been successfully removed by ${user.name}.`,
938+
},
939+
},
940+
],
941+
});
942+
}
943+
944+
// Post success message to the thread
945+
const successMessage = `:white_check_mark: *LLMO enrollment removed successfully!*
946+
947+
:link: *Site:* ${brandURL}
948+
:identification_card: *Site ID:* ${siteId}
949+
:label: *Brand:* ${existingBrand}
950+
:bust_in_silhouette: *Removed by:* ${user.name}
951+
952+
The site enrollment has been revoked. The site can be re-onboarded at any time using the \`onboard-llmo\` command.`;
953+
954+
await client.chat.postMessage({
955+
channel: responseChannel,
956+
text: successMessage,
957+
thread_ts: responseThreadTs,
958+
});
959+
} catch (error) {
960+
log.error(`Error removing LLMO enrollment for site ${siteId}:`, error);
961+
962+
// Update the original message to show error
963+
if (originalMessageTs) {
964+
await client.chat.update({
965+
channel: responseChannel,
966+
ts: originalMessageTs,
967+
text: `:x: Failed to remove LLMO enrollment for ${brandURL}`,
968+
blocks: [
969+
{
970+
type: 'section',
971+
text: {
972+
type: 'mrkdwn',
973+
text: `:x: *Failed to remove LLMO enrollment*\n\nThere was an error removing the enrollment for *${brandURL}*.`,
974+
},
975+
},
976+
],
977+
});
978+
}
979+
980+
// Post error message to the thread
981+
await client.chat.postMessage({
982+
channel: responseChannel,
983+
text: `:x: Failed to remove LLMO enrollment: ${error.message}`,
984+
thread_ts: responseThreadTs,
985+
});
986+
}
987+
988+
log.debug(`LLMO enrollment removal processed for user ${user.id}, site ${brandURL}`);
989+
} catch (error) {
990+
log.error('Error handling confirm remove LLMO enrollment modal:', error);
991+
await ack({
992+
response_action: 'errors',
993+
errors: {
994+
general: 'There was an error processing the removal request.',
995+
},
996+
});
997+
}
998+
};
999+
}

0 commit comments

Comments
 (0)