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

Improve types for message events and all subtypes #709

Merged
merged 12 commits into from
Jan 4, 2021

Conversation

aoberoi
Copy link
Contributor

@aoberoi aoberoi commented Dec 14, 2020

Summary

Previously, the SlackEvent union included a type called MessageEvent which represents all Events API payloads with type:'message'. However, the union didn't include the message event subtypes (such as subtype='bot_message').

With this change, MessageEvent is itself a union of all the subtypes. That union is still part of the overall SlackEvent union. This means that both the app.event() and app.message() methods are now aware of message subtypes.

Fixes #311

Another related change is also in this PR: the index type has been removed from all events in the SlackEvent union. This means you can no longer safely read any property name from any event and treat the value as an any.

NOTE: This will cause new type checking errors where there wasn't one before in some apps. Those errors will be legitimate (unsafe accesses to event properties), but these apps may have been working fine until now and builds could be broken. This is actually a feature of using TypeScript, this isn't considered a breaking change.

If you're reading this because your build broke and you're looking for a quick fix, it may be helpful to know two things:

  1. If you know that the property you're trying to access is available on the event, here is how you should approach it. First, maybe the types in @slack/bolt are out of date or wrong - consider creating an issue in the repo. Second, you can use a cast to say "I know better than Bolt's types" and immediately unblock yourself.

  2. The type that was previously called MessageEvent is now called GenericMessageEvent. This new type is meant to be used to represent Events API messages of type 'message', but which do not have a 'subtype'. You can likely get your build working again by intentionally casting the message argument inside app.message() listeners to the GenericMessageEvent type. But before you do this, consider taking any errors seriously as you may actually have an unsafe access to a property. You can use a conditional check to narrow the type of message and safely use properties like below:

app.message(async ({ message }) => {
  // message is type MessageEvent here
  if (message.subtype === 'message_changed') {
    // message is type MessageChangedEvent here
    message.message // this is now a safe access and TypeScript won't error
  }
  // accessing message.message outside of the previous conditional is unsafe and TypeScript will error
});

Requirements (place an x in each [ ])

Moved all the subtypes into a new file to keep them more organized.
Wrote a test file that should theoretically pass when this issue is fixed.

Does not yet pass TS compiler, more changes to come.
Once we removed StringIndexed from SlackEvent types, many property lookups
became typechecking errors.

I borrowed this refactor mostly from a WIP PR to improve our style/linting
in a proactive effort to redue conflicts when that PR lands. The only
changes I needed to add were the Org Apps changes that had landed on main.

slackapi#669

In a few cases, the event types had other issues that became apparent only
after removing the aggressive casting we had before. Those are fixed.

It's now evident that there's more type related problems with respect to the Authorize, its related types, especially the OrgApps variants. I'll try to fix these next.

It's also not clear if buildSource needs the orgInstall argument. Will seek advice in code review for that.
By making these two types generic, we can resolve the typing issue in buildSource and a couple other places.

This code still doesn't compile. May need to update singleTeamAuthorization. Getting close.
In both App.ts and builtin.ts, issues were resolve simply by
refactoring to an equivalent form that is more type-safe. In
App.ts specifically, processEvent() was refactored to reduce
some code repetition as well.

In helpers.ts this actually fixes bugs.
The getTypeAndConversation() would have failed conversation
lookup for the following event types: pin_added, pin_removed,
call_rejected, channel_created, channel_rename, group_rename,
im_created.
@gitwave gitwave bot added the untriaged label Dec 14, 2020
@codecov
Copy link

codecov bot commented Dec 14, 2020

Codecov Report

Merging #709 (aaf9fbd) into main (8c7434a) will increase coverage by 0.53%.
The diff coverage is 68.57%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #709      +/-   ##
==========================================
+ Coverage   82.32%   82.85%   +0.53%     
==========================================
  Files           8        8              
  Lines         758      770      +12     
  Branches      250      230      -20     
==========================================
+ Hits          624      638      +14     
- Misses         78       87       +9     
+ Partials       56       45      -11     
Impacted Files Coverage Δ
src/helpers.ts 80.95% <58.33%> (-9.96%) ⬇️
src/App.ts 84.51% <68.88%> (+2.43%) ⬆️
src/middleware/builtin.ts 82.78% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8c7434a...aaf9fbd. Read the comment docs.

conversationId: channelId,
isEnterpriseInstall: false,
};

Copy link
Contributor Author

@aoberoi aoberoi Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refactor is mostly borrowed from #669, so that when it eventually lands, there's less chance of a merge conflict.

I found all the Org Apps related changes on main and layered them into this part of code from the linting PR.

Prettier undid some of my style related changes, but these will likely be automatically fixed again once the linting PR lands.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a big fan of this refactor. Much easier to read and follow

type: Type;
}

/* ------- TODO: Generate these interfaces ------- */

// TODO: why are these all StringIndexed? who does that really help when going more than one level deep means you have
Copy link
Contributor Author

@aoberoi aoberoi Dec 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once I removed StringIndexed as a base from all the event interfaces, many unsafe property accesses in our own codebase became type-checking errors. Most of refactoring in the rest of this PR is simply an attempt to address those errors.

userId?: string;
conversationId?: string;
isEnterpriseInstall?: boolean;
isEnterpriseInstall: IsEnterpriseInstall;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely the most meaningful refactor. I've done something similar here to what I did in node-slack-sdk - consolidated the types that were very similar but just varied on IsEnterpriseInstall by making the types generic.

