pasting attachments
parent
d595cc7373
commit
13aff451a8
|
|
@ -59,3 +59,5 @@ thumbs.db
|
|||
|
||||
# docker data
|
||||
docker-volume
|
||||
|
||||
storage
|
||||
|
|
@ -4,6 +4,7 @@ services:
|
|||
db:
|
||||
image: mariadb
|
||||
restart: always
|
||||
command: --max_allowed_packet=200M
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: example
|
||||
MARIADB_DATABASE: corner
|
||||
|
|
@ -20,15 +21,15 @@ services:
|
|||
ports:
|
||||
- 8080:8080
|
||||
|
||||
coturn:
|
||||
image: coturn/coturn
|
||||
restart: always
|
||||
ports:
|
||||
- 3478:3478
|
||||
- 3478:3478/udp
|
||||
- 5349:5349
|
||||
- 5349:5349/udp
|
||||
- 49160-49200:49160-49200/udp
|
||||
# coturn:
|
||||
# image: coturn/coturn
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 3478:3478
|
||||
# - 3478:3478/udp
|
||||
# - 5349:5349
|
||||
# - 5349:5349/udp
|
||||
# - 49160-49200:49160-49200/udp
|
||||
|
||||
# docker run -d -p 3478:3478 -p 3478:3478/udp -p 5349:5349 -p 5349:5349/udp -p 49160-49200:49160-49200/udp \
|
||||
# coturn/coturn -n --log-file=stdout \
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -55,19 +55,25 @@
|
|||
"vue-tsc": "0.38.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-ws": "^3.0.1",
|
||||
"@types/mysql": "^2.15.21",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-timeago": "^4.1.3",
|
||||
"@types/tmp": "^0.2.3",
|
||||
"@types/totp-generator": "^0.0.4",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/ws": "^8.5.3",
|
||||
"@vitejs/plugin-react": "^2.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"cordova": "^11.0.0",
|
||||
"electron-updater": "5.0.5",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"express": "^4.18.1",
|
||||
"express-ws": "^5.0.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"get-port": "^6.1.2",
|
||||
"local-storage": "^2.0.0",
|
||||
"mysql": "^2.18.1",
|
||||
|
|
@ -76,9 +82,12 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-spinners": "^0.13.4",
|
||||
"react-time-ago": "^7.2.1",
|
||||
"react-timeago": "^7.1.0",
|
||||
"react-toastify": "^9.0.8",
|
||||
"reactjs-popup": "^2.0.5",
|
||||
"tmp": "^0.2.1",
|
||||
"totp-generator": "^0.0.13",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "3.2.37",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
default-src 'self' data: https://ssl.gstatic.com https://fonts.gstatic.com;
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||
media-src * data:;
|
||||
img-src 'self' data: content: http://tinygraphs.com;
|
||||
img-src 'self' data: content: *;
|
||||
connect-src *;">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<title>Vite App</title>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import ServerConnection from './components/ServerConnection';
|
|||
import EphemeralState from './contexts/EphemeralState/EphemeralState';
|
||||
import PersistentState from '/@/contexts/PersistentState/PersistentState';
|
||||
import Router from './Router';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
|
||||
export default function App() {
|
||||
const [transparent, setTransparent] = useState(false);
|
||||
|
|
@ -64,6 +65,32 @@ export default function App() {
|
|||
border-radius: 16px;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: unset
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 0px -10px -10px 0px var(--neutral-3);
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: var(--neutral-9);
|
||||
}
|
||||
`}</style>
|
||||
<div style={{
|
||||
background: transparent ? 'rgba(0, 0, 0, 0)' : 'var(--neutral-3)',
|
||||
|
|
@ -78,6 +105,8 @@ export default function App() {
|
|||
<Router></Router>
|
||||
</EphemeralState>
|
||||
</PersistentState>
|
||||
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { v4 } from 'uuid';
|
||||
import { GrFormClose } from 'react-icons/gr';
|
||||
import { IoMdClose } from "react-icons/io";
|
||||
import testImage from '../../assets/pfp.jpg'
|
||||
import useFileUpload, { Upload } from "../hooks/useFileUpload";
|
||||
import useChannel from "../hooks/useChannel";
|
||||
import { useApi } from "../lib/useApi";
|
||||
|
||||
const exampleImages = [
|
||||
{
|
||||
id: v4(),
|
||||
name: 'image.jpg',
|
||||
data: testImage,
|
||||
}
|
||||
]
|
||||
|
||||
export default function ChatInput() {
|
||||
const textBoxRef = useRef<HTMLDivElement>(null);
|
||||
const { channel } = useChannel();
|
||||
|
||||
const [attachments, setAttachments] = useState<string[]>([]);
|
||||
|
||||
const { newFile, getFileInfo } = useFileUpload();
|
||||
|
||||
const PADDING = 8;
|
||||
const CHATBOX_SIZE = 64;
|
||||
|
||||
const addAttachment = (id: string) => {
|
||||
setAttachments(attachments => [...attachments, id])
|
||||
}
|
||||
|
||||
const removeAttachment = (id: string) => {
|
||||
setAttachments(attachments => attachments.filter(a => a !== id));
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// addAttachment(newFile('image.jpg', testImage));
|
||||
// }, []);
|
||||
|
||||
const pasta: React.ClipboardEventHandler<HTMLDivElement> = (event) => {
|
||||
|
||||
let pastedFiles = false;
|
||||
|
||||
for (let idx = 0; idx < event.clipboardData.items.length; idx ++) {
|
||||
const item = event.clipboardData.items[idx];
|
||||
const file = event.clipboardData.files[idx];
|
||||
const type = event.clipboardData.types[idx]
|
||||
|
||||
if (item.kind === 'file') {
|
||||
var blob = item.getAsFile();
|
||||
if(blob === null) continue;
|
||||
|
||||
addAttachment(newFile(file.name, file.type, blob));
|
||||
|
||||
pastedFiles = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(pastedFiles) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
const { send } = useApi({});
|
||||
|
||||
const keyPress = (event: any) => {
|
||||
if(event.code === 'Enter' && !event.shiftKey) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
event.bubbles = false;
|
||||
|
||||
for(const attachment of attachments) {
|
||||
const info = getFileInfo(attachment);
|
||||
if(!info?.processed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const [file, ...restFiles] = attachments.map(a => getFileInfo(a)?.externalId) as string[];
|
||||
const text = textBoxRef.current?.innerHTML ?? '';
|
||||
|
||||
if(text === '' && file === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(channel === null) return true;
|
||||
|
||||
const newMessage: NewMessageRequest = {
|
||||
uid: v4(),
|
||||
text,
|
||||
channel,
|
||||
timestamp: new Date().getTime(),
|
||||
file
|
||||
}
|
||||
|
||||
console.log(file, restFiles);
|
||||
|
||||
send('message:message', newMessage);
|
||||
|
||||
for(const file of restFiles) {
|
||||
const newMessage: NewMessageRequest = {
|
||||
uid: v4(),
|
||||
text: '',
|
||||
channel,
|
||||
timestamp: new Date().getTime(),
|
||||
file
|
||||
}
|
||||
send('message:message', newMessage);
|
||||
}
|
||||
|
||||
setAttachments([]);
|
||||
if(textBoxRef.current !== null) {
|
||||
textBoxRef.current.innerHTML = '';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style ={{
|
||||
minWidth: '0px',
|
||||
}}>
|
||||
<div style={{
|
||||
overflowX: 'auto',
|
||||
}}>
|
||||
{attachments.length > 0 && (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
boxSizing: 'border-box',
|
||||
paddingBottom: '0px',
|
||||
}}>
|
||||
<div style={{
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{attachments.map(attachment => {
|
||||
const info = getFileInfo(attachment);
|
||||
if(!info) return <span>Poop</span>;
|
||||
return (
|
||||
<AttachmentBox
|
||||
key={attachment}
|
||||
attachment={info}
|
||||
onClose={() => removeAttachment(attachment)}
|
||||
></AttachmentBox>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
textBoxRef.current?.focus();
|
||||
}}
|
||||
style={{
|
||||
margin: PADDING + 'px',
|
||||
marginRight: '0px',
|
||||
borderRadius: ((CHATBOX_SIZE - PADDING*2) / 2) + 'px',
|
||||
background: 'var(--neutral-5)',
|
||||
gridArea: 'message',
|
||||
display: 'grid',
|
||||
placeItems: 'center center',
|
||||
padding: '8px 16px',
|
||||
minHeight: '48px',
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'text',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onPaste={pasta}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
// outline: '1px solid white',
|
||||
outline: 'none',
|
||||
}}
|
||||
onKeyPress={keyPress}
|
||||
ref={textBoxRef}
|
||||
contentEditable
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AttachmentBox(props: {
|
||||
attachment: Upload,
|
||||
onClose: React.MouseEventHandler<SVGElement>
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
verticalAlign: 'top',
|
||||
// padding: '8px',
|
||||
background: 'var(--neutral-3)',
|
||||
// border: '1px solid var(--neutral-6)',
|
||||
fontSize: '0.8em',
|
||||
display: 'inline-block',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: '8px',
|
||||
marginRight: '8px',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
// textAlign: 'center',
|
||||
}}>
|
||||
<div style={{
|
||||
transition: 'width 300ms, background 300ms, opacity 800ms',
|
||||
width: `${props.attachment.progress * 100}%`,
|
||||
height: '100%',
|
||||
background: props.attachment.uploaded ? 'white' : 'var(--green)',
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
zIndex: '0',
|
||||
opacity: props.attachment.uploaded ? '0' : '0.3'
|
||||
}}></div>
|
||||
<div style={{
|
||||
zIndex: '1',
|
||||
padding: '8px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<IoMdClose
|
||||
size={16}
|
||||
style={{
|
||||
float: 'right',
|
||||
color: 'var(--neutral-9)',
|
||||
padding: '1px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={props.onClose}
|
||||
></IoMdClose>
|
||||
<div style={{
|
||||
paddingBottom: '8px',
|
||||
paddingRight: '24px',
|
||||
}}>{props.attachment.name}</div>
|
||||
{/* <img style={{
|
||||
display: 'block',
|
||||
height: '100px'
|
||||
}} src={props.attachment.blob}></img> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useApi } from "../lib/useApi";
|
||||
import { v4 } from 'uuid';
|
||||
import { useLog } from "../components/useLog";
|
||||
|
||||
const b64 = async (data: Uint8Array) => {
|
||||
const base64url = await new Promise((r) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => r(reader.result);
|
||||
reader.readAsDataURL(new Blob([data]));
|
||||
});
|
||||
// @ts-ignore
|
||||
return base64url.split(",", 2)[1];
|
||||
};
|
||||
|
||||
export interface Upload {
|
||||
internalId: string;
|
||||
externalId: string | null;
|
||||
data: Blob;
|
||||
reader: any;
|
||||
progress: number;
|
||||
name: string;
|
||||
sent: number;
|
||||
rcvd: number;
|
||||
uploaded: boolean;
|
||||
processed: boolean;
|
||||
}
|
||||
|
||||
// 400 kb
|
||||
const CHUNK_SIZE = 4_000;
|
||||
|
||||
export default function useFileUpload() {
|
||||
const [uploads, setUploads] = useState<Upload[]>([]);
|
||||
|
||||
const updateUpload = (clientId: string, newData: Partial<Upload>) => {
|
||||
setUploads(uploads => uploads.map(upload => {
|
||||
if(upload.internalId !== clientId) return upload;
|
||||
return {
|
||||
...upload,
|
||||
...newData,
|
||||
};
|
||||
}));
|
||||
};
|
||||
|
||||
const { send } = useApi({
|
||||
'file:new'(data: NewFileResponse) {
|
||||
updateUpload(data.clientId, { externalId: data.serverId });
|
||||
},
|
||||
'file:chunk'(data: FileChunkResponse) {
|
||||
updateUpload(data.clientId, { rcvd: data.chunk, progress: data.progress });
|
||||
},
|
||||
'file:end'(data: FileEndResponse) {
|
||||
updateUpload(data.clientId, { processed: true });
|
||||
}
|
||||
}, []);
|
||||
|
||||
type Shit = {
|
||||
chunk: Uint8Array;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
const sendNextChunk = async (upload: Upload) => {
|
||||
if (upload.externalId === null) return;
|
||||
|
||||
const {chunk, done}: Shit = await new Promise(async (res) => {
|
||||
const { value, done } = await upload.reader.read();
|
||||
res({
|
||||
chunk: value,
|
||||
done
|
||||
})
|
||||
});
|
||||
|
||||
if(chunk === undefined && done) {
|
||||
updateUpload(upload.internalId, { uploaded: true });
|
||||
const fileEndRequest: FileEndRequest = {
|
||||
serverId: upload.externalId,
|
||||
}
|
||||
send('file:end', fileEndRequest)
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkb64 = await b64(chunk);
|
||||
|
||||
updateUpload(upload.internalId, { sent: upload.sent + 1 });
|
||||
|
||||
const chunkReq: FileChunkRequest = {
|
||||
chunk: upload.sent,
|
||||
data: chunkb64,
|
||||
serverId: upload.externalId,
|
||||
};
|
||||
|
||||
send('file:chunk', chunkReq);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
for(const upload of uploads) {
|
||||
if(upload.rcvd === upload.sent && !upload.uploaded) {
|
||||
sendNextChunk(upload);
|
||||
}
|
||||
}
|
||||
}, [uploads])
|
||||
|
||||
const newFile = useCallback((name: string, type: string, blob: Blob) => {
|
||||
const id = v4();
|
||||
const newFileReq: NewFileRequest = {
|
||||
clientId: id,
|
||||
length: blob.size,
|
||||
name,
|
||||
type,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const reader = blob.stream().getReader();
|
||||
|
||||
setUploads(uploads => [...uploads, {
|
||||
internalId: id,
|
||||
externalId: null,
|
||||
data: blob,
|
||||
reader: reader,
|
||||
progress: 0,
|
||||
name,
|
||||
sent: 0,
|
||||
rcvd: 0,
|
||||
uploaded: false,
|
||||
processed: false,
|
||||
}]);
|
||||
send('file:new', newFileReq);
|
||||
return id;
|
||||
}, []);
|
||||
|
||||
const getInfo = useCallback((clientId: string) => {
|
||||
const file = uploads.find(upload => upload.internalId === clientId);
|
||||
return file ?? null;
|
||||
}, [uploads])
|
||||
|
||||
// useLog(uploads, 'uploads');
|
||||
|
||||
return {
|
||||
newFile,
|
||||
getFileInfo: getInfo
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ export default function Channels() {
|
|||
useEffect(() => {
|
||||
if(channels.length === 0) return;
|
||||
if(channel !== null) return;
|
||||
setChannel(channels[1].uid, channels[1].type);
|
||||
setChannel(channels[0].uid, channels[0].type);
|
||||
}, [channel, channels]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { MdSend } from 'react-icons/md';
|
|||
import useChannel from '../hooks/useChannel';
|
||||
import useClientId from '../hooks/useClientId';
|
||||
import useSessionToken from '../hooks/useSessionToken';
|
||||
import ChatInput from '../components/ChatInput';
|
||||
|
||||
function createMessage(from: string, text: string,
|
||||
channel: string, t = 0): IMessage {
|
||||
|
|
@ -21,13 +22,9 @@ function createMessage(from: string, text: string,
|
|||
|
||||
export default () => {
|
||||
const [messages, setMessages] = useState<IMessage[]>([]);
|
||||
const [hist, setHist] = useState(false);
|
||||
const { sessionToken } = useSessionToken();
|
||||
|
||||
const CHATBOX_SIZE = 64;
|
||||
const PADDING = 8;
|
||||
|
||||
const textBoxRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { channel, setChannel } = useChannel();
|
||||
const { clientId } = useClientId();
|
||||
|
|
@ -36,38 +33,38 @@ export default () => {
|
|||
'message:message'(data: IMessage) {
|
||||
if(data.channel !== channel) return;
|
||||
|
||||
setMessages([...messages, data]);
|
||||
setMessages(messages => ([...messages, data]));
|
||||
},
|
||||
'message:recent'(data: { messages: IMessage[] }) {
|
||||
setMessages(data.messages.reverse());
|
||||
},
|
||||
}, [messages]);
|
||||
}, [channel]);
|
||||
|
||||
useEffect(() => {
|
||||
send('message:recent', { channel });
|
||||
}, [channel, sessionToken]);
|
||||
}, [channel]);
|
||||
|
||||
const sendMessage = useCallback(() => {
|
||||
if(textBoxRef.current === null) return;
|
||||
if(channel === null) return;
|
||||
if(clientId === null) return;
|
||||
if(sessionToken === null) return;
|
||||
send(
|
||||
'message:message',
|
||||
createMessage(
|
||||
clientId,
|
||||
textBoxRef.current.innerText,
|
||||
channel,
|
||||
)
|
||||
);
|
||||
textBoxRef.current.innerText = '';
|
||||
}, [channel, sessionToken]);
|
||||
// const sendMessage = useCallback(() => {
|
||||
// if(textBoxRef.current === null) return;
|
||||
// if(channel === null) return;
|
||||
// if(clientId === null) return;
|
||||
// if(sessionToken === null) return;
|
||||
// send(
|
||||
// 'message:message',
|
||||
// createMessage(
|
||||
// clientId,
|
||||
// textBoxRef.current.innerText,
|
||||
// channel,
|
||||
// )
|
||||
// );
|
||||
// textBoxRef.current.innerText = '';
|
||||
// }, [channel, sessionToken]);
|
||||
|
||||
const keyDown = useCallback((evt: any) => {
|
||||
if(evt.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
}, [sendMessage]);
|
||||
// const keyDown = useCallback((evt: any) => {
|
||||
// if(evt.key === 'Enter') {
|
||||
// sendMessage();
|
||||
// }
|
||||
// }, [sendMessage]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -76,8 +73,8 @@ export default () => {
|
|||
width: '100%',
|
||||
display: 'grid',
|
||||
background: 'var(--neutral-4)',
|
||||
gridTemplateColumns: `1fr ${CHATBOX_SIZE}px`,
|
||||
gridTemplateRows: `1fr ${CHATBOX_SIZE}px`,
|
||||
gridTemplateColumns: `1fr min-content`,
|
||||
gridTemplateRows: `1fr min-content`,
|
||||
gridTemplateAreas: '"content content" "message send"',
|
||||
}}
|
||||
>
|
||||
|
|
@ -97,45 +94,29 @@ export default () => {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => {
|
||||
textBoxRef.current?.focus();
|
||||
}}style={{
|
||||
margin: PADDING + 'px',
|
||||
marginRight: '0px',
|
||||
borderRadius: ((CHATBOX_SIZE - PADDING*2) / 2) + 'px',
|
||||
background: 'var(--neutral-5)',
|
||||
gridArea: 'message',
|
||||
display: 'grid',
|
||||
placeItems: 'center center',
|
||||
padding: '0px 16px',
|
||||
cursor: 'text',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
<div
|
||||
ref={textBoxRef}
|
||||
onKeyPress={keyDown}
|
||||
className="input"
|
||||
role="textbox"
|
||||
contentEditable
|
||||
style={{
|
||||
background: 'inherit',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
// borderRadius: '8px',
|
||||
// borderRadius: '50%',
|
||||
width: '100%',
|
||||
resize: 'none',
|
||||
// border: '1px solid white',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<ChatInput></ChatInput>
|
||||
<SendButton></SendButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function SendButton() {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: '64px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
padding: '8px',
|
||||
boxSizing: 'border-box',
|
||||
position: 'absolute',
|
||||
bottom: '0px',
|
||||
}}>
|
||||
<div onClick={sendMessage} style={{
|
||||
<div style={{
|
||||
background: '#bd93f9',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
|
@ -152,5 +133,5 @@ export default () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
import { useContext, useEffect } from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { ClientsListContext } from '../contexts/EphemeralState/ClientsListState';
|
||||
import useHover from '../hooks/useHover';
|
||||
|
||||
export interface IMessage {
|
||||
uid: string;
|
||||
timestamp: number;
|
||||
from: string;
|
||||
text: string;
|
||||
channel: string;
|
||||
}
|
||||
export type IMessage = NewMessageResponse;
|
||||
|
||||
interface MessageProps {
|
||||
message: IMessage
|
||||
|
|
@ -16,54 +11,71 @@ interface MessageProps {
|
|||
|
||||
const firstLineIndent = '10px';
|
||||
const multiLineIndent = '16px';
|
||||
const rightMessagePagging = '16px';
|
||||
const rightMessagePadding = '16px';
|
||||
|
||||
export function Message({
|
||||
message,
|
||||
}: MessageProps) {
|
||||
|
||||
const { clientName } = useContext(ClientsListContext);
|
||||
const [hoverRef, hover] = useHover<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
<div ref={hoverRef} style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '4em 1fr',
|
||||
width: '100%',
|
||||
padding: '1px 0px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: hover ? 'var(--neutral-3)' : undefined,
|
||||
position: 'absolute',
|
||||
opacity: '0.5',
|
||||
}}></div>
|
||||
<span style={{
|
||||
fontStyle: 'italic',
|
||||
color: 'var(--neutral-6)',
|
||||
textAlign: 'right',
|
||||
userSelect: 'none',
|
||||
marginRight: '16px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<TimeAgo
|
||||
date={message.timestamp}
|
||||
formatter={(t, u) => u === 'second' ? 'Now' : ('' + t + u[0])}
|
||||
></TimeAgo>
|
||||
</span>
|
||||
<span style={{
|
||||
<div style={{
|
||||
// outline: '1px solid white',
|
||||
marginRight: '16px',
|
||||
position: 'relative',
|
||||
paddingLeft: '1em',
|
||||
textIndent: '-1em',
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: '500',
|
||||
float: 'left',
|
||||
paddingRight: firstLineIndent,
|
||||
// marginRight: '16px',
|
||||
// height: '100%'
|
||||
// borderBottom: '1px solid white'
|
||||
}}>
|
||||
{clientName[message.from]}
|
||||
<div>
|
||||
<span style={{
|
||||
fontWeight: '500',
|
||||
paddingRight: '8px',
|
||||
}}>
|
||||
{clientName[message.from]}
|
||||
</span>
|
||||
<span style={{
|
||||
}}>
|
||||
{message.text}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
marginRight: rightMessagePagging,
|
||||
paddingLeft: multiLineIndent,
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{message.text}
|
||||
</div>
|
||||
</span>
|
||||
{!!message.file && (
|
||||
<div>
|
||||
<img style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '20vh',
|
||||
}} src={message.file.url}></img>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
"types" : ["node"],
|
||||
"types" : [],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#preload": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
CREATE TABLE `files` (
|
||||
`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`data` longblob NOT NULL,
|
||||
`author` varchar(36) NOT NULL,
|
||||
`t_uploaded` bigint unsigned NOT NULL,
|
||||
`t_created` bigint unsigned NOT NULL,
|
||||
`t_deleted` bigint unsigned NULL,
|
||||
`type` varchar(255) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE `files`
|
||||
ADD `uid` varchar(36) NOT NULL AFTER `id`;
|
||||
|
||||
ALTER TABLE `files`
|
||||
ADD UNIQUE `uid` (`uid`);
|
||||
|
||||
ALTER TABLE `files`
|
||||
ADD FOREIGN KEY (`author`) REFERENCES `clients` (`uid`);
|
||||
|
||||
ALTER TABLE `messages`
|
||||
ADD `attachment` varchar(36) COLLATE 'utf8mb4_general_ci' NULL AFTER `text`;
|
||||
|
||||
ALTER TABLE `files`
|
||||
CHANGE `data` `data` longblob NULL AFTER `uid`;
|
||||
|
||||
ALTER TABLE `messages`
|
||||
ADD FOREIGN KEY (`attachment`) REFERENCES `files` (`uid`);
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import { ensureDirSync } from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export const STORAGE_PATH = resolve('../../../storage');
|
||||
|
||||
export const DB_HOST = 'localhost';
|
||||
export const DB_USER = 'root';
|
||||
export const DB_PASSWORD = 'example';
|
||||
export const DB_NAME = 'corner';
|
||||
export const DB_NAME = 'corner';
|
||||
|
||||
|
||||
|
||||
ensureDirSync(STORAGE_PATH);
|
||||
|
|
@ -8,7 +8,7 @@ export default function(sqlFile: any, ...args: any[]): Promise<any[] | null> {
|
|||
if(!err) return resolve(results);
|
||||
console.error(err.errno, err.sqlMessage);
|
||||
console.error('--- Query ---');
|
||||
console.error(err.sql);
|
||||
console.error(err.sql?.substring(0, 10000));
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
INSERT INTO messages
|
||||
(`text`, sender_uid, `uid`, `t_sent`, channel_uid)
|
||||
VALUES ( ?, ?, ?, ?, ? );
|
||||
INSERT INTO messages (
|
||||
`text`,
|
||||
sender_uid,
|
||||
`uid`,
|
||||
`t_sent`,
|
||||
channel_uid,
|
||||
attachment
|
||||
)
|
||||
VALUES ( ?, ?, ?, ?, ?, ? );
|
||||
|
|
@ -4,9 +4,12 @@ SELECT
|
|||
clients.uid as 'from',
|
||||
messages.`text` as 'text',
|
||||
messages.channel_uid,
|
||||
messages.uid as uid
|
||||
messages.uid as uid,
|
||||
files.type as file_type,
|
||||
files.uid as file_uid
|
||||
FROM messages
|
||||
JOIN clients ON messages.sender_uid=clients.uid
|
||||
LEFT JOIN files ON messages.attachment=files.uid
|
||||
WHERE messages.channel_uid=?
|
||||
ORDER BY -messages.t_sent
|
||||
LIMIT 100;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Router } from 'express';
|
||||
import database from '../lib/dbHelpers/database';
|
||||
import { resolve } from 'path';
|
||||
import { STORAGE_PATH } from '../constants';
|
||||
import { createReadStream, lstatSync } from 'fs';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/:uid', async (req, res) => {
|
||||
const info = await database.get.file.by.uid(req.params.uid);
|
||||
res.contentType = info.type;
|
||||
if(info.data !== null) {
|
||||
res.end(info.data);
|
||||
return;
|
||||
} else {
|
||||
// res.end('new hype');
|
||||
const path = resolve(STORAGE_PATH, req.params.uid);
|
||||
const size = lstatSync(path).size;
|
||||
const stream = createReadStream(path);
|
||||
res.header('Content-Length', '' + size);
|
||||
stream.pipe(res);
|
||||
}
|
||||
})
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,10 +1,19 @@
|
|||
import router from './lib/router';
|
||||
import { expose, reply } from './lib/WebSocketServer';
|
||||
|
||||
// ws
|
||||
import message from './routers/message';
|
||||
import channel from './routers/channel';
|
||||
import client from './routers/client';
|
||||
import totp from './routers/totp';
|
||||
import session from './routers/session';
|
||||
import voice from './routers/voice';
|
||||
import file from './routers/file';
|
||||
import express from 'express';
|
||||
import expressWs from 'express-ws';
|
||||
|
||||
// http
|
||||
import fileRouter from './http/file';
|
||||
|
||||
const api = router({
|
||||
up() {
|
||||
|
|
@ -21,16 +30,27 @@ const api = router({
|
|||
totp: totp,
|
||||
session: session,
|
||||
sessions: session,
|
||||
voice: voice
|
||||
voice: voice,
|
||||
file: file,
|
||||
files: file,
|
||||
});
|
||||
|
||||
expose(api, 3000);
|
||||
// var express = require('express');
|
||||
// var app = express();
|
||||
// var expressWs = require('express-ws')(app);
|
||||
|
||||
const app = express();
|
||||
const ewss = expressWs(app);
|
||||
|
||||
// @ts-ignore
|
||||
app.ws('/', expose(api, ewss.getWss()));
|
||||
app.use('/file', fileRouter);
|
||||
|
||||
app.listen(3000);
|
||||
|
||||
// -------------
|
||||
|
||||
import { update } from './db/migrate';
|
||||
import session from './routers/session';
|
||||
import voice from './routers/voice';
|
||||
|
||||
try {
|
||||
update();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,40 @@
|
|||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { inspect } from 'util';
|
||||
import { validateSessionToken } from '../routers/session';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export function expose(router: Function, port: number) {
|
||||
const wss = new WebSocketServer({
|
||||
port: 3000,
|
||||
}, () => {
|
||||
console.log('ws chat server started on dev.valnet.xyz');
|
||||
});
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
function str2color(str: string) {
|
||||
const v = str.split('').reduce((acc, val) => acc + val.charCodeAt(0), 0);
|
||||
return (v % 213) + 17
|
||||
}
|
||||
|
||||
function log(prefix: string, action: string, data: object) {
|
||||
if(action === 'up') return;
|
||||
const strAction = action.split(':').map(v => chalk.ansi256(str2color(v))(v)).join(':');
|
||||
const filteredObject = Object.entries(data)
|
||||
.filter(e => !e[0].startsWith('$'));
|
||||
const keyCount = Object.keys(filteredObject).length;
|
||||
if(keyCount === 0) return console.log(prefix, strAction);
|
||||
|
||||
const stringify = (key: string, value: any) => {
|
||||
switch(typeof value) {
|
||||
case 'string': return chalk.green(key);
|
||||
case 'object':
|
||||
if(value === null) return chalk.blackBright(key);
|
||||
else if(Array.isArray(value)) return chalk.cyanBright(`[${key}]`);
|
||||
else return chalk.magenta(key);
|
||||
case 'number': return chalk.yellow(key);
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
|
||||
const params = filteredObject.map(([k, v]) => stringify(k, v))
|
||||
|
||||
console.log(prefix, strAction, params.join(', '));
|
||||
}
|
||||
|
||||
export function expose(router: Function, wss: WebSocketServer) {
|
||||
return function(ws: WebSocket) {
|
||||
ws.on('message', async (str) => {
|
||||
try {
|
||||
const message = JSON.parse(str.toString());
|
||||
|
|
@ -29,23 +54,21 @@ export function expose(router: Function, port: number) {
|
|||
if(typeof data !== 'object') {
|
||||
throw new Error('action ' + action + ' payload not an object');
|
||||
}
|
||||
console.log('[IN]', action, data);
|
||||
log(chalk.green('>>>'), action, data);
|
||||
const _return = await (router(action, data, ws, wss) as unknown as Promise<any>);
|
||||
if(_return) {
|
||||
try {
|
||||
switch(_return.type) {
|
||||
case ResponseType.BROADCAST: {
|
||||
console.log('[OUT_BROADCAST]', action, _return.data);
|
||||
log(chalk.cyan('(\u25CF)'), action, _return.data)
|
||||
// console.log('[OUT_BROADCAST]', action, _return.data);
|
||||
for(const client of wss.clients) {
|
||||
send(client, action, _return.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ResponseType.REPLY: {
|
||||
console.log('[OUT]', action, inspect(_return.data, {
|
||||
depth: 0,
|
||||
colors: true,
|
||||
}));
|
||||
log(chalk.cyan('<<<'), action, _return.data);
|
||||
send(ws, action, _return.data);
|
||||
break;
|
||||
}
|
||||
|
|
@ -63,7 +86,7 @@ export function expose(router: Function, port: number) {
|
|||
console.error(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum ResponseType {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
INSERT INTO files (
|
||||
uid,
|
||||
author,
|
||||
type,
|
||||
data,
|
||||
t_created,
|
||||
t_uploaded,
|
||||
t_deleted
|
||||
)
|
||||
VALUES (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
NULL,
|
||||
?,
|
||||
?,
|
||||
NULL
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import query from "/@/db/query";
|
||||
import sql from './addFilePath.sql';
|
||||
|
||||
export default function addFile(uid: string, author: string, type: string) {
|
||||
const now = new Date().getTime();
|
||||
return query(sql, uid, author, type, now, now);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
INSERT INTO files (
|
||||
uid,
|
||||
author,
|
||||
type,
|
||||
data,
|
||||
t_created,
|
||||
t_uploaded,
|
||||
t_deleted
|
||||
)
|
||||
VALUES (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
NULL
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import query from "/@/db/query";
|
||||
import sql from './addFileRaw.sql';
|
||||
|
||||
export default function addFile(uid: string, author: string, data: Buffer, type: string) {
|
||||
const now = new Date().getTime();
|
||||
return query(sql, uid, author, type, data, now, now);
|
||||
}
|
||||
|
|
@ -1,11 +1,24 @@
|
|||
|
||||
import getAllDisplayNames from './get/all/displayNames';
|
||||
|
||||
import addFileRaw from './add/file/raw/addFileRaw';
|
||||
import addFilePath from './add/file/path/addFilePath';
|
||||
import getFileByUid from './get/file/by/uid';
|
||||
|
||||
const database = {
|
||||
get: {
|
||||
all: {
|
||||
displayNames: getAllDisplayNames
|
||||
},
|
||||
file: {
|
||||
by: {
|
||||
uid: getFileByUid
|
||||
}
|
||||
}
|
||||
},
|
||||
add: {
|
||||
file: {
|
||||
raw: addFileRaw,
|
||||
path: addFilePath
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
SELECT data, type, uid
|
||||
FROM files
|
||||
WHERE uid=?;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import sql from './uid.sql';
|
||||
import query from '/@/db/query';
|
||||
|
||||
export default async function getFileByUid(uid: string) {
|
||||
return ((await query(sql, uid)) ?? [])[0];
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ export const generateSessionToken = async (clientId: string) => {
|
|||
for (let i = 0; i < 64; i++) {
|
||||
token += rb32();
|
||||
}
|
||||
console.log('created session token', clientId, token);
|
||||
|
||||
// scnd min hr day year
|
||||
const year = 1000 * 60 * 60 * 24 * 365;
|
||||
const expiration = Date.now() + year;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
import router from "../lib/router";
|
||||
import { reply } from "../lib/WebSocketServer";
|
||||
import { v4 } from 'uuid';
|
||||
import tmp from 'tmp';
|
||||
import { writeFile, createWriteStream, WriteStream, readFile, createReadStream } from 'fs';
|
||||
import database from "../lib/dbHelpers/database";
|
||||
import { resolve } from 'path';
|
||||
import { STORAGE_PATH } from "../constants";
|
||||
|
||||
function createFile(id: string): WriteStream {
|
||||
return createWriteStream(resolve(STORAGE_PATH, id));
|
||||
}
|
||||
|
||||
interface ServerUpload {
|
||||
clientId: string;
|
||||
serverId: string;
|
||||
length: number;
|
||||
authorId: string;
|
||||
chunk: number;
|
||||
progress: number;
|
||||
type: string;
|
||||
// tmp file
|
||||
path: string;
|
||||
writeStream: WriteStream,
|
||||
remove: Function;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const tempFiles: ServerUpload[] = [];
|
||||
|
||||
export default router({
|
||||
async 'new'(data: NewFileRequest) {
|
||||
if(typeof data.$clientId === 'undefined') return;
|
||||
const serverId = v4();
|
||||
const temp = tmp.fileSync();
|
||||
const writeStream = createWriteStream(temp.name, 'base64');
|
||||
tempFiles.push({
|
||||
clientId: data.clientId,
|
||||
authorId: data.$clientId,
|
||||
length: data.length,
|
||||
path: temp.name,
|
||||
writeStream,
|
||||
remove: temp.removeCallback,
|
||||
serverId,
|
||||
chunk: 0,
|
||||
progress: 0,
|
||||
type: data.type,
|
||||
});
|
||||
const res: NewFileResponse = {
|
||||
serverId,
|
||||
clientId: data.clientId
|
||||
};
|
||||
return reply(res);
|
||||
},
|
||||
async 'chunk'(data: FileChunkRequest) {
|
||||
const upload = tempFiles.find(upload => upload.serverId === data.serverId);
|
||||
if(!upload) return;
|
||||
if(data.chunk !== upload.chunk) return;
|
||||
if(upload.writeStream.bytesWritten >= upload.length) return;
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
upload.writeStream.write(data.data, (err) => {
|
||||
if(err) rej(err);
|
||||
res(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// if(upload.buffer.length === upload.length) {
|
||||
// await new Promise((res, rej) => {
|
||||
// upload.writeStream.close((err) => {
|
||||
// if(err) rej(err);
|
||||
// console.log('upload file closed!');
|
||||
// res(undefined);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
upload.chunk ++;
|
||||
upload.progress = upload.writeStream.bytesWritten;
|
||||
// upload.buffer += data.data;
|
||||
|
||||
// await new Promise(res => setTimeout(res, 300));
|
||||
|
||||
const res: FileChunkResponse = {
|
||||
chunk: upload.chunk,
|
||||
serverId: upload.serverId,
|
||||
clientId: upload.clientId,
|
||||
progress: (upload.progress / upload.length)
|
||||
}
|
||||
|
||||
return reply(res);
|
||||
|
||||
},
|
||||
async 'end'(data: FileEndRequest) {
|
||||
const upload = tempFiles.find(upload => upload.serverId === data.serverId);
|
||||
if(!upload) return;
|
||||
// if(upload.buffer.length !== upload.length) return;
|
||||
await new Promise(res => upload.writeStream.close(res));
|
||||
|
||||
if(upload.length > 100_000_000) {
|
||||
const read = createReadStream(upload.path);
|
||||
const write = createFile(upload.serverId);
|
||||
const pipe = read.pipe(write);
|
||||
await new Promise((res) => {
|
||||
pipe.on('finish', () => {
|
||||
res(undefined)
|
||||
});
|
||||
});
|
||||
|
||||
await database.add.file.path(
|
||||
upload.serverId,
|
||||
upload.authorId,
|
||||
upload.type
|
||||
);
|
||||
} else {
|
||||
const file: Buffer = await new Promise((res, rej) => {
|
||||
readFile(upload.path, (err, data) => {
|
||||
if(err) rej(err);
|
||||
res(data);
|
||||
})
|
||||
});
|
||||
|
||||
await database.add.file.raw(
|
||||
upload.serverId,
|
||||
upload.authorId,
|
||||
file,
|
||||
upload.type
|
||||
);
|
||||
}
|
||||
|
||||
const res: FileEndResponse = {
|
||||
serverId: upload.serverId,
|
||||
clientId: upload.clientId,
|
||||
url: `https://dev.valnet.xyz/files/${upload.serverId}`
|
||||
}
|
||||
return reply(res);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 012345 67 0123 4567 01 234567
|
||||
// 012345 01 2345 0123 45 012345
|
||||
|
|
@ -3,13 +3,26 @@ import router from '../lib/router';
|
|||
import newMessage from '../db/snippets/message/new.sql';
|
||||
import recentMessages from '../db/snippets/message/recent.sql';
|
||||
import { broadcast, reply } from '../lib/WebSocketServer';
|
||||
import database from '../lib/dbHelpers/database';
|
||||
|
||||
function transformMessage(text: string) {
|
||||
if(text === '/shrug') {
|
||||
return '¯\\_(ツ)_/¯';
|
||||
}
|
||||
else return text;
|
||||
}
|
||||
|
||||
export default router({
|
||||
async message(data: any) {
|
||||
if(!('$clientId' in data)) {
|
||||
async message(data: NewMessageRequest) {
|
||||
if(!('$clientId' in data) || data.$clientId === undefined) {
|
||||
console.error('unauthenticated message rejected.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const file = data.file !== undefined ?
|
||||
await database.get.file.by.uid(data.file) :
|
||||
undefined;
|
||||
|
||||
const response = await query(
|
||||
newMessage,
|
||||
data.text,
|
||||
|
|
@ -17,11 +30,26 @@ export default router({
|
|||
data.uid,
|
||||
data.timestamp,
|
||||
data.channel,
|
||||
data.file ?? null
|
||||
);
|
||||
if(response === null) return;
|
||||
|
||||
data.from = data.$clientId;
|
||||
return broadcast(data);
|
||||
|
||||
const res: NewMessageResponse = {
|
||||
uid: data.uid,
|
||||
from: data.$clientId,
|
||||
text: data.text,
|
||||
timestamp: data.timestamp,
|
||||
channel: data.channel,
|
||||
}
|
||||
|
||||
if(file !== undefined) {
|
||||
res.file = {
|
||||
type: file.type,
|
||||
url: `https://dev.valnet.xyz/file/${file.uid}`
|
||||
}
|
||||
}
|
||||
|
||||
return broadcast(res);
|
||||
},
|
||||
async recent(data: any) {
|
||||
if(!('$clientId' in data)) {
|
||||
|
|
@ -30,13 +58,32 @@ export default router({
|
|||
}
|
||||
const messages = await query(recentMessages, data.channel);
|
||||
if(messages === null) return;
|
||||
|
||||
function convert(row: any) {
|
||||
if(row.file_uid === null) {
|
||||
return {
|
||||
from: row.from,
|
||||
uid: row.uid,
|
||||
timestamp: row.t_sent,
|
||||
text: row.text,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
from: row.from,
|
||||
uid: row.uid,
|
||||
timestamp: row.t_sent,
|
||||
text: row.text,
|
||||
file: {
|
||||
type: row.file_type,
|
||||
url: `https://dev.valnet.xyz/file/${row.file_uid}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reply({
|
||||
messages: messages.map(v => ({
|
||||
from: v.from,
|
||||
uid: v.uid,
|
||||
timestamp: v.t_sent,
|
||||
text: v.text,
|
||||
})),
|
||||
messages: messages.map(convert),
|
||||
channel: data.channel
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -26,7 +26,6 @@ export default router({
|
|||
err: 'Incorrect username or auth code'
|
||||
});
|
||||
const validTotp = await validateClientTotp(clientId, totp);
|
||||
console.log(username, clientId, validTotp);
|
||||
if(!validTotp) return reply({
|
||||
err: 'Incorrect username or auth code'
|
||||
});
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ export default router({
|
|||
async leave(data: any) {
|
||||
const { $clientId } = data;
|
||||
const removed = filterInPlace(participants, (v) => v.clientId !== $clientId);
|
||||
console.log('removed', removed);
|
||||
return broadcast(removed[0]);
|
||||
},
|
||||
})
|
||||
|
|
@ -34,7 +34,7 @@ const setupServerPackageWatcher = () => {
|
|||
let spawnProcess = null;
|
||||
|
||||
const processDied = () => {
|
||||
logger.error('Server has died.', {timestamp: true});
|
||||
logger.error('Server has died.', {timestamp: false});
|
||||
spawnProcess = null;
|
||||
};
|
||||
|
||||
|
|
@ -56,10 +56,14 @@ const setupServerPackageWatcher = () => {
|
|||
|
||||
spawnProcess = spawn(node, ['./index.cjs'], {
|
||||
cwd: './packages/server/dist',
|
||||
env: {
|
||||
...process.env,
|
||||
FORCE_COLOR: "true"
|
||||
}
|
||||
});
|
||||
|
||||
/** Proxy all logs */
|
||||
spawnProcess.stdout.on('data', d => d.toString().trim() && d.toString().trim().split('\n').forEach(str => logger.info(str, {timestamp: true})));
|
||||
spawnProcess.stdout.on('data', d => d.toString().trim() && d.toString().trim().split('\n').forEach(str => logger.info(str, {timestamp: false})));
|
||||
|
||||
/** Proxy error logs but stripe some noisy messages. See {@link stderrFilterPatterns} */
|
||||
spawnProcess.stderr.on('data', d => {
|
||||
|
|
@ -67,7 +71,7 @@ const setupServerPackageWatcher = () => {
|
|||
if (!data) return;
|
||||
const mayIgnore = stderrFilterPatterns.some((r) => r.test(data));
|
||||
if (mayIgnore) return;
|
||||
data.split('\n').forEach(d => logger.error(d, {timestamp: true}));
|
||||
data.split('\n').forEach(d => logger.error(d, {timestamp: false}));
|
||||
});
|
||||
|
||||
/** Stops the watch script when the application has been quit */
|
||||
|
|
@ -117,10 +121,15 @@ const setupMainPackageWatcher = ({resolvedUrls}) => {
|
|||
}
|
||||
|
||||
/** Spawn new electron process */
|
||||
spawnProcess = spawn(String(electronPath), ['.']);
|
||||
spawnProcess = spawn(String(electronPath), ['.'], {
|
||||
env: {
|
||||
...process.env,
|
||||
FORCE_COLOR: "true"
|
||||
}
|
||||
});
|
||||
|
||||
/** Proxy all logs */
|
||||
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: true}));
|
||||
spawnProcess.stdout.on('data', d => d.toString().trim() && logger.warn(d.toString(), {timestamp: false}));
|
||||
|
||||
/** Proxy error logs but stripe some noisy messages. See {@link stderrFilterPatterns} */
|
||||
spawnProcess.stderr.on('data', d => {
|
||||
|
|
@ -128,7 +137,7 @@ const setupMainPackageWatcher = ({resolvedUrls}) => {
|
|||
if (!data) return;
|
||||
const mayIgnore = stderrFilterPatterns.some((r) => r.test(data));
|
||||
if (mayIgnore) return;
|
||||
logger.error(data, {timestamp: true});
|
||||
logger.error(data, {timestamp: false});
|
||||
});
|
||||
|
||||
/** Stops the watch script when the application has been quit */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
interface NewFileRequest {
|
||||
clientId: string;
|
||||
length: number;
|
||||
name: string;
|
||||
type: string;
|
||||
$clientId?: string;
|
||||
}
|
||||
|
||||
interface NewFileResponse {
|
||||
clientId: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
interface FileChunkRequest {
|
||||
chunk: number;
|
||||
serverId: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface FileChunkResponse {
|
||||
chunk: number;
|
||||
serverId: string;
|
||||
clientId: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface FileEndRequest {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
interface FileEndResponse {
|
||||
serverId: string;
|
||||
clientId: string;
|
||||
url: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
interface NewMessageRequest {
|
||||
file?: string;
|
||||
text: string;
|
||||
$clientId?: string;
|
||||
uid: string;
|
||||
timestamp: number;
|
||||
channel: string;
|
||||
}
|
||||
|
||||
interface NewMessageResponse {
|
||||
uid: string;
|
||||
from: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
channel: string;
|
||||
file?: {
|
||||
url: string;
|
||||
type: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface RecentMessagesResponse {
|
||||
channel: string;
|
||||
}
|
||||
|
||||
interface RecentMessagesResponse {
|
||||
messages: {
|
||||
uid: string;
|
||||
from: string;
|
||||
text: string;
|
||||
timestamp: number;
|
||||
}[];
|
||||
channel: string;
|
||||
}
|
||||
Reference in New Issue