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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ long failed = client.posts().stream(PostListParams.create().workspaceId(workspac
| `knowledge()` | `list`, `create`, `createText`, `createUrl`, `createFile`, `update`, `delete`, `sync`, `search` |
| `ads()` | `list`, `external`, `boostable`, `connections`, `sources`, `authorizeMeta`, `deleteConnection`, `boost`, `create`, `refresh`, `setStatus`, `delete`, `accountTree`, `createCampaign`, `campaign`, `updateCampaign`, `deleteCampaign`, `duplicateCampaign`, `createAdSet`, `adSet`, `updateAdSet`, `deleteAdSet`, `duplicateAdSet`, `createNetworkAd`, `networkAd`, `updateNetworkAd`, `deleteNetworkAd`, `duplicateNetworkAd`, `bulkSetStatus`, `creatives`, `createCreative`, `creative`, `deleteCreative`, `estimateReach`, `insights`, `adInsights`, `audiences`, `createAudience`, `audience`, `updateAudience`, `deleteAudience`, `addAudienceUsers`, `searchTargeting`, `leadForms`, `createLeadForm`, `leadForm`, `archiveLeadForm`, `leads`, `leadsFeed`, `leadPages`, `subscribeLeadPage`, `unsubscribeLeadPage`, `goals`, `catalogs`, `createCatalog`, `catalog`, `updateCatalog`, `deleteCatalog`, `catalogProducts`, `writeCatalogProducts`, `productFeeds`, `createProductFeed`, `deleteProductFeed`, `feedUploads`, `startFeedUpload`, `productSets`, `createProductSet`, `updateProductSet`, `deleteProductSet`, `reachFrequency`, `createReachFrequency`, `reachFrequencyPrediction`, `reserveReachFrequency`, `cancelReachFrequency`, `library`, `partnershipCreators`, `requestPartnership`, `revokePartnership`, `accountActivity`, `labels`, `createLabel`, `updateLabel`, `deleteLabel`, `applyLabel`, `studies`, `createStudy`, `study`, `deleteStudy`, `iosCampaignLimits`, `highDemandPeriods`, `createHighDemandPeriod`, `deleteHighDemandPeriod`, `valueRuleSets`, `createValueRuleSet`, `deleteValueRuleSet` |
| `validate()` | `post`, `length`, `media` |
| `activity()` | `list` |

`accounts().communities()` covers the X communities an account can post into: `list`, `sync`,
`search`, `add`, `remove`.
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/fopost/sdk/FoPost.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.fopost.sdk.resource.AccountGroupsResource;
import com.fopost.sdk.resource.AccountsResource;
import com.fopost.sdk.resource.AdsResource;
import com.fopost.sdk.resource.ActivityResource;
import com.fopost.sdk.resource.AiResource;
import com.fopost.sdk.resource.AnalyticsResource;
import com.fopost.sdk.resource.AutomationsResource;
Expand Down Expand Up @@ -74,6 +75,7 @@ public final class FoPost {
private final ContactsResource contacts;
private final BroadcastsResource broadcasts;
private final SequencesResource sequences;
private final ActivityResource activity;
private final InboxResource inbox;
private final AdsResource ads;
private final ValidateResource validate;
Expand All @@ -86,6 +88,7 @@ private FoPost(ApiClient http) {
this.workspaces = new WorkspacesResource(http);
this.knowledge = new KnowledgeResource(http);
this.labels = new LabelsResource(http);
this.activity = new ActivityResource(http);
this.webhooks = new WebhooksResource(http);
this.analytics = new AnalyticsResource(http);
this.automations = new AutomationsResource(http);
Expand Down Expand Up @@ -153,6 +156,10 @@ public MediaResource media() {
return media;
}

public ActivityResource activity() {
return activity;
}

public AiResource ai() {
return ai;
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/fopost/sdk/model/ActivityActor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.fopost.sdk.model;

/** Who did it: {@code user}, {@code api_key}, {@code agent} or {@code system}. */
public record ActivityActor(String type, String name) {}
14 changes: 14 additions & 0 deletions src/main/java/com/fopost/sdk/model/ActivityEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.fopost.sdk.model;

import java.time.Instant;

/** One thing that happened in a workspace. A {@code security} kind is an audit row. */
public record ActivityEvent(
String id,
String workspaceId,
String kind,
String refType,
String refId,
String summary,
ActivityActor actor,
Instant time) {}
29 changes: 29 additions & 0 deletions src/main/java/com/fopost/sdk/model/ActivityPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.fopost.sdk.model;

import java.util.Iterator;
import java.util.List;

/**
* One page of activity, newest first. Pass {@code nextCursor} back as the cursor for the next
* page; it is {@code null} at the end of the list.
*/
public record ActivityPage(List<ActivityEvent> data, String nextCursor)
implements Iterable<ActivityEvent> {

public ActivityPage {
data = data == null ? List.of() : List.copyOf(data);
}

@Override
public Iterator<ActivityEvent> iterator() {
return data.iterator();
}

public int size() {
return data.size();
}

public boolean isEmpty() {
return data.isEmpty();
}
}
77 changes: 77 additions & 0 deletions src/main/java/com/fopost/sdk/param/ActivityParams.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.fopost.sdk.param;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* Filters for the activity log. Every field is optional; leaving {@code workspaceId} unset reads
* every workspace the key can reach.
*/
public final class ActivityParams {

/** The audit log: append-only, and the one kind that never expires. */
public static final String KIND_SECURITY = "security";

public static final String KIND_PUBLISH = "publish";
public static final String KIND_CONNECTION = "connection";
public static final String KIND_WEBHOOK = "webhook";
public static final String KIND_INBOX = "inbox";
public static final String KIND_AUTOMATION = "automation";
public static final String KIND_BILLING = "billing";

private String workspaceId;
private String kind;
private String from;
private String to;
private String cursor;
private Integer limit;

public static ActivityParams create() {
return new ActivityParams();
}

public ActivityParams workspaceId(String workspaceId) {
this.workspaceId = workspaceId;
return this;
}

/** One of the {@code KIND_*} constants. */
public ActivityParams kind(String kind) {
this.kind = kind;
return this;
}

/** ISO 8601. Only events at or after this time. */
public ActivityParams from(String from) {
this.from = from;
return this;
}

/** ISO 8601. Only events at or before this time. */
public ActivityParams to(String to) {
this.to = to;
return this;
}

/** The {@code nextCursor} of the previous page. */
public ActivityParams cursor(String cursor) {
this.cursor = cursor;
return this;
}

public ActivityParams limit(Integer limit) {
this.limit = limit;
return this;
}

public Map<String, Object> toQuery() {
Map<String, Object> query = new LinkedHashMap<>();
Params.put(query, "workspace_id", workspaceId);
Params.put(query, "kind", kind);
Params.put(query, "from", from);
Params.put(query, "to", to);
Params.put(query, "cursor", cursor);
Params.put(query, "limit", limit);
return query;
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/fopost/sdk/resource/ActivityResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.fopost.sdk.resource;

import com.fasterxml.jackson.databind.JsonNode;
import com.fopost.sdk.internal.ApiClient;
import com.fopost.sdk.model.ActivityEvent;
import com.fopost.sdk.model.ActivityPage;
import com.fopost.sdk.param.ActivityParams;
import java.util.List;

/** What happened in a workspace, including the security audit log. */
public final class ActivityResource {

private final ApiClient http;

public ActivityResource(ApiClient http) {
this.http = http;
}

public ActivityPage list() {
return list(ActivityParams.create());
}

/**
* Activity newest first. {@link ActivityParams#KIND_SECURITY} is the audit log: members
* joining, leaving or changing role and access, and changes to two-step verification,
* passkeys, single sign-on and signed-in devices. Those rows are append-only and never expire.
*/
public ActivityPage list(ActivityParams params) {
// The response carries meta beside data, so it is read whole rather than unwrapped.
JsonNode body = http.get("/v1/activity", params == null ? null : params.toQuery());
List<ActivityEvent> events = http.convertList(body.path("data"), ActivityEvent.class);
JsonNode cursor = body.path("meta").path("next_cursor");
return new ActivityPage(events, cursor.isNull() || cursor.isMissingNode() ? null : cursor.asText());
}
}
49 changes: 49 additions & 0 deletions src/test/java/com/fopost/sdk/ActivityTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.fopost.sdk;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fopost.sdk.model.ActivityPage;
import com.fopost.sdk.param.ActivityParams;
import org.junit.jupiter.api.Test;

class ActivityTest {

private static final String SECURITY_EVENT =
"{\"data\":[{\"id\":\"evt_1\",\"workspace_id\":\"w1\",\"kind\":\"security\","
+ "\"ref_type\":\"member_removed\",\"ref_id\":\"u2\","
+ "\"summary\":\"Removed sam@example.com\","
+ "\"actor\":{\"type\":\"user\",\"name\":\"Ada\"},"
+ "\"time\":\"2026-09-20T10:00:00Z\"}],\"meta\":{\"next_cursor\":\"42\"}}";

@Test
void readsTheAuditLogAndKeepsTheCursor() {
FakeTransport transport = new FakeTransport().enqueue(200, SECURITY_EVENT);

ActivityPage page = TestSupport.client(transport)
.activity()
.list(ActivityParams.create()
.workspaceId("w1")
.kind(ActivityParams.KIND_SECURITY)
.limit(1));

assertTrue(transport.last().url().contains("kind=security"));
assertTrue(transport.last().url().contains("workspace_id=w1"));
assertEquals(1, page.size());
assertEquals("member_removed", page.data().get(0).refType());
assertEquals("Ada", page.data().get(0).actor().name());
assertEquals("42", page.nextCursor());
}

@Test
void theEndOfTheListIsANullCursor() {
FakeTransport transport =
new FakeTransport().enqueue(200, "{\"data\":[],\"meta\":{\"next_cursor\":null}}");

ActivityPage page = TestSupport.client(transport).activity().list();

assertTrue(page.isEmpty());
assertNull(page.nextCursor());
}
}
Loading