Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: types v3 message attachment breaking changes #1911

Draft
wants to merge 2 commits into
base: types-v3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 117 additions & 68 deletions packages/types/src/message-attachments.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,46 @@
import { AnyBlock } from './block-kit/blocks';
import { PlainTextElement } from './block-kit/composition-objects';

// TODO: breaking changes, use discriminated union for `fallback`, `text` and `block` properties, maybe LegacyAttachment
// vs. BlocksAttachment? as per https://api.slack.com/reference/messaging/attachments#legacy_fields
// "these fields are optional if you're including blocks as above. If you aren't, one of fallback or text are required"
// also further nested discriminated union types that could be helpful here:
// - LegacyAttachmentWithAuthor: if author_name is present, then author_icon and author_link are optional fields
// - LegacyAttachmentWithFooter: if footer is present, then footer_icon is an optional field
// - image_url and thumb_url cannot be used together
/**
* Add {@link https://api.slack.com/messaging/composing/layouts#attachments secondary attachments} to your messages in Slack.
* Message attachments are considered a legacy part of messaging functionality. They are not deprecated per se, but they may change in the future, in ways that reduce their visibility or utility. We recommend moving to Block Kit instead. Read more about {@link https://api.slack.com/messaging/composing/layouts#when-to-use-attachments when to use message attachments}.
* @see {@link https://api.slack.com/reference/messaging/attachments Secondary message attachments reference documentation}
*/
export interface MessageAttachment {
export type MessageAttachment = (LegacyAttachment | BlocksAttachment) & LegacyAttachmentOptionalFields;

// Either `text` or `fallback` is required.
type LegacyAttachment = LegacyAttachmentWithText | LegacyAttachmentWithFallback;
interface LegacyAttachmentWithText {
/**
* @description An array of {@link KnownBlock layout blocks} in the same format
* {@link https://api.slack.com/block-kit/building as described in the building blocks guide}.
* @description The main body text of the attachment. It can be formatted as plain text, or with
* {@link https://api.slack.com/reference/surfaces/formatting#basics `mrkdwn`} by including it in the `mrkdwn_in` field.
* The content will automatically collapse if it contains 700+ characters or 5+ line breaks, and will display
* a "Show more..." link to expand the content.
*/
blocks?: AnyBlock[];
text: string;
}
interface LegacyAttachmentWithFallback {
/**
* @description A plain text summary of the attachment used in clients that
* don't show formatted text (e.g. mobile notifications).
*/
fallback?: string; // either this or text must be defined
/**
* @description Changes the color of the border on the left side of this attachment from the default gray. Can either
* be one of `good` (green), `warning` (yellow), `danger` (red), or any hex color code (eg. `#439FE0`)
*/
color?: 'good' | 'warning' | 'danger' | string;
fallback: string;
}
interface BlocksAttachment {
/**
* @description Text that appears above the message attachment block. It can be formatted as plain text,
* or with {@link https://api.slack.com/reference/surfaces/formatting#basics `mrkdwn`} by including it in the `mrkdwn_in` field.
* @description An array of {@link KnownBlock layout blocks} in the same format
* {@link https://api.slack.com/block-kit/building as described in the building blocks guide}.
*/
pretext?: string;
blocks: AnyBlock[];
}

// author_link and author_icon require author_name
type AttachmentAuthor = AuthorDetails | RequireAuthorName;
interface AuthorDetails {
/**
* @description Small text used to display the author's name.
*/
author_name?: string;
author_name: string;
/**
* @description A valid URL that will hyperlink the `author_name` text. Will only work if `author_name` is present.
*/
Expand All @@ -47,35 +50,46 @@ export interface MessageAttachment {
* Will only work if `author_name` is present.
*/
author_icon?: string; // author_name must be present
author_subname?: string; // TODO: not documented in https://api.slack.com/reference/messaging/attachments
/**
* @description Large title text near the top of the attachment.
*/
title?: string;
/**
* @description A valid URL that turns the `title` text into a hyperlink.
*/
title_link?: string; // title must be present
}
interface RequireAuthorName {
author_name?: undefined;
author_link?: never;
author_icon?: never;
}

// footer_icon requires footer
type Footer = FooterDetails | RequireFooter;
interface FooterDetails {
/**
* @description The main body text of the attachment. It can be formatted as plain text, or with
* {@link https://api.slack.com/reference/surfaces/formatting#basics `mrkdwn`} by including it in the `mrkdwn_in` field.
* The content will automatically collapse if it contains 700+ characters or 5+ line breaks, and will display
* a "Show more..." link to expand the content.
* @description Some brief text to help contextualize and identify an attachment. Limited to 300 characters,
* and may be truncated further when displayed to users in environments with limited screen real estate.
*/
text?: string; // either this or fallback must be defined
footer: string;
/**
* @description An array of {@link MessageAttachmentField} that get displayed in a table-like way
* (see {@link https://api.slack.com/reference/messaging/attachments#example this example}).
* For best results, include no more than 2-3 field objects.
* @description A valid URL to an image file that will be displayed beside the `footer` text.
* Will only work if `footer` is present. We'll render what you provide at 16px by 16px.
* It's best to use an image that is similarly sized.
*/
fields?: MessageAttachmentField[];
footer_icon?: string;
}
interface RequireFooter {
footer?: undefined;
footer_icon?: never;
}

// image_url and thumb_url cannot be used together
type Image = ImageUrl | ThumbUrl;
interface ImageUrl {
/**
* @description A valid URL to an image file that will be displayed at the bottom of the attachment.
* We support GIF, JPEG, PNG, and BMP formats.
* Large images will be resized to a maximum width of 360px or a maximum height of 500px, while still
* maintaining the original aspect ratio. Cannot be used with `thumb_url`.
*/
image_url?: string;
thumb_url?: never;
}
interface ThumbUrl {
/**
* @description A valid URL to an image file that will be displayed as a thumbnail on the right side of
* a message attachment. We currently support the following formats: GIF, JPEG, PNG, and BMP.
Expand All @@ -84,38 +98,54 @@ export interface MessageAttachment {
* For best results, please use images that are already 75px by 75px.
*/
thumb_url?: string;
image_url?: never;
}

type LegacyAttachmentOptionalFields = Partial<LegacyAttachmentWithText> & Partial<LegacyAttachmentWithFallback> &
Partial<BlocksAttachment> & AttachmentAuthor & Footer & Image & AttachmentActions & {
app_id?: string; // may be present on payloads _sent_ by Slack
bot_id?: string; // may be present on payloads _sent_ by Slack
app_unfurl_url?: string; // may be present on payloads _sent_ by Slack
/**
* @description Some brief text to help contextualize and identify an attachment. Limited to 300 characters,
* and may be truncated further when displayed to users in environments with limited screen real estate.
* @description Changes the color of the border on the left side of this attachment from the default gray. Can either
* be one of `good` (green), `warning` (yellow), `danger` (red), or any hex color code (eg. `#439FE0`)
*/
footer?: string;
color?: 'good' | 'warning' | 'danger' | string;
/**
* @description A valid URL to an image file that will be displayed beside the `footer` text.
* Will only work if `footer` is present. We'll render what you provide at 16px by 16px.
* It's best to use an image that is similarly sized.
* @description An array of {@link MessageAttachmentField} that get displayed in a table-like way
* (see {@link https://api.slack.com/reference/messaging/attachments#example this example}).
* For best results, include no more than 2-3 field objects.
*/
footer_icon?: string;
fields?: MessageAttachmentField[];
is_app_unfurl?: boolean; // TODO: not documented in https://api.slack.com/reference/messaging/attachments
/**
* @description Field names that should be {@link https://api.slack.com/reference/surfaces/formatting#basics formatted by `mrkdwn` syntax}.
* The fields that can be formatted in this way include the names of the `fields` property, or
* the `text` or `pretext` properties.
*/
mrkdwn_in?: ('pretext' | 'text' | 'fields')[]; // TODO: I think `fields` here is wrong? instead they should reference field names from `fields`
/**
* @description Text that appears above the message attachment block. It can be formatted as plain text,
* or with {@link https://api.slack.com/reference/surfaces/formatting#basics `mrkdwn`} by including it in the `mrkdwn_in` field.
*/
pretext?: string;
preview?: MessageAttachmentPreview; // https://api.slack.com/methods/chat.unfurl#markdown
/**
* @description Large title text near the top of the attachment.
*/
title?: string;
/**
* @description A valid URL that turns the `title` text into a hyperlink.
*/
title_link?: string; // title must be present
/**
* @description A Unix timestamp that is used to relate your attachment to a specific time.
* The attachment will display the additional timestamp value as part of the attachment's footer.
* Your message's timestamp will be displayed in varying ways, depending on how far in the past or future it is,
* relative to the present. Form factors, like mobile versus desktop may also transform its rendered appearance.
*/
ts?: string;
actions?: AttachmentAction[]; // TODO: https://api.slack.com/legacy/message-buttons#crafting_your_message
callback_id?: string; // TODO: https://api.slack.com/legacy/message-buttons#crafting_your_message
/**
* @description Field names that should be {@link https://api.slack.com/reference/surfaces/formatting#basics formatted by `mrkdwn` syntax}.
* The fields that can be formatted in this way include the names of the `fields` property, or
* the `text` or `pretext` properties.
*/
mrkdwn_in?: ('pretext' | 'text' | 'fields')[]; // TODO: I think `fields` here is wrong? instead they should reference field names from `fields`
app_unfurl_url?: string; // TODO: not documented in https://api.slack.com/reference/messaging/attachments
is_app_unfurl?: boolean; // TODO: not documented in https://api.slack.com/reference/messaging/attachments
app_id?: string; // TODO: not documented in https://api.slack.com/reference/messaging/attachments
bot_id?: string; // TODO: not documented in https://api.slack.com/reference/messaging/attachments
preview?: MessageAttachmentPreview; // https://api.slack.com/methods/chat.unfurl#markdown TODO: not documented in https://api.slack.com/reference/messaging/attachments, also unclear why this links to chat.unfurl?
}
};

/**
* @description A field object to include in a {@link MessageAttachment}.
Expand Down Expand Up @@ -147,23 +177,42 @@ interface MessageAttachmentPreview {
iconUrl?: string;
}

interface AttachmentAction {
// Either specify both callback_id and actions, or neither.
type AttachmentActions = {
/**
* @deprecated Legacy attachments with buttons are deprecated. See {@link https://api.slack.com/messaging/attachments-to-blocks transitioning to blocks}.
* @see {@link https://api.slack.com/legacy/message-buttons#crafting_your_message Legacy "Crafting messages with buttons documentation}
*/
actions: [AttachmentAction, ...AttachmentAction[]];
/**
* @deprecated Legacy attachments with buttons are deprecated. See {@link https://api.slack.com/messaging/attachments-to-blocks transitioning to blocks}.
* @see {@link https://api.slack.com/legacy/message-buttons#crafting_your_message Legacy "Crafting messages with buttons documentation}
*/
callback_id: string;
} | { callback_id?: never; actions?: never; };
type AttachmentAction = AttachmentButtonAction | AttachmentMenuAction;
interface AttachmentBaseAction {
id?: string;
name: string;
text: string;
}
interface AttachmentButtonAction extends AttachmentBaseAction {
type: 'button';
confirm?: Confirmation;
data_source?: 'static' | 'channels' | 'conversations' | 'users' | 'external';
min_query_length?: number;
name?: string;
style?: 'default' | 'primary' | 'danger';
value?: string;
url?: string;
}
interface AttachmentMenuAction extends AttachmentBaseAction {
type: 'select';
data_source?: 'static' | 'channels' | 'conversations' | 'users' | 'external';
options?: OptionField[];
option_groups?: {
text: string
options: OptionField[];
}[];
selected_options?: OptionField[];
style?: 'default' | 'primary' | 'danger';
text: string;
type: 'button' | 'select';
value?: string;
url?: string;
}

