Skip to content
Snippets Groups Projects
DDMsgReader.js 790 KiB
Newer Older
					this.promptToContinueListingMessages = (valueUpper == "TRUE");
				else if (settingUpper == "PROMPTCONFIRMREADMESSAGE")
					this.promptToReadMessage = (valueUpper == "TRUE");
				else if (settingUpper == "MSGLISTDISPLAYTIME")
					this.msgList_displayMessageDateImported = (valueUpper == "IMPORTED");
				else if (settingUpper == "MSGAREALIST_LASTIMPORTEDMSG_TIME")
					this.msgAreaList_lastImportedMsg_showImportTime = (valueUpper == "IMPORTED");
				else if (settingUpper == "STARTMODE")
				{
					if ((valueUpper == "READER") || (valueUpper == "READ"))
						this.startMode = READER_MODE_READ;
					else if ((valueUpper == "LISTER") || (valueUpper == "LIST"))
						this.startMode = READER_MODE_LIST;
				}
				else if (settingUpper == "TABSPACES")
				{
					var numSpaces = +value;
					// If greater than 0, then set this.numTabSpaces
					if (numSpaces > 0)
						this.numTabSpaces = numSpaces;
				}
				else if (settingUpper == "PAUSEAFTERNEWMSGSCAN")
					this.pauseAfterNewMsgScan = (valueUpper == "TRUE");
				else if (settingUpper == "READINGPOSTONSUBBOARDINSTEADOFGOTONEXT")
					this.readingPostOnSubBoardInsteadOfGoToNext = (valueUpper == "TRUE");
