/
Places.ts
executable file
·271 lines (247 loc) · 13.7 KB
/
Places.ts
1
2
3
4
5
6
7
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// Copyright 2020 Vircadia Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'
import Config from '@Base/config';
import { Domains } from '@Entities/Domains';
import { AccountEntity } from '@Entities/AccountEntity';
import { PlaceEntity } from '@Entities/PlaceEntity';
import { placeFields } from '@Entities/PlaceFields';
import { DomainEntity } from './DomainEntity';
import { AuthToken } from '@Entities/AuthToken';
import { Tokens, TokenScope } from '@Entities/Tokens';
import { CriteriaFilter } from '@Entities/EntityFilters/CriteriaFilter';
import { GenericFilter } from '@Entities/EntityFilters/GenericFilter';
import { ValidateResponse } from '@Route-Tools/EntityFieldDefn';
import { getEntityField, setEntityField, getEntityUpdateForField } from '@Route-Tools/GetterSetter';
import { createObject, getObject, getObjects, updateObjectFields, deleteOne, deleteMany, noCaseCollation } from '@Tools/Db';
import { GenUUID, IsNullOrEmpty, IsNotNullOrEmpty, genRandomString } from '@Tools/Misc';
import { VKeyedCollection } from '@Tools/vTypes';
import { Logger } from '@Tools/Logging';
import { PlaceFilterInfo } from './EntityFilters/PlaceFilterInfo';
import { Accounts } from './Accounts';
export let placeCollection = 'places';
// Initialize place management.
export function initPlaces(): void {
const placer = new PlaceFilterInfo();
// Update current attendance and aliveness for places
// This saves reading the domain information on every place fetch
// [Note: There is a lot of checking for values changing to reduce database updates]
setInterval( async () => {
let numPlaces = 0;
let numUnhookedPlaces = 0;
let numInactivePlaces = 0;
// The date when a place is considered "inactive" (could be the domain)
const inactivePlaceTime = Places.dateWhenNotActive();
// The date when a Place's "current" update information is considered stale
const lastGoodUpdateTime = new Date(Date.now()
- (Config['metaverse-server']['place-current-timeout-minutes'] * 60 * 1000));
// Logger.debug(`PlaceActivity: inactive=${inactivePlaceTime.toISOString()}, stale=${lastGoodUpdateTime.toISOString()}`);
for await (const aPlace of Places.enumerateAsync(placer)) {
numPlaces++;
const updates: VKeyedCollection = {};
// If the Place hasn't set the current info or the information is stale, update
if (IsNullOrEmpty(aPlace.currentLastUpdateTime) || aPlace.currentLastUpdateTime < lastGoodUpdateTime) {
// Logger.debug(`PlaceActivity: place ${aPlace.name} has no currentLastUpdateTime`);
// The place has stale current update info. Use domain attendance information
const aDomain = await Domains.getDomainWithId(aPlace.domainId);
if (aDomain) {
// Logger.debug(` PlaceActivity: found domain for place ${aPlace.name}`);
const domainAttendance = (aDomain.numUsers ?? 0) + (aDomain.anonUsers ?? 0);
// If the Place doesn't have an attendance number or that number doesn't match the domain, update
if (IsNullOrEmpty(aPlace.currentAttendance) || aPlace.currentAttendance !== domainAttendance) {
aPlace.currentAttendance = domainAttendance;
updates.currentAttendance = aPlace.currentAttendance;
};
// If the Place doesn't have a last activity set it to the domain's
if (IsNullOrEmpty(aPlace.lastActivity)) {
if (IsNullOrEmpty(aDomain.timeOfLastHeartbeat)) {
// If the domain doesn't have a last update time either, assume stale Place
aPlace.lastActivity = inactivePlaceTime;
}
else {
aPlace.lastActivity = aDomain.timeOfLastHeartbeat;
};
updates.lastActivity = aPlace.lastActivity;
};
}
else {
// can't find the domain to go with the place. Set everything to zero and update DB
// Logger.debug(` PlaceActivity: no domain found for place ${aPlace.name}`);
numUnhookedPlaces++;
aPlace.currentAttendance = 0;
aPlace.lastActivity = inactivePlaceTime;
updates.currentAttendance = aPlace.currentAttendance;
updates.lastActivity = aPlace.lastActivity;
};
}
else {
// The Place has updated current info so just set the current activity time
// Logger.debug(`PlaceActivity: place ${aPlace.name} has currentLastUpdateTime so updating activity`);
if (IsNullOrEmpty(aPlace.lastActivity) || aPlace.lastActivity !== aPlace.currentLastUpdateTime) {
aPlace.lastActivity = aPlace.currentLastUpdateTime;
updates.lastActivity = aPlace.lastActivity;
};
};
if (aPlace.lastActivity <= inactivePlaceTime) {
// Logger.debug(` PlaceActivity: place ${aPlace.name} deemed inactive`);
numInactivePlaces++;
};
// This updateEntityFields does nothing if 'updates' is empty
await Places.updateEntityFields(aPlace, updates);
};
// Logger.debug(`PlaceActivity: numPlaces=${numPlaces}, unhookedPlaces=${numUnhookedPlaces}, inactivePlaces=${numInactivePlaces}`);
}, 1000 * Config['metaverse-server']['place-check-last-activity-seconds'] );
};
export const Places = {
async getPlaceWithId(pPlaceId: string): Promise<PlaceEntity> {
return IsNullOrEmpty(pPlaceId) ? null : getObject(placeCollection,
new GenericFilter({ 'id': pPlaceId }));
},
async getPlaceWithName(pPlacename: string): Promise<PlaceEntity> {
return IsNullOrEmpty(pPlacename) ? null : getObject(placeCollection,
new GenericFilter({ 'name': pPlacename }),
noCaseCollation);
},
async addPlace(pPlaceEntity: PlaceEntity) : Promise<PlaceEntity> {
Logger.info(`Places: creating place ${pPlaceEntity.name}, id=${pPlaceEntity.id}`);
return IsNullOrEmpty(pPlaceEntity) ? null : createObject(placeCollection, pPlaceEntity);
},
async createPlace(pAccountId: string): Promise<PlaceEntity> {
const newPlace = new PlaceEntity();
newPlace.id = GenUUID();
newPlace.name = 'UNKNOWN-' + genRandomString(5);
newPlace.path = '/0,0,0/0,0,0,1';
newPlace.whenCreated = new Date();
newPlace.currentAttendance = 0;
const APItoken = await Tokens.createToken(pAccountId, [ TokenScope.PLACE ], -1);
await Tokens.addToken(APItoken); // put token into DB
newPlace.currentAPIKeyTokenId = APItoken.id;
return newPlace;
},
// Verify the passed placename is unique and return the unique name
async uniqifyPlaceName(pPlaceName: string): Promise<string> {
let newPlacename = pPlaceName;
const existingPlace = await Places.getPlaceWithName(newPlacename);
if (existingPlace) {
newPlacename = newPlacename + '-' + genRandomString(5);
Logger.info(`uniqifyPlaceName: non-unique place name ${pPlaceName}. Creating ${newPlacename}`);
};
return newPlacename;
},
// Get the value of a place field with the fieldname.
// Checks to make sure the getter has permission to get the values.
// Returns the value. Could be 'undefined' whether the requestor doesn't have permissions or that's
// the actual field value.
async getField(pAuthToken: AuthToken, pPlace: PlaceEntity,
pField: string, pRequestingAccount?: AccountEntity): Promise<any> {
return getEntityField(placeFields, pAuthToken, pPlace, pField, pRequestingAccount);
},
// Set a place field with the fieldname and a value.
// Checks to make sure the setter has permission to set.
// Returns 'true' if the value was set and 'false' if the value could not be set.
async setField(pAuthToken: AuthToken, // authorization for making this change
pPlace: PlaceEntity, // the place being changed
pField: string, pVal: any, // field being changed and the new value
pRequestingAccount?: AccountEntity, // Account associated with pAuthToken, if known
pUpdates?: VKeyedCollection // where to record updates made (optional)
): Promise<ValidateResponse> {
return setEntityField(placeFields, pAuthToken, pPlace, pField, pVal, pRequestingAccount, pUpdates);
},
// Verify that the passed value is legal for the named field
async validateFieldValue(pFieldName: string, pValue: any): Promise<ValidateResponse> {
const defn = placeFields[pFieldName];
if (defn) {
return await defn.validate(defn, defn.request_field_name, pValue);
};
return { 'valid': false, 'reason': 'Unknown field name' };
},
// Generate an 'update' block for the specified field or fields.
// This is a field/value collection that can be passed to the database routines.
// Note that this directly fetches the field value rather than using 'getter' since
// we want the actual value (whatever it is) to go into the database.
// If an existing VKeyedCollection is passed, it is added to an returned.
getUpdateForField(pPlace: PlaceEntity,
pField: string | string[], pExisting?: VKeyedCollection): VKeyedCollection {
return getEntityUpdateForField(placeFields, pPlace, pField, pExisting);
},
async removePlace(pPlaceEntity: PlaceEntity) : Promise<boolean> {
Logger.info(`Places: removing place ${pPlaceEntity.name}, id=${pPlaceEntity.id}`);
return deleteOne(placeCollection, new GenericFilter({ 'id': pPlaceEntity.id }) );
},
async removeMany(pCriteria: CriteriaFilter) : Promise<number> {
return deleteMany(placeCollection, pCriteria);
},
async *enumerateAsync(pPager: CriteriaFilter,
pInfoer?: CriteriaFilter, pScoper?: CriteriaFilter): AsyncGenerator<PlaceEntity> {
for await (const place of getObjects(placeCollection, pPager, pInfoer, pScoper)) {
yield place;
};
// return getObjects(placeCollection, pCriteria, pPager); // not sure why this doesn't work
},
// The contents of this entity have been updated
async updateEntityFields(pEntity: PlaceEntity, pFields: VKeyedCollection): Promise<PlaceEntity> {
return updateObjectFields(placeCollection,
new GenericFilter({ 'id': pEntity.id }), pFields);
},
dateWhenNotActive() : Date {
return new Date(Date.now() - (Config['metaverse-server']['place-inactive-timeout-minutes'] * 60 * 1000));
},
async getCurrentInfoAPIKey(pPlace: PlaceEntity): Promise<string> {
// Return that APIKey value from the access token
let key: string;
const keyToken = await Tokens.getTokenWithTokenId(pPlace.currentAPIKeyTokenId);
if (IsNotNullOrEmpty(keyToken)) {
key = keyToken.token;
};
return key;
},
async getAddressString(pPlace: PlaceEntity): Promise<string> {
// Compute and return the string for the Places's address.
// The address is of the form "optional-domain/x,y,z/x,y,z,w".
// If the domain is missing, the domain-server's network address is added
let addr = pPlace.path ?? '/0,0,0/0,0,0,1';
// If no domain/address specified in path, build addr using reported domain IP/port
const pieces = addr.split('/');
if (pieces[0].length === 0) {
const aDomain = await Domains.getDomainWithId(pPlace.domainId);
if (IsNotNullOrEmpty(aDomain)) {
if (IsNotNullOrEmpty(aDomain.networkAddr)) {
let domainAddr = aDomain.networkAddr;
if (IsNotNullOrEmpty(aDomain.networkPort)) {
domainAddr = aDomain.networkAddr + ":" + aDomain.networkPort;
};
addr = domainAddr + addr;
};
};
};
return addr;
},
// Function that checks to see if there are managers and, if none specified,
// updates place with the name of the domain's sponser.
// This fixes up legacy places with the proper name of the domain owner.
async getManagers(pPlace: PlaceEntity): Promise<string[]> {
if (IsNullOrEmpty(pPlace.managers)) {
pPlace.managers = [];
const aDomain = await Domains.getDomainWithId(pPlace.domainId);
if (aDomain) {
const aAccount = await Accounts.getAccountWithId(aDomain.sponsorAccountId);
if (aAccount) {
pPlace.managers = [ aAccount.username ];
};
};
await Places.updateEntityFields(pPlace, { 'managers': pPlace.managers })
}
return pPlace.managers;
}
};