interface OptionField {
Expand Down
19 changes: 19 additions & 0 deletions packages/types/test/message-attachments.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expectAssignable, expectError } from 'tsd';
import { MessageAttachment } from '../src/index';

// -- sad path
expectError<MessageAttachment>({}); // if no blocks, either text or fallback is required.
expectError<MessageAttachment>({ fallback: 'hi', author_link: 'https://slack.com' }); // use of author_link requires author_name.
expectError<MessageAttachment>({ fallback: 'hi', author_icon: 'https://slack.com' }); // use of author_icon requires author_name.
expectError<MessageAttachment>({ fallback: 'hi', footer_icon: 'https://slack.com' }); // use of footer_icon requires footer.
expectError<MessageAttachment>({ fallback: 'hi', thumb_url: 'https://slack.com', image_url: 'https://slack.com' }); // cant use both image_url and thumb_url.
expectError<MessageAttachment>({ fallback: 'hi', callback_id: 'hollah' }); // cant use callback_id without actions.
expectError<MessageAttachment>({ fallback: 'hi', actions: [{ name: 'sup', text: 'sup', type: 'button'}] }); // cant use callback_id without actions.
expectError<MessageAttachment>({ fallback: 'hi', callback_id: 'hi', actions: [] }); // must specify at least one action.

// -- happy path
expectAssignable<MessageAttachment>({ text: 'hi' }); // if no blocks, either text or fallback is required.
expectAssignable<MessageAttachment>({ fallback: 'hi' }); // if no blocks, either text or fallback is required.
expectAssignable<MessageAttachment>({ fallback: 'hi', author_name: 'filmaj', author_icon: 'https://slack.com' }); // use of author_icon requires author_name.
expectAssignable<MessageAttachment>({ fallback: 'hi', author_name: 'filmaj', author_link: 'https://slack.com' }); // use of author_link requires author_name.
expectAssignable<MessageAttachment>({ fallback: 'hi', footer: 'filmaj', footer_icon: 'https://slack.com' }); // use of footer_icon requires footer.