+ return
+ {
+ !props.disableReplyTag && props.postData.reply_to &&
+
+
+
+ Replied to
+
+
+
+
+ }
+
+
+
-
+
+
{
- props.postData.user?.fullName ?? `${props.postData.user?.username}`
+ props.postData.user?.public_name ?? `${props.postData.user?.username}`
}
+
{
props.postData.user?.verified &&
}
+
{
props.postData.flags?.includes("nsfw") && {
}
-
+
{timeAgo}
-}
\ No newline at end of file
+}
+
+export default PostCardHeader
\ No newline at end of file
diff --git a/packages/app/src/components/PostCard/components/header/index.less b/packages/app/src/components/PostCard/components/header/index.less
index c52dd80f..b9cc748a 100755
--- a/packages/app/src/components/PostCard/components/header/index.less
+++ b/packages/app/src/components/PostCard/components/header/index.less
@@ -1,9 +1,26 @@
-.post_header {
+.post-header {
display: flex;
- flex-direction: row;
- justify-content: space-between;
+ flex-direction: column;
- .user {
+ gap: 10px;
+
+ .post-header-replied_to {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ gap: 7px;
+
+ svg {
+ color: var(--text-color);
+ margin: 0 !important;
+ }
+
+ line-height: 1.5rem;
+ }
+
+ .post-header-user {
display: inline-flex;
flex-direction: row;
align-items: center;
@@ -17,7 +34,7 @@
margin-left: 6px;
}
- .avatar {
+ .post-header-user-avatar {
width: 40px;
height: 40px;
@@ -30,7 +47,7 @@
}
}
- .info {
+ .post-header-user-info {
display: inline-flex;
flex-direction: column;
@@ -51,7 +68,7 @@
color: var(--background-color-contrast);
}
- .timeago {
+ .post-header-user-info-timeago {
font-weight: 400;
font-size: 0.7rem;
diff --git a/packages/app/src/components/PostCard/index.jsx b/packages/app/src/components/PostCard/index.jsx
index 873e90b0..bdf48a8c 100755
--- a/packages/app/src/components/PostCard/index.jsx
+++ b/packages/app/src/components/PostCard/index.jsx
@@ -1,11 +1,8 @@
import React from "react"
-import * as antd from "antd"
import classnames from "classnames"
import Plyr from "plyr-react"
-
-import { CommentsCard } from "components"
+import { motion } from "framer-motion"
import { Icons } from "components/Icons"
-
import { processString } from "utils"
import PostHeader from "./components/header"
@@ -13,6 +10,7 @@ import PostActions from "./components/actions"
import PostAttachments from "./components/attachments"
import "./index.less"
+import { Divider } from "antd"
const messageRegexs = [
{
@@ -43,11 +41,14 @@ const messageRegexs = [
export default class PostCard extends React.PureComponent {
state = {
+ data: this.props.data,
+
countLikes: this.props.data.countLikes ?? 0,
- countComments: this.props.data.countComments ?? 0,
+ countReplies: this.props.data.countComments ?? 0,
hasLiked: this.props.data.isLiked ?? false,
hasSaved: this.props.data.isSaved ?? false,
+ hasReplies: this.props.data.hasReplies ?? false,
open: this.props.defaultOpened ?? false,
@@ -55,13 +56,28 @@ export default class PostCard extends React.PureComponent {
nsfwAccepted: false,
}
+ handleDataUpdate = (data) => {
+ this.setState({
+ data: data,
+ })
+ }
+
+ onDoubleClick = async () => {
+ if (typeof this.props.events.onDoubleClick !== "function") {
+ console.warn("onDoubleClick event is not a function")
+ return
+ }
+
+ return await this.props.events.onDoubleClick(this.state.data)
+ }
+
onClickDelete = async () => {
if (typeof this.props.events.onClickDelete !== "function") {
console.warn("onClickDelete event is not a function")
return
}
- return await this.props.events.onClickDelete(this.props.data)
+ return await this.props.events.onClickDelete(this.state.data)
}
onClickLike = async () => {
@@ -70,7 +86,16 @@ export default class PostCard extends React.PureComponent {
return
}
- return await this.props.events.onClickLike(this.props.data)
+ const actionResult = await this.props.events.onClickLike(this.state.data)
+
+ if (actionResult) {
+ this.setState({
+ hasLiked: actionResult.liked,
+ countLikes: actionResult.count,
+ })
+ }
+
+ return actionResult
}
onClickSave = async () => {
@@ -79,7 +104,15 @@ export default class PostCard extends React.PureComponent {
return
}
- return await this.props.events.onClickSave(this.props.data)
+ const actionResult = await this.props.events.onClickSave(this.state.data)
+
+ if (actionResult) {
+ this.setState({
+ hasSaved: actionResult.saved,
+ })
+ }
+
+ return actionResult
}
onClickEdit = async () => {
@@ -88,57 +121,26 @@ export default class PostCard extends React.PureComponent {
return
}
- return await this.props.events.onClickEdit(this.props.data)
+ return await this.props.events.onClickEdit(this.state.data)
}
- onDoubleClick = async () => {
- this.handleOpen()
- }
-
- onClickComments = async () => {
- this.handleOpen()
- }
-
- handleOpen = (to) => {
- if (typeof to === "undefined") {
- to = !this.state.open
+ onClickReply = async () => {
+ if (typeof this.props.events.onClickReply !== "function") {
+ console.warn("onClickReply event is not a function")
+ return
}
- if (typeof this.props.events?.ontoggleOpen === "function") {
- this.props.events?.ontoggleOpen(to, this.props.data)
- }
-
- this.setState({
- open: to,
- })
-
- //app.controls.openPostViewer(this.props.data)
+ return await this.props.events.onClickReply(this.state.data)
}
- onLikesUpdate = (data) => {
- console.log("onLikesUpdate", data)
-
- if (data.to) {
+ componentDidUpdate = (prevProps) => {
+ if (prevProps.data !== this.props.data) {
this.setState({
- countLikes: this.state.countLikes + 1,
- })
- } else {
- this.setState({
- countLikes: this.state.countLikes - 1,
+ data: this.props.data,
})
}
}
- componentDidMount = async () => {
- // first listen to post changes
- app.cores.api.listenEvent(`post.${this.props.data._id}.likes.update`, this.onLikesUpdate)
- }
-
- componentWillUnmount = () => {
- // remove the listener
- app.cores.api.unlistenEvent(`post.${this.props.data._id}.likes.update`, this.onLikesUpdate)
- }
-
componentDidCatch = (error, info) => {
console.error(error)
@@ -153,12 +155,28 @@ export default class PostCard extends React.PureComponent {
}
+ componentDidMount = () => {
+ app.cores.api.listenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts")
+ }
+
+ componentWillUnmount = () => {
+ app.cores.api.unlistenEvent(`post.update.${this.state.data._id}`, this.handleDataUpdate, "posts")
+ }
+
render() {
- return
{
- processString(messageRegexs)(this.props.data.message ?? "")
+ processString(messageRegexs)(this.state.data.message ?? "")
}
{
- !this.props.disableAttachments && this.props.data.attachments && this.props.data.attachments.length > 0 &&
0 &&
}
-
-
+ {
+ !this.props.disableHasReplies && this.state.hasReplies && <>
+
+
View replies
+ >
+ }
+
+
}
}
\ No newline at end of file
diff --git a/packages/app/src/components/PostCard/index.less b/packages/app/src/components/PostCard/index.less
index 9c65bcb2..9a1e77da 100755
--- a/packages/app/src/components/PostCard/index.less
+++ b/packages/app/src/components/PostCard/index.less
@@ -13,7 +13,7 @@
margin: auto;
gap: 15px;
- padding: 17px 17px 0px 17px;
+ padding: 17px 17px 10px 17px;
background-color: var(--background-color-accent);
@@ -22,8 +22,6 @@
color: rgba(var(--background-color-contrast));
- transition: all 0.2s ease-in-out;
-
h1,
h2,
h3,
diff --git a/packages/app/src/components/PostCreator/index.jsx b/packages/app/src/components/PostCreator/index.jsx
index cde28e8a..cff2f855 100755
--- a/packages/app/src/components/PostCreator/index.jsx
+++ b/packages/app/src/components/PostCreator/index.jsx
@@ -1,24 +1,21 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
-import { DateTime } from "luxon"
import humanSize from "@tsmx/human-readable"
-
+import PostLink from "components/PostLink"
import { Icons } from "components/Icons"
-import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
+import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
import PostModel from "models/post"
import "./index.less"
const DEFAULT_POST_POLICY = {
maxMessageLength: 512,
- acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp"],
- maximumFileSize: 10 * 1024 * 1024,
+ acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp", "video/*"],
maximunFilesPerRequest: 10
}
-// TODO: Fix close window when post created
export default class PostCreator extends React.Component {
state = {
@@ -92,15 +89,30 @@ export default class PostCreator extends React.Component {
const payload = {
message: postMessage,
attachments: postAttachments,
- timestamp: DateTime.local().toISO(),
+ //timestamp: DateTime.local().toISO(),
}
- const response = await PostModel.create(payload).catch(error => {
- console.error(error)
- antd.message.error(error)
+ let response = null
- return false
- })
+ if (this.props.reply_to) {
+ payload.reply_to = this.props.reply_to
+ }
+
+ if (this.props.edit_post) {
+ response = await PostModel.update(this.props.edit_post, payload).catch(error => {
+ console.error(error)
+ antd.message.error(error)
+
+ return false
+ })
+ } else {
+ response = await PostModel.create(payload).catch(error => {
+ console.error(error)
+ antd.message.error(error)
+
+ return false
+ })
+ }
this.setState({
loading: false
@@ -116,6 +128,10 @@ export default class PostCreator extends React.Component {
if (typeof this.props.close === "function") {
this.props.close()
}
+
+ if (this.props.reply_to) {
+ app.navigation.goToPost(this.props.reply_to)
+ }
}
}
@@ -182,8 +198,6 @@ export default class PostCreator extends React.Component {
})
}
- console.log(change)
-
switch (change.file.status) {
case "uploading": {
this.toggleUploaderVisibility(false)
@@ -424,9 +438,37 @@ export default class PostCreator extends React.Component {
dialog.click()
}
- componentDidMount() {
+ componentDidMount = async () => {
+ if (this.props.edit_post) {
+ await this.setState({
+ loading: true,
+ postId: this.props.edit_post,
+ })
+
+ const post = await PostModel.getPost({ post_id: this.props.edit_post })
+
+ await this.setState({
+ loading: false,
+ postMessage: post.message,
+ postAttachments: post.attachments.map((attachment) => {
+ return {
+ ...attachment,
+ uid: attachment.id,
+ }
+ }),
+ fileList: post.attachments.map((attachment) => {
+ return {
+ ...attachment,
+ uid: attachment.id,
+ id: attachment.id,
+ thumbUrl: attachment.url,
+ status: "done",
+ }
+ }),
+ })
+ }
// fetch the posting policy
- this.fetchUploadPolicy()
+ //this.fetchUploadPolicy()
// add a listener to the window
document.addEventListener("paste", this.handlePaste)
@@ -448,6 +490,10 @@ export default class PostCreator extends React.Component {
render() {
const { postMessage, fileList, loading, uploaderVisible, postingPolicy } = this.state
+ const editMode = !!this.props.edit_post
+
+ const showHeader = !!this.props.edit_post || this.props.reply_to
+
return
+ {
+ showHeader &&
+ {
+ this.props.edit_post &&
+ }
+
+ {
+ this.props.reply_to &&
+
+
+ Replaying to
+
+
+
{
+ this.props.close()
+ app.navigation.goToPost(this.props.reply_to)
+ }}
+
+ />
+
+ }
+
+ }
+

@@ -475,7 +552,7 @@ export default class PostCreator extends React.Component {
type="primary"
disabled={loading || !this.canSubmit()}
onClick={this.submit}
- icon={loading ?
:
}
+ icon={loading ?
: (editMode ?
:
)}
/>
diff --git a/packages/app/src/components/PostCreator/index.less b/packages/app/src/components/PostCreator/index.less
index b0779926..4564e970 100755
--- a/packages/app/src/components/PostCreator/index.less
+++ b/packages/app/src/components/PostCreator/index.less
@@ -17,7 +17,29 @@
background-color: var(--background-color-accent);
padding: 15px;
- //transition: all 250ms ease-in-out;
+ gap: 10px;
+
+ .postCreator-header {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ width: 100%;
+
+ p {
+ margin: 0;
+ }
+
+ .postCreator-header-indicator {
+ display: flex;
+ flex-direction: row;
+
+ gap: 7px;
+
+ align-items: center;
+ }
+ }
.actions {
display: inline-flex;
@@ -58,8 +80,6 @@
width: 100%;
height: 100%;
- padding: 10px;
-
overflow: hidden;
overflow-x: scroll;
@@ -125,6 +145,8 @@
display: flex;
flex-direction: row;
+ align-items: center;
+
width: 100%;
overflow-x: auto;
@@ -146,6 +168,8 @@
height: fit-content;
border-radius: @file_preview_borderRadius;
+
+ margin: 0;
}
}
}
diff --git a/packages/app/src/components/PostLink/index.jsx b/packages/app/src/components/PostLink/index.jsx
new file mode 100644
index 00000000..7af76fb5
--- /dev/null
+++ b/packages/app/src/components/PostLink/index.jsx
@@ -0,0 +1,28 @@
+import React from "react"
+import { Tag } from "antd"
+
+import "./index.less"
+
+const PostLink = (props) => {
+ if (!props.post_id) {
+ return null
+ }
+
+ return
{
+ if (props.onClick) {
+ return props.onClick()
+ }
+
+ app.navigation.goToPost(props.post_id)
+ }}
+ >
+
+ #{props.post_id}
+
+
+}
+
+export default PostLink
\ No newline at end of file
diff --git a/packages/app/src/components/PostLink/index.less b/packages/app/src/components/PostLink/index.less
new file mode 100644
index 00000000..b386fd28
--- /dev/null
+++ b/packages/app/src/components/PostLink/index.less
@@ -0,0 +1,24 @@
+.post-link {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ border-radius: 8px;
+
+ font-size: 0.7rem;
+ font-weight: 800;
+
+ font-family: "DM Mono", monospace;
+
+ cursor: pointer;
+
+ overflow: hidden;
+
+ span {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ margin: 0;
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/PostsList/index.jsx b/packages/app/src/components/PostsList/index.jsx
index db59a1b6..360708ec 100755
--- a/packages/app/src/components/PostsList/index.jsx
+++ b/packages/app/src/components/PostsList/index.jsx
@@ -1,6 +1,7 @@
import React from "react"
import * as antd from "antd"
import { Icons } from "components/Icons"
+import { AnimatePresence } from "framer-motion"
import PostCard from "components/PostCard"
import PlaylistTimelineEntry from "components/Music/PlaylistTimelineEntry"
@@ -41,21 +42,21 @@ const Entry = React.memo((props) => {
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
key: data._id,
data: data,
- //disableAttachments: true,
+ disableReplyTag: props.disableReplyTag,
events: {
onClickLike: props.onLikePost,
onClickSave: props.onSavePost,
onClickDelete: props.onDeletePost,
onClickEdit: props.onEditPost,
+ onClickReply: props.onReplyPost,
+ onDoubleClick: props.onDoubleClick,
},
})
})
-const PostList = (props) => {
- const parentRef = React.useRef()
-
+const PostList = React.forwardRef((props, ref) => {
return
{
}
- {
- props.list.map((data) => {
- return
- })
- }
-
- {/*
+
{
- (data) =>
+ props.list.map((data) => {
+ return
+ })
}
- */}
+
-}
+
+})
export class PostsListsComponent extends React.Component {
state = {
@@ -238,12 +219,15 @@ export class PostsListsComponent extends React.Component {
addPost: this.addPost,
removePost: this.removePost,
addRandomPost: () => {
+ const randomId = Math.random().toString(36).substring(7)
+
this.addPost({
- _id: Math.random().toString(36).substring(7),
- message: `Random post ${Math.random().toString(36).substring(7)}`,
+ _id: randomId,
+ message: `Random post ${randomId}`,
user: {
- _id: Math.random().toString(36).substring(7),
+ _id: randomId,
username: "random user",
+ avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
}
})
},
@@ -336,7 +320,7 @@ export class PostsListsComponent extends React.Component {
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
}
- app.cores.api.listenEvent(event, this.timelineWsEvents[event])
+ app.cores.api.listenEvent(event, this.timelineWsEvents[event], "posts")
})
}
}
@@ -358,7 +342,7 @@ export class PostsListsComponent extends React.Component {
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
}
- app.cores.api.unlistenEvent(event, this.timelineWsEvents[event])
+ app.cores.api.unlistenEvent(event, this.timelineWsEvents[event], "posts")
})
}
}
@@ -370,7 +354,7 @@ export class PostsListsComponent extends React.Component {
window._hacks = null
}
- componentDidUpdate = async (prevProps) => {
+ componentDidUpdate = async (prevProps, prevState) => {
if (prevProps.list !== this.props.list) {
this.setState({
list: this.props.list,
@@ -398,6 +382,22 @@ export class PostsListsComponent extends React.Component {
return result
}
+ onEditPost = (data) => {
+ app.controls.openPostCreator({
+ edit_post: data._id,
+ })
+ }
+
+ onReplyPost = (data) => {
+ app.controls.openPostCreator({
+ reply_to: data._id,
+ })
+ }
+
+ onDoubleClickPost = (data) => {
+ app.navigation.goToPost(data._id)
+ }
+
onDeletePost = async (data) => {
antd.Modal.confirm({
title: "Are you sure you want to delete this post?",
@@ -444,13 +444,16 @@ export class PostsListsComponent extends React.Component {
}
const PostListProps = {
- listRef: this.listRef,
list: this.state.list,
+ disableReplyTag: this.props.disableReplyTag,
+
onLikePost: this.onLikePost,
onSavePost: this.onSavePost,
onDeletePost: this.onDeletePost,
onEditPost: this.onEditPost,
+ onReplyPost: this.onReplyPost,
+ onDoubleClick: this.onDoubleClickPost,
onLoadMore: this.onLoadMore,
hasMore: this.state.hasMore,
@@ -463,12 +466,14 @@ export class PostsListsComponent extends React.Component {
if (app.isMobile) {
return
}
return
diff --git a/packages/app/src/components/PostsList/index.less b/packages/app/src/components/PostsList/index.less
index c9d3dc2e..a18d82ea 100755
--- a/packages/app/src/components/PostsList/index.less
+++ b/packages/app/src/components/PostsList/index.less
@@ -59,7 +59,7 @@ html {
position: relative;
// WARN: Only use if is a performance issue (If is using virtualized list)
- will-change: transform;
+ //will-change: transform;
overflow: hidden;
overflow-y: overlay;
@@ -77,10 +77,14 @@ html {
//margin: auto;
z-index: 150;
+ background-color: var(--background-color-accent);
+
.post_card {
border-radius: 0;
border-bottom: 2px solid var(--border-color);
+ background-color: transparent;
+
&:first-child {
border-radius: 8px;
@@ -95,33 +99,11 @@ html {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
- }
- .playlistTimelineEntry {
- border-radius: 0;
- border-bottom: 2px solid var(--border-color);
-
- &:last-child {
- border-top-left-radius: 0px;
- border-top-right-radius: 0px;
-
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
+ &:last-of-type {
+ border-bottom: none;
}
}
-
- .postCard {
- width: 100%;
- min-width: 0;
- // FIXME: This is a walkaround for a bug when a post contains multiple attachments cause a overflow
- max-width: unset;
- }
-
- .playlistTimelineEntry {
- width: 100%;
- min-width: 0;
- max-width: unset;
- }
}
.resume_btn_wrapper {
diff --git a/packages/app/src/components/SyncRoomCard/index.jsx b/packages/app/src/components/SyncRoomCard/index.jsx
index 5a4621a9..b330ecd6 100755
--- a/packages/app/src/components/SyncRoomCard/index.jsx
+++ b/packages/app/src/components/SyncRoomCard/index.jsx
@@ -52,7 +52,7 @@ export default class SyncRoomCard extends React.Component {
}
checkLatency = () => {
- const instance = app.cores.api.instance().wsInstances.music
+ const instance = app.cores.api.instance().sockets.music
if (instance) {
this.setState({
@@ -67,7 +67,7 @@ export default class SyncRoomCard extends React.Component {
})
// chat instance
- const chatInstance = app.cores.api.instance().wsInstances.chat
+ const chatInstance = app.cores.api.instance().sockets.chat
if (chatInstance) {
Object.keys(this.chatEvents).forEach((event) => {
@@ -92,7 +92,7 @@ export default class SyncRoomCard extends React.Component {
}
// chat instance
- const chatInstance = app.cores.api.instance().wsInstances.chat
+ const chatInstance = app.cores.api.instance().sockets.chat
if (chatInstance) {
Object.keys(this.chatEvents).forEach((event) => {
@@ -231,7 +231,7 @@ export default class SyncRoomCard extends React.Component {
{
- app.cores.api.instance().wsInstances.music.latency ?? "..."
+ app.cores.api.instance().sockets.music.latency ?? "..."
}ms
diff --git a/packages/app/src/components/UploadButton/index.jsx b/packages/app/src/components/UploadButton/index.jsx
index 3e57908b..a7de1f64 100755
--- a/packages/app/src/components/UploadButton/index.jsx
+++ b/packages/app/src/components/UploadButton/index.jsx
@@ -1,10 +1,15 @@
import React from "react"
-import { Button, Upload } from "antd"
-
+import { Upload, Progress } from "antd"
+import classnames from "classnames"
import { Icons } from "components/Icons"
+import useHacks from "hooks/useHacks"
+
+import "./index.less"
+
export default (props) => {
const [uploading, setUploading] = React.useState(false)
+ const [progess, setProgess] = React.useState(null)
const handleOnStart = (file_uid, file) => {
if (typeof props.onStart === "function") {
@@ -32,35 +37,37 @@ export default (props) => {
const handleUpload = async (req) => {
setUploading(true)
+ setProgess(1)
handleOnStart(req.file.uid, req.file)
- const response = await app.cores.remoteStorage.uploadFile(req.file, {
+ await app.cores.remoteStorage.uploadFile(req.file, {
onProgress: (file, progress) => {
- return handleOnProgress(file.uid, progress)
- }
- }).catch((err) => {
- app.notification.new({
- title: "Could not upload file",
- description: err
- }, {
- type: "error"
- })
+ setProgess(progress)
+ handleOnProgress(file.uid, progress)
+ },
+ onError: (file, error) => {
+ setProgess(null)
+ handleOnError(file.uid, error)
+ setUploading(false)
+ },
+ onFinish: (file, response) => {
+ if (typeof props.ctx?.onUpdateItem === "function") {
+ props.ctx.onUpdateItem(response.url)
+ }
- return handleOnError(req.file.uid, err)
+ if (typeof props.onUploadDone === "function") {
+ props.onUploadDone(response)
+ }
+
+ setUploading(false)
+ handleOnSuccess(req.file.uid, response)
+
+ setTimeout(() => {
+ setProgess(null)
+ }, 1000)
+ },
})
-
- if (typeof props.ctx?.onUpdateItem === "function") {
- props.ctx.onUpdateItem(response.url)
- }
-
- if (typeof props.onUploadDone === "function") {
- await props.onUploadDone(response)
- }
-
- setUploading(false)
-
- return handleOnSuccess(req.file.uid, response)
}
return
{
props.multiple ?? false
}
accept={
- props.accept ?? "image/*"
+ props.accept ?? [
+ "image/*",
+ "video/*",
+ "audio/*",
+ ]
}
progress={false}
fileList={[]}
- >
- }
- loading={uploading}
- type={
- props.type ?? "round"
+ className={classnames(
+ "uploadButton",
+ {
+ ["uploading"]: !!progess || uploading
}
- >
+ )}
+ disabled={uploading}
+ >
+
+ {
+ !progess && (props.icon ??
)
+ }
+
+ {
+ progess &&
}
\ No newline at end of file
diff --git a/packages/app/src/components/UploadButton/index.less b/packages/app/src/components/UploadButton/index.less
new file mode 100644
index 00000000..3dabb98f
--- /dev/null
+++ b/packages/app/src/components/UploadButton/index.less
@@ -0,0 +1,71 @@
+.uploadButton {
+ display: inline-flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: center;
+
+ background-color: var(--background-color-accent);
+ border-radius: 8px;
+
+ padding: 5px 15px;
+
+ cursor: pointer;
+
+ transition: all 150ms ease-in-out;
+
+ &.uploading {
+ border-radius: 12px;
+
+ .uploadButton-content {
+ gap: 15px;
+ }
+ }
+
+ .ant-upload {
+ display: inline-flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: center;
+ }
+
+ .uploadButton-content {
+ display: inline-flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ gap: 7px;
+
+ transition: all 150ms ease-in-out;
+
+ .ant-progress {
+ position: relative;
+
+ display: inline-flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: center;
+
+ height: 17px;
+ width: 17px;
+
+ .ant-progress-inner,
+ .ant-progress-circle {
+ position: absolute;
+
+ top: 0;
+ left: 0;
+
+ width: 100% !important;
+ height: 100% !important;
+ }
+
+ svg {
+ margin: 0 !important;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/UserCard/index.jsx b/packages/app/src/components/UserCard/index.jsx
index 4b625d68..1c60db0d 100755
--- a/packages/app/src/components/UserCard/index.jsx
+++ b/packages/app/src/components/UserCard/index.jsx
@@ -113,7 +113,7 @@ export const UserCard = React.forwardRef((props, ref) => {
- {user.fullName || user.username}
+ {user.public_name || user.username}
{user.verified && }
diff --git a/packages/app/src/components/UserCard/index.less b/packages/app/src/components/UserCard/index.less
index b6426357..ef1fc49a 100755
--- a/packages/app/src/components/UserCard/index.less
+++ b/packages/app/src/components/UserCard/index.less
@@ -256,8 +256,6 @@ html {
outline: 1px solid var(--border-color);
- filter: drop-shadow(0 0 20px var(--border-color));
-
h1,
h2,
h3,
diff --git a/packages/app/src/components/index.js b/packages/app/src/components/index.js
index 51f4b126..b4850494 100755
--- a/packages/app/src/components/index.js
+++ b/packages/app/src/components/index.js
@@ -15,7 +15,6 @@ export { default as StepsForm } from "./StepsForm"
export { default as SearchButton } from "./SearchButton"
export { default as Skeleton } from "./Skeleton"
export { default as Navigation } from "./Navigation"
-export { default as ImageUploader } from "./ImageUploader"
export { default as ImageViewer } from "./ImageViewer"
export { default as Image } from "./Image"
export { default as LoadMore } from "./LoadMore"
diff --git a/packages/app/src/cores/api/api.core.js b/packages/app/src/cores/api/api.core.js
index 9e003fe3..c791ef4a 100755
--- a/packages/app/src/cores/api/api.core.js
+++ b/packages/app/src/cores/api/api.core.js
@@ -2,8 +2,8 @@ import Core from "evite/src/core"
import createClient from "comty.js"
-import measurePing from "comty.js/handlers/measurePing"
-import request from "comty.js/handlers/request"
+import request from "comty.js/request"
+import measurePing from "comty.js/helpers/measurePing"
import useRequest from "comty.js/hooks/useRequest"
import { reconnectWebsockets, disconnectWebsockets } from "comty.js"
@@ -13,11 +13,11 @@ export default class APICore extends Core {
static bgColor = "coral"
static textColor = "black"
- instance = null
+ client = null
public = {
- instance: function () {
- return this.instance
+ client: function () {
+ return this.client
}.bind(this),
customRequest: request,
listenEvent: this.listenEvent.bind(this),
@@ -28,82 +28,45 @@ export default class APICore extends Core {
disconnectWebsockets: disconnectWebsockets,
}
- listenEvent(key, handler, instance) {
- if (!this.instance.wsInstances[instance ?? "default"]) {
+ listenEvent(key, handler, instance = "default") {
+ if (!this.client.sockets[instance]) {
console.error(`[API] Websocket instance ${instance} not found`)
return false
}
- return this.instance.wsInstances[instance ?? "default"].on(key, handler)
+ return this.client.sockets[instance].on(key, handler)
}
- unlistenEvent(key, handler, instance) {
- if (!this.instance.wsInstances[instance ?? "default"]) {
+ unlistenEvent(key, handler, instance = "default") {
+ if (!this.client.sockets[instance]) {
console.error(`[API] Websocket instance ${instance} not found`)
return false
}
- return this.instance.wsInstances[instance ?? "default"].off(key, handler)
- }
-
- pendingPingsFromInstance = {}
-
- createPingIntervals() {
- // Object.keys(this.instance.wsInstances).forEach((instance) => {
- // this.console.debug(`[API] Creating ping interval for ${instance}`)
-
- // if (this.instance.wsInstances[instance].pingInterval) {
- // clearInterval(this.instance.wsInstances[instance].pingInterval)
- // }
-
- // this.instance.wsInstances[instance].pingInterval = setInterval(() => {
- // if (this.instance.wsInstances[instance].pendingPingTry && this.instance.wsInstances[instance].pendingPingTry > 3) {
- // this.console.debug(`[API] Ping timeout for ${instance}`)
-
- // return clearInterval(this.instance.wsInstances[instance].pingInterval)
- // }
-
- // const timeStart = Date.now()
-
- // //this.console.debug(`[API] Ping ${instance}`, this.instance.wsInstances[instance].pendingPingTry)
-
- // this.instance.wsInstances[instance].emit("ping", () => {
- // this.instance.wsInstances[instance].latency = Date.now() - timeStart
-
- // this.instance.wsInstances[instance].pendingPingTry = 0
- // })
-
- // this.instance.wsInstances[instance].pendingPingTry = this.instance.wsInstances[instance].pendingPingTry ? this.instance.wsInstances[instance].pendingPingTry + 1 : 1
- // }, 5000)
-
- // // clear interval on close
- // this.instance.wsInstances[instance].on("close", () => {
- // clearInterval(this.instance.wsInstances[instance].pingInterval)
- // })
- // })
+ return this.client.sockets[instance].off(key, handler)
}
async onInitialize() {
- this.instance = await createClient({
+ this.client = await createClient({
enableWs: true,
})
- this.instance.eventBus.on("auth:login_success", () => {
+ this.client.eventBus.on("auth:login_success", () => {
app.eventBus.emit("auth:login_success")
})
- this.instance.eventBus.on("auth:logout_success", () => {
+ this.client.eventBus.on("auth:logout_success", () => {
app.eventBus.emit("auth:logout_success")
})
- this.instance.eventBus.on("session.invalid", (error) => {
+ this.client.eventBus.on("session.invalid", (error) => {
app.eventBus.emit("session.invalid", error)
})
// make a basic request to check if the API is available
- await this.instance.instances["default"]({
+ await this.client.baseRequest({
method: "head",
url: "/",
}).catch((error) => {
@@ -115,10 +78,6 @@ export default class APICore extends Core {
`)
})
- this.console.debug("[API] Attached to", this.instance)
-
- //this.createPingIntervals()
-
- return this.instance
+ return this.client
}
}
\ No newline at end of file
diff --git a/packages/app/src/cores/notifications/feedback.js b/packages/app/src/cores/notifications/feedback.js
new file mode 100644
index 00000000..0c3257fe
--- /dev/null
+++ b/packages/app/src/cores/notifications/feedback.js
@@ -0,0 +1,44 @@
+import { Haptics } from "@capacitor/haptics"
+
+const NotfTypeToAudio = {
+ info: "notification",
+ success: "notification",
+ warning: "warn",
+ error: "error",
+}
+
+class NotificationFeedback {
+ static getSoundVolume = () => {
+ return (window.app.cores.settings.get("notifications_sound_volume") ?? 50) / 100
+ }
+
+ static playHaptic = async (options = {}) => {
+ const vibrationEnabled = options.vibrationEnabled ?? window.app.cores.settings.get("notifications_vibrate")
+
+ if (vibrationEnabled) {
+ await Haptics.vibrate()
+ }
+ }
+
+ static playAudio = (options = {}) => {
+ const soundEnabled = options.soundEnabled ?? window.app.cores.settings.get("notifications_sound")
+ const soundVolume = options.soundVolume ? options.soundVolume / 100 : NotificationFeedback.getSoundVolume()
+
+ if (soundEnabled) {
+ if (typeof window.app.cores.sound?.play === "function") {
+ const sound = options.sound ?? NotfTypeToAudio[options.type] ?? "notification"
+
+ window.app.cores.sound.play(sound, {
+ volume: soundVolume,
+ })
+ }
+ }
+ }
+
+ static async feedback(type) {
+ NotificationFeedback.playHaptic(type)
+ NotificationFeedback.playAudio(type)
+ }
+}
+
+export default NotificationFeedback
\ No newline at end of file
diff --git a/packages/app/src/cores/notifications/notifications.core.js b/packages/app/src/cores/notifications/notifications.core.js
new file mode 100755
index 00000000..6c2b8506
--- /dev/null
+++ b/packages/app/src/cores/notifications/notifications.core.js
@@ -0,0 +1,44 @@
+import Core from "evite/src/core"
+
+import NotificationUI from "./ui"
+import NotificationFeedback from "./feedback"
+
+export default class NotificationCore extends Core {
+ static namespace = "notifications"
+ static depenpencies = [
+ "api",
+ "settings",
+ ]
+
+ #newNotifications = []
+
+ onEvents = {
+ "changeNotificationsSoundVolume": (value) => {
+ NotificationFeedback.playAudio({
+ soundVolume: value
+ })
+ },
+ "changeNotificationsVibrate": (value) => {
+ NotificationFeedback.playHaptic({
+ vibrationEnabled: value,
+ })
+ }
+ }
+
+ listenSockets = {
+ "notifications": {
+ "notification.new": (data) => {
+ this.new(data)
+ }
+ }
+ }
+
+ public = {
+ new: this.new,
+ }
+
+ async new(notification, options = {}) {
+ await NotificationUI.notify(notification, options)
+ await NotificationFeedback.feedback(options.type)
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/notifications/notifications.core.jsx b/packages/app/src/cores/notifications/ui.jsx
old mode 100755
new mode 100644
similarity index 64%
rename from packages/app/src/cores/notifications/notifications.core.jsx
rename to packages/app/src/cores/notifications/ui.jsx
index b0034322..96a3c7f0
--- a/packages/app/src/cores/notifications/notifications.core.jsx
+++ b/packages/app/src/cores/notifications/ui.jsx
@@ -1,46 +1,10 @@
-import Core from "evite/src/core"
import React from "react"
import { notification as Notf, Space, Button } from "antd"
import { Icons, createIconRender } from "components/Icons"
import { Translation } from "react-i18next"
-import { Haptics } from "@capacitor/haptics"
-const NotfTypeToAudio = {
- info: "notification",
- success: "notification",
- warning: "warn",
- error: "error",
-}
-
-export default class NotificationCore extends Core {
- static namespace = "notifications"
-
- onEvents = {
- "changeNotificationsSoundVolume": (value) => {
- this.playAudio({ soundVolume: value })
- },
- "changeNotificationsVibrate": (value) => {
- this.playHaptic({
- vibrationEnabled: value,
- })
- }
- }
-
- registerToApp = {
- notification: this
- }
-
- getSoundVolume = () => {
- return (window.app.cores.settings.get("notifications_sound_volume") ?? 50) / 100
- }
-
- new = (notification, options = {}) => {
- this.notify(notification, options)
- this.playHaptic(options)
- this.playAudio(options)
- }
-
- notify(
+class NotificationUI {
+ static async notify(
notification,
options = {
type: "info"
@@ -142,27 +106,6 @@ export default class NotificationCore extends Core {
return Notf[options.type](notfObj)
}
+}
- playHaptic = async (options = {}) => {
- const vibrationEnabled = options.vibrationEnabled ?? window.app.cores.settings.get("notifications_vibrate")
-
- if (vibrationEnabled) {
- await Haptics.vibrate()
- }
- }
-
- playAudio = (options = {}) => {
- const soundEnabled = options.soundEnabled ?? window.app.cores.settings.get("notifications_sound")
- const soundVolume = options.soundVolume ? options.soundVolume / 100 : this.getSoundVolume()
-
- if (soundEnabled) {
- if (typeof window.app.cores.sound?.play === "function") {
- const sound = options.sound ?? NotfTypeToAudio[options.type] ?? "notification"
-
- window.app.cores.sound.play(sound, {
- volume: soundVolume,
- })
- }
- }
- }
-}
\ No newline at end of file
+export default NotificationUI
\ No newline at end of file
diff --git a/packages/app/src/cores/remoteStorage/chunkedUpload.js b/packages/app/src/cores/remoteStorage/chunkedUpload.js
new file mode 100644
index 00000000..fa8faf9b
--- /dev/null
+++ b/packages/app/src/cores/remoteStorage/chunkedUpload.js
@@ -0,0 +1,186 @@
+import EventBus from "evite/src/internals/eventBus"
+import SessionModel from "models/session"
+
+export default class ChunkedUpload {
+ constructor(params) {
+ this.endpoint = params.endpoint
+ this.file = params.file
+ this.headers = params.headers || {}
+ this.postParams = params.postParams
+ this.service = params.service ?? "default"
+ this.retries = params.retries ?? app.cores.settings.get("uploader.retries") ?? 3
+ this.delayBeforeRetry = params.delayBeforeRetry || 5
+
+ this.start = 0
+ this.chunk = null
+ this.chunkCount = 0
+
+ this.splitChunkSize = params.splitChunkSize || 1024 * 1024 * 10
+ this.totalChunks = Math.ceil(this.file.size / this.splitChunkSize)
+
+ this.retriesCount = 0
+ this.offline = false
+ this.paused = false
+
+ this.headers["Authorization"] = `Bearer ${SessionModel.token}`
+ this.headers["uploader-original-name"] = encodeURIComponent(this.file.name)
+ this.headers["uploader-file-id"] = this.uniqid(this.file)
+ this.headers["uploader-chunks-total"] = this.totalChunks
+ this.headers["provider-type"] = this.service
+ this.headers["chunk-size"] = this.splitChunkSize
+
+ this._reader = new FileReader()
+ this.eventBus = new EventBus()
+
+ this.validateParams()
+ this.nextSend()
+
+ console.debug("[Uploader] Created", {
+ splitChunkSize: this.splitChunkSize,
+ totalChunks: this.totalChunks,
+ totalSize: this.file.size,
+ })
+
+ // restart sync when back online
+ // trigger events when offline/back online
+ window.addEventListener("online", () => {
+ if (!this.offline) return
+
+ this.offline = false
+ this.eventBus.emit("online")
+ this.nextSend()
+ })
+
+ window.addEventListener("offline", () => {
+ this.offline = true
+ this.eventBus.emit("offline")
+ })
+ }
+
+ on(event, fn) {
+ this.eventBus.on(event, fn)
+ }
+
+ validateParams() {
+ if (!this.endpoint || !this.endpoint.length) throw new TypeError("endpoint must be defined")
+ if (this.file instanceof File === false) throw new TypeError("file must be a File object")
+ if (this.headers && typeof this.headers !== "object") throw new TypeError("headers must be null or an object")
+ if (this.postParams && typeof this.postParams !== "object") throw new TypeError("postParams must be null or an object")
+ if (this.splitChunkSize && (typeof this.splitChunkSize !== "number" || this.splitChunkSize === 0)) throw new TypeError("splitChunkSize must be a positive number")
+ if (this.retries && (typeof this.retries !== "number" || this.retries === 0)) throw new TypeError("retries must be a positive number")
+ if (this.delayBeforeRetry && (typeof this.delayBeforeRetry !== "number")) throw new TypeError("delayBeforeRetry must be a positive number")
+ }
+
+ uniqid(file) {
+ return Math.floor(Math.random() * 100000000) + Date.now() + this.file.size + "_tmp"
+ }
+
+ loadChunk() {
+ return new Promise((resolve) => {
+ const length = this.totalChunks === 1 ? this.file.size : this.splitChunkSize
+ const start = length * this.chunkCount
+
+ this._reader.onload = () => {
+ this.chunk = new Blob([this._reader.result], { type: "application/octet-stream" })
+ resolve()
+ }
+
+ this._reader.readAsArrayBuffer(this.file.slice(start, start + length))
+ })
+ }
+
+ sendChunk() {
+ const form = new FormData()
+
+ // send post fields on last request
+ if (this.chunkCount + 1 === this.totalChunks && this.postParams) Object.keys(this.postParams).forEach(key => form.append(key, this.postParams[key]))
+
+ form.append("file", this.chunk)
+
+ this.headers["uploader-chunk-number"] = this.chunkCount
+
+ return fetch(this.endpoint, { method: "POST", headers: this.headers, body: form })
+ }
+
+ manageRetries() {
+ if (this.retriesCount++ < this.retries) {
+ setTimeout(() => this.nextSend(), this.delayBeforeRetry * 1000)
+
+ this.eventBus.emit("fileRetry", {
+ message: `An error occured uploading chunk ${this.chunkCount}. ${this.retries - this.retriesCount} retries left`,
+ chunk: this.chunkCount,
+ retriesLeft: this.retries - this.retriesCount
+ })
+
+ return
+ }
+
+ this.eventBus.emit("error", {
+ message: `An error occured uploading chunk ${this.chunkCount}. No more retries, stopping upload`
+ })
+ }
+
+ async nextSend() {
+ if (this.paused || this.offline) {
+ return
+ }
+
+ await this.loadChunk()
+ const res = await this.sendChunk()
+ .catch((err) => {
+ if (this.paused || this.offline) return
+
+ this.console.error(err)
+
+ // this type of error can happen after network disconnection on CORS setup
+ this.manageRetries()
+ })
+
+ if (res.status === 200 || res.status === 201 || res.status === 204) {
+ if (++this.chunkCount < this.totalChunks) {
+ this.nextSend()
+ } else {
+ res.json().then((body) => {
+ this.eventBus.emit("finish", body)
+ })
+ }
+
+ const percentProgress = Math.round((100 / this.totalChunks) * this.chunkCount)
+
+ this.eventBus.emit("progress", {
+ percentProgress
+ })
+ }
+
+ // errors that might be temporary, wait a bit then retry
+ else if ([408, 502, 503, 504].includes(res.status)) {
+ if (this.paused || this.offline) return
+
+ this.manageRetries()
+ }
+
+ else {
+ if (this.paused || this.offline) return
+
+ try {
+ res.json().then((body) => {
+ this.eventBus.emit("error", {
+ message: `[${res.status}] ${body.error ?? body.message}`
+ })
+ })
+ } catch (error) {
+ this.eventBus.emit("error", {
+ message: `[${res.status}] ${res.statusText}`
+ })
+ }
+ }
+ }
+
+ togglePause() {
+ this.paused = !this.paused
+
+ if (!this.paused) {
+ this.nextSend()
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/cores/remoteStorage/remoteStorage.core.js b/packages/app/src/cores/remoteStorage/remoteStorage.core.js
index 63fa9bf2..551608d1 100755
--- a/packages/app/src/cores/remoteStorage/remoteStorage.core.js
+++ b/packages/app/src/cores/remoteStorage/remoteStorage.core.js
@@ -1,171 +1,6 @@
import Core from "evite/src/core"
-import EventBus from "evite/src/internals/eventBus"
-import SessionModel from "models/session"
-class ChunkedUpload {
- constructor(params) {
- this.endpoint = params.endpoint
- this.file = params.file
- this.headers = params.headers || {}
- this.postParams = params.postParams
- this.chunkSize = params.chunkSize || 1000000
- this.service = params.service ?? "default"
- this.retries = params.retries ?? app.cores.settings.get("uploader.retries") ?? 3
- this.delayBeforeRetry = params.delayBeforeRetry || 5
-
- this.start = 0
- this.chunk = null
- this.chunkCount = 0
- this.totalChunks = Math.ceil(this.file.size / this.chunkSize)
- this.retriesCount = 0
- this.offline = false
- this.paused = false
-
- this.headers["Authorization"] = SessionModel.token
- this.headers["uploader-original-name"] = encodeURIComponent(this.file.name)
- this.headers["uploader-file-id"] = this.uniqid(this.file)
- this.headers["uploader-chunks-total"] = this.totalChunks
- this.headers["provider-type"] = this.service
-
- this._reader = new FileReader()
- this.eventBus = new EventBus()
-
- this.validateParams()
- this.sendChunks()
-
- // restart sync when back online
- // trigger events when offline/back online
- window.addEventListener("online", () => {
- if (!this.offline) return
-
- this.offline = false
- this.eventBus.emit("online")
- this.sendChunks()
- })
-
- window.addEventListener("offline", () => {
- this.offline = true
- this.eventBus.emit("offline")
- })
- }
-
- on(event, fn) {
- this.eventBus.on(event, fn)
- }
-
- validateParams() {
- if (!this.endpoint || !this.endpoint.length) throw new TypeError("endpoint must be defined")
- if (this.file instanceof File === false) throw new TypeError("file must be a File object")
- if (this.headers && typeof this.headers !== "object") throw new TypeError("headers must be null or an object")
- if (this.postParams && typeof this.postParams !== "object") throw new TypeError("postParams must be null or an object")
- if (this.chunkSize && (typeof this.chunkSize !== "number" || this.chunkSize === 0)) throw new TypeError("chunkSize must be a positive number")
- if (this.retries && (typeof this.retries !== "number" || this.retries === 0)) throw new TypeError("retries must be a positive number")
- if (this.delayBeforeRetry && (typeof this.delayBeforeRetry !== "number")) throw new TypeError("delayBeforeRetry must be a positive number")
- }
-
- uniqid(file) {
- return Math.floor(Math.random() * 100000000) + Date.now() + this.file.size + "_tmp"
- }
-
- getChunk() {
- return new Promise((resolve) => {
- const length = this.totalChunks === 1 ? this.file.size : this.chunkSize * 1000 * 1000
- const start = length * this.chunkCount
-
- this._reader.onload = () => {
- this.chunk = new Blob([this._reader.result], { type: "application/octet-stream" })
- resolve()
- }
-
- this._reader.readAsArrayBuffer(this.file.slice(start, start + length))
- })
- }
-
- sendChunk() {
- const form = new FormData()
-
- // send post fields on last request
- if (this.chunkCount + 1 === this.totalChunks && this.postParams) Object.keys(this.postParams).forEach(key => form.append(key, this.postParams[key]))
-
- form.append("file", this.chunk)
-
- this.headers["uploader-chunk-number"] = this.chunkCount
-
- return fetch(this.endpoint, { method: "POST", headers: this.headers, body: form })
- }
-
- manageRetries() {
- if (this.retriesCount++ < this.retries) {
- setTimeout(() => this.sendChunks(), this.delayBeforeRetry * 1000)
-
- this.eventBus.emit("fileRetry", {
- message: `An error occured uploading chunk ${this.chunkCount}. ${this.retries - this.retriesCount} retries left`,
- chunk: this.chunkCount,
- retriesLeft: this.retries - this.retriesCount
- })
-
- return
- }
-
- this.eventBus.emit("error", {
- message: `An error occured uploading chunk ${this.chunkCount}. No more retries, stopping upload`
- })
- }
-
- sendChunks() {
- if (this.paused || this.offline) return
-
- this.getChunk()
- .then(() => this.sendChunk())
- .then((res) => {
- if (res.status === 200 || res.status === 201 || res.status === 204) {
- if (++this.chunkCount < this.totalChunks) this.sendChunks()
- else {
- res.json().then((body) => {
- this.eventBus.emit("finish", body)
- })
- }
-
- const percentProgress = Math.round((100 / this.totalChunks) * this.chunkCount)
-
- this.eventBus.emit("progress", {
- percentProgress
- })
- }
-
- // errors that might be temporary, wait a bit then retry
- else if ([408, 502, 503, 504].includes(res.status)) {
- if (this.paused || this.offline) return
-
- this.manageRetries()
- }
-
- else {
- if (this.paused || this.offline) return
-
- this.eventBus.emit("error", {
- message: `An error occured uploading chunk ${this.chunkCount}. Server responded with ${res.status}`
- })
- }
- })
- .catch((err) => {
- if (this.paused || this.offline) return
-
- this.console.error(err)
-
- // this type of error can happen after network disconnection on CORS setup
- this.manageRetries()
- })
- }
-
- togglePause() {
- this.paused = !this.paused
-
- if (!this.paused) {
- this.sendChunks()
- }
- }
-}
+import ChunkedUpload from "./chunkedUpload"
export default class RemoteStorage extends Core {
static namespace = "remoteStorage"
@@ -190,19 +25,15 @@ export default class RemoteStorage extends Core {
onProgress = () => { },
onFinish = () => { },
onError = () => { },
- service = "default",
+ service = "standard",
} = {},
) {
- const apiEndpoint = app.cores.api.instance().instances.files.getUri()
-
- // TODO: get value from settings
- const chunkSize = 2 * 1000 * 1000 // 10MB
-
return new Promise((_resolve, _reject) => {
const fn = async () => new Promise((resolve, reject) => {
const uploader = new ChunkedUpload({
- endpoint: `${apiEndpoint}/upload/chunk`,
- chunkSize: chunkSize,
+ endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
+ // TODO: get chunk size from settings
+ splitChunkSize: 5 * 1024 * 1024, // 5MB in bytes
file: file,
service: service,
})
@@ -210,6 +41,13 @@ export default class RemoteStorage extends Core {
uploader.on("error", ({ message }) => {
this.console.error("[Uploader] Error", message)
+ app.notification.new({
+ title: "Could not upload file",
+ description: message
+ }, {
+ type: "error"
+ })
+
if (typeof onError === "function") {
onError(file, message)
}
@@ -219,8 +57,6 @@ export default class RemoteStorage extends Core {
})
uploader.on("progress", ({ percentProgress }) => {
- //this.console.debug(`[Uploader] Progress: ${percentProgress}%`)
-
if (typeof onProgress === "function") {
onProgress(file, percentProgress)
}
@@ -229,6 +65,12 @@ export default class RemoteStorage extends Core {
uploader.on("finish", (data) => {
this.console.debug("[Uploader] Finish", data)
+ app.notification.new({
+ title: "File uploaded",
+ }, {
+ type: "success"
+ })
+
if (typeof onFinish === "function") {
onFinish(file, data)
}
diff --git a/packages/app/src/cores/rooms/rooms.core.js b/packages/app/src/cores/rooms/rooms.core.js
deleted file mode 100755
index c51ec60a..00000000
--- a/packages/app/src/cores/rooms/rooms.core.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import Core from "evite/src/core"
-import socketio from "socket.io-client"
-import remotes from "comty.js/remotes"
-import SessionModel from "comty.js/models/session"
-
-export default class RoomsController extends Core {
- static namespace = "rooms"
-
- connectedRooms = []
-
- connectToRoom = async (roomId) => {
- if (!this.checkRoomExists(roomId)) {
- await this.createRoom(roomId)
- }
-
- const room = this.createRoomSocket(roomId)
-
- this.connectedRooms.push(room)
-
- return room
- }
-
- disconnectFromRoom = async (roomId) => {
- if (!this.checkRoomExists(roomId)) {
- throw new Error(`Room ${roomId} does not exist`)
- }
-
- const room = this.connectedRooms.find((room) => room.roomId === roomId)
-
- room.leave()
-
- this.connectedRooms = this.connectedRooms.filter((room) => room.roomId !== roomId)
-
- return room
- }
-
- checkRoomExists = (roomId) => {
- return this.connectedRooms.some((room) => room.roomId === roomId)
- }
-
- createRoomSocket = async (roomId) => {
- let roomInterface = {
- roomId: roomId,
- socket: socketio(remotes.chat.origin, {
- transports: ["websocket"],
- query: {
- room: roomId,
- },
- auth: SessionModel.token,
- autoConnect: true,
- }),
- }
-
- room.leave = () => {
- roomInterface.socket.disconnect()
- }
-
- return roomInterface
- }
-}
\ No newline at end of file
diff --git a/packages/app/src/cores/sync/sync.core.js b/packages/app/src/cores/sync/sync.core.js
index dfab6d23..f2ad3bd2 100755
--- a/packages/app/src/cores/sync/sync.core.js
+++ b/packages/app/src/cores/sync/sync.core.js
@@ -189,7 +189,7 @@ class MusicSyncSubCore {
}
async onInitialize() {
- this.musicWs = this.ctx.CORES.api.instance.wsInstances.music
+ this.musicWs = this.ctx.CORES.api.instance.sockets.music
Object.keys(this.hubEvents).forEach((eventName) => {
this.musicWs.on(eventName, this.hubEvents[eventName])
@@ -371,6 +371,7 @@ class MusicSyncSubCore {
}
export default class SyncCore extends Core {
+ static disabled = true
static namespace = "sync"
static dependencies = ["api", "player"]
diff --git a/packages/app/src/cores/widgets/widgets.core.js b/packages/app/src/cores/widgets/widgets.core.js
index 260263c9..85bf881e 100755
--- a/packages/app/src/cores/widgets/widgets.core.js
+++ b/packages/app/src/cores/widgets/widgets.core.js
@@ -6,7 +6,7 @@ export default class WidgetsCore extends Core {
static storeKey = "widgets"
static get apiInstance() {
- return app.cores.api.instance().instances.marketplace
+ return app.cores.api.client().baseRequest
}
public = {
@@ -21,7 +21,7 @@ export default class WidgetsCore extends Core {
async onInitialize() {
try {
- await WidgetsCore.apiInstance()
+ //await WidgetsCore.apiInstance()
const currentStore = this.getInstalled()
diff --git a/packages/app/src/hooks/useUserRemoteConfig/index.jsx b/packages/app/src/hooks/useUserRemoteConfig/index.jsx
new file mode 100644
index 00000000..161a9fa0
--- /dev/null
+++ b/packages/app/src/hooks/useUserRemoteConfig/index.jsx
@@ -0,0 +1,29 @@
+import UserModel from "models/user"
+import React from "react"
+
+export default (props = {}) => {
+ const [firstLoad, setFirstLoad] = React.useState(true)
+ const [localData, setLocalData] = React.useState({})
+
+ React.useEffect(() => {
+ UserModel.getConfig().then((config) => {
+ setLocalData(config)
+ setFirstLoad(false)
+ })
+ }, [])
+
+ async function updateConfig(update) {
+ if (typeof props.onUpdate === "function") {
+ props.onUpdate(localData)
+ }
+
+ const config = await UserModel.updateConfig(update)
+ setLocalData(config)
+ }
+
+ return [
+ localData,
+ updateConfig,
+ firstLoad,
+ ]
+}
\ No newline at end of file
diff --git a/packages/app/src/layouts/components/modals/index.jsx b/packages/app/src/layouts/components/modals/index.jsx
index 69c1a021..33aa4d45 100755
--- a/packages/app/src/layouts/components/modals/index.jsx
+++ b/packages/app/src/layouts/components/modals/index.jsx
@@ -58,19 +58,17 @@ class Modal extends React.Component {
}
handleClickOutside = (e) => {
- if (this.contentRef.current && !this.contentRef.current.contains(e.target)) {
- if (this.props.confirmOnOutsideClick) {
- return AntdModal.confirm({
- title: this.props.confirmOnClickTitle ?? "Are you sure?",
- content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
- onOk: () => {
- this.close()
- }
- })
- }
-
- return this.close()
+ if (this.props.confirmOnOutsideClick) {
+ return AntdModal.confirm({
+ title: this.props.confirmOnClickTitle ?? "Are you sure?",
+ content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
+ onOk: () => {
+ this.close()
+ }
+ })
}
+
+ return this.close()
}
render() {
@@ -82,14 +80,15 @@ class Modal extends React.Component {
["framed"]: this.props.framed,
}
)}
- onTouchEnd={this.handleClickOutside}
- onMouseDown={this.handleClickOutside}
>
+
{
diff --git a/packages/app/src/layouts/components/modals/index.less b/packages/app/src/layouts/components/modals/index.less
index 582e62c9..9b057c56 100755
--- a/packages/app/src/layouts/components/modals/index.less
+++ b/packages/app/src/layouts/components/modals/index.less
@@ -18,6 +18,17 @@
transition: all 150ms ease-in-out;
+ #mask_trigger {
+ position: fixed;
+
+ top: 0;
+ left: 0;
+
+
+ width: 100vw;
+ height: 100vh;
+ }
+
&.framed {
.app_modal_content {
display: flex;
diff --git a/packages/app/src/pages/account/index.jsx b/packages/app/src/pages/account/index.jsx
index b7646c67..05bf19c9 100755
--- a/packages/app/src/pages/account/index.jsx
+++ b/packages/app/src/pages/account/index.jsx
@@ -2,9 +2,10 @@ import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import { Translation } from "react-i18next"
+import { motion, AnimatePresence } from "framer-motion"
import { Icons } from "components/Icons"
-import { Skeleton, FollowButton, UserCard } from "components"
+import { FollowButton, UserCard } from "components"
import { SessionModel, UserModel, FollowsModel } from "models"
import DetailsTab from "./tabs/details"
@@ -21,36 +22,6 @@ const TabsComponent = {
"music": MusicTab,
}
-const TabRender = React.memo((props, ref) => {
- const [transitionActive, setTransitionActive] = React.useState(false)
- const [activeKey, setActiveKey] = React.useState(props.renderKey)
-
- React.useEffect(() => {
- setTransitionActive(true)
-
- setTimeout(() => {
- setActiveKey(props.renderKey)
-
- setTimeout(() => {
- setTransitionActive(false)
- }, 100)
- }, 100)
- }, [props.renderKey])
-
- const Tab = TabsComponent[activeKey]
-
- if (!Tab) {
- return null
- }
-
- // forwards ref to the tab
- return
- {
- React.createElement(Tab, props)
- }
-
-})
-
export default class Account extends React.Component {
state = {
requestedUser: null,
@@ -66,16 +37,8 @@ export default class Account extends React.Component {
isNotExistent: false,
}
- profileRef = React.createRef()
-
contentRef = React.createRef()
- coverComponent = React.createRef()
-
- leftPanelRef = React.createRef()
-
- actionsRef = React.createRef()
-
componentDidMount = async () => {
app.layout.toggleCenteredContent(false)
@@ -129,13 +92,6 @@ export default class Account extends React.Component {
})
}
- onPostListTopVisibility = (to) => {
- if (to) {
- this.profileRef.current.classList.remove("topHidden")
- } else {
- this.profileRef.current.classList.add("topHidden")
- }
- }
onClickFollow = async () => {
const result = await FollowsModel.toggleFollow({
@@ -165,8 +121,6 @@ export default class Account extends React.Component {
return
}
- this.onPostListTopVisibility(true)
-
key = key.toLowerCase()
if (this.state.tabActiveKey === key) {
@@ -195,11 +149,10 @@ export default class Account extends React.Component {
}
return
this.toggleCoverExpanded()}
id="profile-cover"
@@ -217,18 +169,12 @@ export default class Account extends React.Component {
}
-
+
-
+
-
+
+
+ {
+ React.createElement(TabsComponent[this.state.tabActiveKey], {
+ onTopVisibility: this.onPostListTopVisibility,
+ state: this.state
+ })
+ }
+
+
-
+
{
- {props.state.followers.length}
+ {props.state.followersCount}
@@ -117,7 +117,7 @@ export default (props) => {
{
- getJoinLabel(Number(props.state.user.createdAt))
+ getJoinLabel(Number(props.state.user.created_at ?? props.state.user.createdAt))
}
diff --git a/packages/app/src/pages/account/tabs/music/index.jsx b/packages/app/src/pages/account/tabs/music/index.jsx
index 548d4dc3..d9600437 100755
--- a/packages/app/src/pages/account/tabs/music/index.jsx
+++ b/packages/app/src/pages/account/tabs/music/index.jsx
@@ -19,7 +19,7 @@ export default (props) => {
return
}
diff --git a/packages/app/src/pages/home/components/global/index.jsx b/packages/app/src/pages/home/components/global/index.jsx
index 96cb925a..d7ee11d0 100755
--- a/packages/app/src/pages/home/components/global/index.jsx
+++ b/packages/app/src/pages/home/components/global/index.jsx
@@ -2,19 +2,18 @@ import React from "react"
import { PostsList } from "components"
-import Post from "models/post"
+import Feed from "models/feed"
import "./index.less"
export default class ExplorePosts extends React.Component {
render() {
return
diff --git a/packages/app/src/pages/home/components/savedPosts/index.jsx b/packages/app/src/pages/home/components/savedPosts/index.jsx
index 738647ef..490b1330 100755
--- a/packages/app/src/pages/home/components/savedPosts/index.jsx
+++ b/packages/app/src/pages/home/components/savedPosts/index.jsx
@@ -16,8 +16,9 @@ const emptyListRender = () => {
export class SavedPosts extends React.Component {
render() {
return
}
}
diff --git a/packages/app/src/pages/marketplace/index.jsx b/packages/app/src/pages/marketplace/index.jsx
new file mode 100644
index 00000000..aea9f218
--- /dev/null
+++ b/packages/app/src/pages/marketplace/index.jsx
@@ -0,0 +1,80 @@
+import React from "react"
+import SearchButton from "components/SearchButton"
+import { Icons, createIconRender } from "components/Icons"
+import Image from "components/Image"
+
+import "./index.less"
+
+const FieldItem = (props) => {
+ return
+
+
+
+
+
+
+ {props.title}
+
+
+
+ {props.description}
+
+
+
+}
+
+const ExtensionsBrowser = () => {
+ return
+
+
+
+ Extensions
+
+
+
+
+
+
+
+
+
+
+}
+
+const Marketplace = () => {
+ return
+
+
+
+ Marketplace
+
+
+
+
+
+
+
+
+
+
+}
+
+export default Marketplace
\ No newline at end of file
diff --git a/packages/app/src/pages/marketplace/index.less b/packages/app/src/pages/marketplace/index.less
new file mode 100644
index 00000000..81b293a8
--- /dev/null
+++ b/packages/app/src/pages/marketplace/index.less
@@ -0,0 +1,114 @@
+.marketplace {
+ display: flex;
+ flex-direction: column;
+
+ gap: 30px;
+
+ width: 100%;
+
+ .marketplace-header {
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+
+ justify-content: space-between;
+
+ width: 100%;
+
+ .marketplace-header-card {
+ display: flex;
+ flex-direction: row;
+
+ width: fit-content;
+
+ gap: 10px;
+
+ background-color: var(--background-color-accent);
+ padding: 10px 20px;
+
+ border-radius: 12px;
+ }
+
+ h1 {
+ font-family: "Space Grotesk", sans-serif;
+ font-size: 2rem;
+ margin: 0;
+ }
+ }
+
+ .marketplace-field {
+ display: flex;
+ flex-direction: column;
+
+ .marketplace-field-header {
+
+ }
+
+ .marketplace-field-slider {
+ display: flex;
+ flex-direction: row;
+
+ gap: 20px;
+ }
+
+ .marketplace-field-item {
+ position: relative;
+
+ display: flex;
+ flex-direction: column;
+
+ gap: 10px;
+
+ width: 250px;
+ height: 280px;
+
+ background-color: var(--background-color-accent);
+
+ border-radius: 12px;
+
+ padding: 10px;
+
+ .marketplace-field-item-image {
+ width: 100%;
+ height: 60%;
+
+ .lazy-load-image-background {
+ width: 100%;
+ height: 100%;
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+
+ object-fit: cover;
+
+ overflow: hidden;
+
+ border-radius: 12px;
+
+ background-color: black;
+ }
+ }
+
+ .marketplace-field-item-info {
+ display: flex;
+ flex-direction: column;
+
+ gap: 7px;
+
+ height: 40%;
+ width: 100%;
+
+ overflow: hidden;
+
+ h1, p {
+ text-wrap: none;
+ text-overflow: ellipsis;
+ margin: 0;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/pages/post/[post_id].jsx b/packages/app/src/pages/post/[post_id].jsx
index 78c715d3..1ff1a18a 100755
--- a/packages/app/src/pages/post/[post_id].jsx
+++ b/packages/app/src/pages/post/[post_id].jsx
@@ -1,44 +1,50 @@
import React from "react"
import * as antd from "antd"
-import Post from "models/post"
-import { PostCard, CommentsCard } from "components"
+import PostCard from "components/PostCard"
+import PostsList from "components/PostsList"
+
+import PostService from "models/post"
import "./index.less"
export default (props) => {
const post_id = props.params.post_id
- const [data, setData] = React.useState(null)
+ const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
+ post_id,
+ })
- const loadData = async () => {
- setData(null)
-
- const data = await Post.getPost({ post_id }).catch(() => {
- antd.message.error("Failed to get post")
-
- return false
- })
-
- if (data) {
- setData(data)
- }
+ if (error) {
+ return
}
- React.useEffect(() => {
- loadData()
- }, [])
-
- if (!data) {
+ if (loading) {
return
}
- return
-
-
+ return
+
-
}
\ No newline at end of file
diff --git a/packages/app/src/pages/post/index.less b/packages/app/src/pages/post/index.less
index 513ca02d..95cd697d 100755
--- a/packages/app/src/pages/post/index.less
+++ b/packages/app/src/pages/post/index.less
@@ -1,30 +1,8 @@
-.postPage {
+.post-page {
display: flex;
- flex-direction: row;
+ flex-direction: column;
width: 100%;
- height: 100vh;
- overflow: hidden;
-
- .postWrapper {
- margin: 0 10px;
-
- height: 100%;
- width: 70vw;
-
- min-width: 70vw;
- max-width: 70vw;
- }
-
- .commentsWrapper {
- height: 100vh;
- width: 100%;
-
- min-width: 300px;
-
- overflow: scroll;
-
- margin: 0 10px;
- }
+ gap: 20px;
}
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx b/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx
index 8b304dcf..7a3d909d 100755
--- a/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx
+++ b/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx
@@ -308,6 +308,10 @@ export default class SettingItemComponent extends React.PureComponent {
}
}
+ if (typeof this.props.onUpdate === "function") {
+ await this.props.onUpdate(updateValue)
+ }
+
// finaly update value
await this.setState({
value: updateValue
diff --git a/packages/app/src/pages/settings/components/SettingTab/index.jsx b/packages/app/src/pages/settings/components/SettingTab/index.jsx
index 857b6e89..2222afe8 100755
--- a/packages/app/src/pages/settings/components/SettingTab/index.jsx
+++ b/packages/app/src/pages/settings/components/SettingTab/index.jsx
@@ -16,50 +16,49 @@ import SettingItemComponent from "../SettingItemComponent"
export default class SettingTab extends React.Component {
state = {
loading: true,
- processedCtx: {}
+ tab: null,
+ ctx: {},
}
- tab = composedTabs[this.props.activeKey]
+ loadTab = async () => {
+ await this.setState({
+ loading: true,
+ processedCtx: {},
+ })
- processCtx = async () => {
- if (typeof this.tab.ctxData === "function") {
- this.setState({ loading: true })
+ const tab = composedTabs[this.props.activeKey]
- const resultCtx = await this.tab.ctxData()
+ let ctx = {}
- console.log(resultCtx)
-
- this.setState({
- loading: false,
- processedCtx: resultCtx
- })
+ if (typeof tab.ctxData === "function") {
+ ctx = await tab.ctxData()
}
+
+ await this.setState({
+ tab: tab,
+ loading: false,
+ ctx: {
+ baseConfig: this.props.baseConfig,
+ ...ctx
+ },
+ })
}
// check if props.activeKey change
componentDidUpdate = async (prevProps) => {
if (prevProps.activeKey !== this.props.activeKey) {
- this.tab = composedTabs[this.props.activeKey]
-
- this.setState({
- loading: !!this.tab.ctxData,
- processedCtx: {}
- })
-
- await this.processCtx()
+ await this.loadTab()
}
}
componentDidMount = async () => {
- this.setState({
- loading: !!this.tab.ctxData,
- })
+ await this.loadTab()
+ }
- await this.processCtx()
-
- this.setState({
- loading: false
- })
+ handleSettingUpdate = async (key, value) => {
+ if (typeof this.props.onUpdate === "function") {
+ await this.props.onUpdate(key, value)
+ }
}
render() {
@@ -67,14 +66,16 @@ export default class SettingTab extends React.Component {
return
}
- if (this.tab.render) {
- return React.createElement(this.tab.render, {
- ctx: this.state.processedCtx
+ const { ctx, tab } = this.state
+
+ if (tab.render) {
+ return React.createElement(tab.render, {
+ ctx: ctx,
})
}
if (this.props.withGroups) {
- const group = composeGroupsFromSettingsTab(this.tab.settings)
+ const group = composeGroupsFromSettingsTab(tab.settings)
return <>
{
@@ -98,9 +99,11 @@ export default class SettingTab extends React.Component {
{
- settings.map((setting) => this.handleSettingUpdate(setting.id, value)}
/>)
}
@@ -109,8 +112,8 @@ export default class SettingTab extends React.Component {
}
{
- this.tab.footer && React.createElement(this.tab.footer, {
- ctx: this.state.processedCtx
+ tab.footer && React.createElement(tab.footer, {
+ ctx: this.state.ctx
})
}
>
@@ -118,18 +121,22 @@ export default class SettingTab extends React.Component {
return <>
{
- this.tab.settings.map((setting, index) => {
+ tab.settings.map((setting, index) => {
return
this.handleSettingUpdate(setting.id, value)}
/>
})
}
{
- this.tab.footer && React.createElement(this.tab.footer, {
- ctx: this.state.processedCtx
+ tab.footer && React.createElement(tab.footer, {
+ ctx: this.state.ctx
})
}
>
diff --git a/packages/app/src/pages/settings/index.jsx b/packages/app/src/pages/settings/index.jsx
index bd266972..70bd5315 100755
--- a/packages/app/src/pages/settings/index.jsx
+++ b/packages/app/src/pages/settings/index.jsx
@@ -1,11 +1,11 @@
import React from "react"
import * as antd from "antd"
-import { Translation } from "react-i18next"
-import classnames from "classnames"
-import config from "config"
-import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
-
import { createIconRender } from "components/Icons"
+import { Translation } from "react-i18next"
+import config from "config"
+
+import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
+import useUserRemoteConfig from "hooks/useUserRemoteConfig"
import {
composedSettingsByGroups as settings
@@ -88,6 +88,7 @@ const generateMenuItems = () => {
}
export default () => {
+ const [config, setConfig, loading] = useUserRemoteConfig()
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
defaultKey: "general",
queryKey: "tab"
@@ -113,11 +114,14 @@ export default () => {
return items
}, [])
- return
+ function handleOnUpdate(key, value) {
+ setConfig({
+ ...config,
+ [key]: value
+ })
+ }
+
+ return
-
+ {
+ loading &&
+ }
+ {
+ !loading &&
+ }
}
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/index.less b/packages/app/src/pages/settings/index.less
index c130b510..267ccdcc 100755
--- a/packages/app/src/pages/settings/index.less
+++ b/packages/app/src/pages/settings/index.less
@@ -95,6 +95,11 @@
padding: 0 20px;
+ .uploadButton{
+ background-color: var(--background-color-primary);
+ border: 1px solid var(--border-color);
+ }
+
.setting_item_header {
display: inline-flex;
flex-direction: row;
diff --git a/packages/app/src/theme/fixments.less b/packages/app/src/theme/fixments.less
index ce8bb205..1d7cbfc4 100755
--- a/packages/app/src/theme/fixments.less
+++ b/packages/app/src/theme/fixments.less
@@ -299,23 +299,100 @@
}
}
-.ant-notification-notice {
- background-color: var(--background-color-primary) !important;
+.ant-notification {
+ display: flex;
+ flex-direction: column;
+
+ gap: 20px;
+
+ .ant-notification-notice-wrapper {
+ margin: 0;
+ overflow: hidden;
+
+ border-radius: 12px;
+ outline: 1px solid var(--border-color) !important;
+ background-color: var(--background-color-primary) !important;
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
- p,
- span {
- color: var(--text-color) !important;
}
- .ant-notification-notice-message,
- .ant-notification-notice-description {
- color: var(--text-color) !important;
+ .ant-notification-notice {
+ display: flex;
+ flex-direction: column;
+
+ justify-content: center;
+
+ background-color: transparent !important;
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ p,
+ span {
+ color: var(--text-color) !important;
+ }
+
+ .ant-notification-notice-close-x {
+ svg {
+ margin: 0 !important;
+ }
+ }
+
+ .ant-notification-notice-with-icon {
+ display: flex;
+ flex-direction: column;
+
+ gap: 10px;
+
+ .ant-notification-notice-message {
+ margin: 0 !important;
+ }
+ }
+
+ .ant-notification-notice-message,
+ .ant-notification-notice-description {
+ color: var(--text-color) !important;
+ }
+
+ .ant-notification-notice-description {
+ display: none;
+ }
+
+ .ant-notification-notice-description:not(:empty) {
+ display: flex;
+ }
+
+ .ant-notification-notice-with-icon {
+ .ant-notification-notice-message {
+ margin-inline-start: 46px !important;
+ }
+
+ .ant-notification-notice-description {
+ margin-inline-start: 46px !important;
+ }
+
+ .ant-notification-notice-icon {
+ &.ant-notification-notice-icon-success {
+ color: #52c41a !important;
+ }
+
+ max-width: 40px;
+
+ svg {
+ margin: 0 !important;
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+
+ border-radius: 8px;
+ }
+ }
+ }
}
}
@@ -334,6 +411,7 @@
}
}
+
.ant-message-error {
svg {
color: var(--ant-error-color) !important;
@@ -518,30 +596,6 @@
z-index: 1000;
}
-.ant-notification-notice {
- .ant-notification-notice-with-icon {
- .ant-notification-notice-message {
- margin-inline-start: 46px !important;
- }
-
- .ant-notification-notice-description {
- margin-inline-start: 46px !important;
- }
-
- .ant-notification-notice-icon {
- max-width: 40px;
-
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-
- border-radius: 8px;
- }
- }
- }
-}
-
.adm-action-sheet {
.adm-action-sheet-button-item-wrapper {
border-bottom-color: var(--border-color);
diff --git a/packages/server/classes/ChunkFileUpload/index.js b/packages/server/classes/ChunkFileUpload/index.js
new file mode 100755
index 00000000..28757b27
--- /dev/null
+++ b/packages/server/classes/ChunkFileUpload/index.js
@@ -0,0 +1,180 @@
+// Orginal forked from: Buzut/huge-uploader-nodejs
+// Copyright (c) 2018, Quentin Busuttil All rights reserved.
+
+import fs from "node:fs"
+import path from "node:path"
+import mimetypes from "mime-types"
+
+export function checkTotalSize(
+ chunkSize, // in bytes
+ totalChunks, // number of chunks
+ maxFileSize, // in bytes
+) {
+ const totalSize = chunkSize * totalChunks
+
+ if (totalSize > maxFileSize) {
+ return false
+ }
+
+ return true
+}
+
+export function checkChunkUploadHeaders(headers) {
+ if (
+ !headers["uploader-chunk-number"] ||
+ !headers["uploader-chunks-total"] ||
+ !headers["uploader-original-name"] ||
+ !headers["uploader-file-id"] ||
+ !headers["uploader-chunks-total"].match(/^[0-9]+$/) ||
+ !headers["uploader-chunk-number"].match(/^[0-9]+$/)
+ ) {
+ return false
+ }
+
+ return true
+}
+
+export function createAssembleChunksPromise({
+ chunksPath, // chunks to assemble
+ filePath, // final assembled file path
+ maxFileSize,
+}) {
+ return () => new Promise(async (resolve, reject) => {
+ let fileSize = 0
+
+ const chunks = await fs.promises.readdir(chunksPath)
+
+ if (chunks.length === 0) {
+ throw new Error("No chunks found")
+ }
+
+ for await (const chunk of chunks) {
+ const chunkPath = path.join(chunksPath, chunk)
+ const data = await fs.promises.readFile(chunkPath)
+
+ fileSize += data.length
+
+ // check if final file gonna exceed max file size
+ // in case early estimation is wrong (due client send bad headers)
+ if (fileSize > maxFileSize) {
+ return reject(new OperationError(413, "File exceeds max total file size, aborting assembly..."))
+ }
+
+ await fs.promises.appendFile(filePath, data)
+
+ continue
+ }
+
+ return resolve({
+ chunksLength: chunks.length,
+ filePath: filePath,
+ })
+ })
+}
+
+export async function handleChunkFile(fileStream, { tmpDir, headers, maxFileSize, maxChunkSize }) {
+ return await new Promise(async (resolve, reject) => {
+ const chunksPath = path.join(tmpDir, headers["uploader-file-id"], "chunks")
+ const chunkPath = path.join(chunksPath, headers["uploader-chunk-number"])
+
+ const chunkCount = +headers["uploader-chunk-number"]
+ const totalChunks = +headers["uploader-chunks-total"]
+
+ // check if file has all chunks uploaded
+ const isLast = chunkCount === totalChunks - 1
+
+ // make sure chunk is in range
+ if (chunkCount < 0 || chunkCount >= totalChunks) {
+ throw new Error("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 })
+ }
+ }
+
+ let dataWritten = 0
+
+ let writeStream = fs.createWriteStream(chunkPath)
+
+ writeStream.on("error", (err) => {
+ reject(err)
+ })
+
+ writeStream.on("close", () => {
+ if (maxChunkSize !== undefined) {
+ if (dataWritten > maxChunkSize) {
+ reject(new OperationError(413, "Chunk size exceeds max chunk size, aborting upload..."))
+ return
+ }
+
+ // estimate total file size,
+ // if estimation exceeds maxFileSize, abort upload
+ if (chunkCount === 0 && totalChunks > 0) {
+ if ((dataWritten * (totalChunks - 1)) > maxFileSize) {
+ reject(new OperationError(413, "File estimated size exceeds max total file size, aborting upload..."))
+ return
+ }
+ }
+ }
+
+ if (isLast) {
+ const mimetype = mimetypes.lookup(headers["uploader-original-name"])
+ const extension = mimetypes.extension(mimetype)
+
+ let filename = headers["uploader-file-id"]
+
+ if (headers["uploader-use-date"] === "true") {
+ filename = `${filename}_${Date.now()}`
+ }
+
+ return resolve(createAssembleChunksPromise({
+ // build data
+ chunksPath: chunksPath,
+ filePath: path.resolve(chunksPath, `${filename}.${extension}`),
+ maxFileSize: maxFileSize,
+ }))
+ }
+
+ return resolve(null)
+ })
+
+ fileStream.on("data", (buffer) => {
+ dataWritten += buffer.byteLength
+ })
+
+ fileStream.pipe(writeStream)
+ })
+}
+
+export async function uploadChunkFile(req, {
+ tmpDir,
+ maxFileSize,
+ maxChunkSize,
+}) {
+ return await new Promise(async (resolve, reject) => {
+ if (!checkChunkUploadHeaders(req.headers)) {
+ reject(new OperationErrorError(400, "Missing header(s)"))
+ return
+ }
+
+ await req.multipart(async (field) => {
+ try {
+ const result = await handleChunkFile(field.file.stream, {
+ tmpDir: tmpDir,
+ headers: req.headers,
+ maxFileSize: maxFileSize,
+ maxChunkSize: maxChunkSize,
+ })
+
+ return resolve(result)
+ } catch (error) {
+ return reject(error)
+ }
+ })
+ })
+}
+
+export default uploadChunkFile
\ No newline at end of file
diff --git a/packages/server/classes/FileUpload/index.js b/packages/server/classes/FileUpload/index.js
deleted file mode 100755
index 6a609c2f..00000000
--- a/packages/server/classes/FileUpload/index.js
+++ /dev/null
@@ -1,260 +0,0 @@
-// Orginal forked from: Buzut/huge-uploader-nodejs
-// Copyright (c) 2018, Quentin Busuttil All rights reserved.
-
-import fs from "node:fs"
-import path from "node:path"
-import { promisify } from "node:util"
-import mimetypes from "mime-types"
-import crypto from "node:crypto"
-
-import Busboy from "busboy"
-
-export function getFileHash(file) {
- return new Promise((resolve, reject) => {
- const hash = crypto.createHash("sha256")
-
- file.on("data", (chunk) => hash.update(chunk))
-
- file.on("end", () => resolve(hash.digest("hex")))
-
- file.on("error", reject)
- })
-}
-
-export function checkHeaders(headers) {
- if (
- !headers["uploader-chunk-number"] ||
- !headers["uploader-chunks-total"] ||
- !headers["uploader-original-name"] ||
- !headers["uploader-file-id"] ||
- !headers["uploader-chunks-total"].match(/^[0-9]+$/) ||
- !headers["uploader-chunk-number"].match(/^[0-9]+$/)
- ) {
- return false
- }
-
- return true
-}
-
-export function checkTotalSize(maxFileSize, maxChunkSize, totalChunks) {
- if (maxChunkSize * totalChunks > maxFileSize) {
- return false
- }
-
- return true
-}
-
-export function cleanChunks(dirPath) {
- fs.readdir(dirPath, (err, files) => {
- let filesLength = files.length
-
- files.forEach((file) => {
- fs.unlink(path.join(dirPath, file), () => {
- if (--filesLength === 0) fs.rmdir(dirPath, () => { }) // cb does nothing but required
- })
- })
- })
-}
-
-export function createAssembleChunksPromise({
- tmpDir,
- headers,
- useDate,
-}) {
- const asyncReadFile = promisify(fs.readFile)
- const asyncAppendFile = promisify(fs.appendFile)
-
- const originalMimeType = mimetypes.lookup(headers["uploader-original-name"])
- const originalExtension = mimetypes.extension(originalMimeType)
-
- const totalChunks = +headers["uploader-chunks-total"]
-
- const fileId = headers["uploader-file-id"]
- const workPath = path.join(tmpDir, fileId)
- const chunksPath = path.resolve(workPath, "chunks")
- const assembledFilepath = path.join(workPath, `assembled.${originalExtension}`)
-
- let chunkCount = 0
- let finalFilepath = null
-
- return () => {
- return new Promise((resolve, reject) => {
- const onEnd = async () => {
- try {
- const hash = await getFileHash(fs.createReadStream(assembledFilepath))
-
- if (useDate) {
- finalFilepath = path.resolve(workPath, `${hash}_${Date.now()}.${originalExtension}`)
- } else {
- finalFilepath = path.resolve(workPath, `${hash}.${originalExtension}`)
- }
-
- fs.renameSync(assembledFilepath, finalFilepath)
-
- cleanChunks(chunksPath)
-
- return resolve({
- filename: headers["uploader-original-name"],
- filepath: finalFilepath,
- cachePath: workPath,
- hash,
- mimetype: originalMimeType,
- extension: originalExtension,
- })
- } catch (error) {
- return reject(error)
- }
- }
-
- const pipeChunk = () => {
- asyncReadFile(path.join(chunksPath, chunkCount.toString()))
- .then((chunk) => asyncAppendFile(assembledFilepath, chunk))
- .then(() => {
- // 0 indexed files = length - 1, so increment before comparison
- if (totalChunks > ++chunkCount) {
- return pipeChunk(chunkCount)
- }
-
- return onEnd()
- })
- .catch(reject)
- }
-
- pipeChunk()
- })
- }
-}
-
-export function mkdirIfDoesntExist(dirPath, callback) {
- if (!fs.existsSync(dirPath)) {
- fs.mkdir(dirPath, { recursive: true }, callback)
- }
-}
-
-export function handleFile(tmpDir, headers, fileStream) {
- const dirPath = path.join(tmpDir, headers["uploader-file-id"])
- const chunksPath = path.join(dirPath, "chunks")
- const chunkPath = path.join(chunksPath, headers["uploader-chunk-number"])
- const useDate = headers["uploader-use-date"] === "true"
- const chunkCount = +headers["uploader-chunk-number"]
- const totalChunks = +headers["uploader-chunks-total"]
-
- let error
- let assembleChunksPromise
- let finished = false
- let writeStream
-
- const writeFile = () => {
- writeStream = fs.createWriteStream(chunkPath)
-
- writeStream.on("error", (err) => {
- error = err
- fileStream.resume()
- })
-
- writeStream.on("close", () => {
- finished = true
-
- // if all is uploaded
- if (chunkCount === totalChunks - 1) {
- assembleChunksPromise = createAssembleChunksPromise({
- tmpDir,
- headers,
- useDate,
- })
- }
- })
-
- fileStream.pipe(writeStream)
- }
-
- // make sure chunk is in range
- if (chunkCount < 0 || chunkCount >= totalChunks) {
- error = new Error("Chunk is out of range")
- fileStream.resume()
- }
-
- else if (chunkCount === 0) {
- // create file upload dir if it's first chunk
- mkdirIfDoesntExist(chunksPath, (err) => {
- if (err) {
- error = err
- fileStream.resume()
- }
-
- else writeFile()
- })
- }
-
- else {
- // make sure dir exists if it's not first chunk
- fs.stat(dirPath, (err) => {
- if (err) {
- error = new Error("Upload has expired")
- fileStream.resume()
- }
-
- else writeFile()
- })
- }
-
- return (callback) => {
- if (finished && !error) callback(null, assembleChunksPromise)
- else if (error) callback(error)
-
- else {
- writeStream.on("error", callback)
- writeStream.on("close", () => callback(null, assembleChunksPromise))
- }
- }
-}
-
-export function uploadFile(req, tmpDir, maxFileSize, maxChunkSize) {
- return new Promise((resolve, reject) => {
- if (!checkHeaders(req.headers)) {
- reject(new Error("Missing header(s)"))
- return
- }
-
- if (!checkTotalSize(maxFileSize, req.headers["uploader-chunks-total"])) {
- reject(new Error("File is above size limit"))
- return
- }
-
- try {
- let limitReached = false
- let getFileStatus
-
- const busboy = Busboy({ headers: req.headers, limits: { files: 1, fileSize: maxChunkSize * 1000 * 1000 } })
-
- busboy.on("file", (fieldname, fileStream) => {
- fileStream.on("limit", () => {
- limitReached = true
- fileStream.resume()
- })
-
- getFileStatus = handleFile(tmpDir, req.headers, fileStream)
- })
-
- busboy.on("close", () => {
- if (limitReached) {
- reject(new Error("Chunk is above size limit"))
- return
- }
-
- getFileStatus((fileErr, assembleChunksF) => {
- if (fileErr) reject(fileErr)
- else resolve(assembleChunksF)
- })
- })
-
- req.pipe(busboy)
- }
-
- catch (err) {
- reject(err)
- }
- })
-}
-
-export default uploadFile
\ No newline at end of file
diff --git a/packages/server/classes/Limits/index.js b/packages/server/classes/Limits/index.js
new file mode 100644
index 00000000..a6ef78bf
--- /dev/null
+++ b/packages/server/classes/Limits/index.js
@@ -0,0 +1,40 @@
+import { Config } from "@db_models"
+
+export default class Limits {
+ static async get(key) {
+ const { value } = await Config.findOne({
+ key: "limits"
+ }).catch(() => {
+ return {
+ value: {}
+ }
+ })
+
+ const limits = {
+ maxChunkSizeInMB: 5,
+ maxFileSizeInMB: 8,
+ maxNumberOfFiles: 10,
+ maxPostCharacters: 2000,
+ maxAccountsPerIp: 10,
+ ...value,
+ }
+
+ if (typeof key === "string") {
+ return {
+ value: limits[key] ?? null
+ }
+ }
+
+ if (Array.isArray(key)) {
+ const result = {}
+
+ key.forEach((k) => {
+ result[k] = limits[k] ?? null
+ })
+
+ return result
+ }
+
+ return limits
+ }
+}
\ No newline at end of file
diff --git a/packages/server/db_models/post/index.js b/packages/server/db_models/post/index.js
index 8914aad7..73702094 100755
--- a/packages/server/db_models/post/index.js
+++ b/packages/server/db_models/post/index.js
@@ -8,5 +8,6 @@ export default {
attachments: { type: Array, default: [] },
flags: { type: Array, default: [] },
reply_to: { type: String, default: null },
+ updated_at: { type: String, default: null },
}
}
\ No newline at end of file
diff --git a/packages/server/db_models/savedPost/index.js b/packages/server/db_models/postSave/index.js
similarity index 84%
rename from packages/server/db_models/savedPost/index.js
rename to packages/server/db_models/postSave/index.js
index 05634f5b..6589ff74 100755
--- a/packages/server/db_models/savedPost/index.js
+++ b/packages/server/db_models/postSave/index.js
@@ -1,6 +1,6 @@
export default {
- name: "SavedPost",
- collection: "savedPosts",
+ name: "PostSave",
+ collection: "post_saves",
schema: {
post_id: {
type: "string",
diff --git a/packages/server/db_models/user/index.js b/packages/server/db_models/user/index.js
index 1bcf247c..7f267cee 100755
--- a/packages/server/db_models/user/index.js
+++ b/packages/server/db_models/user/index.js
@@ -15,6 +15,6 @@ export default {
badges: { type: Array, default: [] },
links: { type: Array, default: [] },
location: { type: String, default: null },
- birthday: { type: Date, default: null },
+ birthday: { type: Date, default: null, select: false },
}
}
\ No newline at end of file
diff --git a/packages/server/index.js b/packages/server/index.js
index bf3c8844..5ebf633b 100755
--- a/packages/server/index.js
+++ b/packages/server/index.js
@@ -10,18 +10,16 @@ import chalk from "chalk"
import Spinnies from "spinnies"
import chokidar from "chokidar"
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
-import fastify from "fastify"
-import { createProxyMiddleware } from "http-proxy-middleware"
+import treeKill from "tree-kill"
import { dots as DefaultSpinner } from "spinnies/spinners.json"
import getInternalIp from "./lib/getInternalIp"
import comtyAscii from "./ascii"
import pkg from "./package.json"
-import cors from "linebridge/src/server/middlewares/cors"
-
import { onExit } from "signal-exit"
+import Proxy from "./proxy"
const bootloaderBin = path.resolve(__dirname, "boot")
const servicesPath = path.resolve(__dirname, "services")
@@ -51,7 +49,7 @@ async function scanServices() {
return finalServices
}
-let internal_proxy = null
+let internal_proxy = new Proxy()
let allReady = false
let selectedProcessInstance = null
let internalIp = null
@@ -72,7 +70,7 @@ Observable.observe(serviceRegistry, (changes) => {
//console.log(`Updated service | ${path} > ${value}`)
//check if all services all ready
- if (Object.values(serviceRegistry).every((service) => service.ready)) {
+ if (Object.values(serviceRegistry).every((service) => service.initialized)) {
handleAllReady()
}
@@ -176,6 +174,8 @@ async function handleAllReady() {
console.log(comtyAscii)
console.log(`🎉 All services[${services.length}] ready!\n`)
console.log(`USE: select
, reboot, exit`)
+
+ await internal_proxy.listen(9000, "0.0.0.0")
}
// SERVICE WATCHER FUNCTIONS
@@ -189,6 +189,8 @@ async function handleNewServiceStarting(id) {
}
async function handleServiceStarted(id) {
+ serviceRegistry[id].initialized = true
+
if (serviceRegistry[id].ready === false) {
if (spinnies.pick(id)) {
spinnies.succeed(id, { text: `[${id}][${serviceRegistry[id].index}] Ready` })
@@ -199,7 +201,7 @@ async function handleServiceStarted(id) {
}
async function handleServiceExit(id, code, err) {
- //console.log(`🛑 Service ${id} exited with code ${code}`, err)
+ serviceRegistry[id].initialized = true
if (serviceRegistry[id].ready === false) {
if (spinnies.pick(id)) {
@@ -207,29 +209,14 @@ async function handleServiceExit(id, code, err) {
}
}
+ console.log(`[${id}] Exit with code ${code}`)
+
+ // try to unregister from proxy
+ internal_proxy.unregisterAllFromService(id)
+
serviceRegistry[id].ready = false
}
-async function registerProxy(_path, target, pathRewrite) {
- if (internal_proxy.proxys.has(_path)) {
- console.warn(`Proxy already registered [${_path}], skipping...`)
- return false
- }
-
- console.log(`🔗 Registering path proxy [${_path}] -> [${target}]`)
-
- internal_proxy.proxys.add(_path)
-
- internal_proxy.use(_path, createProxyMiddleware({
- target: target,
- changeOrigin: true,
- pathRewrite: pathRewrite,
- ws: true,
- logLevel: "silent",
- }))
-
- return true
-}
async function handleIPCData(service_id, msg) {
if (msg.type === "log") {
@@ -243,21 +230,35 @@ async function handleIPCData(service_id, msg) {
if (msg.type === "router:register") {
if (msg.data.path_overrides) {
for await (let pathOverride of msg.data.path_overrides) {
- await registerProxy(
- `/${pathOverride}`,
- `http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
- {
+ await internal_proxy.register({
+ serviceId: service_id,
+ path: `/${pathOverride}`,
+ target: `http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
+ pathRewrite: {
[`^/${pathOverride}`]: "",
- }
- )
+ },
+ })
}
} else {
- await registerProxy(
- `/${service_id}`,
- `http://${msg.data.listen.ip}:${msg.data.listen.port}`
- )
+ await internal_proxy.register({
+ serviceId: service_id,
+ path: `/${service_id}`,
+ target: `http://${msg.data.listen.ip}:${msg.data.listen.port}`,
+ })
}
}
+
+ if (msg.type === "router:ws:register") {
+ await internal_proxy.register({
+ serviceId: service_id,
+ path: `/${msg.data.namespace}`,
+ target: `http://${internalIp}:${msg.data.listen.port}/${msg.data.namespace}`,
+ pathRewrite: {
+ [`^/${msg.data.namespace}`]: "",
+ },
+ ws: true,
+ })
+ }
}
function spawnService({ id, service, cwd }) {
@@ -276,11 +277,15 @@ function spawnService({ id, service, cwd }) {
silent: true,
cwd: cwd,
env: instanceEnv,
+ killSignal: "SIGKILL",
})
instance.reload = () => {
ipcRouter.unregister({ id, instance })
+ // try to unregister from proxy
+ internal_proxy.unregisterAllFromService(id)
+
instance.kill()
instance = spawnService({ id, service, cwd })
@@ -340,31 +345,6 @@ function createServiceLogTransformer({ id, color = "bgCyan" }) {
async function main() {
internalIp = await getInternalIp()
- internal_proxy = fastify()
-
- internal_proxy.proxys = new Set()
-
- await internal_proxy.register(require("@fastify/middie"))
-
- await internal_proxy.use(cors)
-
- internal_proxy.get("/ping", (request, reply) => {
- return reply.send({
- status: "ok"
- })
- })
-
- internal_proxy.get("/", (request, reply) => {
- return reply.send({
- services: instancePool.map((instance) => {
- return {
- id: instance.id,
- version: instance.version,
- }
- }),
- })
- })
-
console.clear()
console.log(comtyAscii)
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
@@ -417,6 +397,7 @@ async function main() {
if (process.env.NODE_ENV === "development") {
const ignored = [
...await getIgnoredFiles(cwd),
+ "**/.cache/**",
"**/node_modules/**",
"**/dist/**",
"**/build/**",
@@ -438,7 +419,6 @@ async function main() {
}
}
- // create repl
repl.start({
prompt: "> ",
useGlobal: true,
@@ -474,11 +454,6 @@ async function main() {
}
})
- await internal_proxy.listen({
- host: "0.0.0.0",
- port: 9000
- })
-
onExit((code, signal) => {
console.clear()
console.log(`\n🛑 Preparing to exit...`)
@@ -493,7 +468,11 @@ async function main() {
console.log(`Killing ${instance.id} [${instance.instance.pid}]`)
instance.instance.kill()
+
+ treeKill(instance.instance.pid)
}
+
+ treeKill(process.pid)
})
}
diff --git a/packages/server/package.json b/packages/server/package.json
index 7d25d1be..bfbbc95c 100755
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -24,14 +24,21 @@
"clui": "^0.3.6",
"dotenv": "^16.4.4",
"fastify": "^4.26.2",
- "http-proxy-middleware": "^2.0.6",
+ "http-proxy": "^1.18.1",
+ "http-proxy-middleware": "^3.0.0-beta.1",
+ "hyper-express": "^6.14.12",
"jsonwebtoken": "^9.0.2",
"linebridge": "^0.18.1",
"module-alias": "^2.2.3",
"p-map": "^4.0.0",
"p-queue": "^7.3.4",
+ "radix3": "^1.1.1",
"signal-exit": "^4.1.0",
- "spinnies": "^0.5.1"
+ "spinnies": "^0.5.1",
+ "tree-kill": "^1.2.2",
+ "uWebSockets.js": "uNetworking/uWebSockets.js#v20.41.0",
+ "uws-reverse-proxy": "^3.2.1",
+ "yume-server": "^0.0.5"
},
"devDependencies": {
"chai": "^5.1.0",
diff --git a/packages/server/proxy.js b/packages/server/proxy.js
new file mode 100644
index 00000000..90fee2bd
--- /dev/null
+++ b/packages/server/proxy.js
@@ -0,0 +1,188 @@
+import http from "node:http"
+import httpProxy from "http-proxy"
+import defaults from "linebridge/src/server/defaults"
+
+import pkg from "./package.json"
+
+export default class Proxy {
+ constructor() {
+ this.proxys = new Map()
+ this.wsProxys = new Map()
+
+ this.http = http.createServer(this.handleHttpRequest)
+ this.http.on("upgrade", this.handleHttpUpgrade)
+ }
+
+ http = null
+
+ register = ({ serviceId, path, target, pathRewrite, ws } = {}) => {
+ if (!path) {
+ throw new Error("Path is required")
+ }
+
+ if (!target) {
+ throw new Error("Target is required")
+ }
+
+ if (this.proxys.has(path)) {
+ console.warn(`Proxy already registered [${path}], skipping...`)
+ return false
+ }
+
+ const proxy = httpProxy.createProxyServer({
+ target: target,
+ })
+
+ proxy.on("error", (e) => {
+ console.error(e)
+ })
+
+ const proxyObj = {
+ serviceId: serviceId ?? "default_service",
+ path: path,
+ target: target,
+ pathRewrite: pathRewrite,
+ proxy: proxy,
+ }
+
+ if (ws) {
+ console.log(`🔗 Registering websocket proxy [${path}] -> [${target}]`)
+ this.wsProxys.set(path, proxyObj)
+ } else {
+ console.log(`🔗 Registering path proxy [${path}] -> [${target}]`)
+ this.proxys.set(path, proxyObj)
+ }
+
+ return true
+ }
+
+ unregister = (path) => {
+ if (!this.proxys.has(path)) {
+ console.warn(`Proxy not registered [${path}], skipping...`)
+ return false
+ }
+
+ console.log(`🔗 Unregistering path proxy [${path}]`)
+
+ this.proxys.get(path).proxy.close()
+ this.proxys.delete(path)
+ }
+
+ unregisterAllFromService = (serviceId) => {
+ this.proxys.forEach((value, key) => {
+ if (value.serviceId === serviceId) {
+ this.unregister(value.path)
+ }
+ })
+ }
+
+ listen = async (port = 9000, host = "0.0.0.0", cb) => {
+ return await new Promise((resolve, reject) => {
+ this.http.listen(port, host, () => {
+ console.log(`🔗 Proxy listening on ${host}:${port}`)
+
+ if (cb) {
+ cb(this)
+ }
+
+ resolve(this)
+ })
+ })
+ }
+
+ rewritePath = (rewriteConfig, path) => {
+ let result = path
+ const rules = []
+
+ for (const [key, value] of Object.entries(rewriteConfig)) {
+ rules.push({
+ regex: new RegExp(key),
+ value: value,
+ })
+ }
+
+ for (const rule of rules) {
+ if (rule.regex.test(path)) {
+ result = result.replace(rule.regex, rule.value)
+ break
+ }
+ }
+
+ return result
+ }
+
+ setCorsHeaders = (res) => {
+ res.setHeader("Access-Control-Allow-Origin", "*")
+ res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
+ res.setHeader("Access-Control-Allow-Headers", "*")
+
+ return res
+ }
+
+ handleHttpRequest = (req, res) => {
+ res = this.setCorsHeaders(res)
+
+ const sanitizedUrl = req.url.split("?")[0]
+
+ // preflight continue with code 204
+ if (req.method === "OPTIONS") {
+ res.statusCode = 204
+ res.end()
+ return
+ }
+
+ if (sanitizedUrl === "/") {
+ return res.end(`
+ {
+ "name": "${pkg.name}",
+ "version": "${pkg.version}",
+ "lb_version": "${defaults.version}"
+ }
+ `)
+ }
+
+ const namespace = `/${sanitizedUrl.split("/")[1]}`
+ const route = this.proxys.get(namespace)
+
+ if (!route) {
+ res.statusCode = 404
+ res.end(`
+ {
+ "error": "404 Not found"
+ }
+ `)
+ return
+ }
+
+ if (route.pathRewrite) {
+ req.url = this.rewritePath(route.pathRewrite, req.url)
+ }
+
+ //console.log(`HTTP REQUEST :`, req.url)
+
+ route.proxy.web(req, res)
+ }
+
+ handleHttpUpgrade = (req, socket, head) => {
+ const namespace = `/${req.url.split("/")[1]}`
+ const route = this.wsProxys.get(namespace)
+
+ if (!route) {
+ // destroy socket
+ socket.destroy()
+ return false
+ }
+
+ if (route.pathRewrite) {
+ req.url = this.rewritePath(route.pathRewrite, req.url)
+ }
+
+ //console.log(`HTTP UPGRADING :`, req.url)
+
+ route.proxy.ws(req, socket, head)
+ }
+
+ close = () => {
+ this.http.close()
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/auth/routes/auth/post.js b/packages/server/services/auth/routes/auth/post.js
index b9dd04de..d10f4ed2 100644
--- a/packages/server/services/auth/routes/auth/post.js
+++ b/packages/server/services/auth/routes/auth/post.js
@@ -18,7 +18,7 @@ export default async (req, res) => {
})
if (userConfig && userConfig.values) {
- if (userConfig.values.mfa_enabled) {
+ if (userConfig.values["auth:mfa"]) {
let codeVerified = false
// search if is already a mfa session
diff --git a/packages/server/services/files/controllers/stream/index.js b/packages/server/services/files/controllers/stream/index.js
deleted file mode 100755
index 6994bfa9..00000000
--- a/packages/server/services/files/controllers/stream/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import path from "path"
-import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
-import getMiddlewares from "@utils/getMiddlewares"
-
-export default async (router) => {
- const routesPath = path.resolve(__dirname, "routes")
-
- const middlewares = await getMiddlewares(["withOptionalAuth"])
-
- for (const middleware of middlewares) {
- router.use(middleware)
- }
-
- router = createRoutesFromDirectory("routes", routesPath, router)
-
- return {
- path: "/stream",
- router,
- }
-}
\ No newline at end of file
diff --git a/packages/server/services/files/controllers/stream/routes/get/*.js b/packages/server/services/files/controllers/stream/routes/get/*.js
deleted file mode 100755
index 0fda1e2a..00000000
--- a/packages/server/services/files/controllers/stream/routes/get/*.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { NotFoundError, InternalServerError } from "@shared-classes/Errors"
-import mimetypes from "mime-types"
-
-export default async (req, res) => {
- const streamPath = req.params[0]
-
- global.storage.getObject(process.env.S3_BUCKET, streamPath, (err, dataStream) => {
- if (err) {
- console.error(err)
- return new InternalServerError(req, res, "Error while getting file from storage")
- }
-
- const extname = mimetypes.lookup(streamPath)
-
- // send chunked response
- res.status(200)
-
- // set headers
- res.setHeader("Content-Type", extname)
- res.setHeader("Accept-Ranges", "bytes")
-
- return dataStream.pipe(res)
- })
-}
\ No newline at end of file
diff --git a/packages/server/services/files/controllers/upload/index.js b/packages/server/services/files/controllers/upload/index.js
deleted file mode 100755
index 7a718668..00000000
--- a/packages/server/services/files/controllers/upload/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import path from "path"
-import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
-import getMiddlewares from "@utils/getMiddlewares"
-
-export default async (router) => {
- const routesPath = path.resolve(__dirname, "routes")
-
- const middlewares = await getMiddlewares(["withOptionalAuth"])
-
- for (const middleware of middlewares) {
- router.use(middleware)
- }
-
- router = createRoutesFromDirectory("routes", routesPath, router)
-
- return {
- path: "/upload",
- router,
- }
-}
\ No newline at end of file
diff --git a/packages/server/services/files/controllers/upload/routes/post/chunk.js b/packages/server/services/files/controllers/upload/routes/post/chunk.js
deleted file mode 100755
index f3e96bb4..00000000
--- a/packages/server/services/files/controllers/upload/routes/post/chunk.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import path from "path"
-import fs from "fs"
-
-import * as Errors from "@shared-classes/Errors"
-import FileUpload from "@shared-classes/FileUpload"
-import PostProcess from "@services/post-process"
-
-const cachePath = global.cache.constructor.cachePath
-
-export default async (req, res) => {
- // extract authentification header
- let auth = req.session
-
- if (!auth) {
- return new Errors.AuthorizationError(req, res)
- }
-
- const providerType = req.headers["provider-type"]
-
- const userPath = path.join(cachePath, req.session.user_id)
-
- // 10 GB in bytes
- const maxFileSize = 10 * 1000 * 1000 * 1000
-
- // 10MB in bytes
- const maxChunkSize = 10 * 1000 * 1000
-
- let build = await FileUpload(req, userPath, maxFileSize, maxChunkSize)
- .catch((err) => {
- console.log("err", err)
-
- new Errors.InternalServerError(req, res, err.message)
-
- return false
- })
-
- if (build === false) {
- return false
- } else {
- if (typeof build === "function") {
- try {
- build = await build()
-
- if (!req.headers["no-compression"]) {
- build = await PostProcess(build)
- }
-
- // compose remote path
- const remotePath = `${req.session.user_id}/${path.basename(build.filepath)}`
-
- let url = null
-
- switch (providerType) {
- case "premium-cdn": {
- // use backblaze b2
- await global.b2Storage.authorize()
-
- const uploadUrl = await global.b2Storage.getUploadUrl({
- bucketId: process.env.B2_BUCKET_ID,
- })
-
- const data = await fs.promises.readFile(build.filepath)
-
- await global.b2Storage.uploadFile({
- uploadUrl: uploadUrl.data.uploadUrl,
- uploadAuthToken: uploadUrl.data.authorizationToken,
- fileName: remotePath,
- data: data,
- info: build.metadata
- })
-
- url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
-
- break
- }
- default: {
- // upload to storage
- await global.storage.fPutObject(process.env.S3_BUCKET, remotePath, build.filepath, build.metadata ?? {
- "Content-Type": build.mimetype,
- })
-
- // compose url
- url = global.storage.composeRemoteURL(remotePath)
-
- break
- }
- }
-
- // remove from cache
- fs.promises.rm(build.cachePath, { recursive: true, force: true })
-
- return res.json({
- name: build.filename,
- id: remotePath,
- url: url,
- })
- } catch (error) {
- console.log(error)
- return new Errors.InternalServerError(req, res, error.message)
- }
- }
-
- return res.json({
- success: true,
- })
- }
-}
\ No newline at end of file
diff --git a/packages/server/services/files/file.service.js b/packages/server/services/files/file.service.js
index 57dbeec8..593305f1 100755
--- a/packages/server/services/files/file.service.js
+++ b/packages/server/services/files/file.service.js
@@ -2,11 +2,13 @@ import { Server } from "linebridge/src/server"
import B2 from "backblaze-b2"
+import DbManager from "@shared-classes/DbManager"
import RedisClient from "@shared-classes/RedisClient"
import StorageClient from "@shared-classes/StorageClient"
import CacheService from "@shared-classes/CacheService"
import SharedMiddlewares from "@shared-middlewares"
+import LimitsClass from "@shared-classes/Limits"
class API extends Server {
static refName = "files"
@@ -14,13 +16,12 @@ class API extends Server {
static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
- static maxBodyLength = 1000 * 1000 * 1000
-
middlewares = {
...SharedMiddlewares
}
contexts = {
+ db: new DbManager(),
cache: new CacheService(),
redis: RedisClient(),
storage: StorageClient(),
@@ -28,12 +29,19 @@ class API extends Server {
applicationKeyId: process.env.B2_KEY_ID,
applicationKey: process.env.B2_APP_KEY,
}),
+ limits: {},
}
async onInitialize() {
+ global.storage = this.contexts.storage
+ global.b2Storage = this.contexts.b2Storage
+
+ await this.contexts.db.initialize()
await this.contexts.redis.initialize()
await this.contexts.storage.initialize()
await this.contexts.b2Storage.authorize()
+
+ this.contexts.limits = await LimitsClass.get()
}
}
diff --git a/packages/server/services/files/routes/upload/chunk/post.js b/packages/server/services/files/routes/upload/chunk/post.js
index 0fd030c8..d3e44d07 100644
--- a/packages/server/services/files/routes/upload/chunk/post.js
+++ b/packages/server/services/files/routes/upload/chunk/post.js
@@ -1,104 +1,54 @@
import path from "path"
import fs from "fs"
-import FileUpload from "@shared-classes/FileUpload"
-import PostProcess from "@services/post-process"
+import ChunkFileUpload from "@shared-classes/ChunkFileUpload"
+
+import RemoteUpload from "@services/remoteUpload"
export default {
- useContext: ["cache", "storage", "b2Storage"],
+ useContext: ["cache", "limits"],
middlewares: [
"withAuthentication",
],
fn: async (req, res) => {
- const { cache, storage, b2Storage } = this.default.contexts
-
const providerType = req.headers["provider-type"]
- const userPath = path.join(cache.constructor.cachePath, req.session.user_id)
+ const userPath = path.join(this.default.contexts.cache.constructor.cachePath, req.auth.session.user_id)
- // 10 GB in bytes
- const maxFileSize = 10 * 1000 * 1000 * 1000
+ const tmpPath = path.resolve(userPath)
- // 10MB in bytes
- const maxChunkSize = 10 * 1000 * 1000
+ let build = await ChunkFileUpload(req, {
+ tmpDir: tmpPath,
+ maxFileSize: parseInt(this.default.contexts.limits.maxFileSizeInMB) * 1024 * 1024,
+ maxChunkSize: parseInt(this.default.contexts.limits.maxChunkSizeInMB) * 1024 * 1024,
+ }).catch((err) => {
+ throw new OperationError(err.code, err.message)
+ })
- let build = await FileUpload(req, userPath, maxFileSize, maxChunkSize)
- .catch((err) => {
- console.log("err", err)
+ if (typeof build === "function") {
+ try {
+ build = await build()
- throw new OperationError(500, err.message)
- })
+ const result = await RemoteUpload({
+ parentDir: req.auth.session.user_id,
+ source: build.filePath,
+ service: providerType,
+ useCompression: req.headers["use-compression"] ?? true,
+ cachePath: tmpPath,
+ })
- if (build === false) {
- return false
- } else {
- if (typeof build === "function") {
- try {
- build = await build()
+ fs.promises.rm(tmpPath, { recursive: true, force: true })
- if (!req.headers["no-compression"]) {
- build = await PostProcess(build)
- }
+ return result
+ } catch (error) {
+ fs.promises.rm(tmpPath, { recursive: true, force: true })
- // compose remote path
- const remotePath = `${req.session.user_id}/${path.basename(build.filepath)}`
-
- let url = null
-
- switch (providerType) {
- case "premium-cdn": {
- // use backblaze b2
- await b2Storage.authorize()
-
- const uploadUrl = await b2Storage.getUploadUrl({
- bucketId: process.env.B2_BUCKET_ID,
- })
-
- const data = await fs.promises.readFile(build.filepath)
-
- await b2Storage.uploadFile({
- uploadUrl: uploadUrl.data.uploadUrl,
- uploadAuthToken: uploadUrl.data.authorizationToken,
- fileName: remotePath,
- data: data,
- info: build.metadata
- })
-
- url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
-
- break
- }
- default: {
- // upload to storage
- await storage.fPutObject(process.env.S3_BUCKET, remotePath, build.filepath, build.metadata ?? {
- "Content-Type": build.mimetype,
- })
-
- // compose url
- url = storage.composeRemoteURL(remotePath)
-
- break
- }
- }
-
- // remove from cache
- fs.promises.rm(build.cachePath, { recursive: true, force: true })
-
- return res.json({
- name: build.filename,
- id: remotePath,
- url: url,
- })
- } catch (error) {
- console.log(error)
-
- throw new OperationError(500, error.message)
- }
+ throw new OperationError(error.code ?? 500, error.message ?? "Failed to upload file")
}
+ }
- return res.json({
- success: true,
- })
+ return {
+ ok: 1
}
}
}
\ No newline at end of file
diff --git a/packages/server/services/files/routes/upload/file/post.js b/packages/server/services/files/routes/upload/file/post.js
new file mode 100644
index 00000000..d882b179
--- /dev/null
+++ b/packages/server/services/files/routes/upload/file/post.js
@@ -0,0 +1,48 @@
+import path from "node:path"
+import fs from "node:fs"
+
+import RemoteUpload from "@services/remoteUpload"
+
+export default {
+ useContext: ["cache"],
+ middlewares: [
+ "withAuthentication",
+ ],
+ fn: async (req, res) => {
+ const { cache } = this.default.contexts
+
+ const providerType = req.headers["provider-type"] ?? "standard"
+
+ const userPath = path.join(cache.constructor.cachePath, req.auth.session.user_id)
+
+ let localFilepath = null
+ let tmpPath = path.resolve(userPath, `${Date.now()}`)
+
+ await req.multipart(async (field) => {
+ if (!field.file) {
+ throw new OperationError(400, "Missing file")
+ }
+
+ localFilepath = path.join(tmpPath, field.file.name)
+
+ const existTmpDir = await fs.promises.stat(tmpPath).then(() => true).catch(() => false)
+
+ if (!existTmpDir) {
+ await fs.promises.mkdir(tmpPath, { recursive: true })
+ }
+
+ await field.write(localFilepath)
+ })
+
+ const result = await RemoteUpload({
+ parentDir: req.auth.session.user_id,
+ source: localFilepath,
+ service: providerType,
+ useCompression: req.headers["use-compression"] ?? true,
+ })
+
+ fs.promises.rm(tmpPath, { recursive: true, force: true })
+
+ return result
+ }
+}
diff --git a/packages/server/services/files/routes/upload/get.js b/packages/server/services/files/routes/upload/get.js
new file mode 100644
index 00000000..20af2368
--- /dev/null
+++ b/packages/server/services/files/routes/upload/get.js
@@ -0,0 +1,6 @@
+export default {
+ useContext: ["cache", "limits"],
+ fn: async () => {
+ return this.default.contexts.limits
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/files/services/post-process/video/index.js b/packages/server/services/files/services/post-process/video/index.js
index a4ece015..2abacc99 100755
--- a/packages/server/services/files/services/post-process/video/index.js
+++ b/packages/server/services/files/services/post-process/video/index.js
@@ -29,11 +29,14 @@ async function processVideo(
videoBitrate = 2024,
} = options
- const result = await videoTranscode(file.filepath, file.cachePath, {
+ const result = await videoTranscode(file.filepath, {
videoCodec,
format,
audioBitrate,
videoBitrate: [videoBitrate, true],
+ extraOptions: [
+ "-threads 1"
+ ]
})
file.filepath = result.filepath
diff --git a/packages/server/services/files/services/remoteUpload/index.js b/packages/server/services/files/services/remoteUpload/index.js
new file mode 100644
index 00000000..976bb5b3
--- /dev/null
+++ b/packages/server/services/files/services/remoteUpload/index.js
@@ -0,0 +1,123 @@
+import fs from "node:fs"
+import path from "node:path"
+import mimeTypes from "mime-types"
+import getFileHash from "@shared-utils/readFileHash"
+
+import PostProcess from "../post-process"
+
+export async function standardUpload({
+ source,
+ remotePath,
+ metadata,
+}) {
+ // upload to storage
+ await global.storage.fPutObject(process.env.S3_BUCKET, remotePath, source, metadata)
+
+ // compose url
+ const url = storage.composeRemoteURL(remotePath)
+
+ return {
+ id: remotePath,
+ url: url,
+ metadata: metadata,
+ }
+}
+
+export async function b2Upload({
+ source,
+ remotePath,
+ metadata,
+}) {
+ // use backblaze b2
+ await b2Storage.authorize()
+
+ const uploadUrl = await global.b2Storage.getUploadUrl({
+ bucketId: process.env.B2_BUCKET_ID,
+ })
+
+ const data = await fs.promises.readFile(source)
+
+ await global.b2Storage.uploadFile({
+ uploadUrl: uploadUrl.data.uploadUrl,
+ uploadAuthToken: uploadUrl.data.authorizationToken,
+ fileName: remotePath,
+ data: data,
+ info: metadata
+ })
+
+ const url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
+
+ return {
+ id: remotePath,
+ url: url,
+ metadata: metadata,
+ }
+}
+
+export default async ({
+ source,
+ parentDir,
+ service,
+ useCompression,
+ cachePath,
+}) => {
+ if (!source) {
+ throw new OperationError(500, "source is required")
+ }
+
+ if (!service) {
+ service = "standard"
+ }
+
+ if (!parentDir) {
+ parentDir = "/"
+ }
+
+ if (useCompression) {
+ try {
+ const processOutput = await PostProcess({ filepath: source, cachePath })
+
+ if (processOutput) {
+ if (processOutput.filepath) {
+ source = processOutput.filepath
+ }
+ }
+ } catch (error) {
+ console.error(error)
+ throw new OperationError(500, `Failed to process file`)
+ }
+ }
+
+ const type = mimeTypes.lookup(path.basename(source))
+ const hash = await getFileHash(fs.createReadStream(source))
+
+ const remotePath = path.join(parentDir, hash)
+
+ let result = {}
+
+ const metadata = {
+ "Content-Type": type,
+ "File-Hash": hash,
+ }
+
+ switch (service) {
+ case "b2":
+ result = await b2Upload({
+ remotePath,
+ source,
+ metadata,
+ })
+ break
+ case "standard":
+ result = await standardUpload({
+ remotePath,
+ source,
+ metadata,
+ })
+ break
+ default:
+ throw new OperationError(500, "Unsupported service")
+ }
+
+ return result
+}
\ No newline at end of file
diff --git a/packages/server/services/files/services/videoTranscode/index.js b/packages/server/services/files/services/videoTranscode/index.js
index 63228153..c25cedb9 100755
--- a/packages/server/services/files/services/videoTranscode/index.js
+++ b/packages/server/services/files/services/videoTranscode/index.js
@@ -10,11 +10,20 @@ const defaultParams = {
format: "webm",
}
-export default (input, cachePath, params = defaultParams) => {
+const maxTasks = 5
+
+export default (input, params = defaultParams) => {
return new Promise((resolve, reject) => {
- const filename = path.basename(input)
- const outputFilename = `${filename.split(".")[0]}_ff.${params.format ?? "webm"}`
- const outputFilepath = `${cachePath}/${outputFilename}`
+ if (!global.ffmpegTasks) {
+ global.ffmpegTasks = []
+ }
+
+ if (global.ffmpegTasks.length >= maxTasks) {
+ return reject(new Error("Too many transcoding tasks"))
+ }
+
+ const outputFilename = `${path.basename(input).split(".")[0]}_ff.${params.format ?? "webm"}`
+ const outputFilepath = `${path.dirname(input)}/${outputFilename}`
console.debug(`[TRANSCODING] Transcoding ${input} to ${outputFilepath}`)
@@ -22,8 +31,8 @@ export default (input, cachePath, params = defaultParams) => {
console.debug(`[TRANSCODING] Finished transcode ${input} to ${outputFilepath}`)
return resolve({
- filepath: outputFilepath,
filename: outputFilename,
+ filepath: outputFilepath,
})
}
@@ -42,22 +51,33 @@ export default (input, cachePath, params = defaultParams) => {
}
// chain methods
- Object.keys(commands).forEach((key) => {
+ for (let key in commands) {
if (exec === null) {
exec = ffmpeg(commands[key])
- } else {
- if (typeof exec[key] !== "function") {
- console.warn(`[TRANSCODING] Method ${key} is not a function`)
- return false
+ continue
+ }
+
+ if (key === "extraOptions" && Array.isArray(commands[key])) {
+ for (const option of commands[key]) {
+ exec = exec.inputOptions(option)
}
- if (Array.isArray(commands[key])) {
- exec = exec[key](...commands[key])
- } else {
- exec = exec[key](commands[key])
- }
+ continue
}
- })
+
+ if (typeof exec[key] !== "function") {
+ console.warn(`[TRANSCODING] Method ${key} is not a function`)
+ return false
+ }
+
+ if (Array.isArray(commands[key])) {
+ exec = exec[key](...commands[key])
+ } else {
+ exec = exec[key](commands[key])
+ }
+
+ continue
+ }
exec
.on("error", onError)
diff --git a/packages/server/services/main/main.service.js b/packages/server/services/main/main.service.js
index b9171f59..d5d209fb 100755
--- a/packages/server/services/main/main.service.js
+++ b/packages/server/services/main/main.service.js
@@ -1,28 +1,16 @@
import { Server } from "linebridge/src/server"
-import { Config, User } from "@db_models"
import DbManager from "@shared-classes/DbManager"
-import StorageClient from "@shared-classes/StorageClient"
-import Token from "@lib/token"
+import StartupDB from "./startup_db"
import SharedMiddlewares from "@shared-middlewares"
export default class API extends Server {
static refName = "main"
static useEngine = "hyper-express"
+ static routesPath = `${__dirname}/routes`
static listen_port = process.env.HTTP_LISTEN_PORT || 3000
- static requireWSAuth = true
-
- constructor(params) {
- super(params)
-
- global.DEFAULT_POSTING_POLICY = {
- maxMessageLength: 512,
- maximumFileSize: 80 * 1024 * 1024,
- maximunFilesPerRequest: 20,
- }
- }
middlewares = {
...require("@middlewares").default,
@@ -31,102 +19,16 @@ export default class API extends Server {
events = require("./events")
- storage = global.storage = StorageClient()
- DB = new DbManager()
+ contexts = {
+ db: new DbManager(),
+ }
async onInitialize() {
- await this.DB.initialize()
- await this.storage.initialize()
-
- await this.initializeConfigDB()
- await this.checkSetup()
+ await this.contexts.db.initialize()
+ await StartupDB()
}
- initializeConfigDB = async () => {
- let serverConfig = await Config.findOne({ key: "server" }).catch(() => {
- return false
- })
-
- if (!serverConfig) {
- serverConfig = new Config({
- key: "server",
- value: {
- setup: false,
- },
- })
-
-
- await serverConfig.save()
- }
- }
-
- checkSetup = async () => {
- return new Promise(async (resolve, reject) => {
- let setupOk = (await Config.findOne({ key: "server" })).value?.setup ?? false
-
- if (!setupOk) {
- console.log("⚠️ Server setup is not complete, running setup proccess.")
-
- let setupScript = await import("./setup")
-
- setupScript = setupScript.default ?? setupScript
-
- try {
- for await (let script of setupScript) {
- await script()
- }
-
- console.log("✅ Server setup complete.")
-
- await Config.updateOne({ key: "server" }, { value: { setup: true } })
-
- return resolve()
- } catch (error) {
- console.log("❌ Server setup failed.")
- console.error(error)
- process.exit(1)
- }
- }
-
- return resolve()
- })
- }
-
- handleWsAuth = async (socket, token, err) => {
- try {
- const validation = await Token.validate(token)
-
- if (!validation.valid) {
- if (validation.error) {
- return err(`auth:server_error`)
- }
-
- return err(`auth:token_invalid`)
- }
-
- const userData = await User.findById(validation.data.user_id).catch((err) => {
- console.error(`[${socket.id}] failed to get user data caused by server error`, err)
-
- return null
- })
-
- if (!userData) {
- return err(`auth:user_failed`)
- }
-
- socket.userData = userData
- socket.token = token
- socket.session = validation.data
-
- return {
- token: token,
- username: userData.username,
- user_id: userData._id,
- }
- } catch (error) {
- return err(`auth:authentification_failed`, error)
- }
- }
+ handleWsAuth = require("@shared-lib/handleWsAuth").default
}
Boot(API)
\ No newline at end of file
diff --git a/packages/server/services/main/routes/limits/get.js b/packages/server/services/main/routes/limits/get.js
new file mode 100644
index 00000000..bafb50c1
--- /dev/null
+++ b/packages/server/services/main/routes/limits/get.js
@@ -0,0 +1,7 @@
+import LimitsClass from "@shared-classes/Limits"
+
+export default async (req) => {
+ const key = req.query.key
+
+ return await LimitsClass.get(key)
+}
\ No newline at end of file
diff --git a/packages/server/services/main/routes/ping/get.js b/packages/server/services/main/routes/ping/get.js
new file mode 100644
index 00000000..c0414815
--- /dev/null
+++ b/packages/server/services/main/routes/ping/get.js
@@ -0,0 +1,3 @@
+export default () => {
+ return "pong"
+}
\ No newline at end of file
diff --git a/packages/server/services/main/startup_db.js b/packages/server/services/main/startup_db.js
new file mode 100644
index 00000000..e43a7835
--- /dev/null
+++ b/packages/server/services/main/startup_db.js
@@ -0,0 +1,47 @@
+import { Config } from "@db_models"
+
+export default async () => {
+ let serverConfig = await Config.findOne({ key: "server" }).catch(() => {
+ return false
+ })
+
+ if (!serverConfig) {
+ console.log("Server config DB is not created, creating it...")
+
+ serverConfig = new Config({
+ key: "server",
+ value: {
+ setup: false,
+ },
+ })
+
+ await serverConfig.save()
+ }
+
+ const setupScriptsCompleted = (serverConfig.value?.setup) ?? false
+
+ if (!setupScriptsCompleted) {
+ console.log("⚠️ Server setup is not complete, running setup proccess.")
+
+ let setupScript = await import("./setup")
+ setupScript = setupScript.default ?? setupScript
+
+ try {
+ for await (let script of setupScript) {
+ await script()
+ }
+
+ console.log("✅ Server setup complete.")
+
+ await Config.updateOne({ key: "server" }, { value: { setup: true } })
+
+ serverConfig = await Config.findOne({ key: "server" })
+
+ return resolve()
+ } catch (error) {
+ console.log("❌ Server setup failed.")
+ console.error(error)
+ process.exit(1)
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/notifications/notifications.service.js b/packages/server/services/notifications/notifications.service.js
new file mode 100644
index 00000000..beb2cfe8
--- /dev/null
+++ b/packages/server/services/notifications/notifications.service.js
@@ -0,0 +1,32 @@
+import { Server } from "linebridge/src/server"
+
+import DbManager from "@shared-classes/DbManager"
+import RedisClient from "@shared-classes/RedisClient"
+
+import SharedMiddlewares from "@shared-middlewares"
+
+class API extends Server {
+ static refName = "notifications"
+ static useEngine = "hyper-express"
+ static wsRoutesPath = `${__dirname}/ws_routes`
+ static routesPath = `${__dirname}/routes`
+ static listen_port = process.env.HTTP_LISTEN_PORT ?? 3009
+
+ middlewares = {
+ ...SharedMiddlewares
+ }
+
+ contexts = {
+ db: new DbManager(),
+ redis: RedisClient(),
+ }
+
+ async onInitialize() {
+ await this.contexts.db.initialize()
+ await this.contexts.redis.initialize()
+ }
+
+ handleWsAuth = require("@shared-lib/handleWsAuth").default
+}
+
+Boot(API)
\ No newline at end of file
diff --git a/packages/server/services/notifications/package.json b/packages/server/services/notifications/package.json
new file mode 100644
index 00000000..bcd2c871
--- /dev/null
+++ b/packages/server/services/notifications/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "notifications",
+ "version": "1.0.0",
+ "main": "index.js",
+ "license": "MIT"
+}
diff --git a/packages/server/services/notifications/routes/notifications/test/get.js b/packages/server/services/notifications/routes/notifications/test/get.js
new file mode 100644
index 00000000..96496f17
--- /dev/null
+++ b/packages/server/services/notifications/routes/notifications/test/get.js
@@ -0,0 +1,5 @@
+export default () =>{
+ return {
+ hi: "hola xd"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/notifications/ws_routes/self/new.js b/packages/server/services/notifications/ws_routes/self/new.js
new file mode 100644
index 00000000..f0dcfaa9
--- /dev/null
+++ b/packages/server/services/notifications/ws_routes/self/new.js
@@ -0,0 +1,9 @@
+export default async () => {
+ global.rtengine.io.of("/").emit("new", {
+ hi: "hola xd"
+ })
+
+ return {
+ hi: "hola xd"
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/index.js b/packages/server/services/posts/classes/posts/index.js
index 9cd34dd0..6b5946e2 100644
--- a/packages/server/services/posts/classes/posts/index.js
+++ b/packages/server/services/posts/classes/posts/index.js
@@ -1,5 +1,6 @@
export default class Posts {
- static feed = require("./methods/feed").default
+ static timeline = require("./methods/timeline").default
+ static globalTimeline = require("./methods/globalTimeline").default
static data = require("./methods/data").default
static getLiked = require("./methods/getLiked").default
static getSaved = require("./methods/getSaved").default
@@ -10,4 +11,7 @@ export default class Posts {
static toggleLike = require("./methods/toggleLike").default
static report = require("./methods/report").default
static flag = require("./methods/flag").default
+ static delete = require("./methods/delete").default
+ static update = require("./methods/update").default
+ static replies = require("./methods/replies").default
}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/create.js b/packages/server/services/posts/classes/posts/methods/create.js
index 87bd047a..f8118257 100644
--- a/packages/server/services/posts/classes/posts/methods/create.js
+++ b/packages/server/services/posts/classes/posts/methods/create.js
@@ -2,6 +2,7 @@ import requiredFields from "@shared-utils/requiredFields"
import { DateTime } from "luxon"
import { Post } from "@db_models"
+import fullfill from "./fullfill"
export default async (payload = {}) => {
await requiredFields(["user_id"], payload)
@@ -32,9 +33,13 @@ export default async (payload = {}) => {
post = post.toObject()
- // TODO: create background jobs (nsfw dectection)
+ const result = await fullfill({
+ posts: post,
+ for_user_id: user_id
+ })
- // TODO: Push event to Websocket
+ // TODO: create background jobs (nsfw dectection)
+ global.rtengine.io.of("/").emit(`post.new`, result[0])
return post
}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/data.js b/packages/server/services/posts/classes/posts/methods/data.js
index a0b079c2..c2daee3a 100644
--- a/packages/server/services/posts/classes/posts/methods/data.js
+++ b/packages/server/services/posts/classes/posts/methods/data.js
@@ -1,16 +1,23 @@
import { Post } from "@db_models"
import fullfillPostsData from "./fullfill"
+const maxLimit = 300
+
export default async (payload = {}) => {
let {
for_user_id,
post_id,
query = {},
- skip = 0,
+ trim = 0,
limit = 20,
sort = { created_at: -1 },
} = payload
+ // set a hard limit on the number of posts to retrieve, used for pagination
+ if (limit > maxLimit) {
+ limit = maxLimit
+ }
+
let posts = []
if (post_id) {
@@ -24,7 +31,7 @@ export default async (payload = {}) => {
} else {
posts = await Post.find({ ...query })
.sort(sort)
- .skip(skip)
+ .skip(trim)
.limit(limit)
}
@@ -32,7 +39,6 @@ export default async (payload = {}) => {
posts = await fullfillPostsData({
posts,
for_user_id,
- skip,
})
// if post_id is specified, return only one post
diff --git a/packages/server/services/posts/classes/posts/methods/delete.js b/packages/server/services/posts/classes/posts/methods/delete.js
new file mode 100644
index 00000000..b1cda1b0
--- /dev/null
+++ b/packages/server/services/posts/classes/posts/methods/delete.js
@@ -0,0 +1,38 @@
+import { Post, PostLike, PostSave } from "@db_models"
+
+export default async (payload = {}) => {
+ let {
+ post_id
+ } = payload
+
+ if (!post_id) {
+ throw new OperationError(400, "Missing post_id")
+ }
+
+ await Post.deleteOne({
+ _id: post_id,
+ }).catch((err) => {
+ throw new OperationError(500, `An error has occurred: ${err.message}`)
+ })
+
+ // search for likes
+ await PostLike.deleteMany({
+ post_id: post_id,
+ }).catch((err) => {
+ throw new OperationError(500, `An error has occurred: ${err.message}`)
+ })
+
+ // deleted from saved
+ await PostSave.deleteMany({
+ post_id: 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)
+
+ return {
+ deleted: true,
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/fromUserId.js b/packages/server/services/posts/classes/posts/methods/fromUserId.js
index 0ac5e5e7..38f5c731 100644
--- a/packages/server/services/posts/classes/posts/methods/fromUserId.js
+++ b/packages/server/services/posts/classes/posts/methods/fromUserId.js
@@ -4,7 +4,7 @@ export default async (payload = {}) => {
const {
for_user_id,
user_id,
- skip,
+ trim,
limit,
} = payload
@@ -14,8 +14,8 @@ export default async (payload = {}) => {
return await GetData({
for_user_id: for_user_id,
- skip,
- limit,
+ trim: trim,
+ limit: limit,
query: {
user_id: {
$in: user_id
diff --git a/packages/server/services/posts/classes/posts/methods/fullfill.js b/packages/server/services/posts/classes/posts/methods/fullfill.js
index 6551b608..927c2f98 100644
--- a/packages/server/services/posts/classes/posts/methods/fullfill.js
+++ b/packages/server/services/posts/classes/posts/methods/fullfill.js
@@ -1,4 +1,4 @@
-import { User, Comment, PostLike, SavedPost } from "@db_models"
+import { User, PostLike, PostSave, Post } from "@db_models"
export default async (payload = {}) => {
let {
@@ -14,33 +14,26 @@ export default async (payload = {}) => {
return []
}
- let savedPostsIds = []
+ let postsSavesIds = []
if (for_user_id) {
- const savedPosts = await SavedPost.find({ user_id: for_user_id })
+ const postsSaves = await PostSave.find({ user_id: for_user_id })
.sort({ saved_at: -1 })
- savedPostsIds = savedPosts.map((savedPost) => savedPost.post_id)
+ postsSavesIds = postsSaves.map((postSave) => postSave.post_id)
}
- let [usersData, likesData, commentsData] = await Promise.all([
+ let [usersData, likesData, repliesData] = await Promise.all([
User.find({
_id: {
$in: posts.map((post) => post.user_id)
}
- })
- .select("-email")
- .select("-birthday"),
+ }).catch(() => { }),
PostLike.find({
post_id: {
$in: posts.map((post) => post._id)
}
}).catch(() => []),
- Comment.find({
- parent_id: {
- $in: posts.map((post) => post._id)
- }
- }).catch(() => []),
])
// wrap likesData by post_id
@@ -54,19 +47,10 @@ export default async (payload = {}) => {
return acc
}, {})
- // wrap commentsData by post_id
- commentsData = commentsData.reduce((acc, comment) => {
- if (!acc[comment.parent_id]) {
- acc[comment.parent_id] = []
- }
-
- acc[comment.parent_id].push(comment)
-
- return acc
- }, {})
-
posts = await Promise.all(posts.map(async (post, index) => {
- post = post.toObject()
+ if (typeof post.toObject === "function") {
+ post = post.toObject()
+ }
let user = usersData.find((user) => user._id.toString() === post.user_id.toString())
@@ -77,22 +61,21 @@ export default async (payload = {}) => {
}
}
+ if (post.reply_to) {
+ post.reply_to_data = await Post.findById(post.reply_to)
+ }
+
let likes = likesData[post._id.toString()] ?? []
post.countLikes = likes.length
- let comments = commentsData[post._id.toString()] ?? []
-
- post.countComments = comments.length
-
if (for_user_id) {
post.isLiked = likes.some((like) => like.user_id.toString() === for_user_id)
- post.isSaved = savedPostsIds.includes(post._id.toString())
+ post.isSaved = postsSavesIds.includes(post._id.toString())
}
return {
...post,
- comments: comments.map((comment) => comment._id.toString()),
user,
}
}))
diff --git a/packages/server/services/posts/classes/posts/methods/getLiked.js b/packages/server/services/posts/classes/posts/methods/getLiked.js
index bd823c92..84ed4a6a 100644
--- a/packages/server/services/posts/classes/posts/methods/getLiked.js
+++ b/packages/server/services/posts/classes/posts/methods/getLiked.js
@@ -2,7 +2,7 @@ import { PostLike } from "@db_models"
import GetData from "./data"
export default async (payload = {}) => {
- let { user_id } = payload
+ let { user_id, trim, limit } = payload
if (!user_id) {
throw new OperationError(400, "Missing user_id")
@@ -13,6 +13,8 @@ export default async (payload = {}) => {
ids = ids.map((item) => item.post_id)
return await GetData({
+ trim: trim,
+ limit: limit,
for_user_id: user_id,
query: {
_id: {
diff --git a/packages/server/services/posts/classes/posts/methods/getSaved.js b/packages/server/services/posts/classes/posts/methods/getSaved.js
index c9ce760a..e3697df1 100644
--- a/packages/server/services/posts/classes/posts/methods/getSaved.js
+++ b/packages/server/services/posts/classes/posts/methods/getSaved.js
@@ -1,18 +1,24 @@
-import { SavedPost } from "@db_models"
+import { PostSave } from "@db_models"
import GetData from "./data"
export default async (payload = {}) => {
- let { user_id } = payload
+ let { user_id, trim, limit } = payload
if (!user_id) {
throw new OperationError(400, "Missing user_id")
}
- let ids = await SavedPost.find({ user_id })
+ let ids = await PostSave.find({ user_id })
+
+ if (ids.length === 0) {
+ return []
+ }
ids = ids.map((item) => item.post_id)
return await GetData({
+ trim: trim,
+ limit: limit,
for_user_id: user_id,
query: {
_id: {
@@ -20,5 +26,4 @@ export default async (payload = {}) => {
}
}
})
-}
-
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/globalTimeline.js b/packages/server/services/posts/classes/posts/methods/globalTimeline.js
new file mode 100644
index 00000000..29ff1ccf
--- /dev/null
+++ b/packages/server/services/posts/classes/posts/methods/globalTimeline.js
@@ -0,0 +1,20 @@
+import GetPostData from "./data"
+
+export default async (payload = {}) => {
+ let {
+ user_id,
+ trim,
+ limit,
+ } = payload
+
+ let query = {}
+
+ const posts = await GetPostData({
+ for_user_id: user_id,
+ trim,
+ limit,
+ query: query,
+ })
+
+ return posts
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/replies.js b/packages/server/services/posts/classes/posts/methods/replies.js
new file mode 100644
index 00000000..2cc8ec33
--- /dev/null
+++ b/packages/server/services/posts/classes/posts/methods/replies.js
@@ -0,0 +1,29 @@
+import { Post } from "@db_models"
+import fullfillPostsData from "./fullfill"
+
+export default async (payload = {}) => {
+ const {
+ post_id,
+ for_user_id,
+ trim = 0,
+ limit = 50,
+ } = payload
+
+ if (!post_id) {
+ throw new OperationError(400, "Post ID is required")
+ }
+
+ let posts = await Post.find({
+ reply_to: post_id,
+ })
+ .limit(limit)
+ .skip(trim)
+ .sort({ created_at: -1 })
+
+ posts = await fullfillPostsData({
+ posts,
+ for_user_id,
+ })
+
+ return posts
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/feed.js b/packages/server/services/posts/classes/posts/methods/timeline.js
similarity index 96%
rename from packages/server/services/posts/classes/posts/methods/feed.js
rename to packages/server/services/posts/classes/posts/methods/timeline.js
index df292bfd..30702f61 100644
--- a/packages/server/services/posts/classes/posts/methods/feed.js
+++ b/packages/server/services/posts/classes/posts/methods/timeline.js
@@ -5,7 +5,7 @@ import GetPostData from "./data"
export default async (payload = {}) => {
let {
user_id,
- skip,
+ trim,
limit,
} = payload
@@ -34,7 +34,7 @@ export default async (payload = {}) => {
const posts = await GetPostData({
for_user_id: user_id,
- skip,
+ trim,
limit,
query: query,
})
diff --git a/packages/server/services/posts/classes/posts/methods/toggleLike.js b/packages/server/services/posts/classes/posts/methods/toggleLike.js
index 7c7c7f54..64477469 100644
--- a/packages/server/services/posts/classes/posts/methods/toggleLike.js
+++ b/packages/server/services/posts/classes/posts/methods/toggleLike.js
@@ -17,8 +17,8 @@ export default async (payload = {}) => {
}
let likeObj = await PostLike.findOne({
- post_id,
user_id,
+ post_id,
})
if (typeof to === "undefined") {
@@ -40,13 +40,22 @@ export default async (payload = {}) => {
await PostLike.findByIdAndDelete(likeObj._id)
}
- // global.engine.ws.io.of("/").emit(`post.${post_id}.likes.update`, {
- // to,
- // post_id,
- // user_id,
- // })
+ const count = await PostLike.count({
+ post_id,
+ })
+
+ const eventData = {
+ to,
+ post_id,
+ user_id,
+ count: count,
+ }
+
+ global.rtengine.io.of("/").emit(`post.${post_id}.likes.update`, eventData)
+ global.rtengine.io.of("/").emit(`post.like.update`, eventData)
return {
- liked: to
+ liked: to,
+ count: count
}
}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/toggleSave.js b/packages/server/services/posts/classes/posts/methods/toggleSave.js
index df6f686f..53f86eb5 100644
--- a/packages/server/services/posts/classes/posts/methods/toggleSave.js
+++ b/packages/server/services/posts/classes/posts/methods/toggleSave.js
@@ -1,4 +1,4 @@
-import { Post, SavedPost } from "@db_models"
+import { Post, PostSave } from "@db_models"
export default async (payload = {}) => {
let { post_id, user_id } = payload
@@ -16,16 +16,16 @@ export default async (payload = {}) => {
throw new OperationError(404, "Post not found")
}
- let post = await SavedPost.findOne({ post_id, user_id })
+ let post = await PostSave.findOne({ post_id, user_id })
if (post) {
- await SavedPost.findByIdAndDelete(post._id).catch((err) => {
+ await PostSave.findByIdAndDelete(post._id).catch((err) => {
throw new OperationError(500, `An error has occurred: ${err.message}`)
})
post = null
} else {
- post = new SavedPost({
+ post = new PostSave({
post_id,
user_id,
})
diff --git a/packages/server/services/posts/classes/posts/methods/update.js b/packages/server/services/posts/classes/posts/methods/update.js
new file mode 100644
index 00000000..af32da67
--- /dev/null
+++ b/packages/server/services/posts/classes/posts/methods/update.js
@@ -0,0 +1,32 @@
+import { Post } from "@db_models"
+import { DateTime } from "luxon"
+import fullfill from "./fullfill"
+
+export default async (post_id, update) => {
+ let post = await Post.findById(post_id)
+
+ if (!post) {
+ throw new OperationError(404, "Post not found")
+ }
+
+ const updateKeys = Object.keys(update)
+
+ updateKeys.forEach((key) => {
+ post[key] = update[key]
+ })
+
+ post.updated_at = DateTime.local().toISO()
+
+ await post.save()
+
+ post = post.toObject()
+
+ const result = await fullfill({
+ posts: post,
+ })
+
+ global.rtengine.io.of("/").emit(`post.update`, result[0])
+ global.rtengine.io.of("/").emit(`post.update.${post_id}`, result[0])
+
+ return result[0]
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/posts.service.js b/packages/server/services/posts/posts.service.js
index b1cb218d..ffe79cb8 100644
--- a/packages/server/services/posts/posts.service.js
+++ b/packages/server/services/posts/posts.service.js
@@ -17,13 +17,15 @@ export default class API extends Server {
contexts = {
db: new DbManager(),
- redis: RedisClient()
+ redis: RedisClient(),
}
async onInitialize() {
await this.contexts.db.initialize()
await this.contexts.redis.initialize()
}
+
+ handleWsAuth = require("@shared-lib/handleWsAuth").default
}
Boot(API)
\ No newline at end of file
diff --git a/packages/server/services/posts/routes/posts/[post_id]/delete.js b/packages/server/services/posts/routes/posts/[post_id]/delete.js
new file mode 100644
index 00000000..fe1315c5
--- /dev/null
+++ b/packages/server/services/posts/routes/posts/[post_id]/delete.js
@@ -0,0 +1,27 @@
+import PostClass from "@classes/posts"
+import { Post } from "@db_models"
+export default {
+ middlewares: ["withAuthentication"],
+ fn: async (req, res) => {
+ // check if post is owned or if is admin
+ const post = await Post.findById(req.params.post_id).catch(() => {
+ return false
+ })
+
+ if (!post) {
+ throw new OperationError(404, "Post not found")
+ }
+
+ const user = await req.auth.user()
+
+ if (post.user_id.toString() !== user._id.toString()) {
+ if (!user.roles.includes("admin")) {
+ throw new OperationError(403, "You cannot delete this post")
+ }
+ }
+
+ return await PostClass.delete({
+ post_id: req.params.post_id
+ })
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/routes/posts/[post_id]/replies/get.js b/packages/server/services/posts/routes/posts/[post_id]/replies/get.js
new file mode 100644
index 00000000..059837e5
--- /dev/null
+++ b/packages/server/services/posts/routes/posts/[post_id]/replies/get.js
@@ -0,0 +1,13 @@
+import PostClass from "@classes/posts"
+
+export default {
+ middlewares: ["withOptionalAuthentication"],
+ fn: async (req) => {
+ return await PostClass.replies({
+ post_id: req.params.post_id,
+ for_user_id: req.auth?.session.user_id,
+ trim: req.query.trim,
+ limit: req.query.limit
+ })
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/routes/posts/[post_id]/update/put.js b/packages/server/services/posts/routes/posts/[post_id]/update/put.js
new file mode 100644
index 00000000..8f2007e8
--- /dev/null
+++ b/packages/server/services/posts/routes/posts/[post_id]/update/put.js
@@ -0,0 +1,44 @@
+import PostClass from "@classes/posts"
+import { Post } from "@db_models"
+
+const AllowedFields = ["message", "tags", "attachments"]
+
+// TODO: Get limits from LimitsAPI
+const MaxStringsLengths = {
+ message: 2000
+}
+
+export default {
+ middlewares: ["withAuthentication"],
+ fn: async (req) => {
+ let update = {}
+
+ const post = await Post.findById(req.params.post_id)
+
+ if (!post) {
+ throw new OperationError(404, "Post not found")
+ }
+
+ if (post.user_id !== req.auth.session.user_id) {
+ throw new OperationError(403, "You cannot edit this post")
+ }
+
+ AllowedFields.forEach((key) => {
+ if (typeof req.body[key] !== "undefined") {
+ // check maximung strings length
+ if (typeof req.body[key] === "string" && MaxStringsLengths[key]) {
+ if (req.body[key].length > MaxStringsLengths[key]) {
+ // create a substring
+ update[key] = req.body[key].substring(0, MaxStringsLengths[key])
+ } else {
+ update[key] = req.body[key]
+ }
+ } else {
+ update[key] = req.body[key]
+ }
+ }
+ })
+
+ return await PostClass.update(req.params.post_id, update)
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/routes/posts/feed/global/get.js b/packages/server/services/posts/routes/posts/feed/global/get.js
new file mode 100644
index 00000000..102eec4a
--- /dev/null
+++ b/packages/server/services/posts/routes/posts/feed/global/get.js
@@ -0,0 +1,19 @@
+import Posts from "@classes/posts"
+
+export default {
+ middlewares: ["withOptionalAuthentication"],
+ fn: async (req, res) => {
+ const payload = {
+ limit: req.query?.limit,
+ trim: req.query?.trim,
+ }
+
+ if (req.auth) {
+ payload.user_id = req.auth.session.user_id
+ }
+
+ const result = await Posts.globalTimeline(payload)
+
+ return result
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/posts/routes/feed/get.js b/packages/server/services/posts/routes/posts/feed/timeline/get.js
similarity index 78%
rename from packages/server/services/posts/routes/feed/get.js
rename to packages/server/services/posts/routes/posts/feed/timeline/get.js
index 3bad7282..a5bfaa19 100644
--- a/packages/server/services/posts/routes/feed/get.js
+++ b/packages/server/services/posts/routes/posts/feed/timeline/get.js
@@ -5,14 +5,14 @@ export default {
fn: async (req, res) => {
const payload = {
limit: req.query?.limit,
- skip: req.query?.skip,
+ trim: req.query?.trim,
}
if (req.auth) {
payload.user_id = req.auth.session.user_id
}
- const result = await Posts.feed(payload)
+ const result = await Posts.timeline(payload)
return result
}
diff --git a/packages/server/services/posts/routes/posts/liked/get.js b/packages/server/services/posts/routes/posts/liked/get.js
index 10ebfe66..e70c49c3 100644
--- a/packages/server/services/posts/routes/posts/liked/get.js
+++ b/packages/server/services/posts/routes/posts/liked/get.js
@@ -4,6 +4,8 @@ export default {
middlewares: ["withAuthentication"],
fn: async (req) => {
return await Posts.getLiked({
+ trim: req.query.trim,
+ limit: req.query.limit,
user_id: req.auth.session.user_id
})
}
diff --git a/packages/server/services/posts/routes/posts/saved/get.js b/packages/server/services/posts/routes/posts/saved/get.js
index b4b97c77..02391afe 100644
--- a/packages/server/services/posts/routes/posts/saved/get.js
+++ b/packages/server/services/posts/routes/posts/saved/get.js
@@ -4,6 +4,8 @@ export default {
middlewares: ["withAuthentication"],
fn: async (req) => {
return await Posts.getSaved({
+ trim: req.query.trim,
+ limit: req.query.limit,
user_id: req.auth.session.user_id
})
}
diff --git a/packages/server/services/posts/routes/posts/user/[user_id]/get.js b/packages/server/services/posts/routes/posts/user/[user_id]/get.js
index 481b2105..61c102ec 100644
--- a/packages/server/services/posts/routes/posts/user/[user_id]/get.js
+++ b/packages/server/services/posts/routes/posts/user/[user_id]/get.js
@@ -5,7 +5,7 @@ export default {
fn: async (req, res) => {
return await Posts.fromUserId({
skip: req.query.skip,
- limit: req.query.limit,
+ trim: req.query.trim,
user_id: req.params.user_id,
for_user_id: req.auth?.session?.user_id,
})
diff --git a/packages/server/services/users/classes/users/method/update.js b/packages/server/services/users/classes/users/method/update.js
index 6f7a8c1c..35efaa8e 100644
--- a/packages/server/services/users/classes/users/method/update.js
+++ b/packages/server/services/users/classes/users/method/update.js
@@ -1,29 +1,31 @@
import { User } from "@db_models"
-export default async (payload = {}) => {
- if (typeof payload.user_id === "undefined") {
+export default async (user_id, update) => {
+ if (typeof user_id === "undefined") {
throw new Error("No user_id provided")
}
- if (typeof payload.update === "undefined") {
+ if (typeof update === "undefined") {
throw new Error("No update provided")
}
- let user = await User.findById(payload.user_id)
+ let user = await User.findById(user_id)
if (!user) {
- throw new Error("User not found")
+ throw new OperationError(404, "User not found")
}
- const updateKeys = Object.keys(payload.update)
+ const updateKeys = Object.keys(update)
updateKeys.forEach((key) => {
- user[key] = payload.update[key]
+ user[key] = update[key]
})
await user.save()
- global.rtengine.io.of("/").emit(`user.update.${payload.user_id}`, user.toObject())
+ user = user.toObject()
- return user.toObject()
+ global.rtengine.io.of("/").emit(`user.update.${update}`, user)
+
+ return user
}
\ No newline at end of file
diff --git a/packages/server/services/users/routes/users/[user_id]/follow/post.js b/packages/server/services/users/routes/users/[user_id]/follow/post.js
index c1cda11d..1c8cda09 100644
--- a/packages/server/services/users/routes/users/[user_id]/follow/post.js
+++ b/packages/server/services/users/routes/users/[user_id]/follow/post.js
@@ -6,7 +6,7 @@ export default {
return await User.toggleFollow({
user_id: req.params.user_id,
from_user_id: req.auth.session.user_id,
- to: req.body.to,
+ to: req.body?.to,
})
}
}
\ No newline at end of file
diff --git a/packages/server/services/users/routes/users/self/config/get.js b/packages/server/services/users/routes/users/self/config/get.js
new file mode 100644
index 00000000..539da041
--- /dev/null
+++ b/packages/server/services/users/routes/users/self/config/get.js
@@ -0,0 +1,25 @@
+import { UserConfig } from "@db_models"
+
+export default {
+ middlewares: ["withAuthentication"],
+ fn: async (req) => {
+ const key = req.query.key
+
+ let config = await UserConfig.findOne({
+ user_id: req.auth.session.user_id
+ })
+
+ if (!config) {
+ config = await UserConfig.create({
+ user_id: req.auth.session.user_id,
+ values: {}
+ })
+ }
+
+ if (key) {
+ return config.values?.[key]
+ }
+
+ return config.values
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/users/routes/users/self/config/put.js b/packages/server/services/users/routes/users/self/config/put.js
new file mode 100644
index 00000000..88d264ca
--- /dev/null
+++ b/packages/server/services/users/routes/users/self/config/put.js
@@ -0,0 +1,61 @@
+import { UserConfig } from "@db_models"
+import lodash from "lodash"
+
+const baseConfig = [
+ {
+ key: "app:language",
+ type: "string",
+ value: "en-us"
+ },
+ {
+ key: "auth:mfa",
+ type: "boolean",
+ value: false
+ },
+]
+
+export default {
+ middlewares: ["withAuthentication"],
+ fn: async (req) => {
+ let config = await UserConfig.findOne({
+ user_id: req.auth.session.user_id
+ })
+
+ const values = {}
+
+ baseConfig.forEach((config) => {
+ const fromBody = req.body[config.key]
+ if (typeof fromBody !== "undefined") {
+ if (typeof fromBody === config.type) {
+ values[config.key] = req.body[config.key]
+ } else {
+ throw new OperationError(400, `Invalid type for ${config.key}`)
+ }
+ } else {
+ values[config.key] = config.value
+ }
+ })
+
+
+ if (!config) {
+ config = await UserConfig.create({
+ user_id: req.auth.session.user_id,
+ values
+ })
+ } else {
+ const newValues = lodash.merge(config.values, values)
+
+ config = await UserConfig.updateOne({
+ user_id: req.auth.session.user_id
+ }, {
+ values: newValues
+ })
+
+ config = await UserConfig.findOne({
+ user_id: req.auth.session.user_id
+ })
+ }
+
+ return config.values
+ }
+}
\ No newline at end of file
diff --git a/packages/server/services/users/routes/users/self/update/post.js b/packages/server/services/users/routes/users/self/update/post.js
index e4b53d1a..a8e50587 100644
--- a/packages/server/services/users/routes/users/self/update/post.js
+++ b/packages/server/services/users/routes/users/self/update/post.js
@@ -21,7 +21,7 @@ const MaxStringsLengths = {
export default {
middlewares: ["withAuthentication"],
fn: async (req) => {
- let { update } = req.body
+ let update = {}
if (!update) {
throw new OperationError(400, "Missing update")
@@ -33,13 +33,17 @@ export default {
// sanitize update
AllowedPublicUpdateFields.forEach((key) => {
- if (typeof update[key] !== "undefined") {
+ if (typeof req.body[key] !== "undefined") {
// check maximung strings length
- if (typeof update[key] === "string" && MaxStringsLengths[key]) {
- if (update[key].length > MaxStringsLengths[key]) {
+ if (typeof req.body[key] === "string" && MaxStringsLengths[key]) {
+ if (req.body[key].length > MaxStringsLengths[key]) {
// create a substring
- update[key] = update[key].substring(0, MaxStringsLengths[key])
+ update[key] = req.body[key].substring(0, MaxStringsLengths[key])
+ } else {
+ update[key] = req.body[key]
}
+ } else {
+ update[key] = req.body[key]
}
}
})
@@ -56,9 +60,6 @@ export default {
}
}
- return await UserClass.update({
- user_id: req.auth.session.user_id,
- update: update,
- })
+ return await UserClass.update(req.auth.session.user_id, update)
}
}
\ No newline at end of file
diff --git a/packages/server/utils/readFileHash.js b/packages/server/utils/readFileHash.js
new file mode 100644
index 00000000..5f6be7f8
--- /dev/null
+++ b/packages/server/utils/readFileHash.js
@@ -0,0 +1,18 @@
+import fs from "node:fs"
+import crypto from "crypto"
+
+export default async (file) => {
+ return new Promise((resolve, reject) => {
+ if (typeof file === "string") {
+ file = fs.createReadStream(file)
+ }
+
+ const hash = crypto.createHash("sha256")
+
+ file.on("data", (chunk) => hash.update(chunk))
+
+ file.on("end", () => resolve(hash.digest("hex")))
+
+ file.on("error", reject)
+ })
+}
\ No newline at end of file
diff --git a/scripts/post-install.js b/scripts/post-install.js
index b7d5bc85..c6c3a7f3 100755
--- a/scripts/post-install.js
+++ b/scripts/post-install.js
@@ -66,10 +66,13 @@ async function linkSharedResources(pkgJSON, packagePath) {
async function linkInternalSubmodules(packages) {
const appPath = path.resolve(rootPath, pkgjson._web_app_path)
+ const comtyjsPath = path.resolve(rootPath, "comty.js")
const evitePath = path.resolve(rootPath, "evite")
const linebridePath = path.resolve(rootPath, "linebridge")
+ //* EVITE LINKING
console.log(`Linking Evite to app...`)
+
await child_process.execSync("yarn link", {
cwd: evitePath,
stdio: "inherit",
@@ -80,6 +83,20 @@ async function linkInternalSubmodules(packages) {
stdio: "inherit",
})
+ //* COMTY.JS LINKING
+ console.log(`Linking comty.js to app...`)
+
+ await child_process.execSync(`yarn link`, {
+ cwd: comtyjsPath,
+ stdio: "inherit",
+ })
+
+ await child_process.execSync(`yarn link "comty.js"`, {
+ cwd: appPath,
+ stdio: "inherit",
+ })
+
+ //* LINEBRIDE LINKING
console.log(`Linking Linebride to servers...`)
await child_process.execSync(`yarn link`, {
@@ -104,7 +121,7 @@ async function linkInternalSubmodules(packages) {
console.log(`Linking Linebride to package [${packageName}]...`)
}
- console.log(`✅ Evite dependencies installed`)
+ console.log(`✅ All submodules linked!`)
return true
}