Skip to content

Commit b2fd181

Browse files
[CHA-14] Added support for pinning, archiving channels and partial member update (#293)
* feat: added support for pinning / unpinning channels * feat: added support for archiving / unarchiving channels * feat: added support for MemberPartialUpdate * feat: added Go 1.23 * fix: update channel_test.go Co-authored-by: Antti Kupila <[email protected]> * fix: update channel_test.go Co-authored-by: Antti Kupila <[email protected]> --------- Co-authored-by: Antti Kupila <[email protected]>
1 parent de807e7 commit b2fd181

File tree

3 files changed

+270
-5
lines changed

3 files changed

+270
-5
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
max-parallel: 3
1818
fail-fast: false
1919
matrix:
20-
goVer: ['1.18', '1.19', '1.20', '1.21', '1.22']
20+
goVer: ['1.18', '1.19', '1.20', '1.21', '1.22', '1.23']
2121
steps:
2222
- uses: actions/checkout@v4
2323

channel.go

+131-4
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,48 @@ type ChannelMember struct {
2323
User *User `json:"user,omitempty"`
2424
IsModerator bool `json:"is_moderator,omitempty"`
2525

26-
Invited bool `json:"invited,omitempty"`
27-
InviteAcceptedAt *time.Time `json:"invite_accepted_at,omitempty"`
28-
InviteRejectedAt *time.Time `json:"invite_rejected_at,omitempty"`
29-
Role string `json:"role,omitempty"`
26+
Invited bool `json:"invited,omitempty"`
27+
InviteAcceptedAt *time.Time `json:"invite_accepted_at,omitempty"`
28+
InviteRejectedAt *time.Time `json:"invite_rejected_at,omitempty"`
29+
Status string `json:"status,omitempty"`
30+
Role string `json:"role,omitempty"`
31+
ChannelRole string `json:"channel_role"`
32+
Banned bool `json:"banned"`
33+
BanExpires *time.Time `json:"ban_expires,omitempty"`
34+
ShadowBanned bool `json:"shadow_banned"`
35+
ArchivedAt *time.Time `json:"archived_at,omitempty"`
36+
PinnedAt *time.Time `json:"pinned_at,omitempty"`
37+
NotificationsMuted bool `json:"notifications_muted"`
38+
39+
ExtraData map[string]interface{} `json:"-"`
3040

3141
CreatedAt time.Time `json:"created_at,omitempty"`
3242
UpdatedAt time.Time `json:"updated_at,omitempty"`
3343
}
3444

45+
type channelMemberForJSON ChannelMember
46+
47+
// UnmarshalJSON implements json.Unmarshaler.
48+
func (m *ChannelMember) UnmarshalJSON(data []byte) error {
49+
var m2 channelMemberForJSON
50+
if err := json.Unmarshal(data, &m2); err != nil {
51+
return err
52+
}
53+
*m = ChannelMember(m2)
54+
55+
if err := json.Unmarshal(data, &m.ExtraData); err != nil {
56+
return err
57+
}
58+
59+
removeFromMap(m.ExtraData, *m)
60+
return nil
61+
}
62+
63+
// MarshalJSON implements json.Marshaler.
64+
func (m ChannelMember) MarshalJSON() ([]byte, error) {
65+
return addToMapAndMarshal(m.ExtraData, channelMemberForJSON(m))
66+
}
67+
3568
type Channel struct {
3669
ID string `json:"id"`
3770
Type string `json:"type"`
@@ -855,3 +888,97 @@ func (ch *Channel) Unmute(ctx context.Context, userID string) (*Response, error)
855888
err := ch.client.makeRequest(ctx, http.MethodPost, "moderation/unmute/channel", nil, data, &resp)
856889
return &resp, err
857890
}
891+
892+
type ChannelMemberResponse struct {
893+
ChannelMember ChannelMember `json:"channel_member"`
894+
Response
895+
}
896+
897+
// Pin pins the channel for the user.
898+
func (ch *Channel) Pin(ctx context.Context, userID string) (*ChannelMemberResponse, error) {
899+
if userID == "" {
900+
return nil, errors.New("user ID must be not empty")
901+
}
902+
903+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID))
904+
905+
data := map[string]interface{}{
906+
"set": map[string]interface{}{
907+
"pinned": true,
908+
},
909+
}
910+
911+
resp := &ChannelMemberResponse{}
912+
err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp)
913+
return resp, err
914+
}
915+
916+
// Unpin unpins the channel for the user.
917+
func (ch *Channel) Unpin(ctx context.Context, userID string) (*ChannelMemberResponse, error) {
918+
if userID == "" {
919+
return nil, errors.New("user ID must be not empty")
920+
}
921+
922+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID))
923+
924+
data := map[string]interface{}{
925+
"set": map[string]interface{}{
926+
"pinned": false,
927+
},
928+
}
929+
930+
resp := &ChannelMemberResponse{}
931+
err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp)
932+
return resp, err
933+
}
934+
935+
// Archive archives the channel for the user.
936+
func (ch *Channel) Archive(ctx context.Context, userID string) (*ChannelMemberResponse, error) {
937+
if userID == "" {
938+
return nil, errors.New("user ID must be not empty")
939+
}
940+
941+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID))
942+
943+
data := map[string]interface{}{
944+
"set": map[string]interface{}{
945+
"archived": true,
946+
},
947+
}
948+
949+
resp := &ChannelMemberResponse{}
950+
err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp)
951+
return resp, err
952+
}
953+
954+
// Unarchive unarchives the channel for the user.
955+
func (ch *Channel) Unarchive(ctx context.Context, userID string) (*ChannelMemberResponse, error) {
956+
if userID == "" {
957+
return nil, errors.New("user ID must be not empty")
958+
}
959+
960+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID))
961+
962+
data := map[string]interface{}{
963+
"set": map[string]interface{}{
964+
"archived": false,
965+
},
966+
}
967+
968+
resp := &ChannelMemberResponse{}
969+
err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, data, resp)
970+
return resp, err
971+
}
972+
973+
// PartialUpdateMember set and unset specific fields when it is necessary to retain additional custom data fields on the object. AKA a patch style update.
974+
func (ch *Channel) PartialUpdateMember(ctx context.Context, userID string, update PartialUpdate) (*ChannelMemberResponse, error) {
975+
if userID == "" {
976+
return nil, errors.New("user ID must be not empty")
977+
}
978+
979+
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "member", url.PathEscape(userID))
980+
981+
resp := &ChannelMemberResponse{}
982+
err := ch.client.makeRequest(ctx, http.MethodPatch, p, nil, update, resp)
983+
return resp, err
984+
}

