Repository: slack-go/slack
Pull request #1309 (2024-08-09 14:22:23)

Support publishing a messge to a specific thread
Support publishing a messge to a specific thread https://api.slack.com/interactivity/handling#publishing_in_thread

From slack interactivity documentation:
Publishing responses in thread 
If you want to publish a message to a specific thread, you'll need to include an attribute response_type and set its value to in_channel. Then, to specify the thread, include a thread_ts.

Also, be sure to set replace_original to false or you'll overwrite the message you're wanting to respond to!


Patch:
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
index e99b5e6..e860f78 100644
--- a/.github/workflows/stale.yaml
+++ b/.github/workflows/stale.yaml
@@ -13,7 +13,7 @@ jobs:
   stale:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
+      - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
         with:
           any-of-labels: 'feedback given'
           days-before-stale: 45
diff --git a/block_image.go b/block_image.go
index 90cbd14..b3d2cb8 100644
--- a/block_image.go
+++ b/block_image.go
@@ -4,11 +4,21 @@ package slack
 //
 // More Information: https://api.slack.com/reference/messaging/blocks#image
 type ImageBlock struct {
-	Type     MessageBlockType `json:"type"`
-	ImageURL string           `json:"image_url"`
-	AltText  string           `json:"alt_text"`
-	BlockID  string           `json:"block_id,omitempty"`
-	Title    *TextBlockObject `json:"title,omitempty"`
+	Type      MessageBlockType `json:"type"`
+	ImageURL  string           `json:"image_url,omitempty"`
+	AltText   string           `json:"alt_text"`
+	BlockID   string           `json:"block_id,omitempty"`
+	Title     *TextBlockObject `json:"title,omitempty"`
+	SlackFile *SlackFileObject `json:"slack_file,omitempty"`
+}
+
+// SlackFileObject Defines an object containing Slack file information to be used in an
+// image block or image element.
+//
+// More Information: https://api.slack.com/reference/block-kit/composition-objects#slack_file
+type SlackFileObject struct {
+	ID  string `json:"id,omitempty"`
+	URL string `json:"url,omitempty"`
 }
 
 // BlockType returns the type of the block
diff --git a/channels.go b/channels.go
index 2fca8b9..88d567b 100644
--- a/channels.go
+++ b/channels.go
@@ -19,10 +19,11 @@ type channelResponseFull struct {
 // Channel contains information about the channel
 type Channel struct {
 	GroupConversation
-	IsChannel bool   `json:"is_channel"`
-	IsGeneral bool   `json:"is_general"`
-	IsMember  bool   `json:"is_member"`
-	Locale    string `json:"locale"`
+	IsChannel  bool        `json:"is_channel"`
+	IsGeneral  bool        `json:"is_general"`
+	IsMember   bool        `json:"is_member"`
+	Locale     string      `json:"locale"`
+	Properties *Properties `json:"properties"`
 }
 
 func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) {
diff --git a/chat.go b/chat.go
index 916d05a..96843d6 100644
--- a/chat.go
+++ b/chat.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"regexp"
@@ -226,11 +226,11 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
 	}
 
 	if api.Debug() {
-		reqBody, err := ioutil.ReadAll(req.Body)
+		reqBody, err := io.ReadAll(req.Body)
 		if err != nil {
 			return "", "", "", err
 		}
-		req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody))
+		req.Body = io.NopCloser(bytes.NewBuffer(reqBody))
 		api.Debugf("Sending request: %s", redactToken(reqBody))
 	}
 
