mirror of
https://github.com/ragestudio/comty.js.git
synced 2025-06-09 02:24:18 +00:00
init
This commit is contained in:
commit
b7528857c0
58
src/handlers/measurePing.js
Executable file
58
src/handlers/measurePing.js
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
import request from "./request"
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const timings = {}
|
||||||
|
|
||||||
|
const promises = [
|
||||||
|
new Promise(async (resolve) => {
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
const failTimeout = setTimeout(() => {
|
||||||
|
timings.http = "failed"
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/ping",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// set http timing in ms
|
||||||
|
timings.http = Date.now() - start
|
||||||
|
|
||||||
|
failTimeout && clearTimeout(failTimeout)
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
timings.http = "failed"
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
const failTimeout = setTimeout(() => {
|
||||||
|
timings.ws = "failed"
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
__comty_shared_state.wsInstances["default"].on("pong", () => {
|
||||||
|
timings.ws = Date.now() - start
|
||||||
|
|
||||||
|
failTimeout && clearTimeout(failTimeout)
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
__comty_shared_state.wsInstances["default"].emit("ping")
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
return timings
|
||||||
|
}
|
51
src/handlers/request.js
Executable file
51
src/handlers/request.js
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
import handleBeforeRequest from "../helpers/handleBeforeRequest"
|
||||||
|
import handleAfterRequest from "../helpers/handleAfterRequest"
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
request = {
|
||||||
|
method: "GET",
|
||||||
|
},
|
||||||
|
...args
|
||||||
|
) => {
|
||||||
|
const instance = request.instance ?? __comty_shared_state.instances.default
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error("No instance provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle before request
|
||||||
|
await handleBeforeRequest(request)
|
||||||
|
|
||||||
|
if (typeof request === "string") {
|
||||||
|
request = {
|
||||||
|
url: request,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof request.headers !== "object") {
|
||||||
|
request.headers = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = null
|
||||||
|
|
||||||
|
const makeRequest = async () => {
|
||||||
|
const _result = await instance(request, ...args)
|
||||||
|
.catch((error) => {
|
||||||
|
return error
|
||||||
|
})
|
||||||
|
|
||||||
|
result = _result
|
||||||
|
}
|
||||||
|
|
||||||
|
await makeRequest()
|
||||||
|
|
||||||
|
// handle after request
|
||||||
|
await handleAfterRequest(result, makeRequest)
|
||||||
|
|
||||||
|
// if error, throw it
|
||||||
|
if (result instanceof Error) {
|
||||||
|
throw result
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
34
src/helpers/handleAfterRequest.js
Executable file
34
src/helpers/handleAfterRequest.js
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
import handleRegenerationEvent from "./handleRegenerationEvent"
|
||||||
|
|
||||||
|
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`)
|
||||||
|
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`)
|
||||||
|
|
||||||
|
// handle regeneration event
|
||||||
|
await handleRegenerationEvent(data.response.data.refreshToken)
|
||||||
|
|
||||||
|
return await callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
src/helpers/handleBeforeRequest.js
Executable file
13
src/helpers/handleBeforeRequest.js
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
export default async (request) => {
|
||||||
|
if (__comty_shared_state.onExpiredExceptionEvent) {
|
||||||
|
if (__comty_shared_state.excludedExpiredExceptionURL.includes(request.url)) return
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
__comty_shared_state.eventBus.once("session.regenerated", () => {
|
||||||
|
console.log(`Session has been regenerated, retrying request`)
|
||||||
|
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
43
src/helpers/handleRegenerationEvent.js
Executable file
43
src/helpers/handleRegenerationEvent.js
Executable file
@ -0,0 +1,43 @@
|
|||||||
|
import SessionModel from "../models/session"
|
||||||
|
import request from "../handlers/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()
|
||||||
|
}
|
25
src/helpers/withSettings.js
Executable file
25
src/helpers/withSettings.js
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
export default class Settings {
|
||||||
|
static get = (key) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return window?.app?.cores?.settings.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static set = (key, value) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return window?.app?.cores?.settings.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
static is = (key) => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return window?.app?.cores?.settings.is(key)
|
||||||
|
}
|
||||||
|
}
|
31
src/helpers/withStorage.js
Executable file
31
src/helpers/withStorage.js
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
import jscookies from "js-cookie"
|
||||||
|
|
||||||
|
class InternalStorage {
|
||||||
|
#storage = {}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
// get value from storage
|
||||||
|
return this.#storage[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value) {
|
||||||
|
// storage securely in memory
|
||||||
|
return this.#storage[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Storage {
|
||||||
|
static get engine() {
|
||||||
|
// check if is running in browser, if is import js-cookie
|
||||||
|
// else use in-memory safe storage
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return jscookies
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.__comty_shared_state["_internal_storage"]) {
|
||||||
|
globalThis.__comty_shared_state["_internal_storage"] = new InternalStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalThis.__comty_shared_state["_internal_storage"]
|
||||||
|
}
|
||||||
|
}
|
32
src/hooks/useRequest/index.js
Executable file
32
src/hooks/useRequest/index.js
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
const makeRequest = (...newArgs) => {
|
||||||
|
method(...newArgs)
|
||||||
|
.then((data) => {
|
||||||
|
setResult(data)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setError(err)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
makeRequest(...args)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return [loading, result, error, (...newArgs) => {
|
||||||
|
setLoading(true)
|
||||||
|
makeRequest(...newArgs)
|
||||||
|
}]
|
||||||
|
}
|
200
src/index.js
Executable file
200
src/index.js
Executable file
@ -0,0 +1,200 @@
|
|||||||
|
import pkg from "../package.json"
|
||||||
|
import EventEmitter from "@foxify/events"
|
||||||
|
|
||||||
|
import axios from "axios"
|
||||||
|
import { io } from "socket.io-client"
|
||||||
|
|
||||||
|
import remotes from "./remotes"
|
||||||
|
|
||||||
|
import Storage from "./helpers/withStorage"
|
||||||
|
|
||||||
|
import SessionModel from "./models/session"
|
||||||
|
import { createHandlers } from "./models"
|
||||||
|
|
||||||
|
globalThis.isServerMode = typeof window === "undefined" && typeof global !== "undefined"
|
||||||
|
|
||||||
|
if (globalThis.isServerMode) {
|
||||||
|
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() {
|
||||||
|
const instances = globalThis.__comty_shared_state.wsInstances
|
||||||
|
|
||||||
|
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.wsInstances[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let [key, remote] of Object.entries(remotes)) {
|
||||||
|
if (!remote.hasWebsocket) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let opts = {
|
||||||
|
transports: ["websocket"],
|
||||||
|
autoConnect: remote.autoConnect ?? true,
|
||||||
|
...remote.wsParams ?? {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remote.noAuth !== true) {
|
||||||
|
opts.auth = {
|
||||||
|
token: SessionModel.token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.__comty_shared_state.wsInstances[key] = io(remote.wsOrigin ?? remote.origin, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// regsister events
|
||||||
|
for (let [key, instance] of Object.entries(instances)) {
|
||||||
|
instance.on("connect", () => {
|
||||||
|
console.debug(`[WS-API][${key}] Connected`)
|
||||||
|
|
||||||
|
if (remotes[key].useClassicAuth && remotes[key].noAuth !== true) {
|
||||||
|
// try to auth
|
||||||
|
instance.emit("auth", {
|
||||||
|
token: SessionModel.token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.__comty_shared_state.eventBus.emit(`${key}:connected`)
|
||||||
|
})
|
||||||
|
|
||||||
|
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.wsInstances
|
||||||
|
|
||||||
|
for (let [key, instance] of Object.entries(instances)) {
|
||||||
|
if (instance.connected) {
|
||||||
|
instance.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reconnectWebsockets({ force = false } = {}) {
|
||||||
|
const instances = globalThis.__comty_shared_state.wsInstances
|
||||||
|
|
||||||
|
for (let [key, instance] of Object.entries(instances)) {
|
||||||
|
if (instance.connected) {
|
||||||
|
if (!instance.auth) {
|
||||||
|
instance.disconnect()
|
||||||
|
|
||||||
|
instance.auth = {
|
||||||
|
token: SessionModel.token,
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.connect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
instance.emit("reauthenticate", {
|
||||||
|
token: SessionModel.token,
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// disconnect first
|
||||||
|
instance.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remotes[key].noAuth !== true) {
|
||||||
|
instance.auth = {
|
||||||
|
token: SessionModel.token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createClient({
|
||||||
|
accessKey = null,
|
||||||
|
privateKey = null,
|
||||||
|
enableWs = false,
|
||||||
|
} = {}) {
|
||||||
|
const sharedState = globalThis.__comty_shared_state = {
|
||||||
|
onExpiredExceptionEvent: false,
|
||||||
|
excludedExpiredExceptionURL: ["/session/regenerate"],
|
||||||
|
eventBus: new EventEmitter(),
|
||||||
|
mainOrigin: remotes.default.origin,
|
||||||
|
instances: Object(),
|
||||||
|
wsInstances: Object(),
|
||||||
|
rest: null,
|
||||||
|
version: pkg.version,
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedState.rest = createHandlers()
|
||||||
|
if (globalThis.isServerMode) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateKey && accessKey && globalThis.isServerMode) {
|
||||||
|
Storage.engine.set("token", `${accessKey}:${privateKey}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create instances for every remote
|
||||||
|
for (const [key, remote] of Object.entries(remotes)) {
|
||||||
|
sharedState.instances[key] = axios.create({
|
||||||
|
baseURL: remote.origin,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// create a interceptor to attach the token every request
|
||||||
|
sharedState.instances[key].interceptors.request.use((config) => {
|
||||||
|
// check if current request has no Authorization header, if so, attach the token
|
||||||
|
if (!config.headers["Authorization"]) {
|
||||||
|
const sessionToken = SessionModel.token
|
||||||
|
|
||||||
|
if (sessionToken) {
|
||||||
|
config.headers["Authorization"] = `${globalThis.isServerMode ? "Server" : "Bearer"} ${sessionToken}`
|
||||||
|
} else {
|
||||||
|
console.warn("Making a request with no session token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableWs) {
|
||||||
|
createWebsockets()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sharedState
|
||||||
|
}
|
90
src/models/auth/index.js
Executable file
90
src/models/auth/index.js
Executable file
@ -0,0 +1,90 @@
|
|||||||
|
import request from "../../handlers/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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionModel.token = response.data.token
|
||||||
|
|
||||||
|
if (typeof callback === "function") {
|
||||||
|
await callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
__comty_shared_state.eventBus.emit("auth:login_success")
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static logout = async () => {
|
||||||
|
await SessionModel.destroyCurrentSession()
|
||||||
|
|
||||||
|
SessionModel.removeToken()
|
||||||
|
|
||||||
|
__comty_shared_state.eventBus.emit("auth:logout_success")
|
||||||
|
}
|
||||||
|
|
||||||
|
static register = async (payload) => {
|
||||||
|
const { username, password, email } = payload
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: "post",
|
||||||
|
url: "/auth/register",
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("Unable to register user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
static usernameValidation = async (username) => {
|
||||||
|
const response = await request({
|
||||||
|
method: "get",
|
||||||
|
url: `/auth/${username}/exists`,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
throw new Error("Unable to validate user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
56
src/models/feed/index.js
Executable file
56
src/models/feed/index.js
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
import request from "../../handlers/request"
|
||||||
|
import Settings from "../../helpers/withSettings"
|
||||||
|
|
||||||
|
export default class FeedModel {
|
||||||
|
static getMusicFeed = async ({ trim, limit } = {}) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/feed/music`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getGlobalMusicFeed = async ({ trim, limit } = {}) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/feed/music/global`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getTimelineFeed = async ({ trim, limit = 10 } = {}) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/feed/timeline`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPostsFeed = async ({ trim, limit } = {}) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/feed/posts`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
47
src/models/follows/index.js
Executable file
47
src/models/follows/index.js
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
import { SessionModel } from "../../models"
|
||||||
|
import request from "../../handlers/request"
|
||||||
|
|
||||||
|
export default class FollowsModel {
|
||||||
|
static imFollowing = async (user_id) => {
|
||||||
|
if (!user_id) {
|
||||||
|
throw new Error("user_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/users/${user_id}/following`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFollowers = async (user_id, fetchData) => {
|
||||||
|
if (!user_id) {
|
||||||
|
// set current user_id
|
||||||
|
user_id = SessionModel.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/users/${user_id}/followers`,
|
||||||
|
params: {
|
||||||
|
fetchData: fetchData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static toggleFollow = async ({ user_id }) => {
|
||||||
|
if (!user_id) {
|
||||||
|
throw new Error("user_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/users/${user_id}/follow`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
41
src/models/index.js
Executable file
41
src/models/index.js
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
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,
|
||||||
|
}
|
84
src/models/livestream/index.js
Executable file
84
src/models/livestream/index.js
Executable file
@ -0,0 +1,84 @@
|
|||||||
|
import request from "../../handlers/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
|
||||||
|
}
|
||||||
|
}
|
561
src/models/music/index.js
Executable file
561
src/models/music/index.js
Executable file
@ -0,0 +1,561 @@
|
|||||||
|
import request from "../../handlers/request"
|
||||||
|
import pmap from "p-map"
|
||||||
|
import SyncModel from "../sync"
|
||||||
|
|
||||||
|
export default class MusicModel {
|
||||||
|
static get api_instance() {
|
||||||
|
return globalThis.__comty_shared_state.instances["music"]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the official featured playlists.
|
||||||
|
*
|
||||||
|
* @return {Promise<Object>} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves track data for a given ID.
|
||||||
|
*
|
||||||
|
* @param {string} id - The ID of the track.
|
||||||
|
* @return {Promise<Object>} 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 tracks data for the given track IDs.
|
||||||
|
*
|
||||||
|
* @param {Array} ids - An array of track IDs.
|
||||||
|
* @return {Promise<Object>} 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 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<Object>} - 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 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<Object>} - 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 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<Object>} 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 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<Object>} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Object>} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new playlist.
|
||||||
|
*
|
||||||
|
* @param {object} payload - The payload containing the data for the new playlist.
|
||||||
|
* @return {Promise<Object>} The new playlist data.
|
||||||
|
*/
|
||||||
|
static async newPlaylist(payload) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: MusicModel.api_instance,
|
||||||
|
method: "POST",
|
||||||
|
url: `/playlists/new`,
|
||||||
|
data: payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Object>} - 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Object>} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a playlist.
|
||||||
|
*
|
||||||
|
* @param {number} playlist_id - The ID of the playlist to be deleted.
|
||||||
|
* @return {Promise<Object>} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a PUT request to update or create a release.
|
||||||
|
*
|
||||||
|
* @param {object} payload - The payload data.
|
||||||
|
* @return {Promise<Object>} The response data from the server.
|
||||||
|
*/
|
||||||
|
static async putRelease(payload) {
|
||||||
|
const response = await request({
|
||||||
|
instance: MusicModel.api_instance,
|
||||||
|
method: "PUT",
|
||||||
|
url: `/releases/release`,
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the releases associated with the authenticated user.
|
||||||
|
*
|
||||||
|
* @param {string} keywords - The keywords to filter the releases by.
|
||||||
|
* @return {Promise<Object>} A promise that resolves to the data of the releases.
|
||||||
|
*/
|
||||||
|
static async getMyReleases(keywords) {
|
||||||
|
const response = await request({
|
||||||
|
instance: MusicModel.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/releases/self`,
|
||||||
|
params: {
|
||||||
|
keywords,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Object>} - A promise that resolves to the retrieved releases.
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves release data by ID.
|
||||||
|
*
|
||||||
|
* @param {number} id - The ID of the release.
|
||||||
|
* @return {Promise<Object>} 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<Object>} - 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<Object>} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/models/nfc/index.js
Executable file
56
src/models/nfc/index.js
Executable file
@ -0,0 +1,56 @@
|
|||||||
|
import request from "../../handlers/request"
|
||||||
|
|
||||||
|
export default class NFCModel {
|
||||||
|
static async getOwnTags() {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/nfc/tags`
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTagById(id) {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/nfc/tags/${id}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTagBySerial(serial) {
|
||||||
|
if (!serial) {
|
||||||
|
throw new Error("Serial is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/nfc/tag/serial/${serial}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async registerTag(serial, payload) {
|
||||||
|
if (!serial) {
|
||||||
|
throw new Error("Serial is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
throw new Error("Payload is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/nfc/tag/${serial}`,
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
169
src/models/post/index.js
Executable file
169
src/models/post/index.js
Executable file
@ -0,0 +1,169 @@
|
|||||||
|
import request from "../../handlers/request"
|
||||||
|
import Settings from "../../helpers/withSettings"
|
||||||
|
|
||||||
|
export default class Post {
|
||||||
|
static get maxPostTextLength() {
|
||||||
|
return 3200
|
||||||
|
}
|
||||||
|
|
||||||
|
static get maxCommentLength() {
|
||||||
|
return 1200
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPostingPolicy = async () => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/posting_policy",
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPost = async ({ post_id }) => {
|
||||||
|
if (!post_id) {
|
||||||
|
throw new Error("Post ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/posts/post/${post_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPostComments = async ({ post_id }) => {
|
||||||
|
if (!post_id) {
|
||||||
|
throw new Error("Post ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/comments/post/${post_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static sendComment = async ({ post_id, comment }) => {
|
||||||
|
if (!post_id || !comment) {
|
||||||
|
throw new Error("Post ID and/or comment are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/comments/post/${post_id}`,
|
||||||
|
data: {
|
||||||
|
message: comment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static deleteComment = async ({ post_id, comment_id }) => {
|
||||||
|
if (!post_id || !comment_id) {
|
||||||
|
throw new Error("Post ID and/or comment ID are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/comments/post/${post_id}/${comment_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getExplorePosts = async ({ trim, limit }) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/posts/explore`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSavedPosts = async ({ trim, limit }) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/posts/saved`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUserPosts = async ({ user_id, trim, limit }) => {
|
||||||
|
if (!user_id) {
|
||||||
|
// use current user_id
|
||||||
|
user_id = app.userData?._id
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/posts/user/${user_id}`,
|
||||||
|
params: {
|
||||||
|
trim: trim ?? 0,
|
||||||
|
limit: limit ?? Settings.get("feed_max_fetch"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static toggleLike = async ({ post_id }) => {
|
||||||
|
if (!post_id) {
|
||||||
|
throw new Error("Post ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/posts/${post_id}/toggle_like`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static toggleSave = async ({ post_id }) => {
|
||||||
|
if (!post_id) {
|
||||||
|
throw new Error("Post ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/posts/${post_id}/toggle_save`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static create = async (payload) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: `/posts/new`,
|
||||||
|
data: payload,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static deletePost = async ({ post_id }) => {
|
||||||
|
if (!post_id) {
|
||||||
|
throw new Error("Post ID is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/posts/${post_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
26
src/models/search/index.js
Executable file
26
src/models/search/index.js
Executable file
@ -0,0 +1,26 @@
|
|||||||
|
import request from "../../handlers/request"
|
||||||
|
|
||||||
|
export default class Search {
|
||||||
|
static search = async (keywords, params = {}) => {
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/search`,
|
||||||
|
params: {
|
||||||
|
keywords: keywords,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async quickSearch(params) {
|
||||||
|
const response = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/search/quick",
|
||||||
|
params: params
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
108
src/models/session/index.js
Executable file
108
src/models/session/index.js
Executable file
@ -0,0 +1,108 @@
|
|||||||
|
import jwt_decode from "jwt-decode"
|
||||||
|
import request from "../../handlers/request"
|
||||||
|
import Storage from "../../helpers/withStorage"
|
||||||
|
|
||||||
|
export default class Session {
|
||||||
|
static storageTokenKey = "token"
|
||||||
|
|
||||||
|
static get token() {
|
||||||
|
return Storage.engine.get(this.storageTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static set token(token) {
|
||||||
|
return Storage.engine.set(this.storageTokenKey, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
static get roles() {
|
||||||
|
return this.getDecodedToken()?.roles
|
||||||
|
}
|
||||||
|
|
||||||
|
static get user_id() {
|
||||||
|
return this.getDecodedToken()?.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
static get session_uuid() {
|
||||||
|
return this.getDecodedToken()?.session_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDecodedToken = () => {
|
||||||
|
const token = this.token
|
||||||
|
|
||||||
|
return token && jwt_decode(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeToken() {
|
||||||
|
return Storage.engine.remove(Session.storageTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
static getAllSessions = async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: "get",
|
||||||
|
url: "/sessions/all"
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCurrentSession = async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: "get",
|
||||||
|
url: "/sessions/current"
|
||||||
|
})
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
const token = await Session.token
|
||||||
|
const session = await Session.getDecodedToken()
|
||||||
|
|
||||||
|
if (!session || !token) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: "delete",
|
||||||
|
url: "/sessions/current"
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
Session.removeToken()
|
||||||
|
|
||||||
|
__comty_shared_state.eventBus.emit("session.destroyed")
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static destroyAllSessions = async () => {
|
||||||
|
throw new Error("Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias for validateToken method
|
||||||
|
static validSession = async (token) => {
|
||||||
|
return await Session.validateToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
static isCurrentTokenValid = async () => {
|
||||||
|
const health = await Session.getTokenValidation()
|
||||||
|
|
||||||
|
return health.valid
|
||||||
|
}
|
||||||
|
}
|
59
src/models/sync/index.js
Executable file
59
src/models/sync/index.js
Executable file
@ -0,0 +1,59 @@
|
|||||||
|
import spotifyService from "./services/spotify"
|
||||||
|
import tidalService from "./services/tidal"
|
||||||
|
|
||||||
|
import request from "../../handlers/request"
|
||||||
|
|
||||||
|
const namespacesServices = {
|
||||||
|
spotify: spotifyService,
|
||||||
|
tidal: tidalService
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SyncModel {
|
||||||
|
static get spotifyCore() {
|
||||||
|
return namespacesServices.spotify
|
||||||
|
}
|
||||||
|
|
||||||
|
static get tidalCore() {
|
||||||
|
return namespacesServices.tidal
|
||||||
|
}
|
||||||
|
|
||||||
|
static async linkService(namespace) {
|
||||||
|
const service = namespacesServices[namespace]
|
||||||
|
|
||||||
|
if (!service || typeof service.linkAccount !== "function") {
|
||||||
|
throw new Error(`Service ${namespace} not found or not accepting linking.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await service.linkAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async unlinkService(namespace) {
|
||||||
|
const service = namespacesServices[namespace]
|
||||||
|
|
||||||
|
if (!service || typeof service.unlinkAccount !== "function") {
|
||||||
|
throw new Error(`Service ${namespace} not found or not accepting unlinking.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await service.unlinkAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async hasServiceLinked(namespace) {
|
||||||
|
const service = namespacesServices[namespace]
|
||||||
|
|
||||||
|
if (!service || typeof service.isActive !== "function") {
|
||||||
|
throw new Error(`Service ${namespace} not found or not accepting linking.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await service.isActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getLinkedServices() {
|
||||||
|
const response = await request({
|
||||||
|
instance: globalThis.__comty_shared_state.instances["sync"],
|
||||||
|
method: "GET",
|
||||||
|
url: "/active_services",
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
87
src/models/sync/services/spotify.js
Executable file
87
src/models/sync/services/spotify.js
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
export default class SpotifySyncModel {
|
||||||
|
static get spotify_redirect_uri() {
|
||||||
|
return window.location.origin + "/callbacks/sync/spotify"
|
||||||
|
}
|
||||||
|
|
||||||
|
static get spotify_authorize_endpoint() {
|
||||||
|
return "https://accounts.spotify.com/authorize?response_type=code&client_id={{client_id}}&scope={{scope}}&redirect_uri={{redirect_uri}}&response_type=code"
|
||||||
|
}
|
||||||
|
|
||||||
|
static async authorizeAccount() {
|
||||||
|
const scopes = [
|
||||||
|
"user-read-private",
|
||||||
|
"user-modify-playback-state",
|
||||||
|
"user-read-currently-playing",
|
||||||
|
"user-read-playback-state",
|
||||||
|
"streaming",
|
||||||
|
]
|
||||||
|
|
||||||
|
const { client_id } = await SpotifySyncModel.get_client_id()
|
||||||
|
|
||||||
|
const parsedUrl = SpotifySyncModel.spotify_authorize_endpoint
|
||||||
|
.replace("{{client_id}}", client_id)
|
||||||
|
.replace("{{scope}}", scopes.join(" "))
|
||||||
|
.replace("{{redirect_uri}}", SpotifySyncModel.spotify_redirect_uri)
|
||||||
|
|
||||||
|
// open on a new tab
|
||||||
|
window.open(parsedUrl, "_blank")
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get_client_id() {
|
||||||
|
const { data } = await app.cores.api.customRequest({
|
||||||
|
method: "GET",
|
||||||
|
url: `/sync/spotify/client_id`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async syncAuthCode(code) {
|
||||||
|
const { data } = await app.cores.api.customRequest({
|
||||||
|
method: "POST",
|
||||||
|
url: `/sync/spotify/auth`,
|
||||||
|
data: {
|
||||||
|
redirect_uri: SpotifySyncModel.spotify_redirect_uri,
|
||||||
|
code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async unlinkAccount() {
|
||||||
|
const { data } = await app.cores.api.customRequest({
|
||||||
|
method: "POST",
|
||||||
|
url: `/sync/spotify/unlink`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async isAuthorized() {
|
||||||
|
const { data } = await app.cores.api.customRequest({
|
||||||
|
method: "GET",
|
||||||
|
url: `/sync/spotify/is_authorized`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getData() {
|
||||||
|
const { data } = await app.cores.api.customRequest({
|
||||||
|
method: "GET",
|
||||||
|
url: `/sync/spotify/data`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getCurrentPlaying() {
|
||||||
|
const { data } = await app.cores.api.customRequest({
|
||||||
|
method: "GET",
|
||||||
|
url: `/sync/spotify/currently_playing`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
172
src/models/sync/services/tidal.js
Executable file
172
src/models/sync/services/tidal.js
Executable file
@ -0,0 +1,172 @@
|
|||||||
|
import request from "../../../handlers/request"
|
||||||
|
|
||||||
|
export default class TidalService {
|
||||||
|
static get api_instance() {
|
||||||
|
return globalThis.__comty_shared_state.instances["sync"]
|
||||||
|
}
|
||||||
|
|
||||||
|
static async linkAccount() {
|
||||||
|
if (!window) {
|
||||||
|
throw new Error("This method is only available in the browser.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/create_link`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.auth_url) {
|
||||||
|
window.open(data.auth_url, "_blank")
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async unlinkAccount() {
|
||||||
|
if (!window) {
|
||||||
|
throw new Error("This method is only available in the browser.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "POST",
|
||||||
|
url: `/services/tidal/delete_link`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async isActive() {
|
||||||
|
if (!window) {
|
||||||
|
throw new Error("This method is only available in the browser.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/is_active`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getCurrentUser() {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/current`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getPlaybackUrl(track_id) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/playback/${track_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTrackManifest(track_id) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/manifest/${track_id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMyFavoriteTracks({
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
} = {}) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/favorites/tracks`,
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getMyFavoritePlaylists({
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
} = {}) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/favorites/playlists`,
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getPlaylistData({
|
||||||
|
playlist_id,
|
||||||
|
|
||||||
|
resolve_items = false,
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
}) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/playlist/${playlist_id}/data`,
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
resolve_items,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getPlaylistItems({
|
||||||
|
playlist_id,
|
||||||
|
|
||||||
|
resolve_items = false,
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
}) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: "GET",
|
||||||
|
url: `/services/tidal/playlist/${playlist_id}/items`,
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
resolve_items,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static async toggleTrackLike({
|
||||||
|
track_id,
|
||||||
|
to,
|
||||||
|
}) {
|
||||||
|
const { data } = await request({
|
||||||
|
instance: TidalService.api_instance,
|
||||||
|
method: to ? "POST" : "DELETE",
|
||||||
|
url: `/services/tidal/track/${track_id}/like`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
159
src/models/user/index.js
Executable file
159
src/models/user/index.js
Executable file
@ -0,0 +1,159 @@
|
|||||||
|
import SessionModel from "../session"
|
||||||
|
import request from "../../handlers/request"
|
||||||
|
|
||||||
|
export default class User {
|
||||||
|
static data = async (payload = {}) => {
|
||||||
|
let {
|
||||||
|
username,
|
||||||
|
user_id,
|
||||||
|
} = payload
|
||||||
|
|
||||||
|
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`,
|
||||||
|
})
|
||||||
|
|
||||||
|
user_id = resolveResponse.data.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/users/${user_id}/data`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static updateData = async (payload) => {
|
||||||
|
const response = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/users/self/update_data",
|
||||||
|
data: {
|
||||||
|
update: payload,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsetFullName = async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/users/self/public_name",
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static selfRoles = async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: "/users/self/roles",
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
static haveRole = async (role) => {
|
||||||
|
const roles = await User.selfRoles()
|
||||||
|
|
||||||
|
if (!roles) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(roles) && roles.includes(role)
|
||||||
|
}
|
||||||
|
|
||||||
|
static haveAdmin = async () => {
|
||||||
|
return User.haveRole("admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUserBadges = async (user_id) => {
|
||||||
|
if (!user_id) {
|
||||||
|
user_id = SessionModel.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "GET",
|
||||||
|
url: `/users/${user_id}/badges`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static changePassword = async (payload) => {
|
||||||
|
const { currentPassword, newPassword } = payload
|
||||||
|
|
||||||
|
const { data } = await request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/user/self/update_password",
|
||||||
|
data: {
|
||||||
|
currentPassword,
|
||||||
|
newPassword,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUserFollowers = async ({
|
||||||
|
user_id,
|
||||||
|
limit = 20,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
18
src/models/widget/index.js
Executable file
18
src/models/widget/index.js
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
import request from "../../handlers/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
|
||||||
|
}
|
||||||
|
}
|
20
src/remotes.js
Executable file
20
src/remotes.js
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
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 {
|
||||||
|
default: {
|
||||||
|
origin: envOrigins[process.env.NODE_ENV ?? "production"],
|
||||||
|
hasWebsocket: false,
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user