Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
goVer: ["1.18", "1.19", "1.20", "1.21"]
goVer: ["1.22", "1.23", "1.24"]
steps:
- name: Set up Go ${{ matrix.goVer }}
uses: actions/setup-go@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reviewdog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: "1.21"
go-version: "1.22"

- name: Install golangci-lint
run:
Expand Down
70 changes: 70 additions & 0 deletions audit_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package stream

import (
"context"
"encoding/json"
"time"
)

type AuditLogsClient struct {
client *Client
}

type AuditLog struct {
EntityType string `json:"entity_type"`
EntityID string `json:"entity_id"`
Action string `json:"action"`
UserID string `json:"user_id"`
Custom map[string]any `json:"custom"`
CreatedAt time.Time `json:"created_at"`
}

type QueryAuditLogsResponse struct {
AuditLogs []AuditLog `json:"audit_logs"`
Next string `json:"next"`
Prev string `json:"prev"`
response
}

type QueryAuditLogsFilters struct {
EntityType string
EntityID string
UserID string
}

type QueryAuditLogsPager struct {
Next string
Prev string
Limit int
}

func (c *AuditLogsClient) QueryAuditLogs(ctx context.Context, filters QueryAuditLogsFilters, pager QueryAuditLogsPager) (*QueryAuditLogsResponse, error) {
endpoint := c.client.makeEndpoint("audit_logs/")
if filters.EntityType != "" && filters.EntityID != "" {
endpoint.addQueryParam(makeRequestOption("entity_type", filters.EntityType))
endpoint.addQueryParam(makeRequestOption("entity_id", filters.EntityID))
}
if filters.UserID != "" {
endpoint.addQueryParam(makeRequestOption("user_id", filters.UserID))
}
if pager.Next != "" {
endpoint.addQueryParam(makeRequestOption("next", pager.Next))
}
if pager.Prev != "" {
endpoint.addQueryParam(makeRequestOption("prev", pager.Prev))
}
if pager.Limit > 0 {
endpoint.addQueryParam(makeRequestOption("limit", pager.Limit))
}
body, err := c.client.get(ctx, endpoint, nil, c.client.authenticator.auditLogsAuth)
if err != nil {
return nil, err
}

var resp QueryAuditLogsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, err
}

return &resp, nil
}
120 changes: 120 additions & 0 deletions audit_logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package stream_test

import (
"context"
"encoding/json"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

stream "github.com/GetStream/stream-go2/v8"
)

func TestQueryAuditLogs(t *testing.T) {
ctx := context.Background()
client, requester := newClient(t)

// Test with all filters and pager options
filters := stream.QueryAuditLogsFilters{
EntityType: "feed",
EntityID: "123",
UserID: "user-42",
}
pager := stream.QueryAuditLogsPager{
Next: "next-token",
Prev: "prev-token",
Limit: 25,
}

// Set mock response
now := time.Now()
mockResp := struct {
AuditLogs []stream.AuditLog `json:"audit_logs"`
Next string `json:"next"`
Prev string `json:"prev"`
}{
AuditLogs: []stream.AuditLog{
{
EntityType: "feed",
EntityID: "123",
Action: "create",
UserID: "user-42",
Custom: map[string]any{"key": "value"},
CreatedAt: now,
},
},
Next: "next-page-token",
Prev: "prev-page-token",
}
respBytes, err := json.Marshal(mockResp)
require.NoError(t, err)
requester.resp = string(respBytes)

// Call the function
resp, err := client.AuditLogs().QueryAuditLogs(ctx, filters, pager)
require.NoError(t, err)

// Verify request
testRequest(
t,
requester.req,
http.MethodGet,
"https://api.stream-io-api.com/api/v1.0/audit_logs/?api_key=key&entity_id=123&entity_type=feed&limit=25&next=next-token&prev=prev-token&user_id=user-42",
"",
)

// Verify response
assert.Len(t, resp.AuditLogs, 1)
assert.Equal(t, "feed", resp.AuditLogs[0].EntityType)
assert.Equal(t, "123", resp.AuditLogs[0].EntityID)
assert.Equal(t, "create", resp.AuditLogs[0].Action)
assert.Equal(t, "user-42", resp.AuditLogs[0].UserID)
assert.Equal(t, "value", resp.AuditLogs[0].Custom["key"])
assert.Equal(t, now.Truncate(time.Second).UTC(), resp.AuditLogs[0].CreatedAt.Truncate(time.Second).UTC())
assert.Equal(t, "next-page-token", resp.Next)
assert.Equal(t, "prev-page-token", resp.Prev)
}

