Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / Controllers / XMPP / OTRXMPPRoomManager.m @ 74191bbf

History | View | Annotate | Download (22.5 KB)

1
//
2
//  OTRXMPPRoomManager.m
3
//  ChatSecure
4
//
5
//  Created by David Chiles on 10/9/15.
6
//  Copyright © 2015 Chris Ballinger. All rights reserved.
7
//
8

    
9
#import "OTRXMPPRoomManager.h"
10
@import XMPPFramework;
11
#import <ChatSecureCore/ChatSecureCore-Swift.h>
12
#import "OTRXMPPRoomYapStorage.h"
13
#import "OTRBuddy.h"
14
@import YapDatabase;
15
#import "OTRLog.h"
16

    
17

    
18
@interface OTRXMPPRoomManager () <XMPPMUCDelegate, XMPPRoomDelegate, XMPPStreamDelegate, OTRYapViewHandlerDelegateProtocol>
19

    
20
@property (nonatomic, strong, readonly) NSMutableDictionary<XMPPJID*,XMPPRoom*> *rooms;
21

    
22
@property (nonatomic, strong, readonly) XMPPMUC *mucModule;
23

    
24
/** This dictionary has jid as the key and array of buddy unique Ids to invite once we've joined the room*/
25
@property (nonatomic, strong, readonly) NSMutableDictionary<NSString*,NSArray<NSString *> *> *inviteDictionary;
26

    
27
/** This dictionary is a temporary holding for setting a room subject. Once the room is created teh subject is set from this dictionary. */
28
@property (nonatomic, strong, readonly) NSMutableDictionary<NSString*,NSString*> *tempRoomSubject;
29

    
30
/** This array is a temporary holding with rooms we should configure once connected */
31
@property (nonatomic, strong, readonly) NSMutableArray<NSString*> *roomsToConfigure;
32

    
33
@end
34

    
35
@implementation OTRXMPPRoomManager
36

    
37
- (instancetype)init {
38
    if (self = [super init]) {
39
        _mucModule = [[XMPPMUC alloc] init];
40
        _inviteDictionary = [[NSMutableDictionary alloc] init];
41
        _tempRoomSubject = [[NSMutableDictionary alloc] init];
42
        _roomsToConfigure = [[NSMutableArray alloc] init];
43
        _rooms = [[NSMutableDictionary alloc] init];
44
        _bookmarksModule = [[XMPPBookmarksModule alloc] initWithMode:XMPPBookmarksModePrivateXmlStorage dispatchQueue:nil];
45
    }
46
    return self;
47
}
48

    
49
- (BOOL)activate:(XMPPStream *)aXmppStream
50
{
51
    BOOL result = [super activate:aXmppStream];
52
    [self.mucModule activate:aXmppStream];
53
    [self.mucModule addDelegate:self delegateQueue:moduleQueue];
54
    [multicastDelegate addDelegate:self delegateQueue:moduleQueue];
55
    
56
    [self.bookmarksModule activate:self.xmppStream];
57
    
58
    //Register view for sending message queue and occupants
59
    [self.databaseConnection.database asyncRegisterGroupOccupantsView:nil completionBlock:nil];
60
    
61
    return result;
62
}
63

    
64
- (void) deactivate {
65
    [self.mucModule removeDelegate:self];
66
    [self.mucModule deactivate];
67
    [self.bookmarksModule deactivate];
68
    [super deactivate];
69
}
70

    
71
- (NSString *)joinRoom:(XMPPJID *)jid withNickname:(NSString *)name subject:(NSString *)subject password:(nullable NSString *)password
72
{
73
    dispatch_async(moduleQueue, ^{
74
        if ([subject length]) {
75
            [self.tempRoomSubject setObject:subject forKey:jid.bare];
76
        }
77
    });
78
    
79
    XMPPRoom *room = [self roomForJID:jid];
80
    NSString* accountId = self.xmppStream.tag;
81
    NSString *databaseRoomKey = [OTRXMPPRoom createUniqueId:accountId jid:jid.bare];
82
    __block NSString *nickname = name;
83

    
84
    // Already joined? Can happen if we have auto-join bookmarks.
85
    if (room && room.isJoined) {
86
        return databaseRoomKey;
87
    }
88
    
89
    if (!room) {
90
        OTRXMPPRoomYapStorage *storage = [[OTRXMPPRoomYapStorage alloc] initWithDatabaseConnection:self.databaseConnection];
91
        room = [[XMPPRoom alloc] initWithRoomStorage:storage jid:jid];
92
        [self setRoom:room forJID:room.roomJID];
93
        [room activate:self.xmppStream];
94
        [room addDelegate:self delegateQueue:moduleQueue];
95
    }
96
    
97
    /** Create room database object */
98
    [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
99
        OTRXMPPRoom *room = [[OTRXMPPRoom fetchObjectWithUniqueID:databaseRoomKey transaction:transaction] copy];
100
        if(!room) {
101
            room = [[OTRXMPPRoom alloc] init];
102
            room.lastRoomMessageId = @""; // Hack to make it show up in list
103
            room.accountUniqueId = accountId;
104
            room.jid = jid.bare;
105
        } else {
106
            // Clear out roles, we'll getpresence updates once we join
107
            [self clearOccupantRolesInRoom:room withTransaction:transaction];
108
        }
109
        
110
        //Other Room properties should be set here
111
        if ([subject length]) {
112
            room.subject = subject;
113
        }
114
        room.roomPassword = password;
115
        
116
        [room saveWithTransaction:transaction];
117
        
118
        if (!nickname) {
119
            OTRXMPPAccount *account = [OTRXMPPAccount fetchObjectWithUniqueID:accountId transaction:transaction];
120
            nickname = account.bareJID.user;
121
        }
122
    }];
