Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.slack.api.model.block.element.RichTextElement;
import com.slack.api.model.event.FunctionExecutedEvent;
import com.slack.api.model.event.MessageChangedEvent;
import com.slack.api.model.list.ListRecord;
import com.slack.api.model.list.ListView;

import java.time.Instant;
Expand Down Expand Up @@ -87,6 +88,7 @@ public static void registerTypeAdapters(GsonBuilder builder, boolean failOnUnkno
.registerTypeAdapter(AppWorkflow.StepInputValueElementDefault.class, new GsonAppWorkflowStepInputValueDefaultFactory(failOnUnknownProps))
.registerTypeAdapter(LogsResponse.DetailsChangedValue.class, new GsonAuditLogsDetailsChangedValueFactory(failOnUnknownProps))
.registerTypeAdapter(LogsResponse.UserIDs.class, new GsonAuditLogsDetailsUserIDsFactory(failOnUnknownProps))
.registerTypeAdapter(ListView.Grouping.class, new GsonListViewGroupingFactory(failOnUnknownProps));
.registerTypeAdapter(ListView.Grouping.class, new GsonListViewGroupingFactory(failOnUnknownProps))
.registerTypeAdapter(ListRecord.MessageRef.class, new GsonListRecordMessageRefFactory(failOnUnknownProps));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.slack.api.Slack;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.response.auth.AuthTestResponse;
import com.slack.api.methods.response.chat.ChatGetPermalinkResponse;
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
import com.slack.api.methods.response.slack_lists.SlackListsAccessDeleteResponse;
import com.slack.api.methods.response.slack_lists.SlackListsAccessSetResponse;
import com.slack.api.methods.response.slack_lists.SlackListsCreateResponse;
Expand Down Expand Up @@ -148,6 +150,14 @@ public void fullSlackListsWorkflow() throws IOException, SlackApiException {
.build())
.build();

// A message column so the response echoes back the message-reference shape
// ({value, channel_id, ts, thread_ts?}). A message column cannot be primary.
ListColumn relatedMessageCol = ListColumn.builder()
.key("related_message")
.name("Related Message")
.type("message")
.build();

// create list
SlackListsCreateResponse createResponse = slack.methods().slackListsCreate(r -> r
.token(botToken)
Expand All @@ -159,7 +169,7 @@ public void fullSlackListsWorkflow() throws IOException, SlackApiException {
.build()))
.build()))
.build()))
.schema(Arrays.asList(taskNameCol, dueDateCol, estimateCol, ratingCol, statusCol, assigneeCol)));
.schema(Arrays.asList(taskNameCol, dueDateCol, estimateCol, ratingCol, statusCol, assigneeCol, relatedMessageCol)));

assertThat(createResponse.getError(), is(nullValue()));
assertThat(createResponse.isOk(), is(true));
Expand All @@ -174,9 +184,10 @@ public void fullSlackListsWorkflow() throws IOException, SlackApiException {
keyToId.put(col.getKey(), col.getId());
});
}
String taskNameColId = keyToId.get("task_name");

// set access
String taskNameColId = keyToId.get("task_name");
String relatedMessageColId = keyToId.get("related_message");

// set access
SlackListsAccessSetResponse accessSetResponse = slack.methods().slackListsAccessSet(r -> r
.token(botToken)
.listId(listId)
Expand All @@ -185,6 +196,24 @@ public void fullSlackListsWorkflow() throws IOException, SlackApiException {
assertThat(accessSetResponse.getError(), is(nullValue()));
assertThat(accessSetResponse.isOk(), is(true));

// Post a message and resolve its permalink so the item can reference it in the message
// field. The message field takes an array of permalink URL strings on the request side
// (a MessageRef serializes to its value) and echoes back {value, channel_id, ts,
// thread_ts?} objects on the response side.
ChatPostMessageResponse relatedMessage = slack.methods().chatPostMessage(r -> r
.token(botToken)
.channel(channelId)
.text("Related message for the SlackLists remote test"));
assertThat(relatedMessage.getError(), is(nullValue()));
assertThat(relatedMessage.isOk(), is(true));
ChatGetPermalinkResponse relatedPermalink = slack.methods().chatGetPermalink(r -> r
.token(botToken)
.channel(channelId)
.messageTs(relatedMessage.getTs()));
assertThat(relatedPermalink.getError(), is(nullValue()));
String relatedMessageUrl = relatedPermalink.getPermalink();
assertThat(relatedMessageUrl, is(notNullValue()));

// Build initial fields for item creation
ListRecord.Field field = ListRecord.Field.builder()
.columnId(taskNameColId)
Expand All @@ -196,12 +225,18 @@ public void fullSlackListsWorkflow() throws IOException, SlackApiException {
.build()))
.build()))
.build();
// The message field references the posted message by permalink. On the request the
// MessageRef serializes to the permalink string; the response echoes the full reference.
ListRecord.Field messageField = ListRecord.Field.builder()
.columnId(relatedMessageColId)
.message(Arrays.asList(ListRecord.MessageRef.builder().value(relatedMessageUrl).build()))
.build();

