omegga 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude/settings.local.json +9 -2
  2. package/CHANGELOG.md +22 -1
  3. package/dist/cli/terminal.js +5 -1
  4. package/dist/cli/terminal.js.map +1 -1
  5. package/dist/db/connection.js +31 -0
  6. package/dist/db/connection.js.map +1 -0
  7. package/dist/db/migrate.js +20 -0
  8. package/dist/db/migrate.js.map +1 -0
  9. package/dist/db/migrations/0000_long_killer_shrike.sql +111 -0
  10. package/dist/db/migrations/meta/0000_snapshot.json +737 -0
  11. package/dist/db/migrations/meta/_journal.json +13 -0
  12. package/dist/db/nedbImport.js +344 -0
  13. package/dist/db/nedbImport.js.map +1 -0
  14. package/dist/db/plugin-migrations/0000_faulty_tyger_tiger.sql +11 -0
  15. package/dist/db/plugin-migrations/meta/0000_snapshot.json +81 -0
  16. package/dist/db/plugin-migrations/meta/_journal.json +13 -0
  17. package/dist/db/pluginSchema.js +19 -0
  18. package/dist/db/pluginSchema.js.map +1 -0
  19. package/dist/db/schema.js +174 -0
  20. package/dist/db/schema.js.map +1 -0
  21. package/dist/omegga/matchers/version.js +30 -13
  22. package/dist/omegga/matchers/version.js.map +1 -1
  23. package/dist/omegga/plugin.js +47 -49
  24. package/dist/omegga/plugin.js.map +1 -1
  25. package/dist/omegga/server.js.map +1 -1
  26. package/dist/softconfig.js +9 -21
  27. package/dist/softconfig.js.map +1 -1
  28. package/dist/updater/steam.js +1 -1
  29. package/dist/updater/steam.js.map +1 -1
  30. package/dist/util/brdb.js +84 -2
  31. package/dist/util/brdb.js.map +1 -1
  32. package/dist/webserver/backend/database.js +533 -533
  33. package/dist/webserver/backend/database.js.map +1 -1
  34. package/dist/webserver/backend/index.js +19 -8
  35. package/dist/webserver/backend/index.js.map +1 -1
  36. package/dist/webserver/backend/metrics.js +7 -5
  37. package/dist/webserver/backend/metrics.js.map +1 -1
  38. package/dist/webserver/backend/permissions.js +69 -20
  39. package/dist/webserver/backend/permissions.js.map +1 -1
  40. package/dist/webserver/backend/roleHierarchy.js +106 -0
  41. package/dist/webserver/backend/roleHierarchy.js.map +1 -0
  42. package/dist/webserver/backend/router/player.js +13 -16
  43. package/dist/webserver/backend/router/player.js.map +1 -1
  44. package/dist/webserver/backend/router/role.js +195 -7
  45. package/dist/webserver/backend/router/role.js.map +1 -1
  46. package/dist/webserver/backend/router/server.js +32 -2
  47. package/dist/webserver/backend/router/server.js.map +1 -1
  48. package/dist/webserver/backend/router/session.js +3 -5
  49. package/dist/webserver/backend/router/session.js.map +1 -1
  50. package/dist/webserver/backend/router/user.js +89 -42
  51. package/dist/webserver/backend/router/user.js.map +1 -1
  52. package/dist/webserver/backend/scopes.js +73 -44
  53. package/dist/webserver/backend/scopes.js.map +1 -1
  54. package/dist/webserver/backend/trpc.js +3 -6
  55. package/dist/webserver/backend/trpc.js.map +1 -1
  56. package/doc/permissions.md +83 -12
  57. package/drizzle.config.ts +7 -0
  58. package/drizzle.plugin.config.ts +7 -0
  59. package/package.json +7 -2
  60. package/public/app.html +415 -32
  61. package/public/assets/IconWorld-CmdphDgp.js +2 -0
  62. package/public/assets/IconWorld-CmdphDgp.js.map +1 -0
  63. package/public/assets/IconWorldPlus-DKSZjbqh.js +2 -0
  64. package/public/assets/IconWorldPlus-DKSZjbqh.js.map +1 -0
  65. package/public/assets/app-BiBsxujC.js +7 -0
  66. package/public/assets/app-BiBsxujC.js.map +1 -0
  67. package/public/assets/{auth-CEYiizOt.js → auth-nn5rDnJ9.js} +2 -2
  68. package/public/assets/{auth-CEYiizOt.js.map → auth-nn5rDnJ9.js.map} +1 -1
  69. package/public/assets/favicon-DYc6c67x.ico +0 -0
  70. package/public/assets/{index-DEL-PD_8.js → index-2cgXLhrt.js} +2 -2
  71. package/public/assets/{index-DEL-PD_8.js.map → index-2cgXLhrt.js.map} +1 -1
  72. package/public/assets/index-5EkjXNMi.js +12 -0
  73. package/public/assets/index-5EkjXNMi.js.map +1 -0
  74. package/public/assets/index-B2DCW1zi.js +2 -0
  75. package/public/assets/index-B2DCW1zi.js.map +1 -0
  76. package/public/assets/logo-Dl-LlTow.webp +0 -0
  77. package/public/assets/shared/@components-VkA0DIz6.js +2 -0
  78. package/public/assets/shared/@components-VkA0DIz6.js.map +1 -0
  79. package/public/assets/shared/@hooks-CiJaUL6_.js +2 -0
  80. package/public/assets/shared/@hooks-CiJaUL6_.js.map +1 -0
  81. package/public/assets/shared/@tabler_icons-react-B4130bhe.js +3 -0
  82. package/public/assets/shared/{@tabler_icons-react-DrVFX3uL.js.map → @tabler_icons-react-B4130bhe.js.map} +1 -1
  83. package/public/assets/shared/{wouter-CNISJ5i6.js → wouter-CUC1caxu.js} +2 -2
  84. package/public/assets/shared/wouter-CUC1caxu.js.map +1 -0
  85. package/public/assets/user-XF1UyJMA.js +2 -0
  86. package/public/assets/user-XF1UyJMA.js.map +1 -0
  87. package/public/auth.html +163 -16
  88. package/public/assets/IconWorld-XmQNg6JI.js +0 -2
  89. package/public/assets/IconWorld-XmQNg6JI.js.map +0 -1
  90. package/public/assets/IconWorldPlus-CxGGv2eD.js +0 -2
  91. package/public/assets/IconWorldPlus-CxGGv2eD.js.map +0 -1
  92. package/public/assets/app-DMr_-wik.js +0 -7
  93. package/public/assets/app-DMr_-wik.js.map +0 -1
  94. package/public/assets/index-D_STNela.js +0 -2
  95. package/public/assets/index-D_STNela.js.map +0 -1
  96. package/public/assets/index-Dkh3HiBz.js +0 -4
  97. package/public/assets/index-Dkh3HiBz.js.map +0 -1
  98. package/public/assets/shared/@components-CgsSYMe6.js +0 -2
  99. package/public/assets/shared/@components-CgsSYMe6.js.map +0 -1
  100. package/public/assets/shared/@hooks-DT93N7wd.js +0 -2
  101. package/public/assets/shared/@hooks-DT93N7wd.js.map +0 -1
  102. package/public/assets/shared/@tabler_icons-react-DrVFX3uL.js +0 -3
  103. package/public/assets/shared/wouter-CNISJ5i6.js.map +0 -1
  104. package/public/assets/useHasScope-BOdFktko.js +0 -2
  105. package/public/assets/useHasScope-BOdFktko.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sources":["../../../src/webserver/backend/database.ts"],"sourcesContent":["import soft from '@/softconfig';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { explode } from '@util/pattern';\nimport { parseBrickadiaTime } from '@util/time';\nimport bcrypt from 'bcryptjs';\nimport chokidar from 'chokidar';\nimport EventEmitter from 'events';\nimport Datastore from 'nedb-promises';\nimport path from 'path';\nimport Calendar from './calendar';\nimport { serverEvents } from './events';\nimport {\n EMPTY_PERMISSIONS,\n RootLevel,\n decodePermissions,\n encodePermissions,\n type PermissionSet,\n} from './permissions';\nimport {\n IPlayer,\n IPunchcard,\n IStoreAutoRestartConfig,\n IStoreBanHistory,\n IStoreChat,\n IStoreDefaultPermissions,\n IStoreKickHistory,\n IStoreServerInstance,\n IStoreUser,\n IStoreVersion,\n IUserHistory,\n IUserNote,\n IWebAuthnCredential,\n} from './types';\n\n// TODO: online users graph\n// TODO: player online times\n// TODO: bricks over time graph\n// TODO: minute server status for metrics\n// TODO: chat messages per min/hour/day check\n\nlet serverInstance: IStoreServerInstance & { _id: string };\n\ntype FixedSizeArray<N extends number, T, M extends string = '0'> = {\n readonly [k in M]: any;\n} & { length: N } & ReadonlyArray<T>;\n\n// generate a punchcard (days x week)\nconst createPunchcard = (): number[][] =>\n Array.from({ length: 7 }).map(() =>\n Array.from<number>({ length: 24 }).fill(0),\n );\n\n// the database keeps track of metrics for omegga\nexport default class Database extends EventEmitter {\n options: IServerConfig;\n omegga: Omegga;\n stores: {\n users: Datastore;\n chat: Datastore;\n players: Datastore;\n status: Datastore;\n server: Datastore;\n };\n defaultPermissionsCache: IStoreDefaultPermissions | null = null;\n calendar: Calendar;\n\n constructor(options: IServerConfig, omegga: Omegga) {\n super();\n this.options = options;\n this.omegga = omegga;\n this.calendar = new Calendar();\n\n // database\n const dbOpts = {\n autoload: true,\n // case insensitive string comparison\n compareStrings: (a: string, b: string) =>\n a.localeCompare(b, 'en', { ignorePunctuation: true }),\n };\n\n // create all the stores\n this.stores = {\n users: Datastore.create({\n filename: path.join(omegga.dataPath, soft.USER_STORE),\n ...dbOpts,\n }),\n chat: Datastore.create({\n filename: path.join(omegga.dataPath, soft.CHAT_STORE),\n ...dbOpts,\n }),\n players: Datastore.create({\n filename: path.join(omegga.dataPath, soft.PLAYER_STORE),\n ...dbOpts,\n }),\n status: Datastore.create({\n filename: path.join(omegga.dataPath, soft.STATUS_STORE),\n ...dbOpts,\n }),\n server: Datastore.create({\n filename: path.join(omegga.dataPath, soft.SERVER_STORE),\n ...dbOpts,\n }),\n };\n }\n\n // make sure all the databases have valid store versions\n async doMigrations() {\n // current store versions\n const storeVersions: { [key: keyof typeof this.stores]: number } = {\n users: 2,\n chat: 1,\n players: 1,\n status: 1,\n server: 2,\n\n // example version\n dummy: 10,\n };\n\n // migrations to/from certain store versions\n const migrations: {\n [key: keyof typeof this.stores]: {\n version: number;\n upgrade: (store: Datastore) => Promise<void>;\n }[];\n } = {\n users: [\n {\n version: 1,\n upgrade: async (store: Datastore) => {\n const users = await store.find<IStoreUser & { _id: string }>({\n type: 'user',\n });\n for (const user of users) {\n const p = (user.permissions as any) ?? EMPTY_PERMISSIONS;\n const root =\n p.root === 'all' || p.root === 'read' ? p.root : 'off';\n const domains: Record<string, string> = {};\n for (const [k, v] of Object.entries(p.domains ?? {})) {\n if (v !== 'unset' && v !== 'none') domains[k] = v as string;\n }\n const scopes: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(p.scopes ?? {})) {\n if (v === 'enabled' || v === true) scopes[k] = true;\n else if (v === 'disabled' || v === false) scopes[k] = false;\n }\n const encoded = encodePermissions({ root, domains, scopes });\n await store.update(\n { _id: user._id },\n { $set: { permissions: encoded } },\n );\n }\n },\n },\n ],\n chat: [],\n players: [],\n status: [],\n server: [\n {\n version: 1,\n upgrade: async (store: Datastore) => {\n const doc = await store.findOne<any>({\n type: 'defaultPermissions',\n });\n if (!doc) return;\n const root = doc.root === 'unset' ? 'off' : doc.root;\n const domains: Record<string, string> = {};\n for (const [k, v] of Object.entries(doc.domains ?? {})) {\n if (v !== 'unset') domains[k] = v as string;\n }\n const scopes: Record<string, boolean> = {};\n for (const [k, v] of Object.entries(doc.scopes ?? {})) {\n if (v === 'enabled') scopes[k] = true;\n else if (v === 'disabled') scopes[k] = false;\n }\n const encoded = encodePermissions({\n root,\n domains,\n scopes,\n } as PermissionSet);\n await store.update(\n { _id: doc._id },\n {\n $set: {\n root: encoded.root,\n domains: encoded.domains,\n scopes: encoded.scopes,\n },\n },\n );\n },\n },\n ],\n\n // example migration list (all version upgrades go up by 1)\n dummy: [\n {\n version: 1, // target version\n upgrade: async _store => {}, // upgrade function (does not include updating version)\n },\n ],\n };\n\n // loop through all stores to find if any need version upgrades\n await Promise.all(\n Object.entries(this.stores).map(async ([name, store]) => {\n const versionEntry = await store.findOne<IStoreVersion>({\n type: 'storeVersion',\n });\n const expected = storeVersions[name];\n // if the version was not found, insert it\n if (!versionEntry) {\n store.insert({ type: 'storeVersion', version: expected });\n\n // if the version is below the expected version\n } else if (versionEntry.version < expected) {\n // while the version is below the expected version\n let { version } = versionEntry;\n for (; version < expected; version++) {\n // find an available migration\n const migration = migrations[name].find(v => v.version === version);\n\n // no migration found, ignore\n if (!migration) break;\n\n // run the upgrade\n migration.upgrade(store);\n }\n\n // update the stored version\n await store.update({ _id: versionEntry._id }, { version });\n }\n }),\n );\n }\n\n // get the running instance id of this omegga\n async getInstanceId() {\n if (serverInstance) return serverInstance._id;\n\n const doc = await this.stores.server.insert<IStoreServerInstance>({\n type: 'app:start',\n date: Date.now(),\n });\n\n serverInstance = doc;\n\n // create a calendar of all previous chat messages\n const dates = (\n await this.stores.chat.find<IStoreChat>({ type: 'chat' })\n ).map(c => c.created);\n for (const d of dates) {\n this.calendar.addDate(d);\n }\n\n const watcher = chokidar.watch(\n path.join(this.omegga.configPath, 'BanList.json'),\n { persistent: false },\n );\n watcher\n .on('add', () => setTimeout(this.syncBanList.bind(this), 500))\n .on('change', () =>\n setTimeout(() => {\n this.syncBanList();\n this.emit('update.bans');\n }, 500),\n );\n setTimeout(this.syncBanList.bind(this), 500);\n\n const handleKick = (kicked: IPlayer, kicker: IPlayer, reason: string) => {\n // time the event listener should expire\n const eventExpire = Date.now() + 100;\n\n // detect a single leave\n this.omegga.once('leave', async leavingPlayer => {\n const now = Date.now();\n // if a player leaves more than 100ms from the kick command, it's bugged\n if (now > eventExpire) return;\n\n if (leavingPlayer.id === kicked.id) {\n const entry = {\n type: 'kickHistory',\n kicked: kicked.id,\n kickerId: kicker.id,\n created: now,\n reason,\n };\n\n // depending on implementation, this could potentially be a kick event\n await this.stores.players.update(\n entry,\n { $set: entry },\n { upsert: true },\n );\n }\n });\n };\n\n // for a5 is based on the chat event\n this.omegga.on('kick', (kicked, kicker, reason) => {\n const kickedPlayer = this.omegga.findPlayerByName(kicked);\n const kickerPlayer = this.omegga.findPlayerByName(kicker);\n\n // couldn't find kicked or kicker player\n if (!kickedPlayer || !kickerPlayer) return;\n\n handleKick(kickedPlayer, kickerPlayer, reason);\n });\n\n return doc._id;\n }\n\n // synchronize the ban list with the one in the database\n syncBanList() {\n const banList = this.omegga.getBanList()?.banList;\n if (!banList) return;\n\n // upsert all bans\n for (const banned in banList) {\n const entry: IStoreBanHistory = {\n type: 'banHistory',\n banned,\n bannerId: banList[banned].bannerId,\n created: parseBrickadiaTime(banList[banned].created),\n expires: parseBrickadiaTime(banList[banned].expires),\n reason: banList[banned].reason,\n };\n\n this.stores.players.update(entry, { $set: entry }, { upsert: true });\n }\n }\n\n // determine if this user would be the first user (admin user)\n async isFirstUser() {\n return (await this.stores.users.count({ type: 'user' })) === 0;\n }\n\n // hash a password\n async hash(password: string) {\n return await bcrypt.hash(password, 10);\n }\n\n // create an admin user account, username can be blank (no password)\n async createAdminUser(username: string, password: string) {\n const hash = await this.hash(password);\n\n // create an owner user\n const user = this.stores.users.insert({\n // this is a user\n type: 'user',\n created: Date.now(),\n lastOnline: Date.now(),\n\n // credentials\n username,\n hash,\n\n // permissions\n isOwner: true,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n\n // mfa\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n\n // brickadia player uuid\n playerId: '',\n });\n return user;\n }\n\n // determine if a user with this name exists\n async userExists(username: string) {\n return (await this.stores.users.count({ type: 'user', username })) > 0;\n }\n\n // create a regular user account\n async createUser(username: string, password: string) {\n if (await this.userExists(username)) throw new Error('user already exists');\n\n const hash = await this.hash(password);\n // create a regular user\n const user = this.stores.users.insert<IStoreUser>({\n // this is a user\n type: 'user',\n created: Date.now(),\n lastOnline: 0,\n\n // credentials\n username,\n hash,\n\n // permissions\n isOwner: false,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n\n // mfa\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n\n // brickadia player uuid\n playerId: '',\n });\n return user;\n }\n\n // set a user's password\n async userPasswd(username: string, password: string) {\n const hash = await this.hash(password);\n\n if (!(await this.userExists(username)))\n throw new Error('user does not exist');\n\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { hash } },\n );\n }\n\n async banUser(username: string, banned: boolean) {\n const result = await this.stores.users.update(\n { type: 'user', username },\n { $set: { isBanned: banned } },\n );\n if (banned) serverEvents.emit('userInvalidated', username);\n return result;\n }\n\n async deleteUser(username: string) {\n const result = await this.stores.users.remove(\n { type: 'user', username },\n {},\n );\n serverEvents.emit('userInvalidated', username);\n return result;\n }\n\n async setUserPermissions(username: string, permissions: PermissionSet) {\n return await this.stores.users.update(\n { type: 'user', username },\n { $set: { permissions: encodePermissions(permissions) } },\n );\n }\n\n async getDefaultPermissions(): Promise<IStoreDefaultPermissions> {\n if (this.defaultPermissionsCache) return this.defaultPermissionsCache;\n let doc = await this.stores.server.findOne<IStoreDefaultPermissions>({\n type: 'defaultPermissions',\n });\n if (!doc) {\n doc = await this.stores.server.insert<IStoreDefaultPermissions>({\n type: 'defaultPermissions',\n root: RootLevel.Read,\n domains: {},\n scopes: {},\n });\n }\n const decoded = decodePermissions(doc);\n const result = { ...doc, ...decoded };\n this.defaultPermissionsCache = result;\n return result;\n }\n\n async setDefaultPermissions(\n permissions: Omit<IStoreDefaultPermissions, 'type'>,\n ) {\n const encoded = encodePermissions(permissions as PermissionSet);\n await this.stores.server.update(\n { type: 'defaultPermissions' },\n {\n $set: {\n root: encoded.root,\n domains: encoded.domains,\n scopes: encoded.scopes,\n },\n },\n { upsert: true },\n );\n this.defaultPermissionsCache = null;\n }\n\n // get a user from credentials\n async authUser(username: string, password: string) {\n const user = await this.stores.users.findOne<IStoreUser>({ username });\n // user not found\n if (!user || user.isBanned) return null;\n\n // make sure the user's password hash is valid\n if (await bcrypt.compare(password, user.hash)) {\n // update last online status\n await this.stores.users.update(\n { _id: user._id },\n { $set: { lastOnline: Date.now() } },\n );\n return user;\n }\n\n // wrong password\n return null;\n }\n\n // find a user by object id\n async findUserById(id: string) {\n const user = await this.stores.users.findOne<IStoreUser>({\n $or: [\n // the owner has no username, so everyone is the owner\n { type: 'user', username: '', isOwner: true },\n\n // the user exists and has an id\n { type: 'user', _id: id },\n ],\n });\n if (user) {\n user.permissions = decodePermissions(user.permissions);\n if (!user.totpEnabled) user.totpEnabled = false;\n if (!user.passkeys) user.passkeys = [];\n }\n return user;\n }\n\n async setUserTotp(username: string, secret: string, enabled: boolean) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { totpSecret: secret, totpEnabled: enabled } },\n );\n }\n\n async disableUserTotp(username: string) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { totpEnabled: false }, $unset: { totpSecret: true } },\n );\n }\n\n async addPasskey(username: string, credential: IWebAuthnCredential) {\n await this.stores.users.update(\n { type: 'user', username },\n { $push: { passkeys: credential } },\n );\n }\n\n async removePasskey(username: string, credentialId: string) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const passkeys = (user.passkeys ?? []).filter(p => p.id !== credentialId);\n await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });\n }\n\n async updatePasskeyCounter(\n username: string,\n credentialId: string,\n counter: number,\n ) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const passkeys = (user.passkeys ?? []).map(p =>\n p.id === credentialId ? { ...p, counter, lastUsed: Date.now() } : p,\n );\n await this.stores.users.update({ _id: user._id }, { $set: { passkeys } });\n }\n\n async setRecoveryCodes(username: string, hashedCodes: string[]) {\n await this.stores.users.update(\n { type: 'user', username },\n { $set: { recoveryCodes: hashedCodes } },\n );\n }\n\n async removeRecoveryCode(username: string, hashedCode: string) {\n const user = await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n username,\n });\n if (!user) return;\n const codes = (user.recoveryCodes ?? []).filter(c => c !== hashedCode);\n await this.stores.users.update(\n { _id: user._id },\n { $set: { recoveryCodes: codes } },\n );\n }\n\n async resetUserMfa(username: string) {\n await this.stores.users.update(\n { type: 'user', username },\n {\n $set: {\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n },\n $unset: { totpSecret: true },\n },\n );\n }\n\n async findUserByPasskeyId(credentialId: string) {\n return await this.stores.users.findOne<IStoreUser & { _id: string }>({\n type: 'user',\n 'passkeys.id': credentialId,\n });\n }\n\n // get a list of roles\n // TODO: actually implement roles\n getRoles(): Promise<{ type: 'role'; name: 'string' }[]> {\n return this.stores.server.find({ type: 'role' });\n }\n\n // add a chat message to the chat log store\n async addChatLog(\n action: 'msg' | 'server' | 'leave' | 'join',\n user: IPlayer,\n message?: string,\n ) {\n this.calendar.addDate(Date.now());\n return await this.stores.chat.insert<IStoreChat>({\n type: 'chat',\n created: Date.now(),\n instanceId: await this.getInstanceId(),\n action,\n user,\n ...(message ? { message } : {}),\n });\n }\n\n // get recent chat activity\n async getChats({\n count = 50,\n sameServer,\n before,\n after,\n }: {\n count?: number;\n sameServer?: boolean;\n before?: number;\n after?: number;\n } = {}) {\n return await this.stores.chat\n .find<IStoreChat>({\n type: 'chat',\n ...(sameServer ? { instanceId: await this.getInstanceId() } : {}),\n created: before\n ? { $lt: before }\n : after\n ? { $gt: after }\n : { $lt: Date.now() },\n })\n .sort({ created: !before && after ? 1 : -1 })\n .limit(count)\n .exec();\n }\n\n // get paginated players\n async getPlayers({\n count = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n limitId = undefined,\n }: {\n count?: number;\n search?: string;\n page?: number;\n sort?: string;\n direction?: number;\n limitId?: string[];\n } = {}) {\n const pattern = explode(search);\n\n // the query used for finding which players are available\n const query = {\n type: 'userHistory',\n // only filter if there's a query\n $and: [\n // if a limitId is passed in, the id must be in it\n ...(limitId ? [{ id: { $in: limitId } }] : []),\n\n // if there's a search, the search matches...\n ...(search.length > 0\n ? [\n {\n $or: [\n // id was pasted in\n { id: search },\n // user's current name\n { name: { $regex: pattern } },\n { displayName: { $regex: pattern } },\n // user's past name\n {\n nameHistory: { $elemMatch: { name: { $regex: pattern } } },\n },\n {\n nameHistory: {\n $elemMatch: { displayName: { $regex: pattern } },\n },\n },\n ],\n },\n ]\n : []),\n ],\n };\n\n // the query used for finding a specific player\n const exactQuery = {\n type: 'userHistory',\n // only filter if there's a query\n $or: [\n // id was pasted in\n { id: search },\n // user's current name\n { name: search },\n // user's current display name\n { displayName: search },\n ],\n };\n\n // count players and query players\n const [total, players, exactResult] = await Promise.all([\n this.stores.players.count(query),\n this.stores.players\n .find<IUserHistory>(query)\n // TODO: add other sorts and sort directions\n .sort({ [sort]: direction })\n .skip(count * page)\n .limit(count)\n .exec(),\n\n // exact match detection\n search.length > 0 && page === 0\n ? this.stores.players.find(exactQuery)\n : [],\n ]);\n\n // add exact matches to the beginning\n if (exactResult.length > 0) {\n // remove duplicates\n for (const res of exactResult) {\n const index = players.findIndex(p => p._id === res._id);\n players.splice(index, 1);\n }\n\n // add results to the beginning\n players.splice(0, 0, ...exactResult);\n }\n\n return {\n pages: Math.ceil(total / count),\n total,\n players,\n };\n }\n\n // get paginated users\n async getUsers({\n count = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n } = {}) {\n const pattern = explode(search);\n\n // the query used for finding which players are available\n const query = {\n type: 'user',\n // only filter if there's a query\n ...(search.length > 0\n ? {\n $or: [\n // id was pasted in\n { playerId: search },\n // user's current name\n { name: { $regex: pattern } },\n ],\n }\n : {}),\n };\n\n // count players and query players\n const [total, users] = await Promise.all([\n this.stores.users.count(query),\n this.stores.users\n .find<IStoreUser>(query)\n // TODO: add other sorts and sort directions\n .sort({ [sort]: direction })\n .skip(count * page)\n .limit(count)\n .exec(),\n ]);\n\n return {\n pages: Math.ceil(total / count),\n total,\n users,\n };\n }\n\n // get an individual player, ban and kick history, and notes\n async getPlayer(id: string) {\n const [player, banHistory, kickHistory, notes] = await Promise.all([\n this.stores.players.findOne<IUserHistory>({ type: 'userHistory', id }),\n this.stores.players\n .find<IStoreBanHistory>({ type: 'banHistory', banned: id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n this.stores.players\n .find<IStoreKickHistory>({ type: 'kickHistory', kicked: id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n this.stores.players\n .find<IUserNote>({ type: 'note', id })\n .sort({ created: -1 })\n .limit(25)\n .exec(),\n ]);\n\n if (!player) return null;\n\n return { ...player, banHistory, kickHistory, notes };\n }\n\n // add a user to the visit history, returns true if this is a first visit\n async addVisit(user: Required<IPlayer>) {\n const existing = await this.stores.players.findOne<IUserHistory>({\n type: 'userHistory',\n id: user.id,\n });\n const now = Date.now();\n const instanceId = await this.getInstanceId();\n if (!existing) {\n // create the player\n // TODO: add some form of admin note tool for moderators to leave notes\n // on troublesome players + warnings\n await this.stores.players.insert<IUserHistory>({\n type: 'userHistory',\n // base brickadia user info\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n // list of names and when they were first used\n nameHistory: [\n { name: user.name, displayName: user.displayName, date: now },\n ],\n // list of ips this player has been on\n ips: [],\n // first time player was seen\n created: now,\n // last time player was seen\n lastSeen: now,\n // last server this player has been on\n lastInstanceId: instanceId,\n // number of status checks this player has observed\n heartbeats: 0,\n // number of sessions (at least an hour apart) this player has had\n sessions: 1,\n // number of instances (times omegga was started) this player has joined\n instances: 1,\n });\n return true;\n } else {\n await this.stores.players.update<IUserHistory>(\n { _id: existing._id },\n {\n $inc: {\n // increment number of sessions if the player hasn't been seen for an hour\n sessions: existing.lastSeen < now - 60 * 60 * 1000 ? 1 : 0,\n // increment the number of instances this player has joined\n instances: existing.lastInstanceId === instanceId ? 1 : 0,\n },\n\n $set: {\n // update last seen time and user name\n lastSeen: now,\n name: user.name,\n displayName: user.displayName,\n\n // set the last joined instance\n lastInstanceId: instanceId,\n },\n\n // add the name to the history if it was not already in it\n ...(existing.nameHistory.some(\n h => h.name === user.name && h.displayName === user.displayName,\n )\n ? {}\n : {\n $addToSet: {\n nameHistory: {\n name: user.name,\n displayName: user.displayName,\n date: now,\n },\n },\n }),\n },\n );\n return false;\n }\n }\n\n // use data from minutely heartbeats to fuel metrics\n async addHeartbeat(data: {\n bricks: number;\n players: string[];\n ips: Record<string, string>;\n }) {\n const now = Date.now();\n\n // add the heartbeat to the server status store\n await this.stores.status.insert({\n type: 'heartbeat',\n created: now,\n bricks: data.bricks,\n players: data.players,\n });\n\n // update player heartbeats and last seens\n await this.stores.players.update(\n {\n // all players in the status update\n id: { $in: data.players },\n },\n {\n // update last seen\n $set: { lastSeen: now },\n // increment heartbeats\n $inc: { heartbeats: 1 },\n },\n { multi: true },\n );\n\n // get all players in the status update\n const players = await this.stores.players.find<IUserHistory>({\n id: { $in: data.players },\n });\n\n await Promise.all(\n players\n // find players without the discovered ip\n .filter(p => !(p.ips || []).includes(data.ips[p.id]))\n // insert the IP into the player's ip list\n .map(p =>\n this.stores.players.update(\n { _id: p._id },\n {\n $addToSet: { ips: data.ips[p.id] },\n },\n ),\n ),\n );\n }\n\n async getAutoRestartConfig(): Promise<IStoreAutoRestartConfig> {\n let config = await this.stores.server.findOne<IStoreAutoRestartConfig>({\n type: 'autoRestartConfig',\n });\n if (!config) {\n config = await this.stores.server.insert({\n type: 'autoRestartConfig',\n maxUptime: 48,\n maxUptimeEnabled: false,\n emptyUptime: 24,\n emptyUptimeEnabled: false,\n dailyHour: 2,\n dailyHourEnabled: false,\n autoUpdateEnabled: true,\n autoUpdateIntervalMins: 60,\n announcementEnabled: true,\n playersEnabled: true,\n saveWorld: true,\n crashRestartEnabled: true,\n });\n }\n\n config.autoUpdateEnabled ??= true;\n config.autoUpdateIntervalMins ??= 60;\n config.saveWorld ??= true;\n config.crashRestartEnabled ??= true;\n\n return config;\n }\n\n async setAutoRestartConfig(config: IStoreAutoRestartConfig): Promise<void> {\n config.maxUptime = Math.round(Math.max(1, Math.min(config.maxUptime, 168)));\n config.emptyUptime = Math.round(\n Math.max(1, Math.min(config.emptyUptime, 168)),\n );\n config.dailyHour = Math.round(Math.max(0, Math.min(config.dailyHour, 23)));\n config.autoUpdateIntervalMins = Math.round(\n Math.max(10, Math.min(config.autoUpdateIntervalMins ?? 60, Infinity)),\n );\n config.autoUpdateEnabled ??= true;\n await this.stores.server.update(\n { type: 'autoRestartConfig' },\n {\n $set: {\n ...config,\n },\n },\n { upsert: false },\n );\n }\n\n // update player online-time punchcard\n async updatePlayerPunchcard(numNewPlayers: number) {\n // get date, utc hour, and utc day\n const now = new Date();\n const time = now.getTime();\n const hour = now.getUTCHours();\n const day = now.getUTCDay();\n const month = now.getUTCMonth();\n const year = now.getUTCFullYear();\n\n // find the punchcard\n const card = await this.stores.status.findOne<IPunchcard>({\n type: 'punchcard',\n kind: 'playerCount',\n month,\n year,\n });\n\n // create a new punchcard if one does not exist for this month\n if (!card) {\n const punchcard = createPunchcard();\n punchcard[day][hour] = numNewPlayers;\n // insert it\n await this.stores.status.insert<IPunchcard>({\n type: 'punchcard',\n kind: 'playerCount',\n created: time,\n updated: time,\n month,\n year,\n punchcard,\n });\n\n // if the card exists, check if it was already updated this hour\n } else {\n // add the players to this slot\n card.punchcard[day][hour] += numNewPlayers;\n // update the punchcard\n await this.stores.status.update(\n { _id: card._id },\n {\n $set: {\n punchcard: card.punchcard,\n updated: time,\n },\n },\n );\n }\n }\n}\n"],"names":["Calendar","soft","EMPTY_PERMISSIONS","encodePermissions","parseBrickadiaTime","serverEvents","permissions","RootLevel","decodePermissions","pattern","explode","time"],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sources":["../../../src/webserver/backend/database.ts"],"sourcesContent":["import { IServerConfig } from '@config/types';\nimport * as schema from '@/db/schema';\nimport type Omegga from '@omegga/server';\nimport { explode } from '@util/pattern';\nimport { parseBrickadiaTime } from '@util/time';\nimport bcrypt from 'bcryptjs';\nimport type BetterSqlite3 from 'better-sqlite3';\nimport chokidar from 'chokidar';\nimport {\n and,\n count,\n desc,\n eq,\n inArray,\n lt,\n gt,\n or,\n sql,\n asc,\n type SQL,\n} from 'drizzle-orm';\nimport type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';\nimport EventEmitter from 'events';\nimport { randomUUID } from 'node:crypto';\nimport path from 'path';\nimport Calendar from './calendar';\nimport { serverEvents } from './events';\nimport {\n EMPTY_PERMISSIONS,\n RootLevel,\n type PermissionSet,\n} from './permissions';\nimport type {\n IPlayer,\n IStoreAutoRestartConfig,\n IStoreBanHistory,\n IStoreDefaultPermissions,\n IStoreKickHistory,\n IStoreRole,\n IStoreUser,\n IUserHistory,\n IWebAuthnCredential,\n} from './types';\n\n// TODO: online users graph\n// TODO: player online times\n// TODO: bricks over time graph\n// TODO: minute server status for metrics\n// TODO: chat messages per min/hour/day check\n\n// generate a punchcard (days x week)\nconst createPunchcard = (): number[][] =>\n Array.from({ length: 7 }).map(() =>\n Array.from<number>({ length: 24 }).fill(0),\n );\n\n// the database keeps track of metrics for omegga\nexport default class Database extends EventEmitter {\n options: IServerConfig;\n omegga: Omegga;\n sqlite: BetterSqlite3.Database;\n db: BetterSQLite3Database;\n defaultPermissionsCache: IStoreDefaultPermissions | null = null;\n rolesCache: (IStoreRole & { id: string })[] | null = null;\n calendar: Calendar;\n private serverInstance: { id: string; date: number } | null = null;\n\n constructor(\n options: IServerConfig,\n omegga: Omegga,\n sqlite: BetterSqlite3.Database,\n db: BetterSQLite3Database,\n ) {\n super();\n this.options = options;\n this.omegga = omegga;\n this.sqlite = sqlite;\n this.db = db;\n this.calendar = new Calendar();\n }\n\n // get the running instance id of this omegga\n async getInstanceId() {\n if (this.serverInstance) return this.serverInstance.id;\n\n const id = randomUUID();\n this.db\n .insert(schema.serverInstances)\n .values({ id, date: Date.now() })\n .run();\n this.serverInstance = { id, date: Date.now() };\n\n // create a calendar of all previous chat messages\n const dates = this.db\n .select({ created: schema.chatLogs.created })\n .from(schema.chatLogs)\n .all();\n for (const d of dates) {\n this.calendar.addDate(d.created);\n }\n\n const watcher = chokidar.watch(\n path.join(this.omegga.configPath, 'BanList.json'),\n { persistent: false },\n );\n watcher\n .on('add', () => setTimeout(this.syncBanList.bind(this), 500))\n .on('change', () =>\n setTimeout(() => {\n this.syncBanList();\n this.emit('update.bans');\n }, 500),\n );\n setTimeout(this.syncBanList.bind(this), 500);\n\n const handleKick = (kicked: IPlayer, kicker: IPlayer, reason: string) => {\n // time the event listener should expire\n const eventExpire = Date.now() + 100;\n // detect a single leave\n this.omegga.once('leave', async leavingPlayer => {\n const now = Date.now();\n // if a player leaves more than 100ms from the kick command, it's bugged\n if (now > eventExpire) return;\n if (leavingPlayer.id === kicked.id) {\n this.upsertKickHistory({\n kicked: kicked.id,\n kickerId: kicker.id,\n created: now,\n reason,\n });\n }\n });\n };\n\n // for a5 is based on the chat event\n this.omegga.on('kick', (kicked, kicker, reason) => {\n const kickedPlayer = this.omegga.findPlayerByName(kicked);\n const kickerPlayer = this.omegga.findPlayerByName(kicker);\n // couldn't find kicked or kicker player\n if (!kickedPlayer || !kickerPlayer) return;\n handleKick(kickedPlayer, kickerPlayer, reason);\n });\n\n return id;\n }\n\n // synchronize the ban list with the one in the database\n syncBanList() {\n const banList = this.omegga.getBanList()?.banList;\n if (!banList) return;\n\n const tx = this.sqlite.transaction(() => {\n // upsert all bans\n for (const banned in banList) {\n this.upsertBanHistory({\n banned,\n bannerId: banList[banned].bannerId ?? '',\n created: parseBrickadiaTime(banList[banned].created) ?? 0,\n expires: parseBrickadiaTime(banList[banned].expires) ?? 0,\n reason: banList[banned].reason,\n });\n }\n });\n tx();\n }\n\n // determine if this user would be the first user (admin user)\n async isFirstUser() {\n const result = this.db.select({ count: count() }).from(schema.users).get();\n return (result?.count ?? 0) === 0;\n }\n\n // hash a password\n async hash(password: string) {\n return await bcrypt.hash(password, 10);\n }\n\n // create an admin user account, username can be blank (no password)\n async createAdminUser(username: string, password: string) {\n const h = await this.hash(password);\n const id = randomUUID();\n // create an owner user\n this.db\n .insert(schema.users)\n .values({\n id,\n created: Date.now(),\n lastOnline: Date.now(),\n username,\n hash: h,\n isOwner: true,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n playerId: '',\n })\n .run();\n return this.findUserById(id);\n }\n\n // determine if a user with this name exists\n async userExists(username: string) {\n const result = this.db\n .select({ count: count() })\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n return (result?.count ?? 0) > 0;\n }\n\n // create a regular user account\n async createUser(username: string, password: string) {\n if (await this.userExists(username)) throw new Error('user already exists');\n const h = await this.hash(password);\n const id = randomUUID();\n this.db\n .insert(schema.users)\n .values({\n id,\n created: Date.now(),\n lastOnline: 0,\n username,\n hash: h,\n isOwner: false,\n roles: [],\n permissions: EMPTY_PERMISSIONS,\n totpEnabled: false,\n passkeys: [],\n recoveryCodes: [],\n playerId: '',\n })\n .run();\n return this.findUserById(id);\n }\n\n // set a user's password\n async userPasswd(username: string, password: string) {\n const h = await this.hash(password);\n if (!(await this.userExists(username)))\n throw new Error('user does not exist');\n this.db\n .update(schema.users)\n .set({ hash: h })\n .where(eq(schema.users.username, username))\n .run();\n }\n\n async banUser(username: string, banned: boolean) {\n this.db\n .update(schema.users)\n .set({ isBanned: banned })\n .where(eq(schema.users.username, username))\n .run();\n if (banned) serverEvents.emit('userInvalidated', username);\n return 1;\n }\n\n async deleteUser(username: string) {\n this.db\n .delete(schema.users)\n .where(eq(schema.users.username, username))\n .run();\n serverEvents.emit('userInvalidated', username);\n return 1;\n }\n\n async setUserPermissions(username: string, permissions: PermissionSet) {\n this.db\n .update(schema.users)\n .set({ permissions: permissions })\n .where(eq(schema.users.username, username))\n .run();\n }\n\n async getDefaultPermissions(): Promise<IStoreDefaultPermissions> {\n if (this.defaultPermissionsCache) return this.defaultPermissionsCache;\n const row = this.db\n .select()\n .from(schema.serverConfig)\n .where(eq(schema.serverConfig.key, 'defaultPermissions'))\n .get();\n if (!row) {\n const defaults: IStoreDefaultPermissions = {\n type: 'defaultPermissions',\n root: RootLevel.Read,\n domains: {},\n scopes: {},\n };\n this.db\n .insert(schema.serverConfig)\n .values({ key: 'defaultPermissions', value: defaults })\n .run();\n this.defaultPermissionsCache = defaults;\n return defaults;\n }\n const val = row.value as any;\n const result: IStoreDefaultPermissions = {\n type: 'defaultPermissions',\n root: val.root ?? RootLevel.Off,\n domains: val.domains ?? {},\n scopes: val.scopes ?? {},\n };\n this.defaultPermissionsCache = result;\n return result;\n }\n\n async setDefaultPermissions(\n permissions: Omit<IStoreDefaultPermissions, 'type'>,\n ) {\n const value = {\n root: permissions.root,\n domains: permissions.domains,\n scopes: permissions.scopes,\n };\n this.db\n .insert(schema.serverConfig)\n .values({ key: 'defaultPermissions', value })\n .onConflictDoUpdate({\n target: schema.serverConfig.key,\n set: { value },\n })\n .run();\n this.defaultPermissionsCache = null;\n }\n\n // get a user from credentials\n async authUser(username: string, password: string) {\n const row = this.db\n .select()\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n // user not found\n if (!row || row.isBanned) return null;\n // make sure the user's password hash is valid\n if (await bcrypt.compare(password, row.hash)) {\n this.db\n .update(schema.users)\n .set({ lastOnline: Date.now() })\n .where(eq(schema.users.id, row.id))\n .run();\n // update last online status\n return this._toStoreUser(row);\n }\n // wrong password\n return null;\n }\n\n // find a user by object id\n async findUserById(id: string) {\n const row = this.db\n .select()\n .from(schema.users)\n .where(\n or(\n // the owner has no username, so everyone is the owner\n and(eq(schema.users.username, ''), eq(schema.users.isOwner, true)),\n // the user exists and has an id\n eq(schema.users.id, id),\n ),\n )\n .get();\n if (!row) return null;\n return this._toStoreUser(row);\n }\n\n async findUserByUsername(\n username: string,\n ): Promise<(IStoreUser & { id: string }) | null> {\n const row = this.db\n .select()\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n if (!row) return null;\n return this._toStoreUser(row);\n }\n\n async findUserByPasskeyId(credentialId: string) {\n const rows = this.db\n .select()\n .from(schema.users)\n .where(\n sql`${schema.users.passkeys} LIKE ${'%\"id\":\"' + credentialId + '\"%'}`,\n )\n .all();\n for (const row of rows) {\n const passkeys = (row.passkeys ?? []) as IWebAuthnCredential[];\n if (passkeys.some(p => p.id === credentialId)) {\n return this._toStoreUser(row);\n }\n }\n return null;\n }\n\n updateUserLastOnline(id: string) {\n this.db\n .update(schema.users)\n .set({ lastOnline: Date.now() })\n .where(eq(schema.users.id, id))\n .run();\n }\n\n updateOwnerCredentials(id: string, username: string, hash: string) {\n this.db\n .update(schema.users)\n .set({ username, hash })\n .where(eq(schema.users.id, id))\n .run();\n }\n\n addUserRole(userId: string, roleId: string) {\n const row = this.db\n .select({ roles: schema.users.roles })\n .from(schema.users)\n .where(eq(schema.users.id, userId))\n .get();\n if (!row) return;\n const roles = (row.roles ?? []) as string[];\n if (!roles.includes(roleId)) {\n roles.push(roleId);\n this.db\n .update(schema.users)\n .set({ roles })\n .where(eq(schema.users.id, userId))\n .run();\n }\n }\n\n removeUserRole(userId: string, roleId: string) {\n const row = this.db\n .select({ roles: schema.users.roles })\n .from(schema.users)\n .where(eq(schema.users.id, userId))\n .get();\n if (!row) return;\n const roles = ((row.roles ?? []) as string[]).filter(r => r !== roleId);\n this.db\n .update(schema.users)\n .set({ roles })\n .where(eq(schema.users.id, userId))\n .run();\n }\n\n async setUserTotp(username: string, secret: string, enabled: boolean) {\n this.db\n .update(schema.users)\n .set({ totpSecret: secret, totpEnabled: enabled })\n .where(eq(schema.users.username, username))\n .run();\n }\n\n async disableUserTotp(username: string) {\n this.db\n .update(schema.users)\n .set({ totpEnabled: false, totpSecret: null })\n .where(eq(schema.users.username, username))\n .run();\n }\n\n async addPasskey(username: string, credential: IWebAuthnCredential) {\n const row = this.db\n .select({ id: schema.users.id, passkeys: schema.users.passkeys })\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n if (!row) return;\n const passkeys = [\n ...((row.passkeys ?? []) as IWebAuthnCredential[]),\n credential,\n ];\n this.db\n .update(schema.users)\n .set({ passkeys })\n .where(eq(schema.users.id, row.id))\n .run();\n }\n\n async removePasskey(username: string, credentialId: string) {\n const row = this.db\n .select({ id: schema.users.id, passkeys: schema.users.passkeys })\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n if (!row) return;\n const passkeys = ((row.passkeys ?? []) as IWebAuthnCredential[]).filter(\n p => p.id !== credentialId,\n );\n this.db\n .update(schema.users)\n .set({ passkeys })\n .where(eq(schema.users.id, row.id))\n .run();\n }\n\n async updatePasskeyCounter(\n username: string,\n credentialId: string,\n counter: number,\n ) {\n const row = this.db\n .select({ id: schema.users.id, passkeys: schema.users.passkeys })\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n if (!row) return;\n const passkeys = ((row.passkeys ?? []) as IWebAuthnCredential[]).map(p =>\n p.id === credentialId ? { ...p, counter, lastUsed: Date.now() } : p,\n );\n this.db\n .update(schema.users)\n .set({ passkeys })\n .where(eq(schema.users.id, row.id))\n .run();\n }\n\n async setRecoveryCodes(username: string, hashedCodes: string[]) {\n this.db\n .update(schema.users)\n .set({ recoveryCodes: hashedCodes })\n .where(eq(schema.users.username, username))\n .run();\n }\n\n async removeRecoveryCode(username: string, hashedCode: string) {\n const row = this.db\n .select({\n id: schema.users.id,\n recoveryCodes: schema.users.recoveryCodes,\n })\n .from(schema.users)\n .where(eq(schema.users.username, username))\n .get();\n if (!row) return;\n const codes = ((row.recoveryCodes ?? []) as string[]).filter(\n c => c !== hashedCode,\n );\n this.db\n .update(schema.users)\n .set({ recoveryCodes: codes })\n .where(eq(schema.users.id, row.id))\n .run();\n }\n\n async resetUserMfa(username: string) {\n this.db\n .update(schema.users)\n .set({\n totpEnabled: false,\n totpSecret: null,\n passkeys: [],\n recoveryCodes: [],\n })\n .where(eq(schema.users.username, username))\n .run();\n }\n\n async getAllRoles(): Promise<(IStoreRole & { id: string })[]> {\n if (this.rolesCache) return this.rolesCache;\n const rows = this.db\n .select()\n .from(schema.webRoles)\n .orderBy(desc(schema.webRoles.order))\n .all();\n const roles = rows.map(r => ({\n type: 'webRole' as const,\n id: r.id,\n name: r.name,\n description: r.description,\n order: r.order,\n permissions: r.permissions as PermissionSet,\n }));\n this.rolesCache = roles;\n return roles;\n }\n\n invalidateRolesCache() {\n this.rolesCache = null;\n this.defaultPermissionsCache = null;\n }\n\n async getRole(id: string): Promise<(IStoreRole & { id: string }) | null> {\n const row = this.db\n .select()\n .from(schema.webRoles)\n .where(eq(schema.webRoles.id, id))\n .get();\n if (!row) return null;\n return {\n type: 'webRole',\n id: row.id,\n name: row.name,\n description: row.description,\n order: row.order,\n permissions: row.permissions as PermissionSet,\n };\n }\n\n async createRole(\n name: string,\n description: string,\n permissions: PermissionSet,\n ): Promise<IStoreRole & { id: string }> {\n const id = randomUUID();\n const tx = this.sqlite.transaction(() => {\n this.db\n .update(schema.webRoles)\n .set({ order: sql`${schema.webRoles.order} + 1` })\n .run();\n this.db\n .insert(schema.webRoles)\n .values({\n id,\n name,\n description,\n order: 1,\n permissions: permissions,\n })\n .run();\n });\n tx();\n this.invalidateRolesCache();\n return {\n type: 'webRole',\n id,\n name,\n description,\n order: 1,\n permissions: permissions,\n };\n }\n\n async updateRole(\n id: string,\n updates: Partial<Pick<IStoreRole, 'name' | 'description' | 'order'>> & {\n permissions?: PermissionSet;\n },\n ): Promise<void> {\n const $set: Record<string, any> = {};\n if (updates.name !== undefined) $set.name = updates.name;\n if (updates.description !== undefined)\n $set.description = updates.description;\n if (updates.order !== undefined) $set.order = updates.order;\n if (updates.permissions !== undefined)\n $set.permissions = updates.permissions;\n if (Object.keys($set).length > 0) {\n this.db\n .update(schema.webRoles)\n .set($set)\n .where(eq(schema.webRoles.id, id))\n .run();\n }\n this.invalidateRolesCache();\n }\n\n async deleteRole(id: string): Promise<string> {\n const role = await this.getRole(id);\n if (!role) return 'role not found';\n\n const tx = this.sqlite.transaction(() => {\n this.db.delete(schema.webRoles).where(eq(schema.webRoles.id, id)).run();\n const usersWithRole = this.db\n .select({ id: schema.users.id, roles: schema.users.roles })\n .from(schema.users)\n .all();\n for (const u of usersWithRole) {\n const roles = ((u.roles ?? []) as string[]).filter(r => r !== id);\n if (roles.length !== ((u.roles ?? []) as string[]).length) {\n this.db\n .update(schema.users)\n .set({ roles })\n .where(eq(schema.users.id, u.id))\n .run();\n }\n }\n });\n tx();\n this.invalidateRolesCache();\n return '';\n }\n\n async reorderRoles(entries: { id: string; order: number }[]): Promise<void> {\n const tx = this.sqlite.transaction(() => {\n for (const { id, order } of entries) {\n this.db\n .update(schema.webRoles)\n .set({ order })\n .where(eq(schema.webRoles.id, id))\n .run();\n }\n });\n tx();\n this.invalidateRolesCache();\n }\n\n async getUserRolePermissions(user: IStoreUser): Promise<PermissionSet[]> {\n const defaults = await this.getDefaultPermissions();\n const defaultPerms: PermissionSet = {\n root: defaults.root,\n domains: defaults.domains,\n scopes: defaults.scopes,\n };\n const allRoles = await this.getAllRoles();\n const userRoles = allRoles.filter(r => user.roles.includes(r.id));\n return [defaultPerms, ...userRoles.map(r => r.permissions)];\n }\n\n async getUserAssignedRoles(\n user: IStoreUser,\n ): Promise<(IStoreRole & { id: string })[]> {\n const allRoles = await this.getAllRoles();\n return allRoles.filter(r => user.roles.includes(r.id));\n }\n\n // add a chat message to the chat log store\n async addChatLog(\n action: 'msg' | 'server' | 'leave' | 'join',\n user: IPlayer,\n message?: string,\n ) {\n this.calendar.addDate(Date.now());\n const row = this.db\n .insert(schema.chatLogs)\n .values({\n created: Date.now(),\n instanceId: await this.getInstanceId(),\n action,\n user,\n ...(message ? { message } : {}),\n })\n .returning()\n .get();\n return {\n type: 'chat' as const,\n _id: String(row.id),\n id: row.id,\n created: row.created,\n instanceId: row.instanceId,\n action: row.action,\n user: row.user,\n message: row.message,\n };\n }\n\n // get recent chat activity\n async getChats({\n count: limit = 50,\n sameServer,\n before,\n after,\n }: {\n count?: number;\n sameServer?: boolean;\n before?: number;\n after?: number;\n } = {}) {\n const conditions = [];\n if (sameServer) {\n conditions.push(\n eq(schema.chatLogs.instanceId, await this.getInstanceId()),\n );\n }\n if (before) {\n conditions.push(lt(schema.chatLogs.created, before));\n } else if (after) {\n conditions.push(gt(schema.chatLogs.created, after));\n } else {\n conditions.push(lt(schema.chatLogs.created, Date.now()));\n }\n\n const orderDir = !before && after ? asc : desc;\n const rows = this.db\n .select()\n .from(schema.chatLogs)\n .where(conditions.length > 1 ? and(...conditions) : conditions[0])\n .orderBy(orderDir(schema.chatLogs.created))\n .limit(limit)\n .all();\n\n return rows.map(r => ({\n type: 'chat' as const,\n _id: String(r.id),\n created: r.created,\n instanceId: r.instanceId,\n action: r.action,\n user: r.user,\n message: r.message,\n }));\n }\n\n // get paginated players\n async getPlayers({\n count: limit = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n limitId = undefined,\n }: {\n count?: number;\n search?: string;\n page?: number;\n sort?: string;\n direction?: number;\n limitId?: string[];\n } = {}) {\n const pattern = search.length > 0 ? explode(search) : null;\n const patternSource = pattern?.source;\n\n // an empty filter (e.g. \"banned\" with no active bans) matches nothing\n if (limitId && limitId.length === 0)\n return { pages: 0, total: 0, players: [] };\n\n const conditions: SQL[] = [];\n\n if (limitId) {\n conditions.push(inArray(schema.playerHistory.id, limitId));\n }\n\n if (patternSource) {\n // match name history values via json_each so the pattern can't hit\n // JSON keys or date timestamps\n conditions.push(sql`(\n ${schema.playerHistory.id} = ${search}\n OR ${schema.playerHistory.name} REGEXP ${patternSource}\n OR ${schema.playerHistory.displayName} REGEXP ${patternSource}\n OR EXISTS (\n SELECT 1 FROM json_each(${schema.playerHistory.nameHistory})\n WHERE json_each.type = 'object'\n AND (json_extract(json_each.value, '$.name') REGEXP ${patternSource}\n OR json_extract(json_each.value, '$.displayName') REGEXP ${patternSource})\n )\n )`);\n }\n\n const baseWhere = conditions.length > 0 ? and(...conditions) : undefined;\n\n const sortCol =\n sort === 'lastSeen'\n ? schema.playerHistory.lastSeen\n : sort === 'created'\n ? schema.playerHistory.created\n : sort === 'heartbeats'\n ? schema.playerHistory.heartbeats\n : sort === 'sessions'\n ? schema.playerHistory.sessions\n : sql`${schema.playerHistory.name} COLLATE NOCASE`;\n const orderDir = direction === 1 ? asc : desc;\n\n const totalResult = this.db\n .select({ count: count() })\n .from(schema.playerHistory)\n .where(baseWhere)\n .get();\n const players = this.db\n .select()\n .from(schema.playerHistory)\n .where(baseWhere)\n .orderBy(orderDir(sortCol))\n .limit(limit)\n .offset(limit * page)\n .all();\n\n const total = totalResult?.count ?? 0;\n const result = players.map(p => this._toUserHistory(p));\n\n // exact match detection\n if (search.length > 0 && page === 0) {\n const exactRows = this.db\n .select()\n .from(schema.playerHistory)\n .where(\n or(\n eq(schema.playerHistory.id, search),\n eq(schema.playerHistory.name, search),\n eq(schema.playerHistory.displayName, search),\n ),\n )\n .all();\n if (exactRows.length > 0) {\n for (const res of exactRows) {\n const idx = result.findIndex(p => p.id === res.id);\n if (idx >= 0) result.splice(idx, 1);\n }\n result.splice(0, 0, ...exactRows.map(r => this._toUserHistory(r)));\n }\n }\n\n return { pages: Math.ceil(total / limit), total, players: result };\n }\n\n // get paginated users\n async getUsers({\n count: limit = 50,\n search = '',\n page = 0,\n sort = 'name',\n direction = 1,\n } = {}) {\n const pattern = search.length > 0 ? explode(search) : null;\n\n let baseWhere = sql`1=1`;\n if (pattern) {\n baseWhere = sql`(\n ${schema.users.playerId} = ${search}\n OR ${schema.users.username} REGEXP ${pattern.source}\n )`;\n }\n\n const sortCol =\n sort === 'lastOnline'\n ? schema.users.lastOnline\n : sort === 'created'\n ? schema.users.created\n : sql`${schema.users.username} COLLATE NOCASE`;\n const orderDir = direction === 1 ? asc : desc;\n\n const totalResult = this.db\n .select({ count: count() })\n .from(schema.users)\n .where(baseWhere)\n .get();\n const users = this.db\n .select()\n .from(schema.users)\n .where(baseWhere)\n .orderBy(orderDir(sortCol))\n .limit(limit)\n .offset(limit * page)\n .all();\n\n return {\n pages: Math.ceil((totalResult?.count ?? 0) / limit),\n total: totalResult?.count ?? 0,\n users: users.map(u => this._toStoreUser(u)),\n };\n }\n\n // get an individual player, ban and kick history, and notes\n async getPlayer(id: string) {\n const player = this.db\n .select()\n .from(schema.playerHistory)\n .where(eq(schema.playerHistory.id, id))\n .get();\n if (!player) return null;\n\n const banHist = this.db\n .select()\n .from(schema.banHistory)\n .where(eq(schema.banHistory.banned, id))\n .orderBy(desc(schema.banHistory.created))\n .limit(25)\n .all();\n\n const kickHist = this.db\n .select()\n .from(schema.kickHistory)\n .where(eq(schema.kickHistory.kicked, id))\n .orderBy(desc(schema.kickHistory.created))\n .limit(25)\n .all();\n\n const noteRows = this.db\n .select()\n .from(schema.playerNotes)\n .where(eq(schema.playerNotes.playerId, id))\n .limit(25)\n .all();\n\n return {\n ...this._toUserHistory(player),\n banHistory: banHist.map(b => ({\n type: 'banHistory' as const,\n _id: String(b.id),\n banned: b.banned,\n bannerId: b.bannerId,\n created: b.created,\n expires: b.expires,\n reason: b.reason,\n })),\n kickHistory: kickHist.map(k => ({\n type: 'kickHistory' as const,\n _id: String(k.id),\n kicked: k.kicked,\n kickerId: k.kickerId,\n created: k.created,\n reason: k.reason,\n })),\n notes: noteRows.map(n => ({\n type: 'note' as const,\n _id: String(n.id),\n id: n.playerId,\n note: n.note,\n })),\n };\n }\n\n // add a user to the visit history, returns true if this is a first visit\n async addVisit(user: Required<IPlayer>) {\n const now = Date.now();\n // resolve the instance id before opening the transaction — the tx callback\n // must stay synchronous, and this is the only await in the method\n const instanceId = await this.getInstanceId();\n\n // read-modify-write the row atomically so two near-simultaneous joins for\n // the same id can't both insert (PK conflict) or clobber each other's\n // nameHistory/counter updates from a shared stale snapshot\n const visit = this.sqlite.transaction(() => {\n const existing = this.db\n .select()\n .from(schema.playerHistory)\n .where(eq(schema.playerHistory.id, user.id))\n .get();\n\n if (!existing) {\n this.db\n .insert(schema.playerHistory)\n .values({\n id: user.id,\n name: user.name,\n displayName: user.displayName,\n nameHistory: [\n { name: user.name, displayName: user.displayName, date: now },\n ],\n ips: [],\n created: now,\n lastSeen: now,\n lastInstanceId: instanceId,\n heartbeats: 0,\n sessions: 1,\n instances: 1,\n })\n .run();\n return true;\n }\n\n const nameHistory = (existing.nameHistory ?? []) as {\n name: string;\n displayName: string;\n date: number;\n }[];\n const hasName = nameHistory.some(\n h => h.name === user.name && h.displayName === user.displayName,\n );\n if (!hasName) {\n nameHistory.push({\n name: user.name,\n displayName: user.displayName,\n date: now,\n });\n }\n\n this.db\n .update(schema.playerHistory)\n .set({\n lastSeen: now,\n name: user.name,\n displayName: user.displayName,\n lastInstanceId: instanceId,\n nameHistory,\n sessions: sql`${schema.playerHistory.sessions} + ${existing.lastSeen < now - 60 * 60 * 1000 ? 1 : 0}`,\n instances: sql`${schema.playerHistory.instances} + ${existing.lastInstanceId !== instanceId ? 1 : 0}`,\n })\n .where(eq(schema.playerHistory.id, existing.id))\n .run();\n return false;\n });\n\n return visit();\n }\n\n // use data from minutely heartbeats to fuel metrics\n async addHeartbeat(data: {\n bricks: number;\n players: string[];\n ips: Record<string, string>;\n }) {\n const now = Date.now();\n const tx = this.sqlite.transaction(() => {\n this.db\n .insert(schema.heartbeats)\n .values({\n created: now,\n bricks: data.bricks,\n players: data.players,\n })\n .run();\n\n if (data.players.length > 0) {\n // one bulk UPDATE for the shared lastSeen/heartbeats bump\n this.db\n .update(schema.playerHistory)\n .set({\n lastSeen: now,\n heartbeats: sql`${schema.playerHistory.heartbeats} + 1`,\n })\n .where(inArray(schema.playerHistory.id, data.players))\n .run();\n\n const players = this.db\n .select()\n .from(schema.playerHistory)\n .where(inArray(schema.playerHistory.id, data.players))\n .all();\n\n for (const p of players) {\n const ip = data.ips[p.id];\n if (ip && !((p.ips ?? []) as string[]).includes(ip)) {\n const ips = [...((p.ips ?? []) as string[]), ip];\n this.db\n .update(schema.playerHistory)\n .set({ ips })\n .where(eq(schema.playerHistory.id, p.id))\n .run();\n }\n }\n }\n });\n tx();\n }\n\n upsertBanHistory(entry: Omit<IStoreBanHistory, 'type'>) {\n this.db\n .insert(schema.banHistory)\n .values({\n banned: entry.banned,\n bannerId: entry.bannerId ?? '',\n created: typeof entry.created === 'number' ? entry.created : 0,\n expires: typeof entry.expires === 'number' ? entry.expires : 0,\n reason: entry.reason ?? '',\n })\n .onConflictDoNothing()\n .run();\n }\n\n upsertKickHistory(entry: Omit<IStoreKickHistory, 'type'>) {\n this.db\n .insert(schema.kickHistory)\n .values({\n kicked: entry.kicked,\n kickerId: entry.kickerId ?? '',\n created: entry.created ?? 0,\n reason: entry.reason ?? '',\n })\n .onConflictDoNothing()\n .run();\n }\n\n async getAutoRestartConfig(): Promise<IStoreAutoRestartConfig> {\n const row = this.db\n .select()\n .from(schema.serverConfig)\n .where(eq(schema.serverConfig.key, 'autoRestartConfig'))\n .get();\n if (!row) {\n const config: IStoreAutoRestartConfig = {\n type: 'autoRestartConfig',\n maxUptime: 48,\n maxUptimeEnabled: false,\n emptyUptime: 24,\n emptyUptimeEnabled: false,\n dailyHour: 2,\n dailyHourEnabled: false,\n autoUpdateEnabled: true,\n autoUpdateIntervalMins: 60,\n announcementEnabled: true,\n playersEnabled: true,\n saveWorld: true,\n crashRestartEnabled: true,\n };\n this.db\n .insert(schema.serverConfig)\n .values({ key: 'autoRestartConfig', value: config })\n .run();\n return config;\n }\n const config = row.value as IStoreAutoRestartConfig;\n config.type = 'autoRestartConfig';\n config.autoUpdateEnabled ??= true;\n config.autoUpdateIntervalMins ??= 60;\n config.saveWorld ??= true;\n config.crashRestartEnabled ??= true;\n return config;\n }\n\n async setAutoRestartConfig(config: IStoreAutoRestartConfig): Promise<void> {\n config.maxUptime = Math.round(Math.max(1, Math.min(config.maxUptime, 168)));\n config.emptyUptime = Math.round(\n Math.max(1, Math.min(config.emptyUptime, 168)),\n );\n config.dailyHour = Math.round(Math.max(0, Math.min(config.dailyHour, 23)));\n config.autoUpdateIntervalMins = Math.round(\n Math.max(10, Math.min(config.autoUpdateIntervalMins ?? 60, Infinity)),\n );\n config.autoUpdateEnabled ??= true;\n this.db\n .insert(schema.serverConfig)\n .values({ key: 'autoRestartConfig', value: config })\n .onConflictDoUpdate({\n target: schema.serverConfig.key,\n set: { value: config },\n })\n .run();\n }\n\n // update player online-time punchcard\n async updatePlayerPunchcard(numNewPlayers: number) {\n const now = new Date();\n const time = now.getTime();\n const hour = now.getUTCHours();\n const day = now.getUTCDay();\n const month = now.getUTCMonth();\n const year = now.getUTCFullYear();\n\n const card = this.db\n .select()\n .from(schema.punchcards)\n .where(\n and(\n eq(schema.punchcards.kind, 'playerCount'),\n eq(schema.punchcards.month, month),\n eq(schema.punchcards.year, year),\n ),\n )\n .get();\n\n if (!card) {\n const punchcard = createPunchcard();\n punchcard[day][hour] = numNewPlayers;\n this.db\n .insert(schema.punchcards)\n .values({\n kind: 'playerCount',\n created: time,\n updated: time,\n month,\n year,\n punchcard,\n })\n .run();\n } else {\n const punchcard = card.punchcard as number[][];\n punchcard[day][hour] += numNewPlayers;\n this.db\n .update(schema.punchcards)\n .set({ punchcard, updated: time })\n .where(eq(schema.punchcards.id, card.id))\n .run();\n }\n }\n\n private _toStoreUser(\n row: typeof schema.users.$inferSelect,\n ): IStoreUser & { id: string; _id: string } {\n const user: IStoreUser & { id: string; _id: string } = {\n type: 'user',\n id: row.id,\n _id: row.id,\n created: row.created,\n lastOnline: row.lastOnline,\n username: row.username,\n hash: row.hash,\n isOwner: row.isOwner,\n roles: (row.roles ?? []) as string[],\n playerId: row.playerId,\n isBanned: row.isBanned,\n permissions:\n (row.permissions as PermissionSet | null) ?? EMPTY_PERMISSIONS,\n totpSecret: row.totpSecret ?? undefined,\n totpEnabled: row.totpEnabled,\n passkeys: (row.passkeys ?? []) as IWebAuthnCredential[],\n recoveryCodes: (row.recoveryCodes ?? []) as string[],\n };\n return user;\n }\n\n private _toUserHistory(\n row: typeof schema.playerHistory.$inferSelect,\n ): IUserHistory & { _id: string } {\n return {\n type: 'userHistory',\n _id: row.id,\n id: row.id,\n name: row.name,\n displayName: row.displayName,\n nameHistory: (row.nameHistory ?? []) as {\n displayName: string;\n name: string;\n date: number;\n }[],\n ips: (row.ips ?? []) as string[],\n created: row.created,\n lastSeen: row.lastSeen,\n lastInstanceId: row.lastInstanceId,\n heartbeats: row.heartbeats,\n sessions: row.sessions,\n instances: row.instances,\n };\n }\n}\n"],"names":["Calendar","randomUUID","schema.serverInstances","schema.chatLogs","parseBrickadiaTime","count","schema.users","EMPTY_PERMISSIONS","eq","serverEvents","permissions","schema.serverConfig","RootLevel","or","and","sql","schema.webRoles","desc","lt","gt","asc","pattern","explode","inArray","schema.playerHistory","schema.banHistory","schema.kickHistory","schema.playerNotes","schema.heartbeats","config","time","schema.punchcards"],"mappings":"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"}