func TestQueryAuditLogsWithMinimalParams(t *testing.T) {
ctx := context.Background()
client, requester := newClient(t)

// Test with minimal filters and pager options
filters := stream.QueryAuditLogsFilters{}
pager := stream.QueryAuditLogsPager{}

// Set mock response
mockResp := struct {
AuditLogs []stream.AuditLog `json:"audit_logs"`
Next string `json:"next"`
Prev string `json:"prev"`
}{
AuditLogs: []stream.AuditLog{},
Next: "",
Prev: "",
}
respBytes, err := json.Marshal(mockResp)
require.NoError(t, err)
requester.resp = string(respBytes)

// Call the function
resp, err := client.AuditLogs().QueryAuditLogs(ctx, filters, pager)
require.NoError(t, err)

// Verify request
testRequest(
t,
requester.req,
http.MethodGet,
"https://api.stream-io-api.com/api/v1.0/audit_logs/?api_key=key",
"",
)

// Verify response
assert.Empty(t, resp.AuditLogs)
assert.Empty(t, resp.Next)
assert.Empty(t, resp.Prev)
}
10 changes: 10 additions & 0 deletions authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
resAnalytics resource = "analytics"
resAnalyticsRedirect resource = "redirect_and_track"
resModeration resource = "moderation"
resAuditLogs resource = "audit_logs"
)

type action string
Expand Down Expand Up @@ -121,6 +122,15 @@ func (a authenticator) moderationAuth(req *http.Request) error {
return a.jwtSignRequest(req, claims)
}

func (a authenticator) auditLogsAuth(req *http.Request) error {
claims := jwt.MapClaims{
"action": "*",
"feed_id": "*",
"resource": resAuditLogs,
}
return a.jwtSignRequest(req, claims)
}

