Compare commits

...

11 Commits

Author SHA1 Message Date
A'zamov Samandar
d8f25ed430 .gitignore: main va release papkalarini qo'shdi 2025-04-26 17:41:52 +05:00
A'zamov Samandar
53bcbe16be .gitignore updated 2025-04-25 18:05:14 +05:00
A'zamov Samandar
83be69c277 systemd file created 2025-04-25 17:36:36 +05:00
A'zamov Samandar
1209499f6e fix: Token management update and SMS message type change 2025-04-25 15:31:10 +05:00
A'zamov Samandar
bd8fba1d88 feat: Yangilangan README, ko'p kanalli va brokerli xabar yetkazib beruvchi xizmat uchun. 2025-04-25 11:38:53 +05:00
A'zamov Samandar
130f03f973 Ovozli xabarlarni chiqarish funksiyasi qo'shildi va test email manzili yangilandi. 2025-04-25 11:25:15 +05:00
A'zamov Samandar
fbd002d8d4 fix typo 2025-04-25 10:27:49 +05:00
A'zamov Samandar
71634fc19e message broker sifatida redis qo'shildi 2025-04-25 10:25:49 +05:00
A'zamov Samandar
40200a4649 notification serviceda email xabar yuborish qo'shildi 2025-04-24 23:26:46 +05:00
A'zamov Samandar
e79718a3ea Merge commit '5032b6088fbe04c2dd2213db3982bdafdc806bd7' into dev 2025-04-21 20:53:16 +05:00
Samandar Azamov
f38471756c Update README.MD 2025-04-21 20:52:34 +05:00
16 changed files with 360 additions and 136 deletions

View File

@@ -5,11 +5,19 @@ REDIS_ADDRESS=127.0.0.1:6379
REDIS_PASSWORD= REDIS_PASSWORD=
REDIS_DB=0 REDIS_DB=0
BROKER=redis
TOPIC=notification
ESKIZ_DOMAIN="https://notify.eskiz.uz/api" ESKIZ_DOMAIN="https://notify.eskiz.uz/api"
ESKIZ_USER="admin@gmail.com" ESKIZ_USER="admin@gmail.com"
ESKIZ_PASSWORD="password" ESKIZ_PASSWORD="password"
ESKIZ_FROM="4546" ESKIZ_FROM="4546"
MAIL_DOMAIN=smtp.gmail.com
MAIL_USER="JscorpTech@gmail.com"
MAIL_PASSWORD="app password"
MAIL_PORT=587
PMB_DOMAIN="" PMB_DOMAIN=""
PMB_USER="" PMB_USER=""
PMB_PASSWORD="" PMB_PASSWORD=""

5
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.env .env
./bin bin
./main main
release

213
README.MD
View File

