1
0
mirror of https://github.com/deavmi/birchwood synced 2024-09-20 14:43:23 +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
- [ ] rfc1459
* Should be more or less stable in supporting this standard
- [x] [rfc1459](https://www.rfc-editor.org/rfc/rfc1459)
* 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.

View File

@ -71,42 +71,103 @@ public class Client : Thread
//TODO: Do something here, tare downs
}
// TODO: Investigate
public ConnectionInfo getConnInfo()
{
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)
{
/* Default implementation */
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)
{
/* Default implementation */
logger.log("DirectMessage("~nickname~"): "~msgBody);
}
/**
* Called on generic commands
*
* Params:
* commandReply = the generic message
*/
public void onGenericCommand(Message message)
{
/* Default implementation */
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)
{
// TODO: Add numeric response check here for CERTAIN ones which add to client
// ... state
/* Default implementation */
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)
*/
// TODO: Add joinChannels(strung[])
/**
* 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
*
@ -500,6 +630,14 @@ public class Client : Thread
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)*/
long firstSpaceIdx = indexOf(params, " "); //TODO: validity check;
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
else if(ircMessage.isResponseMessage())
{
// TODO: Add numeric response check here for CERTAIN ones which add to client
// ... state
/* Call the command reply handler */
onCommandReply(ircMessage);
}
@ -632,25 +773,6 @@ public class Client : Thread
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
* the client-side send queue.
@ -900,9 +1022,11 @@ public class Client : Thread
client.joinChannel("#birchwood");
// TODO: Add a joinChannels(string[])
client.joinChannel("#birchwood2");
client.joinChannel("#birchwoodLeave1");
client.joinChannel("#birchwoodLeave2");
client.joinChannel("#birchwoodLeave3");
client.joinChannel(["#birchwoodLeave1", "#birchwoodLeave2", "#birchwoodLeave3"]);
// client.joinChannel("#birchwoodLeave1");
// client.joinChannel("#birchwoodLeave2");
// client.joinChannel("#birchwoodLeave3");
Thread.sleep(dur!("seconds")(2));
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`
/**
* Enqueues the raw message into the receieve queue
* for eventual processing
*
* Params:
* encodedMessage = the message to enqueue
*/
public void rq(ubyte[] encodedMessage)
{
/* Lock queue */
@ -117,29 +124,62 @@ public final class ReceiverThread : Thread
/* Lock the receieve queue */
recvQueueLock.lock();
/* Message being analysed */
Message curMsg;
/* Parsed messages */
SList!(Message) currentMessageQueue;
/* Search for a PING */
ubyte[] pingMessage;
ulong pos = 0;
/**
* Parse all messages and save them
* into the above array
*/
foreach(ubyte[] message; recvQueue[])
{
// FIXME: Holy shit this is funny (see https://github.com/deavmi/birchwood/issues/13)
if(indexOf(cast(string)message, "PING") > -1)
{
pingMessage = message;
recvQueue.linearRemoveElement(message);
/* Decode the message */
string decodedMessage = decodeMessage(message);
import std.stdio;
writeln("\n\nHOLY SHIT\n: "~cast(string)(message)~"\n\n");
/* Parse the message */
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;
}
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
@ -156,62 +196,26 @@ public final class ReceiverThread : Thread
* - 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
* till it is empty
*/
while(!recvQueue.empty())
while(!currentMessageQueue.empty())
{
ubyte[] message = recvQueue.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);
/* Get the frontmost Message */
Message curMsg = currentMessageQueue.front();
// TODO: Remove the Eventy push and replace with a handler call (on second thought no)
EventyEvent ircEvent = new IRCEvent(curMsg);
client.engine.push(ircEvent);
/* Remove the message from the queue */
currentMessageQueue.linearRemoveElement(curMsg);
}
/* Clear the receive queue */
recvQueue.clear();
/* Unlock the receive queue */
recvQueueLock.unlock();
}

View File

@ -48,6 +48,13 @@ public final class SenderThread : Thread
}
// 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)
{
/* Lock queue */

View File

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

View File

@ -1,8 +1,14 @@
module birchwood.protocol.constants;
/* Reply object */
public enum ReplyType : ulong
{
/**
* The type of numeric response
*/
public enum ReplyType : ulong
{
/**
* rfc 1459
*/
/* Error replies */
ERR_NOSUCHNICK = 401,
ERR_NOSUCHSERVER = 402,
@ -144,5 +150,20 @@ module birchwood.protocol.constants;
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
}

View File

@ -138,6 +138,9 @@ public final class Message
logger.log(e);
}
}
/* Parse the parameters into key-value pairs (if any) and trailing text (if any) */
parameterParse();
}
/* TODO: Implement encoder function */
@ -276,6 +279,192 @@ public final class Message
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
* a numeric response

View File

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