Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / Controllers / XMPP / OTRXMPPRoomManager.m @ a4bb25f6

History | View | Annotate | Download (21.2 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) initWithDatabaseConnection:(YapDatabaseConnection*)databaseConnection
38
                                capabilities:(XMPPCapabilities*)capabilities
39
                               dispatchQueue:(nullable dispatch_queue_t)dispatchQueue {
40
    if (self = [super initWithDispatchQueue:dispatchQueue]) {
41
        _databaseConnection = databaseConnection;
42
        _capabilities = capabilities;
43
        _mucModule = [[XMPPMUC alloc] init];
44
        _inviteDictionary = [[NSMutableDictionary alloc] init];
45
        _tempRoomSubject = [[NSMutableDictionary alloc] init];
46
        _roomsToConfigure = [[NSMutableArray alloc] init];
47
        _rooms = [[NSMutableDictionary alloc] init];
48
        _bookmarksModule = [[XMPPBookmarksModule alloc] initWithMode:XMPPBookmarksModePrivateXmlStorage dispatchQueue:nil];
49
    }
50
    return self;
51
}
52

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

    
68
- (void) deactivate {
69
    [self.mucModule removeDelegate:self];
70
    [self.mucModule deactivate];
71
    [self.bookmarksModule deactivate];
72
    [super deactivate];
73
}
74

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

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

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

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

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

    
198
#pragma - mark XMPPStreamDelegate Methods
199

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

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

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

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

    
273
#pragma - mark XMPPMUCDelegate Methods
274

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

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

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

    
359
#pragma - mark XMPPRoomDelegate Methods
360

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

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

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

    
414
#pragma mark - Utility
415

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

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

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

    
445
#pragma - mark Class Methods
446

    
447
+ (NSXMLElement *)defaultRoomConfiguration
448
{
449
    NSXMLElement *form = [[NSXMLElement alloc] initWithName:@"x" xmlns:@"jabber:x:data"];
450

    
451
    NSXMLElement *formTypeField = [[NSXMLElement alloc] initWithName:@"field"];
452
    [formTypeField addAttributeWithName:@"var" stringValue:@"FORM_TYPE"];
453
    [formTypeField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"http://jabber.org/protocol/muc#roomconfig"]];
454

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

    
467
    NSXMLElement *membersOnlyField = [[NSXMLElement alloc] initWithName:@"field"];
468
    [membersOnlyField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_membersonly"];
469
    [membersOnlyField addChild:[[NSXMLElement alloc] initWithName:@"value" numberValue:@(1)]];
470

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

    
476
    NSXMLElement *presenceBroadcastField = [[NSXMLElement alloc] initWithName:@"field"];
477
    [presenceBroadcastField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_presencebroadcast"];
478
    [presenceBroadcastField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"moderator"]];
479
    [presenceBroadcastField addChild:[[NSXMLElement alloc] initWithName:@"value" stringValue:@"participant"]];
480

    
481
    [form addChild:formTypeField];
482
    [form addChild:publicField];
483
    [form addChild:persistentField];
484
    [form addChild:whoisField];
485
    [form addChild:membersOnlyField];
486
    [form addChild:getMemberListField];
487
    [form addChild:presenceBroadcastField];
488
    
489
    return form;
490
}
491

    
492
@end
493

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