@@ -1,133 +1,170 @@
# Notification Service # Notification Service
A microservice for handling and delivering notifications through various channels like SMS and email using RabbitMQ as a message broker. A flexible notification service that supports multiple message brokers (Redis, RabbitMQ) and notification channels (SMS, Email). This service is designed to handle asynchronous notification delivery in your applications.
## Overview
This notification service is designed as a standalone microservice that consumes notification requests from a RabbitMQ queue and routes them to the appropriate notification provider based on the notification type. Currently, it supports SMS and email notifications.
## Features ## Features
- Message consumption from RabbitMQ - **Multiple Brokers**: Support for both Redis and RabbitMQ as message brokers
- Support for multiple notification channels (SMS, email) - **Multiple Notification Channels**: SMS and Email notification support
- Extensible architecture for adding new notification types - **Containerized**: Ready to deploy with Docker
- Asynchronous notification handling - **Extensible Architecture**: Easy to add new notification channels or message brokers
## Architecture ## Architecture
The notification service follows a clean architecture approach: The service follows a clean architecture pattern with the following components:
- **Domain Layer**: Contains core business logic and port interfaces - **Broker**: Handles message subscription from different sources (Redis/RabbitMQ)
- **Infrastructure Layer**: Implements the ports with concrete adapters - **Notifier**: Implements different notification channels (SMS/Email)
- **RabbitMQ**: Used as a message broker for consuming notification requests - **Services**: Contains the business logic for each notification channel
- **Domain**: Defines interfaces and data models
## Installation ## Prerequisites
### Prerequisites - Go 1.24 or higher
- Redis (for Redis broker)
- Go 1.x+ - RabbitMQ (for RabbitMQ broker)
- RabbitMQ server - Docker (optional, for containerized deployment)
### Setup
1. Clone the repository:
```bash
git clone https://github.com/JscorpTech/notification.git
cd notification
```
2. Install dependencies:
```bash
go mod download
```
3. Build the application:
```bash
go build -o notification-service
```
## Configuration ## Configuration
Configure your RabbitMQ connection and other settings in the appropriate configuration files. Copy the provided `.env.example` to `.env` and update with your configuration:
```bash
cp .env.example .env
```
### Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| BROKER | Message broker to use (redis or rabbitmq) | redis |
| TOPIC | Topic/queue name for notifications | notification |
| REDIS_ADDRESS | Redis server address | 127.0.0.1:6379 |
| REDIS_PASSWORD | Redis password (if any) | |
| REDIS_DB | Redis database number | 0 |
| RABBITMQ_URL | RabbitMQ connection URL | amqp://guest:guest@localhost:5672/ |
| ESKIZ_DOMAIN | Eskiz SMS API domain | https://notify.eskiz.uz/api |
| ESKIZ_USER | Eskiz SMS API username | admin@example.com |
| ESKIZ_PASSWORD | Eskiz SMS API password | password |
| ESKIZ_FROM | Eskiz SMS sender ID | 4546 |
| MAIL_DOMAIN | SMTP server domain | smtp.gmail.com |
| MAIL_USER | SMTP username | notification@example.com |
| MAIL_PASSWORD | SMTP password | yourpassword |
| MAIL_PORT | SMTP port | 587 |
## Installation
### Local Development
1. Clone the repository
```bash
git clone https://github.com/JscorpTech/notification.git
cd notification
```
2. Install dependencies
```bash
go mod download
```
3. Build and run the application
```bash
go build -o notification ./cmd/main.go
./notification
```
### Docker Deployment
Build and run using Docker:
```bash
docker build -t notification-service .
docker run -p 8080:8080 --env-file .env notification-service
```
Or using Docker Compose:
```bash
docker-compose up -d
```
## Usage ## Usage
### Running the service ### Message Format
```bash The service expects messages in the following JSON format:
./notification-service
```
This will start the notification consumer that listens for incoming notification requests.
### Sending a notification
Notifications should be published to the RabbitMQ exchange with the following JSON format:
```json ```json
{ {
"type": "email", "type": "email",
"message": "Hello, this is a test notification.", "message": "Subject: Welcome\r\n\r\nHello, welcome to our service.",
"to": ["user@example.com"] "to": ["user@example.com"]
} }
``` ```
Python example For SMS notifications:
```json
{
"type": "sms",
"message": "Your verification code is 1234",
"to": ["+998901234567"]
}
```
### Sending Messages
#### Using Redis
```python
import redis
import json
r = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
message = {
'type': 'email',
'message': "Subject: Welcome\r\n\r\nWelcome to our service!",
'to': ["user@example.com"]
}
r.rpush('notification', json.dumps(message))
```
#### Using RabbitMQ
```python ```python
from kombu import Connection, Exchange, Producer from kombu import Connection, Exchange, Producer
# RabbitMQ ulanishi
rabbit_url = 'amqp://guest:guest@127.0.0.1:5672/' rabbit_url = 'amqp://guest:guest@127.0.0.1:5672/'
connection = Connection(rabbit_url) connection = Connection(rabbit_url)
channel = connection.channel() channel = connection.channel()
exchange = Exchange('notification', type='direct') exchange = Exchange('notification', type='direct')
# Producer yaratish
producer = Producer(channel, exchange=exchange, routing_key="notification") producer = Producer(channel, exchange=exchange, routing_key="notification")
# Xabar yuborish message = {
message = {'type': 'sms', 'message': "classcom.uz sayti va mobil ilovasiga ro'yxatdan o'tishingingiz uchun tasdiqlash kodi: 1234", "to": ["+998888112309", "+998943990509"]} 'type': 'sms',
'message': "Your verification code is 1234",
'to': ["+998901234567"]
}
producer.publish(message) producer.publish(message)
print("Message sent to all workers!")
``` ```
Available notification types: ## Adding New Notification Channels
- `email`: For email notifications
- `sms`: For SMS notifications
## Project Structure 1. Create a new notifier implementation in `internal/notifier/`
2. Implement the `domain.NotifierPort` interface
3. Add the new notifier type to the `Handler` function in `internal/consumer/notification.go`
``` ## License
notification/
├── cmd/ MIT License - See [LICENSE](LICENSE) file for details.
│ └── main.go # Entry point
├── internal/
│ ├── domain/
│ │ └── ports.go # Interfaces
│ ├── notifier/
│ │ ├── email.go # Email notification implementation
│ │ └── sms.go # SMS notification implementation
│ ├── rabbitmq/
│ │ └── connection.go # RabbitMQ connection handling
│ └── consumer/
│ └── consumer.go # Implementation of the notification consumer
└── README.md
```
## Contributing ## Contributing
1. Fork the repository 1. Fork the repository
2. Create your feature branch: `git checkout -b feature/my-new-feature` 2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Commit your changes: `git commit -am 'Add some feature'` 3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch: `git push origin feature/my-new-feature` 4. Push to the branch (`git push origin feature/my-feature`)
5. Submit a pull request 5. Create a new Pull Request
## License
[Add your license here]
## Contact
JscorpTech - [GitHub](https://github.com/JscorpTech)

View File

@@ -0,0 +1,46 @@
package broker
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/JscorpTech/notification/internal/domain"
"github.com/JscorpTech/notification/internal/rabbitmq"
)
type rabbitMQBroker struct {
Ctx context.Context
}
func NewRabbitMQBroker(ctx context.Context) domain.BrokerPort {
return &rabbitMQBroker{
Ctx: ctx,
}
}
func (r rabbitMQBroker) Subscribe(topic string, handler func(domain.NotificationMsg)) {
conn, ch, err := rabbitmq.Connect()
if err != nil {
log.Fatal(err)
}
defer conn.Close()
defer ch.Close()
ch.ExchangeDeclare(topic, "direct", true, false, false, false, nil)
q, _ := ch.QueueDeclare(topic, true, false, false, false, nil)
ch.QueueBind(q.Name, topic, topic, false, nil)
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
go func() {
for msg := range msgs {
var notification domain.NotificationMsg
if err := json.Unmarshal(msg.Body, &notification); err != nil {
fmt.Print(err.Error())
}
go handler(notification)
}
}()
}

