import * as z from "zod" import { defineAction } from "../../../automation/actions" import { getSlackApi } from "../lib/api" import { SLACK_USERS_PROFILE_READ_SCOPE, SLACK_USERS_READ_EMAIL_REQUIREMENT, SLACK_USERS_READ_SCOPE, } from "../lib/scopes" import { SLACK_CURSOR_INPUT_SCHEMA, SLACK_MEMBER_PROFILE_SCHEMA, SLACK_MEMBER_SCHEMA, SLACK_PROVIDER_MEMBER_PROFILE_SCHEMA, SLACK_PROVIDER_MEMBER_SCHEMA, SLACK_PROVIDER_RESPONSE_METADATA_SCHEMA, SLACK_RESPONSE_METADATA_SCHEMA, toSlackMember, toSlackResponseMetadata, } from "@automate.ax/integration-contracts/slack" const USERS_LIST_RESPONSE_SCHEMA = z.looseObject({ members: SLACK_PROVIDER_MEMBER_SCHEMA.array(), ok: z.literal(true), response_metadata: SLACK_PROVIDER_RESPONSE_METADATA_SCHEMA, }) const USER_INFO_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), user: SLACK_PROVIDER_MEMBER_SCHEMA, }) const USER_PROFILE_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true), profile: SLACK_PROVIDER_MEMBER_PROFILE_SCHEMA, }) const USER_PRESENCE_RESPONSE_SCHEMA = z.looseObject({ auto_away: z.boolean().optional(), connection_count: z.number().int().nonnegative().optional(), last_activity: z.number().int().nonnegative().optional(), manual_away: z.boolean().optional(), ok: z.literal(true), online: z.boolean().optional(), presence: z.enum(["active", "away"]), }) const MEMBER_PRESENCE_SCHEMA = z.object({ /** Whether Slack automatically marked the authenticated user away. */ autoAway: z.boolean().optional(), /** Number of currently connected clients for the authenticated user. */ connectionCount: z.number().int().nonnegative().optional(), /** Unix timestamp of the authenticated user's latest activity. */ lastActivityAt: z.number().int().nonnegative().optional(), /** Whether the authenticated user manually marked themselves away. */ manualAway: z.boolean().optional(), /** Whether the authenticated user currently has a connected client. */ online: z.boolean().optional(), /** Slack's current aggregate presence value. */ presence: z.enum(["active", "away"]), }) const MEMBER_PAGE_SCHEMA = z.object({ /** Workspace members in the current page. */ members: SLACK_MEMBER_SCHEMA.array(), /** Pagination metadata for requesting the next page. */ responseMetadata: SLACK_RESPONSE_METADATA_SCHEMA, }) /** * Lists workspace members, including invited and deactivated accounts. * * Email addresses are returned only when the installed account separately has * `users:read.email`; the action does not request that sensitive scope. */ export const listSlackMembers = defineAction("List Slack members") .describe("Lists workspace members with rich profile fields.") .account("slack", SLACK_USERS_READ_SCOPE) .input( SLACK_CURSOR_INPUT_SCHEMA.extend({ /** Whether Slack should include each member's locale. */ includeLocale: z.boolean().optional(), }), ) .output(MEMBER_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call("users.list", { query: { cursor: input.cursor, include_locale: input.includeLocale, limit: input.limit, }, responseSchema: USERS_LIST_RESPONSE_SCHEMA, }) return { members: result.members.map(toSlackMember), responseMetadata: toSlackResponseMetadata(result.response_metadata), } }) /** Gets one Slack workspace member and their profile. */ export const getSlackMember = defineAction("Get Slack member") .describe("Gets a Slack workspace member and their profile.") .account("slack", SLACK_USERS_READ_SCOPE) .input( z.object({ /** Whether Slack should include the member's locale. */ includeLocale: z.boolean().optional(), /** Stable Slack user ID. */ userId: z.string().min(1), }), ) .output(SLACK_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toSlackMember( ( await getSlackApi(account.secret).call("users.info", { query: { include_locale: input.includeLocale, user: input.userId, }, responseSchema: USER_INFO_RESPONSE_SCHEMA, }) ).user, ), ) /** Finds an active Slack workspace member by email address. */ export const findSlackMemberByEmail = defineAction("Find Slack member by email") .describe("Finds an active Slack workspace member by email address.") .account("slack", SLACK_USERS_READ_EMAIL_REQUIREMENT) .input(z.object({ email: z.email() })) .output(SLACK_MEMBER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => toSlackMember( ( await getSlackApi(account.secret).call("users.lookupByEmail", { query: { email: input.email }, responseSchema: USER_INFO_RESPONSE_SCHEMA, }) ).user, ), ) /** Gets one Slack member's workspace profile and custom status. */ export const getSlackMemberProfile = defineAction("Get Slack member profile") .describe("Gets one Slack member's workspace profile and custom status.") .account("slack", SLACK_USERS_PROFILE_READ_SCOPE) .input( z.object({ /** Whether Slack should include labels for custom profile fields. */ includeLabels: z.boolean().optional(), /** Slack member to inspect; omit to inspect the authenticated bot. */ userId: z.string().min(1).optional(), }), ) .output(SLACK_MEMBER_PROFILE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return toSlackMemberProfile( ( await getSlackApi(account.secret).call("users.profile.get", { query: { include_labels: input.includeLabels, user: input.userId, }, responseSchema: USER_PROFILE_RESPONSE_SCHEMA, }) ).profile, ) }) /** Gets one Slack member's current presence. */ export const getSlackMemberPresence = defineAction("Get Slack member presence") .describe("Gets one Slack member's current active or away presence.") .account("slack", SLACK_USERS_READ_SCOPE) .input( z.object({ /** Slack member to inspect; omit to inspect the authenticated bot. */ userId: z.string().min(1).optional(), }), ) .output(MEMBER_PRESENCE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call("users.getPresence", { query: { user: input.userId }, responseSchema: USER_PRESENCE_RESPONSE_SCHEMA, }) return { autoAway: result.auto_away, connectionCount: result.connection_count, lastActivityAt: result.last_activity, manualAway: result.manual_away, online: result.online, presence: result.presence, } }) /** * Converts a provider profile into the public casing contract. * * @param profile - Provider-native member profile. */ function toSlackMemberProfile( profile: z.output, ) { return { displayName: profile.display_name ?? undefined, email: profile.email ?? undefined, firstName: profile.first_name ?? undefined, image192: profile.image_192 ?? undefined, image512: profile.image_512 ?? undefined, image72: profile.image_72 ?? undefined, lastName: profile.last_name ?? undefined, pronouns: profile.pronouns ?? undefined, realName: profile.real_name ?? undefined, statusEmoji: profile.status_emoji ?? undefined, statusExpiration: profile.status_expiration ?? undefined, statusText: profile.status_text ?? undefined, title: profile.title ?? undefined, } }