nightfox's avatar
nightfox committed
				else if (settingUpper == "AREACHOOSERHDRFILENAMEBASE")
					this.areaChooserHdrFilenameBase = value;
				else if (settingUpper == "AREACHOOSERHDRMAXLINES")
				{
					var maxNumLines = +value;
					if (maxNumLines > 0)
						this.areaChooserHdrMaxLines = maxNumLines;
				}
				else if (settingUpper == "THEMEFILENAME")
				{
					// First look for the theme config file in the sbbs/mods
					// directory, then sbbs/ctrl, then the same directory as
					// this script.
					themeFilename = system.mods_dir + value;
					if (!file_exists(themeFilename))
						themeFilename = system.ctrl_dir + value;
					if (!file_exists(themeFilename))
						themeFilename = gStartupPath + value;
				}
				else if (settingUpper == "DISPLAYAVATARS")
					this.displayAvatars = (valueUpper == "TRUE");
				else if (settingUpper == "RIGHTJUSTIFYAVATARS")
					this.rightJustifyAvatar = (valueUpper == "TRUE");
				else if (settingUpper == "MSGLISTSORT")
				{
					if (valueUpper == "WRITTEN")
						this.msgListSort = MSG_LIST_SORT_DATETIME_WRITTEN;
				}
			}
		}

		cfgFile.close();
	}
	else
	{
		// Was unable to read the configuration file.  Output a warning to the user
		// that defaults will be used and to notify the sysop.
		console.print("\x01w\x01hUnable to open the configuration file: \x01y" + this.cfgFilename);
		console.print("\x01wDefault settings will be used.  Please notify the sysop.");
		mswait(2000);
	}
	
	// If a theme filename was specified, then read the colors & strings
	// from it.
	if (themeFilename.length > 0)
	{
		var themeFile = new File(themeFilename);
		if (themeFile.open("r"))
		{
			var fileLine = null;     // A line read from the file
			var equalsPos = 0;       // Position of a = in the line
			var commentPos = 0;      // Position of the start of a comment
			var setting = null;      // A setting name (string)
			var value = null;        // To store a value for a setting (string)
			var onlySyncAttrsRegexWholeWord = new RegExp("^(\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz])+$", 'i');
			while (!themeFile.eof)
			{
				// Read the next line from the config file.
				fileLine = themeFile.readln(2048);

				// fileLine should be a string, but I've seen some cases
				// where it isn't, so check its type.
				if (typeof(fileLine) != "string")
					continue;

				// If the line starts with with a semicolon (the comment
				// character) or is blank, then skip it.
				if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
					continue;

				// If the line has a semicolon anywhere in it, then remove
				// everything from the semicolon onward.
				commentPos = fileLine.indexOf(";");
				if (commentPos > -1)
					fileLine = fileLine.substr(0, commentPos);

				// Look for an equals sign, and if found, separate the line
				// into the setting name (before the =) and the value (after the
				// equals sign).
				equalsPos = fileLine.indexOf("=");
				if (equalsPos > 0)
				{
					// Read the setting (without leading/trailing spaces) & value
					setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
					value = fileLine.substr(equalsPos+1);

					// Colors
					if ((setting == "msgListHeaderMsgGroupTextColor") || (setting == "msgListHeaderMsgGroupNameColor") ||
					    (setting == "msgListHeaderSubBoardTextColor") || (setting == "msgListHeaderMsgSubBoardName") ||
					    (setting == "msgListColHeader") ||
					    (setting == "msgListMsgNumColor") || (setting == "msgListFromColor") ||
						(setting == "msgListToColor") || (setting == "msgListSubjectColor") ||
						(setting == "msgListDateColor") || (setting == "msgListTimeColor") ||
					    (setting == "msgListToUserMsgNumColor") || (setting == "msgListToUserFromColor") ||
					    (setting == "msgListToUserToColor") || (setting == "msgListToUserSubjectColor") ||
					    (setting == "msgListToUserDateColor") || (setting == "msgListToUserTimeColor") ||
					    (setting == "msgListFromUserMsgNumColor") || (setting == "msgListFromUserFromColor") ||
					    (setting == "msgListFromUserToColor") || (setting == "msgListFromUserSubjectColor") ||
					    (setting == "msgListFromUserDateColor") || (setting == "msgListFromUserTimeColor") ||
					    (setting == "msgListHighlightBkgColor") || (setting == "msgListMsgNumHighlightColor") ||
					    (setting == "msgListFromHighlightColor") || (setting == "msgListToHighlightColor") ||
					    (setting == "msgListSubjHighlightColor") || (setting == "msgListDateHighlightColor") ||
					    (setting == "msgListTimeHighlightColor") || (setting == "lightbarMsgListHelpLineBkgColor") ||
					    (setting == "lightbarMsgListHelpLineGeneralColor") || (setting == "lightbarMsgListHelpLineHotkeyColor") ||
					    (setting == "lightbarMsgListHelpLineParenColor") || (setting == "tradInterfaceContPromptMainColor") ||
					    (setting == "tradInterfaceContPromptHotkeyColor") || (setting == "tradInterfaceContPromptUserInputColor") ||
					    (setting == "msgBodyColor") || (setting == "readMsgConfirmColor") ||
					    (setting == "readMsgConfirmNumberColor") ||
					    (setting == "afterReadMsg_ListMorePromptColor") ||
					    (setting == "tradInterfaceHelpScreenColor") || (setting == "areaChooserMsgAreaNumColor") ||
					    (setting == "areaChooserMsgAreaDescColor") || (setting == "areaChooserMsgAreaNumItemsColor") ||
					    (setting == "areaChooserMsgAreaHeaderColor") || (setting == "areaChooserSubBoardHeaderColor") ||
					    (setting == "areaChooserMsgAreaMarkColor") || (setting == "areaChooserMsgAreaLatestDateColor") ||
					    (setting == "areaChooserMsgAreaLatestTimeColor") || (setting == "areaChooserMsgAreaBkgHighlightColor") ||
					    (setting == "areaChooserMsgAreaNumHighlightColor") || (setting == "areaChooserMsgAreaDescHighlightColor") ||
					    (setting == "areaChooserMsgAreaDateHighlightColor") || (setting == "areaChooserMsgAreaTimeHighlightColor") ||
					    (setting == "areaChooserMsgAreaNumItemsHighlightColor") || (setting == "lightbarAreaChooserHelpLineBkgColor") ||
					    (setting == "lightbarAreaChooserHelpLineGeneralColor") || (setting == "lightbarAreaChooserHelpLineHotkeyColor") ||
					    (setting == "lightbarAreaChooserHelpLineParenColor") || (setting == "scrollbarBGColor") ||
					    (setting == "scrollbarScrollBlockColor") || (setting == "enhReaderPromptSepLineColor") ||
					    (setting == "enhReaderHelpLineBkgColor") || (setting == "enhReaderHelpLineGeneralColor") ||
					    (setting == "enhReaderHelpLineHotkeyColor") || (setting == "enhReaderHelpLineParenColor") ||
					    (setting == "hdrLineLabelColor") || (setting == "hdrLineValueColor") ||
					    (setting == "selectedMsgMarkColor") || (setting ==  "msgListScoreColor") ||
					    (setting == "msgListToUserScoreColor") || (setting == "msgListFromUserScoreColor") ||
					    (setting == "msgListScoreHighlightColor") || (setting == "msgHdrMsgNumColor") ||
					    (setting == "msgHdrFromColor") || (setting == "msgHdrToColor") ||
					    (setting == "msgHdrToUserColor") || (setting == "msgHdrSubjColor") ||
					    (setting == "msgHdrDateColor"))
					{
						// Trim leading & trailing spaces from the value when
						// setting a color.  Also, replace any instances of "\x01"
						// with the Synchronet attribute control character.
						if (onlySyncAttrsRegexWholeWord.test(value))
							this.colors[setting] = trimSpaces(value, true, false, true).replace(/\\x01/g, "\x01");
					}
					// Text values
					else if ((setting == "scrollbarBGChar") ||
					         (setting == "scrollbarScrollBlockChar") ||
					         (setting == "goToPrevMsgAreaPromptText") ||
					         (setting == "goToNextMsgAreaPromptText") ||
					         (setting == "newMsgScanText") ||
					         (setting == "newToYouMsgScanText") ||
					         (setting == "allToYouMsgScanText") ||
					         (setting == "goToMsgNumPromptText") ||
					         (setting == "msgScanCompleteText") ||
					         (setting == "msgScanAbortedText") ||
					         (setting == "deleteMsgNumPromptText") ||
					         (setting == "editMsgNumPromptText") ||
					         (setting == "noMessagesInSubBoardText") ||
					         (setting == "noSearchResultsInSubBoardText") ||
					         (setting == "invalidMsgNumText") ||
					         (setting == "readMsgNumPromptText") ||
							 (setting == "msgHasBeenDeletedText") ||
					         (setting == "noKludgeLinesForThisMsgText") ||
							 (setting == "searchingPersonalMailText") ||
					         (setting == "searchingSubBoardAbovePromptText") ||
					         (setting == "searchingSubBoardText") ||
							 (setting == "searchTextPromptText") ||
							 (setting == "fromNamePromptText") ||
							 (setting == "toNamePromptText") ||
							 (setting == "abortedText") ||
							 (setting == "loadingPersonalMailText") ||
							 (setting == "msgDelConfirmText") ||
							 (setting == "msgUndelConfirmText") ||
							 (setting == "selectedMsgsUndeletedText") ||
							 (setting == "cannotDeleteMsgText_notYoursNotASysop") ||
							 (setting == "cannotDeleteMsgText_notLastPostedMsg") ||
							 (setting == "msgEditConfirmText") ||
nightfox's avatar
nightfox committed
							 (setting == "noPersonalEmailText") ||
							 (setting == "postOnSubBoard"))
						// Replace any instances of "\x01" with the Synchronet
						this.text[setting] = value.replace(/\\x01/g, "\x01");
					}
				}
			}

			themeFile.close();

			// Ensure that scrollbarBGChar and scrollbarScrollBlockChar are
			// only one character.  If they're longer, use only the first
			// character.
			if (this.text.scrollbarBGChar.length > 1)
				this.text.scrollbarBGChar = this.text.scrollbarBGChar.substr(0, 1);
			if (this.text.scrollbarScrollBlockChar.length > 1)
				this.text.scrollbarScrollBlockChar = this.text.scrollbarScrollBlockChar.substr(0, 1);
		}
		else
		{
			// Was unable to read the theme file.  Output a warning to the user
			// that defaults will be used and to notify the sysop.
			this.cfgFileSuccessfullyRead = false;
			console.print("\x01w\x01hUnable to open the theme file: \x01y" + themeFilename);
			console.print("\x01wDefault settings will be used.  Please notify the sysop.");
// For the DigDistMsgReader class: Reads the user settings file
//
// Parameters:
//  pOnlyTwitlist: Optional boolean - Whether or not to only read the user's twitlist. Defaults to false.
function DigDistMsgReader_ReadUserSettingsFile(pOnlyTwitlist)
{
	var onlyTwitList = (typeof(pOnlyTwitlist) === "boolean" ? pOnlyTwitlist : false);
	// Open the user's personal twit list file, if it exists
	var userTwitlistFile = new File(gUserTwitListFilename);
	if (userTwitlistFile.open("r"))
	{
		while (!userTwitlistFile.eof)
		{
			// Read the next line from the config file.
			var fileLine = userTwitlistFile.readln(2048);

			// fileLine should be a string, but I've seen some cases
			// where for some reason it isn't.  If it's not a string,
			// then continue onto the next line.
			if (typeof(fileLine) != "string")
				continue;

			// If the line starts with with a semicolon (the comment
			// character) or is blank, then skip it.
			if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
				continue;

			// In case there are any commas, split on commas. Add all names to the user's twitlist
			var names = fileLine.split(",");
			for (var i = 0; i < names.length; ++i)
			{
				var twitListNameEntry = names[i].trim().toLowerCase(); // Lowercase for case-insensitive comparisons
				if (twitListNameEntry.length > 0)
					this.userSettings.twitList.push(twitListNameEntry);
			}
		}
		userTwitlistFile.close();
	}

	/*
	if (!onlyTwitList)
	{
		// Open the user settings file, if it exists
		var userSettingsFile = new File(gUserSettingsFilename);
		if (userSettingsFile.open("r"))
		{
			var settingsMode = "behavior";
			var equalsPos = 0;       // Position of a = in the line
			var commentPos = 0;      // Position of the start of a comment
			var setting = null;      // A setting name (string)
			var settingUpper = null; // Upper-case setting name
			var value = null;        // A value for a setting (string)
			var valueUpper = null;   // Upper-cased value
			while (!userSettingsFile.eof)
			{
				// Read the next line from the config file.
				var fileLine = userSettingsFile.readln(2048);

				// fileLine should be a string, but I've seen some cases
				// where for some reason it isn't.  If it's not a string,
				// then continue onto the next line.
				if (typeof(fileLine) != "string")
					continue;

				// If the line starts with with a semicolon (the comment
				// character) or is blank, then skip it.
				if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
					continue;

				// If in the "behavior" section, then set the behavior-related variables.
				if (fileLine.toUpperCase() == "[BEHAVIOR]")
				{
					settingsMode = "behavior";
					continue;
				}

				// If the line has a semicolon anywhere in it, then remove
				// everything from the semicolon onward.
				commentPos = fileLine.indexOf(";");
				if (commentPos > -1)
					fileLine = fileLine.substr(0, commentPos);

				// Look for an equals sign, and if found, separate the line
				// into the setting name (before the =) and the value (after the
				// equals sign).
				equalsPos = fileLine.indexOf("=");
				if (equalsPos > 0)
				{
					// Read the setting & value, and trim leading & trailing spaces.
					setting = trimSpaces(fileLine.substr(0, equalsPos), true, false, true);
					settingUpper = setting.toUpperCase();
					value = trimSpaces(fileLine.substr(equalsPos+1), true, false, true);
					valueUpper = value.toUpperCase();

					if (settingsMode == "behavior")
					{
						if (settingUpper == "SOMETHING")
							this.userSettings.something = (valueUpper == "TRUE");
					}
				}
			}
			userSettingsFile.close();
		}
	}
	*/
}
// For the DigDistMsgReader class: Lets the user edit an existing message.
//
// Parameters:
//  pMsgIndex: The index of the message to edit
//
// Return value: An object with the following parameters:
//               userCannotEdit: Boolean - True if the user can't edit, false if they can
//               userConfirmed: Boolean - Whether or not the user confirmed editing
//               msgEdited: Boolean - Whether or not the message was edited
//               newMsgIdx: The index (offset) of the new (edited) message that was saved.
//                          If the message wasn't edited/saved, this will be -1.
function DigDistMsgReader_EditExistingMsg(pMsgIndex)
{
	var returnObj = {
		userCannotEdit: false,
		userConfirmed: false,
		msgEdited: false,
		newMsgIdx: -1
	};

	// Open the sub-board
	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
	{
		console.print("\x01n\x01h\x01wCan't open the sub-board\x01n");
		console.crlf();
		console.pause();
		return returnObj;
	}

	// Only let the user edit the message if they're a sysop or
	// if they wrote the message.
	var msgHeader = this.GetMsgHdrByIdx(pMsgIndex, false, msgbase);
	if (!user.is_sysop && (msgHeader.from != user.name) && (msgHeader.from != user.alias) && (msgHeader.from != user.handle))
		console.print("\x01n\x01h\x01wCannot edit message #\x01y" + +(pMsgIndex+1) +
		              " \x01wbecause it's not yours or you're not a sysop.");
		returnObj.userCannotEdit = true;
		return returnObj;
	}

	// Confirm the action with the user (default to no).
	returnObj.userConfirmed = !console.noyes(replaceAtCodesInStr(format(this.text.msgEditConfirmText, +(pMsgIndex+1))));
	// Make use of bbs.edit_msg() if the function exists (it was added in
	// Synchronet 3.18c).  Otherwise, edit the old way.
	if (typeof(bbs.edit_msg) === "function")
	{
		if (!bbs.edit_msg(msgHeader))
		{
			var grpIdx = msg_area.sub[this.subBoardCode].grp_index;
			var areaDesc = msg_area.grp_list[grpIdx].description + " - " + msg_area.sub[this.subBoardCode].description;
			var logMsg = user.alias + " was unable to edit message number " + msgHeader.number + " in " + areaDesc;
			log(LOG_ERROR, logMsg);
			bbs.log_str(logMsg);
		}
	}
	else
		this.EditExistingMessageOldWay(msgbase, msgHeader, pMsgIndex);

	msgbase.close();

	return returnObj;
}
// Helper for DigDistMsgReader_EditExistingMsg(): Edits an existing message by writing it
// to a temporary file, having the user edit that, and saving it as a new message.
// This was done before the bbs.edit_msg() function existed (it was added in Synchronet
// 3.18c).
//
// Parameters:
//  pMsgbase: The MessageBase object.  Assumed to be open.
//  pOrigMsgHdr: The header of the original message
//  pMsgIndex: The index of the message to edit
function DigDistMsgReader_EditExistingMessageOldWay(pMsgbase, pOrigMsgHdr, pMsgIndex)
{
	// Dump the message body to a temporary file in the node dir
	//var originalMsgBody = pMsgbase.get_msg_body(true, pMsgIndex, false, false, true, true);
	var tmpMsgHdr = this.GetMsgHdrByIdx(pMsgIndex, false, pMsgbase);
	var msgHdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
	if (msgHdrIsBogus)
		originalMsgBody = pMsgbase.get_msg_body(true, pMsgIndex, false, false, true, true);
		originalMsgBody = pMsgbase.get_msg_body(false, tmpMsgHdr.number, false, false, true, true);
	var tempFilename = system.node_dir + "DDMsgLister_message.txt";
	var tmpFile = new File(tempFilename);
	if (tmpFile.open("w"))
	{
		var wroteToTempFile = tmpFile.write(word_wrap(originalMsgBody, 79));
		tmpFile.close();
		// If we were able to write to the temp file, then let the user
		// edit the file.
		if (wroteToTempFile)
		{
			// The following lines set some attributes in the bbs object
			// in an attempt to make the "To" name and subject appear
			// correct in the editor.
			// TODO: On May 14, 2013, Digital Man said bbs.msg_offset will
			// probably be removed because it doesn't provide any benefit.
			// bbs.msg_number is a unique message identifier that won't
			// change, so it's probably best for scripts to use bbs.msg_number
			// instead of offsets.
			bbs.msg_to = pOrigMsgHdr.to;
			bbs.msg_to_ext = pOrigMsgHdr.to_ext;
			bbs.msg_subject = pOrigMsgHdr.subject;
			bbs.msg_offset = pOrigMsgHdr.offset;
			bbs.msg_number = pOrigMsgHdr.number;

			// Let the user edit the temporary file
			console.editfile(tempFilename);
			// Load the temp file back into msgBodyColor and have pMsgbase
			// save the message.
			if (tmpFile.open("r"))
			{
				var newMsgBody = tmpFile.read();
				tmpFile.close();
				// If the new message body is different from the original message
				// body, then go ahead and save the message and mark the original
				// message for deletion. (Checking the new & original message
				// bodies seems to be the only way to check to see if the user
				// aborted out of the message editor.)
				if (newMsgBody != originalMsgBody)
				{
					var newHdr = { to: pOrigMsgHdr.to, to_ext: pOrigMsgHdr.to_ext, from: pOrigMsgHdr.from,
					               from_ext: pOrigMsgHdr.from_ext, attr: pOrigMsgHdr.attr,
					               subject: pOrigMsgHdr.subject };
					var savedNewMsg = pMsgbase.save_msg(newHdr, newMsgBody);
					// If the message was successfully saved, then mark the original
					// message for deletion and output a message to the user.
					if (savedNewMsg)
					{
						returnObj.msgEdited = true;
						returnObj.newMsgIdx = pMsgbase.total_msgs - 1;
						var message = "\x01n\x01cThe edited message has been saved as a new message.";
						if (pMsgbase.remove_msg(true, pMsgIndex))
							message += "  The original has been\r\nmarked for deletion.";
						else
							message += "  \x01h\x01yHowever, the original\r\ncould not be marked for deletion.";
						message += "\r\n\x01p";
						console.print("\r\n\x01n\x01h\x01yError: \x01wFailed to save the new message\r\n\x01p");
				console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to read the temporary file\r\n");
				console.print("Filename: \x01b" + tempFilename + "\r\n");
			console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to write to temporary file\r\n");
			console.print("Filename: \x01b" + tempFilename + "\r\n");
		console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to open a temporary file for writing\r\n");
		console.print("Filename: \x01b" + tempFilename + "\r\n");
		console.pause();
	}
	// Delete the temporary file from disk.
	tmpFile.remove();
// For the DigDistMsgReader Class: Returns whether or not the user can delete
// their messages in the sub-board (distinct from being able to delete only
// their last message).
function DigDistMsgReader_CanDelete()
{
	var canDelete = user.is_sysop || this.readingPersonalEmail;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		if (msgbase.cfg != null)
			canDelete = canDelete || ((msgbase.cfg.settings & SUB_DEL) == SUB_DEL);
		msgbase.close();
	}
	return canDelete;
}
// For the DigDistMsgReader Class: Returns whether or not the user can delete
// the last message they posted in the sub-board.
function DigDistMsgReader_CanDeleteLastMsg()
{
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		if (msgbase.cfg != null)
			canDelete = canDelete || ((msgbase.cfg.settings & SUB_DELLAST) == SUB_DELLAST);
		msgbase.close();
	}
	return canDelete;
}
// For the DigDistMsgReader Class: Returns whether or not the user can edit
// messages.
function DigDistMsgReader_CanEdit()
{
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		if (msgbase.cfg != null)
			canEdit = canEdit || ((msgbase.cfg.settings & SUB_EDIT) == SUB_EDIT);
		msgbase.close();
	}
	return canEdit;
}
// For the DigDistMsgReader Class: Returns whether or not message quoting
// is enabled.
function DigDistMsgReader_CanQuote()
{
	var canQuote = this.readingPersonalEmail || user.is_sysop;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		if (msgbase.cfg != null)
			canQuote = canQuote || ((msgbase.cfg.settings & SUB_QUOTE) == SUB_QUOTE);
		msgbase.close();
	}
	return canQuote;
}

// For the DigDistMsgReader Class: Displays the stock Synchronet message header file for
// a given message header.
//
// Parameters:
//  pMsgHdr: The message header object
function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
{
	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
		return;
	// Note: The message header has the following fields:
	// 'number': The message number
	// 'offset': The message offset
	// 'to': Who the message is directed to (string)
	// 'from' Who wrote the message (string)
	// 'subject': The message subject (string)
	// 'date': The date - Full text (string)

	// Generate a string containing the message's import date & time.
	//var dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", msgHeader.when_imported_time)
	// Use the date text in the message header, without the time
	// zone offset at the end.
	var dateTimeStr = pMsgHdr["date"].replace(/ [-+][0-9]+$/, "");

	// Check to see if there is a msghdr file in the sbbs/text/menu
	// directory.  If there is, then use it to display the message
	// header information.  Otherwise, output a default message header.
	var msgHdrFileOpened = false;
	var msgHdrFilename = this.GetMsgHdrFilenameFull();
	if (msgHdrFilename.length > 0)
	{
		var msgHdrFile = new File(msgHdrFilename);
		if (msgHdrFile.open("r"))
		{
			msgHdrFileOpened = true;
			var fileLine = null; // To store a line read from the file
			while (!msgHdrFile.eof)
			{
				// Read the next line from the header file
				fileLine = msgHdrFile.readln(2048);

				// fileLine should be a string, but I've seen some cases
				// where it isn't, so check its type.
				if (typeof(fileLine) != "string")
					continue;

				// Since we're displaying the message information ouside of Synchronet's
				// message read prompt, this script now has to parse & replace some of
				// the @-codes in the message header line, since Synchronet doesn't know
				// that the user is reading a message.
				console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, false, true));
				console.crlf();
			}
			msgHdrFile.close();
		}
	}

	// If the msghdr file didn't open (or doesn't exist), then output the default
	// header.
	if (!msgHdrFileOpened)
	{
		// Generate a string describing the message attributes, then output the default
		// header.
		var allMsgAttrStr = makeAllMsgAttrStr(pMsgHdr);
		console.print("\x01n\x01w" + charStr(HORIZONTAL_DOUBLE, 78));
		var horizSingleFive = charStr(HORIZONTAL_SINGLE, 5);
		console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr["from"].substr(0, console.screen_columns-12));
		console.print("\x01n\x01w" + horizSingleFive + "\x01cTo  \x01w\x01h: \x01b" + pMsgHdr["to"].substr(0, console.screen_columns-12));
		console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr["subject"].substr(0, console.screen_columns-12));
		console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12));
		console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + allMsgAttrStr.substr(0, console.screen_columns-12));
}

