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
andTWILIO_AUTH_TOKEN
: Your Twilio account credentials.TWILIO_PHONE_NUMBER
: The phone number you have set up in Twilio to send messages.NGROK_DOMAIN
andNGROK_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.