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

This commit is contained in:
Denozordec
2026-03-09 16:44:49 +07:00
parent 43bb8e4f4c
commit 0c93077030
2 changed files with 113 additions and 8 deletions
+1
View File
@@ -7,6 +7,7 @@
- `/health` -> `GET /v1/health`
- `/summary` -> `GET /v1/stats/summary`
- `/users` -> `GET /v1/stats/users` (первые 20 строк)
- `/create_user <username>` -> `POST /v1/users` (создание пользователя + ссылки на подключение)
Команды обрабатываются только от одного админ-чата (`TELEGRAM_ADMIN_CHAT_ID`).
+112 -8
View File
@@ -87,6 +87,22 @@ type userInfo struct {
CurrentConnections uint64 `json:"current_connections"`
ActiveUniqueIPs uint64 `json:"active_unique_ips"`
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() {
@@ -253,13 +269,15 @@ func (b *bot) handleMessage(ctx context.Context, msg *tgMessage) error {
}
switch command {
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":
return b.handleHealth(ctx, chatID)
case "/summary":
return b.handleSummary(ctx, chatID)
case "/users":
return b.handleUsers(ctx, chatID)
case "/create_user", "/createuser":
return b.handleCreateUser(ctx, chatID, text)
default:
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"))
}
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 {
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)
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 {
return err
}
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
if 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()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("telemt status %s", resp.Status)
}
var env telemtEnvelope
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.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")
}
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 json.Unmarshal(env.Data, out)