diff --git a/.gitignore b/.gitignore index 1ce8ce8ff..aa188a399 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ flask_session/ site_settings.json /files/test.py tags +chat/node_modules +chat/build +chat/.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2ef02f465..6fcc39e63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,18 @@ RUN mkdir /asset_submissions/hats RUN mkdir /asset_submissions/marseys/original RUN mkdir /asset_submissions/hats/original +ENV NODE_VERSION=16.13.0 +RUN apt install -y curl +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +ENV NVM_DIR=/root/.nvm +RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} +RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} +RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} +ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" +RUN node --version +RUN npm --version +RUN npm i -g yarn + EXPOSE 80/tcp CMD [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ] diff --git a/chat/.env.template b/chat/.env.template new file mode 100644 index 000000000..66ceacfec --- /dev/null +++ b/chat/.env.template @@ -0,0 +1,6 @@ +FEATURES_ACTIVITY=false +DEBUG=false +NODE_ENV="production" +EMOJI_INPUT_TOKEN=":" +QUICK_EMOJIS_MAX_COUNT = 20 +APPROXIMATE_CHARACTER_WIDTH = 8 \ No newline at end of file diff --git a/chat/build.js b/chat/build.js new file mode 100644 index 000000000..d747ba3ea --- /dev/null +++ b/chat/build.js @@ -0,0 +1,19 @@ +require('dotenv').config() +const path = require("path"); +const { build } = require("esbuild"); + +const options = { + entryPoints: ["./src/index.tsx"], + outfile: path.resolve(__dirname, "../files/assets/js/chat_done.js"), + bundle: true, + define: { + "process.env.NODE_ENV": `"${process.env.NODE_ENV}"`, + "process.env.DEBUG": process.env.DEBUG, + "process.env.FEATURES_ACTIVITY": process.env.FEATURES_ACTIVITY, + "process.env.EMOJI_INPUT_TOKEN": `"${process.env.EMOJI_INPUT_TOKEN}"`, + "process.env.QUICK_EMOJIS_MAX_COUNT": process.env.QUICK_EMOJIS_MAX_COUNT, + "process.env.APPROXIMATE_CHARACTER_WIDTH": process.env.APPROXIMATE_CHARACTER_WIDTH, + }, +}; + +build(options).catch(() => process.exit(1)); diff --git a/chat/global.d.ts b/chat/global.d.ts new file mode 100644 index 000000000..189c38836 --- /dev/null +++ b/chat/global.d.ts @@ -0,0 +1,21 @@ +declare var process: { + env: Record; +}; + +declare interface ChatSpeakResponse { + username: string; + avatar: string; + hat: string; + namecolor: string; + text: string; + text_censored: string; + text_html: string; + time: number; + timestamp: string; +} + +declare interface EmojiModSelection { + large: boolean; + mirror: boolean; + pat: boolean; +} diff --git a/chat/package.json b/chat/package.json new file mode 100644 index 000000000..b172fd89a --- /dev/null +++ b/chat/package.json @@ -0,0 +1,36 @@ +{ + "name": "chat", + "version": "0.0.2", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@types/lodash.debounce": "^4.0.7", + "@types/lozad": "^1.16.1", + "@types/react": "^18.0.20", + "@types/react-dom": "^18.0.6", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", + "classnames": "^2.3.2", + "dotenv": "^16.0.2", + "esbuild": "^0.15.7", + "lodash.debounce": "^4.0.8", + "lozad": "^1.16.0", + "react": "^18.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dom": "^18.2.0", + "react-virtualized-auto-sizer": "^1.0.7", + "react-window": "^1.8.7", + "run-when-changed": "^2.1.0", + "socket.io-client": "^4.5.2", + "typescript": "^4.8.3", + "weak-key": "^1.0.2" + }, + "scripts": { + "chat": "yarn check && yarn build && yarn css:move", + "chat:watch": "run-when-changed --watch \"**/*.*\" --exec \"yarn chat\"", + "check": "tsc", + "build": "node ./build", + "css:move": "mv ../files/assets/js/chat_done.css ../files/assets/css/chat_done.css" + } +} diff --git a/chat/src/App.css b/chat/src/App.css new file mode 100644 index 000000000..ee61b3cbd --- /dev/null +++ b/chat/src/App.css @@ -0,0 +1,62 @@ +.App { + position: fixed; + width: 100vw; + height: calc(100vh - 42px); + display: flex; +} + +@media screen and (min-width: 1000px) { + .App { + height: calc(100vh - 12rem); + } +} + +.App-wrapper { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 1000px; +} + +.App-heading { + flex-basis: 3rem; + border-bottom: 1px dashed var(--primary); + display: flex; + align-items: center; +} + +.App-side { + position: absolute; + top: 0; + right: 0; + background: var(--gray-500); +} + +@media screen and (min-width: 1100px) { + .App-side { + display: none; + } +} + +.App-content { + position: relative; + flex: 1; +} + +.App-drawer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + z-index: 2; + display: flex; +} + +.App-input { + flex-basis: 7rem; + padding: 0 1rem; +} diff --git a/chat/src/App.tsx b/chat/src/App.tsx new file mode 100644 index 000000000..e604b28ff --- /dev/null +++ b/chat/src/App.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { DndProvider, useDrop } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { + ChatHeading, + ChatMessageList, + UserInput, + UserList, + UsersTyping, +} from "./features"; +import { ChatProvider, DrawerProvider, useDrawer } from "./hooks"; +import "./App.css"; + +export function App() { + return ( + + + + + + + + ); +} + +function AppInner() { + const [_, dropRef] = useDrop({ + accept: "drawer", + }); + const { open, config } = useDrawer(); + + return ( +
+
+
+ +
+
+
+ {open && config.content ? ( +
{config.content}
+ ) : ( + + )} +
+
+ +
+
+
+ + +
+
+
+ ); +} diff --git a/chat/src/drawers/BaseDrawer.css b/chat/src/drawers/BaseDrawer.css new file mode 100644 index 000000000..5d76f8c9a --- /dev/null +++ b/chat/src/drawers/BaseDrawer.css @@ -0,0 +1,5 @@ +.BaseDrawer { + flex: 1; + padding-right: 2rem; + overflow: hidden; +} diff --git a/chat/src/drawers/BaseDrawer.tsx b/chat/src/drawers/BaseDrawer.tsx new file mode 100644 index 000000000..f1fdedb49 --- /dev/null +++ b/chat/src/drawers/BaseDrawer.tsx @@ -0,0 +1,10 @@ +import React, { PropsWithChildren, useEffect } from "react"; +import "./BaseDrawer.css"; + +interface Props extends PropsWithChildren { + onClose?(): void; +} + +export function BaseDrawer({ onClose, children }: Props) { + return
{children}
; +} diff --git a/chat/src/drawers/index.ts b/chat/src/drawers/index.ts new file mode 100644 index 000000000..38fdad714 --- /dev/null +++ b/chat/src/drawers/index.ts @@ -0,0 +1 @@ +export * from "./BaseDrawer" \ No newline at end of file diff --git a/chat/src/features/activity/Activity.css b/chat/src/features/activity/Activity.css new file mode 100644 index 000000000..7868f62f6 --- /dev/null +++ b/chat/src/features/activity/Activity.css @@ -0,0 +1,21 @@ +.Activity { + display: flex; + align-items: center; + padding: 1rem; + margin: 0 4rem; +} + +.Activity > section { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; +} + +.Activity i { + margin-right: 3rem; +} + +.Activity > section:not(:last-child) { + margin-right: 2rem; +} \ No newline at end of file diff --git a/chat/src/features/activity/Activity.tsx b/chat/src/features/activity/Activity.tsx new file mode 100644 index 000000000..7d4e56dd8 --- /dev/null +++ b/chat/src/features/activity/Activity.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import "./Activity.css"; + +const ACTIVITIES = [ + { + icon: "circle", + title: "Roulette", + description: "Round and round the wheel of fate turns.", + }, + { + icon: "cards", + title: "Blackjack", + description: "Twenty one ways to change your life.", + }, + { + icon: "dollar-sign", + title: "Slots", + description: "Today's your lucky day.", + }, + { + icon: "dollar-sign", + title: "Racing", + description: "Make it all back at the track.", + }, + { icon: "dollar-sign", title: "Crossing", description: "Take a load off." }, +]; + +export function Activity() { + return ( +
+ {ACTIVITIES.map((activity) => ( +
+ +

{activity.title}

+
+ ))} +
+ ); +} diff --git a/chat/src/features/activity/index.ts b/chat/src/features/activity/index.ts new file mode 100644 index 000000000..d9f56adb2 --- /dev/null +++ b/chat/src/features/activity/index.ts @@ -0,0 +1 @@ +export * from "./Activity"; diff --git a/chat/src/features/chat/ActivityList.css b/chat/src/features/chat/ActivityList.css new file mode 100644 index 000000000..763807152 --- /dev/null +++ b/chat/src/features/chat/ActivityList.css @@ -0,0 +1,35 @@ +.ActivityList { + margin-left: 2rem; +} + +.ActivityList h4 { + display: flex; + align-items: center; + justify-content: space-between; +} + +.ActivityList h4 hr { + flex: 1; + margin-right: 1rem; +} + +.ActivityList-activity { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.ActivityList-activity-icon { + margin-right: 1rem; +} + +.ActivityList-activity { + transition: background 0.4s ease-in-out; + padding: 0 1rem; +} + +.ActivityList-activity:hover { + cursor: pointer; + background: #ffffff05; +} \ No newline at end of file diff --git a/chat/src/features/chat/ActivityList.tsx b/chat/src/features/chat/ActivityList.tsx new file mode 100644 index 000000000..de2ce30c0 --- /dev/null +++ b/chat/src/features/chat/ActivityList.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import cx from 'classnames' +import { useDrawer } from "../../hooks"; +import "./ActivityList.css"; + +const ACTIVITIES = [ + { + game: "Poker", + description: "Know when to hold 'em.", + icon: "fas fa-cards", + }, + { + game: "Roulette", + description: "Table go brrrr.", + icon: "fas fa-circle", + }, + { + game: "Slots", + description: "Is today your lucky day?", + icon: "fas fa-dollar-sign", + }, + { + game: "Blackjack", + description: "Twenty one ways to change your life.", + icon: "fas fa-cards", + }, + { + game: "Racing", + description: "Look at 'em go.", + icon: "fas fa-cards", + }, + { + game: "Crossing", + description: "A simple life.", + icon: "fas fa-cards", + }, + { + game: "Lottershe", + description: "Can't win if you don't play.", + icon: "fas fa-ticket", + }, +]; + +export function ActivityList() { + const { toggle } = useDrawer(); + + return ( +
+

+
+ Activities +

+
+ {ACTIVITIES.map(({ game, description, icon }) => ( +
+
+
+ +
{game}
{description}
+
+ + 0 +
+
+ ))} +
+
+ ); +} diff --git a/chat/src/features/chat/Chat.css b/chat/src/features/chat/Chat.css new file mode 100644 index 000000000..1ff272bba --- /dev/null +++ b/chat/src/features/chat/Chat.css @@ -0,0 +1,56 @@ +.Chat { + display: flex; + align-items: stretch; + justify-content: center; + position: relative; + overflow: hidden; + width: 100%; + min-width: 350px; + max-width: 1200px; +} + +.Chat-mobile-top { + display: none; + align-items: center; + justify-content: flex-end; + margin-bottom: 1rem; +} + +.Chat-mobile-top i { + margin-right: 0.25rem; + position: relative; + top: -1px; +} + +.Chat-mobile-top span { + font-size: 18px; +} + +.Chat-side { + display: flex; + flex-direction: column; + width: 40%; +} + +@media screen and (max-width: 1180px) { + .Chat-mobile-top { + display: flex; + } + .Chat-side { + display: none; + } +} + +.Chat-drawer { + padding-right: 1rem; +} + +.Chat-typing { + height: 18px; + display: inline-block; +} + + +.Chat-window { + width: 100%; +} \ No newline at end of file diff --git a/chat/src/features/chat/Chat.tsx b/chat/src/features/chat/Chat.tsx new file mode 100644 index 000000000..ed3b106f5 --- /dev/null +++ b/chat/src/features/chat/Chat.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { useChat, useDrawer } from "../../hooks"; +import { ActivityList } from "./ActivityList"; +import { ChatMessageList } from "./ChatMessage"; +import { QuotedMessage } from "./QuotedMessage"; +import { UserInput } from "./UserInput"; +import { UserList } from "./UserList"; +import "./Chat.css"; + +export function Chat() { + const { reveal } = useDrawer(); + const { online, quote } = useChat(); + + return ( +
+
+
+ +
+ + {quote && } + +
+
+ + {process.env.FEATURES_ACTIVITY && } +
+
+ ); +} \ No newline at end of file diff --git a/chat/src/features/chat/ChatHeading.css b/chat/src/features/chat/ChatHeading.css new file mode 100644 index 000000000..5c13a474a --- /dev/null +++ b/chat/src/features/chat/ChatHeading.css @@ -0,0 +1,10 @@ +.ChatHeading { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; +} + +.ChatHeading i { + margin-right: 0.5rem; +} \ No newline at end of file diff --git a/chat/src/features/chat/ChatHeading.tsx b/chat/src/features/chat/ChatHeading.tsx new file mode 100644 index 000000000..c3760d205 --- /dev/null +++ b/chat/src/features/chat/ChatHeading.tsx @@ -0,0 +1,31 @@ +import React, { useCallback } from "react"; +import { useChat, useDrawer } from "../../hooks"; +import { UserList } from "./UserList"; +import "./ChatHeading.css"; + +export function ChatHeading() { + const { reveal } = useDrawer(); + const { online } = useChat(); + const handleOpenUserListDrawer = useCallback( + () => + reveal({ + title: "Users in chat", + content: , + }), + [] + ); + + return ( +
+
+
+ + {online.length} users online +
+
+ ); +} diff --git a/chat/src/features/chat/ChatMessage.css b/chat/src/features/chat/ChatMessage.css new file mode 100644 index 000000000..7cbfff4b9 --- /dev/null +++ b/chat/src/features/chat/ChatMessage.css @@ -0,0 +1,70 @@ +@keyframes fading-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.ChatMessage { + padding: 0.5rem; + padding-right: 3rem; + position: relative; + animation: fading-in 0.3s ease-in-out forwards; +} + +.ChatMessage:nth-of-type(even) { + background: rgba(255, 255, 255, 0.02); +} + +.ChatMessage-top { + display: flex; + align-items: center; +} + +.ChatMessage-timestamp { + margin-left: 0.5rem; +} + +.ChatMessage-bottom { + display: flex; + align-items: center; + justify-content: space-between; + padding-left: 30px; +} + +.ChatMessage-content { + margin-right: 0.5rem; + word-break: break-all; + display: inline-block; +} + +.ChatMessage-button { + background: transparent !important; +} + +.ChatMessage-delete { + position: absolute; + top: 4px; + right: 4px; +} + +.ChatMessageList { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: auto; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.ChatMessageList::-webkit-scrollbar { + display: none; +} + +lite-youtube { + min-width: min(80vw, 500px); +} diff --git a/chat/src/features/chat/ChatMessage.tsx b/chat/src/features/chat/ChatMessage.tsx new file mode 100644 index 000000000..d0266a579 --- /dev/null +++ b/chat/src/features/chat/ChatMessage.tsx @@ -0,0 +1,113 @@ +import React, { useEffect, useRef } from "react"; +import { Username } from "./Username"; +import { useChat, useRootContext } from "../../hooks"; +import "./ChatMessage.css"; +import key from "weak-key"; + +interface ChatMessageProps extends ChatSpeakResponse { + showUser?: boolean; +} + +export function ChatMessage({ + avatar, + showUser = true, + namecolor, + username, + hat, + text, + text_html, + text_censored, + time, + timestamp, +}: ChatMessageProps) { + const message = { + avatar, + namecolor, + username, + hat, + text, + text_html, + text_censored, + time, + timestamp, + }; + const { + username: loggedInUsername, + admin, + censored, + themeColor, + } = useRootContext(); + const { quote, deleteMessage, quoteMessage } = useChat(); + const content = censored ? text_censored : text_html; + const hasMention = content.includes(loggedInUsername); + const mentionStyle = hasMention + ? { backgroundColor: `#${themeColor}55` } + : {}; + const quoteStyle = + quote?.username === username && quote?.text === text && quote?.time === time + ? { borderLeft: `2px solid #${themeColor}` } + : {}; + const style = { ...mentionStyle, ...quoteStyle }; + + return ( +
+ {showUser && ( +
+ +
{timestamp}
+
+ )} +
+
+ + +
+ {admin && ( + + )} +
+
+ ); +} + +export function ChatMessageList() { + const { messages } = useChat(); + const messageWrapper = useRef(null); + + useEffect(() => { + messageWrapper.current.scrollTop = messageWrapper.current.scrollHeight; + }, [messages]); + + return ( +
+ {messages.map((message, index) => ( + + ))} +
+ ); +} diff --git a/chat/src/features/chat/QuotedMessage.css b/chat/src/features/chat/QuotedMessage.css new file mode 100644 index 000000000..00a7a80d3 --- /dev/null +++ b/chat/src/features/chat/QuotedMessage.css @@ -0,0 +1,28 @@ +@keyframes sliding-up { + from { + top: 50px; + } + to { + top: 0; + } +} + +.QuotedMessage { + position: relative; + padding: 0.5rem 0; + border-top: 1px solid var(--primary); + display: flex; + align-items: center; + justify-content: space-between; + animation: sliding-up 0.3s forwards; +} + +.QuotedMessage-content { + margin-left: 1rem; + flex: 1; + max-width: 420px; + max-height: 40px; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 1rem; +} \ No newline at end of file diff --git a/chat/src/features/chat/QuotedMessage.tsx b/chat/src/features/chat/QuotedMessage.tsx new file mode 100644 index 000000000..881a6ebcc --- /dev/null +++ b/chat/src/features/chat/QuotedMessage.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { useChat, useRootContext } from "../../hooks"; +import { Username } from "./Username"; +import "./QuotedMessage.css"; + +export function QuotedMessage() { + const { quote, quoteMessage } = useChat(); + const { censored } = useRootContext(); + + return ( +
+
+ +
+
+ +
+ ); +} diff --git a/chat/src/features/chat/UserInput.css b/chat/src/features/chat/UserInput.css new file mode 100644 index 000000000..3d8d94ab9 --- /dev/null +++ b/chat/src/features/chat/UserInput.css @@ -0,0 +1,11 @@ +.UserInput { + position: relative; +} + +.UserInput-emoji { + cursor: pointer; + position: absolute; + top: 12px; + right: 12px; + font-size: 20px; +} \ No newline at end of file diff --git a/chat/src/features/chat/UserInput.tsx b/chat/src/features/chat/UserInput.tsx new file mode 100644 index 000000000..43476901d --- /dev/null +++ b/chat/src/features/chat/UserInput.tsx @@ -0,0 +1,178 @@ +import React, { + ChangeEvent, + KeyboardEvent, + FormEvent, + useCallback, + useRef, + useMemo, + useState, +} from "react"; +import { useChat, useDrawer, useEmojis } from "../../hooks"; +import { EmojiDrawer, QuickEmojis } from "../emoji"; +import "./UserInput.css"; + +export function UserInput() { + const { draft, sendMessage, updateDraft } = useChat(); + const { reveal, hide, open } = useDrawer(); + const builtChatInput = useRef(null); + const { visible, addQuery } = useEmojis(); + const form = useRef(null); + const [typingOffset, setTypingOffset] = useState(0); + const quickEmojis = useMemo( + () => visible.slice(0, process.env.QUICK_EMOJIS_MAX_COUNT), + [visible] + ); + const handleChange = useCallback( + (event: ChangeEvent) => { + const input = event.target.value; + const [openEmojiToken, closeEmojiToken] = locateEmojiTokens(input); + const emojiSegment = input.slice(openEmojiToken + 1, closeEmojiToken + 1); + + updateDraft(input); + addQuery(openEmojiToken === -1 ? "" : emojiSegment); + setTypingOffset( + emojiSegment.length * process.env.APPROXIMATE_CHARACTER_WIDTH + ); + }, + [] + ); + const handleSendMessage = useCallback( + (event?: FormEvent) => { + event?.preventDefault(); + sendMessage(); + }, + [sendMessage] + ); + const handleKeyUp = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Enter") { + handleSendMessage(); + } + }, + [handleSendMessage] + ); + const handleOpenEmojiDrawer = useCallback( + () => + reveal({ + title: "Select an emoji", + content: ( + builtChatInput.current?.focus()} + /> + ), + }), + [] + ); + const handleCloseEmojiDrawer = useCallback(() => { + builtChatInput.current?.focus(); + hide(); + }, [hide]); + + const handleSelectEmoji = useCallback((emoji: string) => { + updateDraft((prev) => `${prev} :${emoji}: `); + }, []); + const handleInsertQuickEmoji = useCallback( + (emoji: string) => { + const [openEmojiToken, closeEmojiToken] = locateEmojiTokens(draft); + const toReplace = draft.slice(openEmojiToken, closeEmojiToken + 1); + + updateDraft((prev) => prev.replace(toReplace, `:${emoji}: `)); + addQuery(""); + + builtChatInput.current?.focus(); + }, + [draft] + ); + + return ( +
+ {quickEmojis.length > 0 && ( +
+ +
+ )} + - -
-
- - -
-

Users in chat right now

-
- +
- -
- - - - - - -{% include "emoji_modal.html" %} -{% include "expanded_image_modal.html" %} - - - - - -{% if v.admin_level > 1 %} - -{% endif %} + + + + diff --git a/startup_chat.sh b/startup_chat.sh index b61dc982e..0fd7ac901 100644 --- a/startup_chat.sh +++ b/startup_chat.sh @@ -1,4 +1,5 @@ cd /rDrama git pull +cd ./chat && yarn chat && cd ../ . /env gunicorn files.__main__:app load_chat -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 -b 0.0.0.0:5001 --max-requests 30000 --max-requests-jitter 30000 \ No newline at end of file diff --git a/ubuntu_setup.sh b/ubuntu_setup.sh index 80506dc7a..1795291d9 100644 --- a/ubuntu_setup.sh +++ b/ubuntu_setup.sh @@ -4,7 +4,7 @@ # reboot apt -y update apt -y upgrade -apt -y install git redis-server python3-pip ffmpeg imagemagick tmux nginx snapd ufw gpg-agent htop +apt -y install git redis-server python3-pip ffmpeg imagemagick tmux nginx snapd ufw gpg-agent htop git config --global credential.helper store cd /rDrama @@ -32,6 +32,16 @@ psql -U postgres -f schema.sql postgres psql -U postgres -f seed-db.sql postgres pip3 install -r requirements.txt +apt -y install curl +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +. "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} +. "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} +. "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} +PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" +node --version +npm --version +npm i -g yarn + mkdir /images mkdir /songs mkdir /videos