// For the DigDistMsgReader class: Returns the name of the msghdr file in the
// sbbs/text/menu directory. If the user's terminal supports ANSI, this first
// checks to see if an .ans version exists.  Otherwise, checks to see if an
// .asc version exists.  If neither are found, this function will return an
// empty string.
function DigDistMsgReader_GetMsgHdrFilenameFull()
{
  // If the user's terminal supports ANSI and msghdr.ans exists
  // in the text/menu directory, then use that one.  Otherwise,
  // if msghdr.asc exists, then use that one.
  var ansiFileName = "menu/msghdr.ans";
  var asciiFileName = "menu/msghdr.asc";
  var msgHdrFilename = "";
  if (console.term_supports(USER_ANSI) && file_exists(system.text_dir + ansiFileName))
    msgHdrFilename = system.text_dir + ansiFileName;
  else if (file_exists(system.text_dir + asciiFileName))
    msgHdrFilename = system.text_dir + asciiFileName;
  return msgHdrFilename;
}

// For the DigDistMsgReader class: Returns the number of messages in the current
// sub-board.  This will be either the number of headers in this.msgSearchHdrs
// for the current sub-board (if non-empty and a search type specified) or
//  pMsgbase: Optional - A MessageBase object
//  pCheckDeletedAttributes: Optional boolean - Whether or not to check the
//                           'deleted' attributes of the messages and not
//                           count deleted messages.  Defaults to false.
//
// Return value: The number of messages
function DigDistMsgReader_NumMessages(pMsgbase, pCheckDeletedAttributes)
	var checkDeletedAttributes = (typeof(pCheckDeletedAttributes) == "boolean" ? pCheckDeletedAttributes : false);

	var msgbase = null;
	var closeMsgbaseInThisFunc = false;
	if ((pMsgbase != null) && (typeof(pMsgbase) == "object"))
		msgbase = pMsgbase;
	else
	{
		msgbase = new MsgBase(this.subBoardCode);
		if (msgbase.open())
			closeMsgbaseInThisFunc = true;
		else
			return 0;
	}

	if (this.SearchingAndResultObjsDefinedForCurSub())
		numMsgs = this.msgSearchHdrs[this.subBoardCode].indexed.length;
	else if (this.hdrsForCurrentSubBoard.length > 0)
		numMsgs = this.hdrsForCurrentSubBoard.length;
	else if ((msgbase != null) && msgbase.is_open)
		// Count the number of readable messages in the messagebase (i.e.,
		// messages that are not deleted, unvalidated, or null headers)
		numMsgs = 0;
		var totalNumMsgs = msgbase.total_msgs;
		for (var msgIdx = 0; msgIdx < totalNumMsgs; ++msgIdx)
			if (isReadableMsgHdr(msgbase.get_msg_header(true, msgIdx, false), this.subBoardCode))

	// If the caller wants to check the deleted attributes, then do so.
	if ((numMsgs > 0) && checkDeletedAttributes)
	{
		var msgHdr;
		var originalNumMsgs = numMsgs;
		for (var msgIdx = 0; msgIdx < originalNumMsgs; ++msgIdx)
		{
			msgHdr = this.GetMsgHdrByIdx(msgIdx);
			if ((msgHdr.attr & MSG_DELETE) == MSG_DELETE)
			{
				--numMsgs;
				if (numMsgs < 0)
				{
					numMsgs = 0;
					break;
				}
			}
		}
	}

	if (closeMsgbaseInThisFunc)
		msgbase.close();

	return numMsgs;
}

