/*
* Copyright (c) 2010, 2011, 2012, 2013, 2016, 2017, 2018, 2021, 2024
* Jonathan Schleifer <js@nil.im>
*
* https://fossil.nil.im/objirc
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice is present in all copies.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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.
*/
#define IRC_CONNECTION_M
#include <stdarg.h>
#import <ObjFW/ObjFW.h>
#import "IRCConnection.h"
#import "IRCUser.h"
@interface IRCConnection () <OFTCPSocketDelegate>
@end
@implementation IRCConnection
@synthesize socketClass = _socketClass;
@synthesize server = _server, port = _port;
@synthesize nickname = _nickname, username = _username, realname = _realname;
@synthesize delegate = _delegate, socket = _socket;
@synthesize fallbackEncoding = _fallbackEncoding;
@synthesize pingInterval = _pingInterval, pingTimeout = _pingTimeout;
+ (instancetype)connection
{
return [[[self alloc] init] autorelease];
}
- (instancetype)init
{
self = [super init];
@try {
_socketClass = [OFTCPSocket class];
_channels = [[OFMutableDictionary alloc] init];
_port = 6667;
_fallbackEncoding = OFStringEncodingISO8859_1;
_pingInterval = 120;
_pingTimeout = 30;
} @catch (id e) {
[self release];
@throw e;
}
return self;
}
- (void)dealloc
{
[_socket release];
[_server release];
[_nickname release];
[_username release];
[_realname release];
[_channels release];
[_pingData release];
[_pingTimer release];
[super dealloc];
}
- (void)connect
{
void *pool = objc_autoreleasePoolPush();
if (_socket != nil)
@throw [OFAlreadyOpenException exceptionWithObject: self];
_socket = [[_socketClass alloc] init];
[_socket setDelegate: self];
[_socket asyncConnectToHost: _server port: _port];
objc_autoreleasePoolPop(pool);
}
- (void)socket: (OF_KINDOF(OFTCPSocket *))socket
didConnectToHost: (OFString *)host
port: (uint16_t)port
exception: (id)exception
{
if (exception != nil) {
if ([_delegate respondsToSelector:
@selector(connection:didFailToConnectWithException:)])
[_delegate connection: self
didFailToConnectWithException: exception];
return;
}
if ([_delegate respondsToSelector:
@selector(connection:didCreateSocket:)])
[_delegate connection: self didCreateSocket: _socket];
[self sendLineWithFormat: @"NICK %@", _nickname];
[self sendLineWithFormat: @"USER %@ * 0 :%@", _username, _realname];
[socket asyncReadLine];
}
- (void)disconnect
{
[self disconnectWithReason: nil];
}
- (void)disconnectWithReason: (OFString *)reason
{
void *pool = objc_autoreleasePoolPush();
reason = [[reason componentsSeparatedByString: @"\n"] firstObject];
if (reason == nil)
[self sendLine: @"QUIT"];
else
[self sendLineWithFormat: @"QUIT :%@", reason];
objc_autoreleasePoolPop(pool);
}
- (void)joinChannel: (OFString *)channel
{
void *pool = objc_autoreleasePoolPush();
channel = [channel componentsSeparatedByString: @"\n"].firstObject;
[self sendLineWithFormat: @"JOIN %@", channel];
objc_autoreleasePoolPop(pool);
}
- (void)leaveChannel: (OFString *)channel
{
[self leaveChannel: channel reason: nil];
}
- (void)leaveChannel: (OFString *)channel reason: (OFString *)reason
{
void *pool = objc_autoreleasePoolPush();
channel = [channel componentsSeparatedByString: @"\n"].firstObject;
reason = [reason componentsSeparatedByString: @"\n"].firstObject;
if (reason == nil)
[self sendLineWithFormat: @"PART %@", channel];
else
[self sendLineWithFormat: @"PART %@ :%@", channel, reason];
[_channels removeObjectForKey: channel];
objc_autoreleasePoolPop(pool);
}
- (void)sendLine: (OFString *)line
{
if ([_delegate respondsToSelector: @selector(connection:didSendLine:)])
[_delegate connection: self didSendLine: line];
[_socket writeLine: line];
}
- (void)sendLineWithFormat: (OFConstantString *)format, ...
{
void *pool = objc_autoreleasePoolPush();
OFString *line;
va_list args;
va_start(args, format);
line = [[[OFString alloc] initWithFormat: format
arguments: args] autorelease];
va_end(args);
[self sendLine: line];
objc_autoreleasePoolPop(pool);
}
- (void)sendMessage: (OFString *)message to: (OFString *)to
{
void *pool = objc_autoreleasePoolPush();
for (OFString *line in [message componentsSeparatedByString: @"\n"])
[self sendLineWithFormat: @"PRIVMSG %@ :%@", to, line];
objc_autoreleasePoolPop(pool);
}
- (void)sendNotice: (OFString *)notice to: (OFString *)to
{
void *pool = objc_autoreleasePoolPush();
for (OFString *line in [notice componentsSeparatedByString: @"\n"])
[self sendLineWithFormat: @"NOTICE %@ :%@", to, line];
objc_autoreleasePoolPop(pool);
}
- (void)kickUser: (OFString *)user
channel: (OFString *)channel
reason: (OFString *)reason
{
void *pool = objc_autoreleasePoolPush();
reason = [[reason componentsSeparatedByString: @"\n"] firstObject];
[self sendLineWithFormat: @"KICK %@ %@ :%@", channel, user, reason];
objc_autoreleasePoolPop(pool);
}
- (void)changeNicknameTo: (OFString *)nickname
{
void *pool = objc_autoreleasePoolPush();
nickname = [nickname componentsSeparatedByString: @"\n"].firstObject;
[self sendLineWithFormat: @"NICK %@", nickname];
objc_autoreleasePoolPop(pool);
}
- (void)irc_processLine: (OFString *)line
{
OFArray *components;
OFString *action = nil;
if ([_delegate respondsToSelector:
@selector(connection:didReceiveLine:)])
[_delegate connection: self didReceiveLine: line];
components = [line componentsSeparatedByString: @" "];
/* PING */
if (components.count == 2 &&
[components.firstObject isEqual: @"PING"]) {
OFMutableString *s = [[line mutableCopy] autorelease];
[s replaceCharactersInRange: OFMakeRange(0, 4)
withString: @"PONG"];
[self sendLine: s];
return;
}
/* PONG */
if (components.count == 4 &&
[[components objectAtIndex: 1] isEqual: @"PONG"] &&
[[components objectAtIndex: 3] isEqual: _pingData]) {
[_pingTimer invalidate];
[_pingData release];
[_pingTimer release];
_pingData = nil;
_pingTimer = nil;
}
action = [[components objectAtIndex: 1] uppercaseString];
/* Connected */
if ([action isEqual: @"001"] && components.count >= 4) {
if ([_delegate respondsToSelector:
@selector(connectionWasEstablished:)])
[_delegate connectionWasEstablished: self];
[OFTimer scheduledTimerWithTimeInterval: _pingInterval
target: self
selector: @selector(irc_sendPing)
repeats: true];
return;
}
/* JOIN */
if ([action isEqual: @"JOIN"] && components.count == 3) {
OFString *who = [components objectAtIndex: 0];
OFString *where = [components objectAtIndex: 2];
IRCUser *user;
OFMutableSet *channel;
who = [who substringFromIndex: 1];
user = [IRCUser userWithString: who];
if ([who hasPrefix:
[_nickname stringByAppendingString: @"!"]]) {
channel = [OFMutableSet set];
[_channels setObject: channel forKey: where];
} else
channel = [_channels objectForKey: where];
[channel addObject: user.nickname];
if ([_delegate respondsToSelector:
@selector(connection:didSeeUser:joinChannel:)])
[_delegate connection: self
didSeeUser: user
joinChannel: where];
return;
}
/* NAMES reply */
if ([action isEqual: @"353"] && components.count >= 6) {
OFString *where;
OFMutableSet *channel;
OFArray *users;
size_t pos;
where = [components objectAtIndex: 4];
if ((channel = [_channels objectForKey: where]) == nil) {
/* We did not request that */
return;
}
pos = [[components objectAtIndex: 0] length] +
[[components objectAtIndex: 1] length] +
[[components objectAtIndex: 2] length] +
[[components objectAtIndex: 3] length] +
[[components objectAtIndex: 4] length] + 6;
users = [[line substringWithRange:
OFMakeRange(pos, line.length - pos)]
componentsSeparatedByString: @" "];
for (OFString *user in users) {
if ([user hasPrefix: @"@"] || [user hasPrefix: @"+"] ||
[user hasPrefix: @"%"] || [user hasPrefix: @"*"])
user = [user substringFromIndex: 1];
[channel addObject: user];
}
if ([_delegate respondsToSelector:
@selector(connection:didReceiveNamesForChannel:)])
[_delegate connection: self
didReceiveNamesForChannel: where];
return;
}
/* PART */
if ([action isEqual: @"PART"] && [components count] >= 3) {
OFString *who = [components objectAtIndex: 0];
OFString *where = [components objectAtIndex: 2];
IRCUser *user;
OFMutableSet *channel;
OFString *reason = nil;
size_t pos = who.length + 1 +
[[components objectAtIndex: 1] length] + 1 + where.length;
who = [who substringFromIndex: 1];
user = [IRCUser userWithString: who];
channel = [_channels objectForKey: where];
if (components.count > 3)
reason = [line substringFromIndex: pos + 2];
[channel removeObject: user.nickname];
if ([_delegate respondsToSelector:
@selector(connection:didSeeUser:leaveChannel:reason:)])
[_delegate connection: self
didSeeUser: user
leaveChannel: where
reason: reason];
return;
}
/* KICK */
if ([action isEqual: @"KICK"] && components.count >= 4) {
OFString *who = [components objectAtIndex: 0];
OFString *where = [components objectAtIndex: 2];
OFString *whom = [components objectAtIndex: 3];
IRCUser *user;
OFMutableSet *channel;
OFString *reason = nil;
size_t pos = who.length + 1 +
[[components objectAtIndex: 1] length] + 1 +
where.length + 1 + whom.length;
who = [who substringFromIndex: 1];
user = [IRCUser userWithString: who];
channel = [_channels objectForKey: where];
if (components.count > 4)
reason = [line substringFromIndex: pos + 2];
[channel removeObject: user.nickname];
if ([_delegate respondsToSelector:
@selector(connection:didSeeUser:kickUser:channel:reason:)])
[_delegate connection: self
didSeeUser: user
kickUser: whom
channel: where
reason: reason];
return;
}
/* QUIT */
if ([action isEqual: @"QUIT"] && components.count >= 2) {
OFString *who = [components objectAtIndex: 0];
IRCUser *user;
OFString *reason = nil;
size_t pos = who.length + 1 +
[[components objectAtIndex: 1] length];
who = [who substringFromIndex: 1];
user = [IRCUser userWithString: who];
if ([components count] > 2)
reason = [line substringFromIndex: pos + 2];
for (OFString *channel in _channels)
[[_channels objectForKey: channel]
removeObject: user.nickname];
if ([_delegate respondsToSelector:
@selector(connection:didSeeUserQuit:reason:)])
[_delegate connection: self
didSeeUserQuit: user
reason: reason];
return;
}
/* NICK */
if ([action isEqual: @"NICK"] && components.count == 3) {
OFString *who = [components objectAtIndex: 0];
OFString *nickname = [components objectAtIndex: 2];
IRCUser *user;
who = [who substringFromIndex: 1];
nickname = [nickname substringFromIndex: 1];
user = [IRCUser userWithString: who];
if ([user.nickname isEqual: _nickname]) {
[_nickname release];
_nickname = [nickname copy];
}
for (OFMutableSet *channel in _channels) {
if ([channel containsObject: user.nickname]) {
[channel removeObject: user.nickname];
[channel addObject: nickname];
}
}
if ([_delegate respondsToSelector:
@selector(connection:didSeeUser:changeNicknameTo:)])
[_delegate connection: self
didSeeUser: user
changeNicknameTo: nickname];
return;
}
/* PRIVMSG */
if ([action isEqual: @"PRIVMSG"] && components.count >= 4) {
OFString *from = [components objectAtIndex: 0];
OFString *to = [components objectAtIndex: 2];
IRCUser *user;
OFString *message;
size_t pos = from.length + 1 +
[[components objectAtIndex: 1] length] + 1 + to.length;
from = [from substringFromIndex: 1];
message = [line substringFromIndex: pos + 2];
user = [IRCUser userWithString: from];
if (![to isEqual: _nickname]) {
if ([_delegate respondsToSelector: @selector(connection:
didReceiveMessage:channel:user:)])
[_delegate connection: self
didReceiveMessage: message
channel: to
user: user];
} else {
if ([_delegate respondsToSelector: @selector(connection:
didReceivePrivateMessage:user:)])
[_delegate connection: self
didReceivePrivateMessage: message
user: user];
}
return;
}
/* NOTICE */
if ([action isEqual: @"NOTICE"] && components.count >= 4) {
OFString *from = [components objectAtIndex: 0];
OFString *to = [components objectAtIndex: 2];
IRCUser *user = nil;
OFString *notice;
size_t pos = from.length + 1 +
[[components objectAtIndex: 1] length] + 1 + to.length;
from = [from substringFromIndex: 1];
notice = [line substringFromIndex: pos + 2];
if (![from containsString: @"!"] || [to isEqual: @"*"]) {
/* System message - ignore for now */
return;
}
user = [IRCUser userWithString: from];
if (![to isEqual: _nickname]) {
if ([_delegate respondsToSelector: @selector(connection:
didReceiveNotice:channel:user:)])
[_delegate connection: self
didReceiveNotice: notice
channel: to
user: user];
} else {
if ([_delegate respondsToSelector:
@selector(connection:didReceiveNotice:user:)])
[_delegate connection: self
didReceiveNotice: notice
user: user];
}
return;
}
}
- (void)irc_sendPing
{
[_pingData release];
[_pingTimer release];
_pingData = nil;
_pingTimer = nil;
_pingData = [[OFString alloc] initWithFormat: @":%d", rand()];
[_socket writeFormat: @"PING %@\r\n", _pingData];
_pingTimer = [[OFTimer
scheduledTimerWithTimeInterval: _pingTimeout
target: self
selector: @selector(irc_pingTimeout)
repeats: false] retain];
}
- (void)irc_pingTimeout
{
if ([_delegate respondsToSelector: @selector(connectionWasClosed:)])
[_delegate connectionWasClosed: self];
[_socket cancelAsyncRequests];
[_socket release];
_socket = nil;
}
- (bool)stream: (OF_KINDOF(OFStream *))stream
didReadLine: (OFString *)line
exception: (OFException *)exception
{
if (line != nil) {
[self irc_processLine: line];
if (_fallbackEncodingUsed) {
_fallbackEncodingUsed = false;
[stream asyncReadLine];
return false;
}
return true;
}
if ([exception isKindOfClass: [OFInvalidEncodingException class]]) {
_fallbackEncodingUsed = true;
[stream asyncReadLineWithEncoding: _fallbackEncoding];
return false;
}
if ([_delegate respondsToSelector: @selector(connectionWasClosed:)])
[_delegate connectionWasClosed: self];
[_pingTimer invalidate];
[_socket performSelector: @selector(cancelAsyncRequests) afterDelay: 0];
[_socket release];
_socket = nil;
return false;
}
- (OFSet OF_GENERIC(OFString *) *)usersInChannel: (OFString *)channel
{
return [[[_channels objectForKey: channel] copy] autorelease];
}
@end