diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb57f02..36775e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: max-parallel: 3 fail-fast: false matrix: - goVer: ['1.18', '1.19', '1.20', '1.21', '1.22'] + goVer: ['1.18', '1.19', '1.20', '1.21', '1.22', '1.23'] steps: - uses: actions/checkout@v4 diff --git a/channel.go b/channel.go index bb1c5c3..c55eb42 100644 --- a/channel.go +++ b/channel.go @@ -23,15 +23,48 @@ type ChannelMember struct { User *User `json:"user,omitempty"` IsModerator bool `json:"is_moderator,omitempty"` - Invited bool `json:"invited,omitempty"` - InviteAcceptedAt *time.Time `json:"invite_accepted_at,omitempty"` - InviteRejectedAt *time.Time `json:"invite_rejected_at,omitempty"` - Role string `json:"role,omitempty"` + Invited bool `json:"invited,omitempty"` + InviteAcceptedAt *time.Time `json:"invite_accepted_at,omitempty"` + InviteRejectedAt *time.Time `json:"invite_rejected_at,omitempty"` + Status string `json:"status,omitempty"` + Role string `json:"role,omitempty"` + ChannelRole string `json:"channel_role"` + Banned bool `json:"banned"` + BanExpires *time.Time `json:"ban_expires,omitempty"` + ShadowBanned bool `json:"shadow_banned"` + ArchivedAt *time.Time `json:"archived_at,omitempty"` + PinnedAt *time.Time `json:"pinned_at,omitempty"` + NotificationsMuted bool `json:"notifications_muted"` + + ExtraData map[string]interface{} `json:"-"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` } +type channelMemberForJSON ChannelMember + +// UnmarshalJSON implements json.Unmarshaler. +func (m *ChannelMember) UnmarshalJSON(data []byte) error { + var m2 channelMemberForJSON + if err := json.Unmarshal(data, &m2); err != nil { + return err + } + *m = ChannelMember(m2) + + if err := json.Unmarshal(data, &m.ExtraData); err != nil { + return err + } + + removeFromMap(m.ExtraData, *m) + return nil +} + +// MarshalJSON implements json.Marshaler. +func (m ChannelMember) MarshalJSON() ([]byte, error) { + return addToMapAndMarshal(m.ExtraData, channelMemberForJSON(m)) +} + type Channel struct { ID string `json:"id"` Type string `json:"type"` @@ -855,3 +888,97 @@ func (ch *Channel) Unmute(ctx context.Context, userID string) (*Response, error) err := ch.client.makeRequest(ctx, http.MethodPost, "moderation/unmute/channel", nil, data, &resp) return &resp, err } + +type ChannelMemberResponse struct { + ChannelMember ChannelMember `json:"channel_member"` + Response +} + +// Pin pins the channel for the user. +func (ch *Channel) Pin(ctx context.Context, userID string) (*ChannelMemberResponse, error) { + if userID == "" { + return nil, errors.New("user ID must be not empty") + } + + p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID)) + + data := map[string]interface{}{ + "set": map[string]interface{}{ + "pinned": true, + }, + } + + resp := &ChannelMemberResponse{} + err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp) + return resp, err +} + +// Unpin unpins the channel for the user. +func (ch *Channel) Unpin(ctx context.Context, userID string) (*ChannelMemberResponse, error) { + if userID == "" { + return nil, errors.New("user ID must be not empty") + } + + p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID)) + + data := map[string]interface{}{ + "set": map[string]interface{}{ + "pinned": false, + }, + } + + resp := &ChannelMemberResponse{} + err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp) + return resp, err +} + +// Archive archives the channel for the user. +func (ch *Channel) Archive(ctx context.Context, userID string) (*ChannelMemberResponse, error) { + if userID == "" { + return nil, errors.New("user ID must be not empty") + } + + p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID)) + + data := map[string]interface{}{ + "set": map[string]interface{}{ + "archived": true, + }, + } + + resp := &ChannelMemberResponse{} + err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp) + return resp, err +} + +// Unarchive unarchives the channel for the user. +func (ch *Channel) Unarchive(ctx context.Context, userID string) (*ChannelMemberResponse, error) { + if userID == "" { + return nil, errors.New("user ID must be not empty") + } + + p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID)) + + data := map[string]interface{}{ + "set": map[string]interface{}{ + "archived": false, + }, + } + + resp := &ChannelMemberResponse{} + err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp) + return resp, err +} + +// PartialUpdateMember set and unset specific fields when it is necessary to retain additional custom data fields on the object. AKA a patch style update. +func (ch *Channel) PartialUpdateMember(ctx context.Context, userID string, update PartialUpdate) (*ChannelMemberResponse, error) { + if userID == "" { + return nil, errors.New("user ID must be not empty") + } + + p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID)) + + resp := &ChannelMemberResponse{} + err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, update, resp) + return resp, err +} diff --git a/channel_test.go b/channel_test.go index f5750df..3116311 100644 --- a/channel_test.go +++ b/channel_test.go @@ -6,6 +6,7 @@ import ( "os" "path" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -492,6 +493,41 @@ func TestChannel_PartialUpdate(t *testing.T) { require.Nil(t, ch.ExtraData["age"]) } +func TestChannel_MemberPartialUpdate(t *testing.T) { + c := initClient(t) + users := randomUsers(t, c, 5) + ctx := context.Background() + + members := make([]string, 0, len(users)) + for i := range users { + members = append(members, users[i].ID) + } + + req := &ChannelRequest{Members: members} + resp, err := c.CreateChannel(ctx, "team", randomString(12), randomUser(t, c).ID, req) + require.NoError(t, err) + + ch := resp.Channel + member, err := ch.PartialUpdateMember(ctx, members[0], PartialUpdate{ + Set: map[string]interface{}{ + "color": "red", + }, + Unset: []string{"age"}, + }) + require.NoError(t, err) + require.Equal(t, "red", member.ChannelMember.ExtraData["color"]) + + member, err = ch.PartialUpdateMember(ctx, members[0], PartialUpdate{ + Set: map[string]interface{}{ + "age": "18", + }, + Unset: []string{"color"}, + }) + require.NoError(t, err) + require.Equal(t, "18", member.ChannelMember.ExtraData["age"]) + require.Nil(t, member.ChannelMember.ExtraData["color"]) +} + func TestChannel_SendFile(t *testing.T) { c := initClient(t) ch := initChannel(t, c) @@ -647,6 +683,108 @@ func TestChannel_Mute_Unmute(t *testing.T) { require.Len(t, queryChannResp.Channels, 1) } +func TestChannel_Pin(t *testing.T) { + c := initClient(t) + ctx := context.Background() + users := randomUsers(t, c, 5) + + members := make([]string, 0, len(users)) + for i := range users { + members = append(members, users[i].ID) + } + ch := initChannel(t, c, members...) + + //pin the channel + now := time.Now() + member, err := ch.Pin(ctx, users[0].ID) + require.NoError(t, err, "pin channel") + require.NotNil(t, member.ChannelMember.PinnedAt) + require.GreaterOrEqual(t, member.ChannelMember.PinnedAt.Unix(), now.Unix()) + + // query for pinned the channel + queryChannResp, err := c.QueryChannels(ctx, &QueryOption{ + UserID: users[0].ID, + Filter: map[string]interface{}{ + "pinned": true, + "cid": ch.CID, + }, + }) + + channels := queryChannResp.Channels + require.NoError(t, err, "query pinned channel") + require.Len(t, channels, 1) + require.Equal(t, channels[0].CID, ch.CID) + + member, err = ch.Unpin(ctx, users[0].ID) + require.NoError(t, err, "unpin channel") + require.Nil(t, member.ChannelMember.PinnedAt) + + // query for pinned the channel + queryChannResp, err = c.QueryChannels(ctx, &QueryOption{ + UserID: users[0].ID, + Filter: map[string]interface{}{ + "pinned": false, + "cid": ch.CID, + }, + }) + + channels = queryChannResp.Channels + require.NoError(t, err, "query pinned channel") + require.Len(t, channels, 1) + require.Equal(t, channels[0].CID, ch.CID) +} + +func TestChannel_Archive(t *testing.T) { + c := initClient(t) + ctx := context.Background() + users := randomUsers(t, c, 5) + + members := make([]string, 0, len(users)) + for i := range users { + members = append(members, users[i].ID) + } + ch := initChannel(t, c, members...) + + //archive the channel + now := time.Now() + member, err := ch.Archive(ctx, users[0].ID) + require.NoError(t, err, "archive channel") + require.NotNil(t, member.ChannelMember.ArchivedAt) + require.GreaterOrEqual(t, member.ChannelMember.ArchivedAt.Unix(), now.Unix()) + + // query for pinned the channel + queryChannResp, err := c.QueryChannels(ctx, &QueryOption{ + UserID: users[0].ID, + Filter: map[string]interface{}{ + "archived": true, + "cid": ch.CID, + }, + }) + + channels := queryChannResp.Channels + require.NoError(t, err, "query archived channel") + require.Len(t, channels, 1) + require.Equal(t, channels[0].CID, ch.CID) + + member, err = ch.Unarchive(ctx, users[0].ID) + require.NoError(t, err, "unarchive channel") + require.Nil(t, member.ChannelMember.ArchivedAt) + + // query for the archived channel + queryChannResp, err = c.QueryChannels(ctx, &QueryOption{ + UserID: users[0].ID, + Filter: map[string]interface{}{ + "archived": false, + "cid": ch.CID, + }, + }) + + channels = queryChannResp.Channels + require.NoError(t, err, "query archived channel") + require.Len(t, channels, 1) + require.Equal(t, channels[0].CID, ch.CID) +} + func ExampleChannel_Update() { client := &Client{} ctx := context.Background()