merge from local

This commit is contained in:
SrGooglo 2024-03-20 23:14:10 +00:00
parent 500fa23384
commit d6a074a859
40 changed files with 463 additions and 563 deletions

@ -1 +1 @@
Subproject commit 6d553830ab4661ffab952253d77ccb0bfc1363d8
Subproject commit c4b0fbafea7b240eda4c7fa4a6a119e9c4b93dbe

View File

@ -141,6 +141,11 @@ export default class Login extends React.Component {
}
onUpdateInput = (input, value) => {
if (input === "username") {
value = value.toLowerCase()
value = value.trim()
}
// remove error from ref
this.formRef.current.setFields([
{

View File

@ -0,0 +1,33 @@
import React from "react"
import { createIconRender } from "components/Icon"
import "./index.less"
const PollOption = (props) => {
return <div className="poll-option">
<div className="label">
{
createIconRender(props.option.icon)
}
<span>
{props.option.label}
</span>
</div>
</div>
}
const Poll = (props) => {
return <div className="poll">
{
props.options.map((option) => {
return <PollOption
key={option.id}
option={option}
/>
})
}
</div>
}
export default Poll

View File

@ -0,0 +1,26 @@
.poll {
display: flex;
flex-direction: column;
gap: 8px;
.poll-option {
display: flex;
flex-direction: row;
width: 100%;
padding: 10px 20px;
transition: all 150ms ease-in-out;
background-color: var(--background-color-accent);
border-radius: 12px;
cursor: pointer;
&:hover {
background-color: var(--background-color-accent-hover);
}
}
}

View File

@ -1,28 +1,14 @@
import React from "react"
import { DateTime } from "luxon"
import { Tag, Skeleton } from "antd"
import { Tag } from "antd"
import { Image } from "components"
import { Icons } from "components/Icons"
import PostLink from "components/PostLink"
import PostService from "models/post"
import PostReplieView from "components/PostReplieView"
import "./index.less"
const PostReplieView = (props) => {
const { data } = props
if (!data) {
return null
}
return <div>
@{data.user.username}
{data.message}
</div>
}
const PostCardHeader = (props) => {
const [timeAgo, setTimeAgo] = React.useState(0)
@ -60,11 +46,13 @@ const PostCardHeader = (props) => {
!props.disableReplyTag && props.postData.reply_to && <div
className="post-header-replied_to"
>
<Icons.Repeat />
<div className="post-header-replied_to-label">
<Icons.Repeat />
<span>
Replied to
</span>
<span>
Replied to
</span>
</div>
<PostReplieView
data={props.postData.reply_to_data}
@ -83,7 +71,7 @@ const PostCardHeader = (props) => {
<div className="post-header-user-info">
<h1 onClick={goToProfile}>
{
props.postData.user?.public_name ?? `${props.postData.user?.username}`
props.postData.user?.public_name ?? `@${props.postData.user?.username}`
}
{

View File

@ -6,18 +6,25 @@
.post-header-replied_to {
display: flex;
flex-direction: row;
align-items: center;
flex-direction: column;
gap: 7px;
svg {
color: var(--text-color);
margin: 0 !important;
}
.post-header-replied_to-label {
display: flex;
flex-direction: row;
line-height: 1.5rem;
gap: 7px;
align-items: center;
svg {
color: var(--text-color);
margin: 0 !important;
}
line-height: 1.5rem;
}
}
.post-header-user {

View File

@ -10,7 +10,6 @@ import PostActions from "./components/actions"
import PostAttachments from "./components/attachments"
import "./index.less"
import { Divider } from "antd"
const messageRegexs = [
{
@ -230,12 +229,13 @@ export default class PostCard extends React.PureComponent {
/>
{
!this.props.disableHasReplies && this.state.hasReplies && <>
<Divider />
<h1>View replies</h1>
</>
!this.props.disableHasReplies && !!this.state.hasReplies && <div
className="post-card-has_replies"
onClick={() => app.navigation.goToPost(this.state.data._id)}
>
<span>View {this.state.hasReplies} replies</span>
</div>
}
</motion.div>
}
}

View File

@ -48,6 +48,11 @@
.message {
white-space: break-spaces;
user-select: text;
text-wrap: balance;
word-break: break-word;
overflow: hidden;
}
.nsfw_alert {
@ -78,4 +83,26 @@
}
}
}
.post-card-has_replies {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
padding: 10px;
gap: 10px;
transition: all 150ms ease-in-out;
cursor: pointer;
border-radius: 8px;
background-color: rgba(var(--layoutBackgroundColor), 0.7);
&:hover {
background-color: rgba(var(--layoutBackgroundColor), 1);
}
}
}

View File

@ -4,6 +4,8 @@ import classnames from "classnames"
import humanSize from "@tsmx/human-readable"
import PostLink from "components/PostLink"
import { Icons } from "components/Icons"
import { DateTime } from "luxon"
import lodash from "lodash"
import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
import PostModel from "models/post"
@ -16,7 +18,6 @@ const DEFAULT_POST_POLICY = {
maximunFilesPerRequest: 10
}
export default class PostCreator extends React.Component {
state = {
pending: [],
@ -76,10 +77,18 @@ export default class PostCreator extends React.Component {
return true
}
submit = async () => {
if (!this.canSubmit()) return
debounceSubmit = lodash.debounce(() => this.submit(), 50)
this.setState({
submit = async () => {
if (this.state.loading) {
return false
}
if (!this.canSubmit()) {
return false
}
await this.setState({
loading: true,
uploaderVisible: false
})
@ -89,7 +98,7 @@ export default class PostCreator extends React.Component {
const payload = {
message: postMessage,
attachments: postAttachments,
//timestamp: DateTime.local().toISO(),
timestamp: DateTime.local().toISO(),
}
let response = null
@ -255,13 +264,17 @@ export default class PostCreator extends React.Component {
})
}
handleKeyDown = (e) => {
handleKeyDown = async (e) => {
// detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault()
e.stopPropagation()
this.submit()
if (this.state.loading) {
return false
}
return await this.debounceSubmit()
}
}
@ -551,7 +564,7 @@ export default class PostCreator extends React.Component {
<antd.Button
type="primary"
disabled={loading || !this.canSubmit()}
onClick={this.submit}
onClick={this.debounceSubmit}
icon={loading ? <Icons.LoadingOutlined spin /> : (editMode ? <Icons.MdEdit /> : <Icons.Send />)}
/>
</div>

View File

@ -0,0 +1,51 @@
import React from "react"
import { Image } from "components"
import { Icons } from "components/Icons"
import "./index.less"
const PostReplieView = (props) => {
const { data } = props
if (!data) {
return null
}
return <div
className="post-replie-view"
onClick={() => app.navigation.goToPost(data._id)}
>
<div className="user">
<Image
src={data.user.avatar}
alt={data.user.username}
/>
<span>
{
data.user.public_name ?? `@${data.user.username}`
}
{
data.user.verified && <Icons.verifiedBadge />
}
</span>
</div>
<div className="content">
<span>
{data.message}
{
data.message.length === 0 && data.attachments.length > 0 && <>
<Icons.MdAttachment />
Image
</>
}
</span>
</div>
</div>
}
export default PostReplieView

View File

@ -0,0 +1,72 @@
.post-replie-view {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background-color: rgba(var(--layoutBackgroundColor), 0.7);
border-radius: 12px;
transition: all 150ms ease-in-out;
cursor: pointer;
.user {
display: flex;
flex-direction: row;
align-items: center;
gap: 7px;
.lazy-load-image-background {
width: 20px;
height: 20px;
border-radius: 4px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
span {
display: flex;
flex-direction: row;
align-items: center;
gap: 3px;
}
}
.content {
position: relative;
width: 100%;
overflow: hidden;
span {
display: flex;
flex-direction: row;
align-items: center;
gap: 3px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
&:hover {
background-color: rgba(var(--layoutBackgroundColor), 0.9);
}
}

View File

@ -43,6 +43,7 @@ const Entry = React.memo((props) => {
key: data._id,
data: data,
disableReplyTag: props.disableReplyTag,
disableHasReplies: props.disableHasReplies,
events: {
onClickLike: props.onLikePost,
onClickSave: props.onSavePost,
@ -447,6 +448,7 @@ export class PostsListsComponent extends React.Component {
list: this.state.list,
disableReplyTag: this.props.disableReplyTag,
disableHasReplies: this.props.disableHasReplies,
onLikePost: this.onLikePost,
onSavePost: this.onSavePost,

View File

@ -28,7 +28,7 @@ export default class RemoteStorage extends Core {
service = "standard",
} = {},
) {
return new Promise((_resolve, _reject) => {
return await new Promise((_resolve, _reject) => {
const fn = async () => new Promise((resolve, reject) => {
const uploader = new ChunkedUpload({
endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
@ -41,7 +41,7 @@ export default class RemoteStorage extends Core {
uploader.on("error", ({ message }) => {
this.console.error("[Uploader] Error", message)
app.notification.new({
app.cores.notifications.new({
title: "Could not upload file",
description: message
}, {
@ -65,7 +65,7 @@ export default class RemoteStorage extends Core {
uploader.on("finish", (data) => {
this.console.debug("[Uploader] Finish", data)
app.notification.new({
app.cores.notifications.new({
title: "File uploaded",
}, {
type: "success"

View File

@ -44,7 +44,7 @@ class MusicSyncSubCore {
"invite:received": (data) => {
this.console.log("invite:received", data)
app.notification.new({
app.cores.notifications.new({
title: "Sync",
description: `${data.invitedBy.username} invited you to join a sync room`,
icon: React.createElement(Image, {
@ -91,7 +91,7 @@ class MusicSyncSubCore {
this.dettachCard()
this.currentRoomData = null
app.notification.new({
app.cores.notifications.new({
title: "Sync",
description: "Disconnected from sync server"
}, {
@ -177,7 +177,7 @@ class MusicSyncSubCore {
app.cores.player.toggleSyncMode(false, false)
app.notification.new({
app.cores.notifications.new({
title: "Kicked",
description: "You have been kicked from the sync room"
}, {

View File

@ -107,7 +107,7 @@ export default class WidgetsCore extends Core {
store.set(WidgetsCore.storeKey, currentStore)
app.notification.new({
app.cores.notifications.new({
title: params.update ? "Widget updated" : "Widget installed",
description: `Widget [${manifest.name}] has been ${params.update ? "updated" : "installed"}. ${params.update ? `Using current version ${manifest.version}` : ""}`,
}, {
@ -141,7 +141,7 @@ export default class WidgetsCore extends Core {
store.set(WidgetsCore.storeKey, newStore)
app.notification.new({
app.cores.notifications.new({
title: "Widget uninstalled",
description: `Widget [${widget_id}] has been uninstalled.`,
}, {

View File

@ -53,9 +53,9 @@ const EmailStepComponent = (props) => {
})
if (request) {
setEmailAvailable(request.available)
setEmailAvailable(!request.exist)
if (!request.available) {
if (request.exist) {
antd.message.error("Email is already in use")
props.updateValue(null)
} else {

View File

@ -102,10 +102,12 @@ export const UsernameStepComponent = (props) => {
return false
})
if (request) {
setUsernameAvailable(request.available)
console.log(request)
if (!request.available) {
if (request) {
setUsernameAvailable(!request.exists)
if (request.exists) {
props.updateValue(null)
} else {
props.updateValue(username)

View File

@ -0,0 +1,22 @@
import Poll from "components/Poll"
const PollsDebug = (props) => {
return <Poll
options={[
{
id: "option_1",
label: "I like Comty"
},
{
id: "option_2",
label: "I don't like Comty"
},
{
id: "option_3",
label: "I prefer Twitter"
}
]}
/>
}
export default PollsDebug

View File

@ -21,6 +21,7 @@ const emptyListRender = () => {
export class Feed extends React.Component {
render() {
return <PostsList
disableHasReplies
ref={this.props.innerRef}
emptyListRender={emptyListRender}
loadFromModel={FeedModel.getTimelineFeed}

View File

@ -9,6 +9,7 @@ import "./index.less"
export default class ExplorePosts extends React.Component {
render() {
return <PostsList
disableHasReplies
loadFromModel={Feed.getGlobalTimelineFeed}
watchTimeline={[
"post.new",

View File

@ -1,6 +1,8 @@
import React from "react"
import * as antd from "antd"
import { Icons } from "components/Icons"
import PostCard from "components/PostCard"
import PostsList from "components/PostsList"
@ -8,7 +10,7 @@ import PostService from "models/post"
import "./index.less"
export default (props) => {
const PostPage = (props) => {
const post_id = props.params.post_id
const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
@ -29,22 +31,31 @@ export default (props) => {
return <div className="post-page">
<div className="post-page-original">
<h1>Post</h1>
<h1>
<Icons.MdTextSnippet />
Post
</h1>
<PostCard
data={result}
disableHasReplies
/>
</div>
<div className="post-page-replies">
<h1>Replies</h1>
<PostsList
disableReplyTag
loadFromModel={PostService.replies}
loadFromModelProps={{
post_id,
}}
/>
</div>
{
!!result.hasReplies && <div className="post-page-replies">
<h1><Icons.Repeat />Replies</h1>
<PostsList
disableReplyTag
loadFromModel={PostService.replies}
loadFromModelProps={{
post_id,
}}
/>
</div>
}
</div>
}
}
export default PostPage

View File

@ -42,14 +42,23 @@ export function createAssembleChunksPromise({
return () => new Promise(async (resolve, reject) => {
let fileSize = 0
if (!fs.existsSync(chunksPath)) {
return reject(new OperationError(500,"No chunks found"))
}
const chunks = await fs.promises.readdir(chunksPath)
if (chunks.length === 0) {
throw new Error("No chunks found")
throw new OperationError(500, "No chunks found")
}
for await (const chunk of chunks) {
const chunkPath = path.join(chunksPath, chunk)
if (!fs.existsSync(chunkPath)) {
return reject(new OperationError(500, "No chunk data found"))
}
const data = await fs.promises.readFile(chunkPath)
fileSize += data.length
@ -85,13 +94,17 @@ export async function handleChunkFile(fileStream, { tmpDir, headers, maxFileSize
// make sure chunk is in range
if (chunkCount < 0 || chunkCount >= totalChunks) {
throw new Error("Chunk is out of range")
throw new OperationError(500, "Chunk is out of range")
}
// if is the first chunk check if dir exists before write things
if (chunkCount === 0) {
if (!await fs.promises.stat(chunksPath).catch(() => false)) {
await fs.promises.mkdir(chunksPath, { recursive: true })
try {
if (!await fs.promises.stat(chunksPath).catch(() => false)) {
await fs.promises.mkdir(chunksPath, { recursive: true })
}
} catch (error) {
return reject(new OperationError(500, error.message))
}
}

View File

@ -16,5 +16,6 @@ export default {
links: { type: Array, default: [] },
location: { type: String, default: null },
birthday: { type: Date, default: null, select: false },
accept_tos: { type: Boolean, default: false },
}
}

View File

@ -6,9 +6,9 @@ import Account from "@classes/account"
export default async (payload) => {
requiredFields(["username", "password", "email"], payload)
let { username, password, email, public_name, roles, avatar, acceptTos } = payload
let { username, password, email, public_name, roles, avatar, accept_tos } = payload
if (ToBoolean(acceptTos) !== true) {
if (ToBoolean(accept_tos) !== true) {
throw new OperationError(400, "You must accept the terms of service in order to create an account.")
}
@ -44,7 +44,7 @@ export default async (payload) => {
avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles,
created_at: new Date().getTime(),
acceptTos: acceptTos,
accept_tos: accept_tos,
})
await user.save()

View File

@ -37,14 +37,18 @@ export default {
cachePath: tmpPath,
})
fs.promises.rm(tmpPath, { recursive: true, force: true })
fs.promises.rm(tmpPath, { recursive: true, force: true }).catch(() => {
return false
})
return result
} catch (error) {
fs.promises.rm(tmpPath, { recursive: true, force: true })
fs.promises.rm(tmpPath, { recursive: true, force: true }).catch(() => {
return false
})
throw new OperationError(error.code ?? 500, error.message ?? "Failed to upload file")
}
}
}
return {

View File

@ -35,6 +35,10 @@ export async function b2Upload({
bucketId: process.env.B2_BUCKET_ID,
})
if (!fs.existsSync(source)) {
throw new OperationError(500, "File not found")
}
const data = await fs.promises.readFile(source)
await global.b2Storage.uploadFile({

View File

@ -1,25 +0,0 @@
export default async function (req, res, next) {
// extract authentification header
let auth = req.headers.authorization
if (!auth) {
return res.status(401).json({ error: "Unauthorized, missing token" })
}
auth = auth.replace("Bearer ", "")
// check if authentification is valid
const validation = await comty.rest.session.validateToken(auth).catch((error) => {
return {
valid: false,
}
})
if (!validation.valid) {
return res.status(401).json({ error: "Unauthorized" })
}
req.session = validation.data
return next()
}

View File

@ -1,26 +0,0 @@
export default async function (req, res, next) {
// extract authentification header
let auth = req.headers.authorization
if (!auth) {
return next()
}
auth = auth.replace("Bearer ", "")
// check if authentification is valid
const validation = await comty.rest.session.validateToken(auth).catch((error) => {
return {
valid: false,
}
})
if (!validation.valid) {
return next()
}
req.sessionToken = auth
req.session = validation.data
return next()
}

View File

@ -1,55 +0,0 @@
export default async (socket, next) => {
try {
const token = socket.handshake.auth.token
if (!token) {
return next(new Error(`auth:token_missing`))
}
const validation = await global.comty.rest.session.validateToken(token).catch((err) => {
console.error(`[${socket.id}] failed to validate session caused by server error`, err)
return {
valid: false,
error: err,
}
})
if (!validation.valid) {
if (validation.error) {
return next(new Error(`auth:server_error`))
}
return next(new Error(`auth:token_invalid`))
}
const session = validation.data
const userData = await global.comty.rest.user.data({
user_id: session.user_id,
}).catch((err) => {
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
return null
})
if (!userData) {
return next(new Error(`auth:user_failed`))
}
try {
socket.userData = userData
socket.token = token
socket.session = session
}
catch (err) {
return next(new Error(`auth:decode_failed`))
}
next()
} catch (error) {
console.error(`[${socket.id}] failed to connect caused by server error`, error)
next(new Error(`auth:authentification_failed`))
}
}

View File

@ -1,188 +1,29 @@
import fs from "fs"
import path from "path"
import { Server } from "linebridge/src/server"
import express from "express"
import http from "http"
import EventEmitter from "@foxify/events"
import ComtyClient from "@shared-classes/ComtyClient"
import DbManager from "@shared-classes/DbManager"
import RedisClient from "@shared-classes/RedisClient"
import StorageClient from "@shared-classes/StorageClient"
import WebsocketServer from "./ws"
import SharedMiddlewares from "@shared-middlewares"
import LimitsClass from "@shared-classes/Limits"
import pkg from "./package.json"
export default class API extends Server {
static refName = "music"
static useEngine = "hyper-express"
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3003
export default class API {
static useMiddlewaresOrder = ["useLogger", "useCors", "useAuth", "useErrorHandler"]
eventBus = global.eventBus = new EventEmitter()
internalRouter = express.Router()
constructor(options = {}) {
this.server = express()
this._http = http.createServer(this.server)
this.websocketServer = global.ws = new WebsocketServer(this._http)
this.options = {
listenHost: process.env.HTTP_LISTEN_IP ?? "0.0.0.0",
listenPort: process.env.HTTP_LISTEN_PORT ?? 3003,
...options
}
middlewares = {
...SharedMiddlewares
}
comty = global.comty = ComtyClient()
db = new DbManager()
redis = global.redis = RedisClient({
withWsAdapter: true
})
storage = global.storage = StorageClient()
async __registerControllers() {
let controllersPath = fs.readdirSync(path.resolve(__dirname, "controllers"))
this.internalRouter.routes = []
for await (const controllerPath of controllersPath) {
const controller = require(path.resolve(__dirname, "controllers", controllerPath)).default
if (!controller) {
console.error(`Controller ${controllerPath} not found.`)
continue
}
const handler = await controller(express.Router())
if (!handler) {
console.error(`Controller ${controllerPath} returning not valid handler.`)
continue
}
// let middlewares = []
// if (Array.isArray(handler.useMiddlewares)) {
// middlewares = await getMiddlewares(handler.useMiddlewares)
// }
// for (const middleware of middlewares) {
// handler.router.use(middleware)
// }
this.internalRouter.use(handler.path ?? "/", handler.router)
this.internalRouter.routes.push({
path: handler.path ?? "/",
routers: handler.router.routes
})
continue
}
contexts = {
db: new DbManager(),
limits: {},
}
async __registerInternalMiddlewares() {
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares"))
async onInitialize() {
await this.contexts.db.initialize()
// sort middlewares
if (this.constructor.useMiddlewaresOrder) {
middlewaresPath = middlewaresPath.sort((a, b) => {
const aIndex = this.constructor.useMiddlewaresOrder.indexOf(a.replace(".js", ""))
const bIndex = this.constructor.useMiddlewaresOrder.indexOf(b.replace(".js", ""))
if (aIndex === -1) {
return 1
}
if (bIndex === -1) {
return -1
}
return aIndex - bIndex
})
}
for await (const middlewarePath of middlewaresPath) {
const middleware = require(path.resolve(__dirname, "useMiddlewares", middlewarePath)).default
if (!middleware) {
console.error(`Middleware ${middlewarePath} not found.`)
continue
}
this.server.use(middleware)
}
}
__registerInternalRoutes() {
this.internalRouter.get("/", (req, res) => {
return res.status(200).json({
name: pkg.name,
version: pkg.version,
})
})
this.internalRouter.get("/_routes", (req, res) => {
return res.status(200).json(this.__getRegisteredRoutes(this.internalRouter.routes))
})
this.internalRouter.get("*", (req, res) => {
return res.status(404).json({
error: "Not found",
})
})
}
__getRegisteredRoutes(router) {
return router.map((entry) => {
if (Array.isArray(entry.routers)) {
return {
path: entry.path,
routes: this.__getRegisteredRoutes(entry.routers),
}
}
return {
method: entry.method,
path: entry.path,
}
})
}
initialize = async () => {
const startHrTime = process.hrtime()
await this.websocketServer.initialize()
// initialize clients
await this.db.initialize()
await this.redis.initialize()
await this.storage.initialize()
// register controllers & middlewares
this.server.use(express.json({ extended: false }))
this.server.use(express.urlencoded({ extended: true }))
await this.__registerControllers()
await this.__registerInternalMiddlewares()
await this.__registerInternalRoutes()
this.server.use(this.internalRouter)
await this._http.listen(this.options.listenPort, this.options.listenHost)
// calculate elapsed time
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
// log server started
console.log(`🚀 Server started ready on \n\t - http://${this.options.listenHost}:${this.options.listenPort} \n\t - Tooks ${elapsedTimeInMs}ms`)
this.contexts.limits = await LimitsClass.get()
}
}

View File

@ -0,0 +1,66 @@
import { Playlist, Release, Track } from "@db_models"
export default {
middlewares: ["withAuthentication"],
fn: async (req) => {
const { keywords, limit = 10, offset = 0 } = req.query
const user_id = req.auth.session.user_id
let searchQuery = {
user_id,
}
if (keywords) {
searchQuery = {
...searchQuery,
title: {
$regex: keywords,
$options: "i",
},
}
}
const playlistsCount = await Playlist.count(searchQuery)
const releasesCount = await Release.count(searchQuery)
let total_length = playlistsCount + releasesCount
let playlists = await Playlist.find(searchQuery)
.sort({ created_at: -1 })
.limit(limit)
.skip(offset)
playlists = playlists.map((playlist) => {
playlist = playlist.toObject()
playlist.type = "playlist"
return playlist
})
let releases = await Release.find(searchQuery)
.sort({ created_at: -1 })
.limit(limit)
.skip(offset)
let result = [...playlists, ...releases]
if (req.query.resolveItemsData === "true") {
result = await Promise.all(
playlists.map(async playlist => {
playlist.list = await Track.find({
_id: [...playlist.list],
})
return playlist
}),
)
}
return {
total_length: total_length,
items: result,
}
}
}

View File

@ -1,8 +0,0 @@
import cors from "cors"
export default cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
preflightContinue: false,
optionsSuccessStatus: 204,
})

View File

@ -1,19 +0,0 @@
export default (req, res, next) => {
const startHrTime = process.hrtime()
res.on("finish", () => {
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
res._responseTimeMs = elapsedTimeInMs
// cut req.url if is too long
if (req.url.length > 100) {
req.url = req.url.substring(0, 100) + "..."
}
console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`)
})
next()
}

View File

@ -1,12 +0,0 @@
export default function composePayloadData(socket, data = {}) {
return {
user: {
user_id: socket.userData._id,
username: socket.userData.username,
fullName: socket.userData.fullName,
avatar: socket.userData.avatar,
},
command_issuer: data.command_issuer ?? socket.userData._id,
...data
}
}

View File

@ -1,75 +0,0 @@
import fs from "fs"
function createRouteHandler(route, fn) {
if (typeof route !== "string") {
fn = route
route = "Unknown route"
}
return async (req, res) => {
try {
await fn(req, res)
} catch (error) {
console.error(`[ERROR] (${route}) >`, error)
return res.status(500).json({
error: error.message,
})
}
}
}
function createRoutesFromDirectory(startFrom, directoryPath, router) {
const files = fs.readdirSync(directoryPath)
if (typeof router.routes !== "object") {
router.routes = []
}
files.forEach((file) => {
const filePath = `${directoryPath}/${file}`
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
createRoutesFromDirectory(startFrom, filePath, router)
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
let splitedFilePath = filePath.split("/")
// slice the startFrom path
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf(startFrom) + 1)
const method = splitedFilePath[0]
let route = splitedFilePath.slice(1, splitedFilePath.length).join("/")
route = route.replace(".jsx", "")
route = route.replace(".js", "")
route = route.replace(".ts", "")
route = route.replace(".tsx", "")
if (route.endsWith("/index")) {
route = route.replace("/index", "")
}
route = `/${route}`
let handler = require(filePath)
handler = handler.default || handler
router[method](route, createRouteHandler(route, handler))
//console.log(`[ROUTE] ${method.toUpperCase()} [${route}]`, handler)
router.routes.push({
method,
path: route,
})
}
})
return router
}
export default createRoutesFromDirectory

View File

@ -1,21 +0,0 @@
export default function generateFnHandler(fn, socket) {
return async (...args) => {
if (typeof socket === "undefined") {
socket = arguments[0]
}
try {
fn(socket, ...args)
} catch (error) {
console.error(`[HANDLER_ERROR] ${error.message} >`, error.stack)
if (typeof socket.emit !== "function") {
return false
}
return socket.emit("error", {
message: error.message,
})
}
}
}

View File

@ -1,46 +0,0 @@
import fs from "node:fs"
import path from "node:path"
export default async (middlewares, middlewaresPath) => {
if (typeof middlewaresPath === "undefined") {
middlewaresPath = path.resolve(globalThis["__src"], "middlewares")
}
if (!fs.existsSync(middlewaresPath)) {
return undefined
}
if (typeof middlewares === "string") {
middlewares = [middlewares]
}
let fns = []
for await (const middlewareName of middlewares) {
const middlewarePath = path.resolve(middlewaresPath, middlewareName)
if (!fs.existsSync(middlewarePath)) {
console.error(`Middleware ${middlewareName} not found.`)
continue
}
const middleware = require(middlewarePath).default
if (!middleware) {
console.error(`Middleware ${middlewareName} not valid export.`)
continue
}
if (typeof middleware !== "function") {
console.error(`Middleware ${middlewareName} not valid function.`)
continue
}
fns.push(middleware)
}
return fns
}

View File

@ -1,20 +0,0 @@
export default (from, to) => {
const resolvedUrl = new URL(to, new URL(from, "resolve://"))
if (resolvedUrl.protocol === "resolve:") {
let { pathname, search, hash } = resolvedUrl
if (to.includes("@")) {
const fromUrl = new URL(from)
const toUrl = new URL(to, fromUrl.origin)
pathname = toUrl.pathname
search = toUrl.search
hash = toUrl.hash
}
return pathname + search + hash
}
return resolvedUrl.toString()
}

View File

@ -29,6 +29,13 @@ export default async (payload = {}) => {
throw new OperationError(500, `An error has occurred: ${err.message}`)
})
// delete replies
await Post.deleteMany({
reply_to: post_id,
}).catch((err) => {
throw new OperationError(500, `An error has occurred: ${err.message}`)
})
global.rtengine.io.of("/").emit(`post.delete`, post_id)
global.rtengine.io.of("/").emit(`post.delete.${post_id}`, post_id)

View File

@ -23,7 +23,7 @@ export default async (payload = {}) => {
postsSavesIds = postsSaves.map((postSave) => postSave.post_id)
}
let [usersData, likesData, repliesData] = await Promise.all([
let [usersData, likesData] = await Promise.all([
User.find({
_id: {
$in: posts.map((post) => post.user_id)
@ -63,8 +63,18 @@ export default async (payload = {}) => {
if (post.reply_to) {
post.reply_to_data = await Post.findById(post.reply_to)
if (post.reply_to_data) {
post.reply_to_data = post.reply_to_data.toObject()
const replyUserData = await User.findById(post.reply_to_data.user_id)
post.reply_to_data.user = replyUserData.toObject()
}
}
post.hasReplies = await Post.count({ reply_to: post._id })
let likes = likesData[post._id.toString()] ?? []
post.countLikes = likes.length