@@ -377,6 +377,7 @@ func (t responseURLSender) BuildRequestContext(ctx context.Context) (*http.Reque
 	req, err := jsonReq(ctx, t.endpoint, Msg{
 		Text:            t.values.Get("text"),
 		Timestamp:       t.values.Get("ts"),
+		ThreadTimestamp: t.values.Get("thread_ts"),
 		Attachments:     t.attachments,
 		Blocks:          t.blocks,
 		Metadata:        t.metadata,
diff --git a/conversation.go b/conversation.go
index 6e608eb..bfc17fd 100644
--- a/conversation.go
+++ b/conversation.go
@@ -65,6 +65,17 @@ type Purpose struct {
 	LastSet JSONTime `json:"last_set"`
 }
 
+// Properties contains the Canvas associated to the channel.
+type Properties struct {
+	Canvas Canvas `json:"canvas"`
+}
+
+type Canvas struct {
+	FileId       string `json:"file_id"`
+	IsEmpty      bool   `json:"is_empty"`
+	QuipThreadId string `json:"quip_thread_id"`
+}
+
 type GetUsersInConversationParameters struct {
 	ChannelID string
 	Cursor    string
diff --git a/examples/dialog/dialog.go b/examples/dialog/dialog.go
index 4b4d6ab..dc0d431 100644
--- a/examples/dialog/dialog.go
+++ b/examples/dialog/dialog.go
@@ -2,7 +2,7 @@ package main
 
 import (
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
@@ -26,7 +26,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
 
 	// Read request body
 	defer r.Body.Close()
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		w.WriteHeader(http.StatusInternalServerError)
 		log.Printf("[ERROR] Fail to read request body: %v", err)
diff --git a/examples/eventsapi/events.go b/examples/eventsapi/events.go
index 25049ad..b3bb70a 100644
--- a/examples/eventsapi/events.go
+++ b/examples/eventsapi/events.go
@@ -3,7 +3,7 @@ package main
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"os"
 
@@ -18,7 +18,7 @@ func main() {
 	signingSecret := os.Getenv("SLACK_SIGNING_SECRET")
 
 	http.HandleFunc("/events-endpoint", func(w http.ResponseWriter, r *http.Request) {
-		body, err := ioutil.ReadAll(r.Body)
+		body, err := io.ReadAll(r.Body)
 		if err != nil {
 			w.WriteHeader(http.StatusBadRequest)
 			return
diff --git a/examples/modal/modal.go b/examples/modal/modal.go
index 155f2f7..13115a8 100644
--- a/examples/modal/modal.go
+++ b/examples/modal/modal.go
@@ -15,7 +15,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/slack-go/slack"
@@ -95,13 +95,13 @@ func verifySigningSecret(r *http.Request) error {
 		return err
 	}
 
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		fmt.Println(err.Error())
 		return err
 	}
 	// Need to use r.Body again when unmarshalling SlashCommand and InteractionCallback
-	r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+	r.Body = io.NopCloser(bytes.NewBuffer(body))
 
 	verifier.Write(body)
 	if err = verifier.Ensure(); err != nil {
diff --git a/examples/slash/slash.go b/examples/slash/slash.go
index c70c865..e35ab10 100644
--- a/examples/slash/slash.go
+++ b/examples/slash/slash.go
@@ -5,7 +5,6 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/slack-go/slack"
@@ -27,7 +26,7 @@ func main() {
 			return
 		}
 
-		r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &verifier))
+		r.Body = io.NopCloser(io.TeeReader(r.Body, &verifier))
 		s, err := slack.SlashCommandParse(r)
 		if err != nil {
 			w.WriteHeader(http.StatusInternalServerError)
diff --git a/examples/workflow_step/handler.go b/examples/workflow_step/handler.go
index b25a5be..d0eee10 100644
--- a/examples/workflow_step/handler.go
+++ b/examples/workflow_step/handler.go
@@ -3,7 +3,7 @@ package main
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
@@ -25,7 +25,7 @@ func handleMyWorkflowStep(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// see: https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		return
@@ -88,7 +88,7 @@ func handleInteraction(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		return
diff --git a/examples/workflow_step/middleware.go b/examples/workflow_step/middleware.go
index 0684e50..6fe8269 100644
--- a/examples/workflow_step/middleware.go
+++ b/examples/workflow_step/middleware.go
@@ -2,20 +2,20 @@ package main
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/slack-go/slack"
 )
 
 func (v *SecretsVerifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	body, err := ioutil.ReadAll(r.Body)
+	body, err := io.ReadAll(r.Body)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		return
 	}
 	r.Body.Close()
-	r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+	r.Body = io.NopCloser(bytes.NewBuffer(body))
 
 	sv, err := slack.NewSecretsVerifier(r.Header, appCtx.config.signingSecret)
 	if err != nil {
diff --git a/files.go b/files.go
index 6844edd..b263171 100644
--- a/files.go
+++ b/files.go
@@ -355,14 +355,16 @@ func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParamet
 }
 
 // UploadFile uploads a file.
-// DEPRECATED: Use UploadFileV2 instead. This will stop functioning on March 11, 2025.
+//
+// Deprecated: Use [Client.UploadFileV2] instead. This will stop functioning on March 11, 2025.
 // For more details, see: https://api.slack.com/methods/files.upload#markdown
 func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) {
 	return api.UploadFileContext(context.Background(), params)
 }
 
 // UploadFileContext uploads a file and setting a custom context.
-// DEPRECATED: Use UploadFileV2Context instead. This will stop functioning on March 11, 2025.
+//
+// Deprecated: Use [Client.UploadFileV2Context] instead. This will stop functioning on March 11, 2025.
 // For more details, see: https://api.slack.com/methods/files.upload#markdown
 func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) {
 	// Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More
diff --git a/misc.go b/misc.go
index 7e5a8d5..df779c1 100644
--- a/misc.go
+++ b/misc.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"mime"
 	"mime/multipart"
 	"net/http"
@@ -63,13 +62,12 @@ func (e *RateLimitedError) Retryable() bool {
 	return true
 }
 
-func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) {
+func fileUploadReq(ctx context.Context, path string, r io.Reader) (*http.Request, error) {
 	req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, r)
 	if err != nil {
 		return nil, err
 	}
 
-	req.URL.RawQuery = values.Encode()
 	return req, nil
 }
 
@@ -127,7 +125,7 @@ func jsonReq(ctx context.Context, endpoint string, body interface{}) (req *http.
 }
 
 func parseResponseBody(body io.ReadCloser, intf interface{}, d Debug) error {
-	response, err := ioutil.ReadAll(body)
+	response, err := io.ReadAll(body)
 	if err != nil {
 		return err
 	}
@@ -156,9 +154,16 @@ func postLocalWithMultipartResponse(ctx context.Context, client httpClient, meth
 func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname, token string, values url.Values, r io.Reader, intf interface{}, d Debug) error {
 	pipeReader, pipeWriter := io.Pipe()
 	wr := multipart.NewWriter(pipeWriter)
+
 	errc := make(chan error)
 	go func() {
 		defer pipeWriter.Close()
+		defer wr.Close()
+		err := createFormFields(wr, values)
+		if err != nil {
+			errc <- err
+			return
+		}
 		ioWriter, err := wr.CreateFormFile(fieldname, name)
 		if err != nil {
 			errc <- err
@@ -174,7 +179,8 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam
 			return
 		}
 	}()
-	req, err := fileUploadReq(ctx, path, values, pipeReader)
+
+	req, err := fileUploadReq(ctx, path, pipeReader)
 	if err != nil {
 		return err
 	}
@@ -200,6 +206,20 @@ func postWithMultipartResponse(ctx context.Context, client httpClient, path, nam
 	}
 }
 
+func createFormFields(mw *multipart.Writer, values url.Values) error {
+	for key, value := range values {
+		writer, err := mw.CreateFormField(key)
+		if err != nil {
+			return err
+		}
+		_, err = writer.Write([]byte(value[0]))
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d Debug) error {
 	resp, err := client.Do(req)
 	if err != nil {
@@ -316,7 +336,7 @@ func newJSONParser(dst interface{}) responseParser {
 
 func newTextParser(dst interface{}) responseParser {
 	return func(resp *http.Response) error {
-		b, err := ioutil.ReadAll(resp.Body)
+		b, err := io.ReadAll(resp.Body)
 		if err != nil {
 			return err
 		}
diff --git a/remotefiles.go b/remotefiles.go
index 0467d7e..42639a1 100644
--- a/remotefiles.go
+++ b/remotefiles.go
@@ -247,9 +247,7 @@ func (api *Client) UpdateRemoteFile(fileID string, params RemoteFileParameters)
 // Slack API docs: https://api.slack.com/methods/files.remote.update
 func (api *Client) UpdateRemoteFileContext(ctx context.Context, fileID string, params RemoteFileParameters) (remotefile *RemoteFile, err error) {
 	response := &remoteFileResponseFull{}
-	values := url.Values{
-		"token": {api.token},
-	}
+	values := url.Values{}
 	if fileID != "" {
 		values.Add("file", fileID)
 	}
@@ -271,6 +269,7 @@ func (api *Client) UpdateRemoteFileContext(ctx context.Context, fileID string, p
 	if params.PreviewImageReader != nil {
 		err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.remote.update", "preview.png", "preview_image", api.token, values, params.PreviewImageReader, response, api)
 	} else {
+		values.Add("token", api.token)
 		response, err = api.remoteFileRequest(ctx, "files.remote.update", values)
 	}
 
diff --git a/slackevents/inner_events.go b/slackevents/inner_events.go
index e0494fe..8f8effa 100644
--- a/slackevents/inner_events.go
+++ b/slackevents/inner_events.go
@@ -465,6 +465,7 @@ type File struct {
 	DisplayAsBot       bool   `json:"display_as_bot"`
 	Username           string `json:"username"`
 	URLPrivate         string `json:"url_private"`
+	FileAccess         string `json:"file_access"`
 	URLPrivateDownload string `json:"url_private_download"`
 	Thumb64            string `json:"thumb_64"`
 	Thumb80            string `json:"thumb_80"`
@@ -651,6 +652,462 @@ type SharedInvite struct {
 	IsExternalLimited bool            `json:"is_external_limited,omitempty"`
 }
 
+type ChannelHistoryChangedEvent struct {
+	Type    string `json:"type"`
+	Latest  string `json:"latest"`
+	Ts      string `json:"ts"`
+	EventTs string `json:"event_ts"`
+}
+
+type CommandsChangedEvent struct {
+	Type    string `json:"type"`
+	EventTs string `json:"event_ts"`
+}
+
+type DndUpdatedEvent struct {
+	Type      string `json:"type"`
+	User      string `json:"user"`
+	DndStatus struct {
+		DndEnabled     bool  `json:"dnd_enabled"`
+		NextDndStartTs int64 `json:"next_dnd_start_ts"`
+		NextDndEndTs   int64 `json:"next_dnd_end_ts"`
+		SnoozeEnabled  bool  `json:"snooze_enabled"`
+		SnoozeEndtime  int64 `json:"snooze_endtime"`
+	} `json:"dnd_status"`
+}
+
+type DndUpdatedUserEvent struct {
+	Type      string `json:"type"`
+	User      string `json:"user"`
+	DndStatus struct {
+		DndEnabled     bool  `json:"dnd_enabled"`
+		NextDndStartTs int64 `json:"next_dnd_start_ts"`
+		NextDndEndTs   int64 `json:"next_dnd_end_ts"`
+	} `json:"dnd_status"`
+}
+
+type EmailDomainChangedEvent struct {
+	Type        string `json:"type"`
+	EmailDomain string `json:"email_domain"`
+	EventTs     string `json:"event_ts"`
+}
+
+type GroupCloseEvent struct {
+	Type    string `json:"type"`
+	User    string `json:"user"`
+	Channel string `json:"channel"`
+}
+
+type GroupHistoryChangedEvent struct {
+	Type    string `json:"type"`
+	Latest  string `json:"latest"`
+	Ts      string `json:"ts"`
+	EventTs string `json:"event_ts"`
+}
+
+type GroupOpenEvent struct {
+	Type    string `json:"type"`
+	User    string `json:"user"`
+	Channel string `json:"channel"`
+}
+
+type ImCloseEvent struct {
+	Type    string `json:"type"`
+	User    string `json:"user"`
+	Channel string `json:"channel"`
+}
+
+type ImCreatedEvent struct {
+	Type    string `json:"type"`
+	User    string `json:"user"`
+	Channel struct {
+		ID string `json:"id"`
+	} `json:"channel"`
+}
+
+type ImHistoryChangedEvent struct {
+	Type    string `json:"type"`
+	Latest  string `json:"latest"`
+	Ts      string `json:"ts"`
+	EventTs string `json:"event_ts"`
+}
+
+type ImOpenEvent struct {
+	Type    string `json:"type"`
+	User    string `json:"user"`
+	Channel string `json:"channel"`
+}
+
+type SubTeam struct {
+	ID          string `json:"id"`
+	TeamID      string `json:"team_id"`
+	IsUsergroup bool   `json:"is_usergroup"`
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	Handle      string `json:"handle"`
+	IsExternal  bool   `json:"is_external"`
+	DateCreate  int64  `json:"date_create"`
+	DateUpdate  int64  `json:"date_update"`
+	DateDelete  int64  `json:"date_delete"`
+	AutoType    string `json:"auto_type"`
+	CreatedBy   string `json:"created_by"`
+	UpdatedBy   string `json:"updated_by"`
+	DeletedBy   string `json:"deleted_by"`
+	Prefs       struct {
+		Channels []string `json:"channels"`
+		Groups   []string `json:"groups"`
+	} `json:"prefs"`
+	Users     []string `json:"users"`
+	UserCount int      `json:"user_count"`
+}
+
+type SubteamCreatedEvent struct {
+	Type    string  `json:"type"`
+	Subteam SubTeam `json:"subteam"`
+}
+
+type SubteamMembersChangedEvent struct {
+	Type               string   `json:"type"`
+	SubteamID          string   `json:"subteam_id"`
+	TeamID             string   `json:"team_id"`
+	DatePreviousUpdate int      `json:"date_previous_update"`
+	DateUpdate         int64    `json:"date_update"`
+	AddedUsers         []string `json:"added_users"`
+	AddedUsersCount    string   `json:"added_users_count"`
+	RemovedUsers       []string `json:"removed_users"`
+	RemovedUsersCount  string   `json:"removed_users_count"`
+}
+
+type SubteamSelfAddedEvent struct {
+	Type      string `json:"type"`
+	SubteamID string `json:"subteam_id"`
+}
+
+type SubteamSelfRemovedEvent struct {
+	Type      string `json:"type"`
+	SubteamID string `json:"subteam_id"`
+}
+
+type SubteamUpdatedEvent struct {
+	Type    string  `json:"type"`
+	Subteam SubTeam `json:"subteam"`
+}
+
+type TeamDomainChangeEvent struct {
+	Type   string `json:"type"`
+	URL    string `json:"url"`
+	Domain string `json:"domain"`
+	TeamID string `json:"team_id"`
+}
+
+type TeamRenameEvent struct {
+	Type   string `json:"type"`
+	Name   string `json:"name"`
+	TeamID string `json:"team_id"`
+}
+
+type UserChangeEvent struct {
+	Type    string `json:"type"`
+	User    User   `json:"user"`
+	CacheTS int64  `json:"cache_ts"`
+	EventTS string `json:"event_ts"`
+}
+
+type AppDeletedEvent struct {
+	Type       string `json:"type"`
+	AppID      string `json:"app_id"`
+	AppName    string `json:"app_name"`
+	AppOwnerID string `json:"app_owner_id"`
+	TeamID     string `json:"team_id"`
+	TeamDomain string `json:"team_domain"`
+	EventTs    string `json:"event_ts"`
+}
+
+type AppInstalledEvent struct {
+	Type       string `json:"type"`
+	AppID      string `json:"app_id"`
+	AppName    string `json:"app_name"`
+	AppOwnerID string `json:"app_owner_id"`
+	UserID     string `json:"user_id"`
+	TeamID     string `json:"team_id"`
+	TeamDomain string `json:"team_domain"`
+	EventTs    string `json:"event_ts"`
+}
+
+type AppRequestedEvent struct {
+	Type       string `json:"type"`
+	AppRequest struct {
+		ID  string `json:"id"`
+		App struct {
+			ID                     string `json:"id"`
+			Name                   string `json:"name"`
+			Description            string `json:"description"`
+			HelpURL                string `json:"help_url"`
+			PrivacyPolicyURL       string `json:"privacy_policy_url"`
+			AppHomepageURL         string `json:"app_homepage_url"`
+			AppDirectoryURL        string `json:"app_directory_url"`
+			IsAppDirectoryApproved bool   `json:"is_app_directory_approved"`
+			IsInternal             bool   `json:"is_internal"`
+			AdditionalInfo         string `json:"additional_info"`
+		} `json:"app"`
+		PreviousResolution struct {
+			Status string `json:"status"`
+			Scopes []struct {
+				Name        string `json:"name"`
+				Description string `json:"description"`
+				IsSensitive bool   `json:"is_sensitive"`
+				TokenType   string `json:"token_type"`
+			} `json:"scopes"`
+		} `json:"previous_resolution"`
+		User struct {
+			ID    string `json:"id"`
+			Name  string `json:"name"`
+			Email string `json:"email"`
+		} `json:"user"`
+		Team struct {
+			ID     string `json:"id"`
+			Name   string `json:"name"`
+			Domain string `json:"domain"`
+		} `json:"team"`
+		Enterprise interface{} `json:"enterprise"`
+		Scopes     []struct {
+			Name        string `json:"name"`
+			Description string `json:"description"`
+			IsSensitive bool   `json:"is_sensitive"`
+			TokenType   string `json:"token_type"`
+		} `json:"scopes"`
+		Message string `json:"message"`
+	} `json:"app_request"`
+}
+
+type AppUninstalledTeamEvent struct {
+	Type       string `json:"type"`
+	AppID      string `json:"app_id"`
+	AppName    string `json:"app_name"`
+	AppOwnerID string `json:"app_owner_id"`
+	UserID     string `json:"user_id"`
+	TeamID     string `json:"team_id"`
+	TeamDomain string `json:"team_domain"`
+	EventTs    string `json:"event_ts"`
+}
+
+type CallRejectedEvent struct {
+	Token    string `json:"token"`
+	TeamID   string `json:"team_id"`
+	APIAppID string `json:"api_app_id"`
+	Event    struct {
+		Type             string `json:"type"`
+		CallID           string `json:"call_id"`
+		UserID           string `json:"user_id"`
+		ChannelID        string `json:"channel_id"`
+		ExternalUniqueID string `json:"external_unique_id"`
+	} `json:"event"`
+	Type        string   `json:"type"`
+	EventID     string   `json:"event_id"`
+	AuthedUsers []string `json:"authed_users"`
+}
+
+type ChannelSharedEvent struct {
+	Type            string `json:"type"`
+	ConnectedTeamID string `json:"connected_team_id"`
+	Channel         string `json:"channel"`
+	EventTs         string `json:"event_ts"`
+}
+
+type FileCreatedEvent struct {
+	Type   string `json:"type"`
+	FileID string `json:"file_id"`
+	File   struct {
+		ID string `json:"id"`
+	} `json:"file"`
+}
+
+type FilePublicEvent struct {
+	Type   string `json:"type"`
+	FileID string `json:"file_id"`
+	File   struct {
+		ID string `json:"id"`
+	} `json:"file"`
+}
+
+type FunctionExecutedEvent struct {
+	Type     string `json:"type"`
+	Function struct {
+		ID              string `json:"id"`
+		CallbackID      string `json:"callback_id"`
+		Title           string `json:"title"`
+		Description     string `json:"description"`
+		Type            string `json:"type"`
+		InputParameters []struct {
+			Type        string `json:"type"`
+			Name        string `json:"name"`
+			Description string `json:"description"`
+			Title       string `json:"title"`
+			IsRequired  bool   `json:"is_required"`
+		} `json:"input_parameters"`
+		OutputParameters []struct {
+			Type        string `json:"type"`
+			Name        string `json:"name"`
+			Description string `json:"description"`
+			Title       string `json:"title"`
+			IsRequired  bool   `json:"is_required"`
+		} `json:"output_parameters"`
+		AppID       string `json:"app_id"`
+		DateCreated int64  `json:"date_created"`
+		DateUpdated int64  `json:"date_updated"`
+		DateDeleted int64  `json:"date_deleted"`
+	} `json:"function"`
+	Inputs              map[string]string `json:"inputs"`
+	FunctionExecutionID string            `json:"function_execution_id"`
+	WorkflowExecutionID string            `json:"workflow_execution_id"`
+	EventTs             string            `json:"event_ts"`
+	BotAccessToken      string            `json:"bot_access_token"`
+}
+
+type InviteRequestedEvent struct {
+	Type          string `json:"type"`
+	InviteRequest struct {
+		ID            string   `json:"id"`
+		Email         string   `json:"email"`
+		DateCreated   int64    `json:"date_created"`
+		RequesterIDs  []string `json:"requester_ids"`
+		ChannelIDs    []string `json:"channel_ids"`
+		InviteType    string   `json:"invite_type"`
+		RealName      string   `json:"real_name"`
+		DateExpire    int64    `json:"date_expire"`
+		RequestReason string   `json:"request_reason"`
+		Team          struct {
+			ID     string `json:"id"`
+			Name   string `json:"name"`
+			Domain string `json:"domain"`
+		} `json:"team"`
+	} `json:"invite_request"`
+}
+
+type StarAddedEvent struct {
+	Type string `json:"type"`
+	User string `json:"user"`
+	Item struct {
+	} `json:"item"`
+	EventTS string `json:"event_ts"`
+}
+
+type StarRemovedEvent struct {
+	Type string `json:"type"`
+	User string `json:"user"`
+	Item struct {
+	} `json:"item"`
+	EventTS string `json:"event_ts"`
+}
+
+type UserHuddleChangedEvent struct {
+	Type    string `json:"type"`
+	User    User   `json:"user"`
+	CacheTS int64  `json:"cache_ts"`
+	EventTS string `json:"event_ts"`
+}
+
+type User struct {
+	ID                     string  `json:"id"`
+	TeamID                 string  `json:"team_id"`
+	Name                   string  `json:"name"`
+	Deleted                bool    `json:"deleted"`
+	Color                  string  `json:"color"`
+	RealName               string  `json:"real_name"`
+	TZ                     string  `json:"tz"`
+	TZLabel                string  `json:"tz_label"`
+	TZOffset               int     `json:"tz_offset"`
+	Profile                Profile `json:"profile"`
+	IsAdmin                bool    `json:"is_admin"`
+	IsOwner                bool    `json:"is_owner"`
+	IsPrimaryOwner         bool    `json:"is_primary_owner"`
+	IsRestricted           bool    `json:"is_restricted"`
+	IsUltraRestricted      bool    `json:"is_ultra_restricted"`
+	IsBot                  bool    `json:"is_bot"`
+	IsAppUser              bool    `json:"is_app_user"`
+	Updated                int64   `json:"updated"`
+	IsEmailConfirmed       bool    `json:"is_email_confirmed"`
+	WhoCanShareContactCard string  `json:"who_can_share_contact_card"`
+	Locale                 string  `json:"locale"`
+}
+
+type Profile struct {
+	Title                  string                 `json:"title"`
+	Phone                  string                 `json:"phone"`
+	Skype                  string                 `json:"skype"`
+	RealName               string                 `json:"real_name"`
+	RealNameNormalized     string                 `json:"real_name_normalized"`
+	DisplayName            string                 `json:"display_name"`
+	DisplayNameNormalized  string                 `json:"display_name_normalized"`
+	Fields                 map[string]interface{} `json:"fields"`
+	StatusText             string                 `json:"status_text"`
+	StatusEmoji            string                 `json:"status_emoji"`
+	StatusEmojiDisplayInfo []interface{}          `json:"status_emoji_display_info"`
+	StatusExpiration       int                    `json:"status_expiration"`
+	AvatarHash             string                 `json:"avatar_hash"`
+	FirstName              string                 `json:"first_name"`
+	LastName               string                 `json:"last_name"`
+	Image24                string                 `json:"image_24"`
+	Image32                string                 `json:"image_32"`
+	Image48                string                 `json:"image_48"`
+	Image72                string                 `json:"image_72"`
+	Image192               string                 `json:"image_192"`
+	Image512               string                 `json:"image_512"`
+	StatusTextCanonical    string                 `json:"status_text_canonical"`
+	Team                   string                 `json:"team"`
+}
+
+type UserStatusChangedEvent struct {
+	Type    string `json:"type"`
+	User    User   `json:"user"`
+	CacheTS int64  `json:"cache_ts"`
+	EventTS string `json:"event_ts"`
+}
+
+type Actor struct {
+	ID          string `json:"id"`
+	Name        string `json:"name"`
+	IsBot       bool   `json:"is_bot"`
+	TeamID      string `json:"team_id"`
+	Timezone    string `json:"timezone"`
+	RealName    string `json:"real_name"`
+	DisplayName string `json:"display_name"`
+}
+
+type TargetUser struct {
+	Email    string `json:"email"`
+	InviteID string `json:"invite_id"`
+}
+
+type TeamIcon struct {
+	Image34      string `json:"image_34"`
+	ImageDefault bool   `json:"image_default"`
+}
+
+type Team struct {
+	ID                  string   `json:"id"`
+	Icon                TeamIcon `json:"icon"`
+	Name                string   `json:"name"`
+	Domain              string   `json:"domain"`
+	IsVerified          bool     `json:"is_verified"`
+	DateCreated         int64    `json:"date_created"`
+	AvatarBaseURL       string   `json:"avatar_base_url"`
+	RequiresSponsorship bool     `json:"requires_sponsorship"`
+}
+
+type SharedChannelInviteRequestedEvent struct {
+	Actor                       Actor        `json:"actor"`
+	ChannelID                   string       `json:"channel_id"`
+	EventType                   string       `json:"event_type"`
+	ChannelName                 string       `json:"channel_name"`
+	ChannelType                 string       `json:"channel_type"`
+	TargetUsers                 []TargetUser `json:"target_users"`
+	TeamsInChannel              []Team       `json:"teams_in_channel"`
+	IsExternalLimited           bool         `json:"is_external_limited"`
+	ChannelDateCreated          int64        `json:"channel_date_created"`
+	ChannelMessageLatestCounted int64        `json:"channel_message_latest_counted_timestamp"`
+}
+
 type EventsAPIType string
 
 const (
@@ -740,53 +1197,157 @@ const (
 	TeamAccessRevoked = EventsAPIType("team_access_revoked")
 	// UserProfileChanged is sent if a user's profile information has changed.
 	UserProfileChanged = EventsAPIType("user_profile_changed")
+	// ChannelHistoryChanged The history of a channel changed
+	ChannelHistoryChanged = EventsAPIType("channel_history_changed")
+	// CommandsChanged A command was changed
+	CommandsChanged = EventsAPIType("commands_changed")
+	// DndUpdated Do Not Disturb settings were updated
+	DndUpdated = EventsAPIType("dnd_updated")
+	// DndUpdatedUser Do Not Disturb settings for a user were updated
+	DndUpdatedUser = EventsAPIType("dnd_updated_user")
+	// EmailDomainChanged The email domain changed
+	EmailDomainChanged = EventsAPIType("email_domain_changed")
+	// GroupClose A group was closed
+	GroupClose = EventsAPIType("group_close")
+	// GroupHistoryChanged The history of a group changed
+	GroupHistoryChanged = EventsAPIType("group_history_changed")
+	// GroupOpen A group was opened
+	GroupOpen = EventsAPIType("group_open")
+	// ImClose An instant message channel was closed
+	ImClose = EventsAPIType("im_close")
+	// ImCreated An instant message channel was created
+	ImCreated = EventsAPIType("im_created")
+	// ImHistoryChanged The history of an instant message channel changed
+	ImHistoryChanged = EventsAPIType("im_history_changed")
+	// ImOpen An instant message channel was opened
+	ImOpen = EventsAPIType("im_open")
+	// SubteamCreated A subteam was created
+	SubteamCreated = EventsAPIType("subteam_created")
+	// SubteamMembersChanged The members of a subteam changed
+	SubteamMembersChanged = EventsAPIType("subteam_members_changed")
+	// SubteamSelfAdded The current user was added to a subteam
+	SubteamSelfAdded = EventsAPIType("subteam_self_added")
+	// SubteamSelfRemoved The current user was removed from a subteam
+	SubteamSelfRemoved = EventsAPIType("subteam_self_removed")
+	// SubteamUpdated A subteam was updated
+	SubteamUpdated = EventsAPIType("subteam_updated")
+	// TeamDomainChange The team's domain changed
+	TeamDomainChange = EventsAPIType("team_domain_change")
+	// TeamRename The team was renamed
+	TeamRename = EventsAPIType("team_rename")
+	// UserChange A user object has changed
+	UserChange = EventsAPIType("user_change")
+	// AppDeleted is an event when an app is deleted from a workspace
+	AppDeleted = EventsAPIType("app_deleted")
+	// AppInstalled is an event when an app is installed to a workspace
+	AppInstalled = EventsAPIType("app_installed")
+	// AppRequested is an event when a user requests to install an app to a workspace
+	AppRequested = EventsAPIType("app_requested")
+	// AppUninstalledTeam is an event when an app is uninstalled from a team
+	AppUninstalledTeam = EventsAPIType("app_uninstalled_team")
+	// CallRejected is an event when a Slack call is rejected
+	CallRejected = EventsAPIType("call_rejected")
+	// ChannelShared is an event when a channel is shared with another workspace
+	ChannelShared = EventsAPIType("channel_shared")
+	// FileCreated is an event when a file is created in a workspace
+	FileCreated = EventsAPIType("file_created")
+	// FilePublic is an event when a file is made public in a workspace
+	FilePublic = EventsAPIType("file_public")
+	// FunctionExecuted is an event when a Slack function is executed
+	FunctionExecuted = EventsAPIType("function_executed")
+	// InviteRequested is an event when a user requests an invite to a workspace
+	InviteRequested = EventsAPIType("invite_requested")
+	// SharedChannelInviteRequested is an event when an invitation to share a channel is requested
+	SharedChannelInviteRequested = EventsAPIType("shared_channel_invite_requested")
+	// StarAdded is an event when a star is added to a message or file
+	StarAdded = EventsAPIType("star_added")
+	// StarRemoved is an event when a star is removed from a message or file
+	StarRemoved = EventsAPIType("star_removed")
+	// UserHuddleChanged is an event when a user's huddle status changes
+	UserHuddleChanged = EventsAPIType("user_huddle_changed")
+	// UserStatusChanged is an event when a user's status changes
+	UserStatusChanged = EventsAPIType("user_status_changed")
 )
 
 // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct
 // implementations. The structs should be instances of the unmarshalling
 // target for the matching event type.
 var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{
-	AppMention:                  AppMentionEvent{},
-	AppHomeOpened:               AppHomeOpenedEvent{},
-	AppUninstalled:              AppUninstalledEvent{},
-	ChannelCreated:              ChannelCreatedEvent{},
-	ChannelDeleted:              ChannelDeletedEvent{},
-	ChannelArchive:              ChannelArchiveEvent{},
-	ChannelUnarchive:            ChannelUnarchiveEvent{},
-	ChannelLeft:                 ChannelLeftEvent{},
-	ChannelRename:               ChannelRenameEvent{},
-	ChannelIDChanged:            ChannelIDChangedEvent{},
-	FileChange:                  FileChangeEvent{},
-	FileDeleted:                 FileDeletedEvent{},
-	FileShared:                  FileSharedEvent{},
-	FileUnshared:                FileUnsharedEvent{},
-	GroupDeleted:                GroupDeletedEvent{},
-	GroupArchive:                GroupArchiveEvent{},
-	GroupUnarchive:              GroupUnarchiveEvent{},
-	GroupLeft:                   GroupLeftEvent{},
-	GroupRename:                 GroupRenameEvent{},
-	GridMigrationFinished:       GridMigrationFinishedEvent{},
-	GridMigrationStarted:        GridMigrationStartedEvent{},
-	LinkShared:                  LinkSharedEvent{},
-	Message:                     MessageEvent{},
-	MemberJoinedChannel:         MemberJoinedChannelEvent{},
-	MemberLeftChannel:           MemberLeftChannelEvent{},
-	PinAdded:                    PinAddedEvent{},
-	PinRemoved:                  PinRemovedEvent{},
-	ReactionAdded:               ReactionAddedEvent{},
-	ReactionRemoved:             ReactionRemovedEvent{},
-	SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{},
-	SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{},
-	SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{},
-	SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{},
-	TeamJoin:                    TeamJoinEvent{},
-	TokensRevoked:               TokensRevokedEvent{},
-	EmojiChanged:                EmojiChangedEvent{},
-	WorkflowStepExecute:         WorkflowStepExecuteEvent{},
-	MessageMetadataPosted:       MessageMetadataPostedEvent{},
-	MessageMetadataUpdated:      MessageMetadataUpdatedEvent{},
-	MessageMetadataDeleted:      MessageMetadataDeletedEvent{},
-	TeamAccessGranted:           TeamAccessGrantedEvent{},
-	TeamAccessRevoked:           TeamAccessRevokedEvent{},
-	UserProfileChanged:          UserProfileChangedEvent{},
+	AppMention:                   AppMentionEvent{},
+	AppHomeOpened:                AppHomeOpenedEvent{},
+	AppUninstalled:               AppUninstalledEvent{},
+	ChannelCreated:               ChannelCreatedEvent{},
+	ChannelDeleted:               ChannelDeletedEvent{},
+	ChannelArchive:               ChannelArchiveEvent{},
+	ChannelUnarchive:             ChannelUnarchiveEvent{},
+	ChannelLeft:                  ChannelLeftEvent{},
+	ChannelRename:                ChannelRenameEvent{},
+	ChannelIDChanged:             ChannelIDChangedEvent{},
+	FileChange:                   FileChangeEvent{},
+	FileDeleted:                  FileDeletedEvent{},
+	FileShared:                   FileSharedEvent{},
+	FileUnshared:                 FileUnsharedEvent{},
+	GroupDeleted:                 GroupDeletedEvent{},
+	GroupArchive:                 GroupArchiveEvent{},
+	GroupUnarchive:               GroupUnarchiveEvent{},
+	GroupLeft:                    GroupLeftEvent{},
+	GroupRename:                  GroupRenameEvent{},
+	GridMigrationFinished:        GridMigrationFinishedEvent{},
+	GridMigrationStarted:         GridMigrationStartedEvent{},
+	LinkShared:                   LinkSharedEvent{},
+	Message:                      MessageEvent{},
+	MemberJoinedChannel:          MemberJoinedChannelEvent{},
+	MemberLeftChannel:            MemberLeftChannelEvent{},
+	PinAdded:                     PinAddedEvent{},
+	PinRemoved:                   PinRemovedEvent{},
+	ReactionAdded:                ReactionAddedEvent{},
+	ReactionRemoved:              ReactionRemovedEvent{},
+	SharedChannelInviteApproved:  SharedChannelInviteApprovedEvent{},
+	SharedChannelInviteAccepted:  SharedChannelInviteAcceptedEvent{},
+	SharedChannelInviteDeclined:  SharedChannelInviteDeclinedEvent{},
+	SharedChannelInviteReceived:  SharedChannelInviteReceivedEvent{},
+	TeamJoin:                     TeamJoinEvent{},
+	TokensRevoked:                TokensRevokedEvent{},
+	EmojiChanged:                 EmojiChangedEvent{},
+	WorkflowStepExecute:          WorkflowStepExecuteEvent{},
+	MessageMetadataPosted:        MessageMetadataPostedEvent{},
+	MessageMetadataUpdated:       MessageMetadataUpdatedEvent{},
+	MessageMetadataDeleted:       MessageMetadataDeletedEvent{},
+	TeamAccessGranted:            TeamAccessGrantedEvent{},
+	TeamAccessRevoked:            TeamAccessRevokedEvent{},
+	UserProfileChanged:           UserProfileChangedEvent{},
+	ChannelHistoryChanged:        ChannelHistoryChangedEvent{},
+	DndUpdated:                   DndUpdatedEvent{},
+	DndUpdatedUser:               DndUpdatedUserEvent{},
+	EmailDomainChanged:           EmailDomainChangedEvent{},
+	GroupClose:                   GroupCloseEvent{},
+	GroupHistoryChanged:          GroupHistoryChangedEvent{},
+	GroupOpen:                    GroupOpenEvent{},
+	ImClose:                      ImCloseEvent{},
+	ImCreated:                    ImCreatedEvent{},
+	ImHistoryChanged:             ImHistoryChangedEvent{},
+	ImOpen:                       ImOpenEvent{},
+	SubteamCreated:               SubteamCreatedEvent{},
+	SubteamMembersChanged:        SubteamMembersChangedEvent{},
+	SubteamSelfAdded:             SubteamSelfAddedEvent{},
+	SubteamSelfRemoved:           SubteamSelfRemovedEvent{},
+	SubteamUpdated:               SubteamUpdatedEvent{},
+	TeamDomainChange:             TeamDomainChangeEvent{},
+	TeamRename:                   TeamRenameEvent{},
+	UserChange:                   UserChangeEvent{},
+	AppDeleted:                   AppDeletedEvent{},
+	AppInstalled:                 AppInstalledEvent{},
+	AppRequested:                 AppRequestedEvent{},
+	AppUninstalledTeam:           AppUninstalledTeamEvent{},
+	CallRejected:                 CallRejectedEvent{},
+	ChannelShared:                ChannelSharedEvent{},
+	FileCreated:                  FileCreatedEvent{},
+	FilePublic:                   FilePublicEvent{},
+	FunctionExecuted:             FunctionExecutedEvent{},
+	InviteRequested:              InviteRequestedEvent{},
+	SharedChannelInviteRequested: SharedChannelInviteRequestedEvent{},
+	StarAdded:                    StarAddedEvent{},
+	StarRemoved:                  StarRemovedEvent{},
+	UserHuddleChanged:            UserHuddleChangedEvent{},
+	UserStatusChanged:            UserStatusChangedEvent{},
 }
diff --git a/slackevents/parsers.go b/slackevents/parsers.go
index 4daac8b..9e8c22b 100644
--- a/slackevents/parsers.go
+++ b/slackevents/parsers.go
@@ -212,6 +212,32 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error
 		}
 		return innerEvent, nil
 	}
+
+	if e.Type == AppRateLimited {
+		appRateLimitedEvent := &EventsAPIAppRateLimited{}
+		err = json.Unmarshal(rawEvent, appRateLimitedEvent)
+		if err != nil {
+			return EventsAPIEvent{
+				"",
+				"",
+				"unmarshalling_error",
+				"",
+				"",
+				&slack.UnmarshallingErrorEvent{ErrorObj: err},
+				EventsAPIInnerEvent{},
+			}, err
+		}
+		return EventsAPIEvent{
+			e.Token,
+			e.TeamID,
+			e.Type,
+			e.APIAppID,
+			e.EnterpriseID,
+			appRateLimitedEvent,
+			EventsAPIInnerEvent{},
+		}, nil
+	}
+
 	urlVerificationEvent := &EventsAPIURLVerificationEvent{}
 	err = json.Unmarshal(rawEvent, urlVerificationEvent)
 	if err != nil {
diff --git a/webhooks.go b/webhooks.go
index 8fc149d..5a854f3 100644
--- a/webhooks.go
+++ b/webhooks.go
@@ -6,7 +6,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 )
 
@@ -57,7 +56,7 @@ func PostWebhookCustomHTTPContext(ctx context.Context, url string, httpClient *h
 		return fmt.Errorf("failed to post webhook: %w", err)
 	}
 	defer func() {
-		io.Copy(ioutil.Discard, resp.Body)
+		io.Copy(io.Discard, resp.Body)
 		resp.Body.Close()
 	}()
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cbf91c6..61eaa92 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,10 +17,11 @@ jobs:
           - '1.20'
           - '1.21'
           - '1.22'
+          - '1.23'
     name: test go-${{ matrix.go }}
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-go@v3
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v5
         with:
           go-version: ${{ matrix.go }}
       - name: run test
@@ -31,14 +32,14 @@ jobs:
     runs-on: ubuntu-22.04
     name: lint
     steps:
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version: '1.20'
           cache: false
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           fetch-depth: 0
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3.4.0
+        uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
         with:
           version: v1.52.2
diff --git a/chat_test.go b/chat_test.go
index ee93bdd..917ff96 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -3,7 +3,7 @@ package slack
 import (
 	"bytes"
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
@@ -216,7 +216,7 @@ func TestPostMessage(t *testing.T) {
 		t.Run(name, func(t *testing.T) {
 			http.DefaultServeMux = new(http.ServeMux)
 			http.HandleFunc(test.endpoint, func(rw http.ResponseWriter, r *http.Request) {
-				body, err := ioutil.ReadAll(r.Body)
+				body, err := io.ReadAll(r.Body)
 				if err != nil {
 					t.Errorf("unexpected error: %v", err)
 					return
@@ -242,7 +242,7 @@ func TestPostMessageWithBlocksWhenMsgOptionResponseURLApplied(t *testing.T) {
 
 	http.DefaultServeMux = new(http.ServeMux)
 	http.HandleFunc("/response-url", func(rw http.ResponseWriter, r *http.Request) {
-		body, err := ioutil.ReadAll(r.Body)
+		body, err := io.ReadAll(r.Body)
 		if err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
@@ -270,7 +270,7 @@ func TestPostMessageWithBlocksWhenMsgOptionResponseURLApplied(t *testing.T) {
 func TestPostMessageWhenMsgOptionReplaceOriginalApplied(t *testing.T) {
 	http.DefaultServeMux = new(http.ServeMux)
 	http.HandleFunc("/response-url", func(rw http.ResponseWriter, r *http.Request) {
-		body, err := ioutil.ReadAll(r.Body)
+		body, err := io.ReadAll(r.Body)
 		if err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
@@ -297,7 +297,7 @@ func TestPostMessageWhenMsgOptionReplaceOriginalApplied(t *testing.T) {
 func TestPostMessageWhenMsgOptionDeleteOriginalApplied(t *testing.T) {
 	http.DefaultServeMux = new(http.ServeMux)
 	http.HandleFunc("/response-url", func(rw http.ResponseWriter, r *http.Request) {
-		body, err := ioutil.ReadAll(r.Body)
+		body, err := io.ReadAll(r.Body)
 		if err != nil {
 			t.Errorf("unexpected error: %v", err)
 			return
diff --git a/conversation_test.go b/conversation_test.go
index 1c6b8a2..77dfd01 100644
--- a/conversation_test.go
+++ b/conversation_test.go
@@ -149,6 +149,85 @@ func TestCreateSimpleGroup(t *testing.T) {
 	assertSimpleGroup(t, group)
 }
 
+// Channel with Canvas
+var channelWithCanvas = `{
+    "id": "C024BE91L",
+    "name": "fun",
+    "is_channel": true,
+    "created": 1360782804,
+    "creator": "U024BE7LH",
+    "is_archived": false,
+    "is_general": false,
+    "members": [
+        "U024BE7LH"
+    ],
+    "topic": {
+        "value": "Fun times",
+        "creator": "U024BE7LV",
+        "last_set": 1369677212
+    },
+    "purpose": {
+        "value": "This channel is for fun",
+        "creator": "U024BE7LH",
+        "last_set": 1360782804
+    },
+    "is_member": true,
+    "last_read": "1401383885.000061",
+    "unread_count": 0,
+    "unread_count_display": 0,
+	"properties": {
+        "canvas": {
+            "file_id": "F05RQ01LJU0",
+            "is_empty": true,
+            "quip_thread_id": "XFB9AAlvIyJ"
+        }
+    }
+}`
+
+func unmarshalChannelWithCanvas(j string) (*Channel, error) {
+	channel := &Channel{}
+	if err := json.Unmarshal([]byte(j), &channel); err != nil {
+		return nil, err
+	}
+	return channel, nil
+}
+
+func TestChannelWithCanvas(t *testing.T) {
+	channel, err := unmarshalChannelWithCanvas(channelWithCanvas)
+	assert.Nil(t, err)
+	assertChannelWithCanvas(t, channel)
+}
+
+func assertChannelWithCanvas(t *testing.T, channel *Channel) {
+	assertSimpleChannel(t, channel)
+	assert.Equal(t, "F05RQ01LJU0", channel.Properties.Canvas.FileId)
+	assert.Equal(t, true, channel.Properties.Canvas.IsEmpty)
+	assert.Equal(t, "XFB9AAlvIyJ", channel.Properties.Canvas.QuipThreadId)
+}
+
+func TestCreateChannelWithCanvas(t *testing.T) {
+	channel := &Channel{}
+	channel.ID = "C024BE91L"
+	channel.Name = "fun"
+	channel.IsChannel = true
+	channel.Created = JSONTime(1360782804)
+	channel.Creator = "U024BE7LH"
+	channel.IsArchived = false
+	channel.IsGeneral = false
+	channel.IsMember = true
+	channel.LastRead = "1401383885.000061"
+	channel.UnreadCount = 0
+	channel.UnreadCountDisplay = 0
+	channel.Properties = &Properties{
+		Canvas: Canvas{
+			FileId:       "F05RQ01LJU0",
+			IsEmpty:      true,
+			QuipThreadId: "XFB9AAlvIyJ",
+		},
+	}
+	assertChannelWithCanvas(t, channel)
+}
+
 // IM
 var simpleIM = `{
     "id": "D024BFF1M",
diff --git a/files_test.go b/files_test.go
index 59afc1c..1df46aa 100644
--- a/files_test.go
+++ b/files_test.go
@@ -3,7 +3,7 @@ package slack
 import (
 	"bytes"
 	"encoding/json"
-	"io/ioutil"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
@@ -44,7 +44,7 @@ func (h *fileCommentHandler) handler(w http.ResponseWriter, r *http.Request) {
 type mockHTTPClient struct{}
 
 func (m *mockHTTPClient) Do(*http.Request) (*http.Response, error) {
-	return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString(`OK`))}, nil
+	return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewBufferString(`OK`))}, nil
 }
 
 func TestSlack_GetFile(t *testing.T) {
diff --git a/reminders_test.go b/reminders_test.go
index 25291b5..09dd6a0 100644
--- a/reminders_test.go
+++ b/reminders_test.go
@@ -2,7 +2,7 @@ package slack
 
 import (
 	"bytes"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"reflect"
 	"testing"
@@ -185,7 +185,7 @@ func (m *mockRemindersListHTTPClient) Do(*http.Request) (*http.Response, error)
 		]
 	}`
 
-	return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString(responseString))}, nil
+	return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewBufferString(responseString))}, nil
 }
 
 func TestSlack_ListReminders(t *testing.T) {
diff --git a/slackevents/inner_events_test.go b/slackevents/inner_events_test.go
index 71eb402..4307e8a 100644
--- a/slackevents/inner_events_test.go
+++ b/slackevents/inner_events_test.go
@@ -1504,3 +1504,1152 @@ func TestSharedChannelDeclined(t *testing.T) {
 	}
 
 }
+
+func TestChannelHistoryChangedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "channel_history_changed",
+			"latest": "1358877455.000010",
+			"ts": "1358877455.000008",
+			"event_ts": "1358877455.000011"
+		}
+	`)
+
+	var e ChannelHistoryChangedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "channel_history_changed" {
+		t.Errorf("type should be channel_history_changed, was %s", e.Type)
+	}
+	if e.Latest != "1358877455.000010" {
+		t.Errorf("latest should be 1358877455.000010, was %s", e.Latest)
+	}
+	if e.Ts != "1358877455.000008" {
+		t.Errorf("ts should be 1358877455.000008, was %s", e.Ts)
+	}
+	if e.EventTs != "1358877455.000011" {
+		t.Errorf("event_ts should be 1358877455.000011, was %s", e.EventTs)
+	}
+}
+
+func TestDndUpdatedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "dnd_updated",
+			"user": "U1234567890",
+			"dnd_status": {
+				"dnd_enabled": true,
+				"next_dnd_start_ts": 1624473600,
+				"next_dnd_end_ts": 1624516800,
+				"snooze_enabled": false
+			}
+		}
+	`)
+
+	var e DndUpdatedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "dnd_updated" {
+		t.Errorf("type should be dnd_updated, was %s", e.Type)
+	}
+	if e.User != "U1234567890" {
+		t.Errorf("user should be U1234567890, was %s", e.User)
+	}
+	if !e.DndStatus.DndEnabled {
+		t.Errorf("dnd_enabled should be true, was %v", e.DndStatus.DndEnabled)
+	}
+	if e.DndStatus.NextDndStartTs != 1624473600 {
+		t.Errorf("next_dnd_start_ts should be 1624473600, was %d", e.DndStatus.NextDndStartTs)
+	}
+	if e.DndStatus.NextDndEndTs != 1624516800 {
+		t.Errorf("next_dnd_end_ts should be 1624516800, was %d", e.DndStatus.NextDndEndTs)
+	}
+	if e.DndStatus.SnoozeEnabled {
+		t.Errorf("snooze_enabled should be false, was %v", e.DndStatus.SnoozeEnabled)
+	}
+}
+
+func TestDndUpdatedUserEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+    		"type": "dnd_updated_user",
+    		"user": "U1234",
+    		"dnd_status": {
+        		"dnd_enabled": true,
+        		"next_dnd_start_ts": 1450387800,
+        		"next_dnd_end_ts": 1450423800
+    		}
+		}
+	`)
+
+	var e DndUpdatedUserEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "dnd_updated_user" {
+		t.Errorf("type should be dnd_updated_user, was %s", e.Type)
+	}
+	if e.User != "U1234" {
+		t.Errorf("user should be U1234, was %s", e.User)
+	}
+	if !e.DndStatus.DndEnabled {
+		t.Errorf("dnd_enabled should be true, was %v", e.DndStatus.DndEnabled)
+	}
+	if e.DndStatus.NextDndStartTs != 1450387800 {
+		t.Errorf("next_dnd_start_ts should be 1450387800, was %d", e.DndStatus.NextDndStartTs)
+	}
+	if e.DndStatus.NextDndEndTs != 1450423800 {
+		t.Errorf("next_dnd_end_ts should be 1450423800, was %d", e.DndStatus.NextDndEndTs)
+	}
+}
+
+func TestEmailDomainChangedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "email_domain_changed",
+			"email_domain": "example.com",
+			"event_ts": "1234567890.123456"
+		}
+	`)
+
+	var e EmailDomainChangedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "email_domain_changed" {
+		t.Errorf("type should be email_domain_changed, was %s", e.Type)
+	}
+	if e.EmailDomain != "example.com" {
+		t.Errorf("email_domain should be example.com, was %s", e.EmailDomain)
+	}
+	if e.EventTs != "1234567890.123456" {
+		t.Errorf("event_ts should be 1234567890.123456, was %s", e.EventTs)
+	}
+}
+
+func TestGroupHistoryChangedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+    		"type": "group_history_changed",
+    		"latest": "1358877455.000010",
+    		"ts": "1361482916.000003",
+    		"event_ts": "1361482916.000004"
+		}
+	`)
+
+	var e GroupHistoryChangedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "group_history_changed" {
+		t.Errorf("type should be group_history_changed, was %s", e.Type)
+	}
+	if e.Latest != "1358877455.000010" {
+		t.Errorf("latest should be 1358877455.000010, was %s", e.Latest)
+	}
+	if e.Ts != "1361482916.000003" {
+		t.Errorf("ts should be 1361482916.000003, was %s", e.Ts)
+	}
+}
+
+func TestGroupOpenEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+    		"type": "group_open",
+    		"user": "U024BE7LH",
+    		"channel": "G024BE91L"
+		}
+	`)
+
+	var e GroupOpenEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "group_open" {
+		t.Errorf("type should be group_open, was %s", e.Type)
+	}
+	if e.User != "U024BE7LH" {
+		t.Errorf("user should be U024BE7LH, was %s", e.User)
+	}
+	if e.Channel != "G024BE91L" {
+		t.Errorf("channel should be G024BE91L, was %s", e.Channel)
+	}
+}
+
+func TestGroupCloseEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "group_close",
+			"user": "U1234567890",
+			"channel": "G1234567890"
+		}
+	`)
+
+	var e GroupCloseEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "group_close" {
+		t.Errorf("type should be group_close, was %s", e.Type)
+	}
+	if e.User != "U1234567890" {
+		t.Errorf("user should be U1234567890, was %s", e.User)
+	}
+	if e.Channel != "G1234567890" {
+		t.Errorf("channel should be G1234567890, was %s", e.Channel)
+	}
+}
+
+func TestImCloseEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "im_close",
+			"user": "U1234567890",
+			"channel": "D1234567890"
+		}
+	`)
+
+	var e ImCloseEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "im_close" {
+		t.Errorf("type should be im_close, was %s", e.Type)
+	}
+	if e.User != "U1234567890" {
+		t.Errorf("user should be U1234567890, was %s", e.User)
+	}
+	if e.Channel != "D1234567890" {
+		t.Errorf("channel should be D1234567890, was %s", e.Channel)
+	}
+}
+
+func TestImCreatedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "im_created",
+			"user": "U1234567890",
+			"channel": {
+				"id": "C12345678"
+			}
+		}
+	`)
+
+	var e ImCreatedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "im_created" {
+		t.Errorf("type should be im_created, was %s", e.Type)
+	}
+	if e.User != "U1234567890" {
+		t.Errorf("user should be U1234567890, was %s", e.User)
+	}
+	if e.Channel.ID != "C12345678" {
+		t.Errorf("channel.id should be C12345678, was %s", e.Channel.ID)
+	}
+}
+
+func TestImHistoryChangedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+    		"type": "im_history_changed",
+    		"latest": "1358877455.000010",
+    		"ts": "1361482916.000003",
+    		"event_ts": "1361482916.000004"
+		}
+	`)
+
+	var e ImHistoryChangedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "im_history_changed" {
+		t.Errorf("type should be im_created, was %s", e.Type)
+	}
+	if e.Latest != "1358877455.000010" {
+		t.Errorf("latest should be 1358877455.000010, was %s", e.Latest)
+	}
+	if e.Ts != "1361482916.000003" {
+		t.Errorf("ts should be 1361482916.000003, was %s", e.Ts)
+	}
+	if e.EventTs != "1361482916.000004" {
+		t.Errorf("event_ts should be 1361482916.000004, was %s", e.EventTs)
+	}
+}
+
+func TestImOpenEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "im_open",
+			"user": "U1234567890",
+			"channel": "D1234567890"
+		}
+	`)
+
+	var e ImOpenEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "im_open" {
+		t.Errorf("type should be im_open, was %s", e.Type)
+	}
+	if e.User != "U1234567890" {
+		t.Errorf("user should be U1234567890, was %s", e.User)
+	}
+	if e.Channel != "D1234567890" {
+		t.Errorf("channel should be D1234567890, was %s", e.Channel)
+	}
+}
+
+func TestSubteamCreatedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "subteam_created",
+			"subteam": {
+				"id": "S1234567890",
+				"team_id": "T1234567890",
+				"is_usergroup": true,
+				"name": "subteam",
+				"description": "A test subteam",
+				"handle": "subteam_handle",
+				"is_external": false,
+				"date_create": 1624473600,
+				"date_update": 1624473600,
+				"date_delete": 0,
+				"auto_type": "auto",
+				"created_by": "U1234567890",
+				"updated_by": "U1234567890",
+				"deleted_by": "",
+				"prefs": {
+					"channels": ["C1234567890"],
+					"groups": ["G1234567890"]
+				},
+				"users": ["U1234567890"],
+				"user_count": 1
+			}
+		}
+	`)
+
+	var e SubteamCreatedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "subteam_created" {
+		t.Errorf("type should be subteam_created, was %s", e.Type)
+	}
+	if e.Subteam.ID != "S1234567890" {
+		t.Errorf("subteam.id should be S1234567890, was %s", e.Subteam.ID)
+	}
+	if e.Subteam.TeamID != "T1234567890" {
+		t.Errorf("subteam.team_id should be T1234567890, was %s", e.Subteam.TeamID)
+	}
+	if !e.Subteam.IsUsergroup {
+		t.Errorf("subteam.is_usergroup should be true, was %v", e.Subteam.IsUsergroup)
+	}
+	if e.Subteam.Name != "subteam" {
+		t.Errorf("subteam.name should be subteam, was %s", e.Subteam.Name)
+	}
+	if e.Subteam.Description != "A test subteam" {
+		t.Errorf("subteam.description should be 'A test subteam', was %s", e.Subteam.Description)
+	}
+	if e.Subteam.Handle != "subteam_handle" {
+		t.Errorf("subteam.handle should be subteam_handle, was %s", e.Subteam.Handle)
+	}
+	if e.Subteam.IsExternal {
+		t.Errorf("subteam.is_external should be false, was %v", e.Subteam.IsExternal)
+	}
+	if e.Subteam.DateCreate != 1624473600 {
+		t.Errorf("subteam.date_create should be 1624473600, was %d", e.Subteam.DateCreate)
+	}
+	if e.Subteam.DateUpdate != 1624473600 {
+		t.Errorf("subteam.date_update should be 1624473600, was %d", e.Subteam.DateUpdate)
+	}
+	if e.Subteam.DateDelete != 0 {
+		t.Errorf("subteam.date_delete should be 0, was %d", e.Subteam.DateDelete)
+	}
+	if e.Subteam.AutoType != "auto" {
+		t.Errorf("subteam.auto_type should be auto, was %s", e.Subteam.AutoType)
+	}
+	if e.Subteam.CreatedBy != "U1234567890" {
+		t.Errorf("subteam.created_by should be U1234567890, was %s", e.Subteam.CreatedBy)
+	}
+	if e.Subteam.UpdatedBy != "U1234567890" {
+		t.Errorf("subteam.updated_by should be U1234567890, was %s", e.Subteam.UpdatedBy)
+	}
+	if e.Subteam.DeletedBy != "" {
+		t.Errorf("subteam.deleted_by should be empty, was %s", e.Subteam.DeletedBy)
+	}
+	if len(e.Subteam.Prefs.Channels) != 1 || e.Subteam.Prefs.Channels[0] != "C1234567890" {
+		t.Errorf("subteam.prefs.channels should contain C1234567890, was %v", e.Subteam.Prefs.Channels)
+	}
+	if len(e.Subteam.Prefs.Groups) != 1 || e.Subteam.Prefs.Groups[0] != "G1234567890" {
+		t.Errorf("subteam.prefs.groups should contain G1234567890, was %v", e.Subteam.Prefs.Groups)
+	}
+	if len(e.Subteam.Users) != 1 || e.Subteam.Users[0] != "U1234567890" {
+		t.Errorf("subteam.users should contain U1234567890, was %v", e.Subteam.Users)
+	}
+	if e.Subteam.UserCount != 1 {
+		t.Errorf("subteam.user_count should be 1, was %d", e.Subteam.UserCount)
+	}
+}
+
+func TestSubteamMembersChangedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "subteam_members_changed",
+			"subteam_id": "S1234567890",
+			"team_id": "T1234567890",
+			"date_previous_update": 1446670362,
+			"date_update": 1624473600,
+			"added_users": ["U1234567890"],
+			"added_users_count": "3",
+			"removed_users": ["U0987654321"],
+			"removed_users_count": "1"
+		}
+	`)
+
+	var e SubteamMembersChangedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "subteam_members_changed" {
+		t.Errorf("type should be subteam_members_changed, was %s", e.Type)
+	}
+	if e.SubteamID != "S1234567890" {
+		t.Errorf("subteam_id should be S1234567890, was %s", e.SubteamID)
+	}
+	if e.TeamID != "T1234567890" {
+		t.Errorf("team_id should be T1234567890, was %s", e.TeamID)
+	}
+	if e.DateUpdate != 1624473600 {
+		t.Errorf("date_update should be 1624473600, was %d", e.DateUpdate)
+	}
+	if len(e.AddedUsers) != 1 || e.AddedUsers[0] != "U1234567890" {
+		t.Errorf("subteam.users should contain U1234567890, was %v", e.AddedUsers)
+	}
+	if len(e.RemovedUsers) != 1 || e.RemovedUsers[0] != "U0987654321" {
+		t.Errorf("subteam.users should contain U0987654321, was %v", e.RemovedUsers)
+	}
+}
+
+func TestSubteamSelfAddedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "subteam_self_added",
+			"subteam_id": "S1234567890"
+		}
+	`)
+
+	var e SubteamSelfAddedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "subteam_self_added" {
+		t.Errorf("type should be subteam_self_added, was %s", e.Type)
+	}
+	if e.SubteamID != "S1234567890" {
+		t.Errorf("subteam_id should be S1234567890, was %s", e.SubteamID)
+	}
+}
+
+func TestSubteamSelfRemovedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "subteam_self_removed",
+			"subteam_id": "S1234567890"
+		}
+	`)
+
+	var e SubteamSelfRemovedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "subteam_self_removed" {
+		t.Errorf("type should be subteam_self_removed, was %s", e.Type)
+	}
+	if e.SubteamID != "S1234567890" {
+		t.Errorf("subteam_id should be S1234567890, was %s", e.SubteamID)
+	}
+}
+
+func TestSubteamUpdatedEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "subteam_updated",
+			"subteam": {
+				"id": "S1234567890",
+				"team_id": "T1234567890",
+				"is_usergroup": true,
+				"name": "updated_subteam",
+				"description": "An updated test subteam",
+				"handle": "updated_subteam_handle",
+				"is_external": false,
+				"date_create": 1624473600,
+				"date_update": 1624473600,
+				"date_delete": 0,
+				"auto_type": "auto",
+				"created_by": "U1234567890",
+				"updated_by": "U1234567890",
+				"deleted_by": "",
+				"prefs": {
+					"channels": ["C1234567890"],
+					"groups": ["G1234567890"]
+				},
+				"users": ["U1234567890"],
+				"user_count": 1
+			}
+		}
+	`)
+
+	var e SubteamUpdatedEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "subteam_updated" {
+		t.Errorf("type should be subteam_updated, was %s", e.Type)
+	}
+	if e.Subteam.ID != "S1234567890" {
+		t.Errorf("subteam.id should be S1234567890, was %s", e.Subteam.ID)
+	}
+	if e.Subteam.TeamID != "T1234567890" {
+		t.Errorf("subteam.team_id should be T1234567890, was %s", e.Subteam.TeamID)
+	}
+	if !e.Subteam.IsUsergroup {
+		t.Errorf("subteam.is_usergroup should be true, was %v", e.Subteam.IsUsergroup)
+	}
+	if e.Subteam.Name != "updated_subteam" {
+		t.Errorf("subteam.name should be updated_subteam, was %s", e.Subteam.Name)
+	}
+	if e.Subteam.Description != "An updated test subteam" {
+		t.Errorf("subteam.description should be 'An updated test subteam', was %s", e.Subteam.Description)
+	}
+	if e.Subteam.Handle != "updated_subteam_handle" {
+		t.Errorf("subteam.handle should be updated_subteam_handle, was %s", e.Subteam.Handle)
+	}
+	if e.Subteam.IsExternal {
+		t.Errorf("subteam.is_external should be false, was %v", e.Subteam.IsExternal)
+	}
+	if e.Subteam.DateCreate != 1624473600 {
+		t.Errorf("subteam.date_create should be 1624473600, was %d", e.Subteam.DateCreate)
+	}
+	if e.Subteam.DateUpdate != 1624473600 {
+		t.Errorf("subteam.date_update should be 1624473600, was %d", e.Subteam.DateUpdate)
+	}
+	if e.Subteam.DateDelete != 0 {
+		t.Errorf("subteam.date_delete should be 0, was %d", e.Subteam.DateDelete)
+	}
+	if e.Subteam.AutoType != "auto" {
+		t.Errorf("subteam.auto_type should be auto, was %s", e.Subteam.AutoType)
+	}
+	if e.Subteam.CreatedBy != "U1234567890" {
+		t.Errorf("subteam.created_by should be U1234567890, was %s", e.Subteam.CreatedBy)
+	}
+	if e.Subteam.UpdatedBy != "U1234567890" {
+		t.Errorf("subteam.updated_by should be U1234567890, was %s", e.Subteam.UpdatedBy)
+	}
+	if e.Subteam.DeletedBy != "" {
+		t.Errorf("subteam.deleted_by should be empty, was %s", e.Subteam.DeletedBy)
+	}
+	if len(e.Subteam.Prefs.Channels) != 1 || e.Subteam.Prefs.Channels[0] != "C1234567890" {
+		t.Errorf("subteam.prefs.channels should contain C1234567890, was %v", e.Subteam.Prefs.Channels)
+	}
+	if len(e.Subteam.Prefs.Groups) != 1 || e.Subteam.Prefs.Groups[0] != "G1234567890" {
+		t.Errorf("subteam.prefs.groups should contain G1234567890, was %v", e.Subteam.Prefs.Groups)
+	}
+	if len(e.Subteam.Users) != 1 || e.Subteam.Users[0] != "U1234567890" {
+		t.Errorf("subteam.users should contain U1234567890, was %v", e.Subteam.Users)
+	}
+	if e.Subteam.UserCount != 1 {
+		t.Errorf("subteam.user_count should be 1, was %d", e.Subteam.UserCount)
+	}
+}
+
+func TestTeamDomainChangeEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "team_domain_change",
+			"url": "https://newdomain.slack.com",
+			"domain": "newdomain",
+			"team_id": "T1234"
+		}
+	`)
+
+	var e TeamDomainChangeEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "team_domain_change" {
+		t.Errorf("type should be team_domain_change, was %s", e.Type)
+	}
+	if e.URL != "https://newdomain.slack.com" {
+		t.Errorf("url should be https://newdomain.slack.com, was %s", e.URL)
+	}
+	if e.Domain != "newdomain" {
+		t.Errorf("domain should be newdomain, was %s", e.Domain)
+	}
+	if e.TeamID != "T1234" {
+		t.Errorf("team_id should be 'T1234', was %s", e.TeamID)
+	}
+}
+
+func TestTeamRenameEvent(t *testing.T) {
+	rawE := []byte(`
+		{
+			"type": "team_rename",
+			"name": "new_team_name",
+			"team_id": "T1234"
+		}
+	`)
+
+	var e TeamRenameEvent
+	if err := json.Unmarshal(rawE, &e); err != nil {
+		t.Fatal(err)
+	}
+	if e.Type != "team_rename" {
+		t.Errorf("type should be team_rename, was %s", e.Type)
+	}
+	if e.Name != "new_team_name" {
+		t.Errorf("name should be new_team_name, was %s", e.Name)
+	}
+	if e.TeamID != "T1234" {
+		t.Errorf("team_id should be 'T1234', was %s", e.TeamID)
+	}
+}
+
+func TestUserChangeEvent(t *testing.T) {
+	jsonStr := `{
+		"user": {
+			"id": "U1234567",
+			"team_id": "T1234567",
+			"name": "some-user",
+			"deleted": false,
+			"color": "4bbe2e",
+			"real_name": "Some User",
+			"tz": "America/Los_Angeles",
+			"tz_label": "Pacific Daylight Time",
+			"tz_offset": -25200,
+			"profile": {
+				"title": "",
+				"phone": "",
+				"skype": "",
+				"real_name": "Some User",
+				"real_name_normalized": "Some User",
+				"display_name": "",
+				"display_name_normalized": "",
+				"fields": {},
+				"status_text": "riding a train",
+				"status_emoji": ":mountain_railway:",
+				"status_emoji_display_info": [],
+				"status_expiration": 0,
+				"avatar_hash": "g12345678910",
+				"first_name": "Some",
+				"last_name": "User",
+				"image_24": "https://secure.gravatar.com/avatar/cb0c2b2ca5e8de16be31a55a734d0f31.jpg?s=24&d=https%3A%2F%2Fdev.slack.com%2Fdev-cdn%2Fv1648136338%2Fimg%2Favatars%2Fuser_shapes%2Fava_0001-24.png",
+				"image_32": "https://secure.gravatar.com/avatar/cb0c2b2ca5e8de16be31a55a734d0f31.jpg?s=32&d=https%3A%2F%2Fdev.slack.com%2Fdev-cdn%2Fv1648136338%2Fimg%2Favatars%2Fuser_shapes%2Fava_0001-32.png",
+				"image_48": "https://secure.gravatar.com/avatar/cb0c2b2ca5e8de16be31a55a734d0f31.jpg?s=48&d=https%3A%2F%2Fdev.slack.com%2Fdev-cdn%2Fv1648136338%2Fimg%2Favatars%2Fuser_shapes%2Fava_0001-48.png",
+				"image_72": "https://secure.gravatar.com/avatar/cb0c2b2ca5e8de16be31a55a734d0f31.jpg?s=72&d=https%3A%2F%2Fdev.slack.com%2Fdev-cdn%2Fv1648136338%2Fimg%2Favatars%2Fuser_shapes%2Fava_0001-72.png",
+				"image_192": "https://secure.gravatar.com/avatar/cb0c2b2ca5e8de16be31a55a734d0f31.jpg?s=192&d=https%3A%2F%2Fdev.slack.com%2Fdev-cdn%2Fv1648136338%2Fimg%2Favatars%2Fuser_shapes%2Fava_0001-192.png",
+				"image_512": "https://secure.gravatar.com/avatar/cb0c2b2ca5e8de16be31a55a734d0f31.jpg?s=512&d=https%3A%2F%2Fdev.slack.com%2Fdev-cdn%2Fv1648136338%2Fimg%2Favatars%2Fuser_shapes%2Fava_0001-512.png",
+				"status_text_canonical": "",
+				"team": "T1234567"
+			},
+			"is_admin": false,
+			"is_owner": false,
+			"is_primary_owner": false,
+			"is_restricted": false,
+			"is_ultra_restricted": false,
+			"is_bot": false,
+			"is_app_user": false,
+			"updated": 1648596421,
+			"is_email_confirmed": true,
+			"who_can_share_contact_card": "EVERYONE",
+			"locale": "en-US"
+		},
+		"cache_ts": 1648596421,
+		"type": "user_change",
+		"event_ts": "1648596712.000001"
+	}`
+
+	var event UserChangeEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal UserChangeEvent: %v", err)
+	}
+
+	if event.Type != "user_change" {
+		t.Errorf("Expected type to be 'user_change', got %s", event.Type)
+	}
+
+	if event.User.ID != "U1234567" {
+		t.Errorf("Expected user ID to be 'U1234567', got %s", event.User.ID)
+	}
+
+	if event.User.Profile.StatusText != "riding a train" {
+		t.Errorf("Expected status text to be 'riding a train', got %s", event.User.Profile.StatusText)
+	}
+
+	if event.User.Profile.StatusEmoji != ":mountain_railway:" {
+		t.Errorf("Expected status emoji to be ':mountain_railway:', got %s", event.User.Profile.StatusEmoji)
+	}
+
+	if event.CacheTS != 1648596421 {
+		t.Errorf("Expected cache_ts to be 1648596421, got %d", event.CacheTS)
+	}
+
+	if event.EventTS != "1648596712.000001" {
+		t.Errorf("Expected event_ts to be '1648596712.000001', got %s", event.EventTS)
+	}
+}
+
+func TestAppDeletedEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "app_deleted",
+		"app_id": "A015CA1LGHG",
+		"app_name": "my-admin-app",
+		"app_owner_id": "U013B64J7MSZ",
+		"team_id": "E073D7H7BBE",
+		"team_domain": "ACME Enterprises",
+		"event_ts": "1700001891.279278"
+	}`
+
+	var event AppDeletedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal AppDeletedEvent: %v", err)
+	}
+
+	if event.Type != "app_deleted" {
+		t.Errorf("Expected type to be 'app_deleted', got %s", event.Type)
+	}
+
+	if event.AppName != "my-admin-app" {
+		t.Errorf("app_name should be 'my-admin-app', was %s", event.AppName)
+	}
+
+	if event.AppOwnerID != "U013B64J7MSZ" {
+		t.Errorf("app_owner_id should be 'U013B64J7MSZ', was %s", event.AppOwnerID)
+	}
+
+	if event.TeamID != "E073D7H7BBE" {
+		t.Errorf("team_id should be 'E073D7H7BBE', was %s", event.TeamID)
+	}
+}
+
+func TestAppInstalledEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "app_installed",
+		"app_id": "A015CA1LGHG",
+		"app_name": "my-admin-app",
+		"app_owner_id": "U013B64J7MSZ",
+		"user_id": "U013B64J7SZ",
+		"team_id": "E073D7H7BBE",
+		"team_domain": "ACME Enterprises",
+		"event_ts": "1700001891.279278"
+	}`
+
+	var event AppInstalledEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal AppInstalledEvent: %v", err)
+	}
+
+	if event.Type != "app_installed" {
+		t.Errorf("Expected type to be 'app_installed', got %s", event.Type)
+	}
+
+	if event.AppName != "my-admin-app" {
+		t.Errorf("app_name should be 'my-admin-app', was %s", event.AppName)
+	}
+
+	if event.AppOwnerID != "U013B64J7MSZ" {
+		t.Errorf("app_owner_id should be 'U013B64J7MSZ', was %s", event.AppOwnerID)
+	}
+
+	if event.TeamID != "E073D7H7BBE" {
+		t.Errorf("team_id should be 'E073D7H7BBE', was %s", event.TeamID)
+	}
+}
+
+func TestAppRequestedEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "app_requested",
+		"app_request": {
+			"id": "1234",
+			"app": {
+				"id": "A5678",
+				"name": "Brent's app",
+				"description": "They're good apps, Bront.",
+				"help_url": "brontsapp.com",
+				"privacy_policy_url": "brontsapp.com",
+				"app_homepage_url": "brontsapp.com",
+				"app_directory_url": "https://slack.slack.com/apps/A102ARD7Y",
+				"is_app_directory_approved": true,
+				"is_internal": false,
+				"additional_info": "none"
+			},
+			"previous_resolution": {
+				"status": "approved",
+				"scopes": [{
+					"name": "app_requested",
+					"description": "allows this app to listen for app install requests",
+					"is_sensitive": false,
+					"token_type": "user"
+				}]
+			},
+			"user": {
+				"id": "U1234",
+				"name": "Bront",
+				"email": "bront@brent.com"
+			},
+			"team": {
+				"id": "T1234",
+				"name": "Brant App Team",
+				"domain": "brantappteam"
+			},
+			"enterprise": null,
+			"scopes": [{
+				"name": "app_requested",
+				"description": "allows this app to listen for app install requests",
+				"is_sensitive": false,
+				"token_type": "user"
+			}],
+			"message": "none"
+		}
+	}`
+
+	var event AppRequestedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal AppRequestedEvent: %v", err)
+	}
+
+	if event.Type != "app_requested" {
+		t.Errorf("Expected type to be 'app_requested', got %s", event.Type)
+	}
+
+	if event.AppRequest.ID != "1234" {
+		t.Errorf("app_request.id should be '1234', was %s", event.AppRequest.ID)
+	}
+
+	if event.AppRequest.App.ID != "A5678" {
+		t.Fail()
+	}
+
+	if event.AppRequest.User.ID != "U1234" {
+		t.Errorf("app_request.user.id should be 'U1234', was %s", event.AppRequest.User.ID)
+	}
+
+	if event.AppRequest.Team.ID != "T1234" {
+		t.Fail()
+	}
+}
+
+func TestAppUninstalledTeamEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "app_uninstalled_team",
+		"app_id": "A015CA1LGHG",
+		"app_name": "my-admin-app",
+		"app_owner_id": "U013B64J7MSZ",
+		"user_id": "U013B64J7SZ",
+		"team_id": "E073D7H7BBE",
+		"team_domain": "ACME Enterprises",
+		"event_ts": "1700001891.279278"
+	}`
+
+	var event AppUninstalledTeamEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal AppUninstalledTeamEvent: %v", err)
+	}
+
+	if event.Type != "app_uninstalled_team" {
+		t.Errorf("Expected type to be 'app_uninstalled_team', got %s", event.Type)
+	}
+
+	if event.AppName != "my-admin-app" {
+		t.Errorf("app_name should be 'my-admin-app', was %s", event.AppName)
+	}
+
+	if event.AppOwnerID != "U013B64J7MSZ" {
+		t.Errorf("app_owner_id should be 'U013B64J7MSZ', was %s", event.AppOwnerID)
+	}
+
+	if event.TeamID != "E073D7H7BBE" {
+		t.Errorf("team_id should be 'E073D7H7BBE', was %s", event.TeamID)
+	}
+}
+
+func TestCallRejectedEvent(t *testing.T) {
+	jsonStr := `{
+		"token": "12345FVmRUzNDOAu12345h",
+		"team_id": "T123ABC456",
+		"api_app_id": "BBBU04BB4",
+		"event": {
+			"type": "call_rejected",
+			"call_id": "R123ABC456",
+			"user_id": "U123ABC456",
+			"channel_id": "D123ABC456",
+			"external_unique_id": "123-456-7890"
+		},
+		"type": "event_callback",
+		"event_id": "Ev123ABC456",
+		"event_time": 1563448153,
+		"authed_users": ["U123ABC456"]
+	}`
+
+	var event CallRejectedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal CallRejectedEvent: %v", err)
+	}
+
+	if event.Event.Type != "call_rejected" {
+		t.Errorf("Expected event type to be 'call_rejected', got %s", event.Event.Type)
+	}
+	if event.TeamID != "T123ABC456" {
+		t.Errorf("Expected team_id to be 'T123ABC456', got %s", event.TeamID)
+	}
+	if event.Event.CallID != "R123ABC456" {
+		t.Fail()
+	}
+
+}
+
+func TestChannelSharedEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "channel_shared",
+		"connected_team_id": "E163Q94DX",
+		"channel": "C123ABC456",
+		"event_ts": "1561064063.001100"
+	}`
+
+	var event ChannelSharedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal ChannelSharedEvent: %v", err)
+	}
+
+	if event.Type != "channel_shared" {
+		t.Errorf("Expected type to be 'channel_shared', got %s", event.Type)
+	}
+
+	if event.ConnectedTeamID != "E163Q94DX" {
+		t.Errorf("Expected connected_team_id to be 'E163Q94DX', got %s", event.ConnectedTeamID)
+	}
+
+	if event.Channel != "C123ABC456" {
+		t.Fail()
+	}
+}
+
+func TestFileCreatedEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "file_created",
+		"file_id": "F2147483862",
+		"file": {
+			"id": "F2147483862"
+		}
+	}`
+
+	var event FileCreatedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal FileCreatedEvent: %v", err)
+	}
+
+	if event.Type != "file_created" {
+		t.Errorf("Expected type to be 'file_created', got %s", event.Type)
+	}
+	if event.FileID != "F2147483862" {
+		t.Errorf("Expected file_id to be 'F2147483862', got %s", event.FileID)
+	}
+}
+
+func TestFilePublicEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "file_public",
+		"file_id": "F2147483862",
+		"file": {
+			"id": "F2147483862"
+		}
+	}`
+
+	var event FilePublicEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal FilePublicEvent: %v", err)
+	}
+
+	if event.Type != "file_public" {
+		t.Errorf("Expected type to be 'file_public', got %s", event.Type)
+	}
+
+	if event.FileID != "F2147483862" {
+		t.Errorf("Expected file_id to be 'F2147483862', got %s", event.FileID)
+	}
+}
+
+func TestFunctionExecutedEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "function_executed",
+		"function": {
+			"id": "Fn123456789O",
+			"callback_id": "sample_function",
+			"title": "Sample function",
+			"description": "Runs sample function",
+			"type": "app",
+			"input_parameters": [
+				{
+					"type": "slack#/types/user_id",
+					"name": "user_id",
+					"description": "Message recipient",
+					"title": "User",
+					"is_required": true
+				}
+			],
+			"output_parameters": [
+				{
+					"type": "slack#/types/user_id",
+					"name": "user_id",
+					"description": "User that completed the function",
+					"title": "Greeting",
+					"is_required": true
+				}
+			],
+			"app_id": "AP123456789",
+			"date_created": 1694727597,
+			"date_updated": 1698947481,
+			"date_deleted": 0
+		},
+		"inputs": { "user_id": "USER12345678" },
+		"function_execution_id": "Fx1234567O9L",
+		"workflow_execution_id": "WxABC123DEF0",
+		"event_ts": "1698958075.998738",
+		"bot_access_token": "abcd-1325532282098-1322446258629-6123648410839-527a1cab3979cad288c9e20330d212cf"
+	}`
+
+	var event FunctionExecutedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal FunctionExecutedEvent: %v", err)
+	}
+
+	if event.Type != "function_executed" {
+		t.Errorf("Expected type to be 'function_executed', got %s", event.Type)
+	}
+
+	if event.Function.ID != "Fn123456789O" {
+		t.Errorf("Expected function.id to be 'Fn123456789O', got %s", event.Function.ID)
+	}
+
+	if event.FunctionExecutionID != "Fx1234567O9L" {
+		t.Fail()
+	}
+}
+
+func TestInviteRequestedEvent(t *testing.T) {
+	jsonStr := `{
+		"type": "invite_requested",
+		"invite_request": {
+			"id": "12345",
+			"email": "bront@puppies.com",
+			"date_created": 123455,
+			"requester_ids": ["U123ABC456"],
+			"channel_ids": ["C123ABC456"],
+			"invite_type": "full_member",
+			"real_name": "Brent",
+			"date_expire": 123456,
+			"request_reason": "They're good dogs, Brant",
+			"team": {
+				"id": "T12345",
+				"name": "Puppy ratings workspace incorporated",
+				"domain": "puppiesrus"
+			}
+		}
+	}`
+
+	var event InviteRequestedEvent
+	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
+		t.Errorf("Failed to unmarshal InviteRequestedEvent: %v", err)
+	}
+
+	if event.Type != "invite_requested" {
+		t.Errorf("Expected type to be 'invite_requested', got %s", event.Type)
+	}
+
+	if event.InviteRequest.ID != "12345" {
+		t.Errorf("invite_request.id should be '12345', was %s", event.InviteRequest.ID)
+	}
+
+	if event.InviteRequest.Email != "bront@puppies.com" {
+		t.Fail()
+	}
+}
+
+func TestSharedChannelInviteRequested_UnmarshalJSON(t *testing.T) {
+	jsonData := `
+	{
+		"actor": {
+			"id": "U012345ABCD",
+			"name": "primary-owner",
+			"is_bot": false,
+			"team_id": "E0123456ABC",
+			"timezone": "",
+			"real_name": "primary-owner",
+			"display_name": ""
+		},
+		"channel_id": "C0123ABCDEF",
+		"event_type": "slack#/events/shared_channel_invite_requested",
+		"channel_name": "our-channel",
+		"channel_type": "public",
+		"target_users": [
+			{
+				"email": "user@some-corp.com",
+				"invite_id": "I0123456ABC"
+			}
+		],
+		"teams_in_channel": [
+			{
+				"id": "E0123456ABC",
+				"icon": {
+					"image_34": "https://slack.com/some-corp/v123/img/abc_0123.png",
+					"image_default": true
+				},
+				"name": "some_enterprise",
+				"domain": "someenterprise",
+				"is_verified": false,
+				"date_created": 1637947110,
+				"avatar_base_url": "https://slack.com/some-corp/",
+				"requires_sponsorship": false
+			},
+			{
+				"id": "T012345ABCD",
+				"icon": {
+					"image_34": "https://slack.com/another-corp/v456/img/def_4567.png",
+					"image_default": true
+				},
+				"name": "another_enterprise",
+				"domain": "anotherenterprise",
+				"is_verified": false,
+				"date_created": 1645550933,
+				"avatar_base_url": "https://slack.com/another-corp/",
+				"requires_sponsorship": false
+			}
+		],
+		"is_external_limited": true,
+		"channel_date_created": 1718725442,
+		"channel_message_latest_counted_timestamp": 1718745614025449
+	}`
+
+	var event SharedChannelInviteRequestedEvent
+	err := json.Unmarshal([]byte(jsonData), &event)
+	if err != nil {
+		t.Fatalf("Failed to unmarshal JSON: %v", err)
+	}
+
+	if event.Actor.ID != "U012345ABCD" {
+		t.Errorf("Expected Actor.ID to be 'U012345ABCD', got '%s'", event.Actor.ID)
+	}
+	if event.ChannelID != "C0123ABCDEF" {
+		t.Errorf("Expected ChannelID to be 'C0123ABCDEF', got '%s'", event.ChannelID)
+	}
+	if len(event.TargetUsers) != 1 || event.TargetUsers[0].Email != "user@some-corp.com" {
+		t.Errorf("Expected one TargetUser with Email 'user@some-corp.com', got '%v'", event.TargetUsers)
+	}
+	if len(event.TeamsInChannel) != 2 || event.TeamsInChannel[1].Name != "another_enterprise" {
+		t.Errorf("Expected second team to have name 'another_enterprise', got '%v'", event.TeamsInChannel)
+	}
+}
diff --git a/slackevents/parsers_test.go b/slackevents/parsers_test.go
index 821d50f..2e2b634 100644
--- a/slackevents/parsers_test.go
+++ b/slackevents/parsers_test.go
@@ -73,6 +73,33 @@ func TestParseURLVerificationEvent(t *testing.T) {
 	}
 }
 
+func TestParseAppRateLimitedEvent(t *testing.T) {
+	event := `
+		{
+			"token": "fake-token",
+			"team_id": "T123ABC456",
+			"minute_rate_limited": 1518467820,
+			"api_app_id": "A123ABC456",
+			"type": "app_rate_limited"
+		}
+	`
+	msg, e := ParseEvent(json.RawMessage(event), OptionVerifyToken(&TokenComparator{"fake-token"}))
+	if e != nil {
+		fmt.Println(e)
+		t.Fail()
+	}
+	switch ev := msg.Data.(type) {
+	case *EventsAPIAppRateLimited:
+		{
+		}
+	default:
+		{
+			fmt.Println(ev)
+			t.Fail()
+		}
+	}
+}
+
 func TestThatOuterCallbackEventHasInnerEvent(t *testing.T) {
 	eventsAPIRawCallbackEvent := `
 			{
diff --git a/slacktest/handlers.go b/slacktest/handlers.go
index b4a0227..22c1680 100644
--- a/slacktest/handlers.go
+++ b/slacktest/handlers.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
@@ -46,7 +46,7 @@ type GroupConversationResponse struct {
 }
 
 func (sts *Server) conversationsInfoHandler(w http.ResponseWriter, r *http.Request) {
-	data, err := ioutil.ReadAll(r.Body)
+	data, err := io.ReadAll(r.Body)
 	if err != nil {
 		msg := fmt.Sprintf("error reading body: %s", err.Error())
 		log.Printf(msg)
@@ -126,7 +126,7 @@ func reactionAddHandler(w http.ResponseWriter, _ *http.Request) {
 // handle chat.postMessage
 func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) {
 	serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string)
-	data, err := ioutil.ReadAll(r.Body)
+	data, err := io.ReadAll(r.Body)
 	if err != nil {
 		msg := fmt.Sprintf("error reading body: %s", err.Error())
 		log.Printf(msg)
@@ -218,7 +218,7 @@ func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) {
 
 // RTMConnectHandler generates a valid connection
 func RTMConnectHandler(w http.ResponseWriter, r *http.Request) {
-	_, err := ioutil.ReadAll(r.Body)
+	_, err := io.ReadAll(r.Body)
 	if err != nil {
 		msg := fmt.Sprintf("Error reading body: %s", err.Error())
 		log.Printf(msg)
@@ -248,7 +248,7 @@ func RTMConnectHandler(w http.ResponseWriter, r *http.Request) {
 }
 
 func rtmStartHandler(w http.ResponseWriter, r *http.Request) {
-	_, err := ioutil.ReadAll(r.Body)
+	_, err := io.ReadAll(r.Body)
 	if err != nil {
 		msg := fmt.Sprintf("Error reading body: %s", err.Error())
 		log.Printf(msg)
diff --git a/users_test.go b/users_test.go
index 5ba9159..eedfd2d 100644
--- a/users_test.go
+++ b/users_test.go
@@ -8,7 +8,6 @@ import (
 	"image/draw"
 	"image/png"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"reflect"
@@ -556,7 +555,7 @@ func setUserPhotoHandler(wantBytes []byte, wantParams UserSetPhotoParams) http.H
 			httpTestErrReply(w, true, fmt.Sprintf("failed to open uploaded file: %+v", err))
 			return
 		}
-		gotBytes, err := ioutil.ReadAll(file)
+		gotBytes, err := io.ReadAll(file)
 		if err != nil {
 			httpTestErrReply(w, true, fmt.Sprintf("failed to read uploaded file: %+v", err))
 			return
@@ -577,7 +576,7 @@ func createUserPhoto(t *testing.T) (*os.File, []byte, func()) {
 	photo := image.NewRGBA(image.Rect(0, 0, 64, 64))
 	draw.Draw(photo, photo.Bounds(), image.Black, image.ZP, draw.Src)
 
-	f, err := ioutil.TempFile(os.TempDir(), "profile.png")
+	f, err := os.CreateTemp(os.TempDir(), "profile.png")
 	if err != nil {
 		t.Fatalf("failed to create test photo: %+v\n", err)
 	}