{"id":932,"date":"2024-06-05T18:45:18","date_gmt":"2024-06-05T23:45:18","guid":{"rendered":"https:\/\/80bits.blog\/?p=932"},"modified":"2024-06-05T19:09:38","modified_gmt":"2024-06-06T00:09:38","slug":"desarrolla-una-aplicacion-de-envio-de-sms-con-android-ndejs-y-websockets","status":"publish","type":"post","link":"https:\/\/80bits.blog\/index.php\/2024\/06\/05\/desarrolla-una-aplicacion-de-envio-de-sms-con-android-ndejs-y-websockets\/","title":{"rendered":"Desarrolla una aplicaci\u00f3n de env\u00edo de SMS con android, NodeJS y Websockets"},"content":{"rendered":"\n<p>A menudo, cuando necesito un servicio de SMS para alg\u00fan proyecto, recurro a plataformas como Twilio, que son bastante robustas y confiables. Sin embargo, en ocasiones solo necesito probar ciertos escenarios o enviar alertas, y no quiero incurrir en gastos adicionales.<\/p>\n\n\n\n<p>En mi pa\u00eds, los SMS son gratuitos en todos los planes de telefon\u00eda o en la mayor\u00eda de ellos, por lo que enviar un mensaje de texto no me cuesta nada si ya tengo un n\u00famero de tel\u00e9fono. Adem\u00e1s, adquirir uno nuevo no suele superar los 10 d\u00f3lares en muchos casos.<\/p>\n\n\n\n<p>Para resolver esta necesidad, desarroll\u00e9 una aplicaci\u00f3n en Android utilizando Kotlin. Esta aplicaci\u00f3n funciona como un servicio persistente en segundo plano y se conecta a un WebSocket. A trav\u00e9s del WebSocket, la aplicaci\u00f3n recibe el texto del mensaje y el n\u00famero al que se enviar\u00e1 el SMS.<\/p>\n\n\n\n<p>Tambi\u00e9n escrib\u00ed el c\u00f3digo del WebSocket de manera sencilla en NodeJS. Esta soluci\u00f3n me permite aprovechar los SMS gratuitos incluidos en la mayor\u00eda de los planes de telefon\u00eda de mi pa\u00eds, sin incurrir en costos adicionales.<\/p>\n\n\n\n<p>Adem\u00e1s, tanto la aplicaci\u00f3n como el backend pueden soportar m\u00faltiples conexiones simult\u00e1neas. El c\u00f3digo de NodeJS se encarga de gestionar estas conexiones y, de manera aleatoria, selecciona uno de los clientes conectados (la aplicaci\u00f3n Android) para enviar el SMS.<\/p>\n\n\n\n<p>Esta estrategia ofrece varias ventajas:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Distribuci\u00f3n de la carga<\/strong>: Al seleccionar aleatoriamente un dispositivo del pool, se distribuye la carga de env\u00edo de SMS entre m\u00faltiples dispositivos.<\/li>\n\n\n\n<li><strong>Reducci\u00f3n de riesgos<\/strong>: Al utilizar varios dispositivos para enviar mensajes, se minimiza el riesgo de recibir penalizaciones por spam por parte de la operadora.<\/li>\n\n\n\n<li><strong>Escalabilidad<\/strong>: La soluci\u00f3n puede escalar f\u00e1cilmente agregando m\u00e1s dispositivos al pool para manejar un mayor volumen de mensajes.<\/li>\n<\/ol>\n\n\n\n<p>Finalmente, el c\u00f3digo del WebSocket en NodeJS es sencillo y eficiente, lo que facilita su mantenimiento y expansi\u00f3n seg\u00fan sea necesario.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"android\">Android<\/h2>\n\n\n\n<p>La aplicaci\u00f3n Android tiene una UI muy sencilla, que incluye:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Un campo para agregar la URL del WebSocket.<\/li>\n\n\n\n<li>Un texto que muestra el ID del dispositivo para identificarlo en el backend.<\/li>\n\n\n\n<li>Dos botones: uno para conectar y otro para desconectar.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-medium\"><img decoding=\"async\" width=\"300\" height=\"300\" src=\"https:\/\/80bits.blog\/wp-content\/uploads\/2024\/06\/smsgatewayshot01-300x300.jpg\" alt=\"SmsUI\" class=\"wp-image-938\" srcset=\"https:\/\/80bits.blog\/wp-content\/uploads\/2024\/06\/smsgatewayshot01-300x300.jpg 300w, https:\/\/80bits.blog\/wp-content\/uploads\/2024\/06\/smsgatewayshot01-1024x1024.jpg 1024w, https:\/\/80bits.blog\/wp-content\/uploads\/2024\/06\/smsgatewayshot01-150x150.jpg 150w, https:\/\/80bits.blog\/wp-content\/uploads\/2024\/06\/smsgatewayshot01-768x768.jpg 768w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><figcaption class=\"wp-element-caption\">SmsUI<\/figcaption><\/figure>\n\n\n\n<p>La aplicaci\u00f3n est\u00e1 desarrollada con <a href=\"https:\/\/developer.android.com\/develop\/ui\/compose\" target=\"_blank\" rel=\"noopener\" title=\"\">Jetpack Compose<\/a>, por lo que es sencilla de entender a nivel UI. Por lo tanto, solo explicar\u00e9 las partes m\u00e1s esenciales del c\u00f3digo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"conexi\u00f3n-a-websocket\">Conexi\u00f3n a websocket<\/h3>\n\n\n\n<p>La parte central del c\u00f3digo es la conexi\u00f3n al servicio de WebSocket para esto use la librer\u00eda <code>OkHttp<\/code>, agregarla es tan simple como poner esto en las dependencias de graddle.<\/p>\n\n\n\n<p><a href=\"https:\/\/80bits.blog\/index.php\/2023\/12\/13\/flashmq-como-instalar-un-servidor-mqtt-en-ubuntu-22-04\/\" target=\"_blank\" rel=\"noopener\" title=\"\">Tal vez en el futuro cambie esta opci\u00f3n y la conexi\u00f3n sea mediante Mqtt, ya que tengo un servidor para ello<\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>implementation(\"com.squareup.okhttp3:okhttp:4.12.0\")\n<\/code><\/pre>\n\n\n\n<p>Hay una clase llamada <code>WebSocketManager<\/code>. Esta clase tiene una funci\u00f3n llamada <code>start<\/code>, que recibe dos par\u00e1metros: la URL (direcci\u00f3n y puerto) hacia donde se conectar\u00e1 el WebSocket y un ID. Este ID debe ser \u00fanico dentro del backend de WebSocket, por lo que se genera un UUID v4 para este fin.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private var client: OkHttpClient = OkHttpClient() \nprivate var webSocket: WebSocket? = null \nprivate var connectionStatus: CompletableFuture? = null\n\nfun start(url: String, id: String): Boolean { \n return try { \n connectionStatus = CompletableFuture() \n val request = Request.Builder().url(\"ws:\/\/$url?id=$id\").build() \n webSocket = client.newWebSocket(request, MyWebSocketListener(listener)) \n return connectionStatus!!.get() \n } catch (e: Exception) { \n false \n } \n}\n<\/code><\/pre>\n\n\n\n<p>Como se puede ver en el c\u00f3digo anterior, la conexi\u00f3n es bastante sencilla. Hay dos partes esenciales: la primera es que, para crear el WebSocket y conectarse a \u00e9l, se necesita enviar al m\u00e9todo <code>newWebSocket<\/code> un objeto <code>Request<\/code> que se crea b\u00e1sicamente con la URL y una implementaci\u00f3n de la interfaz <code>WebSocketListener<\/code>, que en este caso es mi clase llamada <code>MyWebSocketListener<\/code>.<\/p>\n\n\n\n<p>La diferencia aqu\u00ed radica en que, en el constructor de <code>MyWebSocketListener<\/code>, env\u00edo otra interfaz que puede ser implementada para propagar como callback el m\u00e9todo <code>onMessage<\/code> de la interfaz <code>WebSocketListener<\/code> de OkHttp.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>interface MessageListener { \n fun onMessage(message: SmsModel) \n}\n<\/code><\/pre>\n\n\n\n<p>Esta interface la uso para propagar el menaje que llega dentro del m\u00e9todo <code>onMessage<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private inner class MyWebSocketListener(private val callback: MessageListener)\n<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>override fun onMessage(webSocket: WebSocket, text: String) { \n try { \n println(\"Message received: $text\") \n val gson = Gson() \n val message = gson.fromJson(text, SmsModel::class.java) \n println(\"Message received: $message\") \n callback.onMessage(message) \n } catch (e: Exception) { \n println(\"Message received: $text\") \n } \n}\n<\/code><\/pre>\n\n\n\n<p>Al final del m\u00e9todo se llama a <code>callback.onMessage<\/code>, al que se le env\u00eda un objeto de tipo <code>SmsModel<\/code>, que se crea a partir del string de tipo JSON que recibe desde el WebSocket usando la librer\u00eda <code>Gson<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>implementation(\"com.google.code.gson:gson:2.11.0\")\n<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>data class SmsModel( \n val number: String, \n val text: String \n)\n<\/code><\/pre>\n\n\n\n<p>Esta interface solo sirve para poder propagar el mensaje y no implementar toda la interface <code>WebSocketListener<\/code> en otro lugar.<\/p>\n\n\n\n<p>Como \u00faltimo detalle, tengo un objeto llamado <code>connectionStatus = CompletableFuture()<\/code>. Esto lo \u00fanico que hace es bloquear el hilo actual desde donde se llama a la clase <code>WebSocketManager<\/code> para saber si el WebSocket se conect\u00f3 o no. Debido a que, como pueden ver, la conexi\u00f3n se maneja en una interfaz y callback, esto es un m\u00e9todo asincr\u00f3nico y quer\u00eda convertirlo en sincr\u00f3nico para que desde la interfaz se sepa si la conexi\u00f3n se realiz\u00f3 o no.<\/p>\n\n\n\n<p><a href=\"https:\/\/developer.android.com\/reference\/java\/util\/concurrent\/CompletableFuture\" target=\"_blank\" rel=\"noopener\" title=\"\">CompletableFuture<\/a><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"servicio-persistente\">Servicio persistente<\/h3>\n\n\n\n<p>Una vez que se tiene el c\u00f3digo para la conexi\u00f3n al backend, lo que necesitamos es crear un servicio en Android que persista en segundo plano para estar recibiendo los mensajes del WebSocket y enviar los SMS. Este servicio debe funcionar tanto si el sistema est\u00e1 en otra app como si est\u00e1 con la pantalla apagada.<\/p>\n\n\n\n<p>Para lograr esto en Android, tenemos que crear un servicio implementando la clase abstracta <code>Service<\/code>.<\/p>\n\n\n\n<p>Esta implementaci\u00f3n la hice en la clase <code>ForegroundService<\/code>, donde la conexi\u00f3n al WebSocket se maneja en el m\u00e9todo <code>onStartCommand<\/code>. Pero para que el servicio funcione de manera persistente, se necesitan hacer dos cosas:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>NotificationChannel<\/code>:<\/strong> Es un canal de notificaci\u00f3n necesario a partir de Android 8.0 para agrupar y categorizar notificaciones con configuraciones espec\u00edficas.<\/li>\n\n\n\n<li><strong><code>Notification<\/code>:<\/strong> Es el contenido visible de la notificaci\u00f3n que ves en la barra de notificaciones y que contiene el mensaje que deseas mostrar al usuario.<\/li>\n<\/ul>\n\n\n\n<p>Al final, la notificaci\u00f3n debe pasarse al m\u00e9todo <code>startForeground<\/code> junto con un ID de identificaci\u00f3n para que el sistema mantenga el servicio de manera persistente y no cierre el proceso.<\/p>\n\n\n\n<p>Para ejecutar este servicio, deber\u00e1 crearse un Intent. En el c\u00f3digo, se hace de la siguiente manera:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>viewModel.onIdChange(UUID.randomUUID().toString()) \nval serviceIntent = Intent(context, ForegroundService::class.java) \nserviceIntent.putExtra(context.getString(R.string.extra_url), viewModel.urlText) \nserviceIntent.putExtra(context.getString(R.string.extra_id), viewModel.id) \ncontext.startService(serviceIntent)\n<\/code><\/pre>\n\n\n\n<p>Lo relevante est\u00e1 en la segunda l\u00ednea, donde se pasa la referencia de la clase <code>ForegroundService<\/code> al Intent. Una vez se tiene el objeto Intent, se env\u00eda usando el contexto actual mediante la \u00faltima l\u00ednea con el m\u00e9todo <code>startService<\/code>.<\/p>\n\n\n\n<p>Las otras l\u00edneas son datos que se env\u00edan al servicio mediante <code>Extras<\/code>. Estos datos incluyen el ID y la URL que necesito para que el WebSocket se conecte al servicio. Estos datos se env\u00edan al servicio y del servicio al objeto de WebSocket para su conexi\u00f3n.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"env\u00edo-de-mensajes\">Env\u00edo de mensajes<\/h3>\n\n\n\n<p>El env\u00edo de mensajes es bastante simple, pero se necesitan algunos permisos en el dispositivo para poder hacerlo. Adem\u00e1s, el c\u00f3digo que implement\u00e9 solo funciona para la API 31 en adelante. Esto se debe a que mis snippets de c\u00f3digo para esta funcionalidad estaban desfasados y quer\u00eda implementar m\u00e9todos m\u00e1s nuevos.<\/p>\n\n\n\n<p>En todo caso, la implementaci\u00f3n es realmente sencilla y pueden ver c\u00f3mo funciona la clase <a href=\"https:\/\/developer.android.com\/reference\/android\/telephony\/SmsManager\" target=\"_blank\" rel=\"noopener\" title=\"\">SmsManager aqu\u00ed<\/a>.<\/p>\n\n\n\n<p>Mi clase <code>SmsService<\/code> tiene un solo m\u00e9todo que es <code>sendSms<\/code> y eso es todo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>fun sendSms(message: SmsModel) { \n \n if (ContextCompat.checkSelfPermission( \n context, \n \"android.permission.SEND_SMS\" \n ) == PackageManager.PERMISSION_GRANTED \n ) { \n \n if (message.number.isNotEmpty() &amp;&amp; message.text.isNotEmpty()) { \n val subscriptionManager = \n context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager \n val subscriptionId = \n subscriptionManager.activeSubscriptionInfoList&#91;0].subscriptionId \n val smsManager = context.getSystemService(SmsManager::class.java) \n .createForSubscriptionId(subscriptionId) \n smsManager.sendTextMessage(message.number, null, message.text, null, null) \n } \n } \n}\n<\/code><\/pre>\n\n\n\n<p>Las nuevas APIs para el env\u00edo de mensajes requieren que se verifiquen los permisos de env\u00edo de SMS antes de usarlas. Como pueden ver en el c\u00f3digo, esto se realiza antes de generar un env\u00edo.<\/p>\n\n\n\n<p>Este m\u00e9todo no tiene un mecanismo para saber si el mensaje se envi\u00f3 o no, por lo que, si el env\u00edo falla, no se puede saber de ninguna forma actualmente.<\/p>\n\n\n\n<p>Para implementar un mecanismo, hay que crear un Intent de tipo Broadcast. En el futuro, tal vez haga eso, para que en caso de que el SMS no se env\u00ede, se notifique al WebSocket y se intente nuevamente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"websocket-en-nodejs\">WebSocket en NodeJS<\/h2>\n\n\n\n<p>No entrar\u00e9 en detalles t\u00e9cnicos sobre el c\u00f3digo del WebSocket, ya que es realmente sencillo de implementar en NodeJS y a\u00fan m\u00e1s sencillo es el c\u00f3digo que escrib\u00ed.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const WebSocket = require('ws');\n\nconst { v4: uuidv4 } = require('uuid');\n\nconst { URL } = require('url');\n\nconst getRandomElementFromSet = set =&gt; {\n\nconst arr = Array.from(set).filter(item =&gt; item.id !== 'host');\n\nreturn arr&#91;Math.floor(Math.random() * arr.length)];\n\n};\n\nconst wss = new WebSocket.Server({ port: 8080 });\nlet clients = new Set();\n\nwss.on('connection', (ws, req) =&gt; {\n\nclients.add(ws);\n\nconst url = new URL(req.url, `http:\/\/${req.headers.host}`);\n\nconst deviceId = url.searchParams.get('id');\n\nif (deviceId === null || deviceId === undefined || deviceId === '') {\n\nws.id = uuidv4();\n\n}\n\nelse {\n\nws.id = deviceId;\n\n}\n\nws.on('message', message =&gt; {\n\nconsole.log(`Received message ${message} from connection ${ws.id}`); \n\nconst messages = JSON.parse(message);\n\nif (!messages) {\n\nconsole.log('No messages');\n\nreturn;\n\n}\n\nmessages.forEach(m =&gt; {\n\nconst randomClient = getRandomElementFromSet(clients);\n\nif (!randomClient) {\n\nconsole.log('No clients connected');\n\nreturn;\n\n}\n\nif (randomClient.readyState === WebSocket.OPEN) {\n\nconst messageToSend = JSON.stringify(m);\n\nrandomClient.send(`${messageToSend}`);\n\nconsole.log(`Sent message ${messageToSend} to connection ${randomClient.id}`);\n\n}\n\nelse {\n\nconsole.log(`Client ${randomClient.id} is not connected`);\n\nclients.delete(randomClient);\n\n}\n\n});\n\n});\n\nws.on('close', (code, reason) =&gt; {\n\nconsole.log(`Connection ${ws.id} closed. Code: ${code}. Reason: ${reason}`);\n\nclients.delete(ws);\n\n});\n\nconsole.log(ws.id);\n\n});\n<\/code><\/pre>\n\n\n\n<p>Simplemente se genera una escucha para conexiones WebSocket en el puerto 8080. Cuando un cliente nuevo (la app) se conecta al sistema, se obtiene su ID y se asigna ese ID como una propiedad en el objeto de WebSocket. Si no tiene un ID, se genera un UUID al azar y se agrega a un conjunto de clientes.<\/p>\n\n\n\n<p>Hay un ID que se espera que sea llamado <code>host<\/code>. Este ser\u00eda el servidor que enviar\u00e1 los mensajes a los clientes. Por cada mensaje que se env\u00ede, se obtiene un cliente aleatorio y se le env\u00eda el mensaje para que la app lo reciba y env\u00ede el SMS.<\/p>\n\n\n\n<p>El host deber\u00e1 enviar un string de tipo JSON, que deber\u00e1 ser un array de objetos con dos propiedades, tal como en este ejemplo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;\n{\n\"number\": \"123456\",\n\n\"text\": \"hola\"\n}\n]\n<\/code><\/pre>\n\n\n\n<p>Con esto, nos aseguramos de que se puedan enviar muchos mensajes en una sola llamada y que se env\u00eden desde distintos dispositivos conectados.<\/p>\n\n\n\n<p>Nota: El env\u00edo es aleatorio, por lo que la distribuci\u00f3n y la probabilidad de que se usen todos los tel\u00e9fonos conectados de manera homog\u00e9nea no est\u00e1n aseguradas de ning\u00fan modo.<\/p>\n\n\n\n<p>Para asegurar el uso equitativo de los dispositivos, lo mejor ser\u00eda implementar un mecanismo de planificaci\u00f3n como <a href=\"https:\/\/es.wikipedia.org\/wiki\/Planificacin_Round-robin\" target=\"_blank\" rel=\"noopener\" title=\"\">Round-Robin<\/a>.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"repositorio\">Repositorio<\/h1>\n\n\n\n<p><a href=\"https:\/\/github.com\/genitalico\/smsgateway\" target=\"_blank\" rel=\"noopener\" title=\"\">El c\u00f3digo para la aplicaci\u00f3n en Android se puede clonar desde aqu\u00ed<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>A menudo, cuando necesito un servicio de SMS para alg\u00fan proyecto, recurro a plataformas como Twilio, que son bastante robustas y confiables. Sin embargo, en ocasiones solo necesito probar ciertos escenarios o enviar alertas, y no quiero incurrir en gastos adicionales. En mi pa\u00eds, los SMS son gratuitos en todos los planes de telefon\u00eda o [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":935,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[10,22,28,20,29],"tags":[8,100,34,101,105,104,32,99,103],"class_list":["post-932","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","category-frameworks","category-js","category-lenguajes-de-programacion","category-nodejs","tag-android","tag-compose","tag-javascript","tag-kotlin","tag-mobile","tag-movil","tag-nodejs","tag-sms","tag-websockets"],"jetpack_featured_media_url":"https:\/\/80bits.blog\/wp-content\/uploads\/2024\/06\/walk_sms001.webp","jetpack-related-posts":[],"jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/posts\/932","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/comments?post=932"}],"version-history":[{"count":9,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/posts\/932\/revisions"}],"predecessor-version":[{"id":944,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/posts\/932\/revisions\/944"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/media\/935"}],"wp:attachment":[{"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/media?parent=932"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/categories?post=932"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/80bits.blog\/index.php\/wp-json\/wp\/v2\/tags?post=932"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}