Skip to content
Snippets Groups Projects
DDMsgReader.js 785 KiB
Newer Older

	// New MsgBase method: vote_msg(). it takes a message header object
	// (like save_msg), except you only need a few properties, in order of
	// importarnce:
	// attr: you need to have this set to MSG_UPVOTE, MSG_DOWNVOTE, or MSG_VOTE
	// thread_back or reply_id: either of these must be set to indicate msg to vote on
	// from: name of voter
	// from_net_type and from_net_addr: if applicable

	// Do some initial setup of the header for the vote message to be
	// saved to the messagebase
	var voteMsgHdr = new Object();
	voteMsgHdr.thread_back = pMsgHdr.number;
	voteMsgHdr.from = (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias;
	if (pMsgHdr.from.hasOwnProperty("from_net_type"))
	{
		voteMsgHdr.from_net_type = pMsgHdr.from_net_type;
		if (pMsgHdr.from_net_type != NET_NONE)
			voteMsgHdr.from_net_addr = user.email;
	}

	// Input vote options from the user differently depending on whether
	// the message is a poll or not
	if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL)
	{
		if (pMsgHdr.hasOwnProperty("field_list"))
		{
			console.clear("\1n");
			var selectHdr = bbs.text(typeof(BallotHdr) != "undefined" ? BallotHdr : 791);
			printf("\1n" + selectHdr + "\1n", pMsgHdr.subject);
			var optionFormatStr = "\1n\1c\1h%2d\1n\1c: \1h%s\1n";
			var optionNum = 1;
			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
			{
				if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
				{
					printf(optionFormatStr, optionNum++, pMsgHdr.field_list[fieldI].data);
					console.crlf();
				}
			}
			console.crlf();
			// Get & process the selection from the user
			var userInputNum = 0;
			if (pMsgHdr.votes > 1)
			{
				// Support multiple answers from the user
				console.print("\1n\1gYour vote numbers, separated by commas, up to \1h" + pMsgHdr.votes + "\1n\1g (Blank/Q=Quit):");
				var userInput = consoleGetStrWithValidKeys("0123456789,Q", null, false);
				if ((userInput.length > 0) && (userInput.toUpperCase() != "Q"))
				{
					var userAnswers = userInput.split(",");
					if (userAnswers.length > 0)
					{
						// Generate confirmation text and an array of numbers
						// representing the user's choices, up to the number
						// of responses allowed
						var confirmText = "Vote ";
						var voteNumbers = [];
						for (var i = 0; (i < userAnswers.length) && (i < pMsgHdr.votes); ++i)
						{
							// Trim any whitespace from the user's response
							userAnswers[i] = trimSpaces(userAnswers[i], true, true, true);
							if (/^[0-9]+$/.test(userAnswers[i]))
							{
								voteNumbers.push(+userAnswers[i]);
								confirmText += userAnswers[i] + ",";
							}
						}
						// If the confirmation text has a trailing comma, remove it
						if (/,$/.test(confirmText))
							confirmText = confirmText.substr(0, confirmText.length-1);
						// Confirm from the user and submit their vote if they say yes
						if (voteNumbers.length > 0)
						{
							if (console.yesno(confirmText))
							{
								userInputNum = 0;
								for (var i = 0; i < voteNumbers.length; ++i)
									userInputNum |= (1 << (voteNumbers[i]-1));
							}
							else
								retObj.userQuit = true;
						}
					}
				}
				else
					retObj.userQuit = true;
			}
				// Get the selection prompt text from text.dat and replace the %u or %d with
				// the number 1 (default option)
				var selectPromptText = bbs.text(SelectItemWhich);
				selectPromptText = selectPromptText.replace(/%[uU]/, 1).replace(/%[dD]/, 1);
				console.mnemonics(selectPromptText);
				var maxNum = optionNum - 1;
				userInputNum = console.getnum(maxNum);
				if (userInputNum == -1) // The user chose Q to quit
					retObj.userQuit = true;
				console.print("\1n");
			}
			if (!retObj.userQuit)
			{
		// The message is not a poll - Prompt for up/downvote
		if ((typeof(MSG_UPVOTE) != "undefined") && (typeof(MSG_DOWNVOTE) != "undefined"))
		{
			var voteAttr = 0;
			// Get text line 783 to prompt for voting
			var textDatText = bbs.text(typeof(VoteMsgUpDownOrQuit) != "undefined" ? VoteMsgUpDownOrQuit : 783);
			if (removeNLsFromVoteText)
				textDatText = textDatText.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
			console.print("\1n");
			console.mnemonics(textDatText);
			console.print("\1n");
			// Using getAllowedKeyWithMode() instead of console.getkeys() so we
			// can control the input mode better, so it doesn't output a CRLF
			switch (getAllowedKeyWithMode("UDQ" + KEY_UP + KEY_DOWN, K_NOCRLF|K_NOSPIN))
					voteAttr = MSG_DOWNVOTE;
					break;
				case "Q":
				default:
					retObj.userQuit = true;
					break;
			}
			// If the user voted, then save the user's vote in the attr property
			// in the header
			if (voteAttr != 0)
				voteMsgHdr.attr = voteAttr;
		}
		else
			retObj.errorMsg = "MSG_UPVOTE & MSG_DOWNVOTE are not defined";
	}

	// If the user hasn't quit and there is no error message, then save the vote
	// message header
	if (!retObj.userQuit && (retObj.errorMsg.length == 0))
	{
		console.print("\1n  Submitting.."); // TODO: Does this look good?
		retObj.savedVote = msgbase.vote_msg(voteMsgHdr);
		// If the save was successful, then update
		// this.hdrsForCurrentSubBoard with the updated
		// message header (for the message that was read)
		if (retObj.savedVote)
		{
			if (this.hdrsForCurrentSubBoardByMsgNum.hasOwnProperty(pMsgHdr.number))
			{
				var originalMsgIdx = this.hdrsForCurrentSubBoardByMsgNum[pMsgHdr.number];
				if (typeof(msgbase.get_all_msg_headers) === "function")
					var tmpHdrs = msgbase.get_all_msg_headers();
					if (tmpHdrs.hasOwnProperty(pMsgHdr.number))
						this.hdrsForCurrentSubBoard[originalMsgIdx] = tmpHdrs[pMsgHdr.number];
						retObj.updatedHdr = pMsgHdr;
						if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("total_votes"))
							retObj.updatedHdr.total_votes = this.hdrsForCurrentSubBoard[originalMsgIdx].total_votes;
						if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("upvotes"))
							retObj.updatedHdr.upvotes = this.hdrsForCurrentSubBoard[originalMsgIdx].upvotes;
						if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("tally"))
							retObj.updatedHdr.tally = this.hdrsForCurrentSubBoard[originalMsgIdx].tally;
		{
			// Failed to save the vote
			retObj.errorMsg = "Failed to save your vote";
		}
