@xuda.io/account_module 1.2.2272 → 1.2.2273
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 +1 -5
- package/index.mjs.premerge.bak +4725 -0
- package/package.json +1 -1
- package/scripts/run_backfill.mjs +1 -5
- package/scripts/run_backfill.mjs.premerge.bak +51 -0
- package/scripts/run_migrate_account.mjs +1 -5
- package/scripts/run_migrate_account.mjs.premerge.bak +54 -0
- package/scripts/run_migrate_all.mjs +1 -5
- package/scripts/run_migrate_all.mjs.premerge.bak +109 -0
|
@@ -0,0 +1,4725 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
// import { exec } from 'child_process';
|
|
5
|
+
// import util from 'util';
|
|
6
|
+
import { parsePhoneNumber } from 'libphonenumber-js';
|
|
7
|
+
import { countryCodeEmoji } from 'country-code-emoji';
|
|
8
|
+
|
|
9
|
+
const account_info_properties = [
|
|
10
|
+
'account_type',
|
|
11
|
+
'business_name',
|
|
12
|
+
'bio',
|
|
13
|
+
'phone_number',
|
|
14
|
+
'address',
|
|
15
|
+
'city',
|
|
16
|
+
'state',
|
|
17
|
+
'zip',
|
|
18
|
+
'country',
|
|
19
|
+
'email',
|
|
20
|
+
'first_name',
|
|
21
|
+
'last_name',
|
|
22
|
+
'mainCategory',
|
|
23
|
+
'subCategory',
|
|
24
|
+
'profile_picture',
|
|
25
|
+
'profile_picture_obj',
|
|
26
|
+
'username',
|
|
27
|
+
'website',
|
|
28
|
+
'profile_avatar_stat',
|
|
29
|
+
'active_account_profile_id',
|
|
30
|
+
'profile_avatar',
|
|
31
|
+
'profile_avatar_obj',
|
|
32
|
+
'is_xuda_network_ambassador',
|
|
33
|
+
'network_profile_url',
|
|
34
|
+
'network_lang',
|
|
35
|
+
'network_country_code',
|
|
36
|
+
'network_city_slug',
|
|
37
|
+
'public_profile_disabled',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
global._conf = (
|
|
41
|
+
await import(path.join(process.env.XUDA_HOME, process.env.XUDA_CONFIG), {
|
|
42
|
+
with: { type: 'json' },
|
|
43
|
+
})
|
|
44
|
+
).default;
|
|
45
|
+
|
|
46
|
+
const _common = await import(path.join(process.env.XUDA_HOME, 'common', 'xuda_node_common.mjs'));
|
|
47
|
+
const _utils = await import(path.join(process.env.XUDA_HOME, 'common', 'xuda-cpi-utils.mjs'));
|
|
48
|
+
|
|
49
|
+
// Module Paths
|
|
50
|
+
const module_path = path.join(process.env.XUDA_HOME, 'cpi') + (!_conf.is_debug ? '/node_modules/@xuda.io' : '');
|
|
51
|
+
|
|
52
|
+
const db_module = await import(`${module_path}/db_module/index.mjs`);
|
|
53
|
+
|
|
54
|
+
const ai_ms = await import(`${module_path}/ai_module/index_ms.mjs`);
|
|
55
|
+
const drive_ms = await import(`${module_path}/drive_module/index_ms.mjs`);
|
|
56
|
+
|
|
57
|
+
const team_ms = await import(`${module_path}/team_module/index_ms.mjs`);
|
|
58
|
+
const email_ms = await import(`${module_path}/email_module/index_ms.mjs`);
|
|
59
|
+
const marketplace_ms = await import(`${module_path}/marketplace_module/index_ms.mjs`);
|
|
60
|
+
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
61
|
+
|
|
62
|
+
const ws_dashboard_msa = await import(`${module_path}/ws_dashboard_module/index_msa.mjs`);
|
|
63
|
+
const drive_msa = await import(`${module_path}/drive_module/index_msa.mjs`);
|
|
64
|
+
const email_msa = await import(`${module_path}/email_module/index_msa.mjs`);
|
|
65
|
+
const logs_msa = await import(`${module_path}/logs_module/index_msa.mjs`);
|
|
66
|
+
const ai_msa = await import(`${module_path}/ai_module/index_msa.mjs`);
|
|
67
|
+
const notification_msa = await import(`${module_path}/notification_module/index_msa.mjs`);
|
|
68
|
+
|
|
69
|
+
export const update_account_info = async function (req, job_id, headers) {
|
|
70
|
+
const { uid } = req;
|
|
71
|
+
const data = req;
|
|
72
|
+
|
|
73
|
+
// delete data.profile_avatar;
|
|
74
|
+
|
|
75
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
76
|
+
|
|
77
|
+
function validate_input(string, max_length, is_mandatory, is_pass) {
|
|
78
|
+
if (!/^[a-zA-Z0-9,.@ ]*$/.test(string) && !is_pass) {
|
|
79
|
+
// if (!preg_match('^[a-zA-Z0-9,.@ ]*$', string) && !is_pass) {
|
|
80
|
+
|
|
81
|
+
return -1;
|
|
82
|
+
} else if ((is_mandatory && !string) || string.length < 2) {
|
|
83
|
+
return -2;
|
|
84
|
+
} else if (string.count > max_length) {
|
|
85
|
+
return -3;
|
|
86
|
+
} else {
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
92
|
+
var account_obj = ret.data;
|
|
93
|
+
account_obj.ts = Date.now();
|
|
94
|
+
var change = '';
|
|
95
|
+
var account_info_changes_arr = [];
|
|
96
|
+
var error = {};
|
|
97
|
+
|
|
98
|
+
await marketplace_ms.marketplace_save_user({ uid });
|
|
99
|
+
|
|
100
|
+
for (const key of account_info_properties) {
|
|
101
|
+
let val = data[key];
|
|
102
|
+
if (typeof val === 'undefined') continue;
|
|
103
|
+
if (account_obj.account_info[key] !== val) {
|
|
104
|
+
if (!key.includes('obj')) {
|
|
105
|
+
change += ' from ' + account_obj.account_info[key] + ' to ' + val;
|
|
106
|
+
}
|
|
107
|
+
account_info_changes_arr.push(key);
|
|
108
|
+
|
|
109
|
+
if (key === 'profile_picture') {
|
|
110
|
+
const current_profile_picture = account_obj.account_info?.profile_picture || '';
|
|
111
|
+
if (current_profile_picture.includes('googleusercontent')) {
|
|
112
|
+
account_obj.account_info['profile_picture_google'] = val;
|
|
113
|
+
} else {
|
|
114
|
+
//delete old profile picture
|
|
115
|
+
// const profile_picture = path.basename(account_obj?.account_info?.['profile_picture'] || '');
|
|
116
|
+
// if (profile_picture) {
|
|
117
|
+
// drive_module.delete_drive_files({ uid, drive_type: 'user', files_arr: [profile_picture] });
|
|
118
|
+
// }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const profile_avatar_obj = account_obj?.account_info?.profile_avatar_obj || '';
|
|
122
|
+
const profile_avatar_file = profile_avatar_obj?.data || profile_avatar_obj;
|
|
123
|
+
if (profile_avatar_file?.file_path && profile_avatar_file?.filename) {
|
|
124
|
+
drive_msa.delete_drive_files({ uid, drive_type: 'user', files_arr: [{ type: 'file', file_path: path.join(profile_avatar_file.file_path, profile_avatar_file.filename) }] });
|
|
125
|
+
} else if (profile_avatar_obj) {
|
|
126
|
+
console.warn('Skipping profile avatar cleanup, missing file path data', {
|
|
127
|
+
uid,
|
|
128
|
+
profile_avatar_obj,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
account_obj.account_info[key] = val;
|
|
134
|
+
|
|
135
|
+
if (account_obj.account_info.account_type === 'business') {
|
|
136
|
+
if (key === 'business_name' && validate_input(val, 150, true, false) < 0) {
|
|
137
|
+
error[key] = 'Invalid business name';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (key === 'first_name' && !validate_input(val, 35, true, false)) {
|
|
141
|
+
// print_r(validate_input($val, 35, true, false)<0);
|
|
142
|
+
// exit;
|
|
143
|
+
error[key] = 'Invalid first name';
|
|
144
|
+
}
|
|
145
|
+
if (key === 'last_name' && !validate_input(val, 35, true, false)) {
|
|
146
|
+
error[key] = 'Invalid last name';
|
|
147
|
+
}
|
|
148
|
+
if (key === 'email' && (!validator.isEmail(val) || !val || val.length < 2)) {
|
|
149
|
+
error[key] = 'Invalid email';
|
|
150
|
+
}
|
|
151
|
+
// if (key === 'phone_number' && (!/^[0-9]{3}-[0-9]{4}-[0-9]{4}$/.test(val) || !val || val.length < 2)) {
|
|
152
|
+
// // if (key === "phone_number" && (!preg_match("/^[0-9]{3}-[0-9]{4}-[0-9]{4}$/", $val) || !$val || count($val) < 2)) {
|
|
153
|
+
// error[key] = 'Invalid phone number';
|
|
154
|
+
// }
|
|
155
|
+
if (key === 'address' && !/^[a-z0-9- ]+$/i.test(val)) {
|
|
156
|
+
// if (key === "address" && !preg_match('/^[a-z0-9- ]+$/i', $val)) {
|
|
157
|
+
error[key] = 'Invalid address';
|
|
158
|
+
}
|
|
159
|
+
if (key === 'city' && !validate_input(val, 255, false, false)) {
|
|
160
|
+
error[key] = 'Invalid city';
|
|
161
|
+
}
|
|
162
|
+
if (key === 'state' && !validate_input(val, 50, false, false)) {
|
|
163
|
+
error[key] = 'Invalid state';
|
|
164
|
+
}
|
|
165
|
+
if (key === 'zip' && validate_input(val, 9, false, false) < 0) {
|
|
166
|
+
error[key] = 'Invalid zip';
|
|
167
|
+
}
|
|
168
|
+
if (key === 'country' && !validate_input(val, 55, false, false)) {
|
|
169
|
+
error[key] = 'Invalid country';
|
|
170
|
+
}
|
|
171
|
+
if (key === 'is_xuda_network_ambassador' && typeof val !== 'boolean') {
|
|
172
|
+
error[key] = 'Invalid ambassador flag';
|
|
173
|
+
}
|
|
174
|
+
if (key === 'network_profile_url') {
|
|
175
|
+
let url_ok = false;
|
|
176
|
+
try {
|
|
177
|
+
url_ok = new URL(val).protocol === 'https:';
|
|
178
|
+
} catch (_) {
|
|
179
|
+
/* invalid */
|
|
180
|
+
}
|
|
181
|
+
if (!url_ok) {
|
|
182
|
+
error[key] = 'Invalid network profile url';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (key === 'network_lang' && !/^[a-z]{2}$/.test(val)) {
|
|
186
|
+
error[key] = 'Invalid network lang';
|
|
187
|
+
}
|
|
188
|
+
if (key === 'network_country_code' && !/^[a-z]{2}$/.test(val)) {
|
|
189
|
+
error[key] = 'Invalid network country code';
|
|
190
|
+
}
|
|
191
|
+
if (key === 'network_city_slug' && !/^[a-z0-9-]{2,64}$/.test(val)) {
|
|
192
|
+
error[key] = 'Invalid network city slug';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!_.isEmpty(error)) {
|
|
198
|
+
return { code: -1310, data: error };
|
|
199
|
+
}
|
|
200
|
+
if (!change) {
|
|
201
|
+
if (account_obj.account_info?.profile_picture && !account_obj.account_info?.profile_avatar && account_obj.account_info.profile_avatar_stat !== 2) {
|
|
202
|
+
set_account_profile_picture(uid, uid, account_obj.account_info, job_id, headers, account_profile_info);
|
|
203
|
+
}
|
|
204
|
+
return { code: 1300, data: 'no change' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
account_obj.account_info.full_name = `${account_obj.account_info.first_name} ${account_obj.account_info.last_name}`;
|
|
208
|
+
|
|
209
|
+
//clean up received from req
|
|
210
|
+
delete account_obj.account_info.gtp_token;
|
|
211
|
+
delete account_obj.account_info.client_id;
|
|
212
|
+
delete account_obj.account_info.uid;
|
|
213
|
+
delete account_obj.account_info.token_ret;
|
|
214
|
+
delete account_obj.account_info.job_id;
|
|
215
|
+
delete account_obj.account_info.app_id;
|
|
216
|
+
|
|
217
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_obj);
|
|
218
|
+
|
|
219
|
+
if (account_obj.account_info?.profile_picture) {
|
|
220
|
+
if (!account_obj.account_info?.profile_avatar && account_obj.account_info.profile_avatar_stat !== 2) {
|
|
221
|
+
debugger;
|
|
222
|
+
set_account_profile_picture(uid, uid, account_obj.account_info, job_id, headers, account_profile_info);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Opportunistic Stripe consolidation. Any time the user updates
|
|
227
|
+
// their account info, also check if they're still on the legacy
|
|
228
|
+
// 3-subscription model and migrate them to the consolidated
|
|
229
|
+
// 1-subscription-with-3-items model in the background. The
|
|
230
|
+
// migration helper is idempotent — already-consolidated accounts
|
|
231
|
+
// and accounts without a stripe_customer_id return immediately
|
|
232
|
+
// with no side effects — so this is cheap to run on every save.
|
|
233
|
+
//
|
|
234
|
+
// Fire-and-forget: we don't await it so the user's update_account_info
|
|
235
|
+
// response isn't blocked on Stripe API calls (creating a new sub +
|
|
236
|
+
// scheduling 3 cancels can take several seconds). The migration
|
|
237
|
+
// logs its own errors; if it fails for a particular account the
|
|
238
|
+
// user can still operate normally on the legacy model and we'll
|
|
239
|
+
// retry on the next update_account_info call.
|
|
240
|
+
if (account_obj.stripe_customer_id && !account_obj.stripe_subscription_id) {
|
|
241
|
+
stripe_ms
|
|
242
|
+
.migrate_account_to_consolidated({ uid })
|
|
243
|
+
.then((r) => {
|
|
244
|
+
if (r?.code === 1) {
|
|
245
|
+
console.log(`[update_account_info] migration result for ${uid}:`, typeof r.data === 'string' ? r.data : `migrated → ${r.data?.new_sub_id}`);
|
|
246
|
+
} else if (r?.code < 0) {
|
|
247
|
+
console.warn(`[update_account_info] migration failed for ${uid}:`, r?.data);
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
.catch((err) => {
|
|
251
|
+
console.warn(`[update_account_info] migration errored for ${uid}:`, err?.message || err);
|
|
252
|
+
});
|
|
253
|
+
} else if (account_obj.stripe_subscription_id) {
|
|
254
|
+
// Already on the consolidated model — but if the original
|
|
255
|
+
// migration used `trial_end` (older code path) the Stripe
|
|
256
|
+
// dashboard still shows the sub as "trialing". Idempotently
|
|
257
|
+
// swap it for a no-trial equivalent (cancel + recreate with
|
|
258
|
+
// `billing_cycle_anchor`). First invoice date stays exactly
|
|
259
|
+
// the same so the customer sees no billing change.
|
|
260
|
+
stripe_ms
|
|
261
|
+
.end_consolidated_trial({ uid })
|
|
262
|
+
.then((r) => {
|
|
263
|
+
if (r?.code === 1) {
|
|
264
|
+
const data = r.data;
|
|
265
|
+
if (typeof data === 'object' && data?.new_sub_id) {
|
|
266
|
+
console.log(`[update_account_info] trial removed for ${uid}: new sub ${data.new_sub_id}`);
|
|
267
|
+
}
|
|
268
|
+
// Quiet otherwise — "no consolidated sub" / "no active
|
|
269
|
+
// trial" are the common no-op cases and would spam logs.
|
|
270
|
+
} else if (r?.code < 0) {
|
|
271
|
+
console.warn(`[update_account_info] end_consolidated_trial failed for ${uid}:`, r?.data);
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
.catch((err) => {
|
|
275
|
+
console.warn(`[update_account_info] end_consolidated_trial errored for ${uid}:`, err?.message || err);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (save_ret.code > 0) {
|
|
280
|
+
return { code: 1300, data: 'ok' };
|
|
281
|
+
}
|
|
282
|
+
return save_ret;
|
|
283
|
+
};
|
|
284
|
+
export const update_account_preferences = async function (req) {
|
|
285
|
+
const { uid, hide_create_button, default_dashboard, sidebar_style, enable_thumbnail_avatar_generation } = req;
|
|
286
|
+
try {
|
|
287
|
+
let account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
288
|
+
account_doc.ts = Date.now();
|
|
289
|
+
|
|
290
|
+
account_doc.preferences = { hide_create_button, default_dashboard, sidebar_style, enable_thumbnail_avatar_generation };
|
|
291
|
+
|
|
292
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
293
|
+
if (save_ret.code < 0) {
|
|
294
|
+
throw new Error(save_ret.data);
|
|
295
|
+
}
|
|
296
|
+
return { code: 1300, data: account_doc.preferences };
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return { code: -13, data: err.message };
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export const save_admin_presets = async function (req) {
|
|
303
|
+
const ret = await db_module.get_couch_doc('xuda_master', req.app_id);
|
|
304
|
+
if (ret.code < 0) {
|
|
305
|
+
return ret;
|
|
306
|
+
}
|
|
307
|
+
var app_obj = ret.data;
|
|
308
|
+
app_obj.deploy_data.admin_presets = req.admin_presets;
|
|
309
|
+
app_obj.deploy_data.preset_id = req.preset_id;
|
|
310
|
+
|
|
311
|
+
const ret3 = await db_module.save_app_obj(app_obj, null, req.app_id);
|
|
312
|
+
var obj = {};
|
|
313
|
+
if (ret3.code > -1) {
|
|
314
|
+
obj = {
|
|
315
|
+
code: 1,
|
|
316
|
+
data: 'ok',
|
|
317
|
+
app_obj: _common.get_clean_app_obj(app_obj),
|
|
318
|
+
};
|
|
319
|
+
} else {
|
|
320
|
+
obj = ret3;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return obj;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const increment_account_usage = async function (req) {
|
|
327
|
+
const { uid } = req;
|
|
328
|
+
|
|
329
|
+
var opt = {};
|
|
330
|
+
if (uid) {
|
|
331
|
+
opt.key = uid;
|
|
332
|
+
}
|
|
333
|
+
const accounts_ret = await db_module.get_couch_view_raw('xuda_accounts', 'all_accounts', opt);
|
|
334
|
+
|
|
335
|
+
function bytesToGB(bytes) {
|
|
336
|
+
return bytes / 1073741824; // or bytes / (1024 ** 3)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// check if last calculation made at least hour ago
|
|
340
|
+
const interval = 3600 * 1000; //6000; // 3600 * 1000;
|
|
341
|
+
|
|
342
|
+
const run_account_drive = async function (account_data) {
|
|
343
|
+
try {
|
|
344
|
+
const usage_id = await _common.xuda_get_uuid('usage');
|
|
345
|
+
|
|
346
|
+
const usage_ret = await db_module.get_couch_view_raw('xuda_usage', 'open_drive_usage', {
|
|
347
|
+
key: account_data._id,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
var doc = {
|
|
351
|
+
_id: usage_id,
|
|
352
|
+
docType: 'user_drive_usage',
|
|
353
|
+
date_created: new Date(),
|
|
354
|
+
stat: 1,
|
|
355
|
+
uid: account_data._id,
|
|
356
|
+
stripe_customer_id: account_data.stripe_customer_id,
|
|
357
|
+
account_name: account_data.account_info.first_name + ' ' + account_data.account_info.last_name,
|
|
358
|
+
size: 0,
|
|
359
|
+
price: 0,
|
|
360
|
+
hours: 0,
|
|
361
|
+
ts: 0,
|
|
362
|
+
subscription_id: account_data.stripe_membership_subscription_id,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (usage_ret?.rows?.[0]) {
|
|
366
|
+
doc = usage_ret.rows[0].value;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (Date.now() - doc.ts >= interval) {
|
|
370
|
+
const plan = _conf.PLAN_OBJ[account_data.membership_plan];
|
|
371
|
+
if (!plan) throw new Error(`${account_data.account_info.first_name + ' ' + account_data.account_info.last_name} has no plan in file`);
|
|
372
|
+
const drive_size_total = account_data.user_drive_size + account_data.studio_drive_size + account_data.workspace_drive_size + account_data.builds_drive_size + account_data.plugins_drive_size;
|
|
373
|
+
if (bytesToGB(drive_size_total) > plan.features.drive) {
|
|
374
|
+
var price_per_hour = (bytesToGB(drive_size_total) * _conf.PRICE_OBJ.price_per_gb_drive) / _conf.PRICE_OBJ.avg_hours_in_month;
|
|
375
|
+
|
|
376
|
+
doc.hours++;
|
|
377
|
+
doc.size = drive_size_total;
|
|
378
|
+
doc.price += price_per_hour;
|
|
379
|
+
|
|
380
|
+
doc.ts = Date.now();
|
|
381
|
+
await db_module.save_couch_doc('xuda_usage', doc);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error(err.message);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const run_account_apps = async function (account_data) {
|
|
390
|
+
const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps', {
|
|
391
|
+
startkey: [account_data._id, ''],
|
|
392
|
+
endkey: [account_data._id, 'ZZZZZ'],
|
|
393
|
+
include_docs: true,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
for await (let app of apps_ret.data.rows) {
|
|
398
|
+
if (app.value.app_type === 'master') {
|
|
399
|
+
let size_ret = await drive_ms.get_drive_size({
|
|
400
|
+
drive_type: 'studio',
|
|
401
|
+
app_id: app.id,
|
|
402
|
+
});
|
|
403
|
+
if (size_ret.code > -1) account_data.studio_drive_size += size_ret.data;
|
|
404
|
+
|
|
405
|
+
size_ret = await drive_ms.get_drive_size({
|
|
406
|
+
drive_type: 'workspace',
|
|
407
|
+
app_id: app.id,
|
|
408
|
+
});
|
|
409
|
+
if (size_ret.code > -1) account_data.workspace_drive_size += size_ret.data;
|
|
410
|
+
|
|
411
|
+
size_ret = await drive_ms.get_drive_size({
|
|
412
|
+
drive_type: 'builds',
|
|
413
|
+
app_id: app.id,
|
|
414
|
+
});
|
|
415
|
+
if (size_ret.code > -1) account_data.builds_drive_size += size_ret.data;
|
|
416
|
+
|
|
417
|
+
size_ret = await drive_ms.get_drive_size({
|
|
418
|
+
drive_type: 'plugins',
|
|
419
|
+
app_id: app.id,
|
|
420
|
+
});
|
|
421
|
+
if (size_ret.code > -1) account_data.plugins_drive_size += size_ret.data;
|
|
422
|
+
|
|
423
|
+
// db data
|
|
424
|
+
const couch_info_ret = await db_module.get_couch_app_info(app.id);
|
|
425
|
+
account_data.project_data_size += couch_info_ret?.data?.sizes?.active || 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const usage_id = await _common.xuda_get_uuid('usage');
|
|
429
|
+
|
|
430
|
+
const usage_ret = await db_module.get_couch_view_raw('xuda_usage', 'open_app_usage', {
|
|
431
|
+
key: [app.id, app.doc?.app_hosting?.app_server_type || app.doc.app_type],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
var doc = {
|
|
435
|
+
_id: usage_id,
|
|
436
|
+
docType: 'app_usage',
|
|
437
|
+
date_created: new Date(),
|
|
438
|
+
stat: 1,
|
|
439
|
+
uid: account_data._id,
|
|
440
|
+
stripe_customer_id: account_data.stripe_customer_id,
|
|
441
|
+
account_name: account_data.account_info.first_name + ' ' + account_data.account_info.last_name,
|
|
442
|
+
app_id: app.id,
|
|
443
|
+
price: 0,
|
|
444
|
+
hours: 0,
|
|
445
|
+
ts: 0,
|
|
446
|
+
name: app.value.app_name,
|
|
447
|
+
app_type: app.value.app_type,
|
|
448
|
+
|
|
449
|
+
subscription_id: account_data.stripe_membership_subscription_id,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
if (doc.app_type === 'user_group') {
|
|
453
|
+
doc = {
|
|
454
|
+
...doc,
|
|
455
|
+
...{
|
|
456
|
+
user_group_members: 0,
|
|
457
|
+
user_group_members_accumulated: 0,
|
|
458
|
+
user_group_members_avg: 0,
|
|
459
|
+
price_user_group: 0,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (usage_ret?.rows?.[0]) {
|
|
465
|
+
doc = usage_ret.rows[0].value;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
var price_per_hour;
|
|
469
|
+
|
|
470
|
+
// check if last calculation made at least hour ago
|
|
471
|
+
const interval = 6000; // 3600 * 1000;
|
|
472
|
+
if (Date.now() - doc.ts >= interval) {
|
|
473
|
+
doc.hours++;
|
|
474
|
+
|
|
475
|
+
switch (doc.app_type) {
|
|
476
|
+
case 'backup':
|
|
477
|
+
if (app.value.app_hosting) {
|
|
478
|
+
// backup of deployment
|
|
479
|
+
price_per_hour = (app.value.app_hosting.disk * _conf.PRICE_OBJ.price_per_gb_backup) / _conf.PRICE_OBJ.avg_hours_in_month;
|
|
480
|
+
} else {
|
|
481
|
+
// backup of project
|
|
482
|
+
|
|
483
|
+
const info_ret = await db_module.get_couch_app_info(doc.app_id);
|
|
484
|
+
if (info_ret.code > 0) {
|
|
485
|
+
const size = info_ret.data.sizes.active / 1000 / 1000 / 1000; //gb
|
|
486
|
+
price_per_hour = (size * _conf.PRICE_OBJ.price_per_gb_backup) / _conf.PRICE_OBJ.avg_hours_in_month;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
case 'user_group': {
|
|
492
|
+
const team_ret = await db_module.get_couch_view('xuda_team', 'user_group_shares_count', {
|
|
493
|
+
key: app.id,
|
|
494
|
+
reduce: true,
|
|
495
|
+
group_level: 1,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const team_members = team_ret.data.rows?.[0]?.value || 0;
|
|
499
|
+
const price_per_hour_team = (team_members * _conf.PRICE_OBJ.price_per_user_group_member) / _conf.PRICE_OBJ.avg_hours_in_month;
|
|
500
|
+
doc.price_user_group += price_per_hour_team;
|
|
501
|
+
doc.user_group_members = team_members;
|
|
502
|
+
doc.user_group_members_accumulated += team_members;
|
|
503
|
+
doc.user_group_members_avg = doc.user_group_members_accumulated / doc.hours;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
default:
|
|
507
|
+
// deployments,vps,master,instances and datacenters
|
|
508
|
+
if (!app.doc.app_hosting) continue;
|
|
509
|
+
|
|
510
|
+
doc.app_hosting = app.doc.app_hosting;
|
|
511
|
+
doc.server_type = app.doc.app_hosting.app_server_type;
|
|
512
|
+
price_per_hour = app.doc.app_hosting.price / _conf.PRICE_OBJ.avg_hours_in_month;
|
|
513
|
+
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
doc.price += price_per_hour;
|
|
517
|
+
if (doc.price < 1) {
|
|
518
|
+
doc.price = 1;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
doc.ts = Date.now();
|
|
522
|
+
await db_module.save_couch_doc('xuda_usage', doc);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
console.error(err.message);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// iterate on all status account except deleted accounts
|
|
531
|
+
for await (let val of accounts_ret.rows) {
|
|
532
|
+
var account_data = val.value;
|
|
533
|
+
|
|
534
|
+
const size_ret = await drive_ms.get_drive_size({
|
|
535
|
+
uid: account_data._id,
|
|
536
|
+
drive_type: 'user',
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
account_data.user_drive_size = size_ret.data;
|
|
540
|
+
account_data.studio_drive_size = 0;
|
|
541
|
+
account_data.workspace_drive_size = 0;
|
|
542
|
+
account_data.plugins_drive_size = 0;
|
|
543
|
+
account_data.builds_drive_size = 0;
|
|
544
|
+
account_data.project_data_size = 0;
|
|
545
|
+
|
|
546
|
+
await run_account_apps(account_data);
|
|
547
|
+
await run_account_drive(account_data);
|
|
548
|
+
|
|
549
|
+
account_data.total_drive_size = account_data.user_drive_size + account_data.studio_drive_size + account_data.workspace_drive_size + account_data.plugins_drive_size + account_data.builds_drive_size + account_data.project_data_size;
|
|
550
|
+
await db_module.save_couch_doc('xuda_accounts', account_data);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// ──────────────────────────────────────────────────────────────────
|
|
555
|
+
// Per-deployment billing helpers.
|
|
556
|
+
//
|
|
557
|
+
// HISTORY: an earlier iteration of unified billing had an hourly
|
|
558
|
+
// cron (`accrue_deployment_costs`) that walked every active
|
|
559
|
+
// deployment and pushed accrual into a `deployment_usage` doc per
|
|
560
|
+
// VPS, then flushed those docs to Stripe at end-of-cycle. That
|
|
561
|
+
// approach had cron-coupling, off-by-one bugs at hour boundaries,
|
|
562
|
+
// drift, and a parallel state machine to keep in sync.
|
|
563
|
+
//
|
|
564
|
+
// CURRENT: on-demand. Both the dashboard's `accrued_so_far` /
|
|
565
|
+
// `projected_total` numbers AND the Stripe invoice items at cycle
|
|
566
|
+
// close are computed from `app_cost.created_ts` /
|
|
567
|
+
// `app_cost.terminated_ts` directly. No cron, no accumulator
|
|
568
|
+
// docs. See compute_cycle_billable_amount in deploy_module/cost.mjs
|
|
569
|
+
// for the math. Result: per-second accuracy automatically, idempotent
|
|
570
|
+
// flush, zero between-event state.
|
|
571
|
+
//
|
|
572
|
+
// Only the `backfill_app_costs` one-shot remains here — it stamps
|
|
573
|
+
// `app_cost.*` onto legacy deployments that pre-date the per-VPS
|
|
574
|
+
// ledger, anchoring `created_ts = now` so past hours stay unbilled.
|
|
575
|
+
// ──────────────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
// Doc types that own a billable droplet / add-on bundle. Used by the
|
|
578
|
+
// backfill query to scope its selector — and reusable as a constant
|
|
579
|
+
// elsewhere should anything need to filter by "is this a billable
|
|
580
|
+
// app type?".
|
|
581
|
+
const BILLABLE_APP_TYPES = ['vps', 'datacenter', 'instance', 'balancer'];
|
|
582
|
+
|
|
583
|
+
// `mark_app_terminated` was removed — its job is now done by
|
|
584
|
+
// `pre_destroy_app` in app_module, wired up via http_module's
|
|
585
|
+
// pre_dispatch hook on `destroy_app`. See cpi/app_module/index.mjs.
|
|
586
|
+
|
|
587
|
+
// -------------------------------------------------------------------
|
|
588
|
+
// Abuse detection — see the "How can we detect abuse?" plan.
|
|
589
|
+
// -------------------------------------------------------------------
|
|
590
|
+
|
|
591
|
+
// Admin emails get access to the abuse triage endpoints. Keep this in
|
|
592
|
+
// sync with the dashboard's admin-route gate. Lives here (not in
|
|
593
|
+
// config) so a compromised config_dev.json can't grant admin access.
|
|
594
|
+
const ADMIN_EMAILS = new Set(['info@ioshka.com']);
|
|
595
|
+
|
|
596
|
+
const is_admin_email = (email) => {
|
|
597
|
+
if (!email) return false;
|
|
598
|
+
return ADMIN_EMAILS.has(String(email).toLowerCase());
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// Get the active billing cycle bounds for an account, on demand from
|
|
602
|
+
// Stripe. Used by recompute_abuse_signals to scope its "this cycle"
|
|
603
|
+
// counters correctly. Returns { cycle_start_ts, cycle_end_ts } in ms
|
|
604
|
+
// or null if Stripe isn't reachable / customer not found.
|
|
605
|
+
const get_account_cycle_bounds = async (account) => {
|
|
606
|
+
if (!account?.stripe_customer_id) return null;
|
|
607
|
+
try {
|
|
608
|
+
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
609
|
+
// get_upcoming_invoice returns the upcoming invoice for the customer.
|
|
610
|
+
// Its period_start/period_end define the active cycle window. If
|
|
611
|
+
// that endpoint isn't available, we approximate from docDate.
|
|
612
|
+
const upcoming = await stripe_ms
|
|
613
|
+
.get_upcoming_invoice({
|
|
614
|
+
customer_id: account.stripe_customer_id,
|
|
615
|
+
})
|
|
616
|
+
.catch(() => null);
|
|
617
|
+
if (upcoming?.data?.period_start && upcoming?.data?.period_end) {
|
|
618
|
+
return {
|
|
619
|
+
cycle_start_ts: upcoming.data.period_start * 1000,
|
|
620
|
+
cycle_end_ts: upcoming.data.period_end * 1000,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
} catch (_) {}
|
|
624
|
+
// Fallback: rolling 30-day window from now. Approximation good enough
|
|
625
|
+
// for abuse signals — exact cycle bounds only matter for invoicing.
|
|
626
|
+
const now = Date.now();
|
|
627
|
+
return { cycle_start_ts: now - 30 * 24 * 60 * 60 * 1000, cycle_end_ts: now };
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// Walk an account's deployments and update its abuse_signals object.
|
|
631
|
+
// Idempotent — safe to call on every cycle close, on resize, or on
|
|
632
|
+
// admin demand. Returns the computed signals shape + any flags raised.
|
|
633
|
+
//
|
|
634
|
+
// Rules implemented (from the abuse plan):
|
|
635
|
+
// Rule 1 (negative_margin) — margin_ratio < 0 AND provider > $5
|
|
636
|
+
// Rule 2 (resize_churn) — >3 short segments OR >10 resizes this cycle
|
|
637
|
+
// Rule 3 (new_account_high_spec) — account age < 7d AND any deploy > $150/mo
|
|
638
|
+
//
|
|
639
|
+
// Rules 4/5/6 need additional data sources (project history view, Stripe
|
|
640
|
+
// payment_failed webhook, fingerprint capture) and ship in later phases.
|
|
641
|
+
export const recompute_abuse_signals = async function (req) {
|
|
642
|
+
const { uid } = req || {};
|
|
643
|
+
if (!uid) return { code: -1, data: 'uid required' };
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const acct_ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
647
|
+
if (acct_ret.code < 0) return { code: -1, data: 'account not found' };
|
|
648
|
+
const account = acct_ret.data;
|
|
649
|
+
|
|
650
|
+
const cost_mod = await import(`${module_path}/deploy_module/cost.mjs`);
|
|
651
|
+
const { compute_cycle_billable_amount, is_billable } = cost_mod;
|
|
652
|
+
|
|
653
|
+
const bounds = await get_account_cycle_bounds(account);
|
|
654
|
+
const cycle_start_ts = bounds?.cycle_start_ts || Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
655
|
+
const cycle_end_ts = bounds?.cycle_end_ts || Date.now();
|
|
656
|
+
|
|
657
|
+
// Walk apps via the user_apps view.
|
|
658
|
+
const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps', {
|
|
659
|
+
startkey: [uid, ''],
|
|
660
|
+
endkey: [uid, 'ZZZZZ'],
|
|
661
|
+
include_docs: true,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
let resizes_this_cycle = 0;
|
|
665
|
+
let short_segments_this_cycle = 0;
|
|
666
|
+
let total_billed = 0;
|
|
667
|
+
let total_provider = 0;
|
|
668
|
+
let max_monthly_spec = 0;
|
|
669
|
+
|
|
670
|
+
for (const row of apps_ret?.data?.rows || []) {
|
|
671
|
+
const doc = row.doc;
|
|
672
|
+
if (!is_billable(doc)) continue;
|
|
673
|
+
|
|
674
|
+
// Count cycle-relevant resizes by examining history[] entries
|
|
675
|
+
// whose closing timestamp falls inside the cycle window.
|
|
676
|
+
for (const seg of doc.app_cost.history || []) {
|
|
677
|
+
if (seg.to_ts > cycle_start_ts && seg.to_ts <= cycle_end_ts) {
|
|
678
|
+
resizes_this_cycle++;
|
|
679
|
+
if (seg.to_ts - seg.from_ts < 60 * 60 * 1000) short_segments_this_cycle++;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const billed = compute_cycle_billable_amount(doc, cycle_start_ts, cycle_end_ts);
|
|
684
|
+
total_billed += billed;
|
|
685
|
+
|
|
686
|
+
// Provider cost: use provider_cost_monthly if populated (Phase B
|
|
687
|
+
// will fill this from DO billing API). Until then approximate as
|
|
688
|
+
// monthly_hosting (assume 0% markup baseline — conservative for
|
|
689
|
+
// detecting negative margin).
|
|
690
|
+
const provider_monthly = doc.app_cost.provider_cost_monthly ?? doc.app_cost.monthly_hosting ?? 0;
|
|
691
|
+
const cycle_ms = cycle_end_ts - cycle_start_ts;
|
|
692
|
+
const window_start = Math.max(doc.app_cost.created_ts || cycle_start_ts, cycle_start_ts);
|
|
693
|
+
const window_end = Math.min(doc.app_cost.terminated_ts || cycle_end_ts, cycle_end_ts);
|
|
694
|
+
if (window_end > window_start && cycle_ms > 0) {
|
|
695
|
+
total_provider += provider_monthly * ((window_end - window_start) / cycle_ms);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (doc.app_cost.monthly_total > max_monthly_spec) {
|
|
699
|
+
max_monthly_spec = doc.app_cost.monthly_total;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const account_age_ms = Date.now() - (account.docDate || account.created_ts || Date.now());
|
|
704
|
+
const account_age_days = account_age_ms / (24 * 60 * 60 * 1000);
|
|
705
|
+
const margin_ratio = total_provider > 0 ? (total_billed - total_provider) / total_provider : 0;
|
|
706
|
+
|
|
707
|
+
// Apply rules — first flag wins.
|
|
708
|
+
let flag = null;
|
|
709
|
+
let flag_detail = null;
|
|
710
|
+
if (total_provider > 5 && margin_ratio < 0) {
|
|
711
|
+
flag = 'negative_margin';
|
|
712
|
+
flag_detail = `provider $${total_provider.toFixed(2)} > billed $${total_billed.toFixed(2)} (margin ${(margin_ratio * 100).toFixed(0)}%)`;
|
|
713
|
+
} else if (short_segments_this_cycle > 3 || resizes_this_cycle > 10) {
|
|
714
|
+
flag = 'resize_churn';
|
|
715
|
+
flag_detail = `${resizes_this_cycle} resizes, ${short_segments_this_cycle} sub-1h segments`;
|
|
716
|
+
} else if (account_age_days < 7 && max_monthly_spec > 150) {
|
|
717
|
+
flag = 'new_account_high_spec';
|
|
718
|
+
flag_detail = `${account_age_days.toFixed(1)}d old, biggest VPS $${max_monthly_spec}/mo`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const prev = account.abuse_signals || {};
|
|
722
|
+
const signals = {
|
|
723
|
+
resizes_this_cycle,
|
|
724
|
+
short_segments_this_cycle,
|
|
725
|
+
destroy_recreate_pairs: prev.destroy_recreate_pairs || 0, // populated by Rule 4 (later)
|
|
726
|
+
account_age_days,
|
|
727
|
+
total_provider_cost: Number(total_provider.toFixed(2)),
|
|
728
|
+
total_billed_amount: Number(total_billed.toFixed(2)),
|
|
729
|
+
margin_ratio: Number(margin_ratio.toFixed(4)),
|
|
730
|
+
max_monthly_spec,
|
|
731
|
+
cycle_start_ts,
|
|
732
|
+
cycle_end_ts,
|
|
733
|
+
computed_at: Date.now(),
|
|
734
|
+
flagged: !!flag,
|
|
735
|
+
flag_reason: flag,
|
|
736
|
+
flag_detail,
|
|
737
|
+
flag_ts: flag ? prev.flag_ts || Date.now() : null, // sticky until reviewed
|
|
738
|
+
reviewed_by: flag ? prev.reviewed_by || null : null,
|
|
739
|
+
reviewed_at: flag ? prev.reviewed_at || null : null,
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
account.abuse_signals = signals;
|
|
743
|
+
await db_module.save_couch_doc('xuda_accounts', account);
|
|
744
|
+
|
|
745
|
+
return { code: 1, data: signals };
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error('[recompute_abuse_signals]', err.message);
|
|
748
|
+
return { code: -1, data: err.message };
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// Admin-only: list every account currently flagged. The dashboard's
|
|
753
|
+
// /admin/abuse page reads from here.
|
|
754
|
+
export const get_flagged_accounts = async function (req) {
|
|
755
|
+
const { uid: caller_uid } = req || {};
|
|
756
|
+
if (!caller_uid) return { code: -1, data: 'unauthorized' };
|
|
757
|
+
try {
|
|
758
|
+
const caller_ret = await db_module.get_couch_doc('xuda_accounts', caller_uid);
|
|
759
|
+
if (caller_ret.code < 0 || !is_admin_email(caller_ret.data.email)) {
|
|
760
|
+
return { code: -1, data: 'unauthorized' };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// No view yet — scan via find_couch_query on abuse_signals.flagged.
|
|
764
|
+
// Acceptable at small scale; index this if the flagged list grows.
|
|
765
|
+
const find_ret = await db_module.find_couch_query('xuda_accounts', {
|
|
766
|
+
selector: { 'abuse_signals.flagged': true },
|
|
767
|
+
limit: 500,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const rows = (find_ret?.docs || []).map((a) => ({
|
|
771
|
+
uid: a._id,
|
|
772
|
+
email: a.email,
|
|
773
|
+
first_name: a.first_name,
|
|
774
|
+
last_name: a.last_name,
|
|
775
|
+
username: a.username,
|
|
776
|
+
stripe_customer_id: a.stripe_customer_id,
|
|
777
|
+
abuse_signals: a.abuse_signals,
|
|
778
|
+
stat: a.stat,
|
|
779
|
+
docDate: a.docDate,
|
|
780
|
+
}));
|
|
781
|
+
|
|
782
|
+
// Sort: unreviewed first, then most recent flag.
|
|
783
|
+
rows.sort((a, b) => {
|
|
784
|
+
const ar = a.abuse_signals?.reviewed_by ? 1 : 0;
|
|
785
|
+
const br = b.abuse_signals?.reviewed_by ? 1 : 0;
|
|
786
|
+
if (ar !== br) return ar - br;
|
|
787
|
+
return (b.abuse_signals?.flag_ts || 0) - (a.abuse_signals?.flag_ts || 0);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
return { code: 1, data: rows };
|
|
791
|
+
} catch (err) {
|
|
792
|
+
return { code: -1, data: err.message };
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// Admin-only: sweep every account that has a stripe_customer_id and
|
|
797
|
+
// recompute its abuse_signals. Used by the admin page's "Run sweep"
|
|
798
|
+
// button so signals get refreshed without waiting for the next
|
|
799
|
+
// invoice.upcoming webhook. Returns counts (flagged / clean / errors).
|
|
800
|
+
export const sweep_abuse_signals = async function (req) {
|
|
801
|
+
const { uid: caller_uid } = req || {};
|
|
802
|
+
if (!caller_uid) return { code: -1, data: 'unauthorized' };
|
|
803
|
+
try {
|
|
804
|
+
const caller_ret = await db_module.get_couch_doc('xuda_accounts', caller_uid);
|
|
805
|
+
if (caller_ret.code < 0 || !is_admin_email(caller_ret.data.email)) {
|
|
806
|
+
return { code: -1, data: 'unauthorized' };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const accounts_ret = await db_module.find_couch_query('xuda_accounts', {
|
|
810
|
+
selector: { stripe_customer_id: { $exists: true } },
|
|
811
|
+
limit: 1000,
|
|
812
|
+
});
|
|
813
|
+
const accounts = accounts_ret?.docs || [];
|
|
814
|
+
|
|
815
|
+
const summary = { scanned: 0, flagged: 0, clean: 0, errors: [] };
|
|
816
|
+
for (const acct of accounts) {
|
|
817
|
+
summary.scanned++;
|
|
818
|
+
try {
|
|
819
|
+
const r = await recompute_abuse_signals({ uid: acct._id });
|
|
820
|
+
if (r?.code < 0) {
|
|
821
|
+
summary.errors.push({ uid: acct._id, message: r.data });
|
|
822
|
+
} else if (r?.data?.flagged) {
|
|
823
|
+
summary.flagged++;
|
|
824
|
+
} else {
|
|
825
|
+
summary.clean++;
|
|
826
|
+
}
|
|
827
|
+
} catch (err) {
|
|
828
|
+
summary.errors.push({ uid: acct._id, message: err.message });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return { code: 1, data: summary };
|
|
832
|
+
} catch (err) {
|
|
833
|
+
return { code: -1, data: err.message };
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// Admin-only: mark an account's current flag as reviewed (cleared until
|
|
838
|
+
// next recompute raises it again). Records who reviewed + when.
|
|
839
|
+
export const mark_account_reviewed = async function (req) {
|
|
840
|
+
const { uid: caller_uid, target_uid, note } = req || {};
|
|
841
|
+
if (!caller_uid || !target_uid) return { code: -1, data: 'uid + target_uid required' };
|
|
842
|
+
try {
|
|
843
|
+
const caller_ret = await db_module.get_couch_doc('xuda_accounts', caller_uid);
|
|
844
|
+
if (caller_ret.code < 0 || !is_admin_email(caller_ret.data.email)) {
|
|
845
|
+
return { code: -1, data: 'unauthorized' };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const target_ret = await db_module.get_couch_doc('xuda_accounts', target_uid);
|
|
849
|
+
if (target_ret.code < 0) return { code: -1, data: 'target account not found' };
|
|
850
|
+
const target = target_ret.data;
|
|
851
|
+
|
|
852
|
+
if (!target.abuse_signals) target.abuse_signals = {};
|
|
853
|
+
target.abuse_signals.reviewed_by = caller_ret.data.email;
|
|
854
|
+
target.abuse_signals.reviewed_at = Date.now();
|
|
855
|
+
target.abuse_signals.review_note = note || null;
|
|
856
|
+
target.abuse_signals.flagged = false; // cleared
|
|
857
|
+
|
|
858
|
+
await db_module.save_couch_doc('xuda_accounts', target);
|
|
859
|
+
return { code: 1, data: target.abuse_signals };
|
|
860
|
+
} catch (err) {
|
|
861
|
+
return { code: -1, data: err.message };
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// One-shot backfill: stamp `app_cost.*` onto legacy deployments,
|
|
866
|
+
// anchored at `last_billed_ts = now` so the cron starts forward-only.
|
|
867
|
+
// Idempotent — re-running on a doc that already has app_cost.last_billed_ts
|
|
868
|
+
// is a no-op.
|
|
869
|
+
//
|
|
870
|
+
// opts:
|
|
871
|
+
// dry_run: boolean — compute, don't save.
|
|
872
|
+
// limit: number — cap on docs processed (canary backfill).
|
|
873
|
+
// app_id: string — backfill only this specific app.
|
|
874
|
+
export const backfill_app_costs = async function (opts = {}) {
|
|
875
|
+
const { dry_run = false, limit = null, app_id = null } = opts || {};
|
|
876
|
+
|
|
877
|
+
// compute_app_cost lives in deploy_module/cost.mjs. Imported via
|
|
878
|
+
// the same cross-module path convention used elsewhere in this
|
|
879
|
+
// file (see fs_module / db_module imports at the top). Falls back
|
|
880
|
+
// to inline noop if the import fails so the backfill can at least
|
|
881
|
+
// log what it WOULD have done.
|
|
882
|
+
let compute_app_cost;
|
|
883
|
+
try {
|
|
884
|
+
const cost_mod = await import(`${module_path}/deploy_module/cost.mjs`);
|
|
885
|
+
compute_app_cost = cost_mod.compute_app_cost;
|
|
886
|
+
} catch (err) {
|
|
887
|
+
return { code: -1, data: `cost.mjs not importable from account_module: ${err.message}` };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const start = Date.now();
|
|
891
|
+
const summary = {
|
|
892
|
+
started_at: new Date().toISOString(),
|
|
893
|
+
dry_run,
|
|
894
|
+
scanned: 0,
|
|
895
|
+
stamped: 0,
|
|
896
|
+
terminated: 0,
|
|
897
|
+
skipped: 0,
|
|
898
|
+
nothing_to_bill: 0,
|
|
899
|
+
errors: [],
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const APP_STATUS_DELETED = 4;
|
|
903
|
+
|
|
904
|
+
let selector;
|
|
905
|
+
if (app_id) {
|
|
906
|
+
selector = { _id: app_id };
|
|
907
|
+
} else {
|
|
908
|
+
selector = {
|
|
909
|
+
docType: 'app',
|
|
910
|
+
$or: [{ app_type: { $in: BILLABLE_APP_TYPES } }, { is_deployment: true }],
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const find_ret = await db_module.find_couch_query('xuda_master', { selector, limit: limit || 99999 });
|
|
915
|
+
const docs = find_ret?.docs || [];
|
|
916
|
+
|
|
917
|
+
for (const doc of docs) {
|
|
918
|
+
summary.scanned++;
|
|
919
|
+
try {
|
|
920
|
+
let action = 'skipped';
|
|
921
|
+
let reason = '';
|
|
922
|
+
|
|
923
|
+
if (doc.app_cost?.last_billed_ts) {
|
|
924
|
+
reason = 'already has app_cost.last_billed_ts';
|
|
925
|
+
} else if (!doc.deploy_data) {
|
|
926
|
+
reason = 'no deploy_data';
|
|
927
|
+
} else if (!BILLABLE_APP_TYPES.includes(doc.app_type) && !doc.is_deployment) {
|
|
928
|
+
reason = `app_type ${doc.app_type} not billable`;
|
|
929
|
+
} else {
|
|
930
|
+
const has_addons = doc.deploy_data?.enable_backups || doc.deploy_data?.enable_ai_maintenance || doc.deploy_data?.enable_ai_instructions || doc.deploy_data?.enable_offline || doc.deploy_data?.enable_utility_screen || doc.deploy_data?.enable_user_assist;
|
|
931
|
+
if (doc.is_deployment && !has_addons && !doc.deploy_data?.app_server_type) {
|
|
932
|
+
reason = 'deployment without addons → datacenter bears cost';
|
|
933
|
+
} else {
|
|
934
|
+
const cost = compute_app_cost(doc.deploy_data);
|
|
935
|
+
if (!cost) {
|
|
936
|
+
action = 'nothing-to-bill';
|
|
937
|
+
reason = 'monthly_total = 0';
|
|
938
|
+
} else {
|
|
939
|
+
if (doc.app_status_code === APP_STATUS_DELETED) {
|
|
940
|
+
cost.terminated_ts = doc.app_status_data ? new Date(doc.app_status_data).getTime() : Date.now();
|
|
941
|
+
action = 'terminated';
|
|
942
|
+
} else {
|
|
943
|
+
action = 'stamped';
|
|
944
|
+
}
|
|
945
|
+
reason = `monthly=$${cost.monthly_total}`;
|
|
946
|
+
doc.app_cost = cost;
|
|
947
|
+
if (!dry_run) {
|
|
948
|
+
await db_module.save_couch_doc('xuda_master', doc);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (action === 'stamped') summary.stamped++;
|
|
955
|
+
else if (action === 'terminated') summary.terminated++;
|
|
956
|
+
else if (action === 'nothing-to-bill') summary.nothing_to_bill++;
|
|
957
|
+
else summary.skipped++;
|
|
958
|
+
|
|
959
|
+
if (!summary.per_doc) summary.per_doc = [];
|
|
960
|
+
summary.per_doc.push({ _id: doc._id, app_type: doc.app_type, app_name: doc.app_name, action, reason });
|
|
961
|
+
} catch (err) {
|
|
962
|
+
summary.errors.push({ _id: doc._id, message: err.message });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
summary.duration_ms = Date.now() - start;
|
|
967
|
+
return summary;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
export const get_account_data = async function (req) {
|
|
971
|
+
var { uid, enforce_usage } = req;
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
const { data: acc_obj } = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
975
|
+
|
|
976
|
+
// let _design;
|
|
977
|
+
// if (acc_obj.account_project_id) {
|
|
978
|
+
// _design = await db_module.get_app_couch_doc_native(acc_obj.account_project_id, '_design/xuda');
|
|
979
|
+
// }
|
|
980
|
+
|
|
981
|
+
var ret = { code: 1 };
|
|
982
|
+
ret.data = {
|
|
983
|
+
_id: acc_obj._id,
|
|
984
|
+
account_info: acc_obj.account_info,
|
|
985
|
+
usage: acc_obj.activity_usage,
|
|
986
|
+
membership_plan: acc_obj.membership_plan,
|
|
987
|
+
plan_changed: acc_obj.plan_changed,
|
|
988
|
+
plan_info: acc_obj.plan_info,
|
|
989
|
+
support_plan: acc_obj.support_plan,
|
|
990
|
+
support_plan_changed: acc_obj.support_plan_changed,
|
|
991
|
+
ai_workspace_plan: acc_obj.ai_workspace_plan,
|
|
992
|
+
storage_plan_changed: acc_obj.storage_plan_changed,
|
|
993
|
+
account_hosted_email: acc_obj.account_hosted_email || null,
|
|
994
|
+
account_project_id: acc_obj.account_project_id,
|
|
995
|
+
// Coerce to explicit boolean. New accounts have `isBoarded`
|
|
996
|
+
// undefined; without this, JSON serialization drops the field
|
|
997
|
+
// entirely. The dashboard's SAVE_user uses spread-merge which
|
|
998
|
+
// doesn't overwrite missing-from-response fields, so a stale
|
|
999
|
+
// `is_boarded: true` from a previous user's localStorage would
|
|
1000
|
+
// never get cleared. Always sending an explicit false keeps the
|
|
1001
|
+
// gate honest.
|
|
1002
|
+
is_boarded: !!acc_obj.isBoarded,
|
|
1003
|
+
|
|
1004
|
+
prices_info: acc_obj.prices_info,
|
|
1005
|
+
activity_usage: acc_obj.activity_usage,
|
|
1006
|
+
uid: acc_obj._id,
|
|
1007
|
+
stat: acc_obj.stat,
|
|
1008
|
+
preferences: acc_obj.preferences || {},
|
|
1009
|
+
billing_status: {
|
|
1010
|
+
account_suspension_status: acc_obj.account_suspension_status,
|
|
1011
|
+
account_suspension_data: acc_obj.account_suspension_data,
|
|
1012
|
+
account_billing_hold_status: acc_obj.account_billing_hold_status,
|
|
1013
|
+
account_billing_hold_data: acc_obj.account_billing_hold_date,
|
|
1014
|
+
account_termination_status: acc_obj.account_termination_status,
|
|
1015
|
+
account_termination_data: acc_obj.account_termination_data,
|
|
1016
|
+
},
|
|
1017
|
+
drive_usage: {
|
|
1018
|
+
user_drive_size: acc_obj.user_drive_size || 0,
|
|
1019
|
+
studio_drive_size: acc_obj.studio_drive_size || 0,
|
|
1020
|
+
workspace_drive_size: acc_obj.workspace_drive_size || 0,
|
|
1021
|
+
plugins_drive_size: acc_obj.plugins_drive_size || 0,
|
|
1022
|
+
builds_drive_size: acc_obj.builds_drive_size || 0,
|
|
1023
|
+
project_data_size: acc_obj.project_data_size || 0,
|
|
1024
|
+
total_drive_size: acc_obj.total_drive_size || 0,
|
|
1025
|
+
},
|
|
1026
|
+
|
|
1027
|
+
stripe_connect_account_id: acc_obj?.stripe_connect_account_obj?.id,
|
|
1028
|
+
stripe_connect_account_status: acc_obj?.stripe_connect_account_status,
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
return ret;
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
console.error(err);
|
|
1034
|
+
return { code: -1, data: err.message };
|
|
1035
|
+
}
|
|
1036
|
+
// };
|
|
1037
|
+
|
|
1038
|
+
// return get_info();
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
export const get_account_projects = async function (req) {
|
|
1042
|
+
const { uid } = req;
|
|
1043
|
+
var opt = {
|
|
1044
|
+
key: uid,
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
if (_conf.superuser_account_ids.includes(uid) || _conf.support.support_team_account_ids.includes(uid)) {
|
|
1048
|
+
opt = {};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
var ret = await db_module.get_couch_view('xuda_master', 'user_projects', opt);
|
|
1052
|
+
|
|
1053
|
+
return ret;
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
export const get_account_datacenters = async function (req) {
|
|
1057
|
+
return await db_module.get_couch_view('xuda_master', 'user_datacenters', {
|
|
1058
|
+
key: req.uid,
|
|
1059
|
+
});
|
|
1060
|
+
};
|
|
1061
|
+
export const get_account_deployments = async function (req) {
|
|
1062
|
+
return await db_module.get_couch_view('xuda_master', 'user_deployments', {
|
|
1063
|
+
key: req.uid,
|
|
1064
|
+
});
|
|
1065
|
+
};
|
|
1066
|
+
export const get_account_instances = async function (req) {
|
|
1067
|
+
return await db_module.get_couch_view('xuda_master', 'user_all_instances', {
|
|
1068
|
+
key: req.uid,
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
// Standalone VPS list for the sidenav — lets the dashboard refresh DATA.vps
|
|
1072
|
+
// after a deploy without a full account reload (SideNavData calls this). Same
|
|
1073
|
+
// user_vps view that the initial account load uses.
|
|
1074
|
+
export const get_account_vps = async function (req) {
|
|
1075
|
+
return await db_module.get_couch_view('xuda_master', 'user_vps', {
|
|
1076
|
+
key: req.uid,
|
|
1077
|
+
});
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// Advertising campaigns are apps (app_type:'advertising'). A Mango query by
|
|
1081
|
+
// owner avoids needing a dedicated CouchDB view; shaped like get_couch_view
|
|
1082
|
+
// ({code, data:{rows:[{id,value}]}}) so SideNavData buckets them into
|
|
1083
|
+
// DATA.advertising exactly like deployments/vps.
|
|
1084
|
+
export const get_account_advertising = async function (req) {
|
|
1085
|
+
try {
|
|
1086
|
+
// Superuser sees EVERY campaign across all accounts; everyone else only their own.
|
|
1087
|
+
const isSuper = _conf.superuser_account_ids?.includes(req.uid);
|
|
1088
|
+
const selector = isSuper ? { app_type: 'advertising', docType: 'app' } : { app_uId: req.uid, app_type: 'advertising', docType: 'app' };
|
|
1089
|
+
const ret = await db_module.find_couch_query('xuda_master', {
|
|
1090
|
+
selector,
|
|
1091
|
+
limit: 1000,
|
|
1092
|
+
});
|
|
1093
|
+
const rows = (ret?.docs || []).map((d) => ({ id: d._id, value: d }));
|
|
1094
|
+
return { code: 1, data: { rows } };
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
return { code: -1, data: err.message };
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// Static websites are apps (app_type:'static_website') created via create_app, but
|
|
1101
|
+
// they aren't emitted by any user_* CouchDB view, so they never surface in a sidenav
|
|
1102
|
+
// bucket. Same Mango-query approach as get_account_advertising → DATA.static_website.
|
|
1103
|
+
export const get_account_static_websites = async function (req) {
|
|
1104
|
+
try {
|
|
1105
|
+
const isSuper = _conf.superuser_account_ids?.includes(req.uid);
|
|
1106
|
+
const selector = isSuper ? { app_type: 'static_website', docType: 'app' } : { app_uId: req.uid, app_type: 'static_website', docType: 'app' };
|
|
1107
|
+
const ret = await db_module.find_couch_query('xuda_master', {
|
|
1108
|
+
selector,
|
|
1109
|
+
limit: 1000,
|
|
1110
|
+
});
|
|
1111
|
+
const rows = (ret?.docs || []).map((d) => ({ id: d._id, value: d }));
|
|
1112
|
+
return { code: 1, data: { rows } };
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
return { code: -1, data: err.message };
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
export const get_account_info = async function (req) {
|
|
1119
|
+
return await get_account_data({
|
|
1120
|
+
uid: req.uid,
|
|
1121
|
+
enforce_usage: req.enforce_usage,
|
|
1122
|
+
});
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
const TIPS_FILE_PATH = path.join(process.env.XUDA_HOME, 'dist', 'tips', 'tips.json');
|
|
1126
|
+
let _did_you_know_tips_cache = null;
|
|
1127
|
+
let _did_you_know_tips_cache_mtime = 0;
|
|
1128
|
+
|
|
1129
|
+
const load_did_you_know_tips = function () {
|
|
1130
|
+
try {
|
|
1131
|
+
const stat = fs.statSync(TIPS_FILE_PATH);
|
|
1132
|
+
if (_did_you_know_tips_cache && stat.mtimeMs === _did_you_know_tips_cache_mtime) {
|
|
1133
|
+
return _did_you_know_tips_cache;
|
|
1134
|
+
}
|
|
1135
|
+
const raw = fs.readFileSync(TIPS_FILE_PATH, 'utf8');
|
|
1136
|
+
const parsed = JSON.parse(raw);
|
|
1137
|
+
_did_you_know_tips_cache = Array.isArray(parsed) ? parsed : parsed.tips || [];
|
|
1138
|
+
_did_you_know_tips_cache_mtime = stat.mtimeMs;
|
|
1139
|
+
return _did_you_know_tips_cache;
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
console.error('load_did_you_know_tips failed:', err.message);
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const tip_requirement_passes = function (tip, account_doc) {
|
|
1147
|
+
const r = tip?.requires;
|
|
1148
|
+
if (!r) return true;
|
|
1149
|
+
const account_info = account_doc?.account_info || {};
|
|
1150
|
+
switch (r) {
|
|
1151
|
+
case 'profile_incomplete': {
|
|
1152
|
+
const required = ['first_name', 'last_name', 'bio', 'country', 'profile_picture'];
|
|
1153
|
+
return required.some((k) => !account_info[k]);
|
|
1154
|
+
}
|
|
1155
|
+
case 'free_plan':
|
|
1156
|
+
return account_doc?.membership_plan === 'free';
|
|
1157
|
+
case 'no_support_plan':
|
|
1158
|
+
return !account_doc?.support_plan || account_doc.support_plan === 'none' || account_doc.support_plan === 'free';
|
|
1159
|
+
case 'email_not_connected':
|
|
1160
|
+
return !account_doc?.connected_emails?.length && !account_doc?.email_oauth?.connected;
|
|
1161
|
+
default:
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
export const did_you_know_tips = async function (req, job_id, headers) {
|
|
1167
|
+
const { uid, action = 'next', tip_id } = req || {};
|
|
1168
|
+
try {
|
|
1169
|
+
if (!uid) throw new Error('uid required');
|
|
1170
|
+
|
|
1171
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
1172
|
+
if (!account_doc) throw new Error('account not found');
|
|
1173
|
+
|
|
1174
|
+
account_doc.did_you_know_tips = account_doc.did_you_know_tips || { offered: [], accepted: [], dismissed: [], current_offer: null };
|
|
1175
|
+
const tips_state = account_doc.did_you_know_tips;
|
|
1176
|
+
|
|
1177
|
+
if (action === 'accept' || action === 'dismiss') {
|
|
1178
|
+
const target_id = tip_id || tips_state.current_offer?.id;
|
|
1179
|
+
if (!target_id) throw new Error('tip_id required');
|
|
1180
|
+
|
|
1181
|
+
const bucket = action === 'accept' ? tips_state.accepted : tips_state.dismissed;
|
|
1182
|
+
if (!bucket.includes(target_id)) bucket.push(target_id);
|
|
1183
|
+
|
|
1184
|
+
if (tips_state.current_offer?.id === target_id) {
|
|
1185
|
+
tips_state.current_offer = null;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
account_doc.ts = Date.now();
|
|
1189
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
1190
|
+
if (save_ret.code < 0) throw new Error(save_ret.data);
|
|
1191
|
+
return { code: 1, data: { action, tip_id: target_id, tips_state } };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Hold "Did you know" tips for the account's first week — brand-new
|
|
1195
|
+
// users get a clean onboarding run before we start surfacing tips. The
|
|
1196
|
+
// banner renders nothing when offer === null, so no frontend change is
|
|
1197
|
+
// needed. Fails OPEN for legacy accounts with no creation timestamp
|
|
1198
|
+
// (they're established users, not new ones).
|
|
1199
|
+
const DYK_MIN_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1200
|
+
const created_ts = Number(account_doc.date_created_ts) || (account_doc.date_created ? new Date(account_doc.date_created).getTime() : 0);
|
|
1201
|
+
if (created_ts && Date.now() - created_ts < DYK_MIN_AGE_MS) {
|
|
1202
|
+
return { code: 1, data: { offer: null, message: 'account in first-week grace period' } };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const all_tips = load_did_you_know_tips();
|
|
1206
|
+
if (!all_tips.length) {
|
|
1207
|
+
return { code: 1, data: { offer: null, message: 'no tips available' } };
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const seen = new Set([...(tips_state.accepted || []), ...(tips_state.dismissed || [])]);
|
|
1211
|
+
let candidates = all_tips.filter((t) => !seen.has(t.id) && tip_requirement_passes(t, account_doc));
|
|
1212
|
+
|
|
1213
|
+
if (!candidates.length) {
|
|
1214
|
+
candidates = all_tips.filter((t) => !tips_state.accepted?.includes(t.id) && tip_requirement_passes(t, account_doc));
|
|
1215
|
+
}
|
|
1216
|
+
if (!candidates.length) {
|
|
1217
|
+
return { code: 1, data: { offer: null, message: 'no more tips available' } };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const not_yet_offered = candidates.filter((t) => !tips_state.offered?.includes(t.id));
|
|
1221
|
+
const pool = not_yet_offered.length ? not_yet_offered : candidates;
|
|
1222
|
+
const picked = pool[Math.floor(Math.random() * pool.length)];
|
|
1223
|
+
|
|
1224
|
+
const offer = {
|
|
1225
|
+
id: picked.id,
|
|
1226
|
+
title: picked.title,
|
|
1227
|
+
text: picked.text,
|
|
1228
|
+
image: picked.image,
|
|
1229
|
+
url: picked.url,
|
|
1230
|
+
ts: Date.now(),
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
if (!tips_state.offered.includes(picked.id)) tips_state.offered.push(picked.id);
|
|
1234
|
+
tips_state.current_offer = offer;
|
|
1235
|
+
|
|
1236
|
+
account_doc.ts = Date.now();
|
|
1237
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
1238
|
+
if (save_ret.code < 0) throw new Error(save_ret.data);
|
|
1239
|
+
|
|
1240
|
+
return { code: 1, data: { offer } };
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
return { code: -45, data: err.message };
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
const _warned_no_account_project_id = new Set();
|
|
1247
|
+
|
|
1248
|
+
export const get_active_account_profile_info = async function (uid, profile_id) {
|
|
1249
|
+
try {
|
|
1250
|
+
const acc_obj = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
1251
|
+
|
|
1252
|
+
if (!acc_obj.account_project_id) {
|
|
1253
|
+
if (!_warned_no_account_project_id.has(acc_obj._id)) {
|
|
1254
|
+
_warned_no_account_project_id.add(acc_obj._id);
|
|
1255
|
+
console.warn(`[get_active_account_profile_info] acc ${acc_obj._id} has no account_project_id; returning null`);
|
|
1256
|
+
}
|
|
1257
|
+
return { uid, account_profile_id: null, app_id: null, is_main: false, account_profile_obj: null };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Fallback chain: explicit profile_id > active_account_profile_id > top-level account_profile_id.
|
|
1261
|
+
// Recovers gracefully from accounts where active_account_profile_id was never set during boarding
|
|
1262
|
+
// (e.g. interrupted Google signup flow), instead of throwing on every chat-widget poll.
|
|
1263
|
+
let active_account_profile_id = profile_id || acc_obj.account_info?.active_account_profile_id || acc_obj.account_profile_id;
|
|
1264
|
+
|
|
1265
|
+
if (!active_account_profile_id) {
|
|
1266
|
+
// Soft-fail: log once and return a degraded response. Callers that need a real profile
|
|
1267
|
+
// should null-check `account_profile_obj`. The chat widget treats this as anonymous visitor.
|
|
1268
|
+
console.warn(`[get_active_account_profile_info] acc ${acc_obj._id} has neither active_account_profile_id nor account_profile_id; returning null`);
|
|
1269
|
+
return { uid, account_profile_id: null, app_id: acc_obj.account_project_id, is_main: false, account_profile_obj: null };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const account_profile_obj = await db_module.get_app_couch_doc_native(acc_obj.account_project_id, active_account_profile_id);
|
|
1273
|
+
if (account_profile_obj.share_item_id) {
|
|
1274
|
+
// set the original profile id if shared
|
|
1275
|
+
active_account_profile_id = account_profile_obj.share_item_id;
|
|
1276
|
+
}
|
|
1277
|
+
const app_id = await get_account_default_project_id(account_profile_obj.shared_from_uid || account_profile_obj.uid);
|
|
1278
|
+
|
|
1279
|
+
return { uid: account_profile_obj.shared_from_uid || uid, account_profile_id: active_account_profile_id, app_id, is_main: active_account_profile_id === acc_obj.account_profile_id, account_profile_obj };
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
console.warn('**** get_active_account_profile_info', err);
|
|
1282
|
+
// console.error(err.message);
|
|
1283
|
+
// debugger;
|
|
1284
|
+
throw err;
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
export const get_account_profile_group_member_uids = async function (owner_uid, share_item_id) {
|
|
1289
|
+
const selector = { docType: 'team_request', team_req_stat: 3, share_item_id, access_type: 'account_profile', team_req_from_uid: owner_uid };
|
|
1290
|
+
const ret = await db_module.find_couch_query('xuda_team', { selector });
|
|
1291
|
+
return (ret?.docs || []).map((d) => d.team_req_to_uid).filter(Boolean);
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
export const get_account_name = async function (req) {
|
|
1295
|
+
const data = await db_module.get_couch_doc('xuda_accounts', req.uid_query || req.uid);
|
|
1296
|
+
if (data.code < 0) {
|
|
1297
|
+
return data;
|
|
1298
|
+
}
|
|
1299
|
+
const { membership_plan = '', support_plan = '', ai_workspace_plan = '', date_created_ts = '', stat = '' } = data.data;
|
|
1300
|
+
var obj = {
|
|
1301
|
+
first_name: '',
|
|
1302
|
+
last_name: '',
|
|
1303
|
+
email: '',
|
|
1304
|
+
phone_number: '',
|
|
1305
|
+
profile_picture: '',
|
|
1306
|
+
username: '',
|
|
1307
|
+
membership_plan,
|
|
1308
|
+
support_plan,
|
|
1309
|
+
ai_workspace_plan,
|
|
1310
|
+
date_created_ts,
|
|
1311
|
+
stat,
|
|
1312
|
+
};
|
|
1313
|
+
if (data.data.account_info) {
|
|
1314
|
+
const {
|
|
1315
|
+
first_name,
|
|
1316
|
+
last_name,
|
|
1317
|
+
email,
|
|
1318
|
+
phone_number,
|
|
1319
|
+
profile_picture,
|
|
1320
|
+
profile_avatar,
|
|
1321
|
+
username,
|
|
1322
|
+
website,
|
|
1323
|
+
country,
|
|
1324
|
+
bio,
|
|
1325
|
+
industry,
|
|
1326
|
+
account_type,
|
|
1327
|
+
address,
|
|
1328
|
+
city,
|
|
1329
|
+
state,
|
|
1330
|
+
zip,
|
|
1331
|
+
business_name,
|
|
1332
|
+
auto_respond,
|
|
1333
|
+
auto_respond_mode,
|
|
1334
|
+
auto_respond_agents,
|
|
1335
|
+
avatar_source,
|
|
1336
|
+
active_account_profile_id,
|
|
1337
|
+
network_country_code,
|
|
1338
|
+
public_profile_disabled,
|
|
1339
|
+
} = data.data.account_info;
|
|
1340
|
+
|
|
1341
|
+
obj = {
|
|
1342
|
+
_id: data.data._id,
|
|
1343
|
+
stat,
|
|
1344
|
+
network_country_code,
|
|
1345
|
+
public_profile_disabled,
|
|
1346
|
+
first_name,
|
|
1347
|
+
last_name,
|
|
1348
|
+
email,
|
|
1349
|
+
phone_number,
|
|
1350
|
+
profile_picture,
|
|
1351
|
+
profile_avatar,
|
|
1352
|
+
username,
|
|
1353
|
+
website,
|
|
1354
|
+
country,
|
|
1355
|
+
bio,
|
|
1356
|
+
industry,
|
|
1357
|
+
membership_plan,
|
|
1358
|
+
support_plan,
|
|
1359
|
+
ai_workspace_plan,
|
|
1360
|
+
date_created_ts,
|
|
1361
|
+
account_type,
|
|
1362
|
+
address,
|
|
1363
|
+
city,
|
|
1364
|
+
state,
|
|
1365
|
+
zip,
|
|
1366
|
+
business_name,
|
|
1367
|
+
online: data?.data?.socket_id ? true : false,
|
|
1368
|
+
auto_respond,
|
|
1369
|
+
auto_respond_mode,
|
|
1370
|
+
auto_respond_agents,
|
|
1371
|
+
// email,// email?.[0] || '',
|
|
1372
|
+
avatar_source,
|
|
1373
|
+
active_account_profile_id,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
return { code: 1, data: obj }; //is_online: data?.data?.socket_id ? true : false
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
export const account_validate_username = async function (req) {
|
|
1380
|
+
const opt = {
|
|
1381
|
+
selector: { 'account_info.username': req.username },
|
|
1382
|
+
fields: ['_id'],
|
|
1383
|
+
limit: 1,
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
var ret = await db_module.find_couch_query('xuda_accounts', opt);
|
|
1387
|
+
|
|
1388
|
+
if (ret.docs.length) {
|
|
1389
|
+
return { code: -400, data: 'User exist' };
|
|
1390
|
+
} else {
|
|
1391
|
+
return { code: 1, data: 'ok' };
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// export const get_account_activity = async function (req) {
|
|
1396
|
+
// return await db_module.get_couch_view(
|
|
1397
|
+
// "xuda_activity",
|
|
1398
|
+
// "activity_per_account",
|
|
1399
|
+
// {
|
|
1400
|
+
// key: req.uid,
|
|
1401
|
+
// }
|
|
1402
|
+
// );
|
|
1403
|
+
// };
|
|
1404
|
+
|
|
1405
|
+
export const verify_account = async function (req) {
|
|
1406
|
+
const { account_id } = req;
|
|
1407
|
+
if (!account_id) {
|
|
1408
|
+
return { code: -10, data: 'Missing id' };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', account_id);
|
|
1412
|
+
|
|
1413
|
+
if (ret.code < 0) {
|
|
1414
|
+
return { code: -1, data: 'account not found' };
|
|
1415
|
+
}
|
|
1416
|
+
var obj = ret.data;
|
|
1417
|
+
obj.stat = 3;
|
|
1418
|
+
const ret_acc = await db_module.save_couch_doc('xuda_accounts', obj);
|
|
1419
|
+
if (ret.code < 0) {
|
|
1420
|
+
return ret_acc;
|
|
1421
|
+
}
|
|
1422
|
+
return { code: 1, data: ret_acc.data };
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
export const validate_user_plan = async function (req) {
|
|
1426
|
+
const { account_id, app_obj } = req;
|
|
1427
|
+
|
|
1428
|
+
// Advertising campaigns are charged per-campaign (Stripe), NOT plan-capped — they
|
|
1429
|
+
// (and their team sharing) are available on EVERY plan, free included. Exempt them
|
|
1430
|
+
// from the plan gate so free accounts can invite collaborators to a campaign.
|
|
1431
|
+
if (app_obj?.app_type === 'advertising') return { code: 1, data: { membership_plan: 'advertising_exempt' } };
|
|
1432
|
+
|
|
1433
|
+
const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps_count', {
|
|
1434
|
+
startkey: [account_id, ''],
|
|
1435
|
+
endkey: [account_id, 'ZZZZZ'],
|
|
1436
|
+
reduce: true,
|
|
1437
|
+
group_level: 2,
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
var ret_obj = _.reduce(
|
|
1441
|
+
apps_ret.data.rows,
|
|
1442
|
+
(ret, value) => {
|
|
1443
|
+
ret[value.key[1]] = value.value;
|
|
1444
|
+
return ret;
|
|
1445
|
+
},
|
|
1446
|
+
{},
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', account_id);
|
|
1450
|
+
if (ret.code < 0) {
|
|
1451
|
+
return ret;
|
|
1452
|
+
}
|
|
1453
|
+
var plan = _conf.PLAN_OBJ[ret.data.membership_plan];
|
|
1454
|
+
// get plan from user
|
|
1455
|
+
|
|
1456
|
+
if (!plan) {
|
|
1457
|
+
return { code: -7, data: 'error - no plan defined' };
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// validate number of projects.
|
|
1461
|
+
// The system account-project (is_account_project) backs login + the user
|
|
1462
|
+
// drive and is created automatically by login_maintenance_fix — it must
|
|
1463
|
+
// NEVER be blocked by the plan's project cap, or an account at the limit can
|
|
1464
|
+
// never be provisioned and gets permanently locked out (get_gtp_token_info
|
|
1465
|
+
// throws "fix: account_project_id" -> 401). Exempt it from the count.
|
|
1466
|
+
if (app_obj.app_type === 'master' && !app_obj.is_account_project && ret_obj.master >= plan.features.projects) {
|
|
1467
|
+
return {
|
|
1468
|
+
code: -8,
|
|
1469
|
+
data: `Number of projects (${ret.data.membership_plan}) exceeds to the ${plan.features.projects} plan limits`,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// validate number of team members
|
|
1474
|
+
|
|
1475
|
+
const user_active_app_requests_count_ret = await db_module.get_couch_view('xuda_team', 'user_active_app_requests_count', {
|
|
1476
|
+
key: account_id,
|
|
1477
|
+
reduce: true,
|
|
1478
|
+
// group_level: 2,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
if (app_obj.app_type === 'master' && user_active_app_requests_count_ret.data?.rows?.[0]?.value >= plan.features.team) {
|
|
1482
|
+
return {
|
|
1483
|
+
code: -9,
|
|
1484
|
+
data: `Number of team shares (${user_active_app_requests_count_ret.data.rows[0].value}) exceeds to the ${plan.features.team} plan limits`,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return { code: 1, data: ret.data };
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
export const get_hosting_plan = function (req) {
|
|
1492
|
+
const { app_obj } = req;
|
|
1493
|
+
if (app_obj.app_hosting) {
|
|
1494
|
+
if (_conf.PRICE_OBJ.server_slugs[app_obj.app_hosting.app_server_type]) {
|
|
1495
|
+
return _conf.PRICE_OBJ.server_slugs[app_obj.app_hosting.app_server_type];
|
|
1496
|
+
}
|
|
1497
|
+
debugger;
|
|
1498
|
+
console.error('error: ' + app_obj.app_hosting.app_server_type + ' not found in PRICE_OBJ.server_slugs');
|
|
1499
|
+
return { cpu: 0, price: 0 };
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
export const get_cpu = async function (req) {
|
|
1504
|
+
const { app_obj } = req;
|
|
1505
|
+
var cpu = 1;
|
|
1506
|
+
if (app_obj.app_hosting) {
|
|
1507
|
+
const hosting_plan = get_hosting_plan({
|
|
1508
|
+
app_obj,
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
if (hosting_plan.cpu) {
|
|
1512
|
+
cpu = hosting_plan.cpu;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return cpu;
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
const get_account_log_object = function (body, status, service, source, response_data, ip, headers, security) {
|
|
1519
|
+
return {
|
|
1520
|
+
uid: body.uid,
|
|
1521
|
+
ip,
|
|
1522
|
+
method: service,
|
|
1523
|
+
req_body: body,
|
|
1524
|
+
client_headers: headers,
|
|
1525
|
+
security: _conf.cpi_methods?.[service]?.log,
|
|
1526
|
+
api_pk: body.api_pk,
|
|
1527
|
+
api_sk: body.api_sk,
|
|
1528
|
+
dashboard: !body.api_pk && !body.api_sk,
|
|
1529
|
+
response_status_code: status,
|
|
1530
|
+
response_data,
|
|
1531
|
+
source,
|
|
1532
|
+
security,
|
|
1533
|
+
};
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
export const add_account_log_util = async function (body, status, service, source, response_data, ip, headers) {
|
|
1537
|
+
// non error log
|
|
1538
|
+
// if (status < 400) {
|
|
1539
|
+
// console.log("error", "add_account_log_util", service);
|
|
1540
|
+
|
|
1541
|
+
if (!_conf.cpi_methods?.[service]) return;
|
|
1542
|
+
if (_conf.cpi_methods?.[service]?.private) return;
|
|
1543
|
+
const security = _conf.cpi_methods?.[service].security;
|
|
1544
|
+
if (!security) {
|
|
1545
|
+
if (service.substr(0, 4) === 'get_' && !body.api_pk && !body.api_sk) return;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
logs_msa.add_account_log(get_account_log_object(body, status, service, source, response_data, ip, headers, security));
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
export const save_ssh_key = async function (req) {
|
|
1552
|
+
const { uid, _id, ssh_key_name, ssh_key_content } = req;
|
|
1553
|
+
|
|
1554
|
+
function isValidSSHPublicKey(sshPublicKey) {
|
|
1555
|
+
// Regular expression to match SSH public keys
|
|
1556
|
+
const sshKeyRegex = /^ssh-(rsa|ed25519|ecdsa-sha2-nistp[0-9]+) [A-Za-z0-9+/]+[=]{0,3}( [^@]+@[^@]+)?$/;
|
|
1557
|
+
|
|
1558
|
+
return sshKeyRegex.test(sshPublicKey);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
if (!isValidSSHPublicKey(ssh_key_content)) {
|
|
1562
|
+
return { code: -1, data: 'invalid ssh key' };
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
var doc = {
|
|
1566
|
+
uid,
|
|
1567
|
+
docType: 'ssh_key',
|
|
1568
|
+
date_created_ts: Date.now(),
|
|
1569
|
+
date_created: Date.now(),
|
|
1570
|
+
stat: 3,
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
if (_id) {
|
|
1574
|
+
const ret = await db_module.get_couch_doc('xuda_ssh_keys', _id);
|
|
1575
|
+
if (ret.code < 0) {
|
|
1576
|
+
return ret;
|
|
1577
|
+
}
|
|
1578
|
+
doc = ret.data;
|
|
1579
|
+
doc.date_updated_ts = Date.now();
|
|
1580
|
+
} else {
|
|
1581
|
+
doc._id = await _common.xuda_get_uuid('ssh_key');
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
doc.ssh_key_name = ssh_key_name;
|
|
1585
|
+
doc.ssh_key_content = ssh_key_content;
|
|
1586
|
+
|
|
1587
|
+
const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', doc);
|
|
1588
|
+
|
|
1589
|
+
return save_ret;
|
|
1590
|
+
};
|
|
1591
|
+
export const delete_ssh_key = async function (req) {
|
|
1592
|
+
const { ssh_key_id } = req;
|
|
1593
|
+
|
|
1594
|
+
const ret = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
|
|
1595
|
+
if (ret.code < 0) {
|
|
1596
|
+
return ret;
|
|
1597
|
+
}
|
|
1598
|
+
var doc = ret.data;
|
|
1599
|
+
doc.date_updated_ts = Date.now();
|
|
1600
|
+
doc.stat = 4;
|
|
1601
|
+
|
|
1602
|
+
const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', doc);
|
|
1603
|
+
|
|
1604
|
+
return save_ret;
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
export const get_ssh_keys = async function (req) {
|
|
1608
|
+
const { uid, _id } = req;
|
|
1609
|
+
|
|
1610
|
+
let opt = {
|
|
1611
|
+
selector: { docType: 'ssh_key', uid, stat: { $lt: 4 } },
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
if (_id) {
|
|
1615
|
+
opt.selector._id = _id;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
var ret = await db_module.find_couch_query('xuda_ssh_keys', opt);
|
|
1619
|
+
return ret;
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
export const search_users = async function (req) {
|
|
1623
|
+
const { _id, search, limit, skip, bookmark, uid } = req;
|
|
1624
|
+
|
|
1625
|
+
// search
|
|
1626
|
+
// skip
|
|
1627
|
+
// limit
|
|
1628
|
+
// bookmark
|
|
1629
|
+
try {
|
|
1630
|
+
// if (!_conf.superuser_account_ids.includes(req.uid)) {
|
|
1631
|
+
// throw new Error('user is not authorized for this method');
|
|
1632
|
+
// }
|
|
1633
|
+
|
|
1634
|
+
let selector = {
|
|
1635
|
+
docType: 'account',
|
|
1636
|
+
stat: 3,
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
if (_id) {
|
|
1640
|
+
selector._id = _id;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (search) {
|
|
1644
|
+
selector = {
|
|
1645
|
+
...selector,
|
|
1646
|
+
$or: [
|
|
1647
|
+
{
|
|
1648
|
+
'account_info.full_name': {
|
|
1649
|
+
$regex: `(?i)${search}`,
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
// {
|
|
1653
|
+
// 'account_info.last_name': {
|
|
1654
|
+
// $regex: `(?i)${search}`,
|
|
1655
|
+
// },
|
|
1656
|
+
// },
|
|
1657
|
+
{
|
|
1658
|
+
'account_info.business_name': {
|
|
1659
|
+
$regex: `(?i)${search}`,
|
|
1660
|
+
},
|
|
1661
|
+
},
|
|
1662
|
+
{
|
|
1663
|
+
'account_info.phone_number': {
|
|
1664
|
+
$regex: `(?i)${search}`,
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
'account_info.email': {
|
|
1669
|
+
$regex: `(?i)${search}`,
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
'account_info.address': {
|
|
1674
|
+
$regex: `(?i)${search}`,
|
|
1675
|
+
},
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
_id: {
|
|
1679
|
+
$regex: `(?i)${search}`,
|
|
1680
|
+
},
|
|
1681
|
+
},
|
|
1682
|
+
],
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const opt = {
|
|
1687
|
+
selector,
|
|
1688
|
+
fields: ['_id', 'account_info.first_name', 'account_info.last_name', 'account_info.username', 'account_info.profile_avatar', 'account_info.profile_picture', 'socket_id', 'account_info.country', 'account_info.bio', 'account_info.business_name'],
|
|
1689
|
+
limit: limit ? limit : 99999,
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
if (skip) {
|
|
1693
|
+
opt.skip = skip;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
if (req?.bookmark && req?.bookmark !== 'nil') {
|
|
1697
|
+
opt.bookmark = bookmark;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
try {
|
|
1701
|
+
var ret = await db_module.find_couch_query('xuda_accounts', opt);
|
|
1702
|
+
|
|
1703
|
+
let docs = [];
|
|
1704
|
+
for await (let e of ret.docs) {
|
|
1705
|
+
let doc = await get_user_card({ uid, uid_query: e._id }); //await get_contact_info(uid, e);
|
|
1706
|
+
docs.push(doc);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
return {
|
|
1710
|
+
code: 1,
|
|
1711
|
+
data: docs,
|
|
1712
|
+
};
|
|
1713
|
+
} catch (e) {
|
|
1714
|
+
return {
|
|
1715
|
+
code: -400,
|
|
1716
|
+
data: e,
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
return { code: -1, data: err.message };
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
export const find_contact_duplicates = async function (req) {
|
|
1725
|
+
const { uid, name } = req;
|
|
1726
|
+
var opt = {
|
|
1727
|
+
selector: {
|
|
1728
|
+
docType: 'contact',
|
|
1729
|
+
stat: { $lt: 4 },
|
|
1730
|
+
// uid,
|
|
1731
|
+
},
|
|
1732
|
+
limit: 999999,
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
if (typeof name !== 'undefined') {
|
|
1736
|
+
opt.selector.name = name;
|
|
1737
|
+
}
|
|
1738
|
+
const contact_ret = await find_contact_query(uid, opt);
|
|
1739
|
+
var duplicates = {};
|
|
1740
|
+
for (contact of contact_ret.docs) {
|
|
1741
|
+
if (!duplicates[contact.name]) {
|
|
1742
|
+
duplicates[contact.name] = [];
|
|
1743
|
+
}
|
|
1744
|
+
if (contact.stat === 3) {
|
|
1745
|
+
contact.primary = true;
|
|
1746
|
+
}
|
|
1747
|
+
duplicates[contact.name].push(contact);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
// filter results
|
|
1751
|
+
for await (let [name, val] of Object.entries(duplicates)) {
|
|
1752
|
+
if (val.length < 2) {
|
|
1753
|
+
delete duplicates[name];
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return { code: 1, data: duplicates };
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
export const merge_contact = async function (req) {
|
|
1761
|
+
const { uid, name } = req;
|
|
1762
|
+
var duplicate_ret = await find_contact_duplicates({
|
|
1763
|
+
uid,
|
|
1764
|
+
name,
|
|
1765
|
+
});
|
|
1766
|
+
if (duplicate_ret.code < 0) {
|
|
1767
|
+
return duplicate_ret;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
var duplicate_arr = duplicate_ret?.data?.[name] || [];
|
|
1771
|
+
if (!duplicate_arr.length) {
|
|
1772
|
+
return { code: 1, data: 'no merge performed' };
|
|
1773
|
+
}
|
|
1774
|
+
var contact_to_merge;
|
|
1775
|
+
var contact_to_delete = [];
|
|
1776
|
+
for (contact of duplicate_arr) {
|
|
1777
|
+
if (contact.primary) {
|
|
1778
|
+
contact_to_merge = contact._id;
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
contact_to_delete.push(contact._id);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (!contact_to_merge) {
|
|
1785
|
+
// find best match for primary
|
|
1786
|
+
for (let [i, contact_id] of Object.entries(contact_to_delete)) {
|
|
1787
|
+
var contact_obj = duplicate_arr.find((e) => e._id === contact_id);
|
|
1788
|
+
|
|
1789
|
+
if (contact_obj.name && !['spam', 'newsletter', 'noreply', 'no-reply'].includes(contact_obj.email?.[0]?.toLowerCase())) {
|
|
1790
|
+
contact_to_merge = contact_id;
|
|
1791
|
+
contact_to_delete = contact_to_delete.splice(i, 1);
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (!contact_to_merge) {
|
|
1797
|
+
contact_to_merge = contact_to_delete[0];
|
|
1798
|
+
contact_to_delete = contact_to_delete.slice(1);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// get the primary account
|
|
1803
|
+
|
|
1804
|
+
var primary_contact_doc = await get_contact(uid, contact_to_merge);
|
|
1805
|
+
|
|
1806
|
+
primary_contact_doc.stat_ts = Date.now();
|
|
1807
|
+
primary_contact_doc.stat_reason = 'merge';
|
|
1808
|
+
|
|
1809
|
+
for (let contact_id of contact_to_delete) {
|
|
1810
|
+
var email_address = duplicate_arr.find((e) => e._id === contact_id).email[0];
|
|
1811
|
+
|
|
1812
|
+
primary_contact_doc.email.push(email_address);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const primary_contact_save_ret = await save_contact(uid, primary_contact_doc);
|
|
1816
|
+
|
|
1817
|
+
for (let contact_id of contact_to_delete) {
|
|
1818
|
+
try {
|
|
1819
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
1820
|
+
contact_doc.stat = 4;
|
|
1821
|
+
contact_doc.stat_ts = Date.now();
|
|
1822
|
+
contact_doc.stat_reason = 'merge';
|
|
1823
|
+
await save_contact(uid, contact_doc);
|
|
1824
|
+
} catch (error) {}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
return primary_contact_save_ret; //{ code: 1, data: duplicate_arr };
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
const get_contact_background = function (doc) {
|
|
1831
|
+
let ret = `linear-gradient(145deg,#60496e8c 0%,#71C4FF44 100%)`;
|
|
1832
|
+
|
|
1833
|
+
if (doc.pending) {
|
|
1834
|
+
ret = `linear-gradient(145deg, #009ec2 0%, #002c74 100%)`;
|
|
1835
|
+
} else {
|
|
1836
|
+
switch (doc.contact_mood_level) {
|
|
1837
|
+
case -2: {
|
|
1838
|
+
ret = `linear-gradient(145deg, #f6040494 0%, #f6040476 100%)`;
|
|
1839
|
+
break;
|
|
1840
|
+
}
|
|
1841
|
+
case -1: {
|
|
1842
|
+
ret = `linear-gradient(145deg, #f604048b 0%, #7f98e344 100%)`;
|
|
1843
|
+
break;
|
|
1844
|
+
}
|
|
1845
|
+
case 1: {
|
|
1846
|
+
ret = `linear-gradient(145deg, #04f61447 0%, #7f98e344 100%)`;
|
|
1847
|
+
break;
|
|
1848
|
+
}
|
|
1849
|
+
case 2: {
|
|
1850
|
+
ret = `linear-gradient(145deg, #30f60468 0%, #04f61447 100%)`;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
default:
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return ret;
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
const get_contact_pattern = async function (doc) {
|
|
1861
|
+
let ret = `default-pattern.png`;
|
|
1862
|
+
|
|
1863
|
+
if (doc?.shared_from_uid) {
|
|
1864
|
+
const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
|
|
1865
|
+
ret = shared_from_uid_ret.data.profile_avatar;
|
|
1866
|
+
} else if (doc.profile_avatar_stat === 1) {
|
|
1867
|
+
if (!doc.profile_avatar_stat_ts || Date.now() - doc.profile_avatar_stat_ts > 1000 * 60 * 5) {
|
|
1868
|
+
ret = `error-pattern.png`;
|
|
1869
|
+
} else {
|
|
1870
|
+
ret = `pending-pattern.png`;
|
|
1871
|
+
}
|
|
1872
|
+
} else if (doc.profile_avatar_stat === 2) {
|
|
1873
|
+
if (!doc.profile_avatar_stat_ts || Date.now() - doc.profile_avatar_stat_ts > 1000 * 60 * 5) {
|
|
1874
|
+
ret = `error-pattern.png`;
|
|
1875
|
+
} else {
|
|
1876
|
+
ret = `processing-pattern.png`;
|
|
1877
|
+
}
|
|
1878
|
+
} else if (doc.is_spam) {
|
|
1879
|
+
ret = `spam-pattern.png`;
|
|
1880
|
+
} else {
|
|
1881
|
+
// if (doc.account_type === 'business') {
|
|
1882
|
+
// // if (doc?.is_real_person) {
|
|
1883
|
+
// ret = doc.profile_picture;
|
|
1884
|
+
// // }
|
|
1885
|
+
// } else {
|
|
1886
|
+
// if (doc.my_contact) {
|
|
1887
|
+
// if (doc.account_type === 'business') {
|
|
1888
|
+
// ret = doc.profile_picture;
|
|
1889
|
+
// } else {
|
|
1890
|
+
// switch (doc.avatar_source) {
|
|
1891
|
+
// case 'fictional':
|
|
1892
|
+
// ret = `fictional-avatar-pattern.png`;
|
|
1893
|
+
// break;
|
|
1894
|
+
|
|
1895
|
+
// case 'ai profile':
|
|
1896
|
+
// ret = `fictional-avatar-pattern.png`;
|
|
1897
|
+
// break;
|
|
1898
|
+
|
|
1899
|
+
// case 'authentic profile':
|
|
1900
|
+
// ret = `authentic-avatar-pattern.png`;
|
|
1901
|
+
// break;
|
|
1902
|
+
|
|
1903
|
+
// default:
|
|
1904
|
+
// ret = `xu-pattern.png`;
|
|
1905
|
+
// break;
|
|
1906
|
+
// }
|
|
1907
|
+
// }
|
|
1908
|
+
// } else
|
|
1909
|
+
|
|
1910
|
+
// if (doc?.shared_from_uid) {
|
|
1911
|
+
// //&& (doc.pending || (doc.team_req_id && doc.contact_share_copy))) {
|
|
1912
|
+
// // pending or active share request
|
|
1913
|
+
// const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
|
|
1914
|
+
// ret = shared_from_uid_ret.data.profile_avatar;
|
|
1915
|
+
// }
|
|
1916
|
+
|
|
1917
|
+
// else if (doc?.reference_doc?.shared_from_uid) {
|
|
1918
|
+
// //active share
|
|
1919
|
+
// const shared_from_uid_ret = await get_account_name({ uid_query: doc.reference_doc.shared_from_uid });
|
|
1920
|
+
// ret = shared_from_uid_ret.data.profile_avatar;
|
|
1921
|
+
// }
|
|
1922
|
+
// else
|
|
1923
|
+
if (doc.contact_uid) {
|
|
1924
|
+
switch (doc.avatar_source) {
|
|
1925
|
+
case 'fictional':
|
|
1926
|
+
ret = `fictional-avatar-pattern.png`;
|
|
1927
|
+
break;
|
|
1928
|
+
|
|
1929
|
+
case 'ai profile':
|
|
1930
|
+
ret = `fictional-avatar-pattern.png`;
|
|
1931
|
+
break;
|
|
1932
|
+
|
|
1933
|
+
case 'authentic profile':
|
|
1934
|
+
ret = `authentic-avatar-pattern.png`;
|
|
1935
|
+
break;
|
|
1936
|
+
|
|
1937
|
+
default:
|
|
1938
|
+
ret = `xu-pattern.png`;
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
} else {
|
|
1942
|
+
switch (doc.source) {
|
|
1943
|
+
case 'read emails': {
|
|
1944
|
+
ret = `email-pattern.png`;
|
|
1945
|
+
break;
|
|
1946
|
+
}
|
|
1947
|
+
case 'web': {
|
|
1948
|
+
ret = `web-pattern.png`;
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
default:
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
// }
|
|
1957
|
+
}
|
|
1958
|
+
return ret;
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
const get_contact_border = function (doc) {
|
|
1962
|
+
const disable_color = `#1a3557`;
|
|
1963
|
+
let ret;
|
|
1964
|
+
|
|
1965
|
+
if (doc.pending) {
|
|
1966
|
+
ret = `#6f6c67`; // gray
|
|
1967
|
+
} else if (doc.my_contact) {
|
|
1968
|
+
ret = `transparent`; // gray
|
|
1969
|
+
} else {
|
|
1970
|
+
switch (doc.connection_stat) {
|
|
1971
|
+
case 0: {
|
|
1972
|
+
ret = disable_color;
|
|
1973
|
+
break;
|
|
1974
|
+
}
|
|
1975
|
+
case 1: {
|
|
1976
|
+
ret = `red`;
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
case 2: {
|
|
1981
|
+
ret = `red`;
|
|
1982
|
+
break;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
case 3: {
|
|
1986
|
+
ret = `#164a28`; //`#0070ff`;
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
case 4: {
|
|
1991
|
+
ret = `red`;
|
|
1992
|
+
break;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
default:
|
|
1996
|
+
ret = disable_color;
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return ret;
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
function formatPhoneWithFlag(phoneString, country = 'US') {
|
|
2004
|
+
// try {
|
|
2005
|
+
|
|
2006
|
+
const phoneNumber = parsePhoneNumber(phoneString, country);
|
|
2007
|
+
if (phoneNumber && phoneNumber.country) {
|
|
2008
|
+
const flag = countryCodeEmoji(phoneNumber.country);
|
|
2009
|
+
const formatted = phoneNumber.formatInternational(); // e.g., +1 213 373 4253
|
|
2010
|
+
return `${flag} ${formatted}`;
|
|
2011
|
+
}
|
|
2012
|
+
// } catch (error) {
|
|
2013
|
+
// return 'Invalid phone number';
|
|
2014
|
+
// }
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
export const get_contact_info = async function (uid, contact_doc, _id) {
|
|
2018
|
+
try {
|
|
2019
|
+
let doc = contact_doc;
|
|
2020
|
+
if (_id) {
|
|
2021
|
+
doc = await get_contact(uid, _id);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
delete doc.metadata;
|
|
2025
|
+
delete doc.profile_avatar_obj;
|
|
2026
|
+
delete doc.profile_picture_obj;
|
|
2027
|
+
delete doc.bio;
|
|
2028
|
+
|
|
2029
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
2030
|
+
|
|
2031
|
+
const chat_conversation_count = async function () {
|
|
2032
|
+
let value = 0;
|
|
2033
|
+
|
|
2034
|
+
const key = `chat_conversation_count_${doc._id}`;
|
|
2035
|
+
try {
|
|
2036
|
+
value = await db_module.get_memcached_doc(key);
|
|
2037
|
+
} catch (e) {
|
|
2038
|
+
const counts_ret = await db_module.get_app_couch_view(account_profile_info.app_id, 'chat_conversation_item_counts', {
|
|
2039
|
+
reduce: true,
|
|
2040
|
+
group_level: 1,
|
|
2041
|
+
|
|
2042
|
+
startkey: [doc._id, ''],
|
|
2043
|
+
endkey: [doc._id, 'zzzzzz'],
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
value = counts_ret?.rows?.[0]?.value || 0;
|
|
2047
|
+
db_module.set_memcached_doc(key, value);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
return value;
|
|
2051
|
+
};
|
|
2052
|
+
const chat_conversation_read = async function () {
|
|
2053
|
+
let value = 0;
|
|
2054
|
+
|
|
2055
|
+
const key = `chat_conversation_read_${uid}_${doc._id}`;
|
|
2056
|
+
try {
|
|
2057
|
+
value = await db_module.get_memcached_doc(key);
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
const counts_ret = await db_module.get_app_couch_view(account_profile_info.app_id, 'chat_conversation_item_read', {
|
|
2060
|
+
reduce: true,
|
|
2061
|
+
group_level: 2,
|
|
2062
|
+
|
|
2063
|
+
startkey: [uid, doc._id, ''],
|
|
2064
|
+
endkey: [uid, doc._id, 'zzzzzz'],
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
value = counts_ret?.rows?.[0]?.value || 0;
|
|
2068
|
+
db_module.set_memcached_doc(key, value);
|
|
2069
|
+
}
|
|
2070
|
+
return value;
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
doc.notifications = 0;
|
|
2074
|
+
if (doc.contact_uid) {
|
|
2075
|
+
const account_info_ret = await get_account_name({ uid_query: doc.contact_uid });
|
|
2076
|
+
if (account_info_ret.code < 0) {
|
|
2077
|
+
return; // throw new Error(`account ${contact_uid} not found`);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
2081
|
+
|
|
2082
|
+
//membership_plan
|
|
2083
|
+
// doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
2084
|
+
doc.name = account_info_ret.data?.account_type === 'business' ? account_info_ret.data.business_name : `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
2085
|
+
|
|
2086
|
+
if (account_info_ret.data.phone_number) {
|
|
2087
|
+
try {
|
|
2088
|
+
doc.phone_number = formatPhoneWithFlag(account_info_ret.data.phone_number, account_info_ret.data.country);
|
|
2089
|
+
} catch (err) {
|
|
2090
|
+
// console.error(err);
|
|
2091
|
+
doc.phone_number = account_info_ret.data.phone_number;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
if (doc.team_req_id) {
|
|
2096
|
+
try {
|
|
2097
|
+
const req_doc = await db_module.get_couch_doc_native('xuda_team', doc.team_req_id);
|
|
2098
|
+
doc.connection_stat = req_doc.team_req_stat;
|
|
2099
|
+
doc.connection_type = req_doc.access_type;
|
|
2100
|
+
doc.connection_uid = req_doc.team_req_from_uid === uid ? req_doc.team_req_to_uid : req_doc.team_req_from_uid;
|
|
2101
|
+
doc.connection_contact_id = req_doc.team_req_from_contact_id === doc._id ? req_doc.team_req_to_contact_id : req_doc.team_req_from_contact_id;
|
|
2102
|
+
if (doc.connection_type === 'contact_connection') {
|
|
2103
|
+
doc.friend_since = req_doc.team_req_date;
|
|
2104
|
+
}
|
|
2105
|
+
if (doc.connection_type === 'account_profile') {
|
|
2106
|
+
try {
|
|
2107
|
+
const app_id = await get_account_default_project_id(uid);
|
|
2108
|
+
const account_profile_doc = await db_module.get_app_couch_doc_native(app_id, req_doc.share_item_id);
|
|
2109
|
+
doc.account_profile = account_profile_doc;
|
|
2110
|
+
} catch (error) {}
|
|
2111
|
+
}
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
if (err.message === 'deleted') {
|
|
2114
|
+
try {
|
|
2115
|
+
let contact_doc = await get_contact(uid, doc._id);
|
|
2116
|
+
contact_doc.team_req_id = '';
|
|
2117
|
+
await save_contact(uid, contact_doc);
|
|
2118
|
+
} catch (error) {}
|
|
2119
|
+
}
|
|
2120
|
+
doc.connection_stat = 0;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
2125
|
+
// doc.icon_pattern = await get_contact_pattern(doc);
|
|
2126
|
+
doc.username = account_info_ret.data.username;
|
|
2127
|
+
doc.business_name = account_info_ret.data.business_name;
|
|
2128
|
+
doc.address = account_info_ret.data.address;
|
|
2129
|
+
doc.city = account_info_ret.data.city;
|
|
2130
|
+
doc.state = account_info_ret.data.state;
|
|
2131
|
+
doc.zip = account_info_ret.data.zip;
|
|
2132
|
+
doc.account_type = account_info_ret.data.account_type;
|
|
2133
|
+
doc.email = doc.email;
|
|
2134
|
+
if (doc.connection_stat === 3) {
|
|
2135
|
+
doc.online = account_info_ret.data.online;
|
|
2136
|
+
}
|
|
2137
|
+
} else {
|
|
2138
|
+
doc.email = doc.email;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
doc.business_has_person = doc?.business_has_person && doc?.person_info?.person_full_name !== 'Not available';
|
|
2142
|
+
doc.icon_pattern = await get_contact_pattern(doc);
|
|
2143
|
+
|
|
2144
|
+
delete doc.account_type_info;
|
|
2145
|
+
delete doc.person_info;
|
|
2146
|
+
delete doc.business_info;
|
|
2147
|
+
|
|
2148
|
+
const contact_chat_conversation_count_ret = await chat_conversation_count();
|
|
2149
|
+
const contact_chat_conversation_read_ret = await chat_conversation_read();
|
|
2150
|
+
|
|
2151
|
+
doc.interactions = contact_chat_conversation_count_ret;
|
|
2152
|
+
doc.notifications = contact_chat_conversation_count_ret - contact_chat_conversation_read_ret;
|
|
2153
|
+
doc.chats = doc.interactions;
|
|
2154
|
+
|
|
2155
|
+
doc.card_background = get_contact_background(doc);
|
|
2156
|
+
|
|
2157
|
+
doc.border = get_contact_border(doc);
|
|
2158
|
+
|
|
2159
|
+
return doc;
|
|
2160
|
+
} catch (err) {
|
|
2161
|
+
console.error(err);
|
|
2162
|
+
return {};
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
export const get_pending_contact_out = async function (uid, _id) {
|
|
2167
|
+
let docs = [];
|
|
2168
|
+
const to_opt = {
|
|
2169
|
+
selector: { docType: 'team_request', access_type: 'contact_connection', team_req_stat: 2, team_req_from_uid: uid },
|
|
2170
|
+
fields: ['_id', 'team_req_date', 'team_req_to_uid', 'team_req_to_email', 'sender_account_info.email', 'sender_account_info.first_name', 'sender_account_info.last_name', 'sender_account_info.profile_avatar', 'team_req_from_uid'],
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
if (_id) {
|
|
2174
|
+
to_opt.selector._id = _id;
|
|
2175
|
+
}
|
|
2176
|
+
const requests_to_res = await db_module.find_couch_query('xuda_team', to_opt);
|
|
2177
|
+
|
|
2178
|
+
for await (let e of requests_to_res.docs) {
|
|
2179
|
+
let doc = { docType: 'contact', date_created: e.team_req_date, email: [e.team_req_to_email], name: '', pending: true, contact_uid: e.team_req_to_uid, team_req_id: e._id, team_req_from_uid: uid };
|
|
2180
|
+
|
|
2181
|
+
if (e.team_req_to_uid) {
|
|
2182
|
+
const account_info_ret = await get_account_name({ uid_query: e.team_req_to_uid });
|
|
2183
|
+
if (account_info_ret.code < 0) {
|
|
2184
|
+
continue; // throw new Error(`account ${contact_uid} not found`);
|
|
2185
|
+
}
|
|
2186
|
+
doc.account_type = account_info_ret.data.account_type;
|
|
2187
|
+
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
2188
|
+
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
2189
|
+
doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
2190
|
+
}
|
|
2191
|
+
doc.icon_pattern = await get_contact_pattern(doc);
|
|
2192
|
+
doc.card_background = get_contact_background({ pending: true });
|
|
2193
|
+
docs.push(doc);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
return docs;
|
|
2197
|
+
};
|
|
2198
|
+
|
|
2199
|
+
export const get_pending_contact_in = async function (uid, _id) {
|
|
2200
|
+
let docs = [];
|
|
2201
|
+
const from_opt = { selector: { docType: 'team_request', access_type: 'contact_connection', team_req_stat: 2, team_req_to_uid: uid }, fields: ['_id', 'team_req_date', 'sender_account_info.email', 'team_req_from_uid'] };
|
|
2202
|
+
|
|
2203
|
+
if (_id) {
|
|
2204
|
+
to_opt.selector._id = _id;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
const requests_from_res = await db_module.find_couch_query('xuda_team', from_opt);
|
|
2208
|
+
|
|
2209
|
+
for await (let e of requests_from_res.docs) {
|
|
2210
|
+
let doc = { docType: 'contact', date_created: e.team_req_date, email: [e.sender_account_info.email], name: '', pending: true, contact_uid: e.team_req_from_uid, team_req_id: e._id };
|
|
2211
|
+
|
|
2212
|
+
if (e.team_req_from_uid) {
|
|
2213
|
+
const account_info_ret = await get_account_name({ uid_query: e.team_req_from_uid });
|
|
2214
|
+
if (account_info_ret.code < 0) {
|
|
2215
|
+
continue; // throw new Error(`account ${contact_uid} not found`);
|
|
2216
|
+
}
|
|
2217
|
+
doc.account_type = account_info_ret.data.account_type;
|
|
2218
|
+
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
2219
|
+
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
2220
|
+
doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
2221
|
+
}
|
|
2222
|
+
doc.card_background = get_contact_background({ pending: true });
|
|
2223
|
+
doc.icon_pattern = await get_contact_pattern(doc);
|
|
2224
|
+
docs.push(doc);
|
|
2225
|
+
}
|
|
2226
|
+
return docs;
|
|
2227
|
+
};
|
|
2228
|
+
export const get_pending_share_contact_in = async function (uid, _id) {
|
|
2229
|
+
let docs = [];
|
|
2230
|
+
const from_opt = { selector: { docType: 'team_request', access_type: 'contact', team_req_stat: 2, team_req_to_uid: uid }, fields: ['_id', 'team_req_date', 'sender_account_info.email', 'team_req_from_uid', 'share_item_id'] };
|
|
2231
|
+
|
|
2232
|
+
if (_id) {
|
|
2233
|
+
to_opt.selector._id = _id;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
const requests_from_res = await db_module.find_couch_query('xuda_team', from_opt);
|
|
2237
|
+
|
|
2238
|
+
for await (let e of requests_from_res.docs) {
|
|
2239
|
+
let doc = await get_contact(uid, e.share_item_id);
|
|
2240
|
+
delete doc.contact_mood_level;
|
|
2241
|
+
doc.shared_from_uid = e.team_req_from_uid;
|
|
2242
|
+
doc.pending = true;
|
|
2243
|
+
doc = await get_contact_info(uid, doc);
|
|
2244
|
+
doc.team_req_id = e._id;
|
|
2245
|
+
|
|
2246
|
+
docs.push(doc);
|
|
2247
|
+
}
|
|
2248
|
+
return docs;
|
|
2249
|
+
};
|
|
2250
|
+
|
|
2251
|
+
const get_user_contact = async function (uid, uid_query) {
|
|
2252
|
+
const account_info = (await get_account_name({ uid_query })).data;
|
|
2253
|
+
let doc = { docType: 'contact', account_type: account_info.account_type, date_created: Date.now(), email: account_info.email, contact_uid: uid_query, username: account_info.username };
|
|
2254
|
+
|
|
2255
|
+
doc.name = account_info?.account_type === 'business' ? account_info.business_name : `${account_info.first_name} ${account_info.last_name}`;
|
|
2256
|
+
doc.profile_avatar = account_info.profile_avatar;
|
|
2257
|
+
doc.profile_picture = account_info.profile_picture;
|
|
2258
|
+
doc.avatar_source = account_info.avatar_source;
|
|
2259
|
+
doc.card_background = get_contact_background(doc);
|
|
2260
|
+
doc.icon_pattern = await get_contact_pattern(doc);
|
|
2261
|
+
doc.border = get_contact_border(doc);
|
|
2262
|
+
doc.auto_respond = account_info.auto_respond;
|
|
2263
|
+
doc.auto_respond_mode = account_info.auto_respond_mode;
|
|
2264
|
+
doc.auto_respond_agents = account_info.auto_respond_agents;
|
|
2265
|
+
|
|
2266
|
+
return doc;
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
export const update_contact = async function (req) {
|
|
2270
|
+
const { uid, email, contact_uid, stat, team_req_id } = req;
|
|
2271
|
+
|
|
2272
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
2273
|
+
|
|
2274
|
+
var contact_ret;
|
|
2275
|
+
|
|
2276
|
+
let opt = {
|
|
2277
|
+
selector: {
|
|
2278
|
+
docType: 'contact',
|
|
2279
|
+
stat: 3,
|
|
2280
|
+
},
|
|
2281
|
+
limit: 1,
|
|
2282
|
+
fields: ['_id'],
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
if (email) {
|
|
2286
|
+
opt.selector.email = email;
|
|
2287
|
+
} else if (contact_uid) {
|
|
2288
|
+
opt.selector.contact_uid = contact_uid;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
contact_ret = await find_contact_query(account_profile_info.uid, opt);
|
|
2292
|
+
|
|
2293
|
+
if (contact_ret.docs.length) {
|
|
2294
|
+
var doc = contact_ret.docs[0];
|
|
2295
|
+
if (stat) {
|
|
2296
|
+
doc.stat = stat;
|
|
2297
|
+
doc.stat_ts = Date.now();
|
|
2298
|
+
}
|
|
2299
|
+
if (team_req_id) {
|
|
2300
|
+
doc.team_req_id = team_req_id;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (contact_uid) {
|
|
2304
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', contact_uid);
|
|
2305
|
+
const account_obj = ret.data;
|
|
2306
|
+
doc.profile_picture = account_obj.account_info.profile_picture;
|
|
2307
|
+
doc.profile_avatar = account_obj.account_info.profile_avatar;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
return await save_contact(uid, doc);
|
|
2311
|
+
}
|
|
2312
|
+
return { code: -1, data: 'contact not found' };
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
export const get_account_rt_info = async function (uid) {
|
|
2316
|
+
const doc_ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2317
|
+
if (doc_ret.code < 0) {
|
|
2318
|
+
console.error('get_account_rt_info', doc_ret);
|
|
2319
|
+
return {};
|
|
2320
|
+
}
|
|
2321
|
+
var account_info = doc_ret.data.account_info;
|
|
2322
|
+
|
|
2323
|
+
delete account_info.user_id;
|
|
2324
|
+
|
|
2325
|
+
account_info.uid = doc_ret.data._id;
|
|
2326
|
+
return account_info;
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
export const validate_account_topup = async function (uid, items = []) {
|
|
2330
|
+
const { code, data } = await stripe_ms.get_billing_metrics({ uid });
|
|
2331
|
+
if (code < 0) {
|
|
2332
|
+
throw new Error(data);
|
|
2333
|
+
}
|
|
2334
|
+
// POSTPAID billing: paid features (VPS, custom domain, xuda subdomain, add-ons)
|
|
2335
|
+
// now bill on the Stripe invoice at cycle close — we NO LONGER require a prepaid
|
|
2336
|
+
// credit balance up front, only a working payment method. `items` is kept for
|
|
2337
|
+
// call-site compatibility but is no longer used to gate on a prepaid amount.
|
|
2338
|
+
|
|
2339
|
+
// Gate 1: there must be a card on file to bill against.
|
|
2340
|
+
if (!data?.customer_obj?.default_source) {
|
|
2341
|
+
let err = new Error('no card on file');
|
|
2342
|
+
err.code = -90;
|
|
2343
|
+
err.billing_problem = true;
|
|
2344
|
+
throw err;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// Gate 2: a prior invoice that failed to charge is a real billing problem —
|
|
2348
|
+
// block new paid usage until the card is fixed (otherwise unpaid usage runs
|
|
2349
|
+
// unbounded). This is "pay your overdue invoice", not a prepaid top-up.
|
|
2350
|
+
if (data?.balance?.past_due) {
|
|
2351
|
+
let err = new Error(`past due ${data.balance.past_due}`);
|
|
2352
|
+
err.code = -91;
|
|
2353
|
+
err.billing_problem = true;
|
|
2354
|
+
err.topup = data.balance.past_due;
|
|
2355
|
+
err.open_invoices = data?.open_invoices;
|
|
2356
|
+
throw err;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// No prepaid-credit / "not enough balance" gate anymore — usage accrues on the
|
|
2360
|
+
// app_cost ledger and is charged on the next invoice (Stripe applies any credit
|
|
2361
|
+
// balance first, then the card).
|
|
2362
|
+
|
|
2363
|
+
await release_account_billing_hold_if_current(uid);
|
|
2364
|
+
|
|
2365
|
+
return true;
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
const release_account_billing_hold_if_current = async function (uid) {
|
|
2369
|
+
const account_ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2370
|
+
if (account_ret.code < 0 || !account_ret.data?.account_billing_hold_status) {
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const account_obj = account_ret.data;
|
|
2375
|
+
account_obj.account_billing_hold_status = 0;
|
|
2376
|
+
account_obj.account_billing_hold_date = Date.now();
|
|
2377
|
+
await db_module.save_couch_doc('xuda_accounts', account_obj);
|
|
2378
|
+
|
|
2379
|
+
ws_dashboard_msa.emit_message_to_dashboard({
|
|
2380
|
+
service: 'account_billing_hold_released',
|
|
2381
|
+
to: [uid],
|
|
2382
|
+
data: {
|
|
2383
|
+
account_billing_hold_status: 0,
|
|
2384
|
+
account_billing_hold_date: account_obj.account_billing_hold_date,
|
|
2385
|
+
},
|
|
2386
|
+
});
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
const get_prices = function (uid, item) {
|
|
2390
|
+
let price;
|
|
2391
|
+
|
|
2392
|
+
switch (item) {
|
|
2393
|
+
case 'user_group':
|
|
2394
|
+
case 'pwa':
|
|
2395
|
+
case 'preview':
|
|
2396
|
+
case 'android':
|
|
2397
|
+
case 'ios':
|
|
2398
|
+
case 'macos':
|
|
2399
|
+
case 'emitter':
|
|
2400
|
+
price = _conf.PRICE_OBJ.deployments[item];
|
|
2401
|
+
break;
|
|
2402
|
+
|
|
2403
|
+
case 'drive_ocr':
|
|
2404
|
+
price = _conf.PRICE_OBJ.drive_addons.ocr.price;
|
|
2405
|
+
break;
|
|
2406
|
+
|
|
2407
|
+
case 'drive_edit_image':
|
|
2408
|
+
price = _conf.PRICE_OBJ.drive_addons.edit_image.price;
|
|
2409
|
+
break;
|
|
2410
|
+
|
|
2411
|
+
case 'auto_backup':
|
|
2412
|
+
price = _conf.PRICE_OBJ.auto_backup_per_month;
|
|
2413
|
+
break;
|
|
2414
|
+
|
|
2415
|
+
case 'deployment_offline':
|
|
2416
|
+
price = _conf.PRICE_OBJ.deployment_offline_per_month;
|
|
2417
|
+
break;
|
|
2418
|
+
|
|
2419
|
+
case 'xuda_subdomain':
|
|
2420
|
+
price = _conf.PRICE_OBJ.xuda_subdomain_per_month;
|
|
2421
|
+
break;
|
|
2422
|
+
|
|
2423
|
+
case 'custom_domain':
|
|
2424
|
+
price = _conf.PRICE_OBJ.custom_domain_per_month;
|
|
2425
|
+
break;
|
|
2426
|
+
|
|
2427
|
+
case 'user_monitor':
|
|
2428
|
+
price = _conf.PRICE_OBJ.user_assist_per_month;
|
|
2429
|
+
break;
|
|
2430
|
+
|
|
2431
|
+
case 'branding':
|
|
2432
|
+
price = _conf.PRICE_OBJ.branding_per_month;
|
|
2433
|
+
break;
|
|
2434
|
+
|
|
2435
|
+
case 'utility_screen':
|
|
2436
|
+
price = _conf.PRICE_OBJ.utility_screen_per_month;
|
|
2437
|
+
break;
|
|
2438
|
+
|
|
2439
|
+
default:
|
|
2440
|
+
price = 0;
|
|
2441
|
+
break;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
if (_conf.PRICE_OBJ.server_slugs[item]) {
|
|
2445
|
+
price = _conf.PRICE_OBJ.server_slugs[item].price;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
return price;
|
|
2449
|
+
};
|
|
2450
|
+
|
|
2451
|
+
export const create_account_oauth_link = async function (req, job_id) {
|
|
2452
|
+
const link = `https://accounts.google.com/o/oauth2/v2/auth?scope=https://mail.google.com/&access_type=offline&include_granted_scopes=true&response_type=code&state=${req.uid}&redirect_uri=https://${_conf.is_debug ? 'dev.' : ''}xuda.ai/oauth&client_id=${
|
|
2453
|
+
_conf.gmail.clientId
|
|
2454
|
+
}&prompt=select_account consent`;
|
|
2455
|
+
// await jobs_module.update_job(
|
|
2456
|
+
// {
|
|
2457
|
+
// job_id,
|
|
2458
|
+
// current_step_name: 'creating link',
|
|
2459
|
+
// response: link,
|
|
2460
|
+
// },
|
|
2461
|
+
// global[`_account_module_ch`],
|
|
2462
|
+
// );
|
|
2463
|
+
// await _utils.delay(20000);
|
|
2464
|
+
// debugger;
|
|
2465
|
+
return {
|
|
2466
|
+
code: 1,
|
|
2467
|
+
data: link,
|
|
2468
|
+
};
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
export const get_default_project_account_doc = async function (uid) {
|
|
2472
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2473
|
+
return ret.data;
|
|
2474
|
+
};
|
|
2475
|
+
export const get_account_default_project_id = async function (uid) {
|
|
2476
|
+
const account_obj = await get_default_project_account_doc(uid);
|
|
2477
|
+
return account_obj.account_project_id;
|
|
2478
|
+
};
|
|
2479
|
+
|
|
2480
|
+
export const get_user_name = async function (uid) {
|
|
2481
|
+
try {
|
|
2482
|
+
const { data: account_info } = await get_account_name({ uid });
|
|
2483
|
+
let username = 'unknown';
|
|
2484
|
+
if (account_info) {
|
|
2485
|
+
username = account_info.first_name + ' ' + account_info.last_name;
|
|
2486
|
+
|
|
2487
|
+
if (!username) {
|
|
2488
|
+
username = +account_info.email;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return username;
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
return 'unknown';
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2498
|
+
export const get_user_card = async function (req) {
|
|
2499
|
+
const { uid, uid_query } = req;
|
|
2500
|
+
if (uid === uid_query) return await get_user_contact(uid, uid_query);
|
|
2501
|
+
|
|
2502
|
+
let user_contact_obj;
|
|
2503
|
+
|
|
2504
|
+
const opt = {
|
|
2505
|
+
selector: {
|
|
2506
|
+
docType: 'contact',
|
|
2507
|
+
stat: 3,
|
|
2508
|
+
contact_uid: uid_query,
|
|
2509
|
+
},
|
|
2510
|
+
limit: 1,
|
|
2511
|
+
fields: ['_id'],
|
|
2512
|
+
};
|
|
2513
|
+
// const account_profile_info = await get_active_account_profile_info(uid);
|
|
2514
|
+
const user_contacts = await find_contact_query(uid, opt);
|
|
2515
|
+
|
|
2516
|
+
const contact_id = user_contacts?.docs?.[0]?._id;
|
|
2517
|
+
if (contact_id) {
|
|
2518
|
+
user_contact_obj = await get_contact_info(uid, null, contact_id);
|
|
2519
|
+
} else {
|
|
2520
|
+
user_contact_obj = await get_user_contact(uid, uid_query);
|
|
2521
|
+
}
|
|
2522
|
+
return user_contact_obj;
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
export const onboarding_completed = async function (req, job_id, headers) {
|
|
2526
|
+
const { uid } = req;
|
|
2527
|
+
try {
|
|
2528
|
+
await update_account_info(req, job_id, headers);
|
|
2529
|
+
|
|
2530
|
+
let account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
2531
|
+
|
|
2532
|
+
// NOTE: neither profile_picture nor profile_avatar are required
|
|
2533
|
+
// to complete onboarding. The client UI labels step 4 "(optional)"
|
|
2534
|
+
// and lets the user skip — matching that here means a successful
|
|
2535
|
+
// client flow always succeeds server-side too. Anything they skip
|
|
2536
|
+
// can be added later from Settings → Profile. The avatar service
|
|
2537
|
+
// can also time out or fail transiently, and we don't want a
|
|
2538
|
+
// generation hiccup to block someone from finishing signup.
|
|
2539
|
+
|
|
2540
|
+
account_doc.account_info.full_name = `${account_doc.account_info.first_name} ${account_doc.account_info.last_name}`;
|
|
2541
|
+
|
|
2542
|
+
account_doc.boarded_info = { headers, date_ts: Date.now() };
|
|
2543
|
+
account_doc.isBoarded = true;
|
|
2544
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
2545
|
+
|
|
2546
|
+
// Welcome email is sent once per account by maybe_send_welcome_email, which
|
|
2547
|
+
// is also triggered when avatar generation completes (so it reaches every
|
|
2548
|
+
// signup surface, not only the dashboard onboarding form). It is idempotent
|
|
2549
|
+
// (welcome_email_sent_ts) and suppresses mentors/ambassadors.
|
|
2550
|
+
maybe_send_welcome_email(uid);
|
|
2551
|
+
|
|
2552
|
+
return save_ret;
|
|
2553
|
+
} catch (err) {
|
|
2554
|
+
return { code: -44, data: err.message };
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
// Sends the welcome_aboard email exactly once per account. Safe to call from
|
|
2559
|
+
// multiple triggers (avatar-completion, onboarding_completed) and across every
|
|
2560
|
+
// signup surface (xuda.ai, Google OAuth, xuda.fashion, xuda.network, chat
|
|
2561
|
+
// widget, public profile). Skips mentors/ambassadors (is_xuda_network_ambassador).
|
|
2562
|
+
export const maybe_send_welcome_email = async function (uid) {
|
|
2563
|
+
try {
|
|
2564
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
2565
|
+
if (!account_doc || !account_doc.account_info) return { code: -1, data: 'no account' };
|
|
2566
|
+
if (account_doc.account_info.is_xuda_network_ambassador === true) {
|
|
2567
|
+
console.log('[maybe_send_welcome_email] suppressing for xuda.network ambassador/mentor', uid);
|
|
2568
|
+
return { code: 0, data: 'ambassador' };
|
|
2569
|
+
}
|
|
2570
|
+
if (account_doc.welcome_email_sent_ts) return { code: 0, data: 'already sent' };
|
|
2571
|
+
|
|
2572
|
+
const host = _conf.is_debug ? process.env.XUDA_HOSTNAME || _conf.domain : _conf.domain;
|
|
2573
|
+
const username = account_doc.account_info.username || uid;
|
|
2574
|
+
const base = `https://${host}`;
|
|
2575
|
+
const public_url = `${base}/public_profiles/${encodeURIComponent(username)}`;
|
|
2576
|
+
const params = {
|
|
2577
|
+
name: account_doc.account_info.first_name || 'there',
|
|
2578
|
+
dashboard_url: `${base}/dashboard`,
|
|
2579
|
+
public_url,
|
|
2580
|
+
public_page_label: `${host}/public_profiles/${username}`,
|
|
2581
|
+
qr: `${public_url}/qr.png`,
|
|
2582
|
+
gif_base: `${base}/dist/images/email`,
|
|
2583
|
+
};
|
|
2584
|
+
await notification_msa.submit_notification({ type: 'account', app_id: null, uid_arr: [uid], topic: 'welcome_aboard', params, ref: null, email: null });
|
|
2585
|
+
|
|
2586
|
+
account_doc.welcome_email_sent_ts = Date.now();
|
|
2587
|
+
await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
2588
|
+
return { code: 1, data: 'sent' };
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
console.error('[maybe_send_welcome_email]', err.message);
|
|
2591
|
+
return { code: -1, data: err.message };
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2595
|
+
// Resolve an account uid from a public username (account_info.username).
|
|
2596
|
+
// Used by the public-profile route so /public_profiles/<username> resolves.
|
|
2597
|
+
export const get_uid_by_username = async function (username) {
|
|
2598
|
+
if (!username) return null;
|
|
2599
|
+
const ret = await db_module.find_couch_query('xuda_accounts', {
|
|
2600
|
+
selector: { 'account_info.username': username },
|
|
2601
|
+
fields: ['_id'],
|
|
2602
|
+
limit: 1,
|
|
2603
|
+
});
|
|
2604
|
+
return ret?.docs?.[0]?._id || null;
|
|
2605
|
+
};
|
|
2606
|
+
|
|
2607
|
+
export const ts_contact = async function (uid, contact_id) {
|
|
2608
|
+
try {
|
|
2609
|
+
const contact_doc = await get_contact(uid, contact_id);
|
|
2610
|
+
contact_doc.ts = Date.now();
|
|
2611
|
+
const save_ret = await save_contact(uid, contact_doc);
|
|
2612
|
+
return save_ret;
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
console.error(err);
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
|
|
2618
|
+
const set_account_profile_picture = async function (uid, account_uid, metadata, job_id, headers, account_profile_info) {
|
|
2619
|
+
await update_account_profile_picture_status(account_uid, 1);
|
|
2620
|
+
try {
|
|
2621
|
+
let profile_picture;
|
|
2622
|
+
|
|
2623
|
+
let { code: account_code, data: account_obj } = await db_module.get_couch_doc('xuda_accounts', account_uid);
|
|
2624
|
+
let account_info = account_obj.account_info;
|
|
2625
|
+
|
|
2626
|
+
if (account_code < 0) {
|
|
2627
|
+
throw new Error(`account ${account_uid} not found`);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const name = account_info.account_type === 'business' ? account_info.business_name : account_info.full_name;
|
|
2631
|
+
|
|
2632
|
+
await update_account_profile_picture_status(account_uid, 2);
|
|
2633
|
+
if (!account_info.profile_picture) {
|
|
2634
|
+
const file_ret = await ai_ms.get_profile_picture(uid, account_info.account_type, name, { email: account_info.email, metadata, account_info }, account_profile_info, job_id, headers);
|
|
2635
|
+
|
|
2636
|
+
let { code: account_code, data: account_obj2 } = await db_module.get_couch_doc('xuda_accounts', account_uid);
|
|
2637
|
+
account_obj = account_obj2;
|
|
2638
|
+
account_info = account_obj.account_info;
|
|
2639
|
+
|
|
2640
|
+
if (!_.isObject(file_ret.data)) throw new Error('file_ret not an object');
|
|
2641
|
+
account_info.profile_picture_obj = file_ret.data;
|
|
2642
|
+
|
|
2643
|
+
account_info.profile_picture = account_info.profile_picture_obj.file_url;
|
|
2644
|
+
account_info.profile_picture_source = file_ret.profile_picture_source;
|
|
2645
|
+
const account_save_ret = await db_module.save_couch_doc('xuda_accounts', account_obj);
|
|
2646
|
+
profile_picture = account_info.profile_picture;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
if (!account_info.profile_avatar) {
|
|
2650
|
+
let business_size;
|
|
2651
|
+
|
|
2652
|
+
switch (account_obj.membership_plan) {
|
|
2653
|
+
case 'free': {
|
|
2654
|
+
business_size = 'unknown';
|
|
2655
|
+
break;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
case 'pro': {
|
|
2659
|
+
business_size = 'small';
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
case 'team': {
|
|
2664
|
+
business_size = 'medium';
|
|
2665
|
+
break;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
case 'agency': {
|
|
2669
|
+
business_size = 'large';
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
case 'enterprise_t1':
|
|
2673
|
+
case 'enterprise_t2':
|
|
2674
|
+
case 'enterprise_t3': {
|
|
2675
|
+
business_size = 'enterprise';
|
|
2676
|
+
break;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
default:
|
|
2680
|
+
business_size = 'unknown';
|
|
2681
|
+
break;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
const { bio, country, mainCategory, subCategory } = account_info;
|
|
2685
|
+
|
|
2686
|
+
const file_ret = await ai_ms.get_profile_avatar(account_info.profile_picture, uid, '', account_profile_info, account_info.account_type, 'account', account_uid, { bio, country, mainCategory, subCategory, is_user: true }, business_size, name, account_info.email, job_id, headers);
|
|
2687
|
+
if (file_ret.code < 0) {
|
|
2688
|
+
throw new Error(file_ret.data);
|
|
2689
|
+
}
|
|
2690
|
+
let { code: account_code, data: account_obj3 } = await db_module.get_couch_doc('xuda_accounts', account_uid);
|
|
2691
|
+
account_obj = account_obj3;
|
|
2692
|
+
account_info = account_obj.account_info;
|
|
2693
|
+
|
|
2694
|
+
if (!_.isObject(file_ret.data)) throw new Error('file_ret not an object');
|
|
2695
|
+
account_info.profile_avatar_obj = file_ret.data;
|
|
2696
|
+
account_info.profile_avatar = account_info.profile_avatar_obj.file_url;
|
|
2697
|
+
|
|
2698
|
+
const account_save_ret = await db_module.save_couch_doc('xuda_accounts', account_obj);
|
|
2699
|
+
await update_account_profile_picture_status(account_uid, 3);
|
|
2700
|
+
// Avatar finished generating -> the account is fully set up; send the
|
|
2701
|
+
// one-time welcome email (idempotent, skips mentors/ambassadors).
|
|
2702
|
+
maybe_send_welcome_email(account_uid);
|
|
2703
|
+
}
|
|
2704
|
+
} catch (err) {
|
|
2705
|
+
await update_account_profile_picture_status(account_uid, 1, err.message);
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2709
|
+
// Internal trigger used by the widget Google-signup flow: when a freshly-created
|
|
2710
|
+
// visitor account already has a profile_picture (their Google photo) but no
|
|
2711
|
+
// generated avatar yet, kick off avatar generation. set_account_profile_picture
|
|
2712
|
+
// maintains profile_avatar_stat (1 → 2 → 3); the caller polls that. Mirrors the
|
|
2713
|
+
// opportunistic trigger in update_account_info (profile_avatar_stat !== 2 guards
|
|
2714
|
+
// against re-triggering while a generation is mid-flight).
|
|
2715
|
+
export const ensure_profile_avatar = async function (req, job_id, headers) {
|
|
2716
|
+
try {
|
|
2717
|
+
const { uid } = req || {};
|
|
2718
|
+
if (!uid) return { code: -1, data: 'missing uid' };
|
|
2719
|
+
const { code, data: account_obj } = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2720
|
+
if (code < 0 || !account_obj) return { code: -1, data: 'account not found' };
|
|
2721
|
+
const info = account_obj.account_info || {};
|
|
2722
|
+
if (info.profile_picture && !info.profile_avatar && info.profile_avatar_stat !== 2) {
|
|
2723
|
+
// Best-effort profile context for AI-usage attribution. A freshly-created
|
|
2724
|
+
// widget visitor may have no profile/project yet — tolerate that.
|
|
2725
|
+
let account_profile_info;
|
|
2726
|
+
try {
|
|
2727
|
+
account_profile_info = await get_active_account_profile_info(uid);
|
|
2728
|
+
} catch (e) {}
|
|
2729
|
+
// fire-and-forget inside this worker; generation is heavy (vision + image ops)
|
|
2730
|
+
set_account_profile_picture(uid, uid, info, job_id, headers, account_profile_info);
|
|
2731
|
+
}
|
|
2732
|
+
return { code: 1, data: { profile_avatar_stat: info.profile_avatar_stat || 0 } };
|
|
2733
|
+
} catch (err) {
|
|
2734
|
+
return { code: -1, data: err.message };
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
|
|
2738
|
+
setTimeout(async () => {
|
|
2739
|
+
const app_id = 'prj712ffdf5aa8adce6cedef988f9c12392'; //'prj3937cb6f9a31c8c7dea25055bba845b1'; //
|
|
2740
|
+
const uid = 'd39126e0e2c51ffbd1aad10709fc8335';
|
|
2741
|
+
|
|
2742
|
+
let ret;
|
|
2743
|
+
// read_accounts_emails();
|
|
2744
|
+
// ret = await get_account_ai_usage({ uid }, 111);
|
|
2745
|
+
// ret = await set_deep_research_contact({ uid, contact_id: 'cnt_c07049718613a11d164e90450d5f9902' });
|
|
2746
|
+
// ret = await generate_contact_avatar({ uid, contact_id: 'cnt_c07049718613a11d164e90450d5f2742' });
|
|
2747
|
+
// ret = await not_spam_contact({ uid, contact_id: 'cnt_c07049718613a11d164e90450d5f2742' });
|
|
2748
|
+
// ret = await get_account_ai_usage({ uid });
|
|
2749
|
+
// ret = await create_account_profile({ uid });
|
|
2750
|
+
// ret = await get_account_profiles({ uid });
|
|
2751
|
+
|
|
2752
|
+
// let _design = await db_module.get_app_couch_doc_native(app_id, '_design/xuda');
|
|
2753
|
+
// ret = _design.views.ai_chat_usage;
|
|
2754
|
+
// console.log(_design.views.ai_chat_usage);
|
|
2755
|
+
// ret = await set_contact_profile_picture(uid, 'cnt_2d21d55f8f0cdaf65a7a69f69617f532', {}, null, {});
|
|
2756
|
+
// ret = await get_xuda_cache(uid, 'chat_thumbnail', 'word five title');
|
|
2757
|
+
// add_ai_credits_to_active_accounts();
|
|
2758
|
+
|
|
2759
|
+
console.log(ret);
|
|
2760
|
+
}, 1000);
|
|
2761
|
+
|
|
2762
|
+
//////// CONTACTS //////////////
|
|
2763
|
+
|
|
2764
|
+
function identifyPersonalProvider(domain) {
|
|
2765
|
+
const providers = {
|
|
2766
|
+
'gmail.com': 'Google Gmail',
|
|
2767
|
+
'googlemail.com': 'Google Gmail',
|
|
2768
|
+
'outlook.com': 'Microsoft Outlook',
|
|
2769
|
+
'hotmail.com': 'Microsoft Hotmail',
|
|
2770
|
+
'live.com': 'Microsoft Live',
|
|
2771
|
+
'yahoo.com': 'Yahoo Mail',
|
|
2772
|
+
'ymail.com': 'Yahoo Mail',
|
|
2773
|
+
'aol.com': 'AOL Mail',
|
|
2774
|
+
'icloud.com': 'Apple iCloud',
|
|
2775
|
+
'me.com': 'Apple iCloud',
|
|
2776
|
+
'mac.com': 'Apple iCloud',
|
|
2777
|
+
'protonmail.com': 'ProtonMail',
|
|
2778
|
+
'proton.me': 'ProtonMail',
|
|
2779
|
+
'zoho.com': 'Zoho Mail',
|
|
2780
|
+
'yandex.com': 'Yandex Mail',
|
|
2781
|
+
'mail.com': 'Mail.com',
|
|
2782
|
+
'gmx.com': 'GMX Mail',
|
|
2783
|
+
};
|
|
2784
|
+
|
|
2785
|
+
return providers[domain.toLowerCase()];
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
export const add_contact = async function (req, job_id, headers) {
|
|
2789
|
+
try {
|
|
2790
|
+
const { profile_id, uid, email, name, stat, contact_uid, context, source, team_req_id, metadata = {} } = req;
|
|
2791
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
2792
|
+
const { account_profile_obj } = account_profile_info;
|
|
2793
|
+
|
|
2794
|
+
//////////////////
|
|
2795
|
+
const opt = {
|
|
2796
|
+
selector: {
|
|
2797
|
+
docType: 'contact',
|
|
2798
|
+
stat: { $ne: 4 },
|
|
2799
|
+
email: email.toLowerCase(),
|
|
2800
|
+
},
|
|
2801
|
+
limit: 1,
|
|
2802
|
+
fields: ['_id'],
|
|
2803
|
+
};
|
|
2804
|
+
|
|
2805
|
+
const user_contacts = await find_contact_query(uid, opt);
|
|
2806
|
+
if (user_contacts.docs.length) {
|
|
2807
|
+
// throw new Error('contact already exist');
|
|
2808
|
+
return { code: -1, data: 'contact already exist', contact_id: user_contacts.docs[0]._id };
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
/////////////////
|
|
2812
|
+
|
|
2813
|
+
let is_spam = false;
|
|
2814
|
+
// let email_type_info;
|
|
2815
|
+
// let account_type_info;
|
|
2816
|
+
let business_info;
|
|
2817
|
+
let account_type;
|
|
2818
|
+
let cached_contact;
|
|
2819
|
+
let business_has_person;
|
|
2820
|
+
if (contact_uid) {
|
|
2821
|
+
let { code: account_code, data: account_obj } = await db_module.get_couch_doc('xuda_accounts', contact_uid);
|
|
2822
|
+
// account_info = account_obj.account_info;
|
|
2823
|
+
// account_info_ret = await get_account_name({ uid_query: contact_uid });
|
|
2824
|
+
if (account_code < 0) {
|
|
2825
|
+
throw new Error(`account ${contact_uid} not found`);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
set_account_profile_picture(uid, contact_uid, metadata, job_id, headers, account_profile_info);
|
|
2829
|
+
} else {
|
|
2830
|
+
is_spam = await isLikelySpamEmail(email);
|
|
2831
|
+
|
|
2832
|
+
// contact without use account
|
|
2833
|
+
cached_contact = await get_xuda_cache(uid, 'contact', email, 'metadata');
|
|
2834
|
+
|
|
2835
|
+
if (_.isEmpty(cached_contact)) {
|
|
2836
|
+
if (!is_spam && metadata.subject && !metadata.is_sent && !metadata.is_answered && !metadata.not_junk) {
|
|
2837
|
+
// deep search for spam
|
|
2838
|
+
is_spam = await ai_ms.is_spam_email(uid, email, metadata.subject, account_profile_info);
|
|
2839
|
+
}
|
|
2840
|
+
if (!is_spam) {
|
|
2841
|
+
const is_business = await ai_ms.is_business_contact(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
2842
|
+
account_type = is_business ? 'business' : 'personal';
|
|
2843
|
+
if (is_business) {
|
|
2844
|
+
business_info = await ai_ms.get_business_info(uid, '', email, account_profile_info, { light: true });
|
|
2845
|
+
if (business_info.business_name !== 'Not available') {
|
|
2846
|
+
business_has_person = await ai_ms.is_business_contact_has_person(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
2847
|
+
} else {
|
|
2848
|
+
account_type = 'personal';
|
|
2849
|
+
business_info = undefined;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
if (_.isEmpty(cached_contact) && account_type === 'business' && !business_has_person) {
|
|
2853
|
+
if (!identifyPersonalProvider(business_info.business_domain)) {
|
|
2854
|
+
cached_contact = await get_xuda_cache(uid, 'contact', business_info.business_domain, 'metadata');
|
|
2855
|
+
} else {
|
|
2856
|
+
cached_contact = await get_xuda_cache(uid, 'contact', business_info.business_name, 'metadata');
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
const d = Date.now();
|
|
2864
|
+
const contact_obj = {
|
|
2865
|
+
_id: await _common.xuda_get_uuid('contact'),
|
|
2866
|
+
email: email.toLowerCase(),
|
|
2867
|
+
name: cached_contact?.name || name,
|
|
2868
|
+
stat: stat || 3,
|
|
2869
|
+
date_created: metadata.email_data ? new Date(metadata.email_data).getTime() : d,
|
|
2870
|
+
ts: d,
|
|
2871
|
+
docType: 'contact',
|
|
2872
|
+
contact_uid,
|
|
2873
|
+
uid: account_profile_info.uid,
|
|
2874
|
+
uid_created: uid,
|
|
2875
|
+
context,
|
|
2876
|
+
headers,
|
|
2877
|
+
source,
|
|
2878
|
+
team_req_id,
|
|
2879
|
+
metadata,
|
|
2880
|
+
is_spam: cached_contact?.is_spam || is_spam,
|
|
2881
|
+
business_has_person: cached_contact?.business_has_person || business_has_person,
|
|
2882
|
+
business_info: cached_contact?.business_info || business_info,
|
|
2883
|
+
|
|
2884
|
+
account_profiles: [account_profile_obj._id],
|
|
2885
|
+
account_type: cached_contact?.account_type || account_type,
|
|
2886
|
+
profile_avatar_stat: 0,
|
|
2887
|
+
};
|
|
2888
|
+
|
|
2889
|
+
if (!is_spam) {
|
|
2890
|
+
if (account_type === 'personal' || contact_obj?.business_has_person) {
|
|
2891
|
+
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 });
|
|
2892
|
+
if (!contact_obj.name) {
|
|
2893
|
+
contact_obj.name = await ai_ms.get_name_from_email_addr(uid, contact_obj.email, account_profile_info);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
const conversation_obj = await ai_ms.create_openai_conversation();
|
|
2898
|
+
contact_obj.contact_reference_conversation_id = conversation_obj.id;
|
|
2899
|
+
} else {
|
|
2900
|
+
if (!contact_obj.name) {
|
|
2901
|
+
contact_obj.name = '';
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const save_ret = await save_contact(uid, contact_obj);
|
|
2906
|
+
save_xuda_cache(uid, 'contact', contact_obj.email, null, contact_obj);
|
|
2907
|
+
|
|
2908
|
+
if (!contact_obj.is_spam && contact_obj.name) {
|
|
2909
|
+
set_contact_profile_picture(uid, contact_obj._id, metadata, job_id, headers, account_profile_info, false);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
return save_ret;
|
|
2913
|
+
} catch (err) {
|
|
2914
|
+
return { code: -1, data: err.message };
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
|
|
2918
|
+
const set_contact_profile_picture = async function (uid, contact_id, metadata, job_id, headers, account_profile_info, create_avatar) {
|
|
2919
|
+
let contact_obj;
|
|
2920
|
+
await update_contact_profile_picture_status(uid, contact_id, 1);
|
|
2921
|
+
try {
|
|
2922
|
+
// let profile_picture;
|
|
2923
|
+
|
|
2924
|
+
const contact_ret = await db_module.get_app_couch_doc(account_profile_info.app_id, contact_id);
|
|
2925
|
+
|
|
2926
|
+
if (contact_ret.code < 0) {
|
|
2927
|
+
throw new Error(`contact ${contact_id} not found`);
|
|
2928
|
+
}
|
|
2929
|
+
contact_obj = contact_ret.data;
|
|
2930
|
+
await update_contact_profile_picture_status(uid, contact_id, 2);
|
|
2931
|
+
////// PROFILE PICTURE
|
|
2932
|
+
if (!contact_obj.profile_picture) {
|
|
2933
|
+
let cache = await get_xuda_cache(uid, 'profile_picture', contact_obj.email, 'file');
|
|
2934
|
+
if (!cache && contact_obj?.business_info?.business_domain) {
|
|
2935
|
+
if (!identifyPersonalProvider(contact_obj.business_info.business_domain)) {
|
|
2936
|
+
cache = await get_xuda_cache(uid, 'profile_picture', contact_obj.business_info.business_domain, 'file');
|
|
2937
|
+
} else {
|
|
2938
|
+
cache = await get_xuda_cache(uid, 'profile_picture', contact_obj.business_info.business_name, 'file');
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
let file_ret = { code: 1, data: cache };
|
|
2942
|
+
|
|
2943
|
+
if (!cache) {
|
|
2944
|
+
// if personal, return profile pic, if business return logo
|
|
2945
|
+
file_ret = await ai_ms.get_profile_picture(uid, contact_obj.account_type, contact_obj?.business_info?.business_domain || contact_obj.name, { email: contact_obj.email, metadata: contact_obj, create_avatar }, account_profile_info, job_id, headers);
|
|
2946
|
+
|
|
2947
|
+
if (file_ret.code < 0) {
|
|
2948
|
+
throw new Error(file_ret.data);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
if (!_.isObject(file_ret.data)) throw new Error('file_ret not an object');
|
|
2952
|
+
if (file_ret?.data?.file_url) {
|
|
2953
|
+
if (contact_obj?.business_info?.business_domain) {
|
|
2954
|
+
if (!identifyPersonalProvider(contact_obj.business_info.business_domain)) {
|
|
2955
|
+
save_xuda_cache(uid, 'profile_picture', contact_obj.business_info.business_domain, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
2956
|
+
} else {
|
|
2957
|
+
save_xuda_cache(uid, 'profile_picture', contact_obj.business_info.business_name, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
save_xuda_cache(uid, 'profile_picture', contact_obj.email, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
if (file_ret?.data?.file_url) {
|
|
2964
|
+
let { code: contact_code, data: contact_obj2 } = await db_module.get_app_couch_doc(account_profile_info.app_id, contact_id);
|
|
2965
|
+
contact_obj = contact_obj2;
|
|
2966
|
+
contact_obj.profile_picture_obj = file_ret.data;
|
|
2967
|
+
contact_obj.profile_picture = contact_obj.profile_picture_obj.file_url;
|
|
2968
|
+
contact_obj.profile_picture_source = file_ret.profile_picture_source;
|
|
2969
|
+
|
|
2970
|
+
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
2971
|
+
// profile_picture = contact_obj.profile_picture;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
////// PROFILE AVATAR
|
|
2975
|
+
if (!contact_obj.profile_avatar && contact_obj.profile_picture) {
|
|
2976
|
+
let cache;
|
|
2977
|
+
if (contact_obj?.account_type === 'business') {
|
|
2978
|
+
if (contact_obj?.business_has_person && contact_obj?.person_info?.person_full_name !== 'Not available') {
|
|
2979
|
+
cache = await get_xuda_cache(uid, 'profile_avatar', contact_obj.email, 'file');
|
|
2980
|
+
} else {
|
|
2981
|
+
cache = await get_xuda_cache(uid, 'profile_avatar', `${contact_obj?.business_info?.business_category || ''} ${contact_obj?.business_info?.business_sub_category || ''}`, 'file');
|
|
2982
|
+
}
|
|
2983
|
+
} else {
|
|
2984
|
+
cache = await get_xuda_cache(uid, 'profile_avatar', contact_obj.email, 'file');
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
let file_ret = { code: 1, data: cache };
|
|
2988
|
+
|
|
2989
|
+
if (!cache && create_avatar) {
|
|
2990
|
+
file_ret = await ai_ms.get_profile_avatar(
|
|
2991
|
+
contact_obj.profile_picture,
|
|
2992
|
+
uid,
|
|
2993
|
+
'',
|
|
2994
|
+
account_profile_info,
|
|
2995
|
+
contact_obj.account_type,
|
|
2996
|
+
'contact',
|
|
2997
|
+
contact_id,
|
|
2998
|
+
{
|
|
2999
|
+
business_name: contact_obj?.business_info?.business_name,
|
|
3000
|
+
name: contact_obj.name,
|
|
3001
|
+
bio: contact_obj?.business_info?.bio || contact_obj?.personal_info?.bio,
|
|
3002
|
+
country: contact_obj?.business_info?.business_country || contact_obj?.person_info?.person_nationality,
|
|
3003
|
+
mainCategory: contact_obj?.business_info?.business_category,
|
|
3004
|
+
subCategory: contact_obj?.business_info?.business_sub_category,
|
|
3005
|
+
business_has_person: contact_obj?.business_has_person && contact_obj?.person_info?.person_full_name !== 'Not available',
|
|
3006
|
+
person_nationality: contact_obj?.person_info?.person_nationality,
|
|
3007
|
+
},
|
|
3008
|
+
// { business_info: contact_obj.business_info, personal_info: contact_obj.personal_info, account_type_info: contact_obj.account_type_info },
|
|
3009
|
+
contact_obj?.business_info?.business_size,
|
|
3010
|
+
contact_obj?.business_info?.business_domain || contact_obj.name,
|
|
3011
|
+
contact_obj.email,
|
|
3012
|
+
job_id,
|
|
3013
|
+
headers,
|
|
3014
|
+
);
|
|
3015
|
+
|
|
3016
|
+
if (file_ret.code < 0) {
|
|
3017
|
+
if (file_ret.data === 'Input file contains unsupported image format') {
|
|
3018
|
+
contact_obj.profile_picture = null;
|
|
3019
|
+
contact_obj.profile_picture_obj = null;
|
|
3020
|
+
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
3021
|
+
}
|
|
3022
|
+
throw new Error(file_ret.data);
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
if (!_.isObject(file_ret.data)) throw new Error('file_ret not an object');
|
|
3026
|
+
|
|
3027
|
+
if (contact_obj?.account_type === 'business') {
|
|
3028
|
+
if (contact_obj?.business_has_person && contact_obj?.person_info?.person_full_name !== 'Not available') {
|
|
3029
|
+
cache = await save_xuda_cache(uid, 'profile_avatar', contact_obj.email, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3030
|
+
} else {
|
|
3031
|
+
cache = await save_xuda_cache(uid, 'profile_avatar', `${contact_obj?.business_info?.business_category || ''} ${contact_obj?.business_info?.business_sub_category || ''}`, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3032
|
+
}
|
|
3033
|
+
} else {
|
|
3034
|
+
cache = await save_xuda_cache(uid, 'profile_avatar', contact_obj.email, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// if (contact_obj?.account_type === 'business' && !contact_obj?.account_type_info?.is_real_person) {
|
|
3038
|
+
// cache = await save_xuda_cache(uid, 'profile_avatar', `${contact_obj?.business_info?.mainCategory || ''}_${contact_obj?.business_info?.subCategory || ''}`, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3039
|
+
// } else {
|
|
3040
|
+
// cache = await save_xuda_cache(uid, 'profile_avatar', contact_obj.email, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3041
|
+
// }
|
|
3042
|
+
|
|
3043
|
+
// if (contact_obj.business_name) {
|
|
3044
|
+
// save_xuda_cache(uid, 'profile_avatar', contact_obj.business_name, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3045
|
+
// }
|
|
3046
|
+
// save_xuda_cache(uid, 'profile_avatar', contact_obj.email, file_ret.data, { account_type: contact_obj.account_type, type: 'contact' });
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
let { code: contact_code, data: contact_obj3 } = await db_module.get_app_couch_doc(account_profile_info.app_id, contact_id);
|
|
3050
|
+
contact_obj = contact_obj3;
|
|
3051
|
+
contact_obj.profile_avatar_obj = file_ret.data;
|
|
3052
|
+
contact_obj.profile_avatar = contact_obj?.profile_avatar_obj?.file_url;
|
|
3053
|
+
contact_obj.avatar_source = file_ret?.data?.avatar_source;
|
|
3054
|
+
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
3055
|
+
}
|
|
3056
|
+
await update_contact_profile_picture_status(uid, contact_id, 3);
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
await update_contact_profile_picture_status(uid, contact_id, 1, err.message);
|
|
3059
|
+
delete_xuda_cache(contact_obj);
|
|
3060
|
+
}
|
|
3061
|
+
};
|
|
3062
|
+
|
|
3063
|
+
const update_account_profile_picture_status = async function (uid, stat, error) {
|
|
3064
|
+
try {
|
|
3065
|
+
let doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
3066
|
+
doc.account_info.profile_avatar_stat = stat;
|
|
3067
|
+
doc.account_info.profile_avatar_error = error;
|
|
3068
|
+
doc.account_info.profile_avatar_stat_ts = Date.now();
|
|
3069
|
+
const save_ret = await db_module.save_couch_doc_native('xuda_accounts', doc);
|
|
3070
|
+
} catch (err) {
|
|
3071
|
+
console.error(err);
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
|
|
3075
|
+
const update_contact_profile_picture_status = async function (uid, contact_id, stat, error) {
|
|
3076
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
3077
|
+
try {
|
|
3078
|
+
let doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
|
|
3079
|
+
doc.profile_avatar_stat = stat;
|
|
3080
|
+
doc.profile_avatar_error = error;
|
|
3081
|
+
doc.profile_avatar_stat_ts = Date.now();
|
|
3082
|
+
|
|
3083
|
+
const save_ret = await db_module.save_app_couch_doc_native(account_profile_info.app_id, doc);
|
|
3084
|
+
} catch (err) {
|
|
3085
|
+
console.error(err);
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
|
|
3089
|
+
export const get_contacts = async function (req, job_id, headers) {
|
|
3090
|
+
const { _id, uid, name, limit, skip, bookmark, search, filter_type = 'all', contact_id, profile_id } = req;
|
|
3091
|
+
try {
|
|
3092
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3093
|
+
if (_id) {
|
|
3094
|
+
let data = { code: 34, data: await get_contact(uid, _id) };
|
|
3095
|
+
return data;
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
let { with_requests } = req;
|
|
3099
|
+
var opt = {
|
|
3100
|
+
selector: {
|
|
3101
|
+
docType: 'contact',
|
|
3102
|
+
|
|
3103
|
+
stat: { $lt: 4 },
|
|
3104
|
+
},
|
|
3105
|
+
limit: limit || 9999,
|
|
3106
|
+
skip: skip || 0,
|
|
3107
|
+
sort: [{ ts: 'desc' }],
|
|
3108
|
+
};
|
|
3109
|
+
|
|
3110
|
+
if (!account_profile_info.is_main) {
|
|
3111
|
+
opt.selector.account_profiles = { $in: [account_profile_info.account_profile_id] };
|
|
3112
|
+
} else {
|
|
3113
|
+
const active_profiles = await db_module.find_app_couch_query(account_profile_info.app_id, {
|
|
3114
|
+
selector: {
|
|
3115
|
+
docType: 'account_profile',
|
|
3116
|
+
uid: account_profile_info.uid,
|
|
3117
|
+
stat: { $lt: 4 },
|
|
3118
|
+
},
|
|
3119
|
+
fields: ['_id'],
|
|
3120
|
+
limit: 9999,
|
|
3121
|
+
});
|
|
3122
|
+
|
|
3123
|
+
const active_profile_ids = active_profiles.docs.map((profile) => profile._id).filter(Boolean);
|
|
3124
|
+
opt.selector.account_profiles = { $in: active_profile_ids.length ? active_profile_ids : [account_profile_info.account_profile_id] };
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
switch (filter_type) {
|
|
3128
|
+
case 'archived': {
|
|
3129
|
+
opt.selector.stat = 5;
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3132
|
+
case 'shared': {
|
|
3133
|
+
opt.selector.shared_from_uid = { $gt: null };
|
|
3134
|
+
break;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
case 'pinned': {
|
|
3138
|
+
opt.selector.pinned = true;
|
|
3139
|
+
break;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
case 'online': {
|
|
3143
|
+
opt.limit = 9999;
|
|
3144
|
+
break;
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
default:
|
|
3148
|
+
with_requests = true;
|
|
3149
|
+
break;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
if (_id) {
|
|
3153
|
+
opt.selector._id = _id;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
if (contact_id) {
|
|
3157
|
+
opt.selector._id = contact_id;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
if (typeof name !== 'undefined') {
|
|
3161
|
+
opt.selector.name = { $regex: `(?i)${name}` };
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
if (typeof search !== 'undefined') {
|
|
3165
|
+
opt.selector['$or'] = [
|
|
3166
|
+
{
|
|
3167
|
+
name: {
|
|
3168
|
+
$regex: `(?i)${search}`,
|
|
3169
|
+
},
|
|
3170
|
+
},
|
|
3171
|
+
{
|
|
3172
|
+
email: {
|
|
3173
|
+
$regex: `(?i)${search}`,
|
|
3174
|
+
},
|
|
3175
|
+
},
|
|
3176
|
+
{
|
|
3177
|
+
_id: {
|
|
3178
|
+
$regex: `(?i)${search}`,
|
|
3179
|
+
},
|
|
3180
|
+
},
|
|
3181
|
+
];
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
if (bookmark && bookmark !== 'nil') {
|
|
3185
|
+
opt.bookmark = bookmark;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
let contacts = { docs: [], total_docs: 0 };
|
|
3189
|
+
|
|
3190
|
+
if (filter_type !== 'pending') {
|
|
3191
|
+
contacts = await find_contact_query(account_profile_info.uid, opt);
|
|
3192
|
+
if (!limit || contact_id) {
|
|
3193
|
+
contacts.total_docs = contacts.docs.length;
|
|
3194
|
+
} else {
|
|
3195
|
+
delete opt.sort;
|
|
3196
|
+
delete opt.skip;
|
|
3197
|
+
opt.limit = 9999;
|
|
3198
|
+
opt.fields = ['_id'];
|
|
3199
|
+
|
|
3200
|
+
const counter = await find_contact_query(account_profile_info.uid, opt);
|
|
3201
|
+
contacts.total_docs = counter.docs.length;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
let requests_to = { docs: [], total_docs: 0 };
|
|
3206
|
+
let requests_from = { docs: [], total_docs: 0 };
|
|
3207
|
+
let shared_from = { docs: [], total_docs: 0 };
|
|
3208
|
+
|
|
3209
|
+
let contact_docs = { docs: [], total_docs: contacts.total_docs };
|
|
3210
|
+
|
|
3211
|
+
for await (let doc of contacts.docs) {
|
|
3212
|
+
const ret_to = requests_to.docs.find((e) => {
|
|
3213
|
+
return e.contact_uid === doc.contact_uid;
|
|
3214
|
+
});
|
|
3215
|
+
if (ret_to) continue;
|
|
3216
|
+
|
|
3217
|
+
const ret_from = requests_from.docs.find((e) => {
|
|
3218
|
+
return e.contact_uid === doc.contact_uid;
|
|
3219
|
+
});
|
|
3220
|
+
if (ret_from) continue;
|
|
3221
|
+
|
|
3222
|
+
doc = await get_contact_info(uid, doc);
|
|
3223
|
+
|
|
3224
|
+
// doc.interactions = contact_chat_conversation_count_ret[doc._id];
|
|
3225
|
+
// doc.notifications = contact_chat_conversation_count_ret[doc._id] - contact_chat_conversation_read_ret[doc._id];
|
|
3226
|
+
// doc.chats = doc.interactions;
|
|
3227
|
+
|
|
3228
|
+
if (filter_type === 'online') {
|
|
3229
|
+
if (!doc.online) continue;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
contact_docs.docs.push(doc);
|
|
3233
|
+
|
|
3234
|
+
if (contact_id) {
|
|
3235
|
+
if (!doc.profile_avatar && !doc.is_spam && (!doc.profile_avatar_stat || doc.profile_avatar_stat === 1 || (doc.profile_avatar_stat === 2 && (!doc.profile_avatar_stat_ts || Date.now() - doc.profile_avatar_stat_ts > 1000 * 60 * 5)))) {
|
|
3236
|
+
set_contact_profile_picture(uid, doc._id, {}, job_id, headers, account_profile_info, false);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
if (!contact_id && with_requests) {
|
|
3242
|
+
if (filter_type === 'pending' && account_profile_info.is_main) {
|
|
3243
|
+
////// TO - OUT
|
|
3244
|
+
requests_to.docs = await get_pending_contact_out(uid);
|
|
3245
|
+
requests_to.total_docs = requests_to.docs.length;
|
|
3246
|
+
|
|
3247
|
+
////// FROM - IN
|
|
3248
|
+
requests_from.docs = await get_pending_contact_in(uid);
|
|
3249
|
+
requests_from.total_docs = requests_from.docs.length;
|
|
3250
|
+
shared_from.docs = await get_pending_share_contact_in(uid);
|
|
3251
|
+
shared_from.total_docs = shared_from.docs.length;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
return {
|
|
3256
|
+
code: 1,
|
|
3257
|
+
data: {
|
|
3258
|
+
docs: [...shared_from.docs, ...requests_from.docs, ...requests_to.docs, ...contact_docs.docs],
|
|
3259
|
+
total_docs: shared_from.total_docs + requests_from.total_docs + requests_to.total_docs + contact_docs.total_docs,
|
|
3260
|
+
},
|
|
3261
|
+
};
|
|
3262
|
+
} catch (err) {
|
|
3263
|
+
return { code: -34, data: err.message };
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
|
|
3267
|
+
export const archive_contact = async function (req) {
|
|
3268
|
+
const { uid, contact_id } = req;
|
|
3269
|
+
try {
|
|
3270
|
+
await team_ms.validate_share_exist(uid, 'contact', contact_id);
|
|
3271
|
+
|
|
3272
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3273
|
+
|
|
3274
|
+
if (contact_doc.uid !== uid && contact_doc?.account_profile_info?.uid !== uid) {
|
|
3275
|
+
throw new Error('Operation not allowed');
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
contact_doc.stat = 5;
|
|
3279
|
+
contact_doc.stat_ts = Date.now();
|
|
3280
|
+
contact_doc.stat_reason = 'archived by the user';
|
|
3281
|
+
|
|
3282
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3283
|
+
|
|
3284
|
+
return contact_save_ret;
|
|
3285
|
+
} catch (err) {
|
|
3286
|
+
return {
|
|
3287
|
+
code: -22,
|
|
3288
|
+
data: err.message,
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3293
|
+
export const delete_contact = async function (req, job_id, headers) {
|
|
3294
|
+
const { uid, contact_id } = req;
|
|
3295
|
+
try {
|
|
3296
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3297
|
+
if (contact_doc.stat !== 5) {
|
|
3298
|
+
throw new Error('A document can only be deleted after it has been archived');
|
|
3299
|
+
}
|
|
3300
|
+
contact_doc.stat = 4;
|
|
3301
|
+
contact_doc.stat_ts = Date.now();
|
|
3302
|
+
contact_doc.stat_reason = 'deleted by the user';
|
|
3303
|
+
|
|
3304
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3305
|
+
|
|
3306
|
+
ai_msa.delete_depended_chats(uid, contact_id);
|
|
3307
|
+
return contact_save_ret;
|
|
3308
|
+
} catch (err) {
|
|
3309
|
+
return {
|
|
3310
|
+
code: -22,
|
|
3311
|
+
data: err.message,
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
};
|
|
3315
|
+
|
|
3316
|
+
export const unarchive_contact = async function (req, job_id, headers) {
|
|
3317
|
+
const { uid, contact_id } = req;
|
|
3318
|
+
try {
|
|
3319
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3320
|
+
|
|
3321
|
+
if (contact_doc.stat !== 5) {
|
|
3322
|
+
throw new Error('contact is not archived');
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
contact_doc.stat = 3;
|
|
3326
|
+
contact_doc.stat_ts = Date.now();
|
|
3327
|
+
contact_doc.stat_reason = 'unarchive by the user';
|
|
3328
|
+
|
|
3329
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3330
|
+
|
|
3331
|
+
if (!contact_doc.is_spam) {
|
|
3332
|
+
not_spam_contact(req, job_id, headers);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
return contact_save_ret;
|
|
3336
|
+
} catch (err) {
|
|
3337
|
+
return {
|
|
3338
|
+
code: -22,
|
|
3339
|
+
data: err.message,
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
|
|
3344
|
+
export const unfriend_contact = async function (req) {
|
|
3345
|
+
const { uid, contact_id } = req;
|
|
3346
|
+
try {
|
|
3347
|
+
let contact_doc = await get_contact(uid, contact_id);
|
|
3348
|
+
|
|
3349
|
+
if (!contact_doc.team_req_id) {
|
|
3350
|
+
throw new Error('no friend relationship found');
|
|
3351
|
+
}
|
|
3352
|
+
let req_doc = await db_module.get_couch_doc_native('xuda_team', contact_doc.team_req_id);
|
|
3353
|
+
req_doc.team_req_stat = 4;
|
|
3354
|
+
req_doc.team_req_stat_desc = 'unfriend by the user';
|
|
3355
|
+
await db_module.get_couch_doc_native('xuda_team', contact_doc.team_req_id);
|
|
3356
|
+
|
|
3357
|
+
contact_doc.team_req_id = null;
|
|
3358
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3359
|
+
|
|
3360
|
+
const req_save_ret = await db_module.save_couch_doc('xuda_team', req_doc);
|
|
3361
|
+
|
|
3362
|
+
return req_save_ret;
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
return {
|
|
3365
|
+
code: -21,
|
|
3366
|
+
data: err.message,
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
|
|
3371
|
+
export const pin_contact = async function (req) {
|
|
3372
|
+
const { uid, contact_id } = req;
|
|
3373
|
+
try {
|
|
3374
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3375
|
+
contact_doc.pinned = true;
|
|
3376
|
+
|
|
3377
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3378
|
+
|
|
3379
|
+
ws_dashboard_msa.emit_message_to_dashboard({
|
|
3380
|
+
service: 'contact_pinned',
|
|
3381
|
+
to: [uid],
|
|
3382
|
+
data: await get_contact_info(uid, null, contact_id),
|
|
3383
|
+
});
|
|
3384
|
+
|
|
3385
|
+
return contact_save_ret;
|
|
3386
|
+
} catch (err) {
|
|
3387
|
+
return {
|
|
3388
|
+
code: -24,
|
|
3389
|
+
data: err.message,
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3393
|
+
|
|
3394
|
+
export const set_contact_deep_research = async function (req) {
|
|
3395
|
+
const { uid, contact_id } = req;
|
|
3396
|
+
try {
|
|
3397
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3398
|
+
contact_doc.deep_research = true;
|
|
3399
|
+
|
|
3400
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3401
|
+
|
|
3402
|
+
// attachments transcript
|
|
3403
|
+
// business info
|
|
3404
|
+
// personal info
|
|
3405
|
+
|
|
3406
|
+
return contact_save_ret;
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
return {
|
|
3409
|
+
code: -24,
|
|
3410
|
+
data: err.message,
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
|
|
3415
|
+
export const unpin_contact = async function (req) {
|
|
3416
|
+
const { contact_id, uid } = req;
|
|
3417
|
+
try {
|
|
3418
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3419
|
+
contact_doc.pinned = false;
|
|
3420
|
+
|
|
3421
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3422
|
+
|
|
3423
|
+
ws_dashboard_msa.emit_message_to_dashboard({
|
|
3424
|
+
service: 'contact_unpinned',
|
|
3425
|
+
to: [uid],
|
|
3426
|
+
data: await get_contact_info(uid, null, contact_id),
|
|
3427
|
+
});
|
|
3428
|
+
return contact_save_ret;
|
|
3429
|
+
} catch (err) {
|
|
3430
|
+
return {
|
|
3431
|
+
code: -24,
|
|
3432
|
+
data: err.message,
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
|
|
3437
|
+
export const not_spam_contact = async function (req, job_id, headers) {
|
|
3438
|
+
const { contact_id, uid } = req;
|
|
3439
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
3440
|
+
try {
|
|
3441
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3442
|
+
|
|
3443
|
+
if (contact_doc.is_spam) {
|
|
3444
|
+
await db_module.save_couch_doc_native('xuda_accounts', {
|
|
3445
|
+
_id: await _common.xuda_get_uuid('spam_whitelist'),
|
|
3446
|
+
uid,
|
|
3447
|
+
docType: 'spam_whitelist',
|
|
3448
|
+
email: contact_doc.email,
|
|
3449
|
+
date_created_ts: Date.now(),
|
|
3450
|
+
stat: 3,
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
contact_doc.is_spam = false;
|
|
3454
|
+
|
|
3455
|
+
if (!contact_doc.contact_reference_conversation_id) {
|
|
3456
|
+
const conversation_obj = await ai_ms.create_openai_conversation();
|
|
3457
|
+
contact_doc.contact_reference_conversation_id = conversation_obj.id;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
const is_business = 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);
|
|
3461
|
+
contact_doc.account_type = is_business ? 'business' : 'personal';
|
|
3462
|
+
if (is_business) {
|
|
3463
|
+
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info, { light: true });
|
|
3464
|
+
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
3465
|
+
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);
|
|
3466
|
+
} else {
|
|
3467
|
+
contact_doc.account_type = 'personal';
|
|
3468
|
+
contact_doc.business_info = undefined;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3473
|
+
|
|
3474
|
+
await delete_xuda_cache(contact_doc);
|
|
3475
|
+
set_contact_profile_picture(uid, contact_doc._id, {}, job_id, headers, account_profile_info, false);
|
|
3476
|
+
|
|
3477
|
+
const emails = await db_module.find_app_couch_query(account_profile_info.app_id, {
|
|
3478
|
+
selector: {
|
|
3479
|
+
docType: 'email',
|
|
3480
|
+
contact_id,
|
|
3481
|
+
stat: 3,
|
|
3482
|
+
process_stat: 'partial',
|
|
3483
|
+
},
|
|
3484
|
+
});
|
|
3485
|
+
|
|
3486
|
+
for await (let email of emails.docs) {
|
|
3487
|
+
const app_id = await get_account_default_project_id(uid);
|
|
3488
|
+
if (email.conversation_id) {
|
|
3489
|
+
db_module.delete_app_couch_doc(app_id, email.conversation_id);
|
|
3490
|
+
email.conversation_id = null;
|
|
3491
|
+
}
|
|
3492
|
+
if (email.conversation_item_id) {
|
|
3493
|
+
db_module.delete_app_couch_doc(app_id, email.conversation_item_id);
|
|
3494
|
+
email.conversation_item_id = null;
|
|
3495
|
+
}
|
|
3496
|
+
const save_ret = await db_module.save_app_couch_doc_native(app_id, email);
|
|
3497
|
+
|
|
3498
|
+
// save attachments
|
|
3499
|
+
// summarized_body
|
|
3500
|
+
req.uid = email.uid;
|
|
3501
|
+
req.email_id = email._id;
|
|
3502
|
+
await email_ms.process_pending_email(req, job_id, headers);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
return contact_save_ret;
|
|
3506
|
+
} catch (err) {
|
|
3507
|
+
return {
|
|
3508
|
+
code: -24,
|
|
3509
|
+
data: err.message,
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
};
|
|
3513
|
+
|
|
3514
|
+
export const generate_contact_avatar = async function (req, job_id, headers) {
|
|
3515
|
+
const { contact_id, uid } = req;
|
|
3516
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
3517
|
+
try {
|
|
3518
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
3519
|
+
if (account_doc.membership_plan == 'free' && account_doc.ai_workspace_plan == 'free_ai_workspace') {
|
|
3520
|
+
throw new Error('cannot read emails for free account');
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3524
|
+
|
|
3525
|
+
contact_doc.profile_picture_obj = null;
|
|
3526
|
+
contact_doc.profile_picture = null;
|
|
3527
|
+
contact_doc.profile_avatar_obj = null;
|
|
3528
|
+
contact_doc.profile_avatar = null;
|
|
3529
|
+
|
|
3530
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3531
|
+
await delete_xuda_cache(contact_doc);
|
|
3532
|
+
set_contact_profile_picture(uid, contact_doc._id, {}, job_id, headers, account_profile_info, true);
|
|
3533
|
+
|
|
3534
|
+
return contact_save_ret;
|
|
3535
|
+
} catch (err) {
|
|
3536
|
+
return {
|
|
3537
|
+
code: -24,
|
|
3538
|
+
data: err.message,
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
|
|
3543
|
+
export const set_deep_research_contact = async function (req, job_id, headers) {
|
|
3544
|
+
const { contact_id, uid } = req;
|
|
3545
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
3546
|
+
try {
|
|
3547
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3548
|
+
contact_doc.deep_research = true;
|
|
3549
|
+
|
|
3550
|
+
if (contact_doc.account_type === 'business') {
|
|
3551
|
+
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info);
|
|
3552
|
+
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
3553
|
+
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);
|
|
3554
|
+
} else {
|
|
3555
|
+
contact_doc.account_type = 'personal';
|
|
3556
|
+
contact_doc.business_info = undefined;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
if (contact_doc.account_type === 'personal' || contact_doc?.business_has_person) {
|
|
3561
|
+
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);
|
|
3562
|
+
if (!contact_doc.name) {
|
|
3563
|
+
contact_doc.name = await ai_ms.get_name_from_email_addr(uid, contact_doc.email, account_profile_info);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3568
|
+
await delete_xuda_cache(contact_doc);
|
|
3569
|
+
|
|
3570
|
+
const conversation_items = await db_module.find_app_couch_query(account_profile_info.app_id, {
|
|
3571
|
+
selector: {
|
|
3572
|
+
docType: 'chat_conversation_item',
|
|
3573
|
+
reference_id: contact_id,
|
|
3574
|
+
stat: 3,
|
|
3575
|
+
process_stat: 'partial',
|
|
3576
|
+
},
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
for await (let conversation_item of conversation_items.docs) {
|
|
3580
|
+
try {
|
|
3581
|
+
const account_profile_info = await get_active_account_profile_info(conversation_item.uid);
|
|
3582
|
+
let conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_item.conversation_id);
|
|
3583
|
+
|
|
3584
|
+
const transcript = await ai_ms.get_transcript(conversation_item.uid, conversation_item.app_id, conversation_item.conversation_id, account_profile_info, conversation_item.file.filename);
|
|
3585
|
+
let items;
|
|
3586
|
+
|
|
3587
|
+
if (transcript) {
|
|
3588
|
+
items = await ai_ms.add_transcript_conversation_item(conversation_doc.reference_conversation_id, conversation_item.file.filename, transcript);
|
|
3589
|
+
|
|
3590
|
+
conversation_item.text = transcript?.data || transcript || '';
|
|
3591
|
+
conversation_item.process_stat = 'full';
|
|
3592
|
+
const conversation_item_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, conversation_item);
|
|
3593
|
+
}
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
throw err;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
return contact_save_ret;
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
return {
|
|
3602
|
+
code: -24,
|
|
3603
|
+
data: err.message,
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
};
|
|
3607
|
+
|
|
3608
|
+
export const unset_deep_research_contact = async function (req, job_id, headers) {
|
|
3609
|
+
const { contact_id, uid } = req;
|
|
3610
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
3611
|
+
try {
|
|
3612
|
+
var contact_doc = await get_contact(uid, contact_id);
|
|
3613
|
+
contact_doc.deep_research = false;
|
|
3614
|
+
|
|
3615
|
+
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
3616
|
+
await delete_xuda_cache(contact_doc);
|
|
3617
|
+
|
|
3618
|
+
return contact_save_ret;
|
|
3619
|
+
} catch (err) {
|
|
3620
|
+
return {
|
|
3621
|
+
code: -24,
|
|
3622
|
+
data: err.message,
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
//////// PROFILES //////////////
|
|
3627
|
+
|
|
3628
|
+
export const get_pending_share_profile_in = async function (uid, _id) {
|
|
3629
|
+
let docs = [];
|
|
3630
|
+
const from_opt = { selector: { docType: 'team_request', access_type: 'account_profile', team_req_stat: 2, team_req_to_uid: uid }, fields: ['_id', 'team_req_date', 'sender_account_info.email', 'team_req_from_uid', 'share_item_id'] };
|
|
3631
|
+
|
|
3632
|
+
if (_id) {
|
|
3633
|
+
to_opt.selector._id = _id;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
const requests_from_res = await db_module.find_couch_query('xuda_team', from_opt);
|
|
3637
|
+
|
|
3638
|
+
for await (let e of requests_from_res.docs) {
|
|
3639
|
+
const account_profile_info = await get_active_account_profile_info(e.team_req_from_uid);
|
|
3640
|
+
let doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, e.share_item_id);
|
|
3641
|
+
|
|
3642
|
+
doc.shared_from_uid = e.team_req_from_uid;
|
|
3643
|
+
doc.pending = true;
|
|
3644
|
+
doc = await get_account_profile_info(uid, doc);
|
|
3645
|
+
doc.team_req_id = e._id;
|
|
3646
|
+
|
|
3647
|
+
docs.push(doc);
|
|
3648
|
+
}
|
|
3649
|
+
return docs;
|
|
3650
|
+
};
|
|
3651
|
+
|
|
3652
|
+
export const get_account_profile_info = async function (uid, contact_profile_doc, _id) {
|
|
3653
|
+
const app_id = await get_account_default_project_id(uid);
|
|
3654
|
+
let doc = contact_profile_doc;
|
|
3655
|
+
if (_id) {
|
|
3656
|
+
doc = await db_module.get_couch_doc_native('xuda_accounts', _id);
|
|
3657
|
+
}
|
|
3658
|
+
doc.notifications = 0;
|
|
3659
|
+
|
|
3660
|
+
let uid_query = uid;
|
|
3661
|
+
if (doc.shared_from_uid) uid_query = doc.shared_from_uid;
|
|
3662
|
+
const account_info_ret = await get_account_name({ uid_query });
|
|
3663
|
+
if (account_info_ret.code < 0) {
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
if (!doc.profile_avatar) {
|
|
3667
|
+
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
3668
|
+
}
|
|
3669
|
+
if (!doc.profile_picture) {
|
|
3670
|
+
doc.profile_picture = account_info_ret.data.profile_picture;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
if (!doc.account_type) {
|
|
3674
|
+
doc.account_type = account_info_ret.data.account_type;
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
const get_pattern = async function (doc) {
|
|
3678
|
+
let ret = `default-pattern.png`;
|
|
3679
|
+
|
|
3680
|
+
if (doc.shared_from_uid) {
|
|
3681
|
+
// const account_profile_doc = await db_module.get_couch_doc_native('xuda_accounts', doc.share_item_id);
|
|
3682
|
+
const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
|
|
3683
|
+
ret = shared_from_uid_ret.data.profile_avatar;
|
|
3684
|
+
} else if (doc.is_spam) {
|
|
3685
|
+
ret = `spam-pattern.png`;
|
|
3686
|
+
} else if (doc.account_type === 'business') {
|
|
3687
|
+
ret = doc.profile_picture;
|
|
3688
|
+
} else {
|
|
3689
|
+
switch (doc.avatar_source) {
|
|
3690
|
+
case 'fictional':
|
|
3691
|
+
ret = `fictional-avatar-pattern.png`;
|
|
3692
|
+
break;
|
|
3693
|
+
|
|
3694
|
+
case 'ai profile':
|
|
3695
|
+
ret = `fictional-avatar-pattern.png`;
|
|
3696
|
+
break;
|
|
3697
|
+
|
|
3698
|
+
case 'authentic profile':
|
|
3699
|
+
ret = `authentic-avatar-pattern.png`;
|
|
3700
|
+
break;
|
|
3701
|
+
|
|
3702
|
+
default:
|
|
3703
|
+
ret = `xu-pattern.png`;
|
|
3704
|
+
break;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
return ret;
|
|
3709
|
+
};
|
|
3710
|
+
|
|
3711
|
+
const get_online = async () => {
|
|
3712
|
+
let online = false;
|
|
3713
|
+
let selector = { docType: 'team_request', team_req_stat: 3, share_item_id: doc._id, access_type: 'account_profile', team_req_from_uid: uid };
|
|
3714
|
+
let ret = await db_module.find_couch_query('xuda_team', { selector });
|
|
3715
|
+
for (const req_obj of ret.docs) {
|
|
3716
|
+
const account_info_ret = await get_account_name({ uid_query: req_obj.team_req_to_uid });
|
|
3717
|
+
online = account_info_ret.data.online;
|
|
3718
|
+
if (online) break;
|
|
3719
|
+
}
|
|
3720
|
+
return online;
|
|
3721
|
+
};
|
|
3722
|
+
|
|
3723
|
+
const stringToColour = (str) => {
|
|
3724
|
+
let hash = 0;
|
|
3725
|
+
// Generate a numeric hash from the string
|
|
3726
|
+
for (let i = 0; i < str.length; i++) {
|
|
3727
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
3728
|
+
}
|
|
3729
|
+
let colour = '#';
|
|
3730
|
+
// Convert the hash into a 6-digit hex color code
|
|
3731
|
+
for (let i = 0; i < 3; i++) {
|
|
3732
|
+
const value = (hash >> (i * 8)) & 0xff;
|
|
3733
|
+
colour += ('00' + value.toString(16)).substr(-2);
|
|
3734
|
+
}
|
|
3735
|
+
return colour;
|
|
3736
|
+
};
|
|
3737
|
+
const get_members = async (uid, share_item_id) => {
|
|
3738
|
+
let members = [];
|
|
3739
|
+
let selector = { docType: 'team_request', team_req_stat: 3, share_item_id, access_type: 'account_profile', team_req_from_uid: uid };
|
|
3740
|
+
let ret = await db_module.find_couch_query('xuda_team', { selector });
|
|
3741
|
+
for (const req_obj of ret.docs) {
|
|
3742
|
+
const account_info_ret = await get_account_name({ uid_query: req_obj.team_req_to_uid });
|
|
3743
|
+
const info = await await get_contact_info(uid, account_info_ret.data);
|
|
3744
|
+
members.push(info);
|
|
3745
|
+
}
|
|
3746
|
+
return members;
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3749
|
+
const get_user_account_profiles = async () => {
|
|
3750
|
+
// const account_profile_info = await get_active_account_profile_info(uid);
|
|
3751
|
+
|
|
3752
|
+
let selector = { docType: 'account_profile', stat: 3 };
|
|
3753
|
+
let ret = await db_module.find_app_couch_query(app_id, { selector });
|
|
3754
|
+
|
|
3755
|
+
return ret.docs;
|
|
3756
|
+
};
|
|
3757
|
+
|
|
3758
|
+
// Usage examples:
|
|
3759
|
+
// console.log(stringToColour('User A')); // -> Consistent color for "User A"
|
|
3760
|
+
// console.log(stringToColour('Project Z')); // -> Consistent color for "Project Z"
|
|
3761
|
+
|
|
3762
|
+
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
3763
|
+
doc.icon_pattern = await get_pattern(doc);
|
|
3764
|
+
doc.name = doc.profile_name;
|
|
3765
|
+
doc.interactions = 0;
|
|
3766
|
+
doc.chats = 0;
|
|
3767
|
+
|
|
3768
|
+
doc.online = await get_online();
|
|
3769
|
+
|
|
3770
|
+
// doc.border = account_info_ret.data.active_account_profile_id === doc._id ? 'yellow' : ''; //'#1a3557';
|
|
3771
|
+
// doc.border = stringToColour(doc.shared_from_uid || doc.uid);
|
|
3772
|
+
// const chats_ret = await ai_ms.get_ai_chats({ uid, reference_id: doc._id });
|
|
3773
|
+
|
|
3774
|
+
doc.card_background = `linear-gradient(145deg, #242480 0%, #060619 100%)`;
|
|
3775
|
+
// doc.border = get_contact_border(doc);
|
|
3776
|
+
|
|
3777
|
+
doc.members = await get_members(doc.shared_from_uid || doc.uid, doc.share_item_id || doc._id);
|
|
3778
|
+
const user_account_profiles = await get_user_account_profiles();
|
|
3779
|
+
|
|
3780
|
+
const usage_ret = await get_account_ai_usage({ uid: doc.shared_from_uid || doc.uid });
|
|
3781
|
+
|
|
3782
|
+
doc.max_credits = usage_ret?.data?.credits.total / user_account_profiles.length;
|
|
3783
|
+
doc.used_credits = 0;
|
|
3784
|
+
for (const [user, amount] of Object.entries(usage_ret?.data?.usage?.profile?.[doc._id] || {})) {
|
|
3785
|
+
doc.used_credits += amount;
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
// doc.used_credits = usage_ret?.data?.usage?.profile?.[doc._id] || 0;
|
|
3789
|
+
|
|
3790
|
+
if (doc.shared_from_uid) {
|
|
3791
|
+
doc.max_credits = doc.max_credits / (doc.members.length + 1 || 1);
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
return doc;
|
|
3795
|
+
};
|
|
3796
|
+
|
|
3797
|
+
export const get_account_profiles = async function (req) {
|
|
3798
|
+
const { _id, uid, profile_name, limit, skip, bookmark, search, filter_type = 'all', profile_id, active_tab } = req;
|
|
3799
|
+
const app_id = await get_account_default_project_id(uid);
|
|
3800
|
+
if (_id) {
|
|
3801
|
+
let data = await db_module.get_app_couch_doc(app_id, _id);
|
|
3802
|
+
return data;
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
var opt = {
|
|
3806
|
+
selector: {
|
|
3807
|
+
docType: 'account_profile',
|
|
3808
|
+
uid,
|
|
3809
|
+
stat: { $lt: 4 },
|
|
3810
|
+
},
|
|
3811
|
+
limit: limit || 9999,
|
|
3812
|
+
skip: skip || 0,
|
|
3813
|
+
sort: [{ ts: 'desc' }],
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
switch (filter_type) {
|
|
3817
|
+
case 'archived': {
|
|
3818
|
+
opt.selector.stat = 5;
|
|
3819
|
+
break;
|
|
3820
|
+
}
|
|
3821
|
+
case 'shared': {
|
|
3822
|
+
opt.selector.shared_from_uid = { $gt: null };
|
|
3823
|
+
break;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
default:
|
|
3827
|
+
break;
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
// if (_id) {
|
|
3831
|
+
// opt.selector._id = _id;
|
|
3832
|
+
// }
|
|
3833
|
+
|
|
3834
|
+
if (profile_id) {
|
|
3835
|
+
opt.selector._id = profile_id;
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
if (typeof profile_name !== 'undefined') {
|
|
3839
|
+
opt.selector.name = { $regex: `(?i)${profile_name}` };
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
if (typeof search !== 'undefined') {
|
|
3843
|
+
opt.selector['$or'] = [
|
|
3844
|
+
{
|
|
3845
|
+
profile_name: {
|
|
3846
|
+
$regex: `(?i)${search}`,
|
|
3847
|
+
},
|
|
3848
|
+
},
|
|
3849
|
+
{
|
|
3850
|
+
profile_email: {
|
|
3851
|
+
$regex: `(?i)${search}`,
|
|
3852
|
+
},
|
|
3853
|
+
},
|
|
3854
|
+
{
|
|
3855
|
+
_id: {
|
|
3856
|
+
$regex: `(?i)${search}`,
|
|
3857
|
+
},
|
|
3858
|
+
},
|
|
3859
|
+
];
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
if (bookmark && bookmark !== 'nil') {
|
|
3863
|
+
opt.bookmark = bookmark;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
let profiles = { docs: [], total_docs: 0 };
|
|
3867
|
+
|
|
3868
|
+
if (filter_type !== 'pending') {
|
|
3869
|
+
profiles = await db_module.find_app_couch_query(app_id, opt);
|
|
3870
|
+
if (!limit || profile_id) {
|
|
3871
|
+
profiles.total_docs = profiles.docs.length;
|
|
3872
|
+
} else {
|
|
3873
|
+
delete opt.sort;
|
|
3874
|
+
delete opt.skip;
|
|
3875
|
+
opt.limit = 9999;
|
|
3876
|
+
opt.fields = ['_id'];
|
|
3877
|
+
|
|
3878
|
+
const counter = await db_module.find_app_couch_query(app_id, opt);
|
|
3879
|
+
profiles.total_docs = counter.docs.length;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
let shared_from = { docs: [], total_docs: 0 };
|
|
3884
|
+
|
|
3885
|
+
let profile_docs = { docs: [], total_docs: profiles.total_docs };
|
|
3886
|
+
for await (let doc of profiles.docs) {
|
|
3887
|
+
doc = await get_account_profile_info(uid, doc);
|
|
3888
|
+
if (doc.main) {
|
|
3889
|
+
profile_docs.docs.unshift(doc);
|
|
3890
|
+
} else {
|
|
3891
|
+
profile_docs.docs.push(doc);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
if (!profile_id) {
|
|
3896
|
+
if (filter_type === 'pending') {
|
|
3897
|
+
////// FROM - IN
|
|
3898
|
+
shared_from.docs = await get_pending_share_profile_in(uid);
|
|
3899
|
+
shared_from.total_docs = shared_from.docs.length;
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
// if (filter_type === 'all' && !profile_id && !search && active_tab && profile_docs.docs.length === 1 && profile_docs.docs[0].main) {
|
|
3904
|
+
// // return empty state if no custom profile defined by user
|
|
3905
|
+
// return {
|
|
3906
|
+
// code: 1,
|
|
3907
|
+
// data: {
|
|
3908
|
+
// docs: [],
|
|
3909
|
+
// total_docs: 0,
|
|
3910
|
+
// },
|
|
3911
|
+
// };
|
|
3912
|
+
// }
|
|
3913
|
+
|
|
3914
|
+
return {
|
|
3915
|
+
code: 1,
|
|
3916
|
+
data: {
|
|
3917
|
+
docs: [...shared_from.docs, ...profile_docs.docs],
|
|
3918
|
+
total_docs: shared_from.total_docs + profile_docs.total_docs,
|
|
3919
|
+
},
|
|
3920
|
+
};
|
|
3921
|
+
};
|
|
3922
|
+
|
|
3923
|
+
export const archive_account_profile = async function (req) {
|
|
3924
|
+
const { uid, profile_id } = req;
|
|
3925
|
+
try {
|
|
3926
|
+
const app_id = await get_account_default_project_id(uid);
|
|
3927
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
3928
|
+
|
|
3929
|
+
// await team_ms.validate_share_exist(uid, 'account_profile', profile_id);
|
|
3930
|
+
|
|
3931
|
+
var account_profile_doc = await db_module.get_app_couch_doc_native(app_id, profile_id);
|
|
3932
|
+
|
|
3933
|
+
if (account_profile_doc.main) {
|
|
3934
|
+
throw new Error('reserve');
|
|
3935
|
+
}
|
|
3936
|
+
// if (!account_profile_doc.team_req_id) {
|
|
3937
|
+
account_profile_doc.stat = 5;
|
|
3938
|
+
account_profile_doc.stat_ts = Date.now();
|
|
3939
|
+
account_profile_doc.stat_reason = 'archived by the user';
|
|
3940
|
+
|
|
3941
|
+
const account_profile_save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
|
|
3942
|
+
|
|
3943
|
+
if (account_doc?.account_info?.active_account_profile_id === profile_id) {
|
|
3944
|
+
account_doc.account_info.active_account_profile_id = account_doc.account_profile_id;
|
|
3945
|
+
await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
return account_profile_save_ret;
|
|
3949
|
+
} catch (err) {
|
|
3950
|
+
return {
|
|
3951
|
+
code: -22,
|
|
3952
|
+
data: err.message,
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
|
|
3957
|
+
export const delete_account_profile = async function (req, job_id, headers) {
|
|
3958
|
+
const { profile_id, uid } = req;
|
|
3959
|
+
try {
|
|
3960
|
+
const app_id = await get_account_default_project_id(uid);
|
|
3961
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
3962
|
+
|
|
3963
|
+
await team_ms.validate_share_exist(uid, 'account_profile', profile_id);
|
|
3964
|
+
|
|
3965
|
+
var account_profile_doc = await db_module.get_app_couch_doc_native(app_id, profile_id);
|
|
3966
|
+
if (account_profile_doc.stat !== 5) {
|
|
3967
|
+
throw new Error('A document can only be deleted after it has been archived');
|
|
3968
|
+
}
|
|
3969
|
+
account_profile_doc.stat = 4;
|
|
3970
|
+
account_profile_doc.stat_ts = Date.now();
|
|
3971
|
+
account_profile_doc.stat_reason = 'deleted by the user';
|
|
3972
|
+
|
|
3973
|
+
const account_profile_save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
|
|
3974
|
+
|
|
3975
|
+
if (account_doc?.account_info?.active_account_profile_id === profile_id) {
|
|
3976
|
+
account_doc.account_info.active_account_profile_id = account_doc.account_profile_id;
|
|
3977
|
+
await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
ai_msa.delete_depended_chats(uid, profile_id);
|
|
3981
|
+
return account_profile_save_ret;
|
|
3982
|
+
} catch (err) {
|
|
3983
|
+
return {
|
|
3984
|
+
code: -22,
|
|
3985
|
+
data: err.message,
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
|
|
3990
|
+
export const unarchive_account_profile = async function (req) {
|
|
3991
|
+
const { profile_id, uid } = req;
|
|
3992
|
+
|
|
3993
|
+
try {
|
|
3994
|
+
const app_id = await get_account_default_project_id(uid);
|
|
3995
|
+
|
|
3996
|
+
var account_profile_doc = await db_module.get_app_couch_doc_native(app_id, profile_id);
|
|
3997
|
+
|
|
3998
|
+
if (account_profile_doc.stat !== 5) {
|
|
3999
|
+
throw new Error('account profile is not archived');
|
|
4000
|
+
}
|
|
4001
|
+
if (!account_profile_doc.team_req_id) {
|
|
4002
|
+
account_profile_doc.stat = 3;
|
|
4003
|
+
account_profile_doc.stat_ts = Date.now();
|
|
4004
|
+
account_profile_doc.stat_reason = 'archived by the user';
|
|
4005
|
+
|
|
4006
|
+
const account_profile_save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
|
|
4007
|
+
|
|
4008
|
+
return account_profile_save_ret;
|
|
4009
|
+
}
|
|
4010
|
+
} catch (err) {
|
|
4011
|
+
return {
|
|
4012
|
+
code: -22,
|
|
4013
|
+
data: err.message,
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
};
|
|
4017
|
+
|
|
4018
|
+
export const create_account_profile = async function (req, job_id, headers) {
|
|
4019
|
+
const { uid, profile_name, profile_signature, email_account_id, profile_picture, profile_avatar, profile_picture_obj, profile_avatar_obj, main, account_type } = req;
|
|
4020
|
+
try {
|
|
4021
|
+
const app_id = await get_account_default_project_id(uid);
|
|
4022
|
+
// const { data: acc_obj } = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
4023
|
+
|
|
4024
|
+
const d = Date.now();
|
|
4025
|
+
const doc = {
|
|
4026
|
+
_id: await _common.xuda_get_uuid('account_profile'),
|
|
4027
|
+
stat: 3,
|
|
4028
|
+
date_created_ts: d,
|
|
4029
|
+
ts: d,
|
|
4030
|
+
docType: 'account_profile',
|
|
4031
|
+
account_type,
|
|
4032
|
+
uid,
|
|
4033
|
+
profile_name, //: profile_name || acc_obj.account_info.business_name || acc_obj.account_info.name,
|
|
4034
|
+
profile_signature,
|
|
4035
|
+
email_account_id,
|
|
4036
|
+
profile_picture,
|
|
4037
|
+
profile_avatar,
|
|
4038
|
+
profile_picture_obj,
|
|
4039
|
+
profile_avatar_obj,
|
|
4040
|
+
main,
|
|
4041
|
+
};
|
|
4042
|
+
const save_ret = await db_module.save_app_couch_doc(app_id, doc);
|
|
4043
|
+
// acc_obj.active_profile_id = save_ret.data.id;
|
|
4044
|
+
|
|
4045
|
+
// return { code: 55, data: save_ret };
|
|
4046
|
+
return save_ret;
|
|
4047
|
+
} catch (err) {
|
|
4048
|
+
return { code: -55, data: err.message };
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
|
|
4052
|
+
export const update_account_profile = async function (req, job_id, headers) {
|
|
4053
|
+
const { uid, _id } = req;
|
|
4054
|
+
const app_id = await get_account_default_project_id(uid);
|
|
4055
|
+
const account_profile_properties = [
|
|
4056
|
+
'profile_name',
|
|
4057
|
+
'profile_signature',
|
|
4058
|
+
'email_account_id',
|
|
4059
|
+
'profile_picture',
|
|
4060
|
+
'profile_avatar',
|
|
4061
|
+
'profile_picture_obj',
|
|
4062
|
+
'profile_avatar_obj',
|
|
4063
|
+
'auto_respond',
|
|
4064
|
+
'auto_respond_mode',
|
|
4065
|
+
'auto_respond_agents',
|
|
4066
|
+
'account_type',
|
|
4067
|
+
'active_ai_model',
|
|
4068
|
+
'ai_models',
|
|
4069
|
+
'active_agents',
|
|
4070
|
+
];
|
|
4071
|
+
try {
|
|
4072
|
+
let { data: account_profile_doc } = await db_module.get_app_couch_doc(app_id, _id);
|
|
4073
|
+
|
|
4074
|
+
if (account_profile_doc.docType !== 'account_profile') {
|
|
4075
|
+
throw new Error('not account profile doc');
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
let changes_arr = [];
|
|
4079
|
+
|
|
4080
|
+
for (const key of account_profile_properties) {
|
|
4081
|
+
let val = req[key];
|
|
4082
|
+
if (typeof val === 'undefined') continue;
|
|
4083
|
+
if (account_profile_doc[key] !== val) {
|
|
4084
|
+
changes_arr.push(key);
|
|
4085
|
+
account_profile_doc[key] = val;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
if (!changes_arr.length) {
|
|
4090
|
+
// throw new Error('no change');
|
|
4091
|
+
return { code: 56, data: 'no change' };
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
account_profile_doc.ts = Date.now();
|
|
4095
|
+
|
|
4096
|
+
const save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
|
|
4097
|
+
|
|
4098
|
+
return { code: 56, data: save_ret };
|
|
4099
|
+
} catch (err) {
|
|
4100
|
+
return { code: -56, data: err.message };
|
|
4101
|
+
}
|
|
4102
|
+
};
|
|
4103
|
+
|
|
4104
|
+
export const update_entity_account_profiles = async function (req, job_id, headers) {
|
|
4105
|
+
const { uid, _id, account_profiles = [] } = req;
|
|
4106
|
+
const app_id = await get_account_default_project_id(uid);
|
|
4107
|
+
|
|
4108
|
+
try {
|
|
4109
|
+
if (!account_profiles.length) {
|
|
4110
|
+
throw new Error('account_profiles cannot be empty');
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
let doc = await db_module.get_app_couch_doc_native(app_id, _id);
|
|
4114
|
+
|
|
4115
|
+
switch (doc.docType) {
|
|
4116
|
+
case 'chat_conversation':
|
|
4117
|
+
case 'contact': {
|
|
4118
|
+
doc.account_profiles = account_profiles;
|
|
4119
|
+
break;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
case 'studio': {
|
|
4123
|
+
//mini_app //ai_agent
|
|
4124
|
+
if (doc.studio_meta.miniApp || doc.properties.menuType === 'ai_agent') {
|
|
4125
|
+
doc.studio_meta.account_profiles = account_profiles;
|
|
4126
|
+
break;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
default:
|
|
4131
|
+
throw new Error('entity not supported');
|
|
4132
|
+
break;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
const save_ret = await db_module.save_app_couch_doc(app_id, doc);
|
|
4136
|
+
|
|
4137
|
+
return { code: 57, data: save_ret };
|
|
4138
|
+
} catch (err) {
|
|
4139
|
+
return { code: -57, data: err.message };
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
|
|
4143
|
+
export const get_contact = async function (uid, contact_id) {
|
|
4144
|
+
if (!contact_id) return null;
|
|
4145
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
4146
|
+
|
|
4147
|
+
const contact_ret = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
|
|
4148
|
+
return contact_ret;
|
|
4149
|
+
};
|
|
4150
|
+
|
|
4151
|
+
export const save_contact = async function (uid, contact_doc) {
|
|
4152
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
4153
|
+
const contact_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_doc);
|
|
4154
|
+
return contact_ret;
|
|
4155
|
+
};
|
|
4156
|
+
|
|
4157
|
+
export const find_contact_query = async function (uid, opt) {
|
|
4158
|
+
// const app_id = await get_account_default_project_id(uid);
|
|
4159
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
4160
|
+
const ret = await db_module.find_app_couch_query(account_profile_info.app_id, opt);
|
|
4161
|
+
return ret;
|
|
4162
|
+
};
|
|
4163
|
+
|
|
4164
|
+
//////////////////////////////
|
|
4165
|
+
|
|
4166
|
+
export const delete_xuda_cache = async function (contact_obj) {
|
|
4167
|
+
const ret = await db_module.find_couch_query('xuda_cache', {
|
|
4168
|
+
selector: {
|
|
4169
|
+
$or: [
|
|
4170
|
+
{
|
|
4171
|
+
key: contact_obj?.business_info?.business_domain?.toLowerCase() || '',
|
|
4172
|
+
},
|
|
4173
|
+
{ key: contact_obj?.business_info?.business_name?.toLowerCase() || '' },
|
|
4174
|
+
{ key: `${contact_obj?.business_info?.business_category?.toLowerCase() || ''} ${contact_obj?.business_info?.business_sub_category?.toLowerCase() || ''}` },
|
|
4175
|
+
{ key: contact_obj?.email?.toLowerCase() || '' },
|
|
4176
|
+
],
|
|
4177
|
+
},
|
|
4178
|
+
});
|
|
4179
|
+
|
|
4180
|
+
for (let doc of ret.docs) {
|
|
4181
|
+
doc._deleted = true;
|
|
4182
|
+
const save_ret = await db_module.save_couch_doc('xuda_cache', doc);
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
|
|
4186
|
+
export const save_xuda_cache = async function (uid, docType, key, file, metadata = {}) {
|
|
4187
|
+
if (!key || typeof key !== 'string') return null;
|
|
4188
|
+
|
|
4189
|
+
const cache = await get_xuda_cache(uid, docType, key);
|
|
4190
|
+
if (cache) return;
|
|
4191
|
+
|
|
4192
|
+
const keyWords = key
|
|
4193
|
+
.trim()
|
|
4194
|
+
.toLowerCase()
|
|
4195
|
+
.split(/\s+/)
|
|
4196
|
+
.filter((w) => w.length > 1)
|
|
4197
|
+
.filter((w, i, arr) => arr.indexOf(w) === i); // deduplicate
|
|
4198
|
+
|
|
4199
|
+
let doc = {
|
|
4200
|
+
_id: await _common.xuda_get_uuid('cache'),
|
|
4201
|
+
key: key.toLowerCase().trim(),
|
|
4202
|
+
docType,
|
|
4203
|
+
uid,
|
|
4204
|
+
file,
|
|
4205
|
+
date_created_ts: Date.now(),
|
|
4206
|
+
metadata,
|
|
4207
|
+
hits: 0,
|
|
4208
|
+
keyWords,
|
|
4209
|
+
};
|
|
4210
|
+
const save_ret = await db_module.save_couch_doc('xuda_cache', doc);
|
|
4211
|
+
return save_ret;
|
|
4212
|
+
};
|
|
4213
|
+
|
|
4214
|
+
export const get_xuda_cache = async function (uid, docType, key, return_property) {
|
|
4215
|
+
let ret = await db_module.find_couch_query('xuda_cache', {
|
|
4216
|
+
selector: { docType, key: key.toLowerCase().trim() },
|
|
4217
|
+
limit: 1,
|
|
4218
|
+
});
|
|
4219
|
+
if (ret.docs.length) {
|
|
4220
|
+
save_cache_hit(ret.docs[0]._id);
|
|
4221
|
+
return (return_property ? ret.docs[0]?.[return_property] : ret.docs[0]) ?? null;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
return null;
|
|
4225
|
+
|
|
4226
|
+
// ret = await db_module.find_couch_query('xuda_cache', {
|
|
4227
|
+
// selector: { docType, name: name.trim() },
|
|
4228
|
+
// limit: 1,
|
|
4229
|
+
// });
|
|
4230
|
+
// if (ret.docs.length) {
|
|
4231
|
+
// save_cache_hit(ret.docs[0]._id);
|
|
4232
|
+
// return (return_property ? ret.docs[0]?.[return_property] : ret.docs[0]) ?? null;
|
|
4233
|
+
// }
|
|
4234
|
+
};
|
|
4235
|
+
|
|
4236
|
+
export const save_cache_hit = async function (_id) {
|
|
4237
|
+
let doc_ret = await db_module.get_couch_doc('xuda_cache', _id);
|
|
4238
|
+
if (doc_ret.code > -1) {
|
|
4239
|
+
doc_ret.data.hits++;
|
|
4240
|
+
await db_module.save_couch_doc('xuda_cache', doc_ret.data);
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
|
|
4244
|
+
///////////////////////////////
|
|
4245
|
+
|
|
4246
|
+
export const get_account_ai_usage = async function (req, job_id, headers) {
|
|
4247
|
+
const d = new Date();
|
|
4248
|
+
const curr_month = Number(String(d.getMonth() + 1).padStart(2, '0')); // months are 0-based
|
|
4249
|
+
const curr_year = d.getFullYear();
|
|
4250
|
+
|
|
4251
|
+
const { uid, year_from = curr_year, month_from = curr_month, day_from = 0, year_to = curr_year, month_to = curr_month, day_to = 99 } = req;
|
|
4252
|
+
try {
|
|
4253
|
+
const app_id = await get_account_default_project_id(uid);
|
|
4254
|
+
|
|
4255
|
+
// const timestamp_from = new Date(Number(year_from) || year, (Number(month_from) || month) - 1, Number(day_from) || day).getTime();
|
|
4256
|
+
// const timestamp_to = new Date(Number(year_to) || year, (Number(month_to) || month) - 1, Number(day_to) || day).getTime();
|
|
4257
|
+
|
|
4258
|
+
// BEGINNING OF DAY (00:00:00.000)
|
|
4259
|
+
const timestamp_from = new Date(Number(year_from), Number(month_from) - 1, Number(day_from)).setHours(0, 0, 0, 0);
|
|
4260
|
+
|
|
4261
|
+
// END OF DAY (23:59:59.999)
|
|
4262
|
+
const timestamp_to = new Date(Number(year_to), Number(month_to) - 1, Number(day_to)).setHours(23, 59, 59, 999);
|
|
4263
|
+
|
|
4264
|
+
const query = {
|
|
4265
|
+
// include_docs: true,
|
|
4266
|
+
startkey: [uid, timestamp_from],
|
|
4267
|
+
endkey: [uid, timestamp_to],
|
|
4268
|
+
// reduce: true,
|
|
4269
|
+
// group_level: 999,
|
|
4270
|
+
};
|
|
4271
|
+
const response = await db_module.call_list_function('xuda_usage', 'ai_usage2', 'filter_usage', query);
|
|
4272
|
+
|
|
4273
|
+
// console.log('ai_usage', ai_usage.rows);
|
|
4274
|
+
|
|
4275
|
+
let total_usage = response.total_usage;
|
|
4276
|
+
let profile = response.profiles;
|
|
4277
|
+
|
|
4278
|
+
const ai_credits = await db_module.get_couch_view('xuda_billing', 'ai_credits', {
|
|
4279
|
+
startkey: [uid, ''],
|
|
4280
|
+
endkey: [uid, 'zzz'],
|
|
4281
|
+
reduce: true,
|
|
4282
|
+
group_level: 999,
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
let credits_obj = {},
|
|
4286
|
+
total_credits = 0;
|
|
4287
|
+
for (const row of ai_credits.data.rows) {
|
|
4288
|
+
const user = row.key[0];
|
|
4289
|
+
const source = row.key[1];
|
|
4290
|
+
|
|
4291
|
+
const credits = row.value;
|
|
4292
|
+
|
|
4293
|
+
total_credits += credits;
|
|
4294
|
+
|
|
4295
|
+
if (!credits_obj[source]) {
|
|
4296
|
+
credits_obj[source] = 0;
|
|
4297
|
+
}
|
|
4298
|
+
credits_obj[source] += credits;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
let data = { credits: { ...credits_obj, total: total_credits }, usage: { profile, projects: [], total: total_usage }, packs: _conf.ai_credit_packs || [] };
|
|
4302
|
+
|
|
4303
|
+
if (job_id) {
|
|
4304
|
+
data.profile_info = {};
|
|
4305
|
+
data.user_info = {};
|
|
4306
|
+
for (const [key, val] of Object.entries(data?.usage?.profile || {})) {
|
|
4307
|
+
try {
|
|
4308
|
+
data.profile_info[key] = (await db_module.get_app_couch_doc_native(app_id, key))?.profile_name;
|
|
4309
|
+
|
|
4310
|
+
for (const [user_id, val2] of Object.entries(val || {})) {
|
|
4311
|
+
try {
|
|
4312
|
+
const account_info = (await db_module.get_couch_doc_native('xuda_accounts', user_id)).account_info;
|
|
4313
|
+
data.user_info[user_id] = account_info.account_type === 'business' ? account_info.business_name : `${account_info.first_name} ${account_info.last_name}`;
|
|
4314
|
+
} catch (error) {}
|
|
4315
|
+
}
|
|
4316
|
+
} catch (error) {}
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
return { code: 55, data };
|
|
4321
|
+
} catch (err) {
|
|
4322
|
+
return { code: -55, data: err.message };
|
|
4323
|
+
}
|
|
4324
|
+
};
|
|
4325
|
+
|
|
4326
|
+
export const get_account_ai_usage_old = async function (req, job_id, headers) {
|
|
4327
|
+
const { uid, year_from, month_from, day_from, year_to, month_to, day_to } = req;
|
|
4328
|
+
try {
|
|
4329
|
+
const app_id = await get_account_default_project_id(uid);
|
|
4330
|
+
|
|
4331
|
+
const d = new Date();
|
|
4332
|
+
const curr_month = Number(String(d.getMonth() + 1).padStart(2, '0')); // months are 0-based
|
|
4333
|
+
const curr_year = d.getFullYear();
|
|
4334
|
+
|
|
4335
|
+
const ai_usage = await db_module.get_couch_view('xuda_usage', 'ai_usage', {
|
|
4336
|
+
startkey: [uid, year_from || curr_year, month_from || curr_month, day_from || 0],
|
|
4337
|
+
endkey: [uid, year_to || curr_year, month_to || curr_month, day_to || 99],
|
|
4338
|
+
reduce: true,
|
|
4339
|
+
group_level: 999,
|
|
4340
|
+
});
|
|
4341
|
+
// console.log('ai_usage', ai_usage.rows);
|
|
4342
|
+
debugger;
|
|
4343
|
+
let total_usage = 0;
|
|
4344
|
+
let profile = {};
|
|
4345
|
+
|
|
4346
|
+
for (const row of ai_usage.data.rows) {
|
|
4347
|
+
const user = row.key[0];
|
|
4348
|
+
const year = row.key[1];
|
|
4349
|
+
const month = row.key[2];
|
|
4350
|
+
const day = row.key[3];
|
|
4351
|
+
const account_profile_id = row.key[4];
|
|
4352
|
+
const uid = row.key[5];
|
|
4353
|
+
const model = row.key[6];
|
|
4354
|
+
const input_tokens = row.value[0];
|
|
4355
|
+
const output_tokens = row.value[1];
|
|
4356
|
+
const input_credits = row.value[2];
|
|
4357
|
+
const output_credits = row.value[3];
|
|
4358
|
+
if (!profile[account_profile_id]) {
|
|
4359
|
+
profile[account_profile_id] = {};
|
|
4360
|
+
}
|
|
4361
|
+
if (!profile[account_profile_id][uid]) {
|
|
4362
|
+
profile[account_profile_id][uid] = 0;
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
profile[account_profile_id][uid] += input_credits + output_credits;
|
|
4366
|
+
total_usage += input_credits + output_credits;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
// let membership_plan = _conf.PLAN_OBJ?.[acc_obj?.ai_workspace_plan]?.features?.ai_credits || 0;
|
|
4370
|
+
// let ai_workspace_plan = _conf.PLAN_OBJ?.[acc_obj?.membership_plan]?.features?.ai_credits || 0;
|
|
4371
|
+
// let contact_connection = 0;
|
|
4372
|
+
// let total_credits = membership_plan + ai_workspace_plan + contact_connection;
|
|
4373
|
+
|
|
4374
|
+
const ai_credits = await db_module.get_couch_view('xuda_billing', 'ai_credits', {
|
|
4375
|
+
startkey: [uid, ''],
|
|
4376
|
+
endkey: [uid, 'zzz'],
|
|
4377
|
+
reduce: true,
|
|
4378
|
+
group_level: 999,
|
|
4379
|
+
});
|
|
4380
|
+
|
|
4381
|
+
let credits_obj = {},
|
|
4382
|
+
total_credits = 0;
|
|
4383
|
+
for (const row of ai_credits.data.rows) {
|
|
4384
|
+
const user = row.key[0];
|
|
4385
|
+
const source = row.key[1];
|
|
4386
|
+
|
|
4387
|
+
const credits = row.value;
|
|
4388
|
+
|
|
4389
|
+
total_credits += credits;
|
|
4390
|
+
|
|
4391
|
+
if (!credits_obj[source]) {
|
|
4392
|
+
credits_obj[source] = 0;
|
|
4393
|
+
}
|
|
4394
|
+
credits_obj[source] += credits;
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
let data = { credits: { ...credits_obj, total: total_credits }, usage: { profile, projects: [], total: total_usage }, packs: _conf.ai_credit_packs || [] };
|
|
4398
|
+
|
|
4399
|
+
if (job_id) {
|
|
4400
|
+
data.profile_info = {};
|
|
4401
|
+
data.user_info = {};
|
|
4402
|
+
for (const [key, val] of Object.entries(data?.usage?.profile || {})) {
|
|
4403
|
+
try {
|
|
4404
|
+
data.profile_info[key] = (await db_module.get_app_couch_doc_native(app_id, key))?.profile_name;
|
|
4405
|
+
|
|
4406
|
+
for (const [user_id, val2] of Object.entries(val || {})) {
|
|
4407
|
+
try {
|
|
4408
|
+
data.user_info[user_id] = (await db_module.get_couch_doc_native('xuda_accounts', user_id))?.account_info?.full_name;
|
|
4409
|
+
} catch (error) {}
|
|
4410
|
+
}
|
|
4411
|
+
} catch (error) {}
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
return { code: 55, data };
|
|
4416
|
+
} catch (err) {
|
|
4417
|
+
return { code: -55, data: err.message };
|
|
4418
|
+
}
|
|
4419
|
+
};
|
|
4420
|
+
|
|
4421
|
+
// Push the account's live AI-credit balance to its dashboard WS room, so every
|
|
4422
|
+
// meter (the AiPrompt meter, the sidenav ring, the usage popup) updates the
|
|
4423
|
+
// instant credits are spent or topped up — no refresh. Debounced per-uid so a
|
|
4424
|
+
// burst of usage records (a streaming chat writing many chunks) collapses into a
|
|
4425
|
+
// single balance recompute + emit. Reuses get_account_ai_usage (the ledger read)
|
|
4426
|
+
// and emit_message_to_dashboard (the same uid-room push used for account updates).
|
|
4427
|
+
const _credits_broadcast_timers = {};
|
|
4428
|
+
export const broadcast_credits = function (uid) {
|
|
4429
|
+
if (!uid || uid === 'system' || uid === 'webhook') return;
|
|
4430
|
+
clearTimeout(_credits_broadcast_timers[uid]);
|
|
4431
|
+
_credits_broadcast_timers[uid] = setTimeout(async () => {
|
|
4432
|
+
delete _credits_broadcast_timers[uid];
|
|
4433
|
+
try {
|
|
4434
|
+
const ret = await get_account_ai_usage({ uid });
|
|
4435
|
+
if (!ret || ret.code !== 55) return;
|
|
4436
|
+
const max = Math.round((ret.data?.credits?.total || 0) * 100) / 100;
|
|
4437
|
+
const used = Math.round((ret.data?.usage?.total || 0) * 100) / 100;
|
|
4438
|
+
ws_dashboard_msa.emit_message_to_dashboard({
|
|
4439
|
+
service: 'credits_update',
|
|
4440
|
+
to: [uid],
|
|
4441
|
+
data: { used, max, remaining: Math.round((max - used) * 100) / 100, by_source: ret.data?.credits || {} },
|
|
4442
|
+
});
|
|
4443
|
+
} catch (e) {
|
|
4444
|
+
console.error('[broadcast_credits]', e?.message || e);
|
|
4445
|
+
}
|
|
4446
|
+
}, 800);
|
|
4447
|
+
};
|
|
4448
|
+
|
|
4449
|
+
export const record_ai_usage = async function (uid, input_tokens, output_tokens, source, prompt, model, metadata = {}, account_profile_info, tools) {
|
|
4450
|
+
try {
|
|
4451
|
+
if (!account_profile_info) throw new Error('missing account_profile_info');
|
|
4452
|
+
|
|
4453
|
+
const cost = _conf?.ai_models?.[model] || {
|
|
4454
|
+
input: 1.0,
|
|
4455
|
+
output: 1.0,
|
|
4456
|
+
};
|
|
4457
|
+
|
|
4458
|
+
const t = Date.now();
|
|
4459
|
+
let usage_doc = {
|
|
4460
|
+
_id: await _common.xuda_get_uuid('ai_usage'),
|
|
4461
|
+
docType: 'ai_usage',
|
|
4462
|
+
prompt: typeof prompt === 'string' ? prompt.substr(0, 40) : '',
|
|
4463
|
+
date_created_ts: t,
|
|
4464
|
+
uid,
|
|
4465
|
+
source,
|
|
4466
|
+
input_tokens,
|
|
4467
|
+
output_tokens,
|
|
4468
|
+
stat: 3,
|
|
4469
|
+
metadata,
|
|
4470
|
+
model,
|
|
4471
|
+
cost,
|
|
4472
|
+
account_profile_info,
|
|
4473
|
+
tools,
|
|
4474
|
+
};
|
|
4475
|
+
const save_ret = await db_module.save_couch_doc('xuda_usage', usage_doc);
|
|
4476
|
+
// console.log(save_ret);
|
|
4477
|
+
broadcast_credits(uid); // live meter: this account just spent credits
|
|
4478
|
+
} catch (err) {
|
|
4479
|
+
console.error(err);
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
|
|
4483
|
+
export const record_ai_credit = async function (uid, credits = 0, source, details, credited_uid) {
|
|
4484
|
+
try {
|
|
4485
|
+
var dup = await db_module.find_couch_query('xuda_billing', {
|
|
4486
|
+
selector: {
|
|
4487
|
+
docType: 'ai_credit',
|
|
4488
|
+
credited_uid,
|
|
4489
|
+
details,
|
|
4490
|
+
},
|
|
4491
|
+
fields: ['_id'],
|
|
4492
|
+
limit: 1,
|
|
4493
|
+
});
|
|
4494
|
+
|
|
4495
|
+
if (dup.docs.length) {
|
|
4496
|
+
throw new Error(`${details} - already credited!`);
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
const t = Date.now();
|
|
4500
|
+
let credit_doc = {
|
|
4501
|
+
_id: await _common.xuda_get_uuid('ai_credit'),
|
|
4502
|
+
docType: 'ai_credit',
|
|
4503
|
+
credits,
|
|
4504
|
+
date_created_ts: t,
|
|
4505
|
+
uid,
|
|
4506
|
+
source,
|
|
4507
|
+
stat: 3,
|
|
4508
|
+
credited_uid,
|
|
4509
|
+
details,
|
|
4510
|
+
};
|
|
4511
|
+
const save_ret = await db_module.save_couch_doc('xuda_billing', credit_doc);
|
|
4512
|
+
// console.log(save_ret);
|
|
4513
|
+
broadcast_credits(credited_uid); // live meter: this account just gained credits
|
|
4514
|
+
return save_ret;
|
|
4515
|
+
} catch (err) {
|
|
4516
|
+
if (!err?.message?.includes('already credited!')) {
|
|
4517
|
+
console.error(err);
|
|
4518
|
+
}
|
|
4519
|
+
return { code: -78, data: err.message };
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
|
|
4523
|
+
export const add_ai_credits_to_active_accounts = async function () {
|
|
4524
|
+
let d = new Date();
|
|
4525
|
+
|
|
4526
|
+
let date_str = d.toLocaleDateString('en-US', {
|
|
4527
|
+
month: 'long',
|
|
4528
|
+
day: 'numeric',
|
|
4529
|
+
year: 'numeric',
|
|
4530
|
+
});
|
|
4531
|
+
|
|
4532
|
+
const _24_hr_ms = 1000 * 60 * 60 * 24;
|
|
4533
|
+
const active_accounts = await db_module.find_couch_query('xuda_accounts', { selector: { stat: 3, docType: 'account', last_free_daily_ai_credit_ts: { $lt: Date.now() - _24_hr_ms }, ai_workspace_plan: 'free_ai_workspace' }, limit: 99999 });
|
|
4534
|
+
for await (let account_doc of active_accounts.docs) {
|
|
4535
|
+
const ret = await record_ai_credit('system', 1, 'daily credit', 'free daily credit ' + date_str, account_doc._id);
|
|
4536
|
+
if (ret.code > -1) {
|
|
4537
|
+
account_doc.last_free_daily_ai_credit_ts = Date.now();
|
|
4538
|
+
account_doc.last_free_daily_ai_credit_id = ret.data.id;
|
|
4539
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
};
|
|
4543
|
+
|
|
4544
|
+
export const archive_expire_ai_credits = async function () {
|
|
4545
|
+
const _24_hr_ms = 1000 * 60 * 60 * 24;
|
|
4546
|
+
const _mo_ms = _24_hr_ms * 30;
|
|
4547
|
+
|
|
4548
|
+
const active_ai_credits = await db_module.find_couch_query('xuda_billing', { selector: { stat: 3, docType: 'ai_credit', date_created_ts: { $lt: Date.now() - _mo_ms } }, limit: 99999 });
|
|
4549
|
+
for await (let ai_credit_doc of active_ai_credits.docs) {
|
|
4550
|
+
ai_credit_doc.stat = 5;
|
|
4551
|
+
ai_credit_doc.stat_ts = Date.now();
|
|
4552
|
+
ai_credit_doc.stat_reason = 'expired';
|
|
4553
|
+
const save_ret = await db_module.save_couch_doc('xuda_billing', ai_credit_doc);
|
|
4554
|
+
}
|
|
4555
|
+
};
|
|
4556
|
+
|
|
4557
|
+
export const read_accounts_emails = async function () {
|
|
4558
|
+
const active_accounts = await db_module.find_couch_query('xuda_accounts', { selector: { stat: 3, docType: 'account' }, limit: 99999 });
|
|
4559
|
+
for await (let account_doc of active_accounts.docs) {
|
|
4560
|
+
if (!account_doc?.account_info?.active_account_profile_id) continue;
|
|
4561
|
+
if (account_doc.membership_plan == 'free' && account_doc.ai_workspace_plan == 'free_ai_workspace') continue;
|
|
4562
|
+
email_msa.refresh_mailboxes({ uid: account_doc._id });
|
|
4563
|
+
}
|
|
4564
|
+
};
|
|
4565
|
+
|
|
4566
|
+
export const process_accounts_emails = async function () {
|
|
4567
|
+
const active_accounts = await db_module.find_couch_query('xuda_accounts', { selector: { stat: 3, docType: 'account' }, limit: 99999 });
|
|
4568
|
+
for await (let account_doc of active_accounts.docs) {
|
|
4569
|
+
if (!account_doc?.account_info?.active_account_profile_id) continue;
|
|
4570
|
+
email_msa.process_emails({ uid: account_doc._id });
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4573
|
+
|
|
4574
|
+
const isLikelySpamEmail = async function (email) {
|
|
4575
|
+
const spam_whitelists = await db_module.find_couch_query('xuda_accounts', {
|
|
4576
|
+
selector: {
|
|
4577
|
+
docType: 'spam_whitelist',
|
|
4578
|
+
email,
|
|
4579
|
+
stat: 3,
|
|
4580
|
+
},
|
|
4581
|
+
limit: 1,
|
|
4582
|
+
});
|
|
4583
|
+
if (spam_whitelists.docs.length) return false;
|
|
4584
|
+
|
|
4585
|
+
if (!email || typeof email !== 'string') return true;
|
|
4586
|
+
|
|
4587
|
+
const lower = email.toLowerCase().trim();
|
|
4588
|
+
|
|
4589
|
+
// Must have exactly one @ and non-empty parts
|
|
4590
|
+
if (!lower.includes('@') || lower.split('@').length !== 2) return true;
|
|
4591
|
+
|
|
4592
|
+
const [localPart, domain] = lower.split('@');
|
|
4593
|
+
|
|
4594
|
+
if (!localPart || !domain) return true;
|
|
4595
|
+
|
|
4596
|
+
// 1. Obvious syntax/format red flags (unchanged — strict is good here)
|
|
4597
|
+
if (localPart.length > 64 || domain.length > 255 || localPart.startsWith('.') || localPart.endsWith('.') || localPart.includes('..') || domain.includes('..') || domain.startsWith('-') || domain.endsWith('-') || /[^\w.!#$%&'*+/=?^`{|}~-]/.test(localPart)) {
|
|
4598
|
+
return true;
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
// 2. Common spam patterns (unchanged — these are strong signals)
|
|
4602
|
+
const spamPatterns = [
|
|
4603
|
+
/\d{8,}/, // long consecutive digits
|
|
4604
|
+
/[a-z]{20,}/, // very long random lowercase
|
|
4605
|
+
/unsubscribe/i,
|
|
4606
|
+
/free|win|bonus|deal|promo/i,
|
|
4607
|
+
/\+\d+$/,
|
|
4608
|
+
/^[\d\W_]+$/,
|
|
4609
|
+
];
|
|
4610
|
+
|
|
4611
|
+
if (spamPatterns.some((p) => p.test(localPart))) {
|
|
4612
|
+
return true;
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
// 3. Free / major disposable providers — RELAXED
|
|
4616
|
+
const freeOrDisposableDomains = new Set([
|
|
4617
|
+
'gmail.com',
|
|
4618
|
+
'yahoo.com',
|
|
4619
|
+
'hotmail.com',
|
|
4620
|
+
'outlook.com',
|
|
4621
|
+
'aol.com',
|
|
4622
|
+
'icloud.com',
|
|
4623
|
+
'proton.me',
|
|
4624
|
+
'protonmail.com',
|
|
4625
|
+
'mail.com',
|
|
4626
|
+
'yandex.com',
|
|
4627
|
+
'ymail.com',
|
|
4628
|
+
'rocketmail.com',
|
|
4629
|
+
'gmx.com',
|
|
4630
|
+
'gmx.net',
|
|
4631
|
+
'web.de',
|
|
4632
|
+
'live.com',
|
|
4633
|
+
'msn.com',
|
|
4634
|
+
'inbox.com',
|
|
4635
|
+
'zoho.com',
|
|
4636
|
+
'tutanota.com',
|
|
4637
|
+
'tempmail.com',
|
|
4638
|
+
'10minutemail.com',
|
|
4639
|
+
'guerrillamail.com',
|
|
4640
|
+
'trashmail.com',
|
|
4641
|
+
'mailinator.com',
|
|
4642
|
+
'yopmail.com',
|
|
4643
|
+
'sharklasers.com',
|
|
4644
|
+
// Consider dynamic loading for real disposables (see comment above)
|
|
4645
|
+
]);
|
|
4646
|
+
|
|
4647
|
+
if (freeOrDisposableDomains.has(domain)) {
|
|
4648
|
+
const cleanLocal = localPart.split('+')[0].toLowerCase(); // ignore +tag
|
|
4649
|
+
|
|
4650
|
+
// Expanded serious/role keywords
|
|
4651
|
+
const roleKeywords = ['info', 'support', 'sales', 'contact', 'hello', 'team', 'admin', 'office', 'billing', 'help', 'service', 'orders', 'bookings', 'quotes', 'enquiries', 'noreply', 'no-reply'];
|
|
4652
|
+
|
|
4653
|
+
// New: business/service descriptive keywords (very common for small trades on Gmail)
|
|
4654
|
+
const businessKeywords = [
|
|
4655
|
+
'locksmith',
|
|
4656
|
+
'doors',
|
|
4657
|
+
'garage',
|
|
4658
|
+
'repair',
|
|
4659
|
+
'pro',
|
|
4660
|
+
'expert',
|
|
4661
|
+
'247',
|
|
4662
|
+
'24hr',
|
|
4663
|
+
'emergency',
|
|
4664
|
+
'mobile',
|
|
4665
|
+
'auto',
|
|
4666
|
+
'key',
|
|
4667
|
+
'security',
|
|
4668
|
+
'home',
|
|
4669
|
+
'commercial',
|
|
4670
|
+
'residential',
|
|
4671
|
+
'plumbing',
|
|
4672
|
+
'electric',
|
|
4673
|
+
'hvac',
|
|
4674
|
+
'cleaning',
|
|
4675
|
+
'pest',
|
|
4676
|
+
'movers',
|
|
4677
|
+
'towing',
|
|
4678
|
+
'tutor',
|
|
4679
|
+
// Add more relevant to your audience/use-case
|
|
4680
|
+
];
|
|
4681
|
+
|
|
4682
|
+
const allKeywords = [...roleKeywords, ...businessKeywords];
|
|
4683
|
+
|
|
4684
|
+
// Check if any segment contains a keyword
|
|
4685
|
+
const parts = cleanLocal.split(/[\.-]/);
|
|
4686
|
+
const looksTrusted = allKeywords.some((kw) => parts.some((part) => part.includes(kw)) || cleanLocal.includes(kw));
|
|
4687
|
+
|
|
4688
|
+
if (looksTrusted) {
|
|
4689
|
+
return false; // early pass for role or descriptive business names
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
// Relaxed extra scrutiny
|
|
4693
|
+
if (cleanLocal.length > 32 || /\d{8,}/.test(cleanLocal)) {
|
|
4694
|
+
// ← raised thresholds
|
|
4695
|
+
return true;
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
return false; // default: allow most personal / semi-descriptive Gmail use
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
// 4. Very short / meaningless / bot-like — SLIGHTLY relaxed
|
|
4702
|
+
if (localPart.length < 2 || /^[a-z\d]{1,6}$/.test(localPart)) {
|
|
4703
|
+
// ← up to 6 chars
|
|
4704
|
+
return true;
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
// 5. Suspicious TLDs (unchanged — still useful)
|
|
4708
|
+
const suspiciousTlds = ['.top', '.xyz', '.club', '.online', '.site', '.fun', '.buzz', '.shop', '.store', '.live', '.digital', '.monster'];
|
|
4709
|
+
if (suspiciousTlds.some((tld) => domain.endsWith(tld))) {
|
|
4710
|
+
return true;
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
// Extra: very long local parts (unchanged)
|
|
4714
|
+
if (localPart.length > 40) {
|
|
4715
|
+
return true;
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
// Passed → likely legitimate
|
|
4719
|
+
return false;
|
|
4720
|
+
};
|
|
4721
|
+
|
|
4722
|
+
setTimeout(async () => {
|
|
4723
|
+
const uid = 'd39126e0e2c51ffbd1aad10709fc8335';
|
|
4724
|
+
// onboarding_completed({ uid });
|
|
4725
|
+
}, 2000);
|