Add user creation functionality and update commands in main.go and README.md
Publish telemt-bot Docker image / build-and-push (push) Successful in 52s
Publish telemt-bot Docker image / build-and-push (push) Successful in 52s
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
- `/health` -> `GET /v1/health`
|
- `/health` -> `GET /v1/health`
|
||||||
- `/summary` -> `GET /v1/stats/summary`
|
- `/summary` -> `GET /v1/stats/summary`
|
||||||
- `/users` -> `GET /v1/stats/users` (первые 20 строк)
|
- `/users` -> `GET /v1/stats/users` (первые 20 строк)
|
||||||
|
- `/create_user <username>` -> `POST /v1/users` (создание пользователя + ссылки на подключение)
|
||||||
|
|
||||||
Команды обрабатываются только от одного админ-чата (`TELEGRAM_ADMIN_CHAT_ID`).
|
Команды обрабатываются только от одного админ-чата (`TELEGRAM_ADMIN_CHAT_ID`).
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,22 @@ type userInfo struct {
|
|||||||
CurrentConnections uint64 `json:"current_connections"`
|
CurrentConnections uint64 `json:"current_connections"`
|
||||||
ActiveUniqueIPs uint64 `json:"active_unique_ips"`
|
ActiveUniqueIPs uint64 `json:"active_unique_ips"`
|
||||||
TotalOctets uint64 `json:"total_octets"`
|
TotalOctets uint64 `json:"total_octets"`
|
||||||
|
Links userLinks `json:"links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userLinks struct {
|
||||||
|
Classic []string `json:"classic"`
|
||||||
|
Secure []string `json:"secure"`
|
||||||
|
TLS []string `json:"tls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type createUserRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type createUserResponse struct {
|
||||||
|
User userInfo `json:"user"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -253,13 +269,15 @@ func (b *bot) handleMessage(ctx context.Context, msg *tgMessage) error {
|
|||||||
}
|
}
|
||||||
switch command {
|
switch command {
|
||||||
case "/start", "/help":
|
case "/start", "/help":
|
||||||
return b.sendMessage(ctx, chatID, "Команды:\n/health\n/summary\n/users")
|
return b.sendMessage(ctx, chatID, "Команды:\n/health\n/summary\n/users\n/create_user <username>")
|
||||||
case "/health":
|
case "/health":
|
||||||
return b.handleHealth(ctx, chatID)
|
return b.handleHealth(ctx, chatID)
|
||||||
case "/summary":
|
case "/summary":
|
||||||
return b.handleSummary(ctx, chatID)
|
return b.handleSummary(ctx, chatID)
|
||||||
case "/users":
|
case "/users":
|
||||||
return b.handleUsers(ctx, chatID)
|
return b.handleUsers(ctx, chatID)
|
||||||
|
case "/create_user", "/createuser":
|
||||||
|
return b.handleCreateUser(ctx, chatID, text)
|
||||||
default:
|
default:
|
||||||
return b.sendMessage(ctx, chatID, "Неизвестная команда. Используй /help")
|
return b.sendMessage(ctx, chatID, "Неизвестная команда. Используй /help")
|
||||||
}
|
}
|
||||||
@@ -315,14 +333,91 @@ func (b *bot) handleUsers(ctx context.Context, chatID int64) error {
|
|||||||
return b.sendMessage(ctx, chatID, strings.TrimRight(sb.String(), "\n"))
|
return b.sendMessage(ctx, chatID, strings.TrimRight(sb.String(), "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bot) handleCreateUser(ctx context.Context, chatID int64, text string) error {
|
||||||
|
parts := strings.Fields(text)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return b.sendMessage(ctx, chatID, "Использование: /create_user <username>")
|
||||||
|
}
|
||||||
|
|
||||||
|
username := strings.TrimSpace(parts[1])
|
||||||
|
if !isValidUsername(username) {
|
||||||
|
return b.sendMessage(ctx, chatID, "Некорректный username. Разрешены [A-Za-z0-9_.-], длина 1..64.")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := createUserRequest{Username: username}
|
||||||
|
var created createUserResponse
|
||||||
|
if err := b.callTelemtJSON(ctx, http.MethodPost, "/v1/users", req, &created, http.StatusCreated); err != nil {
|
||||||
|
return b.sendMessage(ctx, chatID, "Ошибка создания пользователя: "+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("Пользователь создан: %s\nsecret: %s", created.User.Username, created.Secret)
|
||||||
|
if linksText := formatUserLinks(created.User.Links); linksText != "" {
|
||||||
|
msg = msg + "\n\nСсылки:\n" + linksText
|
||||||
|
}
|
||||||
|
return b.sendMessage(ctx, chatID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatUserLinks(links userLinks) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
appendLinks := func(title string, items []string) {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sb.WriteString(title)
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
for i := range items {
|
||||||
|
sb.WriteString("- ")
|
||||||
|
sb.WriteString(items[i])
|
||||||
|
sb.WriteByte('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLinks("classic:", links.Classic)
|
||||||
|
appendLinks("secure:", links.Secure)
|
||||||
|
appendLinks("tls:", links.TLS)
|
||||||
|
return strings.TrimRight(sb.String(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidUsername(v string) bool {
|
||||||
|
if len(v) < 1 || len(v) > 64 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(v); i++ {
|
||||||
|
ch := v[i]
|
||||||
|
if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '.' || ch == '-' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bot) callTelemt(ctx context.Context, path string, out any) error {
|
func (b *bot) callTelemt(ctx context.Context, path string, out any) error {
|
||||||
|
return b.callTelemtJSON(ctx, http.MethodGet, path, nil, out, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bot) callTelemtJSON(ctx context.Context, method, path string, payload any, out any, successStatus int) error {
|
||||||
cctx, cancel := context.WithTimeout(ctx, b.cfg.TelemtTimeout)
|
cctx, cancel := context.WithTimeout(ctx, b.cfg.TelemtTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(cctx, http.MethodGet, b.cfg.TelemtAPIBaseURL+path, nil)
|
var bodyReader *bytes.Reader
|
||||||
|
if payload != nil {
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(body)
|
||||||
|
} else {
|
||||||
|
bodyReader = bytes.NewReader(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(cctx, method, b.cfg.TelemtAPIBaseURL+path, bodyReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if payload != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
if b.cfg.TelemtAPIAuth != "" {
|
if b.cfg.TelemtAPIAuth != "" {
|
||||||
req.Header.Set("Authorization", b.cfg.TelemtAPIAuth)
|
req.Header.Set("Authorization", b.cfg.TelemtAPIAuth)
|
||||||
}
|
}
|
||||||
@@ -333,13 +428,19 @@ func (b *bot) callTelemt(ctx context.Context, path string, out any) error {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("telemt status %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var env telemtEnvelope
|
var env telemtEnvelope
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
|
||||||
return err
|
if resp.StatusCode != successStatus {
|
||||||
|
return fmt.Errorf("telemt status %s", resp.Status)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("telemt decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != successStatus {
|
||||||
|
if env.Error != nil {
|
||||||
|
return fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("telemt status %s", resp.Status)
|
||||||
}
|
}
|
||||||
if !env.OK {
|
if !env.OK {
|
||||||
if env.Error != nil {
|
if env.Error != nil {
|
||||||
@@ -347,7 +448,10 @@ func (b *bot) callTelemt(ctx context.Context, path string, out any) error {
|
|||||||
}
|
}
|
||||||
return errors.New("telemt returned ok=false")
|
return errors.New("telemt returned ok=false")
|
||||||
}
|
}
|
||||||
if len(env.Data) == 0 {
|
if out == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(env.Data) == 0 || string(env.Data) == "null" {
|
||||||
return errors.New("telemt empty data")
|
return errors.New("telemt empty data")
|
||||||
}
|
}
|
||||||
return json.Unmarshal(env.Data, out)
|
return json.Unmarshal(env.Data, out)
|
||||||
|
|||||||
Reference in New Issue
Block a user