comty.js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "comty.js",
3
+ "version": "0.1.0",
4
+ "main": "./dist/index.js",
5
+ "author": "RageStudio <support@ragestudio.net>",
6
+ "scripts": {
7
+ "build": "corenode-cli build"
8
+ },
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "@foxify/events": "^2.1.0",
12
+ "axios": "^1.4.0",
13
+ "js-cookie": "^3.0.5",
14
+ "jsonwebtoken": "^9.0.0",
15
+ "jwt-decode": "^3.1.2",
16
+ "linebridge": "^0.15.12"
17
+ },
18
+ "devDependencies": {
19
+ "corenode": "^0.28.26"
20
+ }
21
+ }
@@ -0,0 +1,53 @@
1
+ import request from "./request"
2
+
3
+ export default async () => {
4
+ const timings = {}
5
+
6
+ const promises = [
7
+ new Promise(async (resolve) => {
8
+ const start = Date.now()
9
+
10
+ request({
11
+ method: "GET",
12
+ url: "/ping",
13
+ })
14
+ .then(() => {
15
+ // set http timing in ms
16
+ timings.http = Date.now() - start
17
+
18
+ resolve()
19
+ })
20
+ .catch(() => {
21
+ timings.http = "failed"
22
+ resolve()
23
+ })
24
+
25
+ setTimeout(() => {
26
+ timings.http = "failed"
27
+
28
+ resolve()
29
+ }, 10000)
30
+ }),
31
+ new Promise((resolve) => {
32
+ const start = Date.now()
33
+
34
+ __comty_shared_state.wsInstances["default"].on("pong", () => {
35
+ timings.ws = Date.now() - start
36
+
37
+ resolve()
38
+ })
39
+
40
+ __comty_shared_state.wsInstances["default"].emit("ping")
41
+
42
+ setTimeout(() => {
43
+ timings.ws = "failed"
44
+
45
+ resolve()
46
+ }, 10000)
47
+ })
48
+ ]
49
+
50
+ await Promise.all(promises)
51
+
52
+ return timings
53
+ }
@@ -0,0 +1,60 @@
1
+ import handleBeforeRequest from "../helpers/handleBeforeRequest"
2
+ import handleAfterRequest from "../helpers/handleAfterRequest"
3
+ import SessionModel from "../models/session"
4
+
5
+ export default async (
6
+ request = {
7
+ method: "GET",
8
+ },
9
+ ...args
10
+ ) => {
11
+ const instance = request.instance ?? __comty_shared_state.instances.default
12
+
13
+ if (!instance) {
14
+ throw new Error("No instance provided")
15
+ }
16
+
17
+ // handle before request
18
+ await handleBeforeRequest(request)
19
+
20
+ if (typeof request === "string") {
21
+ request = {
22
+ url: request,
23
+ }
24
+ }
25
+
26
+ if (typeof request.headers !== "object") {
27
+ request.headers = {}
28
+ }
29
+
30
+ let result = null
31
+
32
+ const makeRequest = async () => {
33
+ const sessionToken = await SessionModel.token
34
+
35
+ if (sessionToken) {
36
+ request.headers["Authorization"] = `${globalThis.isServerMode ? "Server" : "Bearer"} ${sessionToken}`
37
+ } else {
38
+ console.warn("Making a request with no session token")
39
+ }
40
+
41
+ const _result = await instance(request, ...args)
42
+ .catch((error) => {
43
+ return error
44
+ })
45
+
46
+ result = _result
47
+ }
48
+
49
+ await makeRequest()
50
+
51
+ // handle after request
52
+ await handleAfterRequest(result, makeRequest)
53
+
54
+ // if error, throw it
55
+ if (result instanceof Error) {
56
+ throw result
57
+ }
58
+
59
+ return result
60
+ }
@@ -0,0 +1,34 @@
1
+ import handleRegenerationEvent from "./handleRegenerationEvent"
2
+
3
+ export default async (data, callback) => {
4
+ // handle 401, 403 responses
5
+ if (data instanceof Error) {
6
+ if (data.code && (data.code === "ECONNABORTED" || data.code === "ERR_NETWORK")) {
7
+ console.error(`Request aborted or network error, ignoring`)
8
+ return false
9
+ }
10
+
11
+ if (data.response.status === 401) {
12
+ // check if the server issue a refresh token on data
13
+ if (data.response.data.refreshToken) {
14
+ console.log(`Session expired, but the server issued a refresh token, handling regeneration event`)
15
+
16
+ // handle regeneration event
17
+ await handleRegenerationEvent(data.response.data.refreshToken)
18
+
19
+ return await callback()
20
+ }
21
+
22
+ // check if route is from "session" namespace
23
+ if (data.config.url.includes("/session")) {
24
+ return __comty_shared_state.eventBus.emit("session.invalid", "Session expired, but the server did not issue a refresh token")
25
+ }
26
+ }
27
+
28
+ if (data.response.status === 403) {
29
+ if (data.config.url.includes("/session")) {
30
+ return __comty_shared_state.eventBus.emit("session.invalid", "Session not valid or not existent")
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,13 @@
1
+ export default async (request) => {
2
+ if (__comty_shared_state.onExpiredExceptionEvent) {
3
+ if (__comty_shared_state.excludedExpiredExceptionURL.includes(request.url)) return
4
+
5
+ await new Promise((resolve) => {
6
+ __comty_shared_state.eventBus.once("session.regenerated", () => {
7
+ console.log(`Session has been regenerated, retrying request`)
8
+
9
+ resolve()
10
+ })
11
+ })
12
+ }
13
+ }
@@ -0,0 +1,39 @@
1
+ import SessionModel from "../models/session"
2
+ import request from "../handlers/request"
3
+
4
+ export default async (refreshToken) =>{
5
+ __comty_shared_state.eventBus.emit("session.expiredExceptionEvent", refreshToken)
6
+
7
+ __comty_shared_state.onExpiredExceptionEvent = true
8
+
9
+ const expiredToken = await SessionModel.token
10
+
11
+ // send request to regenerate token
12
+ const response = await request({
13
+ method: "POST",
14
+ url: "/session/regenerate",
15
+ data: {
16
+ expiredToken: expiredToken,
17
+ refreshToken,
18
+ }
19
+ }).catch((error) => {
20
+ console.error(`Failed to regenerate token: ${error.message}`)
21
+ return false
22
+ })
23
+
24
+ if (!response) {
25
+ return __comty_shared_state.eventBus.emit("session.invalid", "Failed to regenerate token")
26
+ }
27
+
28
+ if (!response.data?.token) {
29
+ return __comty_shared_state.eventBus.emit("session.invalid", "Failed to regenerate token, invalid server response.")
30
+ }
31
+
32
+ // set new token
33
+ SessionModel.token = response.data.token
34
+
35
+ __comty_shared_state.onExpiredExceptionEvent = false
36
+
37
+ // emit event
38
+ __comty_shared_state.eventBus.emit("session.regenerated")
39
+ }
@@ -0,0 +1,25 @@
1
+ export default class Settings {
2
+ static get = (key) => {
3
+ if (typeof window === "undefined") {
4
+ return null
5
+ }
6
+
7
+ return window?.app?.cores?.settings.get(key)
8
+ }
9
+
10
+ static set = (key, value) => {
11
+ if (typeof window === "undefined") {
12
+ return null
13
+ }
14
+
15
+ return window?.app?.cores?.settings.set(key, value)
16
+ }
17
+
18
+ static is = (key) => {
19
+ if (typeof window === "undefined") {
20
+ return null
21
+ }
22
+
23
+ return window?.app?.cores?.settings.is(key)
24
+ }
25
+ }
@@ -0,0 +1,31 @@
1
+ import jscookies from "js-cookie"
2
+
3
+ class InternalStorage {
4
+ #storage = {}
5
+
6
+ get(key) {
7
+ // get value from storage
8
+ return this.#storage[key]
9
+ }
10
+
11
+ set(key, value) {
12
+ // storage securely in memory
13
+ return this.#storage[key] = value
14
+ }
15
+ }
16
+
17
+ export default class Storage {
18
+ static get engine() {
19
+ // check if is running in browser, if is import js-cookie
20
+ // else use in-memory safe storage
21
+ if (typeof window !== "undefined") {
22
+ return jscookies
23
+ }
24
+
25
+ if (!globalThis.__comty_shared_state["_internal_storage"]) {
26
+ globalThis.__comty_shared_state["_internal_storage"] = new InternalStorage()
27
+ }
28
+
29
+ return globalThis.__comty_shared_state["_internal_storage"]
30
+ }
31
+ }
@@ -0,0 +1,32 @@
1
+ import React from "react"
2
+
3
+ export default (method, ...args) => {
4
+ if (typeof method !== "function") {
5
+ throw new Error("useRequest: method must be a function")
6
+ }
7
+
8
+ const [loading, setLoading] = React.useState(true)
9
+ const [result, setResult] = React.useState(null)
10
+ const [error, setError] = React.useState(null)
11
+
12
+ const makeRequest = (...newArgs) => {
13
+ method(...newArgs)
14
+ .then((data) => {
15
+ setResult(data)
16
+ setLoading(false)
17
+ })
18
+ .catch((err) => {
19
+ setError(err)
20
+ setLoading(false)
21
+ })
22
+ }
23
+
24
+ React.useEffect(() => {
25
+ makeRequest(...args)
26
+ }, [])
27
+
28
+ return [loading, result, error, (...newArgs) => {
29
+ setLoading(true)
30
+ makeRequest(...newArgs)
31
+ }]
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,103 @@
1
+ import EventEmitter from "@foxify/events"
2
+
3
+ import axios from "axios"
4
+ import { io } from "socket.io-client"
5
+
6
+ import remotes from "./remotes"
7
+
8
+ import request from "./handlers/request"
9
+ import Storage from "./helpers/withStorage"
10
+
11
+ import SessionModel from "./models/session"
12
+ import { createHandlers } from "./models"
13
+
14
+ globalThis.isServerMode = typeof window === "undefined" && typeof global !== "undefined"
15
+
16
+ if (globalThis.isServerMode) {
17
+ const { Buffer } = require("buffer")
18
+
19
+ globalThis.b64Decode = (data) => {
20
+ return Buffer.from(data, "base64").toString("utf-8")
21
+ }
22
+ globalThis.b64Encode = (data) => {
23
+ return Buffer.from(data, "utf-8").toString("base64")
24
+ }
25
+ }
26
+
27
+ export default function createClient({
28
+ wsEvents = Object(),
29
+ useWs = false,
30
+ accessKey = null,
31
+ privateKey = null,
32
+ } = {}) {
33
+ const sharedState = globalThis.__comty_shared_state = {
34
+ onExpiredExceptionEvent: false,
35
+ excludedExpiredExceptionURL: ["/session/regenerate"],
36
+ eventBus: new EventEmitter(),
37
+ mainOrigin: remotes.default.origin,
38
+ instances: Object(),
39
+ wsInstances: Object(),
40
+ rest: null,
41
+ }
42
+
43
+ if (globalThis.isServerMode) {
44
+ sharedState.rest = createHandlers()
45
+ }
46
+
47
+ if (privateKey && accessKey && globalThis.isServerMode) {
48
+ Storage.engine.set("token", `${accessKey}:${privateKey}`)
49
+ }
50
+
51
+ // create instances for every remote
52
+ for (const [key, remote] of Object.entries(remotes)) {
53
+ sharedState.instances[key] = axios.create({
54
+ baseURL: remote.origin,
55
+ })
56
+
57
+ if (useWs && remote.hasWebsocket) {
58
+ sharedState.wsInstances[key] = io(remote.wsOrigin ?? remote.origin, {
59
+ transports: ["websocket"],
60
+ autoConnect: true,
61
+ ...remote.wsParams ?? {},
62
+ })
63
+ }
64
+ }
65
+
66
+ // register ws events
67
+ Object.keys(sharedState.wsInstances).forEach((key) => {
68
+ const ws = sharedState.wsInstances[key]
69
+
70
+ ws.on("connect", () => {
71
+ console.log(`[WS-API][${key}] Connected`)
72
+
73
+ if (remotes[key].needsAuth) {
74
+ // try to auth
75
+ ws.emit("authenticate", {
76
+ token: SessionModel.token,
77
+ })
78
+ }
79
+ })
80
+
81
+ ws.on("disconnect", () => {
82
+ console.log(`[WS-API][${key}] Disconnected`)
83
+ })
84
+
85
+ ws.on("error", (error) => {
86
+ console.error(`[WS-API][${key}] Error`, error)
87
+ })
88
+
89
+ ws.onAny((event, ...args) => {
90
+ console.log(`[WS-API][${key}] Event recived`, event, ...args)
91
+ })
92
+
93
+ const customEvents = wsEvents[key]
94
+
95
+ if (customEvents) {
96
+ for (const [eventName, eventHandler] of Object.entries(customEvents)) {
97
+ ws.on(eventName, eventHandler)
98
+ }
99
+ }
100
+ })
101
+
102
+ return sharedState
103
+ }
@@ -0,0 +1,53 @@
1
+ import request from "../../handlers/request"
2
+ import SessionModel from "../session"
3
+
4
+ export default class AuthModel {
5
+ static login = async (payload) => {
6
+ const response = await request({
7
+ method: "post",
8
+ url: "/auth/login",
9
+ data: {
10
+ username: payload.username, //window.btoa(payload.username),
11
+ password: payload.password, //window.btoa(payload.password),
12
+ },
13
+ })
14
+
15
+ SessionModel.token = response.data.token
16
+
17
+ __comty_shared_state.eventBus.emit("auth:login_success")
18
+
19
+ return response.data
20
+ }
21
+
22
+ static logout = async () => {
23
+ await SessionModel.destroyCurrentSession()
24
+
25
+ SessionModel.removeToken()
26
+
27
+ __comty_shared_state.eventBus.emit("auth:logout_success")
28
+ }
29
+
30
+ static register = async (payload) => {
31
+ const { username, password, email } = payload
32
+
33
+ const response = await request({
34
+ method: "post",
35
+ url: "/auth/register",
36
+ data: {
37
+ username,
38
+ password,
39
+ email,
40
+ }
41
+ }).catch((error) => {
42
+ console.error(error)
43
+
44
+ return false
45
+ })
46
+
47
+ if (!response) {
48
+ throw new Error("Unable to register user")
49
+ }
50
+
51
+ return response
52
+ }
53
+ }
@@ -0,0 +1,82 @@
1
+ import request from "../../handlers/request"
2
+ import Settings from "../../helpers/withSettings"
3
+
4
+ export default class FeedModel {
5
+ static getMusicFeed = async ({ trim, limit } = {}) => {
6
+ const { data } = await request({
7
+ method: "GET",
8
+ url: `/feed/music`,
9
+ params: {
10
+ trim: trim ?? 0,
11
+ limit: limit ?? Settings.get("feed_max_fetch"),
12
+ }
13
+ })
14
+
15
+ return data
16
+ }
17
+
18
+ static getGlobalMusicFeed = async ({ trim, limit } = {}) => {
19
+ const { data } = await request({
20
+ method: "GET",
21
+ url: `/feed/music/global`,
22
+ params: {
23
+ trim: trim ?? 0,
24
+ limit: limit ?? Settings.get("feed_max_fetch"),
25
+ }
26
+ })
27
+
28
+ return data
29
+ }
30
+
31
+ static getTimelineFeed = async ({ trim, limit } = {}) => {
32
+ const { data } = await request({
33
+ method: "GET",
34
+ url: `/feed/timeline`,
35
+ params: {
36
+ trim: trim ?? 0,
37
+ limit: limit ?? Settings.get("feed_max_fetch"),
38
+ }
39
+ })
40
+
41
+ return data
42
+ }
43
+
44
+ static getPostsFeed = async ({ trim, limit } = {}) => {
45
+ const { data } = await request({
46
+ method: "GET",
47
+ url: `/feed/posts`,
48
+ params: {
49
+ trim: trim ?? 0,
50
+ limit: limit ?? Settings.get("feed_max_fetch"),
51
+ }
52
+ })
53
+
54
+ return data
55
+ }
56
+
57
+ static getPlaylistsFeed = async ({ trim, limit } = {}) => {
58
+ const { data } = await request({
59
+ method: "GET",
60
+ url: `/feed/playlists`,
61
+ params: {
62
+ trim: trim ?? 0,
63
+ limit: limit ?? Settings.get("feed_max_fetch"),
64
+ }
65
+ })
66
+
67
+ return data
68
+ }
69
+
70
+ static search = async (keywords, params = {}) => {
71
+ const { data } = await request({
72
+ method: "GET",
73
+ url: `/search`,
74
+ params: {
75
+ keywords: keywords,
76
+ params: params
77
+ }
78
+ })
79
+
80
+ return data
81
+ }
82
+ }
@@ -0,0 +1,48 @@
1
+ import { SessionModel } from "../../models"
2
+ import request from "../../handlers/request"
3
+
4
+ export default class FollowsModel {
5
+ static imFollowing = async (user_id) => {
6
+ if (!user_id) {
7
+ throw new Error("user_id is required")
8
+ }
9
+
10
+ const response = await request({
11
+ method: "GET",
12
+ url: `/follow/user/${user_id}`,
13
+ })
14
+
15
+ return response.data
16
+ }
17
+
18
+ static getFollowers = async (user_id) => {
19
+ if (!user_id) {
20
+ // set current user_id
21
+ user_id = SessionModel.user_id
22
+ }
23
+
24
+ const response = await request({
25
+ method: "GET",
26
+ url: `/follow/user/${user_id}/followers`,
27
+ })
28
+
29
+ return response.data
30
+ }
31
+
32
+ static toogleFollow = async ({ user_id, username }) => {
33
+ if (!user_id && !username) {
34
+ throw new Error("user_id or username is required")
35
+ }
36
+
37
+ const response = await request({
38
+ method: "POST",
39
+ url: "/follow/user/toogle",
40
+ data: {
41
+ user_id: user_id,
42
+ username: username
43
+ },
44
+ })
45
+
46
+ return response.data
47
+ }
48
+ }
@@ -0,0 +1,44 @@
1
+ import AuthModel from "./auth"
2
+ import FeedModel from "./feed"
3
+ import FollowsModel from "./follows"
4
+ import LivestreamModel from "./livestream"
5
+ import PlaylistsModel from "./playlists"
6
+ import PostModel from "./post"
7
+ import SessionModel from "./session"
8
+ import SyncModel from "./sync"
9
+ import UserModel from "./user"
10
+
11
+ function getEndpointsFromModel(model) {
12
+ return Object.entries(model).reduce((acc, [key, value]) => {
13
+ acc[key] = value
14
+
15
+ return acc
16
+ }, {})
17
+ }
18
+
19
+ function createHandlers() {
20
+ return {
21
+ auth: getEndpointsFromModel(AuthModel),
22
+ feed: getEndpointsFromModel(FeedModel),
23
+ follows: getEndpointsFromModel(FollowsModel),
24
+ livestream: getEndpointsFromModel(LivestreamModel),
25
+ playlists: getEndpointsFromModel(PlaylistsModel),
26
+ post: getEndpointsFromModel(PostModel),
27
+ session: getEndpointsFromModel(SessionModel),
28
+ sync: getEndpointsFromModel(SyncModel),
29
+ user: getEndpointsFromModel(UserModel),
30
+ }
31
+ }
32
+
33
+ export {
34
+ AuthModel,
35
+ FeedModel,
36
+ FollowsModel,
37
+ LivestreamModel,
38
+ PlaylistsModel,
39
+ PostModel,
40
+ SessionModel,
41
+ SyncModel,
42
+ UserModel,
43
+ createHandlers,
44
+ }