@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.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/api/campaign.js +19 -0
- package/api/chat.js +518 -0
- package/api/chatroom.js +254 -0
- package/api/contact.js +45 -0
- package/api/crm-organization.js +101 -0
- package/api/customer.js +143 -0
- package/api/facebook-labels.js +11 -0
- package/api/field.js +11 -0
- package/api/file.js +17 -0
- package/api/filter.js +50 -0
- package/api/group.js +11 -0
- package/api/iam.js +23 -0
- package/api/index.js +54 -0
- package/api/orgTree.js +45 -0
- package/api/post.js +111 -0
- package/api/queue.js +15 -0
- package/api/report.js +422 -0
- package/api/send.js +21 -0
- package/api/session-event.js +30 -0
- package/api/session.js +249 -0
- package/api/settings/assign-task.js +10 -0
- package/api/settings/channel.js +136 -0
- package/api/settings/chat-layout.js +37 -0
- package/api/settings/chat-timeout.js +10 -0
- package/api/settings/common.js +32 -0
- package/api/settings/config.js +19 -0
- package/api/settings/index.js +24 -0
- package/api/settings/ivr.js +25 -0
- package/api/settings/queue-v3.js +56 -0
- package/api/settings/queue.js +48 -0
- package/api/settings/routing.js +32 -0
- package/api/settings/service.js +31 -0
- package/api/settings.js +567 -0
- package/api/shipping.js +19 -0
- package/api/shopee.js +21 -0
- package/api/smart-qc/index.js +4 -0
- package/api/smart-qc/keyword.js +10 -0
- package/api/tag.js +11 -0
- package/api/user.js +177 -0
- package/client.js +391 -0
- package/config/env.js +76 -0
- package/constants/advance-filter.js +358 -0
- package/constants/auto-assign-tag.js +13 -0
- package/constants/chat-message.js +423 -0
- package/constants/chat-room-tag.js +7 -0
- package/constants/chat-source.js +68 -0
- package/constants/chatbot.js +58 -0
- package/constants/common.js +4 -0
- package/constants/crm-organization.js +75 -0
- package/constants/distribute.js +8 -0
- package/constants/distribution-log.js +14 -0
- package/constants/events.js +27 -0
- package/constants/file-upload.js +86 -0
- package/constants/lead.js +111 -0
- package/constants/mask-data-config.js +16 -0
- package/constants/month.js +78 -0
- package/constants/quick-filter.js +49 -0
- package/constants/regex.js +34 -0
- package/constants/setting.js +62 -0
- package/constants/shipping.js +12 -0
- package/constants/social.js +21 -0
- package/constants/sortable-fields.js +146 -0
- package/constants/tenant_config.js +154 -0
- package/constants/use-query-key.js +470 -0
- package/constants/user.js +293 -0
- package/exceptions/axios-error.js +24 -0
- package/exceptions/https-error.js +28 -0
- package/exceptions/validator-error.js +20 -0
- package/lib/avatar-color.js +31 -0
- package/lib/i18n/index.js +85 -0
- package/lib/i18n/language-store.js +74 -0
- package/lib/i18n/locales/en/campaign.json +3 -0
- package/lib/i18n/locales/en/chat.json +656 -0
- package/lib/i18n/locales/en/common.json +1846 -0
- package/lib/i18n/locales/en/customer.json +20 -0
- package/lib/i18n/locales/en/lead.json +5 -0
- package/lib/i18n/locales/en/lumi.json +4 -0
- package/lib/i18n/locales/en/notification.json +107 -0
- package/lib/i18n/locales/en/org.json +4 -0
- package/lib/i18n/locales/en/posts.json +8 -0
- package/lib/i18n/locales/en/settings.json +39 -0
- package/lib/i18n/locales/vi/campaign.json +3 -0
- package/lib/i18n/locales/vi/chat.json +660 -0
- package/lib/i18n/locales/vi/common.json +1846 -0
- package/lib/i18n/locales/vi/customer.json +20 -0
- package/lib/i18n/locales/vi/lead.json +5 -0
- package/lib/i18n/locales/vi/lumi.json +4 -0
- package/lib/i18n/locales/vi/notification.json +107 -0
- package/lib/i18n/locales/vi/org.json +4 -0
- package/lib/i18n/locales/vi/posts.json +8 -0
- package/lib/i18n/locales/vi/settings.json +39 -0
- package/lib/i18n/server-label.js +33 -0
- package/lib/phone.js +70 -0
- package/lib/user-avatar.js +21 -0
- package/package.json +45 -0
- package/socket/index.js +30 -0
- package/transport-registry.js +77 -0
- package/utils/access-detail.js +119 -0
- package/utils/auth.js +205 -0
- package/utils/chunk-reload-guard.js +4 -0
- package/utils/concurrency-limiter.js +30 -0
- package/utils/crm-organization.js +0 -0
- package/utils/currency.js +14 -0
- package/utils/customers.js +42 -0
- package/utils/data.js +178 -0
- package/utils/date.js +144 -0
- package/utils/debounced-invalidate.js +15 -0
- package/utils/emoticon.js +134 -0
- package/utils/export.js +7 -0
- package/utils/field-layout.js +33 -0
- package/utils/filter-adapters.js +9 -0
- package/utils/filter.js +18 -0
- package/utils/formatters.js +6 -0
- package/utils/image-compression.js +169 -0
- package/utils/leads.js +13 -0
- package/utils/microlink.js +20 -0
- package/utils/org-tree.js +108 -0
- package/utils/request.js +461 -0
- package/utils/session-end.js +19 -0
- package/utils/string.js +154 -0
- package/utils/table-display-config.js +80 -0
- package/utils/validation.js +49 -0
package/utils/request.js
ADDED
|
@@ -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
|
+
};
|
package/utils/string.js
ADDED
|
@@ -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
|
+
};
|