mirror of
https://github.com/Codeux-Software/Textual.git
synced 2026-06-16 13:24:32 +00:00
483 lines
12 KiB
Objective-C
Executable File
483 lines
12 KiB
Objective-C
Executable File
/* *********************************************************************
|
|
* _____ _ _
|
|
* |_ _|____ _| |_ _ _ __ _| |
|
|
* | |/ _ \ \/ / __| | | |/ _` | |
|
|
* | | __/> <| |_| |_| | (_| | |
|
|
* |_|\___/_/\_\\__|\__,_|\__,_|_|
|
|
*
|
|
* Copyright (c) 2008 - 2010 Satoshi Nakagawa <psychs AT limechat DOT net>
|
|
* Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors.
|
|
* Please see Acknowledgements.pdf for additional information.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of Textual, "Codeux Software, LLC", nor the
|
|
* names of its contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*
|
|
*********************************************************************** */
|
|
|
|
#import "NSObjectHelperPrivate.h"
|
|
#import "NSStringHelper.h"
|
|
#import "TXGlobalModelsPrivate.h"
|
|
#import "IRCClientPrivate.h"
|
|
#import "IRCPrefix.h"
|
|
#import "IRCMessageInternal.h"
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
@implementation IRCMessage
|
|
|
|
DESIGNATED_INITIALIZER_EXCEPTION_BODY_BEGIN
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
[self populateDefaultsPostflight];
|
|
|
|
return self;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
DESIGNATED_INITIALIZER_EXCEPTION_BODY_END
|
|
|
|
- (nullable instancetype)initWithLine:(NSString *)line
|
|
{
|
|
return [self initWithLine:line onClient:nil];
|
|
}
|
|
|
|
- (nullable instancetype)initWithLine:(NSString *)line onClient:(nullable IRCClient *)client
|
|
{
|
|
if ((self = [super init])) {
|
|
BOOL parseResult = [self parseLine:line forClient:client];
|
|
|
|
if (parseResult == NO) {
|
|
return nil;
|
|
}
|
|
|
|
[self populateDefaultsPostflight];
|
|
|
|
return self;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)populateDefaultsPostflight
|
|
{
|
|
SetVariableIfNil(self->_command, @"")
|
|
SetVariableIfNil(self->_messageTags, @{})
|
|
SetVariableIfNil(self->_params, @[])
|
|
SetVariableIfNil(self->_receivedAt, [NSDate date])
|
|
SetVariableIfNil(self->_sender, [IRCPrefix new])
|
|
}
|
|
|
|
- (NSUInteger)paramsCount
|
|
{
|
|
return self.params.count;
|
|
}
|
|
|
|
- (NSString *)paramAt:(NSUInteger)index
|
|
{
|
|
if (index < self.params.count) {
|
|
return self.params[index];
|
|
}
|
|
|
|
return @"";
|
|
}
|
|
|
|
- (NSString *)sequence
|
|
{
|
|
if (self.params.count < 2) {
|
|
return [self sequence:0];
|
|
} else {
|
|
return [self sequence:1];
|
|
}
|
|
}
|
|
|
|
- (NSString *)sequence:(NSUInteger)index
|
|
{
|
|
NSMutableString *sequence = [NSMutableString string];
|
|
|
|
NSArray *params = self.params;
|
|
|
|
NSUInteger paramsCount = params.count;
|
|
|
|
for (NSUInteger i = index; i < paramsCount; i++) {
|
|
NSString *param = params[i];
|
|
|
|
if (i != index) {
|
|
[sequence appendString:@" "];
|
|
}
|
|
|
|
[sequence appendString:param];
|
|
}
|
|
|
|
return [sequence copy];
|
|
}
|
|
|
|
- (void)markAsNotHistoric
|
|
{
|
|
self->_isHistoric = NO;
|
|
}
|
|
|
|
- (nullable NSString *)senderNickname
|
|
{
|
|
return self.sender.nickname;
|
|
}
|
|
|
|
- (nullable NSString *)senderUsername
|
|
{
|
|
return self.sender.username;
|
|
}
|
|
|
|
- (nullable NSString *)senderAddress
|
|
{
|
|
return self.sender.address;
|
|
}
|
|
|
|
- (nullable NSString *)senderHostmask
|
|
{
|
|
return self.sender.hostmask;
|
|
}
|
|
|
|
- (BOOL)senderIsServer
|
|
{
|
|
return self.sender.isServer;
|
|
}
|
|
|
|
- (void)populateDuringCopy:(__kindof XRPortablePropertyObject *)newObject mutableCopy:(BOOL)mutableCopy
|
|
{
|
|
IRCMessage *object = (IRCMessage *)newObject;
|
|
|
|
object->_batchToken = self->_batchToken;
|
|
object->_command = self->_command;
|
|
object->_commandNumeric = self->_commandNumeric;
|
|
object->_isHistoric = self->_isHistoric;
|
|
object->_isEventOnlyMessage = self->_isEventOnlyMessage;
|
|
object->_isPrintOnlyMessage = self->_isPrintOnlyMessage;
|
|
object->_messageTags = self->_messageTags;
|
|
object->_params = self->_params;
|
|
object->_receivedAt = self->_receivedAt;
|
|
object->_sender = self->_sender;
|
|
}
|
|
|
|
- (__kindof XRPortablePropertyObject *)mutableClass
|
|
{
|
|
return [IRCMessageMutable self];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation IRCMessage (IRCMessageLineParser)
|
|
|
|
- (BOOL)parseLine:(NSString *)line forClient:(nullable IRCClient *)client
|
|
{
|
|
NSParameterAssert(line != nil);
|
|
|
|
NSMutableString *lineMutable = [line mutableCopy];
|
|
|
|
/* Parse extension information (if present) */
|
|
if ([lineMutable hasPrefix:@"@"]) {
|
|
NSString *extensionInfo = lineMutable.token;
|
|
|
|
if (extensionInfo.length <= 1) {
|
|
return NO;
|
|
}
|
|
|
|
extensionInfo = [extensionInfo substringFromIndex:1];
|
|
|
|
[self parseExtensions:extensionInfo forClient:client];
|
|
}
|
|
|
|
/* Parse sender information (if present) */
|
|
if ([lineMutable hasPrefix:@":"]) {
|
|
NSString *senderInfo = lineMutable.token;
|
|
|
|
if (senderInfo.length <= 1) {
|
|
return NO;
|
|
}
|
|
|
|
senderInfo = [senderInfo substringFromIndex:1];
|
|
|
|
[self parseSender:senderInfo forClient:client];
|
|
} else {
|
|
/* If the line does not have a sender, then we use the
|
|
server address as the sender. If that isn't known, then
|
|
we use the the address the user has configured. */
|
|
/* -serverAddress is only nil when the client isn't
|
|
connected anywhere. We are parsing messages when
|
|
connected somewhere so it's safe to cast it as
|
|
as non-nil at least here. */
|
|
NSString * _Nonnull serverAddress = (NSString * _Nonnull)client.serverAddress;
|
|
|
|
IRCPrefixMutable *sender = [IRCPrefixMutable new];
|
|
|
|
sender.nickname = serverAddress;
|
|
|
|
sender.hostmask = serverAddress;
|
|
|
|
sender.isServer = YES;
|
|
|
|
self->_sender = [sender copy];
|
|
}
|
|
|
|
/* Parse command */
|
|
NSString *command = lineMutable.token;
|
|
|
|
if (command.length < 1) {
|
|
return NO;
|
|
}
|
|
|
|
if (command.isNumericOnly) {
|
|
self->_command = [command copy];
|
|
|
|
self->_commandNumeric = command.integerValue;
|
|
} else {
|
|
self->_command = [command.uppercaseString copy];
|
|
|
|
self->_commandNumeric = 0;
|
|
}
|
|
|
|
/* Parse remaining data */
|
|
NSMutableArray<NSString *> *parameters = [NSMutableArray new];
|
|
|
|
while (lineMutable.length > 0) {
|
|
if ([lineMutable hasPrefix:@":"])
|
|
{
|
|
NSString *sequence = [lineMutable substringFromIndex:1];
|
|
|
|
[parameters addObject:sequence];
|
|
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
NSString *sequence = lineMutable.token;
|
|
|
|
[parameters addObject:sequence];
|
|
}
|
|
}
|
|
|
|
self->_params = [parameters copy];
|
|
|
|
/* Return success */
|
|
return YES;
|
|
}
|
|
|
|
- (void)parseExtensions:(NSString *)extensionInfo forClient:(nullable IRCClient *)client
|
|
{
|
|
NSParameterAssert(extensionInfo != nil);
|
|
|
|
/* Chop the tags up using ; as a divider as defined by the syntax
|
|
located at: <http://ircv3.net/specs/core/message-tags-3.2.html> */
|
|
/* An example grouping would look like the following:
|
|
@aaa=bbb;ccc;example.com/ddd=eee */
|
|
NSDictionary<NSString *, NSString *> *extensions =
|
|
[extensionInfo formDataUsingSeparator:@";" decodingBlock:^NSString *(NSString *value) {
|
|
return value.decodedMessageTagString;
|
|
}];
|
|
|
|
self->_messageTags = [extensions copy];
|
|
|
|
/* If there is no client, then further processing is not possible */
|
|
if (client == nil) {
|
|
return;
|
|
}
|
|
|
|
/* Check for known capacities */
|
|
if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityServerTime]) {
|
|
/* We support two time extensions. The time= value is the date and
|
|
time in the format as defined by ISO 8601:2004(E) 4.3.2. */
|
|
/* The t= value is a legacy value in a epoch time. We always favor
|
|
the new time= format over the old. */
|
|
NSString *dateString = extensions[@"time"];
|
|
|
|
if (dateString == nil) {
|
|
dateString = extensions[@"t"];
|
|
}
|
|
|
|
if (dateString) {
|
|
NSDate *dateObject = nil;
|
|
|
|
if ([dateString onlyContainsCharactersFromCharacterSet:[NSCharacterSet ZeroToNineDecimalCharacterSet]]) {
|
|
dateObject = [NSDate dateWithTimeIntervalSince1970:dateString.doubleValue];
|
|
} else {
|
|
dateObject = [TXSharedISOStandardDateFormatter() dateFromString:dateString];
|
|
}
|
|
|
|
if (dateObject) {
|
|
self->_receivedAt = [dateObject copy];
|
|
|
|
self->_isHistoric = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ([client isCapabilityEnabled:ClientIRCv3SupportedCapabilityBatch]) {
|
|
NSString *batchToken = extensions[@"batch"];
|
|
|
|
if ([batchToken onlyContainsCharactersFromCharacterSet:[NSCharacterSet Ato9UnderscoreDash]]) {
|
|
self->_batchToken = [batchToken copy];
|
|
|
|
self->_parentBatchMessage = [client queuedBatchMessageWithToken:batchToken];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)parseSender:(NSString *)senderInfo forClient:(nullable IRCClient *)client
|
|
{
|
|
NSParameterAssert(senderInfo != nil);
|
|
|
|
IRCPrefixMutable *sender = [IRCPrefixMutable new];
|
|
|
|
NSString *senderNickname = nil;
|
|
NSString *senderUsername = nil;
|
|
NSString *senderAddress = nil;
|
|
|
|
sender.hostmask = senderInfo;// Declare entire section as host
|
|
|
|
/* Parse the user info into their appropriate sections or return NO if we can't. */
|
|
if ([senderInfo hostmaskComponents:&senderNickname username:&senderUsername address:&senderAddress onClient:client]) {
|
|
sender.nickname = senderNickname;
|
|
sender.username = senderUsername;
|
|
sender.address = senderAddress;
|
|
} else {
|
|
sender.nickname = senderInfo;
|
|
|
|
sender.isServer = YES;
|
|
}
|
|
|
|
self->_sender = [sender copy];
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation IRCMessageMutable
|
|
|
|
@dynamic batchToken;
|
|
@dynamic command;
|
|
@dynamic commandNumeric;
|
|
@dynamic isHistoric;
|
|
@dynamic isEventOnlyMessage;
|
|
@dynamic isPrintOnlyMessage;
|
|
@dynamic messageTags;
|
|
@dynamic params;
|
|
@dynamic receivedAt;
|
|
@dynamic sender;
|
|
|
|
+ (BOOL)isMutable
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (__kindof XRPortablePropertyObject *)immutableClass
|
|
{
|
|
return [IRCMessage self];
|
|
}
|
|
|
|
- (void)setBatchToken:(nullable NSString *)batchToken
|
|
{
|
|
if (self->_batchToken != batchToken) {
|
|
self->_batchToken = [batchToken copy];
|
|
}
|
|
}
|
|
|
|
- (void)setCommand:(NSString *)command
|
|
{
|
|
NSParameterAssert(command != nil);
|
|
|
|
if (self->_command != command) {
|
|
self->_command = [command copy];
|
|
}
|
|
}
|
|
|
|
- (void)setCommandNumeric:(NSUInteger)commandNumeric
|
|
{
|
|
if (self->_commandNumeric != commandNumeric) {
|
|
self->_commandNumeric = commandNumeric;
|
|
}
|
|
}
|
|
|
|
- (void)setIsHistoric:(BOOL)isHistoric
|
|
{
|
|
if (self->_isHistoric != isHistoric) {
|
|
self->_isHistoric = isHistoric;
|
|
}
|
|
}
|
|
|
|
- (void)setIsEventOnlyMessage:(BOOL)isEventOnlyMessage
|
|
{
|
|
if (self->_isEventOnlyMessage != isEventOnlyMessage) {
|
|
self->_isEventOnlyMessage = isEventOnlyMessage;
|
|
}
|
|
}
|
|
|
|
- (void)setIsPrintOnlyMessage:(BOOL)isPrintOnlyMessage
|
|
{
|
|
if (self->_isPrintOnlyMessage != isPrintOnlyMessage) {
|
|
self->_isPrintOnlyMessage = isPrintOnlyMessage;
|
|
}
|
|
}
|
|
|
|
- (void)setMessageTags:(nullable NSDictionary<NSString *, NSString *> *)messageTags
|
|
{
|
|
if (self->_messageTags != messageTags) {
|
|
self->_messageTags = [messageTags copy];
|
|
}
|
|
}
|
|
|
|
- (void)setParams:(NSArray<NSString *> *)params
|
|
{
|
|
NSParameterAssert(params != nil);
|
|
|
|
if (self->_params != params) {
|
|
self->_params = [params copy];
|
|
}
|
|
}
|
|
|
|
- (void)setReceivedAt:(NSDate *)receivedAt
|
|
{
|
|
NSParameterAssert(receivedAt != nil);
|
|
|
|
if (self->_receivedAt != receivedAt) {
|
|
self->_receivedAt = [receivedAt copy];
|
|
}
|
|
}
|
|
|
|
- (void)setSender:(IRCPrefix *)sender
|
|
{
|
|
NSParameterAssert(sender != nil);
|
|
|
|
if (self->_sender != sender) {
|
|
self->_sender = [sender copy];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|