From a7cd37dda8730409f4745428ae732aa702bf3503 Mon Sep 17 00:00:00 2001 From: Benjamin Aronov Date: Tue, 21 Oct 2025 09:06:12 -0500 Subject: [PATCH] Added inbound WhatsApp functionality and added some logic to make the README the home page --- README.md | 22 +- WHATSAPP_SETUP.md | 127 ------- app/assets/stylesheets/home.css | 141 ++++++++ app/assets/stylesheets/inbound_whatsapp.css | 245 ++++++++++++++ app/assets/stylesheets/outbound_whatsapp.css | 313 ++++++++++++++++++ app/controllers/home_controller.rb | 6 + .../inbound_whatsapp_controller.rb | 64 ++++ app/helpers/home_helper.rb | 35 ++ app/helpers/inbound_whatsapp_helper.rb | 2 + app/views/home/index.html.erb | 10 + app/views/inbound_whatsapp/_message.html.erb | 78 +++++ app/views/inbound_whatsapp/inbound.html.erb | 2 + app/views/inbound_whatsapp/index.html.erb | 36 ++ app/views/inbound_whatsapp/status.html.erb | 2 + config/routes.rb | 8 +- ...add_missing_fields_to_whatsapp_messages.rb | 7 + db/schema.rb | 5 +- test/controllers/home_controller_test.rb | 7 + .../inbound_whatsapp_controller_test.rb | 18 + 19 files changed, 987 insertions(+), 141 deletions(-) delete mode 100644 WHATSAPP_SETUP.md create mode 100644 app/assets/stylesheets/home.css create mode 100644 app/assets/stylesheets/inbound_whatsapp.css create mode 100644 app/assets/stylesheets/outbound_whatsapp.css create mode 100644 app/controllers/home_controller.rb create mode 100644 app/controllers/inbound_whatsapp_controller.rb create mode 100644 app/helpers/home_helper.rb create mode 100644 app/helpers/inbound_whatsapp_helper.rb create mode 100644 app/views/home/index.html.erb create mode 100644 app/views/inbound_whatsapp/_message.html.erb create mode 100644 app/views/inbound_whatsapp/inbound.html.erb create mode 100644 app/views/inbound_whatsapp/index.html.erb create mode 100644 app/views/inbound_whatsapp/status.html.erb create mode 100644 db/migrate/20251021131458_add_missing_fields_to_whatsapp_messages.rb create mode 100644 test/controllers/home_controller_test.rb create mode 100644 test/controllers/inbound_whatsapp_controller_test.rb diff --git a/README.md b/README.md index 097e828..fd039fc 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # Vonage APIs Quickstart Examples for Ruby on Rails - +
The purpose of the quickstart guide is to provide simple examples focused on one goal. For example, sending an SMS, making a Text to Speech call, or sending an image in WhatsApp. ## Configure with Your Vonage API Keys - +
To use this sample you will first need a [Vonage account](https://dashboard.nexmo.com/sign-up). Once you have your own API credentials, rename the `.env.example` file to `.env` and set the values as required. For some of the examples you will need to [buy a number](https://dashboard.nexmo.com/buy-numbers). ## Setup - +
``` git clone git@github.com:Vonage-Community/sample-messages-ruby_on_rails-quickstart.git cd sample-messages-ruby_on_rails-quickstart @@ -18,11 +18,9 @@ bundle install rails db:create db:migrate rails s ``` - ## Tutorials & Sample Code - ### SMS - +
Tutorial | Code Sample --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ [How to Send SMS Messages with Ruby on Rails](https://developer.vonage.com/en/blog/send-sms-ruby-on-rails-dr) | [outbound_sms_controller.rb](app/controllers/outbound_sms_controller.rb) @@ -30,7 +28,7 @@ Tutorial [How to Receive SMS Messages with Ruby on Rails](https://developer.vonage.com/en/blog/receive-sms-messages-ruby-on-rails-dr) | [inbound_sms_controller.rb](app/controllers/inbound_sms_controller.rb) ### Voice - +
Tutorial | Code Sample ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- [How to Make an Outbound Text-to-Speech Phone Call with Rails](#) | [outbound_calls_controller.rb](app/controllers/outbound_calls_controller.rb) @@ -38,7 +36,7 @@ Tutorial [How to Handle Inbound Phone Calls with Ruby on Rails](#) | [inbound_calls_controller.rb](app/controllers/inbound_calls_controller.rb) ### RCS - +
Tutorial | Code Sample --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ [How to Send RCS Suggested Reply Messages with Ruby on Rails](#) | [outbound_rcs_controller.rb](app/controllers/outbound_rcs_controller.rb) @@ -46,18 +44,18 @@ Tutorial [How to Receive RCS Reply Messages with Ruby on Rails](#) | [inbound_rcs_controller.rb](app/controllers/inbound_rcs_controller.rb) ### WhatsApp - +
Tutorial | Code Sample --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ [How to Send WhatsApp Messages with Ruby on Rails](#) | [outbound_whatsapp_controller.rb](app/controllers/outbound_whatsapp_controller.rb) -[How to Receive WhatsApp Messages with Ruby on Rails](#) | [#](#) +[How to Receive WhatsApp Messages with Ruby on Rails](#) | [inbound_whatsapp_controller.rb](app/controllers/inbound_whatsapp_controller.rb) ## Request More Examples - +
For help with the code or to request an example not listed here, please join the [Vonage Community Slack](https://developer.vonage.com/en/community/slack). Feedback and requests are highly appreciated! ## Licenses - +
- The code samples in this repo is under [MIT](LICENSE) - The tutorials contents are under Creative Commons, [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/legalcode) diff --git a/WHATSAPP_SETUP.md b/WHATSAPP_SETUP.md deleted file mode 100644 index 612ebf6..0000000 --- a/WHATSAPP_SETUP.md +++ /dev/null @@ -1,127 +0,0 @@ -# WhatsApp Integration Setup Guide - -This guide will help you set up and use the WhatsApp messaging functionality that has been added to your Rails application. - -## What Was Added - -The following components have been integrated into your existing Rails app: - -### 1. Database -- **Migration**: `db/migrate/20251015112429_create_whatsapp_messages.rb` -- **Model**: `app/models/whatsapp_message.rb` -- Stores WhatsApp message records with fields: `to`, `from`, `text`, `status`, `message_uuid`, `is_inbound` - -### 2. Controller -- **File**: `app/controllers/outbound_whatsapp_controller.rb` -- **Actions**: - - `new` - Display the form for sending WhatsApp messages - - `create` - Send a text WhatsApp message - - `interactive` - Send an interactive WhatsApp message with reply buttons - -### 3. View -- **File**: `app/views/outbound_whatsapp/new.html.erb` -- Contains forms for both text and interactive WhatsApp messages - -### 4. Routes -Added to `config/routes.rb`: -```ruby -get '/outbound_whatsapp/new', to: 'outbound_whatsapp#new', as: :new_outbound_whatsapp -post '/outbound_whatsapp', to: 'outbound_whatsapp#create', as: :outbound_whatsapp -post '/outbound_whatsapp/interactive', to: 'outbound_whatsapp#interactive', as: :interactive_whatsapp -``` - -### 5. Environment Configuration -Added `VONAGE_WHATSAPP_NUMBER` to `.env.example` - -## Setup Instructions - -### Step 1: Run the Migration -```bash -rails db:migrate -``` - -### Step 2: Configure Your Vonage WhatsApp Application - -1. Log in to your [Vonage Dashboard](https://dashboard.nexmo.com/) -2. Create a new application (or use an existing one) and enable the **Messages** capability -3. Add webhook URLs for: - - **Inbound**: `https://your-domain.com/inbound` (or use ngrok for testing) - - **Status**: `https://your-domain.com/status` -4. Click **Generate public and private key** -5. Move the downloaded `private.key` file to the root of your Rails app -6. Note your **Application ID** -7. Click **Save** -8. Under the **Link external accounts** tab, link your WhatsApp number - -### Step 3: Update Your .env File - -Add the following to your `.env` file (create one if it doesn't exist): - -```bash -VONAGE_APPLICATION_ID=your_application_id_here -VONAGE_PRIVATE_KEY=./private.key -VONAGE_WHATSAPP_NUMBER=14157386102 # Your WhatsApp Business number -``` - -### Step 4: Test the Integration - -1. Start your Rails server: - ```bash - rails s - ``` - -2. Navigate to: `http://localhost:3000/outbound_whatsapp/new` - -3. You should see two forms: - - **Send a WhatsApp Message** - For sending text messages - - **Try an Interactive Message** - For sending messages with reply buttons - -## Features - -### Text Messages -Send simple text messages to any WhatsApp number. The form includes: -- **From**: Your WhatsApp Business number (pre-filled from ENV) -- **To**: Recipient's WhatsApp number (format: +14155551234) -- **Message**: The text content to send - -### Interactive Messages -Send messages with reply buttons that users can tap to respond. The example includes: -- A header ("Delivery time") -- Body text with a question -- Footer text with additional info -- Three reply buttons with different time slots - -You can customize the interactive message structure in the `interactive` action of `OutboundWhatsappController`. - -## How It Works - -1. **User fills out the form** and submits -2. **Controller receives the data** and creates a `WhatsappMessage` record -3. **Vonage client is initialized** with your credentials -4. **Message is sent** via the Vonage Messages API -5. **Response is checked** - if successful (HTTP 202), the `message_uuid` is saved -6. **User is redirected** back to the form with a success notice - -## Next Steps - -As mentioned in the tutorial, this implementation covers sending messages. To build a complete WhatsApp integration, you'll want to add: - -1. **Inbound message handling** - Receive messages from users -2. **Status webhooks** - Track message delivery and read status -3. **Interactive response handling** - Process which button the user clicked - -These features would require additional controllers and webhook endpoints, similar to the existing `inbound_sms` and `sms_message_status` controllers in your app. - -## Troubleshooting - -- **Make sure your WhatsApp number is linked** to your Vonage application -- **Verify your .env file** has the correct credentials -- **Check that private.key** is in the root directory -- **Ensure the recipient number** is in E.164 format (e.g., +14155551234) -- **For testing**, you can use the [Vonage Sandbox for WhatsApp](https://developer.vonage.com/en/messages/concepts/messages-api-sandbox) - -## Resources - -- [Vonage Messages API Documentation](https://developer.vonage.com/en/messages/overview) -- [WhatsApp Business API Guide](https://developer.vonage.com/en/messages/concepts/whatsapp) -- [Vonage Ruby SDK](https://github.com/Vonage/vonage-ruby-sdk) diff --git a/app/assets/stylesheets/home.css b/app/assets/stylesheets/home.css new file mode 100644 index 0000000..d3718dc --- /dev/null +++ b/app/assets/stylesheets/home.css @@ -0,0 +1,141 @@ +/* README container */ +.readme-container { + max-width: 1000px; + margin: 0 auto; + padding: 40px 20px; + background-color: #ffffff; + min-height: 100vh; +} + +.readme-content { + background: white; + padding: 40px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* Markdown styling */ +.markdown-body { + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 16px; + line-height: 1.6; + color: #333; +} + +.markdown-body h1 { + font-family: 'Montserrat', sans-serif; + font-size: 32px; + font-weight: 600; + color: #000; + margin: 0 0 24px 0; + padding-bottom: 16px; + border-bottom: 2px solid #871FFF; +} + +.markdown-body h2 { + font-family: 'Montserrat', sans-serif; + font-size: 24px; + font-weight: 600; + color: #333; + margin: 32px 0 16px 0; + padding-bottom: 8px; + border-bottom: 1px solid #e5e5e5; +} + +.markdown-body h3 { + font-family: 'Montserrat', sans-serif; + font-size: 20px; + font-weight: 600; + color: #333; + margin: 24px 0 12px 0; +} + +.markdown-body p { + margin: 0 0 16px 0; + line-height: 1.7; +} + +.markdown-body a { + color: #871FFF; + text-decoration: none; + font-weight: 500; + transition: color 0.2s; +} + +.markdown-body a:hover { + color: #6a19cc; + text-decoration: underline; +} + +.markdown-body code { + background-color: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; + font-family: 'Roboto Mono', monospace; + font-size: 14px; + color: #871FFF; +} + +.markdown-body pre { + background-color: #f5f5f5; + padding: 16px; + border-radius: 6px; + overflow-x: auto; + margin: 16px 0; + border: 1px solid #e5e5e5; +} + +.markdown-body pre code { + background-color: transparent; + padding: 0; + color: #333; + font-size: 13px; + line-height: 1.5; +} + +.markdown-body ul, +.markdown-body ol { + margin: 0 0 16px 0; + padding-left: 24px; +} + +.markdown-body li { + margin: 8px 0; + line-height: 1.6; +} + +.markdown-body table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; + font-size: 14px; +} + +.markdown-body table th { + background: linear-gradient(135deg, #871FFF 0%, #FFA68C 100%); + color: white; + padding: 12px; + text-align: left; + font-weight: 600; + font-family: 'Montserrat', sans-serif; +} + +.markdown-body table td { + padding: 12px; + border-bottom: 1px solid #e5e5e5; +} + +.markdown-body table tr:hover { + background-color: #fafafa; +} + +.markdown-body strong { + font-weight: 600; + color: #000; +} + +.markdown-body hr { + border: none; + border-top: 1px solid #e5e5e5; + margin: 32px 0; +} diff --git a/app/assets/stylesheets/inbound_whatsapp.css b/app/assets/stylesheets/inbound_whatsapp.css new file mode 100644 index 0000000..533afe2 --- /dev/null +++ b/app/assets/stylesheets/inbound_whatsapp.css @@ -0,0 +1,245 @@ +/* Base styles */ +body { + margin: 0; + padding: 0; + background-color: #f5f5f5; + font-family: 'Roboto', sans-serif; + font-weight: 300; +} + +/* Messages container */ +.messages-container { + max-width: 900px; + margin: 40px auto; + padding: 0 20px; +} + +.messages-card { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +/* Header with Vonage brand gradient */ +.messages-header { + background: linear-gradient(135deg, #871FFF 0%, #FFA68C 100%); + padding: 20px 24px; + color: white; +} + +.messages-header h1 { + margin: 0 0 8px 0; + font-size: 24px; + font-family: 'Montserrat', sans-serif; + font-weight: 500; + display: flex; + align-items: center; + gap: 12px; +} + +.messages-header h1 svg { + width: 32px; + height: 32px; + flex-shrink: 0; +} + +.messages-header p { + margin: 0; + font-size: 14px; + opacity: 0.9; +} + +/* Message items */ +.message-item { + padding: 20px 24px; + border-bottom: 1px solid #e5e5e5; + transition: background-color 0.2s; +} + +.message-item:hover { + background-color: #fafafa; +} + +.message-item:last-child { + border-bottom: none; +} + +/* Message content layout */ +.message-content { + display: flex; + gap: 16px; +} + +/* Avatar with Vonage gradient */ +.message-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #871FFF 0%, #FFA68C 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + flex-shrink: 0; +} + +/* Message body */ +.message-body { + flex: 1; + min-width: 0; +} + +.message-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; + gap: 16px; +} + +.message-sender { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.message-sender-name { + font-weight: 500; + color: #333; + font-size: 15px; +} + +.message-sender-phone { + color: #666; + font-size: 13px; +} + +.message-time { + color: #999; + font-size: 12px; + white-space: nowrap; +} + +/* Message text */ +.message-text { + color: #333; + font-size: 14px; + line-height: 1.5; + margin-bottom: 8px; +} + +/* Button reply styling */ +.message-reply { + display: inline-flex; + padding: 10px 14px; + background-color: #F5EBFF; + border: 1px solid #871FFF; + border-radius: 6px; + margin-bottom: 8px; +} + +.message-reply-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.message-reply-label { + font-family: 'Roboto Mono', monospace; + font-size: 11px; + font-weight: 500; + color: #871FFF; + text-transform: uppercase; +} + +.message-reply-text { + font-weight: 600; + color: #333; + font-size: 14px; +} + +.message-reply-id { + font-family: 'Roboto Mono', monospace; + font-size: 11px; + color: #666; +} + +/* Other message types */ +.message-other { + display: inline-flex; + flex-direction: column; + gap: 4px; + padding: 10px 14px; + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 6px; + margin-bottom: 8px; +} + +.message-other-label { + font-family: 'Roboto Mono', monospace; + font-size: 11px; + font-weight: 500; + color: #666; + text-transform: uppercase; +} + +.message-other-text { + color: #333; + font-size: 14px; +} + +/* Message metadata */ +.message-metadata { + display: flex; + gap: 16px; + font-size: 11px; + color: #999; + font-family: 'Roboto Mono', monospace; +} + +/* Empty state */ +.messages-empty { + padding: 60px 24px; + text-align: center; + color: #666; +} + +.messages-empty h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 500; + color: #333; +} + +.messages-empty p { + margin: 0; + font-size: 14px; +} + +/* Footer */ +.messages-footer { + padding: 16px 24px; + background-color: #fafafa; + border-top: 1px solid #e5e5e5; + text-align: center; +} + +.messages-footer p { + margin: 0; + font-size: 13px; + color: #666; +} + +.messages-footer a { + color: #871FFF; + text-decoration: none; + font-weight: 500; +} + +.messages-footer a:hover { + text-decoration: underline; +} diff --git a/app/assets/stylesheets/outbound_whatsapp.css b/app/assets/stylesheets/outbound_whatsapp.css new file mode 100644 index 0000000..57eee67 --- /dev/null +++ b/app/assets/stylesheets/outbound_whatsapp.css @@ -0,0 +1,313 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ + +/* Base styles */ +body { + margin: 0; + padding: 0; + background-color: #f5f5f5; + font-family: 'Roboto', sans-serif; + font-weight: 300; +} + +/* Messages container */ +.messages-container { + max-width: 900px; + margin: 40px auto; + padding: 0 20px; +} + +.messages-card { + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +/* Header */ +.messages-header { + background: linear-gradient(135deg, #871FFF 0%, #FFA68C 100%); + padding: 20px 24px; + color: white; +} + +.messages-header h1 { + margin: 0; + font-size: 24px; + font-family: 'Montserrat', sans-serif; + font-weight: 500; + display: flex; + align-items: center; +} + +.messages-header h1 svg { + width: 32px; + height: 32px; + margin-right: 12px; +} + +.messages-header p { + margin: 4px 0 0 0; + font-size: 14px; + font-family: 'Roboto', sans-serif; + font-weight: 300; + opacity: 0.9; +} + +/* Message list */ +.messages-list { + border-top: 1px solid #e5e5e5; +} + +.message-item { + padding: 20px 24px; + border-bottom: 1px solid #e5e5e5; + transition: background-color 0.2s; +} + +.message-item:hover { + background-color: #fafafa; +} + +.message-item:last-child { + border-bottom: none; +} + +.message-content { + display: flex; + gap: 16px; +} + +/* Avatar */ +.message-avatar { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #871FFF 0%, #FFA68C 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 600; + font-size: 16px; +} + +/* Message body */ +.message-body { + flex: 1; + min-width: 0; +} + +.message-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.message-sender { + display: flex; + align-items: center; + gap: 8px; +} + +.message-sender-name { + font-family: 'Roboto', sans-serif; + font-weight: 500; + color: #000; + font-size: 14px; +} + +.message-sender-phone { + font-family: 'Roboto Mono', monospace; + font-weight: 300; + color: #666; + font-size: 12px; +} + +.message-time { + font-family: 'Roboto Mono', monospace; + font-weight: 300; + color: #999; + font-size: 12px; +} + +/* Message text */ +.message-text { + margin: 12px 0; + color: #333; + font-family: 'Roboto', sans-serif; + font-weight: 300; + font-size: 14px; + line-height: 1.5; + white-space: pre-wrap; +} + +/* Button reply styling */ +.message-reply { + display: inline-flex; + align-items: center; + padding: 10px 14px; + background-color: #F5EBFF; + border: 1px solid #871FFF; + border-radius: 6px; + gap: 8px; +} + +.message-reply svg { + width: 16px; + height: 16px; + color: #871FFF; + flex-shrink: 0; +} + +.message-reply-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.message-reply-label { + font-family: 'Roboto Mono', monospace; + font-size: 11px; + font-weight: 500; + color: #871FFF; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.message-reply-text { + font-family: 'Roboto', sans-serif; + font-size: 14px; + color: #000; + font-weight: 500; +} + +.message-reply-id { + font-family: 'Roboto Mono', monospace; + font-size: 11px; + font-weight: 300; + color: #666; + margin-top: 2px; +} + +/* Other message type */ +.message-other { + display: inline-flex; + align-items: center; + padding: 10px 14px; + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 6px; + gap: 8px; +} + +.message-other svg { + width: 16px; + height: 16px; + color: #666; + flex-shrink: 0; +} + +.message-other-label { + font-family: 'Roboto Mono', monospace; + font-size: 11px; + font-weight: 500; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.message-other-text { + font-family: 'Roboto', sans-serif; + font-size: 14px; + font-weight: 300; + color: #333; +} + +/* Metadata */ +.message-metadata { + display: flex; + gap: 16px; + margin-top: 8px; + font-family: 'Roboto Mono', monospace; + font-size: 11px; + font-weight: 300; + color: #999; +} + +.message-metadata-item { + display: flex; + align-items: center; + gap: 4px; +} + +.message-metadata-item svg { + width: 12px; + height: 12px; +} + +/* Empty state */ +.messages-empty { + padding: 60px 24px; + text-align: center; +} + +.messages-empty svg { + width: 48px; + height: 48px; + color: #ccc; + margin: 0 auto 12px; +} + +.messages-empty h3 { + margin: 0 0 8px 0; + font-family: 'Roboto', sans-serif; + font-size: 14px; + font-weight: 500; + color: #333; +} + +.messages-empty p { + margin: 0; + font-family: 'Roboto', sans-serif; + font-size: 14px; + font-weight: 300; + color: #999; +} + +/* Footer */ +.messages-footer { + background-color: #fafafa; + padding: 12px 24px; + text-align: center; + border-top: 1px solid #e5e5e5; +} + +.messages-footer p { + margin: 0; + font-family: 'Roboto Mono', monospace; + font-size: 12px; + font-weight: 300; + color: #666; +} + +.messages-footer a { + color: #871FFF; + text-decoration: none; + font-family: 'Roboto Mono', monospace; + font-weight: 300; +} + +.messages-footer a:hover { + text-decoration: underline; +} diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..297ca5c --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,6 @@ +class HomeController < ApplicationController + def index + readme_path = Rails.root.join('README.md') + @readme_content = File.read(readme_path) if File.exist?(readme_path) + end +end diff --git a/app/controllers/inbound_whatsapp_controller.rb b/app/controllers/inbound_whatsapp_controller.rb new file mode 100644 index 0000000..1137d53 --- /dev/null +++ b/app/controllers/inbound_whatsapp_controller.rb @@ -0,0 +1,64 @@ +class InboundWhatsappController < ApplicationController + skip_before_action :verify_authenticity_token + + def index + @messages = WhatsappMessage.where(is_inbound: true).order(created_at: :desc) + end + + def inbound + payload = JSON.parse(request.body.read) + Rails.logger.info("📥 Inbound message: #{payload}") + + from = payload["from"] + to = payload["to"] + message_uuid = payload["message_uuid"] + message_type = payload["message_type"] + profile_name = payload.dig("profile", "name") + + # Parse message based on type + if message_type == "reply" + # Interactive button reply + reply_id = payload.dig("reply", "id") + reply_title = payload.dig("reply", "title") + text = "Selected: #{reply_title}" + reply_data = payload["reply"].to_json + elsif message_type == "text" + # Regular text message + text = payload["text"] + reply_data = nil + else + # Fallback for other message types + text = payload["text"] || "Unsupported message type: #{message_type}" + reply_data = nil + end + + WhatsappMessage.create!( + from: from, + to: to, + text: text, + message_uuid: message_uuid, + message_type: message_type, + profile_name: profile_name, + reply_data: reply_data, + is_inbound: true + ) + + head :ok + end + + # Handle delivery and read status updates + def status + payload = JSON.parse(request.body.read) + Rails.logger.info("📡 Status update: #{payload}") + + message_uuid = payload.dig("message_uuid") + status = payload.dig("status") + + if message_uuid && status + message = WhatsappMessage.find_by(message_uuid: message_uuid) + message&.update(status: status) + end + + head :ok + end +end \ No newline at end of file diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000..c2909ed --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,35 @@ +module HomeHelper + def markdown_to_html(text) + return "" if text.blank? + + html = text.dup + + # Convert headers + html.gsub!(/^### (.+)$/, '

\1

') + html.gsub!(/^## (.+)$/, '

\1

') + html.gsub!(/^# (.+)$/, '

\1

') + + # Convert links [text](url) + html.gsub!(/\[([^\]]+)\]\(([^)]+)\)/, '\1') + + # Convert code blocks + html.gsub!(/```([^`]+)```/m, '
\1
') + + # Convert inline code `code` + html.gsub!(/`([^`]+)`/, '\1') + + # Convert bold **text** + html.gsub!(/\*\*([^*]+)\*\*/, '\1') + + # Convert paragraphs (double newlines) + html.gsub!(/\n\n/, '

') + html = "

#{html}

" + + # Clean up empty paragraphs + html.gsub!(/

\s*<\/p>/, '') + html.gsub!(/

\s*\s*<\/p>/, '') + + html.html_safe + end +end diff --git a/app/helpers/inbound_whatsapp_helper.rb b/app/helpers/inbound_whatsapp_helper.rb new file mode 100644 index 0000000..850307b --- /dev/null +++ b/app/helpers/inbound_whatsapp_helper.rb @@ -0,0 +1,2 @@ +module InboundWhatsappHelper +end diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..bf209d8 --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,10 @@ +

+
+ <% if @readme_content.present? %> + <%= markdown_to_html(@readme_content) %> + <% else %> +

Welcome to Vonage APIs Quickstart

+

README.md not found.

+ <% end %> +
+
diff --git a/app/views/inbound_whatsapp/_message.html.erb b/app/views/inbound_whatsapp/_message.html.erb new file mode 100644 index 0000000..a47337c --- /dev/null +++ b/app/views/inbound_whatsapp/_message.html.erb @@ -0,0 +1,78 @@ +
+
+ +
+ <%= message.profile_name&.first&.upcase || 'U' %> +
+ + +
+ +
+
+ + <%= message.profile_name || 'Unknown' %> + + + <%= message.from %> + +
+ +
+ + +
+ <% if message.message_type == 'reply' %> + +
+ + + +
+ Button Reply + <%= message.text %> + <% if message.reply_data.present? %> + <% reply = JSON.parse(message.reply_data) rescue {} %> + <% if reply['id'].present? %> + ID: <%= reply['id'] %> + <% end %> + <% end %> +
+
+ <% elsif message.message_type == 'text' %> + +
<%= message.text %>
+ <% else %> + +
+ + + +
+ <%= message.message_type || 'Unknown' %> + <%= message.text %> +
+
+ <% end %> +
+ + + +
+
+
diff --git a/app/views/inbound_whatsapp/inbound.html.erb b/app/views/inbound_whatsapp/inbound.html.erb new file mode 100644 index 0000000..a8c4171 --- /dev/null +++ b/app/views/inbound_whatsapp/inbound.html.erb @@ -0,0 +1,2 @@ +

InboundWhatsapp#inbound

+

Find me in app/views/inbound_whatsapp/inbound.html.erb

diff --git a/app/views/inbound_whatsapp/index.html.erb b/app/views/inbound_whatsapp/index.html.erb new file mode 100644 index 0000000..33b5f6d --- /dev/null +++ b/app/views/inbound_whatsapp/index.html.erb @@ -0,0 +1,36 @@ +
+
+ +
+

+ + + + WhatsApp Inbound Messages +

+

Real-time message monitoring

+
+ + +
+ <% if @messages.any? %> + <% @messages.each do |message| %> + <%= render partial: 'message', locals: { message: message } %> + <% end %> + <% else %> +
+

No messages yet

+

Waiting for inbound WhatsApp messages...

+
+ <% end %> +
+ + + +
+
diff --git a/app/views/inbound_whatsapp/status.html.erb b/app/views/inbound_whatsapp/status.html.erb new file mode 100644 index 0000000..4239da4 --- /dev/null +++ b/app/views/inbound_whatsapp/status.html.erb @@ -0,0 +1,2 @@ +

InboundWhatsapp#status

+

Find me in app/views/inbound_whatsapp/status.html.erb

diff --git a/config/routes.rb b/config/routes.rb index d8c62a0..51de0b1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do - get "inbound_rcs/create" + # Root path - displays README + root 'home#index' # For RcsMessageStatus controller, create post '/rcs_message_status', to: 'rcs_message_status#create', as: :rcs_message_status @@ -34,4 +35,9 @@ get '/outbound_whatsapp/new', to: 'outbound_whatsapp#new', as: :new_outbound_whatsapp post '/outbound_whatsapp', to: 'outbound_whatsapp#create', as: :outbound_whatsapp post '/outbound_whatsapp/interactive', to: 'outbound_whatsapp#interactive', as: :interactive_whatsapp + + # Routes for InboundWhatsapp controller, inbound and status webhooks and displaying the index + get '/messages', to: 'inbound_whatsapp#index', as: :messages + post '/inbound_whatsapp/inbound', to: 'inbound_whatsapp#inbound' + post '/inbound_whatsapp/status', to: 'inbound_whatsapp#status' end diff --git a/db/migrate/20251021131458_add_missing_fields_to_whatsapp_messages.rb b/db/migrate/20251021131458_add_missing_fields_to_whatsapp_messages.rb new file mode 100644 index 0000000..1293a9c --- /dev/null +++ b/db/migrate/20251021131458_add_missing_fields_to_whatsapp_messages.rb @@ -0,0 +1,7 @@ +class AddMissingFieldsToWhatsappMessages < ActiveRecord::Migration[8.0] + def change + add_column :whatsapp_messages, :message_type, :string + add_column :whatsapp_messages, :profile_name, :string + add_column :whatsapp_messages, :reply_data, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index c6b0d68..ed87e84 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_10_15_112429) do +ActiveRecord::Schema[8.0].define(version: 2025_10_21_131458) do create_table "calls", force: :cascade do |t| t.string "to" t.string "from" @@ -56,5 +56,8 @@ t.boolean "is_inbound" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "message_type" + t.string "profile_name" + t.text "reply_data" end end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb new file mode 100644 index 0000000..eac0e33 --- /dev/null +++ b/test/controllers/home_controller_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class HomeControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/inbound_whatsapp_controller_test.rb b/test/controllers/inbound_whatsapp_controller_test.rb new file mode 100644 index 0000000..9549f1c --- /dev/null +++ b/test/controllers/inbound_whatsapp_controller_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +class InboundWhatsappControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get inbound_whatsapp_index_url + assert_response :success + end + + test "should get inbound" do + get inbound_whatsapp_inbound_url + assert_response :success + end + + test "should get status" do + get inbound_whatsapp_status_url + assert_response :success + end +end