Azure Whatsapp Plugin

A plugin for Notification Service utilize Azure Communication Service's Advance Messaging with Whatsapp.

Prerequisite

Getting start

  • Setup Advance Messaging for WhatsApp in Azure Communication
  • Establish conversations with end devices. One possible way to do this is using free-entry point conversation, which is initiating the conversation from end devices by sending a WhatsApp message to the WhatsApp channel on Azure. Each conversation will be active for 24-hours from the time the message is sent, and if there is any notification message to the end device, that conversation will be active for 72-hours. After that, the conversation needs to be refresh by sending another message from end device. For more options in establishing conversations, please refer to WhatsApp documentation.
  • Obtain WhatsApp Channel ID and Connection String. Create a Generic Credential in Windows Credentials with Channel ID as Username and Connection String as Password. Give it a desirable name in "Internet or network address" field

Configuration

Configuring the Notification Plugin

For the configuration, there will be seven available parameters which will be configured in the Notification Plugin table in the ConfigurationString section. Each parameter will be separated by a semi-colon ( ; ). Keywords are not case sensitive, values are.

KeywordShort
CredentialcredWindows Credential name containing the username/password for the SMTP server. The credential's type must be generic and not windows. Notes: Make sure the credential is on the same account that the service is running.
TraceLevel (Optional)tlDefine trace level for logging. Default value is 3. (Off = 0, Error = 1, Warning = 2, Info =3, Verbose =4, Very Verbose = 5)

Configuring the Notification Subscription

In the Notification Subscription section, the ConfigurationString will contain a list of phone numbers that will receive notifications. Phone number must be in international format with country code and separated by a semi-colons.

e.g.: +358 #########; +1 #########;

Source Codes

namespace ABB.Vtrin.NotificationService
{
	public class NotificationServicePlugin_Whatsapp : INotificationServicePlugin
	{
		private static readonly int								mDefaultTraceLevel	=(int)System.Diagnostics.TraceLevel.Info;
		private static readonly Diagnostics.cTraceSwitch	mLog						=new("WhatsappPlugin", string.Empty, "Info");
		private			readonly	cConfiguration					mConfiguration;
		
		private class cAccount
		{
			public readonly string?                       Username;
			public readonly System.Security.SecureString? Password;
			public cAccount(string? username, System.Security.SecureString? password)
			{
				Username=username;
				Password=password;
			}
		}
		private class cConfiguration
		{
			public readonly string	Credential;
			public readonly int		TraceLevel;

			public cConfiguration(string credential, int tracelevel) 
			{
				Credential=credential;
				TraceLevel=tracelevel;
			}

			public static cConfiguration GetConfiguration(string configurationstring)
			{
				string credential =string.Empty;
				int    tracelevel =NotificationServicePlugin_Whatsapp.mDefaultTraceLevel;
				
				var parts=configurationstring.Split(';', System.StringSplitOptions.RemoveEmptyEntries);
				foreach(string part in parts)
				{
					var keyvalue=part.Split('=');
					if(keyvalue.Length==2)
					{							
						var key=keyvalue[0].Trim().ToLower();
						var value=keyvalue[1].Trim();
						if(string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
						{
							mLog.LogError($"Cannot read configuration string '{part}'. Key or Value is missing.");
							continue;
						}
						switch (key) 
						{							
							case "credential":
							case "cred":
								credential=value;
								break;
							case "tracelevel":
							case "tl":
								tracelevel=Utils.ParseInt(value, tracelevel);
								break;
							default:
								mLog.LogError($"Unrecognized keyword '{key}'");
								break;
						}
					} else
					{
						mLog.LogError($"Check configuration string: '{part}'");
					}
				}

				mLog.LogInfo(
					$"""
					Whatsapp Plugin Configuration:
					- Credential			: {credential}
					- Trace Level			: {tracelevel}
					""");

				return new cConfiguration( credential, tracelevel );
			}
		}
		
		public NotificationServicePlugin_Whatsapp(string configuration)
		{
			mConfiguration=cConfiguration.GetConfiguration(configuration);
			mLog.IntLevel=mConfiguration.TraceLevel;			
		}

		public void HandleMessage(string title, string message, string configurationstring)
		{
			var credential=mFetchCredential(mConfiguration.Credential);
			if(credential is null)
			{
				mLog.LogError($"Failed to fetch credential: '{mConfiguration.Credential}'");
				return;
			}
			if(credential.Username is null)
			{
				mLog.LogError($"UserName in credential '{mConfiguration.Credential}' is null");
				return;
			}

			var client=new Azure.Communication.Messages.NotificationMessagesClient(mConvertToUnsecureString(credential.Password));
			var channelregistrationid=new System.Guid(credential.Username);
			string[] recipients=mGetRecipients(configurationstring);			
			var messagewithtitle=$"{title}\n {message}";
			mLog.LogVerbose($"Sending notification:\n{messagewithtitle}");
			//Only one phone number is currently supported in the recipient list.
			//https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/advanced-messaging/whatsapp/get-started?tabs=visual-studio%2Cconnection-string&pivots=programming-language-csharp
			foreach(var recipient in recipients) 
			{
				mLog.LogVerbose($"Sending notification to recipient: '{recipient}'");
				var textnotificationcontent=new Azure.Communication.Messages.TextNotificationContent(channelregistrationid, new  System.Collections.Generic.List<string> {recipient}, messagewithtitle);
				var sendtextmessageresult=client.Send(textnotificationcontent);
				mLog.LogVerbose(sendtextmessageresult);
			}
		}

		private static cAccount? mFetchCredential(string credential)
		{
			mLog.LogVerbose($"Fetching credential: {credential}");
			try
			{
				var cred=ABB.Vtrin.cDataLoader.cCredential.Read(credential);
				if(cred is not null)
				{
					mLog.LogVerbose($"Succesfully fetched credential: '{credential}'.");
					return new cAccount(cred.UserName, cred.Password);
				} else
				{
					mLog.LogVerbose($"Fetching credential '{credential}' returned NULL.");
				}
			} catch(System.Exception ex)
			{
				mLog.LogError($"Exception raised when fetching credential '{credential}'. Message: {ex.Message}." );				
			}
			return null;
		}

		private static string? mConvertToUnsecureString(System.Security.SecureString? securestring)
		{
			 if (securestring==null)
			 {
				  return null;
			 }

			 var unmanagedstring=nint.Zero;
			 try
			 {
				  unmanagedstring = System.Runtime.InteropServices.Marshal.SecureStringToGlobalAllocUnicode(securestring);
				  return System.Runtime.InteropServices.Marshal.PtrToStringUni(unmanagedstring);
			 }
			 finally
			 {
				  System.Runtime.InteropServices.Marshal.ZeroFreeGlobalAllocUnicode(unmanagedstring);
			 }
		}

		private static string[] mGetRecipients(string recipients)
		{
			string[] recipientarray = recipients.Split(new char[] { ';' }, System.StringSplitOptions.RemoveEmptyEntries);
			return recipientarray;
		}
	}
}