// This code is licensed under the same terms as Habitica: // https://raw.githubusercontent.com/HabitRPG/habitrpg/develop/LICENSE /* ========================================== */ /* [Users] Required script data to fill in */ /* ========================================== */ const USER_ID = "PasteYourUserIdHere" const API_TOKEN = "PasteYourApiTokenHere" // Do not share this to anyone const WEB_APP_URL = "PasteGeneratedWebAppUrlHere" /* ========================================== */ /* [Users] Required customizations to fill in */ /* ========================================== */ /* ========================================== */ /* [Users] Optional customizations to fill in */ /* ========================================== */ /* ========================================== */ /* [Users] Do not edit code below this line */ /* ========================================== */ const AUTHOR_ID = "0034eb14-b4d8-494e-8386-d3f33cff7922" const SCRIPT_NAME = "Duel Announcer Bot" const HEADERS = { "x-client" : AUTHOR_ID + " - " + SCRIPT_NAME, "x-api-user" : USER_ID, "x-api-key" : API_TOKEN, } const scriptProperties = PropertiesService.getScriptProperties() // Constants can have properties changed // Info used to save/load values const GROUP_ID_KEY = "GROUP_ID_KEY" const BOT_USERNAME_KEY = "BOT_USERNAME_KEY" const DUEL_SETUP_IN_PROGRESS_KEY = "DUEL_SETUP_IN_PROGRESS_KEY" const PARTICIPANT1_KEY = "PARTICIPANT1_KEY" const PARTICIPANT2_KEY = "PARTICIPANT2_KEY" const RESPONDER_KEY = "RESPONDER_KEY" const WAGER_KEY = "WAGER_KEY" const SCORE_NEEDED_KEY = "SCORE_NEEDED_KEY" const TIMESTAMP_KEY = "TIMESTAMP_KEY" const WINNER_KEY = "WINNER_KEY" const LOSER_KEY = "LOSER_KEY" const WAGER_WINNER_KEY = "WAGER_WINNER_KEY" const SCORE_NEEDED_WINNER_KEY = "SCORE_NEEDED_WINNER_KEY" const HASH_ALL_KEY = "HASH_ALL_KEY" const HASH_SUM_KEY = "HASH_SUM_KEY" var groupIdKey = "" var botUsernameKey = "" var duelSetupInProgressKey = "" var participant1Key = "" var participant2Key = "" var responderKey = "" var wagerKey = "" var scoreNeededKey = "" var timestampKey = "" var winnerKey = "" var loserKey = "" var wagerWinnerKey = "" var scoreNeededWinnerKey = "" var hashAllKey = "" var hashSumKey = "" // These are all of the search terms the bot is monitoring for const HELP = "HELP" const FAST_START_DUEL = "FAST START DUEL" const END_DUEL = "END DUEL" const AUTOMATED_ERROR_MSG_FROM_SCORING_SCRIPT1 = "This is an automated message generated by the duel script." const AUTOMATED_ERROR_MSG_FROM_SCORING_SCRIPT2 = "ERROR DETECTED:" const START_OVER = "START OVER" const CEASE = "CEASE SETUP" const START_DUEL_ALL_PARAMS = "I WANT TO DUEL AND I KNOW ALL THE PARAMETERS" const START_DUEL_WITH_WALKTHROUGH = "I WANT TO DUEL" const START_DUEL_OPPONENT = "OPPONENT" // The instructions tell them this search term must be followed by a colon, but in case they forget, the script doesn't require the colon const START_DUEL_WAGER = "WAGER" const START_DUEL_SCORE_NEEDED = "SCORE NEEDED" const COUNTEROFFER = "COUNTEROFFER" const DISAGREE = "DISAGREE" const AGREE = "AGREE" const DECLINE = "DECLINE" // These are all of the messages from the announcer bot const MSG_CONTACT_AUTHOR1 = " contact the author of this script (@mike_the_monk)" const MSG_CONTACT_AUTHOR2 = " by sending him a [private message](https://habitica.com/private-messages?uuid=0034eb14-b4d8-494e-8386-d3f33cff7922) in Habitica." const MSG_WIKI_PAGE_LIST_OF_COMMANDS = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#List_of_all_duel_bot_commands)" const MSG_WIKI_PAGE_FAST_START = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#How_to_use_FAST_START_DUEL)" const MSG_WIKI_PAGE_PARAMETER_FORMATTING = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#Ensuring_correct_formatting_for_the_duel_parameters)" const MSG_WIKI_PAGE_STARTING_DUEL = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#Starting_your_duel)" const MSG_WIKI_PAGE_ENDING_DUEL = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#Ending_your_duel)" const MSG_INTRO = "Successfully set up duel announcer bot – I can now help this Group set up duels between two people." const MSG_NOT_ENOUGH_SEMICOLONS = "I couldn't start the duel using what you said, there aren't enough semicolons. What I'm expecting is the opponent's name followed by a semicolon, then your wager followed by a semicolon, then the score needed to win followed by a semicolon. It might look something like @opponent;100;50;" const PARTIAL_MSG_DUEL_END_TO_PASTE1 = " GP.\n\nTo end the duel, both of you please copy and paste the following into the Notes section of the Duel End button and click it. For more detailed instructions, see the" const PARTIAL_MSG_DUEL_END_TO_PASTE2 = ".\n\n ``` \nENDING DUEL:" const MSG_DUEL_END_TO_PASTE = PARTIAL_MSG_DUEL_END_TO_PASTE1 + MSG_WIKI_PAGE_ENDING_DUEL + PARTIAL_MSG_DUEL_END_TO_PASTE2 const MSG_HELP1 = "Below are all the commands I can recognize when you tag me in a post." const MSG_HELP_OPTIONAL = " You won't be able to use any of those commands yet, I'm currently helping set up a duel and can only set up one at a time. Once I'm done with this one, then you can start." const MSG_HELP_JOINER = " \n\n" const PARTIAL_MSG_COMMAND_LIST1 = "To set up a duel:\n+ I WANT TO DUEL - if you want me to help you set it up\n+ I WANT TO DUEL AND ALL THE PARAMETERS - if you don't want me to help you set it up\n+ FAST START DUEL - if I'm in the middle of helping someone else set up a duel" const PARTIAL_MSG_COMMAND_LIST2 = "The duel parameters to include when setting up a duel:\n+ OPPONENT:\n+ WAGER:\n+ SCORE NEEDED:" const PARTIAL_MSG_COMMAND_LIST3 = "How someone can respond:\n+ AGREE\n+ DISAGREE (which is confirmed by saying DECLINE)\n+ COUNTEROFFER - and this one should include a new proposal for WAGER: and SCORE NEEDED:" const PARTIAL_MSG_COMMAND_LIST4 = "If the duel initiator wants to restart the process and propose different duel parameters:\n+ START OVER" const PARTIAL_MSG_COMMAND_LIST5 = "If the duel initiator doesn't want to duel anymore:\n+ CEASE SETUP" const PARTIAL_MSG_COMMAND_LIST6 = "And of course, there's the HELP command, which you already figured out." const MSG_HELP2 = MSG_HELP_JOINER + PARTIAL_MSG_COMMAND_LIST1 + MSG_HELP_JOINER + PARTIAL_MSG_COMMAND_LIST2 + MSG_HELP_JOINER + PARTIAL_MSG_COMMAND_LIST3 + MSG_HELP_JOINER + PARTIAL_MSG_COMMAND_LIST4 + MSG_HELP_JOINER + PARTIAL_MSG_COMMAND_LIST5 + MSG_HELP_JOINER + PARTIAL_MSG_COMMAND_LIST6 const MSG_DUEL_SETUP_IN_PROGRESS = "I'm currently helping set up a duel and can only set up one at a time. Once I'm done with this one, then I can help you start.\n\nYou can override this by using the FAST START DUEL command, which is for experienced duelists. You can read more about it on the" + MSG_WIKI_PAGE_FAST_START + "." const MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER1 = ", I'm waiting for " const MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER2 = " to respond to you. I won't be able to process your messages until they respond." const MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER3 = "\n\nIf there's been an error and I've grabbed the wrong username for the person I'm waiting on, you can use the command START OVER to begin this process all over again.\n\nIf you've decided you don't want to duel anymore, you can use the command CEASE SETUP." const MSG_DUEL_START_FROM_STARTING_OVER = "Okay, starting over.\n\n" const MSG_SETUP_CEASED = "Okay, the duel is off.\n\nIf anyone wants to set up a duel, tag me and say I WANT TO DUEL." const MSG_DUEL_START1 = "I can help you set up your duel. You'll need to answer some questions and then put those answers in the correct formatting.\n\n+ Who do you want to be your duel **opponent**? You'll tag them with their @username.\n+ How much do you want to **wager**? *Both of you will pay that much GP into the pot immediately when the duel starts. Whoever wins gains the GP in the pot, which is double the wager amount.* \n+ What is the **score needed** to win? *As you complete your tasks, you score duel points as you reach towards this goal.* \n\nYour answers to these three questions are called the 'duel parameters'. Please include all of the duel parameters in a single post with the following format: Name of parameter, colon, your answer, semicolon. And, don't forget to tag me in the post.\n\nHere's an example for duel wagering 100 GP that requires 50 points to win. You'll see the parameters in the correct format, and you'll see that I'm tagged in the post.\n\n``` \n" const MSG_DUEL_START2 = "\nOpponent: @opponent;" const MSG_DUEL_START3 = "\nWager: 100; \nScore needed: 50;\n```" const PARTIAL_MSG_UNKNOWN_COMMAND1 = ", it looks like you tagged me but I'm not sure what you're trying to do.\n\nIf you want a list of all the commands you can say, tag me and say HELP, or you can read the" const PARTIAL_MSG_UNKNOWN_COMMAND2 = ".\n\nRight now, the words I'm listening for are" const PARTIAL_MSG_UNKNOWN_COMMAND2_COUNTER = ".\n\nIf you're trying to counteroffer, the words I'm listening for are" const MSG_UNKNOWN_COMMAND = PARTIAL_MSG_UNKNOWN_COMMAND1 + MSG_WIKI_PAGE_LIST_OF_COMMANDS + PARTIAL_MSG_UNKNOWN_COMMAND2 const PARTIAL_MSG_UNKNOWN_COMMAND_BASIC = "WAGER: and SCORE NEEDED: all in the same post, and of course, don't forget to tag me in that post. Here's an example of how it should look:\n\n``` \n" const MSG_UNKNOWN_COMMAND_COUNTER = PARTIAL_MSG_UNKNOWN_COMMAND1 + MSG_WIKI_PAGE_LIST_OF_COMMANDS + PARTIAL_MSG_UNKNOWN_COMMAND2_COUNTER + " COUNTEROFFER and " + PARTIAL_MSG_UNKNOWN_COMMAND_BASIC const MSG_UNKNOWN_COMMAND_COUNTER2 = MSG_DUEL_START3 + "\n\nIf you don't want to counteroffer, you can also say AGREE or DISAGREE." const MSG_UNKNOWN_COMMAND_SETUP = " OPPONENT: and " + PARTIAL_MSG_UNKNOWN_COMMAND_BASIC const MSG_UNKNOWN_COMMAND_SETUP2 = MSG_DUEL_START2 + MSG_DUEL_START3 const MSG_UNKNOWN_COMMAND_ANSWER = " AGREE or DISAGREE or COUNTEROFFER, and of course, don't forget to tag me in that post." const MSG_UNKNOWN_COMMAND_START = ": \n+ I WANT TO DUEL AND I KNOW ALL THE PARAMETERS\n+ I WANT TO DUEL" const PARTIAL_MSG_NOT_ALL_PARAMETERS1_1 = ", I saw that you included some of the duel parameters in your post but you didn't include all of them. Or, perhaps you included them but didn't have a semicolon after, which meant the script couldn't read them.\nPlease make sure all of the duel parameters are included in a single post like in the example below. If you need help, consult the" const MSG_CODE_BLOCK_START = "\n\n``` \n" const MSG_NOT_ALL_PARAMETERS1 = PARTIAL_MSG_NOT_ALL_PARAMETERS1_1 + MSG_WIKI_PAGE_PARAMETER_FORMATTING + "." + MSG_CODE_BLOCK_START const MSG_NOT_ALL_PARAMETERS2 = MSG_DUEL_START2 // Don't include this for the Counteroffer error message const MSG_NOT_ALL_PARAMETERS3 = MSG_DUEL_START3 const MSG_NOT_A_NUMBER_START = "\nError: It looks like for " const MSG_BOTH_ARE_NOT_NUMBERS = "Wager and Score Needed" const MSG_SCORE_NEEDED_IS_NOT_NUMBER = "Score Needed" const MSG_WAGER_IS_NOT_NUMBER = "Wager" const MSG_NOT_A_NUMBER_END = ", you entered something that was not a number. Please try again." const MSG_AGREE_DISAGREE_COUNTEROFFER1 = ", do you want to duel " const MSG_AGREE_DISAGREE_COUNTEROFFER2 = "? You both will wager " const MSG_AGREE_DISAGREE_COUNTEROFFER3 = " GP, losing that GP immediately upon starting the duel, and the first person to " const MSG_AGREE_DISAGREE_COUNTEROFFER4 = " duel points wins the pot of " const PARTIAL_MSG_AGREE_DISAGREE_COUNTEROFFER5 = " GP.\n\nYou can say AGREE, DISAGREE, or COUNTEROFFER, and make sure to tag me in your response.\n\nIf you choose COUNTEROFFER, you can propose different duel parameters, formatting them like the example below. If you need help, please consult the" const MSG_AGREE_DISAGREE_COUNTEROFFER5 = PARTIAL_MSG_AGREE_DISAGREE_COUNTEROFFER5 + MSG_WIKI_PAGE_PARAMETER_FORMATTING + "." + MSG_CODE_BLOCK_START const MSG_AGREE_DISAGREE_COUNTEROFFER6 = "\nWager: #;\nScore needed: #;\n```" const MSG_DISAGREE = "You chose DISAGREE. Does that mean you want to decline the duel entirely? If yes, this duel is off.\n\nTo decline, tag me in a post and say DECLINE.\n\nOtherwise, you can still choose AGREE to start the duel or COUNTEROFFER to propose different duel parameters." const MSG_DECLINE1 = "Okay, this duel is off. " const MSG_DECLINE2 = ", you can start the process over again for dueling someone else. Or, if someone else wants to initiate a duel, they can do so now." const MSG_COUNTEROFFER = ", do you agree with the counteroffer from " const MSG_AGREE0 = "# The duel is on!\n\n" const PARTIAL_MSG_AGREE_FAST_START1 = "If you both agree to the duel, check to ensure the info below is correct, especially the username of both opponents. *If I didn't correctly grab the duel parameters, you'll have to try again on setting up.* ***Do not*** *use the info below if it didn't save correctly.*\n\nTo start the duel, copy the appropriate text below. If you need additional help, see the" const PARTIAL_MSG_AGREE_FAST_START2 = " for more instructions.\n\n" const MSG_AGREE_FAST_START = PARTIAL_MSG_AGREE_FAST_START1 + MSG_WIKI_PAGE_STARTING_DUEL + PARTIAL_MSG_AGREE_FAST_START2 const PARTIAL_MSG_AGREE1_1 = "To start the duel:\n+ copy the appropriate text below\n+ head over to your Custom Rewards section in Habitica\n+ find the button that will start the duel\n+ paste the text you copied into the Notes section of that button\n+ click the button to immediately starts the duel (make sure you have enough GP for your wager!) \n\nOnce your duel starts, completing your tasks earns you duel points.\n\nIf you need help with the above instructions, please consult the" const PARTIAL_MSG_AGREE1_2 = ".\n\n --- \n\n" const MSG_AGREE1 = PARTIAL_MSG_AGREE1_1 + MSG_WIKI_PAGE_STARTING_DUEL + PARTIAL_MSG_AGREE1_2 const MSG_AGREE2 = ", here is the text for you to copy: `" const MSG_AGREE3 = "` \n\n" const MSG_AGREE4 = MSG_AGREE2 const MSG_AGREE5 = "`\n\n --- \n\nSince I can only set up one duel at a time and this one is now successfully set up, **I'm free to set up another duel if anyone wants!**" // Error messages from the announcer bot const MSG_ERROR_BEGINNING = "Error with " const MSG_ERROR_ENDING = ". Please" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 const MSG_ERROR_CASECATEGORY_INIT = "initializing `caseCategory`" const MSG_ERROR_CASECATEGORY = "`caseCategory`" const MSG_ERROR_CASESPECIFIC = "caseSpecific is " const MSG_ERROR_CHECKRESPONDER = "`checkResponder`" const MSG_ERROR_CASE100S = "`createCase100s`" const MSG_ERROR_ENDING_DUEL_START = ", there was an error trying to end the duel.\n\nFor more information about this error, have the player who runs the bot account (" const MSG_DUEL_END_WINNER_PART_1 = "We have a winner!\n\n" const MSG_DUEL_END_WINNER_PART_2 = " achieved " const MSG_DUEL_END_WINNER_PART_3 = " duel points first in a duel against " const MSG_DUEL_END_WINNER_PART_4 = " and won " const MSG_DUEL_END_WINNER_PART_5 = ";" const MSG_DUEL_END_WINNER_PART_6 = ";\n ``` " const MSG_ERROR_ENDING_DUEL_END = ") \n+ open up the duel bot Google Apps Script,\n+ open up the Console log, and \n+ read the error message there.\nFor further instructions, see the wiki page.\n\nIf more in-depth troubleshooting is needed, you or the player who runs the bot account should" + MSG_CONTACT_AUTHOR1 + MSG_CONTACT_AUTHOR2 const MSG_ERROR_CHECK_START_NUMERIC = "`checkCompleteDuelStartStringNumeric`" const MSG_ERROR_CREATE_START = "`createDuelStartString`" const MSG_ERROR_CASE300S = "`createCase300s`" const MSG_ERROR_PARSESAVE = "`parseAndSave`" const MSG_ERROR_RETRIEVECREATE = "`retrieveSavedParametersAndCreateMessage`" const MSG_ERROR_TOGGLERESPONDER = "`toggleResponder`" const MSG_ERROR_CASE700S_NUMERIC = "`createCase700s, validInputStringNumeric`" const MSG_ERROR_CASE700S_SEMICOLONS = "`createCase700s, validInputStringSemicolons`" // Console log error messages const MSG_ERROR_CONSOLE_LOG_START = "The error occurred because " const MSG_ERROR_CONSOLE_LOG_MID1 = "You should look over the Duel End string that was posted in the Group to see if it looks right, or" const MSG_ERROR_CONSOLE_LOG_MID2 = "Ask the person who posted where their post came from. If it was automatically generated by the script, this check should have passed. You may need to" const MSG_ERROR_CONSOLE_LOG_CONTACT = "for him to see if it looks right," const MSG_ERROR_CONSOLE_LOG_BOTH_ARE_NOT_NUMBERS = "neither Wager nor Score Needed were numbers. " const MSG_ERROR_CONSOLE_LOG_SCORE_NEEDED_IS_NOT_NUMBER = "Wager was not a number. " const MSG_ERROR_CONSOLE_LOG_WAGER_IS_NOT_NUMBER = "Score Needed was not a number. " const MSG_ERROR_CONSOLE_LOG_FAILED_FINAL_CHECK_BOTH = "it failed the final two checks, both the 'alphabetized username check' and the 'hash check'. " const MSG_ERROR_CONSOLE_LOG_FAILED_FINAL_CHECK_USERNAME = "it failed one of the final two checks: the 'alphabetized username check'. " const MSG_ERROR_CONSOLE_LOG_FAILED_FINAL_CHECK_HASH = "it failed one of the final two checks: the 'hash check'. " function doOneTimeSetup() { var groupIdKey = GROUP_ID_KEY const PARTY_ID = getParty() scriptProperties.setProperty(groupIdKey, PARTY_ID) const GROUP_ID = scriptProperties.getProperty(groupIdKey) // Create the webhook const options = { "groupId" : GROUP_ID, } const payload = { "url" : WEB_APP_URL, "label" : SCRIPT_NAME + " Webhook", "type" : "groupChatReceived", "options" : options, } apiMult_createNewWebhookNoDuplicates(payload) // Wait 5 seconds Utilities.sleep(5000) // Initiate saved values initScriptProperties() // Wait 5 seconds Utilities.sleep(5000) // Post a test message to the Group to show that the account worked correctly. api_postChatMessageToGroup({"message": MSG_INTRO}) // I will use this message to save the bot username for later reference. return HtmlService.createHtmlOutput() } // do things when the webhook runs function doPost(e) { let response = api_getChatMessagesFromGroup() let chatMessages = JSON.parse(response).data let text = chatMessages[0].text let usernameWhoPosted = chatMessages[0].username let uuidWhoPosted = chatMessages[0].uuid // Ignore messages posted by the bot, we don't want to start a recursive loop where the bot responds to the bot. var botUsernameKey = BOT_USERNAME_KEY var botUsername = scriptProperties.getProperty(botUsernameKey) // The only exception is the intro message, which I will use to save the bot username for later reference. if (uuidWhoPosted == USER_ID) { let indexIntro = findIndex(MSG_INTRO, text) // If intro message is found, the username who posted is the bot username if (indexIntro != -1) { // Add @ to the front, that way you've saved the version where the bot username is tagged botUsername = appendAtSymbol(usernameWhoPosted) // Save the value scriptProperties.setProperty(botUsernameKey, botUsername) } } // Ignore messages posted by the bot, we don't want to start a recursive loop where the bot response to the bot. // Only run script if someone else posted the most recent message. else if ( uuidWhoPosted != USER_ID ) { // Check if the bot was tagged in the most recent post. let indexBotUsername = findIndex(botUsername, text, true) // The rest of the script runs only if the bot was tagged. if ( indexBotUsername != -1 ) { // Check if search terms are found, not case-sensitive let indexFastStartDuel = findIndex(FAST_START_DUEL, text, false) let indexEndDuel = findIndex(END_DUEL, text, false) let indexHelp = findIndex(HELP, text, false) let indexStartOver = findIndex(START_OVER, text, false) let indexCease = findIndex(CEASE, text, false) let indexStartDuelOpponent = findIndex(START_DUEL_OPPONENT, text, false) let indexStartDuelWager = findIndex(START_DUEL_WAGER, text, false) let indexStartDuelScoreNeeded = findIndex(START_DUEL_SCORE_NEEDED, text, false) let indexCounteroffer = findIndex(COUNTEROFFER, text, false) let indexDisagree = findIndex(DISAGREE, text, false) let indexAgree = findIndex(AGREE, text, false) let indexDecline = findIndex(DECLINE, text, false) let indexStartDuelAllParams = findIndex(START_DUEL_ALL_PARAMS, text, false) let indexStartDuelWithWalkthrough = findIndex(START_DUEL_WITH_WALKTHROUGH, text, false) // Search terms from the automated error messages are case-sensitive let indexAutoError1 = findIndex(AUTOMATED_ERROR_MSG_FROM_SCORING_SCRIPT1, text, true) let indexAutoError2 = findIndex(AUTOMATED_ERROR_MSG_FROM_SCORING_SCRIPT2, text, true) // Grab saved values var duelSetupInProgressKey = DUEL_SETUP_IN_PROGRESS_KEY var participant1Key = PARTICIPANT1_KEY var participant2Key = PARTICIPANT2_KEY var responderKey = RESPONDER_KEY var wagerKey = WAGER_KEY var scoreNeededKey = SCORE_NEEDED_KEY var timestampKey = TIMESTAMP_KEY var duelSetupInProgress = Number(scriptProperties.getProperty(duelSetupInProgressKey)) var participant1 = scriptProperties.getProperty(participant1Key) var participant2 = scriptProperties.getProperty(participant2Key) var responder = Number(scriptProperties.getProperty(responderKey)) // Switch-case will make things cleaner/easier. First, do category, then drill deeper into the category let caseCategory = createCaseCategory(indexAutoError1, indexAutoError2, indexFastStartDuel, indexEndDuel, indexHelp, indexStartOver, indexCease, duelSetupInProgress, usernameWhoPosted, participant1, participant2, responder, indexCounteroffer) let caseSpecific = 0 // initialize it // Find category (of message posted), which then finds specific case for specific message switch (caseCategory) { case 0: // shouldn't be possible to get this condition, hence the error message. api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CASECATEGORY_INIT + MSG_ERROR_ENDING}) break case 100: // Category for highest-priority messages. Descriptions of each specific case are in the comments below. caseSpecific = createCase100s(indexAutoError1, indexAutoError2, indexFastStartDuel, indexEndDuel, text, usernameWhoPosted, botUsername, participant2Key, wagerKey, scoreNeededKey) // The function "createCase100s" does all the actions for the specific cases below: // 101: Automated error messages from scoring script detected – do nothing. // 102: Fast start with insufficient semicolons. // 103: Fast start but numeric parameters were not numbers. // 104: Fast start with no errors. // 105: Duel end. break case 200: // Category for help messages caseSpecific = createCase200s(indexHelp, duelSetupInProgress) // The function "createCase200s" does all the actions for the specific cases below: // 201: Help message while duel setup in progress. // 202: Help message while setup not in progress. break case 300: // Category for when incorrect person responded, including the START OVER / CEASE commands caseSpecific = createCase300s(indexStartOver, indexCease, usernameWhoPosted, botUsername, duelSetupInProgress, duelSetupInProgressKey, participant1, participant1Key, participant2, responder) // The function "createCase300s" does all the actions for the specific cases below: // 301: The username who posted isn't any of those allowed to respond. // 302: Incorrect responder. // 303: Duel initiator wants to start over // 304: Duel initiator wants to cease duel setup entirely break case 400: // Category for duel initiator setting up parameters caseSpecific = createCase400s(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, duelSetupInProgressKey, participant1Key, participant2Key, responderKey, wagerKey, scoreNeededKey, timestampKey) // The function "createCase400s" does all the actions for the specific cases below: // 401: Duel initiator setting up parameters but didn't include ANY parameters in the post. // 402: Duel initiator setting up parameters but didn't include ALL parameters in the post. // 403: Numeric parameters weren't entered as numbers. // 404: Duel initiator set up all parameters. break case 500: // Category for counteroffer caseSpecific = createCase500s(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, participant1Key, participant2Key, responder, responderKey, wagerKey, scoreNeededKey, timestampKey) // The function "createCase500s" does all the actions for the specific cases below: // 501: Counteroffer didn't include any parameters in the post. // 502: Counteroffer included only some parameters in the post. // 503: Numeric parameters weren't entered as numbers. // 504: Counteroffer with all parameters. break case 600: // Category for responses other than counteroffer caseSpecific = createCase600s(indexDisagree, indexAgree, indexDecline, usernameWhoPosted, botUsernameKey, participant1, participant1Key, participant2, participant2Key, responder, responderKey, wagerKey, scoreNeededKey, timestampKey) // The function "createCase600s" does all the actions for the specific cases below: // 601: Responder didn't include any of the Answer search terms. // 602: Disagree. // 603: Decline. // 604: Agree. break case 700: // Category for when a duel is not currently being set up caseSpecific = createCase700s(indexStartDuelAllParams, indexStartDuelWithWalkthrough, indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, duelSetupInProgressKey, participant1Key, participant2Key, responderKey, wagerKey, scoreNeededKey, timestampKey) // The function "createCase700s" does all the actions for the specific cases below: // 701: Person who posted didn't include any terms that would start a duel. // 702: Identical to case 401, initiator didn't include any parameters. // 703: Numeric parameters were not numbers. Nearly identical to case 403 except this is the single-input-string version (i.e. no command terms like "Wager:") // 704: Duel initiator set up parameters. Nearly identical to case 404 except this is the single-input-string version (i.e. no command terms like "Wager:") // 705: Initiator setting up a duel, identical to case 404. // 706: Initiator setting up a duel, identical to the 400s (except 404). // 707: Initiator setting up a duel with walkthrough messages. break default: // shouldn't be possible to get this condition, hence the error message. api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CASECATEGORY + MSG_ERROR_ENDING}) break } // If I need to debug, un-comment out the lines below so I can see which case is happening. // api_postChatMessageToGroup({"message": MSG_ERROR_CASESPECIFIC + caseSpecific}) // debugGetSavedValues() // Posts all the saved values. } return HtmlService.createHtmlOutput() } } function getParty(){ let paramsTemplate = { "method": "get", "headers": HEADERS, } let response = UrlFetchApp.fetch("https://habitica.com/api/v3/groups/party", paramsTemplate) let party = JSON.parse(response).data let partyId = party._id return partyId } // Gets all of a user's Groups (Guilds or party) function api_getGroups(payload) { const params = { "method" : "get", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/groups" return UrlFetchApp.fetch(url, params) } // Send a notification as a private message regardless of if they're enabled function api_sendPrivateMessageAlways(payload) { const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/members/send-private-message"; return UrlFetchApp.fetch(url, params) } // Create a webhook if no duplicate exists function apiMult_createNewWebhookNoDuplicates(payload) { const response = api_getWebhooks() const webhooks = JSON.parse(response).data var duplicateExists = 0 for (var i in webhooks) { if (webhooks[i].label == payload.label) { duplicateExists = 1 } } // If webhook to be created doesn't exist yet if (!duplicateExists) { api_createNewWebhook(payload) } } // Used to see existing webhooks, and therefore if there's a duplicate function api_getWebhooks() { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user/webhook" return UrlFetchApp.fetch(url, params) } // Creates a webhook (as part of the "don't make it if there's a duplicate" function) function api_createNewWebhook(payload) { const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user/webhook" return UrlFetchApp.fetch(url, params) } // Sets initial properties that will be used/saved later. function initScriptProperties() { resetDuelSetup() // Since most of these will need to be reset after each duel setup, I put them all in a separate function scriptProperties.setProperty(BOT_USERNAME_KEY, "userBot") // The only one I don't want to reset each time is bot username resetDuelEnd() // Also initialize saved values when a duel is ending } // Resets saved values for duel setup function resetDuelSetup(){ var timestampInit = Date.now() scriptProperties.setProperty(DUEL_SETUP_IN_PROGRESS_KEY, 0) scriptProperties.setProperty(PARTICIPANT1_KEY, "user1") scriptProperties.setProperty(PARTICIPANT2_KEY, "user2") scriptProperties.setProperty(RESPONDER_KEY, 1) scriptProperties.setProperty(WAGER_KEY, 0) scriptProperties.setProperty(SCORE_NEEDED_KEY, 0) scriptProperties.setProperty(TIMESTAMP_KEY, timestampInit) } // Reset saved values for duel end function resetDuelEnd(){ scriptProperties.setProperty(WINNER_KEY, "@personWhoWon") scriptProperties.setProperty(LOSER_KEY, "@personWhoDidntWin") scriptProperties.setProperty(WAGER_WINNER_KEY, 0) scriptProperties.setProperty(SCORE_NEEDED_WINNER_KEY, 0) scriptProperties.setProperty(HASH_ALL_KEY, 0) scriptProperties.setProperty(HASH_SUM_KEY, 0) } function api_postChatMessageToGroup(payload) { var groupIdKey = GROUP_ID_KEY const GROUP_ID = scriptProperties.getProperty(groupIdKey) const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/groups/" + GROUP_ID + "/chat" return UrlFetchApp.fetch(url, params) } // Gets chat messages from the group this script is set up for function api_getChatMessagesFromGroup() { var groupIdKey = GROUP_ID_KEY const GROUP_ID = scriptProperties.getProperty(groupIdKey) const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/groups/" + GROUP_ID + "/chat" return UrlFetchApp.fetch(url, params) } // Find the index of a given search term. Choose whether it's case-sensitive or not. function findIndex(searchTerm, stringToSearch, caseSensitive) { if (caseSensitive){ return stringToSearch.indexOf(searchTerm) } else { let uppercaseSearchTerm = searchTerm.toUpperCase() let uppercaseStringToSearch = stringToSearch.toUpperCase() return uppercaseStringToSearch.indexOf(uppercaseSearchTerm) } } // Adds "@" to beginning of a username so you can tag them. function appendAtSymbol(username){ // First, check if @ is already first character, remove if yes. let baseUsername = checkFirstCharacter(username) let taggedUsername = "@" + baseUsername return taggedUsername } // Check it first character is @ or [, remove if yes function checkFirstCharacter(stringToCheck){ // initialize let result = stringToCheck.trim() // When grabbing a username string, it might instead grab [@username](URL). I want username only. let indexUrlOpen = findIndex("[@", stringToCheck) let indexUrlMid = findIndex("](", stringToCheck) let indexUrlClose = findIndex(")", stringToCheck) // Check if all of them are found. Initiate Boolean it false, flip it if all indices are not -1 // Also check if they are in the right order. let allFound = false let rightOrder = false if ( (indexUrlOpen != -1) && (indexUrlMid != -1) && (indexUrlClose != -1) ) { allFound = true if ( (indexUrlOpen < indexUrlMid) && (indexUrlMid < indexUrlClose) ) { rightOrder = true } } // If all are found and it's in the right order, trim string so it's just username. if (allFound && rightOrder) { result = createSubstring(stringToCheck, indexUrlOpen + 2, indexUrlMid) return result } else { // Check if the first character is @ if (stringToCheck.charAt(0) == "@") { result = stringToCheck.substring(1) return result } else { return result } } } // Switch-case will make the main function eaiser/cleaner. All the logic goes here, based on message priority and other conditions. // This function categorizes cases. Other functions will get more granular within each category. function createCaseCategory(indexAutoError1, indexAutoError2, indexFastStartDuel, indexEndDuel, indexHelp, indexStartOver, indexCease, duelSetupInProgress, usernameWhoPosted, participant1, participant2, responder, indexCounteroffer) { // Create cases for a switch-case. Initialize at 0. let caseCategory = 0 if ( (indexAutoError1 != -1) && (indexAutoError2 != -1) ){ // Only of both automated error messages are found caseCategory = 100 // Cases numbered in the 100s (i.e. 1xx) are the highest priority. } else if ( (indexFastStartDuel != -1) || (indexEndDuel != -1) ) { caseCategory = 100 // Cases numbered in the 100s (i.e. 1xx) are the highest priority. } else if (indexHelp != -1) { caseCategory = 200 // Cases numbered in the 200s are for the HELP messages, which are slightly different depending on whether a duel is currently being set up or not. } else if (duelSetupInProgress >= 1) { // Cases numbered in the 300s are for when a duel is being set up and only certain users can respond, and the wrong user responded. // If person who posted is neither participant if ( ( usernameWhoPosted != participant1 ) && ( usernameWhoPosted != participant2 ) ) { caseCategory = 300 } else { if (duelSetupInProgress == 1) { if (usernameWhoPosted == participant1) { // See if duel initiator is doing START OVER / CEASE if ( (indexStartOver != -1 ) || (indexCease != -1) ) { caseCategory = 300 } else { // Cases numbered in the 400s are for when the initiator is setting up duel parameters caseCategory = 400 } } else { caseCategory = 300;// Wrong person posted } } else if (duelSetupInProgress == 2) { // Duel initiator and opponent are the only ones allowed to respond. // Checks if the correct person responded. Posts no message. let correctResponder = checkResponder(responder, usernameWhoPosted, participant1, participant2, true) if (!correctResponder) { // If the wrong person responded caseCategory = 300 } else if (correctResponder) { // If the correct person responded if (indexCounteroffer != -1) { caseCategory = 500 // Cases numbered in the 500s are for counteroffers } else { caseCategory = 600 // Cases numbered in the 600s are for responses other than Counteroffer } } } } } else if (duelSetupInProgress == 0) { caseCategory = 700 // Cases numbered in the 700s are for when no duel is currently being set up } return caseCategory } // Once it's at a point when only two people can respond, checks if the correct person responded. Sends message if not. function checkResponder(responder, usernameWhoPosted, participant1, participant2, sendNoMessage) { if (sendNoMessage) { // Simply checks if responder is correct, doesn't post any messages. if ( ( (responder == 1) && (usernameWhoPosted == participant1) ) || ( (responder == 2) && (usernameWhoPosted == participant2) ) ) { return true } else if ( ( (responder == 1) && (usernameWhoPosted == participant2) ) || ( (responder == 2) && (usernameWhoPosted == participant1) ) ) { return false } else { // This condition shouldn't be possible, but send error message anyway api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CHECKRESPONDER + MSG_ERROR_ENDING}); } } else { // Checks responder and posts error message if needed. if ( ( (responder == 1) && (usernameWhoPosted == participant1) ) || ( (responder == 2) && (usernameWhoPosted == participant2) ) ) { return true } else if ( ( (responder == 1) && (usernameWhoPosted == participant2) ) || ( (responder == 2) && (usernameWhoPosted == participant1) ) ) { // Change contents of error message depending on which user it is. let MSG_OTHER_RESPONDER = "" let TAG_PERSON_WHO_POSTED = "" if ( (responder == 1) && (usernameWhoPosted == participant2) ) { TAG_PERSON_WHO_POSTED = appendAtSymbol(participant2) MSG_OTHER_RESPONDER = TAG_PERSON_WHO_POSTED + MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER1 + participant1 + MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER2 } else if ( (responder == 2) && (usernameWhoPosted == participant1) ) { TAG_PERSON_WHO_POSTED = appendAtSymbol(participant1) MSG_OTHER_RESPONDER = TAG_PERSON_WHO_POSTED + MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER1 + participant2 + MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER2 + MSG_DUEL_SETUP_IN_PROGRESS_OTHER_RESPONDER3 } // Now the contents of message are correct, send it. api_postChatMessageToGroup({"message": MSG_OTHER_RESPONDER}) return false } else { // This condition shouldn't be possible, but send error message anyway api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CHECKRESPONDER + MSG_ERROR_ENDING}) } } } // Getting more granular for the cases within the 100 category, which are the highest priority function createCase100s(indexAutoError1, indexAutoError2, indexFastStartDuel, indexEndDuel, text, usernameWhoPosted, botUsername, participant2Key, wagerKey, scoreNeededKey){ // Create cases for a switch-case. Initialize at 100. let case100 = 100 // If both parts of the automated error message from the scoring script are found, do nothing if ( (indexAutoError1 != -1) && (indexAutoError2 != -1) ) { case100 = 101 // Automated error messages from scoring script detected – do nothing. } else { if ( indexFastStartDuel != -1) { // Split off the command term from the rest of the string let length = text.length let inputString = createSubstring(text, indexFastStartDuel + 15, length) // Command term is 15 characters, hence why it's the offset // In case the command term was followed by a colon, remove it inputString = removeFirstColon(inputString) // Check if it's a valid string (looking at number of semicolons) let validInputStringSemicolons = checkCompleteDuelStartStringSemicolons(inputString) // If it's not, post a message saying that there aren't enough semicolons. if (!validInputStringSemicolons) { api_postChatMessageToGroup({"message": MSG_NOT_ENOUGH_SEMICOLONS}) case100 = 102 // Fast start with insufficient semicolons } else if (validInputStringSemicolons) { // Now check to see if the numeric parameters are numbers. Don't save any values. let validInputStringNumeric = checkCompleteDuelStartStringNumeric(inputString, participant2Key, wagerKey, scoreNeededKey, usernameWhoPosted, true) if (!validInputStringNumeric) { // Messages have already been posted case100 = 103 // fast start but numeric parameters were not numbers } else if (validInputStringNumeric) { // Begin the process of parsing out each of the parameters // Find location of first semicolon. let firstSemicolonIndex = findIndexOfSemicolon(inputString, 0, 0) let stringLength = inputString.length // Split off the string into two substrings let opponentNameTemp = createSubstring(inputString, 0, firstSemicolonIndex) let remainingSubstring = createSubstring(inputString, firstSemicolonIndex + 1, stringLength) // Two parameters successfully parsed. let tagResponder = appendAtSymbol(opponentNameTemp) let tagInitiator = appendAtSymbol(usernameWhoPosted) // Update length stringLength = remainingSubstring.length // Find second semicolon location, someplace after the first (i.e. use the new substring) let secondSemicolonIndex = findIndexOfSemicolon(remainingSubstring, 0, 1) // Split off into two substrings again. let wagerTemp = createSubstring(remainingSubstring, 0, secondSemicolonIndex) remainingSubstring = createSubstring(remainingSubstring, secondSemicolonIndex + 1, stringLength) // Find third semicolon location, someplace after the second. let thirdSemicolonIndex = findIndexOfSemicolon(remainingSubstring, 0, 1) // Final splitting into a substring let scoreNeededTemp = createSubstring(remainingSubstring, 0, thirdSemicolonIndex) // Grab current timestamp let timestamp = Date.now() // Create correct message and post let MSG = createDuelStartString(tagResponder, tagInitiator, wagerTemp, scoreNeededTemp, timestamp, botUsername, "fast") api_postChatMessageToGroup({"message": MSG}) case100 = 104 // Fast start with no errors } else { // This condition shouldn't be possible api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CASE100S + MSG_ERROR_ENDING}) } } else { // This condition shouldn't be possible api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CASE100S + MSG_ERROR_ENDING}) } } else if ( indexEndDuel != -1 ) { // Check if Duel End string passes all checks. If it was automatically generated by the other script, it should. // If someone tries to cheat, it definitely won't pass. // If there are errors in the other script, it probably won't pass here. let passedDuelEndChecks = parseAndCheckDuelEnd(text, indexEndDuel) // If it didn't pass, create generic error message to post to the party. // Specific causes of the error will be not shown publicly, instead just in the Console log and have already been posted by the function above. if (!passedDuelEndChecks) { let TAG_DUEL_ENDER = appendAtSymbol(usernameWhoPosted) let MSG_ERROR_ENDING_DUEL = TAG_DUEL_ENDER + MSG_ERROR_ENDING_DUEL_START + botUsername + MSG_ERROR_ENDING_DUEL_END api_postChatMessageToGroup({"message": MSG_ERROR_ENDING_DUEL}) } else { // Retrieve saved values from memory var winnerKey = WINNER_KEY var loserKey = LOSER_KEY var wagerWinnerKey = WAGER_WINNER_KEY var scoreNeededWinnerKey = SCORE_NEEDED_WINNER_KEY var hashAllKey = HASH_ALL_KEY var hashSumKey = HASH_SUM_KEY let winner = scriptProperties.getProperty(winnerKey) let loser = scriptProperties.getProperty(loserKey) let wagerWinner = Number(scriptProperties.getProperty(wagerWinnerKey)) let scoreNeededWinner = Number(scriptProperties.getProperty(scoreNeededWinnerKey)) let hashAll = Number(scriptProperties.getProperty(hashAllKey)) let hashSum = Number(scriptProperties.getProperty(hashSumKey)) // Create string declaring winner and parameters let TAG_WINNER = appendAtSymbol(winner) let TAG_LOSER = appendAtSymbol(loser) let gpWon = wagerWinner * 2 let MSG_DUEL_END_WINNER = MSG_DUEL_END_WINNER_PART_1 + TAG_WINNER + MSG_DUEL_END_WINNER_PART_2 + scoreNeededWinner + MSG_DUEL_END_WINNER_PART_3 + TAG_LOSER + MSG_DUEL_END_WINNER_PART_4 + gpWon + MSG_DUEL_END_TO_PASTE + hashAll + MSG_DUEL_END_WINNER_PART_5 + hashSum + MSG_DUEL_END_WINNER_PART_6 api_postChatMessageToGroup({"message": MSG_DUEL_END_WINNER}) // Reset the saved values resetDuelEnd() } case100 = 105 // For ending duel } } return case100 } // Creates a substring of the characters between the search term and the semicolon, and trims whitespace function createSubstring(string, startingIndex, endingIndex) { var untrimmed = string.substring(startingIndex, endingIndex) var trimmed = untrimmed.trim() return trimmed } // If the first character is a colon, remove it function removeFirstColon(input) { if (input.charAt(0) == ":") { let resultRemoved = input.substring(1) return resultRemoved } else { let result = input return result } } // Check to see if it's a valid Duel Start string, based on number of semicolons function checkCompleteDuelStartStringSemicolons(input){ // Check if there are three semicolons. If not, it's an invalid input string. // Find location of first semicolon. let firstSemicolonIndex = findIndexOfSemicolon(input, 0, 0) let stringLength = input.length // If index is -1, there are no semicolons and it's an invalid input string. if (firstSemicolonIndex == -1) { return false } else { // Split off the string, beginning at the first semicolon let remainingSubstring = createSubstring(input, firstSemicolonIndex + 1, stringLength) // Update length stringLength = remainingSubstring.length // Repeat process to find second semicolon let secondSemicolonIndex = findIndexOfSemicolon(remainingSubstring, 0, 1) // If index is -1, there are not enough semicolons and it's an invalid input string. if (secondSemicolonIndex == -1) { return false } else { // Split string again remainingSubstring = createSubstring(remainingSubstring, secondSemicolonIndex + 1, stringLength) // Repeat process to find third semicolon let thirdSemicolonIndex = findIndexOfSemicolon(remainingSubstring, 0, 1) // If index is -1, there are not enough semicolons and it's an invalid input string. if (thirdSemicolonIndex == -1) { return false } else { return true } } } } // Finds the semicolon after the search term previously searched. Builds in an offset to account for the number of characters in the search term. function findIndexOfSemicolon(stringToSearch, startingAt, offset) { return stringToSearch.indexOf(";", startingAt + offset) } // Checks if a complete Duel Start string is valid (numerically). Saves values if yes. Send error message if no. function checkCompleteDuelStartStringNumeric(input, participant2Key, wagerKey, scoreNeededKey, usernameWhoPosted, ignoreSaving) { // A previous function has already confirmed the number of semicolons // Find location of first semicolon. let firstSemicolonIndex = findIndexOfSemicolon(input, 0, 0) let stringLength = input.length // Split off the string into two substrings let opponentNameTemp = createSubstring(input, 0, firstSemicolonIndex) let remainingSubstring = createSubstring(input, firstSemicolonIndex + 1, stringLength) // Update length stringLength = remainingSubstring.length // Find second semicolon location, someplace after the first (i.e. use the new substring) let secondSemicolonIndex = findIndexOfSemicolon(remainingSubstring, 0, 1) // Split off into two substrings again. let wagerTemp = createSubstring(remainingSubstring, 0, secondSemicolonIndex) remainingSubstring = createSubstring(remainingSubstring, secondSemicolonIndex + 1, stringLength) // Find third semicolon location, someplace after the second. let thirdSemicolonIndex = findIndexOfSemicolon(remainingSubstring, 0, 1) // Final splitting into a substring let scoreNeededTemp = createSubstring(remainingSubstring, 0, thirdSemicolonIndex) // Wager and Score Needed both need to be numbers. If either is not, it's an invalid input string. let wagerIsNumber = checkIfNumber(wagerTemp) let scoreNeededIsNumber = checkIfNumber(scoreNeededTemp) let howMany = countHowManyAreNumbers(wagerIsNumber, scoreNeededIsNumber) // Send error message if 0 or 1 are numbers. Message changes based on which one. if (howMany != 2) { let MSG_NOT_A_NUMBER_ALL_PARAMETERS = ""; let TAG_USERNAME_WHO_POSTED = appendAtSymbol(usernameWhoPosted) if (howMany == 0) { MSG_NOT_A_NUMBER_ALL_PARAMETERS = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_BOTH_ARE_NOT_NUMBERS + MSG_NOT_A_NUMBER_END } else if (howMany == 1.1) { MSG_NOT_A_NUMBER_ALL_PARAMETERS = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_SCORE_NEEDED_IS_NOT_NUMBER + MSG_NOT_A_NUMBER_END } else if (howMany == 1.2) { MSG_NOT_A_NUMBER_ALL_PARAMETERS = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_WAGER_IS_NOT_NUMBER + MSG_NOT_A_NUMBER_END } else { // This condition shouldn't be possible, but send an error message anyway MSG_NOT_A_NUMBER_ALL_PARAMETERS = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_BOTH_ARE_NOT_NUMBERS + MSG_NOT_A_NUMBER_END } // Now that the message is correct, post it api_postChatMessageToGroup({"message": MSG_NOT_A_NUMBER_ALL_PARAMETERS}) return false } else if (howMany == 2) { // It's a valid string // Depending on the version, either save parameters or don't. if (ignoreSaving) { return true } else if (!ignoreSaving) { let name = checkFirstCharacter(opponentNameTemp) let wager = parseFloat(wagerTemp) wagerTemp = wager wager = sanitizeInput(wagerTemp, 1, "none") // Ensure that it is greater than 1 let scoreNeeded = parseFloat(scoreNeededTemp) scoreNeededTemp = scoreNeeded scoreNeeded = sanitizeInput(scoreNeededTemp, 1, "none") // Ensure that it is greater than 1 scriptProperties.setProperty(participant2Key, name) scriptProperties.setProperty(wagerKey, wager) scriptProperties.setProperty(scoreNeededKey, scoreNeeded) return true } else { // This condition shouldn't be possible api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CHECK_START_NUMERIC + MSG_ERROR_ENDING}) return false } } } // Checks if the substring is a number function checkIfNumber(substringToCheck) { let substringAsNumber = parseFloat(substringToCheck) let isNan = Number.isNaN(substringAsNumber) if (isNan) { return false } else { return true } } // Counts how many are numbers function countHowManyAreNumbers(wagerIsNumber, scoreNeededIsNumber) { if (wagerIsNumber && scoreNeededIsNumber) { return 2 } else { if (wagerIsNumber) { return 1.1 // This'll clue me in that Wager is a number, Score Needed is not } else { if (scoreNeededIsNumber) { return 1.2 // This'll clue me in that Score Needed is a number, Wager is not } else { return 0 } } } } // Ensures that the input is between the minimum and maximum. Can use "none" in either argument to indicate if there is either no minimum or no maximum. function sanitizeInput(input, minimum, maximum) { if (minimum == "none"){ // Condition for if there is no minimum if ( input > maximum ) { return maximum } else { return input } } else if (maximum == "none"){ // Condition for if there is no maximum if ( input < minimum ) { return minimum } else { return input } } else { // Condition where there is both minimum and maximum if ( input < minimum ) { return minimum } else if ( input > maximum ) { return maximum } else { return input } } } // Creates string that starts a duel function createDuelStartString(tagResponder, tagInitiator, wager, scoreNeeded, timestamp, botUsername, version) { // Get group ID var groupIdKey = GROUP_ID_KEY; const GROUP_ID = scriptProperties.getProperty(groupIdKey) // Strings are identical except usernames. In both cases, it's "@self; vs. @other;" let START_STRING1 = tagInitiator + ";vs. " + tagResponder + ";" + wager + ";" + scoreNeeded + ";" + timestamp + ";" + GROUP_ID + ";" + botUsername + ";" let START_STRING2 = tagResponder + ";vs. " + tagInitiator + ";" + wager + ";" + scoreNeeded + ";" + timestamp + ";" + GROUP_ID + ";" + botUsername + ";" // Input the rest of the AGREE message let MSG = "" if (version == "normal") { MSG = MSG_AGREE0 + MSG_AGREE1 + tagInitiator + MSG_AGREE2 + START_STRING1 + MSG_AGREE3 + tagResponder + MSG_AGREE4 + START_STRING2 + MSG_AGREE5 } else if (version == "fast") { MSG = MSG_AGREE_FAST_START + tagInitiator + MSG_AGREE2 + START_STRING1 + MSG_AGREE3 + tagResponder + MSG_AGREE4 + START_STRING2 + "`" } else { // This condition shouldn't be possible api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CREATE_START + MSG_ERROR_ENDING}) } return MSG } // Parses Duel End string to see if it's valid function parseAndCheckDuelEnd(inputString, indexEndDuel) { // Initialize some values so I can use them later let length = inputString.length let periodIndexFirst = 0 let periodIndexSecond = 0 let periodIndexLast = 0 let periodIndexPenultimate = 0 let semicolonIndexFirst = 0 let semicolonIndexSecond = 0 let inputStringTemp = inputString // Remove search term (and presumably the tagged Duel Bot username) from the beginning inputStringTemp = createSubstring(inputString, indexEndDuel + 8, length) // offset is 8 since "end duel" has eight characters inputString = inputStringTemp // Check if correct number of periods and semicolons are present let punctuationCheck = checkDuelEndPeriodsAndSemicolons(inputString) if (!punctuationCheck) { return false } else { // Use the periods to do the first round of parsing the string into three substrings. // Find first two periods periodIndexFirst = findIndexOfPeriod(inputString, 0, 0) periodIndexSecond = findIndexOfPeriod(inputString, periodIndexFirst, 1) // Second period needs to be after the first one // The substring found between those two periods contains all the duel parameters var allParametersParsed = createSubstring(inputString, periodIndexFirst + 1, periodIndexSecond) // Find the last and penultimate (second-to-last) periods periodIndexLast = inputString.lastIndexOf(".") periodIndexPenultimate = inputString.lastIndexOf(".", periodIndexLast - 1) // The substring found between those two periods contains both hashes var hashesParsed = createSubstring(inputString, periodIndexPenultimate + 1, periodIndexLast) // The substring found between second and penultimate periods contains both usernames var bothUsernamesParsed = createSubstring(inputString, periodIndexSecond + 1, periodIndexPenultimate) // Further parse using the semicolons // In the All Parameters string... // ...the first item is the alphabetized characters of both usernames semicolonIndexFirst = findIndexOfSemicolon(allParametersParsed, 0, 0) var alphabeticalCombinedUsernamesParsed = createSubstring(allParametersParsed, 0, semicolonIndexFirst) // ...the second item is Wager semicolonIndexSecond = findIndexOfSemicolon(allParametersParsed, semicolonIndexFirst, 1) // Second semicolon needs to be after first let wagerParsedTemp = createSubstring(allParametersParsed, semicolonIndexFirst + 1, semicolonIndexSecond) let wagerParsed = parseFloat(wagerParsedTemp) // ...the third item is Score Needed semicolonIndexFirst = semicolonIndexSecond semicolonIndexSecond = findIndexOfSemicolon(allParametersParsed, semicolonIndexFirst, 1) // Second semicolon needs to be after first let scoreNeededParsedTemp = createSubstring(allParametersParsed, semicolonIndexFirst + 1, semicolonIndexSecond); let scoreNeededParsed = parseFloat(scoreNeededParsedTemp) // In the Hashes string, split into the two semicolonIndexFirst = findIndexOfSemicolon(hashesParsed, 0, 0) semicolonIndexSecond = findIndexOfSemicolon(hashesParsed, semicolonIndexFirst, 1) // Second semicolon needs to be after first var hashAllParsed = createSubstring(hashesParsed, 0, semicolonIndexFirst) var hashSumParsed = createSubstring(hashesParsed, semicolonIndexFirst + 1, semicolonIndexSecond) // In the Both Usernames string, split into the two usernames semicolonIndexFirst = findIndexOfSemicolon(bothUsernamesParsed, 0, 0) semicolonIndexSecond = findIndexOfSemicolon(bothUsernamesParsed, semicolonIndexFirst, 1) // Second semicolon needs to be after first let selfUsernameParsedTemp = createSubstring(bothUsernamesParsed, 0, semicolonIndexFirst) let opponentUsernameParsedTemp = createSubstring(bothUsernamesParsed, semicolonIndexFirst + 1, semicolonIndexSecond) // Those usernames probably include the profile URL. Remove it. var selfUsernameParsed = checkFirstCharacter(selfUsernameParsedTemp) var opponentUsernameParsed = checkFirstCharacter(opponentUsernameParsedTemp) // Now, see if they match. The parsed values are the ones you got from the Duel End string. The re-created values are what you got from running the functions here in the script. let passes = matchCheck(selfUsernameParsed, opponentUsernameParsed, alphabeticalCombinedUsernamesParsed, allParametersParsed, hashAllParsed) if (!passes) { return false } else { // Finally, check if the numeric parameters are numbers let passesNumeric = duelEndNumberCheck(wagerParsed, scoreNeededParsed) if (!passesNumeric) { return false } else { // All checks passed. // In order for the bot to create the message that declares the winner, save the values. var winnerKey = WINNER_KEY var loserKey = LOSER_KEY var wagerWinnerKey = WAGER_WINNER_KEY var scoreNeededWinnerKey = SCORE_NEEDED_WINNER_KEY var hashAllKey = HASH_ALL_KEY var hashSumKey = HASH_SUM_KEY scriptProperties.setProperty(winnerKey, selfUsernameParsed) // If the string passed all the checks, we can safely assume that "self" is winner scriptProperties.setProperty(loserKey, opponentUsernameParsed) scriptProperties.setProperty(wagerWinnerKey, wagerParsed) scriptProperties.setProperty(scoreNeededWinnerKey, scoreNeededParsed) scriptProperties.setProperty(hashAllKey, hashAllParsed) scriptProperties.setProperty(hashSumKey, hashSumParsed) return true } } } } // For Duel End string, confirms the correct number of periods and semicolons function checkDuelEndPeriodsAndSemicolons(inputString){ let periodIndexFirst = 0 let periodIndexSecond = 0 let periodIndexLast = 0 let periodIndexPenultimate = 0 let countPeriods = 0 let countSemicolons = 0 // Count number of periods (4 are expected). let periodsRemovedLength = inputString.replace(/\./g,'').length countPeriods = inputString.length - periodsRemovedLength if (countPeriods < 4) { // On purpose, it's not (countPeriods != 4). If they have more than four, the extra ones can be automatically removed. // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + "the Duel End string had fewer than four periods. " + MSG_ERROR_CONSOLE_LOG_MID1 + MSG_CONTACT_AUTHOR1 + MSG_ERROR_CONSOLE_LOG_CONTACT + MSG_CONTACT_AUTHOR2) return false } else { // Use the periods to do the first round of parsing the string into three substrings. // Find first two periods periodIndexFirst = findIndexOfPeriod(inputString, 0, 0) periodIndexSecond = findIndexOfPeriod(inputString, periodIndexFirst, 1) // Second period needs to be after the first one // The substring found between those two periods contains all the duel parameters var allParametersParsed = createSubstring(inputString, periodIndexFirst + 1, periodIndexSecond) // Count number of semicolons in this substring (4 are expected). countSemicolons = ( allParametersParsed.match(/\;/g) || [] ).length if (countSemicolons != 4) { // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + "the section that contains all the duel parameters (i.e. the section between the first two periods) is supposed to have four semicolons. " + MSG_ERROR_CONSOLE_LOG_MID1 + MSG_CONTACT_AUTHOR1 + MSG_ERROR_CONSOLE_LOG_CONTACT + MSG_CONTACT_AUTHOR2) return false } else { // If this substring is all clear, move on to the next two. // Find the last and penultimate (second-to-last) periods periodIndexLast = inputString.lastIndexOf(".") periodIndexPenultimate = inputString.lastIndexOf(".", periodIndexLast - 1) // The substring found between those two periods contains both hashes var hashesParsed = createSubstring(inputString, periodIndexPenultimate + 1, periodIndexLast) // Count number of semicolons in this substring (2 are expected). countSemicolons = ( hashesParsed.match(/\;/g) || [] ).length if (countSemicolons != 2) { // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + "the section that contains both hashes (i.e. the section between the third and fourth periods) is supposed to have two semicolons. " + MSG_ERROR_CONSOLE_LOG_MID1 + MSG_CONTACT_AUTHOR1 + MSG_ERROR_CONSOLE_LOG_CONTACT + MSG_CONTACT_AUTHOR2) return false } else { // If this substring is all clear, move on to the final one. // The substring found between second and penultimate periods contains both usernames var bothUsernamesParsed = createSubstring(inputString, periodIndexSecond + 1, periodIndexPenultimate) // Count number of semicolons in this substring (2 are expected). countSemicolons = ( bothUsernamesParsed.match(/\;/g) || [] ).length if (countSemicolons != 2) { // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + "the section that contains both usernames (i.e. the section between the second and third periods) is supposed to have two semicolons. " + MSG_ERROR_CONSOLE_LOG_MID1 + MSG_CONTACT_AUTHOR1 + MSG_ERROR_CONSOLE_LOG_CONTACT + MSG_CONTACT_AUTHOR2) return false } else { // If all three substrings passed, return true return true } } } } } // Finds the semicolon after the search term previously searched. Builds in an offset to account for the number of characters in the search term. function findIndexOfPeriod(stringToSearch, startingAt, offset) { return stringToSearch.indexOf(".", startingAt + offset) } // Checks if alphabetized usernames and hashes match (parsed vs. recreated) function matchCheck(selfUsernameParsed, opponentUsernameParsed, alphabeticalCombinedUsernamesParsed, allParametersParsed, hashAllParsed){ // Alphabetical combined usernames – if you alphabetized both usernames, does it match what the Duel End string passed over? let alphabeticalCombinedUsernamesRecreated = alphabetizeString(selfUsernameParsed, opponentUsernameParsed) let alphabeticalCheck = (alphabeticalCombinedUsernamesRecreated == alphabeticalCombinedUsernamesParsed) ? true : false // Hash of all parameters – if you hash all the parameters, does it match the hash that the Duel End string passed over? let hashAllRecreated = createHash(allParametersParsed) let hashCheck = (hashAllRecreated == hashAllParsed) ? true : false // Good to go if both of them pass if (alphabeticalCheck && hashCheck) { return true } else { // Figure out if one or both checks failed if (!alphabeticalCheck) { if (!hashCheck) { // Failed both // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + MSG_ERROR_CONSOLE_LOG_FAILED_FINAL_CHECK_BOTH + MSG_ERROR_CONSOLE_LOG_MID2 + MSG_CONTACT_AUTHOR1 + MSG_CONTACT_AUTHOR2) } else { // Failed alphabetical check only // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + MSG_ERROR_CONSOLE_LOG_FAILED_FINAL_CHECK_USERNAME + MSG_ERROR_CONSOLE_LOG_MID2 + MSG_CONTACT_AUTHOR1 + MSG_CONTACT_AUTHOR2) } } else { // Failed hash check only // Specific error message will be visible only to the person who runs the duel announcer bot script console.log(MSG_ERROR_CONSOLE_LOG_START + MSG_ERROR_CONSOLE_LOG_FAILED_FINAL_CHECK_HASH + MSG_ERROR_CONSOLE_LOG_MID2 + MSG_CONTACT_AUTHOR1 + MSG_CONTACT_AUTHOR2) } // Now that the correct console log messages happened, return false return false } } // Alphabetically sorts self&opponent usernames. That way, it's the same no matter which user does it. function alphabetizeString(selfUsername, opponentUsername) { // Neither of these currently start with @. Add it. let str1 = appendAtSymbol(selfUsername) let str2 = appendAtSymbol(opponentUsername) // Combine/concatenate let combinedString = str1.concat(str2) // Split by character, sort alphabetically, join back together. return combinedString.split('').sort().join('').trim() } // A good hash function is deterministic, fast, uniformally distributed, and non-invertible. See for more info: https://www.educba.com/javascript-hash/ // I don't need a powerful cryptographic hash like SHA1 or SHA256, that would be overkill for me (not to mention, too slow) function createHash(string) { // Initialize at 0 var hash = 0 // If the length of the string is 0, return 0 if (string.length == 0) { return hash } else { for (var i = 0 ; i= 1) { api_postChatMessageToGroup({"message": MSG_HELP1 + MSG_HELP_OPTIONAL + MSG_HELP2}); case200 = 201 // Help message while bot is helping set up a duel } else { api_postChatMessageToGroup({"message": MSG_HELP1 + MSG_HELP2}); case200 = 202 // Help message } } return case200 } // Getting more granular for the cases within the 300 category, where only certain users can respond function createCase300s(indexStartOver, indexCease, usernameWhoPosted, botUsername, duelSetupInProgress, duelSetupInProgressKey, participant1, participant1Key, participant2, responder){ // Create cases for a switch-case. Initialize at 300. let case300 = 300 if (duelSetupInProgress >= 1) { // First round of checks are for the START OVER / CEASE SETUP commands, only from participant 1 if ( (usernameWhoPosted == participant1) && ( (indexStartOver != -1) || (indexCease != -1) ) ) { if (indexStartOver != -1) { // Reset all saved parameters resetDuelSetup() // Save username who posted (i.e. duel initiator) as participant1 scriptProperties.setProperty(participant1Key, usernameWhoPosted) // Flip this value back to 1 (i.e. initiator setting up parameters) and save scriptProperties.setProperty(duelSetupInProgressKey, 1) // Create and post message. It's basically the same as Duel Setup With Walkthrough, with one extra section added to the beginning. api_postChatMessageToGroup({"message": MSG_DUEL_START_FROM_STARTING_OVER + MSG_DUEL_START1 + botUsername + MSG_DUEL_START2 + MSG_DUEL_START3}) case300 = 303 // Duel initiator wants to start over } else if (indexCease != -1) { // Reset all saved parameters resetDuelSetup() // Flip this value back to 0 (i.e. no duel setup in progress) and save scriptProperties.setProperty(duelSetupInProgressKey, 0) // Create and post message telling other people that they can set up a duel now. api_postChatMessageToGroup({"message": MSG_SETUP_CEASED}) case300 = 304 // Duel initiator wants to cease duel setup entirely } } else { if (duelSetupInProgress == 1) { // Duel initiator needs to set parameters. They are the only one allowed to respond. if ( usernameWhoPosted != participant1 ) { api_postChatMessageToGroup({"message": MSG_DUEL_SETUP_IN_PROGRESS}) case300 = 301 // The username who posted isn't any of those allowed to respond } } else if (duelSetupInProgress == 2) { // Duel initiator and opponent are the only ones allowed to respond. if ( ( usernameWhoPosted != participant1 ) && ( usernameWhoPosted != participant2 ) ) { api_postChatMessageToGroup({"message": MSG_DUEL_SETUP_IN_PROGRESS}) case300 = 301 // If the username who posted isn't any of those allowed to respond } else { // Check if the correct person responded. Send message if not. let correctResponder = checkResponder(responder, usernameWhoPosted, participant1, participant2, false) if (!correctResponder) { case300 = 302 // Incorrect responder } } } } } else { // This condition shouldn't be possible api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_CASE300S + MSG_ERROR_ENDING}) } return case300 } // Getting more granular for the cases within the 400 category, when duel initiator is setting up parameters function createCase400s(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, duelSetupInProgressKey, participant1Key, participant2Key, responderKey, wagerKey, scoreNeededKey, timestampKey){ // Create cases for a switch-case. Initialize at 400. let case400 = 400 // Count number of search terms found. let numberSearchTerms = countSearchTerms(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, "standard") // If none of these were entered, send error message if (numberSearchTerms == 0) { // Create correct error message, tagging duel initiator and saying which commands the bot is listening for. let MSG_UNKNOWN_COMMAND_DUEL_INITIATOR_SETTING_UP = "" let TAG_DUEL_INITIATOR = appendAtSymbol(usernameWhoPosted) MSG_UNKNOWN_COMMAND_DUEL_INITIATOR_SETTING_UP = TAG_DUEL_INITIATOR + MSG_UNKNOWN_COMMAND + MSG_UNKNOWN_COMMAND_SETUP + botUsername + MSG_UNKNOWN_COMMAND_SETUP2 api_postChatMessageToGroup({"message": MSG_UNKNOWN_COMMAND_DUEL_INITIATOR_SETTING_UP}) case400 = 401 // Duel initiator setting up parameters but didn't include any parameters in the post. } else { // If some search terms are found, check if all are found // Checks if the number of search terms and semicolons equals the expected amount (in this case, 3). Returns True or False. let correctTerms = correctNumberOfTerms(numberSearchTerms, 3, indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, "standard") if (!correctTerms) { let TAG_DUEL_INITIATOR = appendAtSymbol(usernameWhoPosted) api_postChatMessageToGroup({"message": TAG_DUEL_INITIATOR + MSG_NOT_ALL_PARAMETERS1 + botUsername + MSG_NOT_ALL_PARAMETERS2 + MSG_NOT_ALL_PARAMETERS3}) case400 = 402 // Duel initiator setting up parameters but didn't include all parameters in the post. } else if (correctTerms) { // If all search terms are found, check if Wager and Score Needed are numbers. let howMany = howManyAreNumbers(indexStartDuelWager, indexStartDuelScoreNeeded, text) // Send error message if 0 or 1 are numbers. Message changes based on which one. if (howMany != 2) { let MSG_NOT_A_NUMBER = "" let TAG_DUEL_INITIATOR = appendAtSymbol(usernameWhoPosted) if (howMany == 0) { MSG_NOT_A_NUMBER = TAG_DUEL_INITIATOR + MSG_NOT_A_NUMBER_START + MSG_BOTH_ARE_NOT_NUMBERS + MSG_NOT_A_NUMBER_END } else if (howMany == 1.1) { MSG_NOT_A_NUMBER = TAG_DUEL_INITIATOR + MSG_NOT_A_NUMBER_START + + MSG_NOT_A_NUMBER_END } else if (howMany == 1.2) { MSG_NOT_A_NUMBER = TAG_DUEL_INITIATOR + MSG_NOT_A_NUMBER_START + MSG_WAGER_IS_NOT_NUMBER + MSG_NOT_A_NUMBER_END } else { // This condition shouldn't be possible, but send an error message anyway MSG_NOT_A_NUMBER = TAG_DUEL_INITIATOR + MSG_NOT_A_NUMBER_START + MSG_BOTH_ARE_NOT_NUMBERS + MSG_NOT_A_NUMBER_END } // Now that the message is correct, post it api_postChatMessageToGroup({"message": MSG_NOT_A_NUMBER}) case400 = 403 // Numeric parameters weren't entered as numbers } else if (howMany == 2) { // Since it passed all checks, save all values parseAndSave(text, indexStartDuelOpponent, 8, true, false, participant2Key) parseAndSave(text, indexStartDuelWager, 5, false, true, wagerKey) parseAndSave(text, indexStartDuelScoreNeeded, 12, false, true, scoreNeededKey) // Now that the parameters have been set, flip these to the correct value. let duelSetupInProgress = 2 let responder = 2 // Grab current timestamp let timestamp = Date.now() // Save these scriptProperties.setProperty(duelSetupInProgressKey, duelSetupInProgress) scriptProperties.setProperty(responderKey, responder) scriptProperties.setProperty(timestampKey, timestamp) // Create message asking agree/disagree/counteroffer. retrieveSavedParametersAndCreateMessage(participant1Key, participant2Key, botUsernameKey, wagerKey, scoreNeededKey, responderKey, timestampKey, "standard") case400 = 404 // Duel initiator set up all parameters } } } return case400 } // Counts number of search terms found by inputting their index. Also works for counting semicolons. function countSearchTerms(termOpponent, termWager, termScoreNeeded, version) { let countSearchTerms = 0 if ( termWager != -1 ) { countSearchTerms++ } if ( termScoreNeeded != -1 ) { countSearchTerms++ } // Only look for "opponent" search term for standard version, not for counteroffer version. if (version = "standard") { if ( termOpponent != -1 ) { countSearchTerms++ } } return countSearchTerms } // Confirms correct number of search terms and semicolons function correctNumberOfTerms(numberSearchTerms, expectedNumber, indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, version){ // First, it checks number of search terms if (numberSearchTerms != expectedNumber) { return false } else { // Next, it checks number of semicolons let numberSemicolons = findAndCountSemicolons(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, version) if (numberSemicolons == expectedNumber) { return true } else { return false } } } // Finds and counts number of semicolons function findAndCountSemicolons(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, version){ // Find semicolon index locations. let indexWagerSemicolon = findIndexOfSemicolon(text, indexStartDuelWager, 5) // Offset is 5 because the search term has 5 characters let indexScoreNeededSemicolon = findIndexOfSemicolon(text, indexStartDuelScoreNeeded, 12) let indexOpponentSemicolon = -1 // No need to find "opponent" term if it's counteroffer if (version == "standard") { indexOpponentSemicolon = findIndexOfSemicolon(text, indexStartDuelOpponent, 8) } // Now count number of semicolons let numberSemicolons = countSearchTerms(indexOpponentSemicolon, indexWagerSemicolon, indexScoreNeededSemicolon, version) return numberSemicolons } // Checks how many are numbers (of Wager and Score Needed), after parsing. function howManyAreNumbers(indexStartDuelWager, indexStartDuelScoreNeeded, text){ // Find semicolon index locations. let indexWagerSemicolon = findIndexOfSemicolon(text, indexStartDuelWager, 5) // Offset is 5 because the search term has 5 characters let indexScoreNeededSemicolon = findIndexOfSemicolon(text, indexStartDuelScoreNeeded, 12) // Break input string into substrings for each search term. let substringWagerTemp = createSubstring(text, indexStartDuelWager + 5, indexWagerSemicolon) let substringScoreNeededTemp = createSubstring(text, indexStartDuelScoreNeeded + 12, indexScoreNeededSemicolon) // In case the search terms were followed by a colon, remove it let substringWager = removeFirstColon(substringWagerTemp) let substringScoreNeeded = removeFirstColon(substringScoreNeededTemp) // Check if Wager and Score Needed are numbers let wagerIsNumber = checkIfNumber(substringWager) let scoreNeededIsNumber = checkIfNumber(substringScoreNeeded) // Count how many are numbers let howMany = countHowManyAreNumbers(wagerIsNumber, scoreNeededIsNumber) return howMany } // Parses and saves one duel parameter. function parseAndSave(text, indexStart, offset, isName, isNumeric, parameterToSave_Key) { // Find semicolon index location after search term let indexSemicolon = findIndexOfSemicolon(text, indexStart, offset) // Create substring let substringTemp = createSubstring(text, indexStart + offset, indexSemicolon) // In case the search term was followed by a colon, remove it let substring = removeFirstColon(substringTemp) // If it's a name, check if the first character is @, remove if yes. if (isName) { let name = checkFirstCharacter(substring); scriptProperties.setProperty(parameterToSave_Key, name) } else { // If it's a numeric parameter, turn it into a number if (isNumeric) { let substringAsNumber = parseFloat(substring) let positiveNumber = sanitizeInput(substringAsNumber, 1, "none") // Ensure that it is greater than 1 scriptProperties.setProperty(parameterToSave_Key, positiveNumber) } else { // It shouldn't be possible to get this condition api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_PARSESAVE + MSG_ERROR_ENDING}) } } } // Creates and sends message asking for agree, disagree, counteroffer function retrieveSavedParametersAndCreateMessage(participant1Key, participant2Key, botUsernameKey, wagerKey, scoreNeededKey, responderKey, timestampKey, version){ // Grab saved values for the duel parameters let botUsername = scriptProperties.getProperty(botUsernameKey) let participant1 = scriptProperties.getProperty(participant1Key) let participant2 = scriptProperties.getProperty(participant2Key) let wager = Number(scriptProperties.getProperty(wagerKey)) let scoreNeeded = Number(scriptProperties.getProperty(scoreNeededKey)) let responder = Number(scriptProperties.getProperty(responderKey)) let timestamp = Number(scriptProperties.getProperty(timestampKey)) let doubleWager = wager * 2 let tagResponder = "" let tagInitiator = "" // Responder is the participant who is reponder, Initiator is the other one. if (responder == 2) { tagResponder = appendAtSymbol(participant2) tagInitiator = appendAtSymbol(participant1) } else if (responder == 1) { tagResponder = appendAtSymbol(participant1) tagInitiator = appendAtSymbol(participant2) } // Create response message let MSG = "" if (version == "standard") { MSG = tagResponder + MSG_AGREE_DISAGREE_COUNTEROFFER1 + tagInitiator + MSG_AGREE_DISAGREE_COUNTEROFFER2 + wager + MSG_AGREE_DISAGREE_COUNTEROFFER3 + scoreNeeded + MSG_AGREE_DISAGREE_COUNTEROFFER4 + doubleWager + MSG_AGREE_DISAGREE_COUNTEROFFER5 + botUsername + MSG_AGREE_DISAGREE_COUNTEROFFER6 } else if (version == "counter") { MSG = tagResponder + MSG_COUNTEROFFER + tagInitiator + MSG_AGREE_DISAGREE_COUNTEROFFER2 + wager + MSG_AGREE_DISAGREE_COUNTEROFFER3 + scoreNeeded + MSG_AGREE_DISAGREE_COUNTEROFFER4 + doubleWager + MSG_AGREE_DISAGREE_COUNTEROFFER5 + botUsername + MSG_AGREE_DISAGREE_COUNTEROFFER6 } else if (version == "agree") { // Create the correct message MSG = createDuelStartString(tagResponder, tagInitiator, wager, scoreNeeded, timestamp, botUsername, "normal") } else { // This condition shouldn't be possible api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_RETRIEVECREATE + MSG_ERROR_ENDING}) } // Post message asking if opponent agrees, disagrees, or counteroffers api_postChatMessageToGroup({"message": MSG}) } // Getting more granular for the cases within the 500 category, counteroffers function createCase500s(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, participant1Key, participant2Key, responder, responderKey, wagerKey, scoreNeededKey, timestampKey){ // Create cases for a switch-case. Initialize at 500. let case500 = 500 // Count number of search terms found. let numberSearchTerms = countSearchTerms(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, "counter") // If none of these were entered, send error message if (numberSearchTerms == 0) { // Create correct error message, tagging person who posted and saying which commands the bot is listening for. let TAG_USER = appendAtSymbol(usernameWhoPosted) api_postChatMessageToGroup({"message" : TAG_USER + MSG_UNKNOWN_COMMAND_COUNTER + botUsername + "\nCOUNTEROFFER" + MSG_UNKNOWN_COMMAND_COUNTER2}) case500 = 501 // Counteroffer didn't include any parameters in the post. } else { // If some search terms are found, check if all are found // Checks if the number of search terms and semicolons equals the expected amount (in this case, 2). Returns True or False. let correctTerms = correctNumberOfTerms(numberSearchTerms, 2, indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, "counter") if (!correctTerms) { let TAG_USER = appendAtSymbol(usernameWhoPosted) api_postChatMessageToGroup({"message": TAG_USER + MSG_NOT_ALL_PARAMETERS1 + botUsername + MSG_NOT_ALL_PARAMETERS3}) // Skip MSG_NOT_ALL_PARAMETERS2 when it's a counteroffer case500 = 502 // Counteroffer included only some parameters in the post. } else if (correctTerms) { // If all search terms are found, check if Wager and Score Needed are numbers. let howMany = howManyAreNumbers(indexStartDuelWager, indexStartDuelScoreNeeded, text) // Send error message if 0 or 1 are numbers. Message changes based on which one. if (howMany != 2) { let MSG_NOT_A_NUMBER_COUNTER = "" let TAG_USERNAME_WHO_POSTED = appendAtSymbol(usernameWhoPosted) if (howMany == 0) { MSG_NOT_A_NUMBER_COUNTER = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_BOTH_ARE_NOT_NUMBERS + MSG_NOT_A_NUMBER_END } else if (howMany == 1.1) { MSG_NOT_A_NUMBER_COUNTER = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + + MSG_NOT_A_NUMBER_END } else if (howMany == 1.2) { MSG_NOT_A_NUMBER_COUNTER = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_WAGER_IS_NOT_NUMBER + MSG_NOT_A_NUMBER_END } else { // This condition shouldn't be possible, but send an error message anyway MSG_NOT_A_NUMBER_COUNTER = TAG_USERNAME_WHO_POSTED + MSG_NOT_A_NUMBER_START + MSG_BOTH_ARE_NOT_NUMBERS + MSG_NOT_A_NUMBER_END } // Now that the message is correct, post it api_postChatMessageToGroup({"message": MSG_NOT_A_NUMBER_COUNTER}); case500 = 503 // Numeric parameters weren't entered as numbers } else if (howMany == 2) { // Save numeric values as numbers parseAndSave(text, indexStartDuelWager, 5, false, true, wagerKey) parseAndSave(text, indexStartDuelScoreNeeded, 12, false, true, scoreNeededKey) // If Responder was participant1, now it will be participant2, and vice versa responder = toggleResponder(responder) // Save. scriptProperties.setProperty(responderKey, responder) // Create message asking agree/disagree/counteroffer. retrieveSavedParametersAndCreateMessage(participant1Key, participant2Key, botUsernameKey, wagerKey, scoreNeededKey, responderKey, timestampKey, "counter") case500 = 504 // Counteroffer with all parameters } } } return case500 } // Toggles who is the responder function toggleResponder(responder) { if (responder == 1) { return 2 } else if (responder == 2) { return 1 } else { // This condition shouldn't be possible, but send error message anyway api_postChatMessageToGroup({"message": MSG_ERROR_BEGINNING + MSG_ERROR_TOGGLERESPONDER + MSG_ERROR_ENDING}) } } // Getting more granular for the cases within the 600 category, responses other than counteroffer function createCase600s(indexDisagree, indexAgree, indexDecline, usernameWhoPosted, botUsernameKey, participant1, participant1Key, participant2, participant2Key, responder, responderKey, wagerKey, scoreNeededKey, timestampKey){ // Create cases for a switch-case. Initialize at 600. let case600 = 600 // Count number of search terms found. let numberSearchTermsAnswer = countSearchTermsAnswer(indexDisagree, indexAgree, indexDecline) // If none of these were entered, send error message if (numberSearchTermsAnswer == 0) { // Create correct error message, tagging responder and saying which commands the bot is listening for. let MSG_UNKNOWN_COMMAND_RESPONDER_REPLYING = "" let TAG_RESPONDER = appendAtSymbol(usernameWhoPosted) MSG_UNKNOWN_COMMAND_RESPONDER_REPLYING = TAG_RESPONDER + MSG_UNKNOWN_COMMAND + MSG_UNKNOWN_COMMAND_ANSWER api_postChatMessageToGroup({"message": MSG_UNKNOWN_COMMAND_RESPONDER_REPLYING}) case600 = 601 // Responder didn't include any of the Answer search terms } else { // Of the search terms to check, next is agree/disagree and decline. // "Agree" needs to be last so as to avoid scenarios where someone says "I agree to X but not Y" but the bot thinks it's Agree since that word is found. // By that same logic, "decline" needs to be second-to-last. if (indexDisagree != -1) { api_postChatMessageToGroup({"message": MSG_DISAGREE}) case600 = 602 // Disagree } else if (indexDecline != -1) { // Message changes based on who declined. let MSG_DECLINE = "" let MSG_TAG_NONDECLINER = "" if (responder == 1) { MSG_TAG_NONDECLINER = appendAtSymbol(participant2) MSG_DECLINE = MSG_DECLINE1 + MSG_TAG_NONDECLINER + MSG_DECLINE2 } else if (responder == 2) { MSG_TAG_NONDECLINER = appendAtSymbol(participant1) MSG_DECLINE = MSG_DECLINE1 + MSG_TAG_NONDECLINER + MSG_DECLINE2 } // Now that message is correct, post to group. api_postChatMessageToGroup({"message": MSG_DECLINE}) // Reset all saved values resetDuelSetup() case600 = 603 // Decline } else if (indexAgree != -1) { // Post message with string that starts duel retrieveSavedParametersAndCreateMessage(participant1Key, participant2Key, botUsernameKey, wagerKey, scoreNeededKey, responderKey, timestampKey, "agree") // Reset all saved values resetDuelSetup() case600 = 604 // Agree } } return case600 } // Counts number of Answer search terms when you input their index. function countSearchTermsAnswer(indexDisagree, indexAgree, indexDecline){ let countSearchTermsAnswer = 0 if ( indexDisagree != -1 ) { countSearchTermsAnswer++ } if ( indexAgree != -1 ) { countSearchTermsAnswer++ } if ( indexDecline != -1 ) { countSearchTermsAnswer++ } return countSearchTermsAnswer } // Getting more granular for the cases within the 700 category, when no duel is currently being set up function createCase700s(indexStartDuelAllParams, indexStartDuelWithWalkthrough, indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, duelSetupInProgressKey, participant1Key, participant2Key, responderKey, wagerKey, scoreNeededKey, timestampKey) { // Create cases for a switch-case. Initialize at 700. let case700 = 700 // If neither search term entered, send error message if ( (indexStartDuelAllParams == -1) && (indexStartDuelWithWalkthrough == -1) ) { // Create correct error message, tagging responder and saying which commands the bot is listening for. let MSG_UNKNOWN_COMMAND_SETUP_BASE = "" let TAG_INITIATOR = appendAtSymbol(usernameWhoPosted) MSG_UNKNOWN_COMMAND_SETUP_BASE = TAG_INITIATOR + MSG_UNKNOWN_COMMAND + MSG_UNKNOWN_COMMAND_START api_postChatMessageToGroup({"message": MSG_UNKNOWN_COMMAND_SETUP_BASE}) case700 = 701 // Initiator didn't include anything that would set up a duel } else { // As long as one of those search terms is found, save participant1 let participant1 = usernameWhoPosted scriptProperties.setProperty(participant1Key, participant1) if (indexStartDuelAllParams != -1) { // First, see if they included the names of the parameters, i.e. starting a duel the normal way. // Count number of search terms found. let numberSearchTerms = countSearchTerms(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, "standard") // If none are found, check if they tried to include all parameters in a single string if (numberSearchTerms == 0) { // Split off the command term from the rest of the string let lengthWalkthrough = text.length let inputString = createSubstring(text, indexStartDuelAllParams + 43, lengthWalkthrough) // Command term is 43 characters, hence why it's the offset // In case the command term was followed by a colon, remove it inputString = removeFirstColon(inputString) // Check if it's a valid string (looking at number of semicolons) let validInputStringSemicolons = checkCompleteDuelStartStringSemicolons(inputString) // If not a valid string, it's identical to 400s. Run it through that function. if (!validInputStringSemicolons) { let caseTemp = createCase400s(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, duelSetupInProgressKey, participant1Key, participant2Key, responderKey, wagerKey, scoreNeededKey, timestampKey) // Error message gets posted by the above function, so no further action needed. case700 = 702 // Initiator didn't include any parameters. } else if (validInputStringSemicolons) { // Now check to see if the numeric parameters are numbers let validInputStringNumeric = checkCompleteDuelStartStringNumeric(inputString, participant2Key, wagerKey, scoreNeededKey, usernameWhoPosted, false) // If invalid, error messages have already been posted. if (!validInputStringNumeric) { case700 = 703 // Numeric parameters were not numbers } else if (validInputStringNumeric) { // If valid, parameters have already been saved. // Now that the parameters have been set, flip these to the correct value. let duelSetupInProgress = 2 let responder = 2 // Grab current timestamp let timestamp = Date.now() // Save these scriptProperties.setProperty(duelSetupInProgressKey, duelSetupInProgress) scriptProperties.setProperty(responderKey, responder) scriptProperties.setProperty(timestampKey, timestamp) // Create message asking agree/disagree/counteroffer. retrieveSavedParametersAndCreateMessage(participant1Key, participant2Key, botUsernameKey, wagerKey, scoreNeededKey, responderKey, timestampKey, "standard") case700 = 704 // Duel initiator set up parameters, single-string version (i.e. no command terms like "Wager:" } else { // This condition shouldn't be possible let MSG_ERROR_SWITCH_CASE_700_NUMERIC = MSG_ERROR_BEGINNING + MSG_ERROR_CASE700S_NUMERIC + MSG_ERROR_ENDING api_postChatMessageToGroup({"message": MSG_ERROR_SWITCH_CASE_700_NUMERIC}) } } else { // This condition shouldn't be possible let MSG_ERROR_SWITCH_CASE_700_SEMICOLONS = MSG_ERROR_BEGINNING + MSG_ERROR_CASE700S_SEMICOLONS + MSG_ERROR_ENDING api_postChatMessageToGroup({"message": MSG_ERROR_SWITCH_CASE_700_SEMICOLONS}) } } else { // If some command terms are found (such as "Wager:"),this is identical to the 400 category. Use that function. let caseTemp2 = createCase400s(indexStartDuelOpponent, indexStartDuelWager, indexStartDuelScoreNeeded, text, usernameWhoPosted, botUsername, botUsernameKey, duelSetupInProgressKey, participant1Key, participant2Key, responderKey, wagerKey, scoreNeededKey, timestampKey ) // All of the case400s except one post all the relevant messages. Check if it's that one exception. if (caseTemp2 == 404) { case700 = 705 // Initiator setting up a duel, identical to case 404. } else { case700 = 706 // Initiator setting up a duel, identical to the 400s (except 404) } } } else if (indexStartDuelWithWalkthrough != -1) { let duelSetupInProgress = 1 // This number indicates parameters have not yet been set scriptProperties.setProperty(duelSetupInProgressKey, duelSetupInProgress) // Save this value api_postChatMessageToGroup({"message": MSG_DUEL_START1 + botUsername + MSG_DUEL_START2 + MSG_DUEL_START3}) case700 = 707 // Initiator setting up a duel with walkthrough messages } } return case700 } // Updates webhook function api_updateWebhook(webhookUrl, payload){ const params = { "method" : "put", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user/webhook/" + webhookUrl return UrlFetchApp.fetch(url, params) } // FUNCTIONS FOR DEBUGGING. SCRIPT DOES NOT USE THEM, THEY MUST BE TRIGGERED MANUALLY // Retrieves saved values (duel parameters) and posts them function debugGetSavedValuesParameters(){ let duelSetup = Number(scriptProperties.getProperty(DUEL_SETUP_IN_PROGRESS_KEY)) let participant1 = scriptProperties.getProperty(PARTICIPANT1_KEY) let participant2 = scriptProperties.getProperty(PARTICIPANT2_KEY) let responder = Number(scriptProperties.getProperty(RESPONDER_KEY)) let wager = Number(scriptProperties.getProperty(WAGER_KEY)) let scoreNeeded = Number(scriptProperties.getProperty(SCORE_NEEDED_KEY)) let timestamp = Number(scriptProperties.getProperty(TIMESTAMP_KEY)) // Saving these values will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_postChatMessageToGroup({"message": "Saved values are as follows: duelSetupInProgress" + MSG_JOINER_BEFORE + duelSetup + MSG_JOINER_AFTER + "participant1" + MSG_JOINER_BEFORE + participant1 + MSG_JOINER_AFTER + "participant2" + MSG_JOINER_BEFORE + participant2 + MSG_JOINER_AFTER + "responder" + MSG_JOINER_BEFORE + responder + MSG_JOINER_AFTER + "wager" + MSG_JOINER_BEFORE + wager + MSG_JOINER_AFTER + "scoreNeeded" + MSG_JOINER_BEFORE + scoreNeeded + MSG_JOINER_AFTER + "timestamp" + MSG_JOINER_BEFORE + timestamp + "`"}) } // Retrieves saved values (from Duel End string) and posts them function debugGetSavedValuesDuelEndString(){ let winner = scriptProperties.getProperty(WINNER_KEY) let didntWin = scriptProperties.getProperty(LOSER_KEY) let wagerWinner = Number(scriptProperties.getProperty(WAGER_WINNER_KEY)) let scoreNeededWinner = Number(scriptProperties.getProperty(SCORE_NEEDED_WINNER_KEY)) let hashAll = Number(scriptProperties.getProperty(HASH_ALL_KEY)) let hashSum = Number(scriptProperties.getProperty(HASH_SUM_KEY)) // Saving these values will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_postChatMessageToGroup({"message": "Saved values from Duel End string are as follows: winner" + MSG_JOINER_BEFORE + winner + MSG_JOINER_AFTER + "didntWin" + MSG_JOINER_BEFORE + didntWin + MSG_JOINER_AFTER + "wagerWinner" + MSG_JOINER_BEFORE + wagerWinner + MSG_JOINER_AFTER + "scoreNeededWinner" + MSG_JOINER_BEFORE + scoreNeededWinner + MSG_JOINER_AFTER + "hashAll" + MSG_JOINER_BEFORE + hashAll + MSG_JOINER_AFTER + "hashSum" + MSG_JOINER_BEFORE + hashSum + "`"}) } // Resetting bot username, such as if that username changes function debugResetBotUsername() { scriptProperties.setProperty(BOT_USERNAME_KEY, "userBot") // Save bot username as initialized/original value // Repost intro message, which triggers script to save username api_postChatMessageToGroup({"message": MSG_INTRO}) return HtmlService.createHtmlOutput() } // Resetting saved Group ID and updating webhook. // First, make sure that the Required Customizations have been updated correctly, then you can run this function. function debugResetSavedGroupId(){ // Save the correct group ID, whether party or Guild var groupIdKey = GROUP_ID_KEY const PARTY_ID = getParty() scriptProperties.setProperty(groupIdKey, PARTY_ID) // Update the webhook so that it is for the new group ID const GROUP_ID = scriptProperties.getProperty(groupIdKey) const options = { "groupId" : GROUP_ID, } const payload = { "url" : WEB_APP_URL, "label" : SCRIPT_NAME + " Webhook", "type" : "groupChatReceived", "options" : options, } // Update the webhook so that it is for the new group ID api_updateWebhook(WEB_APP_URL, payload) } // Manually have duel announcer bot post announcement message so that the script can save username function debugPostBotIntroMessage(){ api_postChatMessageToGroup({"message": MSG_INTRO}) } // Forcibly resetting Duel Start parameters, such as if they don't save for update like they're supposed to. function debugResetDuelStartParamenters() { resetDuelSetup() } // Forcibly resetting Duel End saved values function debutResetDuelEndParameters() { resetDuelEnd() } // Toggles between who is supposed to be responding (user 1 or user 2), in case the script gets stuck function debugToggleResponder(){ // Retrieve saved value var responderKey = RESPONDER_KEY var responder = Number(scriptProperties.getProperty(responderKey)) // Switch to the other responder if (responder == 1) { responder = 2 } else if (responder == 2) { responder = 1 } // Save new value scriptProperties.setProperty(RESPONDER_KEY, responder) }