1
0
mirror of https://github.com/deavmi/birchwood synced 2024-09-20 16:23:39 +02:00

Merge branch 'deavmi:master' into master

This commit is contained in:
supremestdoggo 2023-03-17 10:20:49 -04:00 committed by GitHub
commit 622d6e509d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 604 additions and 231 deletions

View File

@ -40,8 +40,10 @@ You can take a look at the `Client` API documentation on [DUB](https://birchwood
## Compatibility ## Compatibility
- [ ] rfc1459 - [x] [rfc1459](https://www.rfc-editor.org/rfc/rfc1459)
* Should be more or less stable in supporting this standard * Supports all the numeric codes
- [x] [rfc2812](https://www.rfc-editor.org/rfc/rfc2812)
* Supports all the numeric codes
More standards will be added within the next month or so, mostly relating to new response codes that just need to be added. More standards will be added within the next month or so, mostly relating to new response codes that just need to be added.

View File

@ -71,42 +71,103 @@ public class Client : Thread
//TODO: Do something here, tare downs //TODO: Do something here, tare downs
} }
// TODO: Investigate
public ConnectionInfo getConnInfo() public ConnectionInfo getConnInfo()
{ {
return connInfo; return connInfo;
} }
/** /**
* User overridable handler functions below * Called on reception of a channel message
*
* Params:
* fullMessage = the channel message in its entirety
* channel = the channel
* msgBody = the body of the message
*/ */
public void onChannelMessage(Message fullMessage, string channel, string msgBody) public void onChannelMessage(Message fullMessage, string channel, string msgBody)
{ {
/* Default implementation */ /* Default implementation */
logger.log("Channel("~channel~"): "~msgBody); logger.log("Channel("~channel~"): "~msgBody);
} }
/**
* Called on reception of a direct message
*
* Params:
* fullMessage = the direct message in its entirety
* nickname = the sender
* msgBody = the body of the message
*/
public void onDirectMessage(Message fullMessage, string nickname, string msgBody) public void onDirectMessage(Message fullMessage, string nickname, string msgBody)
{ {
/* Default implementation */ /* Default implementation */
logger.log("DirectMessage("~nickname~"): "~msgBody); logger.log("DirectMessage("~nickname~"): "~msgBody);
} }
/**
* Called on generic commands
*
* Params:
* commandReply = the generic message
*/
public void onGenericCommand(Message message) public void onGenericCommand(Message message)
{ {
/* Default implementation */ /* Default implementation */
logger.log("Generic("~message.getCommand()~", "~message.getFrom()~"): "~message.getParams()); logger.log("Generic("~message.getCommand()~", "~message.getFrom()~"): "~message.getParams());
} }
// TODO: Hook certain ones default style with an implemenation
// ... for things that the client can learn from
// TODO: comment
/**
* Called on command replies
*
* Params:
* commandReply = the command's reply
*/
public void onCommandReply(Message commandReply) public void onCommandReply(Message commandReply)
{ {
// TODO: Add numeric response check here for CERTAIN ones which add to client
// ... state
/* Default implementation */ /* Default implementation */
logger.log("Response("~to!(string)(commandReply.getReplyType())~", "~commandReply.getFrom()~"): "~commandReply.toString()); logger.log("Response("~to!(string)(commandReply.getReplyType())~", "~commandReply.getFrom()~"): "~commandReply.toString());
import birchwood.protocol.constants : ReplyType;
if(commandReply.getReplyType() == ReplyType.RPL_BOUNCE)
{
// TODO: Testing code was here
// logger.log();
// logger.log("<<<>>>");
// logger.log("Take a look:\n\n"~commandReply.getParams());
// logger.log("And here is key-value pairs: ", commandReply.getKVPairs());
// logger.log("And here is array: ", commandReply.getPairs());
// // TODO: DLog bug, this prints nothing
// logger.log("And here is trailing: ", commandReply.getTrailing());
// import std.stdio;
// writeln("Trailer: "~commandReply.getTrailing());
// writeln(cast(ubyte[])commandReply.getTrailing());
// logger.log("<<<>>>");
// logger.log();
}
} }
/** /**
* User operations (request-response type) * User operations (request-response type)
*/ */
// TODO: Add joinChannels(strung[])
/** /**
* Joins the requested channel * Joins the requested channel
* *
@ -138,6 +199,75 @@ public class Client : Thread
} }
} }
/**
* Joins the requested channels
*
* Params:
* channels = the channels to join
* Throws:
* BirchwoodException on invalid channel name
*/
public void joinChannel(string[] channels)
{
/* If single channel */
if(channels.length == 1)
{
/* Join the channel */
joinChannel(channels[0]);
}
/* If multiple channels */
else if(channels.length > 1)
{
string channelLine = channels[0];
/* Ensure valid characters in first channel */
if(isValidText(channelLine))
{
//TODO: Add check for #
/* Append on a trailing `,` */
channelLine ~= ",";
for(ulong i = 1; i < channels.length; i++)
{
string currentChannel = channels[i];
/* Ensure the character channel is valid */
if(isValidText(currentChannel))
{
//TODO: Add check for #
if(i == channels.length-1)
{
channelLine~=currentChannel;
}
else
{
channelLine~=currentChannel~",";
}
}
else
{
throw new BirchwoodException(BirchwoodException.ErrorType.ILLEGAL_CHARACTERS);
}
}
/* Join multiple channels */
Message joinMessage = new Message("", "JOIN", channelLine);
sendMessage(joinMessage);
}
else
{
throw new BirchwoodException(BirchwoodException.ErrorType.ILLEGAL_CHARACTERS);
}
}
/* If no channels provided at all (error) */
else
{
throw new BirchwoodException(BirchwoodException.ErrorType.EMPTY_PARAMS);
}
}
/** /**
* Parts from a list of channel(s) in one go * Parts from a list of channel(s) in one go
* *
@ -500,6 +630,14 @@ public class Client : Thread
if(cmp(command, "PRIVMSG") == 0) if(cmp(command, "PRIVMSG") == 0)
{ {
// TODO: We will need a non kv pair thing as well to see (in the
// ... case of channel messages) the singular pair <channel>
// ... name.
//
// Then our message will be in `getTrailing()`
logger.debug_("PrivMessage parser (kv-pairs): ", ircMessage.getKVPairs());
logger.debug_("PrivMessage parser (trailing): ", ircMessage.getTrailing());
/* Split up into (channel/nick) and (message)*/ /* Split up into (channel/nick) and (message)*/
long firstSpaceIdx = indexOf(params, " "); //TODO: validity check; long firstSpaceIdx = indexOf(params, " "); //TODO: validity check;
string chanNick = params[0..firstSpaceIdx]; string chanNick = params[0..firstSpaceIdx];
@ -524,6 +662,9 @@ public class Client : Thread
// If the command is numeric then it is a reply of some sorts // If the command is numeric then it is a reply of some sorts
else if(ircMessage.isResponseMessage()) else if(ircMessage.isResponseMessage())
{ {
// TODO: Add numeric response check here for CERTAIN ones which add to client
// ... state
/* Call the command reply handler */ /* Call the command reply handler */
onCommandReply(ircMessage); onCommandReply(ircMessage);
} }
@ -632,25 +773,6 @@ public class Client : Thread
receiver.rq(message); receiver.rq(message);
} }
// /**
// * Sends a message to the server by enqueuing it on
// * the client-side send queue
// *
// * Params:
// * messageOut = the message to send
// */
// private void sendMessage(string messageOut)
// {
// // TODO: Do message splits here
// /* Encode the mesage */
// ubyte[] encodedMessage = encodeMessage(messageOut);
// /* Enqueue the message to the send queue */
// sender.sq(encodedMessage);
// }
/** /**
* Sends a message to the server by enqueuing it on * Sends a message to the server by enqueuing it on
* the client-side send queue. * the client-side send queue.
@ -900,9 +1022,11 @@ public class Client : Thread
client.joinChannel("#birchwood"); client.joinChannel("#birchwood");
// TODO: Add a joinChannels(string[]) // TODO: Add a joinChannels(string[])
client.joinChannel("#birchwood2"); client.joinChannel("#birchwood2");
client.joinChannel("#birchwoodLeave1");
client.joinChannel("#birchwoodLeave2"); client.joinChannel(["#birchwoodLeave1", "#birchwoodLeave2", "#birchwoodLeave3"]);
client.joinChannel("#birchwoodLeave3"); // client.joinChannel("#birchwoodLeave1");
// client.joinChannel("#birchwoodLeave2");
// client.joinChannel("#birchwoodLeave3");
Thread.sleep(dur!("seconds")(2)); Thread.sleep(dur!("seconds")(2));
client.command(new Message("", "NAMES", "")); // TODO: add names commdn client.command(new Message("", "NAMES", "")); // TODO: add names commdn
@ -978,4 +1102,6 @@ public class Client : Thread
} }
} }

View File

@ -53,6 +53,13 @@ public final class ReceiverThread : Thread
} }
// TODO: Rename to `receiveQ` // TODO: Rename to `receiveQ`
/**
* Enqueues the raw message into the receieve queue
* for eventual processing
*
* Params:
* encodedMessage = the message to enqueue
*/
public void rq(ubyte[] encodedMessage) public void rq(ubyte[] encodedMessage)
{ {
/* Lock queue */ /* Lock queue */
@ -117,29 +124,62 @@ public final class ReceiverThread : Thread
/* Lock the receieve queue */ /* Lock the receieve queue */
recvQueueLock.lock(); recvQueueLock.lock();
/* Message being analysed */ /* Parsed messages */
Message curMsg; SList!(Message) currentMessageQueue;
/* Search for a PING */ /**
ubyte[] pingMessage; * Parse all messages and save them
* into the above array
ulong pos = 0; */
foreach(ubyte[] message; recvQueue[]) foreach(ubyte[] message; recvQueue[])
{ {
// FIXME: Holy shit this is funny (see https://github.com/deavmi/birchwood/issues/13) /* Decode the message */
if(indexOf(cast(string)message, "PING") > -1) string decodedMessage = decodeMessage(message);
{
pingMessage = message;
recvQueue.linearRemoveElement(message);
import std.stdio; /* Parse the message */
writeln("\n\nHOLY SHIT\n: "~cast(string)(message)~"\n\n"); Message parsedMessage = Message.parseReceivedMessage(decodedMessage);
/* Save it */
currentMessageQueue.insertAfter(currentMessageQueue[], parsedMessage);
}
/**
* Search for any PING messages, then store it if so
* and remove it so it isn't processed again later
*/
Message pingMessage;
foreach(Message curMsg; currentMessageQueue[])
{
import std.string : cmp;
if(cmp(curMsg.getCommand(), "PING") == 0)
{
currentMessageQueue.linearRemoveElement(curMsg);
pingMessage = curMsg;
break; break;
} }
pos++;
} }
/**
* If we have a PING then respond with a PONG
*/
if(pingMessage !is null)
{
logger.log("Found a ping: "~pingMessage.toString());
/* Extract the PING ID */
string pingID = pingMessage.getParams();
/* Spawn a PONG event */
EventyEvent pongEvent = new PongEvent(pingID);
client.engine.push(pongEvent);
}
/** /**
* TODO: Plan of action * TODO: Plan of action
@ -156,62 +196,26 @@ public final class ReceiverThread : Thread
* - we can cache or remember stuff when we get 353 * - we can cache or remember stuff when we get 353
*/ */
/* If we found a PING */
if(pingMessage.length > 0)
{
/* Decode the message and parse it */
curMsg = Message.parseReceivedMessage(decodeMessage(pingMessage));
logger.log("Found a ping: "~curMsg.toString());
// string ogMessage = cast(string)pingMessage;
// long idxSigStart = indexOf(ogMessage, ":")+1;
// long idxSigEnd = lastIndexOf(ogMessage, '\r');
// string pingID = ogMessage[idxSigStart..idxSigEnd];
string pingID = curMsg.getParams();
// this.socket.send(encodeMessage("PONG "~pingID));
// string messageToSend = "PONG "~pingID;
// sendMessage(messageToSend);
// logger.log("Ponged");
/* TODO: Implement */
// TODO: Remove the Eventy push and replace with a handler call (on second thought no)
EventyEvent pongEvent = new PongEvent(pingID);
client.engine.push(pongEvent);
}
/** /**
* Process each message remaining in the queue now * Process each message remaining in the queue now
* till it is empty * till it is empty
*/ */
while(!recvQueue.empty()) while(!currentMessageQueue.empty())
{ {
ubyte[] message = recvQueue.front(); /* Get the frontmost Message */
Message curMsg = currentMessageQueue.front();
/* Decode message */
string messageNormal = decodeMessage(message);
recvQueue.linearRemoveElement(recvQueue.front());
// writeln("Normal message: "~messageNormal);
/* TODO: Parse message and call correct handler */
curMsg = Message.parseReceivedMessage(messageNormal);
// TODO: Remove the Eventy push and replace with a handler call (on second thought no) // TODO: Remove the Eventy push and replace with a handler call (on second thought no)
EventyEvent ircEvent = new IRCEvent(curMsg); EventyEvent ircEvent = new IRCEvent(curMsg);
client.engine.push(ircEvent); client.engine.push(ircEvent);
/* Remove the message from the queue */
currentMessageQueue.linearRemoveElement(curMsg);
} }
/* Clear the receive queue */
recvQueue.clear();
/* Unlock the receive queue */ /* Unlock the receive queue */
recvQueueLock.unlock(); recvQueueLock.unlock();
} }

