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) => { onUpdateInput = (input, value) => {
if (input === "username") {
value = value.toLowerCase()
value = value.trim()
}
// remove error from ref // remove error from ref
this.formRef.current.setFields([ 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 React from "react"
import { DateTime } from "luxon" import { DateTime } from "luxon"
import { Tag, Skeleton } from "antd" import { Tag } from "antd"
import { Image } from "components" import { Image } from "components"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
import PostLink from "components/PostLink"
import PostService from "models/post" import PostReplieView from "components/PostReplieView"
import "./index.less" 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 PostCardHeader = (props) => {
const [timeAgo, setTimeAgo] = React.useState(0) const [timeAgo, setTimeAgo] = React.useState(0)
@ -60,11 +46,13 @@ const PostCardHeader = (props) => {
!props.disableReplyTag && props.postData.reply_to && <div !props.disableReplyTag && props.postData.reply_to && <div
className="post-header-replied_to" className="post-header-replied_to"
> >
<Icons.Repeat /> <div className="post-header-replied_to-label">
<Icons.Repeat />
<span> <span>
Replied to Replied to
</span> </span>
</div>
<PostReplieView <PostReplieView
data={props.postData.reply_to_data} data={props.postData.reply_to_data}
@ -83,7 +71,7 @@ const PostCardHeader = (props) => {
<div className="post-header-user-info"> <div className="post-header-user-info">
<h1 onClick={goToProfile}> <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 { .post-header-replied_to {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center;
gap: 7px; gap: 7px;
svg { .post-header-replied_to-label {
color: var(--text-color); display: flex;
margin: 0 !important; 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 { .post-header-user {

View File

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

View File

@ -48,6 +48,11 @@
.message { .message {
white-space: break-spaces; white-space: break-spaces;
user-select: text; user-select: text;
text-wrap: balance;
word-break: break-word;
overflow: hidden;
} }
.nsfw_alert { .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 humanSize from "@tsmx/human-readable"
import PostLink from "components/PostLink" import PostLink from "components/PostLink"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
import { DateTime } from "luxon"
import lodash from "lodash"
import clipboardEventFileToFile from "utils/clipboardEventFileToFile" import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
import PostModel from "models/post" import PostModel from "models/post"
@ -16,7 +18,6 @@ const DEFAULT_POST_POLICY = {
maximunFilesPerRequest: 10 maximunFilesPerRequest: 10
} }
export default class PostCreator extends React.Component { export default class PostCreator extends React.Component {
state = { state = {
pending: [], pending: [],
@ -76,10 +77,18 @@ export default class PostCreator extends React.Component {
return true return true
} }
submit = async () => { debounceSubmit = lodash.debounce(() => this.submit(), 50)
if (!this.canSubmit()) return
this.setState({ submit = async () => {
if (this.state.loading) {
return false
}
if (!this.canSubmit()) {
return false
}
await this.setState({
loading: true, loading: true,
uploaderVisible: false uploaderVisible: false
}) })
@ -89,7 +98,7 @@ export default class PostCreator extends React.Component {
const payload = { const payload = {
message: postMessage, message: postMessage,
attachments: postAttachments, attachments: postAttachments,
//timestamp: DateTime.local().toISO(), timestamp: DateTime.local().toISO(),
} }
let response = null 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 // 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) { if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault() e.preventDefault()
e.stopPropagation() 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 <antd.Button
type="primary" type="primary"
disabled={loading || !this.canSubmit()} disabled={loading || !this.canSubmit()}
onClick={this.submit} onClick={this.debounceSubmit}
icon={loading ? <Icons.LoadingOutlined spin /> : (editMode ? <Icons.MdEdit /> : <Icons.Send />)} icon={loading ? <Icons.LoadingOutlined spin /> : (editMode ? <Icons.MdEdit /> : <Icons.Send />)}
/> />
</div> </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, key: data._id,
data: data, data: data,
disableReplyTag: props.disableReplyTag, disableReplyTag: props.disableReplyTag,
disableHasReplies: props.disableHasReplies,
events: { events: {
onClickLike: props.onLikePost, onClickLike: props.onLikePost,
onClickSave: props.onSavePost, onClickSave: props.onSavePost,
@ -447,6 +448,7 @@ export class PostsListsComponent extends React.Component {
list: this.state.list, list: this.state.list,
disableReplyTag: this.props.disableReplyTag, disableReplyTag: this.props.disableReplyTag,
disableHasReplies: this.props.disableHasReplies,
onLikePost: this.onLikePost, onLikePost: this.onLikePost,
onSavePost: this.onSavePost, onSavePost: this.onSavePost,

View File

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

View File

@ -44,7 +44,7 @@ class MusicSyncSubCore {
"invite:received": (data) => { "invite:received": (data) => {
this.console.log("invite:received", data) this.console.log("invite:received", data)
app.notification.new({ app.cores.notifications.new({
title: "Sync", title: "Sync",
description: `${data.invitedBy.username} invited you to join a sync room`, description: `${data.invitedBy.username} invited you to join a sync room`,
icon: React.createElement(Image, { icon: React.createElement(Image, {
@ -91,7 +91,7 @@ class MusicSyncSubCore {
this.dettachCard() this.dettachCard()
this.currentRoomData = null this.currentRoomData = null
app.notification.new({ app.cores.notifications.new({
title: "Sync", title: "Sync",
description: "Disconnected from sync server" description: "Disconnected from sync server"
}, { }, {
@ -177,7 +177,7 @@ class MusicSyncSubCore {
app.cores.player.toggleSyncMode(false, false) app.cores.player.toggleSyncMode(false, false)
app.notification.new({ app.cores.notifications.new({
title: "Kicked", title: "Kicked",
description: "You have been kicked from the sync room" 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) store.set(WidgetsCore.storeKey, currentStore)
app.notification.new({ app.cores.notifications.new({
title: params.update ? "Widget updated" : "Widget installed", title: params.update ? "Widget updated" : "Widget installed",
description: `Widget [${manifest.name}] has been ${params.update ? "updated" : "installed"}. ${params.update ? `Using current version ${manifest.version}` : ""}`, 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) store.set(WidgetsCore.storeKey, newStore)
app.notification.new({ app.cores.notifications.new({
title: "Widget uninstalled", title: "Widget uninstalled",
description: `Widget [${widget_id}] has been uninstalled.`, description: `Widget [${widget_id}] has been uninstalled.`,
}, { }, {

View File

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

View File

@ -102,10 +102,12 @@ export const UsernameStepComponent = (props) => {
return false return false
}) })
if (request) { console.log(request)
setUsernameAvailable(request.available)
if (!request.available) { if (request) {
setUsernameAvailable(!request.exists)
if (request.exists) {
props.updateValue(null) props.updateValue(null)
} else { } else {
props.updateValue(username) 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 { export class Feed extends React.Component {
render() { render() {
return <PostsList return <PostsList
disableHasReplies
ref={this.props.innerRef} ref={this.props.innerRef}
emptyListRender={emptyListRender} emptyListRender={emptyListRender}
loadFromModel={FeedModel.getTimelineFeed} loadFromModel={FeedModel.getTimelineFeed}

View File

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

View File

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

View File

@ -42,14 +42,23 @@ export function createAssembleChunksPromise({
return () => new Promise(async (resolve, reject) => { return () => new Promise(async (resolve, reject) => {
let fileSize = 0 let fileSize = 0
if (!fs.existsSync(chunksPath)) {
return reject(new OperationError(500,"No chunks found"))
}
const chunks = await fs.promises.readdir(chunksPath) const chunks = await fs.promises.readdir(chunksPath)
if (chunks.length === 0) { if (chunks.length === 0) {
throw new Error("No chunks found") throw new OperationError(500, "No chunks found")
} }
for await (const chunk of chunks) { for await (const chunk of chunks) {
const chunkPath = path.join(chunksPath, chunk) 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) const data = await fs.promises.readFile(chunkPath)
fileSize += data.length fileSize += data.length
@ -85,13 +94,17 @@ export async function handleChunkFile(fileStream, { tmpDir, headers, maxFileSize
// make sure chunk is in range // make sure chunk is in range
if (chunkCount < 0 || chunkCount >= totalChunks) { 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 is the first chunk check if dir exists before write things
if (chunkCount === 0) { if (chunkCount === 0) {
if (!await fs.promises.stat(chunksPath).catch(() => false)) { try {
await fs.promises.mkdir(chunksPath, { recursive: true }) 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: [] }, links: { type: Array, default: [] },
location: { type: String, default: null }, location: { type: String, default: null },
birthday: { type: Date, default: null, select: false }, 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) => { export default async (payload) => {
requiredFields(["username", "password", "email"], 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.") 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}`, avatar: avatar ?? `https://api.dicebear.com/7.x/thumbs/svg?seed=${username}`,
roles: roles, roles: roles,
created_at: new Date().getTime(), created_at: new Date().getTime(),
acceptTos: acceptTos, accept_tos: accept_tos,
}) })
await user.save() await user.save()

View File

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

View File

@ -35,6 +35,10 @@ export async function b2Upload({
bucketId: process.env.B2_BUCKET_ID, bucketId: process.env.B2_BUCKET_ID,
}) })
if (!fs.existsSync(source)) {
throw new OperationError(500, "File not found")
}
const data = await fs.promises.readFile(source) const data = await fs.promises.readFile(source)
await global.b2Storage.uploadFile({ 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 { Server } from "linebridge/src/server"
import path from "path"
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 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 { middlewares = {
static useMiddlewaresOrder = ["useLogger", "useCors", "useAuth", "useErrorHandler"] ...SharedMiddlewares
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
}
} }
comty = global.comty = ComtyClient() contexts = {
db: new DbManager(),
db = new DbManager() limits: {},
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
}
} }
async __registerInternalMiddlewares() { async onInitialize() {
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares")) await this.contexts.db.initialize()
// sort middlewares this.contexts.limits = await LimitsClass.get()
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`)
} }
} }

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}`) 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)
global.rtengine.io.of("/").emit(`post.delete.${post_id}`, 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) postsSavesIds = postsSaves.map((postSave) => postSave.post_id)
} }
let [usersData, likesData, repliesData] = await Promise.all([ let [usersData, likesData] = await Promise.all([
User.find({ User.find({
_id: { _id: {
$in: posts.map((post) => post.user_id) $in: posts.map((post) => post.user_id)
@ -63,8 +63,18 @@ export default async (payload = {}) => {
if (post.reply_to) { if (post.reply_to) {
post.reply_to_data = await Post.findById(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()] ?? [] let likes = likesData[post._id.toString()] ?? []
post.countLikes = likes.length post.countLikes = likes.length