// create an item
SlackListsItemsCreateResponse createItemResponse = slack.methods().slackListsItemsCreate(r -> r
.token(botToken)
.listId(listId)
.initialFields(Arrays.asList(field)));
.initialFields(Arrays.asList(field, messageField)));
assertThat(createItemResponse.getError(), is(nullValue()));
assertThat(createItemResponse.isOk(), is(true));
assertThat(createItemResponse.getItem(), is(notNullValue()));
Expand All @@ -215,6 +250,16 @@ public void fullSlackListsWorkflow() throws IOException, SlackApiException {
assertThat(taskNameField, is(notNullValue()));
assertThat(taskNameField.getText(), is("Test task item"));

// The message field should echo back the reference object shape.
ListRecord.Field echoedMessageField = createItemResponse.getItem().getFields().stream()
.filter(f -> relatedMessageColId.equals(f.getColumnId()))
.findFirst()
.orElse(null);
assertThat(echoedMessageField, is(notNullValue()));
assertThat(echoedMessageField.getMessage(), is(notNullValue()));
assertThat(echoedMessageField.getMessage().isEmpty(), is(false));
assertThat(echoedMessageField.getMessage().get(0).getValue(), is(notNullValue()));

String itemId = createItemResponse.getItem().getId();
assertThat(itemId, is(notNullValue()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.google.gson.annotations.SerializedName;
import com.slack.api.model.File;
import com.slack.api.model.Message;
import com.slack.api.model.block.RichTextBlock;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -11,7 +10,6 @@
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -47,8 +45,12 @@ public static class Field {
private String text;
@SerializedName("rich_text")
private List<RichTextBlock> richText;
private transient List<Message> messages;
private Message message;
// The message field is an array of message references, verified against the live
// API (slackLists.items.list and the conversations.replies/history nested
// list_record path both return List<{value, channel_id, ts, thread_ts?}>).
// EXPERIMENTAL: this replaces the earlier Message-typed modeling (#1590), which
// did not match the actual response shape. See MessageRef.
private List<MessageRef> message;
private List<Double> number;
private List<String> select;
private List<String> date;
Expand All @@ -62,20 +64,27 @@ public static class Field {
private List<Integer> timestamp;
private List<LinkField> link;
private List<ReferenceField> reference;
}

public List<Message> getMessages() {
if (messages == null && message != null) {
return Collections.singletonList(message);
}
return messages;
}

public void setMessages(List<Message> messages) {
this.messages = messages;
if (messages != null && !messages.isEmpty()) {
this.message = messages.get(0);
}
}
/**
* Message field reference for Slack Lists items. The API returns the message field as
* an array of these references. Verified against the live API — every element carries
* value/channel_id/ts, and thread_ts is present only when the referenced message is a
* threaded reply.
* EXPERIMENTAL: introduced to replace the earlier Message-typed modeling.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MessageRef {
// Permalink URL of the referenced message.
private String value;
@SerializedName("channel_id")
private String channelId;
private String ts;
@SerializedName("thread_ts")
private String threadTs;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.slack.api.util.json;

import com.google.gson.*;
import com.slack.api.model.list.ListRecord.MessageRef;

import java.lang.reflect.Type;

/**
* Direction-aware (de)serialization for the Slack Lists message field element.
*
* The field is asymmetric on the wire (verified against the live API):
* <ul>
* <li>Request: an array of message permalink URL <b>strings</b> — e.g. {@code "message": ["https://.../p123"]}.</li>
* <li>Response: an array of message reference <b>objects</b> — {@code {"value","channel_id","ts","thread_ts"?}}.</li>
* </ul>
*
* This adapter lets a single {@code List<MessageRef>} model both directions: it serializes a
* MessageRef to its {@code value} (the permalink string) so requests carry the string array the
* API expects, and it deserializes either an object (normal response) or a bare string (defensive)
* back into a MessageRef.
*/
public class GsonListRecordMessageRefFactory implements JsonDeserializer<MessageRef>, JsonSerializer<MessageRef> {

private final boolean failOnUnknownProperties;

public GsonListRecordMessageRefFactory() {
this(false);
}

public GsonListRecordMessageRefFactory(boolean failOnUnknownProperties) {
this.failOnUnknownProperties = failOnUnknownProperties;
}

@Override
public MessageRef deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
if (json == null || json.isJsonNull()) {
return null;
}
MessageRef ref = new MessageRef();
if (json.isJsonPrimitive()) {
// Request-shaped or degenerate value: a bare permalink string.
ref.setValue(json.getAsString());
return ref;
}
if (json.isJsonObject()) {
JsonObject obj = json.getAsJsonObject();
if (obj.has("value") && !obj.get("value").isJsonNull()) {
ref.setValue(obj.get("value").getAsString());
}
if (obj.has("channel_id") && !obj.get("channel_id").isJsonNull()) {
ref.setChannelId(obj.get("channel_id").getAsString());
}
if (obj.has("ts") && !obj.get("ts").isJsonNull()) {
ref.setTs(obj.get("ts").getAsString());
}
if (obj.has("thread_ts") && !obj.get("thread_ts").isJsonNull()) {
ref.setThreadTs(obj.get("thread_ts").getAsString());
}
return ref;
}
return null;
}

@Override
public JsonElement serialize(MessageRef src, Type typeOfSrc, JsonSerializationContext context) {
// The request side expects the message field as an array of permalink URL strings.
if (src == null || src.getValue() == null) {
return JsonNull.INSTANCE;
}
return new JsonPrimitive(src.getValue());
}
}
Loading
Loading