View File

@ -48,6 +48,13 @@ public final class SenderThread : Thread
} }
// TODO: Rename to `sendQ` // TODO: Rename to `sendQ`
/**
* Enqueues the raw message into the send queue
* for eventual sending
*
* Params:
* encodedMessage = the message to enqueue
*/
public void sq(ubyte[] encodedMessage) public void sq(ubyte[] encodedMessage)
{ {
/* Lock queue */ /* Lock queue */

View File

@ -1,8 +1,15 @@
/**
* COnfiguration-related types
*/
module birchwood.config.conninfo; module birchwood.config.conninfo;
import std.socket : SocketException, Address, getAddress; import std.socket : SocketException, Address, getAddress;
import birchwood.client.exceptions : BirchwoodException; import birchwood.client.exceptions : BirchwoodException;
/**
* Represents the connection details for a server
* to connect to
*/
public struct ConnectionInfo public struct ConnectionInfo
{ {
/* Server address information */ /* Server address information */
@ -36,16 +43,32 @@ public struct ConnectionInfo
return this.bulkReadSize; return this.bulkReadSize;
} }
/**
* Get the address of the endpoint server
*
* Returns: the server's address
*/
public Address getAddr() public Address getAddr()
{ {
return addrInfo; return addrInfo;
} }
/**
* Get the chosen fake lag
*
* Returns: the fake lag in seconds
*/
public ulong getFakeLag() public ulong getFakeLag()
{ {
return fakeLag; return fakeLag;
} }
/**
* Sets the fake lag in seconds
*
* Params:
* fakeLag = the fake lag to use
*/
public void setFakeLag(ulong fakeLag) public void setFakeLag(ulong fakeLag)
{ {
this.fakeLag = fakeLag; this.fakeLag = fakeLag;

View File

@ -1,8 +1,14 @@
module birchwood.protocol.constants; module birchwood.protocol.constants;
/* Reply object */ /**
* The type of numeric response
*/
public enum ReplyType : ulong public enum ReplyType : ulong
{ {
/**
* rfc 1459
*/
/* Error replies */ /* Error replies */
ERR_NOSUCHNICK = 401, ERR_NOSUCHNICK = 401,
ERR_NOSUCHSERVER = 402, ERR_NOSUCHSERVER = 402,
@ -144,5 +150,20 @@ module birchwood.protocol.constants;
ERR_BADCHANMASK = 476, ERR_BADCHANMASK = 476,
/**
* rfc 2812
*/
RPL_WELCOME = 1,
RPL_YOURHOST = 2,
RPL_CREATED = 3,
RPL_MYINFO = 4,
RPL_BOUNCE = 5, // TODO: We care about the key-value pairs here in RPL_BOUNCE
ERR_NOCHANMODES = 477,
/**
* If no code is matched then this is the default
*/
BIRCHWOOD_UNKNOWN_RESP_CODE = 0 BIRCHWOOD_UNKNOWN_RESP_CODE = 0
} }

View File

@ -138,6 +138,9 @@ public final class Message
logger.log(e); logger.log(e);
} }
} }
/* Parse the parameters into key-value pairs (if any) and trailing text (if any) */
parameterParse();
} }
/* TODO: Implement encoder function */ /* TODO: Implement encoder function */
@ -276,6 +279,192 @@ public final class Message
return params; return params;
} }
/**
* Retrieves the trailing text in the paramaters
* (if any)
*
* Returns: the trailing text
*/
public string getTrailing()
{
return ppTrailing;
}
/**
* Returns the parameters excluding the trailing text
* which are seperated by spaces but only those
* which are key-value pairs
*
* Returns: the key-value pair parameters
*/
public string[string] getKVPairs()
{
return ppKVPairs;
}
/**
* Returns the parameters excluding the trailing text
* which are seperated by spaces
*
* Returns: the parameters
*/
public string[] getPairs()
{
return ppPairs;
}
private string ppTrailing;
private string[string] ppKVPairs;
private string[] ppPairs;
version(unittest)
{
import std.stdio;
}
unittest
{
string testInput = "A:=1 A=2 :Hello this is text";
writeln("Input: ", testInput);
bool hasTrailer;
string[] splitted = splitting(testInput, hasTrailer);
writeln("Input (split): ", splitted);
assert(cmp(splitted[0], "A:=1") == 0);
assert(cmp(splitted[1], "A=2") == 0);
/* Trailer test */
assert(hasTrailer);
assert(cmp(splitted[2], "Hello this is text") == 0);
}
unittest
{
string testInput = ":Hello this is text";
bool hasTrailer;
string[] splitted = splitting(testInput, hasTrailer);
/* Trailer test */
assert(hasTrailer);
assert(cmp(splitted[0], "Hello this is text") == 0);
}
/**
* Imagine: `A:=1 A=2 :Hello`
*
* Params:
* input =
* Returns:
*/
private static string[] splitting(string input, ref bool hasTrailer)
{
string[] splits;
bool trailingMode;
string buildUp;
for(ulong idx = 0; idx < input.length; idx++)
{
/* Get current character */
char curCHar = input[idx];
if(trailingMode)
{
buildUp ~= curCHar;
continue;
}
if(buildUp.length == 0)
{
if(curCHar == ':')
{
trailingMode = true;
continue;
}
}
if(curCHar == ' ')
{
/* Flush */
splits ~= buildUp;
buildUp = "";
}
else
{
buildUp ~= curCHar;
}
}
if(buildUp.length)
{
splits ~= buildUp;
}
hasTrailer = trailingMode;
return splits;
}
/**
* NOTE: This needs more work with trailing support
* we must make sure we only look for lastInex of `:`
* where it is first cyaracter after space but NOT within
* an active parameter
*/
private void parameterParse()
{
/* Only parse if there are params */
if(params.length)
{
/* Trailing text */
string trailing;
/* Split the `<params>` */
bool hasTrailer;
string[] paramsSplit = splitting(params, hasTrailer);
logger.debug_("ParamsSPlit direct:", paramsSplit);
/* Extract the trailer as the last item in the array (if it exists) */
if(hasTrailer)
{
trailing = paramsSplit[paramsSplit.length-1];
/* Remove it from the parameters */
paramsSplit = paramsSplit[0..$-1];
logger.debug_("GOt railer ", trailing);
}
ppPairs = paramsSplit;
/* Generate the key-value pairs */
foreach(string pair; paramsSplit)
{
/* Only do this if we have an `=` in the current pair */
if(indexOf(pair, "=") > -1)
{
string key = split(pair, "=")[0];
string value = split(pair, "=")[1];
ppKVPairs[key] = value;
}
}
/* Save the trailing */
ppTrailing = trailing;
logger.debug_("ppTrailing: ", ppTrailing);
}
}
/** /**
* Returns whether or not this message was * Returns whether or not this message was
* a numeric response * a numeric response

View File

@ -1,6 +1,7 @@
module birchwood.protocol; module birchwood.protocol;
public import birchwood.protocol.messages : Message; public import birchwood.protocol.messages : Message;
public import birchwood.protocol.constants : ReplyType;
// TODO: Look how to neaten up (if any) // TODO: Look how to neaten up (if any)
public import birchwood.protocol.formatting; public import birchwood.protocol.formatting;