diff --git a/package.json b/package.json index 83641c6..23315ce 100755 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "main": "./dist/index.js", "author": "RageStudio ", "scripts": { + "test": "ava", "build": "hermes build" }, "files": [ @@ -12,6 +13,7 @@ "license": "MIT", "dependencies": { "@foxify/events": "^2.1.0", + "ava": "^6.1.2", "axios": "^1.4.0", "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.0", @@ -22,4 +24,4 @@ "devDependencies": { "@ragestudio/hermes": "^0.1.0" } -} \ No newline at end of file +} diff --git a/src/handlers/measurePing.js b/src/handlers/measurePing.js deleted file mode 100755 index 20a3774..0000000 --- a/src/handlers/measurePing.js +++ /dev/null @@ -1,58 +0,0 @@ -import request from "./request" - -export default async () => { - const timings = {} - - const promises = [ - new Promise(async (resolve) => { - const start = Date.now() - - const failTimeout = setTimeout(() => { - timings.http = "failed" - - resolve() - }, 10000) - - request({ - method: "GET", - url: "/ping", - }) - .then(() => { - // set http timing in ms - timings.http = Date.now() - start - - failTimeout && clearTimeout(failTimeout) - - resolve() - }) - .catch(() => { - timings.http = "failed" - - resolve() - }) - }), - new Promise((resolve) => { - const start = Date.now() - - const failTimeout = setTimeout(() => { - timings.ws = "failed" - - resolve() - }, 10000) - - __comty_shared_state.wsInstances["default"].on("pong", () => { - timings.ws = Date.now() - start - - failTimeout && clearTimeout(failTimeout) - - resolve() - }) - - __comty_shared_state.wsInstances["default"].emit("ping") - }) - ] - - await Promise.all(promises) - - return timings -} \ No newline at end of file diff --git a/src/helpers/handleRegenerationEvent.js b/src/helpers/handleRegenerationEvent.js index 9f67575..abd7ae9 100755 --- a/src/helpers/handleRegenerationEvent.js +++ b/src/helpers/handleRegenerationEvent.js @@ -1,5 +1,5 @@ import SessionModel from "../models/session" -import request from "../handlers/request" +import request from "../request" import { reconnectWebsockets } from "../" export default async (refreshToken) => { diff --git a/src/helpers/measurePing.js b/src/helpers/measurePing.js new file mode 100755 index 0000000..ed6dce8 --- /dev/null +++ b/src/helpers/measurePing.js @@ -0,0 +1,62 @@ +import request from "../request" + +const fetchers = { + http: () => new Promise(async (resolve) => { + const start = Date.now() + + const failTimeout = setTimeout(() => { + resolve("timeout") + }, 5000) + + request({ + method: "GET", + url: "/ping", + }) + .then(() => { + clearTimeout(failTimeout) + resolve(Date.now() - start) + }) + .catch((err) => { + console.log(err) + clearTimeout(failTimeout) + resolve("failed") + }) + }), + ws: () => new Promise((resolve) => { + const start = Date.now() + + const failTimeout = setTimeout(() => { + resolve("failed") + }, 5000) + + globalThis.__comty_shared_state.sockets["main"].on("pong", () => { + failTimeout && clearTimeout(failTimeout) + + resolve(Date.now() - start) + }) + + globalThis.__comty_shared_state.sockets["main"].emit("ping") + }) +} + +export default async ({ select } = {}) => { + let selectedPromises = [] + + if (Array.isArray(select)) { + select.forEach((item) => { + if (!fetchers[item]) { + return + } + selectedPromises.push(fetchers[item]()) + }) + } else { + selectedPromises = [ + fetchers["http"](), + fetchers["ws"](), + ] + } + + const result = await Promise.all(selectedPromises) + + return result +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 66f9de9..05e29f1 100755 --- a/src/index.js +++ b/src/index.js @@ -4,12 +4,9 @@ import EventEmitter from "@foxify/events" import axios from "axios" import { io } from "socket.io-client" -import remotes from "./remotes" - -import Storage from "./helpers/withStorage" - -import SessionModel from "./models/session" import { createHandlers } from "./models" +import Storage from "./helpers/withStorage" +import remote from "./remote" globalThis.isServerMode = typeof window === "undefined" && typeof global !== "undefined" @@ -25,7 +22,11 @@ if (globalThis.isServerMode) { } export async function createWebsockets() { - const instances = globalThis.__comty_shared_state.wsInstances + if (!remote.websockets) { + return false + } + + const instances = globalThis.__comty_shared_state.sockets for (let [key, instance] of Object.entries(instances)) { if (instance.connected) { @@ -36,27 +37,25 @@ export async function createWebsockets() { // remove current listeners instance.removeAllListeners() - delete globalThis.__comty_shared_state.wsInstances[key] + delete globalThis.__comty_shared_state.sockets[key] } - for (let [key, remote] of Object.entries(remotes)) { - if (!remote.hasWebsocket) { - continue - } - + for (let ws of remote.websockets) { let opts = { transports: ["websocket"], - autoConnect: remote.autoConnect ?? true, - ...remote.wsParams ?? {}, + autoConnect: ws.autoConnect ?? true, + forceNew: true, + path: ws.path, + ...ws.params ?? {}, } - if (remote.noAuth !== true) { + if (ws.noAuth !== true) { opts.auth = { - token: SessionModel.token, + token: Storage.engine.get("token"), } } - globalThis.__comty_shared_state.wsInstances[key] = io(remote.wsOrigin ?? remote.origin, opts) + globalThis.__comty_shared_state.sockets[ws.namespace] = io(remote.origin, opts) } // regsister events @@ -64,13 +63,6 @@ export async function createWebsockets() { instance.on("connect", () => { console.debug(`[WS-API][${key}] Connected`) - if (remotes[key].useClassicAuth && remotes[key].noAuth !== true) { - // try to auth - instance.emit("auth", { - token: SessionModel.token, - }) - } - globalThis.__comty_shared_state.eventBus.emit(`${key}:connected`) }) @@ -95,7 +87,7 @@ export async function createWebsockets() { } export async function disconnectWebsockets() { - const instances = globalThis.__comty_shared_state.wsInstances + const instances = globalThis.__comty_shared_state.sockets for (let [key, instance] of Object.entries(instances)) { if (instance.connected) { @@ -104,45 +96,24 @@ export async function disconnectWebsockets() { } } -export async function reconnectWebsockets({ force = false } = {}) { - const instances = globalThis.__comty_shared_state.wsInstances +export async function reconnectWebsockets() { + const instances = globalThis.__comty_shared_state.sockets for (let [key, instance] of Object.entries(instances)) { if (instance.connected) { - if (!instance.auth) { - instance.disconnect() - - instance.auth = { - token: SessionModel.token, - } - - instance.connect() - continue - } - - if (!force) { - instance.emit("reauthenticate", { - token: SessionModel.token, - }) - - continue - } - // disconnect first instance.disconnect() } - if (remotes[key].noAuth !== true) { - instance.auth = { - token: SessionModel.token, - } + instance.auth = { + token: Storage.engine.get("token"), } instance.connect() } } -export default function createClient({ +export function createClient({ accessKey = null, privateKey = null, enableWs = false, @@ -151,50 +122,47 @@ export default function createClient({ onExpiredExceptionEvent: false, excludedExpiredExceptionURL: ["/session/regenerate"], eventBus: new EventEmitter(), - mainOrigin: remotes.default.origin, - instances: Object(), - wsInstances: Object(), + mainOrigin: remote.origin, + baseRequest: null, + sockets: new Map(), rest: null, version: pkg.version, } sharedState.rest = createHandlers() - if (globalThis.isServerMode) { - } if (privateKey && accessKey && globalThis.isServerMode) { Storage.engine.set("token", `${accessKey}:${privateKey}`) } - // create instances for every remote - for (const [key, remote] of Object.entries(remotes)) { - sharedState.instances[key] = axios.create({ - baseURL: remote.origin, - headers: { - "Content-Type": "application/json", + sharedState.baseRequest = axios.create({ + baseURL: remote.origin, + headers: { + "Content-Type": "application/json", + } + }) + + // create a interceptor to attach the token every request + sharedState.baseRequest.interceptors.request.use((config) => { + // check if current request has no Authorization header, if so, attach the token + if (!config.headers["Authorization"]) { + const sessionToken = Storage.engine.get("token") + + if (sessionToken) { + config.headers["Authorization"] = `${globalThis.isServerMode ? "Server" : "Bearer"} ${sessionToken}` + } else { + console.warn("Making a request with no session token") } - }) + } - // create a interceptor to attach the token every request - sharedState.instances[key].interceptors.request.use((config) => { - // check if current request has no Authorization header, if so, attach the token - if (!config.headers["Authorization"]) { - const sessionToken = SessionModel.token - - if (sessionToken) { - config.headers["Authorization"] = `${globalThis.isServerMode ? "Server" : "Bearer"} ${sessionToken}` - } else { - console.warn("Making a request with no session token") - } - } - - return config - }) - } + return config + }) if (enableWs) { createWebsockets() } return sharedState -} \ No newline at end of file +} + +export default createClient \ No newline at end of file diff --git a/src/models/auth/index.js b/src/models/auth/index.js index aeb862b..ad71ebb 100755 --- a/src/models/auth/index.js +++ b/src/models/auth/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" import SessionModel from "../session" export default class AuthModel { @@ -87,4 +87,38 @@ export default class AuthModel { return response.data } + + static availability = async (payload) => { + const { username, email } = payload + + const response = await request({ + method: "get", + url: `/availability`, + data: { + username, + email, + } + }).catch((error) => { + console.error(error) + + return false + }) + + return response.data + } + + static changePassword = async (payload) => { + const { currentPassword, newPassword } = payload + + const { data } = await request({ + method: "put", + url: "/auth/password", + data: { + old_password: currentPassword, + new_password: newPassword, + } + }) + + return data + } } \ No newline at end of file diff --git a/src/models/feed/index.js b/src/models/feed/index.js index fef3ba0..61a6495 100755 --- a/src/models/feed/index.js +++ b/src/models/feed/index.js @@ -1,8 +1,16 @@ -import request from "../../handlers/request" +import request from "../../request" import Settings from "../../helpers/withSettings" export default class FeedModel { - static getMusicFeed = async ({ trim, limit } = {}) => { + /** + * Retrieves music feed with optional trimming and limiting. + * + * @param {Object} options - Optional parameters for trimming and limiting the feed + * @param {number} options.trim - The number of items to trim from the feed + * @param {number} options.limit - The maximum number of items to fetch from the feed + * @return {Promise} The music feed data + */ + static async getMusicFeed({ trim, limit } = {}) { const { data } = await request({ method: "GET", url: `/feed/music`, @@ -15,10 +23,18 @@ export default class FeedModel { return data } - static getGlobalMusicFeed = async ({ trim, limit } = {}) => { + /** + * Retrieves the global music feed with optional trimming and limiting. + * + * @param {Object} options - An object containing optional parameters: + * @param {number} options.trim - The number of items to trim from the feed + * @param {number} options.limit - The maximum number of items to fetch from the feed + * @return {Promise} The global music feed data + */ + static async getGlobalMusicFeed({ trim, limit } = {}) { const { data } = await request({ method: "GET", - url: `/feed/music/global`, + url: `/music/feed/global`, params: { trim: trim ?? 0, limit: limit ?? Settings.get("feed_max_fetch"), @@ -28,10 +44,18 @@ export default class FeedModel { return data } - static getTimelineFeed = async ({ trim, limit = 10 } = {}) => { + /** + * Retrieves the timeline feed with optional trimming and limiting. + * + * @param {object} options - Object containing trim and limit properties + * @param {number} options.trim - The number of feed items to trim + * @param {number} options.limit - The maximum number of feed items to retrieve + * @return {Promise} The timeline feed data + */ + static async getTimelineFeed({ trim, limit } = {}) { const { data } = await request({ method: "GET", - url: `/feed/timeline`, + url: `/posts/feed/timeline`, params: { trim: trim ?? 0, limit: limit ?? Settings.get("feed_max_fetch"), @@ -41,10 +65,18 @@ export default class FeedModel { return data } - static getPostsFeed = async ({ trim, limit } = {}) => { + /** + * Retrieves the posts feed with options to trim and limit the results. + * + * @param {Object} options - An object containing optional parameters for trimming and limiting the feed. + * @param {number} options.trim - The number of characters to trim the feed content. + * @param {number} options.limit - The maximum number of posts to fetch from the feed. + * @return {Promise} The posts feed data. + */ + static async getGlobalTimelineFeed({ trim, limit } = {}) { const { data } = await request({ method: "GET", - url: `/feed/posts`, + url: `/posts/feed/global`, params: { trim: trim ?? 0, limit: limit ?? Settings.get("feed_max_fetch"), diff --git a/src/models/follows/index.js b/src/models/follows/index.js index f93405c..007bfe2 100755 --- a/src/models/follows/index.js +++ b/src/models/follows/index.js @@ -1,5 +1,5 @@ import { SessionModel } from "../../models" -import request from "../../handlers/request" +import request from "../../request" export default class FollowsModel { static imFollowing = async (user_id) => { diff --git a/src/models/livestream/index.js b/src/models/livestream/index.js index 5393b23..bd83f94 100755 --- a/src/models/livestream/index.js +++ b/src/models/livestream/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" export default class Livestream { static deleteProfile = async (profile_id) => { diff --git a/src/models/music/index.js b/src/models/music/index.js index bff3c5c..92d4b60 100755 --- a/src/models/music/index.js +++ b/src/models/music/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" import pmap from "p-map" import SyncModel from "../sync" diff --git a/src/models/nfc/index.js b/src/models/nfc/index.js index 2305c13..e5a2093 100755 --- a/src/models/nfc/index.js +++ b/src/models/nfc/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" export default class NFCModel { static async getOwnTags() { diff --git a/src/models/post/index.js b/src/models/post/index.js index c111699..08003f9 100755 --- a/src/models/post/index.js +++ b/src/models/post/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" import Settings from "../../helpers/withSettings" export default class Post { @@ -26,58 +26,20 @@ export default class Post { const { data } = await request({ method: "GET", - url: `/posts/post/${post_id}`, + url: `/posts/${post_id}/data`, }) return data } - static getPostComments = async ({ post_id }) => { + static async replies({ post_id, trim, limit }) { if (!post_id) { throw new Error("Post ID is required") } const { data } = await request({ method: "GET", - url: `/comments/post/${post_id}`, - }) - - return data - } - - static sendComment = async ({ post_id, comment }) => { - if (!post_id || !comment) { - throw new Error("Post ID and/or comment are required") - } - - const { data } = await request({ - method: "POST", - url: `/comments/post/${post_id}`, - data: { - message: comment, - }, - }) - - return data - } - - static deleteComment = async ({ post_id, comment_id }) => { - if (!post_id || !comment_id) { - throw new Error("Post ID and/or comment ID are required") - } - - const { data } = await request({ - method: "DELETE", - url: `/comments/post/${post_id}/${comment_id}`, - }) - - return data - } - - static getExplorePosts = async ({ trim, limit }) => { - const { data } = await request({ - method: "GET", - url: `/posts/explore`, + url: `/posts/${post_id}/replies`, params: { trim: trim ?? 0, limit: limit ?? Settings.get("feed_max_fetch"), @@ -100,6 +62,19 @@ export default class Post { return data } + static getLikedPosts = async ({ trim, limit }) => { + const { data } = await request({ + method: "GET", + url: `/posts/liked`, + params: { + trim: trim ?? 0, + limit: limit ?? Settings.get("feed_max_fetch"), + } + }) + + return data + } + static getUserPosts = async ({ user_id, trim, limit }) => { if (!user_id) { // use current user_id @@ -154,6 +129,20 @@ export default class Post { return data } + static update = async (post_id, update) => { + if (!post_id) { + throw new Error("Post ID is required") + } + + const { data } = await request({ + method: "PUT", + url: `/posts/${post_id}/update`, + data: update, + }) + + return data + } + static deletePost = async ({ post_id }) => { if (!post_id) { throw new Error("Post ID is required") diff --git a/src/models/search/index.js b/src/models/search/index.js index 1aa8617..ac91693 100755 --- a/src/models/search/index.js +++ b/src/models/search/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" export default class Search { static search = async (keywords, params = {}) => { diff --git a/src/models/session/index.js b/src/models/session/index.js index ed0c3ab..961a864 100755 --- a/src/models/session/index.js +++ b/src/models/session/index.js @@ -1,5 +1,5 @@ import jwt_decode from "jwt-decode" -import request from "../../handlers/request" +import request from "../../request" import Storage from "../../helpers/withStorage" export default class Session { diff --git a/src/models/sync/index.js b/src/models/sync/index.js index 0d33576..edc04f9 100755 --- a/src/models/sync/index.js +++ b/src/models/sync/index.js @@ -1,7 +1,7 @@ import spotifyService from "./services/spotify" import tidalService from "./services/tidal" -import request from "../../handlers/request" +import request from "../../request" const namespacesServices = { spotify: spotifyService, diff --git a/src/models/sync/services/tidal.js b/src/models/sync/services/tidal.js index ba52ad2..353c7c0 100755 --- a/src/models/sync/services/tidal.js +++ b/src/models/sync/services/tidal.js @@ -1,4 +1,4 @@ -import request from "../../../handlers/request" +import request from "../../../request" export default class TidalService { static get api_instance() { diff --git a/src/models/user/index.js b/src/models/user/index.js index 4c98a1a..eb035e4 100755 --- a/src/models/user/index.js +++ b/src/models/user/index.js @@ -1,5 +1,5 @@ import SessionModel from "../session" -import request from "../../handlers/request" +import request from "../../request" export default class User { static data = async (payload = {}) => { @@ -33,7 +33,7 @@ export default class User { static updateData = async (payload) => { const response = await request({ method: "POST", - url: "/users/self/update_data", + url: "/users/self/update", data: { update: payload, }, @@ -43,12 +43,9 @@ export default class User { } static unsetFullName = async () => { - const response = await request({ - method: "DELETE", - url: "/users/self/public_name", + return await User.updateData({ + full_name: null, }) - - return response.data } static selfRoles = async () => { @@ -87,21 +84,6 @@ export default class User { return data } - static changePassword = async (payload) => { - const { currentPassword, newPassword } = payload - - const { data } = await request({ - method: "POST", - url: "/user/self/update_password", - data: { - currentPassword, - newPassword, - } - }) - - return data - } - static getUserFollowers = async ({ user_id, limit = 20, @@ -156,4 +138,38 @@ export default class User { return data } + + /** + * Retrive user config from server + * + * @param {type} key - A key of config + * @return {object} - Config object + */ + static async getConfig(key) { + const { data } = await request({ + method: "GET", + url: "/users/self/config", + params: { + key + } + }) + + return data + } + + /** + * Update the configuration with the given update. + * + * @param {Object} update - The object containing the updated configuration data + * @return {Promise} A Promise that resolves with the response data after the configuration is updated + */ + static async updateConfig(update) { + const { data } = await request({ + method: "PUT", + url: "/users/self/config", + data: update + }) + + return data + } } \ No newline at end of file diff --git a/src/models/widget/index.js b/src/models/widget/index.js index 4ba349e..5cfb937 100755 --- a/src/models/widget/index.js +++ b/src/models/widget/index.js @@ -1,4 +1,4 @@ -import request from "../../handlers/request" +import request from "../../request" export default class WidgetModel { static browse = async ({ limit, offset, keywords } = {}) => { diff --git a/src/remotes.js b/src/remote.js similarity index 50% rename from src/remotes.js rename to src/remote.js index 5ed5fbe..9303785 100755 --- a/src/remotes.js +++ b/src/remote.js @@ -13,8 +13,19 @@ const envOrigins = { } export default { - default: { - origin: envOrigins[process.env.NODE_ENV ?? "production"], - hasWebsocket: false, - } + origin: envOrigins[process.env.NODE_ENV ?? "production"], + websockets: [ + { + namespace: "posts", + path: "/posts", + }, + { + namespace: "main", + path: "/main", + }, + { + namespace: "notifications", + path: "/notifications", + } + ] } \ No newline at end of file diff --git a/src/handlers/request.js b/src/request.js similarity index 80% rename from src/handlers/request.js rename to src/request.js index 852e053..16cd088 100755 --- a/src/handlers/request.js +++ b/src/request.js @@ -1,5 +1,5 @@ -import handleBeforeRequest from "../helpers/handleBeforeRequest" -import handleAfterRequest from "../helpers/handleAfterRequest" +import handleBeforeRequest from "./helpers/handleBeforeRequest" +import handleAfterRequest from "./helpers/handleAfterRequest" export default async ( request = { @@ -7,7 +7,7 @@ export default async ( }, ...args ) => { - const instance = request.instance ?? __comty_shared_state.instances.default + const instance = request.instance ?? __comty_shared_state.baseRequest if (!instance) { throw new Error("No instance provided") diff --git a/tests/main.js b/tests/main.js new file mode 100644 index 0000000..30272d0 --- /dev/null +++ b/tests/main.js @@ -0,0 +1,12 @@ +const test = require("ava") +const lib = require("../dist/index") + +test("create client", async (t) => { + console.log(lib) + + const client = await lib.createClient() + + console.log(client) + + t.pass() +})