-
Notifications
You must be signed in to change notification settings - Fork 3
Web #53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
danny8376
wants to merge
17
commits into
james58899:master
Choose a base branch
from
danny8376:web
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Web #53
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
3f9997f
supress tslint/tsc error
danny8376 08622c9
add web backend
danny8376 03a05ee
build
danny8376 c0cb589
add list delAudio api
danny8376 fb98668
get auth token from header instead
danny8376 46ff8a5
arrange owned lists first
danny8376 422a206
list audios with queried audio data
danny8376 fd57095
fix tg digest check
danny8376 1e1adf6
upload audio with uri
danny8376 5d4d2f8
add front-end
danny8376 5d9cf2e
use vue prod js instead
danny8376 a56eabb
build with prod instead
danny8376 ac90857
fix as ts types updated
danny8376 5418d9a
move title & bot name into config.json
danny8376 701e076
fix UI problem that user able to upload to other's list
danny8376 ddb15c3
fix conflict with current master
danny8376 6bcb39a
Merge branch 'master' into web
james58899 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,349 @@ | ||
| import crypto from "crypto"; | ||
| import { Application, NextFunction, Request, Response } from "express"; | ||
| import express from "express"; | ||
| import { promises as fsp } from "fs"; | ||
| import { ObjectID } from "mongodb"; | ||
| import multer from "multer"; | ||
| import { Core } from ".."; | ||
| import { AudioManager, ERR_MISSING_TITLE, ERR_NOT_AUDIO, IAudioData } from "../Core/AudioManager"; | ||
| import { IAudioList, ListManager } from "../Core/ListManager"; | ||
| import { IUserData, UserManager } from "../Core/UserManager"; | ||
| // import { retry, sleep } from "../Core/Utils/PromiseUtils"; | ||
|
|
||
| export const BIND_TYPE = "telegram"; // "web"; | ||
| const ERR_MISSING_TOKEN = Error("Telegram bot api token not found!"); | ||
| // const ERR_NOT_VALID_TITLE = Error("Not valid title"); | ||
| // const ERR_LIST_NOT_FOUND = Error("Playlist not found"); | ||
| const ERR_BAD_REQUEST = "Maybe wrong paramters!"; | ||
| const ERR_FORBIDDEN = "You're not allowed to do this!"; | ||
| const ERR_NOT_REGISTER = "Please use /register to register or bind account via Telegram!"; | ||
| const ERR_NOT_FOUND = "File disappeared!"; | ||
|
|
||
| export class Web { | ||
| private audio: AudioManager; | ||
| private user: UserManager; | ||
| private list: ListManager; | ||
| private digest: Buffer; | ||
| private upload: string; | ||
| private webConfig: any; | ||
| private server: Application; | ||
|
|
||
| constructor(core: Core) { | ||
| if (!core.config.telegram.token) throw ERR_MISSING_TOKEN; | ||
|
|
||
| this.user = core.userManager; | ||
| this.audio = core.audioManager; | ||
| this.list = core.listManager; | ||
|
|
||
| this.digest = crypto.createHash("sha256").update(core.config.telegram.token).digest(); | ||
| this.upload = core.config.web.upload; | ||
| this.webConfig = { | ||
| tgBotName: core.config.telegram.botname, | ||
| title: core.config.web.title, | ||
| }; | ||
|
|
||
| // Create Server | ||
| this.server = express(); | ||
|
|
||
| this.middlewares(); | ||
| this.registerRoutes(); | ||
| this.errorHandler(); | ||
|
|
||
| this.server.listen(8081); | ||
| } | ||
|
|
||
| private async middlewares() { | ||
| this.server.use(express.json()); // for parsing application/json | ||
| } | ||
|
|
||
| private async errorHandler() { | ||
| this.server.use((err: Error, req: Request, res: Response, next: NextFunction) => { | ||
| if (err.message.startsWith("HTTP400")) { | ||
| res.status(400).json({ | ||
| error: ERR_BAD_REQUEST | ||
| }); | ||
| } else if (err.message.startsWith("HTTP401")) { | ||
| res.status(401).json({ | ||
| error: ERR_NOT_REGISTER | ||
| }); | ||
| } else if (err.message.startsWith("HTTP403")) { | ||
| res.status(403).json({ | ||
| error: ERR_FORBIDDEN | ||
| }); | ||
| } else if (err.message.startsWith("HTTP404")) { | ||
| res.status(404).json({ | ||
| error: ERR_NOT_FOUND | ||
| }); | ||
| } else { | ||
| next(err); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| private route(fn: (req: Request, res: Response, next: NextFunction) => Promise<void> | void) { | ||
| return (req: Request, res: Response, next: NextFunction) => { | ||
| const promise = fn.bind(this)(req, res, next); | ||
| if (promise instanceof Promise) { | ||
| promise.catch(next); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| private async registerRoutes() { | ||
| const upload = multer({ dest: this.upload }); | ||
|
|
||
| this.server.get("/", (req: Request, res: Response) => res.send("MusicBot Web Server")); | ||
| this.server.get("/config", this.route(this.getConfig)); | ||
| this.server.get("/login", this.route(this.getLogin)); | ||
| this.server.get("/lists", this.route(this.getLists)); | ||
| this.server.post("/lists", this.route(this.postLists)); | ||
| this.server.get("/list/:lid", this.route(this.getList)); | ||
| this.server.patch("/list/:lid", this.route(this.patchList)); | ||
| this.server.delete("/list/:lid", this.route(this.deleteList)); | ||
| this.server.get("/list/:lid/audios", this.route(this.getListAudios)); | ||
| this.server.post("/list/:lid/audios", upload.array("audio"), this.route(this.postListAudios)); | ||
| this.server.delete("/list/:lid/audio/:aid", this.route(this.deleteListAudio)); | ||
| this.server.get("/audio/:aid", this.route(this.getAudio)); | ||
| this.server.get("/audio/:aid/file", this.route(this.getAudioFile)); | ||
| } | ||
|
|
||
| private async getConfig(req: Request, res: Response) { | ||
| res.json(this.webConfig); | ||
| } | ||
|
|
||
| private async getLogin(req: Request, res: Response) { | ||
| const user = await this.checkUser(req); | ||
| res.json({ | ||
| msg: "OK", | ||
| user | ||
| }); | ||
| } | ||
|
|
||
| private async getLists(req: Request, res: Response) { | ||
| const user = await this.getUser(req); | ||
| const lists = await this.list.getAll().map((list: IAudioList) => { | ||
| return { | ||
| id: list._id, | ||
| name: list.name, | ||
| own: !!user && user._id!.equals(list.owner) | ||
| }; | ||
| }).toArray(); | ||
| const own: any[] = []; | ||
| const other: any[] = []; | ||
| lists.forEach(list => { | ||
| if (list.own) { | ||
| own.push(list); | ||
| } else { | ||
| other.push(list); | ||
| } | ||
| }); | ||
| res.json({ | ||
| lists: [...own, ...other], | ||
| msg: "OK" | ||
| }); | ||
| } | ||
|
|
||
| private async postLists(req: Request, res: Response) { | ||
| const user = await this.checkUser(req); | ||
| const name = (req as any).body.name; | ||
| if (!name) { | ||
| throw Error("HTTP400"); | ||
| } | ||
| const list = await this.list.create(name, user._id!); | ||
| res.json({ | ||
| list, | ||
| msg: "OK" | ||
| }); | ||
| } | ||
|
|
||
| private async getList(req: Request, res: Response) { | ||
| const list = await this.list.get(new ObjectID(req.params.lid)); | ||
| res.json({ | ||
| list, | ||
| msg: "OK" | ||
| }); | ||
| } | ||
|
|
||
| private async patchList(req: Request, res: Response) { | ||
| const user = await this.checkUser(req); | ||
| const list = await this.list.get(new ObjectID(req.params.lid)); | ||
| const name = (req as any).body.name; | ||
| if (!name) { | ||
| throw Error("HTTP400"); | ||
| } | ||
| if (!list) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| if (!user._id!.equals(list.owner)) { | ||
| throw Error("HTTP403"); | ||
| } | ||
| const result = await this.list.rename(list._id, name); | ||
| res.json({ | ||
| msg: "OK", | ||
| result | ||
| }); | ||
| } | ||
|
|
||
| private async deleteList(req: Request, res: Response) { | ||
| const user = await this.checkUser(req); | ||
| const list = await this.list.get(new ObjectID(req.params.lid)); | ||
| if (!list) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| if (!user._id!.equals(list.owner)) { | ||
| throw Error("HTTP403"); | ||
| } | ||
| const result = await this.list.delete(list._id); | ||
| res.json({ | ||
| msg: "OK", | ||
| result | ||
| }); | ||
| } | ||
|
|
||
| private async getListAudios(req: Request, res: Response) { | ||
| const list = await this.list.get(new ObjectID(req.params.lid)); | ||
| if (!list) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| const audios = await this.audio.search({ | ||
| _id: { | ||
| $in: list.audio.map(id => new ObjectID(id)) | ||
| } | ||
| }).toArray(); | ||
| res.json({ | ||
| audios, | ||
| msg: "OK" | ||
| }); | ||
| } | ||
|
|
||
| private async postListAudios(req: Request, res: Response) { | ||
| try { | ||
| const user = await this.checkUser(req); | ||
| const list = await this.list.get(new ObjectID(req.params.lid)); | ||
| if (!list) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| if (!user._id!.equals(list.owner)) { | ||
| throw Error("HTTP403"); | ||
| } | ||
| const audios: IAudioData[] = []; | ||
| const paths: string[] = []; | ||
| if ((req as any).files) { | ||
| (req as any).files.forEach((file: any) => { | ||
| paths.push(file.path); | ||
| }); | ||
| } | ||
| if ((req as any).body) { | ||
| const body = (req as any).body; | ||
| if (body.uris) { | ||
| if (typeof body.uris === "string") { | ||
| paths.push(...JSON.parse(body.uris)); | ||
| } else { | ||
| paths.push(...body.uris); | ||
| } | ||
| } | ||
| } | ||
| await Promise.all(paths.map(async path => { | ||
| const audio = await this.processFile(path, user); | ||
| if (audio) { | ||
| await this.list.addAudio(list._id!, audio._id!); | ||
| audios.push(audio); | ||
| } else { | ||
| // failed | ||
| } | ||
| })); | ||
| res.json({ | ||
| audios, | ||
| msg: "OK" | ||
| }); | ||
| } finally { | ||
| if ((req as any).files) { | ||
| await Promise.all((req as any).files.map(async (file: any) => await fsp.unlink(file.path))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private async deleteListAudio(req: Request, res: Response) { | ||
| const user = await this.checkUser(req); | ||
| const list = await this.list.get(new ObjectID(req.params.lid)); | ||
| if (!list) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| if (!user._id!.equals(list.owner)) { | ||
| throw Error("HTTP403"); | ||
| } | ||
| const result = await this.list.delAudio(list._id, new ObjectID(req.params.aid)); | ||
| res.json({ | ||
| msg: "OK", | ||
| result | ||
| }); | ||
| } | ||
|
|
||
| private async getAudio(req: Request, res: Response) { | ||
| const audio = await this.audio.get(new ObjectID(req.params.aid)); | ||
| res.json({ | ||
| audio, | ||
| msg: "OK" | ||
| }); | ||
| } | ||
|
|
||
| private async getAudioFile(req: Request, res: Response) { | ||
| const audio = await this.audio.get(new ObjectID(req.params.aid)); | ||
| if (!audio) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| const file = await this.audio.getFile(audio); | ||
| if (!file) { | ||
| throw Error("HTTP404"); | ||
| } | ||
| res.download(file); | ||
| } | ||
|
|
||
| private async getUser(req: Request) { | ||
| const tgStr = req.get("X-Auth"); | ||
| if (!tgStr) return null; | ||
| const tg = JSON.parse(tgStr); | ||
| const hash = tg.hash; | ||
| delete tg.hash; | ||
| const payload = Object.keys(tg).sort().map(key => { | ||
| return `${key}=${tg[key]}`; | ||
| }).join("\n"); | ||
| const hmac = crypto.createHmac("sha256", this.digest); | ||
| hmac.update(payload); | ||
| if (hmac.digest("hex") !== hash) { | ||
| return null; | ||
| } | ||
| return this.user.getFromBind(BIND_TYPE, tg.id); | ||
| } | ||
|
|
||
| private async checkUser(req: Request) { | ||
| const user = await this.getUser(req); | ||
| if (!user) { | ||
| throw Error("HTTP401"); | ||
| } | ||
| return user; | ||
| } | ||
|
|
||
| private async processFile(file: string, sender: IUserData) { | ||
| if (!file) return null; | ||
|
|
||
| let audio; | ||
| try { | ||
| audio = await this.audio.add(sender._id!, file); | ||
| } catch (error) { | ||
| if (error === ERR_MISSING_TITLE) { | ||
| // TODO show error | ||
| return null; | ||
| } else if (error === ERR_NOT_AUDIO) { | ||
| // not audio file | ||
| return null; | ||
| } else { | ||
| // unkown dead | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| return audio; | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
config