// For the DigDistMsgReader class: Returns whether there are any non-deleted
// messages in the current sub-board.
//
// Return value: Boolean - Whether or not there are any non-deleted messages
//               in the current sub-board.
function DigDistMsgReader_NonDeletedMessagesExist()
{
	var messagesExist = false;

	var numMsgs = this.NumMessages();
	if (numMsgs > 0)
	{
		var msgHdr;
		for (var msgIdx = 0; (msgIdx < numMsgs) && !messagesExist; ++msgIdx)
		{
			msgHdr = this.GetMsgHdrByIdx(msgIdx);
			if ((msgHdr.attr & MSG_DELETE) == 0)
			{
				messagesExist = true;
				break;
			}
		}
	}

	return messagesExist;
}

// For the DigDistMsgReader class: Returns the highest message number (1-based), either from this.msgSearchHdrs
// (if it has search results for the current sub-board) or msgbase.  If
// there are no search results for the current sub-board in this.msgSearchHdrs,
// the highest message number is the same as the total number of messages
// in the sub-board (unless the Synchronet standard ever changes..).
function DigDistMsgReader_HighestMessageNum()
{
	var highestMessageNum = 0;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		highestMessageNum = msgbase.total_msgs;
		msgbase.close();
	}
	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
		highestMessageNum = this.msgSearchHdrs[this.subBoardCode].indexed.length;
}

