This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

1
resources/app/dist/server/error.mjs vendored Normal file
View File

@@ -0,0 +1 @@
export default class ServerError extends Error{constructor(s,r){super(s),r&&(this.stack=r)}toJSON(){return{class:this.constructor.name,message:this.message,stack:this.stack}}}

1
resources/app/dist/server/express.mjs vendored Normal file

File diff suppressed because one or more lines are too long

1
resources/app/dist/server/sockets.mjs vendored Normal file
View File

@@ -0,0 +1 @@
import ServerError from"./error.mjs";import Activity from"../components/activity.mjs";import Files from"../files/files.mjs";import sessions from"../sessions.mjs";import ProseMirrorAuthority from"../components/prosemirror.mjs";import Document from"../../common/abstract/document.mjs";import{isSubclass}from"../../common/utils/helpers.mjs";export async function handleEvent(e,t,o,s){const r=this.user;if(!r)throw new Error(`Unrecognized User attacked to socket ${this.id} for event ${e}`);let i;o.options=o.options||{};let n={userId:r?.id,request:o};try{n.result=await t(r,o),i=o.broadcast??!0}catch(e){e=new ServerError(e.message,e.stack),logger.error(e),n.error=e,i=!1}return s?(s(n),i&&this.broadcast.emit(e,n)):i&&this.server.emit(e,n),n}export function handleCustomSocket(e,t,{recipients:o}={},s){if(o instanceof Array)for(let s of o){const o=game.users.find((e=>e.id===s));if(o)for(let s of o.sockets)s.emit(e,t,this.user.id)}else this.broadcast.emit(e,t,this.user.id);s instanceof Function&&s()}export function handleMigrateDocumentData(e,t,o){const s=global.db[e];if(isSubclass(s,Document))if("object"==typeof t)try{o({source:s.fromImport(t).toObject()})}catch(e){o({error:e.message})}else o({error:'Invalid Document data provided to "migrateDocumentData" operation'});else o({error:`Invalid Document name "${e}" provided to "migrateDocumentData" operation`})}export async function handleConfirmTeleportToken({behaviorUuid:e,tokenUuid:t,userId:o},s){if(!this.user.isGM)return s(!1);const r=game.users.find((e=>e.id===o));if(!r)return s(!1);s(await new Promise((o=>r.sockets.forEach((s=>s.emit("confirmTeleportToken",{behaviorUuid:e,tokenUuid:t},o))))))}export async function activate(e,t){const o=e.handshake.query,s={sessionId:null,userId:null},r=sessions.sessions.get(o.session);if(!r)return e.emit("session",null);if(e.session=r,s.sessionId=r.id,game.world&&game.ready){const t=r.worlds[game.world.id],o=game.users.find((e=>e.id===t));o?(e.user=o,e.userId=s.userId=o.id):delete r.worlds[game.world.id]}e.emit("session",s);for(const o of t)o.socket&&e.on(o.socket,(e=>o.handleSocket(r,e)));e.on("getWorldStatus",requestWorldState),Files.socketListeners(e,handleEvent),s.userId&&(Activity.socketListeners(e),e.on("chatBubble",handleCustomSocket.bind(e,"chatBubble")),e.on("av",handleCustomSocket.bind(e,"av")),ProseMirrorAuthority.socketListeners(e),db.DatabaseBackend.socketListeners(e),db.Scene.socketListeners(e),db.JournalEntry.socketListeners(e),db.Playlist.socketListeners(e),db.FogExploration.socketListeners(e),db.Actor.socketListeners(e),db.Region.socketListeners(e),packages.World.socketListeners(e,handleEvent),e.on("migrateDocumentData",handleMigrateDocumentData),e.on("confirmTeleportToken",handleConfirmTeleportToken.bind(e)))}function requestWorldState(e){return e(game.world&&game.ready)}

1
resources/app/dist/server/upnp.mjs vendored Normal file
View File

