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
500fa23384
commit
d6a074a859
@ -1 +1 @@
|
||||
Subproject commit 6d553830ab4661ffab952253d77ccb0bfc1363d8
|
||||
Subproject commit c4b0fbafea7b240eda4c7fa4a6a119e9c4b93dbe
|
@ -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([
|
||||
{
|
||||
|
33
packages/app/src/components/Poll/index.jsx
Normal file
33
packages/app/src/components/Poll/index.jsx
Normal 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
|
26
packages/app/src/components/Poll/index.less
Normal file
26
packages/app/src/components/Poll/index.less
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
>
|
||||
<div className="post-header-replied_to-label">
|
||||
<Icons.Repeat />
|
||||
|
||||
<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}`
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -6,12 +6,18 @@
|
||||
|
||||
.post-header-replied_to {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 7px;
|
||||
|
||||
.post-header-replied_to-label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 7px;
|
||||
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
color: var(--text-color);
|
||||
margin: 0 !important;
|
||||
@ -19,6 +25,7 @@
|
||||
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.post-header-user {
|
||||
display: inline-flex;
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
51
packages/app/src/components/PostReplieView/index.jsx
Normal file
51
packages/app/src/components/PostReplieView/index.jsx
Normal 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
|
72
packages/app/src/components/PostReplieView/index.less
Normal file
72
packages/app/src/components/PostReplieView/index.less
Normal 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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}, {
|
||||
|
@ -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.`,
|
||||
}, {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
22
packages/app/src/pages/debug/polls/index.jsx
Normal file
22
packages/app/src/pages/debug/polls/index.jsx
Normal 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
|
@ -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}
|
||||
|
@ -9,6 +9,7 @@ import "./index.less"
|
||||
export default class ExplorePosts extends React.Component {
|
||||
render() {
|
||||
return <PostsList
|
||||
disableHasReplies
|
||||
loadFromModel={Feed.getGlobalTimelineFeed}
|
||||
watchTimeline={[
|
||||
"post.new",
|
||||
|
@ -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,15 +31,21 @@ 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>
|
||||
{
|
||||
!!result.hasReplies && <div className="post-page-replies">
|
||||
<h1><Icons.Repeat />Replies</h1>
|
||||
|
||||
<PostsList
|
||||
disableReplyTag
|
||||
loadFromModel={PostService.replies}
|
||||
@ -46,5 +54,8 @@ export default (props) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default PostPage
|
@ -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,14 +94,18 @@ 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) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
let dataWritten = 0
|
||||
|
@ -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 },
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -37,11 +37,15 @@ 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")
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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`))
|
||||
}
|
||||
}
|
@ -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
|
||||
contexts = {
|
||||
db: new DbManager(),
|
||||
limits: {},
|
||||
}
|
||||
|
||||
const handler = await controller(express.Router())
|
||||
async onInitialize() {
|
||||
await this.contexts.db.initialize()
|
||||
|
||||
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() {
|
||||
let middlewaresPath = fs.readdirSync(path.resolve(__dirname, "useMiddlewares"))
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
})
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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,7 +63,17 @@ 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()] ?? []
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user