// For the DigDistMsgReader class: Returns whether or not a message number (1-based)
// is a valid and existing message number.  This is intended for validating user
// input where not all the message numbers are consecutive.
//
// Parameters:
//  pMsgNum: The message number to validate
//
// Return value: Boolean - Whether or not the message number 
function DigDistMsgReader_IsValidMessageNum(pMsgNum)
{
	// The message numbers start at 1
	if (pMsgNum < 1)
		return false;
	// If there are search results for the current sub-board, then check to see if
	// the message number exists in its indexed array.  Otherwise, check with
	// msgbase.
	var msgNumIsValid = false;
	if (this.SearchingAndResultObjsDefinedForCurSub())
		msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length));
	else
	{
		var msgbase = new MsgBase(this.subBoardCode);
		if (msgbase.open())
		{
			msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= msgbase.total_msgs));
			msgbase.close();
		}
	}
	return msgNumIsValid;
}

// For the DigDistMsgReader class: Returns a message header by index.  Will look
// in this.msgSearchHdrs if it's not empty, then in this.hdrsForCurrentSubBoard
// if it's not empty, then from msgbase.
//
// Parameters:
//  pMsgIdx: The message index (0-based)
//  pExpandFields: Whether or not to expand fields.  Defaults to false.
//  pMsgbase: Optional - An open MsgBase object.  If not passed, the sub-board will be opened in this method.
function DigDistMsgReader_GetMsgHdrByIdx(pMsgIdx, pExpandFields, pMsgbase)
	var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false);

	var msgHdr = null;
	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
	    (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
	{
		if ((pMsgIdx >= 0) && (pMsgIdx < this.msgSearchHdrs[this.subBoardCode].indexed.length))
		{
			if (expandFields)
				msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx];
			else
				msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, false, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx].number, expandFields);
		}
	else if (this.hdrsForCurrentSubBoard.length > 0)
	{
		if ((pMsgIdx >= 0) && (pMsgIdx < this.hdrsForCurrentSubBoard.length))
			if (expandFields)
				msgHdr = this.hdrsForCurrentSubBoard[pMsgIdx];
			else
				msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, false, this.hdrsForCurrentSubBoard[pMsgIdx].number, expandFields);
	else
		msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, true, pMsgIdx, pExpandFields);
	if (msgHdr == null)
		msgHdr = getBogusMsgHdr();
}

