Building a Ruby GUI Text Messaging App with Twilio and GTK

Building a Ruby GUI Text Messaging App with Twilio and GTK

In this tutorial, we will dive deep into the code of a Ruby GUI text messaging app that uses GTK for the GUI and Twilio for sending and receiving messages. We'll break down each part of the code to understand its functionality.

Overview

This app is designed to send and receive text messages using Twilio's API. It leverages GTK for a user-friendly graphical interface.


Setting Up the Project

First, let's look at the project structure:

Ruby-GUI-Text-App/
  ├── .env
  ├── Gemfile
  ├── Gemfile.lock
  ├── main.rb
  ├── message_handler.rb
  ├── twilio_client.rb
  ├── interface.glade
  ├── run_text_app.sh



Gemfile

The Gemfile includes the necessary gems for the project:

source 'https://rubygems.org'

gem 'rack', '~> 2.2.9'
gem 'thin'
gem 'gtk3'
gem 'twilio-ruby'
gem 'dotenv'
gem 'sinatra'

.env File

Create a .env file in the project root and add your Twilio and ngrok configuration:

TWILIO_ACCOUNT_SID='your_twilio_account_sid'
TWILIO_AUTH_TOKEN='your_twilio_auth_token'
TWILIO_PHONE_NUMBER='your_twilio_phone_number'
NGROK_DOMAIN='your_ngrok_domain'
NGROK_PORT=4567
  • TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN: Your Twilio account credentials.
  • TWILIO_PHONE_NUMBER: The phone number you have set up in Twilio to send messages.
  • NGROK_DOMAIN and NGROK_PORT: Your ngrok domain and port configuration.

main.rb

The main.rb file is the core of the Ruby GUI text messaging app, orchestrating the integration of GTK for the graphical interface, Twilio for messaging, and Sinatra for handling web requests. Let's break down its key components and functionality.

Required Libraries

require 'gtk3'
require 'pathname'
require 'twilio-ruby'
require 'dotenv'
require 'sinatra/base'
require 'thin'
require 'open3'
require 'json'
require 'net/http'
require 'uri'
  • gtk3: Provides the graphical interface.
  • pathname: For handling file paths.
  • twilio-ruby: For interacting with the Twilio API.
  • dotenv: For loading environment variables from a .env file.
  • sinatra/base: A lightweight web framework for handling incoming messages.
  • thin: A fast and lightweight web server.
  • open3, json, net/http, uri: Various utilities for process management, JSON handling, and HTTP requests.

Loading Environment Variables

Dotenv.load

Loads environment variables from the .env file.

TextApp Class

The TextApp class encapsulates the GTK application logic.

Initialization

class TextApp
  @instance = nil

  def self.instance
    @instance
  end

  def self.instance=(instance)
    @instance = instance
  end

  def initialize
    puts "Initializing GTK application..."
    Gtk.init

    glade_file = Pathname.new(__FILE__).dirname + 'interface.glade'
    puts "Loading Glade file from #{glade_file.to_s}..."

    unless File.exist?(glade_file.to_s)
      raise "Glade file not found at #{glade_file.to_s}"
    end

    builder = Gtk::Builder.new
    begin
      builder.add_from_file(glade_file.to_s)
    rescue => e
      raise "Error loading Glade file: #{e.message}"
    end

    @window = builder.get_object("window1")
    if @window.nil?
      raise "Could not find object 'window1' in Glade file"
    end
    @window.signal_connect("destroy") { Gtk.main_quit }

    @phone_number_entry = builder.get_object("phone_number_entry")
    if @phone_number_entry.nil?
      raise "Could not find object 'phone_number_entry' in Glade file"
    end

    @message_entry = builder.get_object("message_entry")
    if @message_entry.nil?
      raise "Could not find object 'message_entry' in Glade file"
    end

    @send_button = builder.get_object("send_button")
    if @send_button.nil?
      raise "Could not find object 'send_button' in Glade file"
    end
    @send_button.signal_connect("clicked") { on_send_button_clicked }

    @messages_text_view = builder.get_object("messages_text_view")
    if @messages_text_view.nil?
      raise "Could not find object 'messages_text_view' in Glade file"
    end

    puts "Showing all components..."
    @window.show_all
    puts "Window should now be visible."

    start_server
    set_twilio_webhook('https://gull-shining-peacock.ngrok-free.app')
  end
  • Initializes the GTK application.
  • Loads the Glade file which defines the UI.
  • Connects UI components to their respective variables.
  • Sets up signals for button clicks and window destroy events.
  • Starts the Sinatra server and sets the Twilio webhook.

Button Click Handler

  def on_send_button_clicked
    phone_number = @phone_number_entry.text
    message_body = @message_entry.text

    if phone_number.empty? || message_body.empty?
      puts "Phone number or message body cannot be empty"
      return
    end

    send_message(phone_number, message_body)
    @message_entry.text = ""
  end
  • Retrieves the phone number and message from the UI.
  • Sends the message using send_message method.
  • Clears the message entry after sending.

