import i18n from "./i18nMessages.js"; const baseUrl = "https://emiapi.reynafamily.com"; //const baseUrl = "http://localhost:3000"; const requestErrorCooldownMs = 30000; const profileFailureCooldownMs = 60000; const recentRequestErrors = {}; const failedProfileCache = {}; const getCurrentLanguage = () => (i18n?.locale || "en").toLowerCase(); const normalizePath = (path = "") => { return path.replace(/[a-f0-9]{24}/gi, ":id"); }; const logRequestErrorOnce = (key, message) => { const now = Date.now(); if (recentRequestErrors[key] && now - recentRequestErrors[key] < requestErrorCooldownMs) return; recentRequestErrors[key] = now; console.error(message); }; const getFallbackForPath = (path = "") => { if (path.startsWith("/post/") || path === "/post/") return []; return {}; }; const parseJsonResponse = async (response, path = "", fallback) => { const text = await response.text(); if (!text) return fallback; try { return JSON.parse(text); } catch (error) { const normalizedPath = normalizePath(path); logRequestErrorOnce(`invalid-json:${normalizedPath}`, `Invalid JSON from ${normalizedPath}: ${text.slice(0, 200)}`); return fallback; } }; let getCall = async (path = "", params = {}) => { let queryParams = "?"; Object.keys(params).forEach(p => { queryParams += p + "=" + params[p] + "&" }); let localBaseUrl = global.baseUrl ?? baseUrl; const fallback = getFallbackForPath(path); return fetch(localBaseUrl + path + queryParams, { method: 'GET', mode: 'cors', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Accept-Language': getCurrentLanguage(), 'x-app-language': getCurrentLanguage(), } }).then(async (response) => { const normalizedPath = normalizePath(path); if (!response.ok) { logRequestErrorOnce(`GET:${normalizedPath}:${response.status}`, `GET ${normalizedPath} failed: ${response.status}`); return fallback; } return parseJsonResponse(response, path, fallback); }).catch((error) => { const normalizedPath = normalizePath(path); logRequestErrorOnce(`GET:${normalizedPath}:network`, `GET ${normalizedPath} network error: ${error?.message || error}`); return fallback; }) } let deleteCall = async (path = "", params = {}) => { let queryParams = "?"; Object.keys(params).forEach(p => { queryParams += p + "=" + params[p] + "&" }); let localBaseUrl = global.baseUrl ?? baseUrl; const fallback = {}; return fetch(localBaseUrl + path + queryParams, { method: 'DELETE', mode: 'cors', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Accept-Language': getCurrentLanguage(), 'x-app-language': getCurrentLanguage(), } }).then(async (response) => { const normalizedPath = normalizePath(path); if (!response.ok) { logRequestErrorOnce(`DELETE:${normalizedPath}:${response.status}`, `DELETE ${normalizedPath} failed: ${response.status}`); return fallback; } return parseJsonResponse(response, path, fallback); }).catch((error) => { const normalizedPath = normalizePath(path); logRequestErrorOnce(`DELETE:${normalizedPath}:network`, `DELETE ${normalizedPath} network error: ${error?.message || error}`); return fallback; }) } let postCall = async (path, params) => { let localBaseUrl = global.baseUrl ?? baseUrl; const fallback = {}; return fetch(localBaseUrl + path, { method: 'POST', mode: 'cors', credentials: 'include', body: JSON.stringify(params), headers: { 'Content-Type': 'application/json', 'Accept-Language': getCurrentLanguage(), 'x-app-language': getCurrentLanguage(), } }).then(async (response) => { const normalizedPath = normalizePath(path); if (!response.ok) { logRequestErrorOnce(`POST:${normalizedPath}:${response.status}`, `POST ${normalizedPath} failed: ${response.status}`); return fallback; } return parseJsonResponse(response, path, fallback); }).catch((error) => { const normalizedPath = normalizePath(path); logRequestErrorOnce(`POST:${normalizedPath}:network`, `POST ${normalizedPath} network error: ${error?.message || error}`); return fallback; }) } let CurrentUserId; let CurrentProfile; let CurrentProfileData; let userNameCache = {}; //save this on localstorage let working_on = {}; //Promises const emptyProfileData = (id = "") => ({ _id: id || "", profile: { firstName: "", lastName: "", photo: "", description: "", language: "en", }, data: {}, following: [], }); let getProfileFromCache = async (id, refresh=false) => { if(!id) console.trace(); const lastFailure = failedProfileCache[id] || 0; if (!refresh && lastFailure && Date.now() - lastFailure < profileFailureCooldownMs) { return emptyProfileData(id); } if (userNameCache[id] && !refresh) return userNameCache[id]; if (working_on[id] && !refresh) return working_on[id]; working_on[id] = getCall("/user/" + id).then((profile) => { const hasRealProfile = !!(profile && profile._id); const safeProfile = hasRealProfile ? { ...emptyProfileData(id), ...profile, profile: { ...emptyProfileData(id).profile, ...(profile.profile || {}) } } : emptyProfileData(id); if (hasRealProfile) { userNameCache[id] = safeProfile; delete failedProfileCache[id]; } else { failedProfileCache[id] = Date.now(); } delete working_on[id]; return safeProfile; }).catch(() => { failedProfileCache[id] = Date.now(); delete working_on[id]; return emptyProfileData(id); }); return working_on[id]; } const API = { isLoggedIn: async () => { return getCall().then((data) => { //console.log("isLoggedIn", data) if (data && data.status && data.status === 'ok') { CurrentUserId = data.userInfo._id; CurrentProfile = data.profileInfo._id; return true; } CurrentUserId = ''; CurrentProfile = ''; CurrentProfileData = undefined; return false; }) }, logIn: async (username, password) => { return postCall("/login", { username, password }).then((data) => { if (data && data.status === "ok") { CurrentUserId = data.user_sid; CurrentProfile = data.profile_id; } return data; }) }, logInWithPasswordToken: async (token) => { return postCall("/password/token-login", { token }).then((data) => { if (data && data.status === "ok") { CurrentUserId = data.user_sid; CurrentProfile = data.profile_id; } return data; }); }, async logout(){ console.log("Logging out...") return getCall("/logout").then(()=>{ CurrentUserId = ''; CurrentProfile = ''; CurrentProfileData = undefined; }); }, async signup(username, password, email, profile){ return postCall("/signup", { username, password, email, profile }).then((data) => { //console.log(data) if (data && data.status === "ok") { CurrentUserId = data.user_sid; CurrentProfile = data.profile_id; } return data; }); }, async subscribe(subscription){ return postCall("/subscribe", subscription); }, resetPassword(email){ return postCall("/resetPassword", {username: email}); }, changePassword(newPassword){ return postCall("/resetPassword", {password: newPassword}); }, currentUserId() { if (!CurrentUserId) this.isLoggedIn(); //replace with cookie return CurrentUserId; }, addPaymentCard(cardInfo){ return postCall("/payment/card", {cardInfo}); }, //Posts getPosts(userid) { if (userid) return getCall("/post/usr/" + userid).then((data) => Array.isArray(data) ? data : []); return getCall("/post/").then((data) => Array.isArray(data) ? data : []); }, getPostsByTag(tag) { return getCall("/post/tag/" + tag); }, getPostsWithTag(userid, tag = "images") { if (userid) return getCall("/post/usr/" + userid + "/" + tag); return getCall("/post/" + tag); }, getPost(postid) { return getCall("/post/" + postid); }, getVideo(videoId) { return getCall("/post/video/" + videoId); }, registerToken(token) { return postCall("/token/", {token}); }, deletePost(postid){ return deleteCall("/post/" + postid); }, updatePost(post){ return postCall("/post/" + post._id, {content: post.content}); }, translatePost(postid, targetLang) { return postCall("/post/translate", { postid, targetLang }); }, newPost(content, toProfile, isNews) { //Content is expected to be a string. let params = { content }; if(isNews) params.nonOrganicType = "News" if (toProfile && CurrentUserId !== toProfile) params.toProfile = toProfile; return postCall("/post/", params); }, newPostReaction(postid) { return postCall("/post/react", { postid }); }, removePostReaction(postid) { return postCall("/post/unreact", { postid }); }, newPostBookmark(postid) { return postCall("/post/bookmark", { postid }); }, removePostBookmark(postid) { return postCall("/post/unbookmark", { postid }); }, newPostComment(postid, content) { return postCall("/post/comment/", { postid, content }); }, newCommentReaction(postid, commentDate) { return postCall("/post/comment/react", { postid, commentDate }); }, removeCommentReaction(postid, commentDate) { return postCall("/post/comment/unreact", { postid, commentDate }); }, //Invitations newInvitation(name, email){ return postCall("/user/invite", { name, email }); }, getInvitation(email){ return getCall("/invite/" + email ); }, //Profiles async getMe() { if (!CurrentProfileData) if(!CurrentProfile) await this.isLoggedIn(); CurrentProfileData = await this.getUserProfile(CurrentProfile, true); return CurrentProfileData; }, getMyProfiles() { return getCall("/user/mine"); }, getMyProfile() { return getCall("/user/" + CurrentProfile); }, updateMyProfile(profile, data) { if (CurrentProfile) { delete userNameCache[CurrentProfile]; delete failedProfileCache[CurrentProfile]; } return postCall("/user/myProfile", {profile, data}); }, markNotificationsViewed() { if (CurrentProfile) { delete userNameCache[CurrentProfile]; delete failedProfileCache[CurrentProfile]; } return postCall("/user/notifications/viewed", {}); }, searchProfiles(query){ return getCall("/user/search", query ? {query} : {}).then((data)=>{ if(data.status == "ok"){ data.profiles.forEach((p)=>{ userNameCache[p._id] = p; }); return data } }); }, changeProfile(profileid){ return postCall("/changeProfile", {profileid}).then((profile) =>{ CurrentProfile = profileid; CurrentProfileData = profile; return profile; }); }, getUserProfile(profileid, refresh=false) { return getProfileFromCache(profileid, refresh).then((profile) => profile || emptyProfileData(profileid)); }, deleteProfile(profileid){ return deleteCall("/user/" + profileid); }, currentProfileId() { if (!CurrentProfile) this.isLoggedIn(); //replace with cookie return CurrentProfile; }, followProfile(profileid){ return getCall("/user/" + profileid + "/follow"); }, unfollowProfile(profileid){ return getCall("/user/" + profileid + "/unfollow"); }, setDataValue(key, value){ if (CurrentProfile && userNameCache[CurrentProfile]) { if (!userNameCache[CurrentProfile].data) userNameCache[CurrentProfile].data = {}; userNameCache[CurrentProfile].data[key] = value; } if (CurrentProfileData) { if (!CurrentProfileData.data) CurrentProfileData.data = {}; CurrentProfileData.data[key] = value; } return postCall("/user/setData", {key, value}); }, //Groups newGroup(title, subtitle, description, isPrivate=false, isCourse=false, photo='') { return postCall("/user/groups", { profile: { firstName: title, lastName: subtitle, description, photo }, isPrivate, isCourse }); }, getRecentGroups() { return getCall("/user/groups"); }, getFollowingGroups() { return getCall("/user/groups/following"); }, searchGroups(query){ return getCall("/user/groups/search", query ? {query} : {}).then((data)=>{ if(data.status == "ok"){ data.groups.forEach((p)=>{ userNameCache[p._id] = p; }); return data } }); }, getCourses() { return getCall("/user/courses"); }, getRecentCourses() { return getCall("/posts/course/recent"); }, searchCourses(query){ return getCall("/user/groups/search", query ? {query, courses: true} : {courses: true}).then((data)=>{ if(data.status == "ok"){ data.groups.forEach((p)=>{ userNameCache[p._id] = p; }); return data } }); }, getGroup(groupid) { return getCall("/user/groups/" + groupid); }, async subscribeToGroup(groupid){ delete userNameCache[groupid]; return await getCall("/user/groups/" + groupid + "/subscribe"); }, acceptGroupRequest(profileid, groupid){ delete userNameCache[groupid]; return postCall("/user/groups/accept", {profileid, groupid}); }, rejectGroupRequest(profileid, groupid){ delete userNameCache[groupid]; return postCall("/user/groups/reject", {profileid, groupid}); }, unsubscribeToGroup(groupid){ delete userNameCache[groupid]; return getCall("/user/groups/" + groupid + "/unsubscribe"); }, //Payments paymentIntent(userid, price, description){ return postCall("/payments/intent", {userid, price, description}); }, paymentRegister(userid, result){ return postCall("/payments/register/", {userid, result}); }, //Chat getChatMessages(limit = 100) { return getCall("/chat/messages", { limit, lang: getCurrentLanguage() }).then((data) => Array.isArray(data?.messages) ? data.messages : []); }, sendChatMessage(text) { return postCall("/chat/messages", { text, sourceLang: getCurrentLanguage() }); }, getChatActiveUsers() { return getCall("/chat/active").then((data) => Array.isArray(data?.activeUsers) ? data.activeUsers : []); }, pingChatPresence() { return postCall("/chat/ping", {}).then((data) => Array.isArray(data?.activeUsers) ? data.activeUsers : []); } } export default API;