From 90e3cd0f7c58d5b49bb7e1a50120b23a8b3e86d2 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Fri, 16 Oct 2026 11:41:31 -0400 Subject: [PATCH 1/2] feat: add the user_connection_status_requested event model Slack sends this event when it has no connection status for a user, and apps answer with apps.user.connection.update. The model lets Bolt apps register app.event(UserConnectionStatusRequestedEvent.class, ...). --- .../UserConnectionStatusRequestedEvent.java | 18 ++++++++++ ...serConnectionStatusRequestedEventTest.java | 36 +++++++++++++++++++ .../UserConnectionStatusRequestedHandler.java | 13 +++++++ .../UserConnectionStatusRequestedPayload.java | 25 +++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java create mode 100644 slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java create mode 100644 slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionStatusRequestedHandler.java create mode 100644 slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionStatusRequestedPayload.java diff --git a/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java b/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java new file mode 100644 index 000000000..34c401136 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java @@ -0,0 +1,18 @@ +package com.slack.api.model.event; + +import lombok.Data; + +/** + * Slack sends the user_connection_status_requested event when it has no connection status for a user. + * Answer it with the apps.user.connection.update API method. + *

+ * https://docs.slack.dev/reference/events/user_connection_status_requested + */ +@Data +public class UserConnectionStatusRequestedEvent implements Event { + + public static final String TYPE_NAME = "user_connection_status_requested"; + + private final String type = TYPE_NAME; + private String user; +} diff --git a/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java b/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java new file mode 100644 index 000000000..3c487fe3c --- /dev/null +++ b/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java @@ -0,0 +1,36 @@ +package test_locally.api.model.event; + +import com.google.gson.Gson; +import com.slack.api.model.event.UserConnectionStatusRequestedEvent; +import org.junit.Test; +import test_locally.unit.GsonFactory; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class UserConnectionStatusRequestedEventTest { + + @Test + public void typeName() { + assertThat(UserConnectionStatusRequestedEvent.TYPE_NAME, is("user_connection_status_requested")); + } + + @Test + public void deserialize() { + String json = "{\"type\":\"user_connection_status_requested\",\"user\":\"U123ABC456\"}"; + UserConnectionStatusRequestedEvent event = + GsonFactory.createSnakeCase().fromJson(json, UserConnectionStatusRequestedEvent.class); + assertThat(event.getType(), is("user_connection_status_requested")); + assertThat(event.getUser(), is("U123ABC456")); + } + + @Test + public void serialize() { + Gson gson = GsonFactory.createSnakeCase(); + UserConnectionStatusRequestedEvent event = new UserConnectionStatusRequestedEvent(); + event.setUser("U123ABC456"); + String generatedJson = gson.toJson(event); + String expectedJson = "{\"type\":\"user_connection_status_requested\",\"user\":\"U123ABC456\"}"; + assertThat(generatedJson, is(expectedJson)); + } +} diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionStatusRequestedHandler.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionStatusRequestedHandler.java new file mode 100644 index 000000000..54792dbba --- /dev/null +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionStatusRequestedHandler.java @@ -0,0 +1,13 @@ +package com.slack.api.app_backend.events.handler; + +import com.slack.api.app_backend.events.EventHandler; +import com.slack.api.app_backend.events.payload.UserConnectionStatusRequestedPayload; +import com.slack.api.model.event.UserConnectionStatusRequestedEvent; + +public abstract class UserConnectionStatusRequestedHandler extends EventHandler { + + @Override + public String getEventType() { + return UserConnectionStatusRequestedEvent.TYPE_NAME; + } +} diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionStatusRequestedPayload.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionStatusRequestedPayload.java new file mode 100644 index 000000000..ad3cd8cae --- /dev/null +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionStatusRequestedPayload.java @@ -0,0 +1,25 @@ +package com.slack.api.app_backend.events.payload; + +import com.slack.api.model.event.UserConnectionStatusRequestedEvent; +import lombok.Data; + +import java.util.List; + +@Data +public class UserConnectionStatusRequestedPayload implements EventsApiPayload { + + private String token; + private String enterpriseId; + private String teamId; + private String apiAppId; + private String type; + private List authedUsers; + private List authedTeams; + private List authorizations; + private boolean isExtSharedChannel; + private String eventId; + private Integer eventTime; + private String eventContext; + + private UserConnectionStatusRequestedEvent event; +} From cacc89546a2aeb2644ed78ff17fcb145189d2812 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 22 Oct 2026 16:44:48 -0400 Subject: [PATCH 2/2] feat: add the user_connection event model Add UserConnectionEvent with its handler and payload, so Bolt apps can register app.event(UserConnectionEvent.class, ...) for both the connect and disconnect subtypes. The subtype stays unset by default so Bolt keys the listener on the type alone, like EmojiChangedEvent. Also add event_ts to UserConnectionStatusRequestedEvent to match node-slack-sdk#2783. Co-Authored-By: Claude --- .../api/model/event/UserConnectionEvent.java | 23 ++++++++ .../UserConnectionStatusRequestedEvent.java | 1 + .../model/event/UserConnectionEventTest.java | 54 +++++++++++++++++++ ...serConnectionStatusRequestedEventTest.java | 3 +- .../events/handler/UserConnectionHandler.java | 13 +++++ .../events/payload/UserConnectionPayload.java | 25 +++++++++ 6 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionEvent.java create mode 100644 slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionEventTest.java create mode 100644 slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionHandler.java create mode 100644 slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionPayload.java diff --git a/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionEvent.java b/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionEvent.java new file mode 100644 index 000000000..41e8bd20a --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionEvent.java @@ -0,0 +1,23 @@ +package com.slack.api.model.event; + +import lombok.Data; + +/** + * Slack sends the user_connection event when a user clicks Connect or Manage on an app with user connections. + * The subtype is either connect or disconnect. For connect, open the connection modal with the + * apps.user.connection.requestAuth API method and the trigger_id. + *