Sending Messages

  def send_message(phone_number, message_body)
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token = ENV['TWILIO_AUTH_TOKEN']
    twilio_phone_number = ENV['TWILIO_PHONE_NUMBER']

    client = Twilio::REST::Client.new(account_sid, auth_token)

    message = client.messages.create(
      body: message_body,
      to: phone_number,
      from: twilio_phone_number
    )

    puts " "
    puts "######################################################################"
    puts message.inspect
    puts "######################################################################"
    puts " "
    append_message("Sent to #{phone_number}: #{message_body}")
  end
  • Uses Twilio's API to send a message.
  • Logs the message response.
  • Appends the sent message to the text view.

Appending Messages

  def append_message(text)
    buffer = @messages_text_view.buffer
    end_iter = buffer.end_iter
    buffer.insert(end_iter, text + "\n")
  end

Appends a text message to the messages_text_view buffer.

Starting the Server

  def start_server
    Thread.new do
      MySinatraApp.run!
    end
  end

Starts the Sinatra server in a new thread.

Setting Twilio Webhook

  def set_twilio_webhook(ngrok_url)
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token = ENV['TWILIO_AUTH_TOKEN']
    client = Twilio::REST::Client.new(account_sid, auth_token)
    phone_number_sid = client.incoming_phone_numbers.list.first.sid
    client.incoming_phone_numbers(phone_number_sid).update(
      sms_url: "#{ngrok_url}/incoming",
      sms_method: 'POST'
    )
    puts "Twilio webhook set to #{ngrok_url}/incoming"
  end

Sets the webhook URL for Twilio to forward incoming messages.

Running the GTK Main Loop

  def run
    puts "Running GTK main loop..."
    Gtk.main
  end
end

Runs the GTK main loop.


MySinatraApp Class

The MySinatraApp class handles incoming HTTP requests.

class MySinatraApp < Sinatra::Base
  post '/incoming' do
    body = params['Body']
    from = params['From']
    puts "Incoming message from #{from}: #{body}" # Debug statement
    TextApp.instance.append_message("Received from #{from}: #{body}")

    twiml = Twilio::TwiML::MessagingResponse.new do |r|
      # Uncomment to use reply message upon receipt of texts.
      # r.message body: 'We got your message, thank you!'
    end

    content_type 'text/xml'
    twiml.to_s
  end

  def self.run!
    Rack::Handler::Thin.run(self, Host: '127.0.0.1', Port: 4567)
  end
end
  • Sets up Sinatra to listen for incoming messages on the /incoming endpoint.
  • Appends incoming messages to the GTK app's text view.
  • Sends a TwiML response if needed.

Main Execution

begin
  app = TextApp.new
  TextApp.instance = app
  app.run
rescue StandardError => e
  puts "An error occurred: #{e.message}"
end
  • Initializes the TextApp and starts the GTK main loop.
  • Catches and logs any errors that occur during execution.

This detailed breakdown provides an understanding of how the main.rb file integrates GTK, Twilio, and Sinatra to create a functional GUI text messaging app.


interface.glade

Defines the UI layout using Glade:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
  <requires lib="gtk+" version="3.24"/>
  <object class="GtkWindow" id="window1">
    <property name="title">Texting App</property>
    <property name="default-width">400</property>
    <property name="default-height">400</property>
    <child>
      <object class="GtkBox">
        <property name="orientation">vertical</property>
        <property name="spacing">10</property>
        <property name="margin-top">10</property>
        <property name="margin-bottom">10</property>
        <property name="margin-start">10</property>
        <property name="margin-end">10</property>
        <child>
          <object class="GtkLabel">
            <property name="label" translatable="yes">Message Log</property>
            <property name="visible">True</property>
          </object>
        </child>
        <child>
          <object class="GtkScrolledWindow">
            <property name="visible">True</property>
            <property name="expand">True</property>
            <child>
              <object class="GtkTextView" id="messages_text_view">
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="editable">False</property>
                <property name="wrap-mode">word</property>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkLabel">
            <property name="label" translatable="yes">Phone Number</property>
            <property name="visible">True</property>
          </object>
        </child>
        <child>
          <object class="GtkEntry" id="phone_number_entry">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="placeholder-text" translatable="yes">123-456-7890</property>
            <property name="input-purpose">phone</property>
          </object>
        </child>
        <child>
          <object class="GtkLabel">
            <property name="label" translatable="yes">Message</property>
            <property name="visible">True</property>
          </object>
        </child>
        <child>
          <object class="GtkEntry" id="message_entry">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="placeholder-text" translatable="yes">Add text message here</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="send_button">
            <property name="label" translatable="yes">Send</property>
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="halign">center</property>
          </object>
        </child>
      </object>
    </child>
  </object>
</interface>
  • Defines the main window with entries for the phone number and message, a send button, and a text view for received messages.

run_text_app.sh

This script starts the ngrok and Ruby application:

#!/bin/bash

# Load environment variables from .env file
if [ -f .env ]; then
  export $(grep -v '^#' .env | xargs)
fi

# Start ngrok in the background
echo "Starting NGROK on port $NGROK_PORT"
ngrok http $NGROK_PORT --domain=$NGROK_DOMAIN &

# Give ngrok a moment to start
sleep 2

echo "Starting SMS APP"
# Start the Ruby application
bundle exec ruby main.rb
  • Starts ngrok to forward requests to the local server.
  • Waits for ngrok to initialize before starting the Ruby application.

Conclusion

This tutorial covered the key components of the Ruby GUI text app, including the script and .env file needed for configuration. By understanding each part, you can customize and extend the app according to your needs.

For more details, visit my GitHub repository.


Read more