Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Markus Wollny
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Markus Wollny

Using Contextual SMS Short Code Messages With TextMarks And ColdFusion

By
Published in Comments (11)

Yesterday, I explored the free SMS short code and web integration functionality provided by TextMarks. As I demonstrated, TextMarks acts as a proxy between cellular users and your web-based application. Because TextMarks uses a single, shared SMS short code, routing to your particular web-based application has to be done through a keyword which must be the leading piece of data in each TextMarks SMS request. The use of this routing keyword can be quite cumbersome; so, to alleviate this requirement, TextMarks provides "contextual responses".

When a user makes an SMS request using your keyword, that keyword takes over that user's context. Once the context is set, the user can then make subsequent requests without your routing keyword and TextMarks will assume your keyword based on the existing context. The huge caveat here, however, is that the subsequent requests must contain a minimal amount of data like a single digit or a character or a zip code. While this might seem silly, it's the only way to make sure that a subsequent request and a new routing directive are not confused.

NOTE: The most critical shortcoming of TextMarks' contextual response limitations is that the response, "Y" is not valid. If you want to use a single character response, you can only use characters "A" through "T". This removes the ability to do the most basic, most intuitive (Y)Yes / (N)No responses. As such, I had to fall back on the numerical digits 1/0 rather that Y/N to represent yes and no responses.

To explore this functionality, I came up with a very basic proof of concept: Blackjack. In the card game, Blackjack, the user must Hit or Stand in an attempt to get as close to 21 as possible without going bust. Once the cards are dealt, the user must make subsequent SMS requests back to TextMarks to submit his or her choice. Rather than sending the keyword "blackjack" with each subsequent request, TextMarks allows the user to use simple, one-digit responses to leverage the current context.

Before we get into the code, I just want to make it clear that this is a proof of concept and is not meant to be the most well designed web application.

Because a cell phone does not pass any cookies across with its SMS messages, session management takes on a different paradigm. Rather than letting ColdFusion manage the sessions implicitly, we are going to be creating and caching sessions explicitly in the Application scope. The unique ID used for the session caching and retrieval is passed into our web-application by TextMarks as part of our URL configuration.

Application.cfc

<cfcomponent
	output="false"
	hint="I define the application settings and event handlers.">

	<!--- Define the application. --->
	<cfset this.name = hash( getCurrentTemplatePath() ) />
	<cfset this.applicationTimeout = createTimeSpan( 0, 1, 0, 0 ) />

	<!--- Define page request settings. --->
	<cfsetting showdebugoutput="false" />


	<cffunction
		name="onApplicationStart"
		access="public"
		returntype="boolean"
		output="false"
		hint="I initialize the application.">

		<!---
			Create a struct to hold the active sessions. We will
			need this since there is no way for the cell phone to
			maintain session accross the API requests.
		--->
		<cfset application.phoneSessions = {} />

		<!---
			Create a constant array of the cards in our deck
			so that we can always duplicate it when we need
			a new deck.
		--->
		<cfset application.newCardDeck = listToArray(
			repeatString( "A,K,Q,J,10,9,8,7,6,5,4,3,2,", 4 )
			) />

		<!--- Return true so the request is processed. --->
		<cfreturn true />
	</cffunction>


	<cffunction
		name="onRequestStart"
		access="public"
		returntype="boolean"
		output="false"
		hint="I initialize the request.">

		<!---
			Check to see if we are manually resetting the
			application (for debugging purposes).
		--->
		<cfif structKeyExists( url, "reset" )>

			<!--- Manually initialize the application. --->
			<cfset this.onApplicationStart() />

		</cfif>

		<!--- Return true so the request is processed. --->
		<cfreturn true />
	</cffunction>


	<cffunction
		name="onRequest"
		access="public"
		returntype="void"
		output="true"
		hint="I execute a rendering template.">

		<!--- Include the UDF library. --->
		<cfinclude template="./udf.cfm" />

		<!---
			No matter what page was requested, just include our
			front controller.
		--->
		<cfinclude template="./index.cfm" />

		<!--- Return out. --->
		<cfreturn />
	</cffunction>

</cfcomponent>

As you can see, the Application scope has a cache for our phone sessions (our explicit session management) and a fresh deck of cards. The deck of cards is represented by an array of 52 items - 4 sets of suitless cards. I'm keeping a deck of cards in the application scope so that I can easily duplicate it later when I need a fresh deck.

