├── start.bat ├── .node-version ├── .dockerignore ├── .gitignore ├── Dockerfile ├── jsconfig.json ├── .editorconfig ├── docker-compose.yml ├── .env.example ├── package.json ├── MessagesPatch.d.ts ├── typedef.d.ts ├── README.md ├── newrelic.js ├── web.config ├── Messages.d.ts └── app.js /start.bat: -------------------------------------------------------------------------------- 1 | nodemon app.js -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.10.0 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | log.txt 3 | start.bat 4 | web.config 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | log.txt 3 | restart.bat 4 | *.bat 5 | .idea 6 | .env 7 | *.log 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 2 | 3 | WORKDIR /app 4 | ADD package*.json /app/ 5 | RUN npm ci 6 | ADD . /app/ 7 | CMD [ "npm", "start" ] 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2020", 5 | "lib": ["ES2020"], 6 | "checkJs": true, 7 | "types": [], 8 | "maxNodeModuleJsDepth": 0, 9 | }, 10 | "include": ["typedef.d.ts", "Messages.d.ts", "app.js"], 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | [*.{js,ts}] 8 | indent_style = tab 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = true 14 | indent_style = space 15 | 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | trim_trailing_whitespace = true 20 | insert_final_newline = true 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | image: nginx 4 | volumes: 5 | - ../Bondage-College/:/usr/share/nginx/html 6 | ports: 7 | - 80:80 8 | app: 9 | env_file: .env 10 | depends_on: 11 | - db 12 | build: 13 | context: . 14 | dockerfile: Dockerfile 15 | ports: 16 | - 4288:4288 17 | db: 18 | env_file: .env 19 | image: mongo:4.2 20 | ports: 21 | - 27017:27017 22 | volumes: 23 | - db-data:/data/db 24 | mongo-express: 25 | env_file: .env 26 | depends_on: 27 | - db 28 | image: mongo-express 29 | environment: 30 | - PORT=8081 31 | ports: 32 | - 8081:8081 33 | volumes: 34 | db-data: -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # App settings 2 | DATABASE_NAME=BondageClubDatabase 3 | DATABASE_USER=admin 4 | DATABASE_PASS=password 5 | DATABASE_HOST=db 6 | DATABASE_PORT=27017 7 | DATABASE_WEBADMIN=true 8 | DATABASE_URL=mongodb://${DATABASE_USER}:${DATABASE_PASS}@${DATABASE_HOST}:${DATABASE_PORT} 9 | PORT=4288 10 | EMAIL_ADMIN= 11 | EMAIL_PASSWORD= 12 | IP_CONNECTION_LIMIT= 13 | IP_CONNECTION_RATE_LIMIT= 14 | CLIENT_MESSAGE_RATE_LIMIT= 15 | 16 | # Database settings 17 | MONGO_INITDB_ROOT_USERNAME=${DATABASE_USER} 18 | MONGO_INITDB_ROOT_PASSWORD=${DATABASE_PASS} 19 | MONGO_INITDB_DATABASE=${DATABASE_NAME} 20 | ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_INITDB_ROOT_USERNAME} 21 | ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_INITDB_ROOT_PASSWORD} 22 | ME_CONFIG_MONGODB_SERVER=${DATABASE_HOST} 23 | ME_CONFIG_MONGODB_PORT=${DATABASE_PORT} 24 | ME_CONFIG_MONGODB_ENABLE_ADMIN=${DATABASE_WEBADMIN} 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bondage-club-server", 3 | "version": "1.0.0", 4 | "author": "Ben987", 5 | "engines": { 6 | "node": "20.x" 7 | }, 8 | "repository": "https://github.com/Ben987/Bondage-Club-Server", 9 | "main": "app.js", 10 | "private": true, 11 | "scripts": { 12 | "start": "node --unhandled-rejections=throw app.js", 13 | "check": "tsc -p jsconfig.json", 14 | "docker:start": "docker-compose up -d --build", 15 | "docker:stop": "docker-compose down", 16 | "docker:restart": "npm run docker:stop && npm run docker:start", 17 | "docker:logs": "docker-compose logs -f app db" 18 | }, 19 | "dependencies": { 20 | "base64id": "^2.0.0", 21 | "bcrypt": "^5.1.1", 22 | "mongodb": "^3.7.4", 23 | "newrelic": "^11.16.0", 24 | "nodemailer": "^6.9.13", 25 | "socket.io": "^4.8.0" 26 | }, 27 | "devDependencies": { 28 | "typescript": "^5.3.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MessagesPatch.d.ts: -------------------------------------------------------------------------------- 1 | // Declarations of BC types referenced in Messages.d.ts but originating from other files 2 | // This file should be included when sharing Messages.d.ts with the server 3 | 4 | type ArousalSettingsType = unknown; 5 | type AudioSettingsType = unknown; 6 | type ClubCard = unknown; 7 | type CharacterGameParameters = unknown; 8 | type CharacterOnlineSharedSettings = unknown; 9 | type ChatSettingsType = unknown; 10 | type ControllerSettingsType = unknown; 11 | type CraftingItem = unknown; 12 | type ExtensionSettings = unknown; 13 | type GameplaySettingsType = unknown; 14 | type GenderSettingsType = unknown; 15 | type GraphicsSettingsType = unknown; 16 | type HSVColor = unknown; 17 | type ImmersionSettingsType = unknown; 18 | type InfiltrationType = unknown; 19 | type ItemColor = unknown; 20 | type ItemProperties = unknown; 21 | type LogRecord = unknown; 22 | type NotificationSettingsType = unknown; 23 | type NPCTrait = unknown; 24 | type PlayerOnlineSettings = unknown; 25 | type Skill = unknown; 26 | type VisualSettingsType = unknown; 27 | 28 | type ActivityName = string; 29 | type AssetCategory = string; 30 | type AssetGroupName = string; 31 | type AssetPoseName = string; 32 | type ExpressionGroupName = string; 33 | type ExpressionName = string; 34 | type ReputationType = string; 35 | type TitleName = string; 36 | type SpeechTransformName = string; 37 | -------------------------------------------------------------------------------- /typedef.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /** The type of our sockets */ 3 | type ServerSocket = import("socket.io").Socket; 4 | 5 | /* Things that are missing from Messages.d.ts */ 6 | type AssetGroupName = string; 7 | type ItemColor = string; 8 | type ItemProperties = any; 9 | type CraftingItem = any; 10 | type ActivityName = string; 11 | type DifficultyLevel = 12 | | 0 // Roleplay 13 | | 1 // Regular 14 | | 2 // Hardcore 15 | | 3 // Extreme 16 | ; 17 | type Lovership = ServerLovership; 18 | 19 | interface Account extends ServerAccountData { 20 | ID: ServerSocket["id"]; 21 | AccountName: string; 22 | MemberNumber: number; 23 | Name: string; 24 | /* optional on creation, purged from memory */ 25 | Email?: string; 26 | /* purged from memory */ 27 | Password?: string; 28 | Creation: number; 29 | /* purged from memory */ 30 | LastLogin: number; 31 | Environment: "PROD" | "DEV" | string; 32 | Socket: ServerSocket; 33 | ChatRoom?: Chatroom; 34 | Ownership?: ServerOwnership; 35 | Lovership: Lovership[]; 36 | DelayedAppearanceUpdate?: ServerAccountData["Appearance"]; 37 | DelayedSkillUpdate?: ServerAccountData["Skill"]; 38 | DelayedGameUpdate?: ServerChatRoomGame; 39 | } 40 | 41 | interface Chatroom extends ServerChatRoomData { 42 | ID: string; 43 | Environment: string; 44 | Creator: string; 45 | CreatorMemberNumber: number; 46 | Creation: number; 47 | Account: Account[]; 48 | /** 49 | * This is inherited from {@link ServerChatRoomData}, but 50 | * provided on-the-fly by the server from {@link Chatroom.Account} 51 | */ 52 | Character?: never; 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Using Docker with bondage club for development 2 | 1. Install [Docker](https://docs.docker.com/get-docker/) 3 | 2. Make sure you also install `docker-compose`, Docker Desktop comes with this tool, but Linux does not. If you already have Docker installed, make sure it's at least of version `18.06.0` or higher. 4 | 3. Clone this repo and `Bondage-College` into the same folder so you have the following structure: 5 | ``` 6 | $ ls -l 7 | total 8 8 | drwxr-xr-x 18 user staff 576 Jun 25 07:28 Bondage-Club-Server 9 | drwxr-xr-x 42 user staff 1344 Jun 23 22:14 Bondage-College 10 | ``` 11 | 4. `cd Bondage-Club-Server` 12 | 5. `cp .env.example .env` 13 | 6. run `docker-compose up -d --build`, this will build and start up the required containers, the latter command can be repeated to update the Bondage-Club-Server if you've made server changes. 14 | 15 | Make the required changes to index.html in your Bondage-Club repository, and it will now be available at http://localhost/BondageClub/ 16 | 17 | * Mongo runs at localhost:27017, by default with the username and password `admin` and `password`, this can be changed in the .env file `MONGO_INITDB_ROOT_USERNAME` and `MONGO_INITDB_ROOT_PASSWORD` 18 | * Mongo-Express runs in a separate container at http://localhost:8081 and lets you access the database 19 | 20 | ### Convenience commands 21 | * `docker-compose down` Tear down and remove all containers but not the mongo database 22 | * `docker-compose down --volumes` Remove all the containers and the mongo database 23 | * `docker-compose up -d --build` build the server with any changes and start it up 24 | * `docker-compose up -d --build --no-cache` rebuild the server and start it up 25 | * `docker-compose logs app` View the logs from the bondage club server 26 | * `docker-compose logs db` View the logs of Mongo 27 | -------------------------------------------------------------------------------- /newrelic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * New Relic agent configuration. 4 | * 5 | * See lib/config/default.js in the agent distribution for a more complete 6 | * description of configuration variables and their potential values. 7 | */ 8 | exports.config = { 9 | /** 10 | * Array of application names. 11 | */ 12 | app_name: ['bondage-club-server'], 13 | /** 14 | * Your New Relic license key. 15 | */ 16 | license_key: '561e7371eabbe15892a784d5d8118a33d0d3NRAL', 17 | /** 18 | * This setting controls distributed tracing. 19 | * Distributed tracing lets you see the path that a request takes through your 20 | * distributed system. Enabling distributed tracing changes the behavior of some 21 | * New Relic features, so carefully consult the transition guide before you enable 22 | * this feature: https://docs.newrelic.com/docs/transition-guide-distributed-tracing 23 | * Default is true. 24 | */ 25 | distributed_tracing: { 26 | /** 27 | * Enables/disables distributed tracing. 28 | * 29 | * @env NEW_RELIC_DISTRIBUTED_TRACING_ENABLED 30 | */ 31 | enabled: true 32 | }, 33 | logging: { 34 | /** 35 | * Level at which to log. 'trace' is most useful to New Relic when diagnosing 36 | * issues with the agent, 'info' and higher will impose the least overhead on 37 | * production applications. 38 | */ 39 | level: 'info' 40 | }, 41 | /** 42 | * When true, all request headers except for those listed in attributes.exclude 43 | * will be captured for all traces, unless otherwise specified in a destination's 44 | * attributes include/exclude lists. 45 | */ 46 | allow_all_headers: true, 47 | attributes: { 48 | /** 49 | * Prefix of attributes to exclude from all destinations. Allows * as wildcard 50 | * at end. 51 | * 52 | * NOTE: If excluding headers, they must be in camelCase form to be filtered. 53 | * 54 | * @env NEW_RELIC_ATTRIBUTES_EXCLUDE 55 | */ 56 | exclude: [ 57 | 'request.headers.cookie', 58 | 'request.headers.authorization', 59 | 'request.headers.proxyAuthorization', 60 | 'request.headers.setCookie*', 61 | 'request.headers.x*', 62 | 'response.headers.cookie', 63 | 'response.headers.authorization', 64 | 'response.headers.proxyAuthorization', 65 | 'response.headers.setCookie*', 66 | 'response.headers.x*' 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Messages.d.ts: -------------------------------------------------------------------------------- 1 | //#region Main data exchange types 2 | 3 | type MemberNumber = number; 4 | 5 | type ChatRoomMapPos = { 6 | X: number; 7 | Y: number; 8 | } 9 | 10 | type ChatRoomMapData = { 11 | Pos: ChatRoomMapPos 12 | PrivateState: Record 13 | } 14 | 15 | interface ServerAccountImmutableData { 16 | /** Socket ID */ 17 | ID: string; 18 | MemberNumber: MemberNumber; 19 | Name: string; 20 | AccountName: string; 21 | Creation: number; 22 | Ownership?: ServerOwnership; 23 | Lovership?: ServerLovership[]; 24 | ActivePose?: readonly AssetPoseName[]; 25 | Pose?: readonly AssetPoseName[]; 26 | } 27 | 28 | interface ServerAccountData extends ServerAccountImmutableData { 29 | Owner?: string; 30 | /** 31 | * @deprecated 32 | */ 33 | Lover?: string; 34 | Money: number; 35 | Log?: LogRecord[]; 36 | GhostList?: MemberNumber[]; 37 | BlackList: MemberNumber[]; 38 | FriendList: MemberNumber[]; 39 | WhiteList: MemberNumber[]; 40 | ItemPermission: 0 | 1 | 2 | 3 | 4 | 5; 41 | Skill?: Skill[]; 42 | Reputation?: { Type: ReputationType, Value: number }[]; 43 | Wardrobe?: string; 44 | WardrobeCharacterNames?: string[]; 45 | ChatSettings?: ChatSettingsType; 46 | VisualSettings?: VisualSettingsType; 47 | AudioSettings?: AudioSettingsType; 48 | GameplaySettings?: GameplaySettingsType; 49 | ArousalSettings?: ArousalSettingsType; 50 | OnlineSharedSettings?: CharacterOnlineSharedSettings; 51 | Game?: CharacterGameParameters; 52 | LabelColor?: string; 53 | Appearance?: ServerAppearanceBundle; 54 | Description?: string; 55 | BlockItems?: ServerItemPermissionsPacked | ServerItemPermissions[]; 56 | LimitedItems?: ServerItemPermissionsPacked | ServerItemPermissions[]; 57 | FavoriteItems?: ServerItemPermissionsPacked | ServerItemPermissions[]; 58 | HiddenItems?: ServerItemPermissions[]; 59 | Title?: TitleName; 60 | Nickname?: string; 61 | Crafting?: string; 62 | /** String-based values have been deprecated as of BondageProjects/Bondage-College#2138 */ 63 | Inventory?: string | Partial>; 64 | InventoryData?: string; 65 | /** Initialized by {@link CharacterCreate} */ 66 | AssetFamily: "Female3DCG"; 67 | Infiltration?: InfiltrationType; 68 | SavedColors?: HSVColor[]; 69 | ChatSearchFilterTerms?: string; 70 | Difficulty?: { Level: DifficultyLevel; LastChange: number }; 71 | MapData?: ChatRoomMapData; 72 | PrivateCharacter?: ServerPrivateCharacterData[]; 73 | SavedExpressions?: ({ Group: ExpressionGroupName, CurrentExpression?: ExpressionName }[] | null)[]; 74 | ConfiscatedItems?: { Group: AssetGroupName, Name: string }[]; 75 | RoomCreateLanguage?: ServerChatRoomLanguage; 76 | RoomSearchLanguage?: "" | ServerChatRoomLanguage; 77 | LastMapData?: null | ChatRoomMapData; 78 | // Unfortunately can't @deprecated individual union members 79 | /** String-based values have been deprecated and are superseded by {@link ServerChatRoomSettings} objects */ 80 | LastChatRoom?: null | ServerChatRoomSettings | string; 81 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 82 | LastChatRoomDesc?: string; 83 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 84 | LastChatRoomAdmin?: string; 85 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 86 | LastChatRoomWhitelist?: string; 87 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 88 | LastChatRoomBan?: string; 89 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 90 | LastChatRoomBG?: string; 91 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 92 | LastChatRoomSize?: number; 93 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 94 | LastChatRoomPrivate?: boolean; 95 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 96 | LastChatRoomBlockCategory?: ServerChatRoomBlockCategory[]; 97 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 98 | LastChatRoomSpace?: ServerChatRoomSpace; 99 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 100 | LastChatRoomLanguage?: ServerChatRoomLanguage; 101 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 102 | LastChatRoomCustom?: ServerChatRoomData["Custom"]; 103 | /** @deprecated superseded by the {@link ServerAccountData.LastChatRoom} object */ 104 | LastChatRoomMapData?: ServerChatRoomMapData; 105 | ControllerSettings?: ControllerSettingsType; 106 | ImmersionSettings?: ImmersionSettingsType; 107 | RestrictionSettings?: RestrictionSettingsType; 108 | OnlineSettings?: PlayerOnlineSettings; 109 | GraphicsSettings?: GraphicsSettingsType; 110 | NotificationSettings?: NotificationSettingsType; 111 | GenderSettings?: GenderSettingsType; 112 | ExtensionSettings?: ExtensionSettings; 113 | FriendNames?: string; 114 | SubmissivesList?: string; 115 | KinkyDungeonExploredLore?: unknown[]; 116 | } 117 | 118 | // TODO: Add `Lover` after figuring out why {@link ServerPlayerSync} still passes this field to the server 119 | /** A union of all deprecated {@link ServerAccountData} fields */ 120 | type ServerAccountDataDeprecations = ( 121 | "LastChatRoomDesc" 122 | | "LastChatRoomAdmin" 123 | | "LastChatRoomBan" 124 | | "LastChatRoomBG" 125 | | "LastChatRoomSize" 126 | | "LastChatRoomPrivate" 127 | | "LastChatRoomBlockCategory" 128 | | "LastChatRoomSpace" 129 | | "LastChatRoomLanguage" 130 | | "LastChatRoomCustom" 131 | | "LastChatRoomMapData" 132 | ); 133 | 134 | /** 135 | * A {@link ServerAccountData} variant with all deprecated members set to `never`. 136 | * 137 | * Use of this type over {@link ServerAccountData} is recommended when sending data *to* the server. 138 | */ 139 | type ServerAccountDataNoDeprecated = ServerAccountData & { [k in ServerAccountDataDeprecations]?: never } & { 140 | // Fields with one or more deprecated union members removed 141 | LastChatRoom?: null | ServerChatRoomSettings; 142 | Inventory?: Partial>; 143 | }; 144 | 145 | /** 146 | * A struct for representing an item with special permissions (limited, favorited, etc) in the server. 147 | * @see {@link ServerItemPermissionsPacked} 148 | */ 149 | interface ServerItemPermissions { 150 | /** The {@link Asset.Name} of the item */ 151 | Name: string; 152 | /** The {@link AssetGroup.Name} of the item */ 153 | Group: AssetGroupName; 154 | /** 155 | * Either the item's {@link ItemProperties.Type} or, in the case of modular items, 156 | * a substring thereof denoting the type of a single module 157 | */ 158 | Type?: string | null; 159 | } 160 | 161 | /** A packed record-based version of {@link ServerItemPermissions}. */ 162 | type ServerItemPermissionsPacked = Partial>>; 163 | 164 | interface ServerMapDataResponse { 165 | MemberNumber: number; 166 | MapData: ChatRoomMapData; 167 | } 168 | 169 | type ServerAccountDataSynced = Omit; 170 | 171 | interface ServerOwnership { 172 | MemberNumber?: number; 173 | Name?: string; 174 | Notes?: string; // Public notes from this sub's owner. 175 | Stage?: number; 176 | Start?: number; 177 | StartTrialOfferedByMemberNumber?: number; 178 | EndTrialOfferedByMemberNumber?: number; 179 | } 180 | 181 | interface ServerLovership { 182 | MemberNumber?: number; 183 | Name?: string; 184 | Stage?: number; 185 | Start?: number; 186 | BeginDatingOfferedByMemberNumber?: number; 187 | BeginEngagementOfferedByMemberNumber?: number; 188 | BeginWeddingOfferedByMemberNumber?: number; 189 | } 190 | 191 | /** An ItemBundle is a minified version of the normal Item */ 192 | interface ServerItemBundle { 193 | Group: AssetGroupName; 194 | Name: string; 195 | Difficulty?: number; 196 | Color?: ItemColor; 197 | Property?: ItemProperties; 198 | Craft?: CraftingItem; 199 | } 200 | 201 | interface ServerPrivateCharacterData { 202 | Name: string; 203 | Love: number; 204 | Title: TitleName; 205 | Trait: NPCTrait[]; 206 | Cage: boolean; 207 | Owner: string; 208 | Lover: string; 209 | AssetFamily: "Female3DCG"; 210 | Appearance: ServerAppearanceBundle; 211 | AppearanceFull: ServerAppearanceBundle; 212 | ArousalSettings: ArousalSettingsType; 213 | Event: NPCEvent[]; 214 | FromPandora?: boolean; 215 | } 216 | 217 | /** An AppearanceBundle is whole minified appearance of a character */ 218 | type ServerAppearanceBundle = ServerItemBundle[]; 219 | 220 | type ServerChatRoomSpace = "X" | "" | "M" | "Asylum"; 221 | type ServerChatRoomLanguage = "EN" | "DE" | "FR" | "ES" | "CN" | "RU" | "UA"; 222 | type ServerChatRoomRole = "All" | "Admin" | "Whitelist"; 223 | type ServerChatRoomGame = "" | "ClubCard" | "LARP" | "MagicBattle" | "GGTS" | "Prison"; 224 | type ServerChatRoomBlockCategory = 225 | /** Those are known as AssetCategory to the client */ 226 | "Medical" | "Extreme" | "Pony" | "SciFi" | "ABDL" | "Fantasy" | 227 | /** Those are room features */ 228 | "Leashing" | "Photos" | "Arousal"; 229 | 230 | 231 | 232 | /** 233 | * The chatroom data received from the server 234 | */ 235 | type ServerChatRoomData = { 236 | Name: string; 237 | Description: string; 238 | Admin: number[]; 239 | Whitelist: number[]; 240 | Ban: number[]; 241 | Background: string; 242 | /* FIXME: server actually expects a string there, but we cheat to make the typing simpler */ 243 | Limit: number; 244 | Game: ServerChatRoomGame; 245 | Visibility: ServerChatRoomRole[]; 246 | Access: ServerChatRoomRole[]; 247 | /** 248 | * @deprecated Use {@link ServerChatRoomData.Visibility} instead, this is temporarily maintained for backwards compatibility 249 | */ 250 | Private: boolean; // TODO: Remove following completion of migration 251 | /** 252 | * @deprecated Use {@link ServerChatRoomData.Access} instead, this is temporarily maintained for backwards compatibility 253 | */ 254 | Locked: boolean; // TODO: Remove following completion of migration 255 | BlockCategory: ServerChatRoomBlockCategory[]; 256 | Language: ServerChatRoomLanguage; 257 | Space: ServerChatRoomSpace; 258 | MapData?: ServerChatRoomMapData; 259 | Custom: ServerChatRoomCustomData; 260 | Character: ServerAccountDataSynced[]; 261 | } 262 | 263 | interface ServerChatRoomMapData { 264 | Type: ChatRoomMapType; 265 | Fog?: boolean; 266 | Tiles?: string; 267 | Objects?: string; 268 | } 269 | 270 | interface ServerChatRoomCustomData { 271 | ImageURL?: string; 272 | ImageFilter?: string; 273 | MusicURL?: string; 274 | SizeMode?: number 275 | } 276 | 277 | /** 278 | * A chatroom's settings 279 | * 280 | * Define to `never` any property of {@link ServerChatRoomData} that 281 | * shouldn't be sent back to the server. 282 | */ 283 | type ServerChatRoomSettings = Partial & { 284 | Character?: never; 285 | } 286 | 287 | //#endregion 288 | 289 | //#region Requests & Responses 290 | 291 | type ServerLoginResponse = "InvalidNamePassword" | ServerAccountData; 292 | 293 | type ServerLoginQueueResponse = number; 294 | 295 | interface ServerAccountLoginRequest { 296 | AccountName: string; 297 | Password: string; 298 | } 299 | 300 | interface ServerAccountCreateRequest { 301 | Name: string; 302 | AccountName: string; 303 | Password: string; 304 | Email: string; 305 | } 306 | 307 | interface ServerAccountCreateResponseSuccess { 308 | ServerAnswer: "AccountCreated"; 309 | OnlineID: string; 310 | MemberNumber: number; 311 | } 312 | 313 | type ServerAccountCreateResponse = ServerAccountCreateResponseSuccess | "Account already exists" | "Invalid account information" | "New accounts per day exceeded"; 314 | 315 | type ServerPasswordResetRequest = string; 316 | 317 | interface ServerPasswordResetProcessRequest { 318 | AccountName: string; 319 | ResetNumber: string; 320 | NewPassword: string; 321 | } 322 | 323 | type ServerPasswordResetResponse = "RetryLater" | "EmailSentError" | "EmailSent" | "NoAccountOnEmail" | "PasswordResetSuccessful" | "InvalidPasswordResetInfo"; 324 | 325 | interface ServerInfoMessage { 326 | Time: number; 327 | OnlinePlayers: number; 328 | } 329 | 330 | type ServerForceDisconnectMessage = "ErrorRateLimited" | "ErrorDuplicatedLogin"; 331 | 332 | interface ServerAccountUpdateRequest extends Partial {} 333 | 334 | interface ServerAccountUpdateEmailRequest { 335 | EmailOld: string; 336 | EmailNew: string; 337 | } 338 | 339 | interface ServerFriendInfo { 340 | Type: "Friend" | "Submissive" | "Lover"; 341 | MemberNumber: number; 342 | MemberName: string; 343 | ChatRoomSpace?: ServerChatRoomSpace | null; 344 | ChatRoomName?: string | null; 345 | Private?: true | undefined; 346 | } 347 | 348 | interface ServerAccountQueryRequest { 349 | Query: "EmailStatus" | "EmailUpdate" | "OnlineFriends"; 350 | } 351 | 352 | interface ServerAccountQueryEmailStatus { 353 | Query: "EmailUpdate" | "EmailStatus"; 354 | Result: boolean; 355 | } 356 | 357 | interface ServerAccountQueryOnlineFriends { 358 | Query: "OnlineFriends"; 359 | Result: ServerFriendInfo[]; 360 | } 361 | 362 | type ServerAccountQueryResponse = ServerAccountQueryEmailStatus | ServerAccountQueryOnlineFriends; 363 | 364 | 365 | interface ServerAccountLovershipRefreshRequest { 366 | MemberNumber: number; 367 | Action?: never; 368 | Name?: never; 369 | } 370 | 371 | interface ServerAccountLovershipUpdateRequest { 372 | MemberNumber: number; 373 | Action: "Propose" | "Accept" | "Release"; 374 | Name?: never; 375 | } 376 | 377 | interface ServerAccountLovershipBreakupRequest { 378 | MemberNumber: number; 379 | Action: "Break"; 380 | Name?: never; 381 | } 382 | 383 | interface ServerAccountLovershipBreakupNPCRequest { 384 | MemberNumber: -1; 385 | Action: "Break"; 386 | Name: string; 387 | } 388 | 389 | type ServerAccountLovershipRequest = ServerAccountLovershipRefreshRequest | ServerAccountLovershipUpdateRequest | ServerAccountLovershipBreakupRequest | ServerAccountLovershipBreakupNPCRequest; 390 | 391 | interface ServerAccountLovershipStatus { 392 | MemberNumber: number; 393 | Result: "CanOfferBeginDating" | "CanBeginDating" | "CanOfferBeginEngagement" | "CanBeginEngagement" | "CanOfferBeginWedding" | "CanBeginWedding"; 394 | } 395 | 396 | interface ServerAccountLovershipInfo { 397 | Lovership: ServerLovership[]; 398 | } 399 | 400 | interface ServerAccountLovershipComplete { 401 | Lovership: ServerLovership; 402 | } 403 | 404 | type ServerAccountLovershipResponse = ServerAccountLovershipStatus | ServerAccountLovershipInfo | ServerAccountLovershipComplete; 405 | 406 | interface ServerAccountOwnershipAcquisitionRequest { 407 | MemberNumber: number; 408 | Action?: "Propose" | "Accept" | "Release" | "Break"; 409 | } 410 | 411 | interface ServerAccountOwnershipUpdateNotesRequest { 412 | MemberNumber: number; 413 | Action: "UpdateNotes"; 414 | Notes?: string; 415 | } 416 | 417 | type ServerAccountOwnershipRequest = ServerAccountOwnershipAcquisitionRequest | ServerAccountOwnershipUpdateNotesRequest; 418 | 419 | interface ServerAccountOwnershipStatus { 420 | MemberNumber: number; 421 | Result: "CanOfferStartTrial" | "CanStartTrial" | "CanOfferEndTrial" | "CanEndTrial"; 422 | } 423 | 424 | interface ServerAccountOwnershipClear { 425 | ClearOwnership: true 426 | } 427 | 428 | interface ServerAccountOwnershipComplete { 429 | Ownership: ServerOwnership; 430 | Owner: string; 431 | } 432 | 433 | type ServerAccountOwnershipResponse = ServerAccountOwnershipClear | ServerAccountOwnershipStatus | ServerAccountOwnershipComplete; 434 | 435 | type ServerBeepType = string; 436 | 437 | type ServerAccountBeepRequest = { 438 | MemberNumber: number; 439 | BeepType: ServerBeepType; 440 | Message?: string; 441 | IsSecret?: boolean; 442 | } 443 | 444 | type ServerAccountBeepResponse = { 445 | MemberNumber: number; 446 | MemberName: string; 447 | ChatRoomSpace: ServerChatRoomSpace; 448 | ChatRoomName: string; 449 | Private: boolean; 450 | BeepType: ServerBeepType; 451 | Message: string; 452 | }; 453 | 454 | interface ServerChatRoomSearchRequest { 455 | Query: string; 456 | Space?: ServerChatRoomSpace[] | ServerChatRoomSpace; 457 | Game?: ServerChatRoomGame; 458 | FullRooms?: boolean; 459 | Ignore?: string[]; 460 | Language: "" | ServerChatRoomLanguage | ServerChatRoomLanguage[]; 461 | SearchDescs?: boolean; 462 | ShowLocked?: boolean; 463 | MapTypes?: string[]; 464 | } 465 | 466 | interface ServerChatRoomSearchData { 467 | Name: string; 468 | Language: string; 469 | Creator: string; 470 | CreatorMemberNumber: number; 471 | Creation: number; 472 | MemberCount: number; 473 | MemberLimit: number; 474 | Description: string; 475 | BlockCategory: AssetCategory[]; 476 | Game: ServerChatRoomGame; 477 | Friends: ServerFriendInfo[]; 478 | Space: ServerChatRoomSpace; 479 | Visibility: ServerChatRoomRole[]; 480 | Access: ServerChatRoomRole[]; 481 | /** 482 | * @deprecated Use {@link ServerChatRoomData.Visibility} instead, this is maintained for backwards compatibility 483 | */ 484 | Private?: boolean; 485 | /** 486 | * @deprecated Use {@link ServerChatRoomData.Access} instead, this is maintained for backwards compatibility 487 | */ 488 | Locked?: boolean; 489 | CanJoin: boolean; 490 | MapType: string; 491 | } 492 | 493 | type ServerChatRoomSearchResultResponse = ServerChatRoomSearchData[]; 494 | 495 | interface ServerChatRoomCreateRequest extends ServerChatRoomSettings {} 496 | 497 | type ServerChatRoomCreateResponse = "AccountError" | "RoomAlreadyExist" | "InvalidRoomData" | "ChatRoomCreated"; 498 | 499 | interface ServerChatRoomAdminUpdateRequest { 500 | MemberNumber: number; 501 | Action: "Update"; 502 | Room: Partial; 503 | } 504 | 505 | interface ServerChatRoomAdminMoveRequest { 506 | MemberNumber: number; 507 | Action: "Move" | "MoveLeft" | "MoveRight" | "Kick" | "Ban" | "Unban" | "Promote" | "Demote" | "Whitelist" | "Unwhitelist" | "Shuffle"; 508 | Publish?: boolean; 509 | } 510 | 511 | interface ServerChatRoomAdminSwapRequest { 512 | MemberNumber: number; 513 | Action: "Swap"; 514 | TargetMemberNumber: number; 515 | DestinationMemberNumber: number; 516 | } 517 | 518 | type ServerChatRoomAdminRequest = ServerChatRoomAdminUpdateRequest | ServerChatRoomAdminMoveRequest | ServerChatRoomAdminSwapRequest; 519 | 520 | type ServerChatRoomSearchResponse = "JoinedRoom" | "AlreadyInRoom" | "RoomLocked" | "RoomBanned" | "RoomKicked" | "RoomFull" | "CannotFindRoom" | "AccountError" | "InvalidRoomData"; 521 | 522 | /** Base interface for a chat message */ 523 | interface ServerChatRoomMessageBase { 524 | /** The sender number. Provided by the server to the client, ignored otherwise. */ 525 | Sender?: number; 526 | } 527 | 528 | interface ServerChatRoomJoinRequest { 529 | /** The name of the chatroom to join */ 530 | Name: string; 531 | } 532 | 533 | interface ServerChatRoomSyncMessage extends ServerChatRoomData { 534 | Character: ServerAccountDataSynced[]; 535 | SourceMemberNumber: number; 536 | } 537 | 538 | interface ServerChatRoomSyncPropertiesMessage extends Omit { 539 | SourceMemberNumber: number; 540 | } 541 | 542 | type ServerChatRoomMessageType = "Action" | "Chat" | "Whisper" | "Emote" | "Activity" | "Hidden" | 543 | "LocalMessage" | "ServerMessage" | "Status"; 544 | type ServerChatRoomMessageContentType = string; 545 | 546 | type CharacterReferenceTag = 547 | | "SourceCharacter" 548 | | "DestinationCharacter" 549 | | "DestinationCharacterName" 550 | | "TargetCharacter" 551 | | "TargetCharacterName" 552 | 553 | type CommonChatTags = 554 | | CharacterReferenceTag 555 | | "AssetName" 556 | | "Automatic"; 557 | 558 | /** 559 | * A dictionary entry containing a replacement tag to be replaced by some value. The replacement strategy depends on 560 | * the type of dictionary entry. 561 | */ 562 | interface TaggedDictionaryEntry { 563 | /** The tag that will be replaced in the message */ 564 | Tag: string; 565 | } 566 | 567 | /** 568 | * A dictionary entry used to reference a character. The character reference tag will be replaced with the provided 569 | * character's name or pronoun. The display format will depend on the tag chosen. 570 | * Example substitutions for each tag (assuming the character name is Ben987): 571 | * * SourceCharacter: "Ben987" 572 | * * DestinationCharacter: "Ben987's" (if character is not self), "her"/"him" (if character is self) 573 | * * DestinationCharacterName: "Ben987's" 574 | * * TargetCharacter: "Ben987" (if character is not self), "herself"/"himself" (if character is self) 575 | * * TargetCharacterName: "Ben987" 576 | * @deprecated Use {@link SourceCharacterDictionaryEntry} and {@link TargetCharacterDictionaryEntry} instead. 577 | */ 578 | interface CharacterReferenceDictionaryEntry extends TaggedDictionaryEntry { 579 | /** The member number of the referenced character */ 580 | MemberNumber: number; 581 | /** The character reference tag, determining how the character's name or pronoun will be interpreted */ 582 | Tag: CharacterReferenceTag; 583 | /** 584 | * The nickname of the referenced character 585 | * @deprecated Redundant information 586 | */ 587 | Text?: string; 588 | } 589 | 590 | /** 591 | * A dictionary entry used to indicate the source character of a chat message or action (i.e. the character initiating 592 | * the message or action). 593 | */ 594 | interface SourceCharacterDictionaryEntry { 595 | SourceCharacter: number; 596 | } 597 | 598 | /** 599 | * A dictionary entry used to indicate the target character of a chat message or action (i.e. the character that is 600 | * being acted upon as part of the message or action). 601 | */ 602 | interface TargetCharacterDictionaryEntry { 603 | TargetCharacter: number; 604 | Index?: number; 605 | } 606 | 607 | /** 608 | * A dictionary entry which indicates the focused group. This represents the group that was focused or interacted with 609 | * when sending a chat message. For example, if the message was caused by performing an activity or modifying an item 610 | * on the `ItemArms` group, then it would be appropriate to send this dictionary entry with `ItemArms` as the focus 611 | * group name. 612 | */ 613 | interface FocusGroupDictionaryEntry { 614 | /** 615 | * The tag to be replaced - this is always FocusAssetGroup. 616 | * @deprecated Redundant information. 617 | */ 618 | Tag?: "FocusAssetGroup"; 619 | /** The group name representing focused group for the purposes of the sent message */ 620 | FocusGroupName: AssetGroupName; 621 | } 622 | 623 | /** 624 | * A direct text substitution dictionary entry. Any occurrences of the given {@link Tag} string in the associated 625 | * message will be directly replaced with the {@link Text} from this dictionary entry (no text lookup will be done). 626 | * For example, given the message: 627 | * ``` 628 | * Life is like a box of ConfectionaryName. 629 | * ``` 630 | * and the {@link TextDictionaryEntry}: 631 | * ```js 632 | * {Tag: "ConfectionaryName", Text: "chocolates"} 633 | * ``` 634 | * The resulting message would be: 635 | * ``` 636 | * Life is like a box of chocolates. 637 | * ``` 638 | */ 639 | interface TextDictionaryEntry extends TaggedDictionaryEntry { 640 | /** The text that will be substituted for the tag */ 641 | Text: string; 642 | } 643 | 644 | /** 645 | * A text substitution dictionary entry with text lookup functionality. Any occurrences of the given {@link Tag} string 646 | * in the associated message will be replaced with the {@link Text} from the dictionary entry, but only after a text 647 | * lookup has been done on the {@link Text}, meaning that if the text has localisations, the localised version will be 648 | * used. The text will be looked up against `Dialog_Player.csv`. 649 | * For example, given the message: 650 | * ``` 651 | * Hello, {GreetingObjectName}! 652 | * ``` 653 | * And the {@link TextLookupDictionaryEntry}: 654 | * ```js 655 | * {Tag: "GreetingObjectName", TextToLookup: "WorldObject"} 656 | * ``` 657 | * And the following in `Dialog_Player.csv`: 658 | * ``` 659 | * WorldObject,,,World,, 660 | * ``` 661 | * The text to lookup (`"WorldObject"`) would be looked up against `Dialog_Player.csv`, resolving to `"World"`. This 662 | * would then be used to replace the tag `"GreetingObjectName"` in the message, resulting in: 663 | * ``` 664 | * Hello, World! 665 | * ``` 666 | */ 667 | interface TextLookupDictionaryEntry extends TaggedDictionaryEntry { 668 | /** The text whose lookup will be substituted for the tag */ 669 | TextToLookUp: string; 670 | } 671 | 672 | /** 673 | * A dictionary entry that references an asset group. Note that this is different from 674 | * {@link FocusGroupDictionaryEntry}, which denotes the group being acted on. A dictionary should only ever contain 675 | * one {@link FocusGroupDictionaryEntry}, whereas it may contain many {@link GroupReferenceDictionaryEntry}s. This 676 | * represents any group that might be referenced in the message, but is not necessarily the focused group. 677 | * For example, given the message: 678 | * ``` 679 | * Use your BodyPart! 680 | * ``` 681 | * And the {@link GroupReferenceDictionaryEntry}: 682 | * ``` 683 | * {Tag: "BodyPart", GroupName: "ItemHands"} 684 | * ``` 685 | * The name of the `"ItemHands"` group would be looked up, and this would be used to replace the `"BodyPart"` tag. The 686 | * resulting message would be: 687 | * ``` 688 | * Use your Hands! 689 | * ``` 690 | */ 691 | interface GroupReferenceDictionaryEntry extends TaggedDictionaryEntry { 692 | /** The name of the asset group to reference */ 693 | GroupName: AssetGroupName; 694 | } 695 | 696 | /** 697 | * A dictionary entry that references an asset. Note that a dictionary may contain multiple of these entries, one for 698 | * each asset mentioned or referenced in the message. For example, a message when swapping two restraints might contain 699 | * two of these entries, one for the restraint being removed, and one for the restraint being added. 700 | */ 701 | interface AssetReferenceDictionaryEntry extends GroupReferenceDictionaryEntry { 702 | /** The name of the asset being referenced */ 703 | AssetName: string; 704 | /** The (optional) {@link CraftingItem.Name} in case the asset was referenced via a crafted item */ 705 | CraftName?: string; 706 | } 707 | 708 | /** 709 | * A special instance of an {@link AssetReferenceDictionaryEntry} which indicates that this asset was used to carry 710 | * out an activity. 711 | */ 712 | interface ActivityAssetReferenceDictionaryEntry extends AssetReferenceDictionaryEntry { 713 | Tag: "ActivityAsset"; 714 | } 715 | 716 | /** 717 | * A metadata dictionary entry sent with a shock event message including a shock intensity representing the strength 718 | * of the shock. This is used to determine the severity of any visual or gameplay effects the shock may have. 719 | */ 720 | interface ShockEventDictionaryEntry { 721 | /** The intensity of the shock - must be a non-negative number */ 722 | ShockIntensity: number; 723 | } 724 | 725 | /** 726 | * A metadata dictionary entry sent with a shock event message including a shock intensity representing the strength 727 | * of the shock. This is used to determine the severity of any visual or gameplay effects the shock may have. 728 | */ 729 | interface SuctionEventDictionaryEntry { 730 | /** The intensity of the suction - must be a non-negative number */ 731 | SuctionLevel: number; 732 | } 733 | 734 | /** 735 | * A metadata dictionary entry indicating that the message has been generated due to an automated event. Can be used 736 | * to filter out what might otherwise be spammy chat messages (these include things like automatic vibrator intensity 737 | * changes and events & messages triggered by some futuristic items). 738 | */ 739 | interface AutomaticEventDictionaryEntry { 740 | /** Indicates that this message was triggered by an automatic event */ 741 | Automatic: true; 742 | } 743 | 744 | /** 745 | * A metadata dictionary entry carrying a numeric counter for an associated event or activity. Currently only used by 746 | * the Anal Beads XL to indicate how many beads were inserted. 747 | */ 748 | interface ActivityCounterDictionaryEntry { 749 | /** Counter metadata to be sent with a message */ 750 | ActivityCounter: number; 751 | } 752 | 753 | /** 754 | * A dictionary entry for group lookup & replacement. Used ambiguously for both {@link FocusGroupDictionaryEntry} and 755 | * {@link GroupReferenceDictionaryEntry}. This dictionary entry type is deprecated, and one of the aforementioned entry 756 | * types should be used instead. 757 | * @deprecated Use {@link FocusGroupDictionaryEntry}/{@link GroupReferenceDictionaryEntry} 758 | */ 759 | interface AssetGroupNameDictionaryEntry { 760 | Tag?: "FocusAssetGroup"; 761 | AssetGroupName: AssetGroupName; 762 | } 763 | 764 | /** 765 | * A dictionary entry indicating the name of an activity. Sent with chat messages to indicate that an activity was 766 | * carried out as part of the message. 767 | */ 768 | interface ActivityNameDictionaryEntry { 769 | /** The name of the activity carried out */ 770 | ActivityName: ActivityName; 771 | } 772 | 773 | /** 774 | * A dictionary entry indicating the ID of a message being replied to. 775 | */ 776 | interface ReplyIdDictionaryEntry { 777 | ReplyId: string; 778 | Tag: "ReplyId"; 779 | } 780 | /** 781 | * A dictionary entry indicating the ID of a message. 782 | */ 783 | interface MsgIdDictionaryEntry { 784 | MsgId: string; 785 | Tag: "MsgId"; 786 | } 787 | 788 | /** 789 | * A dictionary entry for teleport events 790 | */ 791 | interface MapViewTeleportEventDictionaryEntry { 792 | Tag: "MapViewTeleport"; 793 | Position: ChatRoomMapPos; 794 | } 795 | 796 | /** 797 | * A dictionary entry with metadata about the chat message transmitted. 798 | * 799 | * Send with Chat and Whisper-type messages to inform the other side about the 800 | * garbling and potentially ungarbled string if provided. 801 | */ 802 | interface MessageEffectEntry { 803 | Effects: SpeechTransformName[]; 804 | Original: string; 805 | } 806 | 807 | type ChatMessageDictionaryEntry = 808 | | CharacterReferenceDictionaryEntry 809 | | SourceCharacterDictionaryEntry 810 | | TargetCharacterDictionaryEntry 811 | | FocusGroupDictionaryEntry 812 | | TextDictionaryEntry 813 | | TextLookupDictionaryEntry 814 | | GroupReferenceDictionaryEntry 815 | | AssetReferenceDictionaryEntry 816 | | ActivityAssetReferenceDictionaryEntry 817 | | ShockEventDictionaryEntry 818 | | SuctionEventDictionaryEntry 819 | | AutomaticEventDictionaryEntry 820 | | ActivityCounterDictionaryEntry 821 | | AssetGroupNameDictionaryEntry 822 | | ActivityNameDictionaryEntry 823 | | MessageEffectEntry 824 | | MsgIdDictionaryEntry 825 | | ReplyIdDictionaryEntry 826 | | MapViewTeleportEventDictionaryEntry; 827 | 828 | 829 | type ChatMessageDictionary = ChatMessageDictionaryEntry[]; 830 | 831 | interface ServerChatRoomMessage extends ServerChatRoomMessageBase { 832 | /** The character to target the message at. null means it's broadcast to the room. */ 833 | Target?: number; 834 | Content: ServerChatRoomMessageContentType; 835 | Type: ServerChatRoomMessageType; 836 | Dictionary?: ChatMessageDictionary; 837 | Timeout?: number; 838 | } 839 | 840 | interface ServerChatRoomGameStart { 841 | GameProgress: "Start" | "Next" | "Stop" | "Skip"; 842 | } 843 | 844 | interface ServerChatRoomGameMagicBattleUpdateRequest { 845 | GameProgress: "Action"; 846 | Action: 847 | /* MagicBattle */ "SpellSuccess" | "SpellFail" | 848 | /* LARP */ "Pass" | "Seduce" | "Struggle" | "Hide" | "Cover" | 849 | "Strip" | "Tighten" | "RestrainArms" | "RestrainLegs" | "RestrainMouth" | 850 | "Silence" | "Immobilize" | "Detain" | "Dress" | "Costume" 851 | ; 852 | Spell: number; 853 | Time: number; 854 | Target: number; 855 | } 856 | 857 | interface ServerChatRoomGameLARPUpdateRequest { 858 | GameProgress: "Action"; 859 | Action: "Pass" | "Seduce" | "Struggle" | "Hide" | "Cover" | 860 | "Strip" | "Tighten" | "RestrainArms" | "RestrainLegs" | "RestrainMouth" | 861 | "Silence" | "Immobilize" | "Detain" | "Dress" | "Costume" 862 | 863 | | ""; 864 | Item?: string; 865 | Target: number; 866 | } 867 | 868 | interface ServerChatRoomGameBountyUpdateRequest { 869 | OnlineBounty: { 870 | finishTime: number, 871 | target: number, 872 | } 873 | } 874 | 875 | interface ServerChatRoomGameKDUpdateRequest { 876 | KinkyDungeon: any; 877 | } 878 | 879 | interface ServerChatRoomGameCardGameData { 880 | MemberNumber: number; 881 | Playing: boolean; 882 | Level: number; 883 | Fame: number; 884 | Money: number; 885 | LastFamePerTurn: number; 886 | LastMoneyPerTurn: number; 887 | FullDeck: string; 888 | Deck: string; 889 | Hand: string; 890 | Board: string; 891 | Event: string; 892 | DiscardPile: string; 893 | CardsPlayedThisTurn: Record; 894 | ClubCardTurnCounter: number; 895 | Sleeve: number; 896 | } 897 | 898 | interface ServerChatRoomGameCardGameQueryRequest { 899 | GameProgress: "Query"; 900 | CCData?: ServerChatRoomGameCardGameData[]; 901 | Player1?: number; 902 | Player2?: number; 903 | } 904 | 905 | interface ServerChatRoomGameCardGameStartRequest { 906 | GameProgress: "Start"; 907 | Player1: number; 908 | Player2: number; 909 | } 910 | 911 | type ServerChatRoomGameCardGameActionRequest = { GameProgress: "Action" } & ({ CCLog: any } | { CCData: any }); 912 | 913 | type ServerChatRoomGameCardGameUpdateRequest = ServerChatRoomGameCardGameStartRequest | ServerChatRoomGameCardGameQueryRequest | ServerChatRoomGameCardGameActionRequest; 914 | 915 | type ServerChatRoomGameUpdateRequest = 916 | | ServerChatRoomGameStart 917 | | ServerChatRoomGameMagicBattleUpdateRequest 918 | | ServerChatRoomGameLARPUpdateRequest 919 | | ServerChatRoomGameBountyUpdateRequest 920 | | ServerChatRoomGameKDUpdateRequest 921 | | ServerChatRoomGameCardGameUpdateRequest; 922 | 923 | interface ServerChatRoomGameResponse extends ServerChatRoomMessageBase { 924 | Data: { 925 | KinkyDungeon?: any; 926 | OnlineBounty?: any; 927 | /* LARP */ 928 | GameProgress?: "Start" | "Stop" | "Next" | "Skip" | "Action" | "Query"; 929 | Action?: undefined; 930 | Target?: number; 931 | Item?: string; 932 | 933 | /* MagicBattle */ 934 | Spell?: string; 935 | Time?: number; /* ms */ 936 | 937 | /* Club Card */ 938 | Player1?: number; 939 | Player2?: number; 940 | CCData: ServerChatRoomGameCardGameData[]; 941 | CCLog: ClubCardMessage; 942 | }; 943 | RNG: number; 944 | } 945 | 946 | interface ServerChatRoomSyncCharacterResponse { 947 | SourceMemberNumber: number; 948 | Character: ServerAccountDataSynced; 949 | } 950 | 951 | interface ServerChatRoomSyncMemberJoinResponse { 952 | SourceMemberNumber: number; 953 | Character: ServerChatRoomSyncCharacterResponse["Character"], 954 | WhiteListedBy: number[]; 955 | BlackListedBy: number[] 956 | } 957 | 958 | interface ServerChatRoomLeaveResponse { 959 | SourceMemberNumber: number; 960 | } 961 | 962 | interface ServerChatRoomReorderResponse { 963 | PlayerOrder: number[]; 964 | } 965 | 966 | interface ServerCharacterUpdate { 967 | ID: string; 968 | ActivePose: readonly string[]; 969 | Appearance: ServerAppearanceBundle; 970 | } 971 | 972 | interface ServerCharacterExpressionUpdate { 973 | Name: string; 974 | Group: string; 975 | Appearance: ServerAppearanceBundle; 976 | } 977 | 978 | interface ServerCharacterExpressionResponse { 979 | MemberNumber: number; 980 | Name: string; 981 | Group: string 982 | } 983 | 984 | interface ServerCharacterPoseUpdate { 985 | Pose: string | readonly string[] | null; 986 | } 987 | 988 | interface ServerCharacterPoseResponse { 989 | MemberNumber: number; 990 | Pose: readonly string[]; 991 | } 992 | 993 | interface ServerCharacterArousalUpdate { 994 | OrgasmTimer: number; 995 | OrgasmCount: number; 996 | Progress: number; 997 | ProgressTimer: number; 998 | } 999 | 1000 | interface ServerCharacterArousalResponse { 1001 | MemberNumber: number; 1002 | OrgasmTimer: number; 1003 | OrgasmCount: number; 1004 | Progress: number; 1005 | ProgressTimer: number; 1006 | } 1007 | 1008 | interface ServerCharacterItemUpdate { 1009 | Target: number; 1010 | Group: AssetGroupName; 1011 | Name?: string; 1012 | Color: string | string[]; 1013 | Difficulty: number; 1014 | Property?: ItemProperties; 1015 | Craft?: CraftingItem; 1016 | } 1017 | 1018 | interface ServerChatRoomSyncItemResponse { 1019 | Source: number; 1020 | Item: ServerCharacterItemUpdate; 1021 | } 1022 | 1023 | type ServerChatRoomUpdateResponse = "RoomAlreadyExist" | "Updated" | "InvalidRoomData"; 1024 | 1025 | interface ServerChatRoomAllowItemRequest { 1026 | MemberNumber: number; 1027 | } 1028 | 1029 | interface ServerChatRoomAllowItemResponse { 1030 | MemberNumber: number; 1031 | AllowItem: boolean; 1032 | } 1033 | 1034 | //#endregion 1035 | 1036 | //#region Socket.io defines 1037 | 1038 | interface ServerToClientEvents { 1039 | ServerInfo: (data: ServerInfoMessage) => void; 1040 | ServerMessage: (data: string) => void; 1041 | ForceDisconnect: (data: ServerForceDisconnectMessage) => void; 1042 | 1043 | CreationResponse: (data: ServerAccountCreateResponse) => void; 1044 | 1045 | PasswordResetResponse: (data: ServerPasswordResetResponse) => void; 1046 | 1047 | LoginResponse: (data: ServerLoginResponse) => void; 1048 | LoginQueue: (data: ServerLoginQueueResponse) => void; 1049 | 1050 | AccountQueryResult: (data: ServerAccountQueryResponse) => void; 1051 | AccountLovership: (data: ServerAccountLovershipResponse) => void; 1052 | AccountOwnership: (data: ServerAccountOwnershipResponse) => void; 1053 | AccountBeep: (data: ServerAccountBeepResponse) => void; 1054 | 1055 | ChatRoomSearchResult: (data: ServerChatRoomSearchResultResponse) => void; 1056 | ChatRoomCreateResponse: (data: ServerChatRoomCreateResponse) => void; 1057 | ChatRoomSearchResponse: (data: ServerChatRoomSearchResponse) => void; 1058 | 1059 | ChatRoomMessage: (data: ServerChatRoomMessage) => void; 1060 | ChatRoomGameResponse: (data: ServerChatRoomGameResponse) => void; 1061 | ChatRoomSync: (data: ServerChatRoomSyncMessage) => void; 1062 | ChatRoomSyncCharacter: (data: ServerChatRoomSyncCharacterResponse) => void; 1063 | ChatRoomSyncMemberJoin: (data: ServerChatRoomSyncMemberJoinResponse) => void; 1064 | ChatRoomSyncMemberLeave: (data: ServerChatRoomLeaveResponse) => void; 1065 | ChatRoomSyncRoomProperties: (data: ServerChatRoomSyncPropertiesMessage) => void; 1066 | ChatRoomSyncReorderPlayers: (data: ServerChatRoomReorderResponse) => void; 1067 | ChatRoomSyncSingle: (data: ServerChatRoomSyncCharacterResponse) => void; 1068 | ChatRoomSyncExpression: (data: ServerCharacterExpressionResponse) => void; 1069 | ChatRoomSyncPose: (data: ServerCharacterPoseResponse) => void; 1070 | ChatRoomSyncArousal: (data: ServerCharacterArousalResponse) => void; 1071 | ChatRoomSyncItem: (data: ServerChatRoomSyncItemResponse) => void; 1072 | ChatRoomSyncMapData: (data: ServerMapDataResponse) => void; 1073 | 1074 | ChatRoomUpdateResponse: (data: ServerChatRoomUpdateResponse) => void; 1075 | 1076 | ChatRoomAllowItem: (data: ServerChatRoomAllowItemResponse) => void; 1077 | 1078 | } 1079 | 1080 | interface ClientToServerEvents { 1081 | AccountLogin: (data: ServerAccountLoginRequest) => void; 1082 | AccountCreate: (data: ServerAccountCreateRequest) => void; 1083 | PasswordReset: (Email: ServerPasswordResetRequest) => void; 1084 | PasswordResetProcess: (data: ServerPasswordResetProcessRequest) => void; 1085 | 1086 | // Post-login events 1087 | AccountUpdate: (data: ServerAccountUpdateRequest) => void; 1088 | AccountUpdateEmail: (data: ServerAccountUpdateEmailRequest) => void; 1089 | AccountQuery: (data: ServerAccountQueryRequest) => void; 1090 | AccountBeep: (data: ServerAccountBeepRequest) => void; 1091 | AccountOwnership: (data: ServerAccountOwnershipRequest) => void; 1092 | AccountLovership: (data: ServerAccountLovershipRequest) => void; 1093 | AccountDifficulty: (level: number) => void; 1094 | AccountDisconnect: (data: never) => void; 1095 | 1096 | ChatRoomSearch: (data: ServerChatRoomSearchRequest) => void; 1097 | ChatRoomCreate: (data: ServerChatRoomCreateRequest) => void; 1098 | ChatRoomJoin: (data: ServerChatRoomJoinRequest) => void; 1099 | ChatRoomLeave: (data: "") => void; 1100 | ChatRoomChat: (data: ServerChatRoomMessage) => void; 1101 | 1102 | ChatRoomCharacterUpdate: (data: ServerCharacterUpdate) => void; 1103 | ChatRoomCharacterExpressionUpdate: (data: ServerCharacterExpressionUpdate) => void; 1104 | ChatRoomCharacterPoseUpdate: (data: ServerCharacterPoseUpdate) => void; 1105 | ChatRoomCharacterArousalUpdate: (data: ServerCharacterArousalUpdate) => void; 1106 | ChatRoomCharacterItemUpdate: (data: ServerCharacterItemUpdate) => void; 1107 | ChatRoomCharacterMapDataUpdate: (data: ChatRoomMapData) => void; 1108 | 1109 | ChatRoomAdmin: (data: ServerChatRoomAdminRequest) => void; 1110 | ChatRoomAllowItem: (data: ServerChatRoomAllowItemRequest) => void; 1111 | 1112 | ChatRoomGame: (data: ServerChatRoomGameUpdateRequest) => void; 1113 | } 1114 | 1115 | //#endregion 1116 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | require('newrelic'); 3 | const base64id = require("base64id"); 4 | 5 | // Reads the SSL key and certificate, if there's no file available, we switch to regular http 6 | /*var SyncRequest = require("sync-request"); 7 | var ServerKey = null; 8 | if ((process.env.SERVER_KEY0 != null) && (process.env.SERVER_KEY0 != "")) { try { ServerKey = SyncRequest("GET", process.env.SERVER_KEY0).getBody(); } catch(err) {} } 9 | if ((ServerKey == null) && (process.env.SERVER_KEY1 != null) && (process.env.SERVER_KEY1 != "")) { try { ServerKey = SyncRequest("GET", process.env.SERVER_KEY1).getBody(); } catch(err) {} } 10 | if ((ServerKey == null) && (process.env.SERVER_KEY2 != null) && (process.env.SERVER_KEY2 != "")) { try { ServerKey = SyncRequest("GET", process.env.SERVER_KEY2).getBody(); } catch(err) {} } 11 | var ServerCert = null; 12 | if ((process.env.SERVER_CERT0 != null) && (process.env.SERVER_CERT0 != "")) { try { ServerCert = SyncRequest("GET", process.env.SERVER_CERT0).getBody(); } catch(err) {} } 13 | if ((ServerCert == null) && (process.env.SERVER_CERT1 != null) && (process.env.SERVER_CERT1 != "")) { try { ServerCert = SyncRequest("GET", process.env.SERVER_CERT1).getBody(); } catch(err) {} } 14 | if ((ServerCert == null) && (process.env.SERVER_CERT2 != null) && (process.env.SERVER_CERT2 != "")) { try { ServerCert = SyncRequest("GET", process.env.SERVER_CERT2).getBody(); } catch(err) {} } 15 | console.log("Using Server Key: " + ServerKey); 16 | console.log("Using Server Certificate: " + ServerCert);*/ 17 | 18 | // Enforce https with a certificate 19 | var App; 20 | var UseSecure; 21 | UseSecure = false; 22 | App = require("http").createServer(); 23 | 24 | /*if ((ServerKey == null) || (ServerCert == null)) { 25 | console.log("No key or certificate found, starting http server with origin " + process.env.CORS_ORIGIN0); 26 | } else { 27 | console.log("Starting https server for certificate with origin " + process.env.CORS_ORIGIN0); 28 | UseSecure = true; 29 | App = require("https").createServer({ key: ServerKey, cert: ServerCert, requestCert: false, rejectUnauthorized: false }); 30 | }*/ 31 | 32 | // Starts socket.io to accept incoming connections on specified origins 33 | const socketio = require("socket.io"); 34 | /** @type {Partial} */ 35 | var Options = { 36 | maxHttpBufferSize: 180000, 37 | pingTimeout: 30000, 38 | pingInterval: 50000, 39 | upgradeTimeout: 30000, 40 | serveClient: false, 41 | httpCompression: true, 42 | perMessageDeflate: true, 43 | allowEIO3: false, 44 | secure: UseSecure 45 | }; 46 | if ((process.env.CORS_ORIGIN0 != null) && (process.env.CORS_ORIGIN0 != "")) 47 | Options.cors = { origin: [process.env.CORS_ORIGIN0 || "", process.env.CORS_ORIGIN1 || "", process.env.CORS_ORIGIN2 || "", process.env.CORS_ORIGIN3 || "", process.env.CORS_ORIGIN4 || "", process.env.CORS_ORIGIN5 || ""] }; 48 | else 49 | Options.cors = { origin: '*' }; 50 | 51 | /** @type {import("socket.io").Server} */ 52 | var IO = new socketio.Server(App, Options); 53 | 54 | // Main game objects 55 | var BCrypt = require("bcrypt"); 56 | var MaxHeapUsage = parseInt(process.env.MAX_HEAP_USAGE, 10) || 16_000_000_000; // 16 gigs allocated by default, can be altered server side 57 | var AccountCollection = process.env.ACCOUNT_COLLECTION || "Accounts"; 58 | /** @type {Account[]} */ 59 | var Account = []; 60 | /** @type {Chatroom[]} */ 61 | var ChatRoom = []; 62 | var ChatRoomMessageType = ["Chat", "Action", "Activity", "Emote", "Whisper", "Hidden", "Status"]; 63 | var ChatRoomProduction = [ 64 | process.env.PRODUCTION0 || "", 65 | process.env.PRODUCTION1 || "", 66 | process.env.PRODUCTION2 || "", 67 | process.env.PRODUCTION3 || "", 68 | process.env.PRODUCTION4 || "", 69 | process.env.PRODUCTION5 || "", 70 | process.env.PRODUCTION6 || "", 71 | process.env.PRODUCTION7 || "", 72 | process.env.PRODUCTION8 || "", 73 | process.env.PRODUCTION9 || "", 74 | process.env.PRODUCTION10 || "", 75 | process.env.PRODUCTION11 || "", 76 | process.env.PRODUCTION12 || "" 77 | ]; 78 | var NextMemberNumber = 1; 79 | var NextPasswordReset = 0; 80 | var OwnershipDelay = 604800000; // 7 days delay for ownership events 81 | var LovershipDelay = 604800000; // 7 days delay for lovership events 82 | var DifficultyDelay = 604800000; // 7 days to activate the higher difficulty tiers 83 | const IP_CONNECTION_LIMIT = parseInt(process.env.IP_CONNECTION_LIMIT, 10) || 64; // Limit of connections per IP address 84 | const IP_CONNECTION_RATE_LIMIT = parseInt(process.env.IP_CONNECTION_RATE_LIMIT, 10) || 2; // Limit of newly established connections per IP address within a second 85 | const CLIENT_MESSAGE_RATE_LIMIT = parseInt(process.env.CLIENT_MESSAGE_RATE_LIMIT, 10) || 20; // Limit the number of messages received from a client within a second 86 | const IP_CONNECTION_PROXY_HEADER = "x-forwarded-for"; // Header with real IP, if set by trusted proxy (lowercase) 87 | const ROOM_LIMIT_DEFAULT = 10; // The default number of players in an online chat room 88 | const ROOM_LIMIT_MINIMUM = 2; // The minimum number of players in an online chat room 89 | const ROOM_LIMIT_MAXIMUM = 20; // The maximum number of players in an online chat room 90 | 91 | // Limits the number of accounts created on each hour & day 92 | var AccountCreationIP = []; 93 | const MAX_IP_ACCOUNT_PER_DAY = parseInt(process.env.MAX_IP_ACCOUNT_PER_DAY, 10) || 12; 94 | const MAX_IP_ACCOUNT_PER_HOUR = parseInt(process.env.MAX_IP_ACCOUNT_PER_HOUR, 10) || 4; 95 | 96 | // DB Access 97 | /** @type { import("mongodb").Db } */ 98 | var Database; 99 | var DatabaseClient = require('mongodb').MongoClient; 100 | var DatabaseURL = process.env.DATABASE_URL || "mongodb://localhost:27017/BondageClubDatabase"; 101 | var ServerPort = process.env.PORT || 4288; 102 | var DatabaseName = process.env.DATABASE_NAME || "BondageClubDatabase"; 103 | 104 | /** 105 | * Email password reset 106 | * @type { { AccountName: string; ResetNumber: string; }[] } 107 | */ 108 | var PasswordResetProgress = []; 109 | var NodeMailer = require("nodemailer"); 110 | var MailTransporter = NodeMailer.createTransport({ 111 | host: "mail.bondageprojects.com", 112 | Port: 465, 113 | secure: true, 114 | auth: { 115 | user: "donotreply@bondageprojects.com", 116 | pass: process.env.EMAIL_PASSWORD || "" 117 | } 118 | }); 119 | 120 | // If the server received an unhandled error, we log it through console for future review, send an email and exit so the application can restart 121 | Error.stackTraceLimit = 100; 122 | process.on('uncaughtException', function(error) { 123 | console.log("*************************"); 124 | console.log("Unhandled error occurred:"); 125 | console.log(error.stack); 126 | console.log("*************************"); 127 | var mailOptions = { 128 | from: "donotreply@bondageprojects.com", 129 | to: process.env.EMAIL_ADMIN || "", 130 | subject: "Bondage Club Server Crash", 131 | html: "Unhandled error occurred:
" + error.stack 132 | }; 133 | MailTransporter.sendMail(mailOptions, function (err, info) { 134 | if (err) console.log("Error while sending error email: " + err); 135 | else console.log("Error email was sent"); 136 | try { 137 | AccountDelayedUpdate(); 138 | } catch (error) { 139 | console.log("Error while doing delayed updates"); 140 | } 141 | process.exit(1); 142 | }); 143 | }); 144 | 145 | // When SIGTERM is received, we send a warning to all logged accounts 146 | process.on('SIGTERM', function() { 147 | console.log("***********************"); 148 | console.log("HEROKU SIGTERM DETECTED"); 149 | console.log("***********************"); 150 | try { 151 | AccountDelayedUpdate(); 152 | } catch (error) { 153 | console.log("Error while doing delayed updates"); 154 | } 155 | for (const Acc of Account) 156 | if ((Acc != null) && (Acc.Socket != null)) 157 | Acc.Socket.emit("ServerMessage", "Server will reboot in 30 seconds." ); 158 | }); 159 | 160 | // When SIGKILL is received, we do the final updates 161 | /*process.on('SIGKILL', function() { 162 | console.log("***********************"); 163 | console.log("HEROKU SIGKILL DETECTED"); 164 | console.log("***********************"); 165 | try { 166 | AccountDelayedUpdate(); 167 | } catch (error) { 168 | console.log("Error while doing delayed updates"); 169 | } 170 | process.exit(2); 171 | });*/ 172 | 173 | /** @type {Map} */ 174 | const IPConnections = new Map(); 175 | 176 | // These regex must be kept in sync with the client 177 | const ServerAccountEmailRegex = /^[a-zA-Z0-9@.!#$%&'*+/=?^_`{|}~-]{5,100}$/; 178 | const ServerAccountNameRegex = /^[a-zA-Z0-9]{1,20}$/; 179 | const ServerAccountPasswordRegex = /^[a-zA-Z0-9]{1,20}$/; 180 | const ServerAccountResetNumberRegex = /^[0-9]{1,20}$/; 181 | const ServerCharacterNameRegex = /^[a-zA-Z ]{1,20}$/; 182 | const ServerCharacterNicknameRegex = /^[\p{L}\p{Nd}\p{Z}'-]+$/u; 183 | const ServerChatRoomNameRegex = /^[\x20-\x7E]{1,20}$/; 184 | const ServerChatMessageMaxLength = 2000; 185 | const ServerChatRoomDescriptionMaxLength = 300; 186 | 187 | /** 188 | * Type guard which checks that a value is a simple object (i.e. a non-null object which is not an array) 189 | * @param {unknown} value - The value to test 190 | * @returns {value is Record} 191 | */ 192 | function CommonIsObject(value) { 193 | return !!value && typeof value === "object" && !Array.isArray(value); 194 | } 195 | 196 | /** 197 | * Check that the passed string looks like an acceptable email address. 198 | * 199 | * @param {string} Email 200 | * @returns {boolean} 201 | */ 202 | function CommonEmailIsValid(Email) { 203 | if (!ServerAccountEmailRegex.test(Email)) return false; 204 | 205 | const parts = Email.split("@"); 206 | if (parts.length !== 2) return false; 207 | if (parts[1].indexOf(".") === -1) return false; 208 | 209 | return true; 210 | } 211 | 212 | // Connects to the Mongo Database 213 | DatabaseClient.connect(DatabaseURL, { useUnifiedTopology: true, useNewUrlParser: true, autoIndex: false }, function(err, db) { 214 | 215 | // Keeps the database object 216 | if (err) throw err; 217 | Database = db.db(DatabaseName); 218 | console.log("****************************************"); 219 | console.log("Database: " + DatabaseName + " connected"); 220 | 221 | // Gets the next unique member number 222 | Database.collection(AccountCollection).find({ MemberNumber : { $exists: true, $ne: null }}).sort({MemberNumber: -1}).limit(1).toArray(function(err, result) { 223 | 224 | // Shows the next member number 225 | if ((result.length > 0) && (result[0].MemberNumber != null)) NextMemberNumber = result[0].MemberNumber + 1; 226 | console.log("Next Member Number: " + NextMemberNumber); 227 | 228 | // Listens for clients on port 4288 if local or a random port if online 229 | App.listen(ServerPort, function () { 230 | 231 | // Sets up the Client/Server events 232 | console.log("Bondage Club server is listening on " + (ServerPort).toString()); 233 | console.log("****************************************"); 234 | IO.on("connection", function ( /** @type {ServerSocket} */ socket) { 235 | /** @type {string} */ 236 | let address = socket.conn.remoteAddress; 237 | 238 | // If there is trusted forward header set by proxy, use that instead 239 | // But only trust the last hop! 240 | if (IP_CONNECTION_PROXY_HEADER && typeof socket.handshake.headers[IP_CONNECTION_PROXY_HEADER] === "string") { 241 | const hops = /** @type {string} */ (socket.handshake.headers[IP_CONNECTION_PROXY_HEADER]).split(","); 242 | address = hops[hops.length-1].trim(); 243 | } 244 | 245 | const sameIPConnections = IPConnections.get(address) || []; 246 | 247 | // True, if there has already been IP_CONNECTION_RATE_LIMIT number of connections in the last second 248 | const ipOverRateLimit = sameIPConnections.length >= IP_CONNECTION_RATE_LIMIT && Date.now() - sameIPConnections[sameIPConnections.length - IP_CONNECTION_RATE_LIMIT] <= 1000; 249 | 250 | // Reject connection if over limits (rate & concurrency) 251 | if (sameIPConnections.length >= IP_CONNECTION_LIMIT || ipOverRateLimit) { 252 | console.log("Rejecting connection (IP connection limit reached) from", address); 253 | socket.emit("ForceDisconnect", "ErrorRateLimited"); 254 | socket.disconnect(true); 255 | return; 256 | } 257 | 258 | // Connection accepted, count it 259 | sameIPConnections.push(Date.now()); 260 | IPConnections.set(address, sameIPConnections); 261 | socket.once("disconnect", () => { 262 | const sameIPConnectionsDisconnect = IPConnections.get(address) || []; 263 | if (sameIPConnectionsDisconnect.length <= 1) { 264 | IPConnections.delete(address); 265 | } else { 266 | sameIPConnectionsDisconnect.shift(); // Delete first (oldest) from array 267 | IPConnections.set(address, sameIPConnectionsDisconnect); 268 | } 269 | }); 270 | 271 | // Rate limit all messages and kill the connection, if limits exceeded. 272 | const messageBucket = []; 273 | for (let i = 0; i < CLIENT_MESSAGE_RATE_LIMIT; i++) { 274 | messageBucket.push(0); 275 | } 276 | socket.onAny(() => { 277 | const lastMessageTime = messageBucket.shift(); 278 | messageBucket.push(Date.now()); 279 | 280 | // More than CLIENT_MESSAGE_RATE_LIMIT number of messages in the last second 281 | if (Date.now() - lastMessageTime <= 1000) { 282 | // Disconnect and close connection 283 | socket.emit("ForceDisconnect", "ErrorRateLimited"); 284 | socket.disconnect(true); 285 | } 286 | }); 287 | 288 | socket.on("AccountCreate", function (data) { AccountCreate(data, socket); }); 289 | socket.on("AccountLogin", function (data) { AccountLogin(data, socket); }); 290 | socket.on("PasswordReset", function(data) { PasswordReset(data, socket); }); 291 | socket.on("PasswordResetProcess", function(data) { PasswordResetProcess(data, socket); }); 292 | AccountSendServerInfo(socket); 293 | }); 294 | 295 | // Refreshes the server information to clients each 60 seconds 296 | setInterval(AccountSendServerInfo, 60000); 297 | 298 | // Updates the database appearance & skills every 300 seconds 299 | setInterval(AccountDelayedUpdate, 300000); 300 | 301 | }); 302 | }); 303 | }); 304 | 305 | /** 306 | * Setups socket on successful login or account creation 307 | * @param {ServerSocket} socket 308 | */ 309 | function OnLogin(socket) { 310 | socket.removeAllListeners("AccountCreate"); 311 | socket.removeAllListeners("AccountLogin"); 312 | socket.removeAllListeners("PasswordReset"); 313 | socket.removeAllListeners("PasswordResetProcess"); 314 | socket.on("AccountUpdate", function(data) { AccountUpdate(data, socket); }); 315 | socket.on("AccountUpdateEmail", function(data) { AccountUpdateEmail(data, socket); }); 316 | socket.on("AccountQuery", function(data) { AccountQuery(data, socket); }); 317 | socket.on("AccountBeep", function(data) { AccountBeep(data, socket); }); 318 | socket.on("AccountOwnership", function(data) { AccountOwnership(data, socket); }); 319 | socket.on("AccountLovership", function(data) { AccountLovership(data, socket); }); 320 | socket.on("AccountDifficulty", function(data) { AccountDifficulty(data, socket); }); 321 | socket.on("AccountDisconnect", function() { AccountRemove(socket.id); }); 322 | socket.on("disconnect", function() { AccountRemove(socket.id); }); 323 | socket.on("ChatRoomSearch", function(data) { ChatRoomSearch(data, socket); }); 324 | socket.on("ChatRoomCreate", function(data) { ChatRoomCreate(data, socket); }); 325 | socket.on("ChatRoomJoin", function(data) { ChatRoomJoin(data, socket); }); 326 | socket.on("ChatRoomLeave", function() { ChatRoomLeave(socket); }); 327 | socket.on("ChatRoomChat", function(data) { ChatRoomChat(data, socket); }); 328 | socket.on("ChatRoomCharacterUpdate", function(data) { ChatRoomCharacterUpdate(data, socket); }); 329 | socket.on("ChatRoomCharacterExpressionUpdate", function(data) { ChatRoomCharacterExpressionUpdate(data, socket); }); 330 | socket.on("ChatRoomCharacterMapDataUpdate", function(data) { ChatRoomCharacterMapDataUpdate(data, socket); }); 331 | socket.on("ChatRoomCharacterPoseUpdate", function(data) { ChatRoomCharacterPoseUpdate(data, socket); }); 332 | socket.on("ChatRoomCharacterArousalUpdate", function(data) { ChatRoomCharacterArousalUpdate(data, socket); }); 333 | socket.on("ChatRoomCharacterItemUpdate", function(data) { ChatRoomCharacterItemUpdate(data, socket); }); 334 | socket.on("ChatRoomAdmin", function(data) { ChatRoomAdmin(data, socket); }); 335 | socket.on("ChatRoomAllowItem", function(data) { ChatRoomAllowItem(data, socket); }); 336 | socket.on("ChatRoomGame", function(data) { ChatRoomGame(data, socket); }); 337 | } 338 | 339 | /** 340 | * Sends the server info to all players or one specific player (socket) 341 | * @param {ServerSocket} [socket] 342 | */ 343 | function AccountSendServerInfo(socket) { 344 | 345 | // Validates if the heap usage is too high and we should reboot, to prevent memory leaks 346 | const MemoryData = process.memoryUsage(); 347 | if ((MemoryData != null) && (MemoryData.heapUsed > MaxHeapUsage)) { 348 | var mailOptions = { 349 | from: "donotreply@bondageprojects.com", 350 | to: process.env.EMAIL_ADMIN || "", 351 | subject: "Bondage Club Server Heap Usage Crash", 352 | html: "Heap usage error occured:
heapTotal: " + MemoryData.heapTotal.toString() + "
heapUsed: " + MemoryData.heapUsed.toString() 353 | }; 354 | MailTransporter.sendMail(mailOptions, function (err, info) { 355 | if (err) console.log("Error while sending error email: " + err); 356 | else console.log("Error email was sent"); 357 | try { 358 | AccountDelayedUpdate(); 359 | } catch (error) { 360 | console.log("Error while doing delayed updates"); 361 | } 362 | process.exit(2); 363 | }); 364 | return; 365 | } 366 | 367 | // Sends the info to all players 368 | var SI = { 369 | Time: CommonTime(), 370 | OnlinePlayers: Account.length 371 | }; 372 | if (socket != null) socket.emit("ServerInfo", SI); 373 | else IO.sockets.volatile.emit("ServerInfo", SI); 374 | 375 | } 376 | 377 | /** 378 | * Return the current time 379 | * @returns {number} 380 | */ 381 | function CommonTime() { 382 | return new Date().getTime(); 383 | } 384 | 385 | /** 386 | * Type guard which checks that a value is a simple object (i.e. a non-null object which is not an array) 387 | * @param {unknown} value - The value to test 388 | * @returns {value is Record} 389 | */ 390 | function CommonIsObject(value) { 391 | return !!value && typeof value === "object" && !Array.isArray(value); 392 | } 393 | 394 | /** 395 | * Parses a integer out of something, with a default value 396 | * @param {unknown} thing 397 | * @param {number} defaultValue 398 | * @returns {number} 399 | */ 400 | function CommonParseInt(thing, defaultValue = 0, base = 10) { 401 | if (typeof thing !== "string" && typeof thing !== "number") return defaultValue; 402 | if (typeof thing === "number") { 403 | if (Number.isInteger(thing)) { 404 | return thing; 405 | } else { 406 | return defaultValue; 407 | } 408 | } 409 | let int = parseInt(thing, base); 410 | if (!Number.isInteger(int)) int = defaultValue; 411 | return int; 412 | } 413 | 414 | /** 415 | * Creates a new account by creating its file 416 | * @param {ServerAccountCreateRequest} data 417 | * @param {ServerSocket} socket 418 | */ 419 | function AccountCreate(data, socket) { 420 | 421 | // Makes sure the account comes with a name and a password 422 | if ((data != null) && (typeof data === "object") && (data.Name != null) && (data.AccountName != null) && (data.Password != null) && (data.Email != null) && (typeof data.Name === "string") && (typeof data.AccountName === "string") && (typeof data.Password === "string") && (typeof data.Email === "string")) { 423 | 424 | // Makes sure the data is valid 425 | if (data.Name.match(ServerCharacterNameRegex) && data.AccountName.match(ServerAccountNameRegex) && data.Password.match(ServerAccountPasswordRegex) && (CommonEmailIsValid(data.Email) || data.Email == "") && (data.Email.length <= 100)) { 426 | 427 | // Gets the current IP Address that's creating the account 428 | /** @type {string} */ 429 | let CurrentIP = socket.conn.remoteAddress; 430 | if (IP_CONNECTION_PROXY_HEADER && typeof socket.handshake.headers[IP_CONNECTION_PROXY_HEADER] === "string") { 431 | const hops = /** @type {string} */ (socket.handshake.headers[IP_CONNECTION_PROXY_HEADER]).split(","); 432 | CurrentIP = hops[hops.length-1].trim(); 433 | } 434 | 435 | // If the IP is valid 436 | if ((CurrentIP != null) && (CurrentIP != "")) { 437 | 438 | // Checks the number of account created in total and in the last hour by this IP 439 | let CurrentTime = CommonTime(); 440 | let TotalCount = 0; 441 | let HourCount = 0; 442 | for (let IP of AccountCreationIP) 443 | if (IP.Address === CurrentIP) { 444 | TotalCount++; 445 | if (IP.Time >= CurrentTime - 3600000) HourCount++; 446 | } 447 | 448 | /*var mailOptions = { 449 | from: "donotreply@bondageprojects.com", 450 | to: process.env.EMAIL_ADMIN || "", 451 | subject: "Bondage Club Server Info", 452 | html: "IP: " + CurrentIP + " is creating account: " + data.AccountName + " at time: " + CommonTime().toString() + "
TotalCount: " + TotalCount.toString() + "
MAX_IP_ACCOUNT_PER_DAY: " + MAX_IP_ACCOUNT_PER_DAY.toString() + "
HourCount: " + HourCount.toString() + "
MAX_IP_ACCOUNT_PER_HOUR: " + MAX_IP_ACCOUNT_PER_HOUR.toString() 453 | }; 454 | MailTransporter.sendMail(mailOptions, function (err, info) {});*/ 455 | 456 | // Exits if we reached the limit 457 | if ((TotalCount >= MAX_IP_ACCOUNT_PER_DAY) || (HourCount >= MAX_IP_ACCOUNT_PER_HOUR)) { 458 | socket.emit("CreationResponse", "New accounts per day exceeded"); 459 | return; 460 | } 461 | 462 | // Keeps the IP in memory for the next run 463 | AccountCreationIP.push({ Address: CurrentIP, Time: CurrentTime }); 464 | 465 | } 466 | 467 | // Checks if the account already exists 468 | data.AccountName = data.AccountName.toUpperCase(); 469 | Database.collection(AccountCollection).findOne({ AccountName : data.AccountName }, function(err, result) { 470 | 471 | // Makes sure the result is null so the account doesn't already exists 472 | if (err) throw err; 473 | if (result != null) { 474 | socket.emit("CreationResponse", "Account already exists"); 475 | } else { 476 | 477 | // Creates a hashed password and saves it with the account info 478 | BCrypt.hash(data.Password.toUpperCase(), 10, function( err, hash ) { 479 | if (err) throw err; 480 | let account = /** @type {Account} */ ({ 481 | // ID and Socket are special; they're used at runtime but cannot be 482 | // persisted to the database so they're set after that happens. 483 | AccountName: data.AccountName, 484 | Email: data.Email, 485 | Password: hash, 486 | // Use the next member number and bump it 487 | MemberNumber: NextMemberNumber++, 488 | Name: data.Name, 489 | Money: 100, 490 | Creation: CommonTime(), 491 | LastLogin: CommonTime(), 492 | Environment: AccountGetEnvironment(socket), 493 | Lovership: [], 494 | ItemPermission: 2, 495 | FriendList: [], 496 | WhiteList: [], 497 | BlackList: [], 498 | }); 499 | Database.collection(AccountCollection).insertOne(account, function(err, res) { 500 | if (err) throw err; 501 | account.ID = socket.id; 502 | account.Socket = socket; 503 | console.log("Creating new account: " + account.AccountName + " ID: " + socket.id + " " + account.Environment); 504 | AccountValidData(account); 505 | Account.push(account); 506 | OnLogin(socket); 507 | socket.emit("CreationResponse", { ServerAnswer: "AccountCreated", OnlineID: account.ID, MemberNumber: account.MemberNumber } ); 508 | AccountSendServerInfo(socket); 509 | AccountPurgeInfo(data); 510 | }); 511 | }); 512 | 513 | } 514 | 515 | }); 516 | 517 | } 518 | 519 | } else socket.emit("CreationResponse", "Invalid account information"); 520 | 521 | } 522 | 523 | /** 524 | * Gets the current environment for online play (www.bondageprojects.com is considered production) 525 | * @param {ServerSocket} socket 526 | * @returns {"PROD"|"DEV"|string} 527 | */ 528 | function AccountGetEnvironment(socket) { 529 | if ((socket != null) && (socket.request != null) && (socket.request.headers != null) && (socket.request.headers.origin != null) && (socket.request.headers.origin != "")) { 530 | if (ChatRoomProduction.indexOf(socket.request.headers.origin.toLowerCase()) >= 0) return "PROD"; 531 | else return "DEV"; 532 | } else return (Math.round(Math.random() * 1000000000000)).toString(); 533 | } 534 | 535 | /** 536 | * Makes sure the account data is valid, creates the missing fields if we need to 537 | * @param {Partial} Account 538 | */ 539 | function AccountValidData(Account) { 540 | if (Account != null) { 541 | if ((Account.ItemPermission == null) || (typeof Account.ItemPermission !== "number")) Account.ItemPermission = 2; 542 | if ((Account.WhiteList == null) || !Array.isArray(Account.WhiteList)) Account.WhiteList = []; 543 | if ((Account.BlackList == null) || !Array.isArray(Account.BlackList)) Account.BlackList = []; 544 | if ((Account.FriendList == null) || !Array.isArray(Account.FriendList)) Account.FriendList = []; 545 | } 546 | } 547 | 548 | /** 549 | * Purge some account info that's not required to be kept in memory on the server side 550 | * @param {Partial} A 551 | */ 552 | function AccountPurgeInfo(A) { 553 | delete A.Log; 554 | delete A.Skill; 555 | delete A.Wardrobe; 556 | delete A.WardrobeCharacterNames; 557 | delete A.ChatSettings; 558 | delete A.VisualSettings; 559 | delete A.AudioSettings; 560 | delete A.GameplaySettings; 561 | delete A.Email; 562 | delete A.Password; 563 | delete A.LastLogin; 564 | delete A.GhostList; 565 | delete A.HiddenItems; 566 | } 567 | 568 | /** 569 | * Load a single account file 570 | * @param {ServerAccountLoginRequest} data 571 | * @param {ServerSocket} socket 572 | */ 573 | function AccountLogin(data, socket) { 574 | 575 | // Makes sure the login comes with a name and a password 576 | if (!data || typeof data !== "object" || typeof data.AccountName !== "string" || typeof data.Password !== "string") { 577 | socket.emit("LoginResponse", "InvalidNamePassword"); 578 | return; 579 | } 580 | 581 | // If connection already has login queued, ignore it 582 | if (pendingLogins.has(socket)) return; 583 | 584 | const shouldRun = loginQueue.length === 0; 585 | loginQueue.push([socket, data.AccountName.toUpperCase(), data.Password]); 586 | pendingLogins.add(socket); 587 | 588 | if (loginQueue.length > 16) { 589 | socket.emit("LoginQueue", loginQueue.length); 590 | } 591 | 592 | // If there are no logins being processed, start the processing of the queue 593 | if (shouldRun) { 594 | AccountLoginRun(); 595 | } 596 | } 597 | 598 | /** 599 | * The queue of logins 600 | * @type {[ServerSocket, string, string][]} - [socket, username, password] 601 | */ 602 | const loginQueue = []; 603 | 604 | /** 605 | * List of sockets, for which there already is a pending login - to prevent duplicate logins during wait time 606 | * @type {WeakSet.} 607 | */ 608 | const pendingLogins = new WeakSet(); 609 | 610 | /** 611 | * Runs the next login in queue, waiting for it to finish before running next one 612 | */ 613 | function AccountLoginRun() { 614 | // Get next waiting login 615 | if (loginQueue.length === 0) return; 616 | let nx = loginQueue[0]; 617 | 618 | // If client disconnected during wait, ignore it 619 | while (!nx[0].connected) { 620 | pendingLogins.delete(nx[0]); 621 | loginQueue.shift(); 622 | if (loginQueue.length === 0) return; 623 | nx = loginQueue[0]; 624 | } 625 | 626 | // Process the login and after it queue the next one 627 | AccountLoginProcess(...nx).then(() => { 628 | pendingLogins.delete(nx[0]); 629 | loginQueue.shift(); 630 | if (loginQueue.length > 0) { 631 | setTimeout(AccountLoginRun, 50); 632 | } 633 | }, err => { throw err; }); 634 | } 635 | 636 | // Removes all instances of that character from all chat rooms 637 | function AccountRemoveFromChatRoom(MemberNumber) { 638 | if ((MemberNumber == null) || (Account == null) || (Account.length == 0) || (ChatRoom == null) || (ChatRoom.length == 0)) return; 639 | for (let C = 0; C < ChatRoom.length; C++) { 640 | if ((ChatRoom[C] != null) && (ChatRoom[C].Account != null) && (ChatRoom[C].Account.length > 0)) { 641 | for (let A = 0; A < ChatRoom[C].Account.length; A++) 642 | if ((ChatRoom[C].Account[A] != null) && (ChatRoom[C].Account[A].MemberNumber != null) && (ChatRoom[C].Account[A].MemberNumber == MemberNumber)) 643 | ChatRoom[C].Account.splice(A, 1); 644 | if (ChatRoom[C].Account.length == 0) 645 | ChatRoom.splice(C, 1); 646 | } 647 | } 648 | } 649 | 650 | /** 651 | * Processes a single login request 652 | * @param {ServerSocket} socket 653 | * @param {string} AccountName The username the user is trying to log in with 654 | * @param {string} Password 655 | */ 656 | async function AccountLoginProcess(socket, AccountName, Password) { 657 | // Checks if there's an account that matches the name 658 | /** @type {Account|null} */ 659 | const result = await Database.collection(AccountCollection).findOne({ AccountName }); 660 | 661 | if (!socket.connected) return; 662 | if (result === null) { 663 | socket.emit("LoginResponse", "InvalidNamePassword"); 664 | return; 665 | } 666 | 667 | // Compare the password to its hashed version 668 | const res = await BCrypt.compare(Password.toUpperCase(), result.Password); 669 | 670 | if (!socket.connected) return; 671 | if (!res) { 672 | socket.emit("LoginResponse", "InvalidNamePassword"); 673 | return; 674 | } 675 | 676 | // Disconnect duplicated logged accounts 677 | for (const Acc of Account) { 678 | if (Acc != null && Acc.AccountName === result.AccountName) { 679 | Acc.Socket.emit("ForceDisconnect", "ErrorDuplicatedLogin"); 680 | Acc.Socket.disconnect(true); 681 | AccountRemove(Acc.ID); 682 | break; 683 | } 684 | } 685 | 686 | // Assigns a member number if there's none 687 | if (result.MemberNumber == null) { 688 | result.MemberNumber = NextMemberNumber; 689 | NextMemberNumber++; 690 | console.log("Assigning missing member number: " + result.MemberNumber + " for account: " + result.AccountName); 691 | Database.collection(AccountCollection).updateOne({ AccountName : result.AccountName }, { $set: { MemberNumber: result.MemberNumber } }, function(err, res) { if (err) throw err; }); 692 | } 693 | 694 | // Updates lovership to an array if needed for conversion 695 | if (!Array.isArray(result.Lovership)) result.Lovership = (result.Lovership != undefined) ? [result.Lovership] : []; 696 | 697 | // Sets the last login date 698 | result.LastLogin = CommonTime(); 699 | Database.collection(AccountCollection).updateOne({ AccountName : result.AccountName }, { $set: { LastLogin: result.LastLogin } }, function(err, res) { if (err) throw err; }); 700 | 701 | // Logs the account 702 | result.ID = socket.id; 703 | result.Environment = AccountGetEnvironment(socket); 704 | //console.log("Login account: " + result.AccountName + " ID: " + socket.id + " " + result.Environment); 705 | AccountValidData(result); 706 | AccountRemoveFromChatRoom(result.MemberNumber); 707 | Account.push(result); 708 | OnLogin(socket); 709 | delete result.Password; 710 | delete result.Email; 711 | socket.compress(false).emit("LoginResponse", result); 712 | result.Socket = socket; 713 | AccountSendServerInfo(socket); 714 | AccountPurgeInfo(result); 715 | 716 | } 717 | 718 | /** 719 | * Returns TRUE if the object is empty 720 | * @param {Record} obj Object to check 721 | * @returns {boolean} 722 | */ 723 | function ObjectEmpty(obj) { 724 | for(var key in obj) 725 | if (obj.hasOwnProperty(key)) 726 | return false; 727 | return true; 728 | } 729 | 730 | /** 731 | * Updates any account data except the basic ones that cannot change 732 | * @param {Partial} data 733 | * @param {ServerSocket} socket 734 | */ 735 | function AccountUpdate(data, socket) { 736 | if ((data != null) && (typeof data === "object") && !Array.isArray(data)) 737 | for (const Acc of Account) 738 | if (Acc.ID == socket.id) { 739 | 740 | // Some data is never saved or updated from the client 741 | delete data.Name; 742 | delete data.AccountName; 743 | delete data.Password; 744 | delete data.Email; 745 | delete data.Creation; 746 | delete data.LastLogin; 747 | delete data.Pose; 748 | delete data.ActivePose; 749 | delete data.ChatRoom; 750 | delete data.ID; 751 | delete data.Socket; 752 | delete data.Inventory; 753 | // @ts-expect-error This is MongoDB's primary key 754 | delete data._id; 755 | delete data.MemberNumber; 756 | delete data.Environment; 757 | delete data.Ownership; 758 | delete data.Lovership; 759 | delete data.Difficulty; 760 | delete data.AssetFamily; 761 | delete data.DelayedAppearanceUpdate; 762 | delete data.DelayedSkillUpdate; 763 | delete data.DelayedGameUpdate; 764 | 765 | // Some data is kept for future use 766 | if (data.InventoryData != null) Acc.InventoryData = data.InventoryData; 767 | if (data.ItemPermission != null) Acc.ItemPermission = data.ItemPermission; 768 | if (data.ArousalSettings != null) Acc.ArousalSettings = data.ArousalSettings; 769 | if (data.OnlineSharedSettings != null) Acc.OnlineSharedSettings = data.OnlineSharedSettings; 770 | if (data.Game != null) Acc.Game = data.Game; 771 | if (data.MapData != null) Acc.MapData = data.MapData; 772 | if (data.LabelColor != null) Acc.LabelColor = data.LabelColor; 773 | if (data.Appearance != null) Acc.Appearance = data.Appearance; 774 | if (data.Reputation != null) Acc.Reputation = data.Reputation; 775 | if (data.Description != null) Acc.Description = data.Description; 776 | if (data.BlockItems != null) Acc.BlockItems = data.BlockItems; 777 | if (data.LimitedItems != null) Acc.LimitedItems = data.LimitedItems; 778 | if (data.FavoriteItems != null) Acc.FavoriteItems = data.FavoriteItems; 779 | if ((data.WhiteList != null) && Array.isArray(data.WhiteList)) Acc.WhiteList = data.WhiteList; 780 | if ((data.BlackList != null) && Array.isArray(data.BlackList)) Acc.BlackList = data.BlackList; 781 | if ((data.FriendList != null) && Array.isArray(data.FriendList)) Acc.FriendList = data.FriendList; 782 | if ((data.Lover != null) && (Array.isArray(Acc.Lovership)) && (Acc.Lovership.length < 5) && (typeof data.Lover === "string") && data.Lover.startsWith("NPC-")) { 783 | var isLoverPresent = false; 784 | for (var L = 0; L < Acc.Lovership.length; L++) { 785 | if ((Acc.Lovership[L].Name != null) && (Acc.Lovership[L].Name == data.Lover)) { 786 | isLoverPresent = true; 787 | break; 788 | } 789 | } 790 | if (!isLoverPresent) { 791 | Acc.Lovership.push({Name: data.Lover}); 792 | data.Lovership = Acc.Lovership; 793 | for (var L = 0; L < data.Lovership.length; L++) { 794 | delete data.Lovership[L].BeginEngagementOfferedByMemberNumber; 795 | delete data.Lovership[L].BeginWeddingOfferedByMemberNumber; 796 | if (data.Lovership[L].BeginDatingOfferedByMemberNumber) { 797 | data.Lovership.splice(L, 1); 798 | L -= 1; 799 | } 800 | } 801 | socket.emit("AccountLovership", { Lovership: data.Lovership }); 802 | } 803 | delete data.Lover; 804 | } 805 | if ((data.Title != null)) Acc.Title = data.Title; 806 | if ((data.Nickname != null)) Acc.Nickname = data.Nickname; 807 | if ((data.Crafting != null)) Acc.Crafting = data.Crafting; 808 | 809 | // Some changes should be synched to other players in chatroom 810 | if ((Acc != null) && Acc.ChatRoom && /** @type {(keyof Account)[]} */ (["MapData", "Title", "Nickname", "Crafting", "Reputation", "Description", "LabelColor", "ItemPermission", "InventoryData", "BlockItems", "LimitedItems", "FavoriteItems", "OnlineSharedSettings", "WhiteList", "BlackList"]).some(k => data[k] != null)) 811 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.MemberNumber); 812 | 813 | // If only the appearance is updated, we keep the change in memory and do not update the database right away 814 | if ((Acc != null) && !ObjectEmpty(data) && (Object.keys(data).length == 1) && (data.Appearance != null)) { 815 | Acc.DelayedAppearanceUpdate = data.Appearance; 816 | //console.log("TO REMOVE - Keeping Appearance in memory for account: " + Acc.AccountName); 817 | return; 818 | } 819 | 820 | // If only the skill is updated, we keep the change in memory and do not update the database right away 821 | if ((Acc != null) && !ObjectEmpty(data) && (Object.keys(data).length == 1) && (data.Skill != null)) { 822 | Acc.DelayedSkillUpdate = data.Skill; 823 | //console.log("TO REMOVE - Keeping Skill in memory for account: " + Acc.AccountName); 824 | return; 825 | } 826 | 827 | // If only the game is updated, we keep the change in memory and do not update the database right away 828 | if ((Acc != null) && !ObjectEmpty(data) && (Object.keys(data).length == 1) && (data.Game != null)) { 829 | Acc.DelayedGameUpdate = data.Game; 830 | //console.log("TO REMOVE - Keeping Game in memory for account: " + Acc.AccountName); 831 | return; 832 | } 833 | 834 | // Removes the delayed data to update if we update that property right now 835 | if ((Acc != null) && !ObjectEmpty(data) && (Object.keys(data).length > 1)) { 836 | if ((data.Appearance != null) && (Acc.DelayedAppearanceUpdate != null)) delete Acc.DelayedAppearanceUpdate; 837 | if ((data.Skill != null) && (Acc.DelayedSkillUpdate != null)) delete Acc.DelayedSkillUpdate; 838 | if ((data.Game != null) && (Acc.DelayedGameUpdate != null)) delete Acc.DelayedGameUpdate; 839 | } 840 | 841 | // Do not save the map in the database 842 | delete data.MapData; 843 | 844 | // If we have data to push 845 | if ((Acc != null) && !ObjectEmpty(data)) Database.collection(AccountCollection).updateOne({ AccountName : Acc.AccountName }, { $set: data }, function(err, res) { if (err) throw err; }); 846 | break; 847 | 848 | } 849 | } 850 | 851 | /** 852 | * Updates email address 853 | * @param {ServerAccountUpdateEmailRequest} data 854 | * @param {ServerSocket} socket 855 | */ 856 | function AccountUpdateEmail(data, socket) { 857 | 858 | // If invalid data is received, we return an error to the client 859 | if (!data || typeof data !== "object" || Array.isArray(data)) { 860 | socket.emit("AccountQueryResult", { Query: "EmailUpdate", Result: false }); 861 | return; 862 | } 863 | 864 | // Make sure the emails are strings 865 | if (typeof data.EmailOld !== "string" || typeof data.EmailNew !== "string") { 866 | socket.emit("AccountQueryResult", { Query: "EmailUpdate", Result: false }); 867 | return; 868 | } 869 | 870 | // Finds the linked account 871 | const Acc = AccountGet(socket.id); 872 | if (!Acc) { 873 | socket.emit("AccountQueryResult", { Query: "EmailUpdate", Result: false }); 874 | return; 875 | } 876 | 877 | // If we're given a new email, check that it is valid (removing the email from the account is allowed) 878 | if (data.EmailNew !== "" && !CommonEmailIsValid(data.EmailNew)) { 879 | socket.emit("AccountQueryResult", { Query: "EmailUpdate", Result: false }); 880 | return; 881 | } 882 | 883 | // At that point we need to load up the account from the database; email is part of the keys we don't keep around 884 | Database.collection(AccountCollection).findOne( 885 | { AccountName : Acc.AccountName }, 886 | { projection: { Email: 1, _id: 0 }}, 887 | ( err, result ) => { 888 | 889 | // If the account already had an email, we validate the old email supplied vs the current in the database 890 | if (err) throw err; 891 | if (result.Email && data.EmailOld.trim().toLowerCase() !== result.Email.trim().toLowerCase()) { 892 | socket.emit("AccountQueryResult", { Query: "EmailUpdate", Result: false }); 893 | return; 894 | } 895 | 896 | // Updates the email in the database 897 | Database.collection(AccountCollection).updateOne( 898 | { AccountName : Acc.AccountName }, 899 | { $set: { Email: data.EmailNew } }, 900 | function(err, res) { 901 | if (err) throw err; 902 | console.log("Account " + Acc.AccountName + " updated email from " + data.EmailOld + " to " + data.EmailNew); 903 | socket.emit("AccountQueryResult", { Query: "EmailUpdate", Result: true }); 904 | } 905 | ); 906 | } 907 | ); 908 | } 909 | 910 | /** 911 | * When the client account sends a query to the server 912 | * @param {ServerAccountQueryRequest} data 913 | * @param {ServerSocket} socket 914 | */ 915 | function AccountQuery(data, socket) { 916 | if ((data != null) && (typeof data === "object") && !Array.isArray(data) && (data.Query != null) && (typeof data.Query === "string")) { 917 | 918 | // Finds the current account 919 | var Acc = AccountGet(socket.id); 920 | if (Acc != null) { 921 | 922 | // OnlineFriends query - returns all friends that are online and the room name they are in 923 | if ((data.Query == "OnlineFriends") && (Acc.FriendList != null)) { 924 | 925 | // Add all submissives owned by the player and all lovers of the players to the list 926 | /** @type {ServerFriendInfo[]} */ 927 | var Friends = []; 928 | var Index = []; 929 | for (const OtherAcc of Account) { 930 | var LoversNumbers = []; 931 | for (var L = 0; L < OtherAcc.Lovership.length; L++) { 932 | if (OtherAcc.Lovership[L].MemberNumber != null) { LoversNumbers.push(OtherAcc.Lovership[L].MemberNumber); } 933 | } 934 | if (OtherAcc.Environment == Acc.Environment) { 935 | var IsOwned = (OtherAcc.Ownership != null) && (OtherAcc.Ownership.MemberNumber != null) && (OtherAcc.Ownership.MemberNumber == Acc.MemberNumber); 936 | var IsLover = LoversNumbers.indexOf(Acc.MemberNumber) >= 0; 937 | if (IsOwned || IsLover) { 938 | Friends.push({ Type: IsOwned ? "Submissive" : "Lover", MemberNumber: OtherAcc.MemberNumber, MemberName: OtherAcc.Name, ChatRoomSpace: (OtherAcc.ChatRoom == null) ? null : OtherAcc.ChatRoom.Space, ChatRoomName: (OtherAcc.ChatRoom == null) ? null : OtherAcc.ChatRoom.Name, Private: (OtherAcc.ChatRoom && ChatRoomRoleListIsRestrictive(OtherAcc.ChatRoom.Visibility)) ? true : undefined }); 939 | Index.push(OtherAcc.MemberNumber); 940 | } 941 | } 942 | } 943 | 944 | // Builds the online friend list, both players must be friends to find each other 945 | for (var F = 0; F < Acc.FriendList.length; F++) 946 | if ((Acc.FriendList[F] != null) && (typeof Acc.FriendList[F] === "number")) 947 | if (Index.indexOf(Acc.FriendList[F]) < 0) // No need to search for the friend if she's owned 948 | for (const OtherAcc of Account) 949 | if (OtherAcc.MemberNumber == Acc.FriendList[F]) { 950 | if ((OtherAcc.Environment == Acc.Environment) && (OtherAcc.FriendList != null) && (OtherAcc.FriendList.indexOf(Acc.MemberNumber) >= 0)) 951 | Friends.push({ Type: "Friend", MemberNumber: OtherAcc.MemberNumber, MemberName: OtherAcc.Name, ChatRoomSpace: ((OtherAcc.ChatRoom != null) && !ChatRoomRoleListIsRestrictive(OtherAcc.ChatRoom.Visibility)) ? OtherAcc.ChatRoom.Space : null, ChatRoomName: (OtherAcc.ChatRoom == null) ? null : (ChatRoomRoleListIsRestrictive(OtherAcc.ChatRoom.Visibility)) ? null : OtherAcc.ChatRoom.Name, Private: (OtherAcc.ChatRoom && ChatRoomRoleListIsRestrictive(OtherAcc.ChatRoom.Visibility)) ? true : undefined }); 952 | break; 953 | } 954 | 955 | // Sends the query result to the client 956 | socket.emit("AccountQueryResult", { Query: data.Query, Result: Friends }); 957 | 958 | } 959 | 960 | // EmailStatus query - returns true if an email is linked to the account 961 | if (data.Query == "EmailStatus") { 962 | Database.collection(AccountCollection).find({ AccountName : Acc.AccountName }).toArray(function(err, result) { 963 | if (err) throw err; 964 | if ((result != null) && (typeof result === "object") && (result.length > 0)) { 965 | socket.emit("AccountQueryResult", /** @type {ServerAccountQueryEmailStatus} */ ({ Query: data.Query, Result: ((result[0].Email != null) && (result[0].Email != "")) })); 966 | } 967 | }); 968 | } 969 | } 970 | 971 | } 972 | } 973 | 974 | /** 975 | * When a player wants to beep another player 976 | * @param {ServerAccountBeepRequest} data 977 | * @param {ServerSocket} socket 978 | */ 979 | function AccountBeep(data, socket) { 980 | if ((data != null) && (typeof data === "object") && !Array.isArray(data) && (data.MemberNumber != null) && (typeof data.MemberNumber === "number")) { 981 | 982 | // Make sure both accounts are online, friends and sends the beep to the friend 983 | var Acc = AccountGet(socket.id); 984 | if (Acc != null) 985 | for (const OtherAcc of Account) 986 | if (OtherAcc.MemberNumber == data.MemberNumber) 987 | if ((OtherAcc.Environment == Acc.Environment) && (((OtherAcc.FriendList != null) && (OtherAcc.FriendList.indexOf(Acc.MemberNumber) >= 0)) || ((OtherAcc.Ownership != null) && (OtherAcc.Ownership.MemberNumber != null) && (OtherAcc.Ownership.MemberNumber == Acc.MemberNumber)) || ((data.BeepType != null) && (typeof data.BeepType === "string") && (data.BeepType == "Leash")))) { 988 | OtherAcc.Socket.emit("AccountBeep", { 989 | MemberNumber: Acc.MemberNumber, 990 | MemberName: Acc.Name, 991 | ChatRoomSpace: (Acc.ChatRoom == null || data.IsSecret) ? null : Acc.ChatRoom.Space, 992 | ChatRoomName: (Acc.ChatRoom == null || data.IsSecret) ? null : Acc.ChatRoom.Name, 993 | Private: (Acc.ChatRoom == null || data.IsSecret) ? null : ChatRoomRoleListIsRestrictive(Acc.ChatRoom.Visibility), 994 | BeepType: (data.BeepType) ? data.BeepType : null, 995 | Message: data.Message 996 | }); 997 | break; 998 | } 999 | 1000 | } 1001 | } 1002 | 1003 | // Updates an account appearance if needed 1004 | function AccountDelayedUpdateOne(AccountName, NewAppearance, NewSkill, NewGame) { 1005 | if ((AccountName == null) || ((NewAppearance == null) && (NewSkill == null) && (NewGame == null))) return; 1006 | //console.log("TO REMOVE - Updating Appearance, Skill or Game in database for account: " + AccountName); 1007 | let UpdateObj = {}; 1008 | if (NewAppearance != null) UpdateObj.Appearance = NewAppearance; 1009 | if (NewSkill != null) UpdateObj.Skill = NewSkill; 1010 | if (NewGame != null) UpdateObj.Game = NewGame; 1011 | Database.collection(AccountCollection).updateOne({ AccountName: AccountName }, { $set: UpdateObj }, function(err, res) { if (err) throw err; }); 1012 | } 1013 | 1014 | // Called every X seconds to update the database with appearance updates 1015 | function AccountDelayedUpdate() { 1016 | //console.log("TO REMOVE - Scanning for account delayed updates"); 1017 | for (const Acc of Account) { 1018 | if (Acc != null) { 1019 | AccountDelayedUpdateOne(Acc.AccountName, Acc.DelayedAppearanceUpdate, Acc.DelayedSkillUpdate, Acc.DelayedGameUpdate); 1020 | delete Acc.DelayedAppearanceUpdate; 1021 | delete Acc.DelayedSkillUpdate; 1022 | delete Acc.DelayedGameUpdate; 1023 | } 1024 | } 1025 | } 1026 | 1027 | // Removes the account from the buffer 1028 | /** 1029 | * Removes the account from the buffer 1030 | * @param {string} ID 1031 | */ 1032 | function AccountRemove(ID) { 1033 | if (ID != null) 1034 | for (const Acc of Account) 1035 | if (Acc.ID == ID) { 1036 | let AccName = Acc.AccountName; 1037 | let AccDelayedAppearanceUpdate = Acc.DelayedAppearanceUpdate; 1038 | let AccDelayedSkillUpdate = Acc.DelayedSkillUpdate; 1039 | let AccDelayedGameUpdate = Acc.DelayedGameUpdate; 1040 | //console.log("Disconnecting account: " + Acc.AccountName + " ID: " + ID); 1041 | ChatRoomRemove(Acc, "ServerDisconnect", []); 1042 | const index = Account.indexOf(Acc); 1043 | if (index >= 0) 1044 | Account.splice(index, 1); 1045 | AccountDelayedUpdateOne(AccName, AccDelayedAppearanceUpdate, AccDelayedSkillUpdate, AccDelayedGameUpdate); 1046 | break; 1047 | } 1048 | } 1049 | 1050 | /** 1051 | * Returns the account object related to it's ID 1052 | * @param {string} ID 1053 | * @returns {Account|null} 1054 | */ 1055 | function AccountGet(ID) { 1056 | for (const Acc of Account) 1057 | if (Acc.ID == ID) 1058 | return Acc; 1059 | return null; 1060 | } 1061 | 1062 | /** 1063 | * The maximum number of search results to return at once 1064 | */ 1065 | const ChatRoomSearchMaxResults = 240; 1066 | 1067 | /** 1068 | * When a user searches for a chat room 1069 | * @param {ServerChatRoomSearchRequest} data 1070 | * @param {ServerSocket} socket 1071 | */ 1072 | function ChatRoomSearch(data, socket) { 1073 | if (!CommonIsObject(data) || typeof data.Query !== "string" || data.Query.length > 20) { 1074 | return; 1075 | } 1076 | 1077 | // Finds the current account 1078 | const Acc = AccountGet(socket.id); 1079 | if (!Acc) return; 1080 | 1081 | // Our search query 1082 | const Query = data.Query.trim(); 1083 | 1084 | // Gets the chat room spaces to return (empty for public, asylum, etc.) 1085 | let Spaces = []; 1086 | if (typeof data.Space === "string" && data.Space.length <= 100) { 1087 | Spaces = [data.Space]; 1088 | } else if (Array.isArray(data.Space)) { 1089 | Spaces = data.Space.filter(space => typeof space === "string" && space.length <= 100); 1090 | } 1091 | 1092 | // Gets the game name currently being played in the chat room (empty for all games and non-games rooms) 1093 | const Game = typeof data.Game === "string" && data.Game.length <= 100 ? data.Game : ""; 1094 | 1095 | // Checks if the user allows full rooms to show up 1096 | const FullRooms = typeof data.FullRooms === "boolean" ? data.FullRooms : false; 1097 | 1098 | // Checks if the user opted to ignore certain rooms 1099 | let IgnoredRooms = []; 1100 | if (Array.isArray(data.Ignore)) { 1101 | // Validate array, only strings are valid. 1102 | IgnoredRooms = data.Ignore.filter(R => typeof R === "string" && R.match(ServerChatRoomNameRegex)).map(r => r.toUpperCase()); 1103 | } 1104 | 1105 | // Grab the search language 1106 | let Languages = [] 1107 | if (typeof data.Language === "string" && data.Language !== "") { 1108 | Languages.push(data.Language); 1109 | } else if (Array.isArray(data.Language)) { 1110 | Languages = Languages.concat(data.Language); 1111 | } 1112 | 1113 | // Whether we also search the room descriptions 1114 | const SearchDescs = typeof data.SearchDescs === "boolean" ? data.SearchDescs : false; 1115 | 1116 | // Check if we have a map type filter 1117 | let MapTypes = []; 1118 | if (Array.isArray(data.MapTypes)) { 1119 | MapTypes = data.MapTypes.filter(t => typeof t === "string" && t.length < 20); 1120 | } 1121 | 1122 | // Builds a list of all public rooms, the last rooms created are shown first 1123 | const rooms = [...ChatRoom].reverse(); 1124 | 1125 | const CR = []; 1126 | for (const room of rooms) { 1127 | if (!room) continue; 1128 | 1129 | const roomName = room.Name.toUpperCase(); 1130 | 1131 | // Room is not in the correct environment, skip 1132 | if (Acc.Environment !== room.Environment) continue; 1133 | 1134 | // We're looking for a specific game and the room's doesn't match, skip 1135 | if (Game !== "" && room.Game !== Game) continue; 1136 | 1137 | // The room is full and we don't want those, skip 1138 | if (room.Account.length >= room.Limit && !FullRooms) continue; 1139 | 1140 | // Room isn't in the correct space, skip 1141 | if (!Spaces.includes(room.Space)) continue; 1142 | 1143 | // Player is banned from the room, skip 1144 | if (room.Ban.includes(Acc.MemberNumber)) continue; 1145 | 1146 | // Room is for a different language than requested, skip 1147 | if (Languages.length !== 0 && !Languages.includes(room.Language)) continue; 1148 | 1149 | // We have a search query 1150 | if (Query !== "") { 1151 | // Query doesn't match the room, skip 1152 | const searchTerms = [roomName]; 1153 | if (SearchDescs) { 1154 | searchTerms.push(room.Description.toUpperCase()); 1155 | } 1156 | if (!searchTerms.some(term => term.includes(Query))) continue; 1157 | 1158 | } 1159 | 1160 | // If query isn't the exact name and player isn't in visibility list, skip 1161 | if (roomName !== Query && !ChatRoomAccountHasAnyRole(Acc, room, room.Visibility)) continue; 1162 | 1163 | // If player doesn't want locked room results and isn't in access list, skip 1164 | if (!data.ShowLocked && !ChatRoomAccountHasAnyRole(Acc, room, room.Access)) continue; 1165 | 1166 | // Room is in our ignore list, skip 1167 | if (IgnoredRooms.includes(roomName)) continue; 1168 | 1169 | // Only allow requested map types 1170 | if (MapTypes.length > 0 && !MapTypes.includes(room.MapData?.Type)) continue; 1171 | 1172 | const result = ChatRoomSearchAddResult(Acc, room); 1173 | if (!result) continue; 1174 | 1175 | CR.push(result); 1176 | 1177 | // We got enough results for one batch, return those 1178 | if (CR.length >= ChatRoomSearchMaxResults) break; 1179 | } 1180 | 1181 | // Sends the list to the client 1182 | socket.emit("ChatRoomSearchResult", CR); 1183 | } 1184 | 1185 | /** 1186 | * Transform a chatroom into its search result form 1187 | * @param {Account} Acc 1188 | * @param {Chatroom} room 1189 | * @returns {ServerChatRoomSearchData} 1190 | */ 1191 | function ChatRoomSearchAddResult(Acc, room) { 1192 | 1193 | // Builds the searching account's list of known individuals in the current room 1194 | /** @type {ServerFriendInfo[]} */ 1195 | const Friends = []; 1196 | for (const RoomAcc of room.Account) { 1197 | if (!RoomAcc) continue; 1198 | if (RoomAcc?.Ownership?.MemberNumber === Acc.MemberNumber) { 1199 | Friends.push({ Type: "Submissive", MemberNumber: RoomAcc.MemberNumber, MemberName: RoomAcc.Name }); 1200 | } else if (Acc?.FriendList?.includes(RoomAcc.MemberNumber) && RoomAcc?.FriendList?.includes(Acc.MemberNumber)) { 1201 | Friends.push({ Type: "Friend", MemberNumber: RoomAcc.MemberNumber, MemberName: RoomAcc.Name }); 1202 | } 1203 | } 1204 | 1205 | // Builds a search result object with the room data 1206 | return { 1207 | Name: room.Name, 1208 | Language: room.Language, 1209 | Creator: room.Creator, 1210 | CreatorMemberNumber: room.CreatorMemberNumber, 1211 | Creation: room.Creation, 1212 | MemberCount: room.Account.length, 1213 | MemberLimit: room.Limit, 1214 | Description: room.Description, 1215 | BlockCategory: room.BlockCategory, 1216 | Game: room.Game, 1217 | Friends: Friends, 1218 | Space: room.Space, 1219 | Visibility: room.Visibility, 1220 | Access: room.Access, 1221 | Locked: room.Locked, 1222 | Private: room.Private, 1223 | MapType: room?.MapData?.Type ?? "Never", 1224 | CanJoin: ChatRoomAccountHasAnyRole(Acc, room, room.Access), 1225 | } 1226 | } 1227 | 1228 | /** 1229 | * Creates a new chat room 1230 | * @param {ServerChatRoomCreateRequest} data 1231 | * @param {ServerSocket} socket 1232 | */ 1233 | function ChatRoomCreate(data, socket) { 1234 | 1235 | // Make sure we have everything to create it 1236 | if ((data != null) && (typeof data === "object") && (data.Name != null) && (data.Description != null) && (data.Background != null) && (typeof data.Name === "string") && (typeof data.Description === "string") && (typeof data.Background === "string")) { 1237 | { // BACKWARD-COMPATIBILITY BLOCK for Private/Locked Transition 1238 | // ! TODO: Remember to add the visibility & access validity checks to the above if statement when removing this block 1239 | const hasVisibility = data.Visibility != null && Array.isArray(data.Visibility); 1240 | const hasPrivate = data.Private != null && typeof data.Private === "boolean"; 1241 | if (hasVisibility == hasPrivate) { // new client: visibility, old client: private; both is unexpected; neither is missing data 1242 | socket.emit("ChatRoomCreateResponse", "InvalidRoomData"); 1243 | return; 1244 | } else if (hasVisibility) { // Help new clients add Private for older clients | any visibility setting = private 1245 | data.Private = !data.Visibility.includes("All"); 1246 | } else if (data.Private != null) { // Help old clients add Visibility for new clients | private = admin only 1247 | data.Visibility = data.Private ? ["Admin"] : ["All"]; 1248 | } 1249 | 1250 | const hasAccess = data.Access != null && Array.isArray(data.Access); 1251 | const hasLocked = data.Locked != null && typeof data.Locked === "boolean"; 1252 | if (hasAccess && hasLocked) { // missing data 1253 | socket.emit("ChatRoomCreateResponse", "InvalidRoomData"); 1254 | return; 1255 | } else if (hasAccess) { // Help new clients add Locked for older clients | any access setting = locked 1256 | data.Locked = !data.Access.includes("All"); 1257 | } else if (hasLocked) { // Help old clients add Access if Locked is set | locked = admin + whitelist only 1258 | data.Access = data.Locked ? ["Admin", "Whitelist"] : ["All"]; 1259 | } else { // Maintaining older backward-compatibility behaviour 1260 | data.Locked = false; 1261 | data.Access = ["All"]; 1262 | } 1263 | } 1264 | 1265 | // Validates the room name 1266 | data.Name = data.Name.trim(); 1267 | if (data.Name.match(ServerChatRoomNameRegex) && (data.Description.length <= ServerChatRoomDescriptionMaxLength) && (data.Background.length <= 100)) { 1268 | // Finds the account and links it to the new room 1269 | var Acc = AccountGet(socket.id); 1270 | if (Acc == null) { 1271 | socket.emit("ChatRoomCreateResponse", "AccountError"); 1272 | return; 1273 | } 1274 | 1275 | // Check if the same name already exists and quits if that's the case 1276 | for (const Room of ChatRoom) 1277 | if (Room.Name.toUpperCase().trim() == data.Name.toUpperCase().trim()) { 1278 | socket.emit("ChatRoomCreateResponse", "RoomAlreadyExist"); 1279 | return; 1280 | } 1281 | 1282 | // Gets the space (regular, asylum), game (none, LARP) and blocked categories of the chat room 1283 | /** @type {ServerChatRoomSpace} */ 1284 | var Space = ""; 1285 | /** @type {ServerChatRoomGame} */ 1286 | var Game = ""; 1287 | if ((data.Space != null) && (typeof data.Space === "string") && (data.Space.length <= 100)) Space = data.Space; 1288 | if ((data.Game != null) && (typeof data.Game === "string") && (data.Game.length <= 100)) Game = data.Game; 1289 | if ((data.BlockCategory == null) || !Array.isArray(data.BlockCategory)) data.BlockCategory = []; 1290 | if (!Array.isArray(data.Ban) || data.Ban.some(i => !Number.isInteger(i))) data.Ban = []; 1291 | if (!Array.isArray(data.Whitelist) || data.Whitelist.some(i => !Number.isInteger(i))) data.Whitelist = []; 1292 | if (!Array.isArray(data.Admin) || data.Admin.some(i => !Number.isInteger(i))) data.Admin = [Acc.MemberNumber]; 1293 | 1294 | // Makes sure the limit is valid 1295 | let Limit = CommonParseInt(data.Limit, ROOM_LIMIT_DEFAULT); 1296 | if (Limit < ROOM_LIMIT_MINIMUM || Limit > ROOM_LIMIT_MAXIMUM) Limit = ROOM_LIMIT_DEFAULT; 1297 | 1298 | // Prepares the room object 1299 | ChatRoomRemove(Acc, "ServerLeave", []); 1300 | /** @type {Chatroom} */ 1301 | var NewRoom = { 1302 | ID: base64id.generateId(), 1303 | Name: data.Name, 1304 | Language: data.Language, 1305 | Description: data.Description, 1306 | Background: data.Background, 1307 | Custom: data.Custom, 1308 | Limit: Limit, 1309 | Visibility: data.Visibility, 1310 | Access: data.Access, 1311 | Private: data.Private || false, 1312 | Locked : data.Locked || false, 1313 | MapData : data.MapData, 1314 | Environment: Acc.Environment, 1315 | Space: Space, 1316 | Game: Game, 1317 | Creator: Acc.Name, 1318 | CreatorMemberNumber: Acc.MemberNumber, 1319 | Creation: CommonTime(), 1320 | Account: [], 1321 | Ban: data.Ban, 1322 | BlockCategory: data.BlockCategory, 1323 | Whitelist: data.Whitelist, 1324 | Admin: data.Admin 1325 | }; 1326 | ChatRoom.push(NewRoom); 1327 | Acc.ChatRoom = NewRoom; 1328 | NewRoom.Account.push(Acc); 1329 | //console.log("Chat room (" + ChatRoom.length.toString() + ") " + data.Name + " created by account " + Acc.AccountName + ", ID: " + socket.id); 1330 | socket.join("chatroom-" + NewRoom.ID); 1331 | socket.emit("ChatRoomCreateResponse", "ChatRoomCreated"); 1332 | ChatRoomSync(NewRoom, Acc.MemberNumber); 1333 | 1334 | } else socket.emit("ChatRoomCreateResponse", "InvalidRoomData"); 1335 | 1336 | } else socket.emit("ChatRoomCreateResponse", "InvalidRoomData"); 1337 | 1338 | } 1339 | 1340 | /** 1341 | * Join an existing chat room 1342 | * @param {ServerChatRoomJoinRequest} data 1343 | * @param {ServerSocket} socket 1344 | */ 1345 | function ChatRoomJoin(data, socket) { 1346 | 1347 | // Make sure we have everything to join it 1348 | if ((data != null) && (typeof data === "object") && (data.Name != null) && (typeof data.Name === "string") && (data.Name != "")) { 1349 | 1350 | // Finds the current account 1351 | var Acc = AccountGet(socket.id); 1352 | if (Acc != null) { 1353 | 1354 | // Finds the room and join it 1355 | for (const Room of ChatRoom) 1356 | if (Room.Name.toUpperCase().trim() == data.Name.toUpperCase().trim()) 1357 | if (Acc.Environment == Room.Environment) 1358 | if (Room.Account.length < Room.Limit) { 1359 | if (Room.Ban.indexOf(Acc.MemberNumber) < 0) { 1360 | 1361 | // If the player is on the access list, we allow them inside 1362 | if (ChatRoomAccountHasAnyRole(Acc, Room, Room.Access)) { 1363 | if (Acc.ChatRoom == null || Acc.ChatRoom.ID !== Room.ID) { 1364 | ChatRoomRemove(Acc, "ServerLeave", []); 1365 | Acc.ChatRoom = Room; 1366 | if (Account.find(A => Acc.MemberNumber === A.MemberNumber)) { 1367 | Room.Account.push(Acc); 1368 | socket.join("chatroom-" + Room.ID); 1369 | socket.emit("ChatRoomSearchResponse", "JoinedRoom"); 1370 | ChatRoomSyncMemberJoin(Room, Acc); 1371 | ChatRoomMessage(Room, Acc.MemberNumber, "ServerEnter", "Action", null, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 1372 | } 1373 | return; 1374 | } else { 1375 | socket.emit("ChatRoomSearchResponse", "AlreadyInRoom"); 1376 | return; 1377 | } 1378 | } else { 1379 | socket.emit("ChatRoomSearchResponse", "RoomLocked"); 1380 | return; 1381 | } 1382 | 1383 | } else { 1384 | socket.emit("ChatRoomSearchResponse", "RoomBanned"); 1385 | return; 1386 | } 1387 | 1388 | } else { 1389 | socket.emit("ChatRoomSearchResponse", "RoomFull"); 1390 | return; 1391 | } 1392 | 1393 | // Since we didn't found the room to join 1394 | socket.emit("ChatRoomSearchResponse", "CannotFindRoom"); 1395 | 1396 | } else socket.emit("ChatRoomSearchResponse", "AccountError"); 1397 | 1398 | } else socket.emit("ChatRoomSearchResponse", "InvalidRoomData"); 1399 | 1400 | } 1401 | 1402 | /** 1403 | * Removes a player from a room 1404 | * @param {Account} Acc 1405 | * @param {string} Reason 1406 | * @param {any[]} Dictionary 1407 | */ 1408 | function ChatRoomRemove(Acc, Reason, Dictionary) { 1409 | if (Acc.ChatRoom != null) { 1410 | Acc.Socket.leave("chatroom-" + Acc.ChatRoom.ID); 1411 | 1412 | // Removes it from the chat room array 1413 | for (const RoomAcc of Acc.ChatRoom.Account) 1414 | if (RoomAcc.ID == Acc.ID) { 1415 | Acc.ChatRoom.Account.splice(Acc.ChatRoom.Account.indexOf(RoomAcc), 1); 1416 | break; 1417 | } 1418 | 1419 | // Destroys the room if it's empty, warn other players if not 1420 | if (Acc.ChatRoom.Account.length == 0) { 1421 | for (var C = 0; C < ChatRoom.length; C++) 1422 | if (Acc.ChatRoom.Name == ChatRoom[C].Name) { 1423 | //console.log("Chat room " + Acc.ChatRoom.Name + " was destroyed. Rooms left: " + (ChatRoom.length - 1).toString()); 1424 | ChatRoom.splice(C, 1); 1425 | break; 1426 | } 1427 | } else { 1428 | if (!Dictionary || (Dictionary.length == 0)) Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 1429 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, Reason, "Action", null, Dictionary); 1430 | ChatRoomSyncMemberLeave(Acc.ChatRoom, Acc.MemberNumber); 1431 | } 1432 | Acc.ChatRoom = null; 1433 | 1434 | } 1435 | } 1436 | 1437 | /** 1438 | * Finds the current account and removes it from it's chat room, nothing is returned to the client 1439 | * @param {ServerSocket} socket 1440 | */ 1441 | function ChatRoomLeave(socket) { 1442 | var Acc = AccountGet(socket.id); 1443 | if (Acc != null) ChatRoomRemove(Acc, "ServerLeave", []); 1444 | } 1445 | 1446 | /** 1447 | * Sends a text message to everyone in the room or a specific target 1448 | * @param {Chatroom|null|undefined} CR 1449 | * @param {number} Sender Sender's MemberNumber 1450 | * @param {string} Content 1451 | * @param {ServerChatRoomMessageType} Type 1452 | * @param {number|null} Target Target's MemberNumber or null if broadcast 1453 | * @param {any[]} [Dictionary] 1454 | */ 1455 | function ChatRoomMessage(CR, Sender, Content, Type, Target, Dictionary) { 1456 | if (CR == null) return; 1457 | if (Target == null) { 1458 | IO.to("chatroom-" + CR.ID).emit("ChatRoomMessage", { Sender: Sender, Content: Content, Type: Type, Dictionary: Dictionary } ); 1459 | } else { 1460 | for (const Acc of CR.Account) { 1461 | if (Acc != null && Target === Acc.MemberNumber) { 1462 | Acc.Socket.emit("ChatRoomMessage", { Sender: Sender, Content: Content, Type: Type, Dictionary: Dictionary } ); 1463 | return; 1464 | } 1465 | } 1466 | } 1467 | } 1468 | 1469 | /** 1470 | * When a user sends a chat message, we propagate it to everyone in the room 1471 | * @param {ServerChatRoomMessage} data 1472 | * @param {ServerSocket} socket 1473 | */ 1474 | function ChatRoomChat(data, socket) { 1475 | if ((data != null) && (typeof data === "object") && (data.Content != null) && (data.Type != null) && (typeof data.Content === "string") && (typeof data.Type === "string") && (ChatRoomMessageType.indexOf(data.Type) >= 0) && (data.Content.length <= ServerChatMessageMaxLength)) { 1476 | var Acc = AccountGet(socket.id); 1477 | if (Acc != null) ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, data.Content.trim(), data.Type, data.Target, data.Dictionary); 1478 | } 1479 | } 1480 | 1481 | /** 1482 | * When a user sends a game packet (for LARP or other games), we propagate it to everyone in the room 1483 | * @param {ServerChatRoomGameUpdateRequest} data 1484 | * @param {ServerSocket} socket 1485 | */ 1486 | function ChatRoomGame(data, socket) { 1487 | if ((data != null) && (typeof data === "object")) { 1488 | var R = Math.random(); 1489 | var Acc = AccountGet(socket.id); 1490 | if (Acc && Acc.ChatRoom) { 1491 | IO.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomGameResponse", /** @type {ServerChatRoomGameResponse} */ ({ Sender: Acc.MemberNumber, Data: data, RNG: R }) ); 1492 | } 1493 | } 1494 | } 1495 | 1496 | /** 1497 | * Builds the character packet to send over to the clients 1498 | * @param {Account} Acc 1499 | * @returns {ServerChatRoomSyncCharacterResponse["Character"]} 1500 | */ 1501 | function ChatRoomSyncGetCharSharedData(Acc) { 1502 | const WhiteList = []; 1503 | const BlackList = []; 1504 | const sendBlacklist = AccountShouldSendBlackList(Acc); 1505 | // We filter whitelist&blacklist based on people in room 1506 | if (Acc.ChatRoom && Acc.ChatRoom.Account) { 1507 | for (const B of Acc.ChatRoom.Account) { 1508 | if (Acc.WhiteList.includes(B.MemberNumber)) { 1509 | WhiteList.push(B.MemberNumber); 1510 | } 1511 | if (sendBlacklist && Acc.BlackList.includes(B.MemberNumber)) { 1512 | BlackList.push(B.MemberNumber); 1513 | } 1514 | } 1515 | } 1516 | 1517 | return { 1518 | ID: Acc.ID, 1519 | Name: Acc.Name, 1520 | AssetFamily: Acc.AssetFamily, 1521 | Title: Acc.Title, 1522 | Nickname: Acc.Nickname, 1523 | Appearance: Acc.Appearance, 1524 | ActivePose: Acc.ActivePose, 1525 | Reputation: Acc.Reputation, 1526 | Creation: Acc.Creation, 1527 | Lovership: Acc.Lovership, 1528 | Description: Acc.Description, 1529 | Owner: Acc.Owner, 1530 | MemberNumber: Acc.MemberNumber, 1531 | LabelColor: Acc.LabelColor, 1532 | ItemPermission: Acc.ItemPermission, 1533 | InventoryData: Acc.InventoryData, 1534 | Ownership: Acc.Ownership, 1535 | BlockItems: Acc.BlockItems, 1536 | LimitedItems: Acc.LimitedItems, 1537 | FavoriteItems: Acc.FavoriteItems, 1538 | ArousalSettings: Acc.ArousalSettings, 1539 | OnlineSharedSettings: Acc.OnlineSharedSettings, 1540 | WhiteList, 1541 | BlackList, 1542 | Game: Acc.Game, 1543 | MapData: Acc.MapData, 1544 | Crafting: Acc.Crafting, 1545 | Difficulty: Acc.Difficulty 1546 | }; 1547 | } 1548 | 1549 | /** 1550 | * Returns a ChatRoom data that can be synced to clients 1551 | * @param {Chatroom} CR 1552 | * @param {number} SourceMemberNumber 1553 | */ 1554 | function ChatRoomGetData(CR, SourceMemberNumber) 1555 | { 1556 | // Exits right away if the chat room was destroyed 1557 | if (CR == null) return; 1558 | 1559 | // Builds the room data 1560 | /** @type {ServerChatRoomSyncMessage} */ 1561 | const R = { 1562 | Name: CR.Name, 1563 | Language: CR.Language, 1564 | Description: CR.Description, 1565 | Admin: CR.Admin, 1566 | Whitelist: CR.Whitelist, 1567 | Ban: CR.Ban, 1568 | Background: CR.Background, 1569 | Custom: CR.Custom, 1570 | Limit: CR.Limit, 1571 | Game: CR.Game, 1572 | SourceMemberNumber, 1573 | Visibility: CR.Visibility, 1574 | Access: CR.Access, 1575 | Private: CR.Private, 1576 | Locked: CR.Locked, 1577 | MapData: CR.MapData, 1578 | BlockCategory: CR.BlockCategory, 1579 | Space: CR.Space, 1580 | Character: CR.Account.map(ChatRoomSyncGetCharSharedData), 1581 | }; 1582 | 1583 | return R; 1584 | } 1585 | 1586 | /** 1587 | * Returns property data for a chatroom 1588 | * @param {Chatroom} CR 1589 | * @param {number} SourceMemberNumber 1590 | */ 1591 | function ChatRoomGetProperties(CR, SourceMemberNumber) 1592 | { 1593 | // Exits right away if the chat room was destroyed 1594 | if (CR == null) return; 1595 | 1596 | // Builds the room data 1597 | /** @type {ServerChatRoomSyncPropertiesMessage} */ 1598 | const R = { 1599 | Name: CR.Name, 1600 | Language: CR.Language, 1601 | Description: CR.Description, 1602 | Admin: CR.Admin, 1603 | Whitelist: CR.Whitelist, 1604 | Ban: CR.Ban, 1605 | Background: CR.Background, 1606 | Custom: CR.Custom, 1607 | Limit: CR.Limit, 1608 | Game: CR.Game, 1609 | SourceMemberNumber, 1610 | Visibility: CR.Visibility, 1611 | Access: CR.Access, 1612 | Private: CR.Private, 1613 | Locked: CR.Locked, 1614 | MapData: CR.MapData, 1615 | BlockCategory: CR.BlockCategory, 1616 | Space: CR.Space, 1617 | }; 1618 | 1619 | return R; 1620 | } 1621 | 1622 | /** 1623 | * Syncs the room data with all of it's members 1624 | * @param {Chatroom} CR 1625 | * @param {number} SourceMemberNumber 1626 | */ 1627 | function ChatRoomSync(CR, SourceMemberNumber) { 1628 | 1629 | // Exits right away if the chat room was destroyed 1630 | if (CR == null) return; 1631 | 1632 | // Sends the full packet to everyone in the room 1633 | IO.to("chatroom-" + CR.ID).emit("ChatRoomSync", ChatRoomGetData(CR, SourceMemberNumber)); 1634 | } 1635 | 1636 | /** 1637 | * Syncs the room data only to target 1638 | * @param {Chatroom} CR 1639 | * @param {number} SourceMemberNumber MemberNumber of account causing change 1640 | * @param {number} TargetMemberNumber The account to which the sync should be sent 1641 | */ 1642 | function ChatRoomSyncToMember(CR, SourceMemberNumber, TargetMemberNumber) { 1643 | // Exits right away if the chat room was destroyed 1644 | if (CR == null) { return; } 1645 | 1646 | // Sends the full packet to everyone in the room 1647 | for (const RoomAcc of CR.Account) // For each player in the chat room... 1648 | { 1649 | if(RoomAcc.MemberNumber == TargetMemberNumber) // If the player is the one who gets synced... 1650 | { 1651 | // Send room data and break loop 1652 | RoomAcc.Socket.emit("ChatRoomSync", ChatRoomGetData(CR, SourceMemberNumber)); 1653 | break; 1654 | } 1655 | } 1656 | } 1657 | 1658 | /** 1659 | * Syncs the room data with all of it's members 1660 | * @param {Chatroom} CR 1661 | * @param {number} SourceMemberNumber 1662 | * @param {number} TargetMemberNumber The character to sync 1663 | */ 1664 | function ChatRoomSyncCharacter(CR, SourceMemberNumber, TargetMemberNumber) { 1665 | // Exits right away if the chat room was destroyed 1666 | if (CR == null) return; 1667 | 1668 | const Target = CR.Account.find(Acc => Acc.MemberNumber === TargetMemberNumber); 1669 | if (!Target) return; 1670 | const Source = CR.Account.find(Acc => Acc.MemberNumber === SourceMemberNumber); 1671 | if (!Source) return; 1672 | 1673 | let characterData = { }; 1674 | characterData.SourceMemberNumber = SourceMemberNumber; 1675 | characterData.Character = ChatRoomSyncGetCharSharedData(Target); 1676 | 1677 | Source.Socket.to("chatroom-" + CR.ID).emit("ChatRoomSyncCharacter", characterData); 1678 | } 1679 | 1680 | /** 1681 | * Sends the newly joined player to all chat room members 1682 | * @param {Chatroom} CR 1683 | * @param {Account} Character 1684 | */ 1685 | function ChatRoomSyncMemberJoin(CR, Character) { 1686 | // Exits right away if the chat room was destroyed 1687 | if (CR == null) return; 1688 | let joinData = { 1689 | SourceMemberNumber: Character.MemberNumber, 1690 | Character: ChatRoomSyncGetCharSharedData(Character), 1691 | WhiteListedBy: [], 1692 | BlackListedBy: [] 1693 | }; 1694 | 1695 | for (const B of CR.Account) { 1696 | if (B.WhiteList.includes(Character.MemberNumber)) { 1697 | joinData.WhiteListedBy.push(B.MemberNumber); 1698 | } 1699 | if (AccountShouldSendBlackList(B) && B.BlackList.includes(Character.MemberNumber)) { 1700 | joinData.BlackListedBy.push(B.MemberNumber); 1701 | } 1702 | } 1703 | 1704 | Character.Socket.to("chatroom-" + CR.ID).emit("ChatRoomSyncMemberJoin", joinData); 1705 | ChatRoomSyncToMember(CR, Character.MemberNumber, Character.MemberNumber); 1706 | } 1707 | 1708 | /** 1709 | * Sends the left player to all chat room members 1710 | * @param {Chatroom} CR 1711 | * @param {number} SourceMemberNumber The leaving player 1712 | */ 1713 | function ChatRoomSyncMemberLeave(CR, SourceMemberNumber) { 1714 | // Exits right away if the chat room was destroyed 1715 | if (CR == null) return; 1716 | 1717 | let leaveData = { }; 1718 | leaveData.SourceMemberNumber = SourceMemberNumber; 1719 | 1720 | IO.to("chatroom-" + CR.ID).emit("ChatRoomSyncMemberLeave", leaveData); 1721 | } 1722 | 1723 | /** 1724 | * Syncs the room data with all of it's members 1725 | * @param {Chatroom} CR 1726 | * @param {number} SourceMemberNumber 1727 | */ 1728 | function ChatRoomSyncRoomProperties(CR, SourceMemberNumber) { 1729 | 1730 | // Exits right away if the chat room was destroyed 1731 | if (CR == null) return; 1732 | IO.to("chatroom-" + CR.ID).emit("ChatRoomSyncRoomProperties", ChatRoomGetProperties(CR, SourceMemberNumber)); 1733 | 1734 | } 1735 | 1736 | /** 1737 | * Syncs the room data with all of it's members 1738 | * @param {Chatroom} CR 1739 | * @param {number} SourceMemberNumber 1740 | */ 1741 | function ChatRoomSyncReorderPlayers(CR, SourceMemberNumber) { 1742 | 1743 | // Exits right away if the chat room was destroyed 1744 | if (CR == null) return; 1745 | 1746 | // Builds the room data 1747 | const newPlayerOrder = []; 1748 | for (const RoomAcc of CR.Account) { 1749 | newPlayerOrder.push(RoomAcc.MemberNumber); 1750 | } 1751 | 1752 | IO.to("chatroom-" + CR.ID).emit("ChatRoomSyncReorderPlayers", { PlayerOrder: newPlayerOrder }); 1753 | 1754 | } 1755 | 1756 | /** 1757 | * Syncs a single character data with all room members 1758 | * @param {Account} Acc 1759 | * @param {number} SourceMemberNumber 1760 | */ 1761 | function ChatRoomSyncSingle(Acc, SourceMemberNumber) { 1762 | const R = { 1763 | SourceMemberNumber, 1764 | Character: ChatRoomSyncGetCharSharedData(Acc) 1765 | }; 1766 | if (Acc.ChatRoom) 1767 | IO.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomSyncSingle", R); 1768 | } 1769 | 1770 | /** 1771 | * Updates a character from the chat room 1772 | * @param {ServerCharacterUpdate} data 1773 | * @param {ServerSocket} socket 1774 | */ 1775 | function ChatRoomCharacterUpdate(data, socket) { 1776 | if ((data != null) && (typeof data === "object") && (data.ID != null) && (typeof data.ID === "string") && (data.ID != "") && (data.Appearance != null)) { 1777 | var Acc = AccountGet(socket.id); 1778 | if ((Acc != null) && (Acc.ChatRoom != null)) 1779 | if (Acc.ChatRoom.Ban.indexOf(Acc.MemberNumber) < 0) 1780 | for (const RoomAcc of Acc.ChatRoom.Account) 1781 | if ((RoomAcc.ID == data.ID) && ChatRoomGetAllowItem(Acc, RoomAcc)) 1782 | if ((typeof data.Appearance === "object") && Array.isArray(data.Appearance)) { 1783 | // Database.collection(AccountCollection).updateOne({ AccountName: RoomAcc.AccountName }, { $set: { Appearance: data.Appearance } }, function(err, res) { if (err) throw err; }); 1784 | //console.log("TO REMOVE - Keeping Appearance in memory for account: " + Acc.AccountName); 1785 | if (data.Appearance != null) RoomAcc.DelayedAppearanceUpdate = data.Appearance; 1786 | RoomAcc.Appearance = data.Appearance; 1787 | RoomAcc.ActivePose = data.ActivePose; 1788 | ChatRoomSyncSingle(RoomAcc, Acc.MemberNumber); 1789 | } 1790 | } 1791 | } 1792 | 1793 | /** 1794 | * Updates a character expression for a chat room 1795 | * 1796 | * *This does not update the database* 1797 | * @param {ServerCharacterExpressionUpdate} data 1798 | * @param {ServerSocket} socket 1799 | */ 1800 | function ChatRoomCharacterExpressionUpdate(data, socket) { 1801 | if ((data != null) && (typeof data === "object") && (typeof data.Group === "string") && (data.Group != "")) { 1802 | const Acc = AccountGet(socket.id); 1803 | if (Acc && Array.isArray(data.Appearance) && data.Appearance.length >= 5) 1804 | Acc.Appearance = data.Appearance; 1805 | if (Acc && Acc.ChatRoom) { 1806 | socket.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomSyncExpression", { MemberNumber: Acc.MemberNumber, Name: data.Name, Group: data.Group }); 1807 | } 1808 | } 1809 | } 1810 | 1811 | /** 1812 | * Updates a character MapData for a chat room 1813 | * 1814 | * This does not update the database 1815 | * @param {ChatRoomMapData} data 1816 | * @param {ServerSocket} socket 1817 | */ 1818 | function ChatRoomCharacterMapDataUpdate(data, socket) { 1819 | if ((data != null) && (typeof data === "object")) { 1820 | const Acc = AccountGet(socket.id); 1821 | if (Acc && Acc.ChatRoom) { 1822 | Acc.MapData = data; 1823 | socket.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomSyncMapData", { MemberNumber: Acc.MemberNumber, MapData: data }); 1824 | } 1825 | } 1826 | } 1827 | 1828 | /** 1829 | * Updates a character pose for a chat room 1830 | * 1831 | * This does not update the database 1832 | * @param {ServerCharacterPoseUpdate} data 1833 | * @param {ServerSocket} socket 1834 | */ 1835 | function ChatRoomCharacterPoseUpdate(data, socket) { 1836 | if (!data || typeof data !== "object" || Array.isArray(data)) return; 1837 | 1838 | const Acc = AccountGet(socket.id); 1839 | if (!Acc) return; 1840 | 1841 | /** @type {readonly string[]} */ 1842 | let Pose; 1843 | if (typeof data.Pose !== "string" && !Array.isArray(data.Pose)) { 1844 | Pose = []; 1845 | } else if (Array.isArray(data.Pose)) { 1846 | Pose = data.Pose.filter(P => typeof P === "string"); 1847 | } else { 1848 | Pose = [data.Pose]; 1849 | } 1850 | 1851 | Acc.ActivePose = Pose; 1852 | if (Acc.ChatRoom) { 1853 | socket.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomSyncPose", { MemberNumber: Acc.MemberNumber, Pose: Pose }); 1854 | } 1855 | } 1856 | 1857 | /** 1858 | * Updates a character arousal meter for a chat room 1859 | * 1860 | * *This does not update the database* 1861 | * @param {ServerCharacterArousalUpdate} data 1862 | * @param {ServerSocket} socket 1863 | */ 1864 | function ChatRoomCharacterArousalUpdate(data, socket) { 1865 | if ((data != null) && (typeof data === "object")) { 1866 | var Acc = AccountGet(socket.id); 1867 | if ((Acc != null) && (Acc.ArousalSettings != null) && (typeof Acc.ArousalSettings === "object")) { 1868 | Acc.ArousalSettings.OrgasmTimer = data.OrgasmTimer; 1869 | Acc.ArousalSettings.OrgasmCount = data.OrgasmCount; 1870 | Acc.ArousalSettings.Progress = data.Progress; 1871 | Acc.ArousalSettings.ProgressTimer = data.ProgressTimer; 1872 | if (Acc && Acc.ChatRoom) { 1873 | socket.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomSyncArousal", { MemberNumber: Acc.MemberNumber, OrgasmTimer: data.OrgasmTimer, OrgasmCount: data.OrgasmCount, Progress: data.Progress, ProgressTimer: data.ProgressTimer }); 1874 | } 1875 | } 1876 | } 1877 | } 1878 | 1879 | /** 1880 | * Updates a character arousal meter for a chat room 1881 | * 1882 | * *This does not update the database* 1883 | * @param {ServerCharacterItemUpdate} data 1884 | * @param {ServerSocket} socket 1885 | */ 1886 | function ChatRoomCharacterItemUpdate(data, socket) { 1887 | if ((data != null) && (typeof data === "object") && (data.Target != null) && (typeof data.Target === "number") && (data.Group != null) && (typeof data.Group === "string")) { 1888 | 1889 | // Make sure the source account isn't banned from the chat room and has access to use items on the target 1890 | var Acc = AccountGet(socket.id); 1891 | if ((Acc == null) || (Acc.ChatRoom == null) || (Acc.ChatRoom.Ban.indexOf(Acc.MemberNumber) >= 0)) return; 1892 | for (const RoomAcc of Acc.ChatRoom.Account) 1893 | if (RoomAcc.MemberNumber == data.Target && !ChatRoomGetAllowItem(Acc, RoomAcc)) 1894 | return; 1895 | 1896 | // Sends the item to use to everyone but the source 1897 | if (Acc && Acc.ChatRoom) { 1898 | socket.to("chatroom-" + Acc.ChatRoom.ID).emit("ChatRoomSyncItem", { Source: Acc.MemberNumber, Item: data }); 1899 | } 1900 | } 1901 | } 1902 | 1903 | /** 1904 | * When an administrator account wants to act on another account in the room 1905 | * @param {ServerChatRoomAdminRequest} data 1906 | * @param {ServerSocket} socket 1907 | */ 1908 | function ChatRoomAdmin(data, socket) { 1909 | 1910 | if ((data != null) && (typeof data === "object") && (data.MemberNumber != null) && (typeof data.MemberNumber === "number") && (data.Action != null) && (typeof data.Action === "string")) { 1911 | 1912 | // Validates that the current account is a room administrator 1913 | var Acc = AccountGet(socket.id); 1914 | if ((Acc != null) && (Acc.ChatRoom != null) && (Acc.ChatRoom.Admin.indexOf(Acc.MemberNumber) >= 0)) { 1915 | 1916 | // Only certain actions can be performed by the administrator on themselves 1917 | if (Acc.MemberNumber == data.MemberNumber && data.Action != "Swap" && data.Action != "MoveLeft" && data.Action != "MoveRight") return; 1918 | 1919 | // An administrator can update lots of room data. The room values are sent back to the clients. 1920 | if (data.Action == "Update") 1921 | if ((data.Room != null) && (typeof data.Room === "object") && (data.Room.Name != null) && (data.Room.Description != null) && (data.Room.Background != null) && (typeof data.Room.Name === "string") && (typeof data.Room.Description === "string") && (typeof data.Room.Background === "string") && (data.Room.Admin != null) && (Array.isArray(data.Room.Admin)) && (!data.Room.Admin.some(i => !Number.isInteger(i))) && (data.Room.Ban != null) && (Array.isArray(data.Room.Ban)) && (!data.Room.Ban.some(i => !Number.isInteger(i)))) { 1922 | data.Room.Name = data.Room.Name.trim(); 1923 | if (data.Room.Name.match(ServerChatRoomNameRegex) && (data.Room.Description.length <= ServerChatRoomDescriptionMaxLength) && (data.Room.Background.length <= 100)) { 1924 | for (const Room of ChatRoom) 1925 | if (Acc.ChatRoom && Acc.ChatRoom.Name != data.Room.Name && Room.Name.toUpperCase().trim() == data.Room.Name.toUpperCase().trim()) { 1926 | socket.emit("ChatRoomUpdateResponse", "RoomAlreadyExist"); 1927 | return; 1928 | } 1929 | 1930 | { // BACKWARD-COMPATIBILITY BLOCK for Private/Locked Transition 1931 | if (data.Room.Visibility != null && Array.isArray(data.Room.Visibility)) { 1932 | data.Room.Private = !data.Room.Visibility.includes("All"); // Help new clients add Private for older clients | any visibility setting = private 1933 | } else if (data.Room.Private != null && typeof data.Room.Private === "boolean") { 1934 | data.Room.Visibility = data.Room.Private ? ["Admin"] : ["All"]; // Help old clients add Visibility for new clients | private = admin only 1935 | } 1936 | 1937 | if (data.Room.Access != null && Array.isArray(data.Room.Access)) { // Help new clients add Locked for older clients | any access setting = locked 1938 | data.Room.Locked = !data.Room.Access.includes("All"); 1939 | } else if (data.Room.Locked != null && typeof data.Room.Locked === "boolean") { // Help old clients add Access for new clients | locked = admin + whitelist only 1940 | data.Room.Access = data.Room.Locked ? ["Admin", "Whitelist"] : ["All"]; 1941 | } 1942 | } 1943 | 1944 | Acc.ChatRoom.Name = data.Room.Name; 1945 | Acc.ChatRoom.Language = data.Room.Language; 1946 | Acc.ChatRoom.Background = data.Room.Background; 1947 | Acc.ChatRoom.Custom = data.Room.Custom; 1948 | Acc.ChatRoom.Description = data.Room.Description; 1949 | if ((data.Room.BlockCategory == null) || !Array.isArray(data.Room.BlockCategory)) data.Room.BlockCategory = []; 1950 | Acc.ChatRoom.BlockCategory = data.Room.BlockCategory; 1951 | Acc.ChatRoom.Ban = data.Room.Ban; 1952 | if (Array.isArray(data.Room.Whitelist)) { 1953 | // Backward-compatibility with pre-whitelist client 1954 | Acc.ChatRoom.Whitelist = data.Room.Whitelist; 1955 | } 1956 | Acc.ChatRoom.Admin = data.Room.Admin; 1957 | Acc.ChatRoom.Game = ((data.Room.Game == null) || (typeof data.Room.Game !== "string") || (data.Room.Game.length > 100)) ? "" : data.Room.Game; 1958 | let Limit = CommonParseInt(data.Room.Limit, ROOM_LIMIT_DEFAULT); 1959 | if (Limit < ROOM_LIMIT_MINIMUM || Limit > ROOM_LIMIT_MAXIMUM) Limit = ROOM_LIMIT_DEFAULT; 1960 | Acc.ChatRoom.Limit = Limit; 1961 | if ((data.Room.Visibility != null) && (Array.isArray(data.Room.Visibility))) Acc.ChatRoom.Visibility = data.Room.Visibility; 1962 | if ((data.Room.Access != null) && (Array.isArray(data.Room.Access))) Acc.ChatRoom.Access = data.Room.Access; 1963 | if ((data.Room.Private != null) && (typeof data.Room.Private === "boolean")) Acc.ChatRoom.Private = data.Room.Private; 1964 | if ((data.Room.Locked != null) && (typeof data.Room.Locked === "boolean")) Acc.ChatRoom.Locked = data.Room.Locked; 1965 | Acc.ChatRoom.MapData = data.Room.MapData; 1966 | socket.emit("ChatRoomUpdateResponse", "Updated"); 1967 | if ((Acc != null) && (Acc.ChatRoom != null)) { 1968 | var Dictionary = []; 1969 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 1970 | Dictionary.push({Tag: "ChatRoomName", Text: Acc.ChatRoom.Name}); 1971 | Dictionary.push({Tag: "ChatRoomLimit", Text: Acc.ChatRoom.Limit.toString()}); 1972 | Dictionary.push({Tag: "ChatRoomPrivacy", TextToLookUp: (ChatRoomRoleListIsRestrictive(Acc.ChatRoom.Visibility) ? "Private" : "Public")}); 1973 | Dictionary.push({Tag: "ChatRoomLocked", TextToLookUp: (ChatRoomRoleListIsRestrictive(Acc.ChatRoom.Access) ? "Locked" : "Unlocked")}); 1974 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerUpdateRoom", "Action", null, Dictionary); 1975 | } 1976 | if ((Acc != null) && (Acc.ChatRoom != null)) ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 1977 | return; 1978 | } else socket.emit("ChatRoomUpdateResponse", "InvalidRoomData"); 1979 | } else socket.emit("ChatRoomUpdateResponse", "InvalidRoomData"); 1980 | 1981 | // An administrator can swap the position of two characters in a room 1982 | if ((data.Action == "Swap") && (data.TargetMemberNumber != null) && (typeof data.TargetMemberNumber === "number") && (data.DestinationMemberNumber != null) && (typeof data.DestinationMemberNumber === "number") && (data.TargetMemberNumber != data.DestinationMemberNumber)) { 1983 | var TargetAccountIndex = Acc.ChatRoom.Account.findIndex(x => x.MemberNumber == data.TargetMemberNumber); 1984 | var DestinationAccountIndex = Acc.ChatRoom.Account.findIndex(x => x.MemberNumber == data.DestinationMemberNumber); 1985 | if ((TargetAccountIndex < 0) || (DestinationAccountIndex < 0)) return; 1986 | var TargetAccount = Acc.ChatRoom.Account[TargetAccountIndex]; 1987 | var DestinationAccount = Acc.ChatRoom.Account[DestinationAccountIndex]; 1988 | const Dictionary = [ 1989 | {SourceCharacter: Acc.MemberNumber}, 1990 | {TargetCharacter: TargetAccount.MemberNumber}, 1991 | {TargetCharacter: DestinationAccount.MemberNumber, Index: 1}, 1992 | ]; 1993 | Acc.ChatRoom.Account[TargetAccountIndex] = DestinationAccount; 1994 | Acc.ChatRoom.Account[DestinationAccountIndex] = TargetAccount; 1995 | ChatRoomSyncReorderPlayers(Acc.ChatRoom, Acc.MemberNumber); 1996 | if ((Acc != null) && (Acc.ChatRoom != null)) 1997 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerSwap", "Action", null, Dictionary); 1998 | return; 1999 | } 2000 | 2001 | // If the account to act upon is in the room, an administrator can ban, kick, move, promote or demote him 2002 | for (var A = 0; (Acc.ChatRoom != null) && (A < Acc.ChatRoom.Account.length); A++) 2003 | if (Acc.ChatRoom.Account[A].MemberNumber == data.MemberNumber) { 2004 | var Dictionary = []; 2005 | if (data.Action == "Ban") { 2006 | Acc.ChatRoom.Ban.push(data.MemberNumber); 2007 | Acc.ChatRoom.Account[A].Socket.emit("ChatRoomSearchResponse", "RoomBanned"); 2008 | if ((Acc != null) && (Acc.ChatRoom != null) && (Acc.ChatRoom.Account[A] != null)) { 2009 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2010 | Dictionary.push({Tag: "TargetCharacterName", Text: Acc.ChatRoom.Account[A].Name, MemberNumber: Acc.ChatRoom.Account[A].MemberNumber}); 2011 | ChatRoomRemove(Acc.ChatRoom.Account[A], "ServerBan", Dictionary); 2012 | } 2013 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2014 | } 2015 | else if (data.Action == "Kick") { 2016 | const kickedAccount = Acc.ChatRoom.Account[A]; 2017 | kickedAccount.Socket.emit("ChatRoomSearchResponse", "RoomKicked"); 2018 | if ((Acc != null) && (Acc.ChatRoom != null) && (Acc.ChatRoom.Account[A] != null)) { 2019 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2020 | Dictionary.push({Tag: "TargetCharacterName", Text: Acc.ChatRoom.Account[A].Name, MemberNumber: Acc.ChatRoom.Account[A].MemberNumber}); 2021 | ChatRoomRemove(kickedAccount, "ServerKick", Dictionary); 2022 | } 2023 | } 2024 | else if ((data.Action == "MoveLeft") && (A != 0)) { 2025 | let MovedAccount = Acc.ChatRoom.Account[A]; 2026 | Acc.ChatRoom.Account[A] = Acc.ChatRoom.Account[A - 1]; 2027 | Acc.ChatRoom.Account[A - 1] = MovedAccount; 2028 | Dictionary.push({Tag: "TargetCharacterName", Text: MovedAccount.Name, MemberNumber: MovedAccount.MemberNumber}); 2029 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2030 | if ((data.Publish != null) && (typeof data.Publish === "boolean") && data.Publish) ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerMoveLeft", "Action", null, Dictionary); 2031 | ChatRoomSyncReorderPlayers(Acc.ChatRoom, Acc.MemberNumber); 2032 | } 2033 | else if ((data.Action == "MoveRight") && (A < Acc.ChatRoom.Account.length - 1)) { 2034 | let MovedAccount = Acc.ChatRoom.Account[A]; 2035 | Acc.ChatRoom.Account[A] = Acc.ChatRoom.Account[A + 1]; 2036 | Acc.ChatRoom.Account[A + 1] = MovedAccount; 2037 | Dictionary.push({Tag: "TargetCharacterName", Text: MovedAccount.Name, MemberNumber: MovedAccount.MemberNumber}); 2038 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2039 | if ((data.Publish != null) && (typeof data.Publish === "boolean") && data.Publish) ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerMoveRight", "Action", null, Dictionary); 2040 | ChatRoomSyncReorderPlayers(Acc.ChatRoom, Acc.MemberNumber); 2041 | } 2042 | else if (data.Action == "Shuffle") { 2043 | Acc.ChatRoom.Account.sort(() => Math.random() - 0.5); 2044 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2045 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerShuffle", "Action", null, Dictionary); 2046 | ChatRoomSyncReorderPlayers(Acc.ChatRoom, Acc.MemberNumber); 2047 | } 2048 | else if ((data.Action == "Promote") && (Acc.ChatRoom.Admin.indexOf(Acc.ChatRoom.Account[A].MemberNumber) < 0)) { 2049 | Acc.ChatRoom.Admin.push(Acc.ChatRoom.Account[A].MemberNumber); 2050 | Dictionary.push({Tag: "TargetCharacterName", Text: Acc.ChatRoom.Account[A].Name, MemberNumber: Acc.ChatRoom.Account[A].MemberNumber}); 2051 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2052 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerPromoteAdmin", "Action", null, Dictionary); 2053 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2054 | } 2055 | else if ((data.Action == "Demote") && (Acc.ChatRoom.Admin.indexOf(Acc.ChatRoom.Account[A].MemberNumber) >= 0)) { 2056 | Acc.ChatRoom.Admin.splice(Acc.ChatRoom.Admin.indexOf(Acc.ChatRoom.Account[A].MemberNumber), 1); 2057 | Dictionary.push({Tag: "TargetCharacterName", Text: Acc.ChatRoom.Account[A].Name, MemberNumber: Acc.ChatRoom.Account[A].MemberNumber}); 2058 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2059 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerDemoteAdmin", "Action", null, Dictionary); 2060 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2061 | } 2062 | else if ((data.Action == "Whitelist") && (Acc.ChatRoom.Whitelist.indexOf(data.MemberNumber) < 0)) 2063 | { 2064 | Acc.ChatRoom.Whitelist.push(data.MemberNumber); 2065 | Dictionary.push({Tag: "TargetCharacterName", Text: Acc.ChatRoom.Account[A].Name, MemberNumber: Acc.ChatRoom.Account[A].MemberNumber}); 2066 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2067 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerRoomWhitelist", "Action", null, Dictionary); 2068 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2069 | } 2070 | else if ((data.Action == "Unwhitelist") && (Acc.ChatRoom.Whitelist.indexOf(Acc.ChatRoom.Account[A].MemberNumber) >= 0)) { 2071 | Acc.ChatRoom.Whitelist.splice(Acc.ChatRoom.Whitelist.indexOf(Acc.ChatRoom.Account[A].MemberNumber), 1); 2072 | Dictionary.push({Tag: "TargetCharacterName", Text: Acc.ChatRoom.Account[A].Name, MemberNumber: Acc.ChatRoom.Account[A].MemberNumber}); 2073 | Dictionary.push({Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber}); 2074 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ServerRoomUnwhitelist", "Action", null, Dictionary); 2075 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2076 | } 2077 | return; 2078 | } 2079 | 2080 | // Can also ban, unban, whitelist, and unwhitelist without having the player in the room, there's no visible output 2081 | if ((data.Action == "Ban") && (Acc.ChatRoom != null) && (Acc.ChatRoom.Ban.indexOf(data.MemberNumber) < 0)) 2082 | { 2083 | Acc.ChatRoom.Ban.push(data.MemberNumber); 2084 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2085 | } 2086 | if ((data.Action == "Unban") && (Acc.ChatRoom != null) && (Acc.ChatRoom.Ban.indexOf(data.MemberNumber) >= 0)) 2087 | { 2088 | Acc.ChatRoom.Ban.splice(Acc.ChatRoom.Ban.indexOf(data.MemberNumber), 1); 2089 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2090 | } 2091 | if ((data.Action == "Whitelist") && (Acc.ChatRoom != null) && (Acc.ChatRoom.Whitelist.indexOf(data.MemberNumber) < 0)) 2092 | { 2093 | Acc.ChatRoom.Whitelist.push(data.MemberNumber); 2094 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2095 | } 2096 | if ((data.Action == "Unwhitelist") && (Acc.ChatRoom != null) && (Acc.ChatRoom.Whitelist.indexOf(data.MemberNumber) >= 0)) 2097 | { 2098 | Acc.ChatRoom.Whitelist.splice(Acc.ChatRoom.Whitelist.indexOf(data.MemberNumber), 1); 2099 | ChatRoomSyncRoomProperties(Acc.ChatRoom, Acc.MemberNumber); 2100 | } 2101 | } 2102 | 2103 | } 2104 | } 2105 | 2106 | /** 2107 | * Checks if the given role list is restrictive (does not allow "All" role) 2108 | * @param {ServerChatRoomRole[]} roles - The roles to check 2109 | */ 2110 | function ChatRoomRoleListIsRestrictive(roles) { 2111 | return !roles.includes("All"); 2112 | } 2113 | 2114 | /** 2115 | * Checks if a given account matches any role given an array of roles and the room to check for 2116 | * @param {Account} Acc - The account to check 2117 | * @param {ServerChatRoomRole[]} roles - The roles to check 2118 | * @param {Chatroom} room - The room to check for (sourcing admin and whitelist) 2119 | * @returns {boolean} - Returns TRUE if the account matches any role. FALSE otherwise. 2120 | */ 2121 | function ChatRoomAccountHasAnyRole(Acc, room, roles) { 2122 | if (Acc == null || roles == null) return false; 2123 | if (roles.includes("All")) return true; 2124 | if (roles.includes("Admin") && room.Admin.includes(Acc.MemberNumber)) return true; 2125 | if (roles.includes("Whitelist") && room.Whitelist.includes(Acc.MemberNumber)) return true; 2126 | return false; 2127 | } 2128 | 2129 | /** 2130 | * Returns a specific reputation value for the player 2131 | * @param {ServerAccountData} Account 2132 | */ 2133 | function ChatRoomDominantValue(Account) { 2134 | if ((Account.Reputation != null) && (Array.isArray(Account.Reputation))) 2135 | for (const Rep of Account.Reputation) 2136 | if ((Rep.Type != null) && (Rep.Value != null) && (typeof Rep.Type === "string") && (typeof Rep.Value === "number") && (Rep.Type == "Dominant")) 2137 | return CommonParseInt(Rep.Value, 0); 2138 | return 0; 2139 | } 2140 | 2141 | /** 2142 | * Checks if account's blacklist should be sent. 2143 | * It should only be sent if it is easily visible a person in blacklisted without this info. 2144 | * This means if the player is on permission that blocks depending on blacklist 2145 | * @see ChatRoomGetAllowItem 2146 | * @param {ServerAccountData} Acc The account to check 2147 | * @returns {boolean} 2148 | */ 2149 | function AccountShouldSendBlackList(Acc) { 2150 | return Acc.ItemPermission === 1 || Acc.ItemPermission === 2; 2151 | } 2152 | 2153 | /** 2154 | * Compares the source account and target account to check if we allow using an item 2155 | * @param {Account} Source 2156 | * @param {Account} Target 2157 | */ 2158 | function ChatRoomGetAllowItem(Source, Target) { 2159 | 2160 | // Make sure we have the required data 2161 | if ((Source == null) || (Target == null)) return false; 2162 | AccountValidData(Source); 2163 | AccountValidData(Target); 2164 | 2165 | // At zero permission level or if target is source or if owner, we allow it 2166 | if ((Target.ItemPermission <= 0) || (Source.MemberNumber == Target.MemberNumber) || ((Target.Ownership != null) && (Target.Ownership.MemberNumber != null) && (Target.Ownership.MemberNumber == Source.MemberNumber))) return true; 2167 | 2168 | // At one, we allow if the source isn't on the blacklist 2169 | if ((Target.ItemPermission == 1) && (Target.BlackList.indexOf(Source.MemberNumber) < 0)) return true; 2170 | 2171 | var LoversNumbers = []; 2172 | for (const Lover of Target.Lovership) { 2173 | if (Lover.MemberNumber != null) { LoversNumbers.push(Lover.MemberNumber); } 2174 | } 2175 | // At two, we allow if the source is Dominant compared to the Target (25 points allowed) or on whitelist or a lover 2176 | if ((Target.ItemPermission == 2) && (Target.BlackList.indexOf(Source.MemberNumber) < 0) && ((ChatRoomDominantValue(Source) + 25 >= ChatRoomDominantValue(Target)) || (Target.WhiteList.indexOf(Source.MemberNumber) >= 0) || (LoversNumbers.indexOf(Source.MemberNumber) >= 0))) return true; 2177 | 2178 | // At three, we allow if the source is on the whitelist of the Target or a lover 2179 | if ((Target.ItemPermission == 3) && ((Target.WhiteList.indexOf(Source.MemberNumber) >= 0) || (LoversNumbers.indexOf(Source.MemberNumber) >= 0))) return true; 2180 | 2181 | // At four, we allow if the source is a lover 2182 | if ((Target.ItemPermission == 4) && (LoversNumbers.indexOf(Source.MemberNumber) >= 0)) return true; 2183 | 2184 | // No valid combo, we don't allow the item 2185 | return false; 2186 | 2187 | } 2188 | 2189 | /** 2190 | * Returns TRUE if we allow applying an item from a character to another 2191 | * @param {ServerChatRoomAllowItemRequest} data 2192 | * @param {ServerSocket} socket 2193 | */ 2194 | function ChatRoomAllowItem(data, socket) { 2195 | if ((data != null) && (typeof data === "object") && (data.MemberNumber != null) && (typeof data.MemberNumber === "number")) { 2196 | 2197 | // Gets the source account and target account to check if we allow or not 2198 | var Acc = AccountGet(socket.id); 2199 | if ((Acc != null) && (Acc.ChatRoom != null)) 2200 | for (const RoomAcc of Acc.ChatRoom.Account) 2201 | if (RoomAcc.MemberNumber == data.MemberNumber) 2202 | socket.emit("ChatRoomAllowItem", { MemberNumber: data.MemberNumber, AllowItem: ChatRoomGetAllowItem(Acc, RoomAcc) }); 2203 | 2204 | } 2205 | } 2206 | 2207 | /** 2208 | * Updates the reset password entry number or creates a new one 2209 | * 2210 | * This number will have to be entered by the user later 2211 | * @param {string} AccountName 2212 | * @param {string} ResetNumber 2213 | */ 2214 | function PasswordResetSetNumber(AccountName, ResetNumber) { 2215 | for (const PasswordReset of PasswordResetProgress) 2216 | if (PasswordReset.AccountName.trim() == AccountName.trim()) { 2217 | PasswordReset.ResetNumber = ResetNumber; 2218 | return; 2219 | } 2220 | PasswordResetProgress.push({ AccountName: AccountName, ResetNumber: ResetNumber }); 2221 | } 2222 | 2223 | /** 2224 | * Generates a password reset number and sends it to the user 2225 | * @param {ServerPasswordResetRequest} data 2226 | * @param {ServerSocket} socket 2227 | */ 2228 | function PasswordReset(data, socket) { 2229 | if ((data != null) && (typeof data === "string") && (data != "") && CommonEmailIsValid(data)) { 2230 | 2231 | // One email reset password per 5 seconds to prevent flooding 2232 | if (NextPasswordReset > CommonTime()) return socket.emit("PasswordResetResponse", "RetryLater"); 2233 | NextPasswordReset = CommonTime() + 5000; 2234 | 2235 | // Gets all accounts that matches the email 2236 | Database.collection(AccountCollection).find({ Email : data }).toArray(function(err, result) { 2237 | 2238 | // If we found accounts with that email 2239 | if (err) throw err; 2240 | if ((result != null) && (typeof result === "object") && (result.length > 0)) { 2241 | 2242 | // Builds a reset number for each account found and creates the email body 2243 | var EmailBody = "To reset your account password, enter your account name and the reset number included in this email. You need to put these in the Bondage Club password reset screen, with your new password.

"; 2244 | for (const res of result) { 2245 | var ResetNumber = (Math.round(Math.random() * 1000000000000)).toString(); 2246 | PasswordResetSetNumber(res.AccountName, ResetNumber); 2247 | EmailBody = EmailBody + "Account Name: " + res.AccountName + "
"; 2248 | EmailBody = EmailBody + "Reset Number: " + ResetNumber + "

"; 2249 | } 2250 | 2251 | // Prepares the email to be sent 2252 | var mailOptions = { 2253 | from: "donotreply@bondageprojects.com", 2254 | to: result[0].Email, 2255 | subject: "Bondage Club Password Reset", 2256 | html: EmailBody 2257 | }; 2258 | 2259 | // Sends the email and logs the result 2260 | MailTransporter.sendMail(mailOptions, function (err, info) { 2261 | if (err) { 2262 | console.log("Error while sending password reset email: " + err); 2263 | socket.emit("PasswordResetResponse", "EmailSentError"); 2264 | } 2265 | else { 2266 | console.log("Password reset email send to: " + result[0].Email); 2267 | socket.emit("PasswordResetResponse", "EmailSent"); 2268 | } 2269 | }); 2270 | 2271 | } else socket.emit("PasswordResetResponse", "NoAccountOnEmail"); 2272 | 2273 | }); 2274 | 2275 | } 2276 | } 2277 | 2278 | /** 2279 | * Generates a password reset number and sends it to the user 2280 | * @param {ServerPasswordResetProcessRequest} data 2281 | * @param {ServerSocket} socket 2282 | */ 2283 | function PasswordResetProcess(data, socket) { 2284 | if ((data != null) && (typeof data === "object") && (data.AccountName != null) && (typeof data.AccountName === "string") && (data.ResetNumber != null) && (typeof data.ResetNumber === "string") && (data.NewPassword != null) && (typeof data.NewPassword === "string")) { 2285 | 2286 | // Makes sure the data is valid 2287 | if (data.AccountName.match(ServerAccountNameRegex) && data.NewPassword.match(ServerAccountPasswordRegex)) { 2288 | 2289 | // Checks if the reset number matches 2290 | for (const PasswordReset of PasswordResetProgress) 2291 | if ((PasswordReset.AccountName == data.AccountName) && (PasswordReset.ResetNumber == data.ResetNumber)) { 2292 | 2293 | // Creates a hashed password and updates the account with it 2294 | BCrypt.hash(data.NewPassword.toUpperCase(), 10, function( err, hash ) { 2295 | if (err) throw err; 2296 | console.log("Updating password for account: " + data.AccountName); 2297 | Database.collection(AccountCollection).updateOne({ AccountName : data.AccountName }, { $set: { Password: hash } }, function(err, res) { if (err) throw err; }); 2298 | socket.emit("PasswordResetResponse", "PasswordResetSuccessful"); 2299 | }); 2300 | return; 2301 | } 2302 | 2303 | // Sends a fail message to the client 2304 | socket.emit("PasswordResetResponse", "InvalidPasswordResetInfo"); 2305 | 2306 | } else socket.emit("PasswordResetResponse", "InvalidPasswordResetInfo"); 2307 | 2308 | } else socket.emit("PasswordResetResponse", "InvalidPasswordResetInfo"); 2309 | } 2310 | 2311 | /** 2312 | * Gets the current ownership status between two players in the same chatroom 2313 | * 2314 | * Can also trigger the progress in the relationship 2315 | * @param {ServerAccountOwnershipRequest} data 2316 | * @param {ServerSocket} socket 2317 | */ 2318 | function AccountOwnership(data, socket) { 2319 | if (data != null && typeof data === "object" && typeof data.MemberNumber === "number") { 2320 | 2321 | // The submissive can flush it's owner at any time in the trial, or after a delay if collared. Players on Extreme mode cannot break the full ownership. 2322 | const Acc = AccountGet(socket.id); 2323 | if (Acc == null) return; 2324 | if (Acc.Ownership != null && Acc.Ownership.Stage != null && Acc.Ownership.Start != null && (Acc.Ownership.Stage == 0 || Acc.Ownership.Start + OwnershipDelay <= CommonTime()) && data.Action === "Break") { 2325 | if (Acc.Difficulty == null || Acc.Difficulty.Level == null || typeof Acc.Difficulty.Level !== "number" || Acc.Difficulty.Level <= 2 || Acc.Ownership == null || Acc.Ownership.Stage == null || typeof Acc.Ownership.Stage !== "number" || Acc.Ownership.Stage == 0) { 2326 | Acc.Owner = ""; 2327 | Acc.Ownership = null; 2328 | let O = { Ownership: Acc.Ownership, Owner: Acc.Owner }; 2329 | Database.collection(AccountCollection).updateOne({ AccountName : Acc.AccountName }, { $set: O }, function(err, res) { if (err) throw err; }); 2330 | socket.emit("AccountOwnership", { ClearOwnership: true }); 2331 | return; 2332 | } 2333 | } 2334 | 2335 | // Get the target within the chatroom 2336 | if (Acc.ChatRoom == null) return; 2337 | const TargetAcc = Acc.ChatRoom.Account.find(A => A.MemberNumber === data.MemberNumber); 2338 | 2339 | // Can release a target that's not in the chatroom 2340 | if (!TargetAcc && (data.Action === "Release") && (Acc.MemberNumber != null) && (data.MemberNumber != null)) { 2341 | 2342 | // Gets the account linked to that member number, make sure 2343 | Database.collection(AccountCollection).findOne({ MemberNumber : data.MemberNumber }, function(err, result) { 2344 | if (err) throw err; 2345 | if ((result != null) && (result.MemberNumber != null) && (result.MemberNumber === data.MemberNumber) && (result.Ownership != null) && (result.Ownership.MemberNumber === Acc.MemberNumber)) { 2346 | Database.collection(AccountCollection).updateOne({ AccountName : result.AccountName }, { $set: { Owner: "", Ownership: null } }, function(err, res) { if (err) throw err; }); 2347 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ReleaseSuccess", "ServerMessage", Acc.MemberNumber); 2348 | let Target = Account.find(A => A.MemberNumber === data.MemberNumber); 2349 | if (!Target) return; 2350 | Target.Owner = ""; 2351 | Target.Ownership = null; 2352 | Target.Socket.emit("AccountOwnership", { ClearOwnership: true }); 2353 | if (Target.ChatRoom != null) { 2354 | ChatRoomSyncCharacter(Target.ChatRoom, Target.MemberNumber, Target.MemberNumber); 2355 | ChatRoomMessage(Target.ChatRoom, Target.MemberNumber, "ReleaseByOwner", "ServerMessage", Target.MemberNumber); 2356 | } 2357 | } else ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "ReleaseFail", "ServerMessage", Acc.MemberNumber); 2358 | }); 2359 | } 2360 | 2361 | // Exit if there's no target 2362 | if (!TargetAcc) return; 2363 | 2364 | // The dominant is setting/updating public notes on their fully owned submissive. 2365 | if ( 2366 | data.Action === "UpdateNotes" 2367 | && TargetAcc.Ownership != null 2368 | && TargetAcc.Ownership.Stage === 1 2369 | && TargetAcc.Ownership.MemberNumber == Acc.MemberNumber 2370 | ) { 2371 | if (typeof data.Notes === "string" && data.Notes.length > 0) { 2372 | // FIXME: This isn't fully Unicode-aware. 2373 | TargetAcc.Ownership.Notes = data.Notes.slice(0, 4000); 2374 | } else { 2375 | TargetAcc.Ownership.Notes = undefined; 2376 | } 2377 | let O = { Ownership: TargetAcc.Ownership, Owner: TargetAcc.Owner }; 2378 | Database.collection(AccountCollection).updateOne( 2379 | { AccountName: TargetAcc.AccountName }, 2380 | { $set: O }, 2381 | function(err, _res) { 2382 | if (err) throw err; 2383 | TargetAcc.Socket.emit("AccountOwnership", O); 2384 | ChatRoomSyncCharacter(TargetAcc.ChatRoom, TargetAcc.MemberNumber, TargetAcc.MemberNumber); 2385 | }); 2386 | return; 2387 | } 2388 | 2389 | // The dominant can release the submissive player at any time 2390 | if (data.Action === "Release" && TargetAcc.Ownership != null && TargetAcc.Ownership.MemberNumber === Acc.MemberNumber) { 2391 | const isTrial = typeof TargetAcc.Ownership.Stage !== "number" || TargetAcc.Ownership.Stage == 0; 2392 | TargetAcc.Owner = ""; 2393 | TargetAcc.Ownership = null; 2394 | let O = { Ownership: TargetAcc.Ownership, Owner: TargetAcc.Owner }; 2395 | Database.collection(AccountCollection).updateOne({ AccountName : TargetAcc.AccountName }, { $set: O }, function(err, res) { if (err) throw err; }); 2396 | TargetAcc.Socket.emit("AccountOwnership", { ClearOwnership: true }); 2397 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, isTrial ? "EndOwnershipTrial" : "EndOwnership", "ServerMessage", null, [ 2398 | { Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }, 2399 | { Tag: "TargetCharacter", Text: TargetAcc.Name, MemberNumber: TargetAcc.MemberNumber }, 2400 | ]); 2401 | ChatRoomSyncCharacter(Acc.ChatRoom, TargetAcc.MemberNumber, TargetAcc.MemberNumber); 2402 | return; 2403 | } 2404 | 2405 | // In a chatroom, the dominant and submissive can enter in a BDSM relationship (4 steps to complete) 2406 | // The dominant player proposes to the submissive player, cannot propose if target player is already owner 2407 | if (Acc.Ownership == null || 2408 | Acc.Ownership.MemberNumber == null || 2409 | Acc.Ownership.MemberNumber != data.MemberNumber 2410 | ) { 2411 | // Cannot propose if on blacklist 2412 | if (TargetAcc.BlackList.indexOf(Acc.MemberNumber) < 0) { 2413 | // Cannot propose if owned by a NPC 2414 | if (TargetAcc.Owner == null || TargetAcc.Owner == "") { 2415 | 2416 | // If there's no ownership, the dominant can propose to start a trial (Step 1 / 4) 2417 | if (TargetAcc.Ownership == null || TargetAcc.Ownership.MemberNumber == null) { 2418 | // Ignore requests for self-owners 2419 | if (Acc.MemberNumber === data.MemberNumber) return; 2420 | 2421 | if (data.Action === "Propose") { 2422 | TargetAcc.Owner = ""; 2423 | TargetAcc.Ownership = { StartTrialOfferedByMemberNumber: Acc.MemberNumber }; 2424 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "OfferStartTrial", "ServerMessage", TargetAcc.MemberNumber, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2425 | } else socket.emit("AccountOwnership", { MemberNumber: data.MemberNumber, Result: "CanOfferStartTrial" }); 2426 | } 2427 | 2428 | // If trial has started, the dominant can offer to end it after the delay (Step 3 / 4) 2429 | if (TargetAcc.Ownership != null && 2430 | TargetAcc.Ownership.MemberNumber == Acc.MemberNumber && 2431 | TargetAcc.Ownership.EndTrialOfferedByMemberNumber == null && 2432 | TargetAcc.Ownership.Stage === 0 && 2433 | TargetAcc.Ownership.Start != null && 2434 | TargetAcc.Ownership.Start + OwnershipDelay <= CommonTime() 2435 | ) { 2436 | if (data.Action === "Propose") { 2437 | TargetAcc.Ownership.EndTrialOfferedByMemberNumber = Acc.MemberNumber; 2438 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "OfferEndTrial", "ServerMessage", null, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2439 | } else socket.emit("AccountOwnership", { MemberNumber: data.MemberNumber, Result: "CanOfferEndTrial" }); 2440 | } 2441 | } 2442 | } 2443 | } 2444 | 2445 | // The submissive player can accept a proposal from the dominant 2446 | // No possible interaction if the player is owned by someone else 2447 | if (Acc.Ownership != null && 2448 | (Acc.Ownership.MemberNumber == null || Acc.Ownership.MemberNumber == data.MemberNumber) 2449 | ) { 2450 | // Cannot accept if on blacklist 2451 | if (TargetAcc.BlackList.indexOf(Acc.MemberNumber) < 0) { 2452 | 2453 | // If the submissive wants to accept to start the trial period (Step 2 / 4) 2454 | if (Acc.Ownership.StartTrialOfferedByMemberNumber != null && Acc.Ownership.StartTrialOfferedByMemberNumber == data.MemberNumber) { 2455 | if (data.Action === "Accept") { 2456 | Acc.Owner = ""; 2457 | Acc.Ownership = { MemberNumber: data.MemberNumber, Name: TargetAcc.Name, Start: CommonTime(), Stage: 0 }; 2458 | let O = { Ownership: Acc.Ownership, Owner: Acc.Owner }; 2459 | Database.collection(AccountCollection).updateOne({ AccountName : Acc.AccountName }, { $set: O }, function(err, res) { if (err) throw err; }); 2460 | socket.emit("AccountOwnership", O); 2461 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "StartTrial", "ServerMessage", null, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2462 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.MemberNumber); 2463 | } else socket.emit("AccountOwnership", { MemberNumber: data.MemberNumber, Result: "CanStartTrial" }); 2464 | } 2465 | 2466 | // If the submissive wants to accept the full collar (Step 4 /4) 2467 | if (Acc.Ownership.Stage != null && 2468 | Acc.Ownership.Stage == 0 && 2469 | Acc.Ownership.EndTrialOfferedByMemberNumber != null && 2470 | Acc.Ownership.EndTrialOfferedByMemberNumber == data.MemberNumber 2471 | ) { 2472 | if (data.Action === "Accept") { 2473 | Acc.Owner = TargetAcc.Name; 2474 | Acc.Ownership = { MemberNumber: data.MemberNumber, Name: TargetAcc.Name, Start: CommonTime(), Stage: 1 }; 2475 | let O = { Ownership: Acc.Ownership, Owner: Acc.Owner }; 2476 | Database.collection(AccountCollection).updateOne({ AccountName : Acc.AccountName }, { $set: O }, function(err, res) { if (err) throw err; }); 2477 | socket.emit("AccountOwnership", O); 2478 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "EndTrial", "ServerMessage", null, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2479 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.MemberNumber); 2480 | } else socket.emit("AccountOwnership", { MemberNumber: data.MemberNumber, Result: "CanEndTrial" }); 2481 | } 2482 | 2483 | } 2484 | } 2485 | } 2486 | } 2487 | 2488 | /** 2489 | * Gets the current lovership status between two players in the same chatroom 2490 | * 2491 | * Can also trigger the progress in the relationship 2492 | * @param {ServerAccountLovershipRequest} data 2493 | * @param {ServerSocket} socket 2494 | */ 2495 | function AccountLovership(data, socket) { 2496 | if ((data != null) && (typeof data === "object") && (data.MemberNumber != null) && (typeof data.MemberNumber === "number")) { 2497 | 2498 | /** 2499 | * Update the lovership and delete all unnecessary information 2500 | * @param {Lovership[]} Lovership 2501 | * @param {number} MemberNumber 2502 | * @param {ServerSocket} [CurrentSocket] 2503 | * @param {boolean} [Emit] 2504 | */ 2505 | function AccountUpdateLovership(Lovership, MemberNumber, CurrentSocket = socket, Emit = true) { 2506 | var newLovership = Lovership.slice(); 2507 | for (let L = newLovership.length - 1; L >= 0; L--) { 2508 | delete newLovership[L].BeginEngagementOfferedByMemberNumber; 2509 | delete newLovership[L].BeginWeddingOfferedByMemberNumber; 2510 | if (newLovership[L].BeginDatingOfferedByMemberNumber) { 2511 | newLovership.splice(L, 1); 2512 | L -= 1; 2513 | } 2514 | } 2515 | const L = { Lovership: newLovership }; 2516 | Database.collection(AccountCollection).updateOne({ MemberNumber : MemberNumber}, { $set: L }, function(err, res) { if (err) throw err; }); 2517 | if (Emit) CurrentSocket.emit("AccountLovership", L); 2518 | } 2519 | 2520 | // A Lover can break her relationship any time if not wed, or after a delay if official 2521 | var Acc = AccountGet(socket.id); 2522 | if ((Acc != null) && (data.Action != null) && (data.Action === "Break")) { 2523 | 2524 | var AccLoversNumbers = []; 2525 | for (const Lover of Acc.Lovership) { 2526 | if (Lover.MemberNumber != null) { AccLoversNumbers.push(Lover.MemberNumber); } 2527 | else if (Lover.Name != null) { AccLoversNumbers.push(Lover.Name); } 2528 | else { AccLoversNumbers.push(-1); } 2529 | } 2530 | var AL = AccLoversNumbers.indexOf(data.MemberNumber); 2531 | 2532 | // breaking with other players 2533 | if ((Acc.Lovership != null) && (AL >= 0) && (Acc.Lovership[AL].Stage != null) 2534 | && (Acc.Lovership[AL].Start != null) && ((Acc.Lovership[AL].Stage != 2) || (Acc.Lovership[AL].Start + LovershipDelay <= CommonTime()))) { 2535 | 2536 | // Update the other account if she's online, then update the database 2537 | var P = []; 2538 | Database.collection(AccountCollection).find({ MemberNumber : data.MemberNumber }).sort({MemberNumber: -1}).limit(1).toArray(function(err, result) { 2539 | if (err) throw err; 2540 | if ((result != null) && (typeof result === "object") && (result.length > 0)) { 2541 | P = result[0].Lovership; 2542 | 2543 | var TargetLoversNumbers = []; 2544 | if ((P != null) && Array.isArray(P)) 2545 | for (const Lover of P) 2546 | TargetLoversNumbers.push(Lover.MemberNumber ? Lover.MemberNumber : -1); 2547 | 2548 | var TL = TargetLoversNumbers.indexOf(Acc.MemberNumber); 2549 | // Don't try to remove an already removed lover 2550 | if (TL >= 0) { 2551 | if (Array.isArray(P)) P.splice(TL, 1); 2552 | else P = []; 2553 | 2554 | for (const OtherAcc of Account) 2555 | if (OtherAcc.MemberNumber == data.MemberNumber) { 2556 | OtherAcc.Lovership = P; 2557 | OtherAcc.Socket.emit("AccountLovership", { Lovership: OtherAcc.Lovership }); 2558 | if (OtherAcc.ChatRoom != null) 2559 | ChatRoomSyncCharacter(OtherAcc.ChatRoom, OtherAcc.MemberNumber, OtherAcc.MemberNumber); 2560 | } 2561 | 2562 | AccountUpdateLovership(P, data.MemberNumber, null, false); 2563 | } 2564 | } 2565 | 2566 | // Make sure we don't do a double-delete in the odd case where we're breaking up with ourselves 2567 | if (data.MemberNumber === Acc.MemberNumber) return; 2568 | 2569 | // Updates the account that triggered the break up 2570 | if ((Acc.Lovership == null) || !Array.isArray(Acc.Lovership)) Acc.Lovership = []; 2571 | else if ((AL < Acc.Lovership.length) && (Acc.Lovership[AL].MemberNumber === data.MemberNumber)) Acc.Lovership.splice(AL, 1); 2572 | AccountUpdateLovership(Acc.Lovership, Acc.MemberNumber); 2573 | 2574 | }); 2575 | return; 2576 | } 2577 | // breaking with NPC 2578 | else if ((Acc.Lovership != null) && (data.MemberNumber < 0) && (data.Name != null)) { 2579 | Acc.Lovership.splice(AccLoversNumbers.indexOf(data.Name), 1); 2580 | AccountUpdateLovership(Acc.Lovership, Acc.MemberNumber); 2581 | return; 2582 | } 2583 | } 2584 | 2585 | // In a chatroom, two players can enter in a lover relationship (6 steps to complete) 2586 | if ((Acc != null) && (Acc.ChatRoom != null)) { 2587 | 2588 | var AccLoversNumbers = []; 2589 | for (const Lover of Acc.Lovership) { 2590 | if (Lover.MemberNumber != null) { AccLoversNumbers.push(Lover.MemberNumber); } 2591 | else if (Lover.BeginDatingOfferedByMemberNumber) { AccLoversNumbers.push(Lover.BeginDatingOfferedByMemberNumber); } 2592 | else { AccLoversNumbers.push(-1); } 2593 | } 2594 | var AL = AccLoversNumbers.indexOf(data.MemberNumber); 2595 | 2596 | // One player propose to another 2597 | if (((Acc.Lovership.length < 5) && (AL < 0)) || (AL >= 0)) // Cannot propose if target player is already a lover, up to 5 loverships 2598 | for (const RoomAcc of Acc.ChatRoom.Account) 2599 | if ((RoomAcc.MemberNumber == data.MemberNumber) && (RoomAcc.BlackList.indexOf(Acc.MemberNumber) < 0)) { // Cannot propose if on blacklist 2600 | 2601 | var TargetLoversNumbers = []; 2602 | for (const RoomAccLover of RoomAcc.Lovership) { 2603 | if (RoomAccLover.MemberNumber != null) { 2604 | TargetLoversNumbers.push(RoomAccLover.MemberNumber); 2605 | } 2606 | else if (RoomAccLover.BeginDatingOfferedByMemberNumber) { 2607 | TargetLoversNumbers.push(RoomAccLover.BeginDatingOfferedByMemberNumber); 2608 | } 2609 | else { TargetLoversNumbers.push(-1); } 2610 | } 2611 | var TL = TargetLoversNumbers.indexOf(Acc.MemberNumber); 2612 | 2613 | // Ignore requests for self-lovers 2614 | if (Acc.MemberNumber === RoomAcc.MemberNumber) return; 2615 | 2616 | // If the target account is not a lover of player yet, can accept up to 5 loverships, one player can propose to start dating (Step 1 / 6) 2617 | if ((RoomAcc.Lovership.length < 5) && (TL < 0)) { 2618 | if ((data.Action != null) && (data.Action === "Propose")) { 2619 | RoomAcc.Lovership.push({ BeginDatingOfferedByMemberNumber: Acc.MemberNumber }); 2620 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "OfferBeginDating", "ServerMessage", RoomAcc.MemberNumber, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2621 | } else socket.emit("AccountLovership", { MemberNumber: data.MemberNumber, Result: "CanOfferBeginDating" }); 2622 | } 2623 | 2624 | // If dating has started, a player can propose to engage after a delay (Step 3 / 6) 2625 | if ((TL >= 0) && (RoomAcc.Lovership[TL].BeginEngagementOfferedByMemberNumber == null) 2626 | && (RoomAcc.Lovership[TL].Stage != null) && (RoomAcc.Lovership[TL].Start != null) 2627 | && (RoomAcc.Lovership[TL].Stage == 0) && (RoomAcc.Lovership[TL].Start + LovershipDelay <= CommonTime())) { 2628 | if ((data.Action != null) && (data.Action === "Propose")) { 2629 | RoomAcc.Lovership[TL].BeginEngagementOfferedByMemberNumber = Acc.MemberNumber; 2630 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "OfferBeginEngagement", "ServerMessage", RoomAcc.MemberNumber, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2631 | } else socket.emit("AccountLovership", { MemberNumber: data.MemberNumber, Result: "CanOfferBeginEngagement" }); 2632 | } 2633 | 2634 | // If engaged, a player can propose to marry after a delay (Step 5 / 6) 2635 | if ((TL >= 0) && (RoomAcc.Lovership[TL].BeginWeddingOfferedByMemberNumber == null) 2636 | && (RoomAcc.Lovership[TL].Stage != null) && (RoomAcc.Lovership[TL].Start != null) 2637 | && (RoomAcc.Lovership[TL].Stage == 1) && (RoomAcc.Lovership[TL].Start + LovershipDelay <= CommonTime())) { 2638 | if ((data.Action != null) && (data.Action === "Propose")) { 2639 | RoomAcc.Lovership[TL].BeginWeddingOfferedByMemberNumber = Acc.MemberNumber; 2640 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "OfferBeginWedding", "ServerMessage", RoomAcc.MemberNumber, [{ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }]); 2641 | } else socket.emit("AccountLovership", { MemberNumber: data.MemberNumber, Result: "CanOfferBeginWedding" }); 2642 | } 2643 | 2644 | } 2645 | 2646 | // A player can accept a proposal from another one 2647 | if (((Acc.Lovership.length <= 5)) && (AL >= 0)) // No possible interaction if the player has reached the number of possible lovership or if isn't already a lover 2648 | for (const AccRoom of Acc.ChatRoom.Account) 2649 | if ((AccRoom.MemberNumber == data.MemberNumber) && (AccRoom.BlackList.indexOf(Acc.MemberNumber) < 0)) { // Cannot accept if on blacklist 2650 | 2651 | var TargetLoversNumbers = []; 2652 | for (const AccRoomLover of AccRoom.Lovership) { 2653 | if (AccRoomLover.MemberNumber) { 2654 | TargetLoversNumbers.push(AccRoomLover.MemberNumber); 2655 | } 2656 | else if (AccRoomLover.BeginDatingOfferedByMemberNumber) { 2657 | TargetLoversNumbers.push(AccRoomLover.BeginDatingOfferedByMemberNumber); 2658 | } 2659 | else { 2660 | TargetLoversNumbers.push(-1); 2661 | } 2662 | } 2663 | var TL = TargetLoversNumbers.indexOf(Acc.MemberNumber); 2664 | 2665 | // If a player wants to accept to start dating (Step 2 / 6) 2666 | if ((Acc.Lovership[AL].BeginDatingOfferedByMemberNumber != null) && (Acc.Lovership[AL].BeginDatingOfferedByMemberNumber == data.MemberNumber) 2667 | && ((AccRoom.Lovership.length < 5) || (TL >= 0))) { 2668 | if ((data.Action != null) && (data.Action === "Accept")) { 2669 | Acc.Lovership[AL] = { MemberNumber: data.MemberNumber, Name: AccRoom.Name, Start: CommonTime(), Stage: 0 }; 2670 | if (TL >= 0) { AccRoom.Lovership[TL] = { MemberNumber: Acc.MemberNumber, Name: Acc.Name, Start: CommonTime(), Stage: 0 }; } 2671 | else { AccRoom.Lovership.push({ MemberNumber: Acc.MemberNumber, Name: Acc.Name, Start: CommonTime(), Stage: 0 }); } 2672 | AccountUpdateLovership( Acc.Lovership, Acc.MemberNumber); 2673 | AccountUpdateLovership( AccRoom.Lovership, Acc.Lovership[AL].MemberNumber, AccRoom.Socket); 2674 | var Dictionary = []; 2675 | Dictionary.push({ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }); 2676 | Dictionary.push({ Tag: "TargetCharacter", Text: Acc.Lovership[AL].Name, MemberNumber: Acc.Lovership[AL].MemberNumber }); 2677 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "BeginDating", "ServerMessage", null, Dictionary); 2678 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.MemberNumber); 2679 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.Lovership[AL].MemberNumber); 2680 | } else socket.emit("AccountLovership", { MemberNumber: data.MemberNumber, Result: "CanBeginDating" }); 2681 | } 2682 | 2683 | // If the player wants to become one's fiancée (Step 4 / 6) 2684 | if ((Acc.Lovership[AL].Stage != null) && (Acc.Lovership[AL].Stage == 0) 2685 | && (Acc.Lovership[AL].BeginEngagementOfferedByMemberNumber != null) && (Acc.Lovership[AL].BeginEngagementOfferedByMemberNumber == data.MemberNumber)) { 2686 | if ((data.Action != null) && (data.Action === "Accept")) { 2687 | Acc.Lovership[AL] = { MemberNumber: data.MemberNumber, Name: AccRoom.Name, Start: CommonTime(), Stage: 1 }; 2688 | AccRoom.Lovership[TL] = { MemberNumber: Acc.MemberNumber, Name: Acc.Name, Start: CommonTime(), Stage: 1 }; 2689 | AccountUpdateLovership( Acc.Lovership, Acc.MemberNumber); 2690 | AccountUpdateLovership( AccRoom.Lovership, Acc.Lovership[AL].MemberNumber, AccRoom.Socket); 2691 | var Dictionary = []; 2692 | Dictionary.push({ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }); 2693 | Dictionary.push({ Tag: "TargetCharacter", Text: Acc.Lovership[AL].Name, MemberNumber: Acc.Lovership[AL].MemberNumber }); 2694 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "BeginEngagement", "ServerMessage", null, Dictionary); 2695 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.MemberNumber); 2696 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.Lovership[AL].MemberNumber); 2697 | } else socket.emit("AccountLovership", { MemberNumber: data.MemberNumber, Result: "CanBeginEngagement" }); 2698 | } 2699 | 2700 | // If the player wants to become one's wife (Step 6 / 6) 2701 | if ((Acc.Lovership[AL].Stage != null) && (Acc.Lovership[AL].Stage == 1) 2702 | && (Acc.Lovership[AL].BeginWeddingOfferedByMemberNumber != null) && (Acc.Lovership[AL].BeginWeddingOfferedByMemberNumber == data.MemberNumber)) { 2703 | if ((data.Action != null) && (data.Action === "Accept")) { 2704 | Acc.Lovership[AL] = { MemberNumber: data.MemberNumber, Name: AccRoom.Name, Start: CommonTime(), Stage: 2 }; 2705 | AccRoom.Lovership[TL] = { MemberNumber: Acc.MemberNumber, Name: Acc.Name, Start: CommonTime(), Stage: 2 }; 2706 | AccountUpdateLovership( Acc.Lovership, Acc.MemberNumber); 2707 | AccountUpdateLovership( AccRoom.Lovership, Acc.Lovership[AL].MemberNumber, AccRoom.Socket); 2708 | var Dictionary = []; 2709 | Dictionary.push({ Tag: "SourceCharacter", Text: Acc.Name, MemberNumber: Acc.MemberNumber }); 2710 | Dictionary.push({ Tag: "TargetCharacter", Text: Acc.Lovership[AL].Name, MemberNumber: Acc.Lovership[AL].MemberNumber }); 2711 | ChatRoomMessage(Acc.ChatRoom, Acc.MemberNumber, "BeginWedding", "ServerMessage", null, Dictionary); 2712 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.MemberNumber); 2713 | ChatRoomSyncCharacter(Acc.ChatRoom, Acc.MemberNumber, Acc.Lovership[AL].MemberNumber); 2714 | } else socket.emit("AccountLovership", { MemberNumber: data.MemberNumber, Result: "CanBeginWedding" }); 2715 | } 2716 | } 2717 | 2718 | } 2719 | 2720 | } 2721 | } 2722 | 2723 | /** 2724 | * Sets a new account difficulty (0 is easy/roleplay, 1 is normal/regular, 2 is hard/hardcore, 3 is very hard/extreme) 2725 | * @param {number} data 2726 | * @param {ServerSocket} socket 2727 | */ 2728 | function AccountDifficulty(data, socket) { 2729 | if ((data != null) && (typeof data === "number") && (data >= 0) && (data <= 3)) { 2730 | 2731 | // Gets the current account 2732 | var Acc = AccountGet(socket.id); 2733 | if (Acc != null) { 2734 | 2735 | // Can only set to 2 or 3 if no change was done for 1 week 2736 | var LastChange = ((Acc.Difficulty == null) || (Acc.Difficulty.LastChange == null) || (typeof Acc.Difficulty.LastChange !== "number")) ? Acc.Creation : Acc.Difficulty.LastChange; 2737 | if ((data <= 1) || (LastChange + DifficultyDelay < CommonTime())) { 2738 | 2739 | // Updates the account and the database 2740 | var NewDifficulty = { Difficulty: { Level: /** @type {DifficultyLevel} */ (data), LastChange: CommonTime() } }; 2741 | Acc.Difficulty = NewDifficulty.Difficulty; 2742 | //console.log("Updating account " + Acc.AccountName + " difficulty to " + NewDifficulty.Difficulty.Level); 2743 | Database.collection(AccountCollection).updateOne({ AccountName : Acc.AccountName }, { $set: NewDifficulty }, function(err, res) { if (err) throw err; }); 2744 | 2745 | } 2746 | 2747 | } 2748 | 2749 | } 2750 | } 2751 | --------------------------------------------------------------------------------