123
    
124
    //Get history if any
125
    NSXMLElement *historyElement = nil;
126
    OTRXMPPRoomYapStorage *storage = room.xmppRoomStorage;
127
    id<OTRMessageProtocol> lastMessage = [storage lastMessageInRoom:room accountKey:accountId];
128
    NSDate *lastMessageDate = [lastMessage messageDate];
129
    if (lastMessageDate) {
130
        //Use since as our history marker if we have a last message
131
        //http://xmpp.org/extensions/xep-0045.html#enter-managehistory
132
        NSString *dateTimeString = [lastMessageDate xmppDateTimeString];
133
        historyElement = [NSXMLElement elementWithName:@"history"];
134
        [historyElement addAttributeWithName:@"since" stringValue:dateTimeString];
135
    }
136
    
137
    [room joinRoomUsingNickname:nickname history:historyElement password:password];
138
    return databaseRoomKey;
139
}
140

    
141
- (void)leaveRoom:(nonnull XMPPJID *)jid
142
{
143
    XMPPRoom *room = [self roomForJID:jid];
144
    [room leaveRoom];
145
    [self removeRoomForJID:jid];
146
    [room removeDelegate:self];
147
    [room deactivate];
148
}
149

    
150
- (void)clearOccupantRolesInRoom:(OTRXMPPRoom *)room withTransaction:(YapDatabaseReadWriteTransaction * _Nonnull)transaction {
151
    //Enumerate of room eges to occupants
152
    NSString *extensionName = [YapDatabaseConstants extensionName:DatabaseExtensionNameRelationshipExtensionName];
153
    [[transaction ext:extensionName] enumerateEdgesWithName:[OTRXMPPRoomOccupant roomEdgeName] destinationKey:room.uniqueId collection:[OTRXMPPRoom collection] usingBlock:^(YapDatabaseRelationshipEdge *edge, BOOL *stop) {
154
        
155
        OTRXMPPRoomOccupant *occupant = [transaction objectForKey:edge.sourceKey inCollection:edge.sourceCollection];
156
        occupant.role = RoomOccupantRoleNone;
157
        [occupant saveWithTransaction:transaction];
158
    }];
159
}
160

    
161
- (NSString *)startGroupChatWithBuddies:(NSArray<NSString *> *)buddiesArray roomJID:(XMPPJID *)roomName nickname:(NSString *)name subject:(nullable NSString *)subject
162
{
163
    if (buddiesArray.count) {
164
        [self performBlockAsync:^{
165
            [self.inviteDictionary setObject:buddiesArray forKey:roomName.bare];
166
        }];
167
    }
168
    [self.roomsToConfigure addObject:roomName.bare];
169
    XMPPConferenceBookmark *bookmark = [[XMPPConferenceBookmark alloc] initWithJID:roomName bookmarkName:subject nick:name autoJoin:YES];
170
    [self.bookmarksModule fetchAndPublishWithBookmarksToAdd:@[bookmark] bookmarksToRemove:nil completion:^(NSArray<id<XMPPBookmark>> * _Nullable newBookmarks, XMPPIQ * _Nullable responseIq) {
171
        if (newBookmarks) {
172
            DDLogInfo(@"Joined new room, added to merged bookmarks: %@", newBookmarks);
173
        }
174
    } completionQueue:nil];
175
    return [self joinRoom:roomName withNickname:name subject:subject password:nil];
176
}
177

    
178
- (void)inviteBuddies:(NSArray<NSString *> *)buddyUniqueIds toRoom:(XMPPRoom *)room {
179
    if (!buddyUniqueIds.count) {
180
        return;
181
    }
182
    NSMutableArray<XMPPJID*> *buddyJIDs = [NSMutableArray arrayWithCapacity:buddyUniqueIds.count];
183
    [self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
184
        [buddyUniqueIds enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
185
            OTRXMPPBuddy *buddy = [OTRXMPPBuddy fetchObjectWithUniqueID:obj transaction:transaction];
186
            XMPPJID *buddyJID = buddy.bareJID;
187
            if (buddyJID) {
188
                [buddyJIDs addObject:buddyJID];
189
            }
190
        }];
191
    }];
192
    // XMPPRoom.inviteUsers doesn't seem to work, so you have
193
    // to send an individual invitation for each person.
194
    [buddyJIDs enumerateObjectsUsingBlock:^(XMPPJID * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
195
        [room inviteUser:obj withMessage:nil];
196
    }];
197
}
198

    
199
#pragma - mark XMPPStreamDelegate Methods
200

    
201
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
202
    
203
    //Once we've connecected and authenticated we find what room services are available
204
    [self.mucModule discoverServices];
205
    //Once we've authenitcated we need to rejoin existing rooms
206
    NSMutableArray <OTRXMPPRoom *>*roomArray = [[NSMutableArray alloc] init];
207
    __block NSString *nickname = self.xmppStream.myJID.user;
208
    [self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
209
        OTRXMPPAccount *account = [OTRXMPPAccount accountForStream:sender transaction:transaction];
210
        if (account) {
211
            nickname = account.displayName;
212
        }
213
        [transaction enumerateKeysAndObjectsInCollection:[OTRXMPPRoom collection] usingBlock:^(NSString * _Nonnull key, id  _Nonnull object, BOOL * _Nonnull stop) {
214
            
215
            if ([object isKindOfClass:[OTRXMPPRoom class]]) {
216
                OTRXMPPRoom *room = (OTRXMPPRoom *)object;
217
                if ([room.jid length]) {
218
                    [roomArray addObject:room];
219
                }
220
            }
221
            
222
        } withFilter:^BOOL(NSString * _Nonnull key) {
223
            //OTRXMPPRoom is saved with the jid and account id as part of the key
224
            if ([key containsString:sender.tag]) {
225
                return YES;
226
            }
227
            return NO;
228
        }];
229
    }];
230
    [roomArray enumerateObjectsUsingBlock:^(OTRXMPPRoom * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
231
        [self joinRoom:[XMPPJID jidWithString:obj.jid] withNickname:nickname subject:obj.subject password:obj.roomPassword];
232
    }];
233
    
234
    [self addRoomsToBookmarks:roomArray];
235
    
236
    [self.bookmarksModule fetchBookmarks:^(NSArray<id<XMPPBookmark>> * _Nullable bookmarks, XMPPIQ * _Nullable responseIq) {
237
        
238
    } completionQueue:nil];
239
}
240

    
241
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error
242
{
243
    //Check id and mark as needs sending
244
    
245
}
246

    
247
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
248
{
249
    //Check id and mark as sent
250
}
251

    
252
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
253
{
254
    XMPPJID *from = [message from];
255
    //Check that this is a message for one of our rooms
256
    if([message isGroupChatMessageWithSubject] && [self roomForJID:from] != nil) {
257
        
258
        NSString *subject = [message subject];
259
        
260
        NSString *databaseRoomKey = [OTRXMPPRoom createUniqueId:self.xmppStream.tag jid:from.bare];
261
        
262
        [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
263
            OTRXMPPRoom *room = [OTRXMPPRoom fetchObjectWithUniqueID:databaseRoomKey transaction:transaction];
264
            room.subject = subject;
265
            [room saveWithTransaction:transaction];
266
        }];
267
        
268
    }
269
    
270
    // Handle group chat message receipts
271
    [OTRXMPPRoomMessage handleDeliveryReceiptResponseWithMessage:message writeConnection:self.databaseConnection];
272
}
273

    
274
#pragma - mark XMPPMUCDelegate Methods
275

    
276
- (void)xmppMUC:(XMPPMUC *)sender didDiscoverServices:(NSArray *)services
277
{
278
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:[services count]];
279
    [services enumerateObjectsUsingBlock:^(NSXMLElement   * _Nonnull element, NSUInteger idx, BOOL * _Nonnull stop) {
280
        NSString *jid = [element attributeStringValueForName:@"jid"];
281
        if ([jid length] && [jid containsString:@"conference"]) {
282
            [array addObject:jid];
283
            //TODO instead of just checking if it has the word 'confernce' in the name we need to preform a iq 'get' to see it's capabilities.
284
            
285
        }
286
        
287
    }];
288
    _conferenceServicesJID = array;
289
}
290

    
291
- (void)xmppMUC:(XMPPMUC *)sender roomJID:(XMPPJID *)roomJID didReceiveInvitation:(XMPPMessage *)message
292
{
293
    // We must check if we trust the person who invited us
294
    // because some servers will send you invites from anyone
295
    // We should probably move some of this code upstream into XMPPFramework
296
    
297
    // Since XMPP is super great, there are (at least) two ways to receive a room invite.
298

    
299
    // Examples from XEP-0045:
300
    // Example 124. Room Sends Invitation to New Member:
301
    //
302
    // <message from='darkcave@chat.shakespeare.lit' to='hecate@shakespeare.lit'>
303
    //   <x xmlns='http://jabber.org/protocol/muc#user'>
304
    //     <invite from='bard@shakespeare.lit'/>
305
    //     <password>cauldronburn</password>
306
    //   </x>
307
    // </message>
308
    //
309
    
310
    // Examples from XEP-0249:
311
    //
312
    //
313
    // Example 1. A direct invitation
314
    //
315
    // <message from='crone1@shakespeare.lit/desktop' to='hecate@shakespeare.lit'>
316
    //   <x xmlns='jabber:x:conference'
317
    //      jid='darkcave@macbeth.shakespeare.lit'
318
    //      password='cauldronburn'
319
    //      reason='Hey Hecate, this is the place for all good witches!'/>
320
    // </message>
321
    
322
    XMPPJID *fromJID = nil;
323
    NSString *password = nil;
324
    
325
    NSXMLElement * roomInvite = [message elementForName:@"x" xmlns:XMPPMUCUserNamespace];
326
    NSXMLElement * directInvite = [message elementForName:@"x" xmlns:@"jabber:x:conference"];
327
    if (roomInvite) {
328
        // XEP-0045
329
        NSXMLElement * invite  = [roomInvite elementForName:@"invite"];
330
        fromJID = [XMPPJID jidWithString:[invite attributeStringValueForName:@"from"]];
331
        password = [roomInvite elementForName:@"password"].stringValue;
332
    } else if (directInvite) {
333
        // XEP-0249
334
        fromJID = [message from];
335
        password = [directInvite attributeStringValueForName:@"password"];
336
    }
337
    if (!fromJID) {
338
        DDLogWarn(@"Could not parse fromJID from room invite: %@", message);
339
        return;
340
    }
341
    __block OTRXMPPBuddy *buddy = nil;
342
    XMPPStream *stream = self.xmppStream;
343
    NSString *accountUniqueId = stream.tag;
344
    __block NSString *nickname = stream.myJID.user;
345
    [self.databaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
346
        OTRXMPPAccount *account = [OTRXMPPAccount accountForStream:stream transaction:transaction];
347
        if (account) {
348
            nickname = account.displayName;
349
        }
350
        buddy = [OTRXMPPBuddy fetchBuddyWithJid:fromJID accountUniqueId:accountUniqueId transaction:transaction];
351
    }];
352
    // We were invited by someone not on our roster. Shady business!
353
    if (!buddy) {
354
        DDLogWarn(@"Received room invitation from someone not on our roster! %@ %@", fromJID, message);
355
        return;
356
    }
357
    [self joinRoom:roomJID withNickname:nickname subject:nil password:password];
358
}
359

    
360
#pragma - mark XMPPRoomDelegate Methods
361

    
362
- (void) xmppRoom:(XMPPRoom *)room didFetchMembersList:(NSArray<NSXMLElement*> *)items {
363
    DDLogInfo(@"Fetched members list: %@", items);
364
    [self xmppRoom:room addOccupantItems:items];
365
}
366

    
367
- (void)xmppRoom:(XMPPRoom *)room didFetchModeratorsList:(NSArray *)items {
368
    DDLogInfo(@"Fetched moderators list: %@", items);
369
    [self xmppRoom:room addOccupantItems:items];
370
}
371

    
372
- (void) xmppRoom:(XMPPRoom *)room addOccupantItems:(NSArray<NSXMLElement*> *)items {
373
    NSString *accountId = room.xmppStream.tag;
374
    [self.databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
375
        [items enumerateObjectsUsingBlock:^(NSXMLElement *item, NSUInteger idx, BOOL * _Nonnull stop) {
376
            NSString *jidString = [item attributeStringValueForName:@"jid"];
377
            XMPPJID *jid = [XMPPJID jidWithString:jidString];
378
            NSString *affiliation = [item attributeStringValueForName:@"affiliation"];
379
            
380
            // jid and affiliation MUST be included
381
            if (!jid || !affiliation) { return; }
382
            
383
            // Make sure occupant object exists/is created
384
            OTRXMPPRoomOccupant *occupant = [OTRXMPPRoomOccupant occupantWithJid:jid realJID:jid roomJID:room.roomJID accountId:accountId createIfNeeded:YES transaction:transaction];
385
            occupant.affiliation = [RoomOccupantAffiliationHelper affiliationWithString:affiliation];
386
            
387
            // Role MAY be included, so get that if it's there
388
            NSString *role = [item attributeStringValueForName:@"role"];
389
            if (role) {
390
                occupant.role = [RoomOccupantRoleHelper roleWithString:role];
391
            }
392
            
393
            [occupant saveWithTransaction:transaction];
394
        }];
395
    }];
396
}
397

    
398
- (void)xmppRoomDidCreate:(XMPPRoom *)sender {
399
    [self.roomsToConfigure removeObject:sender.roomJID.bare];
400
    [sender fetchConfigurationForm];
401
}
402

    
403
- (void)xmppRoom:(XMPPRoom *)sender didFetchConfigurationForm:(DDXMLElement *)configForm {
404
    [sender configureRoomUsingOptions:[[self class] defaultRoomConfiguration]];
405
}
406

    
407
- (void)xmppRoom:(XMPPRoom *)sender didConfigure:(XMPPIQ *)iqResult {
408
    //Set Room Subject
409
    NSString *subject = [self.tempRoomSubject objectForKey:sender.roomJID.bare];
410
    if (subject) {
411
        [self.tempRoomSubject removeObjectForKey:sender.roomJID.bare];
412
        [sender changeRoomSubject:subject];
413
    }
414
    
415
    //Invite buddies
416
    NSArray<NSString*> *buddyUniqueIds = [self.inviteDictionary objectForKey:sender.roomJID.bare];
417
    if (buddyUniqueIds) {
418
        [self.inviteDictionary removeObjectForKey:sender.roomJID.bare];
419
        [self inviteBuddies:buddyUniqueIds toRoom:sender];
420
    }
421

    
422
    // Fetch member list. Ideally this would be done after the invites above have been sent to the network, but the messages pass all kinds of async delegates before they are actually sent, so unfortunately we can't wait for that.
423
    [self performBlockAsync:^{
424
            [sender fetchMembersList];
425
            [sender fetchModeratorsList];
426
    }];
427
}
428

    
429
- (void)xmppRoomDidJoin:(XMPPRoom *)sender
430
{
431
    // Older prosody servers have a bug where they consider all room as already
432
    // existing, so the status 201 is never sent.
433
    if ([self.roomsToConfigure containsObject:sender.roomJID.bare]) {
434
        [self xmppRoomDidCreate:sender];
435
    } else {
436
        // Fetch member list
437
        [self performBlockAsync:^{
438
            [sender fetchMembersList];
439
            [sender fetchModeratorsList];
440
        }];
441
    }
442
}
443

    
444
- (void)xmppRoomDidLeave:(XMPPRoom *)sender {
445
    NSString *databaseRoomKey = [OTRXMPPRoom createUniqueId:self.xmppStream.tag jid:[sender.roomJID bare]];
446
    [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
447
        OTRXMPPRoom *room = [OTRXMPPRoom fetchObjectWithUniqueID:databaseRoomKey transaction:transaction];
448
        if (room) {
449
            [self clearOccupantRolesInRoom:room withTransaction:transaction];
450
        }
451
    }];
