Files
EMI-ExpoAPP/API.js

470 lines
16 KiB
JavaScript

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;