├── src ├── dev_deps.ts ├── deps.ts ├── generated │ └── method-types │ │ ├── api.ts │ │ ├── bots.ts │ │ ├── dialog.ts │ │ ├── migration.ts │ │ ├── emoji.ts │ │ ├── rtm.ts │ │ ├── tooling.ts │ │ ├── pins.ts │ │ ├── search.ts │ │ ├── openid.ts │ │ ├── workflows.ts │ │ ├── views.ts │ │ ├── bookmarks.ts │ │ ├── oauth.ts │ │ ├── stars.ts │ │ ├── dnd.ts │ │ ├── reminders.ts │ │ ├── auth.ts │ │ ├── reactions.ts │ │ ├── calls.ts │ │ ├── enterprise.ts │ │ ├── canvases.ts │ │ ├── usergroups.ts │ │ ├── chat.ts │ │ ├── apps.ts │ │ ├── team.ts │ │ ├── files.ts │ │ ├── users.ts │ │ ├── conversations.ts │ │ ├── mod.ts │ │ └── admin.ts ├── typed-method-types │ ├── workflows │ │ ├── mod.ts │ │ └── triggers │ │ │ ├── event-data │ │ │ ├── common-objects │ │ │ │ ├── all_triggers.ts │ │ │ │ └── shared_channel_invite.ts │ │ │ ├── user_left_channel.ts │ │ │ ├── user_joined_channel.ts │ │ │ ├── channel_shared.ts │ │ │ ├── channel_deleted.ts │ │ │ ├── channel_renamed.ts │ │ │ ├── channel_archived.ts │ │ │ ├── channel_unarchived.ts │ │ │ ├── shared_channel_invite_received.ts │ │ │ ├── reaction_removed.ts │ │ │ ├── channel_unshared.ts │ │ │ ├── channel_created.ts │ │ │ ├── dnd_updated.ts │ │ │ ├── pin_added.ts │ │ │ ├── pin_removed.ts │ │ │ ├── message_metadata_posted.ts │ │ │ ├── emoji_changed.ts │ │ │ ├── app_mentioned.ts │ │ │ ├── message_posted.ts │ │ │ ├── user_joined_team.ts │ │ │ ├── reaction_added.ts │ │ │ ├── shared_channel_invite_accepted.ts │ │ │ ├── shared_channel_invite_declined.ts │ │ │ ├── shared_channel_invite_approved.ts │ │ │ └── mod.ts │ │ │ ├── scheduled-data.ts │ │ │ ├── trigger-filter.ts │ │ │ ├── shortcut.ts │ │ │ ├── webhook.ts │ │ │ ├── tests │ │ │ ├── fixtures │ │ │ │ └── workflows.ts │ │ │ ├── trigger-filter_test.ts │ │ │ ├── context_test.ts │ │ │ ├── shortcut_test.ts │ │ │ ├── webhook_test.ts │ │ │ └── crud_test.ts │ │ │ ├── base_response.ts │ │ │ ├── trigger-event-types.ts │ │ │ ├── shortcut-data.ts │ │ │ ├── inputs.ts │ │ │ ├── scheduled.ts │ │ │ ├── event.ts │ │ │ └── mod.ts │ ├── functions.ts │ ├── typed-method-tests.ts │ ├── mod.ts │ └── chat.ts ├── type-helpers.ts ├── mod.ts ├── base-client-helpers.ts ├── README.md ├── api-proxy_test.ts ├── api-proxy.ts ├── base-client.ts ├── types.ts └── base-client-helpers_test.ts ├── README.md ├── .gitignore ├── .vscode └── settings.json ├── .github ├── dependabot.yml ├── CODEOWNERS ├── workflows │ ├── publish.yml │ ├── npm.yml │ ├── deno.yml │ └── samples.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature.md │ ├── question.md │ └── bug.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── maintainers_guide.md ├── scripts ├── generate ├── build_npm.ts ├── src │ ├── imports │ │ ├── update_test.ts │ │ └── update.ts │ ├── api-method-node.ts │ ├── generate.ts │ └── public-api-methods.ts └── README.md ├── LICENSE ├── testing ├── http.ts └── http_test.ts ├── deno.jsonc └── docs ├── client.md └── triggers ├── trigger-generic-inputs.md ├── trigger-filters.md ├── webhook-triggers.md └── link-triggers.md /src/dev_deps.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | src/README.md -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | export { pascalCase } from "jsr:@wok/case@1.0.1"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | scripts/api_spec.json 3 | .coverage 4 | lcov.info 5 | npm/ 6 | -------------------------------------------------------------------------------- /src/generated/method-types/api.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type ApiAPIType = { 4 | test: SlackAPIMethod; 5 | }; 6 | -------------------------------------------------------------------------------- /src/generated/method-types/bots.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type BotsAPIType = { 4 | info: SlackAPIMethod; 5 | }; 6 | -------------------------------------------------------------------------------- /src/generated/method-types/dialog.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type DialogAPIType = { 4 | open: SlackAPIMethod; 5 | }; 6 | -------------------------------------------------------------------------------- /src/generated/method-types/migration.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type MigrationAPIType = { 4 | exchange: SlackAPIMethod; 5 | }; 6 | -------------------------------------------------------------------------------- /src/generated/method-types/emoji.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPICursorPaginatedMethod } from "../../types.ts"; 2 | 3 | export type EmojiAPIType = { 4 | list: SlackAPICursorPaginatedMethod; 5 | }; 6 | -------------------------------------------------------------------------------- /src/generated/method-types/rtm.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type RtmAPIType = { 4 | connect: SlackAPIMethod; 5 | start: SlackAPIMethod; 6 | }; 7 | -------------------------------------------------------------------------------- /src/generated/method-types/tooling.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type ToolingAPIType = { 4 | tokens: { 5 | rotate: SlackAPIMethod; 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/generated/method-types/pins.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type PinsAPIType = { 4 | add: SlackAPIMethod; 5 | list: SlackAPIMethod; 6 | remove: SlackAPIMethod; 7 | }; 8 | -------------------------------------------------------------------------------- /src/generated/method-types/search.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type SearchAPIType = { 4 | all: SlackAPIMethod; 5 | files: SlackAPIMethod; 6 | messages: SlackAPIMethod; 7 | }; 8 | -------------------------------------------------------------------------------- /src/generated/method-types/openid.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type OpenidAPIType = { 4 | connect: { 5 | token: SlackAPIMethod; 6 | userInfo: SlackAPIMethod; 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/generated/method-types/workflows.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type WorkflowsAPIType = { 4 | stepCompleted: SlackAPIMethod; 5 | stepFailed: SlackAPIMethod; 6 | updateStep: SlackAPIMethod; 7 | }; 8 | -------------------------------------------------------------------------------- /src/generated/method-types/views.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type ViewsAPIType = { 4 | open: SlackAPIMethod; 5 | publish: SlackAPIMethod; 6 | push: SlackAPIMethod; 7 | update: SlackAPIMethod; 8 | }; 9 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/mod.ts: -------------------------------------------------------------------------------- 1 | import type { TypedWorkflowsTriggersMethodTypes } from "./triggers/mod.ts"; 2 | 3 | export type TypedWorkflowsMethodTypes = { 4 | workflows: { 5 | triggers: TypedWorkflowsTriggersMethodTypes; 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/generated/method-types/bookmarks.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type BookmarksAPIType = { 4 | add: SlackAPIMethod; 5 | edit: SlackAPIMethod; 6 | list: SlackAPIMethod; 7 | remove: SlackAPIMethod; 8 | }; 9 | -------------------------------------------------------------------------------- /src/generated/method-types/oauth.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type OauthAPIType = { 4 | access: SlackAPIMethod; 5 | v2: { 6 | access: SlackAPIMethod; 7 | exchange: SlackAPIMethod; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/generated/method-types/stars.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type StarsAPIType = { 7 | add: SlackAPIMethod; 8 | list: SlackAPICursorPaginatedMethod; 9 | remove: SlackAPIMethod; 10 | }; 11 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/common-objects/all_triggers.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * A {@link https://api.slack.com/automation/types#timestamp timestamp} when the trigger was invoked. 4 | */ 5 | event_timestamp: "{{event_timestamp}}", 6 | } as const; 7 | -------------------------------------------------------------------------------- /src/generated/method-types/dnd.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type DndAPIType = { 4 | endDnd: SlackAPIMethod; 5 | endSnooze: SlackAPIMethod; 6 | info: SlackAPIMethod; 7 | setSnooze: SlackAPIMethod; 8 | teamInfo: SlackAPIMethod; 9 | }; 10 | -------------------------------------------------------------------------------- /src/generated/method-types/reminders.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type RemindersAPIType = { 4 | add: SlackAPIMethod; 5 | complete: SlackAPIMethod; 6 | delete: SlackAPIMethod; 7 | info: SlackAPIMethod; 8 | list: SlackAPIMethod; 9 | }; 10 | -------------------------------------------------------------------------------- /src/generated/method-types/auth.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type AuthAPIType = { 7 | revoke: SlackAPIMethod; 8 | teams: { 9 | list: SlackAPICursorPaginatedMethod; 10 | }; 11 | test: SlackAPIMethod; 12 | }; 13 | -------------------------------------------------------------------------------- /src/generated/method-types/reactions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type ReactionsAPIType = { 7 | add: SlackAPIMethod; 8 | get: SlackAPIMethod; 9 | list: SlackAPICursorPaginatedMethod; 10 | remove: SlackAPIMethod; 11 | }; 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.config": "./deno.jsonc", 5 | "[typescript]": { 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "denoland.vscode-deno" 8 | }, 9 | "deno.suggest.imports.hosts": { 10 | "https://deno.land": false 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /src/generated/method-types/calls.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type CallsAPIType = { 4 | add: SlackAPIMethod; 5 | end: SlackAPIMethod; 6 | info: SlackAPIMethod; 7 | participants: { 8 | add: SlackAPIMethod; 9 | remove: SlackAPIMethod; 10 | }; 11 | update: SlackAPIMethod; 12 | }; 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | -------------------------------------------------------------------------------- /src/generated/method-types/enterprise.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type EnterpriseAPIType = { 4 | auth: { 5 | idpconfig: { 6 | apply: SlackAPIMethod; 7 | get: SlackAPIMethod; 8 | list: SlackAPIMethod; 9 | remove: SlackAPIMethod; 10 | set: SlackAPIMethod; 11 | }; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /scripts/generate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Writes the Function files based on a functions.json file existing alongside this script 4 | deno run --allow-read --allow-net --allow-write ./scripts/src/generate.ts 5 | echo "Formatting Slack function files..." 6 | deno fmt --quiet ./src/generated/**/*.ts 7 | echo "Linting Slack function files..." 8 | deno lint --quiet ./src/generated/**/*.ts 9 | -------------------------------------------------------------------------------- /src/generated/method-types/canvases.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type CanvasesAPIType = { 4 | access: { 5 | delete: SlackAPIMethod; 6 | set: SlackAPIMethod; 7 | }; 8 | create: SlackAPIMethod; 9 | delete: SlackAPIMethod; 10 | edit: SlackAPIMethod; 11 | sections: { 12 | lookup: SlackAPIMethod; 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/generated/method-types/usergroups.ts: -------------------------------------------------------------------------------- 1 | import type { SlackAPIMethod } from "../../types.ts"; 2 | 3 | export type UsergroupsAPIType = { 4 | create: SlackAPIMethod; 5 | disable: SlackAPIMethod; 6 | enable: SlackAPIMethod; 7 | list: SlackAPIMethod; 8 | update: SlackAPIMethod; 9 | users: { 10 | list: SlackAPIMethod; 11 | update: SlackAPIMethod; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/type-helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Utility type to convert an object of strings to a union type of that objects values 3 | */ 4 | export type ObjectValueUnion> = T[keyof T]; 5 | 6 | /** 7 | * @description Utility type for the array types which requires minumum one subtype in it. 8 | * @example "PopulatedArray" 9 | */ 10 | export type PopulatedArray = [T, ...T[]]; 11 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source project configuration 2 | # Learn more: https://github.com/salesforce/oss-template 3 | #ECCN:Open Source 4 | #GUSINFO:Open Source,Open Source Workflow 5 | 6 | # @slackapi/denosaurs 7 | # are code reviewers for all changes in this repo. 8 | * @slackapi/denosaurs 9 | 10 | # @slackapi/developer-education 11 | # are code reviewers for changes in the `/docs` directory. 12 | /docs/ @slackapi/developer-education 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write # The OIDC ID token is used for authentication with JSR. 14 | steps: 15 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 16 | with: 17 | persist-credentials: false 18 | - run: npx jsr publish 19 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/scheduled-data.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./event-data/common-objects/all_triggers.ts"; 2 | 3 | /** 4 | * Scheduled-trigger-specific input values that contain information about the scheduled trigger. 5 | */ 6 | export const ScheduledTriggerContextData = { 7 | ...base_trigger_data, 8 | /** 9 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who created the trigger. 10 | */ 11 | user_id: "{{data.user_id}}", 12 | } as const; 13 | -------------------------------------------------------------------------------- /src/generated/method-types/chat.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type ChatAPIType = { 7 | delete: SlackAPIMethod; 8 | deleteScheduledMessage: SlackAPIMethod; 9 | getPermalink: SlackAPIMethod; 10 | meMessage: SlackAPIMethod; 11 | postEphemeral: SlackAPIMethod; 12 | scheduledMessages: { 13 | list: SlackAPICursorPaginatedMethod; 14 | }; 15 | scheduleMessage: SlackAPIMethod; 16 | unfurl: SlackAPIMethod; 17 | update: SlackAPIMethod; 18 | }; 19 | -------------------------------------------------------------------------------- /src/generated/method-types/apps.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type AppsAPIType = { 7 | connections: { 8 | open: SlackAPIMethod; 9 | }; 10 | event: { 11 | authorizations: { 12 | list: SlackAPICursorPaginatedMethod; 13 | }; 14 | }; 15 | manifest: { 16 | create: SlackAPIMethod; 17 | delete: SlackAPIMethod; 18 | export: SlackAPIMethod; 19 | update: SlackAPIMethod; 20 | validate: SlackAPIMethod; 21 | }; 22 | uninstall: SlackAPIMethod; 23 | }; 24 | -------------------------------------------------------------------------------- /src/generated/method-types/team.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type TeamAPIType = { 7 | accessLogs: SlackAPIMethod; 8 | billableInfo: SlackAPIMethod; 9 | billing: { 10 | info: SlackAPIMethod; 11 | }; 12 | externalTeams: { 13 | disconnect: SlackAPIMethod; 14 | list: SlackAPICursorPaginatedMethod; 15 | }; 16 | info: SlackAPIMethod; 17 | integrationLogs: SlackAPIMethod; 18 | preferences: { 19 | list: SlackAPIMethod; 20 | }; 21 | profile: { 22 | get: SlackAPIMethod; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/generated/method-types/files.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type FilesAPIType = { 7 | comments: { 8 | delete: SlackAPIMethod; 9 | }; 10 | delete: SlackAPIMethod; 11 | info: SlackAPIMethod; 12 | list: SlackAPICursorPaginatedMethod; 13 | remote: { 14 | add: SlackAPIMethod; 15 | info: SlackAPIMethod; 16 | list: SlackAPICursorPaginatedMethod; 17 | remove: SlackAPIMethod; 18 | share: SlackAPIMethod; 19 | update: SlackAPIMethod; 20 | }; 21 | revokePublicURL: SlackAPIMethod; 22 | sharedPublicURL: SlackAPIMethod; 23 | upload: SlackAPIMethod; 24 | }; 25 | -------------------------------------------------------------------------------- /src/generated/method-types/users.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type UsersAPIType = { 7 | conversations: SlackAPICursorPaginatedMethod; 8 | deletePhoto: SlackAPIMethod; 9 | discoverableContacts: { 10 | lookup: SlackAPIMethod; 11 | }; 12 | getPresence: SlackAPIMethod; 13 | identity: SlackAPIMethod; 14 | info: SlackAPIMethod; 15 | list: SlackAPICursorPaginatedMethod; 16 | lookupByEmail: SlackAPIMethod; 17 | profile: { 18 | get: SlackAPIMethod; 19 | set: SlackAPIMethod; 20 | }; 21 | setActive: SlackAPIMethod; 22 | setPhoto: SlackAPIMethod; 23 | setPresence: SlackAPIMethod; 24 | }; 25 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | import { BaseSlackAPIClient } from "./base-client.ts"; 2 | import type { SlackAPIClient, SlackAPIOptions } from "./types.ts"; 3 | import { ProxifyAndTypeClient } from "./api-proxy.ts"; 4 | 5 | export { 6 | TriggerContextData, 7 | TriggerTypes, 8 | } from "./typed-method-types/workflows/triggers/mod.ts"; 9 | export { TriggerEventTypes } from "./typed-method-types/workflows/triggers/trigger-event-types.ts"; 10 | 11 | export const SlackAPI = ( 12 | token: string, 13 | options: SlackAPIOptions = {}, 14 | ): SlackAPIClient => { 15 | // Create our base client instance 16 | const baseClient = new BaseSlackAPIClient(token, options); 17 | 18 | // Enrich client w/ a proxy and types to allow all api method calls 19 | const client = ProxifyAndTypeClient(baseClient); 20 | 21 | return client; 22 | }; 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Summary 4 | 5 | 6 | 7 | ### testing 8 | 9 | 10 | 11 | ### Special notes 12 | 13 | 14 | 15 | ### Requirements 16 | 17 | * [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/deno-slack-api/blob/main/.github/CONTRIBUTING.md) and have done my best effort to follow them. 18 | * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). 19 | * [ ] I've ran `deno task test` after making the changes. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for this project 4 | title: '[FEATURE] ' 5 | --- 6 | 7 | <!-- If you have a feature request, please search for it in the [Issues](https://github.com/slackapi/deno-slack-api/issues), and if it isn't already tracked then create a new issue --> 8 | 9 | **Description of the problem being solved** 10 | 11 | <!-- Please describe the problem you want to solve --> 12 | 13 | **Alternative solutions** 14 | 15 | <!-- Please describe the solutions you've considered --> 16 | 17 | **Requirements** 18 | 19 | Please read the [Contributing guidelines](https://github.com/slackapi/deno-slack-api/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. 20 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/user_left_channel.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const UserLeftChannel = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was left. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel type for the channel that was left. Can be one of "public", "private", "mpdm" or "im". 11 | */ 12 | channel_type: "{{data.channel_type}}", 13 | /** 14 | * The event type being invoked. At runtime will always be "slack#/events/user_left_channel". 15 | */ 16 | event_type: "{{data.event_type}}", 17 | /** 18 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who left the channel. 19 | */ 20 | user_id: "{{data.user_id}}", 21 | } as const; 22 | -------------------------------------------------------------------------------- /src/typed-method-types/functions.ts: -------------------------------------------------------------------------------- 1 | import type { BaseResponse } from "../types.ts"; 2 | 3 | type FunctionCompleteSuccessArgs = { 4 | // deno-lint-ignore no-explicit-any 5 | outputs: Record<string, any>; 6 | function_execution_id: string; 7 | // deno-lint-ignore no-explicit-any 8 | [otherOptions: string]: any; 9 | }; 10 | 11 | type FunctionCompleteErrorArgs = { 12 | error: string; 13 | function_execution_id: string; 14 | // deno-lint-ignore no-explicit-any 15 | [otherOptions: string]: any; 16 | }; 17 | 18 | type FunctionCompleteError = { 19 | (args: FunctionCompleteErrorArgs): Promise<BaseResponse>; 20 | }; 21 | 22 | type FunctionCompleteSuccess = { 23 | (args: FunctionCompleteSuccessArgs): Promise<BaseResponse>; 24 | }; 25 | 26 | export type TypedFunctionMethodTypes = { 27 | functions: { 28 | completeError: FunctionCompleteError; 29 | completeSuccess: FunctionCompleteSuccess; 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/user_joined_channel.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const UserJoinedChannel = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was joined. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel type for the channel that was joined. Can be one of "public", "private", "mpdm" or "im". 11 | */ 12 | channel_type: "{{data.channel_type}}", 13 | /** 14 | * The event type being invoked. At runtime will always be "slack#/events/user_joined_channel". 15 | */ 16 | event_type: "{{data.event_type}}", 17 | /** 18 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who joined the channel. 19 | */ 20 | user_id: "{{data.user_id}}", 21 | } as const; 22 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_shared.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelShared = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was shared. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel that was shared. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was shared. Can be one of "public" or "private". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * A unique identifier for the team or workspace that the channel was shared with. 19 | */ 20 | connected_team_id: "{{data.connected_team_id}}", 21 | /** 22 | * The event type being invoked. At runtime will always be "slack#/events/channel_shared". 23 | */ 24 | event_type: "{{data.event_type}}", 25 | } as const; 26 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Introduction 4 | 5 | Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand. 6 | 7 | Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic. 8 | 9 | This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members. 10 | 11 | For more information on our code of conduct, please visit [https://slackhq.github.io/code-of-conduct](https://slackhq.github.io/code-of-conduct) -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_deleted.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelDeleted = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was deleted. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel that was deleted. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was deleted. Can be one of "public" or "private". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * The event type being invoked. At runtime will always be "slack#/events/channel_deleted". 19 | */ 20 | event_type: "{{data.event_type}}", 21 | /** 22 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who deleted the channel. 23 | */ 24 | user_id: "{{data.user_id}}", 25 | } as const; 26 | -------------------------------------------------------------------------------- /.github/workflows/npm.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs a test build for npm against changes on main or PRs 2 | 3 | name: NPM Build 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | runs-on: macos-latest 16 | permissions: 17 | contents: read 18 | steps: 19 | - name: Actions checkout 20 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 21 | with: 22 | persist-credentials: false 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 26 | with: 27 | node-version: latest 28 | registry-url: https://registry.npmjs.org/ 29 | 30 | - name: Setup Deno 31 | uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3 32 | with: 33 | deno-version: v1.x 34 | 35 | - name: Run build_npm.ts 36 | run: deno run -A scripts/build_npm.ts 37 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_renamed.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelRenamed = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was renamed. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The new channel name for the channel that was renamed. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was renamed. Can be one of "public" or "private". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * The event type being invoked. At runtime will always be "slack#/events/channel_renamed". 19 | */ 20 | event_type: "{{data.event_type}}", 21 | /** 22 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who renamed the channel. 23 | */ 24 | user_id: "{{data.user_id}}", 25 | } as const; 26 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_archived.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelArchived = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was archived. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel that was archived. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was archived. Can be one of `public`, `private`, `im` or `mpim`. 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * The event type being invoked. At runtime will always be "slack#/events/channel_archived". 19 | */ 20 | event_type: "{{data.event_type}}", 21 | /** 22 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who archived the channel. 23 | */ 24 | user_id: "{{data.user_id}}", 25 | } as const; 26 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_unarchived.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelUnarchived = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was unarchived. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel that was unarchived. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was unarchived. Can be one of "public" or "private". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * The event type being invoked. At runtime will always be "slack#/events/channel_unarchived". 19 | */ 20 | event_type: "{{data.event_type}}", 21 | /** 22 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who unarchived the channel. 23 | */ 24 | user_id: "{{data.user_id}}", 25 | } as const; 26 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/shared_channel_invite_received.ts: -------------------------------------------------------------------------------- 1 | import { Invite } from "./common-objects/shared_channel_invite.ts"; 2 | import base_trigger_data from "./common-objects/all_triggers.ts"; 3 | 4 | export const SharedChannelInviteReceived = { 5 | ...base_trigger_data, 6 | /** 7 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} being shared. 8 | */ 9 | channel_id: "{{data.channel_id}}", 10 | /** 11 | * The channel name for the channel being shared. 12 | */ 13 | channel_name: "{{data.channel_name}}", 14 | /** 15 | * The channel type for the channel being shared. Can be one of "public", "private", "mpdm" or "im". 16 | */ 17 | channel_type: "{{data.channel_type}}", 18 | /** 19 | * The event type being invoked. At runtime will always be "slack#/events/shared_channel_invite_received". 20 | */ 21 | event_type: "{{data.event_type}}", 22 | /** 23 | * Details for the invite itself. 24 | */ 25 | invite: Invite, 26 | } as const; 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about this project 4 | title: '[QUERY] <title>' 5 | label: question 6 | --- 7 | 8 | <!-- If you have a question, please search for it in the [Issues](https://github.com/slackapi/deno-slack-api/issues), and if it isn't already tracked then create a new issue --> 9 | 10 | **Question** 11 | 12 | <!-- A clear and concise question with steps to reproduce --> 13 | 14 | **Context** 15 | 16 | <!-- Any additional context to your question --> 17 | 18 | **Environment** 19 | 20 | <!-- Paste the output of `cat import_map.json | grep deno-slack` --> 21 | <!-- Paste the output of `deno --version` --> 22 | <!-- Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS --> 23 | 24 | **Requirements** 25 | 26 | Please read the [Contributing guidelines](https://github.com/slackapi/deno-slack-api/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. 27 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/reaction_removed.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ReactionRemoved = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the emoji reaction was removed from. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The event type being invoked. At runtime will always be "slack#/events/reaction_removed". 11 | */ 12 | event_type: "{{data.event_type}}", 13 | /** 14 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message whose reaction is being removed from was sent. 15 | */ 16 | message_ts: "{{data.message_ts}}", 17 | /** 18 | * A string representing the emoji name. 19 | */ 20 | reaction: "{{data.reaction}}", 21 | /** 22 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who removed the emoji reaction. 23 | */ 24 | user_id: "{{data.user_id}}", 25 | } as const; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Slack Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/generated/method-types/conversations.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type ConversationsAPIType = { 7 | acceptSharedInvite: SlackAPIMethod; 8 | approveSharedInvite: SlackAPIMethod; 9 | archive: SlackAPIMethod; 10 | canvases: { 11 | create: SlackAPIMethod; 12 | }; 13 | close: SlackAPIMethod; 14 | create: SlackAPIMethod; 15 | declineSharedInvite: SlackAPIMethod; 16 | externalInvitePermissions: { 17 | set: SlackAPIMethod; 18 | }; 19 | history: SlackAPICursorPaginatedMethod; 20 | info: SlackAPIMethod; 21 | invite: SlackAPIMethod; 22 | inviteShared: SlackAPIMethod; 23 | join: SlackAPIMethod; 24 | kick: SlackAPIMethod; 25 | leave: SlackAPIMethod; 26 | list: SlackAPICursorPaginatedMethod; 27 | listConnectInvites: SlackAPICursorPaginatedMethod; 28 | mark: SlackAPIMethod; 29 | members: SlackAPICursorPaginatedMethod; 30 | open: SlackAPIMethod; 31 | rename: SlackAPIMethod; 32 | replies: SlackAPICursorPaginatedMethod; 33 | setPurpose: SlackAPIMethod; 34 | setTopic: SlackAPIMethod; 35 | unarchive: SlackAPIMethod; 36 | }; 37 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_unshared.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelUnshared = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was unshared. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel that was unshared. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was unshared. Can be one of "public" or "private". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * A unique identifier for the team or workspace that the channel was unshared with. 19 | */ 20 | disconnected_team_id: "{{data.disconnected_team_id}}", 21 | /** 22 | * The event type being invoked. At runtime will always be "slack#/events/channel_unshared". 23 | */ 24 | event_type: "{{data.event_type}}", 25 | /** 26 | * Whether the channel was externally shared or not. 27 | */ 28 | is_ext_shared: "{{data.is_ext_shared}}", 29 | } as const; 30 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/trigger-filter.ts: -------------------------------------------------------------------------------- 1 | export const TriggerFilterOperatorType = { 2 | AND: "AND", 3 | OR: "OR", 4 | NOT: "NOT", 5 | } as const; 6 | 7 | export type FilterType = { 8 | /** @example 1 */ 9 | version: number; 10 | root: TriggerFilterDefinition; 11 | }; 12 | 13 | type TriggerFilterDefinition = 14 | | TriggerFilterBooleanLogic 15 | | TriggerFilterComparator; 16 | 17 | // Boolean Logic 18 | 19 | type TriggerFilterBooleanLogic = { 20 | /** 21 | * @description The logical operator to run against your filter inputs 22 | * @example "AND" */ 23 | operator: TriggerFilterOperatorTypeValues; 24 | /** @description The filter inputs that contain filter statement definitions */ 25 | inputs: [TriggerFilterDefinition, ...TriggerFilterDefinition[]]; 26 | statement?: never; 27 | }; 28 | 29 | type TriggerFilterOperatorTypeValues = 30 | typeof TriggerFilterOperatorType[keyof typeof TriggerFilterOperatorType]; 31 | 32 | // Comparator 33 | 34 | type TriggerFilterComparator = { 35 | /** @description Comparison of values */ 36 | statement: string; 37 | operator?: never; 38 | inputs?: never; 39 | }; 40 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/channel_created.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ChannelCreated = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} that was created. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel that was created. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel that was created. Can be one of "public" or "private". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the channel was created. 19 | */ 20 | created: "{{data.created}}", 21 | /** 22 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who created the channel. 23 | */ 24 | creator_id: "{{data.creator_id}}", 25 | /** 26 | * The event type being invoked. At runtime will always be "slack#/events/channel_created". 27 | */ 28 | event_type: "{{data.event_type}}", 29 | } as const; 30 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/shortcut.ts: -------------------------------------------------------------------------------- 1 | import type { BaseResponse } from "../../../types.ts"; 2 | import type { BaseTriggerResponse } from "./base_response.ts"; 3 | import type { 4 | BaseTrigger, 5 | FailedTriggerResponse, 6 | TriggerTypes, 7 | WorkflowSchema, 8 | } from "./mod.ts"; 9 | 10 | export type ShortcutTrigger<WorkflowDefinition extends WorkflowSchema> = 11 | & BaseTrigger<WorkflowDefinition> 12 | & { 13 | type: typeof TriggerTypes.Shortcut; 14 | shortcut?: { 15 | button_text: string; 16 | }; 17 | }; 18 | 19 | export type ShortcutTriggerResponse< 20 | WorkflowDefinition extends WorkflowSchema, 21 | > = Promise< 22 | | ShortcutResponse<WorkflowDefinition> 23 | | FailedTriggerResponse 24 | >; 25 | export type ShortcutResponse< 26 | WorkflowDefinition extends WorkflowSchema, 27 | > = 28 | & BaseResponse 29 | & { 30 | trigger: ShortcutTriggerResponseObject<WorkflowDefinition>; 31 | }; 32 | export type ShortcutTriggerResponseObject< 33 | WorkflowDefinition extends WorkflowSchema, 34 | > = 35 | & BaseTriggerResponse<WorkflowDefinition> 36 | & { 37 | /** 38 | * @description A URL that will trip a shortcut trigger 39 | */ 40 | shortcut_url?: string; 41 | }; 42 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/dnd_updated.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | const DndStatus = { 4 | ...base_trigger_data, 5 | /** 6 | * Whether Do Not Disturb is enabled or not. 7 | */ 8 | dnd_enabled: "{{data.dnd_status.dnd_enabled}}", 9 | /** 10 | * A UNIX timestamp representing the next Do Not Disturb window end time. 11 | */ 12 | next_dnd_end_ts: "{{data.dnd_status.next_dnd_end_ts}}", 13 | /** 14 | * A UNIX timestamp representing the next Do Not Disturb window start time. 15 | */ 16 | next_dnd_start_ts: "{{data.dnd_status.next_dnd_start_ts}}", 17 | }; 18 | 19 | Object.defineProperty(DndStatus, "toJSON", { 20 | value: () => "{{data.dnd_status}}", 21 | }); 22 | 23 | export const DndUpdated = { 24 | /** 25 | * Do Not Disturb object containing all DND-related data. 26 | */ 27 | dnd_status: DndStatus, 28 | /** 29 | * The event type being invoked. At runtime will always be "slack#/events/dnd_updated". 30 | */ 31 | event_type: "{{data.event_type}}", 32 | /** 33 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} whose DND status was updated. 34 | */ 35 | user_id: "{{data.user_id}}", 36 | } as const; 37 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/pin_added.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const PinAdded = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the message was pinned. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel where the message was pinned. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel where the message was pinned. Can be one of "public", "private", "mpdm" or "im". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * The event type being invoked. At runtime will always be "slack#/events/pin_added". 19 | */ 20 | event_type: "{{data.event_type}}", 21 | /** 22 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message being pinned was sent. 23 | */ 24 | message_ts: "{{data.message_ts}}", 25 | /** 26 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who pinned the message. 27 | */ 28 | user_id: "{{data.user_id}}", 29 | } as const; 30 | -------------------------------------------------------------------------------- /src/typed-method-types/typed-method-tests.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { SlackAPI } from "../mod.ts"; 3 | 4 | Deno.test("Custom Type Methods are valid functions", () => { 5 | const client = SlackAPI("test-token"); 6 | 7 | assertEquals(typeof client.apps.datastore.delete, "function"); 8 | assertEquals(typeof client.apps.datastore.bulkDelete, "function"); 9 | assertEquals(typeof client.apps.datastore.get, "function"); 10 | assertEquals(typeof client.apps.datastore.bulkGet, "function"); 11 | assertEquals(typeof client.apps.datastore.put, "function"); 12 | assertEquals(typeof client.apps.datastore.bulkPut, "function"); 13 | assertEquals(typeof client.apps.datastore.update, "function"); 14 | assertEquals(typeof client.apps.datastore.query, "function"); 15 | assertEquals(typeof client.apps.datastore.count, "function"); 16 | assertEquals(typeof client.apps.auth.external.get, "function"); 17 | assertEquals(typeof client.apps.auth.external.delete, "function"); 18 | assertEquals(typeof client.workflows.triggers.create, "function"); 19 | assertEquals(typeof client.workflows.triggers.list, "function"); 20 | assertEquals(typeof client.workflows.triggers.update, "function"); 21 | assertEquals(typeof client.workflows.triggers.delete, "function"); 22 | }); 23 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/pin_removed.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const PinRemoved = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the message was unpinned. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel name for the channel where the message was unpinned. 11 | */ 12 | channel_name: "{{data.channel_name}}", 13 | /** 14 | * The channel type for the channel where the message was unpinned. Can be one of "public", "private", "mpdm" or "im". 15 | */ 16 | channel_type: "{{data.channel_type}}", 17 | /** 18 | * The event type being invoked. At runtime will always be "slack#/events/pin_removed". 19 | */ 20 | event_type: "{{data.event_type}}", 21 | /** 22 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message being unpinned was sent. 23 | */ 24 | message_ts: "{{data.message_ts}}", 25 | /** 26 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who unpinned the message. 27 | */ 28 | user_id: "{{data.user_id}}", 29 | } as const; 30 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/message_metadata_posted.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const MessageMetadataPosted = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the app that posted metadata. 7 | */ 8 | app_id: "{{data.app_id}}", 9 | /** 10 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the metadata was posted. 11 | */ 12 | channel_id: "{{data.channel_id}}", 13 | /** 14 | * The event type being invoked. At runtime will always be "slack#/events/message_metadata_posted". 15 | */ 16 | event_type: "{{data.event_type}}", 17 | /** 18 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message containing metadata was sent. 19 | */ 20 | message_ts: "{{data.message_ts}}", 21 | /** 22 | * Metadata attached to the message. See {@link https://api.slack.com/metadata Message Metadata documentation} for more details. 23 | */ 24 | metadata: "{{data.metadata}}", 25 | /** 26 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who posted the metadata-containing message. 27 | */ 28 | user_id: "{{data.user_id}}", 29 | } as const; 30 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/emoji_changed.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const EmojiChanged = { 4 | ...base_trigger_data, 5 | /** 6 | * The event type being invoked. At runtime will always be "slack#/events/emoji_changed". 7 | */ 8 | event_type: "{{data.event_type}}", 9 | /** 10 | * The name of the newly-added emoji. Only applies when an emoji was added to the workspace! 11 | */ 12 | name: "{{data.name}}", 13 | /** 14 | * The names of the removed emojis. At runtime this value will always be an array. Only applies when one or more emojis were removed from the workspace! 15 | */ 16 | names: "{{data.names}}", 17 | /** 18 | * The new name of the renamed emoji. Only applies when an emoji was renamed in the workspace! 19 | */ 20 | new_name: "{{data.new_name}}", 21 | /** 22 | * The old name of the renamed emoji. Only applies when an emoji was renamed in the workspace! 23 | */ 24 | old_name: "{{data.old_name}}", 25 | /** 26 | * The emoji change type. At runtime this value will be one of 'add', 'remove', or 'rename'. 27 | */ 28 | subtype: "{{data.subtype}}", 29 | /** 30 | * The URL of the emoji picture. Only applies when an emoji was added to or renamed in the workspace! 31 | */ 32 | value: "{{data.value}}", 33 | } as const; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug encountered while using this project 4 | title: '[BUG] <title>' 5 | --- 6 | 7 | <!-- If you find a bug, please search for it in the [Issues](https://github.com/slackapi/deno-slack-api/issues), and if it isn't already tracked then create a new issue --> 8 | 9 | **The `deno-slack` versions** 10 | 11 | <!-- Paste the output of `cat import_map.json | grep deno-slack` --> 12 | 13 | **Deno runtime version** 14 | 15 | <!-- Paste the output of `deno --version` --> 16 | 17 | **OS info** 18 | 19 | <!-- Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS --> 20 | 21 | **Describe the bug** 22 | 23 | <!-- A clear and concise description of what the bug is. --> 24 | 25 | **Steps to reproduce** 26 | 27 | <!-- Share the commands to run, source code, and project settings --> 28 | 1. 29 | 2. 30 | 3. 31 | 32 | **Expected result** 33 | 34 | <!-- Tell what you expected to happen --> 35 | 36 | **Actual result** 37 | 38 | <!-- Tell what actually happened with logs, screenshots --> 39 | 40 | **Requirements** 41 | 42 | Please read the [Contributing guidelines](https://github.com/slackapi/deno-slack-api/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. 43 | -------------------------------------------------------------------------------- /src/typed-method-types/mod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is intended as a way to add custom types for specific methods beyond the autogenerated method types 3 | * It is meant to be additive to the SlackClient type 4 | */ 5 | import type { TypedAppsMethodTypes } from "./apps.ts"; 6 | import type { TypedChatMethodTypes } from "./chat.ts"; 7 | import type { TypedFunctionMethodTypes } from "./functions.ts"; 8 | import type { TypedWorkflowsMethodTypes } from "./workflows/mod.ts"; 9 | 10 | /** 11 | * When adding a new type here, run `scripts/src/generate` 12 | * to remove any methods that may have gotten entered by that script 13 | */ 14 | export const methodsWithCustomTypes = [ 15 | "apps.datastore.delete", 16 | "apps.datastore.bulkDelete", 17 | "apps.datastore.get", 18 | "apps.datastore.bulkGet", 19 | "apps.datastore.put", 20 | "apps.datastore.bulkPut", 21 | "apps.datastore.update", 22 | "apps.datastore.query", 23 | "apps.datastore.count", 24 | "apps.auth.external.get", 25 | "apps.auth.external.delete", 26 | "chat.postMessage", 27 | "functions.completeSuccess", 28 | "functions.completeError", 29 | "workflows.triggers.create", 30 | "workflows.triggers.list", 31 | "workflows.triggers.update", 32 | "workflows.triggers.delete", 33 | ]; 34 | 35 | export type TypedSlackAPIMethodsType = 36 | & TypedAppsMethodTypes 37 | & TypedChatMethodTypes 38 | & TypedFunctionMethodTypes 39 | & TypedWorkflowsMethodTypes; 40 | -------------------------------------------------------------------------------- /testing/http.ts: -------------------------------------------------------------------------------- 1 | import { type Stub, stub } from "@std/testing/mock"; 2 | 3 | /** 4 | * Creates a simple fetch stub that replaces the global fetch implementation with a mock. 5 | * 6 | * @param matches - A function that validates the request object 7 | * @param response - The Response object to return from the stubbed fetch call 8 | * @returns A Stub object that can be used to restore the original fetch implementation 9 | * 10 | * @example With 'using' statement 11 | * ```typescript 12 | * { 13 | * using fetchStub = stubFetch( 14 | * (req) => { 15 | * assertEquals(req.url, "https://api.example.com/data"); 16 | * assertEquals(req.method, "POST"); 17 | * }, 18 | * new Response(JSON.stringify({ result: "success" })) 19 | * ); 20 | * 21 | * // Test code - stub automatically cleaned up at end of block scope 22 | * } 23 | * ``` 24 | */ 25 | export function stubFetch( 26 | matches: (req: Request) => void | Promise<void>, 27 | response: Response, 28 | ): Stub { 29 | return stub( 30 | globalThis, 31 | "fetch", 32 | async (url: string | URL | Request, options?: RequestInit) => { 33 | const request = url instanceof Request ? url : new Request(url, options); 34 | const matchesResult = matches(request.clone()); 35 | if (matchesResult instanceof Promise) { 36 | await matchesResult; 37 | } 38 | return Promise.resolve(response.clone()); 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/app_mentioned.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const AppMentioned = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the app being mentioned. 7 | */ 8 | app_id: "{{data.app_id}}", 9 | /** 10 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the app was mentioned. 11 | */ 12 | channel_id: "{{data.channel_id}}", 13 | /** 14 | * The channel name where the app was mentioned. 15 | */ 16 | channel_name: "{{data.channel_name}}", 17 | /** 18 | * The channel type where the app was mentioned. Can be one of "public", "private", "mpdm" or "im". 19 | */ 20 | channel_type: "{{data.channel_type}}", 21 | /** 22 | * The event type being invoked. At runtime will always be "slack#/events/app_mentioned". 23 | */ 24 | event_type: "{{data.event_type}}", 25 | /** 26 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message mentioning the app was sent. 27 | */ 28 | message_ts: "{{data.message_ts}}", 29 | /** 30 | * The text in the message containing the app mention. 31 | */ 32 | text: "{{data.text}}", 33 | /** 34 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who mentioned the app. 35 | */ 36 | user_id: "{{data.user_id}}", 37 | } as const; 38 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | // ex. scripts/build_npm.ts 2 | import { emptyDir } from "@std/fs"; 3 | import { build } from "@deno/dnt"; 4 | 5 | await emptyDir("./npm"); 6 | 7 | await build({ 8 | typeCheck: false, 9 | test: false, 10 | entryPoints: ["./src/mod.ts"], 11 | outDir: "./npm", 12 | // ensures that the emitted package is compatible with node v14 later 13 | compilerOptions: { 14 | lib: ["ES2022.Error"], // fix ErrorOptions not exported in ES2020 15 | target: "ES2020", 16 | }, 17 | shims: { 18 | // see JS docs for overview and more options 19 | deno: true, 20 | // Shim fetch, File, FormData, Headers, Request, and Response 21 | undici: true, 22 | }, 23 | package: { 24 | // package.json properties 25 | name: "@slack/deno-slack-api", 26 | version: Deno.args[0], 27 | description: 28 | "Official library for using Deno Slack API client in node Slack apps", 29 | license: "MIT", 30 | repository: { 31 | type: "git", 32 | url: "git+https://github.com/slackapi/deno-slack-api.git", 33 | }, 34 | bugs: { 35 | url: "https://github.com/slackapi/deno-slack-api/issues", 36 | }, 37 | // sets the minimum engine to node v14 38 | // as of writing, dnt transpilation-generated code 39 | // seems to only be able to successfully compile as far back ES2020 40 | engines: { 41 | "node": ">=14.20.1", 42 | "npm": ">=6.14.15", 43 | }, 44 | }, 45 | }); 46 | 47 | // post build steps 48 | Deno.copyFileSync("README.md", "npm/README.md"); 49 | -------------------------------------------------------------------------------- /scripts/src/imports/update_test.ts: -------------------------------------------------------------------------------- 1 | import { isHttpError } from "@std/http/http-errors"; 2 | import { assertEquals, assertRejects } from "@std/assert"; 3 | import { apiDepsIn } from "./update.ts"; 4 | import { stubFetch } from "../../../testing/http.ts"; 5 | 6 | const depsTsMock = 7 | `export { SlackAPI } from "https://deno.land/x/deno_slack_api@2.1.0/mod.ts"; 8 | export type {SlackAPIClient, Trigger} from "https://deno.land/x/deno_slack_api@2.2.0/types.ts";`; 9 | 10 | Deno.test("apiDepsIn should return a list of the api module urls used by a module", async () => { 11 | using _fetchStub = stubFetch((req) => { 12 | assertEquals( 13 | req.url, 14 | "https://deno.land/x/deno_slack_sdk@x.x.x/deps.ts?source,file", 15 | ); 16 | }, new Response(depsTsMock)); 17 | 18 | const apiDeps = await apiDepsIn( 19 | "https://deno.land/x/deno_slack_sdk@x.x.x/", 20 | ); 21 | 22 | assertEquals( 23 | apiDeps, 24 | new Set([ 25 | "https://deno.land/x/deno_slack_api@2.1.0/", 26 | "https://deno.land/x/deno_slack_api@2.2.0/", 27 | ]), 28 | ); 29 | }); 30 | 31 | Deno.test("apiDepsIn should throw http error on response not ok", async () => { 32 | using _fetchStub = stubFetch( 33 | (req) => { 34 | assertEquals(req.url, "https://deno.land/x/deno_slack_sdk@x.x.x/deps.ts"); 35 | }, 36 | new Response("error", { status: 500 }), 37 | ); 38 | 39 | const error = await assertRejects(() => 40 | apiDepsIn("https://deno.land/x/deno_slack_sdk@x.x.x/") 41 | ); 42 | isHttpError(error); 43 | }); 44 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://deno.land/x/deno/cli/schemas/config-file.v1.json", 3 | "fmt": { 4 | "include": [ 5 | "src", 6 | "scripts", 7 | "docs", 8 | "README.md", 9 | ".github/maintainers_guide.md", 10 | ".github/CONTRIBUTING.md", 11 | "testing" 12 | ] 13 | }, 14 | "lint": { 15 | "include": ["src", "scripts", "testing"] 16 | }, 17 | "publish": { 18 | "exclude": ["./README.md"] 19 | }, 20 | "test": { 21 | "include": ["src", "scripts", "testing"] 22 | }, 23 | "tasks": { 24 | "test": "deno fmt --check && deno lint && deno test", 25 | "generate-lcov": "rm -rf .coverage && deno test --reporter=dot --coverage=.coverage && deno coverage --exclude=fixtures --exclude=test --exclude=scripts --exclude=src/generated --lcov --output=lcov.info .coverage", 26 | "test:coverage": "deno task generate-lcov && deno coverage --exclude=fixtures --exclude=test --exclude=scripts --exclude=src/generated .coverage src" 27 | }, 28 | "lock": false, 29 | "name": "@slack/api", 30 | "version": "2.9.0", 31 | "exports": { 32 | ".": "./src/mod.ts", 33 | "./types": "./src/types.ts" 34 | }, 35 | "imports": { 36 | "@deno/dnt": "jsr:@deno/dnt@^0.41", 37 | "@std/assert": "jsr:@std/assert@^1", 38 | "@std/cli": "jsr:@std/cli@^1", 39 | "@std/fs": "jsr:@std/fs@^1", 40 | "@std/http": "jsr:@std/http@^0.206.0", 41 | "@std/path": "jsr:@std/path@^1.0.9", 42 | "@std/testing": "jsr:@std/testing@^1", 43 | "@std/text": "jsr:@std/text@^1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/message_posted.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const MessagePosted = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where message was posted. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The channel type where the message was posted. Can be one of "public", "private", "mpdm" or "im". 11 | */ 12 | channel_type: "{{data.channel_type}}", 13 | /** 14 | * The event type being invoked. At runtime will always be "slack#/events/message_posted". 15 | */ 16 | event_type: "{{data.event_type}}", 17 | /** 18 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message was sent. In the case this is a threaded message, this property becomes the `message_ts` of the parent message. 19 | */ 20 | message_ts: "{{data.message_ts}}", 21 | /** 22 | * The text in the message. 23 | */ 24 | text: "{{data.text}}", 25 | /** 26 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the threaded message was sent. At runtime this value may not exist if the message in question is not a threaded message! 27 | */ 28 | thread_ts: "{{data.thread_ts}}", 29 | /** 30 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who posted the message. 31 | */ 32 | user_id: "{{data.user_id}}", 33 | } as const; 34 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/webhook.ts: -------------------------------------------------------------------------------- 1 | import type { BaseResponse } from "../../../types.ts"; 2 | import type { BaseTriggerResponse } from "./base_response.ts"; 3 | import type { 4 | BaseTrigger, 5 | FailedTriggerResponse, 6 | TriggerTypes, 7 | WorkflowSchema, 8 | } from "./mod.ts"; 9 | import type { FilterType } from "./trigger-filter.ts"; 10 | 11 | export type WebhookTrigger<WorkflowDefinition extends WorkflowSchema> = 12 | & BaseTrigger<WorkflowDefinition> 13 | & { 14 | type: typeof TriggerTypes.Webhook; 15 | webhook?: { 16 | /** @description Defines the condition in which this webhook trigger should execute the workflow */ 17 | filter?: FilterType; 18 | }; 19 | }; 20 | export type WebhookTriggerResponse< 21 | WorkflowDefinition extends WorkflowSchema, 22 | > = Promise< 23 | WebhookResponse<WorkflowDefinition> | FailedTriggerResponse 24 | >; 25 | 26 | export type WebhookResponse< 27 | WorkflowDefinition extends WorkflowSchema, 28 | > = 29 | & BaseResponse 30 | & { 31 | trigger: WebhookTriggerResponseObject<WorkflowDefinition>; 32 | }; 33 | 34 | export type WebhookTriggerResponseObject< 35 | WorkflowDefinition extends WorkflowSchema, 36 | > = 37 | & BaseTriggerResponse<WorkflowDefinition> 38 | & { 39 | /** 40 | * @description The filter object used to define the webhook trigger 41 | */ 42 | filter?: FilterType; 43 | /** 44 | * @description The URL used to trip the webhook trigger 45 | */ 46 | webhook_url?: string; 47 | // deno-lint-ignore no-explicit-any 48 | [otherOptions: string]: any; 49 | }; 50 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/tests/fixtures/workflows.ts: -------------------------------------------------------------------------------- 1 | export type ExampleWorkflow = { 2 | callback_id: "example"; 3 | title: "Example"; 4 | }; 5 | 6 | export type NoInputWorkflow = ExampleWorkflow & { 7 | input_parameters: { 8 | properties: Record<never, never>; 9 | required: never[]; 10 | }; 11 | }; 12 | 13 | export type OptionalInputWorkflow = ExampleWorkflow & { 14 | input_parameters: { 15 | properties: { 16 | optional: { 17 | type: "string"; 18 | }; 19 | }; 20 | required: []; 21 | }; 22 | }; 23 | 24 | export type OptionalCustomizableInputWorkflow = ExampleWorkflow & { 25 | input_parameters: { 26 | properties: { 27 | customizable: { 28 | type: "boolean"; 29 | }; 30 | }; 31 | required: []; 32 | }; 33 | }; 34 | 35 | export type RequiredInputWorkflow = ExampleWorkflow & { 36 | input_parameters: { 37 | properties: { 38 | required: { 39 | type: "string"; 40 | }; 41 | }; 42 | required: ["required"]; 43 | }; 44 | }; 45 | 46 | export type MixedInputWorkflow = ExampleWorkflow & { 47 | input_parameters: { 48 | properties: { 49 | required: { 50 | type: "string"; 51 | }; 52 | optional: { 53 | type: "string"; 54 | }; 55 | customizable: { 56 | type: "boolean"; 57 | }; 58 | }; 59 | required: ["required"]; 60 | }; 61 | }; 62 | 63 | export type CustomizableInputWorkflow = ExampleWorkflow & { 64 | input_parameters: { 65 | properties: { 66 | customizable: { 67 | type: "boolean"; 68 | }; 69 | }; 70 | required: ["customizable"]; 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/user_joined_team.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | const User = { 4 | /** 5 | * The display name of the user who joined, as they chose upon registering to the workspace. 6 | */ 7 | display_name: "{{data.user.display_name}}", 8 | /** 9 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who joined the workspace or team. 10 | */ 11 | id: "{{data.user.id}}", 12 | /** 13 | * Whether the user that joined is a bot or not. 14 | */ 15 | is_bot: "{{data.user.is_bot}}", 16 | /** 17 | * The name of the user who joined (usually based on the user's e-mail) 18 | */ 19 | name: "{{data.user.name}}", 20 | /** 21 | * The real name of the user who joined. May not be available; in such situations will match the display_name. 22 | */ 23 | real_name: "{{data.user.real_name}}", 24 | /** 25 | * A unique identifier for the Slack workspace or team that was joined. 26 | */ 27 | team_id: "{{data.user.team_id}}", 28 | /** 29 | * The timezone of the user who joined, in TZ identifier format. 30 | * @example "America/Toronto" 31 | */ 32 | timezone: "{{data.user.timezone}}", 33 | } as const; 34 | 35 | Object.defineProperty(User, "toJSON", { value: () => "{{data.user}}" }); 36 | 37 | export const UserJoinedTeam = { 38 | ...base_trigger_data, 39 | /** 40 | * The event type being invoked. At runtime will always be "slack#/events/user_joined_channel". 41 | */ 42 | event_type: "{{data.event_type}}", 43 | /** 44 | * An object containing the user's details. 45 | */ 46 | user: User, 47 | } as const; 48 | -------------------------------------------------------------------------------- /src/base-client-helpers.ts: -------------------------------------------------------------------------------- 1 | const API_VERSION_REGEX = /\/deno_slack_api@(.*)\//; 2 | 3 | export function getUserAgent() { 4 | const userAgents = []; 5 | userAgents.push(`Deno/${Deno.version.deno}`); 6 | userAgents.push(`OS/${Deno.build.os}`); 7 | userAgents.push( 8 | `deno-slack-api/${_internals.getModuleVersion()}`, 9 | ); 10 | return userAgents.join(" "); 11 | } 12 | 13 | function getModuleVersion(): string | undefined { 14 | const url = _internals.getModuleUrl(); 15 | // Insure this module is sourced from https://deno.land/x/deno_slack_api 16 | if (url.host === "deno.land") { 17 | return url.pathname.match(API_VERSION_REGEX)?.at(1); 18 | } 19 | return undefined; 20 | } 21 | 22 | function getModuleUrl(): URL { 23 | return new URL(import.meta.url); 24 | } 25 | 26 | // Serialize an object into a string so as to be compatible with x-www-form-urlencoded payloads 27 | export function serializeData(data: Record<string, unknown>): URLSearchParams { 28 | const encodedData: Record<string, string> = {}; 29 | Object.entries(data).forEach(([key, value]) => { 30 | // Objects/arrays, numbers and booleans get stringified 31 | // Slack API accepts JSON-stringified-and-url-encoded payloads for objects/arrays 32 | // Inspired by https://github.com/slackapi/node-slack-sdk/blob/%40slack/web-api%406.7.2/packages/web-api/src/WebClient.ts#L452-L528 33 | 34 | // Skip properties with undefined values. 35 | if (value === undefined) return; 36 | 37 | const serializedValue: string = typeof value !== "string" 38 | ? JSON.stringify(value) 39 | : value; 40 | encodedData[key] = serializedValue; 41 | }); 42 | 43 | return new URLSearchParams(encodedData); 44 | } 45 | 46 | export const _internals = { getModuleVersion, getModuleUrl }; 47 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/reaction_added.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./common-objects/all_triggers.ts"; 2 | 3 | export const ReactionAdded = { 4 | ...base_trigger_data, 5 | /** 6 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the emoji reaction was added to. 7 | */ 8 | channel_id: "{{data.channel_id}}", 9 | /** 10 | * The event type being invoked. At runtime will always be "slack#/events/reaction_added". 11 | */ 12 | event_type: "{{data.event_type}}", 13 | /** 14 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who sent the message that was reacted to. 15 | */ 16 | item_user: "{{data.item_user}}", 17 | /** 18 | * A {@link https://api.slack.com/automation/types#message-context Message Context} object representing the message being reacted to. 19 | */ 20 | message_context: "{{data.message_context}}", 21 | /** 22 | * Link to the message that was reacted to. 23 | */ 24 | message_link: "{{data.message_link}}", 25 | /** 26 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the message being reacted to was sent. 27 | */ 28 | message_ts: "{{data.message_ts}}", 29 | /** 30 | * Link to the parent of the message that was reacted to. Only available if reaction was added to a threaded reply. 31 | */ 32 | parent_message_link: "{{data.parent_message_link}}", 33 | /** 34 | * A string representing the emoji name. 35 | */ 36 | reaction: "{{data.reaction}}", 37 | /** 38 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who reacted with the emoji. 39 | */ 40 | user_id: "{{data.user_id}}", 41 | } as const; 42 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # deno-slack-api 2 | 3 | [![codecov](https://codecov.io/gh/slackapi/deno-slack-api/graph/badge.svg?token=QKJCI23P5S)](https://codecov.io/gh/slackapi/deno-slack-api) 4 | 5 | Slack API Client for Deno Run on Slack projects 6 | 7 | ```ts 8 | import { SlackAPI } from "jsr:@slack/api"; 9 | 10 | const client = SlackAPI(token); 11 | 12 | // ...or create a client with options 13 | const client = SlackAPI(token, { 14 | slackApiUrl: "...", 15 | }); 16 | 17 | await client.chat.postMessage({ 18 | text: "hello there", 19 | channel: "...", 20 | }); 21 | 22 | // respond to a response_url 23 | await client.response("...", payload); 24 | 25 | // use apiCall() w/ method name 26 | await client.apiCall("chat.postMessage", { 27 | text: "hello there", 28 | channel: "...", 29 | }); 30 | ``` 31 | 32 | ## Requirements 33 | 34 | A recent version of `deno`. 35 | 36 | ## Versioning 37 | 38 | Releases for this repository follow the [SemVer](https://semver.org/) versioning 39 | scheme. The SDK's contract is determined by the top-level exports from 40 | `src/mod.ts` and `src/types.ts`. Exports not included in these files are deemed 41 | internal and any modifications will not be treated as breaking changes. As such, 42 | internal exports should be treated as unstable and used at your own risk. 43 | 44 | ## Running Tests 45 | 46 | If you make changes to this repo, or just want to make sure things are working 47 | as desired, you can run: 48 | 49 | deno task test 50 | 51 | To get a full test coverage report, run: 52 | 53 | deno task test:coverage 54 | 55 | --- 56 | 57 | ### Getting Help 58 | 59 | We welcome contributions from everyone! Please check out our 60 | [Contributor's Guide](https://github.com/slackapi/deno-slack-api/blob/main/.github/CONTRIBUTING.md) 61 | for how to contribute in a helpful and collaborative way. 62 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Code Generation 2 | 3 | A majority of the API client code in this project is generated from code in this 4 | directory. 5 | 6 | For details, please read through the code under `scripts/src/generate.ts`, but a 7 | rough overview of how this works is: 8 | 9 | 1. `scripts/src/public-api-methods.ts` contains a list of API methods that code 10 | will be generated for. 11 | 2. API methods follow a dot-notation "node"-based format that can be interpreted 12 | as a kind of tree, e.g. `admin.apps.approve` or `admin.apps.requests.list`. 13 | Each word, separated by a period, in the full API "path" name can be 14 | considered as a node with zero or more child nodes. For example, building off 15 | the previous two API name examples provided, `admin` is a node with a child 16 | node `apps`, and `apps` is a node with two child nodes `approve` and 17 | `requests`. `approve` is a leaf node that is an API method, whereas 18 | `requests` is a node which itself has one further child node `list`; finally, 19 | `list` is a leaf node that is an invokable API method. In this way, we model 20 | the API using a recursive, node-based construct. 21 | 3. `scripts/src/api-method-nodes.ts` contains the code modeling this recursive 22 | node-based approach. This file also contains a hard-coded list of API method 23 | leaf node names that map to API methods that return lists of objects. In a 24 | majority of cases, these API methods support cursor-based pagination 25 | (however, there are a few exceptions to this rule, which is also hard-coded). 26 | In this way we are able to mix in different types that enhance or expand on 27 | the API method parameters and response properties to account for different 28 | patterns of using the APIs (such as cursor-based pagination). In the future, 29 | this approach can be used to model other kinds of generic patterns of use of 30 | Slack's APIs. 31 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/tests/trigger-filter_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertExists } from "@std/assert"; 2 | import { 3 | type FilterType, 4 | TriggerFilterOperatorType, 5 | } from "../trigger-filter.ts"; 6 | 7 | Deno.test("Trigger Filters can use a single statement", () => { 8 | const filter: FilterType = { 9 | version: 1, 10 | root: { 11 | statement: "1 === 1", 12 | }, 13 | }; 14 | assertEquals(filter.root.statement, "1 === 1"); 15 | }); 16 | 17 | Deno.test("Trigger Filters can use simple logical operators", () => { 18 | const filter: FilterType = { 19 | version: 1, 20 | root: { 21 | operator: "OR", 22 | inputs: [{ 23 | statement: "1 === 2", 24 | }, { 25 | statement: "2 === 2", 26 | }], 27 | }, 28 | }; 29 | assertEquals(filter.root.operator, TriggerFilterOperatorType.OR); 30 | assertEquals(filter.root.inputs?.length, 2); 31 | filter.root.inputs?.forEach((input) => { 32 | assertExists(input.statement); 33 | }); 34 | }); 35 | 36 | Deno.test("Trigger Filters can use nested logical operators", () => { 37 | const filter: FilterType = { 38 | version: 1, 39 | root: { 40 | operator: TriggerFilterOperatorType.OR, 41 | inputs: [{ 42 | statement: "1 === 2", 43 | }, { 44 | statement: "2 === 3", 45 | }, { 46 | operator: "AND", 47 | inputs: [{ 48 | statement: "3 === 3", 49 | }, { 50 | operator: "OR", 51 | inputs: [{ 52 | statement: "3 === 4", 53 | }, { 54 | statement: "4 === 4", 55 | }], 56 | }], 57 | }], 58 | }, 59 | }; 60 | assertEquals(filter.root.operator, TriggerFilterOperatorType.OR); 61 | filter.root.inputs?.forEach((input) => { 62 | assertExists(input.statement || (input.operator && input.inputs)); 63 | input.inputs?.forEach((nestedInput) => { 64 | assertExists( 65 | nestedInput.statement || (nestedInput.operator && nestedInput.inputs), 66 | ); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: Deno Format, Lint and Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | # we test on both most recent stable version of deno (v1.x) as well as 17 | # the version of deno used by Run on Slack (as noted in https://api.slack.com/slackcli/metadata.json) 18 | deno-version: 19 | - v1.x 20 | - v1.46.2 21 | - v2.x 22 | permissions: 23 | contents: read 24 | steps: 25 | - name: Setup repo 26 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 27 | with: 28 | persist-credentials: false 29 | - name: Setup Deno 30 | uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3 31 | with: 32 | deno-version: ${{ matrix.deno-version }} 33 | - name: Run formatter, linter and tests 34 | run: deno task test 35 | - name: Generate CodeCov-friendly coverage report 36 | run: deno task generate-lcov 37 | - name: Upload coverage to CodeCov 38 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 39 | if: matrix.deno-version == 'v2.x' 40 | with: 41 | files: ./lcov.info 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | 44 | health-score: 45 | needs: test 46 | permissions: 47 | checks: write 48 | contents: read 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Setup repo 52 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 53 | with: 54 | persist-credentials: false 55 | - name: Report health score 56 | uses: slackapi/slack-health-score@d58a419f15cdaff97e9aa7f09f95772830ab66f7 # v0.1.1 57 | with: 58 | codecov_token: ${{ secrets.FILS_CODECOV_API_TOKEN }} 59 | github_token: ${{ secrets.GITHUB_TOKEN }} 60 | extension: ts 61 | include: src 62 | -------------------------------------------------------------------------------- /src/api-proxy_test.ts: -------------------------------------------------------------------------------- 1 | import { APIProxy } from "./api-proxy.ts"; 2 | import { BaseSlackAPIClient } from "./base-client.ts"; 3 | import type { SlackAPIMethodArgs } from "./types.ts"; 4 | import { assertSpyCall, spy } from "@std/testing/mock"; 5 | 6 | Deno.test("APIProxy", async (t) => { 7 | const baseClient = new BaseSlackAPIClient("test-token"); 8 | const generateClientProxy = (client: BaseSlackAPIClient) => ({ 9 | apiCall: client.apiCall.bind(client), 10 | response: client.response.bind(client), 11 | }); 12 | 13 | await t.step("should proxy legit Slack API calls", async () => { 14 | const clientToProxy = generateClientProxy(baseClient); 15 | 16 | const apiCallHandler = (_method: string, _payload?: SlackAPIMethodArgs) => { 17 | return Promise.resolve({ 18 | ok: true, 19 | toFetchResponse: () => new Response(), 20 | }); 21 | }; 22 | const apiCallHandlerSpy = spy(apiCallHandler); 23 | 24 | const client = APIProxy(clientToProxy, apiCallHandlerSpy); 25 | 26 | const payload = { text: "proxied call", channel: "" }; 27 | await client.chat.postMessage(payload); 28 | 29 | assertSpyCall(apiCallHandlerSpy, 0, { 30 | args: ["chat.postMessage", payload], 31 | }); 32 | 33 | await client.admin.apps.approved.list(); 34 | 35 | assertSpyCall(apiCallHandlerSpy, 1, { 36 | args: ["admin.apps.approved.list", undefined], 37 | }); 38 | }); 39 | 40 | await t.step("should not proxy Promise methods like `then`", () => { 41 | const clientToProxy = generateClientProxy(baseClient); 42 | 43 | const apiCallHandler = (_method: string, _payload?: SlackAPIMethodArgs) => { 44 | return Promise.resolve({ 45 | ok: true, 46 | toFetchResponse: () => new Response(), 47 | }); 48 | }; 49 | const apiCallHandlerSpy = spy(apiCallHandler); 50 | 51 | const client = APIProxy(clientToProxy, apiCallHandlerSpy); 52 | 53 | // re: https://github.com/slackapi/deno-slack-api/issues/107 54 | // @ts-expect-error client does not have `then` but thenable feature detection at runtime may invoke or test for the method 55 | if (client.then) { 56 | throw new Error("APIProxy should have no `then`!"); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/typed-method-types/chat.ts: -------------------------------------------------------------------------------- 1 | import type { BaseResponse } from "../types.ts"; 2 | 3 | type ChatPostMessageOptionalArgs = { 4 | /** @description The formatted text of the message to be published. If blocks are included, this will become the fallback text used in notifications. */ 5 | text?: string; 6 | /** @description A JSON-based array of structured attachments. */ 7 | // deno-lint-ignore no-explicit-any 8 | attachments?: any[]; 9 | /** @description A JSON-based array of structured blocks. */ 10 | // deno-lint-ignore no-explicit-any 11 | blocks?: any[]; 12 | /** @description Provide another message's ts value to make this message a reply. Avoid using a reply's ts value; use its parent instead. */ 13 | thread_ts?: string; 14 | // deno-lint-ignore no-explicit-any 15 | [otherOptions: string]: any; 16 | }; 17 | 18 | type ChatPostMessageOneOfRequired = 19 | & ChatPostMessageOptionalArgs 20 | & Required< 21 | | Pick<ChatPostMessageOptionalArgs, "text"> 22 | | Pick<ChatPostMessageOptionalArgs, "blocks"> 23 | | Pick<ChatPostMessageOptionalArgs, "attachments"> 24 | >; 25 | 26 | type ChatPostMessageArgs = ChatPostMessageOneOfRequired & { 27 | /** @description Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. */ 28 | channel: string; 29 | }; 30 | 31 | type ChatPostMessageSuccessfulResponse = BaseResponse & { 32 | ok: true; 33 | /** @description The channel the message was posted to */ 34 | channel: string; 35 | /** @description The timestamp of when the message was posted */ 36 | ts: string; 37 | // deno-lint-ignore no-explicit-any 38 | message: Record<string, any>; 39 | // deno-lint-ignore no-explicit-any 40 | [otherOptions: string]: any; 41 | }; 42 | 43 | type ChatPostMessageFailedResponse = BaseResponse & { 44 | ok: false; 45 | // deno-lint-ignore no-explicit-any 46 | [otherOptions: string]: any; 47 | }; 48 | 49 | type ChatPostMessageResponse = 50 | | ChatPostMessageSuccessfulResponse 51 | | ChatPostMessageFailedResponse; 52 | 53 | type ChatPostMessage = { 54 | (args: ChatPostMessageArgs): Promise<ChatPostMessageResponse>; 55 | }; 56 | 57 | export type TypedChatMethodTypes = { 58 | chat: { 59 | postMessage: ChatPostMessage; 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /.github/workflows/samples.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs a `deno check` against slack sample apps 2 | name: Samples Integration Type-checking 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | samples: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | sample: 19 | - slack-samples/deno-issue-submission 20 | - slack-samples/deno-starter-template 21 | - slack-samples/deno-blank-template 22 | - slack-samples/deno-message-translator 23 | - slack-samples/deno-request-time-off 24 | - slack-samples/deno-simple-survey 25 | # we test on both most recent stable version of deno (v1.x, v2.x) as well as 26 | # the version of deno used by Run on Slack (as noted in https://api.slack.com/slackcli/metadata.json) 27 | deno-version: 28 | - v1.x 29 | - v1.46.2 30 | - v2.x 31 | permissions: 32 | contents: read 33 | steps: 34 | - name: Setup Deno ${{ matrix.deno-version }} 35 | uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb # v2.0.3 36 | with: 37 | deno-version: ${{ matrix.deno-version }} 38 | 39 | - name: Checkout the api 40 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 41 | with: 42 | path: ./deno-slack-api 43 | persist-credentials: false 44 | - name: Checkout the ${{ matrix.sample }} sample 45 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 46 | with: 47 | repository: ${{ matrix.sample }} 48 | path: ./sample 49 | persist-credentials: false 50 | 51 | - name: Set imports.deno-slack-api/ to ../deno-slack-api/src/ in imports 52 | run: > 53 | deno run 54 | --allow-read --allow-write --allow-net 55 | deno-slack-api/scripts/src/imports/update.ts 56 | --import-file "./sample/deno.jsonc" 57 | --api "./deno-slack-api/" 58 | 59 | - name: Deno check **/*.ts 60 | working-directory: ./sample 61 | run: find . -type f -regex ".*\.ts" | xargs deno check -r 62 | -------------------------------------------------------------------------------- /src/api-proxy.ts: -------------------------------------------------------------------------------- 1 | import type { BaseSlackAPIClient } from "./base-client.ts"; 2 | import type { 3 | BaseResponse, 4 | SlackAPIClient, 5 | SlackAPIMethodArgs, 6 | } from "./types.ts"; 7 | 8 | const DO_NOT_PROXY = ["then"]; 9 | 10 | type APICallback = { 11 | (method: string, payload?: SlackAPIMethodArgs): Promise<BaseResponse>; 12 | }; 13 | 14 | export const ProxifyAndTypeClient = (baseClient: BaseSlackAPIClient) => { 15 | // This callback handles making the correct apiCall() that the Proxy() object defers to 16 | const apiCallHandler = (method: string, payload?: SlackAPIMethodArgs) => { 17 | return baseClient.apiCall(method, payload); 18 | }; 19 | 20 | // Create a subset of the client that we want to wrap our Proxy() around 21 | const clientToProxy = { 22 | setSlackApiUrl: baseClient.setSlackApiUrl.bind(baseClient), 23 | apiCall: baseClient.apiCall.bind(baseClient), 24 | response: baseClient.response.bind(baseClient), 25 | }; 26 | 27 | // Create our proxy, and type it w/ our api method types 28 | const client = APIProxy( 29 | clientToProxy, 30 | apiCallHandler, 31 | ); 32 | return client; 33 | }; 34 | 35 | export const APIProxy = ( 36 | // deno-lint-ignore no-explicit-any 37 | rootClient: any | null, 38 | apiCallback: APICallback, 39 | ...path: (string | undefined)[] 40 | ): SlackAPIClient => { 41 | const method = path.filter(Boolean).join("."); 42 | 43 | // We either proxy the object passed in, which we do for the top level client, 44 | // or a function that wraps the api callback. This allows each node of the 45 | // proxied object to be called, which will attempt an api call using the path as the method 46 | const objectToProxy = rootClient !== null 47 | ? rootClient 48 | : (payload?: SlackAPIMethodArgs) => { 49 | return apiCallback(method, payload); 50 | }; 51 | 52 | const proxy = new Proxy(objectToProxy, { 53 | get(obj, prop) { 54 | // We're attempting to access a property that doesn't exist, so create a new nested proxy 55 | if ( 56 | typeof prop === "string" && !DO_NOT_PROXY.includes(prop) && 57 | !(prop in obj) 58 | ) { 59 | return APIProxy(null, apiCallback, ...path, prop); 60 | } 61 | 62 | // Fallback to trying to access it directly 63 | // deno-lint-ignore no-explicit-any 64 | return Reflect.get.apply(obj, arguments as any); 65 | }, 66 | }); 67 | 68 | return proxy; 69 | }; 70 | -------------------------------------------------------------------------------- /src/generated/method-types/mod.ts: -------------------------------------------------------------------------------- 1 | import type { AdminAPIType } from "./admin.ts"; 2 | import type { ApiAPIType } from "./api.ts"; 3 | import type { AppsAPIType } from "./apps.ts"; 4 | import type { AuthAPIType } from "./auth.ts"; 5 | import type { BookmarksAPIType } from "./bookmarks.ts"; 6 | import type { BotsAPIType } from "./bots.ts"; 7 | import type { CallsAPIType } from "./calls.ts"; 8 | import type { CanvasesAPIType } from "./canvases.ts"; 9 | import type { ChatAPIType } from "./chat.ts"; 10 | import type { ConversationsAPIType } from "./conversations.ts"; 11 | import type { DialogAPIType } from "./dialog.ts"; 12 | import type { DndAPIType } from "./dnd.ts"; 13 | import type { EmojiAPIType } from "./emoji.ts"; 14 | import type { EnterpriseAPIType } from "./enterprise.ts"; 15 | import type { FilesAPIType } from "./files.ts"; 16 | import type { MigrationAPIType } from "./migration.ts"; 17 | import type { OauthAPIType } from "./oauth.ts"; 18 | import type { OpenidAPIType } from "./openid.ts"; 19 | import type { PinsAPIType } from "./pins.ts"; 20 | import type { ReactionsAPIType } from "./reactions.ts"; 21 | import type { RemindersAPIType } from "./reminders.ts"; 22 | import type { RtmAPIType } from "./rtm.ts"; 23 | import type { SearchAPIType } from "./search.ts"; 24 | import type { StarsAPIType } from "./stars.ts"; 25 | import type { TeamAPIType } from "./team.ts"; 26 | import type { ToolingAPIType } from "./tooling.ts"; 27 | import type { UsergroupsAPIType } from "./usergroups.ts"; 28 | import type { UsersAPIType } from "./users.ts"; 29 | import type { ViewsAPIType } from "./views.ts"; 30 | import type { WorkflowsAPIType } from "./workflows.ts"; 31 | 32 | export type SlackAPIMethodsType = { 33 | admin: AdminAPIType; 34 | api: ApiAPIType; 35 | apps: AppsAPIType; 36 | auth: AuthAPIType; 37 | bookmarks: BookmarksAPIType; 38 | bots: BotsAPIType; 39 | calls: CallsAPIType; 40 | canvases: CanvasesAPIType; 41 | chat: ChatAPIType; 42 | conversations: ConversationsAPIType; 43 | dialog: DialogAPIType; 44 | dnd: DndAPIType; 45 | emoji: EmojiAPIType; 46 | enterprise: EnterpriseAPIType; 47 | files: FilesAPIType; 48 | migration: MigrationAPIType; 49 | oauth: OauthAPIType; 50 | openid: OpenidAPIType; 51 | pins: PinsAPIType; 52 | reactions: ReactionsAPIType; 53 | reminders: RemindersAPIType; 54 | rtm: RtmAPIType; 55 | search: SearchAPIType; 56 | stars: StarsAPIType; 57 | team: TeamAPIType; 58 | tooling: ToolingAPIType; 59 | usergroups: UsergroupsAPIType; 60 | users: UsersAPIType; 61 | views: ViewsAPIType; 62 | workflows: WorkflowsAPIType; 63 | }; 64 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/shared_channel_invite_accepted.ts: -------------------------------------------------------------------------------- 1 | import { Invite } from "./common-objects/shared_channel_invite.ts"; 2 | import base_trigger_data from "./common-objects/all_triggers.ts"; 3 | 4 | const AcceptingUser = { 5 | /** 6 | * The display name of the invited user. 7 | */ 8 | display_name: "{{data.accepting_user.display_name}}", 9 | /** 10 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} being invited. 11 | */ 12 | id: "{{data.accepting_user.id}}", 13 | /** 14 | * Whether or not the user being invited is a bot. 15 | */ 16 | is_bot: "{{data.accepting_user.is_bot}}", 17 | /** 18 | * The name of the invited user. 19 | */ 20 | name: "{{data.accepting_user.name}}", 21 | /** 22 | * The real name of the invited user. 23 | */ 24 | real_name: "{{data.accepting_user.real_name}}", 25 | /** 26 | * A unique identifier for the team or workspace the invited user originally belongs to. 27 | */ 28 | team_id: "{{data.accepting_user.team_id}}", 29 | /** 30 | * The timezone of the user who being invited, in TZ identifier format. 31 | * @example "America/Toronto" 32 | */ 33 | timezone: "{{data.accepting_user.timezone}}", 34 | } as const; 35 | 36 | Object.defineProperty(AcceptingUser, "toJSON", { 37 | value: () => "{{data.accepting_user}}", 38 | }); 39 | 40 | export const SharedChannelInviteAccepted = { 41 | ...base_trigger_data, 42 | /** 43 | * Object containing details for the invitee. 44 | */ 45 | accepting_user: AcceptingUser, 46 | /** 47 | * Whether the invite required administrator approval or not. 48 | */ 49 | approval_required: "{{data.approval_required}}", 50 | /** 51 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} being shared. 52 | */ 53 | channel_id: "{{data.channel_id}}", 54 | /** 55 | * The channel name for the channel being shared. 56 | */ 57 | channel_name: "{{data.channel_name}}", 58 | /** 59 | * The channel type for the channel being shared. Can be one of "public", "private", "mpdm" or "im". 60 | */ 61 | channel_type: "{{data.channel_type}}", 62 | /** 63 | * The event type being invoked. At runtime will always be "slack#/events/shared_channel_invite_accepted". 64 | */ 65 | event_type: "{{data.event_type}}", 66 | /** 67 | * Details for the invite itself. 68 | */ 69 | invite: Invite, 70 | /** 71 | * An array of objects containing details for all of the teams or workspaces present in the channel being shared. 72 | */ 73 | teams_in_channel: "{{data.teams_in_channel}}", 74 | } as const; 75 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/shared_channel_invite_declined.ts: -------------------------------------------------------------------------------- 1 | import { Invite } from "./common-objects/shared_channel_invite.ts"; 2 | import base_trigger_data from "./common-objects/all_triggers.ts"; 3 | 4 | const DecliningUser = { 5 | /** 6 | * The display name of the declining user. 7 | */ 8 | display_name: "{{data.declining_user.display_name}}", 9 | /** 10 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} declining the invite. 11 | */ 12 | id: "{{data.declining_user.id}}", 13 | /** 14 | * Whether or not the declining user is a bot. 15 | */ 16 | is_bot: "{{data.declining_user.is_bot}}", 17 | /** 18 | * The name of the declining user. 19 | */ 20 | name: "{{data.declining_user.name}}", 21 | /** 22 | * The real name of the declining user. 23 | */ 24 | real_name: "{{data.declining_user.real_name}}", 25 | /** 26 | * A unique identifier for the team or workspace the declining user originally belongs to. 27 | */ 28 | team_id: "{{data.declining_user.team_id}}", 29 | /** 30 | * The timezone of the declining user, in TZ identifier format. 31 | * @example "America/Toronto" 32 | */ 33 | timezone: "{{data.declining_user.timezone}}", 34 | } as const; 35 | 36 | Object.defineProperty(DecliningUser, "toJSON", { 37 | value: () => "{{data.declining_user}}", 38 | }); 39 | 40 | export const SharedChannelInviteDeclined = { 41 | ...base_trigger_data, 42 | /** 43 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} being shared. 44 | */ 45 | channel_id: "{{data.channel_id}}", 46 | /** 47 | * The channel name for the channel being shared. 48 | */ 49 | channel_name: "{{data.channel_name}}", 50 | /** 51 | * The channel type for the channel being shared. Can be one of "public", "private", "mpdm" or "im". 52 | */ 53 | channel_type: "{{data.channel_type}}", 54 | /** 55 | * A unique identifier for the team or workspace issuing the declination. 56 | */ 57 | declining_team_id: "{{data.declining_team_id}}", 58 | /** 59 | * Object containing details for the administrator declining the invite. 60 | */ 61 | declining_user: DecliningUser, 62 | /** 63 | * The event type being invoked. At runtime will always be "slack#/events/shared_channel_invite_declined". 64 | */ 65 | event_type: "{{data.event_type}}", 66 | /** 67 | * Details for the invite itself. 68 | */ 69 | invite: Invite, 70 | /** 71 | * An array of objects containing details for all of the teams or workspaces present in the channel being shared. 72 | */ 73 | teams_in_channel: "{{data.teams_in_channel}}", 74 | }; 75 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/shared_channel_invite_approved.ts: -------------------------------------------------------------------------------- 1 | import { Invite } from "./common-objects/shared_channel_invite.ts"; 2 | import base_trigger_data from "./common-objects/all_triggers.ts"; 3 | 4 | const ApprovingUser = { 5 | /** 6 | * The display name of the approving user. 7 | */ 8 | display_name: "{{data.approving_user.display_name}}", 9 | /** 10 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} approving the invite. 11 | */ 12 | id: "{{data.approving_user.id}}", 13 | /** 14 | * Whether or not the approving user is a bot. 15 | */ 16 | is_bot: "{{data.approving_user.is_bot}}", 17 | /** 18 | * The name of the approving user. 19 | */ 20 | name: "{{data.approving_user.name}}", 21 | /** 22 | * The real name of the approving user. 23 | */ 24 | real_name: "{{data.approving_user.real_name}}", 25 | /** 26 | * A unique identifier for the team or workspace the approving user originally belongs to. 27 | */ 28 | team_id: "{{data.approving_user.team_id}}", 29 | /** 30 | * The timezone of the approving user, in TZ identifier format. 31 | * @example "America/Toronto" 32 | */ 33 | timezone: "{{data.approving_user.timezone}}", 34 | } as const; 35 | 36 | Object.defineProperty(ApprovingUser, "toJSON", { 37 | value: () => "{{data.approving_user}}", 38 | }); 39 | 40 | export const SharedChannelInviteApproved = { 41 | ...base_trigger_data, 42 | /** 43 | * A unique identifier for the team or workspace issuing the approval. 44 | */ 45 | approving_team_id: "{{data.approving_team_id}}", 46 | /** 47 | * Object containing details for the administrator approving the invite. 48 | */ 49 | approving_user: ApprovingUser, 50 | /** 51 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} being shared. 52 | */ 53 | channel_id: "{{data.channel_id}}", 54 | /** 55 | * The channel name for the channel being shared. 56 | */ 57 | channel_name: "{{data.channel_name}}", 58 | /** 59 | * The channel type for the channel being shared. Can be one of "public", "private", "mpdm" or "im". 60 | */ 61 | channel_type: "{{data.channel_type}}", 62 | /** 63 | * The event type being invoked. At runtime will always be "slack#/events/shared_channel_invite_approved". 64 | */ 65 | event_type: "{{data.event_type}}", 66 | /** 67 | * Details for the invite itself. 68 | */ 69 | invite: Invite, 70 | /** 71 | * An array of objects containing details for all of the teams or workspaces present in the channel being shared. 72 | */ 73 | teams_in_channel: "{{data.teams_in_channel}}", 74 | } as const; 75 | -------------------------------------------------------------------------------- /scripts/src/imports/update.ts: -------------------------------------------------------------------------------- 1 | import { parseArgs } from "@std/cli/parse-args"; 2 | import { createHttpError } from "@std/http/http-errors"; 3 | import { dirname, join, relative } from "@std/path"; 4 | 5 | // Regex for https://deno.land/x/deno_slack_api@x.x.x/ 6 | const API_REGEX = 7 | /(https:\/\/deno.land\/x\/deno_slack_api@[0-9]\.[0-9]+\.[0-9]+\/)/g; 8 | 9 | async function main() { 10 | const flags = parseArgs(Deno.args, { 11 | string: ["import-file", "api"], 12 | default: { 13 | "import-file": `${Deno.cwd()}/deno.jsonc`, 14 | "api": "./deno-slack-api/", 15 | }, 16 | }); 17 | 18 | const importFilePath = await Deno.realPath(flags["import-file"]); 19 | const importFileDir = dirname(importFilePath); 20 | const apiDir = await Deno.realPath(flags.api); 21 | 22 | const importFileJsonIn = await Deno.readTextFile(importFilePath); 23 | console.log(`content in ${flags["import-file"]}:`, importFileJsonIn); 24 | 25 | const importFile = JSON.parse(importFileJsonIn); 26 | const denoSlackSdkValue = importFile["imports"]["deno-slack-sdk/"]; 27 | 28 | const apiDepsInSdk = await apiDepsIn(denoSlackSdkValue); 29 | 30 | const apiPackageSpecifier = join( 31 | relative(importFileDir, apiDir), 32 | "/src/", 33 | ); 34 | 35 | importFile["imports"]["deno-slack-api/"] = apiPackageSpecifier; 36 | importFile["scopes"] = { 37 | [denoSlackSdkValue]: [...apiDepsInSdk].reduce( 38 | (sdkScopes: Record<string, string>, apiDep: string) => { 39 | return { 40 | ...sdkScopes, 41 | [apiDep]: apiPackageSpecifier, 42 | }; 43 | }, 44 | {}, 45 | ), 46 | }; 47 | 48 | const parentFileJsonIn = await Deno.readTextFile( 49 | join(apiDir, "/deno.jsonc"), 50 | ); 51 | console.log("parent `import file` in content:", parentFileJsonIn); 52 | const parentImportFile = JSON.parse(parentFileJsonIn); 53 | for (const entry of Object.entries(parentImportFile["imports"])) { 54 | importFile["imports"][entry[0]] = entry[1]; 55 | } 56 | 57 | const importMapJsonOut = JSON.stringify(importFile, null, 2); 58 | console.log("`import file` out content:", importMapJsonOut); 59 | 60 | await Deno.writeTextFile(flags["import-file"], importMapJsonOut); 61 | } 62 | 63 | export async function apiDepsIn(moduleUrl: string): Promise<Set<string>> { 64 | const fileUrl = moduleUrl.endsWith("/") 65 | ? `${moduleUrl}deps.ts?source,file` 66 | : `${moduleUrl}/deps.ts?source,file`; 67 | const response = await fetch(fileUrl); 68 | 69 | if (!response.ok) { 70 | const err = createHttpError(response.status, await response.text(), { 71 | expose: true, 72 | headers: response.headers, 73 | }); 74 | console.error(err); 75 | throw err; 76 | } 77 | 78 | const depsTs = await response.text(); 79 | return new Set(depsTs.match(API_REGEX)); 80 | } 81 | 82 | if (import.meta.main) main(); 83 | -------------------------------------------------------------------------------- /testing/http_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertNotEquals } from "@std/assert"; 2 | import { describe, it } from "@std/testing/bdd"; 3 | import { assertSpyCalls, spy } from "@std/testing/mock"; 4 | 5 | import { stubFetch } from "./http.ts"; 6 | 7 | describe("stubFetch", () => { 8 | const originalFetch = globalThis.fetch; 9 | 10 | it("should replace global fetch with a stub", async () => { 11 | const matchesSpy = spy(() => {}); 12 | 13 | using _stub = stubFetch( 14 | matchesSpy, 15 | new Response(JSON.stringify({ success: true }), { 16 | status: 200, 17 | headers: { "Content-Type": "application/json" }, 18 | }), 19 | ); 20 | 21 | assertNotEquals(globalThis.fetch, originalFetch); 22 | 23 | const response = await fetch("https://example.com/api"); 24 | 25 | assertSpyCalls(matchesSpy, 1); 26 | assertEquals(await response.json(), { success: true }); 27 | assertEquals(response.status, 200); 28 | assertEquals(response.headers.get("Content-Type"), "application/json"); 29 | }); 30 | 31 | it("should handle Request objects directly", async () => { 32 | const matchesSpy = spy((req: Request) => { 33 | assertEquals(req.method, "POST"); 34 | assertEquals(req.url, "https://example.com/data"); 35 | }); 36 | 37 | using _stub = stubFetch(matchesSpy, new Response("Hello world")); 38 | 39 | const request = new Request("https://example.com/data", { 40 | method: "POST", 41 | headers: { "Content-Type": "application/json" }, 42 | body: JSON.stringify({ key: "value" }), 43 | }); 44 | 45 | await fetch(request); 46 | 47 | assertSpyCalls(matchesSpy, 1); 48 | }); 49 | 50 | it("should handle async matchers", async () => { 51 | let asyncOperationCompleted = false; 52 | 53 | const asyncMatcher = async (req: Request) => { 54 | await new Promise((resolve) => setTimeout(resolve, 10)); 55 | asyncOperationCompleted = true; 56 | assertEquals(req.url.includes("example.com"), true); 57 | }; 58 | 59 | using _stub = stubFetch(asyncMatcher, new Response("Success")); 60 | 61 | await fetch("https://example.com/async"); 62 | 63 | assertEquals(asyncOperationCompleted, true); 64 | }); 65 | 66 | it("should clone the response for each call", async () => { 67 | const testBody = "test"; 68 | using _stub = stubFetch(() => {}, new Response(testBody)); 69 | 70 | const response1 = await fetch("https://example.com/first"); 71 | const response2 = await fetch("https://example.com/second"); 72 | 73 | assertEquals(await response1.text(), testBody); 74 | assertEquals(await response2.text(), testBody); 75 | }); 76 | 77 | it("should restore original fetch when stub is released", () => { 78 | { 79 | using _stub = stubFetch(() => {}, new Response("Test")); 80 | assertEquals(globalThis.fetch !== originalFetch, true); 81 | } 82 | assertEquals(globalThis.fetch, originalFetch); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/base-client.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseResponse, 3 | BaseSlackClient, 4 | SlackAPIMethodArgs, 5 | SlackAPIOptions, 6 | } from "./types.ts"; 7 | import { createHttpError, type HttpError } from "@std/http/http-errors"; 8 | import { getUserAgent, serializeData } from "./base-client-helpers.ts"; 9 | 10 | export class BaseSlackAPIClient implements BaseSlackClient { 11 | #token?: string; 12 | #baseURL: string; 13 | 14 | constructor(token?: string, options: SlackAPIOptions = {}) { 15 | this.#token = token; 16 | this.#baseURL = options.slackApiUrl || "https://slack.com/api/"; 17 | } 18 | 19 | /** 20 | * @description Set an override url endpoint for Slack API calls. 21 | * @param apiURL url endpoint for the Slack API used for api calls. It should include the protocol, the domain and the path. 22 | * @example: "https://slack.com/api/" 23 | * @returns BaseSlackClient 24 | */ 25 | setSlackApiUrl(apiURL: string) { 26 | this.#baseURL = apiURL; 27 | 28 | return this; 29 | } 30 | 31 | // TODO: [brk-chg] return the `Promise<Response>` object 32 | async apiCall( 33 | method: string, 34 | data: SlackAPIMethodArgs = {}, 35 | ): Promise<BaseResponse> { 36 | // ensure there's a slash prior to method 37 | const url = `${this.#baseURL.replace(/\/$/, "")}/${method}`; 38 | const body = serializeData(data); 39 | 40 | const token = data.token || this.#token || ""; 41 | const response = await fetch(url, { 42 | method: "POST", 43 | headers: { 44 | "Authorization": `Bearer ${token}`, 45 | "Content-Type": "application/x-www-form-urlencoded", 46 | "User-Agent": getUserAgent(), 47 | }, 48 | body, 49 | }); 50 | if (!response.ok) { 51 | throw await this.createHttpError(response); 52 | } 53 | return await this.createBaseResponse(response); 54 | } 55 | 56 | // TODO: [brk-chg] return a `Promise<Response>` object 57 | async response( 58 | url: string, 59 | data: Record<string, unknown>, 60 | ): Promise<BaseResponse> { 61 | const response = await fetch(url, { 62 | method: "POST", 63 | headers: { 64 | "Content-Type": "application/json", 65 | "User-Agent": getUserAgent(), 66 | }, 67 | body: JSON.stringify(data), 68 | }); 69 | if (!response.ok) { 70 | throw await this.createHttpError(response); 71 | } 72 | return await this.createBaseResponse(response); 73 | } 74 | 75 | private async createHttpError(response: Response): Promise<HttpError> { 76 | const text = await response.text(); 77 | return createHttpError( 78 | response.status, 79 | `${response.status}: ${text}`, 80 | { 81 | headers: response.headers, 82 | }, 83 | ); 84 | } 85 | 86 | private async createBaseResponse(response: Response): Promise<BaseResponse> { 87 | return { 88 | toFetchResponse: () => response, 89 | ...await response.json(), 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /docs/client.md: -------------------------------------------------------------------------------- 1 | ## Slack API Client 2 | 3 | ### Instantiating the Slack API Client 4 | 5 | To instantiate the Slack API Client, use the top level `SlackAPI` export. 6 | `SlackAPI` accepts two arguments: 7 | 8 | - `token`: Your application's access token 9 | - `options`: An optional object with parameters to customize the client 10 | - `slackApiUrl`: an optional string parameter to specify the Slack API URL. By 11 | default this is set to `"https://slack.com/api/"` 12 | 13 | ```ts 14 | import { SlackAPI } from "jsr:@slack/api"; 15 | 16 | // create a client with defaults 17 | const client = SlackAPI(token); 18 | 19 | // create a client with options 20 | const customClient = SlackAPI(token, { 21 | slackApiUrl: "...", 22 | }); 23 | ``` 24 | 25 | ### Using the Slack API Client 26 | 27 | Now that you have an instance of the Slack API Client, you have access to its 28 | [methods](https://api.slack.com/methods). You can call Slack API methods by 29 | directly referencing them on the client, such as `client.chat.postMessage()` or 30 | `client.pins.add()`. You also have access to the following methods: 31 | 32 | - `apiCall`: An async function that accepts two arguments: 33 | 1. `method`: a string which defines which API method you wish to invoke. 34 | 2. `data`: a JSON object representing parameter data to be passed to the API 35 | method you wish to invoke; the client will handle serializing it 36 | appropriately. 37 | - `response`: An async function that accepts two arguments: 38 | 1. `responseURL`: a string defining the response URL. 39 | 2. `data`: the payload to send to the response URL. 40 | 41 | ```ts 42 | const response = await client.chat.postMessage({ 43 | text: "hello there", 44 | channel: "...", 45 | }); 46 | 47 | // use client.apiCall() directly with the api method name 48 | await client.apiCall("chat.postMessage", { 49 | text: "hello there", 50 | channel: "...", 51 | }); 52 | 53 | // respond to a response_url 54 | await client.response("...", payload); 55 | ``` 56 | 57 | #### Pagination 58 | 59 | A vast majority of Slack API methods support 60 | [cursor-based pagination](https://api.slack.com/docs/pagination#cursors). To use 61 | cursor-based pagination, start by specifying a `limit` parameter to any API 62 | method that returns lists of objects, like so: 63 | 64 | ```ts 65 | const messages = await client.conversations.history({ 66 | channel: myChannelID, 67 | limit: 1, 68 | }); 69 | ``` 70 | 71 | Specifying `limit: 1` will ensure the response from the API will contain at most 72 | one item. 73 | 74 | However, assumining in our above example that the channel being queried has more 75 | than one message, we would expect to be able to retrieve more "pages" of 76 | results. By inspecting the API response's `response_metadata.next_cursor` value, 77 | we can determine if there are any additional pages, and if so, we can use 78 | `next_cursor` to retrieve the next page of results, like so: 79 | 80 | ```ts 81 | if (messages.response_metadata?.next_cursor) { 82 | const moarMessages = await.client.conversations.history({ 83 | channel: myChannelID, 84 | limit: 1, 85 | cursor: messages.response_metadata.next_cursor 86 | }); 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /scripts/src/api-method-node.ts: -------------------------------------------------------------------------------- 1 | import { pascalCase } from "../../src/deps.ts"; 2 | 3 | const CURSOR_PAGINATED_API_NAMES = [ 4 | "getEntities", 5 | "getTeams", 6 | "list", 7 | "search", 8 | "listOriginalConnectedChannelInfo", 9 | "history", 10 | "listConnectInvites", 11 | "members", 12 | "replies", 13 | "conversations", 14 | ]; 15 | const PAGINATION_EXCEPTION_LIST = [ 16 | "bookmarks.list", 17 | "enterprise.auth.idpconfig.list", 18 | "pins.list", 19 | "reminders.list", 20 | "team.preferences.list", 21 | "usergroups.list", 22 | "usergroups.users.list", 23 | ]; 24 | 25 | export class APIMethodNode { 26 | name = ""; 27 | childNodes: APIMethodNode[] = []; 28 | isMethod = false; 29 | nodePath = ""; 30 | isRootNode = false; 31 | 32 | constructor( 33 | name: string, 34 | isMethod: boolean, 35 | // This currently isn't referenced, but could be useful in the future for typed payloads, 36 | // as it provides the underlying method that is passed to apiCall, i.e. `"chat.postMessage"` 37 | nodePath: string, 38 | isRootNode: boolean, 39 | ) { 40 | this.name = name; 41 | this.isMethod = isMethod; 42 | this.nodePath = nodePath; 43 | this.isRootNode = isRootNode; 44 | } 45 | 46 | findChildByName(name: string) { 47 | return this.childNodes.find((node) => { 48 | return node.name === name; 49 | }); 50 | } 51 | 52 | addChild(node: APIMethodNode) { 53 | this.childNodes.push(node); 54 | } 55 | 56 | // Generates the API Type definition code - should be called at the top level api group level 57 | getTypesCode(): string { 58 | let code = ""; 59 | if (this.isRootNode) { 60 | const typeName = `${pascalCase(this.name)}APIType`; 61 | code += `export type ${typeName} = {\n`; 62 | } 63 | 64 | // api method with no child nodes 65 | if (this.isMethod && this.childNodes.length === 0) { 66 | if ( 67 | CURSOR_PAGINATED_API_NAMES.includes(this.name) && 68 | !PAGINATION_EXCEPTION_LIST.includes(this.nodePath) 69 | ) { 70 | code += `${this.name}: SlackAPICursorPaginatedMethod,\n`; 71 | } else { 72 | code += `${this.name}: SlackAPIMethod,\n`; 73 | } 74 | } 75 | 76 | // api method with child nodes 77 | if (this.isMethod && this.childNodes.length > 0) { 78 | code += `${this.name}: SlackAPIMethod & {\n`; 79 | } 80 | 81 | // api node that is not a method, but has child nodes 82 | if (!this.isMethod && this.childNodes.length > 0 && !this.isRootNode) { 83 | code += `${this.name}: {\n`; 84 | } 85 | 86 | for (const child of this.childNodes) { 87 | code += child.getTypesCode(); 88 | } 89 | 90 | // api method with child nodes 91 | if (this.isMethod && this.childNodes.length > 0) { 92 | code += `};\n`; 93 | } 94 | 95 | // api node that is not a method, but has child nodes 96 | if (!this.isMethod && this.childNodes.length > 0 && !this.isRootNode) { 97 | code += `};\n`; 98 | } 99 | 100 | if (this.isRootNode) { 101 | code += "};\n"; 102 | } 103 | 104 | return code; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /docs/triggers/trigger-generic-inputs.md: -------------------------------------------------------------------------------- 1 | # Trigger generics 2 | 3 | When creating or updating a trigger, a workflow definition can be passed into 4 | the `client.workflows.triggers.create` and `client.workflows.triggers.update` 5 | methods as a generic which will allow for typeahead/autocomplete of the input 6 | parameters based on the definition's inputs. 7 | 8 | ## Defining a workflow 9 | 10 | Recall that in the 11 | [SDK](https://github.com/slackapi/deno-slack-sdk/blob/main/docs/workflows.md#workflows), 12 | a `workflow` can be defined using the `DefineWorkflow` method. When a workflow 13 | is created in this manner, it contains a `workflow.definition` object. By 14 | passing this definition into the `create` or `update` method, the trigger will 15 | have access to information related to the workflow's definition. 16 | 17 | ## Workflow definition input with runtime triggers 18 | 19 | When dealing with triggers at runtime, a workflow definition input can 20 | optionally be passed to the trigger's `create` or `update` API call as a 21 | generic. When a generic is passed, the object argument being defined will have 22 | access to typeahead on the `inputs` and `workflow` properties. 23 | 24 | ### Using trigger generics at runtime Example 25 | 26 | ```ts 27 | import { TriggersWorkflow } from "../manifest.ts"; 28 | import { SlackAPI } from "deno-slack-api/mod.ts"; 29 | 30 | ... 31 | 32 | { 33 | //Inside the function execution logic 34 | const client = SlackAPI(token); 35 | 36 | const triggerReturn = await client.workflows.triggers.create< //Also works for update 37 | typeof TriggersWorkflow.definition 38 | >({ 39 | type: "webhook", 40 | name: "Request Time off", 41 | description: "Starts the workflow to request time off", 42 | workflow: "#/workflows/reverse_workflow", 43 | inputs: { //inputs can now be autofilled from the workflow definition 44 | a_string: { 45 | value: "TEST", 46 | }, 47 | a_channel: { 48 | value: "TEST", 49 | }, 50 | b_string: { 51 | value: "TEST", 52 | }, 53 | }, 54 | } 55 | ``` 56 | 57 | ## Workflow definition input with CLI triggers 58 | 59 | A `workflow` definition input can also be passed to the `trigger` file that is 60 | used to create or update a trigger through the CLI. When passing the definition 61 | input in this manner, the `workflow` definition is passed as a generic to the 62 | `trigger` object instead of the `create` or `update` method. With an Input 63 | Generic, the `trigger` object being defined will have access to typeahead on the 64 | expected parameters to be passed in. 65 | 66 | ### Using trigger generics with CLI Example 67 | 68 | ```ts 69 | import { Trigger } from "deno-slack-api/types.ts"; 70 | import { TriggersWorkflow } from "../manifest.ts"; 71 | 72 | const trigger: Trigger<typeof TriggersWorkflow.definition> = { //Workflow definition is passed to the trigger object 73 | type: "shortcut", 74 | name: "Get Triggers List", 75 | description: "Starts the workflow to request time off", 76 | workflow: "#/workflows/list_trigger_workflow", 77 | inputs: { //The inputs parameter will now have typeahead based on the workflow definition being passed in. 78 | a_input: { 79 | value: "test_value", 80 | }, 81 | }, 82 | }; 83 | 84 | export default trigger; 85 | ``` 86 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/base_response.ts: -------------------------------------------------------------------------------- 1 | import type { BaseTrigger, WorkflowSchema } from "./mod.ts"; 2 | 3 | export type BaseTriggerResponse< 4 | WorkflowDefinition extends WorkflowSchema, 5 | > = 6 | & Pick<BaseTrigger<WorkflowDefinition>, "name" | "type" | "description"> 7 | & { 8 | /** @description The trigger's internal uid */ 9 | id: string; 10 | /** @description The metadata for the workflow */ 11 | workflow: { 12 | /** @description the workflow function's internal uid */ 13 | id: string; 14 | /** @description the workflow's internal uid */ 15 | workflow_id: string; 16 | /** @description the workflow's callback_id */ 17 | callback_id: string; 18 | /** @description the workflow's title */ 19 | title: string; 20 | /** @description the workflow's description */ 21 | description: string; 22 | /** @default "workflow" */ 23 | type: string; 24 | /** @description the workflow's input parameters */ 25 | // deno-lint-ignore no-explicit-any 26 | input_parameters: Record<string, any>[]; 27 | /** @description the workflow's output parameters */ 28 | // deno-lint-ignore no-explicit-any 29 | output_parameters: Record<string, any>[]; 30 | /** @description the app_id that the workflow belongs to */ 31 | app_id: string; 32 | /** @description the app metadata that the workflow belongs to */ 33 | app: { 34 | id: string; 35 | name: string; 36 | icons: { 37 | image_32: string; 38 | image_48: string; 39 | image_64: string; 40 | image_72: string; 41 | // deno-lint-ignore no-explicit-any 42 | [otherOptions: string]: any; 43 | }; 44 | // deno-lint-ignore no-explicit-any 45 | [otherOptions: string]: any; 46 | }; 47 | /** @description A UNIX timestamp of when the workflow was created */ 48 | date_created: number; 49 | /** @description A UNIX timestamp of when the workflow was last updated */ 50 | date_updated: number; 51 | /** @description A UNIX timestamp of when the workflow was deleted; this property can be 0 when it's not yet deleted. */ 52 | date_deleted: number; 53 | // deno-lint-ignore no-explicit-any 54 | [otherOptions: string]: any; 55 | }; 56 | /** @description The inputs provided to the workflow */ 57 | inputs: { 58 | // deno-lint-ignore no-explicit-any 59 | [key: string]: Record<string, any>; 60 | }; 61 | /** @deprecated Trigger outputs will be removed. Use `available_data` instead */ 62 | outputs: { 63 | [key: string]: { 64 | type: string; 65 | title: string; 66 | description: string; 67 | is_required: boolean; 68 | name: string; 69 | // deno-lint-ignore no-explicit-any 70 | [otherOptions: string]: any; 71 | }; 72 | }; 73 | available_data: { 74 | // deno-lint-ignore no-explicit-any 75 | [key: string]: Record<string, any>; 76 | }; 77 | /** @description A timestamp of when the trigger was created */ 78 | date_created: number; 79 | /** @description A timestamp of when the workflow was last updated */ 80 | date_updated: number; 81 | // deno-lint-ignore no-explicit-any 82 | [otherOptions: string]: any; 83 | }; 84 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/trigger-event-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @enum {string} Enumerates valid event trigger types. 3 | */ 4 | export const TriggerEventTypes = { 5 | /** 6 | * A message event that mentions your app. 7 | */ 8 | AppMentioned: "slack#/events/app_mentioned", 9 | /** 10 | * A channel was archived. 11 | */ 12 | ChannelArchived: "slack#/events/channel_archived", 13 | /** 14 | * A channel was created. 15 | */ 16 | ChannelCreated: "slack#/events/channel_created", 17 | /** 18 | * A channel was deleted. 19 | */ 20 | ChannelDeleted: "slack#/events/channel_deleted", 21 | /** 22 | * A channel was renamed. 23 | */ 24 | ChannelRenamed: "slack#/events/channel_renamed", 25 | /** 26 | * A channel was shared with an external workspace. 27 | */ 28 | ChannelShared: "slack#/events/channel_shared", 29 | /** 30 | * A channel was unarchived. 31 | */ 32 | ChannelUnarchived: "slack#/events/channel_unarchived", 33 | /** 34 | * A channel was unshared with an external workspace. 35 | */ 36 | ChannelUnshared: "slack#/events/channel_unshared", 37 | /** 38 | * Do not Disturb settings changed for a member. 39 | */ 40 | DndUpdated: "slack#/events/dnd_updated", 41 | /** 42 | * A custom emoji has been added or changed. 43 | */ 44 | EmojiChanged: "slack#/events/emoji_changed", 45 | // MessageMetadataAdded: "slack#/events/message_metadata_added", 46 | // MessageMetadataDeleted: "slack#/events/message_metadata_deleted", 47 | /** 48 | * Message metadata was posted. 49 | */ 50 | MessageMetadataPosted: "slack#/events/message_metadata_posted", 51 | /** 52 | * A message was sent to a conversation. 53 | */ 54 | MessagePosted: "slack#/events/message_posted", 55 | /** 56 | * A pin was added to a channel. 57 | */ 58 | PinAdded: "slack#/events/pin_added", 59 | /** 60 | * A pin was removed from a channel. 61 | */ 62 | PinRemoved: "slack#/events/pin_removed", 63 | /** 64 | * A member has added an emoji reaction to an item. 65 | */ 66 | ReactionAdded: "slack#/events/reaction_added", 67 | /** 68 | * A member has removed an emoji reaction from an item. 69 | */ 70 | ReactionRemoved: "slack#/events/reaction_removed", 71 | /** 72 | * A shared channel invite was accepted. 73 | */ 74 | SharedChannelInviteAccepted: "slack#/events/shared_channel_invite_accepted", 75 | /** 76 | * A shared channel invite was approved. 77 | */ 78 | SharedChannelInviteApproved: "slack#/events/shared_channel_invite_approved", 79 | /** 80 | * A shared channel invite was declined. 81 | */ 82 | SharedChannelInviteDeclined: "slack#/events/shared_channel_invite_declined", 83 | /** 84 | * A shared channel invite was sent to a Slack user. 85 | */ 86 | SharedChannelInviteReceived: "slack#/events/shared_channel_invite_received", 87 | /** 88 | * A user joined a public channel, private channel, or MPDM. 89 | */ 90 | UserJoinedChannel: "slack#/events/user_joined_channel", 91 | /** 92 | * A user joined a team or workspace. 93 | */ 94 | UserJoinedTeam: "slack#/events/user_joined_team", 95 | /** 96 | * A user left a public or private channel. 97 | */ 98 | UserLeftChannel: "slack#/events/user_left_channel", 99 | } as const; 100 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/shortcut-data.ts: -------------------------------------------------------------------------------- 1 | import base_trigger_data from "./event-data/common-objects/all_triggers.ts"; 2 | 3 | const Interactor = { 4 | /** 5 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who invoked the trigger. 6 | */ 7 | id: "{{data.interactivity.interactor.id}}", 8 | /** 9 | * This is a secret! Don't tell anyone! 10 | secret: "{{data.interactivity.interactor.secret}}", 11 | */ 12 | } as const; 13 | 14 | Object.defineProperty(Interactor, "toJSON", { 15 | value: () => "{{data.interactivity.interactor}}", 16 | }); 17 | 18 | const Interactivity = { 19 | /** 20 | * A unique identifier to the particular user interaction. 21 | */ 22 | interactivity_pointer: "{{data.interactivity.interactivity_pointer}}", 23 | /** 24 | * Represents the user who interacted with the trigger. 25 | */ 26 | interactor: Interactor, 27 | } as const; 28 | 29 | Object.defineProperty(Interactivity, "toJSON", { 30 | value: () => "{{data.interactivity}}", 31 | }); 32 | 33 | /** 34 | * Link-trigger-specific input values that contain information about the link trigger. 35 | */ 36 | export const ShortcutTriggerContextData = { 37 | ...base_trigger_data, 38 | /** 39 | * A unique identifier for the action that invoked the trigger. Only available when trigger is invoked from a {@link https://api.slack.com/automation/triggers/link#workflow_buttons Workflow button}! 40 | */ 41 | action_id: "{{data.action_id}}", 42 | /** 43 | * A unique identifier for the block where the trigger was invoked. Only available when trigger is invoked from a {@link https://api.slack.com/automation/triggers/link#workflow_buttons Workflow button}! 44 | */ 45 | block_id: "{{data.block_id}}", 46 | /** 47 | * A unique identifier for the bookmark where the trigger was invoked. Only available when trigger is invoked from a channel's bookmarks bar! 48 | */ 49 | bookmark_id: "{{data.bookmark_id}}", 50 | /** 51 | * A unique identifier for the {@link https://api.slack.com/automation/types#channelid Slack channel} where the trigger was invoked. Only available when trigger is invoked from a channel, DM or MPDM! 52 | */ 53 | channel_id: "{{data.channel_id}}", 54 | /** 55 | * An object that contains context about the particular interactivity event that tripped the trigger. For consumption in functions that involve {@link https://api.slack.com/automation/block-events Block Kit} or {@link https://api.slack.com/automation/view-events Modal View} interactivity. 56 | */ 57 | interactivity: Interactivity, 58 | /** 59 | * Where the trigger was invoked. At runtime, the value will be one of the following strings, depending on the invocation source: `message` when invoked from a message, `bookmark` when invoked from a bookmark, or `button` when invoked from a {@link https://api.slack.com/automation/triggers/link#workflow_buttons Workflow Button}. 60 | */ 61 | location: "{{data.location}}", 62 | /** 63 | * A unique {@link https://api.slack.com/automation/types#message-ts Slack message timestamp string} indicating when the trigger-invoking message was sent. Only available when trigger is invoked from a channel, DM or MPDM! 64 | */ 65 | message_ts: "{{data.message_ts}}", 66 | /** 67 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} who invoked the trigger. 68 | */ 69 | user_id: "{{data.user_id}}", 70 | } as const; 71 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/common-objects/shared_channel_invite.ts: -------------------------------------------------------------------------------- 1 | const Icon = {} as const; 2 | 3 | Object.defineProperty(Icon, "toJSON", { 4 | value: () => "{{data.invite.inviting_team.icon}}", 5 | }); 6 | 7 | const InvitingTeam = { 8 | /** 9 | * A UNIX timestamp in seconds indicating when the inviting team or workspace was created. 10 | */ 11 | date_created: "{{data.invite.inviting_team.date_created}}", 12 | /** 13 | * The domain of the inviting team or workspace. 14 | */ 15 | domain: "{{data.invite.inviting_team.domain}}", 16 | /** 17 | * An object containing CDN-backed slack.com URLs for the inviting team's icon. 18 | */ 19 | icon: Icon, 20 | /** 21 | * A unique identifier for the team or workspace being invited to. 22 | */ 23 | id: "{{data.invite.inviting_team.id}}", 24 | /** 25 | * Whether or not the inviting team or workspace is verified or not. 26 | */ 27 | is_verified: "{{data.invite.inviting_team.is_verified}}", 28 | /** 29 | * The name of the inviting team of workspace. 30 | */ 31 | name: "{{data.invite.inviting_team.name}}", 32 | } as const; 33 | 34 | Object.defineProperty(InvitingTeam, "toJSON", { 35 | value: () => "{{data.invite.inviting_team}}", 36 | }); 37 | 38 | const InvitingUser = { 39 | /** 40 | * The display name of the inviting user. 41 | */ 42 | display_name: "{{data.invite.inviting_user.display_name}}", 43 | /** 44 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} that sent the invite. 45 | */ 46 | id: "{{data.invite.inviting_user.id}}", 47 | /** 48 | * Whether or not a bot invited the user. 49 | */ 50 | is_bot: "{{data.invite.inviting_user.is_bot}}", 51 | /** 52 | * The name of the inviting user. 53 | */ 54 | name: "{{data.invite.inviting_user.name}}", 55 | /** 56 | * The real name of the inviting user. 57 | */ 58 | real_name: "{{data.invite.inviting_user.real_name}}", 59 | /** 60 | * A unique identifier for the team or workspace being invited to. 61 | */ 62 | team_id: "{{data.invite.inviting_user.team_id}}", 63 | /** 64 | * The timezone of the user who sent the invite, in TZ identifier format. 65 | * @example "America/Toronto" 66 | */ 67 | timezone: "{{data.invite.inviting_user.timezone}}", 68 | } as const; 69 | 70 | Object.defineProperty(InvitingUser, "toJSON", { 71 | value: () => "{{data.invite.inviting_user}}", 72 | }); 73 | 74 | export const Invite = { 75 | /** 76 | * A UNIX timestamp in seconds indicating when the invite was issued. 77 | */ 78 | date_created: "{{data.invite.date_created}}", 79 | /** 80 | * A UNIX timestamp in seconds indicating when the invite will expire. 81 | */ 82 | date_invalid: "{{data.invite.date_invalid}}", 83 | /** 84 | * A unique identifier for the invite. 85 | */ 86 | id: "{{data.invite.id}}", 87 | /** 88 | * Object containing details about the team or workspace being invited to. 89 | */ 90 | inviting_team: InvitingTeam, 91 | /** 92 | * Object containing details for the user that sent the invite. 93 | */ 94 | inviting_user: InvitingUser, 95 | /** 96 | * The invitee's email address. 97 | */ 98 | recipient_email: "{{data.invite.recipient_email}}", 99 | /** 100 | * A unique identifier for the {@link https://api.slack.com/automation/types#userid Slack user} that was invited. 101 | */ 102 | recipient_user_id: "{{data.invite.recipient_user_id}}", 103 | } as const; 104 | 105 | Object.defineProperty(Invite, "toJSON", { value: () => "{{data.invite}}" }); 106 | -------------------------------------------------------------------------------- /docs/triggers/trigger-filters.md: -------------------------------------------------------------------------------- 1 | ## Trigger filters 2 | 3 | A trigger filter is an object that can be added to a trigger on creation that 4 | will define the condition in which a trigger should execute its associated 5 | workflow. A trigger filter contains two parameters: 6 | 7 | | Parameter name | Required? | Description | 8 | | -------------- | :-------: | ---------------------------------------------------- | 9 | | `version` | Yes | The version of the filter as a number | 10 | | `root` | Yes | A combination of boolean logic and comparator values | 11 | 12 | The root parameter can contain a combination of `Boolean logic` and 13 | `Conditional Expression` objects with the following attributes: 14 | 15 | ### `Boolean Logic` 16 | 17 | | Parameter name | Required? | Description | 18 | | -------------- | :-------: | --------------------------------------------------------------------------------------- | 19 | | `operator` | Yes | The logical operator to run against your filter inputs (AND, OR, NOT) as a string value | 20 | | `inputs` | Yes | The filter inputs that contain filter statement definitions | 21 | 22 | ### `Conditional expressions` 23 | 24 | | Parameter name | Required? | Description | 25 | | -------------- | :-------: | -------------------------------------------------------------------------------- | 26 | | `statement` | Yes | Comparison of values (uses one of the following operators: ">", "<", "==", "!= ) | 27 | 28 | ## Usage examples 29 | 30 | Trigger filters can be composed of a single statement, or combine multiple 31 | statements using different logical comparators. Follow along to see different 32 | examples that build upon each other. 33 | 34 | ### Single statement 35 | 36 | A trigger filter can use a single statement, which will execute when the 37 | statement is true. 38 | 39 | ```ts 40 | { 41 | version: 1, 42 | root: { 43 | statement: "{{data.value}} > 3", 44 | }, 45 | } 46 | ``` 47 | 48 | ### Logical operators 49 | 50 | A trigger filter can also use simple logical operators to compare multiple 51 | statements and evaluate their outcome. 52 | 53 | ```ts 54 | { 55 | version: 1, 56 | root: { 57 | operator: "OR", 58 | inputs: [{ 59 | statement: "{{data.value}} == apple", 60 | }, { 61 | statement: "{{data.value}} != banana", 62 | }], 63 | }, 64 | } 65 | ``` 66 | 67 | ```ts 68 | { 69 | version: 1, 70 | root: { 71 | operator: "AND", 72 | inputs: [{ 73 | statement: "{{data.value}} > 2", 74 | }, { 75 | statement: "{{data.value}} < 5", 76 | }], 77 | }, 78 | } 79 | ``` 80 | 81 | ### Nested logical operators 82 | 83 | A trigger filter can make use of nested logical operators and statements for 84 | more complicated conditional evaluations. 85 | 86 | ```ts 87 | { 88 | version: 1, 89 | root: { 90 | operator: TriggerFilterOperatorType.OR, 91 | inputs: [{ 92 | statement: "{{data.user_id}} == User1", 93 | }, { 94 | statement: "{{data.user_id}} == User2", 95 | }, { 96 | operator: "AND", 97 | inputs: [{ 98 | statement: "{{data.user_id}} != 3", 99 | }, { 100 | operator: "OR", 101 | inputs: [{ 102 | statement: "{{data.value}} < 4", 103 | }, { 104 | statement: "{{data.value}} > 10", 105 | }], 106 | }], 107 | }], 108 | }, 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/tests/context_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { TriggerContextData } from "../mod.ts"; 3 | 4 | Deno.test("TriggerContextData Objects allow property access or string references", async (t) => { 5 | const { Shortcut, Event } = TriggerContextData; 6 | 7 | await t.step("Shortcut Triggers", async (tt) => { 8 | const { interactivity } = Shortcut; 9 | 10 | await tt.step( 11 | "support string references for objects when stringifying JSON", 12 | () => { 13 | assertEquals( 14 | JSON.stringify(interactivity), 15 | `"{{data.interactivity}}"`, 16 | ); 17 | assertEquals( 18 | JSON.stringify(interactivity.interactor), 19 | `"{{data.interactivity.interactor}}"`, 20 | ); 21 | }, 22 | ); 23 | await tt.step("support property references for objects", () => { 24 | assertEquals( 25 | JSON.stringify(interactivity.interactor.id), 26 | `"{{data.interactivity.interactor.id}}"`, 27 | ); 28 | }); 29 | }); 30 | 31 | await t.step("Event Triggers", async (tt) => { 32 | const { 33 | DndUpdated, 34 | SharedChannelInviteAccepted, 35 | SharedChannelInviteApproved, 36 | SharedChannelInviteDeclined, 37 | UserJoinedTeam, 38 | } = Event; 39 | await tt.step( 40 | "support string references for objects when stringifying JSON", 41 | () => { 42 | assertEquals( 43 | JSON.stringify(DndUpdated.dnd_status), 44 | `"{{data.dnd_status}}"`, 45 | ); 46 | assertEquals( 47 | JSON.stringify(SharedChannelInviteAccepted.accepting_user), 48 | `"{{data.accepting_user}}"`, 49 | ); 50 | assertEquals( 51 | JSON.stringify(SharedChannelInviteApproved.approving_user), 52 | `"{{data.approving_user}}"`, 53 | ); 54 | assertEquals( 55 | JSON.stringify(SharedChannelInviteDeclined.declining_user), 56 | `"{{data.declining_user}}"`, 57 | ); 58 | assertEquals( 59 | JSON.stringify(UserJoinedTeam.user), 60 | `"{{data.user}}"`, 61 | ); 62 | assertEquals( 63 | JSON.stringify(SharedChannelInviteAccepted.invite), 64 | `"{{data.invite}}"`, 65 | ); 66 | assertEquals( 67 | JSON.stringify(SharedChannelInviteAccepted.invite.inviting_team), 68 | `"{{data.invite.inviting_team}}"`, 69 | ); 70 | assertEquals( 71 | JSON.stringify(SharedChannelInviteAccepted.invite.inviting_team.icon), 72 | `"{{data.invite.inviting_team.icon}}"`, 73 | ); 74 | assertEquals( 75 | JSON.stringify(SharedChannelInviteAccepted.invite.inviting_user), 76 | `"{{data.invite.inviting_user}}"`, 77 | ); 78 | }, 79 | ); 80 | await tt.step("support property references for objects", () => { 81 | assertEquals( 82 | JSON.stringify(DndUpdated.dnd_status.dnd_enabled), 83 | `"{{data.dnd_status.dnd_enabled}}"`, 84 | ); 85 | assertEquals( 86 | JSON.stringify(SharedChannelInviteApproved.approving_user.id), 87 | `"{{data.approving_user.id}}"`, 88 | ); 89 | assertEquals( 90 | JSON.stringify(SharedChannelInviteDeclined.declining_user.name), 91 | `"{{data.declining_user.name}}"`, 92 | ); 93 | assertEquals( 94 | JSON.stringify(UserJoinedTeam.user.real_name), 95 | `"{{data.user.real_name}}"`, 96 | ); 97 | assertEquals( 98 | JSON.stringify(SharedChannelInviteAccepted.invite.inviting_team.domain), 99 | `"{{data.invite.inviting_team.domain}}"`, 100 | ); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/generated/method-types/admin.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SlackAPICursorPaginatedMethod, 3 | SlackAPIMethod, 4 | } from "../../types.ts"; 5 | 6 | export type AdminAPIType = { 7 | analytics: { 8 | getFile: SlackAPIMethod; 9 | }; 10 | apps: { 11 | approve: SlackAPIMethod; 12 | approved: { 13 | list: SlackAPICursorPaginatedMethod; 14 | }; 15 | clearResolution: SlackAPIMethod; 16 | requests: { 17 | cancel: SlackAPIMethod; 18 | list: SlackAPICursorPaginatedMethod; 19 | }; 20 | restrict: SlackAPIMethod; 21 | restricted: { 22 | list: SlackAPICursorPaginatedMethod; 23 | }; 24 | uninstall: SlackAPIMethod; 25 | }; 26 | auth: { 27 | policy: { 28 | assignEntities: SlackAPIMethod; 29 | getEntities: SlackAPICursorPaginatedMethod; 30 | removeEntities: SlackAPIMethod; 31 | }; 32 | }; 33 | barriers: { 34 | create: SlackAPIMethod; 35 | delete: SlackAPIMethod; 36 | list: SlackAPICursorPaginatedMethod; 37 | update: SlackAPIMethod; 38 | }; 39 | conversations: { 40 | archive: SlackAPIMethod; 41 | convertToPrivate: SlackAPIMethod; 42 | create: SlackAPIMethod; 43 | delete: SlackAPIMethod; 44 | disconnectShared: SlackAPIMethod; 45 | ekm: { 46 | listOriginalConnectedChannelInfo: SlackAPICursorPaginatedMethod; 47 | }; 48 | getConversationPrefs: SlackAPIMethod; 49 | getCustomRetention: SlackAPIMethod; 50 | getTeams: SlackAPICursorPaginatedMethod; 51 | invite: SlackAPIMethod; 52 | removeCustomRetention: SlackAPIMethod; 53 | rename: SlackAPIMethod; 54 | restrictAccess: { 55 | addGroup: SlackAPIMethod; 56 | listGroups: SlackAPIMethod; 57 | removeGroup: SlackAPIMethod; 58 | }; 59 | search: SlackAPICursorPaginatedMethod; 60 | setConversationPrefs: SlackAPIMethod; 61 | setCustomRetention: SlackAPIMethod; 62 | setTeams: SlackAPIMethod; 63 | unarchive: SlackAPIMethod; 64 | }; 65 | emoji: { 66 | add: SlackAPIMethod; 67 | addAlias: SlackAPIMethod; 68 | list: SlackAPICursorPaginatedMethod; 69 | remove: SlackAPIMethod; 70 | rename: SlackAPIMethod; 71 | }; 72 | inviteRequests: { 73 | approve: SlackAPIMethod; 74 | approved: { 75 | list: SlackAPICursorPaginatedMethod; 76 | }; 77 | denied: { 78 | list: SlackAPICursorPaginatedMethod; 79 | }; 80 | deny: SlackAPIMethod; 81 | list: SlackAPICursorPaginatedMethod; 82 | }; 83 | teams: { 84 | admins: { 85 | list: SlackAPICursorPaginatedMethod; 86 | }; 87 | create: SlackAPIMethod; 88 | list: SlackAPICursorPaginatedMethod; 89 | owners: { 90 | list: SlackAPICursorPaginatedMethod; 91 | }; 92 | settings: { 93 | info: SlackAPIMethod; 94 | setDefaultChannels: SlackAPIMethod; 95 | setDescription: SlackAPIMethod; 96 | setDiscoverability: SlackAPIMethod; 97 | setIcon: SlackAPIMethod; 98 | setName: SlackAPIMethod; 99 | }; 100 | }; 101 | usergroups: { 102 | addChannels: SlackAPIMethod; 103 | addTeams: SlackAPIMethod; 104 | listChannels: SlackAPIMethod; 105 | removeChannels: SlackAPIMethod; 106 | }; 107 | users: { 108 | assign: SlackAPIMethod; 109 | invite: SlackAPIMethod; 110 | list: SlackAPICursorPaginatedMethod; 111 | remove: SlackAPIMethod; 112 | session: { 113 | clearSettings: SlackAPIMethod; 114 | getSettings: SlackAPIMethod; 115 | invalidate: SlackAPIMethod; 116 | list: SlackAPICursorPaginatedMethod; 117 | reset: SlackAPIMethod; 118 | resetBulk: SlackAPIMethod; 119 | setSettings: SlackAPIMethod; 120 | }; 121 | setAdmin: SlackAPIMethod; 122 | setExpiration: SlackAPIMethod; 123 | setOwner: SlackAPIMethod; 124 | setRegular: SlackAPIMethod; 125 | unsupportedVersions: { 126 | export: SlackAPIMethod; 127 | }; 128 | }; 129 | }; 130 | -------------------------------------------------------------------------------- /docs/triggers/webhook-triggers.md: -------------------------------------------------------------------------------- 1 | ## Webhook triggers 2 | 3 | A webhook trigger is a trigger that activates on a webhook activation in the 4 | Slack client. A webhook trigger includes the common trigger parameters along 5 | with a webhook parameter: 6 | 7 | | Parameter name | Required? | Description | 8 | | -------------- | :-------: | --------------------------------------------------------------- | 9 | | `inputs` | Yes | What inputs (defined in the manifest) are passed to the trigger | 10 | | `webook` | No | Contains a [filter](trigger-filters.md) | 11 | 12 | ### Webhook configuration 13 | 14 | A webhook trigger can contain an optional `webhook` configuration object which 15 | specifies a `filter`: 16 | 17 | ```ts 18 | webhook?: { 19 | filter: FilterObject; 20 | }; 21 | ``` 22 | 23 | ### Context data availability 24 | 25 | Like other trigger types, webhook triggers have access to context data which can 26 | be used to fill the `inputs` parameter. Unlike other triggers, the context data 27 | available to a webhook trigger is not predetermined, and will depend on the 28 | information sent along with the webhook to activate the trigger. Whatever data 29 | contained in the HTTP body of the webhook request is what will be available in 30 | `{{data}}`. So an HTTP request made with a body of `{"test": true}` would yield 31 | a context data object that could be referenced like `{{data.test}}`. 32 | 33 | ## Usage 34 | 35 | ### Webhook trigger without filter 36 | 37 | ```ts 38 | const trigger: Trigger = { 39 | type: "webhook", 40 | name: "Sample Webhook Trigger", 41 | description: "Starts the workflow to reverse a string", 42 | workflow: "#/workflows/reverse_workflow", 43 | inputs: { 44 | a_input: { 45 | value: "input", 46 | }, 47 | }, 48 | }; 49 | ``` 50 | 51 | ### Webhook trigger with filter 52 | 53 | ```ts 54 | const trigger: Trigger = { 55 | type: "webhook", 56 | name: "Sample Webhook Trigger", 57 | description: "Starts the workflow to reverse a string", 58 | workflow: "#/workflows/reverse_workflow", 59 | inputs: { 60 | a_input: { 61 | value: "input", 62 | }, 63 | }, 64 | webhook: { 65 | filter: { 66 | version: 1, 67 | root: { 68 | statement: "{{data.value}} == 1", 69 | }, 70 | }, 71 | }, 72 | }; 73 | ``` 74 | 75 | ## Example Response 76 | 77 | ```ts 78 | { 79 | ok: true, 80 | trigger: { 81 | id: "Ft0141BC3F2N", 82 | type: "webhook", 83 | workflow: { 84 | id: "Fn0141SXKUHZ", 85 | workflow_id: "Wf0141SXKULB", 86 | callback_id: "reverse_workflow", 87 | title: "Reverse Workflow", 88 | description: "A sample workflow", 89 | type: "workflow", 90 | input_parameters: [], 91 | output_parameters: [], 92 | app_id: "A01412HH666", 93 | app: { 94 | id: "A01412HH666", 95 | name: "my-app (dev)", 96 | icons: [], 97 | is_workflow_app: false 98 | }, 99 | date_updated: 1658339916 100 | }, 101 | inputs: { a_string: { value: "string", locked: false, hidden: false } }, 102 | outputs: {}, 103 | date_created: 1658339927, 104 | date_updated: 1658339927, 105 | webhook_url: "https://hooks.dev.slack.com/triggers/T013ZG3K1QT/5641534666242/5a398c41c55cbd2sd89q9dqw0qa7" 106 | } 107 | } 108 | ``` 109 | 110 | ## Invoking the trigger 111 | 112 | Send a POST request to invoke the trigger. Within that POST request you can send 113 | values for specific inputs. 114 | 115 | Example POST request 116 | 117 | ```bash 118 | curl \ 119 | -X POST "https://hooks.slack.com/triggers/T123ABC456/.../..." \ 120 | --header "Content-Type: application/json" \ 121 | --data "{"channel":"C123ABC456"}" 122 | ``` 123 | 124 | If successful, you'll get the following response: 125 | 126 | ```json 127 | { 128 | "ok": true 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/inputs.ts: -------------------------------------------------------------------------------- 1 | import type { NO_GENERIC_TITLE, WorkflowSchema } from "./mod.ts"; 2 | 3 | // deno-lint-ignore no-explicit-any 4 | type EmptyObject = Record<any, never>; 5 | 6 | /** The structure we expect that input parameters abide by */ 7 | export type InputParameterSchema = { 8 | // deno-lint-ignore no-explicit-any 9 | properties: Record<string, Record<string, any>>; 10 | required: (string | number)[]; 11 | }; 12 | 13 | interface BaseWorkflowInputs { 14 | /** @description The value of the workflow input parameter during workflow execution. Template variables may be used here. */ 15 | value?: unknown; 16 | /** @description Set to `true` to allow the input parameter to be customizable, meaning its value is provided separately from the trigger. */ 17 | customizable?: true; 18 | } 19 | 20 | type WorkflowInputValue = BaseWorkflowInputs & { 21 | // deno-lint-ignore no-explicit-any 22 | value: any; 23 | customizable?: never; 24 | }; 25 | 26 | type WorkflowInputCustomizableValue = BaseWorkflowInputs & { 27 | customizable: true; 28 | value?: never; 29 | }; 30 | 31 | /** The structure that must be provided to the workflow input to pass a value */ 32 | type WorkflowInput = WorkflowInputValue | WorkflowInputCustomizableValue; 33 | 34 | /** The structure for when inputs are empty */ 35 | type EmptyInputs = { 36 | /** @description The inputs provided to the workflow */ 37 | inputs?: never | EmptyObject; 38 | }; 39 | 40 | /** The structure for when inputs are populated */ 41 | type PopulatedInputs<Params extends InputParameterSchema> = { 42 | /** @description The inputs provided to the workflow */ 43 | inputs?: InputSchema<Params>; 44 | }; 45 | 46 | /** 47 | * Determines which input parameters are required, and which are optional 48 | * Returns an object where any string can be set to a valid WorkflowInput value 49 | */ 50 | type InputSchema<Params extends InputParameterSchema> = Params extends 51 | InputParameterSchema ? 52 | & { [k in keyof Params["properties"]]?: WorkflowInput } 53 | & { [k in Params["required"][number]]: WorkflowInput } 54 | : Record<string, WorkflowInput>; 55 | 56 | /** 57 | * Determines if the input object itself is required to pass to the trigger or not. 58 | * Returns an object where the input object is either required or optional. 59 | */ 60 | type WorkflowInputsType<Params extends InputParameterSchema> = 61 | // This intentionally avoids Distributive Conditional Types, so be careful removing any of the following square brackets 62 | // See https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types for more details 63 | [keyof Params["properties"]] extends [string] // Since never extends string, must check for no properties 64 | ? [keyof Params["properties"]] extends [never] ? EmptyInputs 65 | : Params["required"] extends Array<infer T> ? [T] extends [never] // If there are no required properties, inputs are optional 66 | ? PopulatedInputs<Params> 67 | // If there are required params, inputs are required 68 | : Required<PopulatedInputs<Params>> 69 | // If there are no inputs, don't allow them to be set 70 | : EmptyInputs 71 | : EmptyInputs; 72 | 73 | /** 74 | * Determines whether or not a valid value was passed to the generic. 75 | * Returns an object where the input object is either required or optional. 76 | */ 77 | export type WorkflowInputs<WorkflowDefinition extends WorkflowSchema> = 78 | WorkflowDefinition["title"] extends NO_GENERIC_TITLE ? { 79 | /** @description The inputs provided to the workflow. Either `value` or `customizable` should be provided, but not both. */ 80 | inputs?: Record<string, WorkflowInput>; 81 | } 82 | // This also intentionally avoids Distributive Conditional Types, so be careful removing the following square brackets 83 | : [keyof WorkflowDefinition["input_parameters"]] extends [string] 84 | ? WorkflowInputsType<NonNullable<WorkflowDefinition["input_parameters"]>> 85 | : EmptyInputs; 86 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guide 2 | 3 | Interested in contributing? Awesome! Before you do though, please read our 4 | [Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very 5 | seriously, and expect that you will as well. 6 | 7 | There are many ways you can contribute! :heart: 8 | 9 | ## :bug: Bug Reports and Fixes 10 | 11 | - If you find a bug, please search for it in the 12 | [Issues](https://github.com/slackapi/deno-slack-api/issues), and if it isn't 13 | already tracked, 14 | [create a new Bug Report Issue](https://github.com/slackapi/deno-slack-api/issues/new/choose). 15 | Fill out the "Bug Report" section of the issue template. Even if an Issue is 16 | closed, feel free to comment and add details, it will still be reviewed. 17 | - Issues that have already been identified as a bug (note: able to reproduce) 18 | will be labelled `bug`. 19 | - If you'd like to submit a fix for a bug, 20 | [send a Pull Request](#creating-a-pull-request) and mention the Issue number. 21 | - Include tests that isolate the bug and verifies that it was fixed. 22 | 23 | ## :bulb: New Features 24 | 25 | - If you'd like to add new functionality to this project, describe the problem 26 | you want to solve in a 27 | [new Feature Request Issue](https://github.com/slackapi/deno-slack-api/issues/new/choose). 28 | - Issues that have been identified as a feature request will be labelled 29 | `enhancement`. 30 | - If you'd like to implement the new feature, please wait for feedback from the 31 | project maintainers before spending too much time writing the code. In some 32 | cases, `enhancement`s may not align well with the project objectives at the 33 | time. 34 | 35 | ## :mag: Tests, :books: Documentation,:sparkles: Miscellaneous 36 | 37 | - If you'd like to improve the tests, you want to make the documentation 38 | clearer, you have an alternative implementation of something that may have 39 | advantages over the way its currently done, or you have any other change, we 40 | would be happy to hear about it! 41 | - If its a trivial change, go ahead and 42 | [send a Pull Request](#creating-a-pull-request) with the changes you have in 43 | mind. 44 | - If not, [open an Issue](https://github.com/slackapi/deno-slack-api/issues/new) 45 | to discuss the idea first. 46 | 47 | If you're new to our project and looking for some way to make your first 48 | contribution, look for Issues labelled `good first contribution`. 49 | 50 | ## Requirements 51 | 52 | For your contribution to be accepted: 53 | 54 | - [x] You must have signed the 55 | [Contributor License Agreement (CLA)](https://cla.salesforce.com/sign-cla). 56 | - [x] The test suite must be complete and pass. 57 | - [x] The changes must be approved by code review. 58 | - [x] Commits should be atomic and messages must be descriptive. Related issues 59 | should be mentioned by Issue number. 60 | 61 | If the contribution doesn't meet the above criteria, you may fail our automated 62 | checks or a maintainer will discuss it with you. You can continue to improve a 63 | Pull Request by adding commits to the branch from which the PR was created. 64 | 65 | [Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) 66 | 67 | ## Creating a Pull Request 68 | 69 | 1. :fork_and_knife: Fork the repository on GitHub. 70 | 2. :runner: Clone/fetch your fork to your local development machine. It's a good 71 | idea to run the tests just to make sure everything is in order. 72 | 3. :herb: Create a new branch and check it out. 73 | 4. :crystal_ball: Make your changes and commit them locally. Magic happens here! 74 | 5. :arrow_heading_up: Push your new branch to your fork. (e.g. 75 | `git push username fix-issue-16`). 76 | 6. :inbox_tray: Open a Pull Request on github.com from your new branch on your 77 | fork to `main` in this repository. 78 | 79 | ## Maintainers 80 | 81 | There are more details about processes and workflow in the 82 | [Maintainer's Guide](./maintainers_guide.md). 83 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/tests/shortcut_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertObjectMatch } from "@std/assert"; 2 | import type { ShortcutTrigger } from "../shortcut.ts"; 3 | import { TriggerTypes } from "../mod.ts"; 4 | import { stubFetch } from "../../../../../testing/http.ts"; 5 | import { SlackAPI } from "../../../../mod.ts"; 6 | import { shortcut_response } from "./fixtures/sample_responses.ts"; 7 | import type { 8 | ExampleWorkflow, 9 | RequiredInputWorkflow, 10 | } from "./fixtures/workflows.ts"; 11 | 12 | Deno.test("Shortcut triggers can set the type using the string", () => { 13 | const shortcut: ShortcutTrigger<ExampleWorkflow> = { 14 | type: "shortcut", 15 | name: "test", 16 | workflow: "#/workflows/example", 17 | }; 18 | assertEquals(shortcut.type, TriggerTypes.Shortcut); 19 | }); 20 | 21 | Deno.test("Shortcut triggers can set the type using the TriggerTypes object", () => { 22 | const shortcut: ShortcutTrigger<RequiredInputWorkflow> = { 23 | type: TriggerTypes.Shortcut, 24 | name: "test", 25 | workflow: "#/workflows/example", 26 | inputs: { required: { value: "example" } }, 27 | }; 28 | assertEquals(shortcut.type, TriggerTypes.Shortcut); 29 | }); 30 | 31 | Deno.test("Mock call for shortcut", async (t) => { 32 | await t.step("instantiated with default API URL", async (t) => { 33 | const client = SlackAPI("test-token"); 34 | 35 | await t.step("base methods exist on client", () => { 36 | assertEquals(typeof client.workflows.triggers.create, "function"); 37 | assertEquals(typeof client.workflows.triggers.update, "function"); 38 | }); 39 | 40 | await t.step("shortcut responses", async (t) => { 41 | await t.step( 42 | "should return successful response JSON on create", 43 | async () => { 44 | using _fetchStub = stubFetch( 45 | (req) => { 46 | assertEquals(req.method, "POST"); 47 | assertEquals( 48 | req.url, 49 | "https://slack.com/api/workflows.triggers.create", 50 | ); 51 | }, 52 | new Response(JSON.stringify(shortcut_response)), 53 | ); 54 | 55 | const res = await client.workflows.triggers.create({ 56 | name: "TEST", 57 | type: "shortcut", 58 | workflow: "#/workflows/reverse_workflow", 59 | inputs: { 60 | a_string: { 61 | value: "string", 62 | }, 63 | }, 64 | }); 65 | assertEquals(res.ok, true); 66 | if (res.ok) { 67 | assertObjectMatch(res.trigger, shortcut_response.trigger); 68 | assertEquals( 69 | res.trigger?.shortcut_url, 70 | shortcut_response.trigger.shortcut_url, 71 | ); 72 | } 73 | }, 74 | ); 75 | }); 76 | 77 | await t.step( 78 | "should return successful response JSON on update", 79 | async () => { 80 | using _fetchStub = stubFetch( 81 | (req) => { 82 | assertEquals(req.method, "POST"); 83 | assertEquals( 84 | req.url, 85 | "https://slack.com/api/workflows.triggers.update", 86 | ); 87 | }, 88 | new Response(JSON.stringify(shortcut_response)), 89 | ); 90 | 91 | const res = await client.workflows.triggers.update({ 92 | name: "TEST", 93 | type: "shortcut", 94 | trigger_id: "123", 95 | workflow: "#/workflows/reverse_workflow", 96 | inputs: { 97 | a_string: { 98 | value: "string", 99 | }, 100 | }, 101 | }); 102 | assertEquals(res.ok, true); 103 | if (res.ok) { 104 | assertObjectMatch(res.trigger, shortcut_response.trigger); 105 | assertEquals( 106 | res.trigger?.shortcut_url, 107 | shortcut_response.trigger.shortcut_url, 108 | ); 109 | } 110 | }, 111 | ); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /scripts/src/generate.ts: -------------------------------------------------------------------------------- 1 | import { toPascalCase } from "@std/text/to-pascal-case"; 2 | import { emptyDir, ensureDir } from "@std/fs"; 3 | import { APIMethodNode } from "./api-method-node.ts"; 4 | import { getPublicAPIMethods } from "./public-api-methods.ts"; 5 | 6 | const run = async () => { 7 | const methods = getPublicAPIMethods(); 8 | 9 | // Root node 10 | const api = new APIMethodNode("", false, "", true); 11 | 12 | for (const method of methods) { 13 | // Start with the root api node 14 | let currentNode = api; 15 | 16 | const parts = method.split("."); 17 | 18 | parts.forEach((partName, idx) => { 19 | let partNode = currentNode.findChildByName(partName); 20 | if (!partNode) { 21 | // If this is the last part, then it's the method 22 | const isMethod = idx === parts.length - 1; 23 | const nodePath = parts.slice(0, idx + 1).join("."); 24 | 25 | partNode = new APIMethodNode(partName, isMethod, nodePath, idx === 0); 26 | currentNode.addChild(partNode); 27 | } 28 | 29 | currentNode = partNode; 30 | }); 31 | } 32 | 33 | const outputDir = "./src/generated/method-types/"; 34 | 35 | await ensureDir(outputDir); 36 | await emptyDir(outputDir); 37 | 38 | // Cycle through each top level api group 39 | for (const groupNode of api.childNodes) { 40 | // Generate the code for this group api 41 | const groupCode = getGroupCode(groupNode); 42 | // write the code to a group-specific file 43 | await Deno.writeTextFile( 44 | `${outputDir}${groupNode.name}.ts`, 45 | groupCode, 46 | ); 47 | console.log(`wrote api file: ${groupNode.name}`); 48 | } 49 | 50 | // Write top level file that builds api 51 | const mainAPITypeCode = getMainAPICode(api); 52 | await Deno.writeTextFile( 53 | `${outputDir}mod.ts`, 54 | mainAPITypeCode, 55 | ); 56 | console.log("wrote main api file: mod.ts"); 57 | 58 | // Write generated test file to verify all methods are accounted for 59 | const testCode = getTestCode(api); 60 | await Deno.writeTextFile( 61 | `${outputDir}/api_method_types_test.ts`, 62 | testCode, 63 | ); 64 | console.log("wrote api test code: api_method_types_test.ts"); 65 | }; 66 | 67 | run(); 68 | 69 | const getMainAPICode = (api: APIMethodNode): string => { 70 | const imports = api.childNodes.map((node) => { 71 | const groupAPITypeName = `${toPascalCase(node.name)}APIType`; 72 | return `import { type ${groupAPITypeName} } from "./${node.name}.ts";`; 73 | }).join("\n"); 74 | 75 | const apiTypeMixins = api.childNodes.map((node) => { 76 | const groupAPIName = toPascalCase(node.name); 77 | return `${node.name}: ${groupAPIName}APIType,`; 78 | }).join("\n"); 79 | 80 | return ` 81 | ${imports} 82 | 83 | export type SlackAPIMethodsType = { 84 | ${apiTypeMixins} 85 | }; 86 | `; 87 | }; 88 | 89 | const getGroupCode = (groupNode: APIMethodNode) => { 90 | let imports = null; 91 | const groupCode = ` 92 | ${groupNode.getTypesCode()} 93 | `; 94 | if (groupCode.match(/SlackAPIMethod[,;]/)) { 95 | imports = "{ SlackAPIMethod"; 96 | } 97 | if (groupCode.match(/SlackAPICursorPaginatedMethod[,;]/)) { 98 | if (imports !== null) { 99 | imports += ", SlackAPICursorPaginatedMethod"; 100 | } else { 101 | imports = "{ SlackAPICursorPaginatedMethod"; 102 | } 103 | } 104 | imports += " }"; 105 | 106 | return `import type ${imports} from "../../types.ts";\n${groupCode}`; 107 | }; 108 | 109 | const getTestCode = (api: APIMethodNode) => { 110 | let assertions = ""; 111 | const visitMethodNodes = (node: APIMethodNode) => { 112 | if (node.isMethod) { 113 | assertions += 114 | `assertEquals(typeof client.${node.nodePath}, "function");\n`; 115 | } 116 | for (const child of node.childNodes) { 117 | visitMethodNodes(child); 118 | } 119 | }; 120 | 121 | visitMethodNodes(api); 122 | 123 | return ` 124 | import { assertEquals } from "@std/assert"; 125 | import { SlackAPI } from "../../mod.ts"; 126 | 127 | Deno.test("SlackAPIMethodsType generated types", () => { 128 | const client = SlackAPI("test-token"); 129 | 130 | ${assertions} 131 | }); 132 | 133 | `; 134 | }; 135 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/tests/webhook_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertObjectMatch } from "@std/assert"; 2 | import type { WebhookTrigger } from "../webhook.ts"; 3 | import { TriggerTypes } from "../mod.ts"; 4 | import { SlackAPI } from "../../../../mod.ts"; 5 | import { stubFetch } from "../../../../../testing/http.ts"; 6 | import { webhook_response } from "./fixtures/sample_responses.ts"; 7 | 8 | Deno.test("Webhook triggers can set the type using the string", () => { 9 | // deno-lint-ignore no-explicit-any 10 | const webhook: WebhookTrigger<any> = { 11 | type: "webhook", 12 | name: "test", 13 | workflow: "#/workflows/example", 14 | inputs: {}, 15 | }; 16 | assertEquals(webhook.type, TriggerTypes.Webhook); 17 | }); 18 | 19 | Deno.test("Webhook triggers can set the type using the TriggerTypes object", () => { 20 | // deno-lint-ignore no-explicit-any 21 | const webhook: WebhookTrigger<any> = { 22 | type: TriggerTypes.Webhook, 23 | name: "test", 24 | workflow: "#/workflows/example", 25 | inputs: {}, 26 | }; 27 | assertEquals(webhook.type, TriggerTypes.Webhook); 28 | }); 29 | 30 | Deno.test("Webhook triggers support an optional filter object", () => { 31 | // deno-lint-ignore no-explicit-any 32 | const webhook: WebhookTrigger<any> = { 33 | type: TriggerTypes.Webhook, 34 | name: "test", 35 | workflow: "#/workflows/example", 36 | inputs: {}, 37 | webhook: { 38 | filter: { 39 | version: 1, 40 | root: { 41 | statement: "1 === 1", 42 | }, 43 | }, 44 | }, 45 | }; 46 | assertEquals(typeof webhook.webhook, "object"); 47 | }); 48 | 49 | Deno.test("Mock call for webhook", async (t) => { 50 | await t.step("instantiated with default API URL", async (t) => { 51 | const client = SlackAPI("test-token"); 52 | 53 | await t.step("base methods exist on client", () => { 54 | assertEquals(typeof client.workflows.triggers.create, "function"); 55 | }); 56 | 57 | await t.step("webhook responses", async (t) => { 58 | await t.step( 59 | "should return successful response JSON on create", 60 | async () => { 61 | using _fetchStub = stubFetch( 62 | (req) => { 63 | assertEquals(req.method, "POST"); 64 | assertEquals( 65 | req.url, 66 | "https://slack.com/api/workflows.triggers.create", 67 | ); 68 | }, 69 | new Response(JSON.stringify(webhook_response)), 70 | ); 71 | 72 | const res = await client.workflows.triggers.create({ 73 | name: "TEST", 74 | type: "webhook", 75 | workflow: "#/workflows/reverse_workflow", 76 | inputs: { 77 | a_string: { 78 | value: "string", 79 | }, 80 | }, 81 | }); 82 | assertEquals(res.ok, true); 83 | if (res.ok) { 84 | assertObjectMatch(res.trigger, webhook_response.trigger); 85 | assertEquals( 86 | res.trigger?.webhook_url, 87 | webhook_response.trigger.webhook_url, 88 | ); 89 | } 90 | }, 91 | ); 92 | }); 93 | 94 | await t.step( 95 | "should return successful response JSON on update", 96 | async () => { 97 | using _fetchStub = stubFetch( 98 | (req) => { 99 | assertEquals(req.method, "POST"); 100 | assertEquals( 101 | req.url, 102 | "https://slack.com/api/workflows.triggers.update", 103 | ); 104 | }, 105 | new Response(JSON.stringify(webhook_response)), 106 | ); 107 | 108 | const res = await client.workflows.triggers.update({ 109 | name: "TEST", 110 | type: "webhook", 111 | trigger_id: "123", 112 | workflow: "#/workflows/reverse_workflow", 113 | inputs: { 114 | a_string: { 115 | value: "string", 116 | }, 117 | }, 118 | }); 119 | assertEquals(res.ok, true); 120 | if (res.ok) { 121 | assertObjectMatch(res.trigger, webhook_response.trigger); 122 | assertEquals( 123 | res.trigger?.webhook_url, 124 | webhook_response.trigger.webhook_url, 125 | ); 126 | } 127 | }, 128 | ); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { TypedSlackAPIMethodsType } from "./typed-method-types/mod.ts"; 2 | import type { SlackAPIMethodsType } from "./generated/method-types/mod.ts"; 3 | 4 | export type { DatastoreItem } from "./typed-method-types/apps.ts"; 5 | 6 | export type { 7 | ValidTriggerTypes as Trigger, 8 | } from "./typed-method-types/workflows/triggers/mod.ts"; 9 | 10 | // TODO: [brk-chg] remove this in favor of `Response` 11 | export type BaseResponse = { 12 | /** 13 | * @description `true` if the response from the server was successful, `false` otherwise. 14 | */ 15 | ok: boolean; 16 | /** 17 | * @description: Optional error description returned by the server. 18 | */ 19 | error?: string; 20 | /** 21 | * @description Optional list of warnings returned by the server. 22 | */ 23 | warnings?: string[]; 24 | /** 25 | * @description Optional metadata about the response returned by the server. 26 | */ 27 | "response_metadata"?: { 28 | warnings?: string[]; 29 | messages?: string[]; 30 | }; 31 | 32 | /** 33 | * @description Get the original `Response` object created by `fetch` 34 | * 35 | * ```ts 36 | * const originalResponse = response.toFetchResponse(); 37 | * console.log(originalResponse.headers); 38 | * ``` 39 | */ 40 | toFetchResponse(): Response; 41 | 42 | // deno-lint-ignore no-explicit-any 43 | [otherOptions: string]: any; 44 | }; 45 | 46 | export type SlackAPIClient = 47 | & BaseSlackClient 48 | & TypedSlackAPIMethodsType 49 | & SlackAPIMethodsType; 50 | 51 | export type BaseSlackClient = { 52 | setSlackApiUrl: (slackApiUrl: string) => BaseSlackClient; 53 | apiCall: BaseClientCall; 54 | response: BaseClientResponse; 55 | }; 56 | 57 | // TODO: [brk-chg] return a `Promise<Response>` object 58 | type BaseClientCall = ( 59 | method: string, 60 | data?: SlackAPIMethodArgs, 61 | ) => Promise<BaseResponse>; 62 | 63 | // TODO: [brk-chg] return a `Promise<Response>` object 64 | type BaseClientResponse = ( 65 | url: string, 66 | data: Record<string, unknown>, 67 | ) => Promise<BaseResponse>; 68 | 69 | export type SlackAPIOptions = { 70 | /** 71 | * @description Optional url endpoint for the Slack API used for api calls. Defaults to https://slack.com/api/ 72 | */ 73 | slackApiUrl?: string; 74 | }; 75 | 76 | export type BaseMethodArgs = { 77 | /** 78 | * @description Optional override token. If set, it will be used as the token 79 | * for this single API call rather than the token provided when creating the client. 80 | */ 81 | token?: string; 82 | }; 83 | 84 | export type CursorPaginationArgs = { 85 | /** 86 | * @description Paginate through collections of data by setting the `cursor` parameter 87 | * to a `next_cursor` attribute returned by a previous request's `response_metadata`. 88 | * Default value fetches the first "page" of the collection. 89 | * Used in conjunction with `limit`, these parameters allow for 90 | * {@link https://api.slack.com/docs/pagination#cursors cursor-based pagination}. 91 | */ 92 | cursor?: string; 93 | /** 94 | * @description The maximum number of items to return. Fewer than the requested 95 | * number of items may be returned, even if the end of the result list hasn't 96 | * been reached. 97 | * Used in conjunction with `cursor`, these parameters allow for 98 | * {@link https://api.slack.com/docs/pagination#cursors cursor-based pagination}. 99 | */ 100 | limit?: number; 101 | }; 102 | 103 | export type CursorPaginationResponse = { 104 | "response_metadata"?: { 105 | /** 106 | * @description A pointer that can be provided as parameter for a follow-up 107 | * call to the same API to retrieve the next set of results, should more exist. 108 | * If this property does not exist or is the empty string, there are no further 109 | * results to retrieve. 110 | * See {@link https://api.slack.com/docs/pagination#cursors our docs on cursor-based pagination} 111 | * for more details 112 | */ 113 | next_cursor?: string; 114 | }; 115 | }; 116 | 117 | export type SlackAPIMethodArgs = BaseMethodArgs & { 118 | [name: string]: unknown; 119 | }; 120 | 121 | export type SlackAPIMethod = { 122 | (args?: SlackAPIMethodArgs): Promise<BaseResponse>; 123 | }; 124 | 125 | export type SlackAPICursorPaginatedMethod = { 126 | ( 127 | args?: SlackAPIMethodArgs & CursorPaginationArgs, 128 | ): Promise<BaseResponse & CursorPaginationResponse>; 129 | }; 130 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event-data/mod.ts: -------------------------------------------------------------------------------- 1 | import { AppMentioned } from "./app_mentioned.ts"; 2 | import { ChannelArchived } from "./channel_archived.ts"; 3 | import { ChannelCreated } from "./channel_created.ts"; 4 | import { ChannelDeleted } from "./channel_deleted.ts"; 5 | import { ChannelRenamed } from "./channel_renamed.ts"; 6 | import { ChannelShared } from "./channel_shared.ts"; 7 | import { ChannelUnarchived } from "./channel_unarchived.ts"; 8 | import { ChannelUnshared } from "./channel_unshared.ts"; 9 | import { DndUpdated } from "./dnd_updated.ts"; 10 | import { EmojiChanged } from "./emoji_changed.ts"; 11 | import { MessageMetadataPosted } from "./message_metadata_posted.ts"; 12 | import { MessagePosted } from "./message_posted.ts"; 13 | import { PinAdded } from "./pin_added.ts"; 14 | import { PinRemoved } from "./pin_removed.ts"; 15 | import { ReactionAdded } from "./reaction_added.ts"; 16 | import { ReactionRemoved } from "./reaction_removed.ts"; 17 | import { SharedChannelInviteAccepted } from "./shared_channel_invite_accepted.ts"; 18 | import { SharedChannelInviteApproved } from "./shared_channel_invite_approved.ts"; 19 | import { SharedChannelInviteDeclined } from "./shared_channel_invite_declined.ts"; 20 | import { SharedChannelInviteReceived } from "./shared_channel_invite_received.ts"; 21 | import { UserJoinedChannel } from "./user_joined_channel.ts"; 22 | import { UserJoinedTeam } from "./user_joined_team.ts"; 23 | import { UserLeftChannel } from "./user_left_channel.ts"; 24 | 25 | /** 26 | * Event-trigger-specific input values that contain information about the event trigger. 27 | * Different events will contain different data. 28 | */ 29 | export const EventTriggerContextData = { 30 | /** 31 | * A message event that mentions the app. 32 | */ 33 | AppMentioned, 34 | /** 35 | * A channel was archived in the workspace. 36 | */ 37 | ChannelArchived, 38 | /** 39 | * A channel was created in the workspace. 40 | */ 41 | ChannelCreated, 42 | /** 43 | * A channel was deleted in the workspace. 44 | */ 45 | ChannelDeleted, 46 | /** 47 | * A channel was renamed in the workspace. 48 | */ 49 | ChannelRenamed, 50 | /** 51 | * A channel was shared with another workspace or team. 52 | */ 53 | ChannelShared, 54 | /** 55 | * A channel was unarchived in the workspace. 56 | */ 57 | ChannelUnarchived, 58 | /** 59 | * A channel was unshared with another workspace or team. 60 | */ 61 | ChannelUnshared, 62 | /** 63 | * Do Not Disturb settings updated for a member. 64 | */ 65 | DndUpdated, 66 | /** 67 | * An emoji was added, removed or renamed in the workspace. 68 | */ 69 | EmojiChanged, 70 | /** 71 | * A message event that contains {@link https://api.slack.com/metadata Message Metadata}. 72 | */ 73 | MessageMetadataPosted, 74 | /** 75 | * A message was sent to a channel. NOTE: a {@link https://api.slack.com/automation/triggers/event#filters trigger filter} is required to listen for this event. 76 | */ 77 | MessagePosted, 78 | /** 79 | * A message was pinned to a channel. 80 | */ 81 | PinAdded, 82 | /** 83 | * A message was unpinned from a channel. 84 | */ 85 | PinRemoved, 86 | /** 87 | * An emoji reaction was added to a message. 88 | */ 89 | ReactionAdded, 90 | /** 91 | * An emoji reaction was removed from a message. 92 | */ 93 | ReactionRemoved, 94 | /** 95 | * A user accepted a {@link https://slack.com/connect Slack Connect} invite to a shared channel. 96 | */ 97 | SharedChannelInviteAccepted, 98 | /** 99 | * An admin approved a {@link https://slack.com/connect Slack Connect} invite to a shared channel. 100 | */ 101 | SharedChannelInviteApproved, 102 | /** 103 | * An admin declined a {@link https://slack.com/connect Slack Connect} invite to a shared channel. 104 | */ 105 | SharedChannelInviteDeclined, 106 | /** 107 | * A user received a {@link https://slack.com/connect Slack Connect} invite to a shared channel. 108 | * NOTE: this event will only trip when the {@link https://api.slack.com/methods/conversations.inviteShared inviteShared API} is used programmatically! 109 | */ 110 | SharedChannelInviteReceived, 111 | /** 112 | * A user joined a workspace channel. 113 | */ 114 | UserJoinedChannel, 115 | /** 116 | * A user joined a workspace or team. 117 | */ 118 | UserJoinedTeam, 119 | /** 120 | * A user left a workspace channel. 121 | */ 122 | UserLeftChannel, 123 | } as const; 124 | -------------------------------------------------------------------------------- /src/base-client-helpers_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "@std/assert"; 2 | import { 3 | _internals, 4 | getUserAgent, 5 | serializeData, 6 | } from "./base-client-helpers.ts"; 7 | import { assertSpyCalls, stub } from "@std/testing/mock"; 8 | 9 | Deno.test(`base-client-helpers.${_internals.getModuleVersion.name}`, async (t) => { 10 | await t.step( 11 | "should return the version if the module is sourced from deno.land", 12 | () => { 13 | const getModuleUrlStub = stub(_internals, "getModuleUrl", () => { 14 | return new URL("https://deno.land/x/deno_slack_api@2.1.0/mod.ts)"); 15 | }); 16 | 17 | try { 18 | const moduleVersion = _internals.getModuleVersion(); 19 | 20 | assertSpyCalls(getModuleUrlStub, 1); 21 | assertEquals(moduleVersion, "2.1.0"); 22 | } finally { 23 | getModuleUrlStub.restore(); 24 | } 25 | }, 26 | ); 27 | 28 | await t.step( 29 | "should return undefined if the module is not sourced from deno.land", 30 | () => { 31 | const getModuleUrlStub = stub(_internals, "getModuleUrl", () => { 32 | return new URL("file:///hello/world.ts)"); 33 | }); 34 | try { 35 | const moduleVersion = _internals.getModuleVersion(); 36 | 37 | assertSpyCalls(getModuleUrlStub, 1); 38 | assertEquals(moduleVersion, undefined); 39 | } finally { 40 | getModuleUrlStub.restore(); 41 | } 42 | }, 43 | ); 44 | 45 | await t.step( 46 | "should return undefined if the regex used to parse deno_slack_api@x.x.x fails", 47 | () => { 48 | const getModuleUrlStub = stub(_internals, "getModuleUrl", () => { 49 | return new URL("https://deno.land/x/deno_slack_sdk@2.1.0/mod.ts)"); 50 | }); 51 | try { 52 | const moduleVersion = _internals.getModuleVersion(); 53 | 54 | assertSpyCalls(getModuleUrlStub, 1); 55 | assertEquals(moduleVersion, undefined); 56 | } finally { 57 | getModuleUrlStub.restore(); 58 | } 59 | }, 60 | ); 61 | }); 62 | 63 | Deno.test(`base-client-helpers.${getUserAgent.name}`, async (t) => { 64 | await t.step( 65 | "should return the user agent with deno version, OS name and undefined deno-slack-api version", 66 | () => { 67 | const expectedVersion = undefined; 68 | const getModuleUrlStub = stub(_internals, "getModuleVersion", () => { 69 | return expectedVersion; 70 | }); 71 | 72 | try { 73 | const userAgent = getUserAgent(); 74 | 75 | assertSpyCalls(getModuleUrlStub, 1); 76 | assertEquals( 77 | userAgent, 78 | `Deno/${Deno.version.deno} OS/${Deno.build.os} deno-slack-api/undefined`, 79 | ); 80 | } finally { 81 | getModuleUrlStub.restore(); 82 | } 83 | }, 84 | ); 85 | 86 | await t.step( 87 | "should return the user agent with deno version, OS name and deno-slack-api version", 88 | () => { 89 | const expectedVersion = "2.1.0"; 90 | const getModuleUrlStub = stub(_internals, "getModuleUrl", () => { 91 | return new URL( 92 | `https://deno.land/x/deno_slack_api@${expectedVersion}/mod.ts)`, 93 | ); 94 | }); 95 | 96 | try { 97 | const userAgent = getUserAgent(); 98 | 99 | assertSpyCalls(getModuleUrlStub, 1); 100 | assertEquals( 101 | userAgent, 102 | `Deno/${Deno.version.deno} OS/${Deno.build.os} deno-slack-api/${expectedVersion}`, 103 | ); 104 | } finally { 105 | getModuleUrlStub.restore(); 106 | } 107 | }, 108 | ); 109 | }); 110 | 111 | Deno.test(`${serializeData.name} helper function`, async (t) => { 112 | await t.step( 113 | "should serialize string values as strings and return a URLSearchParams object", 114 | () => { 115 | assertEquals( 116 | serializeData({ "batman": "robin" }).toString(), 117 | "batman=robin", 118 | ); 119 | }, 120 | ); 121 | await t.step( 122 | "should serialize non-string values as JSON-encoded strings and return a URLSearchParams object", 123 | () => { 124 | assertEquals( 125 | serializeData({ "hockey": { "good": true, "awesome": "yes" } }) 126 | .toString(), 127 | "hockey=%7B%22good%22%3Atrue%2C%22awesome%22%3A%22yes%22%7D", 128 | ); 129 | }, 130 | ); 131 | await t.step( 132 | "should not serialize undefined values", 133 | () => { 134 | assertEquals( 135 | serializeData({ 136 | "hockey": { "good": true, "awesome": "yes" }, 137 | "baseball": undefined, 138 | }) 139 | .toString(), 140 | "hockey=%7B%22good%22%3Atrue%2C%22awesome%22%3A%22yes%22%7D", 141 | ); 142 | }, 143 | ); 144 | }); 145 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/scheduled.ts: -------------------------------------------------------------------------------- 1 | import type { BaseResponse } from "../../../types.ts"; 2 | import type { BaseTriggerResponse } from "./base_response.ts"; 3 | import type { 4 | BaseTrigger, 5 | FailedTriggerResponse, 6 | TriggerTypes, 7 | WorkflowSchema, 8 | } from "./mod.ts"; 9 | 10 | export const SCHEDULE_FREQUENCY = { 11 | Once: "once", 12 | Hourly: "hourly", 13 | Daily: "daily", 14 | Weekly: "weekly", 15 | Monthly: "monthly", 16 | Yearly: "yearly", 17 | } as const; 18 | 19 | export const WEEKDAYS = { 20 | Monday: "Monday", 21 | Tuesday: "Tuesday", 22 | Wednesday: "Wednesday", 23 | Thursday: "Thursday", 24 | Friday: "Friday", 25 | Saturday: "Saturday", 26 | Sunday: "Sunday", 27 | } as const; 28 | 29 | type FrequencyUnion = 30 | typeof SCHEDULE_FREQUENCY[keyof typeof SCHEDULE_FREQUENCY]; 31 | type WeekdayUnion = typeof WEEKDAYS[keyof typeof WEEKDAYS]; 32 | 33 | type BaseFrequencyType = { 34 | /** @description How often the trigger will activate */ 35 | type: FrequencyUnion; 36 | /** @description The days of the week the trigger should activate on (not available for daily triggers) */ 37 | on_days?: WeekdayUnion[]; 38 | /** @description How often the trigger will repeat, respective to the frequency type */ 39 | repeats_every?: number; 40 | /** 41 | * @description The nth week of the chosen frequency type (not available for daily, weekly, or yearly triggers) 42 | * @example 3 — The 3rd week of the month 43 | * @example -1 — The last week of the month */ 44 | on_week_num?: 1 | 2 | 3 | 4 | -1; 45 | }; 46 | 47 | type OnceFrequencyType = { 48 | /** @description How often the trigger will activate */ 49 | type: typeof SCHEDULE_FREQUENCY.Once; 50 | }; 51 | 52 | type HourlyFrequencyType = 53 | & { 54 | /** @description How often the trigger will activate */ 55 | type: typeof SCHEDULE_FREQUENCY.Hourly; 56 | } 57 | & Pick<BaseFrequencyType, "repeats_every">; 58 | 59 | type DailyFrequencyType = 60 | & { 61 | /** @description How often the trigger will activate */ 62 | type: typeof SCHEDULE_FREQUENCY.Daily; 63 | } 64 | & Pick<BaseFrequencyType, "repeats_every">; 65 | 66 | type WeeklyFrequencyType = { 67 | /** @description How often the trigger will activate */ 68 | type: typeof SCHEDULE_FREQUENCY.Weekly; 69 | } & Pick<BaseFrequencyType, "on_days" | "repeats_every">; 70 | 71 | type MonthlyFrequencyType = { 72 | /** @description How often the trigger will activate */ 73 | type: typeof SCHEDULE_FREQUENCY.Monthly; 74 | /** @description The days of the week the trigger should activate on (not available for daily triggers) */ 75 | on_days?: [WeekdayUnion]; 76 | } & Pick<BaseFrequencyType, "repeats_every" | "on_week_num">; 77 | 78 | type YearlyFrequencyType = { 79 | /** @description How often the trigger will activate */ 80 | type: typeof SCHEDULE_FREQUENCY.Yearly; 81 | } & Pick<BaseFrequencyType, "repeats_every">; 82 | 83 | type FrequencyType = 84 | | HourlyFrequencyType 85 | | DailyFrequencyType 86 | | WeeklyFrequencyType 87 | | MonthlyFrequencyType 88 | | YearlyFrequencyType; 89 | 90 | type BaseTriggerSchedule = { 91 | /** 92 | * @description An ISO 8601 date string of when this scheduled trigger should first occur 93 | * @example "2022-03-01T14:00:00Z" 94 | */ 95 | start_time: string; 96 | /** 97 | * @description A timezone string to use for scheduling 98 | * @default "UTC" 99 | */ 100 | timezone?: string; 101 | }; 102 | 103 | type SingleOccurrenceTriggerSchedule = BaseTriggerSchedule & { 104 | frequency?: OnceFrequencyType; 105 | end_time?: never; 106 | occurrence_count?: never; 107 | }; 108 | 109 | type RecurringTriggerSchedule = 110 | & BaseTriggerSchedule 111 | & { 112 | /** @description If set, this trigger will not run past the provided date string */ 113 | end_time?: string; 114 | /** @description The maximum number of times the trigger may run */ 115 | occurrence_count?: number; 116 | /** @description The configurable frequency of which this trigger will activate */ 117 | frequency: FrequencyType; 118 | }; 119 | 120 | type TriggerSchedule = 121 | | RecurringTriggerSchedule 122 | | SingleOccurrenceTriggerSchedule; 123 | 124 | export type ScheduledTrigger< 125 | WorkflowDefinition extends WorkflowSchema, 126 | > = 127 | & BaseTrigger<WorkflowDefinition> 128 | & { 129 | type: typeof TriggerTypes.Scheduled; 130 | schedule: TriggerSchedule; 131 | }; 132 | 133 | export type ScheduledTriggerResponse< 134 | WorkflowDefinition extends WorkflowSchema, 135 | > = Promise< 136 | | ScheduledResponse<WorkflowDefinition> 137 | | FailedTriggerResponse 138 | >; 139 | export type ScheduledResponse< 140 | WorkflowDefinition extends WorkflowSchema, 141 | > = 142 | & BaseResponse 143 | & { 144 | trigger: ScheduledTriggerResponseObject<WorkflowDefinition>; 145 | }; 146 | 147 | export type ScheduledTriggerResponseObject< 148 | WorkflowDefinition extends WorkflowSchema, 149 | > = 150 | & BaseTriggerResponse<WorkflowDefinition> 151 | & { 152 | /** 153 | * @description A schedule object returned by scheduled triggers 154 | */ 155 | // deno-lint-ignore no-explicit-any 156 | schedule?: Record<string, any>; 157 | }; 158 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/event.ts: -------------------------------------------------------------------------------- 1 | import type { BaseResponse } from "../../../types.ts"; 2 | import type { BaseTriggerResponse } from "./base_response.ts"; 3 | import type { 4 | BaseTrigger, 5 | FailedTriggerResponse, 6 | TriggerTypes, 7 | WorkflowSchema, 8 | } from "./mod.ts"; 9 | import type { FilterType } from "./trigger-filter.ts"; 10 | import type { 11 | ObjectValueUnion, 12 | PopulatedArray, 13 | } from "../../../type-helpers.ts"; 14 | import type { TriggerEventTypes } from "./trigger-event-types.ts"; 15 | 16 | type MessageMetadataTypes = ObjectValueUnion< 17 | Pick< 18 | typeof TriggerEventTypes, 19 | "MessageMetadataPosted" /* | "MessageMetadataAdded" | "MessageMetadataDeleted" */ 20 | > 21 | >; 22 | 23 | type MessagePostedEventType = ObjectValueUnion< 24 | Pick<typeof TriggerEventTypes, "MessagePosted"> 25 | >; 26 | 27 | type ChannelTypes = ObjectValueUnion< 28 | Pick< 29 | typeof TriggerEventTypes, 30 | | "AppMentioned" 31 | | "ChannelShared" 32 | | "ChannelUnshared" 33 | | "MessageMetadataPosted" 34 | | "PinAdded" 35 | | "PinRemoved" 36 | | "ReactionAdded" 37 | | "ReactionRemoved" 38 | | "UserJoinedChannel" 39 | | "UserLeftChannel" 40 | > 41 | >; 42 | 43 | type WorkspaceTypes = ObjectValueUnion< 44 | Pick< 45 | typeof TriggerEventTypes, 46 | | "ChannelArchived" 47 | | "ChannelCreated" 48 | | "ChannelDeleted" 49 | | "ChannelRenamed" 50 | | "ChannelUnarchived" 51 | | "DndUpdated" 52 | | "EmojiChanged" 53 | | "SharedChannelInviteAccepted" 54 | | "SharedChannelInviteApproved" 55 | | "SharedChannelInviteDeclined" 56 | | "SharedChannelInviteReceived" 57 | | "UserJoinedTeam" 58 | > 59 | >; 60 | 61 | type ChannelEvents = 62 | & (ChannelEvent | MetadataChannelEvent | MessagePostedEvent) // controls `event_type` and `filter` 63 | & (ChannelUnscopedEvent | ChannelScopedEvent); // controls event scoping: `channel_ids` and `all_resources` 64 | 65 | /** 66 | * Event that is unscoped and not limited to a specific channel 67 | */ 68 | type ChannelUnscopedEvent = { 69 | /** @description If set to `true`, will trigger in all channels. `false` by default and mutually exclusive with `channel_ids`. */ 70 | all_resources: true; 71 | /** @description The channel ids that this event listens on. Mutually exclusive with `all_resources: true`. */ 72 | channel_ids?: never; 73 | }; 74 | 75 | /** 76 | * Event that is scoped to specific channel ID(s) 77 | */ 78 | type ChannelScopedEvent = { 79 | /** @description The channel ids that this event listens on. Mutually exclusive with `all_resources: true`. */ 80 | channel_ids: PopulatedArray<string>; 81 | /** @description If set to `true`, will trigger in all channels. `false` by default and mutually exclusive with `channel_ids`. */ 82 | all_resources?: false; 83 | }; 84 | 85 | type ChannelEvent = BaseEvent & { 86 | /** @description The type of event */ 87 | event_type: Exclude<ChannelTypes, MessageMetadataTypes>; 88 | }; 89 | 90 | type MetadataChannelEvent = ChannelEvent & { 91 | /** @description User defined description for the metadata event type */ 92 | metadata_event_type: string; 93 | }; 94 | 95 | // The only event that currently requires a filter 96 | type MessagePostedEvent = 97 | & BaseEvent 98 | & Required<Pick<BaseEvent, "filter">> 99 | & { 100 | /** @description The type of event */ 101 | event_type: MessagePostedEventType; 102 | }; 103 | 104 | type WorkspaceEvents = BaseEvent & { 105 | /** @description The type of event */ 106 | event_type: WorkspaceTypes; 107 | /** @description The team IDs that this event should listen on. Must be included when used on Enterprise Grid and working with workspace-based event triggers. */ 108 | team_ids?: PopulatedArray<string>; 109 | }; 110 | 111 | type BaseEvent = { 112 | // TODO: (breaking change) filter should not be optional here, but explicitly chosen for the events that accept it; 113 | // could use similar technique as we do to manage messagemetadata-specific properties (above) 114 | /** @description Defines the condition in which this event trigger should execute the workflow */ 115 | filter?: FilterType; 116 | // deno-lint-ignore no-explicit-any 117 | } & Record<string, any>; 118 | 119 | export type EventTrigger<WorkflowDefinition extends WorkflowSchema> = 120 | & BaseTrigger<WorkflowDefinition> 121 | & { 122 | type: typeof TriggerTypes.Event; 123 | /** @description The payload object for event triggers */ 124 | event: ChannelEvents | WorkspaceEvents; 125 | }; 126 | 127 | export type EventTriggerResponse< 128 | WorkflowDefinition extends WorkflowSchema, 129 | > = Promise< 130 | EventResponse<WorkflowDefinition> | FailedTriggerResponse 131 | >; 132 | export type EventResponse< 133 | WorkflowDefinition extends WorkflowSchema, 134 | > = 135 | & BaseResponse 136 | & { 137 | trigger: EventTriggerResponseObject<WorkflowDefinition>; 138 | }; 139 | 140 | export type EventTriggerResponseObject< 141 | WorkflowDefinition extends WorkflowSchema, 142 | > = 143 | & BaseTriggerResponse<WorkflowDefinition> 144 | & { 145 | /** 146 | * @description The type of event specified for the event trigger 147 | */ 148 | event_type?: string; 149 | } 150 | // deno-lint-ignore no-explicit-any 151 | & Record<string, any>; 152 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/tests/crud_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertObjectMatch } from "@std/assert"; 2 | import { SlackAPI } from "../../../../mod.ts"; 3 | import { stubFetch } from "../../../../../testing/http.ts"; 4 | import { 5 | delete_response, 6 | shortcut_response, 7 | } from "./fixtures/sample_responses.ts"; 8 | import { list_response } from "./fixtures/list_response.ts"; 9 | 10 | Deno.test("Mock CRUD call", async (t) => { 11 | await t.step("instantiated with default API URL", async (t) => { 12 | const client = SlackAPI("test-token"); 13 | 14 | await t.step("base methods exist on client", () => { 15 | assertEquals(typeof client.workflows.triggers.create, "function"); 16 | assertEquals(typeof client.workflows.triggers.update, "function"); 17 | assertEquals(typeof client.workflows.triggers.delete, "function"); 18 | assertEquals(typeof client.workflows.triggers.list, "function"); 19 | }); 20 | 21 | await t.step("create method", async (t) => { 22 | await t.step("should call the default API URL", async () => { 23 | using _fetchStub = stubFetch( 24 | (req) => { 25 | assertEquals(req.method, "POST"); 26 | assertEquals( 27 | req.url, 28 | "https://slack.com/api/workflows.triggers.create", 29 | ); 30 | }, 31 | new Response('{"ok":true}'), 32 | ); 33 | 34 | await client.workflows.triggers.create({ 35 | name: "TEST", 36 | type: "event", 37 | workflow: "#/workflows/reverse_workflow", 38 | inputs: { 39 | a_string: { 40 | value: "string", 41 | }, 42 | }, 43 | event: { 44 | event_type: "slack#/events/reaction_added", 45 | channel_ids: ["C013ZG3K41Z"], 46 | }, 47 | }); 48 | }); 49 | 50 | await t.step( 51 | "should return successful response JSON on create", 52 | async () => { 53 | using _fetchStub = stubFetch( 54 | (req) => { 55 | assertEquals(req.method, "POST"); 56 | assertEquals( 57 | req.url, 58 | "https://slack.com/api/workflows.triggers.create", 59 | ); 60 | }, 61 | new Response(JSON.stringify(shortcut_response)), 62 | ); 63 | 64 | const res = await client.workflows.triggers.create({ 65 | name: "TEST", 66 | type: "shortcut", 67 | workflow: "#/workflows/reverse_workflow", 68 | inputs: { 69 | a_string: { 70 | value: "string", 71 | }, 72 | }, 73 | }); 74 | assertEquals(res.ok, true); 75 | if (res.ok) { 76 | assertObjectMatch(res.trigger, shortcut_response.trigger); 77 | assertEquals( 78 | res.trigger?.shortcut_url, 79 | shortcut_response.trigger.shortcut_url, 80 | ); 81 | } 82 | }, 83 | ); 84 | }); 85 | 86 | await t.step( 87 | "should return successful response JSON on update", 88 | async () => { 89 | using _fetchStub = stubFetch( 90 | (req) => { 91 | assertEquals(req.method, "POST"); 92 | assertEquals( 93 | req.url, 94 | "https://slack.com/api/workflows.triggers.update", 95 | ); 96 | }, 97 | new Response(JSON.stringify(shortcut_response)), 98 | ); 99 | 100 | const res = await client.workflows.triggers.update({ 101 | name: "TEST", 102 | type: "shortcut", 103 | trigger_id: "123", 104 | workflow: "#/workflows/reverse_workflow", 105 | inputs: { 106 | a_string: { 107 | value: "string", 108 | }, 109 | }, 110 | }); 111 | assertEquals(res.ok, true); 112 | if (res.ok) { 113 | assertObjectMatch(res.trigger, shortcut_response.trigger); 114 | assertEquals( 115 | res.trigger?.shortcut_url, 116 | shortcut_response.trigger.shortcut_url, 117 | ); 118 | } 119 | }, 120 | ); 121 | 122 | await t.step( 123 | "should return successful response JSON on delete", 124 | async () => { 125 | using _fetchStub = stubFetch( 126 | (req) => { 127 | assertEquals(req.method, "POST"); 128 | assertEquals( 129 | req.url, 130 | "https://slack.com/api/workflows.triggers.delete", 131 | ); 132 | }, 133 | new Response(JSON.stringify(delete_response)), 134 | ); 135 | 136 | const res = await client.workflows.triggers.delete({ 137 | trigger_id: "123", 138 | }); 139 | assertEquals(res.ok, true); 140 | }, 141 | ); 142 | 143 | await t.step( 144 | "should return successful response JSON on list", 145 | async () => { 146 | using _fetchStub = stubFetch( 147 | (req) => { 148 | assertEquals(req.method, "POST"); 149 | assertEquals( 150 | req.url, 151 | "https://slack.com/api/workflows.triggers.list", 152 | ); 153 | }, 154 | new Response(JSON.stringify(list_response)), 155 | ); 156 | 157 | const res = await client.workflows.triggers.list(); 158 | assertEquals(res.ok, true); 159 | assertEquals(res.triggers?.length, list_response.triggers.length); 160 | }, 161 | ); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /docs/triggers/link-triggers.md: -------------------------------------------------------------------------------- 1 | ## Link triggers 2 | 3 | A link trigger is an interactive trigger that activates when a link is clicked 4 | in the Slack client. When a link trigger is created, its API response returns a 5 | `shortcut_url` which can be used in Slack to show a button. Clicking on this 6 | button will activate the associated workflow. A link trigger includes the common 7 | [trigger attributes](./trigger-basics.md#trigger-types) along with an optional 8 | shortcut parameter: 9 | 10 | | Parameter name | Required? | Description | 11 | | -------------- | :-------: | ------------------------------------------------------------------------------ | 12 | | `shortcut` | No | Contains information about the [button text](#shortcut-object) of the shortcut | 13 | 14 | ### Shortcut configuration 15 | 16 | A link trigger can contain an optional `shortcut` configuration object which 17 | specifies additional details about the link. Currently, the `shortcut` object is 18 | used to specify the button text of the link associated with the trigger and has 19 | the following shape: 20 | 21 | ```ts 22 | shortcut?: { 23 | button_text: string; 24 | }; 25 | ``` 26 | 27 | ### Context data vailability 28 | 29 | The `data` context parameters available to a Shortcut trigger are as follows: 30 | 31 | | Parameter name | type | Description | 32 | | -------------------- | :----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | 33 | | `data.user_id` | string | A unique identifier for the Slack user who invoked the trigger | 34 | | `data.channel_id` | string | A unique identifier for the channel where the trigger was invoked | 35 | | `data.interactivity` | object | See [Block Kit interactivity](https://api.dev.slack.com/future/triggers/future/block-events) | 36 | | `data.location` | string | Where the trigger was invoked. Can be `message` or `bookmark` | 37 | | `data.message_ts` | string | A unique UNIX timestamp in seconds indicating when the trigger-invoking message was sent | 38 | | `data.user` | object | An object containing a `user_id` and a `secret` that can be used to identify and validate the specific user who invoked the trigger | 39 | | `data.action_id` | string | A unique identifier for the action that invoked the trigger. See [Block Kit interactivity](https://api.dev.slack.com/future/triggers/future/block-events) | 40 | | `data.block_id` | string | A unique identifier for the block where the trigger was invoked. See [Block Kit interactivity](https://api.dev.slack.com/future/triggers/future/block-events) | 41 | | `data.bookmark_id` | string | A unique identifier for the bookmark where the trigger was invoked | 42 | 43 | The data context can be used in the input parameter as follows: 44 | 45 | ```ts 46 | { 47 | inputs: { 48 | a_input_value: { 49 | value: 50 | "{{data.user_id}}"; 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | ## Usage 57 | 58 | Below are some usage examples of `ShortcutTrigger` objects which can be used in 59 | a .ts file to create a `shortcut` trigger through the 60 | [Slack CLI](./trigger-basics.md/#creating-triggers-using-the-slack-cli), 61 | alternatively this object could be passed into a 62 | `client.workflows.triggers.create` method to achieve the same effect at 63 | [runtime](./trigger-basics.md/#creating-triggers-in-the-runtime-environment). 64 | 65 | ### Example configured shortcut trigger 66 | 67 | ```ts 68 | const trigger: ShortcutTrigger = { 69 | type: "shortcut", 70 | name: "Request Time off", 71 | description: "Starts the workflow to request time off", 72 | workflow: "#/workflows/reverse_workflow", 73 | inputs: { 74 | interactivity: { 75 | value: "{{data.interactivity}}", 76 | }, 77 | }, 78 | shortcut: { 79 | button_text: "Click Me", 80 | }, 81 | }; 82 | 83 | export default trigger; 84 | ``` 85 | 86 | ### Example shortcut trigger without shortcut object 87 | 88 | ```ts 89 | const trigger: ShortcutTrigger = { 90 | type: "shortcut", 91 | name: "Request Time off", 92 | description: "Starts the workflow to request time off", 93 | workflow: "#/workflows/reverse_workflow", 94 | inputs: { 95 | interactivity: { 96 | value: "{{data.interactivity}}", 97 | }, 98 | }, 99 | }; 100 | 101 | export default trigger; 102 | ``` 103 | 104 | ### Example Response Payload from Create API 105 | 106 | ```ts 107 | { 108 | ok: true, //true on success, false on failure 109 | trigger: { //information related to the trigger 110 | id: "Ft014646C5ZF3", //The trigger id 111 | type: "shortcut", //The trigger type 112 | workflow: { // Information related to the workflow function 113 | id: "Fn0141SXKUHZ", 114 | workflow_id: "Wf0141SXKWRZ", 115 | callback_id: "example_workflow", 116 | title: "Example Workflow", 117 | description: "A sample workflow", 118 | type: "workflow", 119 | input_parameters: [], 120 | output_parameters: [], 121 | app_id: "A01412GH614", 122 | app: { 123 | id: "A01412GH614", 124 | name: "my-app", 125 | icons: [], 126 | is_workflow_app: false 127 | }, 128 | date_updated: 1658339916 129 | }, 130 | inputs: {}, 131 | outputs: {}, 132 | date_created: 1658339927, 133 | date_updated: 1658339927, 134 | name: "Example Trigger", //The name given to the trigger 135 | description: "An example trigger", //Trigger description 136 | shortcut_url: "https://app.slack.com/app/A01412GH614/shortcut/Ft014646C5ZF3" //The shortcut URL, paste into client to create unfurled link 137 | } 138 | } 139 | ``` 140 | -------------------------------------------------------------------------------- /src/typed-method-types/workflows/triggers/mod.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BaseMethodArgs, 3 | BaseResponse, 4 | CursorPaginationArgs, 5 | CursorPaginationResponse, 6 | } from "../../../types.ts"; 7 | import type { InputParameterSchema, WorkflowInputs } from "./inputs.ts"; 8 | import type { 9 | EventTrigger, 10 | EventTriggerResponse, 11 | EventTriggerResponseObject, 12 | } from "./event.ts"; 13 | import type { 14 | ScheduledTrigger, 15 | ScheduledTriggerResponse, 16 | ScheduledTriggerResponseObject, 17 | } from "./scheduled.ts"; 18 | import type { 19 | ShortcutTrigger, 20 | ShortcutTriggerResponse, 21 | ShortcutTriggerResponseObject, 22 | } from "./shortcut.ts"; 23 | import type { 24 | WebhookTrigger, 25 | WebhookTriggerResponse, 26 | WebhookTriggerResponseObject, 27 | } from "./webhook.ts"; 28 | import { ShortcutTriggerContextData } from "./shortcut-data.ts"; 29 | import { ScheduledTriggerContextData } from "./scheduled-data.ts"; 30 | import { EventTriggerContextData } from "./event-data/mod.ts"; 31 | 32 | /** 33 | * @enum {string} Enumerates valid trigger types. 34 | */ 35 | export const TriggerTypes = { 36 | /** 37 | * An event trigger type specifier, for invoking a workflow when a specific event happens in Slack. 38 | */ 39 | Event: "event", 40 | /** 41 | * A scheduled trigger type specifier, for invoking a workflow at a specific time interval. 42 | */ 43 | Scheduled: "scheduled", 44 | /** 45 | * A shortcut, or link, trigger type specifier, for invoking a workflow from a conversation in Slack. 46 | */ 47 | Shortcut: "shortcut", 48 | /** 49 | * A webhook trigger type specifier, for invoking a workflow when a specific URL receives an HTTP POST request. 50 | */ 51 | Webhook: "webhook", 52 | } as const; 53 | 54 | /** 55 | * Data available on different triggers for use in workflow inputs. 56 | */ 57 | export const TriggerContextData = { 58 | /** 59 | * Data available on scheduled triggers for use in workflow inputs. 60 | */ 61 | Scheduled: ScheduledTriggerContextData, 62 | /** 63 | * Data available on shortcut, or link, triggers for use in workflow inputs. 64 | */ 65 | Shortcut: ShortcutTriggerContextData, 66 | /** 67 | * Data available from the variety of different event triggers for use in workflow inputs. 68 | */ 69 | Event: EventTriggerContextData, 70 | } as const; 71 | 72 | // Set defaults for any direct uses of this type 73 | export type NO_GENERIC_TITLE = "#no-generic"; 74 | type DEFAULT_WORKFLOW_TYPE = { title: NO_GENERIC_TITLE }; 75 | 76 | type WorkflowStringFormat<AcceptedString extends string | undefined> = 77 | AcceptedString extends string ? `${string}#/workflows/${AcceptedString}` 78 | : `${string}#/workflows/${string}`; 79 | 80 | export type BaseTrigger<WorkflowDefinition extends WorkflowSchema> = { 81 | /** @description The type of trigger */ 82 | type: string; 83 | /** @description The workflow that the trigger initiates */ 84 | workflow: WorkflowStringFormat<WorkflowDefinition["callback_id"]>; 85 | /** @description The name of the trigger */ 86 | name: string; 87 | /** @description The description of the trigger */ 88 | description?: string; 89 | // deno-lint-ignore no-explicit-any 90 | [otherOptions: string]: any; 91 | } & WorkflowInputs<WorkflowDefinition>; 92 | 93 | export type WorkflowSchema = { 94 | callback_id?: string; 95 | description?: string; 96 | input_parameters?: InputParameterSchema; 97 | // deno-lint-ignore no-explicit-any 98 | output_parameters?: Record<string, any>; 99 | title: string; 100 | }; 101 | 102 | type ResponseTypes< 103 | WorkflowDefinition extends WorkflowSchema, 104 | > = 105 | & ShortcutTriggerResponse<WorkflowDefinition> 106 | & EventTriggerResponse<WorkflowDefinition> 107 | & ScheduledTriggerResponse<WorkflowDefinition> 108 | & WebhookTriggerResponse<WorkflowDefinition>; 109 | 110 | /** @description Response object content for delete method */ 111 | type DeleteResponse = { 112 | ok: true; 113 | }; 114 | 115 | type DeleteTriggerResponse = Promise< 116 | DeleteResponse | FailedTriggerResponse 117 | >; 118 | 119 | type ListArgs = { 120 | /** @description Lists triggers only if they owned by the caller */ 121 | is_owner?: boolean; 122 | /** @description Lists triggers only if they have been published */ 123 | is_published?: boolean; 124 | }; 125 | 126 | type ValidTriggerResponseObjects = 127 | | ShortcutTriggerResponseObject<WorkflowSchema> 128 | | EventTriggerResponseObject<WorkflowSchema> 129 | | ScheduledTriggerResponseObject<WorkflowSchema> 130 | | WebhookTriggerResponseObject<WorkflowSchema>; 131 | 132 | type ListResponse = CursorPaginationResponse & { 133 | ok: true; 134 | /** @description List of triggers in the workspace */ 135 | triggers: ValidTriggerResponseObjects[]; 136 | }; 137 | 138 | type ListTriggerResponse = Promise< 139 | ListResponse | FailedListTriggerResponse 140 | >; 141 | 142 | type FailedListTriggerResponse = BaseResponse & CursorPaginationResponse & { 143 | ok: false; 144 | /** @description no triggers are returned on a failed response */ 145 | triggers?: never; 146 | }; 147 | 148 | export type FailedTriggerResponse = BaseResponse & { 149 | ok: false; 150 | /** @description no trigger is returned on a failed response */ 151 | trigger?: never; 152 | }; 153 | 154 | type TriggerIdType = { 155 | /** @description The id of a specified trigger */ 156 | trigger_id: string; 157 | }; 158 | 159 | export type ValidTriggerTypes< 160 | WorkflowDefinition extends WorkflowSchema = DEFAULT_WORKFLOW_TYPE, 161 | > = 162 | | EventTrigger<WorkflowDefinition> 163 | | ScheduledTrigger<WorkflowDefinition> 164 | | ShortcutTrigger<WorkflowDefinition> 165 | | WebhookTrigger<WorkflowDefinition>; 166 | 167 | /** @description Function type for create method */ 168 | type CreateType = { 169 | <WorkflowDefinition extends WorkflowSchema = DEFAULT_WORKFLOW_TYPE>( 170 | args: BaseMethodArgs & ValidTriggerTypes<WorkflowDefinition>, 171 | ): ResponseTypes<WorkflowDefinition>; 172 | }; 173 | 174 | /** @description Function type for update method */ 175 | type UpdateType = { 176 | <WorkflowDefinition extends WorkflowSchema = DEFAULT_WORKFLOW_TYPE>( 177 | args: 178 | & BaseMethodArgs 179 | & ValidTriggerTypes<WorkflowDefinition> 180 | & TriggerIdType, 181 | ): ResponseTypes<WorkflowDefinition>; 182 | }; 183 | 184 | export type TypedWorkflowsTriggersMethodTypes = { 185 | /** @description Method to create a new trigger */ 186 | create: CreateType; 187 | /** @description Method to update an existing trigger identified with trigger_id */ 188 | update: UpdateType; 189 | /** @description Method to delete an existing trigger identified with trigger_id */ 190 | delete: ( 191 | args: BaseMethodArgs & TriggerIdType, 192 | ) => DeleteTriggerResponse; 193 | /** @description Method to list existing triggers in the workspace */ 194 | list: ( 195 | args?: BaseMethodArgs & CursorPaginationArgs & ListArgs, 196 | ) => ListTriggerResponse; 197 | }; 198 | -------------------------------------------------------------------------------- /.github/maintainers_guide.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This document describes tools, tasks and workflow that one needs to be familiar 4 | with in order to effectively maintain this project. If you use this package 5 | within your own software as is but don't plan on modifying it, this guide is 6 | **not** for you. 7 | 8 | ## Tools 9 | 10 | All you need to work on this project is a recent version of 11 | [Deno](https://deno.land/) 12 | 13 | ## Tasks 14 | 15 | ### Building 16 | 17 | - The majority of this library (`src/generated`) is generated code based off of 18 | `scripts/src/public-api-methods.ts`. 19 | - Run `./scripts/generate` to regenerate the API methods code. Unit tests 20 | verifying every API method has a corresponding function created for it are 21 | also generated in this step. 22 | 23 | Run the following from the root of the project: 24 | 25 | ```zsh 26 | ./scripts/generate 27 | ``` 28 | 29 | For more information on the code generation, have a look at `scripts/README.md`. 30 | 31 | ### Testing 32 | 33 | Test can be run directly with Deno: 34 | 35 | ```zsh 36 | deno task test 37 | ``` 38 | 39 | You can also run a test coverage report with: 40 | 41 | ```zsh 42 | deno task test:coverage 43 | ``` 44 | 45 | ### Lint and format 46 | 47 | The linting and formatting rules are defined in the `deno.jsonc` file, your IDE 48 | can be set up to follow these rules: 49 | 50 | 1. Refer to the 51 | [Deno Set Up Your Environment](https://deno.land/manual/getting_started/setup_your_environment) 52 | guidelines to set up your IDE with the proper plugin. 53 | 2. Ensure that the `deno.jsonc` file is set as the configuration file for your 54 | IDE plugin 55 | - If you are using VS code 56 | [this](https://deno.land/manual/references/vscode_deno#using-a-configuration-file) 57 | is already configured in `.vscode/settings.json` 58 | 59 | #### Linting 60 | 61 | The list of linting rules can be found in 62 | [the linting deno docs](https://lint.deno.land/). Currently we apply all 63 | recommended rules. 64 | 65 | #### Format 66 | 67 | The list of format options is defined in the `deno.jsonc` file. They closely 68 | resemble the default values. 69 | 70 | ### Releasing 71 | 72 | Releases for this library are automatically generated off of git tags. Before 73 | creating a new release, ensure that everything on the `main` branch since the 74 | last tag is in a releasable state! At a minimum, [run the tests](#testing). 75 | 76 | To create a new release: 77 | 78 | 1. Create a new GitHub Release from the 79 | [Releases page](https://github.com/slackapi/deno-slack-api/releases) by 80 | clicking the "Draft a new release" button. 81 | 2. Input a new version manually into the "Choose a tag" input. You can start off 82 | by incrementing the version to reflect a patch. (i.e. 1.16.0 -> 1.16.1) 83 | - After you input the new version, click the "Create a new tag: x.x.x on 84 | publish" button. This won't create your tag immediately. 85 | - Auto-generate the release notes by clicking the "Auto-generate release 86 | notes" button. This will pull in changes that will be included in your 87 | release. 88 | - Edit the resulting notes to ensure they have decent messaging that are 89 | understandable by non-contributors, but each commit should still have 90 | it's own line. 91 | - Flip to the preview mode and review the pull request labels of the changes 92 | included in this release (i.e. `semver:minor` `semver:patch`, 93 | `semver:major`). Tip: Your release version should be based on the tag of 94 | the largest change, so if this release includes a `semver:minor`, the 95 | release version in your tag should be upgraded to reflect a minor. 96 | - Ensure that this version adheres to [semantic versioning][semver]. See 97 | [Versioning](#versioning-and-tags) for correct version format. Version tags 98 | should match the following pattern: `1.0.1` (no `v` preceding the number). 99 | 3. Set the "Target" input to the "main" branch. 100 | 4. Name the release title after the version tag. 101 | 5. Make any adjustments to generated release notes to make sure they are 102 | accessible and approachable and that an end-user with little context about 103 | this project could still understand. 104 | 6. Make sure "This is a pre-release" is _not_ checked. 105 | 7. Publish the release by clicking the "Publish release" button! 106 | 8. After a few minutes, the corresponding version will be available on 107 | <https://deno.land/x/deno_slack_api>. 108 | 9. Don't forget to also bump this library's version in the deno-slack-sdk's 109 | `deps.ts` file! 110 | 111 | ## Workflow 112 | 113 | ### Versioning and Tags 114 | 115 | This project is versioned using [Semantic Versioning][semver]. 116 | 117 | ### Branches 118 | 119 | > Describe any specific branching workflow. For example: `main` is where active 120 | > development occurs. Long running branches named feature branches are 121 | > occasionally created for collaboration on a feature that has a large scope 122 | > (because everyone cannot push commits to another person's open Pull Request) 123 | 124 | ### Issue Management 125 | 126 | Labels are used to run issues through an organized workflow. Here are the basic 127 | definitions: 128 | 129 | - `bug`: A confirmed bug report. A bug is considered confirmed when reproduction 130 | steps have been documented and the issue has been reproduced. 131 | - `enhancement`: A feature request for something this package might not already 132 | do. 133 | - `docs`: An issue that is purely about documentation work. 134 | - `tests`: An issue that is purely about testing work. 135 | - `needs feedback`: An issue that may have claimed to be a bug but was not 136 | reproducible, or was otherwise missing some information. 137 | - `discussion`: An issue that is purely meant to hold a discussion. Typically 138 | the maintainers are looking for feedback in this issues. 139 | - `question`: An issue that is like a support request because the user's usage 140 | was not correct. 141 | - `semver:major|minor|patch`: Metadata about how resolving this issue would 142 | affect the version number. 143 | - `security`: An issue that has special consideration for security reasons. 144 | - `good first contribution`: An issue that has a well-defined relatively-small 145 | scope, with clear expectations. It helps when the testing approach is also 146 | known. 147 | - `duplicate`: An issue that is functionally the same as another issue. Apply 148 | this only if you've linked the other issue by number. 149 | 150 | **Triage** is the process of taking new issues that aren't yet "seen" and 151 | marking them with a basic level of information with labels. An issue should have 152 | **one** of the following labels applied: `bug`, `enhancement`, `question`, 153 | `needs feedback`, `docs`, `tests`, or `discussion`. 154 | 155 | Issues are closed when a resolution has been reached. If for any reason a closed 156 | issue seems relevant once again, reopening is great and better than creating a 157 | duplicate issue. 158 | 159 | ## Everything else 160 | 161 | When in doubt, find the other maintainers and ask. 162 | 163 | [semver]: http://semver.org/ 164 | -------------------------------------------------------------------------------- /scripts/src/public-api-methods.ts: -------------------------------------------------------------------------------- 1 | import { methodsWithCustomTypes } from "../../src/typed-method-types/mod.ts"; 2 | 3 | // https://api.slack.com/methods 4 | // These are all of the public Slack API methods 5 | export const getPublicAPIMethods = () => { 6 | const publicAPIMethods = [ 7 | "admin.analytics.getFile", 8 | "admin.apps.approve", 9 | "admin.apps.clearResolution", 10 | "admin.apps.restrict", 11 | "admin.apps.uninstall", 12 | "admin.apps.approved.list", 13 | "admin.apps.requests.cancel", 14 | "admin.apps.requests.list", 15 | "admin.apps.restricted.list", 16 | "admin.auth.policy.assignEntities", 17 | "admin.auth.policy.getEntities", 18 | "admin.auth.policy.removeEntities", 19 | "admin.barriers.create", 20 | "admin.barriers.delete", 21 | "admin.barriers.list", 22 | "admin.barriers.update", 23 | "admin.conversations.archive", 24 | "admin.conversations.convertToPrivate", 25 | "admin.conversations.create", 26 | "admin.conversations.delete", 27 | "admin.conversations.disconnectShared", 28 | "admin.conversations.getConversationPrefs", 29 | "admin.conversations.getCustomRetention", 30 | "admin.conversations.getTeams", 31 | "admin.conversations.invite", 32 | "admin.conversations.removeCustomRetention", 33 | "admin.conversations.rename", 34 | "admin.conversations.search", 35 | "admin.conversations.setConversationPrefs", 36 | "admin.conversations.setCustomRetention", 37 | "admin.conversations.setTeams", 38 | "admin.conversations.unarchive", 39 | "admin.conversations.ekm.listOriginalConnectedChannelInfo", 40 | "admin.conversations.restrictAccess.addGroup", 41 | "admin.conversations.restrictAccess.listGroups", 42 | "admin.conversations.restrictAccess.removeGroup", 43 | "admin.emoji.add", 44 | "admin.emoji.addAlias", 45 | "admin.emoji.list", 46 | "admin.emoji.remove", 47 | "admin.emoji.rename", 48 | "admin.inviteRequests.approve", 49 | "admin.inviteRequests.deny", 50 | "admin.inviteRequests.list", 51 | "admin.inviteRequests.approved.list", 52 | "admin.inviteRequests.denied.list", 53 | "admin.teams.admins.list", 54 | "admin.teams.create", 55 | "admin.teams.list", 56 | "admin.teams.owners.list", 57 | "admin.teams.settings.info", 58 | "admin.teams.settings.setDefaultChannels", 59 | "admin.teams.settings.setDescription", 60 | "admin.teams.settings.setDiscoverability", 61 | "admin.teams.settings.setIcon", 62 | "admin.teams.settings.setName", 63 | "admin.usergroups.addChannels", 64 | "admin.usergroups.addTeams", 65 | "admin.usergroups.listChannels", 66 | "admin.usergroups.removeChannels", 67 | "admin.users.assign", 68 | "admin.users.invite", 69 | "admin.users.list", 70 | "admin.users.remove", 71 | "admin.users.setAdmin", 72 | "admin.users.setExpiration", 73 | "admin.users.setOwner", 74 | "admin.users.setRegular", 75 | "admin.users.session.clearSettings", 76 | "admin.users.session.getSettings", 77 | "admin.users.session.invalidate", 78 | "admin.users.session.list", 79 | "admin.users.session.reset", 80 | "admin.users.session.resetBulk", 81 | "admin.users.session.setSettings", 82 | "admin.users.unsupportedVersions.export", 83 | "api.test", 84 | "apps.connections.open", 85 | "apps.event.authorizations.list", 86 | "apps.manifest.create", 87 | "apps.manifest.delete", 88 | "apps.manifest.export", 89 | "apps.manifest.update", 90 | "apps.manifest.validate", 91 | "apps.uninstall", 92 | "auth.revoke", 93 | "auth.test", 94 | "auth.teams.list", 95 | "bookmarks.add", 96 | "bookmarks.edit", 97 | "bookmarks.list", 98 | "bookmarks.remove", 99 | "bots.info", 100 | "calls.add", 101 | "calls.end", 102 | "calls.info", 103 | "calls.update", 104 | "calls.participants.add", 105 | "calls.participants.remove", 106 | "canvases.access.delete", 107 | "canvases.access.set", 108 | "canvases.create", 109 | "canvases.delete", 110 | "canvases.edit", 111 | "canvases.sections.lookup", 112 | "chat.delete", 113 | "chat.deleteScheduledMessage", 114 | "chat.getPermalink", 115 | "chat.meMessage", 116 | "chat.postEphemeral", 117 | "chat.postMessage", 118 | "chat.scheduleMessage", 119 | "chat.unfurl", 120 | "chat.update", 121 | "chat.scheduledMessages.list", 122 | "conversations.acceptSharedInvite", 123 | "conversations.approveSharedInvite", 124 | "conversations.archive", 125 | "conversations.canvases.create", 126 | "conversations.close", 127 | "conversations.create", 128 | "conversations.declineSharedInvite", 129 | "conversations.externalInvitePermissions.set", 130 | "conversations.history", 131 | "conversations.info", 132 | "conversations.invite", 133 | "conversations.inviteShared", 134 | "conversations.join", 135 | "conversations.kick", 136 | "conversations.leave", 137 | "conversations.list", 138 | "conversations.listConnectInvites", 139 | "conversations.mark", 140 | "conversations.members", 141 | "conversations.open", 142 | "conversations.rename", 143 | "conversations.replies", 144 | "conversations.setPurpose", 145 | "conversations.setTopic", 146 | "conversations.unarchive", 147 | "dialog.open", 148 | "dnd.endDnd", 149 | "dnd.endSnooze", 150 | "dnd.info", 151 | "dnd.setSnooze", 152 | "dnd.teamInfo", 153 | "emoji.list", 154 | "enterprise.auth.idpconfig.apply", 155 | "enterprise.auth.idpconfig.get", 156 | "enterprise.auth.idpconfig.list", 157 | "enterprise.auth.idpconfig.remove", 158 | "enterprise.auth.idpconfig.set", 159 | "files.comments.delete", 160 | "files.delete", 161 | "files.info", 162 | "files.list", 163 | "files.revokePublicURL", 164 | "files.sharedPublicURL", 165 | "files.upload", 166 | "files.remote.add", 167 | "files.remote.info", 168 | "files.remote.list", 169 | "files.remote.remove", 170 | "files.remote.share", 171 | "files.remote.update", 172 | "functions.completeError", 173 | "functions.completeSuccess", 174 | "migration.exchange", 175 | "oauth.access", 176 | "oauth.v2.access", 177 | "oauth.v2.exchange", 178 | "openid.connect.token", 179 | "openid.connect.userInfo", 180 | "pins.add", 181 | "pins.list", 182 | "pins.remove", 183 | "reactions.add", 184 | "reactions.get", 185 | "reactions.list", 186 | "reactions.remove", 187 | "reminders.add", 188 | "reminders.complete", 189 | "reminders.delete", 190 | "reminders.info", 191 | "reminders.list", 192 | "rtm.connect", 193 | "rtm.start", 194 | "search.all", 195 | "search.files", 196 | "search.messages", 197 | "stars.add", 198 | "stars.list", 199 | "stars.remove", 200 | "team.accessLogs", 201 | "team.billableInfo", 202 | "team.externalTeams.disconnect", 203 | "team.externalTeams.list", 204 | "team.info", 205 | "team.integrationLogs", 206 | "team.billing.info", 207 | "team.preferences.list", 208 | "team.profile.get", 209 | "tooling.tokens.rotate", 210 | "usergroups.create", 211 | "usergroups.disable", 212 | "usergroups.enable", 213 | "usergroups.list", 214 | "usergroups.update", 215 | "usergroups.users.list", 216 | "usergroups.users.update", 217 | "users.conversations", 218 | "users.deletePhoto", 219 | "users.discoverableContacts.lookup", 220 | "users.getPresence", 221 | "users.identity", 222 | "users.info", 223 | "users.list", 224 | "users.lookupByEmail", 225 | "users.setActive", 226 | "users.setPhoto", 227 | "users.setPresence", 228 | "users.profile.get", 229 | "users.profile.set", 230 | "views.open", 231 | "views.publish", 232 | "views.push", 233 | "views.update", 234 | "workflows.stepCompleted", 235 | "workflows.stepFailed", 236 | "workflows.updateStep", 237 | ]; 238 | 239 | const methodsSet = new Set([ 240 | ...publicAPIMethods, 241 | ]); 242 | 243 | methodsWithCustomTypes.forEach((customMethod) => { 244 | methodsSet.delete(customMethod); 245 | }); 246 | 247 | const methods = Array.from(methodsSet); 248 | 249 | methods.sort((a, b) => a.localeCompare(b)); 250 | 251 | return methods; 252 | }; 253 | --------------------------------------------------------------------------------