// For the DigDistMsgReader class: Returns a message header by message number
// (1-based).  Will look in this.msgSearchHdrs if it's not empty, then in
// this.hdrsForCurrentSubBoard if it's not empty, then from msgbase.
//
// Parameters:
//  pMsgNum: The message number (1-based)
//  pExpandFields: Whether or not to expand fields.  Defaults to false.
//
// Return value: The message header for the message number, or null on error
function DigDistMsgReader_GetMsgHdrByMsgNum(pMsgNum, pExpandFields)
	var msgHdr = null;
	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
			(this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
	{
		if ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length))
			msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgNum-1];
	}
	else if (this.hdrsForCurrentSubBoard.length > 0)
	{
		if ((pMsgNum > 0) && (pMsgNum <= this.hdrsForCurrentSubBoard.length))
			msgHdr = this.hdrsForCurrentSubBoard.length[pMsgNum-1];
		var msgbase = new MsgBase(this.subBoardCode);
		if (msgbase.open())
			if ((pMsgNum > 0) && (pMsgNum <= msgbase.total_msgs))
			{
				var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false);
				msgHdr = msgbase.get_msg_header(true, pMsgNum-1, expandFields);
			}
			msgbase.close();
	if (msgHdr == null)
		msgHdr = getBogusMsgHdr();
}

