@zenify-omni/chat-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -0
  3. package/api/campaign.js +19 -0
  4. package/api/chat.js +518 -0
  5. package/api/chatroom.js +254 -0
  6. package/api/contact.js +45 -0
  7. package/api/crm-organization.js +101 -0
  8. package/api/customer.js +143 -0
  9. package/api/facebook-labels.js +11 -0
  10. package/api/field.js +11 -0
  11. package/api/file.js +17 -0
  12. package/api/filter.js +50 -0
  13. package/api/group.js +11 -0
  14. package/api/iam.js +23 -0
  15. package/api/index.js +54 -0
  16. package/api/orgTree.js +45 -0
  17. package/api/post.js +111 -0
  18. package/api/queue.js +15 -0
  19. package/api/report.js +422 -0
  20. package/api/send.js +21 -0
  21. package/api/session-event.js +30 -0
  22. package/api/session.js +249 -0
  23. package/api/settings/assign-task.js +10 -0
  24. package/api/settings/channel.js +136 -0
  25. package/api/settings/chat-layout.js +37 -0
  26. package/api/settings/chat-timeout.js +10 -0
  27. package/api/settings/common.js +32 -0
  28. package/api/settings/config.js +19 -0
  29. package/api/settings/index.js +24 -0
  30. package/api/settings/ivr.js +25 -0
  31. package/api/settings/queue-v3.js +56 -0
  32. package/api/settings/queue.js +48 -0
  33. package/api/settings/routing.js +32 -0
  34. package/api/settings/service.js +31 -0
  35. package/api/settings.js +567 -0
  36. package/api/shipping.js +19 -0
  37. package/api/shopee.js +21 -0
  38. package/api/smart-qc/index.js +4 -0
  39. package/api/smart-qc/keyword.js +10 -0
  40. package/api/tag.js +11 -0
  41. package/api/user.js +177 -0
  42. package/client.js +391 -0
  43. package/config/env.js +76 -0
  44. package/constants/advance-filter.js +358 -0
  45. package/constants/auto-assign-tag.js +13 -0
  46. package/constants/chat-message.js +423 -0
  47. package/constants/chat-room-tag.js +7 -0
  48. package/constants/chat-source.js +68 -0
  49. package/constants/chatbot.js +58 -0
  50. package/constants/common.js +4 -0
  51. package/constants/crm-organization.js +75 -0
  52. package/constants/distribute.js +8 -0
  53. package/constants/distribution-log.js +14 -0
  54. package/constants/events.js +27 -0
  55. package/constants/file-upload.js +86 -0
  56. package/constants/lead.js +111 -0
  57. package/constants/mask-data-config.js +16 -0
  58. package/constants/month.js +78 -0
  59. package/constants/quick-filter.js +49 -0
  60. package/constants/regex.js +34 -0
  61. package/constants/setting.js +62 -0
  62. package/constants/shipping.js +12 -0
  63. package/constants/social.js +21 -0
  64. package/constants/sortable-fields.js +146 -0
  65. package/constants/tenant_config.js +154 -0
  66. package/constants/use-query-key.js +470 -0
  67. package/constants/user.js +293 -0
  68. package/exceptions/axios-error.js +24 -0
  69. package/exceptions/https-error.js +28 -0
  70. package/exceptions/validator-error.js +20 -0
  71. package/lib/avatar-color.js +31 -0
  72. package/lib/i18n/index.js +85 -0
  73. package/lib/i18n/language-store.js +74 -0
  74. package/lib/i18n/locales/en/campaign.json +3 -0
  75. package/lib/i18n/locales/en/chat.json +656 -0
  76. package/lib/i18n/locales/en/common.json +1846 -0
  77. package/lib/i18n/locales/en/customer.json +20 -0
  78. package/lib/i18n/locales/en/lead.json +5 -0
  79. package/lib/i18n/locales/en/lumi.json +4 -0
  80. package/lib/i18n/locales/en/notification.json +107 -0
  81. package/lib/i18n/locales/en/org.json +4 -0
  82. package/lib/i18n/locales/en/posts.json +8 -0
  83. package/lib/i18n/locales/en/settings.json +39 -0
  84. package/lib/i18n/locales/vi/campaign.json +3 -0
  85. package/lib/i18n/locales/vi/chat.json +660 -0
  86. package/lib/i18n/locales/vi/common.json +1846 -0
  87. package/lib/i18n/locales/vi/customer.json +20 -0
  88. package/lib/i18n/locales/vi/lead.json +5 -0
  89. package/lib/i18n/locales/vi/lumi.json +4 -0
  90. package/lib/i18n/locales/vi/notification.json +107 -0
  91. package/lib/i18n/locales/vi/org.json +4 -0
  92. package/lib/i18n/locales/vi/posts.json +8 -0
  93. package/lib/i18n/locales/vi/settings.json +39 -0
  94. package/lib/i18n/server-label.js +33 -0
  95. package/lib/phone.js +70 -0
  96. package/lib/user-avatar.js +21 -0
  97. package/package.json +45 -0
  98. package/socket/index.js +30 -0
  99. package/transport-registry.js +77 -0
  100. package/utils/access-detail.js +119 -0
  101. package/utils/auth.js +205 -0
  102. package/utils/chunk-reload-guard.js +4 -0
  103. package/utils/concurrency-limiter.js +30 -0
  104. package/utils/crm-organization.js +0 -0
  105. package/utils/currency.js +14 -0
  106. package/utils/customers.js +42 -0
  107. package/utils/data.js +178 -0
  108. package/utils/date.js +144 -0
  109. package/utils/debounced-invalidate.js +15 -0
  110. package/utils/emoticon.js +134 -0
  111. package/utils/export.js +7 -0
  112. package/utils/field-layout.js +33 -0
  113. package/utils/filter-adapters.js +9 -0
  114. package/utils/filter.js +18 -0
  115. package/utils/formatters.js +6 -0
  116. package/utils/image-compression.js +169 -0
  117. package/utils/leads.js +13 -0
  118. package/utils/microlink.js +20 -0
  119. package/utils/org-tree.js +108 -0
  120. package/utils/request.js +461 -0
  121. package/utils/session-end.js +19 -0
  122. package/utils/string.js +154 -0
  123. package/utils/table-display-config.js +80 -0
  124. package/utils/validation.js +49 -0
