diff --git a/ban.go b/ban.go index 0fc0b3b..88c04f6 100644 --- a/ban.go +++ b/ban.go @@ -33,13 +33,24 @@ func (c *Client) BanUser(ctx context.Context, targetID, bannedBy string, options } // UnBanUser removes the ban for targetID. -func (c *Client) UnBanUser(ctx context.Context, targetID string) (*Response, error) { +func (c *Client) UnBanUser(ctx context.Context, targetID string, options ...UnbanOption) (*Response, error) { if targetID == "" { return nil, errors.New("targetID should not be empty") } + opts := &unbanOptions{} + for _, fn := range options { + fn(opts) + } + params := url.Values{} params.Set("target_user_id", targetID) + if opts.RemoveFutureChannelsBan { + params.Set("remove_future_channels_ban", "true") + } + if opts.CreatedBy != "" { + params.Set("created_by", opts.CreatedBy) + } var resp Response err := c.makeRequest(ctx, http.MethodDelete, "moderation/ban", params, nil, &resp) @@ -59,15 +70,23 @@ func (ch *Channel) BanUser(ctx context.Context, targetID, bannedBy string, optio } // UnBanUser removes the ban for targetID from the channel ch. -func (ch *Channel) UnBanUser(ctx context.Context, targetID string) (*Response, error) { +func (ch *Channel) UnBanUser(ctx context.Context, targetID string, options ...UnbanOption) (*Response, error) { if targetID == "" { return nil, errors.New("targetID should not be empty") } + opts := &unbanOptions{} + for _, fn := range options { + fn(opts) + } + params := url.Values{} params.Set("target_user_id", targetID) params.Set("id", ch.ID) params.Set("type", ch.Type) + if opts.RemoveFutureChannelsBan { + params.Set("remove_future_channels_ban", "true") + } var resp Response err := ch.client.makeRequest(ctx, http.MethodDelete, "moderation/ban", params, nil, &resp) @@ -130,15 +149,21 @@ type banOptions struct { Reason string `json:"reason,omitempty"` Expiration int `json:"timeout,omitempty"` - TargetUserID string `json:"target_user_id"` - BannedBy string `json:"user_id"` - Shadow bool `json:"shadow"` + TargetUserID string `json:"target_user_id"` + BannedBy string `json:"user_id"` + Shadow bool `json:"shadow"` + BanFromFutureChannels bool `json:"ban_from_future_channels,omitempty"` // ID and Type of the channel when acting on a channel member. ID string `json:"id"` Type string `json:"type"` } +type unbanOptions struct { + RemoveFutureChannelsBan bool `json:"remove_future_channels_ban,omitempty"` + CreatedBy string `json:"created_by,omitempty"` +} + type BanOption func(*banOptions) func BanWithReason(reason string) func(*banOptions) { @@ -167,3 +192,81 @@ func banFromChannel(_type, id string) func(*banOptions) { opt.ID = id } } + +// BanWithBanFromFutureChannels when set to true, the user will be automatically +// banned from all future channels created by the user who issued the ban. +// Note: This option requires BanWithChannel to also be set. +func BanWithBanFromFutureChannels() func(*banOptions) { + return func(opt *banOptions) { + opt.BanFromFutureChannels = true + } +} + +// BanWithChannel sets the channel context for the ban operation. +// This is required when using BanWithBanFromFutureChannels at the client level. +func BanWithChannel(channelType, channelID string) func(*banOptions) { + return func(opt *banOptions) { + opt.Type = channelType + opt.ID = channelID + } +} + +type UnbanOption func(*unbanOptions) + +// UnbanWithRemoveFutureChannelsBan when set to true, also removes the future +// channel ban, so the user will no longer be auto-banned in new channels. +// Note: This option requires UnbanWithCreatedBy to also be set. +func UnbanWithRemoveFutureChannelsBan() func(*unbanOptions) { + return func(opt *unbanOptions) { + opt.RemoveFutureChannelsBan = true + } +} + +// UnbanWithCreatedBy sets the user ID who originally created the ban. +// This is required when using UnbanWithRemoveFutureChannelsBan. +func UnbanWithCreatedBy(userID string) func(*unbanOptions) { + return func(opt *unbanOptions) { + opt.CreatedBy = userID + } +} + +// FutureChannelBan represents a future channel ban entry. +type FutureChannelBan struct { + User *User `json:"user,omitempty"` + Expires *time.Time `json:"expires,omitempty"` + Reason string `json:"reason,omitempty"` + Shadow bool `json:"shadow,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// QueryFutureChannelBansOptions contains options for querying future channel bans. +type QueryFutureChannelBansOptions struct { + UserID string `json:"user_id,omitempty"` + TargetUserID string `json:"target_user_id,omitempty"` + ExcludeExpiredBans bool `json:"exclude_expired_bans,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` +} + +// QueryFutureChannelBansResponse is the response from QueryFutureChannelBans. +type QueryFutureChannelBansResponse struct { + Bans []*FutureChannelBan `json:"bans"` + Response +} + +// QueryFutureChannelBans queries future channel bans. +// Future channel bans are automatically applied when a user creates a new channel +// or adds a member to an existing channel. +func (c *Client) QueryFutureChannelBans(ctx context.Context, opts *QueryFutureChannelBansOptions) (*QueryFutureChannelBansResponse, error) { + data, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + values := url.Values{} + values.Set("payload", string(data)) + + var resp QueryFutureChannelBansResponse + err = c.makeRequest(ctx, http.MethodGet, "query_future_channel_bans", values, nil, &resp) + return &resp, err +} diff --git a/ban_test.go b/ban_test.go index 30e47d4..3d88d38 100644 --- a/ban_test.go +++ b/ban_test.go @@ -131,6 +131,57 @@ func TestChannelBanUnban(t *testing.T) { require.Empty(t, resp.Bans) } +func TestQueryFutureChannelBans(t *testing.T) { + c := initClient(t) + creator := randomUser(t, c) + target1 := randomUser(t, c) + target2 := randomUser(t, c) + ctx := context.Background() + + // Create a channel to use for future channel bans + ch := initChannel(t, c, creator.ID) + + // Ban both targets from future channels created by creator + _, err := c.BanUser(ctx, target1.ID, creator.ID, BanWithBanFromFutureChannels(), BanWithChannel(ch.Type, ch.ID), BanWithReason("test ban 1")) + require.NoError(t, err) + + _, err = c.BanUser(ctx, target2.ID, creator.ID, BanWithBanFromFutureChannels(), BanWithChannel(ch.Type, ch.ID), BanWithReason("test ban 2")) + require.NoError(t, err) + + // Query all future channel bans by creator + resp, err := c.QueryFutureChannelBans(ctx, &QueryFutureChannelBansOptions{ + UserID: creator.ID, + }) + require.NoError(t, err) + require.GreaterOrEqual(t, len(resp.Bans), 2) + + // Query with target_user_id filter - should only return the specific target + // Note: When filtering by target_user_id, the API doesn't return the user object + // since it's already known from the filter + resp, err = c.QueryFutureChannelBans(ctx, &QueryFutureChannelBansOptions{ + UserID: creator.ID, + TargetUserID: target1.ID, + }) + require.NoError(t, err) + require.Len(t, resp.Bans, 1) + require.Equal(t, "test ban 1", resp.Bans[0].Reason) + + // Query for the other target + resp, err = c.QueryFutureChannelBans(ctx, &QueryFutureChannelBansOptions{ + UserID: creator.ID, + TargetUserID: target2.ID, + }) + require.NoError(t, err) + require.Len(t, resp.Bans, 1) + require.Equal(t, "test ban 2", resp.Bans[0].Reason) + + // Cleanup - unban both users with RemoveFutureChannelsBan + _, err = c.UnBanUser(ctx, target1.ID, UnbanWithRemoveFutureChannelsBan(), UnbanWithCreatedBy(creator.ID)) + require.NoError(t, err) + _, err = c.UnBanUser(ctx, target2.ID, UnbanWithRemoveFutureChannelsBan(), UnbanWithCreatedBy(creator.ID)) + require.NoError(t, err) +} + func ExampleClient_BanUser() { client, _ := NewClient("XXXX", "XXXX") ctx := context.Background()