mid is really fat like if you agree
This commit is contained in:
parent
8145800120
commit
883d5ca4ec
|
@ -4,6 +4,7 @@ import { Song } from "../Schemas/Song";
|
||||||
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
||||||
import { User } from "../Schemas/User";
|
import { User } from "../Schemas/User";
|
||||||
import { Rating } from "../Schemas/Rating";
|
import { Rating } from "../Schemas/Rating";
|
||||||
|
import { DiscordRole } from "../Schemas/DiscordRole";
|
||||||
|
|
||||||
export const DBSource = new DataSource({
|
export const DBSource = new DataSource({
|
||||||
type: "better-sqlite3",
|
type: "better-sqlite3",
|
||||||
|
@ -14,7 +15,8 @@ export const DBSource = new DataSource({
|
||||||
Song,
|
Song,
|
||||||
ForcedCategory,
|
ForcedCategory,
|
||||||
User,
|
User,
|
||||||
Rating
|
Rating,
|
||||||
|
DiscordRole
|
||||||
/*join(__dirname, "..", "Schemas") + "\\*{.js,.ts}"*/ // does not work in prod
|
/*join(__dirname, "..", "Schemas") + "\\*{.js,.ts}"*/ // does not work in prod
|
||||||
],
|
],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
|
|
20
Server/Source/Handlers/DiscordBot.ts
Normal file
20
Server/Source/Handlers/DiscordBot.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { ActivityType, Client, IntentsBitField } from "discord.js";
|
||||||
|
import { Msg } from "../Modules/Logger";
|
||||||
|
import { green } from "colorette";
|
||||||
|
import { BOT_TOKEN } from "../Modules/Constants";
|
||||||
|
|
||||||
|
export const Bot: Client<true> = new Client({
|
||||||
|
intents: IntentsBitField.Flags.Guilds,
|
||||||
|
presence: {
|
||||||
|
status: "online",
|
||||||
|
activities: [
|
||||||
|
{
|
||||||
|
name: "over Partypack",
|
||||||
|
type: ActivityType.Watching
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Bot.on("ready", () => Msg(`Discord bot now ready as ${green(Bot.user.username)}${green("#")}${green(Bot.user.discriminator)}`));
|
||||||
|
Bot.login(BOT_TOKEN);
|
|
@ -1,17 +1,13 @@
|
||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
import j from "joi";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { UserPermissions } from "../Schemas/User";
|
import { UserPermissions } from "../Schemas/User";
|
||||||
import { Song, SongStatus } from "../Schemas/Song";
|
import { Song } from "../Schemas/Song";
|
||||||
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||||
import { writeFileSync } from "fs";
|
|
||||||
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
||||||
import { fromBuffer } from "file-type";
|
import { DiscordRole } from "../Schemas/DiscordRole";
|
||||||
import { Debug } from "../Modules/Logger";
|
import { Bot } from "../Handlers/DiscordBot";
|
||||||
import { magenta } from "colorette";
|
import { DISCORD_SERVER_ID } from "../Modules/Constants";
|
||||||
import ffmpeg from "fluent-ffmpeg";
|
|
||||||
import j from "joi";
|
|
||||||
import sizeOf from "image-size";
|
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
|
@ -28,6 +24,50 @@ App.use((req, res, next) => {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
App.post("/create/role",
|
||||||
|
ValidateBody(j.object({
|
||||||
|
ID: j.string().min(10).max(32).required(),
|
||||||
|
Comment: j.string().max(128).optional(),
|
||||||
|
PermissionLevel: j.number().valid(...(Object.values(UserPermissions).filter(x => !isNaN(Number(x))))).required()
|
||||||
|
})),
|
||||||
|
async (req, res) => {
|
||||||
|
if (!Bot.isReady())
|
||||||
|
return res.status(500).send("This Partypack instance has a misconfigured Discord bot.");
|
||||||
|
|
||||||
|
if (!Bot.guilds.cache.get(DISCORD_SERVER_ID as string)?.roles.cache.has(req.body.ID))
|
||||||
|
return res.status(404).send("This role does not exist in the Discord server.");
|
||||||
|
|
||||||
|
const Existing = await DiscordRole.findOne({ where: { ID: req.body.ID } });
|
||||||
|
if (Existing) {
|
||||||
|
Existing.GrantedPermissions = req.body.PermissionLevel as UserPermissions;
|
||||||
|
Existing.Comment = req.body.Comment ?? Existing.Comment;
|
||||||
|
await Existing.save();
|
||||||
|
return res.json(Existing.Package(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoleEntry = await DiscordRole.create({
|
||||||
|
ID: req.body.ID,
|
||||||
|
Comment: req.body.Comment ?? "No comment",
|
||||||
|
GrantedPermissions: req.body.PermissionLevel as UserPermissions
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
res.json(RoleEntry.Package(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
App.post("/delete/role",
|
||||||
|
ValidateBody(j.object({
|
||||||
|
ID: j.string().min(10).max(32).required()
|
||||||
|
})),
|
||||||
|
async (req, res) => {
|
||||||
|
const RoleData = await DiscordRole.findOne({ where: { ID: req.body.ID } });
|
||||||
|
if (!RoleData)
|
||||||
|
return res.status(404).send("This role does not exist in the database.");
|
||||||
|
|
||||||
|
await RoleData.remove();
|
||||||
|
res.send("Removed role successfully.");
|
||||||
|
})
|
||||||
|
|
||||||
|
App.get("/roles", async (_, res) => res.json((await DiscordRole.find()).map(x => x.Package(true))));
|
||||||
App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package(true))));
|
App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package(true))));
|
||||||
|
|
||||||
App.post("/update/discovery",
|
App.post("/update/discovery",
|
||||||
|
|
|
@ -3,13 +3,16 @@ import jwt from "jsonwebtoken";
|
||||||
import qs from "querystring";
|
import qs from "querystring";
|
||||||
import j from "joi";
|
import j from "joi";
|
||||||
import { Response, Router } from "express";
|
import { Response, Router } from "express";
|
||||||
import { BOT_TOKEN, DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_SERVER_ID, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants";
|
import { DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_SERVER_ID, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants";
|
||||||
import { User, UserPermissions } from "../Schemas/User";
|
import { User, UserPermissions } from "../Schemas/User";
|
||||||
import { ValidateQuery } from "../Modules/Middleware";
|
import { ValidateQuery } from "../Modules/Middleware";
|
||||||
import { Err } from "../Modules/Logger";
|
import { Bot } from "../Handlers/DiscordBot";
|
||||||
|
import { Debug } from "../Modules/Logger";
|
||||||
|
import { DiscordRole } from "../Schemas/DiscordRole";
|
||||||
|
import { In } from "typeorm";
|
||||||
|
import { magenta } from "colorette";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
//let DiscordServerRoleMetadata;
|
|
||||||
|
|
||||||
// ? hacky, if you want, make it less hacky
|
// ? hacky, if you want, make it less hacky
|
||||||
async function QuickRevokeToken(res: Response, Token: string) {
|
async function QuickRevokeToken(res: Response, Token: string) {
|
||||||
|
@ -17,18 +20,6 @@ async function QuickRevokeToken(res: Response, Token: string) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ReloadRoleData() {
|
|
||||||
const DRMt = await axios.get(`https://discord.com/api/guilds/${DISCORD_SERVER_ID}/roles`, { headers: { Authorization: `Bot ${BOT_TOKEN}` } });
|
|
||||||
|
|
||||||
Err(`Discord roles request failed to execute. Did you set up the .env correctly?`)
|
|
||||||
if (DRMt.status !== 200)
|
|
||||||
process.exit(-1);
|
|
||||||
|
|
||||||
//DiscordServerRoleMetadata = DRMt.data as { id: string, name: string, permissions: number }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
//ReloadRoleData();
|
|
||||||
|
|
||||||
App.get("/discord/url", (_ ,res) => res.send(`https://discord.com/api/oauth2/authorize?client_id=${qs.escape(DISCORD_CLIENT_ID!)}&response_type=code&redirect_uri=${qs.escape(`${FULL_SERVER_ROOT}/api/discord`)}&scope=identify`))
|
App.get("/discord/url", (_ ,res) => res.send(`https://discord.com/api/oauth2/authorize?client_id=${qs.escape(DISCORD_CLIENT_ID!)}&response_type=code&redirect_uri=${qs.escape(`${FULL_SERVER_ROOT}/api/discord`)}&scope=identify`))
|
||||||
|
|
||||||
App.get("/discord",
|
App.get("/discord",
|
||||||
|
@ -51,16 +42,42 @@ async (req, res) => {
|
||||||
|
|
||||||
await QuickRevokeToken(res, Discord.data.access_token);
|
await QuickRevokeToken(res, Discord.data.access_token);
|
||||||
|
|
||||||
// TODO: add discord role thingy
|
const AnyUserExists = await User.exists(); // automatically grant the first user on the database administrator permissions
|
||||||
let UserPermissionLevel = UserPermissions.User;
|
let UserPermissionLevel = !AnyUserExists ? UserPermissions.Administrator : UserPermissions.User;
|
||||||
|
|
||||||
|
if (AnyUserExists && Bot.isReady()) {
|
||||||
|
Debug("Using Discord roles to determine user permission level since the Discord bot exists and is ready.");
|
||||||
|
|
||||||
|
const Sewer = Bot.guilds.cache.get(DISCORD_SERVER_ID as string);
|
||||||
|
const Membuh = await Sewer?.members.fetch(UserData.data.id);
|
||||||
|
|
||||||
|
if (Membuh) {
|
||||||
|
const RoulInDeightabaise = await DiscordRole.find({ where: { ID: In(Membuh.roles.cache.map(x => x.id)) }, order: { GrantedPermissions: "DESC" } });
|
||||||
|
if (RoulInDeightabaise.length > 0)
|
||||||
|
UserPermissionLevel = RoulInDeightabaise[0].GrantedPermissions;
|
||||||
|
|
||||||
|
Debug(`Detected ${magenta(RoulInDeightabaise.length)} roles that override Database permissions for user ${magenta(`@${UserData.data.username}`)}. Giving permission level ${magenta(UserPermissionLevel)}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let DBUser = await User.findOne({ where: { ID: UserData.data.id } });
|
let DBUser = await User.findOne({ where: { ID: UserData.data.id } });
|
||||||
if (!DBUser)
|
if (!DBUser)
|
||||||
DBUser = await User.create({
|
DBUser = await User.create({
|
||||||
ID: UserData.data.id,
|
ID: UserData.data.id,
|
||||||
|
Username: UserData.data.username,
|
||||||
|
DisplayName: UserData.data.global_name ?? UserData.data.username,
|
||||||
|
ProfilePictureURL: `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp`,
|
||||||
Library: [],
|
Library: [],
|
||||||
PermissionLevel: UserPermissionLevel
|
PermissionLevel: UserPermissionLevel
|
||||||
}).save();
|
}).save();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DBUser.Username = UserData.data.username;
|
||||||
|
DBUser.DisplayName = UserData.data.global_name ?? UserData.data.username;
|
||||||
|
DBUser.ProfilePictureURL = `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp`;
|
||||||
|
DBUser.PermissionLevel = UserPermissionLevel;
|
||||||
|
await DBUser.save();
|
||||||
|
}
|
||||||
|
|
||||||
const JWT = jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" });
|
const JWT = jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" });
|
||||||
const UserDetails = Buffer.from(JSON.stringify({ ID: UserData.data.id, Username: UserData.data.username, GlobalName: UserData.data.global_name, Avatar: `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp`, IsAdmin: DBUser.PermissionLevel >= UserPermissions.Administrator, Role: DBUser.PermissionLevel })).toString("hex")
|
const UserDetails = Buffer.from(JSON.stringify({ ID: UserData.data.id, Username: UserData.data.username, GlobalName: UserData.data.global_name, Avatar: `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp`, IsAdmin: DBUser.PermissionLevel >= UserPermissions.Administrator, Role: DBUser.PermissionLevel })).toString("hex")
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import j from "joi";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ENVIRONMENT } from "../Modules/Constants";
|
import { ENVIRONMENT, JWT_KEY } from "../Modules/Constants";
|
||||||
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||||
import { User, UserPermissions } from "../Schemas/User";
|
import { User, UserPermissions } from "../Schemas/User";
|
||||||
import j from "joi";
|
|
||||||
import { Song } from "../Schemas/Song";
|
import { Song } from "../Schemas/Song";
|
||||||
|
import { sign } from "jsonwebtoken";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
|
@ -39,6 +40,12 @@ async (req, res) => {
|
||||||
res.json(req.user);
|
res.json(req.user);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
App.post("/create/auth",
|
||||||
|
ValidateBody(j.object({
|
||||||
|
ID: j.string().min(10).max(25).required()
|
||||||
|
})),
|
||||||
|
(req, res) => res.send(sign(req.body, JWT_KEY as string)));
|
||||||
|
|
||||||
App.get("/raw/song/:SongID",
|
App.get("/raw/song/:SongID",
|
||||||
async (req, res) => res.json(await Song.findOne({ where: { ID: req.params.SongID } })));
|
async (req, res) => res.json(await Song.findOne({ where: { ID: req.params.SongID } })));
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { UserPermissions } from "../Schemas/User";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
|
App.get("/api/download/partypacker", (_, res) => res.redirect("https://cdn.discordapp.com/attachments/1202728144935583804/1203083689840607252/Partypacker_OT2.zip"))
|
||||||
|
|
||||||
App.get("/song/download/:InternalID/:File",
|
App.get("/song/download/:InternalID/:File",
|
||||||
RequireAuthentication(),
|
RequireAuthentication(),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
|
@ -60,7 +62,7 @@ async (req, res) => {
|
||||||
if (!/^[\w\-.]+$/g.test(req.params.File))
|
if (!/^[\w\-.]+$/g.test(req.params.File))
|
||||||
return res.status(400).send("File name failed validation.");
|
return res.status(400).send("File name failed validation.");
|
||||||
|
|
||||||
if (!req.params.File.endsWith(".m4s"))
|
if (!req.params.File.endsWith(".m4s") && !req.params.File.endsWith(".webm"))
|
||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
|
|
||||||
if (!existsSync(`${SongData.Directory}/Chunks/${req.params.File}`))
|
if (!existsSync(`${SongData.Directory}/Chunks/${req.params.File}`))
|
||||||
|
|
|
@ -199,7 +199,8 @@ App.post("/upload/audio",
|
||||||
.audioCodec("libopus")
|
.audioCodec("libopus")
|
||||||
.outputOptions([
|
.outputOptions([
|
||||||
"-use_timeline 1",
|
"-use_timeline 1",
|
||||||
"-f dash"
|
"-f dash",
|
||||||
|
"-mapping_family 255"
|
||||||
])
|
])
|
||||||
.output(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`)
|
.output(`${SAVED_DATA_PATH}/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`)
|
||||||
.on("start", cl => Debug(`ffmpeg running with ${magenta(cl)}`))
|
.on("start", cl => Debug(`ffmpeg running with ${magenta(cl)}`))
|
||||||
|
|
21
Server/Source/Schemas/DiscordRole.ts
Normal file
21
Server/Source/Schemas/DiscordRole.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
|
import { UserPermissions } from "./User";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class DiscordRole extends BaseEntity {
|
||||||
|
@PrimaryColumn()
|
||||||
|
ID: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
GrantedPermissions: UserPermissions;
|
||||||
|
|
||||||
|
@Column({ default: "No comment" })
|
||||||
|
Comment: string;
|
||||||
|
|
||||||
|
public Package(IncludeComment: boolean) {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
Comment: IncludeComment ? this.Comment : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -119,7 +119,7 @@ export class Song extends BaseEntity {
|
||||||
return {
|
return {
|
||||||
...this,
|
...this,
|
||||||
Status: IncludeStatus ? this.Status : SongStatus.DEFAULT,
|
Status: IncludeStatus ? this.Status : SongStatus.DEFAULT,
|
||||||
Author: this.Author ? this.Author.ID : undefined,
|
Author: this.Author ? this.Author.Package() : undefined,
|
||||||
Directory: undefined, // we should NOT reveal that
|
Directory: undefined, // we should NOT reveal that
|
||||||
Midi: this.Midi ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/midi.mid`,
|
Midi: this.Midi ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/midi.mid`,
|
||||||
Cover: this.Cover ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/cover.png`
|
Cover: this.Cover ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/cover.png`
|
||||||
|
|
|
@ -39,4 +39,14 @@ export class User extends BaseEntity {
|
||||||
@ManyToMany(() => Song, { eager: true })
|
@ManyToMany(() => Song, { eager: true })
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
BookmarkedSongs: Song[];
|
BookmarkedSongs: Song[];
|
||||||
|
|
||||||
|
public Package(IncludeRatings: boolean = false, IncludeCreatedTracks: boolean = false) {
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
Ratings: IncludeRatings ? this.Ratings : undefined,
|
||||||
|
Library: undefined,
|
||||||
|
CreatedTracks: IncludeCreatedTracks ? this.CreatedTracks : undefined,
|
||||||
|
BookmarkedSongs: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ config();
|
||||||
|
|
||||||
import "./Handlers/Database";
|
import "./Handlers/Database";
|
||||||
import "./Handlers/Server";
|
import "./Handlers/Server";
|
||||||
|
import "./Handlers/DiscordBot";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Welcome to Mc's BasedServer template! (v1.1 - 30.12.2023 update)
|
Welcome to Mc's BasedServer template! (v1.1 - 30.12.2023 update)
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
"build:prod": "vite build",
|
"build:prod": "vite build",
|
||||||
"build:stage": "vite build --mode staging",
|
"build:stage": "vite build --mode staging",
|
||||||
"win:create:prod": "mkdir \"./Out\" ; vite build ; move \"./dist\" \"./Out/dist\" ; cd \"Server\" ; tsc ; cd .. ; copy \"./Server/.env.prod\" \"./Out/.env\" ; copy \"./Server/package.json\" \"./Out/package.json\" ; copy \"./Server/package-lock.json\" \"./Out/package-lock.json\"",
|
"win:create:prod": "mkdir \"./Out\" ; vite build ; move \"./dist\" \"./Out/dist\" ; cd \"Server\" ; tsc ; cd .. ; copy \"./Server/.env.prod\" \"./Out/.env\" ; copy \"./Server/package.json\" \"./Out/package.json\" ; copy \"./Server/package-lock.json\" \"./Out/package-lock.json\"",
|
||||||
"win:publish:prod": "npm run win:create:prod ; ssh partypack \"cd /home/PartypackProd; rm -rf ./Out\" ; scp -r \"./Out\" partypack:/home/PartypackProd ; ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" ; rmdir \"./Out\"",
|
"win:publish:prod": "npm run win:create:prod ; ssh partypack \"cd /home/PartypackProd && rm -rf ./Out\" ; scp -r \"./Out\" partypack:/home/PartypackProd ; ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" ; rmdir \"./Out\"",
|
||||||
"win:create:stage": "mkdir \"./Out\" && vite build --mode staging && move \"./dist\" \"./Out/dist\" && cd \"Server\" && tsc && cd .. && copy \"./Server/.env.staging\" \"./Out/.env\" && copy \"./Server/package.json\" \"./Out/package.json\" && copy \"./Server/package-lock.json\" \"./Out/package-lock.json\"",
|
"win:create:stage": "mkdir \"./Out\" ; vite build --mode staging ; move \"./dist\" \"./Out/dist\" ; cd \"Server\" ; tsc ; cd .. ; copy \"./Server/.env.staging\" \"./Out/.env\" ; copy \"./Server/package.json\" \"./Out/package.json\" ; copy \"./Server/package-lock.json\" \"./Out/package-lock.json\"",
|
||||||
"win:publish:stage": "npm run win:create:stage && ssh partypack \"cd /home/PartypackStage; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackStage && ssh partypack \"cd /home/PartypackStage/Out && npm i && pm2 restart PartypackStage --update-env\" && rmdir \"./Out\"",
|
"win:publish:stage": "mkdir \"./Out\" ; vite build --mode staging ; move \"./dist\" \"./Out/dist\" ; cd \"Server\" ; tsc ; cd .. ; copy \"./Server/.env.staging\" \"./Out/.env\" ; copy \"./Server/package.json\" \"./Out/package.json\" ; copy \"./Server/package-lock.json\" \"./Out/package-lock.json\" ; ssh partypack \"cd /home/PartypackStage && rm -rf ./Out\" ; scp -r \"./Out\" partypack:/home/PartypackStage ; ssh partypack \"cd /home/PartypackStage/Out && npm i && pm2 restart PartypackStage --update-env\" ; rmdir \"./Out\"",
|
||||||
"create:prod": "mkdir ./Out && npm run build:prod && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.prod ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
"create:prod": "mkdir ./Out && npm run build:prod && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.prod ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
||||||
"publish:prod": "npm run create:prod && ssh partypack \"cd /home/PartypackProd; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackProd && ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" && rm -rf ./Out",
|
"publish:prod": "npm run create:prod && ssh partypack \"cd /home/PartypackProd; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackProd && ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" && rm -rf ./Out",
|
||||||
"create:stage": "mkdir ./Out && npm run build:stage && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.staging ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
"create:stage": "mkdir ./Out && npm run build:stage && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.staging ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Tracks } from "./routes/Tracks";
|
||||||
import { TrackSubmission } from "./routes/TrackSubmission";
|
import { TrackSubmission } from "./routes/TrackSubmission";
|
||||||
import { Profile } from "./routes/Profile";
|
import { Profile } from "./routes/Profile";
|
||||||
import { NotFound } from "./routes/404";
|
import { NotFound } from "./routes/404";
|
||||||
|
import { Credits } from "./routes/Credits";
|
||||||
import { AdminHome } from "./routes/AdminHome";
|
import { AdminHome } from "./routes/AdminHome";
|
||||||
import { AdminTrackList } from "./routes/AdminTrackList";
|
import { AdminTrackList } from "./routes/AdminTrackList";
|
||||||
import { AdminSubmissions } from "./routes/AdminSubmissions";
|
import { AdminSubmissions } from "./routes/AdminSubmissions";
|
||||||
|
@ -23,6 +24,7 @@ import merge from "deepmerge";
|
||||||
|
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "./css/index.css";
|
import "./css/index.css";
|
||||||
|
import { FrequentlyAskedQuestions } from "./routes/FrequentlyAskedQuestions";
|
||||||
|
|
||||||
const DefaultTheme = merge(theme, {}); // we'll use this!! eventually!!!
|
const DefaultTheme = merge(theme, {}); // we'll use this!! eventually!!!
|
||||||
|
|
||||||
|
@ -43,9 +45,11 @@ function App() {
|
||||||
{/* User-accessible routes */}
|
{/* User-accessible routes */}
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/download" element={<Download />} />
|
<Route path="/download" element={<Download />} />
|
||||||
|
<Route path="/faq" element={<FrequentlyAskedQuestions />} />
|
||||||
<Route path="/tracks" element={<Tracks />} />
|
<Route path="/tracks" element={<Tracks />} />
|
||||||
<Route path="/submissions" element={<TrackSubmission />} />
|
<Route path="/submissions" element={<TrackSubmission />} />
|
||||||
<Route path="/profile" element={<Profile />} />
|
<Route path="/profile" element={<Profile />} />
|
||||||
|
<Route path="/credits" element={<Credits />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|
||||||
{/* Staff routes */}
|
{/* Staff routes */}
|
||||||
|
|
19
src/routes/Credits.tsx
Normal file
19
src/routes/Credits.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Heading } from "@primer/react";
|
||||||
|
|
||||||
|
export function Credits() {
|
||||||
|
return (
|
||||||
|
<center>
|
||||||
|
<Heading>Partypack</Heading>
|
||||||
|
<h4>Created by</h4>
|
||||||
|
<a href="https://twitter.com/McMistrzYT">McMistrzYT</a><br />
|
||||||
|
<h4>Additional help from</h4>
|
||||||
|
<a href="https://twitter.com/KruzShady">shady</a><br />
|
||||||
|
<a href="https://twitter.com/AveryMadness">AveryMadness</a><br />
|
||||||
|
<a href="https://twitter.com/NotJulesDev">YLS-Dev</a><br />
|
||||||
|
<a href="https://twitter.com/samuels1v">Samuel</a><br />
|
||||||
|
<a href="https://github.com/McMistrzYT/Partypack/graphs/contributors">Contributors on GitHub</a>
|
||||||
|
<h4>Special thanks</h4>
|
||||||
|
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Bryan Griffin</a><br />
|
||||||
|
</center>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,6 +1,13 @@
|
||||||
|
import { Button, Heading, Text } from "@primer/react";
|
||||||
|
|
||||||
export function Download() {
|
export function Download() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<center>
|
||||||
|
<Heading>You're one step away from experiencing peak Fortnite Festival</Heading>
|
||||||
|
<Text>Click the button below to download <b>Partypacker</b> to launch into Fortnite with custom Partypack songs!</Text>
|
||||||
|
<Button onClick={() => window.open((import.meta.env.VITE_SERVER_ROOT_URL ?? "http://localhost:6677/") + "api/download/partypacker")}>Download Partypacker</Button>
|
||||||
|
</center>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
46
src/routes/FrequentlyAskedQuestions.tsx
Normal file
46
src/routes/FrequentlyAskedQuestions.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { Box, Heading, Text } from "@primer/react";
|
||||||
|
|
||||||
|
const FAQ: { Q: string, A: string }[] = [
|
||||||
|
{
|
||||||
|
Q: "Can custom jam tracks get me banned?",
|
||||||
|
A: "No, custom songs do not interfere with Fortnite's anti-cheat and will NOT get you banned."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: "How do custom jam tracks work?",
|
||||||
|
A: "Using our custom launcher, Partypacker, you are able to redirect Fortnite's requests to the Partypack backend, which modifies Fortnite's tracklist. This allows us to replace existing jam tracks with completely custom jam tracks which include metadata, music, and charts."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: "Can I play custom jam tracks with friends?",
|
||||||
|
A: "Yes, you can! ...but they have to activate the exact same songs, replacing the exact same songs as you."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: "Will stats save for custom jam tracks?",
|
||||||
|
A: "Unfortunately, stats and leaderboards will not work for custom songs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: "Can I play jam tracks aleady inside Fortnite for free as a custom jam track?",
|
||||||
|
A: "No, this is not currently allowed and will never be allowed on this instance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: "Do you have to chart every instrument on every difficulty for the custom jam track to work?",
|
||||||
|
A: "No, you can chart as little as you want (or none at all) and the jam track will still load just fine. However, there currently is no way to disable a specific instrument or difficulty on a jam track."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Q: "Does this have a token logger or spyware hidden inside?",
|
||||||
|
A: "No! All our source code used for the server and launcher are available on McMistrzYT's GitHub. (links available on the home page)"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function FrequentlyAskedQuestions() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading>Frequently Asked Questions</Heading>
|
||||||
|
{
|
||||||
|
FAQ.map(x => <>
|
||||||
|
<Text><b>Q:</b> {x.Q}</Text><br />
|
||||||
|
<Text><b>A:</b> {x.A}</Text><br /><br />
|
||||||
|
</>)
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +1,31 @@
|
||||||
import { Box, Heading, Text } from "@primer/react";
|
import { Box, Heading, Text } from "@primer/react";
|
||||||
|
import { SignInIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<Heading>PARTYPACK - Placeholder Place Logo Here</Heading>
|
<Heading>Welcome to Partypack</Heading>
|
||||||
<Text>Welcome to Partypack! blah blah someone please make this text for me im way too lazy to do allat</Text>
|
<Text>
|
||||||
|
Welcome to Partypack, the ultimate game-changing experience for Fortnite Festival! Partypack brings custom songs into the game, letting players turn it into a community driven experience!
|
||||||
|
<br />
|
||||||
|
Here, you can view tracks by other people or submit your own, read tutorials and look at the FAQ for important topics. Be sure to also read the quickstart guide to know how to get started!
|
||||||
|
<br />
|
||||||
|
Partypack was created by <a href="/credits">everyone listed here</a>. If you're interested in hosting your own instance of Partypack, be sure to check the <a href="https://github.com/McMistrzYT/Partypack">GitHub repo</a>.</Text>
|
||||||
<Heading>Quickstart Guide</Heading>
|
<Heading>Quickstart Guide</Heading>
|
||||||
<Text>1. Kill yourself<br />2. Do it again</Text>
|
<Text>
|
||||||
|
<b>Consider watching the easier to understand, visual guide available <a href="/tutorials/Quickstart.mp4">here</a>.</b><br />
|
||||||
|
1. Join this instance's <a href="https://discord.gg/Rhd9Hq4D62">Discord server</a><br />
|
||||||
|
2. Click on the <SignInIcon size={16} /> icon in the top right<br />
|
||||||
|
3. Log in using your Discord account<br />
|
||||||
|
4. Go to <b>Tracks</b> and subscribe to tracks you like<br />
|
||||||
|
5. Click on your profile picture in the top right to access your <b>Profile</b> page<br />
|
||||||
|
6. <b>Activate</b> songs by replacing original Fortnite songs<br />
|
||||||
|
7. <b>Download</b> the Partypacker Launcher from the <b>Download</b> tab<br />
|
||||||
|
8. Run the Partypacker application, log in using your Discord account and press Launch<br />
|
||||||
|
9. If any warnings pop up, press YES on all of them for the proxy to work correctly<br />
|
||||||
|
10. Enter a Festival Main Stage match and try out your custom songs!
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, FormControl, Heading, Text, TextInput } from "@primer/react"
|
import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, FormControl, Heading, Label, Text, TextInput } from "@primer/react"
|
||||||
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
|
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
|
||||||
import { PageHeader } from "@primer/react/drafts";
|
import { PageHeader } from "@primer/react/drafts";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
@ -8,7 +8,8 @@ import { SiteContext } from "../utils/State";
|
||||||
import { useCookies } from "react-cookie";
|
import { useCookies } from "react-cookie";
|
||||||
import { Song } from "../components/Song";
|
import { Song } from "../components/Song";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { SongStatus } from "../utils/Extensions";
|
import { SongStatus, UserPermissions } from "../utils/Extensions";
|
||||||
|
import { LabelColorOptions } from "@primer/react/lib-esm/Label/Label";
|
||||||
|
|
||||||
const formControlStyle = { paddingTop: 3 };
|
const formControlStyle = { paddingTop: 3 };
|
||||||
|
|
||||||
|
@ -17,6 +18,8 @@ export function Profile() {
|
||||||
const { state, setState } = useContext(SiteContext);
|
const { state, setState } = useContext(SiteContext);
|
||||||
const [, , removeCookie] = useCookies();
|
const [, , removeCookie] = useCookies();
|
||||||
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState<boolean>(false);
|
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState<boolean>(false);
|
||||||
|
const [variant, setVariant] = useState<LabelColorOptions>("success");
|
||||||
|
const [labelText, setLabelText] = useState<string>("");
|
||||||
const [librarySongs, setLibrarySongs] = useState<unknown[]>([]);
|
const [librarySongs, setLibrarySongs] = useState<unknown[]>([]);
|
||||||
const [bookmarkedSongs, setBookmarkedSongs] = useState<unknown[]>([]);
|
const [bookmarkedSongs, setBookmarkedSongs] = useState<unknown[]>([]);
|
||||||
const [draftsSongs, setDraftsSongs] = useState<unknown[]>([]);
|
const [draftsSongs, setDraftsSongs] = useState<unknown[]>([]);
|
||||||
|
@ -25,6 +28,44 @@ export function Profile() {
|
||||||
const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState<boolean>(false);
|
const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState<boolean>(false);
|
||||||
const [updating, setUpdating] = useState<unknown>({});
|
const [updating, setUpdating] = useState<unknown>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.UserDetails === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let Variant: LabelColorOptions = "default";
|
||||||
|
let LabelText: string = "";
|
||||||
|
|
||||||
|
switch (state.UserDetails.Role) {
|
||||||
|
case UserPermissions.User:
|
||||||
|
Variant = "secondary";
|
||||||
|
LabelText = "User";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserPermissions.VerifiedUser:
|
||||||
|
Variant = "success";
|
||||||
|
LabelText = "Verified Track Creator";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserPermissions.TrackVerifier:
|
||||||
|
Variant = "done";
|
||||||
|
LabelText = "Track Verifier";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserPermissions.Moderator:
|
||||||
|
Variant = "accent";
|
||||||
|
LabelText = "Moderator";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserPermissions.Administrator:
|
||||||
|
Variant = "danger";
|
||||||
|
LabelText = "Administrator";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVariant(Variant);
|
||||||
|
setLabelText(LabelText);
|
||||||
|
}, [state.UserDetails?.Role])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const Data = await axios.get("/api/library/me");
|
const Data = await axios.get("/api/library/me");
|
||||||
|
@ -63,6 +104,7 @@ export function Profile() {
|
||||||
</PageHeader.LeadingVisual>
|
</PageHeader.LeadingVisual>
|
||||||
<PageHeader.Title>
|
<PageHeader.Title>
|
||||||
{state.UserDetails.GlobalName} (@{state.UserDetails.Username})
|
{state.UserDetails.GlobalName} (@{state.UserDetails.Username})
|
||||||
|
<Label sx={{ alignSelf: "center", marginLeft: 2 }} size="large" variant={variant}>{labelText}</Label>
|
||||||
</PageHeader.Title>
|
</PageHeader.Title>
|
||||||
<PageHeader.Actions>
|
<PageHeader.Actions>
|
||||||
<Button size="large" variant="danger" onClick={() => { removeCookie("UserDetails"); removeCookie("Token"); setState({ ...state, UserDetails: null }); window.location.assign("/") }}>Log out</Button>
|
<Button size="large" variant="danger" onClick={() => { removeCookie("UserDetails"); removeCookie("Token"); setState({ ...state, UserDetails: null }); window.location.assign("/") }}>Log out</Button>
|
||||||
|
|
|
@ -9,8 +9,8 @@ const formControlStyle = { paddingTop: 3 };
|
||||||
export function TrackSubmission() {
|
export function TrackSubmission() {
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const [waiting, setWaiting] = useState<boolean>(false);
|
const [waiting, setWaiting] = useState<boolean>(false);
|
||||||
const [Key, setKey] = useState<string>("Select a key...");
|
const [Key, setKey] = useState<string>("Select key...");
|
||||||
const [Scale, setScale] = useState<string>("Select a scale...");
|
const [Scale, setScale] = useState<string>("Select mode...");
|
||||||
const [GuitarStarterType, setGuitarStarterType] = useState<string>("Select the starter type...");
|
const [GuitarStarterType, setGuitarStarterType] = useState<string>("Select the starter type...");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -61,7 +61,7 @@ export function TrackSubmission() {
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl required={true} sx={formControlStyle}>
|
<FormControl required={true} sx={formControlStyle}>
|
||||||
<FormControl.Label>Scale</FormControl.Label>
|
<FormControl.Label>Mode</FormControl.Label>
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
<ActionMenu.Button>{Scale}</ActionMenu.Button>
|
<ActionMenu.Button>{Scale}</ActionMenu.Button>
|
||||||
<ActionMenu.Overlay width="medium">
|
<ActionMenu.Overlay width="medium">
|
||||||
|
@ -126,7 +126,7 @@ export function TrackSubmission() {
|
||||||
console.log(formRef);
|
console.log(formRef);
|
||||||
|
|
||||||
if (formRef.current == null)
|
if (formRef.current == null)
|
||||||
return;
|
return setWaiting(false);
|
||||||
|
|
||||||
const Name = (formRef.current[0] as HTMLInputElement).value;
|
const Name = (formRef.current[0] as HTMLInputElement).value;
|
||||||
const ArtistName = (formRef.current[1] as HTMLInputElement).value;
|
const ArtistName = (formRef.current[1] as HTMLInputElement).value;
|
||||||
|
@ -162,14 +162,17 @@ export function TrackSubmission() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Object.values(B).includes(NaN) || Object.values(B).includes(null) || Object.values(B).includes(undefined))
|
if (Object.values(B).includes(NaN) || Object.values(B).includes(null) || Object.values(B).includes(undefined))
|
||||||
|
{
|
||||||
|
setWaiting(false);
|
||||||
return toast("One or more required fields missing.", { type: "error" });
|
return toast("One or more required fields missing.", { type: "error" });
|
||||||
|
}
|
||||||
|
|
||||||
const SongData = await axios.post("/api/drafts/create", B);
|
const SongData = await axios.post("/api/drafts/create", B);
|
||||||
|
|
||||||
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
|
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
|
||||||
|
|
||||||
if (SongData.status !== 200)
|
if (SongData.status !== 200)
|
||||||
return;
|
return setWaiting(false);
|
||||||
|
|
||||||
const MidiRes = await axios.post("/api/drafts/upload/midi", { Data: Buffer.from(await Midi.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
|
const MidiRes = await axios.post("/api/drafts/upload/midi", { Data: Buffer.from(await Midi.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
|
||||||
toast(MidiRes.status === 200 ? "Uploaded MIDI chart successfully." : MidiRes.data, { type: MidiRes.status === 200 ? "success" : "error" });
|
toast(MidiRes.status === 200 ? "Uploaded MIDI chart successfully." : MidiRes.data, { type: MidiRes.status === 200 ? "success" : "error" });
|
||||||
|
|
Loading…
Reference in New Issue
Block a user