import * as z from "zod" import type { JsonObject } from "type-fest" import { defineAction } from "../../../automation/actions" import { getSlackApi } from "../lib/api" import { SLACK_USERGROUPS_READ_SCOPE, SLACK_USERGROUPS_WRITE_SCOPE, } from "../lib/scopes" const PROVIDER_USER_GROUP_SCHEMA = z.looseObject({ auto_type: z.string().nullable(), created_by: z.string(), date_create: z.number().int().nonnegative(), date_delete: z.number().int().nonnegative(), date_update: z.number().int().nonnegative(), deleted_by: z.string().nullable(), description: z.string(), handle: z.string(), id: z.string(), is_external: z.boolean(), is_usergroup: z.literal(true), name: z.string(), prefs: z.looseObject({ channels: z.string().array(), groups: z.string().array(), }), team_id: z.string(), updated_by: z.string(), user_count: z.coerce.number().int().nonnegative().optional(), users: z.string().array().optional(), }) const USER_GROUP_SCHEMA = z.object({ automaticType: z.string().nullable(), createdAt: z.number().int().nonnegative(), createdByUserId: z.string(), deletedAt: z.number().int().nonnegative(), deletedByUserId: z.string().nullable(), description: z.string(), handle: z.string(), id: z.string(), isExternal: z.boolean(), name: z.string(), privateChannelIds: z.string().array(), publicChannelIds: z.string().array(), updatedAt: z.number().int().nonnegative(), updatedByUserId: z.string(), userCount: z.number().int().nonnegative().optional(), userIds: z.string().array().optional(), workspaceId: z.string(), }) const USER_GROUP_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), usergroup: PROVIDER_USER_GROUP_SCHEMA, }) const USER_GROUP_LIST_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), usergroups: PROVIDER_USER_GROUP_SCHEMA.array(), }) const USER_GROUP_MEMBERS_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), users: z.string().array(), }) const USER_GROUP_FIELDS_SCHEMA = z.object({ /** Extra public channels members may add to their sidebar section. */ additionalChannelIds: z.string().min(1).array().optional(), /** Default channel IDs associated with the group. */ channelIds: z.string().min(1).array().optional(), /** Human-readable group description. */ description: z.string().optional(), /** Show default channels as a sidebar section for group members. */ enableSection: z.boolean().optional(), /** Unique mention handle without an at sign. */ handle: z.string().min(1).optional(), /** Ask Slack to include the group's member count. */ includeCount: z.boolean().optional(), }) /** Lists Slack user groups in the current workspace. */ export const listSlackUserGroups = defineAction("List Slack user groups") .describe("Lists Slack user groups in the current workspace.") .account("slack", SLACK_USERGROUPS_READ_SCOPE) .input( z.object({ includeCount: z.boolean().optional(), includeDisabled: z.boolean().optional(), includeUsers: z.boolean().optional(), }), ) .output(USER_GROUP_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return ( await getSlackApi(account.secret).call("usergroups.list", { query: { include_count: input.includeCount, include_disabled: input.includeDisabled, include_users: input.includeUsers, }, responseSchema: USER_GROUP_LIST_RESPONSE_SCHEMA, }) ).usergroups.map(toSlackUserGroup) }) /** Creates a Slack user group. */ export const createSlackUserGroup = defineAction("Create Slack user group") .describe("Creates a Slack user group and its default channel preferences.") .account("slack", SLACK_USERGROUPS_WRITE_SCOPE) .input( USER_GROUP_FIELDS_SCHEMA.extend({ /** Unique user group name. */ name: z.string().min(1), }), ) .output(USER_GROUP_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSlackUserGroup(account.secret, "usergroups.create", { additional_channels: input.additionalChannelIds, channels: input.channelIds, description: input.description, enable_section: input.enableSection, handle: input.handle, include_count: input.includeCount, name: input.name, }), ) /** Updates a Slack user group's properties and channel preferences. */ export const updateSlackUserGroup = defineAction("Update Slack user group") .describe("Updates a Slack user group's properties and channel preferences.") .account("slack", SLACK_USERGROUPS_WRITE_SCOPE) .input( USER_GROUP_FIELDS_SCHEMA.extend({ /** New unique group name. */ name: z.string().min(1).optional(), /** Stable Slack user group ID. */ userGroupId: z.string().min(1), }), ) .output(USER_GROUP_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSlackUserGroup(account.secret, "usergroups.update", { additional_channels: input.additionalChannelIds, channels: input.channelIds, description: input.description, enable_section: input.enableSection, handle: input.handle, include_count: input.includeCount, name: input.name, usergroup: input.userGroupId, }), ) /** Enables a previously disabled Slack user group. */ export const enableSlackUserGroup = defineAction("Enable Slack user group") .describe("Enables a previously disabled Slack user group.") .account("slack", SLACK_USERGROUPS_WRITE_SCOPE) .input(z.object({ userGroupId: z.string().min(1) })) .output(USER_GROUP_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSlackUserGroup(account.secret, "usergroups.enable", { usergroup: input.userGroupId, }), ) /** Disables a Slack user group without deleting it. */ export const disableSlackUserGroup = defineAction("Disable Slack user group") .describe("Disables a Slack user group without deleting it.") .account("slack", SLACK_USERGROUPS_WRITE_SCOPE) .input(z.object({ userGroupId: z.string().min(1) })) .output(USER_GROUP_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSlackUserGroup(account.secret, "usergroups.disable", { usergroup: input.userGroupId, }), ) /** Lists all members of one Slack user group. */ export const listSlackUserGroupMembers = defineAction( "List Slack user group members", ) .describe("Lists all member IDs belonging to one Slack user group.") .account("slack", SLACK_USERGROUPS_READ_SCOPE) .input( z.object({ includeDisabled: z.boolean().optional(), userGroupId: z.string().min(1), }), ) .output(z.string().array()) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => ( await getSlackApi(account.secret).call("usergroups.users.list", { query: { include_disabled: input.includeDisabled, usergroup: input.userGroupId, }, responseSchema: USER_GROUP_MEMBERS_RESPONSE_SCHEMA, }) ).users, ) /** Replaces all members of one Slack user group. */ export const updateSlackUserGroupMembers = defineAction( "Update Slack user group members", ) .describe("Replaces the complete member list of one Slack user group.") .account("slack", SLACK_USERGROUPS_WRITE_SCOPE) .input( z.object({ includeCount: z.boolean().optional(), userGroupId: z.string().min(1), /** Slack does not allow an empty replacement list. */ userIds: z.string().min(1).array().min(1), }), ) .output(USER_GROUP_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSlackUserGroup(account.secret, "usergroups.users.update", { include_count: input.includeCount, usergroup: input.userGroupId, users: input.userIds, }), ) /** * Runs a user-group mutation and normalizes its response. * * @param secret - Resolved Slack integration secret. * @param method - User-group method to invoke. * @param body - Provider-native request body. */ async function mutateSlackUserGroup( secret: Record, method: | "usergroups.create" | "usergroups.disable" | "usergroups.enable" | "usergroups.update" | "usergroups.users.update", body: Record, ) { const requestBody: JsonObject = {} for (const [key, value] of Object.entries(body)) { if (value !== undefined) requestBody[key] = value } return toSlackUserGroup( ( await getSlackApi(secret).call(method, { body: requestBody, responseSchema: USER_GROUP_RESPONSE_SCHEMA, }) ).usergroup, ) } /** * Converts one provider user group into the public casing contract. * * @param userGroup - Provider-native user-group fields. */ function toSlackUserGroup( userGroup: z.output, ) { return { automaticType: userGroup.auto_type, createdAt: userGroup.date_create, createdByUserId: userGroup.created_by, deletedAt: userGroup.date_delete, deletedByUserId: userGroup.deleted_by, description: userGroup.description, handle: userGroup.handle, id: userGroup.id, isExternal: userGroup.is_external, name: userGroup.name, privateChannelIds: userGroup.prefs.groups, publicChannelIds: userGroup.prefs.channels, updatedAt: userGroup.date_update, updatedByUserId: userGroup.updated_by, userCount: userGroup.user_count, userIds: userGroup.users, workspaceId: userGroup.team_id, } }