1
0
mirror of https://github.com/deavmi/birchwood synced 2024-09-20 16:23:39 +02:00
birchwood/source/birchwood/client/client.d
Tristan B. Velloza Kildaire 037118afa9 Client
- Added `nick(string)` command to allow setting of nickname

Unit tests

- Moved from `client.command(Message)` to `client.nick(string)` for setting nickname
2023-03-25 13:11:58 +02:00

1172 lines
36 KiB
D

module birchwood.client.client;
import std.socket : Socket, SocketException, Address, getAddress, SocketType, ProtocolType, SocketOSException;
import std.socket : SocketFlags;
import std.conv : to;
import std.container.slist : SList;
import core.sync.mutex : Mutex;
import core.thread : Thread, dur;
import std.string;
import eventy : EventyEvent = Event, Engine, EventType, Signal;
import birchwood.config : ConnectionInfo;
import birchwood.client.exceptions : BirchwoodException, ErrorType;
import birchwood.protocol.messages : Message, encodeMessage, decodeMessage, isValidText;
import birchwood.client.receiver : ReceiverThread;
import birchwood.client.sender : SenderThread;
import birchwood.client.events;
import dlog;
package __gshared Logger logger;
__gshared static this()
{
logger = new DefaultLogger();
}
// TODO: Make abstract and for unit tests make a `DefaultClient`
// ... which logs outputs for the `onX()` handler functions
/**
* IRC client
*/
public class Client : Thread
{
/**
* Connection information
*/
package shared ConnectionInfo connInfo;
/* TODO: We should learn some info in here (or do we put it in connInfo)? */
private string serverName; //TODO: Make use of
/**
* Underlying connection to the server
*/
package Socket socket;
/**
* Receive queue meneger
*/
private ReceiverThread receiver;
/**
* Send queue manager
*/
private SenderThread sender;
/**
* Eventy event engine
*/
package Engine engine;
package bool running = false;
/**
* Constructs a new IRC client with the given configuration
* info
*
* Params:
* connInfo = the connection parameters
*/
this(ConnectionInfo connInfo)
{
super(&loop);
this.connInfo = connInfo;
/**
* Setups the receiver and sender queue managers
*/
this.receiver = new ReceiverThread(this);
this.sender = new SenderThread(this);
}
/**
* TODO: ANything worth callin on destruction?
*/
~this()
{
//TODO: Do something here, tare downs
}
/**
* Retrieve the active configuration at this
* moment
*
* Returns: the ConnectionInfo struct
*/
public ConnectionInfo getConnInfo()
{
return connInfo;
}
/**
* 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();
}
}
/**
* Requests setting of the provided nickname
*
* Params:
* nickname = the nickname to request
* Throws:
* BirchwoodException on invalid nickname
*/
public void nick(string nickname)
{
/* Ensure no illegal characters in nick name */
if(isValidText(nickname))
{
/* Set the nick */
Message nickMessage = new Message("", "NICK", nickname);
sendMessage(nickMessage);
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/**
* Joins the requested channel
*
* Params:
* channel = the channel to join
* Throws:
* BirchwoodException on invalid channel name
*/
public void joinChannel(string channel)
{
/* Ensure no illegal characters in channel name */
if(isValidText(channel))
{
/* Channel name must start with a `#` */
if(channel[0] == '#')
{
/* Join the channel */
Message joinMessage = new Message("", "JOIN", channel);
sendMessage(joinMessage);
}
else
{
throw new BirchwoodException(ErrorType.INVALID_CHANNEL_NAME);
}
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/**
* 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(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* Join multiple channels */
Message joinMessage = new Message("", "JOIN", channelLine);
sendMessage(joinMessage);
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* If no channels provided at all (error) */
else
{
throw new BirchwoodException(ErrorType.EMPTY_PARAMS);
}
}
/**
* Parts from a list of channel(s) in one go
*
* Params:
* channels = the list of channels to part from
* Throws:
* BirchwoodException if the channels list is empty
*/
public void leaveChannel(string[] channels)
{
// TODO: Add check for valid and non-empty channel names
/* If single channel */
if(channels.length == 1)
{
/* Leave the channel */
leaveChannel(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(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* Leave multiple channels */
Message leaveMessage = new Message("", "PART", channelLine);
sendMessage(leaveMessage);
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* If no channels provided at all (error) */
else
{
throw new BirchwoodException(ErrorType.EMPTY_PARAMS);
}
}
/**
* Part from a single channel
*
* Params:
* channel = the channel to leave
*/
public void leaveChannel(string channel)
{
// TODO: Add check for valid and non-empty channel names
/* Leave the channel */
Message leaveMessage = new Message("", "PART", channel);
sendMessage(leaveMessage);
}
/**
* Sends a direct message to the intended recipients
*
* Params:
* message = The message to send
* recipients = The receipients of the message
* Throws:
* BirchwoodException if the recipients list is empty
*/
public void directMessage(string message, string[] recipients)
{
/* Single recipient */
if(recipients.length == 1)
{
/* Send a direct message */
directMessage(message, recipients[0]);
}
/* Multiple recipients */
else if(recipients.length > 1)
{
/* Ensure message is valid */
if(isValidText(message))
{
string recipientLine = recipients[0];
/* Ensure valid characters in first recipient */
if(isValidText(recipientLine))
{
/* Append on a trailing `,` */
recipientLine ~= ",";
for(ulong i = 1; i < recipients.length; i++)
{
string currentRecipient = recipients[i];
/* Ensure valid characters in the current recipient */
if(isValidText(currentRecipient))
{
if(i == recipients.length-1)
{
recipientLine~=currentRecipient;
}
else
{
recipientLine~=currentRecipient~",";
}
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* Send the message */
Message privMessage = new Message("", "PRIVMSG", recipientLine~" "~message);
sendMessage(privMessage);
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* If no recipients provided at all (error) */
else
{
throw new BirchwoodException(ErrorType.EMPTY_PARAMS);
}
}
/**
* Sends a direct message to the intended recipient
*
* Params:
* message = The message to send
* recipients = The receipient of the message
*/
public void directMessage(string message, string recipient)
{
//TODO: Add check on recipient
/* Ensure the message and recipient are valid text */
if(isValidText(message) && isValidText(recipient))
{
/* Ensure the recipient does NOT start with a # (as that is reserved for channels) */
if(recipient[0] != '#')
{
/* Send the message */
Message privMessage = new Message("", "PRIVMSG", recipient~" "~message);
sendMessage(privMessage);
}
else
{
throw new BirchwoodException(ErrorType.INVALID_NICK_NAME);
}
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/**
* Sends a channel message to the intended recipients
*
* Params:
* message = The message to send
* recipients = The receipients of the message
* Throws:
* BirchwoodException if the channels list is empty
*/
public void channelMessage(string message, string[] channels)
{
/* If single channel */
if(channels.length == 1)
{
/* Send to a single channel */
channelMessage(message, channels[0]);
}
/* If multiple channels */
else if(channels.length > 1)
{
/* Ensure message is valid */
if(isValidText(message))
{
string channelLine = channels[0];
/* Ensure valid characters in first channel */
if(isValidText(channelLine))
{
/* Append on a trailing `,` */
channelLine ~= ",";
for(ulong i = 1; i < channels.length; i++)
{
string currentChannel = channels[i];
/* Ensure valid characters in current channel */
if(isValidText(currentChannel))
{
if(i == channels.length-1)
{
channelLine~=currentChannel;
}
else
{
channelLine~=currentChannel~",";
}
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* Send to multiple channels */
Message privMessage = new Message("", "PRIVMSG", channelLine~" "~message);
sendMessage(privMessage);
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
else
{
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/* If no channels provided at all (error) */
else
{
throw new BirchwoodException(ErrorType.EMPTY_PARAMS);
}
}
/**
* Sends a message to a given channel
*
* Params:
* message = The message to send
* channel = The channel to send the message to
*/
public void channelMessage(string message, string channel)
{
//TODO: Add check on recipient
//TODO: Add emptiness check
if(isValidText(message) && isValidText(channel))
{
if(channel[0] == '#')
{
/* Send the channel message */
Message privMessage = new Message("", "PRIVMSG", channel~" "~message);
sendMessage(privMessage);
}
else
{
//TODO: Invalid channel name
throw new BirchwoodException(ErrorType.INVALID_CHANNEL_NAME);
}
}
else
{
//TODO: Illegal characters
throw new BirchwoodException(ErrorType.ILLEGAL_CHARACTERS);
}
}
/**
* Issues a command to the server
*
* Params:
* message = the Message object containing the command to issue
*/
public void command(Message message)
{
/* Send the message */
sendMessage(message);
}
/**
* Initialize the event handlers
*/
private void initEvents()
{
/* TODO: For now we just register one signal type for all messages */
/* Register all event types */
engine.addEventType(new EventType(IRCEventType.GENERIC_EVENT));
engine.addEventType(new EventType(IRCEventType.PONG_EVENT));
/* Base signal with IRC client in it */
abstract class BaseSignal : Signal
{
/* ICR client */
private Client client;
this(Client client, ulong[] eventIDs)
{
super(eventIDs);
this.client = client;
}
}
/* Handles all IRC messages besides PING */
class GenericSignal : BaseSignal
{
this(Client client)
{
super(client, [IRCEventType.GENERIC_EVENT]);
}
public override void handler(EventyEvent e)
{
/* TODO: Insert cast here to our custoim type */
IRCEvent ircEvent = cast(IRCEvent)e;
assert(ircEvent); //Should never fail, unless some BOZO regged multiple handles for 1 - wait idk does eventy do that even mmm
// NOTE: Enable this when debugging
// logger.log("IRCEvent(message): "~ircEvent.getMessage().toString());
/* TODO: We should use a switch statement, imagine how nice */
Message ircMessage = ircEvent.getMessage();
string command = ircMessage.getCommand();
string params = ircMessage.getParams();
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];
/* Extract the message from params */
long firstColonIdx = indexOf(params, ":"); //TODO: validity check
string message = params[firstColonIdx+1..params.length];
/* If it starts with `#` then channel */
if(chanNick[0] == '#')
{
/* Call the channel message handler */
onChannelMessage(ircMessage, chanNick, message);
}
/* Else, direct message */
else
{
/* Call the direct message handler */
onDirectMessage(ircMessage, chanNick, message);
}
}
// 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);
}
/* Generic handler */
else
{
onGenericCommand(ircMessage);
}
//TODO: add more commands
}
}
engine.addSignalHandler(new GenericSignal(this));
/* Handles PING messages */
class PongSignal : BaseSignal
{
this(Client client)
{
super(client, [IRCEventType.PONG_EVENT]);
}
/* Send a PONG back with the received PING id */
public override void handler(EventyEvent e)
{
PongEvent pongEvent = cast(PongEvent)e;
assert(pongEvent);
// string messageToSend = "PONG "~pongEvent.getID();
Message pongMessage = new Message("", "PONG", pongEvent.getID());
client.sendMessage(pongMessage);
logger.log("Ponged back with "~pongEvent.getID());
}
}
engine.addSignalHandler(new PongSignal(this));
}
/**
* Connects to the server
*
* Throws: BirchwoodException
*/
public void connect()
{
if(socket is null)
{
try
{
/* Attempt to connect */
this.socket = new Socket(connInfo.getAddr().addressFamily(), SocketType.STREAM, ProtocolType.TCP);
this.socket.connect(connInfo.getAddr());
/* Start the event engine */
this.engine = new Engine();
/* Register default handler */
initEvents();
// /**
// * Initialize the ready events for both the
// * receive and send queue managers, then after
// * doing so start both managers and spin for
// * both of them to enter a ready state (i.e.
// * they have ensured a waiting-pipe pair for
// * libsnooze exists)
// */
/* Set the running status to true */
running = true;
/* Start the receive queue and send queue managers */
this.receiver.start();
this.sender.start();
// while(!receiver.isReady() || !sender.isReady()) {}
/* Start the socket read-decode loop */
this.start();
}
catch(SocketOSException e)
{
throw new BirchwoodException(ErrorType.CONNECT_ERROR);
}
}
// TODO: Do actual liveliness check here
else
{
throw new BirchwoodException(ErrorType.ALREADY_CONNECTED);
}
}
/**
* Adds a given message onto the receieve queue for
* later processing by the receieve queue worker thread
*
* Params:
* message = the message to enqueue to the receieve queue
*/
private void receiveQ(ubyte[] message)
{
/* Enqueue the message to the receive queue */
receiver.rq(message);
}
/**
* Sends a message to the server by enqueuing it on
* the client-side send queue.
*
* A BirchwoodException is thrown if the messages
* final length exceeds 512 bytes
*
* Params:
* message = the message to send
*/
private void sendMessage(Message message)
{
// TODO: Do message splits here
/* Encode the message */
ubyte[] encodedMessage = encodeMessage(message.encode());
/* If the message is 512 bytes or less then send */
if(encodedMessage.length <= 512)
{
/* Enqueue the message to the send queue */
sender.sq(encodedMessage);
}
/* If above then throw an exception */
else
{
throw new BirchwoodException(ErrorType.COMMAND_TOO_LONG);
}
}
/**
* Disconnect from the IRC server gracefully
*/
public void quit()
{
/* Generate the quit command using the custom quit message */
Message quitCommand = new Message("", "QUIT", connInfo.quitMessage);
sendMessage(quitCommand);
/* TODO: I don't know how long we should wait here */
Thread.sleep(dur!("seconds")(1));
/* Tare down the client */
disconnect();
}
/**
* Tare down the client by setting the run state
* to false, closing the socket, stopping the
* receieve and send handlers and the event engine
*/
private void disconnect()
{
/* Set the state of running to false */
running = false;
logger.log("disconnect() begin");
/* Close the socket */
socket.close();
logger.log("disconnect() socket closed");
// TODO: See libsnooze notes in `receiver.d` and `sender.d`, we could technically in some
// ... teribble situation have a unregistered situaion which would then have a fallthrough
// ... notify and a wait which never wakes up (the solution is mentioned in `receiver.d`/`sender.d`)
receiver.end();
sender.end();
/* Wait for receive queue manager to realise it needs to stop */
receiver.join();
logger.log("disconnect() recvQueue manager stopped");
/* Wait for the send queue manager to realise it needs to stop */
sender.join();
logger.log("disconnect() sendQueue manager stopped");
/* TODO: Stop eventy (FIXME: I don't know if this is implemented in Eventy yet, do this!) */
engine.shutdown();
logger.log("disconnect() eventy stopped");
logger.log("disconnect() end");
}
/**
* Called by the main loop thread to process the received
* and CRLF-delimited message
*
* Params:
* message = the message to add to the receive queue
*/
private void processMessage(ubyte[] message)
{
// import std.stdio;
// logger.log("Message length: "~to!(string)(message.length));
// logger.log("InterpAsString: "~cast(string)message);
receiveQ(message);
}
/**
* The main loop for the Client thread which receives data
* sent from the server
*/
private void loop()
{
/* TODO: We could do below but nah for now as we know max 512 bytes */
/* TODO: Make the read bulk size a configurable parameter */
/* TODO: Make static array allocation outside, instead of a dynamic one */
// ulong bulkReadSize = 20;
/* Fixed allocation of `bulkReadSize` for temporary data */
ubyte[] currentData;
currentData.length = connInfo.getBulkReadSize();
// malloc();
/* Total built message */
ubyte[] currentMessage;
bool hasCR = false;
/**
* Message loop
*
* FIXME: We need to find a way to tare down this socket, we don't
* want to block forever after running quit
*/
while(running)
{
/* Receieve at most 512 bytes (as per RFC) */
ptrdiff_t bytesRead = socket.receive(currentData, SocketFlags.PEEK);
import std.stdio;
// writeln(bytesRead);
// writeln(currentData);
/* FIXME: CHECK BYTES READ FOR SOCKET ERRORS! */
/* If we had a CR previously then now we need a LF */
if(hasCR)
{
/* First byte following it should be LF */
if(currentData[0] == 10)
{
/* Add to the message */
currentMessage~=currentData[0];
/* TODO: Process mesaage */
processMessage(currentMessage);
/* Reset state for next message */
currentMessage.length = 0;
hasCR=false;
/* Chop off the LF */
ubyte[] scratch;
scratch.length = 1;
this.socket.receive(scratch);
continue;
}
else
{
/* TODO: This is an error */
assert(false);
}
}
ulong pos;
for(pos = 0; pos < bytesRead; pos++)
{
/* Find first CR */
if(currentData[pos] == 13)
{
/* If we already have CR then that is an error */
if(hasCR)
{
/* TODO: Handle this */
assert(false);
}
hasCR = true;
break;
}
}
/* If we have a CR, then read up to that */
if(hasCR)
{
/* Read up to CR */
currentMessage~=currentData[0..pos+1];
/* Dequeue this (TODO: way to dispose without copy over) */
/* Guaranteed as we peeked this lenght */
ubyte[] scratch;
scratch.length = pos+1;
this.socket.receive(scratch);
continue;
}
/* Add whatever we have read to build-up */
currentMessage~=currentData[0..bytesRead];
/* TODO: Dequeue without peek after this */
ubyte[] scratch;
scratch.length = bytesRead;
this.socket.receive(scratch);
/* TODO: Yield here and in other places before continue */
}
}
version(unittest)
{
import core.thread;
}
unittest
{
/* FIXME: Get domaina name resolution support */
// ConnectionInfo connInfo = ConnectionInfo.newConnection("irc.freenode.net", 6667, "testBirchwood");
//freenode: 149.28.246.185
//snootnet: 178.62.125.123
//bonobonet: fd08:8441:e254::5
ConnectionInfo connInfo = ConnectionInfo.newConnection("worcester.community.networks.deavmi.assigned.network", 6667, "testBirchwood");
// // Set the fakelag to 1 second
// connInfo.setFakeLag(1);
// Create a new Client
Client client = new Client(connInfo);
client.connect();
// TODO: The below should all be automatic, maybe once IRCV3 is done
// ... we should automate sending in NICK and USER stuff
Thread.sleep(dur!("seconds")(2));
// client.command(new Message("", "NICK", "birchwood")); // TODO: add nickcommand
client.nick("birchwood");
Thread.sleep(dur!("seconds")(2));
client.command(new Message("", "USER", "doggie doggie irc.frdeenode.net :Tristan B. Kildaire"));
Thread.sleep(dur!("seconds")(4));
// client.command(new Message("", "JOIN", "#birchwood"));
client.joinChannel("#birchwood");
// TODO: Add a joinChannels(string[])
client.joinChannel("#birchwood2");
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
Thread.sleep(dur!("seconds")(2));
client.channelMessage("naai", "#birchwood");
Thread.sleep(dur!("seconds")(2));
client.directMessage("naai", "deavmi");
/**
* Test sending a message to a single channel (multi)
*/
client.channelMessage("This is a test message sent to a channel 1", ["#birchwood"]);
/**
* Test sending a message to a single channel (singular)
*/
client.channelMessage("This is a test message sent to a channel 2", "#birchwood");
/**
* Test sending a message to multiple channels (multi)
*/
client.channelMessage("This is a message sent to multiple channels one-shot", ["#birchwood", "#birchwood2"]);
/* TODO: Add a check here to make sure the above worked I guess? */
/* TODO: Make this end */
// while(true)
// {
// }
/**
* Test sending a message to myself (singular)
*/
client.directMessage("(1) Message to myself", "birchwood");
/**
* Test sending a message to myself (multi)
*/
client.directMessage("(2) Message to myself (multi)", ["birchwood"]);
/**
* Test sending a message to myself 2x (multi)
*/
client.directMessage("(3) Message to myself (multi)", ["birchwood", "birchwood"]);
/**
* Test formatting of text
*/
import birchwood.protocol.formatting;
string formattedTextBold = bold("Hello in bold!");
string formattedTextItalics = italics("Hello in italics!");
string formattedTextUnderline = underline("Hello in underline!");
string formattedTextMonospace = monospace("Hello in monospace!");
string formattedTextStrikthrough = strikethrough("Hello in strikethrough!");
client.channelMessage(formattedTextBold, "#birchwood");
client.channelMessage(formattedTextItalics, "#birchwood");
client.channelMessage(formattedTextUnderline, "#birchwood");
client.channelMessage(formattedTextMonospace, "#birchwood");
client.channelMessage(formattedTextStrikthrough, "#birchwood");
string combination = bold(italics("Italiano Boldino"));
client.channelMessage(combination, "#birchwood");
string foregroundRedtext = setForeground(SimpleColor.RED)~"This is red text";
client.channelMessage(foregroundRedtext, "#birchwood");
string alternatePattern = setForeground(SimpleColor.RED)~"This "~setForeground(SimpleColor.WHITE)~"is "~setForeground(SimpleColor.BLUE)~"America!";
client.channelMessage(alternatePattern, "#birchwood");
string backgroundText = setForegroundBackground(SimpleColor.RED, SimpleColor.CYAN)~"Birchwood";
client.channelMessage(backgroundText, "#birchwood");
string combined = combination~foregroundRedtext~resetForegroundBackground()~backgroundText~resetForegroundBackground()~alternatePattern;
client.channelMessage(combined, "#birchwood");
/**
* Test leaving multiple channels (multi)
*/
Thread.sleep(dur!("seconds")(2));
client.leaveChannel(["#birchwood", "#birchwood2"]);
/**
* Test leaving a single channel (singular)
*/
client.leaveChannel("#birchwoodLeave1");
/**
* Test leaving a single channel (multi)
*/
client.leaveChannel(["#birchwoodLeave2"]);
// TODO: Don't forget to re-enable this when done testing!
Thread.sleep(dur!("seconds")(15));
client.quit();
}
}