diff --git a/go.mod b/go.mod index 3943989d..b675aab5 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/refraction-networking/utls v1.6.7 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/rvinnie/yookassa-sdk-go v0.0.0-20240629113713-dfd7cc31b343 // indirect github.com/sagernet/sing v0.4.3 // indirect github.com/sagernet/sing-shadowsocks v0.2.7 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect diff --git a/go.sum b/go.sum index 03b50bf1..d5cb682b 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rvinnie/yookassa-sdk-go v0.0.0-20240629113713-dfd7cc31b343 h1:oVZiMHABPKz0OZgx/h1BxMNkRFK4DOBdDpSrMebYmsI= +github.com/rvinnie/yookassa-sdk-go v0.0.0-20240629113713-dfd7cc31b343/go.mod h1:flatybkcu+7YLaB7mMnj9JTNKeim4jZ+ZrXNFjVA0pA= github.com/sagernet/sing v0.4.3 h1:Ty/NAiNnVd6844k7ujlL5lkzydhcTH5Psc432jXA4Y8= github.com/sagernet/sing v0.4.3/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= diff --git a/web/service/setting.go b/web/service/setting.go index e3ea3ece..7d44b26f 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -68,6 +68,14 @@ var defaultValueMap = map[string]string{ "subJsonRules": "", "datepicker": "gregorian", "warp": "", + "price1": "500.0", + "price2": "1400.0", + "price3": "2500.0", + "price4": "4000.0", + "month1": "1", + "month2": "3", + "month3": "6", + "month4": "12", } type SettingService struct{} @@ -231,6 +239,18 @@ func (s *SettingService) getInt(key string) (int, error) { return strconv.Atoi(str) } +func (s *SettingService) getFloat64(key string) (float64, error) { + str, err := s.getString(key) + if err != nil { + return 0, err + } + return strconv.ParseFloat(str, 64) +} + +func (s *SettingService) setFloat64(key string, value float64) error { + return s.setString(key, strconv.FormatFloat(value, 'f', -1, 64)) +} + func (s *SettingService) setInt(key string, value int) error { return s.setString(key, strconv.Itoa(value)) } @@ -480,6 +500,130 @@ func (s *SettingService) GetSubJsonMux() (string, error) { return s.getString("subJsonMux") } +func (s *SettingService) GetFirstPriceFloat() (float64, error) { + return s.getFloat64("price1") +} + +func (s *SettingService) GetSecondPriceFloat() (float64, error) { + return s.getFloat64("price2") +} + +func (s *SettingService) GetThirdPriceFloat() (float64, error) { + return s.getFloat64("price3") +} + +func (s *SettingService) GetFourthPriceFloat() (float64, error) { + return s.getFloat64("price4") +} + +func (s *SettingService) GetFirstPriceString() (string, error) { + return s.getString("price1") +} + +func (s *SettingService) GetSecondPriceString() (string, error) { + return s.getString("price2") +} + +func (s *SettingService) GetThirdPriceString() (string, error) { + return s.getString("price3") +} + +func (s *SettingService) GetFourthPriceString() (string, error) { + return s.getString("price4") +} + +func (s *SettingService) GetAllPricesString() (string, string, string, string, error) { + price1, err := s.GetFirstPriceString() + if err != nil { + return "", "", "", "", err + } + price2, err := s.GetSecondPriceString() + if err != nil { + return "", "", "", "", err + } + price3, err := s.GetThirdPriceString() + if err != nil { + return "", "", "", "", err + } + price4, err := s.GetFourthPriceString() + if err != nil { + return "", "", "", "", err + } + return price1, price2, price3, price4, nil +} + +func (s *SettingService) GetAllPricesFloat() (float64, float64, float64, float64, error) { + price1, err := s.GetFirstPriceFloat() + if err != nil { + return 0, 0, 0, 0, err + } + price2, err := s.GetSecondPriceFloat() + if err != nil { + return 0, 0, 0, 0, err + } + price3, err := s.GetThirdPriceFloat() + if err != nil { + return 0, 0, 0, 0, err + } + price4, err := s.GetFourthPriceFloat() + if err != nil { + return 0, 0, 0, 0, err + } + return price1, price2, price3, price4, nil +} + +func (s *SettingService) GetFirstMonthString() (string, error) { + return s.getString("month1") +} + +func (s *SettingService) GetSecondMonthString() (string, error) { + return s.getString("month2") +} + +func (s *SettingService) GetThirdMonthString() (string, error) { + return s.getString("month3") +} + +func (s *SettingService) GetFourthMonthString() (string, error) { + return s.getString("month4") +} + +func (s *SettingService) GetAllMonthsString() (string, string, string, string, error) { + month1, err := s.GetFirstMonthString() + if err != nil { + return "", "", "", "", err + } + month2, err := s.GetSecondMonthString() + if err != nil { + return "", "", "", "", err + } + month3, err := s.GetThirdMonthString() + if err != nil { + return "", "", "", "", err + } + month4, err := s.GetFourthMonthString() + if err != nil { + return "", "", "", "", err + } + return month1, month2, month3, month4, nil +} + +func (s *SettingService) GetFirstMonthInt() (int, error) { + return s.getInt("month1") +} + +func (s *SettingService) GetSecondMonthInt() (int, error) { + return s.getInt("month2") +} + +func (s *SettingService) GetThirdMonthInt() (int, error) { + return s.getInt("month3") +} + +func (s *SettingService) GetFourthMonthInt() (int, error) { + return s.getInt("month4") +} + func (s *SettingService) GetSubJsonRules() (string, error) { return s.getString("subJsonRules") } diff --git a/web/service/tgbot.go b/web/service/tgbot.go index bce7e2a6..d07bf498 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -881,6 +881,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.clientUsage")) t.getClientUsage(chatId, tgUserID) case "admin_help": + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.adminContact")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.adminContact")) case "client_commands": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) @@ -894,6 +895,38 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool case "commands": t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) + case "payments": + price1, price2, price3, price4, err := t.settingService.GetAllPricesString() + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + month1, month2, month3, month4, err := t.settingService.GetAllMonthsString() + if err != nil { + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) + return + } + + t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.payments")) + t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.payments", + "Price1=="+price1, + "Month1=="+month1, + "Price2=="+price2, + "Month2=="+month2, + "Price3=="+price3, + "Month3=="+month3, + "Price4=="+price4, + "Month4=="+month4), + tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(price1).WithCallbackData(t.encodeQuery("first_price_payment")), + tu.InlineKeyboardButton(price2).WithCallbackData(t.encodeQuery("second_price_payment")), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(price3).WithCallbackData(t.encodeQuery("third_price_payment")), + tu.InlineKeyboardButton(price4).WithCallbackData(t.encodeQuery("fourth_price_payment")), + ), + )) } } @@ -933,6 +966,12 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) { ), ) + if t.isClientExist(chatId) { + numericKeyboardClient.InlineKeyboard = append(numericKeyboardClient.InlineKeyboard, tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.payments")).WithCallbackData(t.encodeQuery("payments")), + )) + } + var ReplyMarkup telego.ReplyMarkup if isAdmin { ReplyMarkup = numericKeyboard @@ -1457,6 +1496,17 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... } } +func (t *Tgbot) isClientExist(tgUserID int64) bool { + traffic, err := t.inboundService.GetClientTrafficTgBot(tgUserID) + if err != nil { + return false + } + if len(traffic) > 0 { + return true + } + return false +} + func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { traffic, err := t.inboundService.GetClientTrafficByEmail(email) if err != nil { diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 4c77d2a8..9a1db21f 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -573,6 +573,7 @@ "limitTraffic" = "🚧 Π›ΠΈΠΌΠΈΡ‚ Ρ‚Ρ€Π°Ρ„ΠΈΠΊΠ°" "getBanLogs" = "Π›ΠΎΠ³ΠΈ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΎΠΊ" "allClients" = "ВсС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρ‹" +"payments" = "ΠŸΡ€ΠΎΠ΄Π»ΠΈΡ‚ΡŒ подписку" [tgbot.answers] "successfulOperation" = "βœ… Π£ΡΠΏΠ΅ΡˆΠ½Ρ‹ΠΉ!" @@ -596,4 +597,5 @@ "askToAddUserId" = "Π’Π°ΡˆΠ° конфигурация Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½Π°!\r\nΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, попроситС администратора ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ваш ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Telegram Π² Π²Π°ΡˆΠΈΡ… конфигурациях.\r\n\r\nΠ’Π°Ρˆ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ: {{ .TgUserID }}" "chooseClient" = "Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ для ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ {{ .Inbound }}" "chooseInbound" = "Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅" -"adminContact" = "Если Π²Π°ΠΌ Π½ΡƒΠΆΠ½Π° ΠΏΠΎΠΌΠΎΡ‰ΡŒ, ΡΠ²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором: @sainthrill" \ No newline at end of file +"adminContact" = "Если Π²Π°ΠΌ Π½ΡƒΠΆΠ½Π° ΠΏΠΎΠΌΠΎΡ‰ΡŒ, ΡΠ²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором: @sainthrill" +"payments" = "Π’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ ΠΎΠΏΠ»Π°Ρ‚Ρ‹ подписки:\r\nπŸ’΅ {{ .Price1 }} Π·Π° {{ .Month1 }} мСсяцСв\r\nπŸ’Ά {{ .Price2 }} Π·Π° {{ .Month2 }} мСсяцСв\r\nπŸ’· {{ .Price3 }} Π·Π° {{ .Month3 }} мСсяцСв\r\nπŸ’° {{ .Price4 }} Π·Π° {{ .Month4 }} мСсяцСв" \ No newline at end of file diff --git a/web/yookassa/yookassa.go b/web/yookassa/yookassa.go new file mode 100644 index 00000000..a05c70e6 --- /dev/null +++ b/web/yookassa/yookassa.go @@ -0,0 +1,105 @@ +package yookassa + +import ( + "fmt" + + "github.com/mymmrac/telego" + "github.com/rvinnie/yookassa-sdk-go/yookassa" + yoocommon "github.com/rvinnie/yookassa-sdk-go/yookassa/common" + yoopayment "github.com/rvinnie/yookassa-sdk-go/yookassa/payment" + + "x-ui/logger" +) + +type YooKassa struct { + bot *telego.Bot + providerToken string + kassa *yookassa.Client + returnURL string +} + +func New(bot *telego.Bot, providerToken, accountID, secretkey, returnURL string) *YooKassa { + return &YooKassa{ + bot: bot, + providerToken: providerToken, + kassa: yookassa.NewClient(accountID, secretkey), + returnURL: returnURL, + } +} + +func (k *YooKassa) SendInvoice(chatID telego.ChatID, title, description string, price int) error { + _, err := k.bot.SendInvoice(&telego.SendInvoiceParams{ + ChatID: chatID, + Title: title, + Description: description, + Payload: "UNIQUE_PAYLOAD", + ProviderToken: k.providerToken, + Currency: "RUB", + Prices: []telego.LabeledPrice{ + {Label: title, Amount: price}, + }, + }) + if err != nil { + logger.Warning("error occurred while sending invoice", err) + return err + } + + return nil +} + +func (k *YooKassa) CreatePayment(amount float64, description, paymentMethod string, userID int64) (*yoopayment.Payment, error) { + paymentHandler := yookassa.NewPaymentHandler(k.kassa) + + newPayment, err := paymentHandler.CreatePayment( + &yoopayment.Payment{ + Amount: &yoocommon.Amount{ + Value: fmt.Sprintf("%.2f", amount), + Currency: "RUB", + }, + PaymentMethod: yoopayment.PaymentMethodType(paymentMethod), + Confirmation: yoopayment.Redirect{ + Type: "redirect", + ReturnURL: k.returnURL, + }, + Description: description, + }) + if err != nil { + logger.Warning("error occurred while creating payment", err) + return nil, err + } + + return newPayment, nil +} + +func (k *YooKassa) ConfirmPayment(payment *yoopayment.Payment) error { + paymentHandler := yookassa.NewPaymentHandler(k.kassa) + + confirmedPayment, err := paymentHandler.CapturePayment(payment) + if err != nil { + return err + } + + if confirmedPayment.Status != yoopayment.Succeeded { + return fmt.Errorf("payment status is not succeeded") + } + + return nil +} + +func (k *YooKassa) CancelPayment(paymentID string) error { + paymentHandler := yookassa.NewPaymentHandler(k.kassa) + + _, err := paymentHandler.CancelPayment(paymentID) + return err +} + +func (k *YooKassa) GetPaymentInfo(paymentID string) (*yoopayment.Payment, error) { + paymentHandler := yookassa.NewPaymentHandler(k.kassa) + + paymentInfo, err := paymentHandler.FindPayment(paymentID) + if err != nil { + return nil, err + } + + return paymentInfo, nil +}