@@ -2,14 +2,18 @@
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
3
  const logger = require("../../logger.js");
4
4
  const softconfig = require("../../softconfig.js");
5
+ const connection = require("../../db/connection.js");
6
+ const migrate = require("../../db/migrate.js");
7
+ const nedbImport = require("../../db/nedbImport.js");
5
8
  const express$1 = require("@trpc/server/adapters/express");
6
9
  const bodyParser = require("body-parser");
10
+ const betterSqlite3 = require("drizzle-orm/better-sqlite3");
7
11
  const express = require("express");
8
12
  const expressSession = require("express-session");
9
13
  const hasbin = require("hasbin");
10
14
  const http = require("http");
11
15
  const https = require("https");
12
- const NedbStore = require("nedb-promises-session-store");
16
+ const BetterSqlite3SessionStore = require("better-sqlite3-session-store");
13
17
  const path = require("path");
14
18
  const bcrypt = require("bcryptjs");
15
19
  const server$1 = require("@simplewebauthn/server");
@@ -34,7 +38,10 @@ class Webserver {
34
38
  this.options = options;
35
39
  this.omegga = omegga;
36
40
  this.dataPath = path.join(omegga.path, softconfig.default.DATA_PATH);
37
- this.database = new database.default(options, omegga);
41
+ const mainSqlite = connection.openDb(path.join(this.dataPath, softconfig.default.MAIN_DB));
42
+ const mainDb = betterSqlite3.drizzle(mainSqlite);
43
+ migrate.runMigrations(mainDb);
44
+ this.database = new database.default(options, omegga, mainSqlite, mainDb);
38
45
  this.https = false;
39
46
  this.started = false;
40
47
  this.created = this.createServer();
@@ -42,7 +49,11 @@ class Webserver {
42
49
  // create the webserver
43
50
  async createServer() {
44
51
  const hasOpenSSL = hasbin.sync("openssl");
45
- await this.database.doMigrations();
52
+ await nedbImport.importNedbIfNeeded(
53
+ this.dataPath,
54
+ this.database.sqlite,
55
+ this.database.db
56
+ );
46
57
  this.app = express();
47
58
  const pickProtocol = async () => {
48
59
  if (this.options.https) {
@@ -76,6 +87,8 @@ class Webserver {
76
87
  };
77
88
  this.server = await pickProtocol();
78
89
  this.app.set("trust proxy", 1);
90
+ const SqliteStore = (BetterSqlite3SessionStore["default"] ?? BetterSqlite3SessionStore)(expressSession);
91
+ const sessionSqlite = connection.openDb(path.join(this.dataPath, softconfig.default.SESSIONS_DB));
79
92
  const session = expressSession({
80
93
  secret: util.getSessionSecret(this.dataPath),
81
94
  resave: false,
@@ -86,11 +99,9 @@ class Webserver {
86
99
  maxAge: 365 * 24 * 60 * 60 * 1e3
87
100
  // 1 year
88
101
  },
89
- // use a nedb database for session store
90
- // For some reason vite is not importing the default export properly without this...
91
- store: NedbStore["default"]({
92
- filename: path.join(this.dataPath, softconfig.default.SESSION_STORE),
93
- connect: expressSession
102
+ store: new SqliteStore({
103
+ client: sessionSqlite,
104
+ expired: { clear: true, intervalMs: 9e5 }
94
105
  })
95
106
  });
96
107
  this.app.use(session);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/webserver/backend/index.ts"],"sourcesContent":["import Logger from '@/logger';\nimport soft from '@/softconfig';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { IServerStatus } from '@omegga/types';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport bodyParser from 'body-parser';\nimport express from 'express';\nimport expressSession from 'express-session';\nimport hasbin from 'hasbin';\nimport http from 'http';\nimport https from 'https';\nimport NedbStore from 'nedb-promises-session-store';\nimport path from 'path';\nimport bcrypt from 'bcryptjs';\nimport {\n generateAuthenticationOptions,\n verifyAuthenticationResponse,\n} from '@simplewebauthn/server';\nimport Database from './database';\nimport { verifyToken as verifyTOTP } from './totp';\nimport setupMetrics from './metrics';\nimport { appRouter } from './router';\nimport { setWebserver } from './router/server';\nimport { createContext, setContextDeps } from './trpc';\nimport * as util from './util';\n\n// path to vite-built data\nconst PUBLIC_PATH = path.join(__dirname, '../../../public');\nconst ASSETS_PATH = path.join(__dirname, '../../../public/assets');\n\nconst log = (...args: any[]) => Logger.log(...args);\nconst error = (...args: any[]) => Logger.error(...args);\n\n// the webserver serves an authenticated web UI\nexport default class Webserver {\n database: Database;\n port: number;\n omegga: Omegga;\n\n app: express.Express;\n server: http.Server | https.Server;\n\n options: IServerConfig;\n https: boolean;\n started: boolean;\n created: Promise<boolean>;\n\n serverStatusInterval: ReturnType<typeof setInterval>;\n lastReportedStatus: IServerStatus | null = null;\n\n dataPath: string;\n\n // create a webserver\n constructor(options: IServerConfig, omegga: Omegga) {\n this.port = Number(\n options.port || process.env.OMEGGA_PORT || soft.DEFAULT_PORT,\n );\n this.options = options;\n this.omegga = omegga;\n this.dataPath = path.join(omegga.path, soft.DATA_PATH);\n\n // the database provides omegga with metrics, chat logs, and more\n // to help administrators keep track of their users and server\n this.database = new Database(options, omegga);\n\n // https status of the server\n this.https = false;\n // started status of the server\n this.started = false;\n this.created = this.createServer();\n }\n\n // create the webserver\n async createServer() {\n const hasOpenSSL = hasbin.sync('openssl');\n\n // let the database do migrations\n await this.database.doMigrations();\n\n // create express app\n this.app = express();\n\n // create a server with https if applicable\n const pickProtocol = async () => {\n if (this.options.https) {\n if (hasOpenSSL) {\n const certFile = await util.getSSLKeys(this.dataPath);\n if (certFile) {\n // handy notification of generated ssl certs\n if (certFile.new) log('>>'.green, 'Generated new SSL certificate');\n\n this.https = true;\n return https.createServer(\n {\n key: certFile.keys.serviceKey,\n cert: certFile.keys.certificate,\n },\n this.app,\n );\n } else {\n error(\n '!>'.red,\n 'Error generating SSL certificate - falling back to http',\n );\n }\n }\n\n // warn the user\n log(\n ':>'.yellow,\n 'Running web server with http - install',\n 'openssl'.yellow.underline,\n 'for https (more secure)',\n );\n }\n\n // fallback to http\n return new http.Server(this.app);\n };\n\n // create http(s) server based on openssl availability\n this.server = await pickProtocol();\n\n this.app.set('trust proxy', 1);\n const session = expressSession({\n secret: util.getSessionSecret(this.dataPath),\n resave: false,\n saveUninitialized: false,\n cookie: {\n path: '/',\n secure: this.https,\n maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year\n },\n // use a nedb database for session store\n // For some reason vite is not importing the default export properly without this...\n store: (NedbStore['default'] as typeof NedbStore)({\n filename: path.join(this.dataPath, soft.SESSION_STORE),\n connect: expressSession,\n }),\n });\n this.app.use(session);\n\n // setup routes and webserver\n await this.initWebUI();\n return true;\n }\n\n // setup the web ui routes\n async initWebUI() {\n await this.database.getInstanceId();\n\n // Initialize tRPC context deps\n setContextDeps({\n database: this.database,\n omegga: this.omegga,\n });\n setWebserver(this);\n\n // provide assets in the /public path\n this.app.use('/assets', express.static(ASSETS_PATH));\n this.app.use(bodyParser.json());\n\n // --- Auth routes (outside tRPC) ---\n const openApi = express.Router();\n const api = express.Router();\n\n // check if this is the first user in the database\n openApi.get('/first', async (req, res) =>\n res.json(await this.database.isFirstUser()),\n );\n\n // login / create admin user route\n openApi.post('/auth', async (req, res) => {\n if (\n typeof req.body !== 'object' ||\n typeof req.body.username !== 'string' ||\n typeof req.body.password !== 'string'\n ) {\n return res.status(422).json({ message: 'invalid body' });\n }\n const { username, password } = req.body;\n\n if (!username.match(/^\\w{0,32}$/)) {\n return res.status(422).json({ message: 'invalid body' });\n }\n\n const isFirst = await this.database.isFirstUser();\n let user;\n if (isFirst) {\n user = await this.database.createAdminUser(\n username,\n username === '' ? '' : password,\n );\n } else {\n user = await this.database.authUser(username, password);\n }\n\n if (user) {\n req.session.userId = user._id;\n if (user.totpEnabled) {\n req.session.mfaPending = true;\n req.session.save();\n res.status(200).json({ mfaRequired: true });\n } else {\n req.session.save();\n res.status(200).json({});\n }\n } else {\n res.status(404).json({ message: 'no user found' });\n }\n });\n\n // rate limiting for MFA verification (per session)\n const mfaAttempts = new Map<\n string,\n { count: number; lockedUntil: number }\n >();\n\n // TOTP verification during MFA challenge\n openApi.post('/auth/mfa/totp', async (req, res) => {\n if (!req.session.mfaPending || !req.session.userId) {\n return res.status(400).json({ message: 'no pending MFA challenge' });\n }\n\n // rate limit: 5 attempts, then 60s lockout\n const sid = req.sessionID;\n const attempt = mfaAttempts.get(sid) ?? { count: 0, lockedUntil: 0 };\n if (Date.now() < attempt.lockedUntil) {\n return res\n .status(429)\n .json({ message: 'too many attempts, try again later' });\n }\n\n const { code } = req.body ?? {};\n if (typeof code !== 'string') {\n return res.status(422).json({ message: 'code required' });\n }\n const user = await this.database.findUserById(req.session.userId);\n if (!user || !user.totpSecret) {\n return res.status(400).json({ message: 'MFA not configured' });\n }\n\n let verified = false;\n\n if (verifyTOTP(code, user.totpSecret)) {\n verified = true;\n }\n\n // try recovery codes\n if (!verified && user.recoveryCodes?.length) {\n for (const hashedCode of user.recoveryCodes) {\n if (await bcrypt.compare(code, hashedCode)) {\n await this.database.removeRecoveryCode(user.username, hashedCode);\n verified = true;\n break;\n }\n }\n }\n\n if (verified) {\n mfaAttempts.delete(sid);\n delete req.session.mfaPending;\n req.session.save();\n return res.status(200).json({});\n }\n\n attempt.count++;\n if (attempt.count >= 5) {\n attempt.lockedUntil = Date.now() + 60_000;\n attempt.count = 0;\n }\n mfaAttempts.set(sid, attempt);\n res.status(401).json({ message: 'invalid code' });\n });\n\n const getRpID = (req: express.Request) => {\n const origin = req.headers.origin;\n if (origin) {\n try {\n return new URL(origin as string).hostname;\n } catch {}\n }\n return req.hostname;\n };\n\n // WebAuthn authentication options (passwordless login)\n openApi.get('/auth/webauthn/options', async (req, res) => {\n const rpID = getRpID(req);\n const options = await generateAuthenticationOptions({ rpID });\n req.session.mfaChallenge = options.challenge;\n req.session.save();\n res.json(options);\n });\n\n // WebAuthn authentication verification\n openApi.post('/auth/webauthn/verify', async (req, res) => {\n const challenge = req.session.mfaChallenge;\n if (!challenge) {\n return res.status(400).json({ message: 'no pending challenge' });\n }\n // clear challenge immediately to prevent replay\n delete req.session.mfaChallenge;\n req.session.save();\n try {\n const { credential } = req.body;\n const user = await this.database.findUserByPasskeyId(credential?.id);\n if (!user || user.isBanned) {\n return res.status(401).json({ message: 'unknown credential' });\n }\n const passkey = user.passkeys!.find(p => p.id === credential.id)!;\n const verification = await verifyAuthenticationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin:\n req.headers.origin ?? `${req.protocol}://${req.get('host')}`,\n expectedRPID: getRpID(req),\n credential: {\n id: passkey.id,\n publicKey: Buffer.from(passkey.publicKey, 'base64url'),\n counter: passkey.counter,\n transports: passkey.transports as any,\n },\n });\n if (!verification.verified) {\n return res.status(401).json({ message: 'verification failed' });\n }\n await this.database.updatePasskeyCounter(\n user.username,\n passkey.id,\n verification.authenticationInfo.newCounter,\n );\n req.session.userId = (user as any)._id;\n delete req.session.mfaPending;\n req.session.save();\n res.status(200).json({});\n } catch (e) {\n res.status(400).json({ message: 'verification error' });\n }\n });\n\n // authentication middleware for protected api routes\n api.all('*', (req, _res, next) => {\n (async () => {\n if (req.session.mfaPending) return next(new Error('unauthorized'));\n const user = await this.database.findUserById(req.session.userId);\n if (!user || user.isBanned) return next(new Error('unauthorized'));\n next();\n })();\n });\n\n // kill a session\n api.get('/logout', (req, res) => {\n req.session.destroy(e => {\n res.status(e ? 500 : 200).json({});\n });\n });\n\n this.app.use('/api/v1', openApi);\n this.app.use('/api/v1', api);\n\n // --- tRPC ---\n this.app.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext,\n }),\n );\n\n // setup metrics and tracking\n setupMetrics(this);\n\n // every request goes through the index file (frontend handles 404s)\n this.app.use(async (req, res) => {\n if (req.session.mfaPending) {\n return res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n const user = await this.database.findUserById(req.session.userId);\n const isAuth = user && !user.isBanned;\n if (isAuth) {\n res.sendFile(path.join(PUBLIC_PATH, 'app.html'));\n } else {\n res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n });\n }\n\n // start the webserver\n async start() {\n if (this.started) return;\n await this.created;\n return await new Promise<void>(resolve => {\n this.server.listen(this.port, () => {\n log(\n `${'>>'.green} Web UI available at`,\n `http${this.https ? 's' : ''}://127.0.0.1:${this.port}`.green,\n );\n this.started = true;\n this.database.addChatLog('server', {}, 'Server started');\n resolve();\n });\n });\n }\n\n // stop the webserver\n stop() {\n this.database.addChatLog('server', {}, 'Server stopped');\n this.server.close();\n this.started = false;\n clearInterval(this.serverStatusInterval);\n }\n}\n"],"names":["Logger","soft","Database","util.getSSLKeys","util.getSessionSecret","setContextDeps","setWebserver","verifyTOTP","generateAuthenticationOptions","verifyAuthenticationResponse","createExpressMiddleware","appRouter","createContext","setupMetrics"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/webserver/backend/index.ts"],"sourcesContent":["import Logger from '@/logger';\nimport soft from '@/softconfig';\nimport { openDb } from '@/db/connection';\nimport { runMigrations } from '@/db/migrate';\nimport { importNedbIfNeeded } from '@/db/nedbImport';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { IServerStatus } from '@omegga/types';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport bodyParser from 'body-parser';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport express from 'express';\nimport expressSession from 'express-session';\nimport hasbin from 'hasbin';\nimport http from 'http';\nimport https from 'https';\nimport BetterSqlite3SessionStore from 'better-sqlite3-session-store';\nimport path from 'path';\nimport bcrypt from 'bcryptjs';\nimport {\n generateAuthenticationOptions,\n verifyAuthenticationResponse,\n} from '@simplewebauthn/server';\nimport Database from './database';\nimport { verifyToken as verifyTOTP } from './totp';\nimport setupMetrics from './metrics';\nimport { appRouter } from './router';\nimport { setWebserver } from './router/server';\nimport { createContext, setContextDeps } from './trpc';\nimport * as util from './util';\n\n// path to vite-built data\nconst PUBLIC_PATH = path.join(__dirname, '../../../public');\nconst ASSETS_PATH = path.join(__dirname, '../../../public/assets');\n\nconst log = (...args: any[]) => Logger.log(...args);\nconst error = (...args: any[]) => Logger.error(...args);\n\n// the webserver serves an authenticated web UI\nexport default class Webserver {\n database: Database;\n port: number;\n omegga: Omegga;\n\n app: express.Express;\n server: http.Server | https.Server;\n\n options: IServerConfig;\n https: boolean;\n started: boolean;\n created: Promise<boolean>;\n\n serverStatusInterval: ReturnType<typeof setInterval>;\n lastReportedStatus: IServerStatus | null = null;\n\n dataPath: string;\n\n // create a webserver\n constructor(options: IServerConfig, omegga: Omegga) {\n this.port = Number(\n options.port || process.env.OMEGGA_PORT || soft.DEFAULT_PORT,\n );\n this.options = options;\n this.omegga = omegga;\n this.dataPath = path.join(omegga.path, soft.DATA_PATH);\n\n const mainSqlite = openDb(path.join(this.dataPath, soft.MAIN_DB));\n const mainDb = drizzle(mainSqlite);\n\n runMigrations(mainDb);\n\n // the database provides omegga with metrics, chat logs, and more\n // to help administrators keep track of their users and server\n this.database = new Database(options, omegga, mainSqlite, mainDb);\n\n // https status of the server\n this.https = false;\n // started status of the server\n this.started = false;\n this.created = this.createServer();\n }\n\n // create the webserver\n async createServer() {\n const hasOpenSSL = hasbin.sync('openssl');\n\n await importNedbIfNeeded(\n this.dataPath,\n this.database.sqlite,\n this.database.db,\n );\n\n // create express app\n this.app = express();\n\n // create a server with https if applicable\n const pickProtocol = async () => {\n if (this.options.https) {\n if (hasOpenSSL) {\n const certFile = await util.getSSLKeys(this.dataPath);\n if (certFile) {\n // handy notification of generated ssl certs\n if (certFile.new) log('>>'.green, 'Generated new SSL certificate');\n\n this.https = true;\n return https.createServer(\n {\n key: certFile.keys.serviceKey,\n cert: certFile.keys.certificate,\n },\n this.app,\n );\n } else {\n error(\n '!>'.red,\n 'Error generating SSL certificate - falling back to http',\n );\n }\n }\n\n // warn the user\n log(\n ':>'.yellow,\n 'Running web server with http - install',\n 'openssl'.yellow.underline,\n 'for https (more secure)',\n );\n }\n\n // fallback to http\n return new http.Server(this.app);\n };\n\n // create http(s) server based on openssl availability\n this.server = await pickProtocol();\n\n this.app.set('trust proxy', 1);\n const SqliteStore = (\n BetterSqlite3SessionStore['default'] ?? BetterSqlite3SessionStore\n )(expressSession);\n const sessionSqlite = openDb(path.join(this.dataPath, soft.SESSIONS_DB));\n const session = expressSession({\n secret: util.getSessionSecret(this.dataPath),\n resave: false,\n saveUninitialized: false,\n cookie: {\n path: '/',\n secure: this.https,\n maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year\n },\n store: new SqliteStore({\n client: sessionSqlite,\n expired: { clear: true, intervalMs: 900000 },\n }),\n });\n this.app.use(session);\n\n // setup routes and webserver\n await this.initWebUI();\n return true;\n }\n\n // setup the web ui routes\n async initWebUI() {\n await this.database.getInstanceId();\n\n // Initialize tRPC context deps\n setContextDeps({\n database: this.database,\n omegga: this.omegga,\n });\n setWebserver(this);\n\n // provide assets in the /public path\n this.app.use('/assets', express.static(ASSETS_PATH));\n this.app.use(bodyParser.json());\n\n // --- Auth routes (outside tRPC) ---\n const openApi = express.Router();\n const api = express.Router();\n\n // check if this is the first user in the database\n openApi.get('/first', async (req, res) =>\n res.json(await this.database.isFirstUser()),\n );\n\n // login / create admin user route\n openApi.post('/auth', async (req, res) => {\n if (\n typeof req.body !== 'object' ||\n typeof req.body.username !== 'string' ||\n typeof req.body.password !== 'string'\n ) {\n return res.status(422).json({ message: 'invalid body' });\n }\n const { username, password } = req.body;\n\n if (!username.match(/^\\w{0,32}$/)) {\n return res.status(422).json({ message: 'invalid body' });\n }\n\n const isFirst = await this.database.isFirstUser();\n let user;\n if (isFirst) {\n user = await this.database.createAdminUser(\n username,\n username === '' ? '' : password,\n );\n } else {\n user = await this.database.authUser(username, password);\n }\n\n if (user) {\n req.session.userId = user._id;\n if (user.totpEnabled) {\n req.session.mfaPending = true;\n req.session.save();\n res.status(200).json({ mfaRequired: true });\n } else {\n req.session.save();\n res.status(200).json({});\n }\n } else {\n res.status(404).json({ message: 'no user found' });\n }\n });\n\n // rate limiting for MFA verification (per session)\n const mfaAttempts = new Map<\n string,\n { count: number; lockedUntil: number }\n >();\n\n // TOTP verification during MFA challenge\n openApi.post('/auth/mfa/totp', async (req, res) => {\n if (!req.session.mfaPending || !req.session.userId) {\n return res.status(400).json({ message: 'no pending MFA challenge' });\n }\n\n // rate limit: 5 attempts, then 60s lockout\n const sid = req.sessionID;\n const attempt = mfaAttempts.get(sid) ?? { count: 0, lockedUntil: 0 };\n if (Date.now() < attempt.lockedUntil) {\n return res\n .status(429)\n .json({ message: 'too many attempts, try again later' });\n }\n\n const { code } = req.body ?? {};\n if (typeof code !== 'string') {\n return res.status(422).json({ message: 'code required' });\n }\n const user = await this.database.findUserById(req.session.userId);\n if (!user || !user.totpSecret) {\n return res.status(400).json({ message: 'MFA not configured' });\n }\n\n let verified = false;\n\n if (verifyTOTP(code, user.totpSecret)) {\n verified = true;\n }\n\n // try recovery codes\n if (!verified && user.recoveryCodes?.length) {\n for (const hashedCode of user.recoveryCodes) {\n if (await bcrypt.compare(code, hashedCode)) {\n await this.database.removeRecoveryCode(user.username, hashedCode);\n verified = true;\n break;\n }\n }\n }\n\n if (verified) {\n mfaAttempts.delete(sid);\n delete req.session.mfaPending;\n req.session.save();\n return res.status(200).json({});\n }\n\n attempt.count++;\n if (attempt.count >= 5) {\n attempt.lockedUntil = Date.now() + 60_000;\n attempt.count = 0;\n }\n mfaAttempts.set(sid, attempt);\n res.status(401).json({ message: 'invalid code' });\n });\n\n const getRpID = (req: express.Request) => {\n const origin = req.headers.origin;\n if (origin) {\n try {\n return new URL(origin as string).hostname;\n } catch {}\n }\n return req.hostname;\n };\n\n // WebAuthn authentication options (passwordless login)\n openApi.get('/auth/webauthn/options', async (req, res) => {\n const rpID = getRpID(req);\n const options = await generateAuthenticationOptions({ rpID });\n req.session.mfaChallenge = options.challenge;\n req.session.save();\n res.json(options);\n });\n\n // WebAuthn authentication verification\n openApi.post('/auth/webauthn/verify', async (req, res) => {\n const challenge = req.session.mfaChallenge;\n if (!challenge) {\n return res.status(400).json({ message: 'no pending challenge' });\n }\n // clear challenge immediately to prevent replay\n delete req.session.mfaChallenge;\n req.session.save();\n try {\n const { credential } = req.body;\n const user = await this.database.findUserByPasskeyId(credential?.id);\n if (!user || user.isBanned) {\n return res.status(401).json({ message: 'unknown credential' });\n }\n const passkey = user.passkeys!.find(p => p.id === credential.id)!;\n const verification = await verifyAuthenticationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin:\n req.headers.origin ?? `${req.protocol}://${req.get('host')}`,\n expectedRPID: getRpID(req),\n credential: {\n id: passkey.id,\n publicKey: Buffer.from(passkey.publicKey, 'base64url'),\n counter: passkey.counter,\n transports: passkey.transports as any,\n },\n });\n if (!verification.verified) {\n return res.status(401).json({ message: 'verification failed' });\n }\n await this.database.updatePasskeyCounter(\n user.username,\n passkey.id,\n verification.authenticationInfo.newCounter,\n );\n req.session.userId = (user as any)._id;\n delete req.session.mfaPending;\n req.session.save();\n res.status(200).json({});\n } catch (e) {\n res.status(400).json({ message: 'verification error' });\n }\n });\n\n // authentication middleware for protected api routes\n api.all('*', (req, _res, next) => {\n (async () => {\n if (req.session.mfaPending) return next(new Error('unauthorized'));\n const user = await this.database.findUserById(req.session.userId);\n if (!user || user.isBanned) return next(new Error('unauthorized'));\n next();\n })();\n });\n\n // kill a session\n api.get('/logout', (req, res) => {\n req.session.destroy(e => {\n res.status(e ? 500 : 200).json({});\n });\n });\n\n this.app.use('/api/v1', openApi);\n this.app.use('/api/v1', api);\n\n // --- tRPC ---\n this.app.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext,\n }),\n );\n\n // setup metrics and tracking\n setupMetrics(this);\n\n // every request goes through the index file (frontend handles 404s)\n this.app.use(async (req, res) => {\n if (req.session.mfaPending) {\n return res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n const user = await this.database.findUserById(req.session.userId);\n const isAuth = user && !user.isBanned;\n if (isAuth) {\n res.sendFile(path.join(PUBLIC_PATH, 'app.html'));\n } else {\n res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n });\n }\n\n // start the webserver\n async start() {\n if (this.started) return;\n await this.created;\n return await new Promise<void>(resolve => {\n this.server.listen(this.port, () => {\n log(\n `${'>>'.green} Web UI available at`,\n `http${this.https ? 's' : ''}://127.0.0.1:${this.port}`.green,\n );\n this.started = true;\n this.database.addChatLog('server', {}, 'Server started');\n resolve();\n });\n });\n }\n\n // stop the webserver\n stop() {\n this.database.addChatLog('server', {}, 'Server stopped');\n this.server.close();\n this.started = false;\n clearInterval(this.serverStatusInterval);\n }\n}\n"],"names":["Logger","soft","openDb","drizzle","runMigrations","Database","importNedbIfNeeded","util.getSSLKeys","util.getSessionSecret","setContextDeps","setWebserver","verifyTOTP","generateAuthenticationOptions","verifyAuthenticationResponse","createExpressMiddleware","appRouter","createContext","setupMetrics"],"mappings":"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"}
@@ -264,6 +264,7 @@ function setupMetrics(server) {
264
264
  events.serverEvents.emit("chat", await database.addChatLog("msg", user, message));
265
265
  });
266
266
  omegga.on("leave", async ({ id, name, displayName }) => {
267
+ events.serverEvents.emit("playerCount", omegga.players.length);
267
268
  events.serverEvents.emit(
268
269
  "chat",
269
270
  await database.addChatLog("leave", { id, name, displayName })
@@ -271,6 +272,7 @@ function setupMetrics(server) {
271
272
  });
272
273
  omegga.on("join", async ({ id, name, displayName }) => {
273
274
  const isFirst = await database.addVisit({ id, name, displayName });
275
+ events.serverEvents.emit("playerCount", omegga.players.length);
274
276
  events.serverEvents.emit(
275
277
  "chat",
276
278
  await database.addChatLog("join", {
@@ -311,14 +313,14 @@ function setupMetrics(server) {
311
313
  stopping: false
312
314
  })
313
315
  );
314
- omegga.on(
315
- "server:stopped",
316
- () => events.serverEvents.emit("serverStatus", {
316
+ omegga.on("server:stopped", () => {
317
+ events.serverEvents.emit("serverStatus", {
317
318
  started: false,
318
319
  starting: false,
319
320
  stopping: false
320
- })
321
- );
321
+ });
322
+ events.serverEvents.emit("playerCount", 0);
323
+ });
322
324
  omegga.on(
323
325
  "server:stopping",
324
326
  () => events.serverEvents.emit("serverStatus", {
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.js","sources":["../../../src/webserver/backend/metrics.ts"],"sourcesContent":["import Logger from '@/logger';\nimport { AutoRestartConfig, IServerStatus } from '@/plugin';\nimport soft from '@/softconfig';\nimport {\n clearLastSteamUpdateCheck,\n getLastSteamUpdateCheck,\n hasSteamUpdate,\n steamcmdDownloadGame,\n} from '@/updater/steam';\nimport { readFileSync, statfsSync } from 'fs';\nimport os from 'os';\nimport { serverEvents } from './events';\nimport type Webserver from './index';\nimport type { IStoreAutoRestartConfig, SystemUtilization } from './types';\n\nlet prevCpuIdle = 0;\nlet prevCpuTotal = 0;\nlet prevNetRx = 0;\nlet prevNetTx = 0;\nlet prevNetTime = 0;\n\nfunction getCpuUsage(): number {\n const cpus = os.cpus();\n let idle = 0;\n let total = 0;\n for (const cpu of cpus) {\n idle += cpu.times.idle;\n total +=\n cpu.times.user +\n cpu.times.nice +\n cpu.times.sys +\n cpu.times.irq +\n cpu.times.idle;\n }\n const dIdle = idle - prevCpuIdle;\n const dTotal = total - prevCpuTotal;\n prevCpuIdle = idle;\n prevCpuTotal = total;\n if (dTotal === 0) return 0;\n return Math.round((1 - dIdle / dTotal) * 100);\n}\n\nfunction getDiskUsage(path: string): { used: number; total: number } {\n try {\n const stat = statfsSync(path);\n const total = stat.blocks * stat.bsize;\n const free = stat.bfree * stat.bsize;\n return { used: total - free, total };\n } catch {\n return { used: 0, total: 1 };\n }\n}\n\nfunction getNetworkBytes(): { rx: number; tx: number } {\n if (process.platform !== 'linux') return { rx: 0, tx: 0 };\n try {\n const data = readFileSync('/proc/net/dev', 'utf8');\n let rx = 0;\n let tx = 0;\n for (const line of data.split('\\n').slice(2)) {\n const parts = line.trim().split(/\\s+/);\n if (!parts[0] || parts[0] === 'lo:') continue;\n rx += Number(parts[1]) || 0;\n tx += Number(parts[9]) || 0;\n }\n return { rx, tx };\n } catch {\n return { rx: 0, tx: 0 };\n }\n}\n\nconst UTILIZATION_HISTORY_LEN = 30;\nlet lastUtilization: SystemUtilization | null = null;\nconst utilizationHistory: SystemUtilization[] = [];\n\nexport function getLastUtilization(): {\n current: SystemUtilization | null;\n history: SystemUtilization[];\n} {\n return { current: lastUtilization, history: utilizationHistory };\n}\n\nfunction pushHistory(util: SystemUtilization) {\n utilizationHistory.push(util);\n if (utilizationHistory.length > UTILIZATION_HISTORY_LEN) {\n utilizationHistory.shift();\n }\n}\n\nfunction collectUtilization(omeggaPath: string): SystemUtilization {\n const cpu = getCpuUsage();\n const totalMem = os.totalmem();\n const freeMem = os.freemem();\n const disk = getDiskUsage(omeggaPath);\n const net = getNetworkBytes();\n const now = Date.now();\n const dt = prevNetTime ? (now - prevNetTime) / 1000 : 1;\n const rxSec = prevNetTime ? Math.max(0, (net.rx - prevNetRx) / dt) : 0;\n const txSec = prevNetTime ? Math.max(0, (net.tx - prevNetTx) / dt) : 0;\n prevNetRx = net.rx;\n prevNetTx = net.tx;\n prevNetTime = now;\n return {\n cpu,\n mem: { used: totalMem - freeMem, total: totalMem },\n disk,\n net: { rxSec, txSec },\n };\n}\n\nconst error = (...args: any[]) => Logger.error(...args);\nlet lastRestart = 0;\n\nconst sleep = t => new Promise(resolve => setTimeout(resolve, t));\n\nexport default function (server: Webserver) {\n const { database, omegga } = server;\n\n // Prime CPU and network counters so the first real snapshot reflects a\n // short interval rather than computing a delta from zero (host uptime).\n getCpuUsage();\n getNetworkBytes();\n\n // Collect initial utilization snapshot so queries return data immediately\n lastUtilization = collectUtilization(omegga.path);\n pushHistory(lastUtilization);\n\n // server status is checked every minute\n clearInterval(server.serverStatusInterval);\n // heartbeat happens every 60 seconds\n let empties = 0;\n\n // last heartbeat hour\n let lastHour = -1;\n // players that have joined in the last hour\n let hourlyPlayers: string[] = [];\n\n const exitOnStop = () => {\n if (omegga.stopping || !omegga.started)\n throw new Error('Omegga is already closing');\n };\n\n async function restartServer(\n config: IStoreAutoRestartConfig,\n update?: boolean,\n ) {\n lastRestart = Date.now();\n const iconfig: AutoRestartConfig = {\n players: config.playersEnabled,\n announcement: config.announcementEnabled,\n saveWorld: config.saveWorld,\n };\n omegga.emit('autorestart', iconfig);\n await sleep(1000);\n exitOnStop();\n\n const action = update ? 'Updating' : 'Restarting';\n\n if (config.announcementEnabled) {\n database.addChatLog('server', {}, action + ' in 30 seconds...');\n Logger.logp(action + ' in 30 seconds...');\n const announce = (t: number) =>\n omegga.broadcast(\n `<size=\"20\">${action} in <b><color=\"ffffbb\">${t} second${\n t !== 1 ? 's' : ''\n }</></></>`,\n );\n announce(30);\n await sleep(15000);\n announce(15);\n await sleep(10000);\n announce(5);\n await sleep(1000);\n announce(4);\n await sleep(1000);\n announce(3);\n await sleep(1000);\n announce(2);\n await sleep(1000);\n announce(1);\n await sleep(1000);\n }\n\n exitOnStop();\n\n await omegga.saveServer(iconfig);\n\n Logger.logp(action + '...');\n database.addChatLog('server', {}, action + ' server...');\n omegga.once('mapchange', () => {\n omegga.restoreServer();\n });\n\n if (update && omegga.config.__STEAM) {\n Logger.logp('Stopping server for auto-update...');\n await omegga.stop();\n Logger.logp('Downloading update...');\n try {\n await steamcmdDownloadGame({\n steambeta: omegga.config.server?.steambeta,\n });\n } catch (e) {\n error('An error occurred while downloading the update:', e);\n }\n Logger.logp('Starting server after update...');\n await omegga.start();\n } else {\n await omegga.restartServer();\n }\n }\n\n async function checkAutoRestart(status: IServerStatus) {\n const now = new Date();\n const currHour = now.getHours();\n const currMinute = now.getMinutes();\n\n /// skip restart check if we sent restart command within 5 minutes\n if (lastRestart > now.getTime() - 5 * 60 * 1000) {\n Logger.verbose('Skipping autorestart');\n return;\n }\n\n const config = await database.getAutoRestartConfig();\n Logger.verbose('Autorestart check', config);\n\n const uptimeMinutes = Math.floor(status.time / 1000 / 60);\n const uptimeHours = Math.floor(uptimeMinutes / 60);\n\n if (config.maxUptimeEnabled && uptimeHours >= config.maxUptime) {\n Logger.verbose('Restarting due to max uptime');\n return await restartServer(config);\n }\n\n if (\n config.emptyUptimeEnabled &&\n uptimeHours >= config.emptyUptime &&\n status.players.length === 0\n ) {\n Logger.verbose('Restarting due to max empty uptime');\n return await restartServer(config);\n }\n\n if (\n config.dailyHourEnabled &&\n currHour === config.dailyHour &&\n uptimeMinutes > currMinute\n ) {\n Logger.verbose('Restarting due to daily schedule');\n return await restartServer(config);\n }\n\n const lastCheck = getLastSteamUpdateCheck();\n if (omegga.config.__STEAM && config.autoUpdateEnabled) {\n const now = Date.now();\n if (\n !lastCheck.result &&\n now - lastCheck.attempt < config.autoUpdateIntervalMins * 60 * 1000\n ) {\n Logger.verbose(\n `Skipping auto update check, last check was ${Math.floor(\n (now - lastCheck.attempt) / 1000 / 60,\n )} minutes ago`,\n );\n return;\n }\n Logger.verbose('Checking for steam update');\n const hasUpdate = await hasSteamUpdate(omegga.config.server?.steambeta);\n if (hasUpdate) {\n clearLastSteamUpdateCheck();\n return await restartServer(config, true);\n }\n Logger.verbose('No steam update found');\n }\n }\n\n server.lastReportedStatus = null;\n server.serverStatusInterval = setInterval(async () => {\n if (!omegga.started) return;\n try {\n // get the server status\n const status = await omegga.getServerStatus();\n if (!status) return;\n\n try {\n await checkAutoRestart(status);\n } catch (err) {\n error('Error in autorestart check', err);\n }\n\n // get players by id\n const players = status.players.map(p => p.id);\n\n // send the unaltered status to the frontend via serverEvents\n server.lastReportedStatus = status;\n serverEvents.emit('heartbeat', status);\n\n // collect and emit system utilization metrics\n const utilization = collectUtilization(omegga.path);\n lastUtilization = utilization;\n pushHistory(utilization);\n serverEvents.emit('utilization', utilization);\n try {\n omegga.emit('metrics:heartbeat', status);\n } catch (e) {\n // prevent the omegga callback handlers from crashing this\n error('Error in heartbeat emit', e);\n }\n\n // stop recording metrics after 3 empty server statuses\n if (\n players.length === 0 &&\n ++empties > soft.METRIC_EMPTIES_BEFORE_PAUSE\n ) {\n return;\n }\n\n const now = new Date();\n const hour = now.getUTCHours();\n // check if it's a new hour (for punchcard unique player tracking)\n if (hour !== lastHour) {\n lastHour = hour;\n hourlyPlayers = [];\n }\n\n // find all the players unique to this hour\n const newPlayers = players.filter(p => !hourlyPlayers.includes(p));\n if (newPlayers.length > 0) {\n // update the punchcard\n await database.updatePlayerPunchcard(newPlayers.length);\n // mark those players as previously joined players\n hourlyPlayers.push(...newPlayers);\n }\n\n // server is not empty, reset the counter\n empties = 0;\n\n const data = {\n // number of bricks\n bricks: status.bricks,\n // unique players by id\n players: players.filter((p, i) => players.indexOf(p) === i),\n // addresses by player id\n ips: Object.fromEntries(status.players.map(p => [p.id, p.address])),\n };\n\n // hand the server status off to the database\n await database.addHeartbeat(data);\n } catch (e) {\n // probably an issue getting server status\n error('Server Not Responding...', e);\n }\n }, soft.METRIC_HEARTBEAT_INTERVAL);\n\n // chat events\n omegga.on('chat', async (name, message) => {\n const p = omegga.getPlayer(name);\n const user = {\n id: p.id,\n name,\n displayName: p.displayName,\n color: p.getNameColor(),\n };\n\n // tell web users about a chat message\n serverEvents.emit('chat', await database.addChatLog('msg', user, message));\n });\n\n // player leave events\n omegga.on('leave', async ({ id, name, displayName }) => {\n // tell web users a player left\n serverEvents.emit(\n 'chat',\n await database.addChatLog('leave', { id, name, displayName }),\n );\n });\n\n // player join events\n omegga.on('join', async ({ id, name, displayName }) => {\n // add the visit to the database\n const isFirst = await database.addVisit({ id, name, displayName });\n\n // tell web users a player joined (and if it's their first time joining)\n serverEvents.emit(\n 'chat',\n await database.addChatLog('join', {\n id,\n name,\n displayName,\n ...(isFirst ? { isFirst } : {}),\n }),\n );\n });\n\n // tell web users plugin status\n omegga.on(\n 'plugin:status',\n (\n shortPath: string,\n info: { name: string; isLoaded: boolean; isEnabled: boolean },\n ) => {\n serverEvents.emit('plugin', { shortPath, ...info });\n },\n );\n\n // server status events\n omegga.on('start', () =>\n serverEvents.emit('serverStatus', {\n started: true,\n starting: false,\n stopping: false,\n }),\n );\n omegga.on('server:starting', () =>\n serverEvents.emit('serverStatus', {\n started: false,\n starting: true,\n stopping: false,\n }),\n );\n omegga.on('mapchange', () =>\n serverEvents.emit('serverStatus', {\n started: true,\n starting: false,\n stopping: false,\n }),\n );\n omegga.on('server:stopped', () =>\n serverEvents.emit('serverStatus', {\n started: false,\n starting: false,\n stopping: false,\n }),\n );\n omegga.on('server:stopping', () =>\n serverEvents.emit('serverStatus', {\n started: true,\n starting: false,\n stopping: true,\n }),\n );\n}\n"],"names":["statfsSync","readFileSync","Logger","steamcmdDownloadGame","getLastSteamUpdateCheck","now","hasSteamUpdate","clearLastSteamUpdateCheck","serverEvents","soft"],"mappings":"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"}
1
+ {"version":3,"file":"metrics.js","sources":["../../../src/webserver/backend/metrics.ts"],"sourcesContent":["import Logger from '@/logger';\nimport { AutoRestartConfig, IServerStatus } from '@/plugin';\nimport soft from '@/softconfig';\nimport {\n clearLastSteamUpdateCheck,\n getLastSteamUpdateCheck,\n hasSteamUpdate,\n steamcmdDownloadGame,\n} from '@/updater/steam';\nimport { readFileSync, statfsSync } from 'fs';\nimport os from 'os';\nimport { serverEvents } from './events';\nimport type Webserver from './index';\nimport type { IStoreAutoRestartConfig, SystemUtilization } from './types';\n\nlet prevCpuIdle = 0;\nlet prevCpuTotal = 0;\nlet prevNetRx = 0;\nlet prevNetTx = 0;\nlet prevNetTime = 0;\n\nfunction getCpuUsage(): number {\n const cpus = os.cpus();\n let idle = 0;\n let total = 0;\n for (const cpu of cpus) {\n idle += cpu.times.idle;\n total +=\n cpu.times.user +\n cpu.times.nice +\n cpu.times.sys +\n cpu.times.irq +\n cpu.times.idle;\n }\n const dIdle = idle - prevCpuIdle;\n const dTotal = total - prevCpuTotal;\n prevCpuIdle = idle;\n prevCpuTotal = total;\n if (dTotal === 0) return 0;\n return Math.round((1 - dIdle / dTotal) * 100);\n}\n\nfunction getDiskUsage(path: string): { used: number; total: number } {\n try {\n const stat = statfsSync(path);\n const total = stat.blocks * stat.bsize;\n const free = stat.bfree * stat.bsize;\n return { used: total - free, total };\n } catch {\n return { used: 0, total: 1 };\n }\n}\n\nfunction getNetworkBytes(): { rx: number; tx: number } {\n if (process.platform !== 'linux') return { rx: 0, tx: 0 };\n try {\n const data = readFileSync('/proc/net/dev', 'utf8');\n let rx = 0;\n let tx = 0;\n for (const line of data.split('\\n').slice(2)) {\n const parts = line.trim().split(/\\s+/);\n if (!parts[0] || parts[0] === 'lo:') continue;\n rx += Number(parts[1]) || 0;\n tx += Number(parts[9]) || 0;\n }\n return { rx, tx };\n } catch {\n return { rx: 0, tx: 0 };\n }\n}\n\nconst UTILIZATION_HISTORY_LEN = 30;\nlet lastUtilization: SystemUtilization | null = null;\nconst utilizationHistory: SystemUtilization[] = [];\n\nexport function getLastUtilization(): {\n current: SystemUtilization | null;\n history: SystemUtilization[];\n} {\n return { current: lastUtilization, history: utilizationHistory };\n}\n\nfunction pushHistory(util: SystemUtilization) {\n utilizationHistory.push(util);\n if (utilizationHistory.length > UTILIZATION_HISTORY_LEN) {\n utilizationHistory.shift();\n }\n}\n\nfunction collectUtilization(omeggaPath: string): SystemUtilization {\n const cpu = getCpuUsage();\n const totalMem = os.totalmem();\n const freeMem = os.freemem();\n const disk = getDiskUsage(omeggaPath);\n const net = getNetworkBytes();\n const now = Date.now();\n const dt = prevNetTime ? (now - prevNetTime) / 1000 : 1;\n const rxSec = prevNetTime ? Math.max(0, (net.rx - prevNetRx) / dt) : 0;\n const txSec = prevNetTime ? Math.max(0, (net.tx - prevNetTx) / dt) : 0;\n prevNetRx = net.rx;\n prevNetTx = net.tx;\n prevNetTime = now;\n return {\n cpu,\n mem: { used: totalMem - freeMem, total: totalMem },\n disk,\n net: { rxSec, txSec },\n };\n}\n\nconst error = (...args: any[]) => Logger.error(...args);\nlet lastRestart = 0;\n\nconst sleep = t => new Promise(resolve => setTimeout(resolve, t));\n\nexport default function (server: Webserver) {\n const { database, omegga } = server;\n\n // Prime CPU and network counters so the first real snapshot reflects a\n // short interval rather than computing a delta from zero (host uptime).\n getCpuUsage();\n getNetworkBytes();\n\n // Collect initial utilization snapshot so queries return data immediately\n lastUtilization = collectUtilization(omegga.path);\n pushHistory(lastUtilization);\n\n // server status is checked every minute\n clearInterval(server.serverStatusInterval);\n // heartbeat happens every 60 seconds\n let empties = 0;\n\n // last heartbeat hour\n let lastHour = -1;\n // players that have joined in the last hour\n let hourlyPlayers: string[] = [];\n\n const exitOnStop = () => {\n if (omegga.stopping || !omegga.started)\n throw new Error('Omegga is already closing');\n };\n\n async function restartServer(\n config: IStoreAutoRestartConfig,\n update?: boolean,\n ) {\n lastRestart = Date.now();\n const iconfig: AutoRestartConfig = {\n players: config.playersEnabled,\n announcement: config.announcementEnabled,\n saveWorld: config.saveWorld,\n };\n omegga.emit('autorestart', iconfig);\n await sleep(1000);\n exitOnStop();\n\n const action = update ? 'Updating' : 'Restarting';\n\n if (config.announcementEnabled) {\n database.addChatLog('server', {}, action + ' in 30 seconds...');\n Logger.logp(action + ' in 30 seconds...');\n const announce = (t: number) =>\n omegga.broadcast(\n `<size=\"20\">${action} in <b><color=\"ffffbb\">${t} second${\n t !== 1 ? 's' : ''\n }</></></>`,\n );\n announce(30);\n await sleep(15000);\n announce(15);\n await sleep(10000);\n announce(5);\n await sleep(1000);\n announce(4);\n await sleep(1000);\n announce(3);\n await sleep(1000);\n announce(2);\n await sleep(1000);\n announce(1);\n await sleep(1000);\n }\n\n exitOnStop();\n\n await omegga.saveServer(iconfig);\n\n Logger.logp(action + '...');\n database.addChatLog('server', {}, action + ' server...');\n omegga.once('mapchange', () => {\n omegga.restoreServer();\n });\n\n if (update && omegga.config.__STEAM) {\n Logger.logp('Stopping server for auto-update...');\n await omegga.stop();\n Logger.logp('Downloading update...');\n try {\n await steamcmdDownloadGame({\n steambeta: omegga.config.server?.steambeta,\n });\n } catch (e) {\n error('An error occurred while downloading the update:', e);\n }\n Logger.logp('Starting server after update...');\n await omegga.start();\n } else {\n await omegga.restartServer();\n }\n }\n\n async function checkAutoRestart(status: IServerStatus) {\n const now = new Date();\n const currHour = now.getHours();\n const currMinute = now.getMinutes();\n\n /// skip restart check if we sent restart command within 5 minutes\n if (lastRestart > now.getTime() - 5 * 60 * 1000) {\n Logger.verbose('Skipping autorestart');\n return;\n }\n\n const config = await database.getAutoRestartConfig();\n Logger.verbose('Autorestart check', config);\n\n const uptimeMinutes = Math.floor(status.time / 1000 / 60);\n const uptimeHours = Math.floor(uptimeMinutes / 60);\n\n if (config.maxUptimeEnabled && uptimeHours >= config.maxUptime) {\n Logger.verbose('Restarting due to max uptime');\n return await restartServer(config);\n }\n\n if (\n config.emptyUptimeEnabled &&\n uptimeHours >= config.emptyUptime &&\n status.players.length === 0\n ) {\n Logger.verbose('Restarting due to max empty uptime');\n return await restartServer(config);\n }\n\n if (\n config.dailyHourEnabled &&\n currHour === config.dailyHour &&\n uptimeMinutes > currMinute\n ) {\n Logger.verbose('Restarting due to daily schedule');\n return await restartServer(config);\n }\n\n const lastCheck = getLastSteamUpdateCheck();\n if (omegga.config.__STEAM && config.autoUpdateEnabled) {\n const now = Date.now();\n if (\n !lastCheck.result &&\n now - lastCheck.attempt < config.autoUpdateIntervalMins * 60 * 1000\n ) {\n Logger.verbose(\n `Skipping auto update check, last check was ${Math.floor(\n (now - lastCheck.attempt) / 1000 / 60,\n )} minutes ago`,\n );\n return;\n }\n Logger.verbose('Checking for steam update');\n const hasUpdate = await hasSteamUpdate(omegga.config.server?.steambeta);\n if (hasUpdate) {\n clearLastSteamUpdateCheck();\n return await restartServer(config, true);\n }\n Logger.verbose('No steam update found');\n }\n }\n\n server.lastReportedStatus = null;\n server.serverStatusInterval = setInterval(async () => {\n if (!omegga.started) return;\n try {\n // get the server status\n const status = await omegga.getServerStatus();\n if (!status) return;\n\n try {\n await checkAutoRestart(status);\n } catch (err) {\n error('Error in autorestart check', err);\n }\n\n // get players by id\n const players = status.players.map(p => p.id);\n\n // send the unaltered status to the frontend via serverEvents\n server.lastReportedStatus = status;\n serverEvents.emit('heartbeat', status);\n\n // collect and emit system utilization metrics\n const utilization = collectUtilization(omegga.path);\n lastUtilization = utilization;\n pushHistory(utilization);\n serverEvents.emit('utilization', utilization);\n try {\n omegga.emit('metrics:heartbeat', status);\n } catch (e) {\n // prevent the omegga callback handlers from crashing this\n error('Error in heartbeat emit', e);\n }\n\n // stop recording metrics after 3 empty server statuses\n if (\n players.length === 0 &&\n ++empties > soft.METRIC_EMPTIES_BEFORE_PAUSE\n ) {\n return;\n }\n\n const now = new Date();\n const hour = now.getUTCHours();\n // check if it's a new hour (for punchcard unique player tracking)\n if (hour !== lastHour) {\n lastHour = hour;\n hourlyPlayers = [];\n }\n\n // find all the players unique to this hour\n const newPlayers = players.filter(p => !hourlyPlayers.includes(p));\n if (newPlayers.length > 0) {\n // update the punchcard\n await database.updatePlayerPunchcard(newPlayers.length);\n // mark those players as previously joined players\n hourlyPlayers.push(...newPlayers);\n }\n\n // server is not empty, reset the counter\n empties = 0;\n\n const data = {\n // number of bricks\n bricks: status.bricks,\n // unique players by id\n players: players.filter((p, i) => players.indexOf(p) === i),\n // addresses by player id\n ips: Object.fromEntries(status.players.map(p => [p.id, p.address])),\n };\n\n // hand the server status off to the database\n await database.addHeartbeat(data);\n } catch (e) {\n // probably an issue getting server status\n error('Server Not Responding...', e);\n }\n }, soft.METRIC_HEARTBEAT_INTERVAL);\n\n // chat events\n omegga.on('chat', async (name, message) => {\n const p = omegga.getPlayer(name);\n const user = {\n id: p.id,\n name,\n displayName: p.displayName,\n color: p.getNameColor(),\n };\n\n // tell web users about a chat message\n serverEvents.emit('chat', await database.addChatLog('msg', user, message));\n });\n\n // player leave events\n omegga.on('leave', async ({ id, name, displayName }) => {\n // update realtime player count subscribers (the player has already been\n // removed from omegga.players by the time 'leave' fires)\n serverEvents.emit('playerCount', omegga.players.length);\n\n // tell web users a player left\n serverEvents.emit(\n 'chat',\n await database.addChatLog('leave', { id, name, displayName }),\n );\n });\n\n // player join events\n omegga.on('join', async ({ id, name, displayName }) => {\n // add the visit to the database\n const isFirst = await database.addVisit({ id, name, displayName });\n\n // update realtime player count subscribers (the awaited DB call above means\n // omegga.players has been pushed to by the time we read it here)\n serverEvents.emit('playerCount', omegga.players.length);\n\n // tell web users a player joined (and if it's their first time joining)\n serverEvents.emit(\n 'chat',\n await database.addChatLog('join', {\n id,\n name,\n displayName,\n ...(isFirst ? { isFirst } : {}),\n }),\n );\n });\n\n // tell web users plugin status\n omegga.on(\n 'plugin:status',\n (\n shortPath: string,\n info: { name: string; isLoaded: boolean; isEnabled: boolean },\n ) => {\n serverEvents.emit('plugin', { shortPath, ...info });\n },\n );\n\n // server status events\n omegga.on('start', () =>\n serverEvents.emit('serverStatus', {\n started: true,\n starting: false,\n stopping: false,\n }),\n );\n omegga.on('server:starting', () =>\n serverEvents.emit('serverStatus', {\n started: false,\n starting: true,\n stopping: false,\n }),\n );\n omegga.on('mapchange', () =>\n serverEvents.emit('serverStatus', {\n started: true,\n starting: false,\n stopping: false,\n }),\n );\n omegga.on('server:stopped', () => {\n serverEvents.emit('serverStatus', {\n started: false,\n starting: false,\n stopping: false,\n });\n // a stopped server has no players - reset the realtime count to 0.\n // ('server:stopped' fires before omegga.players is cleared, so emit 0\n // explicitly rather than reading the still-stale length)\n serverEvents.emit('playerCount', 0);\n });\n omegga.on('server:stopping', () =>\n serverEvents.emit('serverStatus', {\n started: true,\n starting: false,\n stopping: true,\n }),\n );\n}\n"],"names":["statfsSync","readFileSync","Logger","steamcmdDownloadGame","getLastSteamUpdateCheck","now","hasSteamUpdate","clearLastSteamUpdateCheck","serverEvents","soft"],"mappings":"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"}