Skip to content

Commit 9e3d20a

Browse files
committed
feat(rtm): add sh_room and channel_updated event mappings
- `SHRoomJoinEvent` - `SHRoomLeaveEvent` - `SHRoomUpdateEvent` - `ChannelUpdatedEvent` Closes #858.
1 parent bfd53c6 commit 9e3d20a

5 files changed

Lines changed: 331 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6161
events** — these events are now mapped in `EventMapping` with dedicated structs
6262
(`UserStatusChangedEvent`, `UserHuddleChangedEvent`, `UserProfileChangedEvent`).
6363
Previously they triggered `UnmarshallingErrorEvent`. ([#1541])
64+
- **RTM support for `sh_room_join`, `sh_room_leave`, `sh_room_update`, `channel_updated`
65+
events** — Slack Call/Huddle room events and channel property updates are now mapped with
66+
dedicated structs (`SHRoomJoinEvent`, `SHRoomLeaveEvent`, `SHRoomUpdateEvent`,
67+
`ChannelUpdatedEvent`). ([#858])
6468
- **`CacheTS` and `EventTS` fields on `UserChangeEvent`** — these fields were sent by Slack
6569
but silently dropped during unmarshalling.
6670
- **`workflows.featured` API support** — add, list, remove, and set featured workflows on
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// This example connects via RTM and prints Slack Call/Huddle room events
2+
// (sh_room_join, sh_room_leave). Start a call or huddle in a channel where
3+
// the bot is present to see the events.
4+
//
5+
// To run:
6+
//
7+
// export SLACK_BOT_TOKEN=xoxb-...
8+
// go run examples/rtm_call_events/rtm_call_events.go
9+
package main
10+
11+
import (
12+
"fmt"
13+
"log"
14+
"os"
15+
16+
"github.com/slack-go/slack"
17+
)
18+
19+
func main() {
20+
token := os.Getenv("SLACK_BOT_TOKEN")
21+
if token == "" {
22+
fmt.Fprintln(os.Stderr, "SLACK_BOT_TOKEN environment variable is required")
23+
os.Exit(1)
24+
}
25+
26+
api := slack.New(
27+
token,
28+
slack.OptionDebug(true),
29+
slack.OptionLog(log.New(os.Stdout, "rtm: ", log.Lshortfile|log.LstdFlags)),
30+
)
31+
32+
rtm := api.NewRTM()
33+
go rtm.ManageConnection()
34+
35+
fmt.Println("Listening for call/huddle events... start a call in a channel where this bot is present.")
36+
37+
for msg := range rtm.IncomingEvents {
38+
switch ev := msg.Data.(type) {
39+
case *slack.ConnectedEvent:
40+
fmt.Printf("Connected as %s\n", ev.Info.User.Name)
41+
42+
case *slack.SHRoomJoinEvent:
43+
fmt.Printf("User %s joined call in room %s (channels: %v, participants: %v)\n",
44+
ev.User, ev.Room.ID, ev.Room.Channels, ev.Room.Participants)
45+
46+
case *slack.SHRoomLeaveEvent:
47+
fmt.Printf("User %s left call in room %s (remaining: %v)\n",
48+
ev.User, ev.Room.ID, ev.Room.Participants)
49+
50+
case *slack.SHRoomUpdateEvent:
51+
name := "<unnamed>"
52+
if ev.Room.Name != nil {
53+
name = *ev.Room.Name
54+
}
55+
fmt.Printf("Room %s updated: %q (family: %s, participants: %v)\n",
56+
ev.Room.ID, name, ev.Room.CallFamily, ev.Room.Participants)
57+
58+
case *slack.RTMError:
59+
fmt.Printf("RTM Error: %s\n", ev.Error())
60+
61+
case *slack.InvalidAuthEvent:
62+
fmt.Fprintln(os.Stderr, "Invalid credentials")
63+
return
64+
}
65+
}
66+
}

websocket_managed_conn.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,4 +611,10 @@ var EventMapping = map[string]interface{}{
611611

612612
"desktop_notification": DesktopNotificationEvent{},
613613
"mobile_in_app_notification": MobileInAppNotificationEvent{},
614+
615+
"channel_updated": ChannelUpdatedEvent{},
616+
617+
"sh_room_join": SHRoomJoinEvent{},
618+
"sh_room_leave": SHRoomLeaveEvent{},
619+
"sh_room_update": SHRoomUpdateEvent{},
614620
}

websocket_misc.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,91 @@ type MemberLeftChannelEvent struct {
165165
ChannelType string `json:"channel_type"`
166166
Team string `json:"team"`
167167
}
168+
169+
// ChannelUpdatedEvent is fired when a channel's properties are updated (tabs, meeting notes, etc.).
170+
type ChannelUpdatedEvent struct {
171+
Type string `json:"type"`
172+
Updates map[string]any `json:"updates"`
173+
Channel string `json:"channel"`
174+
Channels []string `json:"channels"`
175+
EventTS string `json:"event_ts"`
176+
TS string `json:"ts"`
177+
}
178+
179+
// SHRoomRecording holds recording metadata for a Slack Call/Huddle room.
180+
type SHRoomRecording struct {
181+
CanRecordSummary string `json:"can_record_summary,omitempty"`
182+
}
183+
184+
// SHRoom represents a Slack Huddle/Call room.
185+
type SHRoom struct {
186+
ID string `json:"id"`
187+
Name *string `json:"name"` // nullable in Slack's response
188+
MediaServer string `json:"media_server"`
189+
CreatedBy string `json:"created_by"`
190+
DateStart int64 `json:"date_start"`
191+
DateEnd int64 `json:"date_end"`
192+
Participants []string `json:"participants"`
193+
ParticipantHistory []string `json:"participant_history"`
194+
ParticipantsEvents map[string]map[string]any `json:"participants_events,omitempty"`
195+
ParticipantsCameraOn []string `json:"participants_camera_on"`
196+
ParticipantsCameraOff []string `json:"participants_camera_off"`
197+
ParticipantsScreenshareOn []string `json:"participants_screenshare_on"`
198+
ParticipantsScreenshareOff []string `json:"participants_screenshare_off"`
199+
CanvasThreadTS string `json:"canvas_thread_ts,omitempty"`
200+
ThreadRootTS string `json:"thread_root_ts,omitempty"`
201+
Channels []string `json:"channels"`
202+
IsDMCall bool `json:"is_dm_call"`
203+
WasRejected bool `json:"was_rejected"`
204+
WasMissed bool `json:"was_missed"`
205+
WasAccepted bool `json:"was_accepted"`
206+
HasEnded bool `json:"has_ended"`
207+
BackgroundID string `json:"background_id,omitempty"`
208+
CanvasBackground string `json:"canvas_background,omitempty"`
209+
IsPrewarmed bool `json:"is_prewarmed,omitempty"`
210+
IsScheduled bool `json:"is_scheduled,omitempty"`
211+
Recording *SHRoomRecording `json:"recording,omitempty"`
212+
Locale string `json:"locale,omitempty"`
213+
AttachedFileIDs []string `json:"attached_file_ids,omitempty"`
214+
MediaBackendType string `json:"media_backend_type"`
215+
DisplayID string `json:"display_id,omitempty"`
216+
ExternalUniqueID string `json:"external_unique_id"`
217+
AppID string `json:"app_id"`
218+
CallFamily string `json:"call_family,omitempty"`
219+
HuddleLink string `json:"huddle_link,omitempty"`
220+
}
221+
222+
// SHRoomHuddle holds the huddle-specific metadata on sh_room events.
223+
type SHRoomHuddle struct {
224+
ChannelID string `json:"channel_id"`
225+
}
226+
227+
// SHRoomJoinEvent is fired when a user joins a Slack Call/Huddle room.
228+
type SHRoomJoinEvent struct {
229+
Type string `json:"type"`
230+
Room SHRoom `json:"room"`
231+
User string `json:"user"`
232+
Huddle *SHRoomHuddle `json:"huddle,omitempty"`
233+
EventTS string `json:"event_ts"`
234+
TS string `json:"ts"`
235+
}
236+
237+
// SHRoomLeaveEvent is fired when a user leaves a Slack Call/Huddle room.
238+
type SHRoomLeaveEvent struct {
239+
Type string `json:"type"`
240+
Room SHRoom `json:"room"`
241+
User string `json:"user"`
242+
Huddle *SHRoomHuddle `json:"huddle,omitempty"`
243+
EventTS string `json:"event_ts"`
244+
TS string `json:"ts"`
245+
}
246+
247+
// SHRoomUpdateEvent is fired when a Slack Call/Huddle room is updated.
248+
type SHRoomUpdateEvent struct {
249+
Type string `json:"type"`
250+
Room SHRoom `json:"room"`
251+
User string `json:"user"`
252+
Huddle *SHRoomHuddle `json:"huddle,omitempty"`
253+
EventTS string `json:"event_ts"`
254+
TS string `json:"ts"`
255+
}

websocket_misc_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package slack
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestSHRoomJoinEventUnmarshal(t *testing.T) {
12+
raw := `{
13+
"type": "sh_room_join",
14+
"room": {
15+
"id": "R01XXXBW",
16+
"name": null,
17+
"media_server": "",
18+
"created_by": "U12334",
19+
"date_start": 1607089008,
20+
"date_end": 0,
21+
"participants": ["U12334", "U56789"],
22+
"participant_history": ["U12334", "U56789"],
23+
"participants_camera_on": [],
24+
"participants_camera_off": [],
25+
"participants_screenshare_on": [],
26+
"participants_screenshare_off": [],
27+
"channels": ["C12334"],
28+
"is_dm_call": false,
29+
"was_rejected": false,
30+
"was_missed": false,
31+
"was_accepted": false,
32+
"has_ended": false,
33+
"media_backend_type": "free_willy",
34+
"external_unique_id": "8c92471f-test",
35+
"app_id": "A00"
36+
},
37+
"user": "U12334",
38+
"event_ts": "1607089059.080900",
39+
"ts": "1607089059.080900"
40+
}`
41+
42+
var ev SHRoomJoinEvent
43+
err := json.Unmarshal([]byte(raw), &ev)
44+
require.NoError(t, err)
45+
46+
assert.Equal(t, "sh_room_join", ev.Type)
47+
assert.Equal(t, "U12334", ev.User)
48+
assert.Equal(t, "R01XXXBW", ev.Room.ID)
49+
assert.Nil(t, ev.Room.Name)
50+
assert.Equal(t, "U12334", ev.Room.CreatedBy)
51+
assert.Equal(t, int64(1607089008), ev.Room.DateStart)
52+
assert.Equal(t, []string{"U12334", "U56789"}, ev.Room.Participants)
53+
assert.Equal(t, []string{"C12334"}, ev.Room.Channels)
54+
assert.False(t, ev.Room.IsDMCall)
55+
assert.Equal(t, "free_willy", ev.Room.MediaBackendType)
56+
assert.Equal(t, "1607089059.080900", ev.EventTS)
57+
}
58+
59+
func TestSHRoomLeaveEventUnmarshal(t *testing.T) {
60+
raw := `{
61+
"type": "sh_room_leave",
62+
"room": {
63+
"id": "R01XXXBW",
64+
"name": null,
65+
"media_server": "",
66+
"created_by": "U12334",
67+
"date_start": 1607089008,
68+
"date_end": 0,
69+
"participants": ["U12334"],
70+
"participant_history": ["U12334", "U56789"],
71+
"participants_camera_on": [],
72+
"participants_camera_off": [],
73+
"participants_screenshare_on": [],
74+
"participants_screenshare_off": [],
75+
"channels": ["C12334"],
76+
"is_dm_call": false,
77+
"was_rejected": false,
78+
"was_missed": false,
79+
"was_accepted": false,
80+
"has_ended": false,
81+
"media_backend_type": "free_willy",
82+
"external_unique_id": "8c92471f-test",
83+
"app_id": "A00"
84+
},
85+
"user": "U56789",
86+
"event_ts": "1607091086.081500",
87+
"ts": "1607091086.081500"
88+
}`
89+
90+
var ev SHRoomLeaveEvent
91+
err := json.Unmarshal([]byte(raw), &ev)
92+
require.NoError(t, err)
93+
94+
assert.Equal(t, "sh_room_leave", ev.Type)
95+
assert.Equal(t, "U56789", ev.User)
96+
assert.Equal(t, "R01XXXBW", ev.Room.ID)
97+
assert.Equal(t, []string{"U12334"}, ev.Room.Participants)
98+
assert.Equal(t, "1607091086.081500", ev.EventTS)
99+
}
100+
101+
func TestSHRoomUpdateEventUnmarshal(t *testing.T) {
102+
raw := `{
103+
"type": "sh_room_update",
104+
"room": {
105+
"id": "R0AQSG0Q859",
106+
"name": "A sort of topic",
107+
"media_server": "",
108+
"created_by": "U031L4VDD",
109+
"date_start": 1775402709,
110+
"date_end": 0,
111+
"participants": ["U031L4VDD"],
112+
"participant_history": ["U031L4VDD"],
113+
"participants_events": {"U031L4VDD": {"joined": true, "camera_on": false}},
114+
"participants_camera_on": [],
115+
"participants_camera_off": [],
116+
"participants_screenshare_on": [],
117+
"participants_screenshare_off": [],
118+
"canvas_thread_ts": "1775402709.576349",
119+
"thread_root_ts": "1775402709.576349",
120+
"channels": ["C031L4VDP"],
121+
"is_dm_call": false,
122+
"was_rejected": false,
123+
"was_missed": false,
124+
"was_accepted": false,
125+
"has_ended": false,
126+
"background_id": "GRADIENT_02",
127+
"canvas_background": "GRADIENT_02",
128+
"is_prewarmed": true,
129+
"is_scheduled": false,
130+
"recording": {"can_record_summary": "unavailable"},
131+
"locale": "en-US",
132+
"attached_file_ids": [],
133+
"media_backend_type": "free_willy",
134+
"display_id": "",
135+
"external_unique_id": "755e016f-aae1-4d4f-abcc-952b1b872713",
136+
"app_id": "A00",
137+
"call_family": "huddle",
138+
"huddle_link": "https://app.slack.com/huddle/T031L4VD9/C031L4VDP"
139+
},
140+
"user": "U031L4VDD",
141+
"huddle": {"channel_id": "C031L4VDP"},
142+
"event_ts": "1775402785.000200",
143+
"ts": "1775402785.000200"
144+
}`
145+
146+
var ev SHRoomUpdateEvent
147+
err := json.Unmarshal([]byte(raw), &ev)
148+
require.NoError(t, err)
149+
150+
assert.Equal(t, "sh_room_update", ev.Type)
151+
assert.Equal(t, "U031L4VDD", ev.User)
152+
assert.Equal(t, "R0AQSG0Q859", ev.Room.ID)
153+
assert.NotNil(t, ev.Room.Name)
154+
assert.Equal(t, "A sort of topic", *ev.Room.Name)
155+
assert.Equal(t, "huddle", ev.Room.CallFamily)
156+
assert.True(t, ev.Room.IsPrewarmed)
157+
assert.Equal(t, "1775402709.576349", ev.Room.CanvasThreadTS)
158+
assert.Equal(t, "GRADIENT_02", ev.Room.BackgroundID)
159+
assert.Equal(t, "en-US", ev.Room.Locale)
160+
assert.NotNil(t, ev.Room.Recording)
161+
assert.Equal(t, "unavailable", ev.Room.Recording.CanRecordSummary)
162+
assert.Equal(t, "https://app.slack.com/huddle/T031L4VD9/C031L4VDP", ev.Room.HuddleLink)
163+
assert.NotNil(t, ev.Huddle)
164+
assert.Equal(t, "C031L4VDP", ev.Huddle.ChannelID)
165+
assert.NotNil(t, ev.Room.ParticipantsEvents)
166+
assert.Contains(t, ev.Room.ParticipantsEvents, "U031L4VDD")
167+
}

0 commit comments

Comments
 (0)