merge from local

This commit is contained in:
SrGooglo 2024-11-17 20:35:20 +00:00
parent 17892508e7
commit 00f6e34c53
45 changed files with 757 additions and 395 deletions

View File

@ -1,4 +1,13 @@
services:
app:
build: packages/app
restart: unless-stopped
networks:
- internal_network
ports:
- "3000:3000"
env_file:
- ./.env
api:
build: packages/server
restart: unless-stopped

View File

@ -1,4 +1,4 @@
FROM node:16-alpine
FROM node:20-alpine
RUN apk add git
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

View File

@ -4,17 +4,6 @@
"label": "Search",
"icon": "FiSearch"
},
{
"id": "messages",
"label": "Messages",
"icon": "FiMessageCircle",
"path": "/messages"
},
{
"id": "notifications",
"label": "Notifications",
"icon": "FiBell"
},
{
"id": "settings",
"label": "Settings",

View File

@ -45,7 +45,7 @@
"fast-average-color": "^9.2.0",
"framer-motion": "^10.12.17",
"fuse.js": "6.5.3",
"hls.js": "^1.5.15",
"hls.js": "^1.5.17",
"howler": "2.2.3",
"i18next": "21.6.6",
"js-cookie": "3.0.1",

View File

@ -75,17 +75,9 @@ const Attachment = React.memo((props) => {
return <ImageViewer src={url} />
}
case "video": {
return <Plyr
source={{
type: "video",
sources: [{
src: url,
}],
}}
options={{
controls: ["play", "progress", "current-time", "mute", "volume"],
}}
/>
return <video controls>
<source src={url} type={mimeType} />
</video>
}
case "audio": {
return <audio controls>

View File

@ -68,6 +68,7 @@
video {
height: 100%;
width: 100%;
user-select: none;
-webkit-user-drag: none;

View File

@ -12,6 +12,7 @@ import Poll from "@components/Poll"
import clipboardEventFileToFile from "@utils/clipboardEventFileToFile"
import PostModel from "@models/post"
import SearchModel from "@models/search"
import "./index.less"
@ -33,6 +34,8 @@ export default class PostCreator extends React.Component {
fileList: [],
postingPolicy: DEFAULT_POST_POLICY,
mentionsLoadedData: []
}
pollRef = React.createRef()
@ -59,14 +62,6 @@ export default class PostCreator extends React.Component {
})
}
fetchUploadPolicy = async () => {
const policy = await PostModel.getPostingPolicy()
this.setState({
postingPolicy: policy
})
}
canSubmit = () => {
const { postMessage, postAttachments, pending, postingPolicy } = this.state
@ -83,9 +78,7 @@ export default class PostCreator extends React.Component {
return true
}
debounceSubmit = lodash.debounce(() => this.submit(), 50)
submit = async () => {
submit = lodash.debounce(async () => {
if (this.state.loading) {
return false
}
@ -154,10 +147,9 @@ export default class PostCreator extends React.Component {
app.navigation.goToPost(this.props.reply_to)
}
}
}
}, 50)
uploadFile = async (req) => {
// hide uploader
this.toggleUploaderVisibility(false)
const request = await app.cores.remoteStorage.uploadFile(req.file)
@ -265,18 +257,18 @@ export default class PostCreator extends React.Component {
}
}
onChangeMessageInput = (event) => {
handleMessageInputChange = (inputText) => {
// if the fist character is a space or a whitespace remove it
if (event.target.value[0] === " " || event.target.value[0] === "\n") {
event.target.value = event.target.value.slice(1)
if (inputText[0] === " " || inputText[0] === "\n") {
inputText = inputText.slice(1)
}
this.setState({
postMessage: event.target.value
postMessage: inputText
})
}
handleKeyDown = async (e) => {
handleMessageInputKeydown = async (e) => {
// detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed
if (e.keyCode === 13 && !e.shiftKey) {
e.preventDefault()
@ -290,6 +282,15 @@ export default class PostCreator extends React.Component {
}
}
handleOnMentionSearch = lodash.debounce(async (value) => {
const results = await SearchModel.userSearch(`username:${value}`)
this.setState({
mentionsLoadedData: results
})
}, 300)
updateFileList = (uid, newValue) => {
let updatedFileList = this.state.fileList
@ -577,16 +578,28 @@ export default class PostCreator extends React.Component {
<img src={app.userData?.avatar} />
</div>
<antd.Input.TextArea
<antd.Mentions
placeholder="What are you thinking?"
value={postMessage}
autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={postingPolicy.maxMessageLength}
onChange={this.onChangeMessageInput}
onKeyDown={this.handleKeyDown}
onChange={this.handleMessageInputChange}
onKeyDown={this.handleMessageInputKeydown}
disabled={loading}
draggable={false}
prefix="@"
allowClear
options={this.state.mentionsLoadedData.map((item) => {
return {
key: item.id,
value: item.username,
label: <>
<antd.Avatar src={item.avatar} />
<span>{item.username}</span>
</>,
}
})}
onSearch={this.handleOnMentionSearch}
/>
<div>

View File

@ -21,6 +21,9 @@ const TrendingsCard = (props) => {
{
E_Trendings && <span>Something went wrong</span>
}
{
!L_Trendings && !E_Trendings && R_Trendings && R_Trendings.length === 0 && <span>No trendings</span>
}
{
!L_Trendings && !E_Trendings && R_Trendings && R_Trendings.map((trending, index) => {
return <div

View File

@ -273,7 +273,7 @@ export default class Player extends Core {
})
}
async playbackMode(mode) {
playbackMode(mode) {
if (typeof mode !== "string") {
return this.state.playback_mode
}
@ -289,7 +289,7 @@ export default class Player extends Core {
return mode
}
async stopPlayback() {
stopPlayback() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}

View File

@ -131,7 +131,6 @@ export default class StyleCore extends Core {
mutateTheme: (...args) => this.mutateTheme(...args),
resetToDefault: () => this.resetToDefault(),
toggleCompactMode: () => this.toggleCompactMode(),
}
async onInitialize() {
@ -251,20 +250,6 @@ export default class StyleCore extends Core {
StyleCore.storagedModifications = this.public.mutation
}
toggleCompactMode(value = !window.app.cores.settings.get("style.compactMode")) {
if (value === true) {
return this.applyStyles({
layoutMargin: 0,
layoutPadding: 0,
})
}
return this.applyStyles({
layoutMargin: this.getVar("layoutMargin"),
layoutPadding: this.getVar("layoutPadding"),
})
}
resetToDefault() {
store.remove(StyleCore.modificationStorageKey)

View File

@ -21,8 +21,6 @@ export default class WidgetsCore extends Core {
async onInitialize() {
try {
//await WidgetsCore.apiInstance()
const currentStore = this.getInstalled()
if (!Array.isArray(currentStore)) {

View File

@ -3,14 +3,14 @@ import React from "react"
export default () => {
const enterPlayerAnimation = () => {
app.cores.style.applyTemporalVariant("dark")
app.cores.style.toggleCompactMode(true)
app.layout.toggleCompactMode(true)
app.layout.toggleCenteredContent(false)
app.controls.toggleUIVisibility(false)
}
const exitPlayerAnimation = () => {
app.cores.style.applyVariant(app.cores.style.getStoragedVariantKey())
app.cores.style.toggleCompactMode(false)
app.layout.toggleCompactMode(false)
app.layout.toggleCenteredContent(true)
app.controls.toggleUIVisibility(true)
}

View File

@ -108,6 +108,9 @@ export default class Layout extends React.PureComponent {
togglePagePanelSpacer: (to) => {
return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to)
},
toggleCompactMode: (to) => {
return this.layoutInterface.toggleRootContainerClassname("compact-mode", to)
},
toggleRootContainerClassname: (classname, to) => {
const root = document.documentElement

View File

@ -61,10 +61,14 @@ export default class ToolsBar extends React.Component {
}
render() {
const hasAnyRenders = this.state.renders.top.length > 0 || this.state.renders.bottom.length > 0
const isVisible = hasAnyRenders && this.state.visible
return <Motion
style={{
x: spring(this.state.visible ? 0 : 100),
width: spring(this.state.visible ? 100 : 0),
x: spring(isVisible ? 0 : 100),
width: spring(isVisible ? 100 : 0),
}}
>
{({ x, width }) => {
@ -76,7 +80,7 @@ export default class ToolsBar extends React.Component {
className={classnames(
"tools-bar-wrapper",
{
visible: this.state.visible,
visible: isVisible,
}
)}
>

View File

@ -18,34 +18,6 @@ function isOverflown(element) {
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
}
const RenderArtist = (props) => {
const { artist } = props
if (!artist) {
return null
}
if (Array.isArray(artist)) {
return <h3>{artist.join(",")}</h3>
}
return <h3>{artist}</h3>
}
const RenderAlbum = (props) => {
const { album } = props
if (!album) {
return null
}
if (Array.isArray(album)) {
return <h3>{album.join(",")}</h3>
}
return <h3>{album}</h3>
}
const PlayerController = React.forwardRef((props, ref) => {
const [playerState] = usePlayerStateContext()
@ -152,9 +124,7 @@ const PlayerController = React.forwardRef((props, ref) => {
</div>
<div className="lyrics-player-controller-info-details">
<RenderArtist artist={playerState.track_manifest?.artists} />
-
<RenderAlbum album={playerState.track_manifest?.album} />
<span>{playerState.track_manifest?.artistStr}</span>
</div>
</div>

View File

@ -11,6 +11,7 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
const { lyrics } = props
const [initialLoading, setInitialLoading] = React.useState(true)
const [syncInterval, setSyncInterval] = React.useState(null)
const [syncingVideo, setSyncingVideo] = React.useState(false)
const [currentVideoLatency, setCurrentVideoLatency] = React.useState(0)
@ -29,21 +30,18 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
setSyncingVideo(true)
videoRef.current.currentTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000
let newTime = currentTrackTime + (lyrics.sync_audio_at_ms / 1000) + app.cores.player.gradualFadeMs / 1000
// dec some ms to ensure the video seeks correctly
newTime -= 5 / 1000
videoRef.current.currentTime = newTime
}
async function syncPlayback() {
if (!lyrics) {
return false
}
// if `sync_audio_at_ms` is present, it means the video must be synced with audio
if (lyrics.video_source && typeof lyrics.sync_audio_at_ms !== "undefined") {
if (!videoRef.current) {
clearInterval(syncInterval)
setSyncInterval(null)
setCurrentVideoLatency(0)
return false
// if something is wrong, stop syncing
if (videoRef.current === null || !lyrics || !lyrics.video_source || typeof lyrics.sync_audio_at_ms === "undefined" || playerState.playback_status !== "playing") {
return stopSyncInterval()
}
const currentTrackTime = app.cores.player.controls.seek()
@ -64,51 +62,18 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
seekVideoToSyncAudio()
}
}
}
function startSyncInterval() {
setSyncInterval(setInterval(syncPlayback, 300))
}
React.useEffect(() => {
videoRef.current.addEventListener("seeked", (event) => {
function stopSyncInterval() {
setSyncingVideo(false)
})
// videoRef.current.addEventListener("error", (event) => {
// console.log("Failed to load", event)
// })
// videoRef.current.addEventListener("ended", (event) => {
// console.log("Video ended", event)
// })
// videoRef.current.addEventListener("stalled", (event) => {
// console.log("Failed to fetch data, but trying")
// })
// videoRef.current.addEventListener("waiting", (event) => {
// console.log("Waiting for data...")
// })
}, [])
//* Handle when playback status change
React.useEffect(() => {
if (lyrics?.video_source && typeof lyrics?.sync_audio_at_ms !== "undefined") {
if (playerState.playback_status === "playing") {
videoRef.current.play()
setSyncInterval(setInterval(syncPlayback, 500))
} else {
videoRef.current.pause()
if (syncInterval) {
setSyncInterval(null)
clearInterval(syncInterval)
}
}
}
}, [playerState.playback_status])
//* handle when player is loading
React.useEffect(() => {
if (lyrics?.video_source && playerState.loading === true && playerState.playback_status === "playing") {
videoRef.current.pause()
@ -119,50 +84,61 @@ const LyricsVideo = React.forwardRef((props, videoRef) => {
}
}, [playerState.loading])
//* Handle when lyrics object change
//* Handle when playback status change
React.useEffect(() => {
clearInterval(syncInterval)
setCurrentVideoLatency(0)
setSyncingVideo(false)
if (lyrics) {
if (lyrics.video_source) {
hls.current.loadSource(lyrics.video_source)
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
if (initialLoading === false) {
console.log(`VIDEO:: Playback status changed to ${playerState.playback_status}`)
if (lyrics && lyrics.video_source) {
if (playerState.playback_status === "playing") {
videoRef.current.play()
startSyncInterval()
} else {
videoRef.current.pause()
stopSyncInterval()
}
const currentTime = app.cores.player.controls.seek()
if (currentTime > 0) {
seekVideoToSyncAudio()
}
}
}, [playerState.playback_status])
//* Handle when lyrics object change
React.useEffect(() => {
setCurrentVideoLatency(0)
stopSyncInterval()
if (lyrics) {
if (lyrics.video_source) {
console.log("Loading video source >", lyrics.video_source)
hls.current.loadSource(lyrics.video_source)
if (typeof lyrics.sync_audio_at_ms !== "undefined") {
videoRef.current.loop = false
videoRef.current.currentTime = lyrics.sync_audio_at_ms / 1000
startSyncInterval()
} else {
videoRef.current.loop = true
videoRef.current.currentTime = 0
}
if (playerState.playback_status === "playing"){
videoRef.current.play()
}
} else {
videoRef.current
}
} else {
videoRef.current
}
setInitialLoading(false)
}, [lyrics])
React.useEffect(() => {
clearInterval(syncInterval)
videoRef.current.addEventListener("seeked", (event) => {
setSyncingVideo(false)
})
hls.current.attachMedia(videoRef.current)
return () => {
clearInterval(syncInterval)
stopSyncInterval()
}
}, [])

View File

@ -8,11 +8,15 @@ import PostsList from "@components/PostsList"
import PostService from "@models/post"
import useCenteredContainer from "@hooks/useCenteredContainer"
import "./index.less"
const PostPage = (props) => {
const post_id = props.params.post_id
useCenteredContainer(true)
const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
post_id,
})

View File

@ -0,0 +1,27 @@
import React from "react"
import PostList from "@components/PostsList"
import PostModel from "@models/post"
import "./index.less"
const TrendingPage = (props) => {
const { trending } = props.params
return <div className="trending-page">
<div className="trending-page-header">
<h1>#{trending.toLowerCase()}</h1>
</div>
<div className="trending-page-content">
<PostList
loadFromModel={PostModel.getTrending}
loadFromModelProps={{
trending,
}}
/>
</div>
</div>
}
export default TrendingPage

View File

@ -0,0 +1,18 @@
.trending-page {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
.trending-page-header {
display: flex;
flex-direction: column;
}
.trending-page-content {
display: flex;
flex-direction: column;
}
}

View File

@ -257,14 +257,14 @@ export default class StreamViewer extends React.Component {
enterPlayerAnimation = () => {
app.cores.style.applyTemporalVariant("dark")
app.cores.style.toggleCompactMode(true)
app.layout.toggleCompactMode(true)
app.layout.toggleCenteredContent(false)
app.controls.toggleUIVisibility(false)
}
exitPlayerAnimation = () => {
app.cores.style.applyVariant(app.cores.style.currentVariantKey)
app.cores.style.toggleCompactMode(false)
app.layout.toggleCompactMode(false)
app.layout.toggleCenteredContent(true)
app.controls.toggleUIVisibility(true)
}

View File

@ -14,29 +14,4 @@ export default [
component: LivestreamsList,
disabled: true,
},
{
key: "controlPanel",
label: "Creator Panel",
icon: "MdSpaceDashboard",
children: [
{
key: "controlPanel.uploads",
label: "Uploads",
icon: "FiUpload",
disabled: true
},
{
key: "controlPanel.streaming_settings",
label: "Stream Configuration",
icon: "FiSettings",
disabled: true,
},
{
key: "controlPanel.dvr_settings",
label: "DVR",
icon: "MdFiberDvr",
disabled: true
}
]
}
]

View File

@ -7,16 +7,21 @@ import WidgetItemPreview from "@components/WidgetItemPreview"
import "./index.less"
export default class WidgetsManager extends React.Component {
state = {
loadedWidgets: app.cores.widgets.getInstalled() ?? [],
}
const WidgetsManager = () => {
const [loadedWidgets, setLoadedWidgets] = React.useState(app.cores.widgets.getInstalled() ?? [])
React.useEffect(() => {
if (app.layout.tools_bar) {
app.layout.tools_bar.toggleVisibility(true)
}
}, [])
render() {
return <div className="widgets-manager">
<h1>Widgets</h1>
<div className="widgets-manager-list">
{
Array.isArray(this.state.loadedWidgets) && this.state.loadedWidgets.map((manifest) => {
Array.isArray(loadedWidgets) && loadedWidgets.map((manifest) => {
return <React.Fragment>
<WidgetItemPreview
manifest={manifest}
@ -50,12 +55,13 @@ export default class WidgetsManager extends React.Component {
<antd.Button
type="primary"
icon={<Icons.FiPlus />}
onClick={openWidgetsBrowserModal}
onClick={() => { }}
>
Install more
</antd.Button>
</div>
</div>
</div>
}
}
export default WidgetsManager

View File

@ -1,5 +1,3 @@
import React from "react"
import WidgetsManager from "../components/widgetsManager"
export default {
@ -7,17 +5,5 @@ export default {
icon: "FiList",
label: "Widgets",
group: "app",
render: () => {
React.useEffect(() => {
if (app.layout.tools_bar) {
app.layout.tools_bar.toggleVisibility(true)
}
}, [])
return <div>
<h1>Widgets</h1>
<WidgetsManager />
</div>
},
render: WidgetsManager
}

View File

@ -577,7 +577,8 @@
}
// fix input
.ant-input {
.ant-input,
.ant-mentions {
background-color: var(--background-color-accent);
color: var(--text-color);
@ -588,11 +589,37 @@
}
}
.ant-input-affix-wrapper {
background-color: var(--background-color-accent);
.ant-mentions-outlined {
background: transparent;
&:hover {
background: transparent;
background-color: var(--background-color-accent) !important;
}
&:focus {
background: transparent;
background-color: var(--background-color-accent) !important;
}
&:focus-within {
background: transparent;
background-color: var(--background-color-accent) !important;
}
}
.ant-input-affix-wrapper,
.ant-mentions-affix-wrapper {
background: transparent;
background-color: var(--background-color-accent);
color: var(--text-color);
border: 0 !important;
:hover {
border: 0 !important;
}
::placeholder {
color: var(--text-color);
opacity: 0.5;

View File

@ -83,6 +83,8 @@ a {
transition: all 150ms ease-in-out;
overflow-x: hidden;
&.electron {
.ant-layout-sider {
padding-top: 0px;
@ -167,6 +169,13 @@ html {
// }
// }
&.compact-mode {
.content_layout {
padding: 0 !important;
margin: 0 !important;
}
}
&.centered-content {
.content_layout {
margin: 0 auto;

View File

@ -2,13 +2,33 @@ export default {
name: "Extension",
collection: "extensions",
schema: {
user_id: {
type: String,
required: true
},
assetsUrl: {
type: String,
required: true
},
srcUrl: {
type: String,
required: true
},
registryId: {
type: String,
required: true
},
packageUrl: {
type: String,
required: true
},
version: {
type: String,
required: true
},
title: {
name: {
type: String,
default: "Untitled"
default: "untitled"
},
description: {
type: String,
@ -22,9 +42,5 @@ export default {
type: Date,
required: true,
},
experimental: {
type: Boolean,
default: false
},
}
}

View File

@ -172,12 +172,14 @@ export default class Proxy {
if (!route) {
res.statusCode = 404
res.end(`
{
"error": "404 Not found"
}
`)
return
res.end(JSON.stringify({
error: "Gateway route not found",
details: "The gateway route you are trying to access does not exist, maybe the service is down...",
namespace: namespace,
}))
return null
}
if (route.pathRewrite) {

View File

@ -1,8 +1,6 @@
import templates from "../templates"
export default async (ctx, data) => {
console.log(`EMS Send account activation `, data)
const { user, activation_code } = data
const result = await ctx.mailTransporter.sendMail({

View File

@ -0,0 +1,3 @@
export default class Extension {
static resolve = require("./methods/resolve").default
}

View File

@ -0,0 +1,24 @@
import { Extension } from "@db_models"
export default async function resolve(payload) {
let { user_id, pkg } = payload
const [pkgName, pkgVersion] = pkg.split("@")
if (!pkgVersion) {
pkgVersion = "latest"
}
if (pkgVersion === "latest") {
return await Extension.findOne({
user_id,
name: pkgName,
}).sort({ version: -1 }).limit(1).exec()
}
return await Extension.findOne({
user_id,
name: pkgName,
version: pkgVersion,
})
}

View File

@ -1,6 +1,8 @@
import { Server } from "linebridge"
import B2 from "backblaze-b2"
import DbManager from "@shared-classes/DbManager"
import CacheService from "@shared-classes/CacheService"
import SharedMiddlewares from "@shared-middlewares"
@ -16,10 +18,21 @@ class API extends Server {
contexts = {
db: new DbManager(),
b2: new B2({
applicationKeyId: process.env.B2_KEY_ID,
applicationKey: process.env.B2_APP_KEY,
}),
cache: new CacheService({
fsram: false
}),
}
async onInitialize() {
await this.contexts.db.initialize()
await this.contexts.b2.authorize()
global.cache = this.contexts.cache
global.b2 = this.contexts.b2
}
handleWsAuth = require("@shared-lib/handleWsAuth").default

View File

@ -2,8 +2,9 @@
"name": "marketplace",
"version": "0.60.2",
"dependencies": {
"7zip-min": "^1.4.4",
"@octokit/rest": "^19.0.7",
"7zip-min": "^1.4.4",
"backblaze-b2": "^1.7.0",
"sucrase": "^3.32.0",
"uglify-js": "^3.17.4"
}

View File

@ -0,0 +1,10 @@
import ExtensionClass from "@classes/extension"
export default async (req) => {
const { user_id, pkg } = req.params
return await ExtensionClass.resolveManifest({
user_id,
pkg,
})
}

View File

@ -0,0 +1,12 @@
import ExtensionClass from "@classes/extension"
export default async (req, res) => {
const { user_id, pkg } = req.params
const manifest = await ExtensionClass.resolve({
user_id,
pkg,
})
return manifest
}

View File

@ -0,0 +1,133 @@
import { Extension } from "@db_models"
import fs from "node:fs"
import path from "node:path"
import sevenzip from "7zip-min"
async function uploadFolderToB2(bucketId, folderPath, b2Directory) {
try {
const uploadFiles = async (dir) => {
const files = fs.readdirSync(dir)
for (const file of files) {
const fullPath = path.join(dir, file)
const stats = fs.statSync(fullPath)
if (stats.isDirectory()) {
await uploadFiles(fullPath)
} else {
const fileData = fs.readFileSync(fullPath)
const b2FileName = path.join(b2Directory, path.relative(folderPath, fullPath)).replace(/\\/g, '/')
console.log(`Uploading ${b2FileName}...`)
const uploadUrl = await b2.getUploadUrl({
bucketId: bucketId,
})
await b2.uploadFile({
uploadUrl: uploadUrl.data.uploadUrl,
uploadAuthToken: uploadUrl.data.authorizationToken,
fileName: b2FileName,
data: fileData,
})
console.log(`Uploaded ${b2FileName}`)
}
}
}
await uploadFiles(folderPath)
console.log('All files uploaded successfully.')
} catch (error) {
console.error('Error uploading folder:', error)
}
}
export default {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
let { pkg } = req.headers
if (!pkg) {
throw new OperationError(400, "Missing package")
}
if (!req.auth) {
throw new OperationError(401, "Unauthorized")
}
pkg = JSON.parse(pkg)
const { user_id } = req.auth.session
const registryId = `${user_id}/${pkg.name}@${pkg.version}`
const s3Path = `${process.env.B2_BUCKET_ID}/extensions/${pkg.name}/${pkg.version}`
const assetsUrl = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${s3Path}`
const workPath = path.resolve(global.cache.constructor.cachePath, String(Date.now()), registryId)
const pkgPath = path.resolve(workPath, "pkg")
const bundlePath = path.resolve(workPath, "bundle.7z")
// console.log({
// user_id,
// pkg,
// registryId,
// s3Path,
// workPath,
// bundlePath
// })
let extensionRegistry = await Extension.findOne({
user_id: user_id,
registryId: registryId,
version: pkg.version
})
if (extensionRegistry) {
throw new OperationError(400, "Extension already exists")
}
try {
if (!fs.existsSync(workPath)) {
await fs.promises.mkdir(workPath, { recursive: true })
}
// read multipart form
await req.multipart(async (field) => {
await field.write(bundlePath)
})
await new Promise((resolve, reject) => {
sevenzip.unpack(bundlePath, pkgPath, (error) => {
if (error) {
fs.promises.rm(workPath, { recursive: true, force: true })
reject(error)
} else {
resolve()
}
})
})
await uploadFolderToB2(process.env.B2_BUCKET_ID, pkgPath, s3Path)
fs.promises.rm(workPath, { recursive: true, force: true })
extensionRegistry = await Extension.create({
user_id: user_id,
name: pkg.name,
version: pkg.version,
registryId: registryId,
assetsUrl: assetsUrl,
srcUrl: `${assetsUrl}/src`,
packageUrl: `${assetsUrl}/package.json`,
created_at: Date.now(),
})
return extensionRegistry
} catch (error) {
fs.promises.rm(workPath, { recursive: true, force: true })
throw error
}
}
}

View File

@ -1,8 +0,0 @@
import { Extension } from "@db_models"
export default {
middlewares: ["withAuthentication"],
fn: async (req, res) => {
}
}

View File

@ -18,6 +18,13 @@ export default {
throw new OperationError(403, "Unauthorized")
}
console.log(`Setting lyrics for track ${track_id} >`, {
track_id: track_id,
video_source: video_source,
lrc: lrc,
track: track,
})
let trackLyric = await TrackLyric.findOne({
track_id: track_id
}).lean()
@ -35,9 +42,8 @@ export default {
trackLyric.sync_audio_at = sync_audio_at
}
trackLyric = await TrackLyric.findOneAndUpdate(
{
_id: trackLyric._id
trackLyric = await TrackLyric.findOneAndUpdate({
track_id: track_id
},
trackLyric
)

View File

@ -0,0 +1,25 @@
import { Post } from "@db_models"
import fullfill from "@classes/posts/methods/fullfill"
export default {
middlewares: ["withOptionalAuthentication"],
fn: async (req) => {
const { limit, trim } = req.query
let result = await Post.find({
message: {
$regex: new RegExp(`#${req.params.trending}`, "gi")
}
})
.sort({ created_at: -1 })
.skip(trim ?? 0)
.limit(limit ?? 20)
result = await fullfill({
posts: result,
for_user_id: req.auth.session.user_id,
})
return result
}
}

View File

@ -2,56 +2,44 @@ import { Post } from "@db_models"
import { DateTime } from "luxon"
const maxDaysOld = 30
const maxHashtags = 5
export default async (req) => {
// fetch all posts that contain in message an #, with a maximun of 5 diferent hashtags
let posts = await Post.find({
message: {
$regex: /#/gi
const startDate = DateTime.local().minus({ days: maxDaysOld }).toISO()
const trendings = await Post.aggregate([
{
$match: {
message: { $regex: /#/gi },
created_at: { $gte: startDate }
}
},
created_at: {
$gte: DateTime.local().minus({ days: maxDaysOld }).toISO()
{
$project: {
hashtags: {
$regexFindAll: {
input: "$message",
regex: /#[a-zA-Z0-9_]+/g
}
})
.lean()
// get the hastag content
posts = posts.map((post) => {
post.hashtags = post.message.match(/#[a-zA-Z0-9_]+/gi)
post.hashtags = post.hashtags.map((hashtag) => {
return hashtag.substring(1)
})
return post
})
// build trendings
let trendings = posts.reduce((acc, post) => {
post.hashtags.forEach((hashtag) => {
if (acc.find((trending) => trending.hashtag === hashtag)) {
acc = acc.map((trending) => {
if (trending.hashtag === hashtag) {
trending.count++
}
return trending
})
} else {
acc.push({
hashtag,
count: 1
})
}
})
},
{ $unwind: "$hashtags" },
{
$project: {
hashtag: { $substr: ["$hashtags.match", 1, -1] }
}
},
{
$group: {
_id: "$hashtag",
count: { $sum: 1 }
}
},
{ $sort: { count: -1 } },
{ $limit: maxHashtags }
])
return acc
}, [])
// sort by count
trendings = trendings.sort((a, b) => {
return b.count - a.count
})
return trendings
return trendings.map(({ _id, count }) => ({ hashtag: _id, count }));
}

View File

@ -0,0 +1,44 @@
import { User } from "@db_models"
const ALLOWED_FIELDS = [
"username",
"publicName",
"id",
]
export default {
middlewares: ["withOptionalAuthentication"],
fn: async (req, res) => {
const { keywords, limit = 50 } = req.query
let filters = {}
if (keywords) {
keywords.split(";").forEach((pair) => {
const [field, value] = pair.split(":")
if (value === "" || value === " ") {
return
}
// Verifica que el campo esté en los permitidos y que tenga un valor
if (ALLOWED_FIELDS.includes(field) && value) {
// Si el campo es "id", se busca coincidencia exacta
if (field === "id") {
filters[field] = value
} else {
// Para otros campos, usa $regex para coincidencias parciales
filters[field] = { $regex: `\\b${value}`, $options: "i" }
}
}
})
}
console.log(filters)
let users = await User.find(filters)
.limit(limit)
return users
}
}

View File

@ -1,12 +1,11 @@
#!/usr/bin/env node
import path from "path"
import fs from "fs"
import axios from "axios"
import sevenzip from "7zip-min"
import formdata from "form-data"
const tmpPath = path.join(process.cwd(), ".tmp")
const widgetsApi = "http://localhost:3040"
const marketplaceAPIOrigin = "https://indev.comty.app/api/extensions"
const token = process.argv[2]
const excludedFiles = [
@ -17,9 +16,18 @@ const excludedFiles = [
"/package-lock.json",
]
async function copyToTmp(origin) {
const rootPath = process.cwd()
const tmpPath = path.join(rootPath, ".tmp")
const buildPath = path.join(tmpPath, "build")
const bundlePath = path.join(tmpPath, "bundle.7z")
async function copySources(origin, to) {
const files = fs.readdirSync(origin)
if (!fs.existsSync(to)) {
await fs.promises.mkdir(to, { recursive: true })
}
for (const file of files) {
const filePath = path.join(origin, file)
@ -33,14 +41,9 @@ async function copyToTmp(origin) {
}
if (fs.lstatSync(filePath).isDirectory()) {
await copyToTmp(filePath)
await copySources(filePath, path.join(to, file))
} else {
const fileContent = fs.readFileSync(filePath)
const relativePath = filePath.replace(process.cwd(), "")
const tmpFilePath = path.join(tmpPath, relativePath)
fs.mkdirSync(path.dirname(tmpFilePath), { recursive: true })
fs.writeFileSync(tmpFilePath, fileContent)
await fs.promises.copyFile(filePath, path.join(to, file))
}
}
}
@ -63,22 +66,37 @@ async function main() {
return
}
const rootPath = process.cwd()
// create a .tmp folder
if (!fs.existsSync(tmpPath)) {
fs.mkdirSync(tmpPath)
if (fs.existsSync(tmpPath)) {
await fs.promises.rm(tmpPath, { recursive: true, force: true })
}
const bundlePath = path.join(rootPath, "bundle.7z")
try {
// try to read package.json
if (!fs.existsSync(path.resolve(rootPath, "package.json"))) {
console.error("🛑 package.json not found")
return
}
const packageJSON = require(path.resolve(rootPath, "package.json"))
// check if package.json has a main file
if (!packageJSON.main) {
console.error("🛑 package.json does not have a main file")
return
}
if (!fs.existsSync(path.resolve(rootPath, packageJSON.main))) {
console.error("🛑 main file not found")
return
}
console.log(packageJSON)
console.log("📦 Creating bundle...")
await copyToTmp(rootPath)
await createBundle(`${tmpPath}/*`, bundlePath)
await fs.promises.rm(tmpPath, { recursive: true, force: true })
await copySources(rootPath, buildPath)
await createBundle(`${buildPath}/*`, bundlePath)
console.log("📦✅ Bundle created successfully")
@ -86,13 +104,14 @@ async function main() {
const formData = new formdata()
formData.append("bundle", fs.createReadStream(bundlePath))
formData.append("file", fs.createReadStream(bundlePath))
const response = await axios({
method: "POST",
url: `${widgetsApi}/widgets/publish`,
method: "PUT",
url: `${marketplaceAPIOrigin}/publish`,
headers: {
...formData.getHeaders(),
pkg: JSON.stringify(packageJSON),
Authorization: `Bearer ${token}`,
},
data: formData,
@ -102,12 +121,15 @@ async function main() {
return false
})
await fs.promises.rm(bundlePath, { recursive: true, force: true })
await fs.promises.rm(tmpPath, { recursive: true, force: true })
if (response) {
console.log("🚚✅ Bundle published successfully! \n", response.data)
}
await fs.promises.rm(tmpPath, { recursive: true, force: true })
} catch (error) {
console.error("🛑 Error while publishing bundle \n\t", error)
await fs.promises.rm(tmpPath, { recursive: true, force: true })
}
}
main().catch(console.error)

View File

@ -0,0 +1,6 @@
{
"name": "clock",
"version": "1.0.6",
"description": "Display the current time",
"main": "./src/extension.js"
}

View File

@ -0,0 +1,21 @@
import React from "react"
import "./index.less"
const Clock = () => {
const [time, setTime] = React.useState(new Date())
React.useEffect(() => {
const interval = setInterval(() => {
setTime(new Date())
}, 1000)
return () => clearInterval(interval)
}, [])
return <div className="clock">
{time.toLocaleTimeString()}
</div>
}
export default Clock

View File

@ -0,0 +1,45 @@
export default class Clock {
registerWidgets = [
{
name: "Clock",
description: "Display the current time",
component: () => import("./clock.jsx"),
}
]
registerPages = [
{
path: "/clock",
component: () => import("./clock.jsx"),
}
]
public = {
echo: (...str) => {
this.console.log(...str)
},
fib: (n) => {
let a = 0, b = 1
for (let i = 0; i < n; i++) {
let c = a + b
a = b
b = c
}
return a
}
}
events = {
"test": (data) => {
this.console.log("test")
if (data) {
this.console.log(data)
}
}
}
async onInitialize() {
this.console.log("Hi from the extension worker!")
}
}

View File

@ -0,0 +1,6 @@
.clock {
font-size: 2rem;
color: blue;
font-family: "DM Mono", monospace;
}