The bulk of the processing happens in the front-controller, index.cfm. When a user interacts with the Blackjack application, there are essentially two phases that any game can be in: New, in which the cards are dealt or Active, in which the user is making Hit / Stand decisions. Once a game is over (a winner is selected), any subsequent request to the Blackjack application deals a new hand in the user's session.

Index.cfm

<!---
	Param the request data. In TextMarks, we are going to
	set up "smsdata" to contain the "\0" data, which is the
	entire request made by the user.
--->
<cfparam name="url.smsdata" type="string" default="" />

<!---
	This is the unique ID of the cell phone making the request.
	This ID is supplied by TextMarks and is garanteed to be a
	unique numeric value.
--->
<cfparam name="url.uid" type="numeric" default="0" />

<!---
	This is the phone number of the user placeing the request.
	We don't really need it for this, but I just wanted to see
	how the number came across in the demo.
--->
<cfparam name="url.number" type="string" default="" />


<!---
	Check to see if this SMS user already has an active session
	in the application cache. If so, we need to grab it. If not,
	then we will need to create a new one.
--->
<cfif structKeyExists( application.phoneSessions, url.uid )>

	<!--- Grab the cached user session. --->
	<cfset phoneSession = application.phoneSessions[ url.uid ] />

<cfelse>

	<!---
		Create a new user session for this cell user. This will
		contain information about the current game.
	--->
	<cfset phoneSession = {
		cardDeck = [],
		userHand = [],
		dealerHand = [],
		gameStatus = "",
		previousResponse = ""
		} />

	<!--- Cache the user session. --->
	<cfset application.phoneSessions[ url.uid ] = phoneSession />

</cfif>


<!---
	ASSERT: At this point, no matter whether a cell user made
	their first SMS request or a subsequent request, we now have
	a valid phone session variable.
--->



<!---
	Check to see if we need to take action on the current game
	based on it's status.

	NOTE: We are doing this if the game is fresh or if it is
	over. Therefore, we will never truly have a valid "over"
	state in the proceeding logic (seeing as the status will
	be switched over to "new" right now).
--->
<cfif (
	(phoneSession.gameStatus eq "") ||
	(phoneSession.gameStatus eq "over")
	)>

	<!--- Reset the game status. --->
	<cfset phoneSession.gameStatus = "new" />

	<!--- Reset the player hands. --->
	<cfset phoneSession.userHand = [] />
	<cfset phoneSession.dealerHand = [] />

	<!--- Create a new deck of cards. --->
	<cfset phoneSession.cardDeck = duplicate(
		application.newCardDeck
		) />

	<!--- Shuffle the cards. --->
	<cfset createObject( "java", "java.util.Collections" ).shuffle(
		phoneSession.cardDeck
		) />

	<!--- Clear the previous response. --->
	<cfset phoneSession.previousResponse = "" />

</cfif>



<!---
	Now that we have the SMS data, let's break it up into
	an array so that we can start to treat it like actions.
--->
<cfset tokens = listToArray( trim( url.smsdata ), " ,*-" ) />


<!---
	Wrap the processing in a try/catch so if something does
	go wrong, we can catch it and return a response.
