Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / Controllers / XMPP / OTRXMPPRoomManager.m @ 80684e32

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

    
136
- (void)leaveRoom:(nonnull XMPPJID *)jid
137
{
138
    XMPPRoom *room = [self roomForJID:jid];
139
    [room leaveRoom];
140
    [self removeRoomForJID:jid];
141
    [room removeDelegate:self];
142
    [room deactivate];
143
}
144

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

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

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

    
194
#pragma - mark XMPPStreamDelegate Methods
195

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

    
236
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error
237
{
238
    //Check id and mark as needs sending
239
    
240
}
241

    
242
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
243
{
244
    //Check id and mark as sent
245
}
246

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

    
269
#pragma - mark XMPPMUCDelegate Methods
270

    
271
- (void)xmppMUC:(XMPPMUC *)sender didDiscoverServices:(NSArray *)services
272
{
273
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:[services count]];
274
    [services enumerateObjectsUsingBlock:^(NSXMLElement   * _Nonnull element, NSUInteger idx, BOOL * _Nonnull stop) {
275
        NSString *jid = [element attributeStringValueForName:@"jid"];
276
        if ([jid length] && [jid containsString:@"conference"]) {
277
            [array addObject:jid];
278
            //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.
279
            
280
        }
281
        
282
    }];
283
    _conferenceServicesJID = array;
284
}
285

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

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

    
355
#pragma - mark XMPPRoomDelegate Methods
356

    
357
- (void) xmppRoom:(XMPPRoom *)room didFetchMembersList:(NSArray<NSXMLElement*> *)items {
358
    DDLogInfo(@"Fetched members list: %@", items);
359
    NSString *accountId = room.xmppStream.tag;
360
    [self.databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
361
        [items enumerateObjectsUsingBlock:^(NSXMLElement *item, NSUInteger idx, BOOL * _Nonnull stop) {
362
            NSString *jidString = [item attributeStringValueForName:@"jid"];
363
            XMPPJID *jid = [XMPPJID jidWithString:jidString];
364
            if (!jid) { return; }
365
            // Make sure occupant object exists/is created
366
            OTRXMPPRoomOccupant *occupant = [OTRXMPPRoomOccupant occupantWithJid:jid realJID:jid roomJID:room.roomJID accountId:accountId createIfNeeded:YES transaction:transaction];
367
            [occupant saveWithTransaction:transaction];
368
        }];
369
    }];
370
}
371

    
372
- (void)xmppRoomDidJoin:(XMPPRoom *)sender
373
{
374
    [self performBlockAsync:^{
375
        //Configure room if we are the creator
376
        if ([self.roomsToConfigure containsObject:sender.roomJID.bare]) {
377
            [self.roomsToConfigure removeObject:sender.roomJID.bare];
378
            [sender configureRoomUsingOptions:[[self class] defaultRoomConfiguration]];
379
            
380
            //Set Room Subject
381
            NSString *subject = [self.tempRoomSubject objectForKey:sender.roomJID.bare];
382
            if (subject) {
383
                [self.tempRoomSubject removeObjectForKey:sender.roomJID.bare];
384
                [sender changeRoomSubject:subject];
385
            }
386
        }
387
        
388
        //Invite buddies
389
        NSArray<NSString*> *buddyUniqueIds = [self.inviteDictionary objectForKey:sender.roomJID.bare];
390
        if (buddyUniqueIds) {
391
            [self.inviteDictionary removeObjectForKey:sender.roomJID.bare];
392
            [self inviteBuddies:buddyUniqueIds toRoom:sender];
393
        }
394
        
395
        //Fetch member list
396
        [sender fetchMembersList];
397
    }];
398
}
399

    
400
- (void)xmppRoomDidLeave:(XMPPRoom *)sender {
401
    NSString *databaseRoomKey = [OTRXMPPRoom createUniqueId:self.xmppStream.tag jid:[sender.roomJID bare]];
402
    [self.databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
403
        OTRXMPPRoom *room = [OTRXMPPRoom fetchObjectWithUniqueID:databaseRoomKey transaction:transaction];
404
        if (room) {
405
            [self clearOccupantRolesInRoom:room withTransaction:transaction];
406
        }
407
    }];
408
}
409

    
410
#pragma mark - Utility
411

    
412
- (void) removeRoomForJID:(nonnull XMPPJID*)jid {
413
    NSParameterAssert(jid != nil);
414
    if (!jid) { return; }
415
    [self performBlockAsync:^{
416
        [self.rooms removeObjectForKey:jid.bareJID];
417
    }];
418
}
419

    
420
- (void) setRoom:(nonnull XMPPRoom*)room forJID:(nonnull XMPPJID*)jid {
421
    NSParameterAssert(room != nil);
422
    NSParameterAssert(jid != nil);
423
    if (!room || !jid) {
424
        return;
425
    }
426
    [self performBlockAsync:^{
427
        [self.rooms setObject:room forKey:jid.bareJID];
428
    }];
429
}
430

    
431
- (nullable XMPPRoom*) roomForJID:(nonnull XMPPJID*)jid {
432
    NSParameterAssert(jid != nil);
433
    if (!jid) { return nil; }
434
    __block XMPPRoom *room = nil;
435
    [self performBlock:^{
436
        room = [self.rooms objectForKey:jid.bareJID];
437
    }];
438
    return room;
439
}
440

    
441
#pragma - mark Class Methods
442

    
443
+ (NSXMLElement *)defaultRoomConfiguration
444
{
445
    NSXMLElement *form = [[NSXMLElement alloc] initWithName:@"x" xmlns:@"jabber:x:data"];
446

    
447
    NSXMLElement *formTypeField = [[NSXMLElement alloc] initWithName:@"field"];
448
    [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"];
449
    [formTypeField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"http://jabber.org/protocol/muc#roomconfig"]];
450

    
451
    NSXMLElement *publicField = [[NSXMLElement alloc] initWithName:@"field"];
452
    [publicField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_publicroom"];
453
    [publicField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(0)]];
454
    
455
    NSXMLElement *persistentField = [[NSXMLElement alloc] initWithName:@"field"];
456
    [persistentField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_persistentroom"];
457
    [persistentField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(1)]];
458
    
459
    NSXMLElement *whoisField = [[NSXMLElement alloc] initWithName:@"field"];
460
    [whoisField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_whois"];
461
    [whoisField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"anyone"]];
462

    
463
    NSXMLElement *membersOnlyField = [[NSXMLElement alloc] initWithName:@"field"];
464
    [membersOnlyField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_membersonly"];
465
    [membersOnlyField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(1)]];
466

    
467
    NSXMLElement *getMemberListField = [[NSXMLElement alloc] initWithName:@"field"];
468
    [getMemberListField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_getmemberlist"];
469
    [getMemberListField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"moderator"]];
470
    [getMemberListField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"participant"]];
471

    
472
    NSXMLElement *presenceBroadcastField = [[NSXMLElement alloc] initWithName:@"field"];
473
    [presenceBroadcastField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_presencebroadcast"];
474
    [presenceBroadcastField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"moderator"]];
475
    [presenceBroadcastField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"participant"]];
476

    
477
    [form addChild:formTypeField];
478
    [form addChild:publicField];
479
    [form addChild:persistentField];
480
    [form addChild:whoisField];
481
    [form addChild:membersOnlyField];
482
    [form addChild:getMemberListField];
483
    [form addChild:presenceBroadcastField];
484
    
485
    return form;
486
}
487

    
488
@end
489

    
490
@implementation XMPPRoom(RoomManager)
491
- (void) sendRoomMessage:(OTRXMPPRoomMessage *)roomMessage {
492
    NSParameterAssert(roomMessage);
493
    if (!roomMessage) { return; }
494
    NSString *elementId = roomMessage.xmppId;
495
    if (!elementId.length) {
496
        elementId = roomMessage.uniqueId;
497
    }
498
    NSXMLElement *body = [NSXMLElement elementWithName:@"body" stringValue:roomMessage.text];
499
    // type=groupchat and to=room.full are set inside XMPPRoom.sendMessage
500
    XMPPMessage *message = [XMPPMessage messageWithType:nil elementID:roomMessage.xmppId child:body];
501
    [message addReceiptRequest];
502
    [self sendMessage:message];
503
}
504
@end