@iann29/rastro 0.10.9 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/agent/integration.md +45 -0
- package/agent/manifest.json +4 -4
- package/dist/client/federation.d.ts +82 -9
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +40 -2
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +464 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +259 -7
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +10 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +273 -21
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +2 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/constants.d.ts +31 -0
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +31 -0
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +2 -0
- package/dist/component/convex.config.js +7 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/errors.d.ts +1 -1
- package/dist/component/errors.d.ts.map +1 -1
- package/dist/component/errors.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +64 -0
- package/dist/component/http.js.map +1 -1
- package/dist/component/leadAdsValidators.d.ts +1092 -0
- package/dist/component/leadAdsValidators.d.ts.map +1 -0
- package/dist/component/leadAdsValidators.js +304 -0
- package/dist/component/leadAdsValidators.js.map +1 -0
- package/dist/component/leadFormRules.d.ts +60 -0
- package/dist/component/leadFormRules.d.ts.map +1 -0
- package/dist/component/leadFormRules.js +140 -0
- package/dist/component/leadFormRules.js.map +1 -0
- package/dist/component/leadFormStore.d.ts +16 -0
- package/dist/component/leadFormStore.d.ts.map +1 -0
- package/dist/component/leadFormStore.js +92 -0
- package/dist/component/leadFormStore.js.map +1 -0
- package/dist/component/leadgenToken.d.ts +56 -0
- package/dist/component/leadgenToken.d.ts.map +1 -0
- package/dist/component/leadgenToken.js +262 -0
- package/dist/component/leadgenToken.js.map +1 -0
- package/dist/component/leads.d.ts +76 -7
- package/dist/component/leads.d.ts.map +1 -1
- package/dist/component/leads.js +112 -78
- package/dist/component/leads.js.map +1 -1
- package/dist/component/meta.d.ts.map +1 -1
- package/dist/component/meta.js +21 -1
- package/dist/component/meta.js.map +1 -1
- package/dist/component/metaFake.d.ts +44 -0
- package/dist/component/metaFake.d.ts.map +1 -1
- package/dist/component/metaFake.js +400 -1
- package/dist/component/metaFake.js.map +1 -1
- package/dist/component/metaGraph.d.ts +91 -0
- package/dist/component/metaGraph.d.ts.map +1 -1
- package/dist/component/metaGraph.js +217 -0
- package/dist/component/metaGraph.js.map +1 -1
- package/dist/component/metaLeads.d.ts +681 -0
- package/dist/component/metaLeads.d.ts.map +1 -0
- package/dist/component/metaLeads.js +2634 -0
- package/dist/component/metaLeads.js.map +1 -0
- package/dist/component/schema.d.ts +414 -5
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +169 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/validators.d.ts +235 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +14 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +1 -1
- package/dist/tracker/generated.js +1 -1
- package/dist/ui/campaigns/CampaignsView.d.ts +5 -1
- package/dist/ui/campaigns/CampaignsView.d.ts.map +1 -1
- package/dist/ui/campaigns/CampaignsView.js.map +1 -1
- package/dist/ui/campaigns/Drawer.d.ts.map +1 -1
- package/dist/ui/campaigns/Drawer.js +8 -26
- package/dist/ui/campaigns/Drawer.js.map +1 -1
- package/dist/ui/campaigns/labels.d.ts +29 -1
- package/dist/ui/campaigns/labels.d.ts.map +1 -1
- package/dist/ui/campaigns/labels.js +77 -2
- package/dist/ui/campaigns/labels.js.map +1 -1
- package/dist/ui/campaigns/model.js +2 -2
- package/dist/ui/campaigns/model.js.map +1 -1
- package/dist/ui/forms/FirstContact.d.ts +19 -0
- package/dist/ui/forms/FirstContact.d.ts.map +1 -0
- package/dist/ui/forms/FirstContact.js +81 -0
- package/dist/ui/forms/FirstContact.js.map +1 -0
- package/dist/ui/forms/FormLead.d.ts +28 -0
- package/dist/ui/forms/FormLead.d.ts.map +1 -0
- package/dist/ui/forms/FormLead.js +199 -0
- package/dist/ui/forms/FormLead.js.map +1 -0
- package/dist/ui/forms/FormRulesDrawer.d.ts +49 -0
- package/dist/ui/forms/FormRulesDrawer.d.ts.map +1 -0
- package/dist/ui/forms/FormRulesDrawer.js +149 -0
- package/dist/ui/forms/FormRulesDrawer.js.map +1 -0
- package/dist/ui/forms/FormsView.d.ts +10 -0
- package/dist/ui/forms/FormsView.d.ts.map +1 -0
- package/dist/ui/forms/FormsView.js +215 -0
- package/dist/ui/forms/FormsView.js.map +1 -0
- package/dist/ui/forms/Icons.d.ts +46 -0
- package/dist/ui/forms/Icons.d.ts.map +1 -0
- package/dist/ui/forms/Icons.js +45 -0
- package/dist/ui/forms/Icons.js.map +1 -0
- package/dist/ui/forms/RuleRow.d.ts +37 -0
- package/dist/ui/forms/RuleRow.d.ts.map +1 -0
- package/dist/ui/forms/RuleRow.js +80 -0
- package/dist/ui/forms/RuleRow.js.map +1 -0
- package/dist/ui/forms/WhatsAppButtonGuide.d.ts +33 -0
- package/dist/ui/forms/WhatsAppButtonGuide.d.ts.map +1 -0
- package/dist/ui/forms/WhatsAppButtonGuide.js +90 -0
- package/dist/ui/forms/WhatsAppButtonGuide.js.map +1 -0
- package/dist/ui/forms/index.d.ts +11 -0
- package/dist/ui/forms/index.d.ts.map +1 -0
- package/dist/ui/forms/index.js +7 -0
- package/dist/ui/forms/index.js.map +1 -0
- package/dist/ui/forms/journey.d.ts +19 -0
- package/dist/ui/forms/journey.d.ts.map +1 -0
- package/dist/ui/forms/journey.js +54 -0
- package/dist/ui/forms/journey.js.map +1 -0
- package/dist/ui/forms/model.d.ts +239 -0
- package/dist/ui/forms/model.d.ts.map +1 -0
- package/dist/ui/forms/model.js +556 -0
- package/dist/ui/forms/model.js.map +1 -0
- package/dist/ui/forms/sample.d.ts +36 -0
- package/dist/ui/forms/sample.d.ts.map +1 -0
- package/dist/ui/forms/sample.js +444 -0
- package/dist/ui/forms/sample.js.map +1 -0
- package/dist/ui/forms/types.d.ts +454 -0
- package/dist/ui/forms/types.d.ts.map +1 -0
- package/dist/ui/forms/types.js +2 -0
- package/dist/ui/forms/types.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +4 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/integrations/Overlay.d.ts +3 -1
- package/dist/ui/integrations/Overlay.d.ts.map +1 -1
- package/dist/ui/integrations/Overlay.js +2 -2
- package/dist/ui/integrations/Overlay.js.map +1 -1
- package/dist/ui/portfolio/PortfolioView.d.ts +4 -1
- package/dist/ui/portfolio/PortfolioView.d.ts.map +1 -1
- package/dist/ui/portfolio/PortfolioView.js.map +1 -1
- package/dist/ui/sample.d.ts +3 -1
- package/dist/ui/sample.d.ts.map +1 -1
- package/dist/ui/sample.js +2 -0
- package/dist/ui/sample.js.map +1 -1
- package/dist/ui/seed.d.ts +1 -1
- package/dist/ui/ui.css +2301 -0
- package/docs/federation.md +19 -0
- package/docs/meta-ads.md +713 -7
- package/docs/upgrading.md +70 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/component/_generated/api.ts +10 -0
- package/src/component/_generated/component.ts +289 -20
- package/src/component/_generated/server.ts +2 -0
- package/src/component/constants.ts +33 -0
- package/src/component/convex.config.ts +7 -0
- package/src/component/errors.ts +3 -1
- package/src/component/http.ts +75 -0
- package/src/component/leadAdsValidators.ts +365 -0
- package/src/component/leadFormRules.ts +202 -0
- package/src/component/leadFormStore.ts +129 -0
- package/src/component/leadgenToken.ts +348 -0
- package/src/component/leads.ts +136 -84
- package/src/component/meta.ts +21 -1
- package/src/component/metaFake.ts +485 -1
- package/src/component/metaGraph.ts +327 -0
- package/src/component/metaLeads.ts +3101 -0
- package/src/component/schema.ts +187 -1
- package/src/component/validators.ts +17 -1
- package/src/tracker/generated.ts +1 -1
package/src/component/meta.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
MAX_LINKS_PER_CONNECTION,
|
|
33
33
|
PAUSED_AT,
|
|
34
34
|
} from "./metaSync.js";
|
|
35
|
+
import { removeConnectionPages, syncOwnerPages } from "./metaLeads.js";
|
|
35
36
|
import { sanitizeOpaqueId } from "./sanitize.js";
|
|
36
37
|
import { decryptSecret, encryptSecret, hasSecretsKey } from "./secrets.js";
|
|
37
38
|
import {
|
|
@@ -584,6 +585,13 @@ async function connectToken(
|
|
|
584
585
|
lastError: login?.lastError ?? null,
|
|
585
586
|
},
|
|
586
587
|
);
|
|
588
|
+
// The Pages and their own tokens, on a host that opted into Lead Ads. Never
|
|
589
|
+
// fails the login: a login without the Page permissions simply lists none.
|
|
590
|
+
await syncOwnerPages(ctx, client, {
|
|
591
|
+
connectionId,
|
|
592
|
+
token,
|
|
593
|
+
tokenIv: sealed.iv,
|
|
594
|
+
});
|
|
587
595
|
return { connectionId, businessName, adAccounts, pixels };
|
|
588
596
|
}
|
|
589
597
|
|
|
@@ -683,8 +691,8 @@ export const listAdAccounts = action({
|
|
|
683
691
|
let adAccounts: MetaAdAccount[];
|
|
684
692
|
let pixels: MetaPixel[];
|
|
685
693
|
let pixelsComplete: boolean;
|
|
694
|
+
const client = graph();
|
|
686
695
|
try {
|
|
687
|
-
const client = graph();
|
|
688
696
|
adAccounts = (await client.adAccounts(token, optionalLoginApp())).map(
|
|
689
697
|
toAccount,
|
|
690
698
|
);
|
|
@@ -710,6 +718,13 @@ export const listAdAccounts = action({
|
|
|
710
718
|
pixels,
|
|
711
719
|
pixelsComplete,
|
|
712
720
|
});
|
|
721
|
+
// The Pages too (Lead Ads hosts): a Page granted since the login shows
|
|
722
|
+
// up, one taken back stops.
|
|
723
|
+
await syncOwnerPages(ctx, client, {
|
|
724
|
+
connectionId: connection.connectionId,
|
|
725
|
+
token,
|
|
726
|
+
tokenIv: connection.tokenIv,
|
|
727
|
+
});
|
|
713
728
|
return adAccounts;
|
|
714
729
|
},
|
|
715
730
|
});
|
|
@@ -787,6 +802,10 @@ async function removeOwnerBatch(
|
|
|
787
802
|
.take(UNLINK_BATCH)
|
|
788
803
|
: [];
|
|
789
804
|
for (const link of links) await unlinkRow(ctx, link);
|
|
805
|
+
// Its Pages and their links go with it (the leads already in stay).
|
|
806
|
+
const morePages = connectionId
|
|
807
|
+
? await removeConnectionPages(ctx, connectionId, UNLINK_BATCH)
|
|
808
|
+
: false;
|
|
790
809
|
// A pixel picked through the connection sent with its token: it goes too.
|
|
791
810
|
const connectionPixels = connectionId
|
|
792
811
|
? await ctx.db
|
|
@@ -819,6 +838,7 @@ async function removeOwnerBatch(
|
|
|
819
838
|
const cursor = ownerPixels.at(-1)?._creationTime;
|
|
820
839
|
if (
|
|
821
840
|
links.length === UNLINK_BATCH ||
|
|
841
|
+
morePages ||
|
|
822
842
|
connectionPixels.length === UNLINK_BATCH ||
|
|
823
843
|
ownerPixels.length === UNLINK_BATCH
|
|
824
844
|
) {
|
|
@@ -16,7 +16,10 @@ import {
|
|
|
16
16
|
* which Meta refuses like an expired token. A token
|
|
17
17
|
* `rastro-fake-sample-token:<days>` moves every date by that many whole days,
|
|
18
18
|
* so a demo seeded today reads the sample as its last 30 days, sync after
|
|
19
|
-
* sync. Conversions API sends are accepted.
|
|
19
|
+
* sync. Conversions API sends are accepted. Lead Ads: a Facebook Page per
|
|
20
|
+
* client (and "Sorriso Kids", whose leads Meta did not grant), their instant
|
|
21
|
+
* forms, and a steady stream of made-up leads (`FAKE_PAGES` below). No
|
|
22
|
+
* network call is made.
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
const BUSINESS = { id: "sample-business", name: "Connection Company" };
|
|
@@ -177,6 +180,375 @@ function account(adAccountId: string): FakeAccount {
|
|
|
177
180
|
return found;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Lead Ads: a Facebook Page per sample client, their instant forms, and a
|
|
185
|
+
// steady stream of leads. Lead `k` of a form is created at `k × every +
|
|
186
|
+
// offset` (UTC ms), so the leads before now always exist and new ones show up
|
|
187
|
+
// as time passes, for the webhook-free reconciliation to find. Answers and
|
|
188
|
+
// contacts derive from `k` (made-up people, never real ones).
|
|
189
|
+
|
|
190
|
+
const HOUR_MS = 3_600_000;
|
|
191
|
+
const FAKE_PAGE_TOKEN = "rastro-fake-page-token:";
|
|
192
|
+
const LEAD_HISTORY_MS = 90 * DAY_MS;
|
|
193
|
+
/** How many leads one fake listing holds at most (Meta's cap); tests lower it. */
|
|
194
|
+
export const fakeLimits = { maxLeads: 2_000 };
|
|
195
|
+
/**
|
|
196
|
+
* The Pages the app is subscribed to, as Meta keeps it: one subscription per
|
|
197
|
+
* Page and app, whichever host asked for it. Tests read it, and take a Page
|
|
198
|
+
* off it the way another host's unsubscription would.
|
|
199
|
+
*/
|
|
200
|
+
export const fakeSubscriptions = new Set<string>();
|
|
201
|
+
|
|
202
|
+
type FakeQuestion = {
|
|
203
|
+
key: string;
|
|
204
|
+
label: string;
|
|
205
|
+
type: string;
|
|
206
|
+
options?: Array<{ key: string; value: string }>;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
interface FakeForm {
|
|
210
|
+
formId: string;
|
|
211
|
+
/** The two digits every leadgen id of this form starts with (after "7"). */
|
|
212
|
+
serial: string;
|
|
213
|
+
name: string;
|
|
214
|
+
status: string;
|
|
215
|
+
questions: FakeQuestion[];
|
|
216
|
+
whatsappButton: boolean | null;
|
|
217
|
+
/** How long after its creation (days before now) the form exists. */
|
|
218
|
+
createdDaysAgo: number;
|
|
219
|
+
/** A lead every this many ms; 0 for a form that brings none. */
|
|
220
|
+
everyMs: number;
|
|
221
|
+
offsetMs: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface FakePage {
|
|
225
|
+
pageId: string;
|
|
226
|
+
name: string;
|
|
227
|
+
/** The sample client whose ads bring the leads, or null. */
|
|
228
|
+
sampleSiteId: string | null;
|
|
229
|
+
leadsAllowed: boolean;
|
|
230
|
+
forms: FakeForm[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const CONTACT_QUESTIONS: FakeQuestion[] = [
|
|
234
|
+
{ key: "full_name", label: "Nome completo", type: "FULL_NAME" },
|
|
235
|
+
{ key: "phone_number", label: "Telefone", type: "PHONE" },
|
|
236
|
+
];
|
|
237
|
+
const EMAIL_QUESTION: FakeQuestion = {
|
|
238
|
+
key: "email",
|
|
239
|
+
label: "E-mail",
|
|
240
|
+
type: "EMAIL",
|
|
241
|
+
};
|
|
242
|
+
const choice = (
|
|
243
|
+
key: string,
|
|
244
|
+
label: string,
|
|
245
|
+
options: Array<[string, string]>,
|
|
246
|
+
): FakeQuestion => ({
|
|
247
|
+
key,
|
|
248
|
+
label,
|
|
249
|
+
type: "CUSTOM",
|
|
250
|
+
options: options.map(([optionKey, value]) => ({ key: optionKey, value })),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
/** The fake Pages: one per sample client, plus one without lead access. */
|
|
254
|
+
export const FAKE_PAGES: readonly FakePage[] = [
|
|
255
|
+
{
|
|
256
|
+
pageId: "4100000000001",
|
|
257
|
+
name: "Loja X",
|
|
258
|
+
sampleSiteId: "sample-loja-x",
|
|
259
|
+
leadsAllowed: true,
|
|
260
|
+
forms: [
|
|
261
|
+
{
|
|
262
|
+
formId: "4200000000101",
|
|
263
|
+
serial: "11",
|
|
264
|
+
name: "Provador VIP",
|
|
265
|
+
status: "ACTIVE",
|
|
266
|
+
questions: [
|
|
267
|
+
...CONTACT_QUESTIONS,
|
|
268
|
+
choice("tamanho", "Qual o seu tamanho?", [
|
|
269
|
+
["p", "P"],
|
|
270
|
+
["m", "M"],
|
|
271
|
+
["g", "G"],
|
|
272
|
+
]),
|
|
273
|
+
],
|
|
274
|
+
whatsappButton: true,
|
|
275
|
+
createdDaysAgo: 120,
|
|
276
|
+
everyMs: 8 * HOUR_MS,
|
|
277
|
+
offsetMs: 25 * 60_000,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
pageId: "4100000000002",
|
|
283
|
+
name: "Clínica Sorriso",
|
|
284
|
+
sampleSiteId: "sample-clinica-sorriso",
|
|
285
|
+
leadsAllowed: true,
|
|
286
|
+
forms: [
|
|
287
|
+
{
|
|
288
|
+
formId: "4200000000201",
|
|
289
|
+
serial: "21",
|
|
290
|
+
name: "Avaliação gratuita",
|
|
291
|
+
status: "ACTIVE",
|
|
292
|
+
questions: [
|
|
293
|
+
...CONTACT_QUESTIONS,
|
|
294
|
+
EMAIL_QUESTION,
|
|
295
|
+
choice("orcamento", "Orçamento para o tratamento", [
|
|
296
|
+
["ate_1000", "Até R$ 1.000"],
|
|
297
|
+
["de_1000_a_5000", "R$ 1.000 a R$ 5.000"],
|
|
298
|
+
["acima_5000", "Acima de R$ 5.000"],
|
|
299
|
+
]),
|
|
300
|
+
choice("agendar", "Quer agendar a avaliação?", [
|
|
301
|
+
["sim", "Sim"],
|
|
302
|
+
["nao", "Não"],
|
|
303
|
+
]),
|
|
304
|
+
choice("tratamento", "Tratamento de interesse", [
|
|
305
|
+
["implante", "Implante"],
|
|
306
|
+
["lente", "Lente de contato"],
|
|
307
|
+
["clareamento", "Clareamento"],
|
|
308
|
+
]),
|
|
309
|
+
],
|
|
310
|
+
whatsappButton: false,
|
|
311
|
+
createdDaysAgo: 200,
|
|
312
|
+
everyMs: 675 * 60_000,
|
|
313
|
+
offsetMs: 4 * 60_000,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
formId: "4200000000202",
|
|
317
|
+
serial: "22",
|
|
318
|
+
name: "Clareamento — orçamento",
|
|
319
|
+
status: "ACTIVE",
|
|
320
|
+
questions: [
|
|
321
|
+
...CONTACT_QUESTIONS,
|
|
322
|
+
choice("quando", "Quando quer começar?", [
|
|
323
|
+
["esta_semana", "Esta semana"],
|
|
324
|
+
["este_mes", "Este mês"],
|
|
325
|
+
["sem_pressa", "Sem pressa"],
|
|
326
|
+
]),
|
|
327
|
+
],
|
|
328
|
+
whatsappButton: true,
|
|
329
|
+
createdDaysAgo: 90,
|
|
330
|
+
everyMs: 40 * HOUR_MS,
|
|
331
|
+
offsetMs: 19 * HOUR_MS + 10 * 60_000,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
formId: "4200000000203",
|
|
335
|
+
serial: "23",
|
|
336
|
+
name: "Lente de contato dental",
|
|
337
|
+
status: "ACTIVE",
|
|
338
|
+
questions: [
|
|
339
|
+
...CONTACT_QUESTIONS,
|
|
340
|
+
EMAIL_QUESTION,
|
|
341
|
+
choice("lente_dentes", "Quantos dentes?", [
|
|
342
|
+
["ate_4", "Até 4"],
|
|
343
|
+
["de_6_a_10", "De 6 a 10"],
|
|
344
|
+
["arcada", "A arcada toda"],
|
|
345
|
+
]),
|
|
346
|
+
],
|
|
347
|
+
whatsappButton: null,
|
|
348
|
+
createdDaysAgo: 2,
|
|
349
|
+
everyMs: 12 * HOUR_MS,
|
|
350
|
+
offsetMs: 9 * HOUR_MS + 2 * 60_000,
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
formId: "4200000000204",
|
|
354
|
+
serial: "24",
|
|
355
|
+
name: "Promo setembro",
|
|
356
|
+
status: "ARCHIVED",
|
|
357
|
+
questions: [
|
|
358
|
+
...CONTACT_QUESTIONS,
|
|
359
|
+
EMAIL_QUESTION,
|
|
360
|
+
choice("promo", "Qual tratamento da promoção?", [
|
|
361
|
+
["limpeza", "Limpeza"],
|
|
362
|
+
["clareamento", "Clareamento"],
|
|
363
|
+
]),
|
|
364
|
+
],
|
|
365
|
+
whatsappButton: null,
|
|
366
|
+
createdDaysAgo: 60,
|
|
367
|
+
everyMs: 0,
|
|
368
|
+
offsetMs: 0,
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
pageId: "4100000000003",
|
|
374
|
+
name: "Pet Mania",
|
|
375
|
+
sampleSiteId: "sample-pet-mania",
|
|
376
|
+
leadsAllowed: true,
|
|
377
|
+
forms: [
|
|
378
|
+
{
|
|
379
|
+
formId: "4200000000301",
|
|
380
|
+
serial: "31",
|
|
381
|
+
name: "Agende o banho",
|
|
382
|
+
status: "ACTIVE",
|
|
383
|
+
questions: [
|
|
384
|
+
...CONTACT_QUESTIONS,
|
|
385
|
+
choice("porte", "Porte do pet", [
|
|
386
|
+
["pequeno", "Pequeno"],
|
|
387
|
+
["medio", "Médio"],
|
|
388
|
+
["grande", "Grande"],
|
|
389
|
+
]),
|
|
390
|
+
],
|
|
391
|
+
whatsappButton: null,
|
|
392
|
+
createdDaysAgo: 150,
|
|
393
|
+
everyMs: 10 * HOUR_MS,
|
|
394
|
+
offsetMs: 2 * HOUR_MS,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
pageId: "4100000000004",
|
|
400
|
+
name: "Casa Aurora",
|
|
401
|
+
sampleSiteId: "sample-casa-aurora",
|
|
402
|
+
leadsAllowed: true,
|
|
403
|
+
forms: [
|
|
404
|
+
{
|
|
405
|
+
formId: "4200000000401",
|
|
406
|
+
serial: "41",
|
|
407
|
+
name: "Orçamento de móveis",
|
|
408
|
+
status: "ACTIVE",
|
|
409
|
+
questions: [
|
|
410
|
+
...CONTACT_QUESTIONS,
|
|
411
|
+
EMAIL_QUESTION,
|
|
412
|
+
choice("ambiente", "Qual ambiente?", [
|
|
413
|
+
["sala", "Sala"],
|
|
414
|
+
["quarto", "Quarto"],
|
|
415
|
+
["cozinha", "Cozinha"],
|
|
416
|
+
]),
|
|
417
|
+
{
|
|
418
|
+
key: "detalhes",
|
|
419
|
+
label: "Conte o que você precisa",
|
|
420
|
+
type: "CUSTOM",
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
whatsappButton: false,
|
|
424
|
+
createdDaysAgo: 100,
|
|
425
|
+
everyMs: 20 * HOUR_MS,
|
|
426
|
+
offsetMs: 13 * HOUR_MS,
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
pageId: "4100000000099",
|
|
432
|
+
name: "Sorriso Kids",
|
|
433
|
+
sampleSiteId: null,
|
|
434
|
+
leadsAllowed: false,
|
|
435
|
+
forms: [],
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const FAKE_FIRST = [
|
|
440
|
+
"Marina",
|
|
441
|
+
"Rafael",
|
|
442
|
+
"Juliana",
|
|
443
|
+
"Bruno",
|
|
444
|
+
"Patrícia",
|
|
445
|
+
"Thiago",
|
|
446
|
+
"Larissa",
|
|
447
|
+
"Felipe",
|
|
448
|
+
"Camila",
|
|
449
|
+
"Gustavo",
|
|
450
|
+
];
|
|
451
|
+
const FAKE_LAST = ["Costa", "Souza", "Lima", "Alves", "Rocha", "Pires"];
|
|
452
|
+
|
|
453
|
+
const fakePage = (pageId: string) =>
|
|
454
|
+
FAKE_PAGES.find((page) => page.pageId === pageId) ?? null;
|
|
455
|
+
|
|
456
|
+
function fakeForm(formId: string) {
|
|
457
|
+
for (const page of FAKE_PAGES) {
|
|
458
|
+
const form = page.forms.find((row) => row.formId === formId);
|
|
459
|
+
if (form) return { page, form };
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const formCreatedAt = (form: FakeForm, now: number) =>
|
|
465
|
+
Math.floor((now - form.createdDaysAgo * DAY_MS) / DAY_MS) * DAY_MS;
|
|
466
|
+
|
|
467
|
+
/** The `k`-th lead of a form, as Meta returns it. */
|
|
468
|
+
function fakeLead(page: FakePage, form: FakeForm, k: number) {
|
|
469
|
+
const createdTime = k * form.everyMs + form.offsetMs;
|
|
470
|
+
const first = FAKE_FIRST[k % FAKE_FIRST.length]!;
|
|
471
|
+
const last = FAKE_LAST[(k * 7) % FAKE_LAST.length]!;
|
|
472
|
+
const digits = String((k * 7919 + Number(form.serial) * 104_729) % 1e8)
|
|
473
|
+
.padStart(8, "0")
|
|
474
|
+
.slice(0, 8);
|
|
475
|
+
const fields = form.questions.map((question, index) => {
|
|
476
|
+
switch (question.type) {
|
|
477
|
+
case "FULL_NAME":
|
|
478
|
+
return { name: question.key, values: [`${first} ${last}`] };
|
|
479
|
+
case "PHONE":
|
|
480
|
+
return { name: question.key, values: [`+55549${digits}`] };
|
|
481
|
+
case "EMAIL":
|
|
482
|
+
return {
|
|
483
|
+
name: question.key,
|
|
484
|
+
values: [
|
|
485
|
+
`${first.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase()}.${k}@example.com`,
|
|
486
|
+
],
|
|
487
|
+
};
|
|
488
|
+
default: {
|
|
489
|
+
const options = question.options ?? [];
|
|
490
|
+
if (options.length === 0) {
|
|
491
|
+
return { name: question.key, values: ["Preciso de um orçamento"] };
|
|
492
|
+
}
|
|
493
|
+
// Meta returns the option's shown value; the ingestion maps it back.
|
|
494
|
+
const option = options[(k + index * 3) % options.length]!;
|
|
495
|
+
return { name: question.key, values: [option.value] };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
const ads =
|
|
500
|
+
page.sampleSiteId === null
|
|
501
|
+
? []
|
|
502
|
+
: (SAMPLE_SEED.find(
|
|
503
|
+
(site) => site.siteId === page.sampleSiteId,
|
|
504
|
+
)?.campaigns.flatMap((campaign) =>
|
|
505
|
+
campaign.adsets.flatMap((adset) =>
|
|
506
|
+
adset.ads.map((ad) => ({
|
|
507
|
+
adId: ad.adId,
|
|
508
|
+
adsetId: adset.adsetId,
|
|
509
|
+
campaignId: campaign.campaignId,
|
|
510
|
+
})),
|
|
511
|
+
),
|
|
512
|
+
) ?? []);
|
|
513
|
+
const ad = ads.length > 0 ? ads[k % ads.length]! : null;
|
|
514
|
+
return {
|
|
515
|
+
leadgenId: `7${form.serial}${String(k).padStart(9, "0")}`,
|
|
516
|
+
createdTime,
|
|
517
|
+
formId: form.formId,
|
|
518
|
+
adId: ad?.adId ?? null,
|
|
519
|
+
adsetId: ad?.adsetId ?? null,
|
|
520
|
+
campaignId: ad?.campaignId ?? null,
|
|
521
|
+
platform: k % 3 === 0 ? "ig" : "fb",
|
|
522
|
+
isOrganic: ad === null,
|
|
523
|
+
fields,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function checkPageToken(token: string, pageId?: string) {
|
|
528
|
+
checkToken(token);
|
|
529
|
+
if (pageId !== undefined && token !== `${FAKE_PAGE_TOKEN}${pageId}`) {
|
|
530
|
+
throw new MetaGraphError(
|
|
531
|
+
"permission",
|
|
532
|
+
"the Page token does not act for this Page (fake)",
|
|
533
|
+
200,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const pageIdOfToken = (token: string) =>
|
|
539
|
+
token.startsWith(FAKE_PAGE_TOKEN) ? token.slice(FAKE_PAGE_TOKEN.length) : "";
|
|
540
|
+
|
|
541
|
+
function leadPermission(page: FakePage | null): FakePage {
|
|
542
|
+
if (!page?.leadsAllowed) {
|
|
543
|
+
throw new MetaGraphError(
|
|
544
|
+
"permission",
|
|
545
|
+
"leads_retrieval was not granted for this Page (fake)",
|
|
546
|
+
200,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return page;
|
|
550
|
+
}
|
|
551
|
+
|
|
180
552
|
export function fakeMetaGraph(): MetaGraph {
|
|
181
553
|
return {
|
|
182
554
|
async exchangeCode(_app, code) {
|
|
@@ -277,5 +649,117 @@ export function fakeMetaGraph(): MetaGraph {
|
|
|
277
649
|
checkToken(token);
|
|
278
650
|
return { eventsReceived: payload.data.length, fbtraceId: "fake-trace" };
|
|
279
651
|
},
|
|
652
|
+
async pages(token) {
|
|
653
|
+
checkToken(token);
|
|
654
|
+
return {
|
|
655
|
+
pages: FAKE_PAGES.map((page) => ({
|
|
656
|
+
pageId: page.pageId,
|
|
657
|
+
name: page.name,
|
|
658
|
+
accessToken: `${FAKE_PAGE_TOKEN}${page.pageId}`,
|
|
659
|
+
tasks: ["ADVERTISE", "ANALYZE", "CREATE_CONTENT", "MANAGE"],
|
|
660
|
+
})),
|
|
661
|
+
complete: true,
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
async leadAccess(token) {
|
|
665
|
+
checkToken(token);
|
|
666
|
+
return {
|
|
667
|
+
all: false,
|
|
668
|
+
pageIds: FAKE_PAGES.filter((page) => page.leadsAllowed).map(
|
|
669
|
+
(page) => page.pageId,
|
|
670
|
+
),
|
|
671
|
+
};
|
|
672
|
+
},
|
|
673
|
+
async subscribePage(pageToken, pageId) {
|
|
674
|
+
checkPageToken(pageToken, pageId);
|
|
675
|
+
leadPermission(fakePage(pageId));
|
|
676
|
+
fakeSubscriptions.add(pageId);
|
|
677
|
+
},
|
|
678
|
+
async leadForms(pageToken, pageId) {
|
|
679
|
+
checkPageToken(pageToken, pageId);
|
|
680
|
+
const page = leadPermission(fakePage(pageId));
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
return {
|
|
683
|
+
forms: page.forms.map((form) => ({
|
|
684
|
+
formId: form.formId,
|
|
685
|
+
name: form.name,
|
|
686
|
+
status: form.status,
|
|
687
|
+
questions: form.questions.map((question) => ({ ...question })),
|
|
688
|
+
whatsappButton: form.whatsappButton,
|
|
689
|
+
createdTime: formCreatedAt(form, now),
|
|
690
|
+
})),
|
|
691
|
+
usage: USAGE,
|
|
692
|
+
complete: true,
|
|
693
|
+
};
|
|
694
|
+
},
|
|
695
|
+
async lead(pageToken, leadgenId) {
|
|
696
|
+
checkToken(pageToken);
|
|
697
|
+
const match = /^7(\d{2})(\d{9})$/.exec(leadgenId);
|
|
698
|
+
const found = match
|
|
699
|
+
? FAKE_PAGES.flatMap((page) =>
|
|
700
|
+
page.forms
|
|
701
|
+
.filter((form) => form.serial === match[1])
|
|
702
|
+
.map((form) => ({ page, form })),
|
|
703
|
+
)[0]
|
|
704
|
+
: undefined;
|
|
705
|
+
const now = Date.now();
|
|
706
|
+
const lead =
|
|
707
|
+
found && found.form.everyMs > 0
|
|
708
|
+
? fakeLead(found.page, found.form, Number(match![2]))
|
|
709
|
+
: null;
|
|
710
|
+
if (
|
|
711
|
+
!found ||
|
|
712
|
+
!lead ||
|
|
713
|
+
pageIdOfToken(pageToken) !== found.page.pageId ||
|
|
714
|
+
lead.createdTime > now ||
|
|
715
|
+
lead.createdTime < formCreatedAt(found.form, now)
|
|
716
|
+
) {
|
|
717
|
+
throw new MetaGraphError(
|
|
718
|
+
"permission",
|
|
719
|
+
"Unsupported get request: the lead does not exist (fake)",
|
|
720
|
+
100,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return lead;
|
|
724
|
+
},
|
|
725
|
+
async formLeads(pageToken, formId, sinceSec, untilSec) {
|
|
726
|
+
checkToken(pageToken);
|
|
727
|
+
const found = fakeForm(formId);
|
|
728
|
+
if (!found || pageIdOfToken(pageToken) !== found.page.pageId) {
|
|
729
|
+
throw new MetaGraphError(
|
|
730
|
+
"permission",
|
|
731
|
+
"the form is not this Page's (fake)",
|
|
732
|
+
100,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
leadPermission(found.page);
|
|
736
|
+
const { page, form } = found;
|
|
737
|
+
const now = Date.now();
|
|
738
|
+
if (form.everyMs <= 0) return { leads: [], usage: USAGE, complete: true };
|
|
739
|
+
const from = Math.max(
|
|
740
|
+
sinceSec * 1000 + 1,
|
|
741
|
+
now - LEAD_HISTORY_MS,
|
|
742
|
+
formCreatedAt(form, now),
|
|
743
|
+
);
|
|
744
|
+
const to =
|
|
745
|
+
untilSec === undefined ? now : Math.min(now, untilSec * 1000 - 1);
|
|
746
|
+
const first = Math.max(
|
|
747
|
+
Math.ceil((from - form.offsetMs) / form.everyMs),
|
|
748
|
+
0,
|
|
749
|
+
);
|
|
750
|
+
const last = Math.floor((to - form.offsetMs) / form.everyMs);
|
|
751
|
+
// Meta lists the newest first and stops at its cap: a truncated
|
|
752
|
+
// listing holds the newest leads of the window, never the oldest.
|
|
753
|
+
const leads = [];
|
|
754
|
+
for (let k = last; k >= first; k -= 1) {
|
|
755
|
+
if (leads.length >= fakeLimits.maxLeads) break;
|
|
756
|
+
leads.push(fakeLead(page, form, k));
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
leads,
|
|
760
|
+
usage: USAGE,
|
|
761
|
+
complete: last - first + 1 <= fakeLimits.maxLeads,
|
|
762
|
+
};
|
|
763
|
+
},
|
|
280
764
|
};
|
|
281
765
|
}
|