// For the DigDistMsgReader class: Checks to see whether a user has voted on a message.
// The message must belong to the currently-open sub-board.
//
// Parameters:
//  pMsgNum: The message number
//  pUser: Optional - A user account to check.  If omitted, the current logged-in
//         user will be used.
function DigDistMsgReader_HasUserVotedOnMsg(pMsgNum, pUser)
	// Don't do this for personal email
	if (this.subBoardCode == "mail")
		return false;

	// Thanks to echicken for explaining how to check this.  To check a user's
	// vote, use MsgBase.how_user_voted().
	/*
	The return value will be:
	0 - user hasn't voted
	1 - upvoted
	2 - downvoted
	Or, if the message was a poll, it's a bitfield:
	if (votes&(1<<2)) {
	 // User selected answer 2
	}
	*/
	var userHasVotedOnMsg = false;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
		if (typeof(msgbase.how_user_voted) === "function")
			var votes = 0;
			if (typeof(pUser) == "object")
				votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? pUser.name : pUser.alias);
				votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias);
// Gets information about the upvotes and downvotes for a message.
// If the user is a sysop, this will also get who voted on the message.
//
// Parameters:
//  pMsgHdr: A header of a message that has upvotes & downvotes
//
// Return value: An array of strings containing information about the upvotes,
//               downvotes, tally, and (if the user is a sysop) who submitted
//               votes on the message.
function DigDistMsgReader_GetUpvoteAndDownvoteInfo(pMsgHdr)
{
	// If the message header doesn't have the "total_votes" or "upvotes" properties,
	// then there's no vote information, so just return an empty array.
	if (!pMsgHdr.hasOwnProperty("total_votes") || !pMsgHdr.hasOwnProperty("upvotes"))
		return [];

	var voteInfo = getMsgUpDownvotesAndScore(pMsgHdr);
	voteInfo.push("Upvotes: " + voteInfo.upvotes);
	voteInfo.push("Downvotes: " + voteInfo.downvotes);
	voteInfo.push("Score: " + voteInfo.voteScore);
	if (pMsgHdr.hasOwnProperty("tally"))
		voteInfo.push("Tally: " + pMsgHdr.tally);

	if (gIsSysop)
	{
		// Check all the messages in the messagebase after the current one
		// to find response messages
		var msgbase = new MsgBase(this.subBoardCode);
		if (msgbase.open())
			if (typeof(msgbase.get_all_msg_headers) === "function")
				// Pass true to get_all_msg_headers() to tell it to return vote messages
				// (the parameter was introduced in Synchronet 3.17+)
				var tmpHdrs = msgbase.get_all_msg_headers(true);
				for (var tmpProp in tmpHdrs)
					if (tmpHdrs[tmpProp] == null)
						continue;
					// If this header's thread_back or reply_id matches the poll message
					// number, then append the 'user voted' string to the message body.
					if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
						var tmpMessageBody = msgbase.get_msg_body(false, tmpHdrs[tmpProp].number);
						if ((tmpHdrs[tmpProp].field_list.length == 0) && (tmpMessageBody.length == 0))
						{
							var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
							var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime);
							voteInfo.push("\1n\1c\1h" + tmpHdrs[tmpProp].from + "\1n\1c voted on this message on " + voteDate + "\1n");
						}
// For the DigDistMsgReader class: Gets the body (text) of a message.  If it's
// a poll, this method will format the message body with poll results.  Otherwise,
// this method will simply get the message body.
//
// Parameters:
//  pMsgHeader: The message header
//
// Return value: The poll results, colorized.  If the message is not a
//               poll message, then an empty string will be returned.
function DigDistMsgReader_GetMsgBody(pMsgHdr)
	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
		return "";
	var msgBody = "";
	if ((typeof(MSG_TYPE_POLL) != "undefined") && (pMsgHdr.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
	{
		// A poll is intended to be parsed (and displayed) using on the header data. The
		// (optional) comments are stored in the hdr.field_list[] with type values of
		// SMB_COMMENT (now defined in sbbsdefs.js) and the available answers are in the
		// field_list[] with type values of SMB_POLL_ANSWER.

		// The 'comments' and 'answers' are also a part of the message header, so you can
		// grab them separately, then format and display them however you want.  You can
		// find them in the header.field_list array; each element in that array should be
		// an object with a 'type' and a 'data' property.  Relevant types here are
		// SMB_COMMENT and SMB_POLL_ANSWER.  (This is what I'm doing on the web, and I
		// just ignore the message body for poll messages.)

		if (pMsgHdr.hasOwnProperty("field_list"))
		{
			// Figure out the longest length of the poll choices, with
			// a maximum of 22 characters less than the terminal width.
			// Use a minimum of 27 characters.
			// That length will be used for the formatting strings for
			// the poll results.
			var voteOptDescLen = 0;
			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
			{
				if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
				{
					if (pMsgHdr.field_list[fieldI].data.length > voteOptDescLen)
						voteOptDescLen = pMsgHdr.field_list[fieldI].data.length;
				}
			}
			if (voteOptDescLen > console.screen_columns - 22)
				voteOptDescLen = console.screen_columns - 22;
			else if (voteOptDescLen < 27)
				voteOptDescLen = 27;

			// Format strings for outputting the voting option lines
			var unvotedOptionFormatStr = "\1n\1c\1h%2d\1n\1c: \1w\1h%-" + voteOptDescLen + "s [%-4d %6.2f%]\1n";
			var votedOptionFormatStr = "\1n\1c\1h%2d\1n\1c: \1" + "5\1w\1h%-" + voteOptDescLen + "s [%-4d %6.2f%]\1n";
			// Add up the total number of votes so that we can
			// calculate vote percentages.
			var totalNumVotes = 0;
			if (pMsgHdr.hasOwnProperty("tally"))
			{
				for (var tallyI = 0; tallyI < pMsgHdr.tally.length; ++tallyI)
					totalNumVotes += pMsgHdr.tally[tallyI];
			}
			// Go through field_list and append the voting options and stats to
			// msgBody
			var optionNum = 1;
			var numVotes = 0;
			var votePercentage = 0;
			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
			{
				if (pMsgHdr.field_list[fieldI].type == SMB_COMMENT)
					pollComment += pMsgHdr.field_list[fieldI].data + "\r\n";
				else if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
				{
					// Figure out the number of votes on this option and the
					// vote percentage
					if (pMsgHdr.hasOwnProperty("tally"))
					{
						if (tallyIdx < pMsgHdr.tally.length)
							numVotes = pMsgHdr.tally[tallyIdx];
							votePercentage = (numVotes / totalNumVotes) * 100;
						}
					}
					// Append to the message text
					msgBody += format(numVotes == 0 ? unvotedOptionFormatStr : votedOptionFormatStr,
					                  optionNum++, pMsgHdr.field_list[fieldI].data.substr(0, voteOptDescLen),
					                  numVotes, votePercentage);
					if (numVotes > 0)
						msgBody += " " + CHECK_CHAR;
					msgBody += "\r\n";
			if (pollComment.length > 0)
				msgBody = pollComment + "\r\n" + msgBody;
			// If voting is allowed in this sub-board and the current logged-in
			// user has not voted on this message, then append some text saying
			// how to vote.
			var votingAllowed = ((this.subBoardCode != "mail") && (((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == 0)));
			if (votingAllowed && !this.HasUserVotedOnMsg(pMsgHdr.number))
				msgBody += "\1n\r\n\1gTo vote in this poll, press \1w\1h" + this.enhReaderKeys.vote + "\1n\1g now.\r\n";

			// If the current logged-in user created this poll, then show the
			// users who have voted on it so far.
			var msgFromUpper = pMsgHdr.from.toUpperCase();
			if ((msgFromUpper == user.name.toUpperCase()) || (msgFromUpper == user.handle.toUpperCase()))
			{
				// Check all the messages in the messagebase after the current one
				// to find poll response messages
				if (typeof(msgbase.get_all_msg_headers) === "function")
				{
					// Get the line from text.dat for writing who voted & when.  It
					// is a format string and should look something like this:
					//"\r\n\1n\1hOn %s, in \1c%s \1n\1c%s\r\n\1h\1m%s voted in your poll: \1n\1h%s\r\n" 787 PollVoteNotice
					var userVotedInYourPollText = bbs.text(typeof(PollVoteNotice) != "undefined" ? PollVoteNotice : 787);
					// Pass true to get_all_msg_headers() to tell it to return vote messages
					// (the parameter was introduced in Synchronet 3.17+)
					var tmpHdrs = msgbase.get_all_msg_headers(true);
						if (tmpHdrs[tmpProp] == null)
							continue;
						// If this header's thread_back or reply_id matches the poll message
						// number, then append the 'user voted' string to the message body.
						if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
						{
							var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
							var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime);
							var grpName = "";
							var msgbaseCfgName = "";
							var msgbase = new MsgBase(this.subBoardCode);
							if (msgbase.open())
							{
								grpName = msgbase.cfg.grp_name;
								msgbaseCfgName = msgbase.cfg.name;
								msgbase.close();
							}
							msgBody += format(userVotedInYourPollText, voteDate, grpName, msgbaseCfgName, tmpHdrs[tmpProp].from, pMsgHdr.subject);
	{
		// If the message is UTF8 and the terminal is not UTF8-capable, then convert
		// the text to cp437.
		msgBody = msgbase.get_msg_body(false, pMsgHdr.number);
		if (pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8)
		{
			var userConsoleSupportsUTF8 = false;
			if (typeof(USER_UTF8) != "undefined")
				userConsoleSupportsUTF8 = console.term_supports(USER_UTF8);
			if (!userConsoleSupportsUTF8)
				msgBody = utf8_cp437(msgBody);
		}
		// Remove any initial coloring from the message body, which can color the whole message
		msgBody = removeInitialColorFromMsgBody(msgBody);
	}
	// Remove any Synchronet pause codes that might exist in the message
	msgBody = msgBody.replace("\1p", "").replace("\1P", "");

	// If the user is a sysop, this is a moderated message area, and the message
	// hasn't been validated, then prepend the message with a message to let the
	// sysop now know to validate it.
	if (this.subBoardCode != "mail")
	{
		if (gIsSysop && msg_area.sub[this.subBoardCode].is_moderated && ((pMsgHdr.attr & MSG_VALIDATED) == 0))
		{
			var validateNotice = "\1n\1h\1yThis is an unvalidated message in a moderated area.  Press "
							   + this.enhReaderKeys.validateMsg + " to validate it.\r\n\1g";
			for (var i = 0; i < 79; ++i)
				validateNotice += HORIZONTAL_SINGLE;
			validateNotice += "\1n\r\n";
			msgBody = validateNotice + msgBody;
		}
// For the DigDistMsgReader class: Refreshes a message header in one of the
// internal message arrays.
//
// Parameters:
//  pMsgNum: The number of the message to replace
function DigDistMsgReader_RefreshMsgHdrInArrays(pMsgNum)
{
	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
		return;
	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
	    (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
	{
		for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i)
		{
			if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == pMsgNum)
			{
				var newMsgHdr = msgbase.get_msg_header(false, pMsgNum, true);
				if (newMsgHdr != null)
					this.msgSearchHdrs[this.subBoardCode].indexed[i] = newMsgHdr;
				break;
			}
		}
	}
	else if (this.hdrsForCurrentSubBoard.length > 0)
	{
		if (this.hdrsForCurrentSubBoardByMsgNum.hasOwnProperty(pMsgNum))
		{
			var msgHdrs = msgbase.get_all_msg_headers();
			if (msgHdrs.hasOwnProperty(pMsgNum))
			{
				var msgIdx = this.hdrsForCurrentSubBoardByMsgNum[pMsgNum];
				this.hdrsForCurrentSubBoard[msgIdx] = msgHdrs[pMsgNum];
			}
		}
	}
	msgbase.close();
}

// For the DigDistMessageReader class: Re-calculates the message list widths and
// format strings
//
// Parameters:
//  pMsgNumLen: Optional - Length to use for the message number field.  If not specified,
//              then this will get the number of messages in the sub-board and use that
//              length.
function DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs(pMsgNumLen)
{
	// Note: Constructing these strings must be done after reading the configuration
	// file in order for the configured colors to be used

	this.sMsgListHdrFormatStr = "";
	this.sMsgInfoFormatStr = "";
	this.sMsgInfoToUserFormatStr = "";
	this.sMsgInfoFromUserFormatStr = "";
	this.sMsgInfoFormatHighlightStr = "";

	this.MSGNUM_LEN = (typeof(pMsgNumLen) == "number" ? pMsgNumLen : this.NumMessages(null, true).toString().length);
	if (this.MSGNUM_LEN < 4)
		this.MSGNUM_LEN = 4;
	this.DATE_LEN = 10; // i.e., YYYY-MM-DD
	this.TIME_LEN = 8;  // i.e., HH:MM:SS
	// Variable field widths: From, to, and subject
	this.FROM_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
	this.TO_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
	var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
	this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0);

	if (this.showScoresInMsgList)
	{
		this.SUBJ_LEN -= (this.SCORE_LEN + 1);
		this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-"
		                          + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %"
		                          + this.SCORE_LEN + "s %-" + this.DATE_LEN + "s %-"
		                          + this.TIME_LEN + "s";

		this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                       + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s "
		                       + this.colors.msgListToColor + "%-" + this.TO_LEN + "s "
		                       + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s "
		                       + this.colors.msgListScoreColor + "%" + this.SCORE_LEN + "d "
		                       + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s "
		                       + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// written to the user.
		this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                             + this.colors.msgListToUserFromColor
		                             + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-"
		                             + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-"
		                             + this.SUBJ_LEN + "s " + this.colors.msgListToUserScoreColor + "%"
		                             + this.SCORE_LEN + "d " + this.colors.msgListToUserDateColor
		                             + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor
		                             + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// from the user.
		this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                               + this.colors.msgListFromUserFromColor
		                               + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-"
		                               + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-"
		                               + this.SUBJ_LEN + "s " + this.colors.msgListFromUserScoreColor + "%"
		                               + this.SCORE_LEN + "d " + this.colors.msgListFromUserDateColor
		                               + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor
		                               + "%-" + this.TIME_LEN + "s";
		// Highlighted message information line for the message list (used for the
		// lightbar interface)
		this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor
		                                + "%" + this.MSGNUM_LEN + "d%s"
		                                + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN
		                                + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s "
		                                + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s "
		                                + this.colors.msgListScoreHighlightColor + "%" + this.SCORE_LEN + "d "
		                                + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s "
		                                + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s";
	}
	else
	{
		this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-"
		                          + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-"
		                          + this.DATE_LEN + "s %-" + this.TIME_LEN + "s";

		this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                       + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s "
		                       + this.colors.msgListToColor + "%-" + this.TO_LEN + "s "
		                       + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s "
		                       + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s "
		                       + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// written to the user.
		this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                             + this.colors.msgListToUserFromColor
		                             + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-"
		                             + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-"
		                             + this.SUBJ_LEN + "s " + this.colors.msgListToUserDateColor
		                             + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor
		                             + "%-" + this.TIME_LEN + "s";
		// Message information format string with colors to use when the message is
		// from the user.
		this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d%s"
		                               + this.colors.msgListFromUserFromColor
		                               + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-"
		                               + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-"
		                               + this.SUBJ_LEN + "s " + this.colors.msgListFromUserDateColor
		                               + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor
		                               + "%-" + this.TIME_LEN + "s";
		// Highlighted message information line for the message list (used for the
		// lightbar interface)
		this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor
		                                + "%" + this.MSGNUM_LEN + "d%s"
		                                + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN
		                                + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s "
		                                + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s "
		                                + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s "
		                                + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s";
	}

	// If the user's terminal doesn't support ANSI, then append a newline to
	// the end of the header format string (we won't be able to move the cursor).
///////////////////////////////////////////////////////////////////////////////////
// Helper functions

// Displays the program information.
function DisplayProgramInfo()
{
	displayTextWithLineBelow("Digital Distortion Message Reader", true, "\1n\1c\1h", "\1k\1h")
	console.center("\1n\1cVersion \1g" + READER_VERSION + " \1w\1h(\1b" + READER_DATE + "\1w)");
	console.crlf();
}

// This function returns an array of default colors used in the
// DigDistMessageReader class.
function getDefaultColors()
{
	colorObj.msgListHeaderMsgGroupTextColor = "\1n\1" + "4\1c"; // Normal cyan on blue background
	//colorObj.msgListHeaderMsgGroupTextColor = "\1n\1" + "4\1w"; // Normal white on blue background
	colorObj.msgListHeaderMsgGroupNameColor = "\1h\1c"; // High cyan
	//colorObj.msgListHeaderMsgGroupNameColor = "\1h\1w"; // High white
	colorObj.msgListHeaderSubBoardTextColor = "\1n\1" + "4\1c"; // Normal cyan on blue background
	//colorObj.msgListHeaderSubBoardTextColor = "\1n\1" + "4\1w"; // Normal white on blue background
	colorObj.msgListHeaderMsgSubBoardName = "\1h\1c"; // High cyan
	//colorObj.msgListHeaderMsgSubBoardName = "\1h\1w"; // High white
	//colorObj.msgListColHeader = "\1h\1w"; // High white (keep blue background)
	colorObj.msgListColHeader = "\1n\1h\1w"; // High white on black background
	//colorObj.msgListColHeader = "\1h\1c"; // High cyan (keep blue background)
	//colorObj.msgListColHeader = "\1" + "4\1h\1y"; // High yellow (keep blue background)
	colorObj.msgListMsgNumColor = "\1n\1h\1y";
	colorObj.msgListFromColor = "\1n\1c";
	colorObj.msgListToColor = "\1n\1c";
	colorObj.msgListSubjectColor = "\1n\1c";
	colorObj.msgListScoreColor = "\1n\1c";
	colorObj.msgListDateColor = "\1h\1b";
	colorObj.msgListTimeColor = "\1h\1b";
	// Message information for messages written to the user
	colorObj.msgListToUserMsgNumColor = "\1n\1h\1y";
	colorObj.msgListToUserFromColor = "\1h\1g";
	colorObj.msgListToUserToColor = "\1h\1g";
	colorObj.msgListToUserSubjectColor = "\1h\1g";
	colorObj.msgListToUserScoreColor = "\1h\1g";
	colorObj.msgListToUserDateColor = "\1h\1b";
	colorObj.msgListToUserTimeColor = "\1h\1b";
	// Message information for messages from the user
	colorObj.msgListFromUserMsgNumColor = "\1n\1h\1y";
	colorObj.msgListFromUserFromColor = "\1n\1c";
	colorObj.msgListFromUserToColor = "\1n\1c";
	colorObj.msgListFromUserSubjectColor = "\1n\1c";
	colorObj.msgListFromUserScoreColor = "\1n\1c";
	colorObj.msgListFromUserDateColor = "\1h\1b";
	colorObj.msgListFromUserTimeColor = "\1h\1b";
	colorObj.msgListHighlightBkgColor = "\1" + "4"; // Background
	colorObj.msgListMsgNumHighlightColor = "\1h\1y";
	colorObj.msgListFromHighlightColor = "\1h\1c";
	colorObj.msgListToHighlightColor = "\1h\1c";
	colorObj.msgListSubjHighlightColor = "\1h\1c";
	colorObj.msgListScoreHighlightColor = "\1h\1c";
	colorObj.msgListDateHighlightColor = "\1h\1w";
	colorObj.msgListTimeHighlightColor = "\1h\1w";
	colorObj.lightbarMsgListHelpLineBkgColor = "\1" + "7"; // Background
	colorObj.lightbarMsgListHelpLineGeneralColor = "\1b";
	colorObj.lightbarMsgListHelpLineHotkeyColor = "\1r";
	colorObj.lightbarMsgListHelpLineParenColor = "\1m";
	colorObj.tradInterfaceContPromptMainColor = "\1n\1g"; // Main text color
	colorObj.tradInterfaceContPromptHotkeyColor = "\1h\1c"; // Hotkey color
	colorObj.tradInterfaceContPromptUserInputColor = "\1h\1g"; // User input color
	colorObj.readMsgConfirmColor = "\1n\1c";
	colorObj.readMsgConfirmNumberColor = "\1h\1c";
	// Prompt for continuing to list messages after reading a message
	colorObj.afterReadMsg_ListMorePromptColor = "\1n\1c";
	colorObj.tradInterfaceHelpScreenColor = "\1n\1h\1w";

	// Colors for choosing a message group & sub-board
	colorObj.areaChooserMsgAreaNumColor = "\1n\1w\1h";
	colorObj.areaChooserMsgAreaDescColor = "\1n\1c";
	colorObj.areaChooserMsgAreaNumItemsColor = "\1b\1h";
	colorObj.areaChooserMsgAreaHeaderColor = "\1n\1y\1h";
	colorObj.areaChooserSubBoardHeaderColor = "\1n\1g";
	colorObj.areaChooserMsgAreaMarkColor = "\1g\1h";
	colorObj.areaChooserMsgAreaLatestDateColor = "\1n\1g";
	colorObj.areaChooserMsgAreaLatestTimeColor = "\1n\1m";
	// Highlighted colors (for lightbar mode)
	colorObj.areaChooserMsgAreaBkgHighlightColor = "\1" + "4"; // Blue background
	colorObj.areaChooserMsgAreaNumHighlightColor = "\1w\1h";
	colorObj.areaChooserMsgAreaDescHighlightColor = "\1c";
	colorObj.areaChooserMsgAreaDateHighlightColor = "\1w\1h";
	colorObj.areaChooserMsgAreaTimeHighlightColor = "\1w\1h";
	colorObj.areaChooserMsgAreaNumItemsHighlightColor = "\1w\1h";
	colorObj.lightbarAreaChooserHelpLineBkgColor = "\1" + "7"; // Background
	colorObj.lightbarAreaChooserHelpLineGeneralColor = "\1b";
	colorObj.lightbarAreaChooserHelpLineHotkeyColor = "\1r";
	colorObj.lightbarAreaChooserHelpLineParenColor = "\1m";

	// Scrollbar background and scroll block colors (for the enhanced
	// message reader interface)
	colorObj.scrollbarBGColor = "\1n\1h\1k";
	colorObj.scrollbarScrollBlockColor = "\1n\1h\1w";
	// Color for the line drawn in the 2nd to last line of the message
	// area in the enhanced reader mode before a prompt
	colorObj.enhReaderPromptSepLineColor = "\1n\1h\1g";
	// Colors for the enhanced reader help line
	colorObj.enhReaderHelpLineBkgColor = "\1" + "7";
	colorObj.enhReaderHelpLineGeneralColor = "\1b";
	colorObj.enhReaderHelpLineHotkeyColor = "\1r";
	colorObj.enhReaderHelpLineParenColor = "\1m";
	colorObj.hdrLineLabelColor = "\1n\1c";
	colorObj.hdrLineValueColor = "\1n\1b\1h";
	colorObj.selectedMsgMarkColor = "\1n\1w\1h";
}

// This function returns the month number (1-based) from a capitalized
// month name.
//
// Parameters:
//  pMonthName: The name of the month
//
// Return value: The number of the month (1-12).
function getMonthNum(pMonthName)
{
	var monthNum = 1;

	if (pMonthName.substr(0, 3) == "Jan")
		monthNum = 1;
	else if (pMonthName.substr(0, 3) == "Feb")
		monthNum = 2;
	else if (pMonthName.substr(0, 3) == "Mar")
		monthNum = 3;
	else if (pMonthName.substr(0, 3) == "Apr")
		monthNum = 4;
	else if (pMonthName.substr(0, 3) == "May")
		monthNum = 5;
	else if (pMonthName.substr(0, 3) == "Jun")
		monthNum = 6;
	else if (pMonthName.substr(0, 3) == "Jul")
		monthNum = 7;
	else if (pMonthName.substr(0, 3) == "Aug")
		monthNum = 8;
	else if (pMonthName.substr(0, 3) == "Sep")
		monthNum = 9;
	else if (pMonthName.substr(0, 3) == "Oct")
		monthNum = 10;
	else if (pMonthName.substr(0, 3) == "Nov")
		monthNum = 11;
	else if (pMonthName.substr(0, 3) == "Dec")
		monthNum = 12;

	return monthNum;
}

// Clears each line from a given line to the end of the screen.
//
// Parameters:
//  pStartLineNum: The line number to start at (1-based)
function clearToEOS(pStartLineNum)
{
	if (typeof(pStartLineNum) == "undefined")
		return;
	if (pStartLineNum == null)
		return;

	for (var lineNum = pStartLineNum; lineNum <= console.screen_rows; ++lineNum)
	{
		console.gotoxy(1, lineNum);
		console.clearline();
	}
}

// Returns the number of messages in a sub-board.
//
// Parameters:
//  pSubBoardCode: The sub-board code (i.e., from bbs.cursub_code)
//
// Return value: The number of messages in the sub-board, or 0
//               if the sub-board could not be opened.
function numMessages(pSubBoardCode)
{
   var messageCount = 0;

   var myMsgbase = new MsgBase(pSubBoardCode);
	if (myMsgbase.open())
		messageCount = myMsgbase.total_msgs;
	myMsgbase.close();
	myMsgbase = null;

	return messageCount;
}

// Removes multiple, leading, and/or trailing spaces
// The search & replace regular expressions used in this
// function came from the following URL:
//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
//
// Parameters:
//  pString: The string to trim
//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
//
// Return value: The string with whitespace trimmed
function trimSpaces(pString, pLeading, pMultiple, pTrailing)
{
	var leading = true;
	var multiple = true;
	var trailing = true;
	if(typeof(pLeading) != "undefined")
		leading = pLeading;
	if(typeof(pMultiple) != "undefined")
		multiple = pMultiple;
	if(typeof(pTrailing) != "undefined")
		trailing = pTrailing;

	// To remove both leading & trailing spaces:
	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");

	if (leading)
		pString = pString.replace(/(^\s*)/gi,"");
	if (multiple)
		pString = pString.replace(/[ ]{2,}/gi," ");
	if (trailing)
		pString = pString.replace(/(\s*$)/gi,"");

	return pString;
}

// Returns whether an internal sub-board code is valid.
//
// Parameters:
//  pSubBoardCode: The internal sub-board code to test
//
// Return value: Boolean - Whether or not the given internal code is a valid
//               sub-board code
function subBoardCodeIsValid(pSubBoardCode)
{
   return ((pSubBoardCode == "mail") || (typeof(msg_area.sub[pSubBoardCode]) == "object"))
}

// Displays some text with a solid horizontal line on the next line.
//
// Parameters:
//  pText: The text to display
//  pCenter: Whether or not to center the text.  Optional; defaults
//           to false.
//  pTextColor: The color to use for the text.  Optional; by default,
//              normal white will be used.
//  pLineColor: The color to use for the line underneath the text.
//              Optional; by default, bright black will be used.
function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
{
	var centerText = (typeof(pCenter) == "boolean" ? pCenter : false);
	var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\1n\1w");
	var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\1n\1k\1h");
	// Output the text and a solid line on the next line.
	if (centerText)
	{
		console.center(textColor + pText);
		var solidLine = "";
		var textLength = console.strlen(pText);
		for (var i = 0; i < textLength; ++i)
			solidLine += "Ä";
		console.center(lineColor + solidLine);
	}
	else
	{
		console.print(textColor + pText);
		console.crlf();
		console.print(lineColor);
		var textLength = console.strlen(pText);
		for (var i = 0; i < textLength; ++i)
			console.print("Ä");
		console.crlf();
	}
}

// Returns whether the Synchronet compile date is at least May 12, 2013.  That
// was when Digital Man's change to make bbs.msg_number work when a script is
// running first went into the Synchronet daily builds.
function compileDateAtLeast2013_05_12()
{
  // system.compiled_when is in the following format:
  // May 12 2013 05:02

  var compileDateParts = system.compiled_when.split(" ");
  if (compileDateParts.length < 4)
    return false;

  // Convert the month to a 1-based number
  var compileMonth = 0;
  if (/^Jan/.test(compileDateParts[0]))
    compileMonth = 1;
  else if (/^Feb/.test(compileDateParts[0]))
    compileMonth = 2;
  else if (/^Mar/.test(compileDateParts[0]))
    compileMonth = 3;
  else if (/^Apr/.test(compileDateParts[0]))
    compileMonth = 4;
  else if (/^May/.test(compileDateParts[0]))
    compileMonth = 5;
  else if (/^Jun/.test(compileDateParts[0]))
    compileMonth = 6;
  else if (/^Jul/.test(compileDateParts[0]))
    compileMonth = 7;
  else if (/^Aug/.test(compileDateParts[0]))
    compileMonth = 8;
  else if (/^Sep/.test(compileDateParts[0]))
    compileMonth = 9;
  else if (/^Oct/.test(compileDateParts[0]))
    compileMonth = 10;
  else if (/^Nov/.test(compileDateParts[0]))
    compileMonth = 11;
  else if (/^Dec/.test(compileDateParts[0]))
    compileMonth = 12;

  // Get the compileDay and compileYear as numeric variables
  var compileDay = +compileDateParts[1];
  var compileYear = +compileDateParts[2];

  // Determine if the compile date is at least 2013-05-12
  var compileDateIsAtLeastMin = true;
  if (compileYear > 2013)
    compileDateIsAtLeastMin = true;