channel_test.go

+138
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path"
88
"testing"
9+
"time"
910

1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
@@ -492,6 +493,41 @@ func TestChannel_PartialUpdate(t *testing.T) {
492493
require.Nil(t, ch.ExtraData["age"])
493494
}
494495

496+
func TestChannel_MemberPartialUpdate(t *testing.T) {
497+
c := initClient(t)
498+
users := randomUsers(t, c, 5)
499+
ctx := context.Background()
500+
501+
members := make([]string, 0, len(users))
502+
for i := range users {
503+
members = append(members, users[i].ID)
504+
}
505+
506+
req := &ChannelRequest{Members: members}
507+
resp, err := c.CreateChannel(ctx, "team", randomString(12), randomUser(t, c).ID, req)
508+
require.NoError(t, err)
509+
510+
ch := resp.Channel
511+
member, err := ch.PartialUpdateMember(ctx, members[0], PartialUpdate{
512+
Set: map[string]interface{}{
513+
"color": "red",
514+
},
515+
Unset: []string{"age"},
516+
})
517+
require.NoError(t, err)
518+
require.Equal(t, "red", member.ChannelMember.ExtraData["color"])
519+
520+
member, err = ch.PartialUpdateMember(ctx, members[0], PartialUpdate{
521+
Set: map[string]interface{}{
522+
"age": "18",
523+
},
524+
Unset: []string{"color"},
525+
})
526+
require.NoError(t, err)
527+
require.Equal(t, "18", member.ChannelMember.ExtraData["age"])
528+
require.Nil(t, member.ChannelMember.ExtraData["color"])
529+
}
530+
495531
func TestChannel_SendFile(t *testing.T) {
496532
c := initClient(t)
497533
ch := initChannel(t, c)
@@ -647,6 +683,108 @@ func TestChannel_Mute_Unmute(t *testing.T) {
647683
require.Len(t, queryChannResp.Channels, 1)
648684
}
649685

686+
func TestChannel_Pin(t *testing.T) {
687+
c := initClient(t)
688+
ctx := context.Background()
689+
users := randomUsers(t, c, 5)
690+
691+
members := make([]string, 0, len(users))
692+
for i := range users {
693+
members = append(members, users[i].ID)
694+
}
695+
ch := initChannel(t, c, members...)
696+
697+
//pin the channel
698+
now := time.Now()
699+
member, err := ch.Pin(ctx, users[0].ID)
700+
require.NoError(t, err, "pin channel")
701+
require.NotNil(t, member.ChannelMember.PinnedAt)
702+
require.GreaterOrEqual(t, member.ChannelMember.PinnedAt.Unix(), now.Unix())
703+
704+
// query for pinned the channel
705+
queryChannResp, err := c.QueryChannels(ctx, &QueryOption{
706+
UserID: users[0].ID,
707+
Filter: map[string]interface{}{
708+
"pinned": true,
709+
"cid": ch.CID,
710+
},
711+
})
712+
713+
channels := queryChannResp.Channels
714+
require.NoError(t, err, "query pinned channel")
715+
require.Len(t, channels, 1)
716+
require.Equal(t, channels[0].CID, ch.CID)
717+
718+
member, err = ch.Unpin(ctx, users[0].ID)
719+
require.NoError(t, err, "unpin channel")
720+
require.Nil(t, member.ChannelMember.PinnedAt)
721+
722+
// query for pinned the channel
723+
queryChannResp, err = c.QueryChannels(ctx, &QueryOption{
724+
UserID: users[0].ID,
725+
Filter: map[string]interface{}{
726+
"pinned": false,
727+
"cid": ch.CID,
728+
},
729+
})
730+
731+
channels = queryChannResp.Channels
732+
require.NoError(t, err, "query pinned channel")
733+
require.Len(t, channels, 1)
734+
require.Equal(t, channels[0].CID, ch.CID)
735+
}
736+
737+
func TestChannel_Archive(t *testing.T) {
738+
c := initClient(t)
739+
ctx := context.Background()
740+
users := randomUsers(t, c, 5)
741+
742+
members := make([]string, 0, len(users))
743+
for i := range users {
744+
members = append(members, users[i].ID)
745+
}
746+
ch := initChannel(t, c, members...)
747+
748+
//archive the channel
749+
now := time.Now()
750+
member, err := ch.Archive(ctx, users[0].ID)
751+
require.NoError(t, err, "archive channel")
752+
require.NotNil(t, member.ChannelMember.ArchivedAt)
753+
require.GreaterOrEqual(t, member.ChannelMember.ArchivedAt.Unix(), now.Unix())
754+
755+
// query for pinned the channel
756+
queryChannResp, err := c.QueryChannels(ctx, &QueryOption{
757+
UserID: users[0].ID,
758+
Filter: map[string]interface{}{
759+
"archived": true,
760+
"cid": ch.CID,
761+
},
762+
})
763+
764+
channels := queryChannResp.Channels
765+
require.NoError(t, err, "query archived channel")
766+
require.Len(t, channels, 1)
767+
require.Equal(t, channels[0].CID, ch.CID)
768+
769+
member, err = ch.Unarchive(ctx, users[0].ID)
770+
require.NoError(t, err, "unarchive channel")
771+
require.Nil(t, member.ChannelMember.ArchivedAt)
772+
773+
// query for the archived channel
774+
queryChannResp, err = c.QueryChannels(ctx, &QueryOption{
775+
UserID: users[0].ID,
776+
Filter: map[string]interface{}{
777+
"archived": false,
778+
"cid": ch.CID,
779+
},
780+
})
781+
782+
channels = queryChannResp.Channels
783+
require.NoError(t, err, "query archived channel")
784+
require.Len(t, channels, 1)
785+
require.Equal(t, channels[0].CID, ch.CID)
786+
}
787+
650788
func ExampleChannel_Update() {
651789
client := &Client{}
652790
ctx := context.Background()

0 commit comments

Comments
 (0)