diff --git a/package.json b/package.json index 92b1f18ef..12d8222e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "teledrive", - "version": "0.1.0", + "version": "1.0.0", "repository": "git@github.com:mgilangjanuar/teledrive.git", "author": "M Gilang Januar ", "license": "MIT", diff --git a/press/(EN) Press Release TeleDrive.pdf b/press/(EN) Press Release_ TeleDrive.pdf similarity index 96% rename from press/(EN) Press Release TeleDrive.pdf rename to press/(EN) Press Release_ TeleDrive.pdf index d9fedd714..148f750a7 100644 Binary files a/press/(EN) Press Release TeleDrive.pdf and b/press/(EN) Press Release_ TeleDrive.pdf differ diff --git a/press/(ID) Press Release TeleDrive.pdf b/press/(ID) Press Release_ TeleDrive.pdf similarity index 99% rename from press/(ID) Press Release TeleDrive.pdf rename to press/(ID) Press Release_ TeleDrive.pdf index d659be60a..4ac989886 100644 Binary files a/press/(ID) Press Release TeleDrive.pdf and b/press/(ID) Press Release_ TeleDrive.pdf differ diff --git a/server/package.json b/server/package.json index fb4900332..ca6fa8191 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.1.0", + "version": "1.0.0", "main": "dist/index.js", "license": "MIT", "private": true, @@ -9,7 +9,7 @@ "build": "rimraf dist && eslint --fix -c .eslintrc.js --ext .ts . && tsc" }, "dependencies": { - "@mgilangjanuar/telegram": "2.0.9", + "@mgilangjanuar/telegram": "2.0.11", "@sentry/node": "^6.14.1", "@sentry/tracing": "^6.14.1", "@types/moment": "^2.13.0", @@ -76,4 +76,4 @@ "rimraf": "^3.0.2", "typescript": "^4.4.2" } -} +} \ No newline at end of file diff --git a/server/src/api/v1/Files.ts b/server/src/api/v1/Files.ts index ed00eb772..5feecd4fc 100644 --- a/server/src/api/v1/Files.ts +++ b/server/src/api/v1/Files.ts @@ -203,6 +203,10 @@ export class Files { key = AES.encrypt(JSON.stringify({ file: { id: file.id }, session: req.tg.session.save() }), process.env.FILES_JWT_SECRET).toString() } + if (!file.sharing_options?.length && !currentFile.sharing_options?.length) { + key = null + } + const { affected } = await Model.createQueryBuilder('files') .update({ ...file.name ? { name: file.name } : {}, diff --git a/server/src/api/v1/Messages.ts b/server/src/api/v1/Messages.ts index f99f9d935..308767d65 100644 --- a/server/src/api/v1/Messages.ts +++ b/server/src/api/v1/Messages.ts @@ -37,6 +37,47 @@ export class Messages { return res.send({ messages: result }) } + @Endpoint.GET('/sponsoredMessages/:type/:id', { middlewares: [Auth] }) + public async sponsoredMessages(req: Request, res: Response): Promise { + const { type, id } = req.params + const { accessHash } = req.query + + let peer: Api.InputPeerChannel + if (type === 'channel') { + peer = new Api.InputPeerChannel({ + channelId: bigInt(id), + accessHash: bigInt(accessHash as string) }) + } else { + return res.send({ messages: { + messages: [], + chats: [], + users: [] + } }) + } + const messages = await req.tg.invoke(new Api.channels.GetSponsoredMessages({ channel: peer })) + return res.send({ messages }) + } + + @Endpoint.POST('/readSponsoredMessages/:type/:id', { middlewares: [Auth] }) + public async readSponsoredMessages(req: Request, res: Response): Promise { + const { type, id } = req.params + const { accessHash } = req.query + const { random_id: randomId } = req.body + + let peer: Api.InputPeerChannel + if (type === 'channel') { + peer = new Api.InputPeerChannel({ + channelId: bigInt(id), + accessHash: bigInt(accessHash as string) }) + } else { + return res.status(202).send({ accepted: true }) + } + const accepted = await req.tg.invoke(new Api.channels.ViewSponsoredMessage({ + channel: peer, randomId: Buffer.from(randomId) + })) + return res.status(202).send({ accepted }) + } + @Endpoint.POST('/read/:type/:id', { middlewares: [Auth] }) public async read(req: Request, res: Response): Promise { const { type, id } = req.params @@ -155,7 +196,7 @@ export class Messages { @Endpoint.POST('/forward/:msgId', { middlewares: [Auth] }) public async forward(req: Request, res: Response): Promise { const { msgId } = req.params - const { from, to } = req.body as { from: { + const { from, to } = req.body as { from?: { type: string, id: number, accessHash?: string @@ -163,11 +204,13 @@ export class Messages { type: string, id: number, accessHash?: string - } } + } | string } - let fromPeer: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat - let toPeer: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat - if (from.type === 'channel') { + let fromPeer: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat | 'me' + let toPeer: Api.InputPeerChannel | Api.InputPeerUser | Api.InputPeerChat | string + if (!from) { + fromPeer = 'me' + } else if (from.type === 'channel') { fromPeer = new Api.InputPeerChannel({ channelId: bigInt(from.id), accessHash: bigInt(from.accessHash as string) }) @@ -181,7 +224,9 @@ export class Messages { accessHash: bigInt(from.accessHash as string) }) } - if (to.type === 'channel') { + if (typeof to === 'string') { + toPeer = to + } else if (to.type === 'channel') { toPeer = new Api.InputPeerChannel({ channelId: bigInt(to.id), accessHash: bigInt(to.accessHash as string) }) diff --git a/upgrade.js b/upgrade.js new file mode 100644 index 000000000..def6fe546 --- /dev/null +++ b/upgrade.js @@ -0,0 +1,22 @@ +const fs = require('fs') +const { execSync } = require('child_process') + +const root = fs.readFileSync('./package.json', 'utf-8') +const rootObj = JSON.parse(root) +rootObj.version = process.argv[2] +fs.writeFileSync('./package.json', JSON.stringify(rootObj, null, 2)) + +const api = fs.readFileSync('./server/package.json', 'utf-8') +const apiObj = JSON.parse(api) +apiObj.version = process.argv[2] +fs.writeFileSync('./server/package.json', JSON.stringify(apiObj, null, 2)) +// execSync('cd ./server && yarn install && cd ..') + +const web = fs.readFileSync('./web/package.json', 'utf-8') +const webObj = JSON.parse(web) +webObj.version = process.argv[2] +fs.writeFileSync('./web/package.json', JSON.stringify(webObj, null, 2)) +// execSync('cd ./web && yarn install && cd ..') + +execSync('yarn install && yarn workspaces run build') +execSync(`git add . && git commit -m "${process.argv[2]}"`) \ No newline at end of file diff --git a/web/package.json b/web/package.json index 1b8ea14b9..439518751 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.1.0", + "version": "1.0.0", "private": true, "dependencies": { "@craco/craco": "^6.3.0", @@ -87,4 +87,4 @@ "workbox-strategies": "^5.1.3", "workbox-streams": "^5.1.3" } -} +} \ No newline at end of file diff --git a/web/src/pages/dashboard/components/Messaging.tsx b/web/src/pages/dashboard/components/Messaging.tsx index ecebb87c5..c58c37344 100644 --- a/web/src/pages/dashboard/components/Messaging.tsx +++ b/web/src/pages/dashboard/components/Messaging.tsx @@ -120,7 +120,7 @@ const Messaging: React.FC = ({ me, collapsed, parent, setCollapsed }) => }, [messageHistory]) useEffect(() => { - const setDataMessages = (dialog?: any) => { + const setDataMessages = (dialog?: any, sponsoredMessages?: { messages: any[], chats: any[], users: any[] }) => { setMessagesParsed(messages?.messages.reduce((res: any[], msg: any) => { let user = messages?.users.find((user: any) => user.id === (msg.fromId || msg.peerId)?.userId) if (!user) { @@ -197,16 +197,41 @@ const Messaging: React.FC = ({ me, collapsed, parent, setCollapsed }) => } : undefined } : null ] - }, []).filter(Boolean).sort((a: any, b: any) => a.date - b.date) || []) + }, sponsoredMessages?.messages?.map((msg: any) => { + let user = sponsoredMessages?.users.find((user: any) => user.id === (msg.fromId || msg.peerId)?.userId) + if (!user) { + user = sponsoredMessages?.chats.find((user: any) => user.id === (msg.fromId || msg.peerId)?.channelId) + } + return { + id: `${message?.id.replace(/\?.*$/gi, '')}/sponsor`, + messageId: message?.id, + key: 'sponsor', + position: 'left', + type: 'text', + // status: me?.user.tg_id == user?.id ? msg.id <= dialog?.dialog?.readOutboxMaxId ? 'read' : 'received' : undefined, + title: user ? user.title || `${user.firstName || ''} ${user.lastName || ''}`.trim() : 'Unknown', + text: {msg.message ? `${msg.message.replaceAll('\n', ' \n')}\n\n_(sponsored message)_` : 'Unknown message'}, + message: msg.message, + fwdFrom: msg.fwdFrom, + date: new Date().getTime(), + titleColor: `#${`${user?.id.toString(16)}000000`.slice(0, 6)}`, + user + } + }) || []).filter(Boolean).sort((a: any, b: any) => a.date - b.date) || []) // messageList.current?.scrollToRow = 50 } if (message) { req.get(`/dialogs/${message.id}`).then(({ data }) => { - setDataMessages(data.dialog) - // const sidebar = document.querySelector('.ant-layout-sider.ant-layout-sider-light.messaging') - // if (sidebar) { - // sidebar.scroll({ top: sidebar.scrollHeight, behavior: 'smooth' }) - // } + req.get(`/messages/sponsoredMessages/${message.id}`).then(({ data: sponsoredData }) => { + setDataMessages(data.dialog, sponsoredData?.messages) + sponsoredData.messages?.messages.map((msg: any) => req.post(`/messages/readSponsoredMessages/${message.id}`, { random_id: msg.randomId?.data })) + // const sidebar = document.querySelector('.ant-layout-sider.ant-layout-sider-light.messaging') + // if (sidebar) { + // sidebar.scroll({ top: sidebar.scrollHeight, behavior: 'smooth' }) + // } + }).catch(_ => { + setDataMessages(data.dialog) + }) }) } else { setDataMessages() diff --git a/web/src/pages/dashboard/components/Share.tsx b/web/src/pages/dashboard/components/Share.tsx index 2f3468380..602d09b4f 100644 --- a/web/src/pages/dashboard/components/Share.tsx +++ b/web/src/pages/dashboard/components/Share.tsx @@ -1,4 +1,4 @@ -import { CopyOutlined, InfoCircleOutlined, LinkOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons' +import { ArrowRightOutlined, CopyOutlined, InfoCircleOutlined, LinkOutlined, MinusCircleOutlined, PlusOutlined, WarningOutlined } from '@ant-design/icons' import { AutoComplete, Button, Col, Divider, Empty, Form, Input, message, Modal, notification, Row, Spin, Switch, Typography } from 'antd' import { useForm } from 'antd/lib/form/Form' import * as clipboardy from 'clipboardy' @@ -10,7 +10,7 @@ interface Props { me: any, dataSource?: [any[], (data: any[]) => void], onFinish?: () => void, - dataSelect: [any, (data: any) => void] + dataSelect: [{ row: any, action: string }, (data?: { row: any, action: string }) => void] } const Share: React.FC = ({ @@ -41,15 +41,16 @@ const Share: React.FC = ({ useEffect(() => { if (selectShare) { - const isPublic = (selectShare.sharing_options || [])?.includes('*') + const isPublic = (selectShare.row.sharing_options || [])?.includes('*') setIsPublic(isPublic) - setSharingOptions(selectShare.sharing_options) + setSharingOptions(selectShare.row.sharing_options) formShare.setFieldsValue({ - id: selectShare.id, + id: selectShare.row.id, message: 'Hey, please check this out! 👆', public: isPublic, - sharing_options: selectShare.sharing_options?.length ? selectShare.sharing_options.filter((opt: string) => opt !== '*') : [''], - link: selectShare.type === 'folder' ? `${window.location.origin}/dashboard/shared?parent=${selectShare.id}` : `${window.location.origin}/view/${selectShare.id}` + sharing_options: selectShare.row.sharing_options?.length ? selectShare.row.sharing_options.filter((opt: string) => opt !== '*') : [''], + link: selectShare.row.type === 'folder' ? `${window.location.origin}/dashboard/shared?parent=${selectShare.row.id}` : `${window.location.origin}/view/${selectShare.row.id}`, + username: null }) } else { formShare.resetFields() @@ -58,17 +59,36 @@ const Share: React.FC = ({ const share = async () => { setLoadingShare(true) - const { id, public: isPublic, sharing_options: sharingOpts } = formShare.getFieldsValue() + const { id, public: isPublic, sharing_options: sharingOpts, username } = formShare.getFieldsValue() - const sharing = [ + const sharing = sharingOpts?.length ? [ ...new Set([...sharingOpts === undefined ? sharingOptions : sharingOpts, isPublic ? '*' : null] .filter(sh => isPublic ? sh : sh !== '*').filter(Boolean)) as any - ] + ] : [] setSharingOptions(sharing) try { - await req.patch(`/files/${id}`, { file: { sharing_options: sharing } }) - dataSource?.[1](dataSource?.[0].map(file => file.id === id ? { ...file, sharing_options: sharing } : file)) + if (selectShare?.action === 'share') { + await req.patch(`/files/${id}`, { file: { sharing_options: sharing } }) + dataSource?.[1](dataSource?.[0].map(file => file.id === id ? { ...file, sharing_options: sharing } : file)) + } else { + const [type, peerId, _id, accessHash] = selectShare.row.forward_info?.split('/') || [null, null, null, null] + await req.post(`/messages/forward/${selectShare.row.message_id}`, { + ...selectShare.row.forward_info ? { + from: { + id: peerId, + type, + accessHash + } + } : {}, + to: username + }) + notification.success({ + message: 'Success', + description: `${selectShare?.row.name} sent to @${username} successfully` + }) + formShare.setFieldsValue({ username: null }) + } } catch (error: any) { if (error?.response?.status === 402) { notification.error({ @@ -77,6 +97,7 @@ const Share: React.FC = ({ }) setSelectShare(undefined) } + setLoadingShare(false) } setLoadingShare(false) onFinish?.() @@ -87,21 +108,21 @@ const Share: React.FC = ({ return message.info('Copied!') } - return setSelectShare(undefined)} footer={null} - title={`Share ${selectShare?.name}`}> -
+ title={`${selectShare?.action === 'share' ? 'Share' : 'Send'} ${selectShare?.row.name}`}> + - {selectShare?.type !== 'folder' ? + {selectShare?.row.type !== 'folder' && selectShare?.action === 'share' ? { setIsPublic(val) share() }} /> : ''} - {!isPublic && + {!isPublic && selectShare?.action === 'share' && {(fields, { add, remove }) => <> {fields.map((field, i) => @@ -126,16 +147,33 @@ const Share: React.FC = ({ } } + {selectShare?.action === 'forward' && <> + + } options={users?.map((user: any) => ({ value: user.username }))}> + setUsername(e.target.value)} /> + + + + + + } + {selectShare?.action === 'share' && + Your encrypted session will be saved for downloading this file + } - - You are shared {isPublic ? 'with anyone.' : - `with ${formShare.getFieldValue('sharing_options')?.[0] || 'no one'} - ${formShare.getFieldValue('sharing_options')?.filter(Boolean).length > 1 ? ` and ${formShare.getFieldValue('sharing_options')?.filter(Boolean).length - 1} people` : ''}`} - - {sharingOptions?.[0] ?  Share URL} name="link"> - } onSearch={copy} /> - : ''} + {selectShare?.action === 'share' ? <> + + You are shared {isPublic ? 'with anyone.' : + `with ${formShare.getFieldValue('sharing_options')?.[0] || 'no one'} + ${formShare.getFieldValue('sharing_options')?.filter(Boolean).length > 1 ? ` and ${formShare.getFieldValue('sharing_options')?.filter(Boolean).length - 1} people` : ''}`} + + {sharingOptions?.[0] ?  Share URL} name="link"> + } onSearch={copy} /> + : ''} + : + You will send this file to the user directly + }
diff --git a/web/src/pages/dashboard/components/TableFiles.tsx b/web/src/pages/dashboard/components/TableFiles.tsx index 98ff6af47..de5cb5615 100644 --- a/web/src/pages/dashboard/components/TableFiles.tsx +++ b/web/src/pages/dashboard/components/TableFiles.tsx @@ -12,6 +12,7 @@ import { GlobalOutlined, ScissorOutlined, ShareAltOutlined, + ArrowRightOutlined, SnippetsOutlined, TeamOutlined, VideoCameraOutlined @@ -29,7 +30,7 @@ interface Props { onChange: (...args: any[]) => void, onDelete: (row: any) => void, onRename: (row: any) => void, - onShare: (row: any) => void, + onShare: (row: any, action: string) => void, onRowClick: (row: any) => void, onCut?: (row: any) => void, onCopy?: (row: any) => void, @@ -107,7 +108,11 @@ const TableFiles: React.FC = ({ } key="share" - onClick={() => onShare(popup?.row)}>Share + onClick={() => onShare(popup?.row, 'share')}>Share + {popup?.row.type !== 'folder' ? } + key="send" + onClick={() => onShare(popup?.row, 'forward')}>Send to : ''} {popup?.row.type !== 'folder' ? } key="download" diff --git a/web/src/pages/dashboard/index.tsx b/web/src/pages/dashboard/index.tsx index 2b7b08771..d0ca3ef9a 100644 --- a/web/src/pages/dashboard/index.tsx +++ b/web/src/pages/dashboard/index.tsx @@ -356,9 +356,9 @@ const Dashboard: React.FC = ({ match }) => { setSelected([row]) setFileRename(row) }} - onShare={row => { + onShare={(row, action) => { setSelected([row]) - setSelectShare(row) + setSelectShare({ row, action }) }} onRowClick={row => { if (row.type === 'folder') { diff --git a/yarn.lock b/yarn.lock index 6146b9816..664bc4cde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1544,10 +1544,10 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@mgilangjanuar/telegram@2.0.9": - version "2.0.9" - resolved "https://npm.pkg.github.com/download/@mgilangjanuar/telegram/2.0.9/7de66d2ca37cf1bed8f889157d8e6c07cc53fc6477a2cfcf6e68ee7c839c0514#0087d944d7d1ed8bd4db9e580d43148d8580a7f0" - integrity sha512-COHrncMWDFdmUhWQUAODs7AtcSPw9ngMXzULdmf9cYlxDrX+mOh8boyl2js/nIdRl2U2hKxq9HwmVz13DBDovw== +"@mgilangjanuar/telegram@2.0.11": + version "2.0.11" + resolved "https://npm.pkg.github.com/download/@mgilangjanuar/telegram/2.0.11/2b252136afa7cdbda3db412e39793132577da961dfc2f652ca59a2cfe88f1151#3f80099404c5c62bd5bcd30b0a2c5f4aec4a71d8" + integrity sha512-g/OnlhULvwUenJYrJzk/NUywA/QEa5UgPLdHbFtFIemXpbx+ULuR+kOI8HR9Yf7jJH3VXA63zMHr/vrn6RTvGg== dependencies: "@cryptography/aes" "^0.1.1" async-mutex "^0.3.0"