This allowed me to safely reduce code repetition within App.processEvent() and singleTeamAuthorization() (now singleAuthorization()).

@@ -979,10 +943,10 @@ function defaultErrorHandler(logger: Logger): ErrorHandler {
};
}

function singleTeamAuthorization(
function singleAuthorization(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rename is because this function addresses authorization on a single team OR a single enterprise. I considered having two separate functions for those purposes, but realized the body would look mostly identical except for the value of isEnterpriseInstall in the return value. That could easily be consolidated because isEnterpriseInstall is actually passed to the Authorize function as a property of source anyway.

const { event } = body as SlackEventMiddlewareArgs<string>['body'];

// Find conversationId
const conversationId: string | undefined = (() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code actually got a bit longer because it fixes some actual bugs (not just a straight refactor). Previously, events of types pin_added, pin_removed, call_rejected, channel_created, channel_rename, group_rename, and im_created would not have a conversationId in the return value because the conditional checks were incomplete.


/** Org Authorize */
private orgAuthorize!: Authorize;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was technically wrong previously. The Authorize type describes a function which non-optionally took a teamId.

};
}

function isBodyWithTypeEnterpriseInstall(body: AnyMiddlewareArgs['body'], type: IncomingEventType): boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New helper function so the calling code is more readable.

@seratch
Copy link
Member

seratch commented Dec 15, 2020

Regarding the removal of StringIndexed and the better (magical) way to determine sub types in message events look great to me!

It seems this pull request consists of the following changes. And, if I understand correctly, 3 & 4 changes do not rely on 1 & 2.

  • 1 refactoring authorize in accordance with org-wide app support
  • 2 bugfix on conversation id extraction in getTypeAndConversation
  • 3 StringIndexed removal for all events
  • 4 Improvements on message event types

I haven't checked the details for authorize changes and a bug fix in getTypeAndConversation yet. I'm fine to have 3 & 4 in this PR but I think we can have separate PRs for 1 & 2. I personally think 1 & 2 are already great but having only 3 & 4 in this PR makes reviews much easier. We still have other tasks for the next major version. So, I think trying to minimize each pull request and quickly merge should help us smoothly deliver more changes.

Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my approval for the event payload related parts

| MessageRepliedEvent
| ThreadBroadcastMessageEvent;

export interface GenericMessageEvent {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type definition seems to be missing some fields such as team and channel_type. You can check the Java SDK's type definition to learn them. https://github.com/slackapi/java-slack-sdk/tree/v1.4.0/slack-api-model/src/main/java/com/slack/api/model/event

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! Should I create another issue for this so it can be addressed in a separate PR? This PR was meant to solve the structural issue with the union types, but the issue you've pointed out is independent of that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create a new issue for it!

@stevengill stevengill added enhancement M-T: A feature request for new functionality semver:minor and removed untriaged labels Dec 16, 2020
conversationId: channelId,
isEnterpriseInstall: false,
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a big fan of this refactor. Much easier to read and follow

src/App.ts Show resolved Hide resolved
src/App.ts Outdated Show resolved Hide resolved

// When the app is installed using org-wide deployment, team property will be null
if (bodyAsActionOrOptionsOrViewActionOrShortcut.team !== null) {
return bodyAsActionOrOptionsOrViewActionOrShortcut.team.enterprise_id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this check should be moved below line 850 check. Why would body.team.enterprise_id take priority over body.enterprise.id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

@@ -994,8 +958,8 @@ function singleTeamAuthorization(
};
});

return async () => {
return { botToken: authorization.botToken, ...(await identifiers) };
return async ({ isEnterpriseInstall }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably just a brain fart, but where is this isEnterpriseInstall coming from here?

Copy link
Contributor Author

@aoberoi aoberoi Dec 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah higher order functions can be mind-bending for me too. singleAuthorization() returns a function, which as the type Authorize. So this expression is of type Authorize. Which means the argument is of type AuthorizeSourceData. So this isEnterpriseInstall is the property of AuthorizeSourceData, defined on line 94.

As for the actual value, its from the return value of buildSource().

Co-authored-by: Steve Gill <stevengill97@gmail.com>
@aoberoi
Copy link
Contributor Author

aoberoi commented Dec 17, 2020

I'm fine to have 3 & 4 in this PR but I think we can have separate PRs for 1 & 2.

@seratch I initially started this PR to only work on 3 & 4, but as soon as those were fixed (specifically removing StringIndexed) the build was failing because of the unsafe accesses. Then I started working on 1 & 2 to fix those unsafe accesses.

I agree that keeping PRs small and focused makes them easier to review, and easier to land when there's several changes worked on by different people. In order to do that here, I think I have to first land the changes related to 1 & 2 and then land the changes related to 3 & 4 (since the changes in 3 & 4 will not build otherwise). This PR would represent the latter (that's already the portion you reviewed). I'll give that a try.

@stevengill
Copy link
Member

Instead of spending time breaking this into two, I suggest we move forward with merging this PR! It is looking pretty solid to me!

src/App.ts Outdated Show resolved Hide resolved
@aoberoi aoberoi merged commit 30af88b into slackapi:main Jan 4, 2021
@aoberoi aoberoi deleted the fix-loosely-indexed-event-types branch January 4, 2021 20:39
@seratch seratch mentioned this pull request Jan 29, 2021
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement M-T: A feature request for new functionality semver:minor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MessageEvent type definition does not match event data when subtype is 'message_changed'
3 participants