Skip to content

Decentralized Instant Messaging (Objective-C SDK)

License

Notifications You must be signed in to change notification settings

dimchat/sdk-objc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Decentralized Instant Messaging (Objective-C SDK)

license Version PRs Welcome Platform

Dependencies

cd GitHub/
mkdir dimchat; cd dimchat/

git clone https://github.com/dimchat/sdk-objc.git
git clone https://github.com/dimchat/core-objc.git
git clone https://github.com/dimchat/dkd-objc.git
git clone https://github.com/dimchat/mkm-objc.git

Account

User private key, ID, meta, and visa document are generated in client, and broadcast only meta & document onto DIM station.

Register User Account

Step 1. generate private key (with asymmetric algorithm)

id<MKMPrivateKey> SK = MKMPrivateKeyGenerate(ACAlgorithmRSA);

NOTICE: After registered, the client should save the private key in secret storage.

Step 2. generate meta with private key (and meta seed)

NSString *seed = @"username";
id<MKMMeta> meta = MKMMetaGenerate(MKMMetaDefaultVersion, SK, seed);

Step 3. generate ID with meta (and network type)

NSString *terminal = nil;
id<MKMID> ID = MKMIDGenerate(meta, MKMEntityType_User, terminal);

Create and upload User Document

Step 4. create document with ID and sign with private key

id<MKMVisa> doc = MKMDocumentNew(MKMDocument_Visa, ID);
// set nickname and avatar URL
[doc setName:@"Albert Moky"];
[doc setAvatar:@"https://secure.gravatar.com/avatar/34aa0026f924d017dcc7a771f495c086"];
// sign
[doc sign:SK];

Step 5. send meta & document to station

id<MKMID> gid = MKMEveryone();
id<MKMID> sid = MKMIDParse(@"station@anywhere");
id<DKDContent> cmd = [[DIMDocumentCommand alloc] initWithID:ID meta:meta document:doc];
[cmd setGroup:gid];
[messenger sendContent:cmd sender:nil receiver:sid priority:0];

The visa document should be sent to station after connected and handshake accepted, details are provided in later chapters

Connect and Handshake

Step 1. connect to DIM station (TCP)

Step 2. prepare for receiving message data package

- (void)onReceive:(NSData *)data {
    NSData *response = [messenger onReceivePackage:data];
    if ([response length] > 0) {
        // send processing result back to the station
        [self send:response];
    }
}

Step 3. send first handshake command

(1) create handshake command

// first handshake will have no session key
NSString *session = nil;
cmd = [[DIMHandshakeCommand alloc] initWithSessionKey:session];

(2) pack, encrypt and sign

NSDate *now = nil;
id<DKDEnvelope> env = DKDEnvelopeCreate(sender, receiver, now);
id<DKDInstantMessage> iMsg = DKDInstantMessageCreate(env, cmd);
id<DKDSecureMessage>  sMsg = [messenger encryptMessage:iMsg];
id<DKDReliableMessage rMsg = [messenger signMessage:sMsg];

(3) Meta protocol

Attaching meta in the first message package is to make sure the station can find it, particularly when it's first time the user connect to this station.

if (cmd.state == DIMHandshake_Start) {
    rMsg.meta = user.meta;
}

(4) send out serialized message data package

NSData *data = [messenger serializeMessage:rMsg];
[self send:data];

Step 4. waiting handshake response

The CPU (Command Processing Units) will catch the handshake command response from station, and CPU will process them automatically, so just wait untill handshake success or network error.

Message

Content

  • Text message
id<DKDContent> content = [[DIMTextContent alloc] initWithText:@"Hey, girl!"];
  • Image message
content = [[DIMImageContent alloc] initWithImageData:data
                                            filename:@"image.png"];
  • Voice message
content = [[DIMAudioContent alloc] initWithAudioData:data
                                            filename:@"voice.mp3"];
  • Video message
content = [[DIMVideoContent alloc] initWithVideoData:data
                                            filename:@"movie.mp4"];

NOTICE: file message content (Image, Audio, Video) will be sent out only includes the filename and a URL where the file data (encrypted with the same symmetric key) be stored.

Command

  • Query meta with contact ID
id<DKDCommand> cmd = [[DIMMetaCommand alloc] initWithID:ID];
  • Query document with contact ID
id<DKDCommand> cmd = [[DIMDocumentCommand alloc] initWithID:ID];

Send command

@implementation DIMMessenger (Extension)

- (BOOL)sendCommand:(id<DKDCommand>)cmd {
    DIMStation *server = [self currentServer];
    NSAssert(server, @"server not connected yet");
    return [self sendContent:cmd receiver:server.ID];
}

