From e6cc61d85d2bd56c924ce8b8d8e2791ae69cfdb1 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 16 May 2023 19:37:44 +0000 Subject: [PATCH] create new public package `comty.js` --- packages/comty.js/package.json | 14 ++ packages/comty.js/src/handlers/measurePing.js | 53 ++++++ packages/comty.js/src/handlers/request.js | 60 +++++++ .../src/helpers/handleAfterRequest.js | 34 ++++ .../src/helpers/handleBeforeRequest.js | 13 ++ .../src/helpers/handleRegenerationEvent.js | 39 ++++ packages/comty.js/src/helpers/withSettings.js | 25 +++ packages/comty.js/src/helpers/withStorage.js | 31 ++++ .../comty.js/src/hooks/useRequest/index.js | 32 ++++ packages/comty.js/src/index.js | 103 +++++++++++ packages/comty.js/src/models/auth/index.js | 53 ++++++ packages/comty.js/src/models/feed/index.js | 82 +++++++++ packages/comty.js/src/models/follows/index.js | 48 +++++ packages/comty.js/src/models/index.js | 44 +++++ .../comty.js/src/models/livestream/index.js | 84 +++++++++ .../comty.js/src/models/playlists/index.js | 48 +++++ packages/comty.js/src/models/post/index.js | 169 ++++++++++++++++++ packages/comty.js/src/models/session/index.js | 114 ++++++++++++ .../src/models/sync/cores/spotifyCore.js | 87 +++++++++ packages/comty.js/src/models/sync/index.js | 11 ++ packages/comty.js/src/models/user/index.js | 159 ++++++++++++++++ packages/comty.js/src/models/widget/index.js | 18 ++ packages/comty.js/src/remotes.js | 45 +++++ 23 files changed, 1366 insertions(+) create mode 100644 packages/comty.js/package.json create mode 100644 packages/comty.js/src/handlers/measurePing.js create mode 100644 packages/comty.js/src/handlers/request.js create mode 100644 packages/comty.js/src/helpers/handleAfterRequest.js create mode 100644 packages/comty.js/src/helpers/handleBeforeRequest.js create mode 100644 packages/comty.js/src/helpers/handleRegenerationEvent.js create mode 100644 packages/comty.js/src/helpers/withSettings.js create mode 100644 packages/comty.js/src/helpers/withStorage.js create mode 100644 packages/comty.js/src/hooks/useRequest/index.js create mode 100644 packages/comty.js/src/index.js create mode 100755 packages/comty.js/src/models/auth/index.js create mode 100755 packages/comty.js/src/models/feed/index.js create mode 100755 packages/comty.js/src/models/follows/index.js create mode 100644 packages/comty.js/src/models/index.js create mode 100755 packages/comty.js/src/models/livestream/index.js create mode 100755 packages/comty.js/src/models/playlists/index.js create mode 100755 packages/comty.js/src/models/post/index.js create mode 100755 packages/comty.js/src/models/session/index.js create mode 100755 packages/comty.js/src/models/sync/cores/spotifyCore.js create mode 100755 packages/comty.js/src/models/sync/index.js create mode 100755 packages/comty.js/src/models/user/index.js create mode 100644 packages/comty.js/src/models/widget/index.js create mode 100644 packages/comty.js/src/remotes.js diff --git a/packages/comty.js/package.json b/packages/comty.js/package.json new file mode 100644 index 00000000..67344bab --- /dev/null +++ b/packages/comty.js/package.json @@ -0,0 +1,14 @@ +{ + "name": "comty.js", + "version": "0.1.0", + "main": "./dist/index.js", + "author": "RageStudio ", + "license": "MIT", + "dependencies": { + "@foxify/events": "^2.1.0", + "axios": "^1.4.0", + "js-cookie": "^3.0.5", + "jwt-decode": "^3.1.2", + "linebridge": "^0.15.12" + } +} diff --git a/packages/comty.js/src/handlers/measurePing.js b/packages/comty.js/src/handlers/measurePing.js new file mode 100644 index 00000000..955729a8 --- /dev/null +++ b/packages/comty.js/src/handlers/measurePing.js @@ -0,0 +1,53 @@ +import request from "./request" + +export default async () => { + const timings = {} + + const promises = [ + new Promise(async (resolve) => { + const start = Date.now() + + request({ + method: "GET", + url: "/ping", + }) + .then(() => { + // set http timing in ms + timings.http = Date.now() - start + + resolve() + }) + .catch(() => { + timings.http = "failed" + resolve() + }) + + setTimeout(() => { + timings.http = "failed" + + resolve() + }, 10000) + }), + new Promise((resolve) => { + const start = Date.now() + + __comty_shared_state.wsInstances["default"].on("pong", () => { + timings.ws = Date.now() - start + + resolve() + }) + + __comty_shared_state.wsInstances["default"].emit("ping") + + setTimeout(() => { + timings.ws = "failed" + + resolve() + }, 10000) + }) + ] + + await Promise.all(promises) + + return timings +} \ No newline at end of file diff --git a/packages/comty.js/src/handlers/request.js b/packages/comty.js/src/handlers/request.js new file mode 100644 index 00000000..91bf694f --- /dev/null +++ b/packages/comty.js/src/handlers/request.js @@ -0,0 +1,60 @@ +import handleBeforeRequest from "../helpers/handleBeforeRequest" +import handleAfterRequest from "../helpers/handleAfterRequest" +import SessionModel from "../models/session" + +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 sessionToken = await SessionModel.token + + if (sessionToken) { + request.headers["Authorization"] = `${globalThis.isServerMode ? "Server" : "Bearer"} ${sessionToken}` + } else { + console.warn("Making a request with no session token") + } + + 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 +} \ No newline at end of file diff --git a/packages/comty.js/src/helpers/handleAfterRequest.js b/packages/comty.js/src/helpers/handleAfterRequest.js new file mode 100644 index 00000000..3840d158 --- /dev/null +++ b/packages/comty.js/src/helpers/handleAfterRequest.js @@ -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") + } + } + } +} \ No newline at end of file diff --git a/packages/comty.js/src/helpers/handleBeforeRequest.js b/packages/comty.js/src/helpers/handleBeforeRequest.js new file mode 100644 index 00000000..be7cbee3 --- /dev/null +++ b/packages/comty.js/src/helpers/handleBeforeRequest.js @@ -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() + }) + }) + } +} \ No newline at end of file diff --git a/packages/comty.js/src/helpers/handleRegenerationEvent.js b/packages/comty.js/src/helpers/handleRegenerationEvent.js new file mode 100644 index 00000000..354cecb8 --- /dev/null +++ b/packages/comty.js/src/helpers/handleRegenerationEvent.js @@ -0,0 +1,39 @@ +import SessionModel from "../models/session" +import request from "../handlers/request" + +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") +} \ No newline at end of file diff --git a/packages/comty.js/src/helpers/withSettings.js b/packages/comty.js/src/helpers/withSettings.js new file mode 100644 index 00000000..d48e243b --- /dev/null +++ b/packages/comty.js/src/helpers/withSettings.js @@ -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) + } +} \ No newline at end of file diff --git a/packages/comty.js/src/helpers/withStorage.js b/packages/comty.js/src/helpers/withStorage.js new file mode 100644 index 00000000..bbea9678 --- /dev/null +++ b/packages/comty.js/src/helpers/withStorage.js @@ -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"] + } +} \ No newline at end of file diff --git a/packages/comty.js/src/hooks/useRequest/index.js b/packages/comty.js/src/hooks/useRequest/index.js new file mode 100644 index 00000000..0dd2b7a6 --- /dev/null +++ b/packages/comty.js/src/hooks/useRequest/index.js @@ -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) + }] +} \ No newline at end of file diff --git a/packages/comty.js/src/index.js b/packages/comty.js/src/index.js new file mode 100644 index 00000000..a02e6152 --- /dev/null +++ b/packages/comty.js/src/index.js @@ -0,0 +1,103 @@ +import EventEmitter from "@foxify/events" + +import axios from "axios" +import { io } from "socket.io-client" + +import remotes from "./remotes" + +import request from "./handlers/request" +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 default function createClient({ + wsEvents = Object(), + useWs = false, + accessKey = null, + privateKey = null, +} = {}) { + const sharedState = globalThis.__comty_shared_state = { + onExpiredExceptionEvent: false, + excludedExpiredExceptionURL: ["/session/regenerate"], + eventBus: new EventEmitter(), + mainOrigin: remotes.default.origin, + instances: Object(), + wsInstances: Object(), + curl: null, + } + + if (globalThis.isServerMode) { + sharedState.curl = createHandlers() + } + + 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, + }) + + if (useWs && remote.hasWebsocket) { + sharedState.wsInstances[key] = io(remote.wsOrigin ?? remote.origin, { + transports: ["websocket"], + autoConnect: true, + ...remote.wsParams ?? {}, + }) + } + } + + // register ws events + Object.keys(sharedState.wsInstances).forEach((key) => { + const ws = sharedState.wsInstances[key] + + ws.on("connect", () => { + console.log(`[WS-API][${key}] Connected`) + + if (remotes[key].needsAuth) { + // try to auth + ws.emit("authenticate", { + token: SessionModel.token, + }) + } + }) + + ws.on("disconnect", () => { + console.log(`[WS-API][${key}] Disconnected`) + }) + + ws.on("error", (error) => { + console.error(`[WS-API][${key}] Error`, error) + }) + + ws.onAny((event, ...args) => { + console.log(`[WS-API][${key}] Event recived`, event, ...args) + }) + + const customEvents = wsEvents[key] + + if (customEvents) { + for (const [eventName, eventHandler] of Object.entries(customEvents)) { + ws.on(eventName, eventHandler) + } + } + }) + + return sharedState +} \ No newline at end of file diff --git a/packages/comty.js/src/models/auth/index.js b/packages/comty.js/src/models/auth/index.js new file mode 100755 index 00000000..cc5c833a --- /dev/null +++ b/packages/comty.js/src/models/auth/index.js @@ -0,0 +1,53 @@ +import request from "../../handlers/request" +import SessionModel from "../session" + +export default class AuthModel { + static login = async (payload) => { + const response = await request({ + method: "post", + url: "/auth/login", + data: { + username: payload.username, //window.btoa(payload.username), + password: payload.password, //window.btoa(payload.password), + }, + }) + + SessionModel.token = response.data.token + + __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 + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/feed/index.js b/packages/comty.js/src/models/feed/index.js new file mode 100755 index 00000000..3b3e30f0 --- /dev/null +++ b/packages/comty.js/src/models/feed/index.js @@ -0,0 +1,82 @@ +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 } = {}) => { + 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 + } + + static getPlaylistsFeed = async ({ trim, limit } = {}) => { + const { data } = await request({ + method: "GET", + url: `/feed/playlists`, + params: { + trim: trim ?? 0, + limit: limit ?? Settings.get("feed_max_fetch"), + } + }) + + return data + } + + static search = async (keywords, params = {}) => { + const { data } = await request({ + method: "GET", + url: `/search`, + params: { + keywords: keywords, + params: params + } + }) + + return data + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/follows/index.js b/packages/comty.js/src/models/follows/index.js new file mode 100755 index 00000000..df01c5b3 --- /dev/null +++ b/packages/comty.js/src/models/follows/index.js @@ -0,0 +1,48 @@ +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: `/follow/user/${user_id}`, + }) + + return response.data + } + + static getFollowers = async (user_id) => { + if (!user_id) { + // set current user_id + user_id = SessionModel.user_id + } + + const response = await request({ + method: "GET", + url: `/follow/user/${user_id}/followers`, + }) + + return response.data + } + + static toogleFollow = async ({ user_id, username }) => { + if (!user_id && !username) { + throw new Error("user_id or username is required") + } + + const response = await request({ + method: "POST", + url: "/follow/user/toogle", + data: { + user_id: user_id, + username: username + }, + }) + + return response.data + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/index.js b/packages/comty.js/src/models/index.js new file mode 100644 index 00000000..404a82e5 --- /dev/null +++ b/packages/comty.js/src/models/index.js @@ -0,0 +1,44 @@ +import AuthModel from "./auth" +import FeedModel from "./feed" +import FollowsModel from "./follows" +import LivestreamModel from "./livestream" +import PlaylistsModel from "./playlists" +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), + playlists: getEndpointsFromModel(PlaylistsModel), + post: getEndpointsFromModel(PostModel), + session: getEndpointsFromModel(SessionModel), + sync: getEndpointsFromModel(SyncModel), + user: getEndpointsFromModel(UserModel), + } +} + +export { + AuthModel, + FeedModel, + FollowsModel, + LivestreamModel, + PlaylistsModel, + PostModel, + SessionModel, + SyncModel, + UserModel, + createHandlers, +} diff --git a/packages/comty.js/src/models/livestream/index.js b/packages/comty.js/src/models/livestream/index.js new file mode 100755 index 00000000..5393b23e --- /dev/null +++ b/packages/comty.js/src/models/livestream/index.js @@ -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 + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/playlists/index.js b/packages/comty.js/src/models/playlists/index.js new file mode 100755 index 00000000..0cf52343 --- /dev/null +++ b/packages/comty.js/src/models/playlists/index.js @@ -0,0 +1,48 @@ +import request from "../../handlers/request" + +export default class PlaylistsModel { + static putPlaylist = async (payload) => { + if (!payload) { + throw new Error("Payload is required") + } + + const { data } = await request({ + method: "PUT", + url: `/playlist`, + data: payload, + }) + + return data + } + + static getPlaylist = async (id) => { + const { data } = await request({ + method: "GET", + url: `/playlist/data/${id}`, + }) + + return data + } + + static getMyReleases = async () => { + const { data } = await request({ + method: "GET", + url: `/playlist/self`, + }) + + return data + } + + static deletePlaylist = async (id) => { + if (!id) { + throw new Error("ID is required") + } + + const { data } = await request({ + method: "DELETE", + url: `/playlist/${id}`, + }) + + return data + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/post/index.js b/packages/comty.js/src/models/post/index.js new file mode 100755 index 00000000..c03d15b2 --- /dev/null +++ b/packages/comty.js/src/models/post/index.js @@ -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 request = await request({ + method: "POST", + url: `/comments/post/${post_id}`, + data: { + message: comment, + }, + }) + + return request + } + + static deleteComment = async ({ post_id, comment_id }) => { + if (!post_id || !comment_id) { + throw new Error("Post ID and/or comment ID are required") + } + + const request = await request({ + method: "DELETE", + url: `/comments/post/${post_id}/${comment_id}`, + }) + + return request + } + + 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 toogleLike = async ({ post_id }) => { + if (!post_id) { + throw new Error("Post ID is required") + } + + const { data } = await request({ + method: "POST", + url: `/posts/${post_id}/toogle_like`, + }) + + return data + } + + static toogleSave = async ({ post_id }) => { + if (!post_id) { + throw new Error("Post ID is required") + } + + const { data } = await request({ + method: "POST", + url: `/posts/${post_id}/toogle_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 + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/session/index.js b/packages/comty.js/src/models/session/index.js new file mode 100755 index 00000000..43fe92c3 --- /dev/null +++ b/packages/comty.js/src/models/session/index.js @@ -0,0 +1,114 @@ +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 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 getAllSessions = async () => { + const response = await request({ + method: "get", + url: "/session/all" + }) + + return response.data + } + + static getCurrentSession = async () => { + const response = await request({ + method: "get", + url: "/session/current" + }) + + return response.data + } + + static getTokenValidation = async () => { + const session = await Session.token + + const response = await request({ + method: "get", + url: "/session/validate", + data: { + session: session + } + }) + + return response.data + } + + static removeToken() { + return Storage.engine.remove(Session.storageTokenKey) + } + + 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: "/session/current" + }).catch((error) => { + console.error(error) + + return false + }) + + Session.removeToken() + + __comty_shared_state.emit("session.destroyed") + + return response.data + } + + static destroyAllSessions = async () => { + const session = await Session.getDecodedToken() + + if (!session) { + return false + } + + const response = await request({ + method: "delete", + url: "/session/all" + }) + + Session.removeToken() + + __comty_shared_state.emit("session.destroyed") + + return response.data + } + + static isCurrentTokenValid = async () => { + const health = await Session.getTokenValidation() + + return health.valid + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/sync/cores/spotifyCore.js b/packages/comty.js/src/models/sync/cores/spotifyCore.js new file mode 100755 index 00000000..82fb2d74 --- /dev/null +++ b/packages/comty.js/src/models/sync/cores/spotifyCore.js @@ -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 + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/sync/index.js b/packages/comty.js/src/models/sync/index.js new file mode 100755 index 00000000..43bcbf0c --- /dev/null +++ b/packages/comty.js/src/models/sync/index.js @@ -0,0 +1,11 @@ +import SpotifySyncModel from "./cores/spotifyCore" + +export default class SyncModel { + static get bridge() { + return window.app?.cores.api.withEndpoints() + } + + static get spotifyCore() { + return SpotifySyncModel + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/user/index.js b/packages/comty.js/src/models/user/index.js new file mode 100755 index 00000000..fc04f194 --- /dev/null +++ b/packages/comty.js/src/models/user/index.js @@ -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: `/user/user_id/${username}`, + }) + + user_id = resolveResponse.data.user_id + } + + const response = await request({ + method: "GET", + url: `/user/${user_id}/data`, + }) + + return response.data + } + + static updateData = async (payload) => { + const response = await request({ + method: "POST", + url: "/user/self/update_data", + data: { + update: payload, + }, + }) + + return response.data + } + + static unsetFullName = async () => { + const response = await request({ + method: "DELETE", + url: "/user/self/public_name", + }) + + return response.data + } + + static selfRoles = async () => { + const response = await request({ + method: "GET", + url: "/roles/self", + }) + + 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: `/badge/user/${user_id}`, + }) + + return data + } + + static changePassword = async (payload) => { + const { currentPassword, newPassword } = payload + + const { data } = await request({ + method: "POST", + url: "/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 + } +} \ No newline at end of file diff --git a/packages/comty.js/src/models/widget/index.js b/packages/comty.js/src/models/widget/index.js new file mode 100644 index 00000000..4ba349e8 --- /dev/null +++ b/packages/comty.js/src/models/widget/index.js @@ -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 + } +} \ No newline at end of file diff --git a/packages/comty.js/src/remotes.js b/packages/comty.js/src/remotes.js new file mode 100644 index 00000000..95b71e1d --- /dev/null +++ b/packages/comty.js/src/remotes.js @@ -0,0 +1,45 @@ +function composeRemote(path) { + return envOrigins[process.env.NODE_ENV][path] +} + +function getCurrentHostname() { + if (typeof window === "undefined") { + return "localhost" + } + + return window?.location?.hostname ?? "localhost" +} + +const envOrigins = { + "development": { + default: `http://${getCurrentHostname()}:3010`, + messaging: `http://${getCurrentHostname()}:3020`, + livestreaming: `http://${getCurrentHostname()}:3030`, + marketplace: `http://${getCurrentHostname()}:3040`, + }, + "production": { + default: "https://api.comty.app", + messaging: `https://messaging_api.comty.app`, + livestreaming: `https://livestreaming_api.comty.app`, + marketplace: `https://marketplace_api.comty.app`, + } +} + +export default { + default: { + origin: composeRemote("default"), + hasWebsocket: true, + needsAuth: true, + }, + messaging: { + origin: composeRemote("messaging"), + hasWebsocket: true, + needsAuth: true, + }, + livestreaming: { + origin: composeRemote("livestreaming"), + }, + marketplace: { + origin: composeRemote("marketplace"), + }, +} \ No newline at end of file