39
internal/broker/redis.go Normal file
View File

@@ -0,0 +1,39 @@
package broker
import (
"context"
"encoding/json"
"fmt"
"github.com/JscorpTech/notification/internal/domain"
"github.com/JscorpTech/notification/internal/redis"
)
type redisBroker struct {
Ctx context.Context
}
func NewRedisBroker(ctx context.Context) domain.BrokerPort {
return &redisBroker{
Ctx: ctx,
}
}
func (r redisBroker) Subscribe(topic string, handler func(domain.NotificationMsg)) {
go func() {
for {
var notification domain.NotificationMsg
val, err := redis.RDB.BLPop(r.Ctx, 0, topic).Result()
if err != nil {
fmt.Print(err.Error())
return
}
if err := json.Unmarshal([]byte(val[1]), &notification); err != nil {
fmt.Print(err.Error())
return
}
go handler(notification)
}
}()
}

View File

@@ -2,14 +2,12 @@ package consumer
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"log" "os"
"github.com/JscorpTech/notification/internal/broker"
"github.com/JscorpTech/notification/internal/domain" "github.com/JscorpTech/notification/internal/domain"
"github.com/JscorpTech/notification/internal/notifier" "github.com/JscorpTech/notification/internal/notifier"
"github.com/JscorpTech/notification/internal/rabbitmq"
"github.com/streadway/amqp"
) )
type notificationConsumer struct { type notificationConsumer struct {
@@ -23,39 +21,26 @@ func NewNotificationConsumer(ctx context.Context) domain.NotificationConsumerPor
} }
func (n *notificationConsumer) Start() { func (n *notificationConsumer) Start() {
conn, ch, err := rabbitmq.Connect()
if err != nil { brokerName := os.Getenv("BROKER")
log.Fatal(err) if brokerName == "" {
brokerName = "redis"
} }
defer conn.Close() var brokerService domain.BrokerPort
defer ch.Close() switch brokerName {
case "redis":
exchangeName := "notification" brokerService = broker.NewRedisBroker(n.Ctx)
queueName := "notification" case "rabbitmq":
routingKey := "notification" brokerService = broker.NewRabbitMQBroker(n.Ctx)
default:
ch.ExchangeDeclare(exchangeName, "direct", true, false, false, false, nil) brokerService = broker.NewRedisBroker(n.Ctx)
q, _ := ch.QueueDeclare(queueName, true, false, false, false, nil) }
ch.QueueBind(q.Name, routingKey, exchangeName, false, nil) brokerService.Subscribe(os.Getenv("TOPIC"), n.Handler)
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
go func() {
for msg := range msgs {
go n.Handler(msg)
}
}()
fmt.Println("🚀 Server started. Ctrl+C to quit.") fmt.Println("🚀 Server started. Ctrl+C to quit.")
select {} select {}
} }
func (n *notificationConsumer) Handler(msg amqp.Delivery) { func (n *notificationConsumer) Handler(notification domain.NotificationMsg) {
var notification domain.NotificationMsg
err := json.Unmarshal(msg.Body, &notification)
if err != nil {
fmt.Print(err.Error())
}
var ntf domain.NotifierPort var ntf domain.NotifierPort
switch notification.Type { switch notification.Type {
case "sms": case "sms":
@@ -63,5 +48,6 @@ func (n *notificationConsumer) Handler(msg amqp.Delivery) {
case "email": case "email":
ntf = notifier.NewEmailNotifier() ntf = notifier.NewEmailNotifier()
} }
fmt.Println(notification.Message)
ntf.SendMessage(notification.To, notification.Message) ntf.SendMessage(notification.To, notification.Message)
} }

View File

@@ -0,0 +1,6 @@
package domain
type BrokerPort interface {
Subscribe(string, func(NotificationMsg))
// Publish()
}

5
internal/domain/email.go Normal file
View File

@@ -0,0 +1,5 @@
package domain
type EmailServicePort interface {
SendMail([]string, []byte)
}

View File

@@ -1,10 +1,8 @@
package domain package domain
import "github.com/streadway/amqp"
type NotificationConsumerPort interface { type NotificationConsumerPort interface {
Start() Start()
Handler(amqp.Delivery) Handler(NotificationMsg)
} }
type SMSServicePort interface { type SMSServicePort interface {

View File

@@ -2,15 +2,19 @@ package notifier
import ( import (
"github.com/JscorpTech/notification/internal/domain" "github.com/JscorpTech/notification/internal/domain"
"github.com/k0kubun/pp/v3" "github.com/JscorpTech/notification/internal/services"
) )
type emailNotifier struct{} type emailNotifier struct {
EmailService domain.EmailServicePort
}
func NewEmailNotifier() domain.NotifierPort { func NewEmailNotifier() domain.NotifierPort {
return &emailNotifier{} return &emailNotifier{
EmailService: services.NewEmailService(),
}
} }
func (n *emailNotifier) SendMessage(to []string, body string) { func (n *emailNotifier) SendMessage(to []string, body string) {
pp.Print(to, body) n.EmailService.SendMail(to, []byte(body))
} }

View File

@@ -0,0 +1,33 @@
package services
import (
"fmt"
"net/smtp"
"os"
"github.com/JscorpTech/notification/internal/domain"
)
type emailService struct{}
func NewEmailService() domain.EmailServicePort {
return &emailService{}
}
func (e *emailService) SendMail(to []string, body []byte) {
// Gmail konfiguratsiyasi
from := os.Getenv("MAIL_USER")
password := os.Getenv("MAIL_PASSWORD")
smtpHost := os.Getenv("MAIL_DOMAIN")
smtpPort := os.Getenv("MAIL_PORT")
auth := smtp.PlainAuth("", from, password, smtpHost)
err := smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, body)
if err != nil {
fmt.Println("Xatolik:", err)
return
}
fmt.Println("Email yuborildi!")
}

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"os" "os"
"time" "time"
@@ -35,7 +36,7 @@ func (e *eskizSMSService) Request(payload any, path string, isAuth bool, retry b
req, err := http.NewRequest("POST", e.BaseURL+path, &buf) req, err := http.NewRequest("POST", e.BaseURL+path, &buf)
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
if isAuth { if isAuth {
req.Header.Add("Authorization", "Bearer "+e.GetToken(true)) req.Header.Add("Authorization", "Bearer "+e.GetToken(true, true))
} }
if err != nil { if err != nil {
@@ -43,24 +44,32 @@ func (e *eskizSMSService) Request(payload any, path string, isAuth bool, retry b
} }
res, err := client.Do(req) res, err := client.Do(req)
if res.StatusCode == http.StatusUnauthorized && retry { if res.StatusCode == http.StatusUnauthorized && retry {
time.Sleep(time.Second * 5)
pp.Print("Qayta urunish") pp.Print("Qayta urunish")
e.GetToken(false) e.GetToken(false, false)
return e.Request(payload, path, isAuth, false) return e.Request(payload, path, isAuth, false)
} }
return res, err return res, err
} }
func (e *eskizSMSService) GetToken(cache bool) string { func (e *eskizSMSService) GetToken(cache bool, retry bool) string {
email := os.Getenv("ESKIZ_USER")
password := os.Getenv("ESKIZ_PASSWORD")
if email == "" || password == "" {
log.Fatal("password or fmail not found")
}
token, err := redis.RDB.Get(e.Ctx, "eskiz_token").Result() token, err := redis.RDB.Get(e.Ctx, "eskiz_token").Result()
if err == nil && cache { if err == nil && cache {
pp.Print("Eskiz token topildi 😁") pp.Print("Eskiz token topildi 😁")
return token return token
} }
payload := domain.EskizLogin{ payload := domain.EskizLogin{
Email: os.Getenv("ESKIZ_USER"), Email: email,
Password: os.Getenv("ESKIZ_PASSWORD"), Password: password,
} }
res, err := e.Request(payload, "/auth/login", false, true) res, err := e.Request(payload, "/auth/login", false, retry)
if err != nil { if err != nil {
pp.Print(err.Error()) pp.Print(err.Error())
} }

15
notify.service Normal file
View File

@@ -0,0 +1,15 @@
[Unit]
Description="Notification service"
After=network.target
[Service]
User=root
Group=root
Type=simple
Restart=on-failure
RestartSec=5s
ExecStart=/home/user/projects/notification/main
WorkingDirectory=/home/user/projects/notification
[Install]
WantedBy=multi-user.target

36
test.py Normal file
View File

@@ -0,0 +1,36 @@
# from kombu import Connection, Exchange, Producer
# # RabbitMQ ulanishi
# rabbit_url = 'amqp://guest:guest@127.0.0.1:5672/'
# connection = Connection(rabbit_url)
# channel = connection.channel()
# exchange = Exchange('notification', type='direct')
# # Producer yaratish
# producer = Producer(channel, exchange=exchange, routing_key="notification")
# # Xabar yuborish
# message = {'type': 'email', 'message': "Subject: test\r\n\r\nclasscom.uz sayti va mobil ilovasiga ro'yxatdan o'tishingingiz uchun tasdiqlash kodi: 1234", "to": ["JscorpTech@gmail.com", "admin@jscorp.uz"]}
# producer.publish(message)
# print("Message sent to all workers!")
import redis
import json
# Redis ulanishi
r = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
# Subject: tasdiqlash ko'di\r\n\r\n
# Xabar tayyorlash
message = {
'type': 'sms',
'message': "Assalomu alaykum samandar sizni https://classcom.uz oqituvchining virtual kаbinetida muallif sifatida tasdiqlanganingiz bilan tabriklaymiz!!!",
'to': ["+998888112309"]
}
# Xabarni JSON formatga otkazib, Redis listga push qilish
r.rpush('notification', json.dumps(message))
print("Message pushed to Redis list!")

2
tmp/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1 +0,0 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1