@end

MetaCommand or DocumentCommand with only ID means querying, and the CPUs will catch and process all the response automatically.

Command Processing Units

You can send a customized command (such as search command) and prepare a processor to handle the response.

Search command processor

@interface DIMSearchCommandProcessor : DIMCommandProcessor

@end
NSString * const kNotificationName_SearchUsersUpdated = @"SearchUsersUpdated";

@implementation DIMSearchCommandProcessor

- (NSArray<id<DKDContent>> *)processContent:(id<DKDContent>)content
                                withMessage:(id<DKDReliableMessage>)rMsg {
    NSAssert([content isKindOfClass:[DIMSearchCommand class]], @"search command error: %@", content);
    DIMSearchCommand *command = (DIMSearchCommand *)content;
    NSString *cmd = command.cmd;
    
    NSString *notificationName;
    if ([cmd isEqualToString:DIMCommand_Search]) {
        notificationName = kNotificationName_SearchUsersUpdated;
    } else if ([cmd isEqualToString:DIMCommand_OnlineUsers]) {
        notificationName = kNotificationName_OnlineUsersUpdated;
    } else {
        NSAssert(false, @"search command error: %@", cmd);
        return nil;
    }
    
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc postNotificationName:notificationName
                      object:self
                    userInfo:content];
    // response nothing to the station
    return nil;
}

@end

Handshake command processor

@interface DIMHandshakeCommandProcessor : DIMCommandProcessor

@end
@implementation DIMHandshakeCommandProcessor

- (NSArray<id<DKDContent>> *)success {
    DIMServer *server = (DIMServer *)[self.messenger currentServer];
    [server handshakeAccepted:YES];
    return nil;
}

- (NSArray<id<DKDContent>> *)ask:(NSString *)sessionKey {
    DIMServer *server = (DIMServer *)[self.messenger currentServer];
    [server handshakeWithSession:sessionKey];
    return nil;
}

- (NSArray<id<DKDContent>> *)processContent:(id<DKDContent>)content
                                withMessage:(id<DKDReliableMessage>)rMsg {
    NSAssert([content isKindOfClass:[DIMHandshakeCommand class]], @"handshake error: %@", content);
    DIMHandshakeCommand *command = (DIMHandshakeCommand *)content;
    NSString *title = command.title;
    if ([title isEqualToString:@"DIM!"]) {
        // S -> C
        return [self success];
    } else if ([title isEqualToString:@"DIM?"]) {
        // S -> C
        return [self ask:command.sessionKey];
    } else {
        // C -> S: Hello world!
        NSAssert(false, @"handshake command error: %@", command);
        return nil;
    }
}

@end

And don't forget to register them.

@implementation DIMClientContentProcessorCreator

- (id<DIMContentProcessor>)createCommandProcessor:(NSString *)name type:(DKDContentType)msgType {
    // handshake
    if ([name isEqualToString:DIMCommand_Handshake]) {
        return CREATE_CPU(DIMHandshakeCommandProcessor);
    }
    // search
    if ([name isEqualToString:DIMCommand_Search]) {
        return CREATE_CPU(DIMSearchCommandProcessor);
    } else if ([name isEqualToString:DIMCommand_OnlineUsers]) {
        // TODO: shared the same processor with 'search'?
        return CREATE_CPU(DIMSearchCommandProcessor);
    }

    // others
    return [super createCommandProcessor:name type:msgType];
}

@end

Save instant message

Override interface saveMessage: in DIMMessenger to store instant message:

- (BOOL)saveMessage:(id<DKDInstantMessage>)iMsg {
    id<DKDContent> content = iMsg.content;
    // TODO: check message type
    //       only save normal message and group commands
    //       ignore 'Handshake', ...
    //       return true to allow responding
    
    if ([content conformsToProtocol:@protocol(DKDHandshakeCommand)]) {
        // handshake command will be processed by CPUs
        // no need to save handshake command here
        return YES;
    }
    if ([content conformsToProtocol:@protocol(DKDMetaCommand)]) {
        // meta & document command will be checked and saved by CPUs
        // no need to save meta & document command here
        return YES;
    }
    if ([content conformsToProtocol:@protocol(DKDSearchCommand)]) {
        // search result will be parsed by CPUs
        // no need to save search command here
        return YES;
    }
    
    DIMAmanuensis *clerk = [DIMAmanuensis sharedInstance];
    
    if ([content conformsToProtocol:@protocol(DKDReceiptCommand)]) {
        return [clerk saveReceipt:iMsg];
    } else {
        return [clerk saveMessage:iMsg];
    }
}

Copyright © 2018-2023 Albert Moky