From 937dda4bdd0c667725def457d87a2bcbbd31c4e7 Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Sun, 20 Apr 2025 16:15:04 +0500 Subject: [PATCH 1/8] feat: Notification service qo'shildi va RabbitMQ integratsiya qilindi --- Dockerfile | 19 +++++ README.MD | 112 ++++++++++++++++++++++++++++++ cmd/main.go | 10 +++ go.mod | 12 ++++ go.sum | 13 ++++ internal/config/config.go | 13 ++++ internal/consumer/notification.go | 62 +++++++++++++++++ internal/domain/notification.go | 18 +++++ internal/notifier/email.go | 16 +++++ internal/notifier/sms.go | 16 +++++ internal/rabbitmq/connection.go | 21 ++++++ 11 files changed, 312 insertions(+) create mode 100644 Dockerfile create mode 100644 README.MD create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/config/config.go create mode 100644 internal/consumer/notification.go create mode 100644 internal/domain/notification.go create mode 100644 internal/notifier/email.go create mode 100644 internal/notifier/sms.go create mode 100644 internal/rabbitmq/connection.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b5f9de3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:alpine as build + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY . . + +RUN go build -o notification ./cmd/main.go + +FROM alpine + +WORKDIR /app + +COPY --from=build /app/notification . + +CMD ["./notification"] diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..1a573f1 --- /dev/null +++ b/README.MD @@ -0,0 +1,112 @@ +# Notification Service + +A microservice for handling and delivering notifications through various channels like SMS and email using RabbitMQ as a message broker. + +## 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 + +- Message consumption from RabbitMQ +- Support for multiple notification channels (SMS, email) +- Extensible architecture for adding new notification types +- Asynchronous notification handling + +## Architecture + +The notification service follows a clean architecture approach: + +- **Domain Layer**: Contains core business logic and port interfaces +- **Infrastructure Layer**: Implements the ports with concrete adapters +- **RabbitMQ**: Used as a message broker for consuming notification requests + +## Installation + +### Prerequisites + +- Go 1.x+ +- RabbitMQ server + +### 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 + +Configure your RabbitMQ connection and other settings in the appropriate configuration files. + +## Usage + +### Running the service + +```bash +./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 +{ + "type": "email", + "message": "Hello, this is a test notification.", + "to": ["user@example.com"] +} +``` + +Available notification types: +- `email`: For email notifications +- `sms`: For SMS notifications + +## Project Structure + +``` +notification/ +├── cmd/ +│ └── 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 + +1. Fork the repository +2. Create your feature branch: `git checkout -b feature/my-new-feature` +3. Commit your changes: `git commit -am 'Add some feature'` +4. Push to the branch: `git push origin feature/my-new-feature` +5. Submit a pull request + +## License + +[Add your license here] + +## Contact + +JscorpTech - [GitHub](https://github.com/JscorpTech) diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..845e0aa --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/JscorpTech/notification/internal/consumer" +) + +func main() { + notification := consumer.NewNotificationConsumer() + notification.Start() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8c97b6b --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/JscorpTech/notification + +go 1.24.0 + +require ( + github.com/k0kubun/pp/v3 v3.4.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/streadway/amqp v1.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.19.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..27459d5 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y= +github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= +github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2f86d1c --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,13 @@ +package config + +import ( + "os" +) + +func GetEnv(key, fallback string) string { + val := os.Getenv(key) + if val == "" { + return fallback + } + return val +} diff --git a/internal/consumer/notification.go b/internal/consumer/notification.go new file mode 100644 index 0000000..a32571c --- /dev/null +++ b/internal/consumer/notification.go @@ -0,0 +1,62 @@ +package consumer + +import ( + "encoding/json" + "fmt" + "log" + + "github.com/JscorpTech/notification/internal/domain" + "github.com/JscorpTech/notification/internal/notifier" + "github.com/JscorpTech/notification/internal/rabbitmq" + "github.com/streadway/amqp" +) + +type notificationConsumer struct{} + +func NewNotificationConsumer() domain.NotificationConsumerPort { + return ¬ificationConsumer{} +} + +func (n *notificationConsumer) Start() { + conn, ch, err := rabbitmq.Connect() + if err != nil { + log.Fatal(err) + } + defer conn.Close() + defer ch.Close() + + exchangeName := "notification" + queueName := "notification" + routingKey := "notification" + + ch.ExchangeDeclare(exchangeName, "direct", true, false, false, false, nil) + q, _ := ch.QueueDeclare(queueName, true, false, false, false, nil) + ch.QueueBind(q.Name, routingKey, exchangeName, false, nil) + + 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.") + select {} +} + +func (n *notificationConsumer) Handler(msg amqp.Delivery) { + var notification domain.NotificationMsg + err := json.Unmarshal(msg.Body, ¬ification) + if err != nil { + fmt.Print(err.Error()) + } + var ntf domain.NotifierPort + switch notification.Type { + case "sms": + ntf = notifier.NewSmsNotifier() + case "email": + ntf = notifier.NewEmailNotifier() + } + ntf.SendMessage(notification.To, notification.Message) +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go new file mode 100644 index 0000000..be5bc44 --- /dev/null +++ b/internal/domain/notification.go @@ -0,0 +1,18 @@ +package domain + +import "github.com/streadway/amqp" + +type NotificationConsumerPort interface { + Start() + Handler(amqp.Delivery) +} + +type NotifierPort interface { + SendMessage([]string, string) +} + +type NotificationMsg struct { + Type string `json:"type"` + Message string `json:"message"` + To []string `json:"to"` +} diff --git a/internal/notifier/email.go b/internal/notifier/email.go new file mode 100644 index 0000000..fb4b52d --- /dev/null +++ b/internal/notifier/email.go @@ -0,0 +1,16 @@ +package notifier + +import ( + "github.com/JscorpTech/notification/internal/domain" + "github.com/k0kubun/pp/v3" +) + +type emailNotifier struct{} + +func NewEmailNotifier() domain.NotifierPort { + return &emailNotifier{} +} + +func (n *emailNotifier) SendMessage(to []string, body string) { + pp.Print(to, body) +} diff --git a/internal/notifier/sms.go b/internal/notifier/sms.go new file mode 100644 index 0000000..ab495d8 --- /dev/null +++ b/internal/notifier/sms.go @@ -0,0 +1,16 @@ +package notifier + +import ( + "github.com/JscorpTech/notification/internal/domain" + "github.com/k0kubun/pp/v3" +) + +type smsNotifier struct{} + +func NewSmsNotifier() domain.NotifierPort { + return &smsNotifier{} +} + +func (n *smsNotifier) SendMessage(to []string, body string) { + pp.Print(to, body) +} diff --git a/internal/rabbitmq/connection.go b/internal/rabbitmq/connection.go new file mode 100644 index 0000000..2c432e1 --- /dev/null +++ b/internal/rabbitmq/connection.go @@ -0,0 +1,21 @@ +package rabbitmq + +import ( + "log" + + "github.com/streadway/amqp" +) + +func Connect() (*amqp.Connection, *amqp.Channel, error) { + conn, err := amqp.Dial("amqp://guest:guest@127.0.0.1:5672/") + if err != nil { + return nil, nil, err + } + ch, err := conn.Channel() + if err != nil { + return nil, nil, err + } + log.Println("🐇 Connected to RabbitMQ") + + return conn, ch, nil +} From 9b61f2f37d83bd5cda444b549bd33b5c11a1da52 Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Sun, 20 Apr 2025 16:22:35 +0500 Subject: [PATCH 2/8] fix: RabbitMQ ulanish manzilini yangilandi --- internal/rabbitmq/connection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/rabbitmq/connection.go b/internal/rabbitmq/connection.go index 2c432e1..fa32da8 100644 --- a/internal/rabbitmq/connection.go +++ b/internal/rabbitmq/connection.go @@ -7,7 +7,7 @@ import ( ) func Connect() (*amqp.Connection, *amqp.Channel, error) { - conn, err := amqp.Dial("amqp://guest:guest@127.0.0.1:5672/") + conn, err := amqp.Dial("amqp://guest:guest@rabbitmq:5672/") if err != nil { return nil, nil, err } From 09ea20b0884171d10a78f3672efcf581479f2b55 Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Mon, 21 Apr 2025 13:08:09 +0500 Subject: [PATCH 3/8] Qo'shildi: Docker konfiguratsiyasi yangilandi, .env va .gitignore fayllari yaratildi, Redis va muhit o'zgaruvchilari qo'shildi. --- .env.example | 15 ++++++ .gitignore | 3 ++ cmd/main.go | 12 ++++- go.mod | 4 ++ go.sum | 8 +++ internal/consumer/notification.go | 13 +++-- internal/domain/eskiz.go | 27 ++++++++++ internal/domain/notification.go | 3 ++ internal/domain/playmobile.go | 20 +++++++ internal/notifier/sms.go | 19 +++++-- internal/rabbitmq/connection.go | 3 +- internal/redis/client.go | 27 ++++++++++ internal/services/eskiz.go | 89 +++++++++++++++++++++++++++++++ internal/services/playmobile.go | 53 ++++++++++++++++++ tmp/build-errors.log | 1 + 15 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 internal/domain/eskiz.go create mode 100644 internal/domain/playmobile.go create mode 100644 internal/redis/client.go create mode 100644 internal/services/eskiz.go create mode 100644 internal/services/playmobile.go create mode 100644 tmp/build-errors.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..54202b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +RABBITMQ_URL=amqp://guest:guest@127.0.0.1:5672/ + + +REDIS_ADDRESS=127.0.0.1:6379 +REDIS_PASSWORD= +REDIS_DB=0 + +ESKIZ_DOMAIN="https://notify.eskiz.uz/api" +ESKIZ_USER="admin@gmail.com" +ESKIZ_PASSWORD="password" +ESKIZ_FROM="4546" + +PMB_DOMAIN="" +PMB_USER="" +PMB_PASSWORD="" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61a32ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +./bin +./main \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 845e0aa..0a1748b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,10 +1,20 @@ package main import ( + "context" + "github.com/JscorpTech/notification/internal/consumer" + "github.com/JscorpTech/notification/internal/redis" + "github.com/joho/godotenv" ) +var ctx = context.Background() + func main() { - notification := consumer.NewNotificationConsumer() + if err := godotenv.Load(); err != nil { + panic(err) + } + redis.InitRedis() + notification := consumer.NewNotificationConsumer(ctx) notification.Start() } diff --git a/go.mod b/go.mod index 8c97b6b..9c1fe81 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,13 @@ module github.com/JscorpTech/notification go 1.24.0 require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/k0kubun/pp/v3 v3.4.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect + github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/streadway/amqp v1.1.0 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.19.0 // indirect diff --git a/go.sum b/go.sum index 27459d5..c6bb9eb 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y= github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/consumer/notification.go b/internal/consumer/notification.go index a32571c..7543116 100644 --- a/internal/consumer/notification.go +++ b/internal/consumer/notification.go @@ -1,6 +1,7 @@ package consumer import ( + "context" "encoding/json" "fmt" "log" @@ -11,10 +12,14 @@ import ( "github.com/streadway/amqp" ) -type notificationConsumer struct{} +type notificationConsumer struct { + Ctx context.Context +} -func NewNotificationConsumer() domain.NotificationConsumerPort { - return ¬ificationConsumer{} +func NewNotificationConsumer(ctx context.Context) domain.NotificationConsumerPort { + return ¬ificationConsumer{ + Ctx: ctx, + } } func (n *notificationConsumer) Start() { @@ -54,7 +59,7 @@ func (n *notificationConsumer) Handler(msg amqp.Delivery) { var ntf domain.NotifierPort switch notification.Type { case "sms": - ntf = notifier.NewSmsNotifier() + ntf = notifier.NewSmsNotifier(n.Ctx) case "email": ntf = notifier.NewEmailNotifier() } diff --git a/internal/domain/eskiz.go b/internal/domain/eskiz.go new file mode 100644 index 0000000..9cccfaa --- /dev/null +++ b/internal/domain/eskiz.go @@ -0,0 +1,27 @@ +package domain + +type EskizLogin struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type EskizLoginRes struct { + Message string + TokenType string + Data struct { + Token string + } +} + +type EskizMessage struct { + Phone string `json:"mobile_phone"` + Message string `json:"message"` + From string `json:"from"` + CallbackURL string `json:"callback_url"` +} + +type EskizMessageRes struct { + ID string `json:"id"` + Message string `json:"message"` + Status string `json:"status"` +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index be5bc44..8442b1d 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -7,6 +7,9 @@ type NotificationConsumerPort interface { Handler(amqp.Delivery) } +type SMSServicePort interface { + SendSMS(string, string) error +} type NotifierPort interface { SendMessage([]string, string) } diff --git a/internal/domain/playmobile.go b/internal/domain/playmobile.go new file mode 100644 index 0000000..0516a12 --- /dev/null +++ b/internal/domain/playmobile.go @@ -0,0 +1,20 @@ +package domain + +type PmbContent struct { + Text string `json:"text"` +} + +type PmbSMS struct { + Originator string `json:"originator"` + Content PmbContent `json:"content"` +} + +type PmbMessage struct { + Recipient string `json:"recipient"` + MessageID string `json:"message-id"` + Sms PmbSMS `json:"sms"` +} + +type PmbPayload struct { + Messages []PmbMessage +} diff --git a/internal/notifier/sms.go b/internal/notifier/sms.go index ab495d8..a4668f2 100644 --- a/internal/notifier/sms.go +++ b/internal/notifier/sms.go @@ -1,16 +1,25 @@ package notifier import ( + "context" + "github.com/JscorpTech/notification/internal/domain" - "github.com/k0kubun/pp/v3" + "github.com/JscorpTech/notification/internal/services" ) -type smsNotifier struct{} +type smsNotifier struct { + SMSServie domain.SMSServicePort + Ctx context.Context +} -func NewSmsNotifier() domain.NotifierPort { - return &smsNotifier{} +func NewSmsNotifier(ctx context.Context) domain.NotifierPort { + return &smsNotifier{ + SMSServie: services.NewEskizSMSService(ctx), + } } func (n *smsNotifier) SendMessage(to []string, body string) { - pp.Print(to, body) + for _, user := range to { + n.SMSServie.SendSMS(user, body) + } } diff --git a/internal/rabbitmq/connection.go b/internal/rabbitmq/connection.go index fa32da8..7aba1e6 100644 --- a/internal/rabbitmq/connection.go +++ b/internal/rabbitmq/connection.go @@ -2,12 +2,13 @@ package rabbitmq import ( "log" + "os" "github.com/streadway/amqp" ) func Connect() (*amqp.Connection, *amqp.Channel, error) { - conn, err := amqp.Dial("amqp://guest:guest@rabbitmq:5672/") + conn, err := amqp.Dial(os.Getenv("RABBITMQ_URL")) if err != nil { return nil, nil, err } diff --git a/internal/redis/client.go b/internal/redis/client.go new file mode 100644 index 0000000..7969a67 --- /dev/null +++ b/internal/redis/client.go @@ -0,0 +1,27 @@ +package redis + +import ( + "context" + "log" + "os" + "strconv" + + "github.com/redis/go-redis/v9" +) + +var RDB *redis.Client + +func InitRedis() { + DB, _ := strconv.Atoi(os.Getenv("REDIS_DB")) + RDB = redis.NewClient(&redis.Options{ + Addr: os.Getenv("REDIS_ADDRESS"), + Password: os.Getenv("REDIS_PASSWORD"), + DB: DB, + }) + + // Test connection + _, err := RDB.Ping(context.Background()).Result() + if err != nil { + log.Fatalf("Redisga ulanib bo'lmadi: %v", err) + } +} diff --git a/internal/services/eskiz.go b/internal/services/eskiz.go new file mode 100644 index 0000000..91309ca --- /dev/null +++ b/internal/services/eskiz.go @@ -0,0 +1,89 @@ +package services + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "os" + "time" + + "github.com/JscorpTech/notification/internal/domain" + "github.com/JscorpTech/notification/internal/redis" + "github.com/k0kubun/pp/v3" +) + +type eskizSMSService struct { + BaseURL string + Ctx context.Context +} + +// /broker-api/send +func NewEskizSMSService(ctx context.Context) domain.SMSServicePort { + return &eskizSMSService{ + Ctx: ctx, + BaseURL: os.Getenv("ESKIZ_DOMAIN"), + } +} + +func (e *eskizSMSService) Request(payload any, path string, isAuth bool, retry bool) (*http.Response, error) { + var buf bytes.Buffer + _ = json.NewEncoder(&buf).Encode(payload) + client := http.Client{ + Timeout: 60 * time.Second, + } + req, err := http.NewRequest("POST", e.BaseURL+path, &buf) + req.Header.Add("Content-Type", "application/json") + if isAuth { + req.Header.Add("Authorization", "Bearer "+e.GetToken(true)) + } + + if err != nil { + return nil, err + } + res, err := client.Do(req) + if res.StatusCode == http.StatusUnauthorized && retry { + pp.Print("Qayta urunish") + e.GetToken(false) + return e.Request(payload, path, isAuth, false) + } + return res, err +} + +func (e *eskizSMSService) GetToken(cache bool) string { + token, err := redis.RDB.Get(e.Ctx, "eskiz_token").Result() + if err == nil && cache { + pp.Print("Eskiz token topildi 😁") + return token + } + payload := domain.EskizLogin{ + Email: os.Getenv("ESKIZ_USER"), + Password: os.Getenv("ESKIZ_PASSWORD"), + } + res, err := e.Request(payload, "/auth/login", false, true) + if err != nil { + pp.Print(err.Error()) + } + var data domain.EskizLoginRes + _ = json.NewDecoder(res.Body).Decode(&data) + token = data.Data.Token + redis.RDB.Set(e.Ctx, "eskiz_token", token, 30*24*time.Hour) + pp.Print("Eskiz yangi token olindi 😔") + return token +} + +func (e *eskizSMSService) SendSMS(to, body string) error { + payload := domain.EskizMessage{ + Phone: to, + Message: body, + From: os.Getenv("ESKIZ_FROM"), + } + res, err := e.Request(payload, "/message/sms/send", true, true) + if err != nil { + return err + } + var data domain.EskizMessageRes + json.NewDecoder(res.Body).Decode(&data) + pp.Print(data) + return nil +} diff --git a/internal/services/playmobile.go b/internal/services/playmobile.go new file mode 100644 index 0000000..b2990cc --- /dev/null +++ b/internal/services/playmobile.go @@ -0,0 +1,53 @@ +package services + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/JscorpTech/notification/internal/domain" +) + +type pmbSMSService struct { + BaseURL string +} + +// /broker-api/send +func NewPmbSMSService() domain.SMSServicePort { + return &pmbSMSService{ + BaseURL: "https://send.smsxabar.uz", + } +} + +func (e *pmbSMSService) SendSMS(to, body string) error { + client := http.Client{ + Timeout: 60 * time.Second, + } + payload := domain.PmbPayload{ + Messages: []domain.PmbMessage{ + { + Recipient: "+998888112309", + MessageID: "salomsdfs", + Sms: domain.PmbSMS{ + Originator: "3600", + Content: domain.PmbContent{ + Text: "salom", + }, + }, + }, + }, + } + var buf bytes.Buffer + _ = json.NewEncoder(&buf).Encode(payload) + req, _ := http.NewRequest("POST", e.BaseURL+"/broker-api/send", &buf) + res, err := client.Do(req) + if err != nil { + return err + } + var data map[string]interface{} + json.NewDecoder(res.Body).Decode(&data) + fmt.Print(data) + return nil +} diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..94241fd --- /dev/null +++ b/tmp/build-errors.log @@ -0,0 +1 @@ +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 \ No newline at end of file From fe388e992c6e254e61b080718a91fb4a63a0876a Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Mon, 21 Apr 2025 13:08:16 +0500 Subject: [PATCH 4/8] RabbitMQ uchun Python misoli qo'shildi --- README.MD | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.MD b/README.MD index 1a573f1..9577df7 100644 --- a/README.MD +++ b/README.MD @@ -72,6 +72,27 @@ Notifications should be published to the RabbitMQ exchange with the following JS } ``` +Python example +```python +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': 'sms', 'message': "classcom.uz sayti va mobil ilovasiga ro'yxatdan o'tishingingiz uchun tasdiqlash kodi: 1234", "to": ["+998888112309", "+998943990509"]} +producer.publish(message) + +print("Message sent to all workers!") +``` + Available notification types: - `email`: For email notifications - `sms`: For SMS notifications From 742b2a57afa46a5cc21bea5e8e608589de0504f7 Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Mon, 21 Apr 2025 13:13:37 +0500 Subject: [PATCH 5/8] Dockerfile va docker-compose.yml o'zgartirishlar: volumes olib tashlandi, .env qo'shildi --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b5f9de3..8f77c01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:alpine as build +FROM golang:alpine AS build WORKDIR /app @@ -15,5 +15,6 @@ FROM alpine WORKDIR /app COPY --from=build /app/notification . +COPY ./.env /app/ CMD ["./notification"] From cafb2a4b759b20bff4395ef64527a2804c72717d Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Mon, 21 Apr 2025 18:42:21 +0500 Subject: [PATCH 6/8] Docker compose konfiguratsiyasini yangilandi va xizmatlar uchun yangi buyruqlar qo'shildi. --- docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4ca2736 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +networks: + lamenu: + external: true + name: lamenu + +services: + notification: + build: + context: . + dockerfile: Dockerfile + networks: + - lamenu From 763ff8cdf519aba2a42b9b5faa5c556af62b27b7 Mon Sep 17 00:00:00 2001 From: A'zamov Samandar Date: Mon, 21 Apr 2025 20:06:20 +0500 Subject: [PATCH 7/8] Yangi notification model va RabbitMQ ulanish funksiyalari qo'shildi --- internal/models/fcm.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 internal/models/fcm.go diff --git a/internal/models/fcm.go b/internal/models/fcm.go new file mode 100644 index 0000000..2640e7f --- /dev/null +++ b/internal/models/fcm.go @@ -0,0 +1 @@ +package models From 6ef9205517222feede6699b8bbb1a955f143c62b Mon Sep 17 00:00:00 2001 From: Samandar Azamov <162783893+JscorpTech@users.noreply.github.com> Date: Mon, 21 Apr 2025 20:23:41 +0500 Subject: [PATCH 8/8] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..54f1b33 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Samandar Azamov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.