mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
bbb94d195e
commit
65d75ef939
1
comty.js
Submodule
1
comty.js
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 8acb3f008477bbb782eca1c7f747b494a293e57b
|
@ -1 +1 @@
|
|||||||
Subproject commit c011f2353f8db14a2ed287015d108c2620098a84
|
Subproject commit 6d553830ab4661ffab952253d77ccb0bfc1363d8
|
@ -18,7 +18,7 @@ const aliases = {
|
|||||||
layouts: path.join(__dirname, "src/layouts"),
|
layouts: path.join(__dirname, "src/layouts"),
|
||||||
hooks: path.join(__dirname, "src/hooks"),
|
hooks: path.join(__dirname, "src/hooks"),
|
||||||
classes: path.join(__dirname, "src/classes"),
|
classes: path.join(__dirname, "src/classes"),
|
||||||
"comty.js": path.join(__dirname, "../", "comty.js", "src"),
|
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
||||||
models: path.join(__dirname, "../comty.js/src/models"),
|
models: path.join(__dirname, "../comty.js/src/models"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +77,11 @@ export default [
|
|||||||
useLayout: "default",
|
useLayout: "default",
|
||||||
public: true
|
public: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/apr/*",
|
||||||
|
useLayout: "minimal",
|
||||||
|
public: true
|
||||||
|
},
|
||||||
// THIS MUST BE THE LAST ROUTE
|
// THIS MUST BE THE LAST ROUTE
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
@ -143,9 +143,7 @@ class ComtyApp extends React.Component {
|
|||||||
document.getElementById("root").classList.add("electron")
|
document.getElementById("root").classList.add("electron")
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(import.meta.env)
|
if (import.meta.env.VITE_SENTRY_DSN && import.meta.env.PROD) {
|
||||||
|
|
||||||
if (import.meta.env.VITE_SENTRY_DSN) {
|
|
||||||
console.log(`Initializing Sentry...`)
|
console.log(`Initializing Sentry...`)
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
@ -47,7 +47,7 @@ export default (props) => {
|
|||||||
|
|
||||||
console.log(`Loading Followers for [${props.user_id}]...`)
|
console.log(`Loading Followers for [${props.user_id}]...`)
|
||||||
|
|
||||||
const followers = await FollowsModel.getFollowers(props.user_id).catch((err) => {
|
const followers = await FollowsModel.getFollowers(props.user_id, true).catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
app.message.error("Failed to fetch followers")
|
app.message.error("Failed to fetch followers")
|
||||||
|
|
||||||
|
@ -40,30 +40,45 @@ export default class Login extends React.Component {
|
|||||||
loginInputs: {},
|
loginInputs: {},
|
||||||
error: null,
|
error: null,
|
||||||
phase: 0,
|
phase: 0,
|
||||||
|
mfa_required: null
|
||||||
}
|
}
|
||||||
|
|
||||||
formRef = React.createRef()
|
formRef = React.createRef()
|
||||||
|
|
||||||
handleFinish = async () => {
|
handleFinish = async () => {
|
||||||
|
this.setState({
|
||||||
|
mfa_required: false,
|
||||||
|
})
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
username: this.state.loginInputs.username,
|
username: this.state.loginInputs.username,
|
||||||
password: this.state.loginInputs.password,
|
password: this.state.loginInputs.password,
|
||||||
|
mfa_code: this.state.loginInputs.mfa_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearError()
|
this.clearError()
|
||||||
this.toggleLoading(true)
|
this.toggleLoading(true)
|
||||||
|
|
||||||
await AuthModel.login(payload, () => this.onDone()).catch((error) => {
|
await AuthModel.login(payload, this.onDone).catch((error) => {
|
||||||
console.error(error, error.response)
|
console.error(error, error.response)
|
||||||
|
|
||||||
this.toggleLoading(false)
|
this.toggleLoading(false)
|
||||||
this.onError(error.response.data.message)
|
this.onError(error.response.data.error)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onDone = async () => {
|
onDone = async ({ mfa_required } = {}) => {
|
||||||
|
if (mfa_required) {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
mfa_required: mfa_required,
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof this.props.close === "function") {
|
if (typeof this.props.close === "function") {
|
||||||
await this.props.close({
|
await this.props.close({
|
||||||
unlock: true
|
unlock: true
|
||||||
@ -77,6 +92,18 @@ export default class Login extends React.Component {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClickForgotPassword = () => {
|
||||||
|
if (this.props.locked) {
|
||||||
|
this.props.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.close === "function") {
|
||||||
|
this.props.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.location.push("/apr")
|
||||||
|
}
|
||||||
|
|
||||||
onClickRegister = () => {
|
onClickRegister = () => {
|
||||||
if (this.props.locked) {
|
if (this.props.locked) {
|
||||||
this.props.unlock()
|
this.props.unlock()
|
||||||
@ -238,6 +265,34 @@ export default class Login extends React.Component {
|
|||||||
onPressEnter={this.nextStep}
|
onPressEnter={this.nextStep}
|
||||||
/>
|
/>
|
||||||
</antd.Form.Item>
|
</antd.Form.Item>
|
||||||
|
|
||||||
|
<antd.Form.Item
|
||||||
|
name="mfa_code"
|
||||||
|
className={classnames(
|
||||||
|
"field",
|
||||||
|
{
|
||||||
|
["hidden"]: !this.state.mfa_required,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span><Icons.Lock /> Verification Code</span>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.state.mfa_required && <>
|
||||||
|
<p>We send a verification code to [{this.state.mfa_required.sended_to}]</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Didn't receive the code? <a onClick={this.handleFinish}>Resend</a>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
<antd.Input
|
||||||
|
placeholder="4 Digit MFA code"
|
||||||
|
onChange={(e) => this.onUpdateInput("mfa_code", e.target.value)}
|
||||||
|
onPressEnter={this.nextStep}
|
||||||
|
/>
|
||||||
|
</antd.Form.Item>
|
||||||
</antd.Form>
|
</antd.Form>
|
||||||
|
|
||||||
<div className="component-row">
|
<div className="component-row">
|
||||||
@ -262,6 +317,10 @@ export default class Login extends React.Component {
|
|||||||
{this.state.error}
|
{this.state.error}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="field" onClick={this.onClickForgotPassword}>
|
||||||
|
<a>Forgot your password?</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="field" onClick={this.onClickRegister}>
|
<div className="field" onClick={this.onClickRegister}>
|
||||||
<a>You need a account?</a>
|
<a>You need a account?</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,48 +29,60 @@ export default class APICore extends Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listenEvent(key, handler, instance) {
|
listenEvent(key, handler, instance) {
|
||||||
this.instance.wsInstances[instance ?? "default"].on(key, handler)
|
if (!this.instance.wsInstances[instance ?? "default"]) {
|
||||||
|
console.error(`[API] Websocket instance ${instance} not found`)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.wsInstances[instance ?? "default"].on(key, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
unlistenEvent(key, handler, instance) {
|
unlistenEvent(key, handler, instance) {
|
||||||
this.instance.wsInstances[instance ?? "default"].off(key, handler)
|
if (!this.instance.wsInstances[instance ?? "default"]) {
|
||||||
|
console.error(`[API] Websocket instance ${instance} not found`)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.instance.wsInstances[instance ?? "default"].off(key, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingPingsFromInstance = {}
|
pendingPingsFromInstance = {}
|
||||||
|
|
||||||
createPingIntervals() {
|
createPingIntervals() {
|
||||||
Object.keys(this.instance.wsInstances).forEach((instance) => {
|
// Object.keys(this.instance.wsInstances).forEach((instance) => {
|
||||||
this.console.debug(`[API] Creating ping interval for ${instance}`)
|
// this.console.debug(`[API] Creating ping interval for ${instance}`)
|
||||||
|
|
||||||
if (this.instance.wsInstances[instance].pingInterval) {
|
// if (this.instance.wsInstances[instance].pingInterval) {
|
||||||
clearInterval(this.instance.wsInstances[instance].pingInterval)
|
// clearInterval(this.instance.wsInstances[instance].pingInterval)
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.instance.wsInstances[instance].pingInterval = setInterval(() => {
|
// this.instance.wsInstances[instance].pingInterval = setInterval(() => {
|
||||||
if (this.instance.wsInstances[instance].pendingPingTry && this.instance.wsInstances[instance].pendingPingTry > 3) {
|
// if (this.instance.wsInstances[instance].pendingPingTry && this.instance.wsInstances[instance].pendingPingTry > 3) {
|
||||||
this.console.debug(`[API] Ping timeout for ${instance}`)
|
// this.console.debug(`[API] Ping timeout for ${instance}`)
|
||||||
|
|
||||||
return clearInterval(this.instance.wsInstances[instance].pingInterval)
|
// return clearInterval(this.instance.wsInstances[instance].pingInterval)
|
||||||
}
|
// }
|
||||||
|
|
||||||
const timeStart = Date.now()
|
// const timeStart = Date.now()
|
||||||
|
|
||||||
//this.console.debug(`[API] Ping ${instance}`, this.instance.wsInstances[instance].pendingPingTry)
|
// //this.console.debug(`[API] Ping ${instance}`, this.instance.wsInstances[instance].pendingPingTry)
|
||||||
|
|
||||||
this.instance.wsInstances[instance].emit("ping", () => {
|
// this.instance.wsInstances[instance].emit("ping", () => {
|
||||||
this.instance.wsInstances[instance].latency = Date.now() - timeStart
|
// this.instance.wsInstances[instance].latency = Date.now() - timeStart
|
||||||
|
|
||||||
this.instance.wsInstances[instance].pendingPingTry = 0
|
// this.instance.wsInstances[instance].pendingPingTry = 0
|
||||||
})
|
// })
|
||||||
|
|
||||||
this.instance.wsInstances[instance].pendingPingTry = this.instance.wsInstances[instance].pendingPingTry ? this.instance.wsInstances[instance].pendingPingTry + 1 : 1
|
// this.instance.wsInstances[instance].pendingPingTry = this.instance.wsInstances[instance].pendingPingTry ? this.instance.wsInstances[instance].pendingPingTry + 1 : 1
|
||||||
}, 5000)
|
// }, 5000)
|
||||||
|
|
||||||
// clear interval on close
|
// // clear interval on close
|
||||||
this.instance.wsInstances[instance].on("close", () => {
|
// this.instance.wsInstances[instance].on("close", () => {
|
||||||
clearInterval(this.instance.wsInstances[instance].pingInterval)
|
// clearInterval(this.instance.wsInstances[instance].pingInterval)
|
||||||
})
|
// })
|
||||||
})
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
@ -92,8 +104,8 @@ export default class APICore extends Core {
|
|||||||
|
|
||||||
// make a basic request to check if the API is available
|
// make a basic request to check if the API is available
|
||||||
await this.instance.instances["default"]({
|
await this.instance.instances["default"]({
|
||||||
method: "GET",
|
method: "head",
|
||||||
url: "/ping",
|
url: "/",
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.console.error("[API] Ping error", error)
|
this.console.error("[API] Ping error", error)
|
||||||
|
|
||||||
@ -105,7 +117,7 @@ export default class APICore extends Core {
|
|||||||
|
|
||||||
this.console.debug("[API] Attached to", this.instance)
|
this.console.debug("[API] Attached to", this.instance)
|
||||||
|
|
||||||
this.createPingIntervals()
|
//this.createPingIntervals()
|
||||||
|
|
||||||
return this.instance
|
return this.instance
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,10 @@ export default class Account extends React.Component {
|
|||||||
requestedUser: null,
|
requestedUser: null,
|
||||||
|
|
||||||
user: null,
|
user: null,
|
||||||
followers: [],
|
|
||||||
|
|
||||||
isSelf: false,
|
isSelf: false,
|
||||||
isFollowed: false,
|
|
||||||
|
followersCount: 0,
|
||||||
|
following: false,
|
||||||
|
|
||||||
tabActiveKey: "posts",
|
tabActiveKey: "posts",
|
||||||
|
|
||||||
@ -87,8 +87,7 @@ export default class Account extends React.Component {
|
|||||||
|
|
||||||
let isSelf = false
|
let isSelf = false
|
||||||
let user = null
|
let user = null
|
||||||
let isFollowed = false
|
let followersCount = 0
|
||||||
let followers = []
|
|
||||||
|
|
||||||
if (requestedUser != null) {
|
if (requestedUser != null) {
|
||||||
if (token.username === requestedUser) {
|
if (token.username === requestedUser) {
|
||||||
@ -113,33 +112,24 @@ export default class Account extends React.Component {
|
|||||||
|
|
||||||
console.log(`Loaded User [${user.username}] >`, user)
|
console.log(`Loaded User [${user.username}] >`, user)
|
||||||
|
|
||||||
if (!isSelf) {
|
|
||||||
const followedResult = await FollowsModel.imFollowing(user._id).catch(() => false)
|
|
||||||
|
|
||||||
if (followedResult) {
|
|
||||||
isFollowed = followedResult.isFollowed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const followersResult = await FollowsModel.getFollowers(user._id).catch(() => false)
|
const followersResult = await FollowsModel.getFollowers(user._id).catch(() => false)
|
||||||
|
|
||||||
if (followersResult) {
|
if (followersResult) {
|
||||||
followers = followersResult
|
followersCount = followersResult.count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setState({
|
await this.setState({
|
||||||
isSelf,
|
isSelf,
|
||||||
user,
|
|
||||||
requestedUser,
|
requestedUser,
|
||||||
isFollowed,
|
user,
|
||||||
followers,
|
|
||||||
|
following: user.following,
|
||||||
|
followersCount: followersCount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onPostListTopVisibility = (to) => {
|
onPostListTopVisibility = (to) => {
|
||||||
console.log("onPostListTopVisibility", to)
|
|
||||||
|
|
||||||
if (to) {
|
if (to) {
|
||||||
this.profileRef.current.classList.remove("topHidden")
|
this.profileRef.current.classList.remove("topHidden")
|
||||||
} else {
|
} else {
|
||||||
@ -149,7 +139,7 @@ export default class Account extends React.Component {
|
|||||||
|
|
||||||
onClickFollow = async () => {
|
onClickFollow = async () => {
|
||||||
const result = await FollowsModel.toggleFollow({
|
const result = await FollowsModel.toggleFollow({
|
||||||
username: this.state.requestedUser,
|
user_id: this.state.user._id,
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
antd.message.error(error.message)
|
antd.message.error(error.message)
|
||||||
@ -158,8 +148,8 @@ export default class Account extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await this.setState({
|
await this.setState({
|
||||||
isFollowed: result.following,
|
following: result.following,
|
||||||
followers: result.followers,
|
followersCount: result.count,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,9 +230,9 @@ export default class Account extends React.Component {
|
|||||||
ref={this.actionsRef}
|
ref={this.actionsRef}
|
||||||
>
|
>
|
||||||
<FollowButton
|
<FollowButton
|
||||||
count={this.state.followers.length}
|
count={this.state.followersCount}
|
||||||
onClick={this.onClickFollow}
|
onClick={this.onClickFollow}
|
||||||
followed={this.state.isFollowed}
|
followed={this.state.following}
|
||||||
self={this.state.isSelf}
|
self={this.state.isSelf}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
11
packages/app/src/pages/apr/index.jsx
Normal file
11
packages/app/src/pages/apr/index.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const AccountPasswordRecovery = () => {
|
||||||
|
return <div className="password_recover">
|
||||||
|
<h1>Account Password Recovery</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountPasswordRecovery
|
3
packages/app/src/pages/apr/index.less
Normal file
3
packages/app/src/pages/apr/index.less
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.password_recover {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
@ -46,6 +46,7 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
width: 55vw;
|
width: 55vw;
|
||||||
|
min-width: 700px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -83,6 +84,8 @@
|
|||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
min-width: 420px;
|
||||||
|
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
|
||||||
.content_header {
|
.content_header {
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "comty.js",
|
|
||||||
"version": "0.60.3",
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"author": "RageStudio <support@ragestudio.net>",
|
|
||||||
"scripts": {
|
|
||||||
"build": "hermes build"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@foxify/events": "^2.1.0",
|
|
||||||
"axios": "^1.4.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
|
||||||
"jsonwebtoken": "^9.0.0",
|
|
||||||
"jwt-decode": "^3.1.2",
|
|
||||||
"luxon": "^3.3.0",
|
|
||||||
"socket.io-client": "^4.6.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@ragestudio/hermes": "^0.1.0",
|
|
||||||
"corenode": "^0.28.26"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import request from "./request"
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
const timings = {}
|
|
||||||
|
|
||||||
const promises = [
|
|
||||||
new Promise(async (resolve) => {
|
|
||||||
const start = Date.now()
|
|
||||||
|
|
||||||
const failTimeout = setTimeout(() => {
|
|
||||||
timings.http = "failed"
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
}, 10000)
|
|
||||||
|
|
||||||
request({
|
|
||||||
method: "GET",
|
|
||||||
url: "/ping",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// set http timing in ms
|
|
||||||
timings.http = Date.now() - start
|
|
||||||
|
|
||||||
failTimeout && clearTimeout(failTimeout)
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
timings.http = "failed"
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const start = Date.now()
|
|
||||||
|
|
||||||
const failTimeout = setTimeout(() => {
|
|
||||||
timings.ws = "failed"
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
}, 10000)
|
|
||||||
|
|
||||||
__comty_shared_state.wsInstances["default"].on("pong", () => {
|
|
||||||
timings.ws = Date.now() - start
|
|
||||||
|
|
||||||
failTimeout && clearTimeout(failTimeout)
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
|
|
||||||
__comty_shared_state.wsInstances["default"].emit("ping")
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
return timings
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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"]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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)
|
|
||||||
}]
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
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 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 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalThis.isServerMode) {
|
|
||||||
sharedState.rest = 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,
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
import request from "../../handlers/request"
|
|
||||||
import SessionModel from "../session"
|
|
||||||
|
|
||||||
const emailRegex = new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
|
|
||||||
|
|
||||||
export default class AuthModel {
|
|
||||||
static login = async (payload, callback) => {
|
|
||||||
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
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
let payload = {}
|
|
||||||
|
|
||||||
// check if usename arguemnt is an email
|
|
||||||
if (emailRegex.test(username)) {
|
|
||||||
payload.email = username
|
|
||||||
} else {
|
|
||||||
payload.username = username
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await request({
|
|
||||||
method: "get",
|
|
||||||
url: "/auth/login/validation",
|
|
||||||
params: payload,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
throw new Error("Unable to validate user")
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
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 toggleFollow = 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/toggle",
|
|
||||||
data: {
|
|
||||||
user_id: user_id,
|
|
||||||
username: username
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import AuthModel from "./auth"
|
|
||||||
import FeedModel from "./feed"
|
|
||||||
import FollowsModel from "./follows"
|
|
||||||
import LivestreamModel from "./livestream"
|
|
||||||
import PostModel from "./post"
|
|
||||||
import SessionModel from "./session"
|
|
||||||
import SyncModel from "./sync"
|
|
||||||
import UserModel from "./user"
|
|
||||||
|
|
||||||
function getEndpointsFromModel(model) {
|
|
||||||
return Object.entries(model).reduce((acc, [key, value]) => {
|
|
||||||
acc[key] = value
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHandlers() {
|
|
||||||
return {
|
|
||||||
auth: getEndpointsFromModel(AuthModel),
|
|
||||||
feed: getEndpointsFromModel(FeedModel),
|
|
||||||
follows: getEndpointsFromModel(FollowsModel),
|
|
||||||
livestream: getEndpointsFromModel(LivestreamModel),
|
|
||||||
post: getEndpointsFromModel(PostModel),
|
|
||||||
session: getEndpointsFromModel(SessionModel),
|
|
||||||
sync: getEndpointsFromModel(SyncModel),
|
|
||||||
user: getEndpointsFromModel(UserModel),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
AuthModel,
|
|
||||||
FeedModel,
|
|
||||||
FollowsModel,
|
|
||||||
LivestreamModel,
|
|
||||||
PostModel,
|
|
||||||
SessionModel,
|
|
||||||
SyncModel,
|
|
||||||
UserModel,
|
|
||||||
createHandlers,
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,561 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
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 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.eventBus.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.eventBus.emit("session.destroyed")
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// alias for validateToken method
|
|
||||||
static validSession = async (token) => {
|
|
||||||
return await Session.validateToken(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
static validateToken = async (token) => {
|
|
||||||
const response = await request({
|
|
||||||
method: "post",
|
|
||||||
url: "/session/validate",
|
|
||||||
data: {
|
|
||||||
token: token
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
static isCurrentTokenValid = async () => {
|
|
||||||
const health = await Session.getTokenValidation()
|
|
||||||
|
|
||||||
return health.valid
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,172 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
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: "/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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
function composeRemote(path) {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
if (window.localStorage.getItem("comty:use_indev") || window.location.hostname === "indev.comty.app") {
|
|
||||||
return envOrigins["indev"][path]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return envOrigins[process.env.NODE_ENV ?? "production"][path]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentHostname() {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
return "localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
return window?.location?.hostname ?? "localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
const envOrigins = {
|
|
||||||
"development": {
|
|
||||||
default: `http://${getCurrentHostname()}:3010`,
|
|
||||||
chat: `http://${getCurrentHostname()}:3020`,
|
|
||||||
livestreaming: `http://${getCurrentHostname()}:3030`,
|
|
||||||
marketplace: `http://${getCurrentHostname()}:3040`,
|
|
||||||
music: `http://${getCurrentHostname()}:3050`,
|
|
||||||
files: `http://${getCurrentHostname()}:3060`,
|
|
||||||
sync: `http://${getCurrentHostname()}:3070`,
|
|
||||||
},
|
|
||||||
"indev": {
|
|
||||||
default: `https://indev_api.comty.app/main`,
|
|
||||||
chat: `https://indev_api.comty.app/chat`,
|
|
||||||
livestreaming: `https://indev_api.comty.app/livestreaming`,
|
|
||||||
marketplace: `https://indev_api.comty.app/marketplace`,
|
|
||||||
music: `https://indev_api.comty.app/music`,
|
|
||||||
files: `https://indev_api.comty.app/files`,
|
|
||||||
sync: `https://indev_api.comty.app/sync`,
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
default: "https://api.comty.app",
|
|
||||||
chat: `https://chat_api.comty.app`,
|
|
||||||
livestreaming: `https://livestreaming_api.comty.app`,
|
|
||||||
marketplace: `https://marketplace_api.comty.app`,
|
|
||||||
music: `https://music_api.comty.app`,
|
|
||||||
files: `https://files_api.comty.app`,
|
|
||||||
sync: `https://sync_api.comty.app`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
default: {
|
|
||||||
origin: composeRemote("default"),
|
|
||||||
hasWebsocket: true,
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
origin: composeRemote("chat"),
|
|
||||||
hasWebsocket: true,
|
|
||||||
},
|
|
||||||
music: {
|
|
||||||
origin: composeRemote("music"),
|
|
||||||
hasWebsocket: true,
|
|
||||||
},
|
|
||||||
livestreaming: {
|
|
||||||
origin: composeRemote("livestreaming"),
|
|
||||||
hasWebsocket: false,
|
|
||||||
},
|
|
||||||
marketplace: {
|
|
||||||
origin: composeRemote("marketplace"),
|
|
||||||
hasWebsocket: false,
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
origin: composeRemote("files"),
|
|
||||||
hasWebsocket: false,
|
|
||||||
},
|
|
||||||
sync: {
|
|
||||||
origin: composeRemote("sync"),
|
|
||||||
hasWebsocket: false,
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,6 +14,7 @@ export default {
|
|||||||
verified: { type: Boolean, default: false },
|
verified: { type: Boolean, default: false },
|
||||||
badges: { type: Array, default: [] },
|
badges: { type: Array, default: [] },
|
||||||
links: { type: Array, default: [] },
|
links: { type: Array, default: [] },
|
||||||
|
location: { type: String, default: null },
|
||||||
birthday: { type: Date, default: null },
|
birthday: { type: Date, default: null },
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,14 +9,20 @@ import { Observable } from "@gullerya/object-observer"
|
|||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import Spinnies from "spinnies"
|
import Spinnies from "spinnies"
|
||||||
import chokidar from "chokidar"
|
import chokidar from "chokidar"
|
||||||
|
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
|
||||||
|
import fastify from "fastify"
|
||||||
|
import { createProxyMiddleware } from "http-proxy-middleware"
|
||||||
|
|
||||||
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
||||||
|
|
||||||
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
|
|
||||||
import getInternalIp from "./lib/getInternalIp"
|
import getInternalIp from "./lib/getInternalIp"
|
||||||
import comtyAscii from "./ascii"
|
import comtyAscii from "./ascii"
|
||||||
import pkg from "./package.json"
|
import pkg from "./package.json"
|
||||||
|
|
||||||
|
import cors from "linebridge/src/server/middlewares/cors"
|
||||||
|
|
||||||
|
import { onExit } from "signal-exit"
|
||||||
|
|
||||||
|
|
||||||
const bootloaderBin = path.resolve(__dirname, "boot")
|
const bootloaderBin = path.resolve(__dirname, "boot")
|
||||||
const servicesPath = path.resolve(__dirname, "services")
|
const servicesPath = path.resolve(__dirname, "services")
|
||||||
|
|
||||||
@ -45,6 +51,7 @@ async function scanServices() {
|
|||||||
return finalServices
|
return finalServices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let internal_proxy = null
|
||||||
let allReady = false
|
let allReady = false
|
||||||
let selectedProcessInstance = null
|
let selectedProcessInstance = null
|
||||||
let internalIp = null
|
let internalIp = null
|
||||||
@ -158,6 +165,10 @@ async function getIgnoredFiles(cwd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleAllReady() {
|
async function handleAllReady() {
|
||||||
|
if (allReady) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
console.clear()
|
console.clear()
|
||||||
|
|
||||||
allReady = true
|
allReady = true
|
||||||
@ -199,29 +210,53 @@ async function handleServiceExit(id, code, err) {
|
|||||||
serviceRegistry[id].ready = false
|
serviceRegistry[id].ready = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// PROCESS HANDLERS
|
async function registerProxy(_path, target, pathRewrite) {
|
||||||
async function handleProcessExit(error, code) {
|
if (internal_proxy.proxys.has(_path)) {
|
||||||
if (error) {
|
console.warn(`Proxy already registered [${_path}], skipping...`)
|
||||||
console.error(error)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nPreparing to exit...`)
|
console.log(`🔗 Registering path proxy [${_path}] -> [${target}]`)
|
||||||
|
|
||||||
for await (let instance of instancePool) {
|
internal_proxy.proxys.add(_path)
|
||||||
console.log(`🛑 Killing ${instance.id} [${instance.instance.pid}]`)
|
|
||||||
await instance.instance.kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
internal_proxy.use(_path, createProxyMiddleware({
|
||||||
|
target: target,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: pathRewrite,
|
||||||
|
ws: true,
|
||||||
|
logLevel: "silent",
|
||||||
|
}))
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleIPCData(id, data) {
|
async function handleIPCData(service_id, msg) {
|
||||||
if (data.type === "log") {
|
if (msg.type === "log") {
|
||||||
console.log(`[${id}] ${data.message}`)
|
console.log(`[${service_id}] ${msg.message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.status === "ready") {
|
if (msg.status === "ready") {
|
||||||
await handleServiceStarted(id)
|
await handleServiceStarted(service_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === "router:register") {
|
||||||
|
if (msg.data.path_overrides) {
|
||||||
|
for await (let pathOverride of msg.data.path_overrides) {
|
||||||
|
await registerProxy(
|
||||||
|
`/${pathOverride}`,
|
||||||
|
`http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
|
||||||
|
{
|
||||||
|
[`^/${pathOverride}`]: "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await registerProxy(
|
||||||
|
`/${service_id}`,
|
||||||
|
`http://${msg.data.listen.ip}:${msg.data.listen.port}`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,6 +340,31 @@ function createServiceLogTransformer({ id, color = "bgCyan" }) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
internalIp = await getInternalIp()
|
internalIp = await getInternalIp()
|
||||||
|
|
||||||
|
internal_proxy = fastify()
|
||||||
|
|
||||||
|
internal_proxy.proxys = new Set()
|
||||||
|
|
||||||
|
await internal_proxy.register(require("@fastify/middie"))
|
||||||
|
|
||||||
|
await internal_proxy.use(cors)
|
||||||
|
|
||||||
|
internal_proxy.get("/ping", (request, reply) => {
|
||||||
|
return reply.send({
|
||||||
|
status: "ok"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
internal_proxy.get("/", (request, reply) => {
|
||||||
|
return reply.send({
|
||||||
|
services: instancePool.map((instance) => {
|
||||||
|
return {
|
||||||
|
id: instance.id,
|
||||||
|
version: instance.version,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
console.clear()
|
console.clear()
|
||||||
console.log(comtyAscii)
|
console.log(comtyAscii)
|
||||||
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
|
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
|
||||||
@ -323,7 +383,7 @@ async function main() {
|
|||||||
const instanceFile = path.basename(service)
|
const instanceFile = path.basename(service)
|
||||||
const instanceBasePath = path.dirname(service)
|
const instanceBasePath = path.dirname(service)
|
||||||
|
|
||||||
const { name: id, version, proxy } = require(path.resolve(instanceBasePath, "package.json"))
|
const { name: id, version } = require(path.resolve(instanceBasePath, "package.json"))
|
||||||
|
|
||||||
serviceFileReference[instanceFile] = id
|
serviceFileReference[instanceFile] = id
|
||||||
|
|
||||||
@ -333,7 +393,6 @@ async function main() {
|
|||||||
version: version,
|
version: version,
|
||||||
file: instanceFile,
|
file: instanceFile,
|
||||||
cwd: instanceBasePath,
|
cwd: instanceBasePath,
|
||||||
proxy: proxy,
|
|
||||||
buffer: [],
|
buffer: [],
|
||||||
ready: false,
|
ready: false,
|
||||||
}
|
}
|
||||||
@ -413,14 +472,29 @@ async function main() {
|
|||||||
|
|
||||||
return command_fn.fn(callback, ...args)
|
return command_fn.fn(callback, ...args)
|
||||||
}
|
}
|
||||||
}).on("exit", () => {
|
})
|
||||||
process.exit(0)
|
|
||||||
|
await internal_proxy.listen({
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 9000
|
||||||
|
})
|
||||||
|
|
||||||
|
onExit((code, signal) => {
|
||||||
|
console.clear()
|
||||||
|
console.log(`\n🛑 Preparing to exit...`)
|
||||||
|
|
||||||
|
console.log(`Stoping proxy...`)
|
||||||
|
|
||||||
|
internal_proxy.close()
|
||||||
|
|
||||||
|
console.log(`Kill all ${instancePool.length} instances...`)
|
||||||
|
|
||||||
|
for (let instance of instancePool) {
|
||||||
|
console.log(`Killing ${instance.id} [${instance.instance.pid}]`)
|
||||||
|
|
||||||
|
instance.instance.kill()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on("exit", handleProcessExit)
|
|
||||||
process.on("SIGINT", handleProcessExit)
|
|
||||||
process.on("uncaughtException", handleProcessExit)
|
|
||||||
process.on("unhandledRejection", handleProcessExit)
|
|
||||||
|
|
||||||
main()
|
main()
|
@ -1,120 +0,0 @@
|
|||||||
require("dotenv").config()
|
|
||||||
|
|
||||||
const path = require("path")
|
|
||||||
const { registerBaseAliases } = require("linebridge/dist/server")
|
|
||||||
const { webcrypto: crypto } = require("crypto")
|
|
||||||
const infisical = require("infisical-node")
|
|
||||||
|
|
||||||
global.isProduction = process.env.NODE_ENV === "production"
|
|
||||||
|
|
||||||
globalThis["__root"] = path.resolve(process.cwd())
|
|
||||||
globalThis["__src"] = path.resolve(globalThis["__root"], global.isProduction ? "dist" : "src")
|
|
||||||
|
|
||||||
const customAliases = {
|
|
||||||
"root": globalThis["__root"],
|
|
||||||
"src": globalThis["__src"],
|
|
||||||
"@shared-classes": path.resolve(globalThis["__src"], "_shared/classes"),
|
|
||||||
"@services": path.resolve(globalThis["__src"], "services"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!global.isProduction) {
|
|
||||||
customAliases["comty.js"] = path.resolve(globalThis["__src"], "../../comty.js/src")
|
|
||||||
customAliases["@shared-classes"] = path.resolve(globalThis["__src"], "shared-classes")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.USE_LINKED_SHARED) {
|
|
||||||
customAliases["@shared-classes"] = path.resolve(globalThis["__src"], "shared-classes")
|
|
||||||
}
|
|
||||||
|
|
||||||
registerBaseAliases(globalThis["__src"], customAliases)
|
|
||||||
|
|
||||||
// patches
|
|
||||||
const { Buffer } = require("buffer")
|
|
||||||
|
|
||||||
global.b64Decode = (data) => {
|
|
||||||
return Buffer.from(data, "base64").toString("utf-8")
|
|
||||||
}
|
|
||||||
global.b64Encode = (data) => {
|
|
||||||
return Buffer.from(data, "utf-8").toString("base64")
|
|
||||||
}
|
|
||||||
|
|
||||||
global.nanoid = (t = 21) => crypto.getRandomValues(new Uint8Array(t)).reduce(((t, e) => t += (e &= 63) < 36 ? e.toString(36) : e < 62 ? (e - 26).toString(36).toUpperCase() : e > 62 ? "-" : "_"), "");
|
|
||||||
|
|
||||||
Array.prototype.updateFromObjectKeys = function (obj) {
|
|
||||||
this.forEach((value, index) => {
|
|
||||||
if (obj[value] !== undefined) {
|
|
||||||
this[index] = obj[value]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
global.toBoolean = (value) => {
|
|
||||||
if (typeof value === "boolean") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.toLowerCase() === "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function injectEnvFromInfisical() {
|
|
||||||
const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev"
|
|
||||||
|
|
||||||
console.log(`🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`)
|
|
||||||
|
|
||||||
const client = new infisical({
|
|
||||||
token: process.env.INFISICAL_TOKEN,
|
|
||||||
})
|
|
||||||
|
|
||||||
const secrets = await client.getAllSecrets({
|
|
||||||
environment: envMode,
|
|
||||||
attachToProcessEnv: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// inject to process.env
|
|
||||||
secrets.forEach((secret) => {
|
|
||||||
if (!(process.env[secret.secretName])) {
|
|
||||||
process.env[secret.secretName] = secret.secretValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleExit(instance, code) {
|
|
||||||
if (instance.server) {
|
|
||||||
if (typeof instance.server.close === "function") {
|
|
||||||
instance.server.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main({
|
|
||||||
force_infisical,
|
|
||||||
} = {}) {
|
|
||||||
const API = require(path.resolve(globalThis["__src"], "api.js")).default
|
|
||||||
|
|
||||||
if (force_infisical || process.env.INFISICAL_TOKEN) {
|
|
||||||
await injectEnvFromInfisical()
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = new API()
|
|
||||||
|
|
||||||
process.on("exit", () => handleExit(instance, 0))
|
|
||||||
process.on("SIGINT", () => handleExit(instance, 0))
|
|
||||||
process.on("uncaughtException", () => handleExit(instance, 1))
|
|
||||||
process.on("unhandledRejection", () => handleExit(instance, 1))
|
|
||||||
|
|
||||||
await instance.initialize()
|
|
||||||
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error(`🆘 [FATAL ERROR] >`, error)
|
|
||||||
})
|
|
38
packages/server/lib/handleWsAuth/index.js
Normal file
38
packages/server/lib/handleWsAuth/index.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import AuthToken from "../../classes/AuthToken"
|
||||||
|
import { User } from "../../db_models"
|
||||||
|
|
||||||
|
export default async (socket, token, err) => {
|
||||||
|
try {
|
||||||
|
const validation = await AuthToken.validate(token)
|
||||||
|
|
||||||
|
if (!validation.valid) {
|
||||||
|
if (validation.error) {
|
||||||
|
return err(`auth:server_error`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(`auth:token_invalid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await User.findById(validation.data.user_id).catch((err) => {
|
||||||
|
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return err(`auth:user_failed`)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.userData = userData
|
||||||
|
socket.token = token
|
||||||
|
socket.session = validation.data
|
||||||
|
|
||||||
|
return {
|
||||||
|
token: token,
|
||||||
|
username: userData.username,
|
||||||
|
user_id: userData._id,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return err(`auth:authentification_failed`, error)
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,8 @@
|
|||||||
"run:prod": "cross-env NODE_ENV=production node ./dist/index.js"
|
"run:prod": "cross-env NODE_ENV=production node ./dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cors": "^9.0.1",
|
||||||
|
"@fastify/middie": "^8.3.0",
|
||||||
"@gullerya/object-observer": "^6.1.3",
|
"@gullerya/object-observer": "^6.1.3",
|
||||||
"@infisical/sdk": "^2.1.8",
|
"@infisical/sdk": "^2.1.8",
|
||||||
"@ragestudio/hermes": "^0.1.1",
|
"@ragestudio/hermes": "^0.1.1",
|
||||||
@ -21,12 +23,14 @@
|
|||||||
"cli-color": "^2.0.3",
|
"cli-color": "^2.0.3",
|
||||||
"clui": "^0.3.6",
|
"clui": "^0.3.6",
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.4",
|
||||||
|
"fastify": "^4.26.2",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy-middleware": "^2.0.6",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"linebridge": "^0.16.0",
|
"linebridge": "^0.18.1",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"p-map": "^4.0.0",
|
"p-map": "^4.0.0",
|
||||||
"p-queue": "^7.3.4",
|
"p-queue": "^7.3.4",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
"spinnies": "^0.5.1"
|
"spinnies": "^0.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -4,4 +4,6 @@ export default class Account {
|
|||||||
static loginStrategy = require("./methods/loginStrategy").default
|
static loginStrategy = require("./methods/loginStrategy").default
|
||||||
static changePassword = require("./methods/changePassword").default
|
static changePassword = require("./methods/changePassword").default
|
||||||
static create = require("./methods/create").default
|
static create = require("./methods/create").default
|
||||||
|
static sessions = require("./methods/sessions").default
|
||||||
|
static deleteSession = require("./methods/deleteSession").default
|
||||||
}
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Session } from "@db_models"
|
||||||
|
|
||||||
|
export default async (payload = {}) => {
|
||||||
|
const { user_id, token } = payload
|
||||||
|
|
||||||
|
if (!user_id) {
|
||||||
|
throw new OperationError(400, "user_id not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new OperationError(400, "token not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await Session.findOne({
|
||||||
|
user_id,
|
||||||
|
token
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
throw new OperationError(400, "Session not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.delete()
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { Session } from "@db_models"
|
||||||
|
|
||||||
|
export default async (payload = {}) => {
|
||||||
|
const { user_id } = payload
|
||||||
|
|
||||||
|
if (!user_id) {
|
||||||
|
throw new OperationError(400, "user_id not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions = await Session.find({ user_id })
|
||||||
|
|
||||||
|
return sessions
|
||||||
|
}
|
@ -2,5 +2,9 @@
|
|||||||
"name": "auth",
|
"name": "auth",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"proxy":{
|
||||||
|
"namespace": "/auth",
|
||||||
|
"port": 3020
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import { User } from "@db_models"
|
||||||
|
|
||||||
|
const emailRegex = new RegExp(/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)
|
||||||
|
|
||||||
|
export default async (req) => {
|
||||||
|
const { username } = req.params
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
username
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailRegex.test(username)) {
|
||||||
|
delete query.username
|
||||||
|
query.email = username
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exists: !!await User.exists(query)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import AuthToken from "@shared-classes/AuthToken"
|
import AuthToken from "@shared-classes/AuthToken"
|
||||||
import { UserConfig, MFASession } from "@db_models"
|
import { UserConfig, MFASession } from "@db_models"
|
||||||
import requiredFields from "@shared-utils/requiredFields"
|
import requiredFields from "@shared-utils/requiredFields"
|
||||||
|
import obscureEmail from "@shared-utils/obscureEmail"
|
||||||
|
|
||||||
import Account from "@classes/account"
|
import Account from "@classes/account"
|
||||||
|
|
||||||
@ -66,6 +67,8 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
message: `MFA required, using [${mfa.type}] method.`,
|
message: `MFA required, using [${mfa.type}] method.`,
|
||||||
|
method: mfa.type,
|
||||||
|
sended_to: obscureEmail(user.email),
|
||||||
mfa_required: true,
|
mfa_required: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
packages/server/services/auth/routes/sessions/all/get.js
Normal file
10
packages/server/services/auth/routes/sessions/all/get.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import AccountClass from "@classes/account"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
middlewares: ["withAuthentication"],
|
||||||
|
fn: async (req, res) => {
|
||||||
|
return await AccountClass.sessions({
|
||||||
|
user_id: req.auth.session.user_id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import AccountClass from "@classes/account"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
middlewares: ["withAuthentication"],
|
||||||
|
fn: async (req, res) => {
|
||||||
|
return await AccountClass.deleteSession({
|
||||||
|
user_id: req.auth.session.user_id,
|
||||||
|
token: req.auth.token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
method: "GET",
|
|
||||||
route: "/current",
|
|
||||||
middlewares: ["withAuthentication"],
|
middlewares: ["withAuthentication"],
|
||||||
fn: async (req, res) => {
|
fn: async (req, res) => {
|
||||||
return res.json(req.currentSession)
|
return req.auth.session
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,12 +3,14 @@ import { User } from "@db_models"
|
|||||||
import templates from "../templates"
|
import templates from "../templates"
|
||||||
|
|
||||||
export default async (ctx, data) => {
|
export default async (ctx, data) => {
|
||||||
const user = await User.findById(data.user_id)
|
const user = await User.findById(data.user_id).select("+email")
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new OperationError(404, "User not found")
|
throw new OperationError(404, "User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Sending MFA code to ${user.email}...`)
|
||||||
|
|
||||||
const result = await ctx.mailTransporter.sendMail({
|
const result = await ctx.mailTransporter.sendMail({
|
||||||
from: process.env.SMTP_USERNAME,
|
from: process.env.SMTP_USERNAME,
|
||||||
to: user.email,
|
to: user.email,
|
||||||
|
@ -2,7 +2,7 @@ import { User } from "@db_models"
|
|||||||
import templates from "../templates"
|
import templates from "../templates"
|
||||||
|
|
||||||
export default async (ctx, data) => {
|
export default async (ctx, data) => {
|
||||||
const user = await User.findById(data.user_id)
|
const user = await User.findById(data.user_id).select("+email")
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new OperationError(404, "User not found")
|
throw new OperationError(404, "User not found")
|
||||||
|
@ -2,7 +2,7 @@ import { User } from "@db_models"
|
|||||||
import templates from "../templates"
|
import templates from "../templates"
|
||||||
|
|
||||||
export default async (ctx, data) => {
|
export default async (ctx, data) => {
|
||||||
const user = await User.findById(data.user_id)
|
const user = await User.findById(data.user_id).select("+email")
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new OperationError(404, "User not found")
|
throw new OperationError(404, "User not found")
|
||||||
|
@ -12,7 +12,7 @@ class API extends Server {
|
|||||||
static refName = "files"
|
static refName = "files"
|
||||||
static useEngine = "hyper-express"
|
static useEngine = "hyper-express"
|
||||||
static routesPath = `${__dirname}/routes`
|
static routesPath = `${__dirname}/routes`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3008
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
|
||||||
|
|
||||||
static maxBodyLength = 1000 * 1000 * 1000
|
static maxBodyLength = 1000 * 1000 * 1000
|
||||||
|
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { Schematized } from "@lib"
|
|
||||||
import newComment from "../services/newComment"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/post/:post_id",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
required: ["message"],
|
|
||||||
select: ["message"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
const { post_id } = req.params
|
|
||||||
const { message } = req.selection
|
|
||||||
|
|
||||||
try {
|
|
||||||
const comment = await newComment({
|
|
||||||
user_id: req.user._id.toString(),
|
|
||||||
parent_id: post_id,
|
|
||||||
message: message,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(comment)
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: error.message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import deleteComment from "../services/deleteComment"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "DELETE",
|
|
||||||
route: "/post/:post_id/:comment_id",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const result = await deleteComment({
|
|
||||||
comment_id: req.params.comment_id,
|
|
||||||
issuer_id: req.user._id.toString(),
|
|
||||||
}).catch((err) => {
|
|
||||||
res.status(500).json({ message: err.message })
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
return res.json(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import getComments from "../services/getComments"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/post/:post_id",
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const { post_id } = req.params
|
|
||||||
|
|
||||||
const comments = await getComments({ parent_id: post_id }).catch((err) => {
|
|
||||||
res.status(400).json({
|
|
||||||
error: err.message,
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!comments) return
|
|
||||||
|
|
||||||
return res.json(comments)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
|
|
||||||
|
|
||||||
export default class CommentsController extends Controller {
|
|
||||||
static refName = "CommentsController"
|
|
||||||
static useRoute = "/comments"
|
|
||||||
|
|
||||||
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import { Comment } from "@db_models"
|
|
||||||
import CheckUserAdmin from "../../../lib/checkUserAdmin"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
const { issuer_id, comment_id } = payload
|
|
||||||
|
|
||||||
if (!issuer_id) {
|
|
||||||
throw new Error("Missing issuer_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!comment_id) {
|
|
||||||
throw new Error("Missing comment_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = await CheckUserAdmin(issuer_id)
|
|
||||||
|
|
||||||
const comment = await Comment.findById(comment_id)
|
|
||||||
|
|
||||||
if (!comment) {
|
|
||||||
throw new Error("Comment not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comment.user_id !== issuer_id && !isAdmin) {
|
|
||||||
throw new Error("You can't delete this comment, cause you are not the owner.")
|
|
||||||
}
|
|
||||||
|
|
||||||
await comment.delete()
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`post.delete.comment.${comment.parent_id.toString()}`, comment_id)
|
|
||||||
|
|
||||||
return comment
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { User, Comment } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload = {}) => {
|
|
||||||
const { parent_id } = payload
|
|
||||||
|
|
||||||
if (!parent_id) {
|
|
||||||
throw new Error("Missing parent_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get comments by descending order
|
|
||||||
let comments = await Comment.find({ parent_id: parent_id })
|
|
||||||
.sort({ created_at: -1 })
|
|
||||||
|
|
||||||
// fullfill comments with user data
|
|
||||||
comments = await Promise.all(comments.map(async comment => {
|
|
||||||
const user = await User.findById(comment.user_id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...comment.toObject(),
|
|
||||||
user: user.toObject(),
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return comments
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { User, Comment } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
const { parent_id, message, user_id } = payload
|
|
||||||
|
|
||||||
if (!parent_id) {
|
|
||||||
throw new Error("Missing parent_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
throw new Error("Missing message")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user_id) {
|
|
||||||
throw new Error("Missing user_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
const comment = new Comment({
|
|
||||||
user_id: user_id,
|
|
||||||
parent_id: parent_id,
|
|
||||||
message: message,
|
|
||||||
})
|
|
||||||
|
|
||||||
await comment.save()
|
|
||||||
|
|
||||||
const userData = await User.findById(user_id)
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`post.new.comment.${parent_id}`, {
|
|
||||||
...comment.toObject(),
|
|
||||||
user: userData.toObject(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return comment
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
|
|
||||||
import pmap from "p-map"
|
|
||||||
|
|
||||||
import getPosts from "./services/getPosts"
|
|
||||||
|
|
||||||
import getGlobalReleases from "./services/getGlobalReleases"
|
|
||||||
import getReleasesFromFollowing from "./services/getReleasesFromFollowing"
|
|
||||||
import getPlaylistsFromFollowing from "./services/getPlaylistsFromFollowing"
|
|
||||||
|
|
||||||
export default class FeedController extends Controller {
|
|
||||||
static refName = "FeedController"
|
|
||||||
static useRoute = "/feed"
|
|
||||||
|
|
||||||
httpEndpoints = {
|
|
||||||
get: {
|
|
||||||
"/timeline": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const for_user_id = req.user?._id.toString()
|
|
||||||
|
|
||||||
if (!for_user_id) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "Invalid user id"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch posts
|
|
||||||
let posts = await getPosts({
|
|
||||||
for_user_id,
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
})
|
|
||||||
|
|
||||||
// add type to posts and playlists
|
|
||||||
posts = posts.map((data) => {
|
|
||||||
data.type = "post"
|
|
||||||
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
|
|
||||||
let feed = [
|
|
||||||
...posts,
|
|
||||||
]
|
|
||||||
|
|
||||||
// sort feed
|
|
||||||
feed.sort((a, b) => {
|
|
||||||
return new Date(b.created_at) - new Date(a.created_at)
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(feed)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/music/global": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const for_user_id = req.user?._id.toString()
|
|
||||||
|
|
||||||
if (!for_user_id) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "Invalid user id"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch playlists from global
|
|
||||||
const result = await getGlobalReleases({
|
|
||||||
for_user_id,
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(result)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/music": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const for_user_id = req.user?._id.toString()
|
|
||||||
|
|
||||||
if (!for_user_id) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "Invalid user id"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchers = [
|
|
||||||
getGlobalReleases,
|
|
||||||
//getReleasesFromFollowing,
|
|
||||||
//getPlaylistsFromFollowing,
|
|
||||||
]
|
|
||||||
|
|
||||||
let result = await pmap(
|
|
||||||
searchers,
|
|
||||||
async (fn, index) => {
|
|
||||||
const data = await fn({
|
|
||||||
for_user_id,
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
}, {
|
|
||||||
concurrency: 3,
|
|
||||||
},)
|
|
||||||
|
|
||||||
result = result.reduce((acc, cur) => {
|
|
||||||
return [...acc, ...cur]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return res.json(result)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/posts": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const for_user_id = req.user?._id.toString()
|
|
||||||
|
|
||||||
if (!for_user_id) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "Invalid user id"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let feed = []
|
|
||||||
|
|
||||||
// fetch posts
|
|
||||||
const posts = await getPosts({
|
|
||||||
for_user_id,
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
})
|
|
||||||
|
|
||||||
feed = feed.concat(posts)
|
|
||||||
|
|
||||||
return res.json(feed)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { Release } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
const {
|
|
||||||
limit = 20,
|
|
||||||
skip = 0,
|
|
||||||
} = payload
|
|
||||||
|
|
||||||
let releases = await Release.find({
|
|
||||||
$or: [
|
|
||||||
{ public: true },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.sort({ created_at: -1 })
|
|
||||||
.limit(limit)
|
|
||||||
.skip(skip)
|
|
||||||
|
|
||||||
releases = Promise.all(releases.map(async (release) => {
|
|
||||||
release = release.toObject()
|
|
||||||
|
|
||||||
return release
|
|
||||||
}))
|
|
||||||
|
|
||||||
return releases
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
import { Post, UserFollow } from "@db_models"
|
|
||||||
|
|
||||||
import fullfillPostsData from "@utils/fullfillPostsData"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
const {
|
|
||||||
for_user_id,
|
|
||||||
limit = 20,
|
|
||||||
skip = 0,
|
|
||||||
} = payload
|
|
||||||
|
|
||||||
// get post from users that the user follows
|
|
||||||
const followingUsers = await UserFollow.find({
|
|
||||||
user_id: for_user_id
|
|
||||||
})
|
|
||||||
|
|
||||||
const followingUserIds = followingUsers.map((followingUser) => followingUser.to)
|
|
||||||
|
|
||||||
const fetchPostsFromIds = [
|
|
||||||
for_user_id,
|
|
||||||
...followingUserIds,
|
|
||||||
]
|
|
||||||
|
|
||||||
let posts = await Post.find({
|
|
||||||
user_id: { $in: fetchPostsFromIds }
|
|
||||||
})
|
|
||||||
.sort({ created_at: -1 })
|
|
||||||
.limit(limit)
|
|
||||||
.skip(skip)
|
|
||||||
|
|
||||||
// fullfill data
|
|
||||||
posts = await fullfillPostsData({
|
|
||||||
posts,
|
|
||||||
for_user_id,
|
|
||||||
skip,
|
|
||||||
})
|
|
||||||
|
|
||||||
return posts
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { UserFollow } from "@db_models"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/user/:user_id",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const isFollowed = await UserFollow.findOne({
|
|
||||||
user_id: req.user._id.toString(),
|
|
||||||
to: req.params.user_id,
|
|
||||||
}).catch(() => false)
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
isFollowed: Boolean(isFollowed),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { User, UserFollow } from "@db_models"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/user/:user_id/followers",
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const { limit = 30, offset } = req.query
|
|
||||||
|
|
||||||
let followers = []
|
|
||||||
|
|
||||||
const follows = await UserFollow.find({
|
|
||||||
to: req.params.user_id,
|
|
||||||
})
|
|
||||||
.limit(limit)
|
|
||||||
.skip(offset)
|
|
||||||
|
|
||||||
for await (const follow of follows) {
|
|
||||||
const user = await User.findById(follow.user_id)
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
followers.push(user.toObject())
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(followers)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
import { Schematized } from "@lib"
|
|
||||||
import { User, UserFollow } from "@db_models"
|
|
||||||
|
|
||||||
import followUser from "../services/followUser"
|
|
||||||
import unfollowUser from "../services/unfollowUser"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/user/toggle",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
select: ["user_id", "username"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
const selfUserId = req.user._id.toString()
|
|
||||||
|
|
||||||
let targetUserId = null
|
|
||||||
let result = null
|
|
||||||
|
|
||||||
if (typeof req.selection.user_id === "undefined" && typeof req.selection.username === "undefined") {
|
|
||||||
return res.status(400).json({ message: "No user_id or username provided" })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof req.selection.user_id !== "undefined") {
|
|
||||||
targetUserId = req.selection.user_id
|
|
||||||
} else {
|
|
||||||
const user = await User.findOne({ username: req.selection.username })
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ message: "User not found" })
|
|
||||||
}
|
|
||||||
|
|
||||||
targetUserId = user._id.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if already following
|
|
||||||
const isFollowed = await UserFollow.findOne({
|
|
||||||
user_id: selfUserId,
|
|
||||||
to: targetUserId,
|
|
||||||
})
|
|
||||||
|
|
||||||
// if already following, delete
|
|
||||||
if (isFollowed) {
|
|
||||||
result = await unfollowUser({
|
|
||||||
user_id: selfUserId,
|
|
||||||
to: targetUserId,
|
|
||||||
}).catch((error) => {
|
|
||||||
return res.status(500).json({ message: error.message })
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
result = await followUser({
|
|
||||||
user_id: selfUserId,
|
|
||||||
to: targetUserId,
|
|
||||||
}).catch((error) => {
|
|
||||||
return res.status(500).json({ message: error.message })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(result)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
|
|
||||||
|
|
||||||
export default class FollowController extends Controller {
|
|
||||||
static refName = "FollowController"
|
|
||||||
static useRoute = "/follow"
|
|
||||||
|
|
||||||
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import { User, UserFollow } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
if (typeof payload.user_id === "undefined") {
|
|
||||||
throw new Error("No user_id provided")
|
|
||||||
}
|
|
||||||
if (typeof payload.to === "undefined") {
|
|
||||||
throw new Error("No to provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findById(payload.user_id)
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const follow = await UserFollow.findOne({
|
|
||||||
user_id: payload.user_id,
|
|
||||||
to: payload.to,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (follow) {
|
|
||||||
throw new Error("Already following")
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFollow = await UserFollow.create({
|
|
||||||
user_id: payload.user_id,
|
|
||||||
to: payload.to,
|
|
||||||
})
|
|
||||||
|
|
||||||
await newFollow.save()
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`user.follow`, {
|
|
||||||
...user.toObject(),
|
|
||||||
})
|
|
||||||
global.engine.ws.io.of("/").emit(`user.follow.${payload.user_id}`, {
|
|
||||||
...user.toObject(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const followers = await UserFollow.find({
|
|
||||||
to: payload.to,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
following: true,
|
|
||||||
followers: followers,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import { User, UserFollow } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
if (typeof payload.user_id === "undefined") {
|
|
||||||
throw new Error("No user_id provided")
|
|
||||||
}
|
|
||||||
if (typeof payload.to === "undefined") {
|
|
||||||
throw new Error("No to provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findById(payload.user_id)
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const follow = await UserFollow.findOne({
|
|
||||||
user_id: payload.user_id,
|
|
||||||
to: payload.to,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!follow) {
|
|
||||||
throw new Error("Not following")
|
|
||||||
}
|
|
||||||
|
|
||||||
await follow.remove()
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`user.unfollow`, {
|
|
||||||
...user.toObject(),
|
|
||||||
})
|
|
||||||
global.engine.ws.io.of("/").emit(`user.unfollow.${payload.user_id}`, {
|
|
||||||
...user.toObject(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const followers = await UserFollow.find({
|
|
||||||
to: payload.to,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
following: false,
|
|
||||||
followers: followers,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
import { Post, } from "@db_models"
|
|
||||||
import toggleLike from "../../PostsController/services/toggleLike"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/:post_id/mok_likes",
|
|
||||||
middlewares: ["withAuthentication", "onlyAdmin"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const {
|
|
||||||
count,
|
|
||||||
interval = 100,
|
|
||||||
} = req.body
|
|
||||||
|
|
||||||
if (count < 1) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: "Invalid count, must be greater than 0",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let postData = await Post.findById(req.params.post_id)
|
|
||||||
|
|
||||||
if (!postData) {
|
|
||||||
return res.status(404).json({
|
|
||||||
error: "Post not found",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const mokUserId = `mok_${i}_${count}`
|
|
||||||
|
|
||||||
toggleLike({
|
|
||||||
post_id: postData._id.toString(),
|
|
||||||
user_id: mokUserId,
|
|
||||||
to: true
|
|
||||||
})
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, interval ?? 100))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
message: "Success",
|
|
||||||
data: postData
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
|
|
||||||
|
|
||||||
export default class ModerationController extends Controller {
|
|
||||||
static refName = "ModerationController"
|
|
||||||
static useRoute = "/mod"
|
|
||||||
static reachable = false
|
|
||||||
|
|
||||||
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import { Schematized } from "@lib"
|
|
||||||
import { CreatePost } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/new",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
required: ["timestamp"],
|
|
||||||
select: ["message", "attachments", "timestamp", "reply_to"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
const post = await CreatePost({
|
|
||||||
user_id: req.user._id.toString(),
|
|
||||||
message: req.selection.message,
|
|
||||||
timestamp: req.selection.timestamp,
|
|
||||||
attachments: req.selection.attachments,
|
|
||||||
reply_to: req.selection.reply_to,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(post)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { DeletePost } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "DELETE",
|
|
||||||
route: "/:post_id",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const post = await DeletePost({
|
|
||||||
post_id: req.params.post_id,
|
|
||||||
by_user_id: req.user._id.toString(),
|
|
||||||
}).catch((err) => {
|
|
||||||
res.status(400).json({
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post) return
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
post
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { Schematized } from "@lib"
|
|
||||||
import { GetPostData } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/explore",
|
|
||||||
middlewares: ["withOptionalAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
select: ["user_id"]
|
|
||||||
}, async (req, res) => {
|
|
||||||
let posts = await GetPostData({
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
from_user_id: req.query?.user_id,
|
|
||||||
for_user_id: req.user?._id.toString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(posts)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { GetPostData } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/post/:post_id",
|
|
||||||
middlewares: ["withOptionalAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
let post = await GetPostData({
|
|
||||||
post_id: req.params.post_id,
|
|
||||||
for_user_id: req.user?._id.toString(),
|
|
||||||
}).catch((error) => {
|
|
||||||
res.status(404).json({ error: error.message })
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post) return
|
|
||||||
|
|
||||||
return res.json(post)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { GetPostData } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/user/:user_id",
|
|
||||||
middlewares: ["withOptionalAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
let posts = await GetPostData({
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
for_user_id: req.user?._id.toString(),
|
|
||||||
from_user_id: req.params.user_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(posts)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { Post } from "@db_models"
|
|
||||||
import fullfillPostsData from "@utils/fullfillPostsData"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/post/:post_id/replies",
|
|
||||||
middlewares: ["withOptionalAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const {
|
|
||||||
limit = 50,
|
|
||||||
offset = 0,
|
|
||||||
} = req.query
|
|
||||||
|
|
||||||
let replies = await Post.find({
|
|
||||||
reply_to: req.params.post_id,
|
|
||||||
})
|
|
||||||
.skip(offset)
|
|
||||||
.limit(limit)
|
|
||||||
.sort({ created_at: -1 })
|
|
||||||
|
|
||||||
replies = await fullfillPostsData({
|
|
||||||
posts: replies,
|
|
||||||
for_user_id: req.user?._id.toString(),
|
|
||||||
skip: offset,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(replies)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { Schematized } from "@lib"
|
|
||||||
import { GetPostData } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/saved",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
select: ["user_id"]
|
|
||||||
}, async (req, res) => {
|
|
||||||
let posts = await GetPostData({
|
|
||||||
limit: req.query?.limit,
|
|
||||||
skip: req.query?.trim,
|
|
||||||
for_user_id: req.user?._id.toString(),
|
|
||||||
savedOnly: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json(posts)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { Schematized } from "@lib"
|
|
||||||
import { ToogleLike } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/:post_id/toggle_like",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
select: ["to"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
const post = await ToogleLike({
|
|
||||||
user_id: req.user._id.toString(),
|
|
||||||
post_id: req.params.post_id,
|
|
||||||
to: req.selection.to,
|
|
||||||
}).catch((err) => {
|
|
||||||
res.status(400).json({
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post) return
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
post
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { ToogleSavePost } from "../services"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/:post_id/toggle_save",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const post = await ToogleSavePost({
|
|
||||||
user_id: req.user._id.toString(),
|
|
||||||
post_id: req.params.post_id,
|
|
||||||
}).catch((err) => {
|
|
||||||
res.status(400).json({
|
|
||||||
error: err.message
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post) return
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
post
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
|
|
||||||
|
|
||||||
export default class PostsController extends Controller {
|
|
||||||
static refName = "PostsController"
|
|
||||||
static useRoute = "/posts"
|
|
||||||
|
|
||||||
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
|
|
||||||
|
|
||||||
// put = {
|
|
||||||
// "/:post_id": {
|
|
||||||
// middlewares: ["withAuthentication"],
|
|
||||||
// fn: (req, res) => {
|
|
||||||
// // TODO: Implement Post update
|
|
||||||
// return res.status(501).json({ error: "Not implemented" })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import momentTimezone from "moment-timezone"
|
|
||||||
import { Post } from "@db_models"
|
|
||||||
|
|
||||||
import getPostData from "./getPostData"
|
|
||||||
import flagNsfwByAttachments from "./flagNsfwByAttachments"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
let { user_id, message, attachments, timestamp, reply_to } = payload
|
|
||||||
|
|
||||||
// check if is a Array and have at least one element
|
|
||||||
const isAttachmentsValid = Array.isArray(attachments) && attachments.length > 0
|
|
||||||
|
|
||||||
if (!isAttachmentsValid && !message) {
|
|
||||||
throw new Error("Cannot create a post without message or attachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
message = String(message).toString().trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const current_timezone = momentTimezone.tz.guess()
|
|
||||||
const created_at = momentTimezone.tz(Date.now(), current_timezone).format()
|
|
||||||
|
|
||||||
const post = new Post({
|
|
||||||
created_at: created_at,
|
|
||||||
user_id: typeof user_id === "object" ? user_id.toString() : user_id,
|
|
||||||
message: message,
|
|
||||||
attachments: attachments ?? [],
|
|
||||||
timestamp: timestamp,
|
|
||||||
reply_to: reply_to,
|
|
||||||
flags: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
await post.save()
|
|
||||||
|
|
||||||
const resultPost = await getPostData({ post_id: post._id.toString() })
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`post.new`, resultPost)
|
|
||||||
global.engine.ws.io.of("/").emit(`post.new.${post.user_id}`, resultPost)
|
|
||||||
|
|
||||||
// push to background job to check if is NSFW
|
|
||||||
flagNsfwByAttachments(post._id.toString())
|
|
||||||
|
|
||||||
return post
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import { Post, User } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
const { post_id, by_user_id } = payload
|
|
||||||
|
|
||||||
if (!by_user_id) {
|
|
||||||
throw new Error("by_user_id not provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = await Post.findById(post_id)
|
|
||||||
|
|
||||||
if (!post) {
|
|
||||||
throw new Error("Post not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await User.findById(by_user_id)
|
|
||||||
|
|
||||||
if (!userData) {
|
|
||||||
throw new Error("User not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAdmin = userData.roles.includes("admin")
|
|
||||||
|
|
||||||
// check if user is the owner of the post
|
|
||||||
if (post.user_id !== by_user_id && !hasAdmin) {
|
|
||||||
throw new Error("You are not allowed to delete this post")
|
|
||||||
}
|
|
||||||
|
|
||||||
await post.remove()
|
|
||||||
global.engine.ws.io.of("/").emit(`post.delete`, post_id)
|
|
||||||
|
|
||||||
return post.toObject()
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import { Post } from "@db_models"
|
|
||||||
import indecentPrediction from "../../../utils/indecent-prediction"
|
|
||||||
import isNSFW from "../../../utils/is-nsfw"
|
|
||||||
|
|
||||||
import modifyPostData from "./modifyPostData"
|
|
||||||
|
|
||||||
export default async (post_id) => {
|
|
||||||
try {
|
|
||||||
if (!post_id) {
|
|
||||||
throw new Error("Post ID is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
let post = await Post.findById(post_id)
|
|
||||||
|
|
||||||
if (!post) {
|
|
||||||
throw new Error("Post not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = []
|
|
||||||
|
|
||||||
// run indecentPrediction to all attachments
|
|
||||||
if (Array.isArray(post.attachments) && post.attachments.length > 0) {
|
|
||||||
for await (const attachment of post.attachments) {
|
|
||||||
const prediction = await indecentPrediction({
|
|
||||||
url: attachment.url,
|
|
||||||
}).catch((err) => {
|
|
||||||
console.log("Error while checking", attachment, err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (prediction) {
|
|
||||||
const isNsfw = isNSFW(prediction)
|
|
||||||
|
|
||||||
if (isNsfw) {
|
|
||||||
flags.push("nsfw")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if is there new flags update post
|
|
||||||
if (post.flags !== flags) {
|
|
||||||
await modifyPostData(post_id, {
|
|
||||||
flags: flags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return flags
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import { Post, SavedPost } from "@db_models"
|
|
||||||
import fullfillPostsData from "@utils/fullfillPostsData"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
let {
|
|
||||||
from_user_id,
|
|
||||||
for_user_id,
|
|
||||||
post_id,
|
|
||||||
query = {},
|
|
||||||
skip = 0,
|
|
||||||
limit = 20,
|
|
||||||
sort = { created_at: -1 },
|
|
||||||
savedOnly = false,
|
|
||||||
} = payload
|
|
||||||
|
|
||||||
let posts = []
|
|
||||||
let savedPostsIds = []
|
|
||||||
|
|
||||||
// if for_user_id is provided, get saved posts
|
|
||||||
if (for_user_id) {
|
|
||||||
const savedPosts = await SavedPost.find({ user_id: for_user_id })
|
|
||||||
.sort({ saved_at: -1 })
|
|
||||||
|
|
||||||
savedPostsIds = savedPosts.map((savedPost) => savedPost.post_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if from_user_id is provided, get posts from that user
|
|
||||||
if (from_user_id) {
|
|
||||||
query.user_id = from_user_id
|
|
||||||
}
|
|
||||||
|
|
||||||
// if savedOnly is true,set to query to get only saved posts
|
|
||||||
if (savedOnly) {
|
|
||||||
query._id = { $in: savedPostsIds }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (post_id) {
|
|
||||||
const post = await Post.findById(post_id).catch(() => false)
|
|
||||||
|
|
||||||
posts = [post]
|
|
||||||
} else {
|
|
||||||
posts = await Post.find({ ...query })
|
|
||||||
.sort(sort)
|
|
||||||
.skip(skip)
|
|
||||||
.limit(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// short posts if is savedOnly argument
|
|
||||||
if (savedOnly) {
|
|
||||||
posts.sort((a, b) => {
|
|
||||||
return (
|
|
||||||
savedPostsIds.indexOf(a._id.toString()) -
|
|
||||||
savedPostsIds.indexOf(b._id.toString())
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// fullfill data
|
|
||||||
posts = await fullfillPostsData({
|
|
||||||
posts,
|
|
||||||
for_user_id,
|
|
||||||
skip,
|
|
||||||
})
|
|
||||||
|
|
||||||
// if post_id is specified, return only one post
|
|
||||||
if (post_id) {
|
|
||||||
return posts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return posts
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
export { default as CreatePost } from "./createPost"
|
|
||||||
export { default as ToogleLike } from "./toggleLike"
|
|
||||||
export { default as ToogleSavePost } from "./togglePostSave"
|
|
||||||
|
|
||||||
export { default as GetPostData } from "./getPostData"
|
|
||||||
export { default as DeletePost } from "./deletePost"
|
|
||||||
|
|
||||||
export { default as ModifyPostData } from "./modifyPostData"
|
|
@ -1,31 +0,0 @@
|
|||||||
import { Post } from "@db_models"
|
|
||||||
import getPostData from "./getPostData"
|
|
||||||
|
|
||||||
export default async (post_id, modification) => {
|
|
||||||
if (!post_id) {
|
|
||||||
throw new Error("Cannot modify post data: post not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
let post = await getPostData({ post_id: post_id })
|
|
||||||
|
|
||||||
if (!post) {
|
|
||||||
throw new Error("Cannot modify post data: post not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof modification === "object") {
|
|
||||||
const result = await Post.findByIdAndUpdate(post_id, modification)
|
|
||||||
|
|
||||||
await result.save()
|
|
||||||
|
|
||||||
post = {
|
|
||||||
...post,
|
|
||||||
...result.toObject(),
|
|
||||||
...modification,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`post.dataUpdate`, post)
|
|
||||||
global.engine.ws.io.of("/").emit(`post.dataUpdate.${post_id}`, post)
|
|
||||||
|
|
||||||
return post
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import { PostLike } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
let { post_id, user_id, to } = payload
|
|
||||||
|
|
||||||
let likeObj = await PostLike.findOne({
|
|
||||||
post_id,
|
|
||||||
user_id,
|
|
||||||
}).catch(() => false)
|
|
||||||
|
|
||||||
if (typeof to === "undefined") {
|
|
||||||
if (likeObj) {
|
|
||||||
to = false
|
|
||||||
} else {
|
|
||||||
to = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (to) {
|
|
||||||
likeObj = new PostLike({
|
|
||||||
post_id,
|
|
||||||
user_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
await likeObj.save()
|
|
||||||
} else {
|
|
||||||
await PostLike.findByIdAndDelete(likeObj._id)
|
|
||||||
}
|
|
||||||
|
|
||||||
global.engine.ws.io.of("/").emit(`post.${post_id}.likes.update`, {
|
|
||||||
to,
|
|
||||||
post_id,
|
|
||||||
user_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
return likeObj
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { SavedPost } from "@db_models"
|
|
||||||
|
|
||||||
export default async (payload) => {
|
|
||||||
let { post_id, user_id } = payload
|
|
||||||
|
|
||||||
if (!post_id || !user_id) {
|
|
||||||
throw new Error("Missing post_id or user_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
let post = await SavedPost.findOne({ post_id, user_id }).catch((err) => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (post) {
|
|
||||||
await SavedPost.findByIdAndDelete(post._id).catch((err) => {
|
|
||||||
throw new Error("Cannot delete saved post")
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
post = new SavedPost({
|
|
||||||
post_id,
|
|
||||||
user_id,
|
|
||||||
})
|
|
||||||
|
|
||||||
await post.save().catch((err) => {
|
|
||||||
throw new Error("Cannot save post")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return post
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
import { Role, User } from "@db_models"
|
|
||||||
import { Schematized } from "@lib"
|
|
||||||
|
|
||||||
export default class RolesController extends Controller {
|
|
||||||
static refName = "RolesController"
|
|
||||||
static useMiddlewares = ["roles"]
|
|
||||||
|
|
||||||
httpEndpoints = {
|
|
||||||
get: {
|
|
||||||
"/roles": Schematized({
|
|
||||||
select: ["user_id", "username"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
const roles = await Role.find()
|
|
||||||
|
|
||||||
return res.json(roles)
|
|
||||||
}),
|
|
||||||
"/roles/self": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const user = await User.findOne({
|
|
||||||
_id: req.user._id.toString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ error: "No user founded" })
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(user.roles)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
post: {
|
|
||||||
"/role": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
required: ["name"],
|
|
||||||
select: ["name", "description"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
await Role.findOne(req.selection).then((data) => {
|
|
||||||
if (data) {
|
|
||||||
return res.status(409).json("This role is already created")
|
|
||||||
}
|
|
||||||
|
|
||||||
let role = new Role({
|
|
||||||
name: req.selection.name,
|
|
||||||
description: req.selection.description,
|
|
||||||
})
|
|
||||||
|
|
||||||
role.save()
|
|
||||||
|
|
||||||
return res.json(role)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"/update_user_roles": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
required: ["update"],
|
|
||||||
select: ["update"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
// check if issuer user is admin
|
|
||||||
if (!req.isAdmin()) {
|
|
||||||
return res.status(403).json("You do not have administrator permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(req.selection.update)) {
|
|
||||||
return res.status(400).json("Invalid update request")
|
|
||||||
}
|
|
||||||
|
|
||||||
req.selection.update.forEach(async (update) => {
|
|
||||||
const user = await User.findById(update._id).catch(err => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(update.roles)
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
user.roles = update.roles
|
|
||||||
|
|
||||||
await user.save()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json("done")
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: {
|
|
||||||
"/role": {
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: Schematized({
|
|
||||||
required: ["name"],
|
|
||||||
select: ["name"],
|
|
||||||
}, async (req, res) => {
|
|
||||||
if (req.selection.name === "admin") {
|
|
||||||
return res.status(409).json("You can't delete admin role")
|
|
||||||
}
|
|
||||||
|
|
||||||
await Role.findOne(req.selection).then((data) => {
|
|
||||||
if (!data) {
|
|
||||||
return res.status(404).json("This role is not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
data.remove()
|
|
||||||
|
|
||||||
return res.json(data)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
export default {
|
|
||||||
method: "DELETE",
|
|
||||||
route: "/all",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const user_id = req.user._id.toString()
|
|
||||||
|
|
||||||
const allSessions = await Session.deleteMany({ user_id })
|
|
||||||
|
|
||||||
if (allSessions) {
|
|
||||||
return res.json("done")
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(404).json("not found")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { Session } from "@db_models"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "DELETE",
|
|
||||||
route: "/current",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const token = req.jwtToken
|
|
||||||
const user_id = req.user._id.toString()
|
|
||||||
|
|
||||||
if (typeof token === "undefined") {
|
|
||||||
return res.status(400).json("Cannot access to token")
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await Session.findOneAndDelete({ user_id, token })
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
return res.json({
|
|
||||||
message: "done",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(404).json({
|
|
||||||
error: "Session not found",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { Session } from "@db_models"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/all",
|
|
||||||
middlewares: ["withAuthentication"],
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const sessions = await Session.find({ user_id: req.user._id.toString() }, { token: 0 })
|
|
||||||
.sort({ date: -1 })
|
|
||||||
|
|
||||||
return res.json(sessions)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import Token from "@lib/token"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/regenerate",
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const { expiredToken, refreshToken } = req.body
|
|
||||||
|
|
||||||
const token = await Token.regenerate(expiredToken, refreshToken).catch((error) => {
|
|
||||||
res.status(400).json({ error: error.message })
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!token) return
|
|
||||||
|
|
||||||
return res.json({ token })
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import Token from "@lib/token"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "POST",
|
|
||||||
route: "/validate",
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const token = req.body.token
|
|
||||||
|
|
||||||
const result = await Token.validate(token)
|
|
||||||
|
|
||||||
return res.json(result)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
import { Controller } from "linebridge/dist/server"
|
|
||||||
import generateEndpointsFromDir from "linebridge/dist/server/lib/generateEndpointsFromDir"
|
|
||||||
|
|
||||||
export default class SessionController extends Controller {
|
|
||||||
static refName = "SessionController"
|
|
||||||
static useRoute = "/session"
|
|
||||||
|
|
||||||
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import { User } from "@db_models"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
method: "GET",
|
|
||||||
route: "/email_available",
|
|
||||||
fn: async (req, res) => {
|
|
||||||
const user = await User.findOne({
|
|
||||||
email: req.query.email,
|
|
||||||
})
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
available: !user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user