func (a authenticator) signAnalyticsRedirectEndpoint(endpoint *endpoint) error {
claims := jwt.MapClaims{
"action": "*",
Expand Down
11 changes: 10 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ func (c *Client) Moderation() *ModerationClient {
return &ModerationClient{client: c.cloneWithURLBuilder(b)}
}

// AuditLogs returns a new AuditLogsClient.
func (c *Client) AuditLogs() *AuditLogsClient {
b := newAPIURLBuilder(c.addr, c.region, c.version)
return &AuditLogsClient{client: c.cloneWithURLBuilder(b)}
}

// Personalization returns a new PersonalizationClient.
func (c *Client) Personalization() *PersonalizationClient {
b := newPersonalizationURLBuilder(c.region)
Expand Down Expand Up @@ -557,8 +563,11 @@ func (c *Client) addActivities(ctx context.Context, feed Feed, activities ...Act
return &out, nil
}

func (c *Client) removeActivityByID(ctx context.Context, feed Feed, activityID string) (*RemoveActivityResponse, error) {
func (c *Client) removeActivityByID(ctx context.Context, feed Feed, activityID string, opts ...RemoveActivityOption) (*RemoveActivityResponse, error) {
endpoint := c.makeEndpoint("feed/%s/%s/%s/", feed.Slug(), feed.UserID(), activityID)
for _, opt := range opts {
endpoint.addQueryParam(opt)
}
resp, err := c.delete(ctx, endpoint, nil, c.authenticator.feedAuth(resFeed, feed))
if err != nil {
return nil, err
Expand Down
8 changes: 5 additions & 3 deletions feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Feed interface {
UserID() string
AddActivity(context.Context, Activity) (*AddActivityResponse, error)
AddActivities(context.Context, ...Activity) (*AddActivitiesResponse, error)
RemoveActivityByID(context.Context, string) (*RemoveActivityResponse, error)
RemoveActivityByID(context.Context, string, ...RemoveActivityOption) (*RemoveActivityResponse, error)
RemoveActivityByForeignID(context.Context, string) (*RemoveActivityResponse, error)
Follow(context.Context, *FlatFeed, ...FollowFeedOption) (*BaseResponse, error)
GetFollowing(context.Context, ...FollowingOption) (*FollowingResponse, error)
Expand Down Expand Up @@ -79,8 +79,10 @@ func (f *feed) AddActivities(ctx context.Context, activities ...Activity) (*AddA

// RemoveActivityByID removes an activity from the feed (if present), using the provided
// id string argument as the ID field of the activity.
func (f *feed) RemoveActivityByID(ctx context.Context, id string) (*RemoveActivityResponse, error) {
return f.client.removeActivityByID(ctx, f, id)
// Optional RemoveActivityOption parameters can be provided, such as WithRemoveByUserID
// to specify a user ID in the query string.
func (f *feed) RemoveActivityByID(ctx context.Context, id string, opts ...RemoveActivityOption) (*RemoveActivityResponse, error) {
return f.client.removeActivityByID(ctx, f, id, opts...)
}

// RemoveActivityByForeignID removes an activity from the feed (if present), using the provided
Expand Down
11 changes: 11 additions & 0 deletions feed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,14 @@ func TestRealtimeToken(t *testing.T) {
assert.Equal(t, tc.expected, token)
}
}

func TestRemoveActivityByIDWithUserID(t *testing.T) {
ctx := context.Background()
client, requester := newClient(t)
flat, _ := newFlatFeedWithUserID(client, "123")

_, err := flat.RemoveActivityByID(ctx, "activity-id", stream.WithRemoveByUserID("user-id"))
require.NoError(t, err)

testRequest(t, requester.req, http.MethodDelete, "https://api.stream-io-api.com/api/v1.0/feed/flat/123/activity-id/?api_key=key&user_id=user-id", "")
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module github.com/GetStream/stream-go2/v8

go 1.22

require (
github.com/fatih/structs v1.1.0
github.com/golang-jwt/jwt/v4 v4.5.0
Expand All @@ -12,5 +14,3 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)

go 1.18
7 changes: 5 additions & 2 deletions moderation.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,29 @@ func (c *ModerationClient) flagContent(ctx context.Context, r flagRequest) error
type updateStatusRequest struct {
EntityType string `json:"entity_type"`
EntityID string `json:"entity_id"`
ModeratorID string `json:"moderator_id"`
Status string `json:"status"`
RecommendedAction string `json:"recommended_action"`
LatestModeratorAction string `json:"latest_moderator_action"`
}

func (c *ModerationClient) UpdateActivityModerationStatus(ctx context.Context, activityID, status, recAction, modAction string) error {
func (c *ModerationClient) UpdateActivityModerationStatus(ctx context.Context, activityID, modID, status, recAction, modAction string) error {
r := updateStatusRequest{
EntityType: ModerationActivity,
EntityID: activityID,
ModeratorID: modID,
Status: status,
RecommendedAction: recAction,
LatestModeratorAction: modAction,
}
return c.updateStatus(ctx, r)
}

func (c *ModerationClient) UpdateReactionModerationStatus(ctx context.Context, reactionID, status, recAction, modAction string) error {
func (c *ModerationClient) UpdateReactionModerationStatus(ctx context.Context, reactionID, modID, status, recAction, modAction string) error {
r := updateStatusRequest{
EntityType: ModerationReaction,
EntityID: reactionID,
ModeratorID: modID,
Status: status,
RecommendedAction: recAction,
LatestModeratorAction: modAction,
Expand Down
8 changes: 4 additions & 4 deletions moderation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,28 +53,28 @@ func TestFlagUser(t *testing.T) {
func TestUpdateActivityModerationStatus(t *testing.T) {
ctx := context.Background()
client, requester := newClient(t)
err := client.Moderation().UpdateActivityModerationStatus(ctx, "foo", "complete", "watch", "mark_safe")
err := client.Moderation().UpdateActivityModerationStatus(ctx, "foo", "moderator_123", "complete", "watch", "mark_safe")
require.NoError(t, err)
testRequest(
t,
requester.req,
http.MethodPost,
"https://api.stream-io-api.com/api/v1.0/moderation/status/?api_key=key",
`{"entity_id":"foo", "entity_type":"stream:feeds:v2:activity", "latest_moderator_action":"mark_safe", "recommended_action":"watch", "status":"complete"}`,
`{"entity_id":"foo", "entity_type":"stream:feeds:v2:activity", "moderator_id": "moderator_123", "latest_moderator_action":"mark_safe", "recommended_action":"watch", "status":"complete"}`,
)
}

func TestUpdateReactionModerationStatus(t *testing.T) {
ctx := context.Background()
client, requester := newClient(t)
err := client.Moderation().UpdateReactionModerationStatus(ctx, "foo", "complete", "watch", "mark_safe")
err := client.Moderation().UpdateReactionModerationStatus(ctx, "foo", "moderator_123", "complete", "watch", "mark_safe")
require.NoError(t, err)
testRequest(
t,
requester.req,
http.MethodPost,
"https://api.stream-io-api.com/api/v1.0/moderation/status/?api_key=key",
`{"entity_id":"foo", "entity_type":"stream:feeds:v2:reaction", "latest_moderator_action":"mark_safe", "recommended_action":"watch", "status":"complete"}`,
`{"entity_id":"foo", "entity_type":"stream:feeds:v2:reaction", "moderator_id": "moderator_123", "latest_moderator_action":"mark_safe", "recommended_action":"watch", "status":"complete"}`,
)
}

Expand Down
Loading