452
}
453

    
454
#pragma mark - Utility
455

    
456
- (void) removeRoomForJID:(nonnull XMPPJID*)jid {
457
    NSParameterAssert(jid != nil);
458
    if (!jid) { return; }
459
    [self performBlockAsync:^{
460
        [self.rooms removeObjectForKey:jid.bareJID];
461
    }];
462
}
463

    
464
- (void) setRoom:(nonnull XMPPRoom*)room forJID:(nonnull XMPPJID*)jid {
465
    NSParameterAssert(room != nil);
466
    NSParameterAssert(jid != nil);
467
    if (!room || !jid) {
468
        return;
469
    }
470
    [self performBlockAsync:^{
471
        [self.rooms setObject:room forKey:jid.bareJID];
472
    }];
473
}
474

    
475
- (nullable XMPPRoom*) roomForJID:(nonnull XMPPJID*)jid {
476
    NSParameterAssert(jid != nil);
477
    if (!jid) { return nil; }
478
    __block XMPPRoom *room = nil;
479
    [self performBlock:^{
480
        room = [self.rooms objectForKey:jid.bareJID];
481
    }];
482
    return room;
483
}
484

    
485
#pragma - mark Class Methods
486

    
487
+ (NSXMLElement *)defaultRoomConfiguration
488
{
489
    NSXMLElement *form = [[NSXMLElement alloc] initWithName:@"x" xmlns:@"jabber:x:data"];
490

    
491
    NSXMLElement *formTypeField = [[NSXMLElement alloc] initWithName:@"field"];
492
    [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"];
493
    [formTypeField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"http://jabber.org/protocol/muc#roomconfig"]];
494

    
495
    NSXMLElement *publicField = [[NSXMLElement alloc] initWithName:@"field"];
496
    [publicField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_publicroom"];
497
    [publicField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(0)]];
498
    
499
    NSXMLElement *persistentField = [[NSXMLElement alloc] initWithName:@"field"];
500
    [persistentField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_persistentroom"];
501
    [persistentField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(1)]];
502
    
503
    NSXMLElement *whoisField = [[NSXMLElement alloc] initWithName:@"field"];
504
    [whoisField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_whois"];
505
    [whoisField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"anyone"]];
506

    
507
    NSXMLElement *membersOnlyField = [[NSXMLElement alloc] initWithName:@"field"];
508
    [membersOnlyField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_membersonly"];
509
    [membersOnlyField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(1)]];
510

    
511
    NSXMLElement *getMemberListField = [[NSXMLElement alloc] initWithName:@"field"];
512
    [getMemberListField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_getmemberlist"];
513
    [getMemberListField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"moderator"]];
514
    [getMemberListField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"participant"]];
515

    
516
    NSXMLElement *presenceBroadcastField = [[NSXMLElement alloc] initWithName:@"field"];
517
    [presenceBroadcastField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_presencebroadcast"];
518
    [presenceBroadcastField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"moderator"]];
519
    [presenceBroadcastField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"participant"]];
520

    
521
    [form addChild:formTypeField];
522
    [form addChild:publicField];
523
    [form addChild:persistentField];
524
    [form addChild:whoisField];
525
    [form addChild:membersOnlyField];
526
    [form addChild:getMemberListField];
527
    [form addChild:presenceBroadcastField];
528
    
529
    return form;
530
}
531

    
532
@end
533

    
534
@implementation XMPPRoom(RoomManager)
535
- (void) sendRoomMessage:(OTRXMPPRoomMessage *)roomMessage {
536
    NSParameterAssert(roomMessage);
537
    if (!roomMessage) { return; }
538
    NSString *elementId = roomMessage.xmppId;
539
    if (!elementId.length) {
540
        elementId = roomMessage.uniqueId;
541
    }
542
    NSXMLElement *body = [NSXMLElement elementWithName:@"body" stringValue:roomMessage.text];
543
    // type=groupchat and to=room.full are set inside XMPPRoom.sendMessage
544
    XMPPMessage *message = [XMPPMessage messageWithType:nil elementID:roomMessage.xmppId child:body];
545
    [message addReceiptRequest];
546
    [self sendMessage:message];
547
}
548
@end