@xuda.io/account_module 1.2.2309 → 1.2.2311
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/index.mjs +262 -126
- package/index_ms.mjs +4 -0
- package/index_msa.mjs +4 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -3535,6 +3535,7 @@ export const get_account_name = async function (req) {
|
|
|
3535
3535
|
active_account_profile_id,
|
|
3536
3536
|
network_country_code,
|
|
3537
3537
|
public_profile_disabled,
|
|
3538
|
+
verify_level,
|
|
3538
3539
|
} = data.data.account_info;
|
|
3539
3540
|
|
|
3540
3541
|
obj = {
|
|
@@ -3570,6 +3571,11 @@ export const get_account_name = async function (req) {
|
|
|
3570
3571
|
// email,// email?.[0] || '',
|
|
3571
3572
|
avatar_source,
|
|
3572
3573
|
active_account_profile_id,
|
|
3574
|
+
// The verification level of the account behind a card. Every caller that
|
|
3575
|
+
// builds a contact card off another account copies this onto the card doc
|
|
3576
|
+
// so the background tile can say what the level is; without it a card for
|
|
3577
|
+
// a verified person read as L0, which is a false statement about them.
|
|
3578
|
+
verify_level,
|
|
3573
3579
|
};
|
|
3574
3580
|
}
|
|
3575
3581
|
return { code: 1, data: obj }; //is_online: data?.data?.socket_id ? true : false
|
|
@@ -4247,6 +4253,7 @@ export const merge_contact = async function (req) {
|
|
|
4247
4253
|
}
|
|
4248
4254
|
|
|
4249
4255
|
const primary_contact_save_ret = await save_contact(uid, primary_contact_doc);
|
|
4256
|
+
log_contact_activity(uid, contact_to_merge, 'merged_in', { count: contact_to_delete.length, emails: primary_contact_doc.email, note: 'kept as the primary of a duplicate set' });
|
|
4250
4257
|
|
|
4251
4258
|
for (let contact_id of contact_to_delete) {
|
|
4252
4259
|
try {
|
|
@@ -4255,6 +4262,7 @@ export const merge_contact = async function (req) {
|
|
|
4255
4262
|
contact_doc.stat_ts = Date.now();
|
|
4256
4263
|
contact_doc.stat_reason = 'merge';
|
|
4257
4264
|
await save_contact(uid, contact_doc);
|
|
4265
|
+
log_contact_activity(uid, contact_id, 'merged_away', { into: contact_to_merge, note: 'folded into the primary duplicate and closed' });
|
|
4258
4266
|
} catch (error) {}
|
|
4259
4267
|
}
|
|
4260
4268
|
|
|
@@ -4303,6 +4311,13 @@ const level_pattern = function (doc) {
|
|
|
4303
4311
|
return `level-${level}-pattern.png`;
|
|
4304
4312
|
};
|
|
4305
4313
|
|
|
4314
|
+
// The level tile is the DEFAULT for every contact card, not a fallback. It used
|
|
4315
|
+
// to be computed first and then overwritten by a tile keyed off where the
|
|
4316
|
+
// contact came from (xu / authentic / fictional / email / web), so a card for a
|
|
4317
|
+
// verified person showed the generic XU tile and the level never appeared on any
|
|
4318
|
+
// card that had a contact_uid, which is every card for a real account. Only the
|
|
4319
|
+
// states that say the card cannot show a level at all still win: a shared card
|
|
4320
|
+
// wears its owner's avatar, an avatar being processed says so, and spam says so.
|
|
4306
4321
|
const get_contact_pattern = async function (doc) {
|
|
4307
4322
|
let ret = level_pattern(doc);
|
|
4308
4323
|
|
|
@@ -4323,84 +4338,12 @@ const get_contact_pattern = async function (doc) {
|
|
|
4323
4338
|
}
|
|
4324
4339
|
} else if (doc.is_spam) {
|
|
4325
4340
|
ret = `spam-pattern.png`;
|
|
4326
|
-
} else {
|
|
4327
|
-
// if (doc.account_type === 'business') {
|
|
4328
|
-
// // if (doc?.is_real_person) {
|
|
4329
|
-
// ret = doc.profile_picture;
|
|
4330
|
-
// // }
|
|
4331
|
-
// } else {
|
|
4332
|
-
// if (doc.my_contact) {
|
|
4333
|
-
// if (doc.account_type === 'business') {
|
|
4334
|
-
// ret = doc.profile_picture;
|
|
4335
|
-
// } else {
|
|
4336
|
-
// switch (doc.avatar_source) {
|
|
4337
|
-
// case 'fictional':
|
|
4338
|
-
// ret = `fictional-avatar-pattern.png`;
|
|
4339
|
-
// break;
|
|
4340
|
-
|
|
4341
|
-
// case 'ai profile':
|
|
4342
|
-
// ret = `fictional-avatar-pattern.png`;
|
|
4343
|
-
// break;
|
|
4344
|
-
|
|
4345
|
-
// case 'authentic profile':
|
|
4346
|
-
// ret = `authentic-avatar-pattern.png`;
|
|
4347
|
-
// break;
|
|
4348
|
-
|
|
4349
|
-
// default:
|
|
4350
|
-
// ret = `xu-pattern.png`;
|
|
4351
|
-
// break;
|
|
4352
|
-
// }
|
|
4353
|
-
// }
|
|
4354
|
-
// } else
|
|
4355
|
-
|
|
4356
|
-
// if (doc?.shared_from_uid) {
|
|
4357
|
-
// //&& (doc.pending || (doc.team_req_id && doc.contact_share_copy))) {
|
|
4358
|
-
// // pending or active share request
|
|
4359
|
-
// const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
|
|
4360
|
-
// ret = shared_from_uid_ret.data.profile_avatar;
|
|
4361
|
-
// }
|
|
4362
|
-
|
|
4363
|
-
// else if (doc?.reference_doc?.shared_from_uid) {
|
|
4364
|
-
// //active share
|
|
4365
|
-
// const shared_from_uid_ret = await get_account_name({ uid_query: doc.reference_doc.shared_from_uid });
|
|
4366
|
-
// ret = shared_from_uid_ret.data.profile_avatar;
|
|
4367
|
-
// }
|
|
4368
|
-
// else
|
|
4369
|
-
if (doc.contact_uid) {
|
|
4370
|
-
switch (doc.avatar_source) {
|
|
4371
|
-
case 'fictional':
|
|
4372
|
-
ret = `fictional-avatar-pattern.png`;
|
|
4373
|
-
break;
|
|
4374
|
-
|
|
4375
|
-
case 'ai profile':
|
|
4376
|
-
ret = `fictional-avatar-pattern.png`;
|
|
4377
|
-
break;
|
|
4378
|
-
|
|
4379
|
-
case 'authentic profile':
|
|
4380
|
-
ret = `authentic-avatar-pattern.png`;
|
|
4381
|
-
break;
|
|
4382
|
-
|
|
4383
|
-
default:
|
|
4384
|
-
ret = `xu-pattern.png`;
|
|
4385
|
-
break;
|
|
4386
|
-
}
|
|
4387
|
-
} else {
|
|
4388
|
-
switch (doc.source) {
|
|
4389
|
-
case 'read emails': {
|
|
4390
|
-
ret = `email-pattern.png`;
|
|
4391
|
-
break;
|
|
4392
|
-
}
|
|
4393
|
-
case 'web': {
|
|
4394
|
-
ret = `web-pattern.png`;
|
|
4395
|
-
break;
|
|
4396
|
-
}
|
|
4397
|
-
|
|
4398
|
-
default:
|
|
4399
|
-
break;
|
|
4400
|
-
}
|
|
4401
|
-
}
|
|
4402
|
-
// }
|
|
4403
4341
|
}
|
|
4342
|
+
// Everything else keeps the level tile. The old where-it-came-from tiles
|
|
4343
|
+
// (xu / authentic / fictional for an account, email / web for a scraped
|
|
4344
|
+
// contact) are gone on purpose: they occupied the one surface that now
|
|
4345
|
+
// carries the verification level, and a contact with no account behind it
|
|
4346
|
+
// has no level to show, which is exactly what L0 says.
|
|
4404
4347
|
return ret;
|
|
4405
4348
|
};
|
|
4406
4349
|
|
|
@@ -4568,6 +4511,10 @@ export const get_contact_info = async function (uid, contact_doc, _id) {
|
|
|
4568
4511
|
}
|
|
4569
4512
|
|
|
4570
4513
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
4514
|
+
// The card's background tile is the contact's verification level, and the
|
|
4515
|
+
// level lives on the account, not on my copy of them. Carry it over here,
|
|
4516
|
+
// where the account is already in hand, so the tile costs no extra read.
|
|
4517
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
4571
4518
|
// doc.icon_pattern = await get_contact_pattern(doc);
|
|
4572
4519
|
doc.username = account_info_ret.data.username;
|
|
4573
4520
|
doc.business_name = account_info_ret.data.business_name;
|
|
@@ -4632,6 +4579,7 @@ export const get_pending_contact_out = async function (uid, _id) {
|
|
|
4632
4579
|
doc.account_type = account_info_ret.data.account_type;
|
|
4633
4580
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
4634
4581
|
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
4582
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
4635
4583
|
doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
4636
4584
|
}
|
|
4637
4585
|
doc.icon_pattern = await get_contact_pattern(doc);
|
|
@@ -4663,6 +4611,7 @@ export const get_pending_contact_in = async function (uid, _id) {
|
|
|
4663
4611
|
doc.account_type = account_info_ret.data.account_type;
|
|
4664
4612
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
4665
4613
|
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
4614
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
4666
4615
|
doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
4667
4616
|
}
|
|
4668
4617
|
doc.card_background = get_contact_background({ pending: true });
|
|
@@ -4702,6 +4651,7 @@ const get_user_contact = async function (uid, uid_query) {
|
|
|
4702
4651
|
doc.profile_avatar = account_info.profile_avatar;
|
|
4703
4652
|
doc.profile_picture = account_info.profile_picture;
|
|
4704
4653
|
doc.avatar_source = account_info.avatar_source;
|
|
4654
|
+
doc.verify_level = account_info.verify_level;
|
|
4705
4655
|
doc.card_background = get_contact_background(doc);
|
|
4706
4656
|
doc.icon_pattern = await get_contact_pattern(doc);
|
|
4707
4657
|
doc.border = get_contact_border(doc);
|
|
@@ -4753,7 +4703,9 @@ export const update_contact = async function (req) {
|
|
|
4753
4703
|
doc.profile_avatar = account_obj.account_info.profile_avatar;
|
|
4754
4704
|
}
|
|
4755
4705
|
|
|
4756
|
-
|
|
4706
|
+
const save_ret = await save_contact(uid, doc);
|
|
4707
|
+
log_contact_activity(uid, doc._id, 'updated', { stat, team_req_id, linked_account: contact_uid, note: contact_uid ? 'picture and avatar taken from the linked Xuda account' : undefined });
|
|
4708
|
+
return save_ret;
|
|
4757
4709
|
}
|
|
4758
4710
|
return { code: -1, data: 'contact not found' };
|
|
4759
4711
|
};
|
|
@@ -5614,28 +5566,18 @@ setTimeout(async () => {
|
|
|
5614
5566
|
|
|
5615
5567
|
//////// CONTACTS //////////////
|
|
5616
5568
|
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
'yahoo.com': 'Yahoo Mail',
|
|
5625
|
-
'ymail.com': 'Yahoo Mail',
|
|
5626
|
-
'aol.com': 'AOL Mail',
|
|
5627
|
-
'icloud.com': 'Apple iCloud',
|
|
5628
|
-
'me.com': 'Apple iCloud',
|
|
5629
|
-
'mac.com': 'Apple iCloud',
|
|
5630
|
-
'protonmail.com': 'ProtonMail',
|
|
5631
|
-
'proton.me': 'ProtonMail',
|
|
5632
|
-
'zoho.com': 'Zoho Mail',
|
|
5633
|
-
'yandex.com': 'Yandex Mail',
|
|
5634
|
-
'mail.com': 'Mail.com',
|
|
5635
|
-
'gmx.com': 'GMX Mail',
|
|
5636
|
-
};
|
|
5569
|
+
// A "business" that resolves to the address's own mail provider is not the contact's
|
|
5570
|
+
// employer, it is Gmail. get_business_info, handed a bare gmail.com address, answers
|
|
5571
|
+
// "Gmail / gmail.com / Technology" rather than "Not available", so the only escape hatch the
|
|
5572
|
+
// classification had (business_name === 'Not available') never fired and every personal
|
|
5573
|
+
// Gmail contact came out as a business, storefront placeholder and all. This is the second
|
|
5574
|
+
// escape hatch, built on the provider list that already exists directly below.
|
|
5575
|
+
const businessIsJustTheMailProvider = (business_info) => !!identifyPersonalProvider(business_info?.business_domain || '');
|
|
5637
5576
|
|
|
5638
|
-
|
|
5577
|
+
function identifyPersonalProvider(domain) {
|
|
5578
|
+
// The list moved to common so ai_module's contact classifier reads the same one. It used to
|
|
5579
|
+
// live here alone, which is exactly why the classifier could call gmail.com a business.
|
|
5580
|
+
return _common.personal_email_provider(domain);
|
|
5639
5581
|
}
|
|
5640
5582
|
|
|
5641
5583
|
export const add_contact = async function (req, job_id, headers) {
|
|
@@ -5663,6 +5605,16 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5663
5605
|
|
|
5664
5606
|
/////////////////
|
|
5665
5607
|
|
|
5608
|
+
// Minted up front rather than at save time, so every classification step
|
|
5609
|
+
// below can file its verdict under the contact it is deciding about. The
|
|
5610
|
+
// steps run BEFORE the doc exists, which is exactly why their reasoning
|
|
5611
|
+
// used to be unrecoverable.
|
|
5612
|
+
const contact_id = await _common.xuda_get_uuid('contact');
|
|
5613
|
+
const app_id = account_profile_info.app_id;
|
|
5614
|
+
const activity = (event, detail) => log_contact_activity(uid, contact_id, event, detail, app_id);
|
|
5615
|
+
|
|
5616
|
+
await activity('created', { source: source || 'unknown', email: email.toLowerCase(), name: name || '', from_account: !!contact_uid, subject: metadata.subject, profile_id: account_profile_obj?._id });
|
|
5617
|
+
|
|
5666
5618
|
let is_spam = false;
|
|
5667
5619
|
// let email_type_info;
|
|
5668
5620
|
// let account_type_info;
|
|
@@ -5679,27 +5631,45 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5679
5631
|
}
|
|
5680
5632
|
|
|
5681
5633
|
set_account_profile_picture(uid, contact_uid, metadata, job_id, headers, account_profile_info);
|
|
5634
|
+
await activity('linked_account', { contact_uid });
|
|
5682
5635
|
} else {
|
|
5683
5636
|
is_spam = await isLikelySpamEmail(email);
|
|
5637
|
+
if (is_spam) await activity('spam_check', { by: 'pattern', is_spam: true, note: 'address matched the local spam pattern list, no AI step ran' });
|
|
5684
5638
|
|
|
5685
5639
|
// contact without use account
|
|
5686
5640
|
cached_contact = await get_xuda_cache(uid, 'contact', email, 'metadata');
|
|
5641
|
+
if (!_.isEmpty(cached_contact)) {
|
|
5642
|
+
// Every classification below is SKIPPED on a cache hit, which is the single
|
|
5643
|
+
// most confusing thing about the result: the contact inherits a verdict that
|
|
5644
|
+
// was reached for an earlier contact on the same address or domain.
|
|
5645
|
+
await activity('classification_reused', { from_cache: true, key: email.toLowerCase(), account_type: cached_contact.account_type, business_name: cached_contact?.business_info?.business_name, is_spam: cached_contact.is_spam });
|
|
5646
|
+
}
|
|
5687
5647
|
|
|
5688
5648
|
if (_.isEmpty(cached_contact)) {
|
|
5689
5649
|
if (!is_spam && metadata.subject && !metadata.is_sent && !metadata.is_answered && !metadata.not_junk) {
|
|
5690
5650
|
// deep search for spam
|
|
5691
5651
|
is_spam = await ai_ms.is_spam_email(uid, email, metadata.subject, account_profile_info);
|
|
5652
|
+
await activity('spam_check', { by: 'ai', is_spam: !!is_spam, subject: metadata.subject });
|
|
5692
5653
|
}
|
|
5693
5654
|
if (!is_spam) {
|
|
5694
|
-
const
|
|
5655
|
+
const verdict = await ai_ms.is_business_contact(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
5656
|
+
// Tolerates the old bare boolean, in case a stale ai_module answers this call.
|
|
5657
|
+
const is_business = typeof verdict === 'object' && verdict !== null ? verdict.is_business : !!verdict;
|
|
5695
5658
|
account_type = is_business ? 'business' : 'personal';
|
|
5659
|
+
await activity('classified', { by: 'ai', account_type, confidence: verdict?.confidence, why: verdict?.reason, inputs: { email: email.toLowerCase(), name: name || '', subject: metadata.subject || '' } });
|
|
5696
5660
|
if (is_business) {
|
|
5697
5661
|
business_info = await ai_ms.get_business_info(uid, '', email, account_profile_info, { light: true });
|
|
5698
|
-
if (business_info.business_name !== 'Not available') {
|
|
5662
|
+
if (business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(business_info)) {
|
|
5663
|
+
await activity('business_info', { by: 'ai', depth: 'light', business_name: business_info.business_name, business_domain: business_info.business_domain, business_category: business_info.business_category });
|
|
5699
5664
|
business_has_person = await ai_ms.is_business_contact_has_person(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
5665
|
+
await activity('business_has_person', { by: 'ai', has_person: !!business_has_person });
|
|
5700
5666
|
} else {
|
|
5667
|
+
const provider = identifyPersonalProvider(business_info?.business_domain || '');
|
|
5701
5668
|
account_type = 'personal';
|
|
5702
5669
|
business_info = undefined;
|
|
5670
|
+
// The reversal matters more than the original verdict: the card says
|
|
5671
|
+
// personal even though the business call answered yes.
|
|
5672
|
+
await activity('classified', { by: 'ai', account_type, reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'no business could be identified behind the address' });
|
|
5703
5673
|
}
|
|
5704
5674
|
}
|
|
5705
5675
|
if (_.isEmpty(cached_contact) && account_type === 'business' && !business_has_person) {
|
|
@@ -5708,6 +5678,9 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5708
5678
|
} else {
|
|
5709
5679
|
cached_contact = await get_xuda_cache(uid, 'contact', business_info.business_name, 'metadata');
|
|
5710
5680
|
}
|
|
5681
|
+
if (!_.isEmpty(cached_contact)) {
|
|
5682
|
+
await activity('classification_reused', { from_cache: true, key: business_info.business_domain || business_info.business_name, account_type: cached_contact.account_type, business_name: cached_contact?.business_info?.business_name });
|
|
5683
|
+
}
|
|
5711
5684
|
}
|
|
5712
5685
|
}
|
|
5713
5686
|
}
|
|
@@ -5715,7 +5688,7 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5715
5688
|
|
|
5716
5689
|
const d = Date.now();
|
|
5717
5690
|
const contact_obj = {
|
|
5718
|
-
_id:
|
|
5691
|
+
_id: contact_id,
|
|
5719
5692
|
email: email.toLowerCase(),
|
|
5720
5693
|
name: cached_contact?.name || name,
|
|
5721
5694
|
stat: stat || 3,
|
|
@@ -5742,13 +5715,16 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5742
5715
|
if (!is_spam) {
|
|
5743
5716
|
if (account_type === 'personal' || contact_obj?.business_has_person) {
|
|
5744
5717
|
contact_obj.person_info = await ai_ms.get_person_info(uid, contact_obj.name, contact_obj.email, account_profile_info, metadata?.summarized_body, { light: true });
|
|
5718
|
+
await activity('person_info', { by: 'ai', depth: 'light', full_name: contact_obj.person_info?.full_name, job_title: contact_obj.person_info?.job_title, company: contact_obj.person_info?.company_name });
|
|
5745
5719
|
if (!contact_obj.name) {
|
|
5746
5720
|
contact_obj.name = await ai_ms.get_name_from_email_addr(uid, contact_obj.email, account_profile_info);
|
|
5721
|
+
await activity('name_resolved', { by: 'ai', from: 'email address', name: contact_obj.name });
|
|
5747
5722
|
}
|
|
5748
5723
|
}
|
|
5749
5724
|
|
|
5750
5725
|
const conversation_obj = await ai_ms.create_openai_conversation();
|
|
5751
5726
|
contact_obj.contact_reference_conversation_id = conversation_obj.id;
|
|
5727
|
+
await activity('ai_thread_opened', { conversation_id: conversation_obj.id });
|
|
5752
5728
|
} else {
|
|
5753
5729
|
if (!contact_obj.name) {
|
|
5754
5730
|
contact_obj.name = '';
|
|
@@ -5758,6 +5734,10 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5758
5734
|
const save_ret = await save_contact(uid, contact_obj);
|
|
5759
5735
|
save_xuda_cache(uid, 'contact', contact_obj.email, null, contact_obj);
|
|
5760
5736
|
|
|
5737
|
+
// The saved verdict, in one row, so the trail ends on the answer the card
|
|
5738
|
+
// actually shows rather than making the reader replay the steps above.
|
|
5739
|
+
await activity('saved', { account_type: contact_obj.account_type || 'unset', is_spam: !!contact_obj.is_spam, business_name: contact_obj?.business_info?.business_name, name: contact_obj.name || '' });
|
|
5740
|
+
|
|
5761
5741
|
if (!contact_obj.is_spam && contact_obj.name) {
|
|
5762
5742
|
set_contact_profile_picture(uid, contact_obj._id, metadata, job_id, headers, account_profile_info, false);
|
|
5763
5743
|
}
|
|
@@ -5821,6 +5801,7 @@ const set_contact_profile_picture = async function (uid, contact_id, metadata, j
|
|
|
5821
5801
|
contact_obj.profile_picture_source = file_ret.profile_picture_source;
|
|
5822
5802
|
|
|
5823
5803
|
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
5804
|
+
log_contact_activity(uid, contact_id, 'picture_found', { by: cache ? 'cache' : 'ai', source: file_ret.profile_picture_source, account_type: contact_obj.account_type || 'unset' });
|
|
5824
5805
|
// profile_picture = contact_obj.profile_picture;
|
|
5825
5806
|
}
|
|
5826
5807
|
}
|
|
@@ -5905,10 +5886,14 @@ const set_contact_profile_picture = async function (uid, contact_id, metadata, j
|
|
|
5905
5886
|
contact_obj.profile_avatar = contact_obj?.profile_avatar_obj?.file_url;
|
|
5906
5887
|
contact_obj.avatar_source = file_ret?.data?.avatar_source;
|
|
5907
5888
|
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
5889
|
+
log_contact_activity(uid, contact_id, 'avatar_ready', { by: cache ? 'cache' : 'ai', source: contact_obj.avatar_source, account_type: contact_obj.account_type || 'unset' });
|
|
5908
5890
|
}
|
|
5909
5891
|
await update_contact_profile_picture_status(uid, contact_id, 3);
|
|
5910
5892
|
} catch (err) {
|
|
5911
5893
|
await update_contact_profile_picture_status(uid, contact_id, 1, err.message);
|
|
5894
|
+
// Worth a row of its own: a contact stuck on the shop / silhouette placeholder
|
|
5895
|
+
// looks like a classification result, and this is the line that says otherwise.
|
|
5896
|
+
log_contact_activity(uid, contact_id, 'picture_failed', { error: err?.message });
|
|
5912
5897
|
delete_xuda_cache(contact_obj);
|
|
5913
5898
|
}
|
|
5914
5899
|
};
|
|
@@ -6141,6 +6126,7 @@ export const archive_contact = async function (req) {
|
|
|
6141
6126
|
contact_doc.stat_reason = 'archived by the user';
|
|
6142
6127
|
|
|
6143
6128
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6129
|
+
log_contact_activity(uid, contact_id, 'archived', { by: 'user', reason: contact_doc.stat_reason });
|
|
6144
6130
|
|
|
6145
6131
|
return contact_save_ret;
|
|
6146
6132
|
} catch (err) {
|
|
@@ -6163,6 +6149,7 @@ export const delete_contact = async function (req, job_id, headers) {
|
|
|
6163
6149
|
contact_doc.stat_reason = 'deleted by the user';
|
|
6164
6150
|
|
|
6165
6151
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6152
|
+
log_contact_activity(uid, contact_id, 'deleted', { by: 'user', reason: contact_doc.stat_reason, note: 'conversations attached to this contact were deleted with it' });
|
|
6166
6153
|
|
|
6167
6154
|
ai_msa.delete_depended_chats(uid, contact_id);
|
|
6168
6155
|
return contact_save_ret;
|
|
@@ -6188,6 +6175,7 @@ export const unarchive_contact = async function (req, job_id, headers) {
|
|
|
6188
6175
|
contact_doc.stat_reason = 'unarchive by the user';
|
|
6189
6176
|
|
|
6190
6177
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6178
|
+
log_contact_activity(uid, contact_id, 'unarchived', { by: 'user', reason: contact_doc.stat_reason });
|
|
6191
6179
|
|
|
6192
6180
|
if (!contact_doc.is_spam) {
|
|
6193
6181
|
not_spam_contact(req, job_id, headers);
|
|
@@ -6217,6 +6205,7 @@ export const unfriend_contact = async function (req) {
|
|
|
6217
6205
|
|
|
6218
6206
|
contact_doc.team_req_id = null;
|
|
6219
6207
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6208
|
+
log_contact_activity(uid, contact_id, 'unfriended', { by: 'user' });
|
|
6220
6209
|
|
|
6221
6210
|
const req_save_ret = await db_module.save_couch_doc('xuda_team', req_doc);
|
|
6222
6211
|
|
|
@@ -6236,6 +6225,7 @@ export const pin_contact = async function (req) {
|
|
|
6236
6225
|
contact_doc.pinned = true;
|
|
6237
6226
|
|
|
6238
6227
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6228
|
+
log_contact_activity(uid, contact_id, 'pinned', { by: 'user' });
|
|
6239
6229
|
|
|
6240
6230
|
ws_dashboard_msa.emit_message_to_dashboard({
|
|
6241
6231
|
service: 'contact_pinned',
|
|
@@ -6280,6 +6270,7 @@ export const unpin_contact = async function (req) {
|
|
|
6280
6270
|
contact_doc.pinned = false;
|
|
6281
6271
|
|
|
6282
6272
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6273
|
+
log_contact_activity(uid, contact_id, 'unpinned', { by: 'user' });
|
|
6283
6274
|
|
|
6284
6275
|
ws_dashboard_msa.emit_message_to_dashboard({
|
|
6285
6276
|
service: 'contact_unpinned',
|
|
@@ -6311,22 +6302,42 @@ export const not_spam_contact = async function (req, job_id, headers) {
|
|
|
6311
6302
|
stat: 3,
|
|
6312
6303
|
});
|
|
6313
6304
|
}
|
|
6305
|
+
const was_spam = !!contact_doc.is_spam;
|
|
6314
6306
|
contact_doc.is_spam = false;
|
|
6307
|
+
log_contact_activity(uid, contact_id, 'marked_not_spam', { by: 'user', was_spam, note: was_spam ? 'address added to the spam whitelist, classification is being redone' : 'classification is being redone' });
|
|
6315
6308
|
|
|
6316
6309
|
if (!contact_doc.contact_reference_conversation_id) {
|
|
6317
6310
|
const conversation_obj = await ai_ms.create_openai_conversation();
|
|
6318
6311
|
contact_doc.contact_reference_conversation_id = conversation_obj.id;
|
|
6312
|
+
await log_contact_activity(uid, contact_id, 'ai_thread_opened', { conversation_id: conversation_obj.id });
|
|
6319
6313
|
}
|
|
6320
6314
|
|
|
6321
|
-
const
|
|
6315
|
+
const previous_account_type = contact_doc.account_type;
|
|
6316
|
+
const verdict = await ai_ms.is_business_contact(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
|
|
6317
|
+
const is_business = typeof verdict === 'object' && verdict !== null ? verdict.is_business : !!verdict;
|
|
6322
6318
|
contact_doc.account_type = is_business ? 'business' : 'personal';
|
|
6319
|
+
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', trigger: 'not spam', account_type: contact_doc.account_type, confidence: verdict?.confidence, why: verdict?.reason, previous_account_type: previous_account_type || 'unset' });
|
|
6320
|
+
if (!is_business && (contact_doc.business_info || contact_doc.business_has_person)) {
|
|
6321
|
+
// A contact that reclassifies from business to person used to keep the company profile
|
|
6322
|
+
// the old verdict attached to it, because business_info is only ever written inside the
|
|
6323
|
+
// is_business branch and nothing clears it on the way out. The card then showed a person
|
|
6324
|
+
// with a company still on file.
|
|
6325
|
+
const dropped = contact_doc?.business_info?.business_name;
|
|
6326
|
+
contact_doc.business_info = undefined;
|
|
6327
|
+
contact_doc.business_has_person = undefined;
|
|
6328
|
+
await log_contact_activity(uid, contact_id, 'business_info_cleared', { reason: 'contact reclassified as a person', dropped });
|
|
6329
|
+
}
|
|
6323
6330
|
if (is_business) {
|
|
6324
6331
|
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info, { light: true });
|
|
6325
|
-
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
6332
|
+
if (contact_doc.business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(contact_doc.business_info)) {
|
|
6333
|
+
await log_contact_activity(uid, contact_id, 'business_info', { by: 'ai', depth: 'light', business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
|
|
6326
6334
|
contact_doc.business_has_person = await ai_ms.is_business_contact_has_person(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
|
|
6335
|
+
await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
|
|
6327
6336
|
} else {
|
|
6337
|
+
const provider = identifyPersonalProvider(contact_doc.business_info?.business_domain || '');
|
|
6328
6338
|
contact_doc.account_type = 'personal';
|
|
6329
6339
|
contact_doc.business_info = undefined;
|
|
6340
|
+
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'no business could be identified behind the address' });
|
|
6330
6341
|
}
|
|
6331
6342
|
}
|
|
6332
6343
|
|
|
@@ -6362,6 +6373,7 @@ export const not_spam_contact = async function (req, job_id, headers) {
|
|
|
6362
6373
|
req.email_id = email._id;
|
|
6363
6374
|
await email_ms.process_pending_email(req, job_id, headers);
|
|
6364
6375
|
}
|
|
6376
|
+
if (emails.docs.length) log_contact_activity(uid, contact_id, 'emails_reprocessed', { count: emails.docs.length, trigger: 'not spam', note: 'partially processed emails were re-read in full' });
|
|
6365
6377
|
|
|
6366
6378
|
return contact_save_ret;
|
|
6367
6379
|
} catch (err) {
|
|
@@ -6390,6 +6402,7 @@ export const generate_contact_avatar = async function (req, job_id, headers) {
|
|
|
6390
6402
|
|
|
6391
6403
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6392
6404
|
await delete_xuda_cache(contact_doc);
|
|
6405
|
+
log_contact_activity(uid, contact_id, 'avatar_regenerate_requested', { by: 'user', note: 'existing picture and avatar cleared, a new avatar is being generated' });
|
|
6393
6406
|
set_contact_profile_picture(uid, contact_doc._id, {}, job_id, headers, account_profile_info, true);
|
|
6394
6407
|
|
|
6395
6408
|
return contact_save_ret;
|
|
@@ -6407,21 +6420,28 @@ export const set_deep_research_contact = async function (req, job_id, headers) {
|
|
|
6407
6420
|
try {
|
|
6408
6421
|
var contact_doc = await get_contact(uid, contact_id);
|
|
6409
6422
|
contact_doc.deep_research = true;
|
|
6423
|
+
await log_contact_activity(uid, contact_id, 'deep_research_started', { by: 'user', account_type: contact_doc.account_type || 'unset' });
|
|
6410
6424
|
|
|
6411
6425
|
if (contact_doc.account_type === 'business') {
|
|
6412
6426
|
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info);
|
|
6413
|
-
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
6427
|
+
if (contact_doc.business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(contact_doc.business_info)) {
|
|
6428
|
+
await log_contact_activity(uid, contact_id, 'business_info', { by: 'ai', depth: 'deep', business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
|
|
6414
6429
|
contact_doc.business_has_person = await ai_ms.is_business_contact_has_person(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
|
|
6430
|
+
await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
|
|
6415
6431
|
} else {
|
|
6432
|
+
const provider = identifyPersonalProvider(contact_doc.business_info?.business_domain || '');
|
|
6416
6433
|
contact_doc.account_type = 'personal';
|
|
6417
6434
|
contact_doc.business_info = undefined;
|
|
6435
|
+
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'deep research found no business behind the address' });
|
|
6418
6436
|
}
|
|
6419
6437
|
}
|
|
6420
6438
|
|
|
6421
6439
|
if (contact_doc.account_type === 'personal' || contact_doc?.business_has_person) {
|
|
6422
6440
|
contact_doc.person_info = await ai_ms.get_person_info(uid, contact_doc.name, contact_doc.email, account_profile_info, contact_doc?.metadata?.summarized_body);
|
|
6441
|
+
await log_contact_activity(uid, contact_id, 'person_info', { by: 'ai', depth: 'deep', full_name: contact_doc.person_info?.full_name, job_title: contact_doc.person_info?.job_title, company: contact_doc.person_info?.company_name });
|
|
6423
6442
|
if (!contact_doc.name) {
|
|
6424
6443
|
contact_doc.name = await ai_ms.get_name_from_email_addr(uid, contact_doc.email, account_profile_info);
|
|
6444
|
+
await log_contact_activity(uid, contact_id, 'name_resolved', { by: 'ai', from: 'email address', name: contact_doc.name });
|
|
6425
6445
|
}
|
|
6426
6446
|
}
|
|
6427
6447
|
|
|
@@ -6456,6 +6476,8 @@ export const set_deep_research_contact = async function (req, job_id, headers) {
|
|
|
6456
6476
|
throw err;
|
|
6457
6477
|
}
|
|
6458
6478
|
}
|
|
6479
|
+
if (conversation_items.docs.length) log_contact_activity(uid, contact_id, 'attachments_transcribed', { count: conversation_items.docs.length, trigger: 'deep research' });
|
|
6480
|
+
log_contact_activity(uid, contact_id, 'deep_research_finished', { account_type: contact_doc.account_type || 'unset', business_name: contact_doc?.business_info?.business_name });
|
|
6459
6481
|
|
|
6460
6482
|
return contact_save_ret;
|
|
6461
6483
|
} catch (err) {
|
|
@@ -6475,6 +6497,7 @@ export const unset_deep_research_contact = async function (req, job_id, headers)
|
|
|
6475
6497
|
|
|
6476
6498
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6477
6499
|
await delete_xuda_cache(contact_doc);
|
|
6500
|
+
log_contact_activity(uid, contact_id, 'deep_research_turned_off', { by: 'user' });
|
|
6478
6501
|
|
|
6479
6502
|
return contact_save_ret;
|
|
6480
6503
|
} catch (err) {
|
|
@@ -6484,6 +6507,89 @@ export const unset_deep_research_contact = async function (req, job_id, headers)
|
|
|
6484
6507
|
};
|
|
6485
6508
|
}
|
|
6486
6509
|
};
|
|
6510
|
+
|
|
6511
|
+
// The trail for one contact, newest first.
|
|
6512
|
+
//
|
|
6513
|
+
// Recorded rows are only half the answer: every contact that existed before the
|
|
6514
|
+
// trail did has none, and those are most of them. So the contact doc is read back
|
|
6515
|
+
// into the same shape first (derived: true). The doc keeps one timestamp per
|
|
6516
|
+
// FIELD, not per step, so a derived row is stamped with the closest timestamp it
|
|
6517
|
+
// honestly has (date_created for the creation and the classification that ran
|
|
6518
|
+
// inside it, profile_avatar_stat_ts for the picture, stat_ts for the archive) and
|
|
6519
|
+
// says derived so the UI can mark it as reconstructed rather than observed.
|
|
6520
|
+
export const get_contact_activity = async function (req) {
|
|
6521
|
+
const { uid, contact_id } = req;
|
|
6522
|
+
try {
|
|
6523
|
+
if (!contact_id) throw new Error('contact_id is missing');
|
|
6524
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
6525
|
+
// get_app_couch_doc_native THROWS a bare couch 'missing' for an id that is not in
|
|
6526
|
+
// this account's app db (an id from another account is the common way to get here),
|
|
6527
|
+
// so the not-found check below never ran and the caller saw "missing" with nothing
|
|
6528
|
+
// to act on. Catch it and answer the question that was actually asked.
|
|
6529
|
+
let contact_doc;
|
|
6530
|
+
try {
|
|
6531
|
+
contact_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
|
|
6532
|
+
} catch (_) {
|
|
6533
|
+
contact_doc = null;
|
|
6534
|
+
}
|
|
6535
|
+
if (!contact_doc || contact_doc.docType !== 'contact') throw new Error(`contact ${contact_id} not found`);
|
|
6536
|
+
if (contact_doc.uid !== uid && contact_doc?.uid_created !== uid) throw new Error('Operation not allowed');
|
|
6537
|
+
|
|
6538
|
+
const recorded_ret = await db_module.find_app_couch_query(account_profile_info.app_id, {
|
|
6539
|
+
selector: { docType: 'contact_activity', contact_id },
|
|
6540
|
+
limit: 500,
|
|
6541
|
+
});
|
|
6542
|
+
const recorded = (recorded_ret?.docs || []).map((d) => ({ event: d.event, detail: d.detail || {}, ts: d.ts, derived: false }));
|
|
6543
|
+
|
|
6544
|
+
const derived = [];
|
|
6545
|
+
const add = (event, ts, detail) => {
|
|
6546
|
+
if (!ts) return;
|
|
6547
|
+
// A recorded row always wins: it has the real timestamp and the real inputs.
|
|
6548
|
+
if (recorded.some((r) => r.event === event)) return;
|
|
6549
|
+
derived.push({ event, detail, ts, derived: true });
|
|
6550
|
+
};
|
|
6551
|
+
|
|
6552
|
+
const created_ts = contact_doc.date_created || contact_doc.ts;
|
|
6553
|
+
add('created', created_ts, { source: contact_doc.source || 'unknown', email: contact_doc.email, name: contact_doc.name || '', from_account: !!contact_doc.contact_uid, subject: contact_doc?.metadata?.subject });
|
|
6554
|
+
if (contact_doc.is_spam) add('spam_check', created_ts, { is_spam: true });
|
|
6555
|
+
if (contact_doc.account_type) add('classified', created_ts, { account_type: contact_doc.account_type });
|
|
6556
|
+
if (contact_doc?.business_info?.business_name) add('business_info', created_ts, { business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
|
|
6557
|
+
if (contact_doc.business_has_person !== undefined && contact_doc.business_has_person !== null) add('business_has_person', created_ts, { has_person: !!contact_doc.business_has_person });
|
|
6558
|
+
if (contact_doc?.person_info) add('person_info', created_ts, { full_name: contact_doc.person_info.full_name || contact_doc.person_info.person_full_name, job_title: contact_doc.person_info.job_title, company: contact_doc.person_info.company_name });
|
|
6559
|
+
if (contact_doc.contact_reference_conversation_id) add('ai_thread_opened', created_ts, { conversation_id: contact_doc.contact_reference_conversation_id });
|
|
6560
|
+
if (contact_doc.profile_picture) add('picture_found', contact_doc.profile_avatar_stat_ts || created_ts, { source: contact_doc.profile_picture_source });
|
|
6561
|
+
if (contact_doc.profile_avatar) add('avatar_ready', contact_doc.profile_avatar_stat_ts || created_ts, { source: contact_doc.avatar_source });
|
|
6562
|
+
if (contact_doc.profile_avatar_stat === 1 && contact_doc.profile_avatar_error) add('picture_failed', contact_doc.profile_avatar_stat_ts, { error: contact_doc.profile_avatar_error });
|
|
6563
|
+
if (contact_doc.deep_research) add('deep_research_started', contact_doc.stat_ts || created_ts, {});
|
|
6564
|
+
if (contact_doc.pinned) add('pinned', contact_doc.stat_ts || created_ts, {});
|
|
6565
|
+
// stat carries only the LAST transition, which is why an archive/unarchive
|
|
6566
|
+
// history is exactly what the recorded trail adds from here on.
|
|
6567
|
+
if (contact_doc.stat === 5) add('archived', contact_doc.stat_ts, { reason: contact_doc.stat_reason });
|
|
6568
|
+
if (contact_doc.stat === 4) add('deleted', contact_doc.stat_ts, { reason: contact_doc.stat_reason });
|
|
6569
|
+
|
|
6570
|
+
const rows = [...recorded, ...derived].sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
|
6571
|
+
|
|
6572
|
+
return {
|
|
6573
|
+
code: 1,
|
|
6574
|
+
data: {
|
|
6575
|
+
contact_id,
|
|
6576
|
+
// What the card shows today, so the UI can head the trail with the outcome.
|
|
6577
|
+
current: {
|
|
6578
|
+
account_type: contact_doc.account_type || null,
|
|
6579
|
+
is_spam: !!contact_doc.is_spam,
|
|
6580
|
+
stat: contact_doc.stat,
|
|
6581
|
+
source: contact_doc.source || null,
|
|
6582
|
+
deep_research: !!contact_doc.deep_research,
|
|
6583
|
+
business_name: contact_doc?.business_info?.business_name || null,
|
|
6584
|
+
profile_avatar_stat: contact_doc.profile_avatar_stat,
|
|
6585
|
+
},
|
|
6586
|
+
rows,
|
|
6587
|
+
},
|
|
6588
|
+
};
|
|
6589
|
+
} catch (err) {
|
|
6590
|
+
return { code: -25, data: err.message };
|
|
6591
|
+
}
|
|
6592
|
+
};
|
|
6487
6593
|
//////// PROFILES //////////////
|
|
6488
6594
|
|
|
6489
6595
|
export const get_pending_share_profile_in = async function (uid, _id) {
|
|
@@ -6545,7 +6651,11 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6545
6651
|
|
|
6546
6652
|
const get_pattern = async function (doc) {
|
|
6547
6653
|
// Same level-based default as get_contact_pattern, so a card shows the same
|
|
6548
|
-
// verification tile whichever path built it
|
|
6654
|
+
// verification tile whichever path built it, and the same two states that
|
|
6655
|
+
// outrank a level: a shared profile wears its owner's avatar, spam says
|
|
6656
|
+
// spam. A business profile used to paint its own logo across the card face,
|
|
6657
|
+
// which is why a verified business showed no level at all; the logo is
|
|
6658
|
+
// already the avatar on the same card, so the tile is the level here too.
|
|
6549
6659
|
let ret = level_pattern(doc);
|
|
6550
6660
|
|
|
6551
6661
|
if (doc.shared_from_uid) {
|
|
@@ -6554,26 +6664,6 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6554
6664
|
ret = shared_from_uid_ret.data.profile_avatar;
|
|
6555
6665
|
} else if (doc.is_spam) {
|
|
6556
6666
|
ret = `spam-pattern.png`;
|
|
6557
|
-
} else if (doc.account_type === 'business') {
|
|
6558
|
-
ret = doc.profile_picture;
|
|
6559
|
-
} else {
|
|
6560
|
-
switch (doc.avatar_source) {
|
|
6561
|
-
case 'fictional':
|
|
6562
|
-
ret = `fictional-avatar-pattern.png`;
|
|
6563
|
-
break;
|
|
6564
|
-
|
|
6565
|
-
case 'ai profile':
|
|
6566
|
-
ret = `fictional-avatar-pattern.png`;
|
|
6567
|
-
break;
|
|
6568
|
-
|
|
6569
|
-
case 'authentic profile':
|
|
6570
|
-
ret = `authentic-avatar-pattern.png`;
|
|
6571
|
-
break;
|
|
6572
|
-
|
|
6573
|
-
default:
|
|
6574
|
-
ret = `xu-pattern.png`;
|
|
6575
|
-
break;
|
|
6576
|
-
}
|
|
6577
6667
|
}
|
|
6578
6668
|
|
|
6579
6669
|
return ret;
|
|
@@ -6631,6 +6721,10 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6631
6721
|
// console.log(stringToColour('Project Z')); // -> Consistent color for "Project Z"
|
|
6632
6722
|
|
|
6633
6723
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
6724
|
+
// A profile inherits the verification level of the account that owns it (a
|
|
6725
|
+
// shared profile, the account that shared it), so its card reads the same
|
|
6726
|
+
// level as its owner's contact card instead of falling back to L0.
|
|
6727
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
6634
6728
|
doc.icon_pattern = await get_pattern(doc);
|
|
6635
6729
|
doc.name = doc.profile_name;
|
|
6636
6730
|
doc.interactions = 0;
|
|
@@ -7030,6 +7124,48 @@ export const get_contact = async function (uid, contact_id) {
|
|
|
7030
7124
|
return contact_ret;
|
|
7031
7125
|
};
|
|
7032
7126
|
|
|
7127
|
+
// ---------------------------------------------------------------------------
|
|
7128
|
+
// Contact activity trail
|
|
7129
|
+
//
|
|
7130
|
+
// A contact accumulates a lot of history that was previously only visible as
|
|
7131
|
+
// the END STATE on its own doc: it was created from somewhere, several AI steps
|
|
7132
|
+
// ran on it and each decided something (spam, business or personal, the business
|
|
7133
|
+
// profile, the person profile, the name, the avatar), and it may since have been
|
|
7134
|
+
// archived, unarchived, pinned, re-classified or sent to deep research. The doc
|
|
7135
|
+
// keeps the last answer of each and nothing about when, in what order, or what
|
|
7136
|
+
// the previous answer was, so "why is this contact a business" had no trace to
|
|
7137
|
+
// read back.
|
|
7138
|
+
//
|
|
7139
|
+
// One `contact_activity` doc per event, in the same app db as the contact.
|
|
7140
|
+
// Deliberately append-only and deliberately silent: a failure to WRITE the trail
|
|
7141
|
+
// must never fail the action being recorded, so every call is wrapped and logged
|
|
7142
|
+
// to the console instead of thrown. Callers do not await it for that reason
|
|
7143
|
+
// either, except where the next line already awaits something else anyway.
|
|
7144
|
+
// ---------------------------------------------------------------------------
|
|
7145
|
+
const log_contact_activity = async function (uid, contact_id, event, detail = {}, app_id) {
|
|
7146
|
+
try {
|
|
7147
|
+
if (!uid || !contact_id || !event) return null;
|
|
7148
|
+
const app = app_id || (await get_active_account_profile_info(uid))?.app_id;
|
|
7149
|
+
if (!app) return null;
|
|
7150
|
+
|
|
7151
|
+
return await db_module.save_app_couch_doc_native(app, {
|
|
7152
|
+
_id: await _common.xuda_get_uuid('contact_activity'),
|
|
7153
|
+
docType: 'contact_activity',
|
|
7154
|
+
contact_id,
|
|
7155
|
+
uid,
|
|
7156
|
+
event,
|
|
7157
|
+
// Values the UI prints back verbatim, so keep them short and human. Never
|
|
7158
|
+
// put a full AI response or an email body here: the trail is a summary.
|
|
7159
|
+
detail,
|
|
7160
|
+
ts: Date.now(),
|
|
7161
|
+
stat: 3,
|
|
7162
|
+
});
|
|
7163
|
+
} catch (err) {
|
|
7164
|
+
console.error('[account_module] contact activity not recorded:', event, contact_id, err?.message);
|
|
7165
|
+
return null;
|
|
7166
|
+
}
|
|
7167
|
+
};
|
|
7168
|
+
|
|
7033
7169
|
export const save_contact = async function (uid, contact_doc) {
|
|
7034
7170
|
const account_profile_info = await get_active_account_profile_info(uid);
|
|
7035
7171
|
const contact_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_doc);
|
package/index_ms.mjs
CHANGED
|
@@ -449,6 +449,10 @@ export const unset_deep_research_contact = async function (...args) {
|
|
|
449
449
|
return await broker.send_to_queue("unset_deep_research_contact", ...args);
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
+
export const get_contact_activity = async function (...args) {
|
|
453
|
+
return await broker.send_to_queue("get_contact_activity", ...args);
|
|
454
|
+
};
|
|
455
|
+
|
|
452
456
|
export const get_pending_share_profile_in = async function (...args) {
|
|
453
457
|
return await broker.send_to_queue("get_pending_share_profile_in", ...args);
|
|
454
458
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -449,6 +449,10 @@ export const unset_deep_research_contact = function (...args) {
|
|
|
449
449
|
broker.send_to_queue_async("unset_deep_research_contact", ...args);
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
+
export const get_contact_activity = function (...args) {
|
|
453
|
+
broker.send_to_queue_async("get_contact_activity", ...args);
|
|
454
|
+
};
|
|
455
|
+
|
|
452
456
|
export const get_pending_share_profile_in = function (...args) {
|
|
453
457
|
broker.send_to_queue_async("get_pending_share_profile_in", ...args);
|
|
454
458
|
};
|