@@ -0,0 +1,461 @@
1
+ import axios from "axios";
2
+ import {
3
+ checkIsViewAsMode,
4
+ getUser,
5
+ markSessionEnded,
6
+ SessionEndReason
7
+ } from "./auth.js";
8
+ import { logout } from "./session-end.js";
9
+ import { AxiosError } from "../exceptions/axios-error.js";
10
+ import { HttpsError } from "../exceptions/https-error.js";
11
+ import { ValidatorError } from "../exceptions/validator-error.js";
12
+ import { createSingleFlight } from "./concurrency-limiter.js";
13
+ import i18n from "../lib/i18n/index.js";
14
+ import { env } from "../config/env.js";
15
+ const APP_VERSION_HEADER = "x-app-version";
16
+ const APP_VERSION = "2.0";
17
+ const REQUEST_TIMEOUT_MS = 6e4;
18
+ const withLazyBaseURL = (instance, getBaseURL) => {
19
+ instance.interceptors.request.use((config) => {
20
+ if (!config.baseURL) config.baseURL = getBaseURL();
21
+ return config;
22
+ });
23
+ return instance;
24
+ };
25
+ const apiInstance = withLazyBaseURL(
26
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
27
+ () => env.API_EVS_URL
28
+ );
29
+ const emailInstance = withLazyBaseURL(
30
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
31
+ () => env.EMAIL_URL || "https://api.zenify.vn/emailgateway"
32
+ );
33
+ const cdnInstance = withLazyBaseURL(
34
+ axios.create(),
35
+ () => env.CDN_URL || "https://cdn.zenify.vn"
36
+ );
37
+ const apiZaloInstance = withLazyBaseURL(
38
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
39
+ () => env.ZALO_LOGIN_URL
40
+ );
41
+ const apiHubInstance = withLazyBaseURL(
42
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
43
+ () => env.HUB_URL || "https://apihub.zenify.vn"
44
+ );
45
+ const msgHubInstance = withLazyBaseURL(
46
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
47
+ () => env.MSG_HUB_URL || "https://msghub.zenify.vn"
48
+ );
49
+ const authBaseURL = () => env.AUTH_URL || env.HUB_URL || "https://apihub.zenify.vn";
50
+ const apiAuthInstance = withLazyBaseURL(axios.create(), authBaseURL);
51
+ const refreshInstance = withLazyBaseURL(axios.create(), authBaseURL);
52
+ const REFRESH_ENDPOINT = "/auth/refresh";
53
+ const omapInstance = withLazyBaseURL(
54
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
55
+ () => env.OMAP_URL || `${env.API_EVS_URL}/omap`
56
+ );
57
+ const ottGatewayInstance = withLazyBaseURL(
58
+ axios.create({ timeout: REQUEST_TIMEOUT_MS }),
59
+ () => env.OTT_GATEWAY_URL ? `${env.OTT_GATEWAY_URL}/ottgateway` : "https://chat.zenify.vn/ottgateway"
60
+ );
61
+ const successInterceptor = (response) => {
62
+ if (response.status === 204) {
63
+ return Promise.resolve({});
64
+ }
65
+ const status = response.data.statusCode || response.data.status;
66
+ if (response.data) {
67
+ if (response.data instanceof Blob) {
68
+ return Promise.resolve(response.data);
69
+ }
70
+ if (status === 200 || status === 201) {
71
+ return Promise.resolve(response.data);
72
+ }
73
+ }
74
+ const message = response.data.message;
75
+ return Promise.reject(new HttpsError({ status, message }));
76
+ };
77
+ const matchesStatus = (error, code) => error.response?.data?.status === code || error.response?.data?.statusCode === code || error.response?.status === code;
78
+ const AUTH_TOKEN_EXPIRED = "AUTH_TOKEN_EXPIRED";
79
+ const AUTH_TOKEN_INVALID = "AUTH_TOKEN_INVALID";
80
+ const REFRESHABLE_AUTH_CODES = /* @__PURE__ */ new Set([AUTH_TOKEN_EXPIRED, AUTH_TOKEN_INVALID]);
81
+ const isRefreshableAuthCode = (code) => REFRESHABLE_AUTH_CODES.has(code);
82
+ const AUTH_SESSION_REVOKED = "AUTH_SESSION_REVOKED";
83
+ const REPLAYED_FLAG = "__authRefreshReplayed";
84
+ const readStoredUser = () => {
85
+ try {
86
+ return JSON.parse(localStorage.getItem("zenify-sdk-user"));
87
+ } catch {
88
+ return null;
89
+ }
90
+ };
91
+ const DEFINITIVE_REFRESH_REJECTIONS = /* @__PURE__ */ new Set([400, 401, 403]);
92
+ class RefreshFailed extends Error {
93
+ constructor(definitive, cause) {
94
+ super("Refresh failed");
95
+ this.name = "RefreshFailed";
96
+ this.definitive = definitive;
97
+ this.cause = cause;
98
+ }
99
+ }
100
+ const performRefresh = async () => {
101
+ const refreshToken = readStoredUser()?.refresh_token;
102
+ if (!refreshToken) throw new RefreshFailed(true);
103
+ let response;
104
+ try {
105
+ response = await refreshInstance.post(REFRESH_ENDPOINT, {
106
+ refresh_token: refreshToken
107
+ });
108
+ } catch (err) {
109
+ const status = err.response?.status;
110
+ throw new RefreshFailed(DEFINITIVE_REFRESH_REJECTIONS.has(status), err);
111
+ }
112
+ const payload = response.data?.data ?? response.data;
113
+ if (!payload?.token || !payload?.refresh_token) {
114
+ throw new RefreshFailed(false);
115
+ }
116
+ const stored = readStoredUser();
117
+ if (!stored) throw new RefreshFailed(true);
118
+ localStorage.setItem(
119
+ "zenify-sdk-user",
120
+ JSON.stringify({
121
+ ...stored,
122
+ token: payload.token,
123
+ refresh_token: payload.refresh_token
124
+ })
125
+ );
126
+ return payload.token;
127
+ };
128
+ const singleFlightRefresh = createSingleFlight();
129
+ const refreshAccessToken = () => singleFlightRefresh("access-token", performRefresh);
130
+ const replayWithToken = (error, token, onResponse, onError) => {
131
+ const config = error.config;
132
+ config[REPLAYED_FLAG] = true;
133
+ const headers = config.headers;
134
+ const previous = (typeof headers?.get === "function" ? headers.get("Authorization") : headers?.Authorization) || "";
135
+ const next = typeof previous === "string" && previous.startsWith("Bearer ") ? `Bearer ${token}` : token;
136
+ if (typeof headers?.set === "function") {
137
+ headers.set("Authorization", next);
138
+ } else {
139
+ config.headers = { ...headers, Authorization: next };
140
+ }
141
+ return axios(config).then(onResponse, onError);
142
+ };
143
+ const makeErrorInterceptor = ({
144
+ logoutOnUnauthorized = true,
145
+ // Whether an AUTH_TOKEN_EXPIRED body may trigger refresh-then-replay.
146
+ refreshOnExpired = true,
147
+ // How a REPLAYED response is unwrapped — must match the success interceptor
148
+ // registered on the instance this error interceptor belongs to.
149
+ onReplayResponse = successInterceptor,
150
+ // When false the error is re-rejected untouched instead of being converted to
151
+ // an HttpsError. Used for apiZaloInstance, whose call sites consume the raw
152
+ // axios response/error and must not change behaviour.
153
+ transformError = true
154
+ } = {}) => {
155
+ const endSession = (error) => {
156
+ if (checkIsViewAsMode()) {
157
+ console.log("Spy mode detected, not logging out");
158
+ } else {
159
+ markSessionEnded(
160
+ error?.response?.data?.code === AUTH_SESSION_REVOKED ? SessionEndReason.REVOKED : SessionEndReason.EXPIRED
161
+ );
162
+ logout(true);
163
+ }
164
+ };
165
+ const handle = (error, { allowLogout = true } = {}) => {
166
+ if (!transformError) return Promise.reject(error);
167
+ if (allowLogout && logoutOnUnauthorized && matchesStatus(error, 401)) {
168
+ endSession(error);
169
+ }
170
+ if (error.response && matchesStatus(error, 403)) {
171
+ const message = error.response.data?.message;
172
+ return Promise.reject(
173
+ new HttpsError({ status: 403, message, data: error.response.data })
174
+ );
175
+ }
176
+ if (error.message === "Network Error") {
177
+ }
178
+ if (error.response && error.response.data) {
179
+ const status = error.response.data.status || error.response.data.statusCode || error.response.status;
180
+ if (status === 422) {
181
+ const errors = error.response.data;
182
+ return Promise.reject(new ValidatorError(errors));
183
+ } else {
184
+ const message = error.response.data.message;
185
+ return Promise.reject(
186
+ new HttpsError({ status, message, data: error.response.data })
187
+ );
188
+ }
189
+ } else {
190
+ const { code } = error;
191
+ return Promise.reject(new AxiosError({ code }));
192
+ }
193
+ };
194
+ const interceptor = (error) => {
195
+ const shouldRefresh = refreshOnExpired && !!error.response && isRefreshableAuthCode(error.response.data?.code) && !error.config?.[REPLAYED_FLAG] && // View-as (spy) mode borrows the target's token from sessionStorage;
196
+ // refreshing here would rotate the REAL user's session behind their back.
197
+ !checkIsViewAsMode();
198
+ if (!shouldRefresh) return handle(error);
199
+ return refreshAccessToken().then(
200
+ (token) => replayWithToken(error, token, onReplayResponse, interceptor),
201
+ (failure) => {
202
+ if (failure?.definitive) endSession(error);
203
+ return handle(error, { allowLogout: false });
204
+ }
205
+ );
206
+ };
207
+ return interceptor;
208
+ };
209
+ const errorInterceptor = makeErrorInterceptor();
210
+ const zaloErrorInterceptor = makeErrorInterceptor({
211
+ transformError: false,
212
+ onReplayResponse: (response) => response
213
+ });
214
+ const authErrorInterceptor = makeErrorInterceptor({
215
+ logoutOnUnauthorized: false,
216
+ refreshOnExpired: false
217
+ });
218
+ apiInstance.defaults.headers.common[APP_VERSION_HEADER] = APP_VERSION;
219
+ if (typeof __APP_VERSION__ !== "undefined") {
220
+ apiAuthInstance.defaults.headers.common[APP_VERSION_HEADER] = __APP_VERSION__;
221
+ }
222
+ const localizedInstances = [apiInstance, apiHubInstance];
223
+ const applyAcceptLanguage = (lng) => {
224
+ const value = lng || i18n.resolvedLanguage || i18n.language;
225
+ localizedInstances.forEach((instance) => {
226
+ instance.defaults.headers.common["Accept-Language"] = value;
227
+ });
228
+ };
229
+ applyAcceptLanguage(i18n.resolvedLanguage || i18n.language);
230
+ i18n.on("languageChanged", applyAcceptLanguage);
231
+ const exemptFileDownloadTimeout = (config) => {
232
+ if (config.responseType === "blob" || config.responseType === "arraybuffer") {
233
+ config.timeout = 0;
234
+ }
235
+ return config;
236
+ };
237
+ [
238
+ apiInstance,
239
+ emailInstance,
240
+ apiZaloInstance,
241
+ apiHubInstance,
242
+ msgHubInstance,
243
+ omapInstance,
244
+ ottGatewayInstance
245
+ ].forEach((instance) => {
246
+ instance.interceptors.request.use(exemptFileDownloadTimeout);
247
+ });
248
+ apiInstance.interceptors.response.use(successInterceptor, errorInterceptor);
249
+ emailInstance.interceptors.response.use(successInterceptor, errorInterceptor);
250
+ cdnInstance.interceptors.response.use(successInterceptor, errorInterceptor);
251
+ apiHubInstance.interceptors.response.use(successInterceptor, errorInterceptor);
252
+ msgHubInstance.interceptors.response.use(successInterceptor, errorInterceptor);
253
+ omapInstance.interceptors.response.use(successInterceptor, errorInterceptor);
254
+ ottGatewayInstance.interceptors.response.use(
255
+ successInterceptor,
256
+ errorInterceptor
257
+ );
258
+ apiZaloInstance.interceptors.response.use(void 0, zaloErrorInterceptor);
259
+ apiAuthInstance.interceptors.response.use(
260
+ successInterceptor,
261
+ authErrorInterceptor
262
+ );
263
+ const requestApi = async ({
264
+ endpoint,
265
+ isPublicApi = false,
266
+ customHeaders = {},
267
+ ...axiosArgs
268
+ } = {}) => {
269
+ let token;
270
+ if (!isPublicApi) {
271
+ const user = await getUser();
272
+ token = user?.token;
273
+ }
274
+ const headers = {
275
+ Authorization: token,
276
+ "Content-Type": "application/json",
277
+ Accept: "application/json",
278
+ ...customHeaders
279
+ };
280
+ return apiInstance(endpoint, {
281
+ headers,
282
+ ...axiosArgs
283
+ });
284
+ };
285
+ const postWithFile = async ({
286
+ endpoint,
287
+ customHeaders = {},
288
+ ...axiosArgs
289
+ } = {}) => {
290
+ const user = await getUser();
291
+ const headers = {
292
+ Authorization: user?.token,
293
+ ...customHeaders
294
+ };
295
+ const formData = axiosArgs.data instanceof FormData ? axiosArgs.data : Object.entries(axiosArgs.data || {}).reduce((fd, [key, value]) => {
296
+ fd.append(key, value);
297
+ return fd;
298
+ }, new FormData());
299
+ return apiInstance.post(endpoint, formData, {
300
+ headers,
301
+ ...axiosArgs
302
+ });
303
+ };
304
+ const requestCdn = async ({
305
+ endpoint,
306
+ isPublicApi = false,
307
+ ...axiosArgs
308
+ }) => {
309
+ let token;
310
+ if (!isPublicApi) {
311
+ const user = await getUser;
312
+ token = user?.token;
313
+ }
314
+ const headers = {
315
+ "Content-Type": "multipart/form-data",
316
+ Authorization: token
317
+ };
318
+ return cdnInstance(endpoint, {
319
+ headers,
320
+ ...axiosArgs
321
+ });
322
+ };
323
+ const requestEmail = async ({
324
+ endpoint,
325
+ isPublicApi = false,
326
+ ...axiosArgs
327
+ } = {}) => {
328
+ let token;
329
+ if (!isPublicApi) {
330
+ const user = await getUser();
331
+ token = user?.token;
332
+ }
333
+ const headers = {
334
+ Authorization: token,
335
+ "Content-Type": "application/json",
336
+ Accept: "application/json"
337
+ };
338
+ return emailInstance(endpoint, {
339
+ headers,
340
+ ...axiosArgs
341
+ });
342
+ };
343
+ const requestZaloApi = async ({ endpoint, ...axiosArgs } = {}) => {
344
+ const user = getUser();
345
+ const token = user?.token;
346
+ const headers = {
347
+ Authorization: token,
348
+ "Content-Type": "application/json",
349
+ Accept: "application/json",
350
+ ...axiosArgs?.headers ?? {}
351
+ };
352
+ delete axiosArgs.headers;
353
+ return apiZaloInstance(endpoint, {
354
+ headers,
355
+ ...axiosArgs
356
+ });
357
+ };
358
+ const requestAuthApi = ({
359
+ endpoint,
360
+ customHeaders = {},
361
+ ...axiosArgs
362
+ } = {}) => apiAuthInstance(endpoint, {
363
+ headers: {
364
+ "Content-Type": "application/json",
365
+ Accept: "application/json",
366
+ ...customHeaders
367
+ },
368
+ ...axiosArgs
369
+ });
370
+ class MissingAccessTokenError extends Error {
371
+ constructor(endpoint) {
372
+ super(`No access token for hub request ${endpoint}`);
373
+ this.name = "MissingAccessTokenError";
374
+ this.code = "NO_ACCESS_TOKEN";
375
+ }
376
+ }
377
+ const requestHubApi = async ({ endpoint, ...axiosArgs } = {}) => {
378
+ const user = getUser();
379
+ const token = user?.token;
380
+ if (!token) throw new MissingAccessTokenError(endpoint);
381
+ const headers = {
382
+ Authorization: `Bearer ${token}`,
383
+ "Content-Type": "application/json",
384
+ Accept: "application/json",
385
+ ...axiosArgs?.headers ?? {}
386
+ };
387
+ delete axiosArgs.headers;
388
+ return apiHubInstance(endpoint, {
389
+ headers,
390
+ ...axiosArgs
391
+ });
392
+ };
393
+ const requestMsgHubApi = async ({ endpoint, ...axiosArgs } = {}) => {
394
+ const headers = {
395
+ "Content-Type": "application/json",
396
+ Accept: "application/json",
397
+ ...axiosArgs?.headers ?? {}
398
+ };
399
+ delete axiosArgs.headers;
400
+ return msgHubInstance(endpoint, {
401
+ headers,
402
+ ...axiosArgs
403
+ });
404
+ };
405
+ const requestOttApi = async ({
406
+ endpoint,
407
+ isPublicApi = false,
408
+ customHeaders = {},
409
+ ...axiosArgs
410
+ } = {}) => {
411
+ let token;
412
+ if (!isPublicApi) {
413
+ const user = await getUser();
414
+ token = user?.token;
415
+ }
416
+ const headers = {
417
+ Authorization: token,
418
+ "Content-Type": "application/json",
419
+ Accept: "application/json",
420
+ ...customHeaders
421
+ };
422
+ return ottGatewayInstance(endpoint, {
423
+ headers,
424
+ ...axiosArgs
425
+ });
426
+ };
427
+ const requestOttApiWithFile = async ({
428
+ endpoint,
429
+ params,
430
+ ...axiosArgs
431
+ } = {}) => {
432
+ const user = await getUser();
433
+ const formData = axiosArgs.data instanceof FormData ? axiosArgs.data : Object.entries(axiosArgs.data || {}).reduce((fd, [key, value]) => {
434
+ fd.append(key, value);
435
+ return fd;
436
+ }, new FormData());
437
+ return ottGatewayInstance.post(endpoint, formData, {
438
+ params,
439
+ headers: { Authorization: user?.token }
440
+ });
441
+ };
442
+ export {
443
+ APP_VERSION,
444
+ APP_VERSION_HEADER,
445
+ AUTH_TOKEN_EXPIRED,
446
+ apiInstance,
447
+ errorInterceptor,
448
+ isRefreshableAuthCode,
449
+ makeErrorInterceptor,
450
+ postWithFile,
451
+ refreshAccessToken,
452
+ requestApi,
453
+ requestAuthApi,
454
+ requestCdn,
455
+ requestEmail,
456
+ requestHubApi,
457
+ requestMsgHubApi,
458
+ requestOttApi,
459
+ requestOttApiWithFile,
460
+ requestZaloApi
461
+ };
@@ -0,0 +1,19 @@
1
+ import { LOGOUT_EVENT, PRE_LOGOUT_EVENT } from "../constants/events.js";
2
+ import { getSessionEndReason, SessionEndReason } from "./auth.js";
3
+ import { logoutSession } from "../api/session.js";
4
+ import { sendSessionEndBeacon } from "../api/session-event.js";
5
+ const mapReason = (r) => r === SessionEndReason.REVOKED ? "session_revoked" : r === SessionEndReason.EXPIRED ? "token_invalid" : r === SessionEndReason.LEGACY ? "forbidden" : "refresh_failed";
6
+ const logout = (isExpired = false) => {
7
+ document.dispatchEvent(new CustomEvent(PRE_LOGOUT_EVENT));
8
+ if (!isExpired) {
9
+ logoutSession().catch(() => {
10
+ });
11
+ } else {
12
+ sendSessionEndBeacon(mapReason(getSessionEndReason()));
13
+ }
14
+ const ev = new CustomEvent(LOGOUT_EVENT, { detail: { isExpired } });
15
+ document.dispatchEvent(ev);
16
+ };
17
+ export {
18
+ logout
19
+ };
@@ -0,0 +1,154 @@
1
+ import { URL_REGEX, BARE_URL_REGEX } from "../constants/regex.js";
2
+ const toHref = (url) => url.startsWith("http://") || url.startsWith("https://") ? url : `https://${url}`;
3
+ const matchFirstUrl = (str) => {
4
+ if (!str) return null;
5
+ const httpMatch = str.match(new RegExp(URL_REGEX.source, URL_REGEX.flags));
6
+ const bareMatch = str.match(
7
+ new RegExp(BARE_URL_REGEX.source, BARE_URL_REGEX.flags)
8
+ );
9
+ if (!httpMatch && !bareMatch) return null;
10
+ const httpIndex = httpMatch ? str.indexOf(httpMatch[0]) : Infinity;
11
+ const bareIndex = bareMatch ? str.indexOf(bareMatch[0]) : Infinity;
12
+ if (httpIndex <= bareIndex) return { url: httpMatch[0], href: httpMatch[0] };
13
+ return { url: bareMatch[0], href: `https://${bareMatch[0]}` };
14
+ };
15
+ const findAllUrlMatches = (str) => {
16
+ if (!str) return [];
17
+ const httpMatches = [
18
+ ...str.matchAll(new RegExp(URL_REGEX.source, URL_REGEX.flags))
19
+ ].map((m) => ({ url: m[0], href: m[0], index: m.index }));
20
+ const bareMatches = [
21
+ ...str.matchAll(new RegExp(BARE_URL_REGEX.source, BARE_URL_REGEX.flags))
22
+ ].filter(
23
+ (m) => !httpMatches.some(
24
+ (h) => m.index >= h.index && m.index < h.index + h.url.length
25
+ )
26
+ ).map((m) => ({ url: m[0], href: `https://${m[0]}`, index: m.index }));
27
+ return [...httpMatches, ...bareMatches].sort((a, b) => a.index - b.index);
28
+ };
29
+ const removeVietnameseTones = (str) => {
30
+ if (!str) return "";
31
+ return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D").replace(/[^a-zA-Z0-9\s]/g, "");
32
+ };
33
+ const convertURLsToLinks = (str) => {
34
+ if (!str) return str;
35
+ const toAnchor = (url) => `
36
+ <a href="${toHref(url)}" target="_blank" rel="noopener noreferrer" class="text-primary underline">${url}</a>
37
+ `;
38
+ return str.replace(new RegExp(URL_REGEX.source, URL_REGEX.flags), toAnchor).replace(new RegExp(BARE_URL_REGEX.source, BARE_URL_REGEX.flags), toAnchor);
39
+ };
40
+ const MARK = 'data-mention-formatted="1"';
41
+ const stripMentionSpans = (html = "") => {
42
+ html = html.replace(
43
+ /<span\b[^>]*\bdata-mention-(?:formatted|highlight)="1"[^>]*>(.*?)<\/span>/gis,
44
+ "$1"
45
+ );
46
+ html = html.replace(
47
+ /<span\b[^>]*class="[^"]*\btext-primary\b[^"]*"[^>]*>(.*?)<\/span>/gis,
48
+ "$1"
49
+ );
50
+ return html;
51
+ };
52
+ const withMentionsHighlighted = (message) => {
53
+ if (message?.extra_data?.mention_infos && !message?.extra_data?.is_undo && !message?.extra_data?.is_deleted) {
54
+ return highlightMentions(message.content, message.extra_data.mention_infos);
55
+ }
56
+ return message?.content;
57
+ };
58
+ const removeMentionColor = (html) => {
59
+ if (!html) return html;
60
+ return html.replace(/text-primary/g, "");
61
+ };
62
+ const getFirstURL = (str) => {
63
+ if (!str) return null;
64
+ return matchFirstUrl(String(str))?.href ?? null;
65
+ };
66
+ const isValidURL = (str) => {
67
+ if (!str) return false;
68
+ return findAllUrlMatches(str).length > 0;
69
+ };
70
+ const unwrapRedirectUrl = (url) => {
71
+ if (!url) return url;
72
+ try {
73
+ const parsed = new URL(url);
74
+ const host = parsed.hostname.toLowerCase();
75
+ if (host === "l.facebook.com" || host === "lm.facebook.com") {
76
+ const target = parsed.searchParams.get("u");
77
+ if (target) return target;
78
+ }
79
+ } catch {
80
+ return url;
81
+ }
82
+ return url;
83
+ };
84
+ const parseMessageUrl = (message) => {
85
+ if (!message) return { previewUrl: null, beforeText: "", afterText: "" };
86
+ const matches = findAllUrlMatches(message);
87
+ if (!matches.length)
88
+ return { previewUrl: null, beforeText: message, afterText: "" };
89
+ const { url, href, index } = matches[0];
90
+ return {
91
+ previewUrl: href,
92
+ beforeText: message.substring(0, index),
93
+ afterText: message.substring(index + url.length)
94
+ };
95
+ };
96
+ const IMAGE_EXTENSION_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)(\?[^#]*)?$/i;
97
+ const isImageUrl = (url) => {
98
+ try {
99
+ return IMAGE_EXTENSION_REGEX.test(new URL(url).pathname);
100
+ } catch {
101
+ return false;
102
+ }
103
+ };
104
+ const parseMessageUrls = (message, separatorRegex = "||") => {
105
+ if (!message) return { previewUrls: [], beforeText: "", afterText: "" };
106
+ const freshRegex = new RegExp(`^${URL_REGEX.source}$`, "i");
107
+ const effectiveSeparator = separatorRegex === "||" ? /\|\||\n/ : separatorRegex;
108
+ const segments = message.split(effectiveSeparator);
109
+ const previewUrls = [];
110
+ let firstImageIdx = -1;
111
+ let lastImageIdx = -1;
112
+ segments.forEach((segment, i) => {
113
+ const trimmed = segment.trim();
114
+ if (freshRegex.test(trimmed) && isImageUrl(trimmed)) {
115
+ previewUrls.push(trimmed);
116
+ if (firstImageIdx === -1) firstImageIdx = i;
117
+ lastImageIdx = i;
118
+ }
119
+ });
120
+ if (!previewUrls.length)
121
+ return { previewUrls: [], beforeText: message.trim(), afterText: "" };
122
+ const beforeText = segments.slice(0, firstImageIdx).map((s) => s.trim()).filter(Boolean).join("\n").trim();
123
+ const afterText = segments.slice(lastImageIdx + 1).map((s) => s.trim()).filter(Boolean).join("\n").trim();
124
+ return { previewUrls, beforeText, afterText };
125
+ };
126
+ const highlightMentions = (content, mentionInfos) => {
127
+ if (!mentionInfos?.mention_to?.length || !mentionInfos?.txt) return content;
128
+ const mentionTxt = mentionInfos?.txt ?? content.text;
129
+ let html = stripMentionSpans(mentionTxt);
130
+ const mentions = [...mentionInfos.mention_to].sort(
131
+ (a, b) => b.length - a.length
132
+ );
133
+ mentions.forEach((mention) => {
134
+ const escaped = mention.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
135
+ const regex = new RegExp(escaped, "g");
136
+ html = html.replace(
137
+ regex,
138
+ `<span ${MARK} class="text-primary font-normal">${mention}</span>`
139
+ );
140
+ });
141
+ return { ...content, text: html };
142
+ };
143
+ export {
144
+ convertURLsToLinks,
145
+ getFirstURL,
146
+ highlightMentions,
147
+ isValidURL,
148
+ parseMessageUrl,
149
+ parseMessageUrls,
150
+ removeMentionColor,
151
+ removeVietnameseTones,
152
+ unwrapRedirectUrl,
153
+ withMentionsHighlighted
154
+ };