+ * https://docs.slack.dev/reference/events/user_connection + */ +@Data +public class UserConnectionEvent implements Event { + + public static final String TYPE_NAME = "user_connection"; + + private final String type = TYPE_NAME; + private String subtype; + private String user; + private String triggerId; + private String enterpriseId; + private String eventTs; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java b/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java index 34c401136..22938c54c 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java +++ b/slack-api-model/src/main/java/com/slack/api/model/event/UserConnectionStatusRequestedEvent.java @@ -15,4 +15,5 @@ public class UserConnectionStatusRequestedEvent implements Event { private final String type = TYPE_NAME; private String user; + private String eventTs; } diff --git a/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionEventTest.java b/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionEventTest.java new file mode 100644 index 000000000..a0e66ae31 --- /dev/null +++ b/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionEventTest.java @@ -0,0 +1,54 @@ +package test_locally.api.model.event; + +import com.google.gson.Gson; +import com.slack.api.model.event.UserConnectionEvent; +import org.junit.Test; +import test_locally.unit.GsonFactory; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +public class UserConnectionEventTest { + + @Test + public void typeName() { + assertThat(UserConnectionEvent.TYPE_NAME, is("user_connection")); + } + + @Test + public void deserializeConnect() { + String json = "{\"type\":\"user_connection\",\"subtype\":\"connect\",\"user\":\"U123ABC456\"," + + "\"trigger_id\":\"1293638028594.1249184885746.0d121a0e01d2e7a795ecc7a62880a406\"," + + "\"event_ts\":\"1764264284.841251\"}"; + UserConnectionEvent event = GsonFactory.createSnakeCase().fromJson(json, UserConnectionEvent.class); + assertThat(event.getType(), is("user_connection")); + assertThat(event.getSubtype(), is("connect")); + assertThat(event.getUser(), is("U123ABC456")); + assertThat(event.getTriggerId(), is("1293638028594.1249184885746.0d121a0e01d2e7a795ecc7a62880a406")); + assertThat(event.getEnterpriseId(), is(nullValue())); + assertThat(event.getEventTs(), is("1764264284.841251")); + } + + @Test + public void deserializeDisconnect() { + String json = "{\"type\":\"user_connection\",\"subtype\":\"disconnect\",\"user\":\"U123ABC456\"," + + "\"enterprise_id\":\"E123ABC456\",\"event_ts\":\"1764264317.061589\"}"; + UserConnectionEvent event = GsonFactory.createSnakeCase().fromJson(json, UserConnectionEvent.class); + assertThat(event.getSubtype(), is("disconnect")); + assertThat(event.getUser(), is("U123ABC456")); + assertThat(event.getTriggerId(), is(nullValue())); + assertThat(event.getEnterpriseId(), is("E123ABC456")); + assertThat(event.getEventTs(), is("1764264317.061589")); + } + + @Test + public void serialize() { + Gson gson = GsonFactory.createSnakeCase(); + UserConnectionEvent event = new UserConnectionEvent(); + event.setUser("U123ABC456"); + String generatedJson = gson.toJson(event); + String expectedJson = "{\"type\":\"user_connection\",\"user\":\"U123ABC456\"}"; + assertThat(generatedJson, is(expectedJson)); + } +} diff --git a/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java b/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java index 3c487fe3c..6c6e3fec9 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/event/UserConnectionStatusRequestedEventTest.java @@ -17,11 +17,12 @@ public void typeName() { @Test public void deserialize() { - String json = "{\"type\":\"user_connection_status_requested\",\"user\":\"U123ABC456\"}"; + String json = "{\"type\":\"user_connection_status_requested\",\"user\":\"U123ABC456\",\"event_ts\":\"1764264284.841251\"}"; UserConnectionStatusRequestedEvent event = GsonFactory.createSnakeCase().fromJson(json, UserConnectionStatusRequestedEvent.class); assertThat(event.getType(), is("user_connection_status_requested")); assertThat(event.getUser(), is("U123ABC456")); + assertThat(event.getEventTs(), is("1764264284.841251")); } @Test diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionHandler.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionHandler.java new file mode 100644 index 000000000..9034facfa --- /dev/null +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/handler/UserConnectionHandler.java @@ -0,0 +1,13 @@ +package com.slack.api.app_backend.events.handler; + +import com.slack.api.app_backend.events.EventHandler; +import com.slack.api.app_backend.events.payload.UserConnectionPayload; +import com.slack.api.model.event.UserConnectionEvent; + +public abstract class UserConnectionHandler extends EventHandler { + + @Override + public String getEventType() { + return UserConnectionEvent.TYPE_NAME; + } +} diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionPayload.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionPayload.java new file mode 100644 index 000000000..cd4bf22c1 --- /dev/null +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/UserConnectionPayload.java @@ -0,0 +1,25 @@ +package com.slack.api.app_backend.events.payload; + +import com.slack.api.model.event.UserConnectionEvent; +import lombok.Data; + +import java.util.List; + +@Data +public class UserConnectionPayload implements EventsApiPayload { + + private String token; + private String enterpriseId; + private String teamId; + private String apiAppId; + private String type; + private List authedUsers; + private List authedTeams; + private List authorizations; + private boolean isExtSharedChannel; + private String eventId; + private Integer eventTime; + private String eventContext; + + private UserConnectionEvent event; +}