// For the DigDistMsgReader class: Returns a message header by absolute message
// number.  If there is a problem, this method will return null.
//
// Parameters:
//  pMsgNum: The absolute message number
//  pExpandFields: Whether or not to expand fields.  Defaults to false.
//  pGetVoteInfo: Whether or not to get voting information.  Defaults to false.
//
// Return value: The message header for the message number, or null on error
function DigDistMsgReader_GetMsgHdrByAbsoluteNum(pMsgNum, pExpandFields, pGetVoteInfo)
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false);
		var getVoteInfo = (typeof(pGetVoteInfo) == "boolean" ? pGetVoteInfo : false);
		msgHdr = msgbase.get_msg_header(false, pMsgNum, expandFields, getVoteInfo);
		msgbase.close();
	if (msgHdr == null)
		msgHdr = getBogusMsgHdr();
}

// For the DigDistMsgReader class: Takes an absolute message number and returns
// its message index (offset).  On error, returns -1.
//
// Parameters:
//  pMsgNum: The absolute message number
//
// Return value: The message's index.  On error, returns -1.
function DigDistMsgReader_AbsMsgNumToIdx(pMsgNum)
{
	var msgIdx = -1;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		msgIdx = absMsgNumToIdx(msgbase, pMsgNum);
		msgbase.close();
	}
	return msgIdx;
}

