diff --git a/package.json b/package.json index 23315ce..ea1c7aa 100755 --- a/package.json +++ b/package.json @@ -1,27 +1,26 @@ { - "name": "comty.js", - "version": "0.60.3", - "main": "./dist/index.js", - "author": "RageStudio ", - "scripts": { - "test": "ava", - "build": "hermes build" - }, - "files": [ - "dist" - ], - "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", - "jwt-decode": "^3.1.2", - "luxon": "^3.3.0", - "socket.io-client": "^4.6.1" - }, - "devDependencies": { - "@ragestudio/hermes": "^0.1.0" - } + "name": "comty.js", + "version": "0.62.0", + "main": "./dist/index.js", + "author": "RageStudio ", + "scripts": { + "build": "hermes build" + }, + "files": [ + "dist" + ], + "license": "MIT", + "dependencies": { + "@foxify/events": "^2.1.0", + "axios": "^1.8.4", + "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.0", + "jwt-decode": "^4.0.0", + "linebridge-client": "^1.0.0", + "luxon": "^3.6.0", + "socket.io-client": "^4.8.1" + }, + "devDependencies": { + "@ragestudio/hermes": "^1.0.0" + } } diff --git a/src/addons.js b/src/addons.js new file mode 100644 index 0000000..0a29691 --- /dev/null +++ b/src/addons.js @@ -0,0 +1,23 @@ +export default class AddonsManager { + addons = new Map() + + register(name, addon) { + this.addons.set(name, addon) + } + + get(name) { + return this.addons.get(name) + } + + // search all addons registered, and find all addons that has a addon[operation] function + getByOperation(operation) { + return Array.from(this.addons.values()) + .filter((addon) => addon[operation]) + .map((addon) => { + return { + id: addon.constructor.id, + fn: addon[operation], + } + }) + } +} diff --git a/src/helpers/handleAfterRequest.js b/src/helpers/handleAfterRequest.js index 3840d15..1ae858c 100755 --- a/src/helpers/handleAfterRequest.js +++ b/src/helpers/handleAfterRequest.js @@ -1,33 +1,31 @@ -import handleRegenerationEvent from "./handleRegenerationEvent" +import refreshToken from "./refreshToken" export default async (data, callback) => { // handle 401, 403 responses if (data instanceof Error) { if (data.code && (data.code === "ECONNABORTED" || data.code === "ERR_NETWORK")) { - console.error(`Request aborted or network error, ignoring`) + console.error(`Request aborted or network error`) return false } - if (data.response.status === 401) { - // check if the server issue a refresh token on data - if (data.response.data.refreshToken) { - console.log(`Session expired, but the server issued a refresh token, handling regeneration event`) + if (data.response) { + if (data.response.status === 401) { + // check if the server issue a refresh token on data + if (data.response.data.expired) { + try { + console.log(`Session expired, trying to regenerate...`) - // handle regeneration event - await handleRegenerationEvent(data.response.data.refreshToken) + await refreshToken() + } catch (error) { + __comty_shared_state.eventBus.emit("session.invalid", error.message) - return await callback() - } + console.error(`Failed to regenerate token: ${error.message}`) - // check if route is from "session" namespace - if (data.config.url.includes("/session")) { - return __comty_shared_state.eventBus.emit("session.invalid", "Session expired, but the server did not issue a refresh token") - } - } + throw new Error(`Invalid or Expired session`) + } - if (data.response.status === 403) { - if (data.config.url.includes("/session")) { - return __comty_shared_state.eventBus.emit("session.invalid", "Session not valid or not existent") + return await callback() + } } } } diff --git a/src/helpers/handleBeforeRequest.js b/src/helpers/handleBeforeRequest.js index be7cbee..526c034 100755 --- a/src/helpers/handleBeforeRequest.js +++ b/src/helpers/handleBeforeRequest.js @@ -1,13 +1,7 @@ -export default async (request) => { - if (__comty_shared_state.onExpiredExceptionEvent) { - if (__comty_shared_state.excludedExpiredExceptionURL.includes(request.url)) return - +export default async () => { + if (__comty_shared_state.refreshingToken === true) { await new Promise((resolve) => { - __comty_shared_state.eventBus.once("session.regenerated", () => { - console.log(`Session has been regenerated, retrying request`) - - resolve() - }) + __comty_shared_state.eventBus.once("session:refreshed", resolve) }) } } \ No newline at end of file diff --git a/src/helpers/handleRegenerationEvent.js b/src/helpers/handleRegenerationEvent.js deleted file mode 100755 index abd7ae9..0000000 --- a/src/helpers/handleRegenerationEvent.js +++ /dev/null @@ -1,43 +0,0 @@ -import SessionModel from "../models/session" -import request from "../request" -import { reconnectWebsockets } from "../" - -export default async (refreshToken) => { - __comty_shared_state.eventBus.emit("session.expiredExceptionEvent", refreshToken) - - __comty_shared_state.onExpiredExceptionEvent = true - - const expiredToken = await SessionModel.token - - // send request to regenerate token - const response = await request({ - method: "POST", - url: "/session/regenerate", - data: { - expiredToken: expiredToken, - refreshToken, - } - }).catch((error) => { - console.error(`Failed to regenerate token: ${error.message}`) - return false - }) - - if (!response) { - return __comty_shared_state.eventBus.emit("session.invalid", "Failed to regenerate token") - } - - if (!response.data?.token) { - return __comty_shared_state.eventBus.emit("session.invalid", "Failed to regenerate token, invalid server response.") - } - - // set new token - SessionModel.token = response.data.token - - __comty_shared_state.onExpiredExceptionEvent = false - - // emit event - __comty_shared_state.eventBus.emit("session.regenerated") - - // reconnect websockets - reconnectWebsockets() -} \ No newline at end of file diff --git a/src/helpers/measurePing.js b/src/helpers/measurePing.js deleted file mode 100755 index ed6dce8..0000000 --- a/src/helpers/measurePing.js +++ /dev/null @@ -1,62 +0,0 @@ -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/helpers/processWithAddons.js b/src/helpers/processWithAddons.js new file mode 100644 index 0000000..5f2c5c9 --- /dev/null +++ b/src/helpers/processWithAddons.js @@ -0,0 +1,30 @@ +export default async function processAddons({ + operation, + initialData, + fnArguments, + normalizeAddonResult, +}) { + const addons = __comty_shared_state.addons.getByOperation(operation) + + let processedData = initialData + + if (typeof fnArguments === "undefined") { + fnArguments = [] + } + + for (const addon of addons) { + try { + const addonResult = await addon.fn(...fnArguments) + + processedData = normalizeAddonResult({ + operation, + currentData: processedData, + addonResult, + }) + } catch (error) { + console.error(`Error in [${operation}] addon:`, error) + } + } + + return processedData +} diff --git a/src/helpers/refreshToken.js b/src/helpers/refreshToken.js new file mode 100755 index 0000000..3720d9d --- /dev/null +++ b/src/helpers/refreshToken.js @@ -0,0 +1,46 @@ +import SessionModel from "../models/session" + +export default async () => { + __comty_shared_state.eventBus.emit("session:refreshing") + __comty_shared_state.refreshingToken = true + + // send request to regenerate token + const response = await __comty_shared_state + .baseRequest({ + method: "POST", + url: "/auth", + data: { + authToken: await SessionModel.token, + refreshToken: await SessionModel.refreshToken, + }, + }) + .catch((error) => { + return false + }) + + if (!response) { + __comty_shared_state.refreshingToken = false + + throw new Error("Failed to regenerate token.") + } + + if (!response.data?.token) { + __comty_shared_state.refreshingToken = false + + throw new Error("Failed to regenerate token, invalid server response.") + } + + // set new token + SessionModel.token = response.data.token + SessionModel.refreshToken = response.data.refreshToken + + // emit event + __comty_shared_state.eventBus.emit("session:refreshed") + __comty_shared_state.refreshingToken = false + + if (typeof __comty_shared_state.ws === "object") { + await __comty_shared_state.ws.connectAll() + } + + return true +} diff --git a/src/hooks/useRequest/index.js b/src/hooks/useRequest/index.js index 0dd2b7a..8b7ab7a 100755 --- a/src/hooks/useRequest/index.js +++ b/src/hooks/useRequest/index.js @@ -1,14 +1,13 @@ import React from "react" export default (method, ...args) => { - if (typeof method !== "function") { - throw new Error("useRequest: method must be a function") - } - const [loading, setLoading] = React.useState(true) const [result, setResult] = React.useState(null) const [error, setError] = React.useState(null) + if (typeof method !== "function") { + return [() => {}, null, new Error("Method is not a function"), () => {}] + } const makeRequest = (...newArgs) => { method(...newArgs) .then((data) => { diff --git a/src/index.js b/src/index.js index 05e29f1..19acf4b 100755 --- a/src/index.js +++ b/src/index.js @@ -1,168 +1,91 @@ import pkg from "../package.json" import EventEmitter from "@foxify/events" - import axios from "axios" -import { io } from "socket.io-client" -import { createHandlers } from "./models" +import AddonsManager from "./addons" +import WebsocketManager from "./ws" import Storage from "./helpers/withStorage" -import remote from "./remote" +import Remotes from "./remotes" -globalThis.isServerMode = typeof window === "undefined" && typeof global !== "undefined" +globalThis.isServerMode = + typeof window === "undefined" && typeof global !== "undefined" if (globalThis.isServerMode) { - const { Buffer } = require("buffer") + const { Buffer } = require("buffer") - globalThis.b64Decode = (data) => { - return Buffer.from(data, "base64").toString("utf-8") - } - globalThis.b64Encode = (data) => { - return Buffer.from(data, "utf-8").toString("base64") - } -} - -export async function createWebsockets() { - if (!remote.websockets) { - return false - } - - const instances = globalThis.__comty_shared_state.sockets - - for (let [key, instance] of Object.entries(instances)) { - if (instance.connected) { - // disconnect first - instance.disconnect() - } - - // remove current listeners - instance.removeAllListeners() - - delete globalThis.__comty_shared_state.sockets[key] - } - - for (let ws of remote.websockets) { - let opts = { - transports: ["websocket"], - autoConnect: ws.autoConnect ?? true, - forceNew: true, - path: ws.path, - ...ws.params ?? {}, - } - - if (ws.noAuth !== true) { - opts.auth = { - token: Storage.engine.get("token"), - } - } - - globalThis.__comty_shared_state.sockets[ws.namespace] = io(remote.origin, opts) - } - - // regsister events - for (let [key, instance] of Object.entries(instances)) { - instance.on("connect", () => { - console.debug(`[WS-API][${key}] Connected`) - - globalThis.__comty_shared_state.eventBus.emit(`${key}:connected`) - }) - - instance.on("disconnect", () => { - console.debug(`[WS-API][${key}] Disconnected`) - - globalThis.__comty_shared_state.eventBus.emit(`${key}:disconnected`) - }) - - instance.on("error", (error) => { - console.error(`[WS-API][${key}] Error`, error) - - globalThis.__comty_shared_state.eventBus.emit(`${key}:error`, error) - }) - - instance.onAny((event, ...args) => { - console.debug(`[WS-API][${key}] Event (${event})`, ...args) - - globalThis.__comty_shared_state.eventBus.emit(`${key}:${event}`, ...args) - }) - } -} - -export async function disconnectWebsockets() { - const instances = globalThis.__comty_shared_state.sockets - - for (let [key, instance] of Object.entries(instances)) { - if (instance.connected) { - instance.disconnect() - } - } -} - -export async function reconnectWebsockets() { - const instances = globalThis.__comty_shared_state.sockets - - for (let [key, instance] of Object.entries(instances)) { - if (instance.connected) { - // disconnect first - instance.disconnect() - } - - instance.auth = { - token: Storage.engine.get("token"), - } - - instance.connect() - } + globalThis.b64Decode = (data) => { + return Buffer.from(data, "base64").toString("utf-8") + } + globalThis.b64Encode = (data) => { + return Buffer.from(data, "utf-8").toString("base64") + } } +/** + * Create a client with the specified access key, private key, and websocket enablement. + * + * @param {Object} options - Optional parameters for accessKey, privateKey, and enableWs + * @return {Object} sharedState - Object containing eventBus, mainOrigin, baseRequest, sockets, rest, and version + */ export function createClient({ - accessKey = null, - privateKey = null, - enableWs = false, + accessKey = null, + privateKey = null, + ws = { + enable: false, + autoConnect: false, + }, + origin = Remotes.origin, + eventBus = new EventEmitter(), } = {}) { - const sharedState = globalThis.__comty_shared_state = { - onExpiredExceptionEvent: false, - excludedExpiredExceptionURL: ["/session/regenerate"], - eventBus: new EventEmitter(), - mainOrigin: remote.origin, - baseRequest: null, - sockets: new Map(), - rest: null, - version: pkg.version, - } + const sharedState = (globalThis.__comty_shared_state = { + eventBus: eventBus, + mainOrigin: origin, + baseRequest: null, + ws: null, + rest: null, + version: pkg.version, + addons: new AddonsManager(), + }) - sharedState.rest = createHandlers() + if (privateKey && accessKey && globalThis.isServerMode) { + Storage.engine.set("token", `${accessKey}:${privateKey}`) + } - if (privateKey && accessKey && globalThis.isServerMode) { - Storage.engine.set("token", `${accessKey}:${privateKey}`) - } + sharedState.baseRequest = axios.create({ + baseURL: 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") - // 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") + } + } - 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 (typeof ws === "object") { + if (ws.enable === true) { + __comty_shared_state.ws = new WebsocketManager() - if (enableWs) { - createWebsockets() - } + if (ws.autoConnect === true) { + sharedState.ws.connectAll() + } + } + } - return sharedState + return sharedState } -export default createClient \ No newline at end of file +export default createClient diff --git a/src/models/api/index.js b/src/models/api/index.js new file mode 100644 index 0000000..f5b9e13 --- /dev/null +++ b/src/models/api/index.js @@ -0,0 +1,78 @@ +import request from "../../request" + +export default class API { + /** + * Retrieves the server keys associated with the current user. + * + * @return {object} The server keys data + */ + static async getMyServerKeys() { + const response = await request({ + method: "GET", + url: "/server-keys/my", + }) + + return response.data + } + + /** + * Creates a new server key. + * + * @param {object} options - Options for the new server key. + * @param {string} options.name - The name of the server key. + * @param {string} options.description - The description of the server key. + * @param {string} options.access - The access level of the server key. + * @return {object} The newly created server key data. + */ + static async createNewServerKey({ + name, + description, + access, + } = {}) { + const response = await request({ + method: "POST", + url: "/server-keys/generate", + data: { + name, + description, + access + } + }) + + return response.data + } + + /** + * Regenerates a secret token for a server key. + * + * @param {object} options - Options for regenerating the secret token. + * @param {string} access_id - The ID of the server key to regenerate the secret token for. + * @return {object} The regenerated secret token data. + */ + static async regenerateSecretToken(access_id) { + const response = await request({ + method: "POST", + url: `/server-keys/${access_id}/regenerate`, + }) + + return response.data + } + + /** + * Deletes a server key by its access ID. + * + * @param {string} access_id - The ID of the server key to delete. + * @return {Promise} - A promise that resolves to the response data. + */ + static async deleteServerKey(access_id) { + const response = await request({ + method: "DELETE", + url: `/server-keys/${access_id}`, + data: { + access_id + } + }) + + return response.data + } +} \ No newline at end of file diff --git a/src/models/auth/index.js b/src/models/auth/index.js index ad71ebb..7abcd9a 100755 --- a/src/models/auth/index.js +++ b/src/models/auth/index.js @@ -2,123 +2,264 @@ import request from "../../request" import SessionModel from "../session" export default class AuthModel { - static login = async (payload, callback) => { - const response = await request({ - method: "post", - url: "/auth", - data: { - username: payload.username, - password: payload.password, - mfa_code: payload.mfa_code, - }, - }) + /** + * Async function to handle the login process. + * + * @param {Object} payload - The payload containing username, password, and MFA code. + * @param {Function} callback - Optional callback function to handle further actions. + * @return {Object|boolean} The response data if login successful, false if MFA is required. + */ + static async login(payload, callback) { + const response = await request({ + method: "post", + url: "/auth", + data: payload, + }) - if (response.data.mfa_required) { - __comty_shared_state.eventBus.emit("auth:mfa_required") + if (response.data.mfa_required) { + __comty_shared_state.eventBus.emit("auth:mfa_required") - if (typeof callback === "function") { - await callback({ - mfa_required: { - method: response.data.method, - sended_to: response.data.sended_to, - }, - }) - } + if (typeof callback === "function") { + await callback({ + mfa_required: { + method: response.data.method, + sended_to: response.data.sended_to, + }, + }) + } - return false - } + return false + } - SessionModel.token = response.data.token + SessionModel.token = response.data.token + SessionModel.refreshToken = response.data.refreshToken - if (typeof callback === "function") { - await callback() - } + if (typeof callback === "function") { + await callback() + } - __comty_shared_state.eventBus.emit("auth:login_success") + __comty_shared_state.eventBus.emit("auth:login_success") - return response.data - } + return response.data + } - static logout = async () => { - await SessionModel.destroyCurrentSession() + /** + * Asynchronously logs out the user by destroying the current session and emitting an event for successful logout. + * + * @return {Promise} A Promise that resolves after the logout process is completed. + */ + static async logout() { + await SessionModel.destroyCurrentSession() - SessionModel.removeToken() + await SessionModel.removeToken() - __comty_shared_state.eventBus.emit("auth:logout_success") - } + __comty_shared_state.eventBus.emit("auth:logout_success") + } - static register = async (payload) => { - const { username, password, email } = payload + /** + * Registers a new user with the provided payload. + * + * @param {Object} payload - The payload containing the user's information. + * @param {string} payload.username - The username of the user. + * @param {string} payload.password - The password of the user. + * @param {string} payload.email - The email of the user. + * @param {boolean} payload.tos - The acceptance of the terms of service. + * @return {Promise} A Promise that resolves with the response data if the registration is successful, or false if there was an error. + * @throws {Error} Throws an error if the registration fails. + */ + static async register(payload) { + const { username, password, email, tos } = payload - const response = await request({ - method: "post", - url: "/auth/register", - data: { - username, - password, - email, - } - }).catch((error) => { - console.error(error) + const response = await request({ + method: "post", + url: "/register", + data: { + username, + password, + email, + accept_tos: tos, + }, + }).catch((error) => { + console.error(error) - return false - }) + return false + }) - if (!response) { - throw new Error("Unable to register user") - } + if (!response) { + throw new Error("Unable to register user") + } - return response - } + return response + } - static usernameValidation = async (username) => { - const response = await request({ - method: "get", - url: `/auth/${username}/exists`, - }).catch((error) => { - console.error(error) + /** + * Verifies the given token and returns the user data associated with it. + * + * @param {string} [token] - The token to verify. If not provided, the stored token is used. + * @return {Promise} A Promise that resolves with the user data if the token is valid, or false if the token is invalid. + * @throws {Error} Throws an error if there was an issue with the request. + */ + static async authToken(token) { + if (!token) { + token = await SessionModel.token + } - return false - }) + const response = await request({ + method: "POST", + url: "/auth/token", + data: { + token: token, + }, + }) - if (!response) { - throw new Error("Unable to validate user") - } + return response.data + } - return response.data - } + /** + * Validates the existence of a username by making a GET request to the `/auth/{username}/exists` endpoint. + * + * @param {string} username - The username to validate. + * @return {Promise} A Promise that resolves with the response data if the validation is successful, + * or false if there was an error. Throws an error if the validation fails. + */ + static async usernameValidation(username) { + const response = await request({ + method: "get", + url: `/auth/${username}/exists`, + }).catch((error) => { + console.error(error) - static availability = async (payload) => { - const { username, email } = payload + return false + }) - const response = await request({ - method: "get", - url: `/availability`, - data: { - username, - email, - } - }).catch((error) => { - console.error(error) + if (!response) { + throw new Error("Unable to validate user") + } - return false - }) + return response.data + } - return response.data - } + /** + * Retrieves the availability of a username and email by making a GET request to the `/availability` endpoint. + * + * @param {Object} payload - The payload containing the username and email. + * @param {string} payload.username - The username to check availability for. + * @param {string} payload.email - The email to check availability for. + * @return {Promise} A Promise that resolves with the availability data if successful, or false if an error occurred. + */ + static async availability(payload) { + const { username, email } = payload - static changePassword = async (payload) => { - const { currentPassword, newPassword } = payload + const response = await request({ + method: "get", + url: `/availability`, + params: { + username, + email, + }, + }).catch((error) => { + console.error(error) - const { data } = await request({ - method: "put", - url: "/auth/password", - data: { - old_password: currentPassword, - new_password: newPassword, - } - }) + return false + }) - return data - } -} \ No newline at end of file + return response.data + } + + /** + * A function to change the user's password. + * + * @param {Object} payload - An object containing the currentPassword, newPassword, and code. + * @param {string} payload.currentPassword - The current password of the user. + * @param {string} payload.newPassword - The new password to set for the user. + * @param {string} [payload.code] - The activation code sent to the user's email, optional if the old password is provided. + * @return {Promise} The data response after changing the password. + */ + static async changePassword(payload) { + const { currentPassword, newPassword, code, verificationToken } = + payload + + const { data } = await request({ + method: "put", + url: "/auth/password", + data: { + code: code, + verificationToken: verificationToken, + old_password: currentPassword, + new_password: newPassword, + }, + }) + + return data + } + + /** + * Activates a user account using the provided activation code. + * + * @param {string} user_id - The ID of the user to activate. + * @param {string} code - The activation code sent to the user's email. + * @return {Promise} A promise that resolves with the response data after activation. + * @throws {Error} Throws an error if the activation process fails. + */ + static async activateAccount(user_id, code) { + const { data } = await request({ + method: "post", + url: "/auth/activate", + data: { + code: code, + user_id: user_id, + }, + }) + + return data + } + + /** + * Resends the activation code to the user. + * + * @return {Promise} A promise that resolves with the response data after sending the activation code. + * @throws {Error} Throws an error if the resend activation code process fails. + * @param user_id + */ + static async resendActivationCode(user_id) { + const { data } = await request({ + method: "post", + url: "/auth/resend-activation-code", + data: { + user_id: user_id, + }, + }) + + return data + } + + static async disableAccount({ confirm = false } = {}) { + if (!confirm) { + console.error( + "In order to disable your account, you must confirm the action.", + ) + return null + } + + const { data } = await request({ + method: "post", + url: "/auth/disable-account", + }) + + __comty_shared_state.eventBus.emit("auth:disabled_account") + + return data + } + + static async recoverPassword(usernameOrEmail) { + const { data } = await request({ + method: "post", + url: "/auth/recover-password", + data: { + account: usernameOrEmail, + }, + }) + + return data + } +} diff --git a/src/models/chats/index.js b/src/models/chats/index.js new file mode 100644 index 0000000..1713415 --- /dev/null +++ b/src/models/chats/index.js @@ -0,0 +1,37 @@ +import request from "../../request" + +export default class ChatsService { + /** + * Retrieves the chat history for a given chat ID. + * + * @param {string} chat_id - The ID of the chat. + * @return {Promise} The chat history data. + * @throws {Error} If the chat_id is not provided. + */ + static async getChatHistory(chat_id) { + if (!chat_id) { + throw new Error("chat_id is required") + } + + const { data } = await request({ + method: "GET", + url: `/chats/${chat_id}/history`, + }) + + return data + } + + /** + * Retrieves the recent chats for the current user. + * + * @return {Promise} The chat history data. + */ + static async getRecentChats() { + const { data } = await request({ + method: "GET", + url: "/chats/my", + }) + + return data + } +} \ No newline at end of file diff --git a/src/models/feed/index.js b/src/models/feed/index.js index 61a6495..7da57b9 100755 --- a/src/models/feed/index.js +++ b/src/models/feed/index.js @@ -13,7 +13,7 @@ export default class FeedModel { static async getMusicFeed({ trim, limit } = {}) { const { data } = await request({ method: "GET", - url: `/feed/music`, + url: `/music/feed/my`, params: { trim: trim ?? 0, limit: limit ?? Settings.get("feed_max_fetch"), @@ -34,7 +34,7 @@ export default class FeedModel { static async getGlobalMusicFeed({ trim, limit } = {}) { const { data } = await request({ method: "GET", - url: `/music/feed/global`, + url: `/music/feed`, 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 007bfe2..10f2770 100755 --- a/src/models/follows/index.js +++ b/src/models/follows/index.js @@ -1,8 +1,15 @@ -import { SessionModel } from "../../models" +import SessionModel from "../../models/session" import request from "../../request" export default class FollowsModel { - static imFollowing = async (user_id) => { + /** + * Checks if the current user is following the specified user. + * + * @param {string} user_id - The ID of the user to check if the current user is following. + * @return {Promise} A promise that resolves with the response data indicating if the current user is following the specified user. + * @throws {Error} If the user_id parameter is not provided. + */ + static async imFollowing(user_id) { if (!user_id) { throw new Error("user_id is required") } @@ -15,9 +22,15 @@ export default class FollowsModel { return response.data } - static getFollowers = async (user_id, fetchData) => { + /** + * Retrieves the list of followers for a given user. + * + * @param {string} user_id - The ID of the user. If not provided, the current user ID will be used. + * @param {boolean} fetchData - Whether to fetch additional data for each follower. Defaults to false. + * @return {Promise} A promise that resolves with the list of followers and their data. + */ + static async getFollowers(user_id, fetchData) { if (!user_id) { - // set current user_id user_id = SessionModel.user_id } @@ -32,7 +45,13 @@ export default class FollowsModel { return response.data } - static toggleFollow = async ({ user_id }) => { + /** + * Toggles the follow status for a user. + * + * @param {Object} user_id - The ID of the user to toggle follow status. + * @return {Promise} The response data after toggling follow status. + */ + static async toggleFollow({ user_id }) { if (!user_id) { throw new Error("user_id is required") } diff --git a/src/models/index.js b/src/models/index.js deleted file mode 100755 index 11d514c..0000000 --- a/src/models/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import AuthModel from "./auth" -import FeedModel from "./feed" -import FollowsModel from "./follows" -import LivestreamModel from "./livestream" -import PostModel from "./post" -import SessionModel from "./session" -import SyncModel from "./sync" -import UserModel from "./user" - -function getEndpointsFromModel(model) { - return Object.entries(model).reduce((acc, [key, value]) => { - acc[key] = value - - return acc - }, {}) -} - -function createHandlers() { - return { - auth: getEndpointsFromModel(AuthModel), - feed: getEndpointsFromModel(FeedModel), - follows: getEndpointsFromModel(FollowsModel), - livestream: getEndpointsFromModel(LivestreamModel), - post: getEndpointsFromModel(PostModel), - session: getEndpointsFromModel(SessionModel), - sync: getEndpointsFromModel(SyncModel), - user: getEndpointsFromModel(UserModel), - } -} - -export { - AuthModel, - FeedModel, - FollowsModel, - LivestreamModel, - PostModel, - SessionModel, - SyncModel, - UserModel, - createHandlers, -} diff --git a/src/models/livestream/index.js b/src/models/livestream/index.js deleted file mode 100755 index bd83f94..0000000 --- a/src/models/livestream/index.js +++ /dev/null @@ -1,84 +0,0 @@ -import request from "../../request" - -export default class Livestream { - static deleteProfile = async (profile_id) => { - const response = await request({ - method: "DELETE", - url: `/tv/streaming/profile`, - data: { - profile_id - } - }) - - return response.data - } - - static postProfile = async (payload) => { - const response = await request({ - method: "POST", - url: `/tv/streaming/profile`, - data: payload, - }) - - return response.data - } - - static getProfiles = async () => { - const response = await request({ - method: "GET", - url: `/tv/streaming/profiles`, - }) - - return response.data - } - - static regenerateStreamingKey = async (profile_id) => { - const response = await request({ - method: "POST", - url: `/tv/streaming/regenerate_key`, - data: { - profile_id - } - }) - - return response.data - } - - static getCategories = async (key) => { - const response = await request({ - method: "GET", - url: `/tv/streaming/categories`, - params: { - key, - } - }) - - return response.data - } - - static getLivestream = async (payload = {}) => { - if (!payload.username) { - throw new Error("Username is required") - } - - let response = await request({ - method: "GET", - url: `/tv/streams`, - params: { - username: payload.username, - profile_id: payload.profile_id, - } - }) - - return response.data - } - - static getLivestreams = async () => { - const response = await request({ - method: "GET", - url: `/tv/streams`, - }) - - return response.data - } -} \ No newline at end of file diff --git a/src/models/music/getters/favouriteFolder.js b/src/models/music/getters/favouriteFolder.js new file mode 100644 index 0000000..269063a --- /dev/null +++ b/src/models/music/getters/favouriteFolder.js @@ -0,0 +1,42 @@ +import request from "../../../request" +import processAddons from "../../../helpers/processWithAddons" +import standartListMerge from "../../../utils/standartListMerge" + +export default async ({ limit = 100, offset = 0, order = "desc" }) => { + const addons = + __comty_shared_state.addons.getByOperation("getFavoriteFolder") + + const dividedLimit = limit / (addons.length + 1) + + const { data } = await request({ + method: "GET", + url: "/music/my/folder", + params: { + limit: dividedLimit, + offset: offset, + order: order, + }, + }) + + let results = await processAddons({ + operation: "getFavoriteFolder", + initialData: data, + fnArguments: [{ limit: dividedLimit, offset: offset, order: order }], + normalizeAddonResult: ({ currentData, addonResult }) => { + return standartListMerge(currentData, addonResult) + }, + }) + + // sort by liked_at + results.tracks.items.sort((a, b) => { + if (a.liked_at > b.liked_at) { + return -1 + } + if (a.liked_at < b.liked_at) { + return 1 + } + return 0 + }) + + return results +} diff --git a/src/models/music/getters/featuredPlaylists.ts b/src/models/music/getters/featuredPlaylists.ts new file mode 100644 index 0000000..6cbf0c7 --- /dev/null +++ b/src/models/music/getters/featuredPlaylists.ts @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async () => { + const response = await request({ + method: "GET", + url: "/music/playlists/featured", + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/index.js b/src/models/music/getters/index.js new file mode 100644 index 0000000..3700f30 --- /dev/null +++ b/src/models/music/getters/index.js @@ -0,0 +1,33 @@ +function exportObjs() { + if (typeof window !== "undefined") { + const paths = { + ...import.meta.glob("./**.ts", { eager: true, import: "default" }), + ...import.meta.glob("./**.js", { eager: true, import: "default" }), + } + + return Object.entries(paths).reduce((acc, [path, module]) => { + const name = path + .split("/") + .pop() + .replace(/\.(ts|js)$/, "") + acc[name] = module + return acc + }, {}) + } else { + const fs = require("fs") + const path = require("path") + + return fs + .readdirSync(__dirname) + .filter((file) => file !== "index.js" && /\.js$/.test(file)) + .reduce((acc, file) => { + const name = file.replace(/\.js$/, "") + acc[name] = require(path.join(__dirname, file)).default + return acc + }, {}) + } +} + +const exportedObjs = exportObjs() + +export default exportedObjs diff --git a/src/models/music/getters/isItemFavourited.js b/src/models/music/getters/isItemFavourited.js new file mode 100644 index 0000000..4278b72 --- /dev/null +++ b/src/models/music/getters/isItemFavourited.js @@ -0,0 +1,33 @@ +import request from "../../../request" + +const typeToNamespace = { + track: "tracks", + //playlist: "playlists", + //release: "releases", +} + +export default async (type, track_id) => { + if (!type) { + throw new Error("type is required") + } + + if (!track_id) { + throw new Error("track_id is required") + } + + type = type.toLowerCase() + + type = typeToNamespace[type] + + if (!type) { + throw new Error(`Unsupported type: ${type}`) + } + + const response = await request({ + method: "GET", + url: `/music/${type}/${track_id}/is_favourite`, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/myReleases.ts b/src/models/music/getters/myReleases.ts new file mode 100644 index 0000000..999a4bc --- /dev/null +++ b/src/models/music/getters/myReleases.ts @@ -0,0 +1,26 @@ +import request from "../../../request" + +type Arguments = { + limit: Number + offset: Number + keywords: String +} + +export default async ({ + limit, + offset, + keywords, +}: Arguments) => { + const response = await request({ + method: "GET", + url: "/music/releases/self", + params: { + limit: limit, + offset: offset, + keywords: keywords, + } + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/playlistData.ts b/src/models/music/getters/playlistData.ts new file mode 100644 index 0000000..558ba03 --- /dev/null +++ b/src/models/music/getters/playlistData.ts @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async (id: String) => { + const response = await request({ + method: "GET", + url: `/music/playlists/${id}/data`, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/playlistItem.ts b/src/models/music/getters/playlistItem.ts new file mode 100644 index 0000000..f36a3f7 --- /dev/null +++ b/src/models/music/getters/playlistItem.ts @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async (id: String) => { + const response = await request({ + method: "GET", + url: `/music/playlists/${id}/items`, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/playlists.ts b/src/models/music/getters/playlists.ts new file mode 100644 index 0000000..4f71138 --- /dev/null +++ b/src/models/music/getters/playlists.ts @@ -0,0 +1,29 @@ +import request from "../../../request" + +type Arguments = { + keywords: String + user_id: String + limit: Number + offset: Number +} + +export default async ({ + keywords, + user_id, + limit, + offset, +}: Arguments) => { + const response = await request({ + method: "GET", + url: "/music/playlists", + params: { + keywords: keywords, + user_id: user_id, + limit: limit, + offset: offset, + } + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/recentlyPlayed.js b/src/models/music/getters/recentlyPlayed.js new file mode 100644 index 0000000..ebd2053 --- /dev/null +++ b/src/models/music/getters/recentlyPlayed.js @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async (params) => { + const response = await request({ + method: "GET", + url: `/music/recently`, + params: params, + }) + + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/releaseData.ts b/src/models/music/getters/releaseData.ts new file mode 100644 index 0000000..04593ce --- /dev/null +++ b/src/models/music/getters/releaseData.ts @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async (id: String) => { + const response = await request({ + method: "GET", + url: `/music/releases/${id}/data`, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/releases.ts b/src/models/music/getters/releases.ts new file mode 100644 index 0000000..44ed170 --- /dev/null +++ b/src/models/music/getters/releases.ts @@ -0,0 +1,29 @@ +import request from "../../../request" + +type Arguments = { + keywords: String + user_id: String + limit: Number + offset: Number +} + +export default async ({ + keywords, + user_id, + limit, + offset, +}: Arguments) => { + const response = await request({ + method: "GET", + url: "/music/releases", + params: { + keywords: keywords, + user_id: user_id, + limit: limit, + offset: offset, + } + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/search.ts b/src/models/music/getters/search.ts new file mode 100644 index 0000000..e39b12f --- /dev/null +++ b/src/models/music/getters/search.ts @@ -0,0 +1,26 @@ +import request from "../../../request" + +type Arguments = { + keywords: String + limit: Number + offset: Number +} + +export default async ({ + keywords, + limit, + offset, +}: Arguments) => { + const response = await request({ + method: "GET", + url: "/music/search", + params: { + keywords: keywords, + limit: limit, + offset: offset, + } + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/trackData.ts b/src/models/music/getters/trackData.ts new file mode 100644 index 0000000..fdfe487 --- /dev/null +++ b/src/models/music/getters/trackData.ts @@ -0,0 +1,12 @@ +import request from "../../../request" + +export default async (id: String, options: Object) => { + const response = await request({ + method: "GET", + url: `/music/tracks/${id}/data`, + params: options + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/trackLyrics.ts b/src/models/music/getters/trackLyrics.ts new file mode 100644 index 0000000..57dc59e --- /dev/null +++ b/src/models/music/getters/trackLyrics.ts @@ -0,0 +1,31 @@ +import request from "../../../request" + +type RequestOptions = { + preferTranslation?: Boolean +} + +type RequestParams = { + translate_lang?: String +} + +export default async ( + id: String, + options: RequestOptions = { + preferTranslation: false, + } +) => { + const requestParams: RequestParams = Object() + + if (options.preferTranslation) { + requestParams.translate_lang = app.cores.settings.get("app:language") + } + + const response = await request({ + method: "GET", + url: `/music/lyrics/${id}`, + params: requestParams + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/getters/tracks.ts b/src/models/music/getters/tracks.ts new file mode 100644 index 0000000..cf12636 --- /dev/null +++ b/src/models/music/getters/tracks.ts @@ -0,0 +1,29 @@ +import request from "../../../request" + +type Arguments = { + keywords: String + user_id: String + limit: Number + offset: Number +} + +export default async ({ + keywords, + user_id, + limit, + offset, +}: Arguments) => { + const response = await request({ + method: "GET", + url: "/music/tracks", + params: { + keywords: keywords, + user_id: user_id, + limit: limit, + offset: offset, + } + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/index.js b/src/models/music/index.js index 92d4b60..12173e5 100755 --- a/src/models/music/index.js +++ b/src/models/music/index.js @@ -1,561 +1,151 @@ -import request from "../../request" -import pmap from "p-map" -import SyncModel from "../sync" +import Getters from "./getters" +import Setters from "./setters" export default class MusicModel { - static get api_instance() { - return globalThis.__comty_shared_state.instances["music"] - } + static Getters = Getters + static Setters = Setters /** - * Retrieves the official featured playlists. - * - * @return {Promise} The data containing the featured playlists. - */ - static async getFeaturedPlaylists() { - const response = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: "/featured/playlists", - }) - - return response.data - } + * Performs a search based on the provided keywords, with optional parameters for limiting the number of results and pagination. + * + * @param {string} keywords - The keywords to search for. + * @param {object} options - An optional object containing additional parameters. + * @param {number} options.limit - The maximum number of results to return. Defaults to 5. + * @param {number} options.offset - The offset to start returning results from. Defaults to 0. + * @param {boolean} options.useTidal - Whether to use Tidal for the search. Defaults to false. + * @return {Promise} The search results. + */ + static search = Getters.search /** - * Retrieves track data for a given ID. - * - * @param {string} id - The ID of the track. - * @return {Promise} The track data. - */ - static async getTrackData(id) { - const response = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/tracks/${id}/data`, - }) - - return response.data - } + * Retrieves playlist items based on the provided parameters. + * + * @param {Object} options - The options object. + * @param {string} options.playlist_id - The ID of the playlist. + * @param {string} options.service - The service from which to retrieve the playlist items. + * @param {number} options.limit - The maximum number of items to retrieve. + * @param {number} options.offset - The number of items to skip before retrieving. + * @return {Promise} Playlist items data. + */ + static getPlaylistItems = Getters.PlaylistItems /** - * Retrieves tracks data for the given track IDs. - * - * @param {Array} ids - An array of track IDs. - * @return {Promise} A promise that resolves to the tracks data. - */ - static async getTracksData(ids) { - const response = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/tracks/many`, - params: { - ids, - } - }) - - return response.data - } + * Retrieves playlist data based on the provided parameters. + * + * @param {Object} options - The options object. + * @param {string} options.playlist_id - The ID of the playlist. + * @param {string} options.service - The service to use. + * @param {number} options.limit - The maximum number of items to retrieve. + * @param {number} options.offset - The offset for pagination. + * @return {Promise} Playlist data. + */ + static getPlaylistData = Getters.PlaylistData /** - * Retrieves favorite tracks based on specified parameters. - * - * @param {Object} options - The options for retrieving favorite tracks. - * @param {boolean} options.useTidal - Whether to use Tidal for retrieving tracks. Defaults to false. - * @param {number} options.limit - The maximum number of tracks to retrieve. - * @param {number} options.offset - The offset from which to start retrieving tracks. - * @return {Promise} - An object containing the total length of the tracks and the retrieved tracks. - */ - static async getFavoriteTracks({ useTidal = false, limit, offset }) { - let result = [] - - let limitPerRequesters = limit - - if (useTidal) { - limitPerRequesters = limitPerRequesters / 2 - } - - const requesters = [ - async () => { - let { data } = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/tracks/liked`, - params: { - limit: limitPerRequesters, - offset, - }, - }) - - return data - }, - async () => { - if (!useTidal) { - return { - total_length: 0, - tracks: [], - } - } - - const tidalResult = await SyncModel.tidalCore.getMyFavoriteTracks({ - limit: limitPerRequesters, - offset, - }) - - return tidalResult - }, - ] - - result = await pmap( - requesters, - async requester => { - const data = await requester() - - return data - }, - { - concurrency: 3, - }, - ) - - let total_length = 0 - - result.forEach(result => { - total_length += result.total_length - }) - - let tracks = result.reduce((acc, cur) => { - return [...acc, ...cur.tracks] - }, []) - - tracks = tracks.sort((a, b) => { - return b.liked_at - a.liked_at - }) - - return { - total_length, - tracks, - } - } + * Retrieves releases based on the provided parameters. + * If user_id is not provided, it will retrieve self authenticated user releases. + * + * @param {object} options - The options for retrieving releases. + * @param {string} options.user_id - The ID of the user. + * @param {string[]} options.keywords - The keywords to filter releases by. + * @param {number} options.limit - The maximum number of releases to retrieve. + * @param {number} options.offset - The offset for paginated results. + * @return {Promise} - A promise that resolves to the retrieved releases. + */ + static getReleases = Getters.releases /** - * Retrieves favorite playlists based on the specified parameters. - * - * @param {Object} options - The options for retrieving favorite playlists. - * @param {number} options.limit - The maximum number of playlists to retrieve. Default is 50. - * @param {number} options.offset - The offset of playlists to retrieve. Default is 0. - * @param {Object} options.services - The services to include for retrieving playlists. Default is an empty object. - * @param {string} options.keywords - The keywords to filter playlists by. - * @return {Promise} - An object containing the total length of the playlists and the playlist items. - */ - static async getFavoritePlaylists({ limit = 50, offset = 0, services = {}, keywords } = {}) { - let result = [] - - let limitPerRequesters = limit - - const requesters = [ - async () => { - const { data } = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/playlists/self`, - params: { - keywords, - }, - }) - - return data - }, - ] - - if (services["tidal"] === true) { - limitPerRequesters = limitPerRequesters / (requesters.length + 1) - - requesters.push(async () => { - const _result = await SyncModel.tidalCore.getMyFavoritePlaylists({ - limit: limitPerRequesters, - offset, - }) - - return _result - }) - } - - result = await pmap( - requesters, - async requester => { - const data = await requester() - - return data - }, - { - concurrency: 3, - }, - ) - - // calculate total length - let total_length = 0 - - result.forEach(result => { - total_length += result.total_length - }) - - // reduce items - let items = result.reduce((acc, cur) => { - return [...acc, ...cur.items] - }, []) - - - // sort by created_at - items = items.sort((a, b) => { - return new Date(b.created_at) - new Date(a.created_at) - }) - - return { - total_length: total_length, - items, - } - } + * Retrieves self releases. + * + * @param {object} options - The options for retrieving my releases. + * @param {number} options.limit - The maximum number of releases to retrieve. + * @param {number} options.offset - The offset for paginated results. + * @return {Promise} - A promise that resolves to the retrieved releases. + */ + static getMyReleases = Getters.myReleases /** - * Retrieves playlist items based on the provided parameters. - * - * @param {Object} options - The options object. - * @param {string} options.playlist_id - The ID of the playlist. - * @param {string} options.service - The service from which to retrieve the playlist items. - * @param {number} options.limit - The maximum number of items to retrieve. - * @param {number} options.offset - The number of items to skip before retrieving. - * @return {Promise} Playlist items data. - */ - static async getPlaylistItems({ - playlist_id, - service, - - limit, - offset, - }) { - if (service === "tidal") { - const result = await SyncModel.tidalCore.getPlaylistItems({ - playlist_id, - - limit, - offset, - - resolve_items: true, - }) - - return result - } - - const { data } = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/playlists/${playlist_id}/items`, - params: { - limit, - offset, - } - }) - - return data - } + * Retrieves release data by ID. + * + * @param {number} id - The ID of the release. + * @return {Promise} The release data. + */ + static getReleaseData = Getters.releaseData /** - * Retrieves playlist data based on the provided parameters. - * - * @param {Object} options - The options object. - * @param {string} options.playlist_id - The ID of the playlist. - * @param {string} options.service - The service to use. - * @param {number} options.limit - The maximum number of items to retrieve. - * @param {number} options.offset - The offset for pagination. - * @return {Promise} Playlist data. - */ - static async getPlaylistData({ - playlist_id, - service, - - limit, - offset, - }) { - if (service === "tidal") { - const result = await SyncModel.tidalCore.getPlaylistData({ - playlist_id, - - limit, - offset, - - resolve_items: true, - }) - - return result - } - - const { data } = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/playlists/${playlist_id}/data`, - params: { - limit, - offset, - } - }) - - return data - } + * Retrieves track data for a given ID. + * + * @param {string} id - The ID of the track or multiple IDs separated by commas. + * @return {Promise} The track data. + */ + static getTrackData = Getters.trackData /** - * Performs a search based on the provided keywords, with optional parameters for limiting the number of results and pagination. - * - * @param {string} keywords - The keywords to search for. - * @param {object} options - An optional object containing additional parameters. - * @param {number} options.limit - The maximum number of results to return. Defaults to 5. - * @param {number} options.offset - The offset to start returning results from. Defaults to 0. - * @param {boolean} options.useTidal - Whether to use Tidal for the search. Defaults to false. - * @return {Promise} The search results. - */ - static async search(keywords, { limit = 5, offset = 0, useTidal = false }) { - const { data } = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/search`, - params: { - keywords, - limit, - offset, - useTidal, - }, - }) - - return data - } + * Retrieves the official featured playlists. + * + * @return {Promise} The data containing the featured playlists. + */ + static getFeaturedPlaylists = Getters.featuredPlaylists /** - * Creates a new playlist. - * - * @param {object} payload - The payload containing the data for the new playlist. - * @return {Promise} The new playlist data. - */ - static async newPlaylist(payload) { - const { data } = await request({ - instance: MusicModel.api_instance, - method: "POST", - url: `/playlists/new`, - data: payload, - }) + * Retrieves track lyrics for a given ID. + * + * @param {string} id - The ID of the track. + * @return {Promise} The track lyrics. + */ + static getTrackLyrics = Getters.trackLyrics - return data - } + + static putTrackLyrics = Setters.putTrackLyrics /** - * Updates a playlist item in the specified playlist. - * - * @param {string} playlist_id - The ID of the playlist to update. - * @param {object} item - The updated playlist item to be added. - * @return {Promise} - The updated playlist item. - */ - static async putPlaylistItem(playlist_id, item) { - const response = await request({ - instance: MusicModel.api_instance, - method: "PUT", - url: `/playlists/${playlist_id}/items`, - data: item, - }) - - return response.data - } + * Create or modify a track. + * + * @param {object} TrackManifest - The track manifest. + * @return {Promise} The result track data. + */ + static putTrack = Setters.putTrack /** - * Delete a playlist item. - * - * @param {string} playlist_id - The ID of the playlist. - * @param {string} item_id - The ID of the item to delete. - * @return {Promise} The data returned by the server after the item is deleted. - */ - static async deletePlaylistItem(playlist_id, item_id) { - const response = await request({ - instance: MusicModel.api_instance, - method: "DELETE", - url: `/playlists/${playlist_id}/items/${item_id}`, - }) - - return response.data - } + * Create or modify a release. + * + * @param {object} ReleaseManifest - The release manifest. + * @return {Promise} The result release data. + */ + static putRelease = Setters.putRelease /** - * Deletes a playlist. - * - * @param {number} playlist_id - The ID of the playlist to be deleted. - * @return {Promise} The response data from the server. - */ - static async deletePlaylist(playlist_id) { - const response = await request({ - instance: MusicModel.api_instance, - method: "DELETE", - url: `/playlists/${playlist_id}`, - }) - - return response.data - } + * Deletes a release by its ID. + * + * @param {string} id - The ID of the release to delete. + * @return {Promise} - A Promise that resolves to the data returned by the API. + */ + static deleteRelease = Setters.deleteRelease /** - * Execute a PUT request to update or create a release. - * - * @param {object} payload - The payload data. - * @return {Promise} The response data from the server. + * Retrieves the favourite tracks of the current user. + * + * @return {Promise} The favorite tracks data. */ - static async putRelease(payload) { - const response = await request({ - instance: MusicModel.api_instance, - method: "PUT", - url: `/releases/release`, - data: payload - }) - - return response.data - } - + static getFavouriteTracks = null /** - * Retrieves the releases associated with the authenticated user. - * - * @param {string} keywords - The keywords to filter the releases by. - * @return {Promise} A promise that resolves to the data of the releases. + * Retrieves the favourite tracks/playlists/releases of the current user. + * + * @return {Promise} The favorite playlists data. */ - static async getMyReleases(keywords) { - const response = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/releases/self`, - params: { - keywords, - } - }) - - return response.data - } - + static getFavouriteFolder = Getters.favouriteFolder /** - * Retrieves releases based on the provided parameters. - * - * @param {object} options - The options for retrieving releases. - * @param {string} options.user_id - The ID of the user. - * @param {string[]} options.keywords - The keywords to filter releases by. - * @param {number} options.limit - The maximum number of releases to retrieve. - * @param {number} options.offset - The offset for paginated results. - * @return {Promise} - A promise that resolves to the retrieved releases. + * Toggles the favourite status of a track, playlist or folder. + * + * @param {string} track_id - The ID of the track to toggle the favorite status. + * @throws {Error} If the track_id is not provided. + * @return {Promise} The response data after toggling the favorite status. */ - static async getReleases({ - user_id, - keywords, - limit = 50, - offset = 0, - }) { - const response = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/releases/user/${user_id}`, - params: { - keywords, - limit, - offset, - } - }) + static toggleItemFavourite = Setters.toggleItemFavourite - return response.data - } + static isItemFavourited = Getters.isItemFavourited - /** - * Retrieves release data by ID. - * - * @param {number} id - The ID of the release. - * @return {Promise} The release data. - */ - static async getReleaseData(id) { - const response = await request({ - instance: MusicModel.api_instance, - method: "GET", - url: `/releases/${id}/data` - }) - - return response.data - } - - /** - * Deletes a release by its ID. - * - * @param {string} id - The ID of the release to delete. - * @return {Promise} - A Promise that resolves to the data returned by the API. - */ - static async deleteRelease(id) { - const response = await request({ - instance: MusicModel.api_instance, - method: "DELETE", - url: `/releases/${id}` - }) - - return response.data - } - - /** - * Refreshes the track cache for a given track ID. - * - * @param {string} track_id - The ID of the track to refresh the cache for. - * @throws {Error} If track_id is not provided. - * @return {Promise} The response data from the API call. - */ - static async refreshTrackCache(track_id) { - if (!track_id) { - throw new Error("Track ID is required") - } - - const response = await request({ - instance: MusicModel.api_instance, - method: "POST", - url: `/tracks/${track_id}/refresh-cache`, - }) - - return response.data - } - - /** - * Toggles the like status of a track. - * - * @param {Object} manifest - The manifest object containing track information. - * @param {boolean} to - The like status to toggle (true for like, false for unlike). - * @throws {Error} Throws an error if the manifest is missing. - * @return {Object} The response data from the API. - */ - static async toggleTrackLike(manifest, to) { - if (!manifest) { - throw new Error("Manifest is required") - } - - console.log(`Toggling track ${manifest._id} like status to ${to}`) - - const track_id = manifest._id - - switch (manifest.service) { - case "tidal": { - const response = await SyncModel.tidalCore.toggleTrackLike({ - track_id, - to, - }) - - return response - } - default: { - const response = await request({ - instance: MusicModel.api_instance, - method: to ? "POST" : "DELETE", - url: `/tracks/${track_id}/like`, - params: { - service: manifest.service - } - }) - - return response.data - } - } - } -} + static getRecentyPlayed = Getters.recentlyPlayed +} \ No newline at end of file diff --git a/src/models/music/setters/deleteRelease.js b/src/models/music/setters/deleteRelease.js new file mode 100644 index 0000000..d4f61de --- /dev/null +++ b/src/models/music/setters/deleteRelease.js @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async (release_id) => { + const response = await request({ + method: "delete", + url: `/music/releases/${release_id}`, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/setters/index.js b/src/models/music/setters/index.js new file mode 100644 index 0000000..3700f30 --- /dev/null +++ b/src/models/music/setters/index.js @@ -0,0 +1,33 @@ +function exportObjs() { + if (typeof window !== "undefined") { + const paths = { + ...import.meta.glob("./**.ts", { eager: true, import: "default" }), + ...import.meta.glob("./**.js", { eager: true, import: "default" }), + } + + return Object.entries(paths).reduce((acc, [path, module]) => { + const name = path + .split("/") + .pop() + .replace(/\.(ts|js)$/, "") + acc[name] = module + return acc + }, {}) + } else { + const fs = require("fs") + const path = require("path") + + return fs + .readdirSync(__dirname) + .filter((file) => file !== "index.js" && /\.js$/.test(file)) + .reduce((acc, file) => { + const name = file.replace(/\.js$/, "") + acc[name] = require(path.join(__dirname, file)).default + return acc + }, {}) + } +} + +const exportedObjs = exportObjs() + +export default exportedObjs diff --git a/src/models/music/setters/putRelease.js b/src/models/music/setters/putRelease.js new file mode 100644 index 0000000..e7cc272 --- /dev/null +++ b/src/models/music/setters/putRelease.js @@ -0,0 +1,12 @@ +import request from "../../../request" + +export default async (release) => { + const response = await request({ + method: "PUT", + url: "/music/releases", + data: release, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/setters/putTrack.js b/src/models/music/setters/putTrack.js new file mode 100644 index 0000000..09ae6d8 --- /dev/null +++ b/src/models/music/setters/putTrack.js @@ -0,0 +1,12 @@ +import request from "../../../request" + +export default async (track) => { + const response = await request({ + method: "PUT", + url: "/music/tracks", + data: track, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/setters/putTrackLyrics.js b/src/models/music/setters/putTrackLyrics.js new file mode 100644 index 0000000..fdfc19d --- /dev/null +++ b/src/models/music/setters/putTrackLyrics.js @@ -0,0 +1,12 @@ +import request from "../../../request" + +export default async (track_id, data) => { + const response = await request({ + method: "put", + url: `/music/lyrics/${track_id}`, + data: data, + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/music/setters/toggleItemFavourite.js b/src/models/music/setters/toggleItemFavourite.js new file mode 100644 index 0000000..23d6584 --- /dev/null +++ b/src/models/music/setters/toggleItemFavourite.js @@ -0,0 +1,36 @@ +import request from "../../../request" + +const typeToNamespace = { + track: "tracks", + //playlist: "playlists", + //release: "releases", +} + +export default async (type, track_id, to) => { + if (!type) { + throw new Error("type is required") + } + + if (!track_id) { + throw new Error("track_id is required") + } + + type = type.toLowerCase() + + type = typeToNamespace[type] + + if (!type) { + throw new Error(`Unsupported type: ${type}`) + } + + const response = await request({ + method: "post", + url: `/music/${type}/${track_id}/favourite`, + data: { + to: to, + } + }) + + // @ts-ignore + return response.data +} \ No newline at end of file diff --git a/src/models/nfc/index.js b/src/models/nfc/index.js index e5a2093..85fb935 100755 --- a/src/models/nfc/index.js +++ b/src/models/nfc/index.js @@ -1,15 +1,26 @@ import request from "../../request" export default class NFCModel { + /** + * Retrieves the list of tags owned by the current user. + * + * @return {Promise} A promise that resolves with the data of the tags. + */ static async getOwnTags() { const { data } = await request({ method: "GET", - url: `/nfc/tags` + url: `/nfc/tag/my` }) return data } + /** + * Retrieves a tag by its ID. + * + * @param {type} id - The ID of the tag to retrieve. + * @return {type} The data of the retrieved tag. + */ static async getTagById(id) { if (!id) { throw new Error("ID is required") @@ -17,12 +28,19 @@ export default class NFCModel { const { data } = await request({ method: "GET", - url: `/nfc/tags/${id}` + url: `/nfc/tag/id/${id}` }) return data } + /** + * Retrieves a tag by its serial number. + * + * @param {string} serial - The serial number of the tag to retrieve. + * @throws {Error} If the serial number is not provided. + * @return {Promise} A promise that resolves with the data of the tag. + */ static async getTagBySerial(serial) { if (!serial) { throw new Error("Serial is required") @@ -36,6 +54,14 @@ export default class NFCModel { return data } + /** + * Registers a tag with the given serial number and payload. + * + * @param {string} serial - The serial number of the tag. + * @param {Object} payload - The payload data for the tag. + * @throws {Error} If the serial or payload is not provided. + * @return {Promise} The data of the registered tag. + */ static async registerTag(serial, payload) { if (!serial) { throw new Error("Serial is required") @@ -45,10 +71,27 @@ export default class NFCModel { throw new Error("Payload is required") } + if (window) { + payload.origin = window.location.host + } + const { data } = await request({ method: "POST", - url: `/nfc/tag/${serial}`, - data: payload + url: `/nfc/tag/register/${serial}`, + data: payload, + }) + + return data + } + + static async deleteTag(id) { + if (!id) { + throw new Error("ID is required") + } + + const { data } = await request({ + method: "DELETE", + url: `/nfc/tag/id/${id}` }) return data diff --git a/src/models/payments/index.js b/src/models/payments/index.js new file mode 100644 index 0000000..307b10c --- /dev/null +++ b/src/models/payments/index.js @@ -0,0 +1,17 @@ +import request from "../../request" + +export default class PaymentsModel { + /** + * Fetches the current balance from the server. + * + * @return {object} The balance data received from the server. + */ + static async fetchBalance() { + const response = await request({ + method: "GET", + url: "/payments/balance", + }) + + return response.data.balance + } +} \ No newline at end of file diff --git a/src/models/post/index.js b/src/models/post/index.js index 08003f9..8c6da95 100755 --- a/src/models/post/index.js +++ b/src/models/post/index.js @@ -2,15 +2,30 @@ import request from "../../request" import Settings from "../../helpers/withSettings" export default class Post { + /** + * Retrieves the maximum length allowed for the post text. + * + * @return {number} The maximum length allowed for the post text. + */ static get maxPostTextLength() { return 3200 } + /** + * Returns the maximum length allowed for a comment. + * + * @return {number} The maximum length allowed for a comment. + */ static get maxCommentLength() { return 1200 } - static getPostingPolicy = async () => { + /** + * Retrieves the posting policy from the server. + * + * @return {Promise} The posting policy data. + */ + static async getPostingPolicy() { const { data } = await request({ method: "GET", url: "/posting_policy", @@ -19,7 +34,15 @@ export default class Post { return data } - static getPost = async ({ post_id }) => { + /** + * Retrieves the data of a post by its ID. + * + * @param {Object} options - The options for retrieving the post. + * @param {string} options.post_id - The ID of the post to retrieve. + * @throws {Error} If the post_id is not provided. + * @return {Promise} The data of the post. + */ + static async post({ post_id }) { if (!post_id) { throw new Error("Post ID is required") } @@ -32,6 +55,18 @@ export default class Post { return data } + static getPost = Post.post + + /** + * Retrieves the replies of a post by its ID. + * + * @param {Object} options - The options for retrieving the replies. + * @param {string} options.post_id - The ID of the post to retrieve replies for. + * @param {number} [options.trim=0] - The number of characters to trim the reply content. + * @param {number} [options.limit=Settings.get("feed_max_fetch")] - The maximum number of replies to fetch. + * @throws {Error} If the post_id is not provided. + * @return {Promise} The data of the replies. + */ static async replies({ post_id, trim, limit }) { if (!post_id) { throw new Error("Post ID is required") @@ -49,7 +84,15 @@ export default class Post { return data } - static getSavedPosts = async ({ trim, limit }) => { + /** + * Retrieves the saved posts with optional trimming and limiting. + * + * @param {Object} options - The options for retrieving the saved posts. + * @param {number} [options.trim=0] - The number of posts to trim from the result. + * @param {number} [options.limit=Settings.get("feed_max_fetch")] - The maximum number of posts to fetch. + * @return {Promise} The data of the saved posts. + */ + static async getSavedPosts({ trim, limit }) { const { data } = await request({ method: "GET", url: `/posts/saved`, @@ -62,7 +105,14 @@ export default class Post { return data } - static getLikedPosts = async ({ trim, limit }) => { + /** + * Retrieves the liked posts with optional trimming and limiting. + * + * @param {number} trim - The number of characters to trim the post content. + * @param {number} limit - The maximum number of liked posts to fetch. + * @return {Promise} The data of the liked posts. + */ + static async getLikedPosts({ trim, limit }) { const { data } = await request({ method: "GET", url: `/posts/liked`, @@ -75,7 +125,16 @@ export default class Post { return data } - static getUserPosts = async ({ user_id, trim, limit }) => { + /** + * Retrieves the posts of a user with optional trimming and limiting. + * + * @param {Object} options - The options for retrieving the user's posts. + * @param {string} options.user_id - The ID of the user whose posts to retrieve. If not provided, the current user's ID will be used. + * @param {number} [options.trim=0] - The number of characters to trim the post content. + * @param {number} [options.limit=Settings.get("feed_max_fetch")] - The maximum number of posts to fetch. + * @return {Promise} The data of the user's posts. + */ + static async getUserPosts({ user_id, trim, limit }) { if (!user_id) { // use current user_id user_id = app.userData?._id @@ -93,7 +152,15 @@ export default class Post { return data } - static toggleLike = async ({ post_id }) => { + /** + * Toggles the like status of a post. + * + * @param {Object} options - The options for toggling the like status. + * @param {string} options.post_id - The ID of the post to toggle the like status. + * @throws {Error} If the post_id is not provided. + * @return {Promise} The response data after toggling the like status. + */ + static async toggleLike({ post_id }) { if (!post_id) { throw new Error("Post ID is required") } @@ -106,7 +173,13 @@ export default class Post { return data } - static toggleSave = async ({ post_id }) => { + /** + * Toggles the save status of a post. + * + * @param {string} post_id - The ID of the post to toggle the save status. + * @return {Promise} The response data after toggling the save status. + */ + static async toggleSave({ post_id }) { if (!post_id) { throw new Error("Post ID is required") } @@ -119,7 +192,13 @@ export default class Post { return data } - static create = async (payload) => { + /** + * Creates a new post with the given payload. + * + * @param {Object} payload - The data to create the post with. + * @return {Promise} The response data after creating the post. + */ + static async create(payload) { const { data } = await request({ method: "POST", url: `/posts/new`, @@ -129,7 +208,17 @@ export default class Post { return data } - static update = async (post_id, update) => { + static createPost = Post.create + + /** + * Updates a post with the given post ID and update payload. + * + * @param {string} post_id - The ID of the post to update. + * @param {Object} update - The data to update the post with. + * @throws {Error} If the post_id is not provided. + * @return {Promise} The response data after updating the post. + */ + static async update(post_id, update) { if (!post_id) { throw new Error("Post ID is required") } @@ -143,7 +232,15 @@ export default class Post { return data } - static deletePost = async ({ post_id }) => { + static updatePost = Post.update + + /** + * Deletes a post with the given post ID. + * + * @param {string} post_id - The ID of the post to delete. + * @return {Object} The response data after deleting the post. + */ + static async delete({ post_id }) { if (!post_id) { throw new Error("Post ID is required") } @@ -155,4 +252,94 @@ export default class Post { return data } + + static deletePost = Post.delete + + /** + * Votes for a poll with the given post ID and option ID. + * + * @param {Object} options - The options for voting. + * @param {string} options.post_id - The ID of the post to vote for. + * @param {string} options.option_id - The ID of the option to vote for. + * @throws {Error} If the post_id or option_id is not provided. + * @return {Promise} The response data after voting. + */ + static async votePoll({ post_id, option_id }) { + if (!post_id) { + throw new Error("post_id is required") + } + + if (!option_id) { + throw new Error("option_id is required") + } + + const { data } = await request({ + method: "POST", + url: `/posts/${post_id}/vote_poll/${option_id}`, + }) + + return data + } + + /** + * Deletes a vote for a poll with the given post ID and option ID. + * + * @param {Object} options - The options for deleting a vote. + * @param {string} options.post_id - The ID of the post to delete the vote from. + * @param {string} options.option_id - The ID of the option to delete the vote from. + * @throws {Error} If the post_id or option_id is not provided. + * @return {Promise} The response data after deleting the vote. + */ + static async deleteVotePoll({ post_id, option_id }) { + if (!post_id) { + throw new Error("post_id is required") + } + + if (!option_id) { + throw new Error("option_id is required") + } + + const { data } = await request({ + method: "DELETE", + url: `/posts/${post_id}/vote_poll/${option_id}`, + }) + + return data + } + + /** + * Retrieves the trending hashtags and their counts. + * + * @return {Promise} An array of objects with two properties: "hashtag" and "count". + */ + static async getTrendings() { + const { data } = await request({ + method: "GET", + url: `/posts/trendings`, + }) + + return data + } + + /** + * Retrieves the trending posts for a specific hashtag with optional trimming and limiting. + * + * @param {Object} options - The options for retrieving trending posts. + * @param {string} options.trending - The hashtag to retrieve trending posts for. + * @param {number} [options.trim=0] - The number of characters to trim the post content. + * @param {number} [options.limit=Settings.get("feed_max_fetch")] - The maximum number of posts to fetch. + * @return {Promise} An array of posts that are trending for the given hashtag. + */ + static async getTrending({ trending, trim, limit } = {}) { + const { data } = await request({ + method: "GET", + url: `/posts/trending/${trending}`, + params: { + trim: trim ?? 0, + limit: limit ?? Settings.get("feed_max_fetch"), + } + }) + + return data + } } \ No newline at end of file diff --git a/src/models/radio/getters/list.js b/src/models/radio/getters/list.js new file mode 100644 index 0000000..b12a711 --- /dev/null +++ b/src/models/radio/getters/list.js @@ -0,0 +1,11 @@ +import request from "../../../request" + +export default async ({ limit = 50, offset = 0 } = {}) => { + const { data } = await request({ + method: "GET", + url: "/music/radio/list", + params: { limit, offset }, + }) + + return data +} diff --git a/src/models/radio/index.js b/src/models/radio/index.js new file mode 100644 index 0000000..fe04939 --- /dev/null +++ b/src/models/radio/index.js @@ -0,0 +1,5 @@ +import getRadioList from "./getters/list" + +export default class Radio { + static getRadioList = getRadioList +} diff --git a/src/models/search/index.js b/src/models/search/index.js index ac91693..f22390f 100755 --- a/src/models/search/index.js +++ b/src/models/search/index.js @@ -1,26 +1,58 @@ import request from "../../request" +import processAddons from "../../helpers/processWithAddons" +import standartListMerge from "../../utils/standartListMerge" export default class Search { - static search = async (keywords, params = {}) => { - const { data } = await request({ - method: "GET", - url: `/search`, - params: { - keywords: keywords, - params: params - } - }) + /** + * Performs a search using the provided keywords and optional parameters. + * + * @description + * This method is used to perform a search using the provided keywords and optional parameters. + * Additionally, supports for external addons to extend the search functionality. + * + * @param {string} keywords - The keywords to search for. + * @param {Object} [params={}] - Optional parameters for the search. + * @param {Array} [returnFields=[]] - An array of fields to return in the results. If empty, all fields will be returned. + * @return {Promise} A promise that resolves with the search results. + */ + static async search(keywords, params = {}, returnFields) { + let { limit = 50, offset = 0, sort = "desc" } = params - return data - } + const { data } = await request({ + method: "GET", + url: `/search`, + params: { + keywords: keywords, + limit: limit, + offset: offset, + sort: sort, + fields: params.fields, + }, + }) - static async quickSearch(params) { - const response = await request({ - method: "GET", - url: "/search/quick", - params: params - }) + let results = await processAddons({ + operation: "search", + initialData: data, + fnArguments: [ + keywords, + { + limit: limit, + offset: offset, + sort: sort, + }, + ], + normalizeAddonResult: ({ currentData, addonResult }) => { + return standartListMerge(currentData, addonResult) + }, + }) - return response.data - } -} \ No newline at end of file + if (Array.isArray(returnFields)) { + return Array.from(new Set(returnFields)).reduce((acc, field) => { + acc[field] = results[field] + return acc + }, {}) + } + + return results + } +} diff --git a/src/models/session/index.js b/src/models/session/index.js index 961a864..c15fbbf 100755 --- a/src/models/session/index.js +++ b/src/models/session/index.js @@ -1,41 +1,102 @@ -import jwt_decode from "jwt-decode" +import { jwtDecode } from "jwt-decode" import request from "../../request" import Storage from "../../helpers/withStorage" export default class Session { static storageTokenKey = "token" + static storageRefreshTokenKey = "refreshToken" + /** + * Retrieves the token from the storage engine. + * + * @return {type} description of return value + */ static get token() { return Storage.engine.get(this.storageTokenKey) } + /** + * Sets the token in the storage engine. + * + * @param {string} token - The token to be set. + * @return {Promise} A promise that resolves when the token is successfully set. + */ static set token(token) { return Storage.engine.set(this.storageTokenKey, token) } + /** + * Retrieves the refresh token from the storage engine. + * + * @return {string} The refresh token stored in the storage engine. + */ + static get refreshToken() { + return Storage.engine.get(this.storageRefreshTokenKey) + } + + /** + * Sets the refresh token in the storage engine. + * + * @param {string} token - The refresh token to be set. + * @return {Promise} A promise that resolves when the refresh token is successfully set. + */ + static set refreshToken(token) { + return Storage.engine.set(this.storageRefreshTokenKey, token) + } + + /** + * Retrieves the roles from the decoded token object. + * + * @return {Array|undefined} The roles if they exist, otherwise undefined. + */ static get roles() { return this.getDecodedToken()?.roles } + /** + * Retrieves the user ID from the decoded token object. + * + * @return {string|undefined} The user ID if it exists, otherwise undefined. + */ static get user_id() { return this.getDecodedToken()?.user_id } + /** + * Retrieves the session UUID from the decoded token object. + * + * @return {string} The session UUID if it exists, otherwise undefined. + */ static get session_uuid() { return this.getDecodedToken()?.session_uuid } - static getDecodedToken = () => { + /** + * Retrieves the decoded token from the session storage. + * + * @return {Object|null} The decoded token object if it exists, otherwise null. + */ + static getDecodedToken() { const token = this.token - return token && jwt_decode(token) + return token && jwtDecode(token) } + /** + * Removes the token from the storage engine. + * + * @return {Promise} A promise that resolves when the token is successfully removed. + */ static removeToken() { return Storage.engine.remove(Session.storageTokenKey) } - static getAllSessions = async () => { + /** + * Retrieves all sessions from the server. + * + * @return {Promise} The data of all sessions. + */ + static async getAllSessions() { const response = await request({ method: "get", url: "/sessions/all" @@ -44,7 +105,12 @@ export default class Session { return response.data } - static getCurrentSession = async () => { + /** + * Retrieves the current session from the server. + * + * @return {Promise} The data of the current session. + */ + static async getCurrentSession() { const response = await request({ method: "get", url: "/sessions/current" @@ -53,21 +119,12 @@ export default class Session { return response.data } - static getTokenValidation = async () => { - const session = await Session.token - - const response = await request({ - method: "get", - url: "/sessions/validate", - data: { - session: session - } - }) - - return response.data - } - - static destroyCurrentSession = async () => { + /** + * Destroys the current session by deleting it from the server. + * + * @return {Promise} The response data from the server after deleting the session. + */ + static async destroyCurrentSession() { const token = await Session.token const session = await Session.getDecodedToken() @@ -91,16 +148,16 @@ export default class Session { return response.data } - static destroyAllSessions = async () => { + static async destroyAllSessions() { throw new Error("Not implemented") } - // alias for validateToken method - static validSession = async (token) => { - return await Session.validateToken(token) - } - - static isCurrentTokenValid = async () => { + /** + * Retrieves the validity of the current token. + * + * @return {boolean} The validity status of the current token. + */ + static async isCurrentTokenValid() { const health = await Session.getTokenValidation() return health.valid diff --git a/src/models/spectrum/index.js b/src/models/spectrum/index.js new file mode 100644 index 0000000..c387093 --- /dev/null +++ b/src/models/spectrum/index.js @@ -0,0 +1,136 @@ +import axios from "axios" +import SessionService from "../session" +//import User from "comty.js/models/user" + +async function injectUserData(list) { + if (!Array.isArray(list)) { + return list + } + + const user_ids = list.map((item) => { + return item.user_id + }) + + //const users = await User.data(user_ids.join(",")) + + return list +} + +export default class Streaming { + static apiHostname = "https://live.ragestudio.net" + + static get base() { + const baseInstance = axios.create({ + baseURL: Streaming.apiHostname, + headers: { + Accept: "application/json", + "ngrok-skip-browser-warning": "any", + }, + }) + + if (SessionService.token) { + baseInstance.defaults.headers.common["Authorization"] = + `Bearer ${SessionService.token}` + } + + return baseInstance + } + + static async serverInfo() { + const { data } = await Streaming.base({ + method: "get", + }) + + return { + ...data, + hostname: Streaming.apiHostname, + } + } + + static async getOwnProfiles() { + const { data } = await Streaming.base({ + method: "get", + url: "/streaming/profiles/self", + }) + + return data + } + + static async getProfile({ profile_id }) { + if (!profile_id) { + return null + } + + const { data } = await Streaming.base({ + method: "get", + url: `/streaming/profiles/${profile_id}`, + }) + + return data + } + + static async getStream({ profile_id }) { + if (!profile_id) { + return null + } + + const { data } = await Streaming.base({ + method: "get", + url: `/streaming/${profile_id}`, + }) + + return data + } + + static async deleteProfile({ profile_id }) { + if (!profile_id) { + return null + } + + const { data } = await Streaming.base({ + method: "delete", + url: `/streaming/profiles/${profile_id}`, + }) + + return data + } + + static async createOrUpdateStream(update) { + const { data } = await Streaming.base({ + method: "put", + url: `/streaming/profiles/self`, + data: update, + }) + + return data + } + + static async getConnectionStatus({ profile_id }) { + console.warn("getConnectionStatus() | Not implemented") + return false + } + + static async getLivestreamsList({ limit, offset } = {}) { + let { data } = await Streaming.base({ + method: "get", + url: "/streaming/list", + params: { + limit, + offset, + }, + }) + + data = await injectUserData(data) + + return data + } + + static async getLivestreamData(livestream_id) { + const { data } = await Streaming.base({ + method: "get", + url: `/streaming/${livestream_id}`, + }) + + return data + } +} diff --git a/src/models/user/index.js b/src/models/user/index.js index eb035e4..1b387e6 100755 --- a/src/models/user/index.js +++ b/src/models/user/index.js @@ -2,174 +2,134 @@ import SessionModel from "../session" import request from "../../request" export default class User { - static data = async (payload = {}) => { - let { - username, - user_id, - } = payload + /** + * Retrieves the data of a user. + * + * @param {Object} payload - An object containing the username and user_id. + * @param {string} payload.username - The username of the user. + * @param {string} payload.user_id - The ID of the user. + * @return {Promise} - A promise that resolves with the data of the user. + */ + static async data(payload = {}) { + let { username, user_id, basic = false } = payload - if (!username && !user_id) { - user_id = SessionModel.user_id - } + if (!username && !user_id) { + user_id = SessionModel.user_id + } - if (username && !user_id) { - // resolve user_id from username - const resolveResponse = await request({ - method: "GET", - url: `/users/${username}/resolve-user_id`, - }) + if (username && !user_id) { + // resolve user_id from username + const resolveResponse = await request({ + method: "GET", + url: `/users/${username}/resolve-user_id`, + }) - user_id = resolveResponse.data.user_id - } + user_id = resolveResponse.data.user_id + } - const response = await request({ - method: "GET", - url: `/users/${user_id}/data`, - }) + const response = await request({ + method: "GET", + url: `/users/${user_id}/data`, + params: { + basic, + }, + }) - return response.data - } + return response.data + } - static updateData = async (payload) => { - const response = await request({ - method: "POST", - url: "/users/self/update", - data: { - update: payload, - }, - }) + /** + * Updates the user data with the given payload. + * + * @param {Object} payload - The data to update the user with. + * @return {Promise} - A promise that resolves with the updated user data. + */ + static async updateData(payload) { + const response = await request({ + method: "POST", + url: "/users/self/update", + data: payload, + }) - return response.data - } + return response.data + } - static unsetFullName = async () => { - return await User.updateData({ - full_name: null, - }) - } + /** + * Update the public name to null in the user data. + * + * @return {Promise} A Promise that resolves with the response data after updating the public name + */ + static async unsetPublicName() { + return await User.updateData({ + public_name: null, + }) + } - static selfRoles = async () => { - const response = await request({ - method: "GET", - url: "/users/self/roles", - }) + /** + * Retrieves the roles of a user. + * + * @param {string} user_id - The ID of the user. If not provided, the current user ID will be used. + * @return {Promise} An array of roles for the user. + */ + static async getRoles(user_id) { + const response = await request({ + method: "GET", + url: `/users/${user_id ?? "self"}/roles`, + }) - return response.data - } + return response.data + } - static haveRole = async (role) => { - const roles = await User.selfRoles() + /** + * Retrieves the badges for a given user. + * + * @param {string} user_id - The ID of the user. If not provided, the current session user ID will be used. + * @return {Promise} An array of badges for the user. + */ + static async getBadges(user_id) { + if (!user_id) { + user_id = SessionModel.user_id + } - if (!roles) { - return false - } + const { data } = await request({ + method: "GET", + url: `/users/${user_id}/badges`, + }) - return Array.isArray(roles) && roles.includes(role) - } + return data + } - static haveAdmin = async () => { - return User.haveRole("admin") - } + /** + * 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, + }, + }) - static getUserBadges = async (user_id) => { - if (!user_id) { - user_id = SessionModel.user_id - } + return data + } - const { data } = await request({ - method: "GET", - url: `/users/${user_id}/badges`, - }) + /** + * 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 - } - - static getUserFollowers = async ({ - user_id, - limit = 20, - offset = 0, - }) => { - // if user_id or username is not provided, set with current user - if (!user_id && !username) { - user_id = SessionModel.user_id - } - - const { data } = await request({ - method: "GET", - url: `/user/${user_id}/followers`, - params: { - limit, - offset, - } - }) - - return data - } - - static getConnectedUsersFollowing = async () => { - const { data } = await request({ - method: "GET", - url: "/status/connected/following", - }) - - return data - } - - static checkUsernameAvailability = async (username) => { - const { data } = await request({ - method: "GET", - url: `/user/username_available`, - params: { - username, - } - }) - - return data - } - - static checkEmailAvailability = async (email) => { - const { data } = await request({ - method: "GET", - url: `/user/email_available`, - params: { - email, - } - }) - - 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 + return data + } +} diff --git a/src/models/widget/index.js b/src/models/widget/index.js deleted file mode 100755 index 5cfb937..0000000 --- a/src/models/widget/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import request from "../../request" - -export default class WidgetModel { - static browse = async ({ limit, offset, keywords } = {}) => { - const response = await request({ - instance: globalThis.__comty_shared_state.instances["marketplace"], - method: "GET", - url: "/widgets", - params: { - limit, - offset, - keywords: JSON.stringify(keywords), - }, - }) - - return response.data - } -} \ No newline at end of file diff --git a/src/remote.js b/src/remote.js deleted file mode 100755 index 9303785..0000000 --- a/src/remote.js +++ /dev/null @@ -1,31 +0,0 @@ -function getCurrentHostname() { - if (typeof window === "undefined") { - return "localhost" - } - - return window?.location?.hostname ?? "localhost" -} - -const envOrigins = { - "development": `http://${getCurrentHostname()}:9000`, - "indev": "https://indev_api.comty.app", - "production": "https://api.comty.app", -} - -export default { - 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/remotes.js b/src/remotes.js new file mode 100755 index 0000000..674257a --- /dev/null +++ b/src/remotes.js @@ -0,0 +1,36 @@ +const envOrigins = { + development: `https://fr01.ragestudio.net:9000`, //`${location.origin}/api`, + indev: "https://indev.comty.app/api", + production: "https://api.comty.app", +} + +export default { + origin: envOrigins[process.env.NODE_ENV ?? "production"], + websockets: [ + { + namespace: "posts", + path: "/posts", + ng: true, + }, + { + namespace: "main", + path: "/main", + }, + { + namespace: "notifications", + path: "/notifications", + }, + { + namespace: "chats", + path: "/chats", + }, + { + namespace: "music", + path: "/music", + }, + // { + // namespace: "payments", + // path: "/payments", + // } + ], +} diff --git a/src/utils/generateRequest.ts b/src/utils/generateRequest.ts new file mode 100644 index 0000000..3f62110 --- /dev/null +++ b/src/utils/generateRequest.ts @@ -0,0 +1,52 @@ +import request from "../request" + +// create a regex to detect params with %% symbol, from the url +const paramMatchRegex = /(%[0-9a-f]{2}%)/g + +export default (method: string = "GET", url: string = "/", params?: object, data?: object) => { + return async function generatedRequest(arg0: any, arg1: any) { + const requestObj = { + method: method, + url: url, + params: params, + data: data, + } + + // search url for params + // example: /namespace/search/[0]/data => /namespace/search/${arguments[0]}/data + // if no url matches, merge params with arg0 and override data in requestObj + if (url.match(paramMatchRegex)) { + requestObj.url = url.replace(paramMatchRegex, (match) => { + console.log(match) + + // replace with arguments + const fnArgumentIndex = "" + + return match + }) + } else { + requestObj.params = { + ...requestObj.params, + ...arg0 + } + requestObj.data = { + ...requestObj.data, + ...arg1 + } + } + + if (typeof requestObj.params === "object" && requestObj.params) { + Object.keys(requestObj.params).forEach((key) => { + if (requestObj.params && typeof requestObj.params[key] === "string") { + + } + }) + } + + + const response = await request(requestObj) + + // @ts-ignore + return response.data + } +} \ No newline at end of file diff --git a/src/utils/importFrom.js b/src/utils/importFrom.js new file mode 100644 index 0000000..0069a25 --- /dev/null +++ b/src/utils/importFrom.js @@ -0,0 +1,22 @@ +async function importFilesFrom(from) { + let paths = { + // @ts-ignore + ...import.meta.glob(`${from}/**.ts`), + // @ts-ignore + ...import.meta.glob(`${from}/**.js`), + } + + let fns = {} + + for (const path in paths) { + // @ts-ignore + const name = path.split("/").pop().replace(".ts", "").replace(".js", "") + const fn = await paths[path]() + + fns[name] = fn.default + } + + return fns +} + +export default importFilesFrom \ No newline at end of file diff --git a/src/utils/measurePing.js b/src/utils/measurePing.js new file mode 100755 index 0000000..90ec5e2 --- /dev/null +++ b/src/utils/measurePing.js @@ -0,0 +1,63 @@ +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.ws.sockets + .get("main") + .once("pong", () => { + failTimeout && clearTimeout(failTimeout) + + resolve(Date.now() - start) + }) + + globalThis.__comty_shared_state.ws.sockets.get("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 +} diff --git a/src/utils/standartListMerge.js b/src/utils/standartListMerge.js new file mode 100644 index 0000000..7554204 --- /dev/null +++ b/src/utils/standartListMerge.js @@ -0,0 +1,18 @@ +export default (base, obj) => { + const validGroups = Object.keys(obj).filter( + (key) => Array.isArray(obj[key]?.items) && obj[key].items.length > 0, + ) + + return validGroups.reduce( + (acc, group) => { + if (!acc[group]) { + acc[group] = { items: [], total_items: 0 } + } + + acc[group].items = [...acc[group].items, ...obj[group].items] + acc[group].total_items += obj[group].total_items || 0 + return acc + }, + { ...base }, + ) +} diff --git a/src/ws.js b/src/ws.js new file mode 100644 index 0000000..48c558b --- /dev/null +++ b/src/ws.js @@ -0,0 +1,145 @@ +import Remotes from "./remotes" +import Storage from "./helpers/withStorage" + +import { io } from "socket.io-client" +import { RTEngineClient } from "linebridge-client/src" +//import { RTEngineClient } from "../../linebridge/client/src" + +class WebsocketManager { + sockets = new Map() + + async connect(remote) { + let opts = { + transports: ["websocket"], + autoConnect: remote.autoConnect ?? true, + forceNew: true, + path: remote.path, + ...(remote.params ?? {}), + } + + if (remote.noAuth !== true) { + opts.auth = { + token: Storage.engine.get("token"), + } + } + + const socket = io(Remotes.origin, opts) + + socket.on("connect", () => { + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:connected`, + ) + }) + + socket.on("disconnect", () => { + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:disconnected`, + ) + }) + + socket.on("error", (error) => { + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:error`, + error, + ) + }) + + this.sockets.set(remote.namespace, socket) + + return socket + } + + async connectNg(remote) { + console.warn( + `Creating experimental socket client, some features may not work as expected:`, + remote, + ) + + const client = new RTEngineClient({ + refName: remote.namespace, + url: `${Remotes.origin}/${remote.namespace}`, + token: Storage.engine.get("token"), + }) + + client.on("connect", () => { + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:connected`, + ) + }) + + client.on("disconnect", () => { + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:disconnected`, + ) + }) + + client.on("error", (error) => { + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:error`, + error, + ) + }) + + await client.connect() + + this.sockets.set(remote.namespace, client) + + return client + } + + async disconnect(key) { + const socket = this.sockets.get(key) + + if (!socket) { + return null + } + + if ( + socket.connected === true && + typeof socket.disconnect === "function" + ) { + await socket.disconnect() + } + + if (typeof socket.removeAllListeners === "function") { + await socket.removeAllListeners() + } + + this.sockets.delete(key) + } + + async connectAll() { + if (this.sockets.size > 0) { + await this.disconnectAll() + } + + for await (const remote of Remotes.websockets) { + try { + if (remote.ng === true) { + await this.connectNg(remote) + } else { + await this.connect(remote) + } + } catch (error) { + console.error( + `Failed to connect to [${remote.namespace}]:`, + error, + ) + globalThis.__comty_shared_state.eventBus.emit( + `wsmanager:${remote.namespace}:error`, + error, + ) + } + } + + globalThis.__comty_shared_state.eventBus.emit("wsmanager:all:connected") + } + + async disconnectAll() { + for (const [key, socket] of this.sockets) { + await this.disconnect(key) + } + } +} + +export default WebsocketManager