@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
14
14
limitations under the License.
15
15
*/
16
16
17
+ import AwaitLock from "await-lock" ;
17
18
import { UnstableValue } from "matrix-events-sdk" ;
18
19
19
20
import { MatrixClient } from "../client" ;
@@ -73,6 +74,16 @@ export enum PolicyScope {
73
74
* our data structures.
74
75
*/
75
76
export class IgnoredInvites {
77
+ // A lock around method `getOrCreateTargetRoom`.
78
+ // Used to ensure that only one async task of this class
79
+ // is creating a new target room and modifying the
80
+ // `target` property of account key `IGNORE_INVITES_POLICIES`.
81
+ private _getOrCreateTargetRoomLock = new AwaitLock ( ) ;
82
+
83
+ // A lock around method `withIgnoreInvitesPoliciesLock`.
84
+ // Used to ensure that only one async task of this class is
85
+ // modifying `IGNORE_INVITES_POLICIES` at any point in time.
86
+ private _withIgnoreInvitesPoliciesLock = new AwaitLock ( ) ;
76
87
constructor (
77
88
private readonly client : MatrixClient ,
78
89
) {
@@ -85,6 +96,12 @@ export class IgnoredInvites {
85
96
* @param entity The entity covered by this rule. Globs are supported.
86
97
* @param reason A human-readable reason for introducing this new rule.
87
98
* @return The event id for the new rule.
99
+ *
100
+ * # Safety
101
+ *
102
+ * This method will rewrite the `Policies` object in the user's account data.
103
+ * This rewrite is inherently racy and could overwrite or be overwritten by
104
+ * other concurrent rewrites of the same object.
88
105
*/
89
106
public async addRule ( scope : PolicyScope , entity : string , reason : string ) : Promise < string > {
90
107
const target = await this . getOrCreateTargetRoom ( ) ;
@@ -121,11 +138,10 @@ export class IgnoredInvites {
121
138
*/
122
139
public async addSource ( roomId : string ) : Promise < boolean > {
123
140
// We attempt to join the room *before* calling
124
- // `await this.getOrCreateSourceRooms ()` to decrease the duration
141
+ // `await this.getSourceRooms ()` to decrease the duration
125
142
// of the racy section.
126
143
await this . client . joinRoom ( roomId ) ;
127
- // Race starts.
128
- const sources = ( await this . getOrCreateSourceRooms ( ) )
144
+ const sources = this . getSourceRooms ( )
129
145
. map ( room => room . roomId ) ;
130
146
if ( sources . includes ( roomId ) ) {
131
147
return false ;
@@ -135,7 +151,6 @@ export class IgnoredInvites {
135
151
ignoreInvitesPolicies . sources = sources ;
136
152
} ) ;
137
153
138
- // Race ends.
139
154
return true ;
140
155
}
141
156
@@ -146,10 +161,10 @@ export class IgnoredInvites {
146
161
* @param roomId The room to which the user is invited.
147
162
* @returns A rule matching the entity, if any was found, `null` otherwise.
148
163
*/
149
- public async getRuleForInvite ( { sender, roomId } : {
164
+ public getRuleForInvite ( { sender, roomId } : {
150
165
sender : string ;
151
166
roomId : string ;
152
- } ) : Promise < Readonly < MatrixEvent | null > > {
167
+ } ) : Readonly < MatrixEvent | null > {
153
168
// In this implementation, we perform a very naive lookup:
154
169
// - search in each policy room;
155
170
// - turn each (potentially glob) rule entity into a regexp.
@@ -160,7 +175,7 @@ export class IgnoredInvites {
160
175
// - match several entities per go;
161
176
// - pre-compile each rule entity into a regexp;
162
177
// - pre-compile entire rooms into a single regexp.
163
- const policyRooms = await this . getOrCreateSourceRooms ( ) ;
178
+ const policyRooms = this . getSourceRooms ( ) ;
164
179
const senderServer = sender . split ( ":" ) [ 1 ] ;
165
180
const roomServer = roomId . split ( ":" ) [ 1 ] ;
166
181
for ( const room of policyRooms ) {
@@ -229,21 +244,30 @@ export class IgnoredInvites {
229
244
const room = this . client . getRoom ( target ) ;
230
245
if ( room ) {
231
246
return room ;
232
- } else {
233
- target = null ;
234
247
}
235
248
}
236
- // We need to create our own policy room for ignoring invites.
237
- target = ( await this . client . createRoom ( {
238
- name : "Individual Policy Room" ,
239
- preset : Preset . PrivateChat ,
240
- } ) ) . room_id ;
241
- await this . withIgnoreInvitesPolicies ( ignoreInvitesPolicies => {
242
- ignoreInvitesPolicies . target = target ;
243
- } ) ;
249
+ try {
250
+ // We need to create our own policy room for ignoring invites.
251
+ await this . _getOrCreateTargetRoomLock . acquireAsync ( ) ;
252
+ target = ( await this . client . createRoom ( {
253
+ name : "Individual Policy Room" ,
254
+ preset : Preset . PrivateChat ,
255
+ } ) ) . room_id ;
256
+ await this . withIgnoreInvitesPolicies ( ignoreInvitesPolicies => {
257
+ ignoreInvitesPolicies . target = target ;
258
+ if ( ! ( "sources" in ignoreInvitesPolicies ) ) {
259
+ // `[target]` is a reasonable default for `sources`.
260
+ ignoreInvitesPolicies . sources = [ target ] ;
261
+ }
262
+ } ) ;
244
263
245
- // Since we have just called `createRoom`, `getRoom` should not be `null`.
246
- return this . client . getRoom ( target ) ! ;
264
+ // Since we have just called `createRoom`, `getRoom` should not be `null`.
265
+ // Note that this is unavoidably racy, e.g. another client could have left
266
+ // the room during the call to `this.withIgnoreInvitesPolicies`.
267
+ return this . client . getRoom ( target ) ! ;
268
+ } finally {
269
+ this . _getOrCreateTargetRoomLock . release ( ) ;
270
+ }
247
271
}
248
272
249
273
/**
@@ -260,40 +284,21 @@ export class IgnoredInvites {
260
284
* This rewrite is inherently racy and could overwrite or be overwritten by
261
285
* other concurrent rewrites of the same object.
262
286
*/
263
- public async getOrCreateSourceRooms ( ) : Promise < Room [ ] > {
287
+ public getSourceRooms ( ) : Room [ ] {
264
288
const ignoreInvitesPolicies = this . getIgnoreInvitesPolicies ( ) ;
265
289
let sources = ignoreInvitesPolicies . sources ;
266
290
267
291
// Validate `sources`. If it is invalid, trash out the current `sources`
268
292
// and create a new list of sources from `target`.
269
- let hasChanges = false ;
270
293
if ( ! Array . isArray ( sources ) ) {
271
294
// `sources` could not be an array.
272
- hasChanges = true ;
273
295
sources = [ ] ;
274
296
}
275
- let sourceRooms : Room [ ] = sources
297
+ const sourceRooms : Room [ ] = sources
276
298
// `sources` could contain non-string / invalid room ids
277
299
. filter ( roomId => typeof roomId === "string" )
278
300
. map ( roomId => this . client . getRoom ( roomId ) )
279
301
. filter ( room => ! ! room ) ;
280
- if ( sourceRooms . length != sources . length ) {
281
- hasChanges = true ;
282
- }
283
- if ( sourceRooms . length == 0 ) {
284
- // `sources` could be empty (possibly because we've removed
285
- // invalid content)
286
- const target = await this . getOrCreateTargetRoom ( ) ;
287
- hasChanges = true ;
288
- sourceRooms = [ target ] ;
289
- }
290
- if ( hasChanges ) {
291
- // Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
292
- // during or by our call to `this.getTargetRoom()`.
293
- await this . withIgnoreInvitesPolicies ( ignoreInvitesPolicies => {
294
- ignoreInvitesPolicies . sources = sources ;
295
- } ) ;
296
- }
297
302
return sourceRooms ;
298
303
}
299
304
@@ -315,10 +320,15 @@ export class IgnoredInvites {
315
320
* Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
316
321
*/
317
322
private async withIgnoreInvitesPolicies ( cb : ( ignoreInvitesPolicies : { [ key : string ] : any } ) => void ) {
318
- const { policies, ignoreInvitesPolicies } = this . getPoliciesAndIgnoreInvitesPolicies ( ) ;
319
- cb ( ignoreInvitesPolicies ) ;
320
- policies [ IGNORE_INVITES_ACCOUNT_EVENT_KEY . name ] = ignoreInvitesPolicies ;
321
- await this . client . setAccountData ( POLICIES_ACCOUNT_EVENT_TYPE . name , policies ) ;
323
+ await this . _withIgnoreInvitesPoliciesLock . acquireAsync ( ) ;
324
+ try {
325
+ const { policies, ignoreInvitesPolicies } = this . getPoliciesAndIgnoreInvitesPolicies ( ) ;
326
+ cb ( ignoreInvitesPolicies ) ;
327
+ policies [ IGNORE_INVITES_ACCOUNT_EVENT_KEY . name ] = ignoreInvitesPolicies ;
328
+ await this . client . setAccountData ( POLICIES_ACCOUNT_EVENT_TYPE . name , policies ) ;
329
+ } finally {
330
+ this . _withIgnoreInvitesPoliciesLock . release ( ) ;
331
+ }
322
332
}
323
333
324
334
/**
0 commit comments