@@ -0,0 +1 @@
import natupnp from"nat-upnp";export default class UPnP{constructor({port:t=3e4,ttl:e}={}){this.port=t,this.ttl=e||600}client=natupnp.createClient();#t;createMapping(){global.logger.info(`Requesting UPnP port forwarding to destination port ${this.port}`);try{this.#e()}catch(t){return t.message=`Failed to created requested UPnP mapping: ${t.message}`,global.logger.error(t),null}return this.#t=setInterval(this.#e.bind(this),1e3*this.ttl/2),this}#e(){config.options?.debug&&global.logger.debug(`Renewing UPnP port forwarding lease to destination port ${this.port}`),this.client.portMapping({public:this.port,private:this.port,ttl:this.ttl,description:"Foundry Virtual Tabletop"})}removeMapping(){this.#t&&(clearInterval(this.#t),this.#t=void 0),this.client.portUnmapping({public:this.port})}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";import*as util from"../../../common/utils/helpers.mjs";export default class APIView extends View{route="/api/status";_methods=["get"];async handleGet(e,s){const{game:t,release:i}=globalThis,r={active:!1,version:i?.version};t.ready&&util.mergeObject(r,{active:!0,world:t.world.id,system:t.system.id,systemVersion:t.system.version,users:Object.values(t.activity.users).length,uptime:t.activity.serverTime}),s.json(r)}}

View File

@@ -0,0 +1 @@
import Express from"../express.mjs";import sessions from"../../sessions.mjs";import*as util from"../../../common/utils/helpers.mjs";import{Module}from"../../packages/_module.mjs";import View from"./view.mjs";export default class AuthView extends View{route="/auth";socket="getAuthData";_template="auth";_methods=["get","post"];async handleGet(e,s){if(config.license.needsSignature)return s.redirect(`${e.baseUrl}/license`);if(global.game.world)return s.redirect(`${e.baseUrl}/join`);const t=View._getStaticContent({setup:!0}),o={bodyClass:`auth flexcol theme-${config.options.cssTheme}`,messages:sessions.getMessages(e,{clear:!1}),layout:"setup",scripts:t.scripts,styles:t.styles};s.render(this._template,o)}async handlePost(e,s){const{game:t}=global,o=sessions.authenticateAdmin(e,s),a=!t.world&&o.success;return s.redirect(e.baseUrl+util.getRoute(a?"setup":"auth"))}async handleSocket(e,s){return s({release:global.release,worlds:[],systems:[],modules:Module.getPackages({coreTranslation:!0}).map((e=>e.vend())),options:{language:global.config.options.language}})}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";export default class ErrorView extends View{route="/no";_methods=["get"];async handleGet(e,r,...o){View.error(e,r,...o)}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";export default class GameView extends View{route="/game";_template="game";_methods=["get"];async handleGet(e,r){const{db:t,game:o,logger:s}=global;if(!o.world)return this._noWorld(e,r);if(!e.user)return r.redirect(`${e.baseUrl}/join`);if(!await t.User.get(e.user))return s.error(Error(`User ${e.user} not found!`)),r.redirect(`${e.baseUrl}/join`);const i=await t.Setting.getValue("core.moduleConfiguration")||{},a=View._getStaticContent({world:!0,moduleConfig:i});r.render(this._template,{scripts:a.scripts,styles:a.styles,watermark:null})}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";import sessions from"../../sessions.mjs";import{Module}from"../../packages/_module.mjs";import{PASSWORD_SAFE_STRING}from"../../../common/constants.mjs";import Express from"../express.mjs";export default class JoinView extends View{route="/join";socket="getJoinData";_template="join";_methods=["get","post"];async handleGet(e,s){const{game:o}=global;if(!o.world)return this._noWorld(e,s);if(!o.ready)return setTimeout((()=>this.handleGet(e,s)),1e3);await sessions.logoutWorld(e,s);const t=o.world.background,a=t?`body.background {\n --background-url: url("${URL.parseSafe(t)?t:foundry.utils.getRoute(t,{prefix:express.routePrefix})}");\n }`:"",n=View._getStaticContent({setup:!0});return s.render(this._template,{bodyClass:["auth","join","flexcol",t?"background":"",`theme-${config.options.cssTheme}`,`join-theme-${o.world.joinTheme??"default"}`].filterJoin(" "),messages:sessions.getMessages(e,{clear:!1}),pageTitle:o.world.title,layout:"setup",scripts:n.scripts,styles:n.styles,inlineStyles:a})}async handlePost(e,s){const{config:o,game:t}=global;if(!t.world)return this._noWorld(e,s);let a={};switch(e.body.action){case"shutdown":if(o.options.demoMode)return s.status(401),s.send("This option is not available for servers running in demo mode"),s;if(!o.adminPassword)return s.status(403),s.send("ERROR.InvalidAdminKey"),s;if(!sessions.authenticateAdmin(e,s).success)return s.send(e.session.messages.pop()?.message),e.session.messages=[],s;a=await t.world.deactivate(e,{asAdmin:!0}),a.status="success",a.message="The game world has been successfully deactivated";break;case"join":if(await sessions.logoutWorld(e,s),a=await sessions.authenticateUser(e,s),"failed"===a.status)return s}return s.json(a)}async handleSocket(e,s){const{game:o}=global;return o.world?s({release:global.release,world:o.world.vend(),modules:Module.getPackages({coreTranslation:!0}).map((e=>e.vend())),passwordString:PASSWORD_SAFE_STRING,isAdmin:e.admin,users:await db.User.dump(),activeUsers:Array.from(Object.keys(o.activity.users)),userId:e.worlds[o.world.id]||null,options:{language:global.config.options.language}}):s({})}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";import sessions from"../../sessions.mjs";export default class LicenseView extends View{route="/license";_template="license";_methods=["get","post"];async handleGet(e,s){const t=global.config.license;if(!t.needsSignature)return s.redirect("/");const n=t.license?"eula":"key";return this.#e(e,s,n)}async handlePost(e,s){const{config:t}=global,n=t.license;if(!n.needsSignature)return s.redirect("/");let i=n.license?"eula":"key";if("key"===i)try{n.applyLicense(e.body.licenseKey),sessions.setMessage(e,s,"info","License key entered, please sign the End User License Agreement."),i="eula"}catch(t){sessions.setMessage(e,s,"error",t.message)}else{"decline"in e.body&&(n.applyLicense(null),t.app?t.app.quit():process.exit());const i=await n.sign();if("success"===i.status)return sessions.setMessage(e,s,"info",i.message),s.redirect(`${e.baseUrl}/setup`);if("error"===i.status)return sessions.setMessage(e,s,"error",i.message),n.applyLicense(null),s.redirect(`${e.baseUrl}/license`)}return this.#e(e,s,i)}#e(e,s,t){const n=View._getStaticContent({setup:!0});return s.render(this._template,{layout:"setup",bodyClass:"auth flexcol",scripts:n.scripts,styles:n.styles,isEULA:"eula"===t,licenseKey:e.body.licenseKey,messages:sessions.getMessages(e,{clear:!1}),step:t})}}

View File

@@ -0,0 +1 @@
import sessions from"../../sessions.mjs";import{Module}from"../../packages/_module.mjs";import{PASSWORD_SAFE_STRING,USER_ROLES}from"../../../common/constants.mjs";import View from"./view.mjs";export default class PlayersView extends View{route="/players";socket="getPlayersData";_template="players";_methods=["get"];async handleGet(e,s){const{db:t,game:r}=global;if(!r.world)return s.redirect(`${e.baseUrl}/setup`),!1;const a=await t.User.getUsers();if(!PlayersView.#e(a,e.user))return s.redirect(`${e.baseUrl}/join`),!1;const o=r.world.background,i=o?`body.background {\n --background-url: url("${URL.parseSafe(o)?o:foundry.utils.getRoute(o,{prefix:express.routePrefix})}");\n }`:"",l=View._getStaticContent({setup:!0});s.render(this._template,{layout:"setup",bodyClass:["auth","players","flexcol",o?"background":"",`theme-${config.options.cssTheme}`].filterJoin(" "),pageTitle:r.world.title,messages:sessions.getMessages(e,{clear:!1}),scripts:l.scripts,styles:l.styles,inlineStyles:i})}async handleSocket(e,s){if(!game.world||!game.ready)return s({});const t=e.worlds[game.world.id]||null;return e.admin||PlayersView.#e(game.users,t)?s({modules:Module.getPackages({coreTranslation:!0}).map((e=>e.vend())),options:{language:global.config.options.language},passwordString:PASSWORD_SAFE_STRING,release:global.release,settings:await db.Setting.dump(),userId:t,users:await db.User.dump()}):s({})}static#e(e,s){return!e.some((e=>e.hasRole(USER_ROLES.GAMEMASTER)))||!!s&&(e.find((e=>e._id===s))?.isGM??!1)}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";import sessions from"../../sessions.mjs";export default class QuitView extends View{route="/quit";_methods=["post"];async handlePost(s,e){const{config:t}=global;if(!sessions.authenticateAdmin(s,e).success)return e.send({status:"failed"});e.send({status:"failed"}),t.app?t.app.quit():process.exit()}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import GameView from"./game.mjs";export default class StreamView extends GameView{route="/stream";_template="stream"}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";import sessions from"../../sessions.mjs";import{Module}from"../../packages/_module.mjs";import*as packages from"../../packages/views.mjs";export default class ApplicationUpdateView extends View{route="/update";socket="getUpdateData";_template="update";_methods=["get","post"];async handleGet(e,s){if(config.license.needsSignature)return s.redirect(`${e.baseUrl}/license`);if(global.game.world)return e.user?s.redirect(`${e.baseUrl}/game`):s.redirect(`${e.baseUrl}/join`);if(!sessions.authenticateAdmin(e,s).success)return s.redirect(`${e.baseUrl}/auth`);const a=View._getStaticContent({setup:!0}),t={layout:"setup",bodyClass:`update auth flexcol theme-${config.options.cssTheme}`,messages:sessions.getMessages(e,{clear:!1}),requireAuth:!1,scripts:a.scripts,styles:a.styles};s.render(this._template,t)}async handlePost(e,s){let a={};const{config:t,game:r}=global,{action:o,...i}=e.body,c=sessions.authenticateAdmin(e,s);if(!(!r.world&&c.success))return s.status(403),s.json({error:"You lack server administrator permission to submit this request."});switch(o){case"updateCheck":try{a=await t.updater.check(i),a||(a={info:"SETUP.UpdateNotAvailable"})}catch(e){const{message:s,stack:t,messageCode:r}=e;a={error:s,stack:t,messageCode:r}}break;case"updateDownload":try{a=await t.updater.update()}catch(e){a={error:e.message,stack:e.stack}}break;case"createSnapshot":a=packages.handleCreateSnapshot(e.body);break;case"checkCreateSnapshotDiskSpace":a=await global.packages.backups.checkCreateSnapshotDiskSpace();break;case"previewCompatibility":a=await packages.handlePreviewCompatibility(e.body);break;default:a={error:`Unsupported ApplicationUpdateView action "${o}" submitted`}}return s.json(a)}async handleSocket(e,s){const a=global.config;if(a.adminPassword&&!e.admin||game.world)return s({});return s({options:a.options.vend(),release:global.release,addresses:a.express.getInvitationLinks(),coreUpdate:await a.updater.checkCoreUpdateAvailability(),worlds:[],systems:[],modules:Module.getPackages({coreTranslation:!0}).map((e=>e.vend()))})}}

View File

@@ -0,0 +1 @@
import View from"./view.mjs";import Files from"../../files/files.mjs";import{hasFileExtension}from"../../../common/data/validators.mjs";import{handleDocumentAssetUpload}from"../../database/sanitization.mjs";export default class FileUploadView extends View{route="/upload";_methods=["post"];async handlePost(e,s){const{game:o,config:a}=global;let t=null;if(!(e.session.admin||!o.ready&&!a.adminPassword)&&(o.ready&&(t=await db.User.get(e.user)),!t||!t.hasPermission("FILES_UPLOAD")))throw new Error(`User ${n.userId} does not have permission to upload files.`);let i=e.body.source;const r=e.files.upload,n={...e.body};if(n.uuid){if(!hasFileExtension(r.name,Object.keys(CONST.IMAGE_FILE_EXTENSIONS)))return s.json({error:"Not an image file."});i="data",n.target=await handleDocumentAssetUpload(n.uuid,r)}const d=await Files.upload(i,r,n).catch((e=>({error:e.message||e})));return s.json(d)}}

View File

@@ -0,0 +1 @@
import Express from"../express.mjs";import{Module}from"../../packages/_module.mjs";export default class View{route;socket=!1;_template;_methods=[];get hasGet(){return this._methods.includes("get")}get hasPost(){return this._methods.includes("post")}async handleGet(e,s){}async handlePost(e,s){}async handleSocket(e,s){}_noWorld(e,s){return View.error(e,s,{pageTitle:"No Active Game",message:"There is currently no active game session. Please wait for the host to configure the world and then refresh this page."})}static error(e,s,...t){const r=t.pop()||global.fatalError,o=this._getStaticContent({setup:!0});s.render("error",{layout:"setup",bodyClass:["auth","error","flexcol",`theme-${config.options.cssTheme}`].join(" "),pageTitle:r.title||"Critical Failure!",message:r.message||"Something went wrong with the Foundry Virtual Tabletop server.",setupUrl:foundry.utils.getRoute("setup",{prefix:express.routePrefix}),stack:r.stack||null,styles:o.styles})}static home(e,s){const{config:t,game:r}=global;return t.license.needsSignature?s.redirect(`${e.baseUrl}/license`):r.world?e.user?s.redirect(`${e.baseUrl}/game`):s.redirect(`${e.baseUrl}/join`):s.redirect(`${e.baseUrl}/setup`)}static _getStaticContent({world:e=!1,setup:s=!1,moduleConfig:t={}}){const r=[];let o=[];const i=new Set,c=(e,s,t)=>{const c="style"===s?o:r;i.has(e)||c.push({src:e,type:s,priority:t,isModule:"module"===s})},l=0,a=1,n=2,p=3,u=4,d=5,h=6,m=7,f=8,y=9,g=10,E=0,_=1,S=2,w=3,b=4;if(Express.CORE_VIEW_SCRIPTS.forEach((e=>c(e,"script",l))),e&&c("scripts/simplepeer.min.js","script",l),Express.CORE_VIEW_MODULES.forEach((e=>c(e,"module",a))),Express.CORE_VIEW_STYLES.forEach((e=>c(e,"style",_))),e){const e=global.game.world;e.system.esmodules.forEach((e=>c(e,"module",h))),e.system.scripts.forEach((e=>c(e,"script",d))),e.system.styles.forEach((e=>c(e,"style",S)));for(let s of e.modules){if(!0!==t[s.id])continue;const e=s.library??!1;s.esmodules.forEach((s=>c(s,"module",e?u:f))),s.scripts.forEach((s=>c(s,"script",e?p:m))),s.styles.forEach((s=>c(s,"style",e?E:w)))}e.esmodules.forEach((e=>c(e,"module",g))),e.scripts.forEach((e=>c(e,"script",y))),e.styles.forEach((e=>c(e,"style",b)))}if(c("scripts/foundry-esm.js","script",n),c("scripts/foundry.js","script",n),s){o.find((e=>"css/style.css"===e.src)).src="css/foundry2.css",c("scripts/setup.js","script",n);Module.getCoreTranslationStyles().forEach((e=>c(e,"style",w)))}const x=(e,s)=>e.priority-s.priority;return r.sort(x),o.sort(x),{scripts:r,styles:o}}}