// For the DigDistMsgReader class: Takes a message index and returns
// its absolute message number.  On error, returns -1.
//
// Parameters:
//  pMsgIdx: The message index
//
// Return value: The message's absolute message number.  On error, returns -1.
function DigDistMsgReader_IdxToAbsMsgNum(pMsgIdx)
{
	var msgIdx = -1;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		msgIdx = idxToAbsMsgNum(msgbase, pMsgIdx);
		msgbase.close();
	}
	return msgIdx;
}

// This function takes an absolute message number for a given messagebase objectand
// and returns the message index (offset).  On error, returns -1.
//
// Parameters:
//  pMsgbase: The messagebase object from which to retrieve the message header
//  pMsgNum: The absolute message number
//
// Return value: The message's index, or -1 on error.
function absMsgNumToIdx(pMsgbase, pMsgNum)
{
	if ((pMsgbase == null) || (typeof(pMsgbase) != "object"))
		return -1;
	if (!pMsgbase.is_open)
		return -1;

	// If pMsgNum is 0xffffffff (0xffffffff, or ~0), that is a special value
	// for the user's scan_ptr meaning it should point to the latest message
	// in the messagebase.
		messageIdx = pMsgbase.total_msgs - 1; // Or this.NumMessages() - 1 but can't because this isn't a class member function
	else
	{
		var msgHdr = pMsgbase.get_msg_header(false, pMsgNum, false);
		if ((msgHdr == null) && gCmdLineArgVals.verboselogging)
		{
			writeToSysAndNodeLog("Message area " + pMsgbase.cfg.code + ": Tried to get message header for absolute message number " +
			                     pMsgNum + " but got a null header object.");