--->
<cftry>

	<!---
		Only certain tokens will be available in certain phases
		of the game. Therefore, we will break this down by game
		status before we check token.

		NOTE: We never check for the status of "OVER" as this is
		not really a valid status. Any game with status "over"
		will be updated above and converted to "NEW".
	--->
	<cfswitch expression="#phoneSession.gameStatus#">

		<cfcase value="new">

			<!--- Deal two cards to the user. --->
			<cfset phoneSession.userHand[ 1 ] = phoneSession.cardDeck[ 1 ] />
			<cfset phoneSession.userHand[ 2 ] = phoneSession.cardDeck[ 2 ] />

			<!--- Deal two cards to the dealer. --->
			<cfset phoneSession.dealerHand[ 1 ] = phoneSession.cardDeck[ 3 ] />
			<cfset phoneSession.dealerHand[ 2 ] = phoneSession.cardDeck[ 4 ] />

			<!--- Remove top four cards. --->
			<cfset arrayDeleteAt( phoneSession.cardDeck, 1 ) />
			<cfset arrayDeleteAt( phoneSession.cardDeck, 1 ) />
			<cfset arrayDeleteAt( phoneSession.cardDeck, 1 ) />
			<cfset arrayDeleteAt( phoneSession.cardDeck, 1 ) />

			<!--- Update the status of the game. --->
			<cfset phoneSession.gameStatus = "active" />

			<!--- Build the response. --->
			<cfsavecontent variable="responseData">
				<cfoutput>

					Dealer: ---<br />
					#phoneSession.dealerHand[ 1 ]# *<br />
					.<br />
					You: ---<br />
					<cfloop
						index="card"
						array="#phoneSession.userHand#"
						>#card# </cfloop
					><br />
					.<br />
					Hit? (1=Y/0=N)

				</cfoutput>
			</cfsavecontent>

		</cfcase>


		<!--- ---------------------------------------------- --->
		<!--- ---------------------------------------------- --->


		<cfcase value="active">

			<!---
				Check to see how the user responded to the
				request to HIT. They could only respond with "1"
				(yes) or "0" (no).

				Param the value such that if it is not met, it
				will throw an exception - we'll let the exception
				handling just repopulate the current screen.
			--->
			<cfparam
				name="tokens[ 1 ]"
				type="regex"
				pattern="(1|0)"
				/>

			<!--- Check to see if the user wants to hit. --->
			<cfif (tokens[ 1 ] eq 1)>

				<!--- User wants to hit. --->

				<!--- Grab the next card off the deck. --->
				<cfset arrayAppend(
					phoneSession.userHand,
					phoneSession.cardDeck[ 1 ]
					) />

				<!---
					Delete the card from the deck so that it
					cannot be used on subsequent hits.
				--->
				<cfset arrayDeleteAt( phoneSession.cardDeck, 1 ) />

				<!---
					Now that the user has taken another card,
					let's check to see if the user is bust (hand
					is over 21). If so, then the game is over.
				--->
				<cfif !isHandBust( phoneSession.userHand )>

					<!---
						The user hand is valid. They can still
						go again. Present the current cards and
						the choice to go again.
					--->
					<cfsavecontent variable="responseData">
						<cfoutput>

							Dealer: ---<br />
							#phoneSession.dealerHand[ 1 ]# *<br />
							.<br />
							You: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.userHand#"
								>#card# </cfloop
							><br />
							.<br />
							Hit? (1=Y/0=N)

						</cfoutput>
					</cfsavecontent>

				<cfelse>

					<!---
						The user busted; update the status of the
						game to be over (this will trigger a new
						game on the next request).
					--->
					<cfset phoneSession.gameStatus = "over" />

					<!---
						The user hand is bust. Display the game
						over screen and allow user to start a
						new game.
					--->
					<cfsavecontent variable="responseData">
						<cfoutput>

							Dealer: ---<br />
							#phoneSession.dealerHand[ 1 ]# *<br />
							.<br />
							You: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.userHand#"
								>#card# </cfloop
							><br />
							BUSTED!! (#getHandTotal( phoneSession.userHand )#)<br />
							.<br />
							Play Again? (1=Y/0=N)

						</cfoutput>
					</cfsavecontent>

				</cfif>

			<cfelse>

				<!---
					The user wants to "Stand". Now, no matter what
					the outcome, the game is over - update the
					status of the game to "over". This will trigger
					a new game on the next request.
				--->
				<cfset phoneSession.gameStatus = "over" />

				<!---
					User wants to stand. Now, it's the dealer's
					turn to go. The dealer has to keep taking
					cards until they are over a sum of 17.
				--->
				<cfloop
					condition="dealerMustHit( phoneSession.dealerHand )">

					<!--- Grab the next card off the deck. --->
					<cfset arrayAppend(
						phoneSession.dealerHand,
						phoneSession.cardDeck[ 1 ]
						) />

					<!---
						Delete the card from the deck so that it
						cannot be used on subsequent hits.
					--->
					<cfset arrayDeleteAt(
						phoneSession.cardDeck,
						1
						) />

				</cfloop>


				<!---
					Now that we have populated the dealer's hand,
					let's check to see if it is bust.
				--->
				<cfif isHandBust( phoneSession.dealerHand )>

					<!---
						The dealer hand is bust. Display the game
						over screen and allow user to start a
						new game.
					--->
					<cfsavecontent variable="responseData">
						<cfoutput>

							Dealer: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.dealerHand#"
								>#card# </cfloop
							><br />
							BUSTED!! (#getHandTotal( phoneSession.dealerHand )#)<br />
							.<br />
							You: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.userHand#"
								>#card# </cfloop
							><br />
							.<br />
							Play Again? (1=Y/0=N)

						</cfoutput>
					</cfsavecontent>

				<!---
					If the dealer is not bust, check to see if
					their hand is greater than the user's hand.
				--->
				<cfelseif (getHandTotal( phoneSession.dealerHand ) gte getHandTotal( phoneSession.userHand ))>

					<!--- Dealer wins! --->
					<cfsavecontent variable="responseData">
						<cfoutput>

							Dealer: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.dealerHand#"
								>#card# </cfloop
							><br />
							WINNER!!<br />
							.<br />
							You: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.userHand#"
								>#card# </cfloop
							><br />
							.<br />
							Play Again? (1=Y/0=N)

						</cfoutput>
					</cfsavecontent>

				<!---
					If the dealer is not bust and the dealer's
					hand is not as good as the user's hand then
					the user has won.
				--->
				<cfelse>

					<!--- User wins! --->
					<cfsavecontent variable="responseData">
						<cfoutput>

							Dealer: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.dealerHand#"
								>#card# </cfloop
							><br />
							.<br />
							You: ---<br />
							<cfloop
								index="card"
								array="#phoneSession.userHand#"
								>#card# </cfloop
							><br />
							WINNER!!<br />
							.<br />
							Play Again? (1=Y/0=N)

						</cfoutput>
					</cfsavecontent>

				</cfif>

			</cfif>

		</cfcase>

	</cfswitch>


	<!--- Catch any exception. --->
	<cfcatch>

		<!---
			Check to see if we have a previous output we
			could try resending.
		--->
		<cfif len( phoneSession.previousResponse )>

			<!---
				Use the previous response as the current
				response. This way, they may have a chance
				to recheck their inputs.
			--->
			<cfset responseData = phoneSession.previousResponse />

		<cfelse>

			<!--- An error occurred. Alert the user and. --->
			<cfsavecontent variable="responseData">

				Ooops<br />
				An error occurred.<br />
				Check your inputs.<br />

			</cfsavecontent>

		</cfif>

	</cfcatch>

</cftry>


<!---
	Store the current response into the previous output
	variable. This way, if something goes wrong, we can
	return the previous screen.
--->
<cfset phoneSession.previousResponse = responseData />


<!---
	At this point, we should have a valid response in our
	respondData variable. Now, we need to clean it up for
	SMS response. Let strip out the extra white space,
	including the leading / trailing line spaces.

	NOTE: TextMarks will automatically take care of stripping
	out our <BR /> tags and replacing them with text-valid
	line breaks.
--->
<cfset responseData = reReplace(
	trim( responseData ),
	"(?m)(^[ \t]+|[ \t]+$)",
	"",
	"all"
	) />


<!---
	Convert the respond data into a binary variable so that we
	can easily stream it back using CFContent without having to
	be fanatical about clearing the content buffer.
--->
<cfset binaryResponse = toBinary( toBase64( responseData ) ) />

<!--- Set headers. --->
<cfheader
	name="content-length"
	value="#arrayLen( binaryResponse )#"
	/>

<!---
	Stream content back as HTML. By using the Variable
	attribute, we are ensuring that no extra white space is
	being passed back.
--->
<cfcontent
	type="text/html"
	variable="#binaryResponse#"
	/>

Because cell phone SMS requests do not pass cookies, I am manually managing the sessions based on the unique phone ID passed in from TextMarks. The first thing I do in the request is check to see if the current user already has an active session. If they do, I get a reference to it; if they do not, I create one and then cache if for subsequent requests. Once this is done, the rest of the page logic can refer to the current user's "phoneSession" which will be persisted from request to request.

The rest of the code is fairly top-down self-explanatory. Each game gets a new deck of cards, which is duplicated from the application-cached deck and then shuffled. As each user hits, a card is popped off of the deck and added to the given hand such that it cannot be used in subsequent hits.

The code above makes use of a user-defined function library which is included right before the front-controller. This library has some utility methods used to help count the sum of each hand and determine if the hand is bust.

UDF.cfm

<cffunction
	name="dealerMustHit"
	access="public"
	returntype="boolean"
	output="false"
	hint="I determine if the given dealer hand must hit (less than 17).">

	<!--- Define arguments. --->
	<cfargument
		name="hand"
		type="array"
		required="true"
		hint="I am the dealer hand being checked for hit requirement."
		/>

	<!--- Return true if the hand sum is less than 17. --->
	<cfreturn (getHandTotal( arguments.hand ) lt 17) />
</cffunction>


<cffunction
	name="getHandTotal"
	access="public"
	returntype="numeric"
	output="false"
	hint="I get the total sum of the cards in the given blackjack hand.">

	<!--- Define arguments. --->
	<cfargument
		name="hand"
		type="array"
		required="true"
		hint="I am the blackjack hand being counted (summed)."
		/>

	<!--- Define the local scope. --->
	<cfset var local = {} />

	<!--- Create a running total for the cards. --->
	<cfset local.sum = 0 />

	<!---
		Loop over cards to get the initial count. On the first
		pass, the Aces (A) will be counted as 11. If the hand
		is bust, they will be re-tallied with Ace being 2.
	--->
	<cfloop
		index="local.card"
		array="#arguments.hand#">

		<!--- Check to see what kind of card we have. --->
		<cfif isNumeric( local.card )>

			<!--- Simply add given value. --->
			<cfset local.sum += local.card />

		<cfelseif (toString( local.card ) eq "A")>

			<!--- On the first pass, add the Ace as an 11. --->
			<cfset local.sum += 11 />

		<cfelse>

			<!---
				All other cards will be face cards at this time,
				which will 10.
			--->
			<cfset local.sum += 10 />

		</cfif>

	</cfloop>


	<!---
		Now that we have summed the cards for the first time,
		check to see if the hand is bust.
	--->
	<cfif (local.sum lte 21)>

		<!--- The hand is not bust, so return sum. --->
		<cfreturn local.sum />

	</cfif>


	<!---
		The first pass resulted in a busted hand. This may or may
		not have been due to the way we counted Aces. As such,
		let's reset the sum and run through summing process again,
		this time with the Ace counting for 2.
	--->
	<cfset local.sum = 0 />

	<!--- Loop over cards a second time. --->
	<cfloop
		index="local.card"
		array="#arguments.hand#">

		<!--- Check to see what kind of card we have. --->
		<cfif isNumeric( local.card )>

			<!--- Simply add given value. --->
			<cfset local.sum += local.card />

		<cfelseif (local.card eq "A")>

			<!--- On the second pass, add the Ace as an 2. --->
			<cfset local.sum += 2 />

		<cfelse>

			<!---
				All other cards will be face cards at this time,
				which will 10.
			--->
			<cfset local.sum += 10 />

		</cfif>

	</cfloop>


	<!--- Now, no matter what sum we have, just return it. --->
	<cfreturn local.sum />
</cffunction>


<cffunction
	name="isHandBust"
	access="public"
	returntype="boolean"
	output="false"
	hint="I determine if the given hand is bust (over 21).">

	<!--- Define arguments. --->
	<cfargument
		name="hand"
		type="array"
		required="true"
		hint="I am the blackjack hand being checked for bust."
		/>

	<!--- Return true if the hand sum is over 21. --->
	<cfreturn (getHandTotal( arguments.hand ) gt 21) />
</cffunction>

I know that I'm not giving a lot of explanation as to how the code works, but hopefully you can look at the code and the comments and figure out what's going on. This post was more to explore the contextual response functionality that TextMarks provides rather than to explore Blackjack game architecture. One of the nice things about "contextual responses" is that your application doesn't have to know about it - the "context" is used in the TextMarks proxy and a normal request is made to your web-based application. The only thing that your web application needs to do is maintain some sort of session information across requests such that the contextual responses do have a valid meaning within your application.

Want to use code from this post? Check out the license.

Reader Comments

32 Comments

@Ben Great post, interesting idea on how to keep the phone in a session. I found the post was very easy to follow and very interesting.

I am interested on how the number did actually show up and where that interface from your video is available if it is for testing like that.

15,848 Comments

@Daniel,

The testing interface is actually provided by TextMarks itself on the TextMarks website. It's really easy to use - it acts just like a real phone. You might have to be logged-in, however because it send across your phone number.

The number shows up as something like:
+9175551234

1 Comments

Ben, nice experiment!

This won't work if more than one people are playing this game, right? How to make it work for multiple independent gamers?

32 Comments

@john,

From they way he has it set up it should be able to work with multiple users at the same time. Since phones can't keep track of their sessions he uses the application.phoneSessions[] variable to keep track of them for the phones. As he stated the unique id is what is used to tell the phones apart.

1 Comments

Just found this. Great job Ben. I'm new to SMS stuff in general. I'm really interested in how the phone number is matched up to the unique id. Is that a CF thing? Where is that being handled? Maybe I missed something in the code. Apologies in advance.

15,848 Comments

@Alex,

The Phone number to unique ID pairing is handled by the TextMarks people. I am not sure how they are doing it; but apparently, each phone is guaranteed to be unique. Maybe it's something that the phone sends with SMS meta-data or something behind the scenes?

1 Comments

Hello, I can't get mine to work. How did you get yours to work? You can send GameGoal to the 41411. Please tell me what I am doing wrong so I can correct it.

Thanks

15,848 Comments

@Matthew,

There are many things that could be going wrong; there's no way I would be able to tell the one thing that is going wrong.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel