How to build your own SMS voting system
First you need to install it yourself Go and connect the following libraries:
Supabase: is a great database tool that integrates well with Golang via GORM. In our case, it will store vote data.
Exolve SMS API: the main tool for working with SMS. You can get acquainted with the API Here.
Gin: lightweight and fast framework for creating web applications.
GORM: One of the best ORM for Golang, allowing you to easily work with databases.
Initializing the Project
Let's create a new project and initialize it using the go mod init command:
$ mkdir sms-voting
$ cd sms-voting
$ go mod init sms-voting
We will use Supabase to store vote data.
Let's install the necessary libraries:
$ go get -u github.com/gin-gonic/gin
$ go get -u gorm.io/gorm
$ go get -u gorm.io/driver/postgres
We register on the very Supabaselet's create a new project and add a table for votes:
CREATE TABLE votes (
id SERIAL PRIMARY KEY,
candidate_name VARCHAR(255) NOT NULL,
vote_count INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
This will allow information about each vote to be stored and the number of votes for each candidate to be tracked.
Project structure
The structure will look like this:
sms-voting/
├── config/
│ └── config.go
├── db/
│ └── db.go
├── sms/
│ └── sms.go
├── handlers/
│ └── sms_handler.go
├── models/
│ └── vote.go
├── main.go
└── go.mod
Project configuration
Let's create a file config/config.go to store configuration parameters:
// config/config.go
package config
import (
"log"
"os"
)
type Config struct {
DBHost string
DBUser string
DBPassword string
DBName string
ExolveAPIKey string
SenderNumber string
ServerPort string
}
func LoadConfig() *Config {
config := &Config{
DBHost: getEnv("DB_HOST", "localhost"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "password"),
DBName: getEnv("DB_NAME", "votes_db"),
ExolveAPIKey: getEnv("EXOLVE_API_KEY", ""),
SenderNumber: getEnv("SENDER_NUMBER", ""),
ServerPort: getEnv("SERVER_PORT", "8080"),
}
if config.ExolveAPIKey == "" || config.SenderNumber == "" {
log.Fatal("EXOLVE_API_KEY and SENDER_NUMBER must be set")
}
return config
}
func getEnv(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return fallback
}
Don't forget to set environment variables before running the application:
export DB_HOST=your_db_host
export DB_USER=your_db_user
export DB_PASSWORD=your_db_password
export DB_NAME=your_db_name
export EXOLVE_API_KEY=your_exolve_api_key
export SENDER_NUMBER=your_sender_number
export SERVER_PORT=8080
Data Model
Let's create a models/vote.go file to describe the voting model:
// models/vote.go
package models
import (
"time"
"gorm.io/gorm"
)
type Vote struct {
ID uint `gorm:"primaryKey" json:"id"`
CandidateName string `gorm:"not null" json:"candidate_name"`
VoteCount int `gorm:"default:0" json:"vote_count"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
}
Working with DB
Let's create a db/db.go file to set up a connection to the database:
// db/db.go
package db
import (
"fmt"
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"sms-voting/config"
"sms-voting/models"
)
type Database struct {
Conn *gorm.DB
}
func NewDatabase(cfg *config.Config) *Database {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=5432 sslmode=require",
cfg.DBHost,
cfg.DBUser,
cfg.DBPassword,
cfg.DBName)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database:", err)
}
// Миграция схемы
if err := db.AutoMigrate(&models.Vote{}); err != nil {
log.Fatal("failed to migrate database:", err)
}
return &Database{Conn: db}
}
Interfaces
In order for everything to be flexible and convenient, while still testing, we will introduce interfaces for working with the database and sending SMS.
Database interface
Let's create a file models/vote_repository.go:
// models/vote_repository.go
package models
import (
"errors"
"gorm.io/gorm"
)
type VoteRepository interface {
GetOrCreateVote(candidateName string) (*Vote, error)
IncrementVote(vote *Vote) error
GetAllVotes() ([]Vote, error)
}
type voteRepository struct {
db *gorm.DB
}
func NewVoteRepository(db *gorm.DB) VoteRepository {
return &voteRepository{db}
}
func (r *voteRepository) GetOrCreateVote(candidateName string) (*Vote, error) {
var vote Vote
if err := r.db.Where("candidate_name = ?", candidateName).First(&vote).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
vote = Vote{CandidateName: candidateName}
if err := r.db.Create(&vote).Error; err != nil {
return nil, err
}
} else {
return nil, err
}
}
return &vote, nil
}
func (r *voteRepository) IncrementVote(vote *Vote) error {
vote.VoteCount += 1
return r.db.Save(vote).Error
}
func (r *voteRepository) GetAllVotes() ([]Vote, error) {
var votes []Vote
if err := r.db.Find(&votes).Error; err != nil {
return nil, err
}
return votes, nil
}
Interface for sending SMS
// sms/sms_service.go
package sms
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type SMSService interface {
SendSMS(to string, message string) error
}
type exolveSMSService struct {
apiKey string
senderNumber string
apiURL string
}
func NewExolveSMSService(apiKey, senderNumber string) SMSService {
return &exolveSMSService{
apiKey: apiKey,
senderNumber: senderNumber,
apiURL: "https://api.exolve.ru/sms/send",
}
}
type SMSSendRequest struct {
To string `json:"to"`
From string `json:"from"`
Message string `json:"message"`
}
func (s *exolveSMSService) SendSMS(to string, message string) error {
smsSendReq := SMSSendRequest{
To: to,
From: s.senderNumber,
Message: message,
}
body, err := json.Marshal(smsSendReq)
if err != nil {
return fmt.Errorf("error marshalling SMS request: %w", err)
}
req, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("error creating SMS request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+s.apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error sending SMS: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
respBody, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("failed to send SMS, status code: %d, response: %s", resp.StatusCode, string(respBody))
}
return nil
}
HTTP Request Handlers
Let's create a file handlers/sms_handler.go to process incoming SMS:
// handlers/sms_handler.go
package handlers
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
"sms-voting/models"
"sms-voting/sms"
)
type SMSRequest struct {
From string `json:"from"`
Body string `json:"body"`
}
type SMSHandler struct {
VoteRepo models.VoteRepository
SMSService sms.SMSService
}
func NewSMSHandler(voteRepo models.VoteRepository, smsService sms.SMSService) *SMSHandler {
return &SMSHandler{
VoteRepo: voteRepo,
SMSService: smsService,
}
}
func (h *SMSHandler) HandleSMS(c *gin.Context) {
var smsReq SMSRequest
if err := c.ShouldBindJSON(&smsReq); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
candidateName := smsReq.Body
vote, err := h.VoteRepo.GetOrCreateVote(candidateName)
if err != nil {
log.Printf("Error fetching/creating vote for candidate %s: %v", candidateName, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
return
}
if err := h.VoteRepo.IncrementVote(vote); err != nil {
log.Printf("Error incrementing vote for candidate %s: %v", candidateName, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
return
}
responseMessage := fmt.Sprintf("Ваш голос за %s принят. Спасибо за участие!", vote.CandidateName)
if err := h.SMSService.SendSMS(smsReq.From, responseMessage); err != nil {
log.Printf("Error sending SMS to %s: %v", smsReq.From, err)
// Можно решить, возвращать ли ошибку пользователю или нет
}
c.JSON(http.StatusOK, gin.H{"message": responseMessage})
}
Main application file
Let's create a main.go file that will launch the server:
// main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
"sms-voting/config"
"sms-voting/db"
"sms-voting/handlers"
"sms-voting/models"
"sms-voting/sms"
)
func main() {
// Загрузка конфигурации
cfg := config.LoadConfig()
// Инициализация базы данных
database := db.NewDatabase(cfg)
voteRepo := models.NewVoteRepository(database.Conn)
// Инициализация SMS-сервиса
smsService := sms.NewExolveSMSService(cfg.ExolveAPIKey, cfg.SenderNumber)
// Инициализация обработчиков
smsHandler := handlers.NewSMSHandler(voteRepo, smsService)
// Настройка роутера Gin
router := gin.Default()
router.POST("/sms", smsHandler.HandleSMS)
// Запуск сервера
log.Printf("Сервер запущен на порту %s", cfg.ServerPort)
if err := router.Run(":" + cfg.ServerPort); err != nil {
log.Fatalf("Не удалось запустить сервер: %v", err)
}
}
Now you can start the server:
$ go run main.go
Example request body for testing:
{
"from": "+79678880033",
"body": "Кандидат"
}
After processing the request, the system will increase the vote counter for the specified candidate and send a confirmation SMS to the user.
What's next?
Additional functions can be added to this system: reports, campaign management, integration with other services, or the same scaling. Exolve SMS API also has many opportunities to improve scripts.
Share your ideas and improvements in the comments. Until next time!