diff --git a/.woodpecker/notification.yml b/.woodpecker/notification.yml index db4133c..1a586f2 100644 --- a/.woodpecker/notification.yml +++ b/.woodpecker/notification.yml @@ -5,6 +5,7 @@ matrix: NOTIFICATION_MONGO_SECRET_PATH: sendico/db NOTIFICATION_MAIL_SECRET_PATH: sendico/notification/mail NOTIFICATION_API_SECRET_PATH: sendico/api/endpoint + NOTIFICATION_TELEGRAM_SECRET_PATH: sendico/notification/telegram NOTIFICATION_ENV: prod when: diff --git a/api/notification/config.yml b/api/notification/config.yml index bbbd57b..7e7c8d7 100755 --- a/api/notification/config.yml +++ b/api/notification/config.yml @@ -53,14 +53,21 @@ api: password_env: MAIL_SECRET host: "smtp.mail.ru" port: 465 - from: "MeetX Tech" + from: "Sendico Tech" network_timeout: 10 + telegram: + bot_token_env: TELEGRAM_BOT_TOKEN + chat_id_env: TELEGRAM_CHAT_ID + thread_id_env: TELEGRAM_THREAD_ID + api_url: "https://api.telegram.org" + timeout_seconds: 10 + parse_mode: "" localizer: path: "./i18n" languages: ["en", "ru", "uk"] service_name: "Sendico" - support: "support@meetx.space" + support: "support@sendico.io" app: @@ -82,4 +89,4 @@ database: collection_name_env: PERMISSION_COLLECTION database_name_env: MONGO_DATABASE timeout_seconds_env: PERMISSION_TIMEOUT - is_filtered_env: PERMISSION_IS_FILTERED \ No newline at end of file + is_filtered_env: PERMISSION_IS_FILTERED diff --git a/api/notification/interface/services/notification/config/config.go b/api/notification/interface/services/notification/config/config.go index 2e34fd9..61d576c 100644 --- a/api/notification/interface/services/notification/config/config.go +++ b/api/notification/interface/services/notification/config/config.go @@ -1,6 +1,16 @@ package notificationimp type Config struct { - Driver string `yaml:"driver"` - Settings map[string]any `yaml:"settings,omitempty"` + Driver string `yaml:"driver"` + Settings map[string]any `yaml:"settings,omitempty"` + Telegram *TelegramConfig `yaml:"telegram"` +} + +type TelegramConfig struct { + BotTokenEnv string `yaml:"bot_token_env"` + ChatIDEnv string `yaml:"chat_id_env"` + ThreadIDEnv string `yaml:"thread_id_env,omitempty"` + APIURL string `yaml:"api_url,omitempty"` + ParseMode string `yaml:"parse_mode,omitempty"` + TimeoutSeconds int `yaml:"timeout_seconds"` } diff --git a/api/notification/internal/server/notificationimp/notification.go b/api/notification/internal/server/notificationimp/notification.go index e6d8f5b..35913b4 100644 --- a/api/notification/internal/server/notificationimp/notification.go +++ b/api/notification/internal/server/notificationimp/notification.go @@ -2,13 +2,17 @@ package notificationimp import ( "context" + "fmt" "github.com/tech/sendico/notification/interface/api" mmail "github.com/tech/sendico/notification/internal/server/notificationimp/mail" + "github.com/tech/sendico/notification/internal/server/notificationimp/telegram" "github.com/tech/sendico/pkg/domainprovider" na "github.com/tech/sendico/pkg/messaging/notifications/account" ni "github.com/tech/sendico/pkg/messaging/notifications/invitation" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" "go.uber.org/zap" ) @@ -17,6 +21,7 @@ type NotificationAPI struct { logger mlogger.Logger client mmail.Client dp domainprovider.DomainProvider + tg telegram.Client } func (a *NotificationAPI) Name() mservice.Type { @@ -33,11 +38,22 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { } p.logger = a.Logger().Named(p.Name()) + if a.Config().Notification == nil { + return nil, fmt.Errorf("notification configuration is missing") + } + if a.Config().Notification.Telegram == nil { + return nil, fmt.Errorf("telegram configuration is missing") + } + var err error if p.client, err = mmail.CreateMailClient(p.logger.Named("mailer"), p.Name(), a.Register().Producer(), a.Localizer(), a.DomainProvider(), a.Config().Notification); err != nil { p.logger.Error("Failed to create mail connection", zap.Error(err), zap.String("driver", a.Config().Notification.Driver)) return nil, err } + if p.tg, err = telegram.NewClient(p.logger.Named("telegram"), a.Config().Notification.Telegram); err != nil { + p.logger.Error("Failed to create telegram client", zap.Error(err)) + return nil, err + } db, err := a.DBFactory().NewAccountDB() if err != nil { @@ -64,5 +80,22 @@ func CreateAPI(a api.API) (*NotificationAPI, error) { return nil, err } + if err := a.Register().Consumer(snotifications.NewDemoRequestProcessor(p.logger, p.onDemoRequest)); err != nil { + p.logger.Error("Failed to register demo request handler", zap.Error(err)) + return nil, err + } + return p, nil } + +func (a *NotificationAPI) onDemoRequest(ctx context.Context, request *model.DemoRequest) error { + if a.tg == nil { + return fmt.Errorf("telegram client is not configured") + } + if err := a.tg.SendDemoRequest(ctx, request); err != nil { + a.logger.Warn("Failed to send demo request via telegram", zap.Error(err)) + return err + } + a.logger.Info("Demo request sent via Telegram", zap.String("name", request.Name), zap.String("organization", request.OrganizationName)) + return nil +} diff --git a/api/notification/internal/server/notificationimp/telegram/client.go b/api/notification/internal/server/notificationimp/telegram/client.go new file mode 100644 index 0000000..d91c7b9 --- /dev/null +++ b/api/notification/internal/server/notificationimp/telegram/client.go @@ -0,0 +1,149 @@ +package telegram + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + "time" + + notconfig "github.com/tech/sendico/notification/interface/services/notification/config" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" +) + +const defaultAPIURL = "https://api.telegram.org" + +type Client interface { + SendDemoRequest(ctx context.Context, request *model.DemoRequest) error +} + +type client struct { + logger mlogger.Logger + httpClient *http.Client + apiURL string + botToken string + chatID string + threadID *int64 + parseMode string +} + +type sendMessagePayload struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` + ThreadID *int64 `json:"message_thread_id,omitempty"` + DisablePreview bool `json:"disable_web_page_preview,omitempty"` + DisableNotify bool `json:"disable_notification,omitempty"` + ProtectContent bool `json:"protect_content,omitempty"` +} + +func NewClient(logger mlogger.Logger, cfg *notconfig.TelegramConfig) (Client, error) { + if cfg == nil { + return nil, fmt.Errorf("telegram configuration is not provided") + } + token := strings.TrimSpace(os.Getenv(cfg.BotTokenEnv)) + if token == "" { + return nil, fmt.Errorf("telegram bot token env %s is empty", cfg.BotTokenEnv) + } + chatID := strings.TrimSpace(os.Getenv(cfg.ChatIDEnv)) + if chatID == "" { + return nil, fmt.Errorf("telegram chat id env %s is empty", cfg.ChatIDEnv) + } + + var threadID *int64 + if env := strings.TrimSpace(cfg.ThreadIDEnv); env != "" { + raw := strings.TrimSpace(os.Getenv(env)) + if raw != "" { + val, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return nil, fmt.Errorf("telegram thread id env %s is invalid: %w", env, err) + } + threadID = &val + } + } + + timeout := time.Duration(cfg.TimeoutSeconds) * time.Second + if timeout <= 0 { + timeout = 10 * time.Second + } + + apiURL := strings.TrimSpace(cfg.APIURL) + if apiURL == "" { + apiURL = defaultAPIURL + } + + return &client{ + logger: logger.Named("telegram"), + httpClient: &http.Client{ + Timeout: timeout, + }, + apiURL: strings.TrimRight(apiURL, "/"), + botToken: token, + chatID: chatID, + threadID: threadID, + parseMode: strings.TrimSpace(cfg.ParseMode), + }, nil +} + +func (c *client) SendDemoRequest(ctx context.Context, request *model.DemoRequest) error { + if request == nil { + return fmt.Errorf("demo request payload is nil") + } + message := buildMessage(request) + payload := sendMessagePayload{ + ChatID: c.chatID, + Text: message, + ParseMode: c.parseMode, + ThreadID: c.threadID, + DisablePreview: true, + } + return c.sendMessage(ctx, payload) +} + +func (c *client) sendMessage(ctx context.Context, payload sendMessagePayload) error { + body, err := json.Marshal(&payload) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint(), bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + return nil + } + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) + return fmt.Errorf("telegram sendMessage failed with status %d: %s", resp.StatusCode, string(respBody)) +} + +func (c *client) endpoint() string { + return fmt.Sprintf("%s/bot%s/sendMessage", c.apiURL, c.botToken) +} + +func buildMessage(req *model.DemoRequest) string { + var builder strings.Builder + builder.WriteString("New demo request received\n") + builder.WriteString(fmt.Sprintf("Name: %s\n", req.Name)) + builder.WriteString(fmt.Sprintf("Organization: %s\n", req.OrganizationName)) + builder.WriteString(fmt.Sprintf("Phone: %s\n", req.Phone)) + builder.WriteString(fmt.Sprintf("Work email: %s\n", req.WorkEmail)) + builder.WriteString(fmt.Sprintf("Payout volume: %s\n", req.PayoutVolume)) + if req.Comment != "" { + builder.WriteString(fmt.Sprintf("Comment: %s\n", req.Comment)) + } + return builder.String() +} diff --git a/api/pkg/messaging/internal/notifications/site/notification.go b/api/pkg/messaging/internal/notifications/site/notification.go new file mode 100644 index 0000000..0225c23 --- /dev/null +++ b/api/pkg/messaging/internal/notifications/site/notification.go @@ -0,0 +1,47 @@ +package notifications + +import ( + "fmt" + + gmessaging "github.com/tech/sendico/pkg/generated/gmessaging" + messaging "github.com/tech/sendico/pkg/messaging/envelope" + "github.com/tech/sendico/pkg/model" + nm "github.com/tech/sendico/pkg/model/notification" + "github.com/tech/sendico/pkg/mservice" + "google.golang.org/protobuf/proto" +) + +type DemoRequestNotification struct { + messaging.Envelope + request *model.DemoRequest +} + +func (drn *DemoRequestNotification) Serialize() ([]byte, error) { + if drn.request == nil { + return nil, fmt.Errorf("demo request payload is empty") + } + msg := gmessaging.DemoRequestEvent{ + Name: drn.request.Name, + OrganizationName: drn.request.OrganizationName, + Phone: drn.request.Phone, + WorkEmail: drn.request.WorkEmail, + PayoutVolume: drn.request.PayoutVolume, + Comment: drn.request.Comment, + } + data, err := proto.Marshal(&msg) + if err != nil { + return nil, err + } + return drn.Envelope.Wrap(data) +} + +func NewDemoRequestEvent() model.NotificationEvent { + return model.NewNotification(mservice.Site, nm.NACreated) +} + +func NewDemoRequestEnvelope(sender string, request *model.DemoRequest) messaging.Envelope { + return &DemoRequestNotification{ + Envelope: messaging.CreateEnvelope(sender, NewDemoRequestEvent()), + request: request, + } +} diff --git a/api/pkg/messaging/notifications/site/demo_request.go b/api/pkg/messaging/notifications/site/demo_request.go new file mode 100644 index 0000000..bb21277 --- /dev/null +++ b/api/pkg/messaging/notifications/site/demo_request.go @@ -0,0 +1,11 @@ +package notifications + +import ( + messaging "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + "github.com/tech/sendico/pkg/model" +) + +func DemoRequest(sender string, request *model.DemoRequest) messaging.Envelope { + return internalsite.NewDemoRequestEnvelope(sender, request) +} diff --git a/api/pkg/messaging/notifications/site/handler/handler.go b/api/pkg/messaging/notifications/site/handler/handler.go new file mode 100644 index 0000000..ef1b3ef --- /dev/null +++ b/api/pkg/messaging/notifications/site/handler/handler.go @@ -0,0 +1,9 @@ +package notifications + +import ( + "context" + + "github.com/tech/sendico/pkg/model" +) + +type DemoRequestHandler = func(context.Context, *model.DemoRequest) error diff --git a/api/pkg/messaging/notifications/site/processor.go b/api/pkg/messaging/notifications/site/processor.go new file mode 100644 index 0000000..33e5eab --- /dev/null +++ b/api/pkg/messaging/notifications/site/processor.go @@ -0,0 +1,50 @@ +package notifications + +import ( + "context" + + gmessaging "github.com/tech/sendico/pkg/generated/gmessaging" + me "github.com/tech/sendico/pkg/messaging/envelope" + internalsite "github.com/tech/sendico/pkg/messaging/internal/notifications/site" + np "github.com/tech/sendico/pkg/messaging/notifications/processor" + handler "github.com/tech/sendico/pkg/messaging/notifications/site/handler" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" + "google.golang.org/protobuf/proto" +) + +type DemoRequestProcessor struct { + logger mlogger.Logger + handler handler.DemoRequestHandler + event model.NotificationEvent +} + +func (drp *DemoRequestProcessor) Process(ctx context.Context, envelope me.Envelope) error { + var msg gmessaging.DemoRequestEvent + if err := proto.Unmarshal(envelope.GetData(), &msg); err != nil { + drp.logger.Warn("Failed to decode demo request envelope", zap.Error(err), zap.String("topic", drp.event.ToString())) + return err + } + request := &model.DemoRequest{ + Name: msg.GetName(), + OrganizationName: msg.GetOrganizationName(), + Phone: msg.GetPhone(), + WorkEmail: msg.GetWorkEmail(), + PayoutVolume: msg.GetPayoutVolume(), + Comment: msg.GetComment(), + } + return drp.handler(ctx, request) +} + +func (drp *DemoRequestProcessor) GetSubject() model.NotificationEvent { + return drp.event +} + +func NewDemoRequestProcessor(logger mlogger.Logger, handler handler.DemoRequestHandler) np.EnvelopeProcessor { + return &DemoRequestProcessor{ + logger: logger.Named("demo_request_processor"), + handler: handler, + event: internalsite.NewDemoRequestEvent(), + } +} diff --git a/api/pkg/model/demorequest.go b/api/pkg/model/demorequest.go new file mode 100644 index 0000000..007b357 --- /dev/null +++ b/api/pkg/model/demorequest.go @@ -0,0 +1,53 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/merrors" +) + +// DemoRequest represents a request submitted from the marketing site to request a demo. +type DemoRequest struct { + Name string `json:"name"` + OrganizationName string `json:"organizationName"` + Phone string `json:"phone"` + WorkEmail string `json:"workEmail"` + PayoutVolume string `json:"payoutVolume"` + Comment string `json:"comment,omitempty"` +} + +// Normalize trims whitespace from all string fields. +func (dr *DemoRequest) Normalize() { + if dr == nil { + return + } + dr.Name = strings.TrimSpace(dr.Name) + dr.OrganizationName = strings.TrimSpace(dr.OrganizationName) + dr.Phone = strings.TrimSpace(dr.Phone) + dr.WorkEmail = strings.TrimSpace(dr.WorkEmail) + dr.PayoutVolume = strings.TrimSpace(dr.PayoutVolume) + dr.Comment = strings.TrimSpace(dr.Comment) +} + +// Validate ensures that all required fields are present. +func (dr *DemoRequest) Validate() error { + if dr == nil { + return merrors.InvalidArgument("request payload is empty") + } + if dr.Name == "" { + return merrors.InvalidArgument("name must not be empty") + } + if dr.OrganizationName == "" { + return merrors.InvalidArgument("organization name must not be empty") + } + if dr.Phone == "" { + return merrors.InvalidArgument("phone must not be empty") + } + if dr.WorkEmail == "" { + return merrors.InvalidArgument("work email must not be empty") + } + if dr.PayoutVolume == "" { + return merrors.InvalidArgument("payout volume must not be empty") + } + return nil +} diff --git a/api/pkg/model/demorequest_test.go b/api/pkg/model/demorequest_test.go new file mode 100644 index 0000000..428f386 --- /dev/null +++ b/api/pkg/model/demorequest_test.go @@ -0,0 +1,31 @@ +package model + +import "testing" + +func TestDemoRequestNormalizeAndValidate(t *testing.T) { + req := &DemoRequest{ + Name: " Alice ", + OrganizationName: " Sendico ", + Phone: " +1 234 ", + WorkEmail: " demo@sendico.io ", + PayoutVolume: " 100k ", + Comment: " Excited ", + } + + req.Normalize() + if err := req.Validate(); err != nil { + t.Fatalf("expected request to be valid, got error: %v", err) + } + + if req.Name != "Alice" || req.OrganizationName != "Sendico" || req.Phone != "+1 234" || req.WorkEmail != "demo@sendico.io" || req.PayoutVolume != "100k" || req.Comment != "Excited" { + t.Fatalf("normalize failed: %+v", req) + } +} + +func TestDemoRequestValidateMissing(t *testing.T) { + req := &DemoRequest{} + req.Normalize() + if err := req.Validate(); err == nil { + t.Fatalf("expected validation error for empty request") + } +} diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index 9154426..be4e30f 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -7,6 +7,7 @@ type Type = string const ( Accounts Type = "accounts" // Represents user accounts in the system Amplitude Type = "amplitude" // Represents analytics integration with Amplitude + Site Type = "site" // Represents public site endpoints Automations Type = "automation" // Represents automation workflows Changes Type = "changes" // Tracks changes made to resources Clients Type = "clients" // Represents client information @@ -59,7 +60,7 @@ const ( func StringToSType(s string) (Type, error) { switch Type(s) { - case Accounts, Amplitude, Automations, Changes, Clients, Comments, ChainGateway, ChainWallets, ChainWalletBalances, + case Accounts, Amplitude, Site, Automations, Changes, Clients, Comments, ChainGateway, ChainWallets, ChainWalletBalances, ChainTransfers, ChainDeposits, FXOracle, FeePlans, FilterProjects, Invitations, Invoices, Logo, Ledger, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications, Organizations, Payments, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, Priorities, diff --git a/api/proto/demo_request.proto b/api/proto/demo_request.proto new file mode 100644 index 0000000..68aeea5 --- /dev/null +++ b/api/proto/demo_request.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +option go_package = "github.com/tech/sendico/pkg/generated/gmessaging"; + +message DemoRequestEvent { + string Name = 1; + string OrganizationName = 2; + string Phone = 3; + string WorkEmail = 4; + string PayoutVolume = 5; + string Comment = 6; +} diff --git a/api/server/interface/services/site/site.go b/api/server/interface/services/site/site.go new file mode 100644 index 0000000..5b1ff53 --- /dev/null +++ b/api/server/interface/services/site/site.go @@ -0,0 +1,11 @@ +package site + +import ( + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "github.com/tech/sendico/server/internal/server/siteimp" +) + +func Create(a eapi.API) (mservice.MicroService, error) { + return siteimp.CreateAPI(a) +} diff --git a/api/server/internal/api/api.go b/api/server/internal/api/api.go index 6a255ec..822e514 100644 --- a/api/server/internal/api/api.go +++ b/api/server/internal/api/api.go @@ -16,6 +16,7 @@ import ( "github.com/tech/sendico/server/interface/services/logo" "github.com/tech/sendico/server/interface/services/organization" "github.com/tech/sendico/server/interface/services/permission" + "github.com/tech/sendico/server/interface/services/site" "go.uber.org/zap" ) @@ -79,6 +80,7 @@ func (a *APIImp) installServices() error { srvf = append(srvf, invitation.Create) srvf = append(srvf, logo.Create) srvf = append(srvf, permission.Create) + srvf = append(srvf, site.Create) for _, v := range srvf { if err := a.addMicroservice(v); err != nil { diff --git a/api/server/internal/server/siteimp/service.go b/api/server/internal/server/siteimp/service.go new file mode 100644 index 0000000..fce8434 --- /dev/null +++ b/api/server/internal/server/siteimp/service.go @@ -0,0 +1,60 @@ +package siteimp + +import ( + "context" + "encoding/json" + "net/http" + + api "github.com/tech/sendico/pkg/api/http" + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/messaging" + snotifications "github.com/tech/sendico/pkg/messaging/notifications/site" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + eapi "github.com/tech/sendico/server/interface/api" + "go.uber.org/zap" +) + +type SiteAPI struct { + logger mlogger.Logger + producer messaging.Producer +} + +func (a *SiteAPI) Name() mservice.Type { + return mservice.Site +} + +func (a *SiteAPI) Finish(_ context.Context) error { + return nil +} + +func (a *SiteAPI) demoRequest(r *http.Request) http.HandlerFunc { + var request model.DemoRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + a.logger.Warn("Failed to decode demo request payload", zap.Error(err)) + return response.BadRequest(a.logger, a.Name(), "invalid_payload", "Failed to decode demo request payload") + } + request.Normalize() + if err := request.Validate(); err != nil { + a.logger.Warn("Demo request validation failed", zap.Error(err)) + return response.BadPayload(a.logger, a.Name(), err) + } + + if err := a.producer.SendMessage(snotifications.DemoRequest(a.Name(), &request)); err != nil { + a.logger.Warn("Failed to enqueue demo request notification", zap.Error(err)) + return response.Internal(a.logger, a.Name(), err) + } + + return response.Accepted(a.logger, map[string]string{"status": "queued"}) +} + +func CreateAPI(a eapi.API) (*SiteAPI, error) { + p := &SiteAPI{ + logger: a.Logger().Named(mservice.Site), + producer: a.Register().Messaging().Producer(), + } + + a.Register().Handler(mservice.Site, "/demo/request", api.Post, p.demoRequest) + return p, nil +} diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 497b2ad..13712ad 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -15,9 +15,9 @@ PERMISSION_IS_FILTERED=false AMPLI_ENVIRONMENT=production API_PROTOCOL=https SERVICE_HOST=app.sendico.io -API_ENDPOINT=https://app.sendico.io/api +API_ENDPOINT=/api/v1 WS_PROTOCOL=wss -WS_ENDPOINT=wss://app.sendico.io/ws +WS_ENDPOINT=/ws AMPLITUDE_SECRET=c3d75b3e2520d708440acbb16b923e79 DEFAULT_LOCALE=en DEFAULT_CURRENCY=EUR diff --git a/ci/prod/compose/notification.yml b/ci/prod/compose/notification.yml index 391ba94..2ad532b 100644 --- a/ci/prod/compose/notification.yml +++ b/ci/prod/compose/notification.yml @@ -31,6 +31,9 @@ services: NATS_URL: ${NATS_URL} MAIL_USER: ${MAIL_USER} MAIL_SECRET: ${MAIL_SECRET} + TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} + TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID} + TELEGRAM_THREAD_ID: ${TELEGRAM_THREAD_ID} MONGO_HOST: ${MONGO_HOST} MONGO_PORT: ${MONGO_PORT} MONGO_DATABASE: ${MONGO_DATABASE} diff --git a/ci/prod/scripts/deploy/notification.sh b/ci/prod/scripts/deploy/notification.sh index 8e5ab99..5f9a75a 100755 --- a/ci/prod/scripts/deploy/notification.sh +++ b/ci/prod/scripts/deploy/notification.sh @@ -24,6 +24,8 @@ REQUIRED_SECRETS=( NATS_USER NATS_PASSWORD NATS_URL + TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID ) for var in "${REQUIRED_SECRETS[@]}"; do @@ -50,6 +52,9 @@ API_ENDPOINT_SECRET_B64="$(b64enc "${API_ENDPOINT_SECRET}")" NATS_USER_B64="$(b64enc "${NATS_USER}")" NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")" NATS_URL_B64="$(b64enc "${NATS_URL}")" +TELEGRAM_BOT_TOKEN_B64="$(b64enc "${TELEGRAM_BOT_TOKEN}")" +TELEGRAM_CHAT_ID_B64="$(b64enc "${TELEGRAM_CHAT_ID}")" +TELEGRAM_THREAD_ID_B64="$(b64enc "${TELEGRAM_THREAD_ID:-}")" SSH_OPTS=( -i /root/.ssh/id_rsa @@ -86,6 +91,9 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ NATS_USER_B64="$NATS_USER_B64" \ NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \ NATS_URL_B64="$NATS_URL_B64" \ + TELEGRAM_BOT_TOKEN_B64="$TELEGRAM_BOT_TOKEN_B64" \ + TELEGRAM_CHAT_ID_B64="$TELEGRAM_CHAT_ID_B64" \ + TELEGRAM_THREAD_ID_B64="$TELEGRAM_THREAD_ID_B64" \ bash -s <<'EOSSH' set -euo pipefail cd "${REMOTE_DIR}/compose" @@ -135,10 +143,14 @@ API_ENDPOINT_SECRET="$(decode_b64 "$API_ENDPOINT_SECRET_B64")" NATS_USER="$(decode_b64 "$NATS_USER_B64")" NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")" NATS_URL="$(decode_b64 "$NATS_URL_B64")" +TELEGRAM_BOT_TOKEN="$(decode_b64 "$TELEGRAM_BOT_TOKEN_B64")" +TELEGRAM_CHAT_ID="$(decode_b64 "$TELEGRAM_CHAT_ID_B64")" +TELEGRAM_THREAD_ID="$(decode_b64 "$TELEGRAM_THREAD_ID_B64")" export MONGO_USER MONGO_PASSWORD export MAIL_USER MAIL_SECRET API_ENDPOINT_SECRET export NATS_USER NATS_PASSWORD NATS_URL +export TELEGRAM_BOT_TOKEN TELEGRAM_CHAT_ID TELEGRAM_THREAD_ID COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT" export COMPOSE_PROJECT_NAME read -r -a SERVICES <<<"${SERVICES_LINE}" diff --git a/ci/scripts/common/bump_version.sh b/ci/scripts/common/bump_version.sh index 50b1656..d8c6c7b 100755 --- a/ci/scripts/common/bump_version.sh +++ b/ci/scripts/common/bump_version.sh @@ -4,7 +4,14 @@ set -eu START_DIR="$(pwd)" echo "[bump-version] invoked from ${START_DIR}" -REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="" +if command -v git >/dev/null 2>&1; then + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" +fi +if [ -z "${REPO_ROOT}" ]; then + REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +fi echo "[bump-version] repo root resolved to ${REPO_ROOT}" cd "${REPO_ROOT}" @@ -60,4 +67,20 @@ if [ -z "${BRANCH}" ] || [ "${BRANCH}" = "HEAD" ]; then BRANCH="$(git rev-parse --abbrev-ref HEAD)" fi +NETRC_MACHINE="${CI_NETRC_MACHINE:-${WOODPECKER_NETRC_MACHINE:-}}" +NETRC_USERNAME="${CI_NETRC_USERNAME:-${WOODPECKER_NETRC_USERNAME:-${CI_NETRC_LOGIN:-${WOODPECKER_NETRC_LOGIN:-}}}}" +NETRC_PASSWORD="${CI_NETRC_PASSWORD:-${WOODPECKER_NETRC_PASSWORD:-}}" +if [ -n "${NETRC_MACHINE}" ] && [ -n "${NETRC_USERNAME}" ] && [ -n "${NETRC_PASSWORD}" ]; then + NETRC_FILE="${HOME:-/root}/.netrc" + if [ ! -f "${NETRC_FILE}" ]; then + { + printf 'machine %s\n' "${NETRC_MACHINE}" + printf 'login %s\n' "${NETRC_USERNAME}" + printf 'password %s\n' "${NETRC_PASSWORD}" + } > "${NETRC_FILE}" + chmod 600 "${NETRC_FILE}" + echo "[bump-version] wrote credentials for ${NETRC_MACHINE}" + fi +fi + git push origin "HEAD:${BRANCH}" diff --git a/ci/scripts/notification/deploy.sh b/ci/scripts/notification/deploy.sh index 4b97613..b354a66 100755 --- a/ci/scripts/notification/deploy.sh +++ b/ci/scripts/notification/deploy.sh @@ -49,6 +49,7 @@ load_env_file ./.env.version NOTIFICATION_MONGO_SECRET_PATH="${NOTIFICATION_MONGO_SECRET_PATH:?missing NOTIFICATION_MONGO_SECRET_PATH}" NOTIFICATION_MAIL_SECRET_PATH="${NOTIFICATION_MAIL_SECRET_PATH:?missing NOTIFICATION_MAIL_SECRET_PATH}" NOTIFICATION_API_SECRET_PATH="${NOTIFICATION_API_SECRET_PATH:?missing NOTIFICATION_API_SECRET_PATH}" +NOTIFICATION_TELEGRAM_SECRET_PATH="${NOTIFICATION_TELEGRAM_SECRET_PATH:?missing NOTIFICATION_TELEGRAM_SECRET_PATH}" : "${NATS_HOST:?missing NATS_HOST}" : "${NATS_PORT:?missing NATS_PORT}" @@ -60,6 +61,14 @@ export MAIL_SECRET="$(./ci/vlt kv_get kv "${NOTIFICATION_MAIL_SECRET_PATH}" pass export API_ENDPOINT_SECRET="$(./ci/vlt kv_get kv "${NOTIFICATION_API_SECRET_PATH}" secret)" +export TELEGRAM_BOT_TOKEN="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" bot_token)" +export TELEGRAM_CHAT_ID="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" chat_id)" +TELEGRAM_THREAD_ID="" +if TELEGRAM_THREAD_ID_VALUE="$(./ci/vlt kv_get kv "${NOTIFICATION_TELEGRAM_SECRET_PATH}" thread_id 2>/dev/null)"; then + TELEGRAM_THREAD_ID="$TELEGRAM_THREAD_ID_VALUE" +fi +export TELEGRAM_THREAD_ID + export NATS_USER="$(./ci/vlt kv_get kv sendico/nats user)" export NATS_PASSWORD="$(./ci/vlt kv_get kv sendico/nats password)" export NATS_URL="nats://${NATS_USER}:${NATS_PASSWORD}@${NATS_HOST}:${NATS_PORT}"